This commit is contained in:
2026-08-20 18:02:38 +08:00
parent b55b049e77
commit 1cd8b6a473
210 changed files with 6974 additions and 825 deletions
@@ -5,12 +5,14 @@ import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
import com.baiyue.lastleg.security.OverseasPermissions;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -21,12 +23,13 @@ import org.springframework.web.server.ResponseStatusException;
@RequestMapping("/api/platform/v1/last-leg/inventory")
@Tag(name = "Tail-leg inbound-order package inventory commands")
public class InventoryCommandController {
public static final String MANAGE_PERMISSION = "lastleg:inventory:manage";
public static final String ADJUST_PERMISSION = "lastleg:inventory:adjust";
public static final String MANAGE_PERMISSION = OverseasPermissions.INVENTORY_EDIT;
public static final String ADJUST_PERMISSION = OverseasPermissions.INVENTORY_ADJUST;
@Resource private InventoryCommandService service;
@PostMapping("/hold")
@Operation(summary = "Move available inbound-order packages to hold")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_EDIT + "')")
@Operation(summary = "Move available inbound-order packages to hold", description = "Required authority: " + OverseasPermissions.INVENTORY_EDIT)
public ApiResponse<InventoryCommandResponse> hold(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Move request) {
requirePermission(user, MANAGE_PERMISSION);
@@ -34,47 +37,17 @@ public class InventoryCommandController {
}
@PostMapping("/release")
@Operation(summary = "Release held inbound-order packages")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_EDIT + "')")
@Operation(summary = "Release held inbound-order packages", description = "Required authority: " + OverseasPermissions.INVENTORY_EDIT)
public ApiResponse<InventoryCommandResponse> release(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Move request) {
requirePermission(user, MANAGE_PERMISSION);
return ApiResponse.success(service.release(scope(user, request.customerId()), operator(user), request));
}
@PostMapping("/allocate")
@Operation(summary = "Allocate available inbound-order packages to an outbound order")
public ApiResponse<InventoryCommandResponse> allocate(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Move request) {
requirePermission(user, MANAGE_PERMISSION);
return ApiResponse.success(service.allocate(scope(user, request.customerId()), operator(user), request));
}
@PostMapping("/unallocate")
@Operation(summary = "Cancel an outbound allocation")
public ApiResponse<InventoryCommandResponse> unallocate(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Move request) {
requirePermission(user, MANAGE_PERMISSION);
return ApiResponse.success(service.unallocate(scope(user, request.customerId()), operator(user), request));
}
@PostMapping("/pick")
@Operation(summary = "Confirm picked inbound-order packages")
public ApiResponse<InventoryCommandResponse> pick(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Move request) {
requirePermission(user, MANAGE_PERMISSION);
return ApiResponse.success(service.pick(scope(user, request.customerId()), operator(user), request));
}
@PostMapping("/ship")
@Operation(summary = "Ship picked inbound-order packages and report whether the pallet is empty")
public ApiResponse<InventoryCommandResponse> ship(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Move request) {
requirePermission(user, MANAGE_PERMISSION);
return ApiResponse.success(service.ship(scope(user, request.customerId()), operator(user), request));
}
@PostMapping("/adjust")
@Operation(summary = "Adjust inbound-order package inventory after a physical count")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_ADJUST + "')")
@Operation(summary = "Adjust inbound-order package inventory after a physical count", description = "Required authority: " + OverseasPermissions.INVENTORY_ADJUST)
public ApiResponse<InventoryCommandResponse> adjust(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Adjustment request) {
requirePermission(user, ADJUST_PERMISSION);
@@ -22,6 +22,9 @@ public final class InventoryCommandRequests {
@NotNull Long inboundOrderId,
@NotBlank String inboundOrderNo,
@NotBlank String inboundCode,
String skuCode,
String productNameZh,
String productNameEn,
@NotNull Long palletId,
@NotBlank String palletNo,
String batchNo,
@@ -175,8 +175,9 @@ public class InventoryCommandService {
}
private InventoryBalance balance(String customerId, InventoryCommandRequests.StockIn request) {
String skuCode = sku(request.skuCode());
InventoryBalance balance = balances.lockDimension(customerId, request.overseasWarehouseId(),
request.destinationWarehouseCode(), request.inboundOrderId(), request.inboundCode());
request.destinationWarehouseCode(), request.inboundOrderId(), request.inboundCode(), skuCode);
if (balance != null) return balance;
balance = new InventoryBalance();
balance.setId(IdWorker.getId());
@@ -191,6 +192,9 @@ public class InventoryCommandService {
balance.setInboundOrderId(request.inboundOrderId());
balance.setInboundOrderNo(request.inboundOrderNo().trim());
balance.setInboundCode(request.inboundCode().trim());
balance.setSkuCode(skuCode);
balance.setProductNameZh(trim(request.productNameZh()));
balance.setProductNameEn(trim(request.productNameEn()));
zero(balance);
balance.setVersion(0L);
balance.setCreatedAt(LocalDateTime.now());
@@ -200,7 +204,7 @@ public class InventoryCommandService {
return balance;
} catch (DuplicateKeyException exception) {
InventoryBalance concurrent = balances.lockDimension(customerId, request.overseasWarehouseId(),
request.destinationWarehouseCode(), request.inboundOrderId(), request.inboundCode());
request.destinationWarehouseCode(), request.inboundOrderId(), request.inboundCode(), skuCode);
if (concurrent != null) return concurrent;
throw exception;
}
@@ -314,6 +318,7 @@ public class InventoryCommandService {
ledger.setPalletNo(position.getPalletNo());
ledger.setBatchNo(position.getBatchNo());
ledger.setWarehouseLocationCode(position.getWarehouseLocationCode());
ledger.setSkuCode(balance.getSkuCode());
ledger.setBusinessType(businessType);
ledger.setBusinessOrderId(balance.getInboundOrderId());
ledger.setBusinessOrderNo(trim(businessOrderNo));
@@ -434,6 +439,11 @@ public class InventoryCommandService {
return value == null || value.isBlank() ? null : value.trim();
}
private static String sku(String value) {
String normalized = trim(value);
return normalized == null ? "UNSPECIFIED" : normalized;
}
private static String remarkSuffix(String remark) {
String value = trim(remark);
return value == null ? "" : "; " + value;
@@ -24,6 +24,9 @@ public class InventoryBalance {
private Long inboundOrderId;
private String inboundOrderNo;
private String inboundCode;
private String skuCode;
private String productNameZh;
private String productNameEn;
private Long availablePackages;
private Long holdPackages;
private Long allocatedPackages;
@@ -12,22 +12,24 @@ public interface InventoryBalanceMapper extends BaseMapper<InventoryBalance> {
@Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} " +
"AND overseas_warehouse_id=#{overseasWarehouseId} " +
"AND destination_warehouse_code=#{destinationWarehouseCode} " +
"AND inbound_order_id=#{inboundOrderId} AND inbound_code=#{inboundCode} LIMIT 1")
"AND inbound_order_id=#{inboundOrderId} AND inbound_code=#{inboundCode} AND sku_code=#{skuCode} LIMIT 1")
InventoryBalance findDimension(@Param("customerId") String customerId,
@Param("overseasWarehouseId") String overseasWarehouseId,
@Param("destinationWarehouseCode") String destinationWarehouseCode,
@Param("inboundOrderId") Long inboundOrderId,
@Param("inboundCode") String inboundCode);
@Param("inboundCode") String inboundCode,
@Param("skuCode") String skuCode);
@Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} " +
"AND overseas_warehouse_id=#{overseasWarehouseId} " +
"AND destination_warehouse_code=#{destinationWarehouseCode} " +
"AND inbound_order_id=#{inboundOrderId} AND inbound_code=#{inboundCode} LIMIT 1 FOR UPDATE")
"AND inbound_order_id=#{inboundOrderId} AND inbound_code=#{inboundCode} AND sku_code=#{skuCode} LIMIT 1 FOR UPDATE")
InventoryBalance lockDimension(@Param("customerId") String customerId,
@Param("overseasWarehouseId") String overseasWarehouseId,
@Param("destinationWarehouseCode") String destinationWarehouseCode,
@Param("inboundOrderId") Long inboundOrderId,
@Param("inboundCode") String inboundCode);
@Param("inboundCode") String inboundCode,
@Param("skuCode") String skuCode);
@Select("<script>SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId}" +
"<if test='overseasWarehouseId != null and overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{overseasWarehouseId}</if>" +
@@ -9,6 +9,7 @@ import com.baiyue.lastleg.inventory.query.dto.InventoryLedgerView;
import com.baiyue.lastleg.inventory.query.dto.InventoryPositionView;
import com.baiyue.lastleg.inventory.query.dto.InventoryQueryRequests;
import com.baiyue.lastleg.inventory.query.service.InventoryQueryService;
import com.baiyue.lastleg.security.OverseasPermissions;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
@@ -18,6 +19,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
@@ -35,7 +37,8 @@ public class InventoryQueryController {
@Resource private InventoryQueryService service;
@GetMapping(BASE_PATH + "/summary")
@Operation(summary = "Get inbound-order package inventory totals")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_LIST + "')")
@Operation(summary = "Get inbound-order package inventory totals", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
public ApiResponse<InventoryDashboardResponse> summary(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@@ -44,7 +47,8 @@ public class InventoryQueryController {
}
@GetMapping(BASE_PATH + "/balances")
@Operation(summary = "Page inventory by customer, warehouse and inbound order")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_LIST + "')")
@Operation(summary = "Page inventory by customer, warehouse and inbound order", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
public ApiResponse<PageResult<InventoryBalanceView>> balances(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@@ -53,7 +57,8 @@ public class InventoryQueryController {
}
@GetMapping(BASE_PATH + "/balances/{balanceId}/positions")
@Operation(summary = "List pallet and batch positions for one inbound-order balance")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_LIST + "')")
@Operation(summary = "List pallet and batch positions for one inbound-order balance", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
public ApiResponse<List<InventoryPositionView>> positions(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@@ -63,7 +68,8 @@ public class InventoryQueryController {
}
@GetMapping(BASE_PATH + "/ledger")
@Operation(summary = "Page append-only inventory movements")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_LIST + "')")
@Operation(summary = "Page append-only inventory movements", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
public ApiResponse<PageResult<InventoryLedgerView>> ledgers(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@@ -72,7 +78,8 @@ public class InventoryQueryController {
}
@GetMapping(value = BASE_PATH + "/balances/export", produces = "text/csv")
@Operation(summary = "Export filtered inbound-order inventory as UTF-8 CSV")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_EXPORT + "')")
@Operation(summary = "Export filtered inbound-order inventory as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.INVENTORY_EXPORT)
public ResponseEntity<byte[]> exportBalances(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@@ -81,7 +88,8 @@ public class InventoryQueryController {
}
@GetMapping(value = BASE_PATH + "/ledger/export", produces = "text/csv")
@Operation(summary = "Export filtered inventory movements as UTF-8 CSV")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_EXPORT + "')")
@Operation(summary = "Export filtered inventory movements as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.INVENTORY_EXPORT)
public ResponseEntity<byte[]> exportLedgers(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@@ -15,6 +15,9 @@ public record InventoryBalanceView(
Long inboundOrderId,
String inboundOrderNo,
String inboundCode,
String skuCode,
String productNameZh,
String productNameEn,
long totalPackages,
long availablePackages,
long holdPackages,
@@ -19,6 +19,8 @@ public final class InventoryQueryRequests {
String inboundOrderNo,
String inboundCode,
String businessOrderNo,
String skuCode,
String productName,
String inventoryStatus
) {
public BalanceQuery {
@@ -19,6 +19,8 @@ public interface InventoryQueryMapper {
"<if test='q.inboundOrderNo != null and q.inboundOrderNo != \"\"'> AND inbound_order_no LIKE CONCAT('%',#{q.inboundOrderNo},'%')</if>" +
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=ll_inventory_balance.id AND l.customer_id=#{customerId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
"<if test='q.skuCode != null and q.skuCode != \"\"'> AND sku_code LIKE CONCAT('%',#{q.skuCode},'%')</if>" +
"<if test='q.productName != null and q.productName != \"\"'> AND (product_name_zh LIKE CONCAT('%',#{q.productName},'%') OR product_name_en LIKE CONCAT('%',#{q.productName},'%'))</if>" +
"<choose>" +
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND available_packages &gt; 0</when>" +
"<when test='q.inventoryStatus == \"HOLD\"'> AND hold_packages &gt; 0</when>" +
@@ -37,6 +39,8 @@ public interface InventoryQueryMapper {
"<if test='q.inboundOrderNo != null and q.inboundOrderNo != \"\"'> AND b.inbound_order_no LIKE CONCAT('%',#{q.inboundOrderNo},'%')</if>" +
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND b.inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=b.id AND l.customer_id=#{customerId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
"<if test='q.skuCode != null and q.skuCode != \"\"'> AND b.sku_code LIKE CONCAT('%',#{q.skuCode},'%')</if>" +
"<if test='q.productName != null and q.productName != \"\"'> AND (b.product_name_zh LIKE CONCAT('%',#{q.productName},'%') OR b.product_name_en LIKE CONCAT('%',#{q.productName},'%'))</if>" +
"<choose>" +
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND b.available_packages &gt; 0</when>" +
"<when test='q.inventoryStatus == \"HOLD\"'> AND b.hold_packages &gt; 0</when>" +
@@ -78,12 +78,12 @@ public class InventoryQueryService {
validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
List<String> rows = new ArrayList<>();
rows.add("customerId,customerName,overseasWarehouseCode,overseasWarehouseName,destinationWarehouseCode," +
"destinationWarehouseName,inboundOrderId,inboundOrderNo,inboundCode,totalPackages,availablePackages," +
"destinationWarehouseName,inboundOrderId,inboundOrderNo,inboundCode,skuCode,productNameZh,productNameEn,totalPackages,availablePackages," +
"holdPackages,allocatedPackages,pickedPackages,inTransitPackages,receivingPackages,procurementPackages");
mapper.balances(customerId, query, 0, EXPORT_LIMIT).stream().map(this::balanceView).forEach(v -> rows.add(
String.join(",", csv(v.customerId()), csv(v.customerName()), csv(v.overseasWarehouseCode()), csv(v.overseasWarehouseName()),
csv(v.destinationWarehouseCode()), csv(v.destinationWarehouseName()), csv(v.inboundOrderId()),
csv(v.inboundOrderNo()), csv(v.inboundCode()),
csv(v.inboundOrderNo()), csv(v.inboundCode()), csv(v.skuCode()), csv(v.productNameZh()), csv(v.productNameEn()),
csv(v.totalPackages()), csv(v.availablePackages()), csv(v.holdPackages()), csv(v.allocatedPackages()),
csv(v.pickedPackages()), csv(v.inTransitPackages()), csv(v.receivingPackages()),
csv(v.procurementPackages()))));
@@ -116,7 +116,8 @@ public class InventoryQueryService {
return new InventoryBalanceView(value.getId(), value.getCustomerId(), value.getCustomerName(), value.getOverseasWarehouseId(),
value.getOverseasWarehouseCode(), value.getOverseasWarehouseName(), value.getDestinationWarehouseId(),
value.getDestinationWarehouseCode(), value.getDestinationWarehouseName(), value.getInboundOrderId(),
value.getInboundOrderNo(), value.getInboundCode(), availablePackages + holdPackages + allocatedPackages + pickedPackages
value.getInboundOrderNo(), value.getInboundCode(), value.getSkuCode(), value.getProductNameZh(), value.getProductNameEn(),
availablePackages + holdPackages + allocatedPackages + pickedPackages
+ inTransitPackages + receivingPackages + procurementPackages,
availablePackages, holdPackages, allocatedPackages, pickedPackages, inTransitPackages,
receivingPackages, procurementPackages, value.getUpdatedAt());