一件代发拣货

This commit is contained in:
2026-09-01 13:41:00 +08:00
parent ffed9bf898
commit 8b0c99593e
29 changed files with 910 additions and 29 deletions
@@ -16702,3 +16702,82 @@ CALL baiyue_firstleg.assert_unified_final_schema();
DROP PROCEDURE IF EXISTS baiyue_firstleg.assert_unified_final_schema;
SELECT 'PASS' AS migration_status,
'统一增量全部执行完成;父子订单、履约分片、装柜门禁、订舱拖车、报关船期、船司查询、仓库角色、费用、Finance人工财务、公共资源、可靠审计及IAM安全事件结构校验通过' AS message;
-- 一件代发测量完成后的独立 PDA 合托;不代表国内仓库存入库。
USE baiyue_firstleg;
CREATE TABLE IF NOT EXISTS fl_dropship_pallet_task (
id BIGINT UNSIGNED NOT NULL COMMENT '合托任务ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离键',
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '一件代发入仓单ID',
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '作业仓库ID',
task_status VARCHAR(24) NOT NULL COMMENT 'PENDING/CLAIMED/COMPLETED',
total_cartons INT UNSIGNED NOT NULL COMMENT '测量确认总箱数',
allocated_cartons INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已上托箱数',
assignee_id BIGINT UNSIGNED NULL,
assignee_name VARCHAR(64) NULL,
claimed_at DATETIME(3) NULL,
completed_at DATETIME(3) NULL,
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_dropship_task_inbound (tenant_id,inbound_id),
KEY idx_dropship_task_operator (task_status,assignee_id,warehouse_id,created_at),
CONSTRAINT fk_dropship_task_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order(id),
CONSTRAINT chk_dropship_task_status CHECK (task_status IN ('PENDING','CLAIMED','COMPLETED')),
CONSTRAINT chk_dropship_task_cartons CHECK (allocated_cartons<=total_cartons)
) ENGINE=InnoDB COMMENT='一件代发测量后PDA合托任务';
CREATE TABLE IF NOT EXISTS fl_dropship_pallet (
id BIGINT UNSIGNED NOT NULL COMMENT '合托托盘ID',
task_id BIGINT UNSIGNED NOT NULL,
tenant_id BIGINT UNSIGNED NOT NULL,
inbound_id BIGINT UNSIGNED NOT NULL,
warehouse_id BIGINT UNSIGNED NOT NULL,
pallet_no VARCHAR(32) NULL COMMENT '封托时生成的DP托盘号',
pallet_status VARCHAR(16) NOT NULL COMMENT 'OPEN/COMPLETED',
open_operation_id VARCHAR(128) NOT NULL,
seal_operation_id VARCHAR(128) NULL,
created_by BIGINT UNSIGNED NOT NULL,
created_by_name VARCHAR(64) NULL,
completed_by BIGINT UNSIGNED NULL,
completed_by_name VARCHAR(64) NULL,
completed_at DATETIME(3) NULL,
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_dropship_pallet_no (pallet_no),
UNIQUE KEY uk_dropship_pallet_open_op (open_operation_id),
UNIQUE KEY uk_dropship_pallet_seal_op (seal_operation_id),
KEY idx_dropship_pallet_inbound (tenant_id,inbound_id,pallet_status),
CONSTRAINT fk_dropship_pallet_task FOREIGN KEY (task_id) REFERENCES fl_dropship_pallet_task(id),
CONSTRAINT chk_dropship_pallet_status CHECK (pallet_status IN ('OPEN','COMPLETED'))
) ENGINE=InnoDB COMMENT='一件代发合托形成的可打印托盘';
CREATE TABLE IF NOT EXISTS fl_dropship_pallet_carton (
id BIGINT UNSIGNED NOT NULL,
task_id BIGINT UNSIGNED NOT NULL,
pallet_id BIGINT UNSIGNED NOT NULL,
tenant_id BIGINT UNSIGNED NOT NULL,
inbound_id BIGINT UNSIGNED NOT NULL,
inbound_detail_id BIGINT UNSIGNED NOT NULL,
carton_label_id BIGINT UNSIGNED NOT NULL,
sku_code VARCHAR(128) NOT NULL,
cargo_name VARCHAR(255) NULL,
box_code VARCHAR(64) NOT NULL,
operation_id VARCHAR(128) NOT NULL,
bound_by BIGINT UNSIGNED NOT NULL,
bound_by_name VARCHAR(64) NULL,
bound_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_dropship_carton_task_label (task_id,carton_label_id),
KEY idx_dropship_carton_pallet_sku (pallet_id,sku_code,id),
KEY idx_dropship_carton_inbound (tenant_id,inbound_id,pallet_id),
KEY idx_dropship_carton_operation (operation_id),
CONSTRAINT fk_dropship_carton_task FOREIGN KEY (task_id) REFERENCES fl_dropship_pallet_task(id),
CONSTRAINT fk_dropship_carton_pallet FOREIGN KEY (pallet_id) REFERENCES fl_dropship_pallet(id),
CONSTRAINT fk_dropship_carton_label FOREIGN KEY (carton_label_id) REFERENCES fl_inbound_carton_label(id)
) ENGINE=InnoDB COMMENT='一件代发托盘内逐箱SKU和入仓单明细';
+2 -1
View File
@@ -15,7 +15,8 @@
| 履约任务/分配 | `controller/PlatformFulfillmentAssignmentController.java`、`service/FulfillmentTaskService.java` |
| 入库单查询/聚合 | `controller/PlatformInboundOrderController.java`、`service/InboundOrderQueryService.java` |
| 平台测量/OCR | `controller/PlatformInboundMeasurementController.java`、`service/InboundMeasurementService.java`、`service/CartonMeasurementService.java`;手工单箱测量也从该 Controller 与 `CartonMeasurementService` 进入 |
| 箱唛箱码生成、打印与测量绑定 | 审核建单生成钩子 `controller/InternalInboundOrderController.java`;生成/解析 `service/InboundCartonLabelService.java`、`repository/InboundCartonLabelRepository.java`;平台与客户打印 `controller/PlatformInboundOrderController.java`、`MerchantInboundCartonLabelController.java`、`service/InboundCartonLabelPrintService.java`。`fl_inbound_carton_label.sku_code` 是箱唛的 SKU 冻结快照,生成时经 `client/OrderCargoClient.java` 获取;既有记录按 `inbound_detail_id` 回填。`ONE_CLICK_DROPSHIP` 缺 SKU 时禁止生成箱唛。`box_code`、二维码内容和现有打印版式不携带 SKU,不能因该绑定改变。基线/升级 SQL 为 `src/main/resources/db/phase2-inbound-carton-label.sql`、`phase2-carton-label-versioning-migration.sql`、`../deploy/sql/头程数据库统一增量-MySQL8.sql` |
| 箱唛箱码生成、打印与测量绑定 | 审核建单生成钩子 `controller/InternalInboundOrderController.java`;生成/解析 `service/InboundCartonLabelService.java`、`repository/InboundCartonLabelRepository.java`;平台与客户打印 `controller/PlatformInboundOrderController.java`、`MerchantInboundCartonLabelController.java`、`service/InboundCartonLabelPrintService.java`。`fl_inbound_carton_label.sku_code` 是箱唛的 SKU 冻结快照,生成时经 `client/OrderCargoClient.java` 获取;既有记录按 `inbound_detail_id` 回填。`ONE_CLICK_DROPSHIP` 缺 SKU 时禁止生成箱唛,打印面显示 SKU;`box_code` 与二维码内容仍只携带稳定箱码。基线/升级 SQL 为 `src/main/resources/db/phase2-inbound-carton-label.sql`、`phase2-carton-label-versioning-migration.sql`、`../deploy/sql/头程数据库统一增量-MySQL8.sql` |
| 一件代发测量后 PDA 合托 | `controller/PlatformDropshipPalletizationController.java`、`service/DropshipPalletizationService.java`、`mapper/DropshipPalletizationMapper.java`;测量通过进入 `WAIT_PALLETIZE` 并下发独立任务,不创建国内库存,封托生成 `DP` 托盘号,全部箱数完成后进入 `WAIT_SPLIT`。契约 `src/main/resources/openapi/dropship-palletization-v1.yaml` |
| 墨家设备测量上传 | `controller/OpenApiInboundMeasurementController.java`、`dto/MojiaMeasurementDtos.java`、`service/MojiaMeasurementOpenApiService.java`、`security/MeasurementRequestAuthenticator.java`;接口契约见 `src/main/resources/openapi/firstleg-v1.yaml` 与 `../../docs/墨家设备测量上传接口说明-V3.md` |
| 审核与初始计价 | `controller/PlatformInboundMeasurementReviewController.java`、`service/MeasurementReviewService.java`、`service/InitialInboundPricingService.java`、`dto/MeasurementApprovalResponse.java` |
| FBA 主数据/计费 | `controller/PlatformFbaMasterDataController.java`、`PlatformFbaPricingController.java`、`service/FbaPricingService.java` |
@@ -34,7 +34,7 @@ public interface PrintPdfClient {
}
record Label(String topCode, String warehouseText, String cartonText,
String routeText, String qrContent) {
String routeText, String qrContent, String skuCode) {
}
record FreightA4PdfRequest(String templateCode, PageSpec pageSpec,
@@ -0,0 +1,117 @@
package com.baiyue.firstleg.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Pallet;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Task;
import com.baiyue.firstleg.service.DropshipPalletizationService;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
import com.baiyue.firstleg.service.InboundCartonLabelPrintService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestHeader;
import java.util.List;
/** 国内 PDA 一件代发合托;与装柜 PDA 分离。 */
@Validated
@RestController
@RequestMapping("/api/platform/v1/first-leg/pda-dropship")
public class PlatformDropshipPalletizationController {
@Resource private DropshipPalletizationService service;
@Resource private InboundCartonLabelPrintService printService;
@GetMapping("/tasks/pending")
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
public ApiResponse<List<DropshipPalletizationDtos.PendingTask>> pending(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) @Size(max = 64) String keyword) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.pending(user.userId(), keyword));
}
@GetMapping("/tasks/{taskId}")
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
public ApiResponse<Task> task(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@RequestParam @Positive long warehouseId) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.task(taskId, warehouseId, user.userId()));
}
@PostMapping("/tasks/{taskId}/claim")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<Task> claim(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@Valid @RequestBody DropshipPalletizationDtos.ClaimRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.claim(taskId, request.warehouseId(), user.userId(), user.username()));
}
@PostMapping("/tasks/{taskId}/pallets/open")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<Pallet> open(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@Valid @RequestBody DropshipPalletizationDtos.OpenPalletRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.open(taskId, request, user.userId(), user.username()));
}
@PostMapping("/tasks/{taskId}/pallets/{palletId}/scan")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<DropshipPalletizationDtos.ScanResult> scan(
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId, @PathVariable @Positive long palletId,
@Valid @RequestBody DropshipPalletizationDtos.ScanRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.scan(taskId, palletId, request, user.userId(), user.username()));
}
@PostMapping("/tasks/{taskId}/pallets/{palletId}/seal")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<Task> seal(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@PathVariable @Positive long palletId,
@Valid @RequestBody DropshipPalletizationDtos.SealPalletRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.seal(taskId, palletId, request, user.userId(), user.username()));
}
@GetMapping("/inbound-orders/{inboundId}/pallets")
@PreAuthorize("hasAuthority('firstleg:inbound:list')")
public ApiResponse<List<Pallet>> inboundPallets(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long inboundId,
@RequestParam @Positive long accountId) {
FirstlegScopeGuard.platform(user, accountId);
return ApiResponse.success(service.inboundPallets(accountId, inboundId));
}
@GetMapping("/inbound-orders/{inboundId}/pallets/{palletId}/print")
@PreAuthorize("hasAuthority('firstleg:inbound:list')")
public ResponseEntity<byte[]> printPallet(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long inboundId,
@PathVariable @Positive long palletId,
@RequestParam @Positive long accountId,
@RequestHeader(value = "X-Request-Id", required = false) String requestId) {
FirstlegScopeGuard.platform(user, accountId);
Pallet pallet = service.inboundPallets(accountId, inboundId).stream()
.filter(item -> item.palletId() == palletId).findFirst()
.orElseThrow(() -> new com.baiyue.common.core.exception.BizException(
com.baiyue.common.core.api.ResultCode.NOT_FOUND, "托盘不存在"));
return printService.generateDropshipPallet(pallet, requestId);
}
}
@@ -20,6 +20,8 @@ public final class InboundProcessStatus {
public static final String FEE_CONFIRMING = "FEE_CONFIRMING";
/** 费用已确认,正在执行托盘、库位占用和入库收口。 */
public static final String INBOUNDING = "INBOUNDING";
/** 一件代发已完成测量但未真实入库,等待 PDA 合托。 */
public static final String WAIT_PALLETIZE = "WAIT_PALLETIZE";
/** 入库和费用发布完成,等待分柜。 */
public static final String WAIT_SPLIT = "WAIT_SPLIT";
/** 已进入装柜作业,PDA 可执行拣货。 */
@@ -33,7 +35,7 @@ public final class InboundProcessStatus {
public static final Set<String> VALUES = Set.of(
PREPARING, WAIT_MEASURE, MEASURING, MEASURE_PENDING, MEASURE_REJECTED,
WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, WAIT_SPLIT,
WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, WAIT_PALLETIZE, WAIT_SPLIT,
LOADING, LOADED, DELIVERED, VOIDED
);
@@ -0,0 +1,51 @@
package com.baiyue.firstleg.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.List;
/** 一件代发测量完成后的独立 PDA 合托契约。 */
public final class DropshipPalletizationDtos {
private DropshipPalletizationDtos() {}
public record ClaimRequest(@Positive long warehouseId) {}
public record OpenPalletRequest(@Positive long warehouseId,
@NotBlank @Size(max = 128) String operationId) {}
public record ScanRequest(@Positive long warehouseId,
@NotBlank @Size(max = 64) String boxCode,
@Positive @Max(2000) int cartonCount,
@NotBlank @Size(max = 128) String operationId) {}
public record SealPalletRequest(@Positive long warehouseId,
@NotBlank @Size(max = 128) String operationId,
@Positive int expectedVersion) {}
public record PendingTask(Long taskId, Long inboundId, String inboundCode, Long tenantId,
String customerName, Long warehouseId, String warehouseName,
Integer totalCartons, Integer allocatedCartons, String taskStatus,
LocalDateTime createdAt) {}
public record Task(Long taskId, Long inboundId, String inboundCode, Long tenantId,
String customerName, Long warehouseId, String warehouseName,
Integer totalCartons, Integer allocatedCartons, String taskStatus,
Long assigneeId, String assigneeName, Integer version,
List<Pallet> pallets) {}
public record Pallet(Long palletId, String palletNo, String palletStatus,
Integer cartonCount, Integer skuCount, Integer version,
List<SkuItem> items, LocalDateTime completedAt) {}
public record SkuItem(Long inboundId, String inboundCode, Long inboundDetailId,
String skuCode, String cargoName, Integer cartonCount,
List<String> cartonMarks) {}
public record ScanResult(String skuCode, String cargoName, String inboundCode,
int addedCartons, int allocatedCartons, int remainingCartons,
Pallet pallet) {}
}
@@ -15,8 +15,17 @@ public final class InboundCartonLabels {
public record Label(Long id, Long inboundDetailId, Integer cargoItemNo, String cargoName,
Integer detailPackageCount, String boxCode, Integer totalCount,
Integer cartonIndex, String boxSpecification, BigDecimal lengthCm,
BigDecimal widthCm, BigDecimal heightCm, String topCode,
BigDecimal widthCm, BigDecimal heightCm, String skuCode, String topCode,
String warehouseText, String cartonText, String routeText) {
public Label(Long id, Long inboundDetailId, Integer cargoItemNo, String cargoName,
Integer detailPackageCount, String boxCode, Integer totalCount,
Integer cartonIndex, String boxSpecification, BigDecimal lengthCm,
BigDecimal widthCm, BigDecimal heightCm, String topCode,
String warehouseText, String cartonText, String routeText) {
this(id, inboundDetailId, cargoItemNo, cargoName, detailPackageCount, boxCode,
totalCount, cartonIndex, boxSpecification, lengthCm, widthCm, heightCm,
null, topCode, warehouseText, cartonText, routeText);
}
}
public record Resolution(Long inboundId, Long orderId, Long inboundDetailId, String boxCode,
@@ -8,11 +8,18 @@ import java.util.List;
public record InboundOrderDetail(
InboundOrderSummary summary,
String publicRemark,
List<DropshipPalletizationDtos.Pallet> dropshipPallets,
List<PalletLocationSummary> palletLocations,
List<FileSummary> files,
List<InternalNoteSummary> internalNotes,
List<StatusHistorySummary> statusHistory
) {
public InboundOrderDetail(InboundOrderSummary summary, String publicRemark,
List<PalletLocationSummary> palletLocations, List<FileSummary> files,
List<InternalNoteSummary> internalNotes,
List<StatusHistorySummary> statusHistory) {
this(summary, publicRemark, List.of(), palletLocations, files, internalNotes, statusHistory);
}
public record PalletLocationSummary(
Integer allocationNo, Long warehouseId, Long palletId, String palletCode,
BigDecimal palletCapacityCbm, BigDecimal allocatedVolumeCbm, Integer allocatedPackageCount,
@@ -14,7 +14,7 @@ public record UnifiedInboundQuery(
Long tenantId,
@Size(max = 64) String keyword,
@Size(max = 64) String orderNo,
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_SPLIT",
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "WAIT_SPLIT",
"LOADING", "LOADED", "DELIVERED"})
@Size(max = 32) String mainStatus,
Long customerId,
@@ -0,0 +1,203 @@
package com.baiyue.firstleg.mapper;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.PendingTask;
import lombok.Getter;
import lombok.Setter;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDateTime;
import java.util.List;
/** 一件代发合托持久化;任务、托盘和逐箱绑定均独立于装柜拣货。 */
@Mapper
public interface DropshipPalletizationMapper {
@Insert("""
INSERT IGNORE INTO fl_dropship_pallet_task
(id,tenant_id,inbound_id,warehouse_id,task_status,total_cartons,allocated_cartons,version,created_at,updated_at)
VALUES(#{id},#{tenantId},#{inboundId},#{warehouseId},'PENDING',#{totalCartons},0,1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
""")
int insertTask(@Param("id") long id, @Param("tenantId") long tenantId,
@Param("inboundId") long inboundId, @Param("warehouseId") long warehouseId,
@Param("totalCartons") int totalCartons);
@Select("""
SELECT t.id task_id,t.inbound_id,o.inbound_code,t.tenant_id,o.customer_name,t.warehouse_id,
o.warehouse_name,t.total_cartons,t.allocated_cartons,t.task_status,t.created_at
FROM fl_dropship_pallet_task t JOIN fl_inbound_order o ON o.id=t.inbound_id AND o.tenant_id=t.tenant_id
WHERE t.task_status IN ('PENDING','CLAIMED')
AND (t.assignee_id IS NULL OR t.assignee_id=#{operatorId})
AND (#{keyword} IS NULL OR o.inbound_code LIKE CONCAT('%',#{keyword},'%')
OR o.customer_name LIKE CONCAT('%',#{keyword},'%'))
ORDER BY t.created_at,t.id
""")
List<PendingTask> pending(@Param("operatorId") long operatorId, @Param("keyword") String keyword);
@Select("""
SELECT t.*,o.inbound_code,o.customer_name,o.warehouse_name,o.process_status,o.fulfillment_type
FROM fl_dropship_pallet_task t JOIN fl_inbound_order o ON o.id=t.inbound_id AND o.tenant_id=t.tenant_id
WHERE t.id=#{taskId} AND t.warehouse_id=#{warehouseId}
""")
TaskRow task(@Param("taskId") long taskId, @Param("warehouseId") long warehouseId);
@Update("""
UPDATE fl_dropship_pallet_task
SET task_status='CLAIMED',assignee_id=#{operatorId},assignee_name=#{operatorName},
claimed_at=COALESCE(claimed_at,CURRENT_TIMESTAMP(3)),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{taskId} AND warehouse_id=#{warehouseId} AND task_status IN ('PENDING','CLAIMED')
AND (assignee_id IS NULL OR assignee_id=#{operatorId})
""")
int claim(@Param("taskId") long taskId, @Param("warehouseId") long warehouseId,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Insert("""
INSERT INTO fl_dropship_pallet
(id,task_id,tenant_id,inbound_id,warehouse_id,pallet_status,open_operation_id,version,created_by,created_by_name,created_at,updated_at)
VALUES(#{id},#{taskId},#{tenantId},#{inboundId},#{warehouseId},'OPEN',#{operationId},1,
#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
""")
int insertPallet(@Param("id") long id, @Param("taskId") long taskId,
@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
@Param("warehouseId") long warehouseId, @Param("operationId") String operationId,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Select("SELECT * FROM fl_dropship_pallet WHERE open_operation_id=#{operationId}")
PalletRow palletByOpenOperation(@Param("operationId") String operationId);
@Select("SELECT * FROM fl_dropship_pallet WHERE seal_operation_id=#{operationId}")
PalletRow palletBySealOperation(@Param("operationId") String operationId);
@Select("SELECT * FROM fl_dropship_pallet WHERE id=#{palletId} AND task_id=#{taskId} AND warehouse_id=#{warehouseId}")
PalletRow pallet(@Param("taskId") long taskId, @Param("palletId") long palletId,
@Param("warehouseId") long warehouseId);
@Select("SELECT * FROM fl_dropship_pallet WHERE task_id=#{taskId} ORDER BY id")
List<PalletRow> pallets(@Param("taskId") long taskId);
@Select("""
SELECT p.* FROM fl_dropship_pallet p JOIN fl_dropship_pallet_task t ON t.id=p.task_id
WHERE t.tenant_id=#{tenantId} AND t.inbound_id=#{inboundId} ORDER BY p.id
""")
List<PalletRow> palletsByInbound(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
@Select("SELECT inbound_code FROM fl_inbound_order WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0")
String inboundCode(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
@Select("""
SELECT l.id carton_label_id,l.inbound_detail_id,l.sku_code,l.cargo_name,l.detail_package_count,
l.inbound_code,l.box_code
FROM fl_inbound_carton_label l
JOIN fl_dropship_pallet_task t ON t.inbound_id=l.inbound_id AND t.tenant_id=l.tenant_id
WHERE t.id=#{taskId} AND l.box_code=#{boxCode} AND l.batch_status='ACTIVE'
""")
LabelRow label(@Param("taskId") long taskId, @Param("boxCode") String boxCode);
@Select("""
SELECT l.id carton_label_id,l.inbound_detail_id,l.sku_code,l.cargo_name,l.detail_package_count,
l.inbound_code,l.box_code
FROM fl_inbound_carton_label l
WHERE l.tenant_id=#{tenantId} AND l.inbound_id=#{inboundId}
AND l.inbound_detail_id=#{detailId} AND l.batch_status='ACTIVE'
AND NOT EXISTS (SELECT 1 FROM fl_dropship_pallet_carton c
WHERE c.task_id=#{taskId} AND c.carton_label_id=l.id)
ORDER BY CASE WHEN l.box_code=#{preferredBoxCode} THEN 0 ELSE 1 END,l.carton_index,l.id
LIMIT #{count}
""")
List<LabelRow> unboundLabels(@Param("taskId") long taskId, @Param("tenantId") long tenantId,
@Param("inboundId") long inboundId, @Param("detailId") long detailId,
@Param("preferredBoxCode") String preferredBoxCode,
@Param("count") int count);
@Select("SELECT COUNT(*) FROM fl_dropship_pallet_carton WHERE operation_id=#{operationId}")
int operationCount(@Param("operationId") String operationId);
@Insert("""
INSERT INTO fl_dropship_pallet_carton
(id,task_id,pallet_id,tenant_id,inbound_id,inbound_detail_id,carton_label_id,sku_code,cargo_name,
box_code,operation_id,bound_by,bound_by_name,bound_at)
VALUES(#{id},#{taskId},#{palletId},#{tenantId},#{inboundId},#{detailId},#{labelId},#{skuCode},#{cargoName},
#{boxCode},#{operationId},#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3))
""")
int insertCarton(@Param("id") long id, @Param("taskId") long taskId,
@Param("palletId") long palletId, @Param("tenantId") long tenantId,
@Param("inboundId") long inboundId, @Param("detailId") long detailId,
@Param("labelId") long labelId, @Param("skuCode") String skuCode,
@Param("cargoName") String cargoName, @Param("boxCode") String boxCode,
@Param("operationId") String operationId, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_dropship_pallet_task
SET allocated_cartons=allocated_cartons+#{count},version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{taskId} AND task_status='CLAIMED' AND assignee_id=#{operatorId}
AND allocated_cartons+#{count}<=total_cartons
""")
int addAllocated(@Param("taskId") long taskId, @Param("operatorId") long operatorId,
@Param("count") int count);
@Update("""
UPDATE fl_dropship_pallet
SET pallet_no=#{palletNo},pallet_status='COMPLETED',seal_operation_id=#{operationId},
completed_by=#{operatorId},completed_by_name=#{operatorName},completed_at=CURRENT_TIMESTAMP(3),
version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{palletId} AND task_id=#{taskId} AND pallet_status='OPEN' AND version=#{expectedVersion}
AND EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.pallet_id=#{palletId})
""")
int seal(@Param("taskId") long taskId, @Param("palletId") long palletId,
@Param("expectedVersion") int expectedVersion, @Param("palletNo") String palletNo,
@Param("operationId") String operationId, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Select("SELECT COUNT(*) FROM fl_dropship_pallet WHERE task_id=#{taskId} AND pallet_status='OPEN'")
int openPalletCount(@Param("taskId") long taskId);
@Update("""
UPDATE fl_dropship_pallet_task
SET task_status='COMPLETED',completed_at=CURRENT_TIMESTAMP(3),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{taskId} AND task_status='CLAIMED' AND allocated_cartons=total_cartons
AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet p WHERE p.task_id=#{taskId} AND p.pallet_status='OPEN')
""")
int completeTask(@Param("taskId") long taskId);
@Update("""
UPDATE fl_inbound_order o
SET o.process_status='WAIT_SPLIT',o.pallet_count=(SELECT COUNT(*) FROM fl_dropship_pallet p
WHERE p.inbound_id=o.id AND p.pallet_status='COMPLETED'),o.updated_by=#{operatorId},o.version=o.version+1
WHERE o.id=#{inboundId} AND o.tenant_id=#{tenantId} AND o.process_status='WAIT_PALLETIZE'
AND EXISTS(SELECT 1 FROM fl_dropship_pallet_task t WHERE t.id=#{taskId} AND t.task_status='COMPLETED')
""")
int completeInbound(@Param("taskId") long taskId, @Param("tenantId") long tenantId,
@Param("inboundId") long inboundId, @Param("operatorId") long operatorId);
@Select("""
SELECT c.* FROM fl_dropship_pallet_carton c WHERE c.pallet_id=#{palletId}
ORDER BY c.sku_code,c.inbound_detail_id,c.id
""")
List<CartonRow> cartons(@Param("palletId") long palletId);
@Getter @Setter class TaskRow {
private Long id; private Long tenantId; private Long inboundId; private Long warehouseId;
private String inboundCode; private String customerName; private String warehouseName;
private String taskStatus; private Integer totalCartons; private Integer allocatedCartons;
private Long assigneeId; private String assigneeName; private Integer version;
private String processStatus; private String fulfillmentType;
}
@Getter @Setter class PalletRow {
private Long id; private Long taskId; private Long tenantId; private Long inboundId;
private Long warehouseId; private String palletNo; private String palletStatus;
private String openOperationId; private String sealOperationId; private Integer version;
private LocalDateTime completedAt;
}
@Getter @Setter class LabelRow {
private Long cartonLabelId; private Long inboundDetailId; private String skuCode;
private String cargoName; private Integer detailPackageCount; private String inboundCode;
private String boxCode;
}
@Getter @Setter class CartonRow {
private Long inboundId; private Long inboundDetailId; private String skuCode;
private String cargoName; private String boxCode;
}
}
@@ -26,7 +26,9 @@ public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
@Update("""
UPDATE fl_inbound_order
SET process_status = 'WAIT_SPLIT', measure_review_status = 'APPROVED',
SET process_status = CASE WHEN fulfillment_type='ONE_CLICK_DROPSHIP'
THEN 'WAIT_PALLETIZE' ELSE 'WAIT_SPLIT' END,
measure_review_status = 'APPROVED',
actual_gross_weight_kg = (SELECT SUM(weight.gross_weight_kg)
FROM fl_inbound_weight_result weight
WHERE weight.tenant_id = #{tenantId} AND weight.inbound_id = #{inboundId}
@@ -41,7 +43,9 @@ public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
volume_weight_kg = (SELECT COALESCE(SUM(item.volume_weight_kg), 0)
FROM fl_inbound_measurement_item item
WHERE item.tenant_id = #{tenantId} AND item.measurement_id = #{measurementId}),
inbounded_at = CURRENT_TIMESTAMP(3), updated_by = #{operatorId}, version = version + 1
inbounded_at = CASE WHEN fulfillment_type='ONE_CLICK_DROPSHIP'
THEN NULL ELSE CURRENT_TIMESTAMP(3) END,
updated_by = #{operatorId}, version = version + 1
WHERE tenant_id = #{tenantId} AND id = #{inboundId} AND deleted = 0
AND process_status = 'MEASURE_PENDING' AND measure_review_status = 'PENDING'
AND version = #{expectedVersion}
@@ -65,7 +69,10 @@ public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
@Update("""
UPDATE fl_inbound_order
SET process_status = 'WAIT_SPLIT', inbounded_at = CURRENT_TIMESTAMP(3),
SET process_status = CASE WHEN fulfillment_type='ONE_CLICK_DROPSHIP'
THEN 'WAIT_PALLETIZE' ELSE 'WAIT_SPLIT' END,
inbounded_at = CASE WHEN fulfillment_type='ONE_CLICK_DROPSHIP'
THEN NULL ELSE CURRENT_TIMESTAMP(3) END,
updated_by = #{operatorId}, version = version + 1
WHERE tenant_id = #{tenantId} AND id = #{inboundId} AND deleted = 0
AND process_status = 'MEASURE_REJECTED' AND measure_review_status = 'REJECTED'
@@ -80,7 +80,7 @@ public interface LoadingCompletedEventMapper {
""")
List<PalletRow> palletRows(@Param("planId") long planId);
/** 一件代发现场逐箱扫描结果;同一实物托盘可以包含多个客户,但行仍保留客户与入仓单归属。 */
/** 一件代发测量后合托的逐箱结果;装柜只读取已冻结事实,不再现场重新打托。 */
@Select("""
SELECT carton.tenant_id AS account_id,carton.inbound_id AS source_inbound_id,
pallet.id AS pallet_id,1 AS actual_binding,
@@ -88,20 +88,28 @@ public interface LoadingCompletedEventMapper {
COUNT(*) OVER (PARTITION BY pallet.id,carton.tenant_id,carton.inbound_id) AS allocated_package_count,
carton.inbound_detail_id AS source_inbound_detail_id,carton.sku_code,
item.product_name_zh,item.package_count,item.product_quantity,carton.box_code
FROM fl_loading_work_pallet pallet
JOIN fl_loading_work_pallet_carton carton ON carton.work_pallet_id=pallet.id
JOIN fl_loading_plan_item item ON item.id=carton.loading_plan_item_id AND item.deleted=0
JOIN fl_loading_pick_task task ON task.id=pallet.pick_task_id
AND task.loading_plan_id=pallet.loading_plan_id
WHERE pallet.loading_plan_id=#{planId} AND pallet.pallet_status='SEALED'
FROM fl_dropship_pallet pallet
JOIN fl_dropship_pallet_carton carton ON carton.pallet_id=pallet.id
JOIN fl_loading_plan_item item ON item.loading_plan_id=#{planId}
AND item.account_id=carton.tenant_id AND item.inbound_id=carton.inbound_id
AND item.inbound_detail_id=carton.inbound_detail_id AND item.deleted=0
WHERE pallet.pallet_status='COMPLETED'
ORDER BY carton.tenant_id,carton.inbound_id,pallet.id,item.inbound_detail_id,carton.id
""")
List<PalletRow> actualPalletRows(@Param("planId") long planId);
@Select("SELECT COUNT(*) FROM fl_loading_work_pallet WHERE loading_plan_id=#{planId} AND pallet_status<>'CANCELLED'")
@Select("""
SELECT COUNT(DISTINCT p.id) FROM fl_dropship_pallet p
JOIN fl_loading_plan_item i ON i.loading_plan_id=#{planId} AND i.account_id=p.tenant_id
AND i.inbound_id=p.inbound_id AND i.deleted=0 WHERE p.pallet_status='COMPLETED'
""")
int workPalletCount(@Param("planId") long planId);
@Select("SELECT COUNT(*) FROM fl_loading_work_pallet WHERE loading_plan_id=#{planId} AND pallet_status='OPEN'")
@Select("""
SELECT COUNT(DISTINCT p.id) FROM fl_dropship_pallet p
JOIN fl_loading_plan_item i ON i.loading_plan_id=#{planId} AND i.account_id=p.tenant_id
AND i.inbound_id=p.inbound_id AND i.deleted=0 WHERE p.pallet_status='OPEN'
""")
int openWorkPalletCount(@Param("planId") long planId);
@Getter @Setter
@@ -244,7 +244,7 @@ public class InboundOrderRepository {
.filter(StringUtils::hasText)
.collect(Collectors.joining("、"));
return new InboundOrderDetail(InboundOrderSummary.from(order, transport, pickup, locationCodes),
order.getPublicRemark(),
order.getPublicRemark(), List.of(),
palletSummaries, fileSummaries, noteSummaries, historySummaries);
}
@@ -0,0 +1,197 @@
package com.baiyue.firstleg.service;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Pallet;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.ScanResult;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.SkuItem;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Task;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.CartonRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.LabelRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PalletRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.TaskRow;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** 测量完成即下发的一件代发合托,不创建国内仓库托盘、库位或库存。 */
@Service
public class DropshipPalletizationService {
@Resource private DropshipPalletizationMapper mapper;
@Resource private FirstlegDocumentNumberGenerator numbers;
public void createTask(InboundOrderEntity inbound) {
if (!"ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType())) return;
if (inbound.getTotalPackageCount() == null || inbound.getTotalPackageCount() < 1) {
throw invalid("一件代发测量完成箱数无效,不能下发合托任务");
}
mapper.insertTask(IdWorker.getId(), inbound.getTenantId(), inbound.getId(),
inbound.getWarehouseId(), inbound.getTotalPackageCount());
}
public List<DropshipPalletizationDtos.PendingTask> pending(long operatorId, String keyword) {
return mapper.pending(operatorId, normalize(keyword));
}
@Transactional
public Task claim(long taskId, long warehouseId, long operatorId, String operatorName) {
if (mapper.claim(taskId, warehouseId, operatorId, operatorName) != 1) {
throw conflict("任务已被其他人员领取或状态已变化");
}
return task(taskId, warehouseId, operatorId);
}
public Task task(long taskId, long warehouseId, long operatorId) {
TaskRow row = requireTask(taskId, warehouseId);
if (row.getAssigneeId() != null && row.getAssigneeId() != operatorId) {
throw conflict("任务已由其他人员领取");
}
return view(row);
}
@Transactional
public Pallet open(long taskId, DropshipPalletizationDtos.OpenPalletRequest request,
long operatorId, String operatorName) {
TaskRow task = requireWorkingTask(taskId, request.warehouseId(), operatorId);
String operationId = request.operationId().trim();
PalletRow replay = mapper.palletByOpenOperation(operationId);
if (replay != null) {
if (replay.getTaskId() != taskId) throw conflict("开托幂等号已用于其他任务");
return palletView(replay, task.getInboundCode());
}
long id = IdWorker.getId();
mapper.insertPallet(id, taskId, task.getTenantId(), task.getInboundId(), task.getWarehouseId(),
operationId, operatorId, operatorName);
return palletView(mapper.pallet(taskId, id, request.warehouseId()), task.getInboundCode());
}
@Transactional
public ScanResult scan(long taskId, long palletId, DropshipPalletizationDtos.ScanRequest request,
long operatorId, String operatorName) {
TaskRow task = requireWorkingTask(taskId, request.warehouseId(), operatorId);
PalletRow pallet = requireOpenPallet(taskId, palletId, request.warehouseId());
String operationId = request.operationId().trim();
LabelRow scanned = mapper.label(taskId, request.boxCode().trim());
if (scanned == null) throw new BizException(ResultCode.NOT_FOUND, "箱唛不属于当前一件代发入仓单");
if (!StringUtils.hasText(scanned.getSkuCode())) throw conflict("箱唛缺少SKU,不能合托");
if (mapper.operationCount(operationId) > 0) {
return scanResult(scanned, request.cartonCount(), task, pallet);
}
List<LabelRow> labels = mapper.unboundLabels(taskId, task.getTenantId(), task.getInboundId(),
scanned.getInboundDetailId(), scanned.getBoxCode(), request.cartonCount());
if (labels.size() != request.cartonCount()) {
throw conflict("本SKU剩余未上托箱数不足,请刷新任务后重试");
}
for (LabelRow label : labels) {
mapper.insertCarton(IdWorker.getId(), taskId, palletId, task.getTenantId(), task.getInboundId(),
label.getInboundDetailId(), label.getCartonLabelId(), label.getSkuCode(), label.getCargoName(),
label.getBoxCode(), operationId, operatorId, operatorName);
}
if (mapper.addAllocated(taskId, operatorId, labels.size()) != 1) {
throw conflict("任务箱数已变化,请刷新后重试");
}
TaskRow refreshed = requireTask(taskId, request.warehouseId());
return scanResult(scanned, labels.size(), refreshed,
mapper.pallet(taskId, palletId, request.warehouseId()));
}
@Transactional
public Task seal(long taskId, long palletId, DropshipPalletizationDtos.SealPalletRequest request,
long operatorId, String operatorName) {
TaskRow task = requireWorkingTask(taskId, request.warehouseId(), operatorId);
PalletRow replay = mapper.palletBySealOperation(request.operationId().trim());
if (replay != null) {
if (replay.getTaskId() != taskId || replay.getId() != palletId) {
throw conflict("封托幂等号已用于其他托盘");
}
return view(requireTask(taskId, request.warehouseId()));
}
requireOpenPallet(taskId, palletId, request.warehouseId());
String palletNo = numbers.next("DP");
if (mapper.seal(taskId, palletId, request.expectedVersion(), palletNo, request.operationId().trim(),
operatorId, operatorName) != 1) {
throw conflict("空托盘、托盘状态或版本已变化,请刷新后重试");
}
TaskRow refreshed = requireTask(taskId, request.warehouseId());
if (refreshed.getAllocatedCartons().equals(refreshed.getTotalCartons())
&& mapper.openPalletCount(taskId) == 0) {
if (mapper.completeTask(taskId) != 1
|| mapper.completeInbound(taskId, refreshed.getTenantId(), refreshed.getInboundId(), operatorId) != 1) {
throw conflict("合托完成状态已变化,请刷新后重试");
}
}
return view(requireTask(taskId, request.warehouseId()));
}
public List<Pallet> inboundPallets(long tenantId, long inboundId) {
String inboundCode = mapper.inboundCode(tenantId, inboundId);
if (inboundCode == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
return mapper.palletsByInbound(tenantId, inboundId).stream()
.map(row -> palletView(row, inboundCode)).toList();
}
private ScanResult scanResult(LabelRow label, int added, TaskRow task, PalletRow pallet) {
return new ScanResult(label.getSkuCode(), label.getCargoName(), label.getInboundCode(), added,
task.getAllocatedCartons(), task.getTotalCartons() - task.getAllocatedCartons(),
palletView(pallet, task.getInboundCode()));
}
private Task view(TaskRow row) {
return new Task(row.getId(), row.getInboundId(), row.getInboundCode(), row.getTenantId(),
row.getCustomerName(), row.getWarehouseId(), row.getWarehouseName(), row.getTotalCartons(),
row.getAllocatedCartons(), row.getTaskStatus(), row.getAssigneeId(), row.getAssigneeName(),
row.getVersion(), mapper.pallets(row.getId()).stream()
.map(pallet -> palletView(pallet, row.getInboundCode())).toList());
}
private Pallet palletView(PalletRow row, String inboundCode) {
List<CartonRow> cartons = mapper.cartons(row.getId());
Map<String, List<CartonRow>> groups = new LinkedHashMap<>();
for (CartonRow carton : cartons) {
groups.computeIfAbsent(carton.getInboundDetailId() + "|" + carton.getSkuCode(), ignored -> new ArrayList<>())
.add(carton);
}
List<SkuItem> items = groups.values().stream().map(values -> {
CartonRow first = values.get(0);
return new SkuItem(first.getInboundId(), inboundCode, first.getInboundDetailId(), first.getSkuCode(),
first.getCargoName(), values.size(), values.stream().map(CartonRow::getBoxCode).toList());
}).toList();
return new Pallet(row.getId(), row.getPalletNo(), row.getPalletStatus(), cartons.size(), items.size(),
row.getVersion(), items, row.getCompletedAt());
}
private TaskRow requireWorkingTask(long taskId, long warehouseId, long operatorId) {
TaskRow task = requireTask(taskId, warehouseId);
if (!"CLAIMED".equals(task.getTaskStatus())) throw conflict("请先领取待合托任务");
if (task.getAssigneeId() == null || task.getAssigneeId() != operatorId) throw conflict("任务不属于当前操作员");
if (!"WAIT_PALLETIZE".equals(task.getProcessStatus())) throw conflict("入仓单已不在待合托状态");
return task;
}
private TaskRow requireTask(long taskId, long warehouseId) {
TaskRow task = mapper.task(taskId, warehouseId);
if (task == null) throw new BizException(ResultCode.NOT_FOUND, "未找到当前仓库的一件代发合托任务");
return task;
}
private PalletRow requireOpenPallet(long taskId, long palletId, long warehouseId) {
PalletRow pallet = mapper.pallet(taskId, palletId, warehouseId);
if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "托盘不存在");
if (!"OPEN".equals(pallet.getPalletStatus())) throw conflict("托盘已完成,不能继续上托");
return pallet;
}
private static String normalize(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); }
private static BizException invalid(String message) { return new BizException(ResultCode.VALIDATION_FAILED, message); }
}
@@ -19,7 +19,7 @@ import java.util.Set;
public class FirstlegDocumentNumberGenerator {
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyMMdd", Locale.ROOT);
private static final Set<String> SUPPORTED_PREFIXES = Set.of("OB", "TA", "CB", "CAB", "LP");
private static final Set<String> SUPPORTED_PREFIXES = Set.of("OB", "TA", "CB", "CAB", "LP", "DP");
private static final long MAX_DAILY_SEQUENCE = 9_999L;
private final JdbcTemplate jdbc;
@@ -19,6 +19,7 @@ import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.util.List;
import java.util.ArrayList;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Pallet;
/** Allocates stable carton codes and forwards immutable label facts to Print. */
@Service
@@ -91,6 +92,28 @@ public class InboundCartonLabelPrintService {
return ResponseEntity.status(response.getStatusCode()).headers(headers).body(response.getBody());
}
/** 一件代发托盘标签:托盘号、入仓码、SKU箱数和托盘二维码。 */
public ResponseEntity<byte[]> generateDropshipPallet(Pallet pallet, String requestId) {
if (pallet == null || !StringUtils.hasText(pallet.palletNo()) || pallet.items().isEmpty()) {
throw new BizException(ResultCode.CONFLICT, "只有已完成且包含货物的一件代发托盘可以打印");
}
String inboundCodes = pallet.items().stream().map(item -> item.inboundCode())
.filter(StringUtils::hasText).distinct().collect(java.util.stream.Collectors.joining("/"));
String skuSummary = pallet.items().stream()
.map(item -> item.skuCode() + "×" + item.cartonCount())
.collect(java.util.stream.Collectors.joining(" / "));
PrintPdfClient.Label label = new PrintPdfClient.Label(pallet.palletNo(), inboundCodes,
"CTNS:" + pallet.cartonCount(), skuSummary, pallet.palletNo(), null);
PrintPdfClient.InboundMarkPdfRequest request = new PrintPdfClient.InboundMarkPdfRequest(
"INBOUND_MARK_V1", new PrintPdfClient.PageSpec(PAGE_MM, PAGE_MM),
pallet.palletNo() + "-托盘唛.pdf", List.of(label));
try {
return requirePdf(printClient.generateInboundMarks(internalToken, requestId, request));
} catch (FeignException | NoFallbackAvailableException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "打印服务暂不可用,请稍后重试");
}
}
private ResponseEntity<byte[]> generatePdf(String fileName, List<InboundCartonLabels.Label> labels,
String requestId) {
PrintPdfClient.InboundMarkPdfRequest request = new PrintPdfClient.InboundMarkPdfRequest(
@@ -105,7 +128,7 @@ public class InboundCartonLabelPrintService {
private PrintPdfClient.Label toPrintLabel(InboundCartonLabels.Label label) {
return new PrintPdfClient.Label(label.topCode(), label.warehouseText(), label.cartonText(),
label.routeText(), label.boxCode());
label.routeText(), label.boxCode(), label.skuCode());
}
private ResponseEntity<byte[]> requirePdf(ResponseEntity<byte[]> response) {
@@ -291,7 +291,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
new InboundCartonLabels.Label(entity.getId(), entity.getInboundDetailId(), entity.getCargoItemNo(),
entity.getCargoName(), entity.getDetailPackageCount(), entity.getBoxCode(),
entity.getTotalCartonCount(), entity.getCartonIndex(), entity.getBoxSpecification(),
entity.getLengthCm(), entity.getWidthCm(), entity.getHeightCm(), inbound.getInboundCode(),
entity.getLengthCm(), entity.getWidthCm(), entity.getHeightCm(), entity.getSkuCode(), inbound.getInboundCode(),
fallback(inbound.getWarehouseName(), "WAREHOUSE"),
"CTNS:" + entity.getTotalCartonCount() + "-" + entity.getCartonIndex(),
fallback(inbound.getChannelName(), fallback(inbound.getChannelCode(), "ROUTE"))))
@@ -19,6 +19,8 @@ import org.springframework.util.StringUtils;
public class InboundOrderQueryService {
@Resource
private InboundOrderRepository repository;
@Resource
private DropshipPalletizationService dropshipPalletization;
public PageResult<InboundOrderSummary> page(Long tenantId, InboundOrderQuery query) {
validateQuery(query);
@@ -30,7 +32,11 @@ public class InboundOrderQueryService {
if (order == null) {
throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
}
return repository.detail(tenantId, order);
InboundOrderDetail detail = repository.detail(tenantId, order);
if (!"ONE_CLICK_DROPSHIP".equals(order.getFulfillmentType())) return detail;
return new InboundOrderDetail(detail.summary(), detail.publicRemark(),
dropshipPalletization.inboundPallets(tenantId, inboundId), detail.palletLocations(),
detail.files(), detail.internalNotes(), detail.statusHistory());
}
private void validateQuery(InboundOrderQuery query) {
@@ -33,6 +33,8 @@ public class MeasurementReviewService {
private InboundIdempotencyService idempotency;
@Resource
private InboundProperties properties;
@Resource
private DropshipPalletizationService dropshipPalletization;
@Transactional
public void approve(long tenantId, long operatorId, String operatorName, long inboundId,
@@ -117,8 +119,13 @@ public class MeasurementReviewService {
}
repository.insertReview(reviewRecord(tenantId, inboundId, request.measurementId(), result,
request.reason(), false, null, false, operatorId, operatorName));
if (approved && "ONE_CLICK_DROPSHIP".equals(order.getFulfillmentType())) {
dropshipPalletization.createTask(order);
}
String approvedStatus = "ONE_CLICK_DROPSHIP".equals(order.getFulfillmentType())
? "WAIT_PALLETIZE" : "WAIT_SPLIT";
repository.insertHistory(history(tenantId, inboundId, "MEASURE_PENDING",
approved ? "WAIT_SPLIT" : "MEASURE_REJECTED",
approved ? approvedStatus : "MEASURE_REJECTED",
approved ? "MEASURE_APPROVE" : "MEASURE_REJECT", request.reason(), operatorId, operatorName));
idempotency.complete(context, inboundId);
}
@@ -152,9 +159,13 @@ public class MeasurementReviewService {
if (repository.releaseRejected(tenantId, inboundId, operatorId, request.expectedVersion()) != 1) {
throw concurrentConflict();
}
if ("ONE_CLICK_DROPSHIP".equals(order.getFulfillmentType())) {
dropshipPalletization.createTask(order);
}
repository.insertReview(reviewRecord(tenantId, inboundId, request.measurementId(), "REJECTED",
null, true, request.reason(), true, operatorId, operatorName));
repository.insertHistory(history(tenantId, inboundId, "MEASURE_REJECTED", "WAIT_SPLIT",
repository.insertHistory(history(tenantId, inboundId, "MEASURE_REJECTED",
"ONE_CLICK_DROPSHIP".equals(order.getFulfillmentType()) ? "WAIT_PALLETIZE" : "WAIT_SPLIT",
"MEASURE_EXCEPTION_RELEASE", request.reason(), operatorId, operatorName));
idempotency.complete(context, inboundId);
}
@@ -28,7 +28,7 @@ import java.util.stream.Collectors;
@Service
public class UnifiedInboundQueryService {
private static final Set<String> MAIN_STATUSES = Set.of(
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
private static final Set<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED");
@Resource private InboundOrderRepository inbounds;
@@ -165,6 +165,7 @@ public class UnifiedInboundQueryService {
case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
"WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING" -> "WAIT_MEASURE";
case "WAIT_SPLIT" -> "WAIT_SPLIT";
case "WAIT_PALLETIZE" -> "WAIT_PALLETIZE";
case "LOADING", "LOADED", "DELIVERED" -> status;
default -> null;
};
@@ -175,6 +176,7 @@ public class UnifiedInboundQueryService {
return switch (mainStatus) {
case "PENDING_REVIEW" -> "APPROVE_ORDER";
case "WAIT_MEASURE" -> "MEASURE_PENDING".equals(processStatus) ? "CONFIRM_MEASUREMENT" : null;
case "WAIT_PALLETIZE" -> "PALLETIZE_DROPSHIP";
case "WAIT_SPLIT" -> !"WAIT_SPLIT".equals(processStatus) ? null
: "CONFIRMED".equals(preloadStatus) ? null
: preloadStatus == null ? "SMART_SPLIT" : "CONFIRM_PRELOAD";
@@ -195,6 +197,7 @@ public class UnifiedInboundQueryService {
case "WAIT_MEASURE" -> List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
"WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING");
case "WAIT_SPLIT" -> List.of("WAIT_SPLIT");
case "WAIT_PALLETIZE" -> List.of("WAIT_PALLETIZE");
case "LOADING", "LOADED", "DELIVERED" -> List.of(mainStatus);
default -> List.of("__NO_MATCH__");
};
@@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键',
order_no VARCHAR(32) NOT NULL COMMENT '订单号快照',
inbound_code VARCHAR(40) NOT NULL COMMENT '全平台唯一入仓码',
process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、WAIT_SPLIT待分柜',
process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURE_PENDING待审核、WAIT_PALLETIZE一件代发待合托、WAIT_SPLIT待分柜',
order_status_snapshot VARCHAR(24) NOT NULL COMMENT '订单状态快照',
measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回',
fee_status VARCHAR(24) NOT NULL DEFAULT 'UNPRICED' COMMENT '费用状态:UNPRICED未计费、CALCULATED已计算、CONFIRMED已确认、SENT已发送、VOIDED已作废',
@@ -847,3 +847,81 @@ CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
CONSTRAINT chk_customer_dimension_edge_addition
CHECK (edge_addition_cm >= 0.001 AND edge_addition_cm <= 100.000)
) ENGINE=InnoDB COMMENT='客户固定四箱规尺寸配置';
-- 一件代发测量完成后进入 WAIT_PALLETIZE,完成合托后才进入 WAIT_SPLIT。
-- 一件代发测量完成后的独立 PDA 合托;不代表国内仓库存入库。
CREATE TABLE IF NOT EXISTS fl_dropship_pallet_task (
id BIGINT UNSIGNED NOT NULL COMMENT '合托任务ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离键',
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '一件代发入仓单ID',
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '作业仓库ID',
task_status VARCHAR(24) NOT NULL COMMENT 'PENDING/CLAIMED/COMPLETED',
total_cartons INT UNSIGNED NOT NULL COMMENT '测量确认总箱数',
allocated_cartons INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已上托箱数',
assignee_id BIGINT UNSIGNED NULL,
assignee_name VARCHAR(64) NULL,
claimed_at DATETIME(3) NULL,
completed_at DATETIME(3) NULL,
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_dropship_task_inbound (tenant_id,inbound_id),
KEY idx_dropship_task_operator (task_status,assignee_id,warehouse_id,created_at),
CONSTRAINT fk_dropship_task_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order(id),
CONSTRAINT chk_dropship_task_status CHECK (task_status IN ('PENDING','CLAIMED','COMPLETED')),
CONSTRAINT chk_dropship_task_cartons CHECK (allocated_cartons<=total_cartons)
) ENGINE=InnoDB COMMENT='一件代发测量后PDA合托任务';
CREATE TABLE IF NOT EXISTS fl_dropship_pallet (
id BIGINT UNSIGNED NOT NULL COMMENT '合托托盘ID',
task_id BIGINT UNSIGNED NOT NULL,
tenant_id BIGINT UNSIGNED NOT NULL,
inbound_id BIGINT UNSIGNED NOT NULL,
warehouse_id BIGINT UNSIGNED NOT NULL,
pallet_no VARCHAR(32) NULL COMMENT '封托时生成的DP托盘号',
pallet_status VARCHAR(16) NOT NULL COMMENT 'OPEN/COMPLETED',
open_operation_id VARCHAR(128) NOT NULL,
seal_operation_id VARCHAR(128) NULL,
created_by BIGINT UNSIGNED NOT NULL,
created_by_name VARCHAR(64) NULL,
completed_by BIGINT UNSIGNED NULL,
completed_by_name VARCHAR(64) NULL,
completed_at DATETIME(3) NULL,
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_dropship_pallet_no (pallet_no),
UNIQUE KEY uk_dropship_pallet_open_op (open_operation_id),
UNIQUE KEY uk_dropship_pallet_seal_op (seal_operation_id),
KEY idx_dropship_pallet_inbound (tenant_id,inbound_id,pallet_status),
CONSTRAINT fk_dropship_pallet_task FOREIGN KEY (task_id) REFERENCES fl_dropship_pallet_task(id),
CONSTRAINT chk_dropship_pallet_status CHECK (pallet_status IN ('OPEN','COMPLETED'))
) ENGINE=InnoDB COMMENT='一件代发合托形成的可打印托盘';
CREATE TABLE IF NOT EXISTS fl_dropship_pallet_carton (
id BIGINT UNSIGNED NOT NULL,
task_id BIGINT UNSIGNED NOT NULL,
pallet_id BIGINT UNSIGNED NOT NULL,
tenant_id BIGINT UNSIGNED NOT NULL,
inbound_id BIGINT UNSIGNED NOT NULL,
inbound_detail_id BIGINT UNSIGNED NOT NULL,
carton_label_id BIGINT UNSIGNED NOT NULL,
sku_code VARCHAR(128) NOT NULL,
cargo_name VARCHAR(255) NULL,
box_code VARCHAR(64) NOT NULL,
operation_id VARCHAR(128) NOT NULL,
bound_by BIGINT UNSIGNED NOT NULL,
bound_by_name VARCHAR(64) NULL,
bound_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_dropship_carton_task_label (task_id,carton_label_id),
KEY idx_dropship_carton_pallet_sku (pallet_id,sku_code,id),
KEY idx_dropship_carton_inbound (tenant_id,inbound_id,pallet_id),
KEY idx_dropship_carton_operation (operation_id),
CONSTRAINT fk_dropship_carton_task FOREIGN KEY (task_id) REFERENCES fl_dropship_pallet_task(id),
CONSTRAINT fk_dropship_carton_pallet FOREIGN KEY (pallet_id) REFERENCES fl_dropship_pallet(id),
CONSTRAINT fk_dropship_carton_label FOREIGN KEY (carton_label_id) REFERENCES fl_inbound_carton_label(id)
) ENGINE=InnoDB COMMENT='一件代发托盘内逐箱SKU和入仓单明细';
@@ -0,0 +1,54 @@
openapi: 3.0.3
info:
title: 一件代发 PDA 合托 API
version: 1.0.0
description: 测量完成后独立下发;不创建国内库存。PDA 扫箱唛识别 SKU,并按箱数合托。
paths:
/api/platform/v1/first-leg/pda-dropship/tasks/pending:
get:
summary: 查询待领取或本人已领取的一件代发合托任务
responses: { '200': { description: OK } }
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}:
get:
summary: 查询任务、托盘、入仓单、SKU 和箱数明细
parameters:
- { name: taskId, in: path, required: true, schema: { type: integer, format: int64 } }
- { name: warehouseId, in: query, required: true, schema: { type: integer, format: int64 } }
responses: { '200': { description: OK } }
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/claim:
post:
summary: 领取合托任务
parameters: [{ name: taskId, in: path, required: true, schema: { type: integer, format: int64 } }]
responses: { '200': { description: OK } }
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/pallets/open:
post:
summary: 开启一个现场托盘
parameters: [{ name: taskId, in: path, required: true, schema: { type: integer, format: int64 } }]
responses: { '200': { description: OK } }
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/pallets/{palletId}/scan:
post:
summary: 扫箱唛识别 SKU 并填写上托箱数
parameters:
- { name: taskId, in: path, required: true, schema: { type: integer, format: int64 } }
- { name: palletId, in: path, required: true, schema: { type: integer, format: int64 } }
responses: { '200': { description: OK } }
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/pallets/{palletId}/seal:
post:
summary: 完成合托并生成 DP 托盘号
parameters:
- { name: taskId, in: path, required: true, schema: { type: integer, format: int64 } }
- { name: palletId, in: path, required: true, schema: { type: integer, format: int64 } }
responses: { '200': { description: OK } }
/api/platform/v1/first-leg/pda-dropship/inbound-orders/{inboundId}/pallets:
get:
summary: 入仓单查看绑定的一个或多个托盘及各自箱数
parameters: [{ name: inboundId, in: path, required: true, schema: { type: integer, format: int64 } }]
responses: { '200': { description: OK } }
/api/platform/v1/first-leg/pda-dropship/inbound-orders/{inboundId}/pallets/{palletId}/print:
get:
summary: 打印托盘号、入仓码和 SKU 箱数托盘唛
parameters:
- { name: inboundId, in: path, required: true, schema: { type: integer, format: int64 } }
- { name: palletId, in: path, required: true, schema: { type: integer, format: int64 } }
responses:
'200': { description: PDF, content: { application/pdf: { schema: { type: string, format: binary } } } }
@@ -1140,7 +1140,7 @@ components:
orderId: {type: integer, format: int64, nullable: true}
orderNo: {type: string, nullable: true}
inboundCode: {type: string, nullable: true}
processStatus: {type: string, nullable: true, description: Firstleg 内部状态;入库完成记录进入 WAIT_SPLIT}
processStatus: {type: string, nullable: true, description: Firstleg 内部状态;一件代发测量后先进入 WAIT_PALLETIZE,合托完成再进入 WAIT_SPLIT}
orderStatusSnapshot: {type: string, nullable: true}
measureReviewStatus: {type: string, nullable: true}
feeStatus: {type: string, nullable: true}
@@ -27,6 +27,8 @@ import static org.mockito.Mockito.when;
class InboundOrderQueryServiceTest {
@MockBean
private InboundOrderRepository repository;
@MockBean
private DropshipPalletizationService dropshipPalletization;
@Resource
private InboundOrderQueryService service;
@@ -46,6 +46,7 @@ class MeasurementReviewServiceTest {
@MockBean private InboundMeasurementRepository measurements;
@MockBean private InboundIdempotencyService idempotency;
@MockBean private InboundProperties properties;
@MockBean private DropshipPalletizationService dropshipPalletization;
@Resource private MeasurementReviewService service;
@Test
@@ -26,7 +26,12 @@ public final class InboundMarkPdfDtos {
String warehouseText,
String cartonText,
String routeText,
String qrContent
String qrContent,
String skuCode
) {
public Label(String topCode, String warehouseText, String cartonText,
String routeText, String qrContent) {
this(topCode, warehouseText, cartonText, routeText, qrContent, null);
}
}
}
@@ -41,6 +41,9 @@ public class InboundMarkV1Renderer {
drawCenteredText(stream, units, font, label.warehouseText(), 36, 26);
drawCenteredText(stream, units, font, label.cartonText(), 22, 37);
drawCenteredText(stream, units, font, label.routeText(), 21, 47);
if (label.skuCode() != null) {
drawCenteredText(stream, units, font, "SKU: " + label.skuCode(), 14, 55);
}
qrRenderer.draw(stream, qrMatrix, QR_LEFT_PT, QR_BOTTOM_PT, QR_SIZE_PT);
drawCenteredText(stream, units, font, label.qrContent(), 12, 85.5);
drawCenteredText(stream, units, font, "MADE IN CHINA", 14, 92);
@@ -110,7 +110,8 @@ public class InboundMarkPdfService {
normalizeRequired(label.warehouseText(), field(index, "warehouseText")),
normalizeRequired(label.cartonText(), field(index, "cartonText")),
normalizeRequired(label.routeText(), field(index, "routeText")),
normalizeQr(label.qrContent(), field(index, "qrContent"))));
normalizeQr(label.qrContent(), field(index, "qrContent")),
normalizeOptional(label.skuCode(), field(index, "skuCode"))));
}
return new NormalizedRequest(normalizeFileName(request.downloadFileName()), List.copyOf(labels));
}
@@ -165,6 +166,14 @@ public class InboundMarkPdfService {
return normalized;
}
private String normalizeOptional(String value, String field) {
if (value == null || value.isBlank()) return null;
String normalized = Normalizer.normalize(value.trim(), Normalizer.Form.NFC);
if (normalized.codePointCount(0, normalized.length()) > 64) invalid(field + " must not exceed 64 characters");
normalized.codePoints().forEach(codePoint -> { if (isForbiddenControl(codePoint)) invalid(field + " contains a control character"); });
return normalized;
}
private boolean isForbiddenControl(int codePoint) {
int type = Character.getType(codePoint);
return Character.isISOControl(codePoint)
@@ -160,6 +160,11 @@ components:
minLength: 1
maxLength: 34
pattern: '^[\\x20-\\x7E]+$'
skuCode:
type: string
maxLength: 64
nullable: true
description: 一件代发箱唛显示的 SKU;二维码仍只使用 qrContent 稳定箱码
FreightA4PdfRequest:
type: object
required: [templateCode, pageSpec, documents]