This commit is contained in:
2026-08-06 14:55:24 +08:00
parent dca3a48284
commit f3cfbdf59b
9 changed files with 351 additions and 20 deletions
+41 -5
View File
@@ -1,10 +1,10 @@
# 测试环境 SQL 执行说明
## 唯一数据库发布入口
## 唯一数据库初始化与发布入口
- 固定入口:`头程数据库统一增量-MySQL8.sql`。
- 该脚本汇总一期、二期当前全部数据库基线、存量升级、IAM 权限资源和最终结构校验,适用于 MySQL 8.0。
- 后续数据库变更只允许在该文件末尾增加带日期和业务说明的幂等增量区块,不再创建新的部署入口 SQL。
- 固定入口:`头程数据库统一增量-MySQL8.sql`。文件名保留历史兼容,内容维护为当前完整数据库基线和存量环境兼容升级,不再按日期拆分发布脚本。
- 该脚本汇总一期、二期及已实现三期出仓能力的数据库基线、存量升级、IAM 权限资源和最终结构校验,适用于 MySQL 8.0。
- 正式上线前的数据库变更直接维护该文件中的当前基线、存量兼容处理和最终校验,不再创建按日期拆分的发布 SQL;正式上线后的迁移策略在生产发布规范中另行冻结。
- 模块 `src/main/resources/db/` 下的 SQL 用于开发溯源;`deploy/sql/` 下已有日期文件全部冻结为历史记录,不再单独部署,也不得与固定入口重复执行。
- 固定入口不包含 `auth/phase1-admin-accounts.sql` 和 `warehouse/warehouse-seed.sql`,不会写入固定密码账号、演示仓库、演示快递公司或演示订单;正式平台公共仓库主数据已内联到固定入口。
- `warehouse/phase2-pallet-location-schema-migration.sql` 的有效结构已被当前 `phase2-pallet-location-schema.sql` 和后续共享溢出位迁移完整覆盖,因此不重复内联,避免每次部署无条件删除并重建同名唯一索引。
@@ -46,7 +46,43 @@
在 Navicat、DataGrip 或 MySQL 命令行中以“运行脚本”方式完整执行,确保客户端支持 `DELIMITER`。不要只选中局部语句执行。
执行结束后必须看到 `migration_status = PASS`,并确认消息为“一期与二期统一增量执行完成,关键结构与二期权限校验通过”。如果中途出现 `SQLSTATE 45000`,必须按错误中给出的缺失表、字段或权限处理后重新完整执行。
执行结束后必须看到 `migration_status = PASS`。如果中途出现 `SQLSTATE 45000`,必须按错误中给出的缺失表、字段或权限处理后重新完整执行。
## 三期分柜、预装、拣货与装柜当前基线
- 模块表结构来源:`firstleg/src/main/resources/db/phase3-split-preload-schema.sql`、`firstleg/src/main/resources/db/phase3-loading-delivery-schema.sql`。
- 三期平台权限直接维护在统一初始化与发布入口中,避免额外增加独立权限脚本。
- 测试和生产环境仍只执行固定入口 `头程数据库统一增量-MySQL8.sql`,不得再串行执行上述模块脚本。
- 必须先执行统一增量并取得 `PASS`,再启动或重启 Firstleg Service;否则统一订单摘要查询会因缺少三期操作表而失败。
- 统一脚本会创建 `fl_preload_plan`、`fl_split_operation`、`fl_loading_evidence`、`fl_loading_operation`、`fl_loading_pick_task`,并补齐 9 项平台操作权限。新增权限默认只授予平台系统管理员,其他角色由平台权限管理按职责配置。
部署前可只读盘点:
```sql
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'baiyue_firstleg'
AND table_name IN (
'fl_preload_plan', 'fl_split_operation', 'fl_loading_evidence',
'fl_loading_operation', 'fl_loading_pick_task'
)
ORDER BY table_name;
SELECT permission_code
FROM baiyue_iam.iam_permission
WHERE tenant_id = 0 AND app_code = 'PLATFORM' AND status = 'ACTIVE' AND deleted = 0
AND permission_code IN (
'firstleg:inbound:split', 'firstleg:inbound:preload',
'firstleg:inbound:preload-view',
'platform:firstleg:inbound:loading:edit',
'platform:firstleg:inbound:loading:view',
'platform:firstleg:inbound:loading:complete',
'platform:firstleg:inbound:delivery:complete',
'platform:firstleg:inbound:picking:edit',
'platform:firstleg:inbound:picking:view'
)
ORDER BY permission_code;
```
## 公共导出中心存量升级
@@ -4503,10 +4503,253 @@ CALL unified_publish_fee_add_index('fl_inbound_fee_detail', 'idx_inbound_fee_pub
'KEY `idx_inbound_fee_published` (`tenant_id`, `inbound_id`, `publication_status`, `source_fee_version`)');
DROP PROCEDURE IF EXISTS unified_publish_fee_add_index;
-- ============================================================================
-- 2026-08-06:三期分柜、预装、拣货、装柜和派送当前基线
-- ============================================================================
USE baiyue_firstleg;
CREATE TABLE IF NOT EXISTS fl_preload_plan (
id BIGINT UNSIGNED NOT NULL COMMENT '预装清单ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单/订单ID',
status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/CONFIRMING/CONFIRMED',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本',
items_json JSON NOT NULL COMMENT '可编辑分柜明细快照',
remark VARCHAR(1000) NULL COMMENT '预装备注',
created_by BIGINT UNSIGNED NOT NULL,
created_by_name VARCHAR(64) NULL,
updated_by BIGINT UNSIGNED NOT NULL,
updated_by_name VARCHAR(64) NULL,
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_preload_tenant_inbound (tenant_id, inbound_id),
KEY idx_preload_status (tenant_id, status, updated_at),
CONSTRAINT fk_preload_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
) ENGINE=InnoDB COMMENT='订单分柜预装清单';
CREATE TABLE IF NOT EXISTS fl_split_operation (
id BIGINT UNSIGNED NOT NULL COMMENT '幂等记录ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
inbound_id BIGINT UNSIGNED NOT NULL,
operator_id BIGINT UNSIGNED NOT NULL,
operation VARCHAR(40) NOT NULL,
idempotency_key VARCHAR(128) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT 'PROCESSING/SUCCESS',
result_json JSON NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_split_operation (tenant_id, inbound_id, operator_id, operation, idempotency_key),
KEY idx_split_operation_time (tenant_id, inbound_id, created_at),
CONSTRAINT fk_split_operation_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
) ENGINE=InnoDB COMMENT='分柜操作幂等与审计索引';
CREATE TABLE IF NOT EXISTS fl_loading_evidence (
id BIGINT UNSIGNED NOT NULL,
tenant_id BIGINT UNSIGNED NOT NULL,
inbound_id BIGINT UNSIGNED NOT NULL,
evidence_type VARCHAR(32) NOT NULL COMMENT 'PHOTO_STEP_1..4/SEAL',
file_id BIGINT UNSIGNED NULL COMMENT 'File 服务逻辑文件ID',
file_name VARCHAR(255) NULL,
seal_no VARCHAR(64) NULL,
remark VARCHAR(1000) NULL,
version INT NOT NULL DEFAULT 0,
created_by BIGINT UNSIGNED NOT NULL,
created_by_name VARCHAR(64) NULL,
updated_by BIGINT UNSIGNED NOT NULL,
updated_by_name VARCHAR(64) NULL,
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_loading_evidence_type (tenant_id, inbound_id, evidence_type),
KEY idx_loading_evidence_time (tenant_id, inbound_id, updated_at),
CONSTRAINT fk_loading_evidence_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
) ENGINE=InnoDB COMMENT='订单装柜四步照片和铅封证据';
CREATE TABLE IF NOT EXISTS fl_loading_operation (
id BIGINT UNSIGNED NOT NULL,
tenant_id BIGINT UNSIGNED NOT NULL,
inbound_id BIGINT UNSIGNED NOT NULL,
operator_id BIGINT UNSIGNED NOT NULL,
operation VARCHAR(48) NOT NULL,
idempotency_key VARCHAR(128) NOT NULL,
request_hash CHAR(64) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING',
result_json JSON NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
completed_at DATETIME(3) NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_loading_operation (tenant_id, inbound_id, operator_id, operation, idempotency_key),
KEY idx_loading_operation_time (tenant_id, inbound_id, created_at),
CONSTRAINT fk_loading_operation_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
) ENGINE=InnoDB COMMENT='装柜与派送写操作幂等记录';
CREATE TABLE IF NOT EXISTS fl_loading_pick_task (
id BIGINT UNSIGNED NOT NULL,
tenant_id BIGINT UNSIGNED NOT NULL,
inbound_id BIGINT UNSIGNED NOT NULL,
status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/IN_PROGRESS/COMPLETED',
version INT NOT NULL DEFAULT 0,
expected_count INT UNSIGNED NULL,
picked_count INT UNSIGNED NULL,
remark VARCHAR(1000) NULL,
updated_by BIGINT UNSIGNED NOT NULL,
updated_by_name VARCHAR(64) NULL,
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_loading_pick_tenant_inbound (tenant_id, inbound_id),
KEY idx_loading_pick_status (tenant_id, status, updated_at),
CONSTRAINT fk_loading_pick_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
) ENGINE=InnoDB COMMENT='PDA拣货任务及结果';
USE baiyue_iam;
INSERT INTO iam_permission (
id, tenant_id, app_code, permission_type, parent_id, permission_code,
permission_name, route_path, http_method, sort_no, status, built_in, deleted
)
VALUES
(5102091, 0, 'PLATFORM', 'BUTTON', 5102030,
'firstleg:inbound:split', '智能分柜', NULL, NULL, 140, 'ACTIVE', 1, 0),
(5102092, 0, 'PLATFORM', 'BUTTON', 5102030,
'firstleg:inbound:preload', '编辑并确认预装清单', NULL, NULL, 150, 'ACTIVE', 1, 0),
(5102093, 0, 'PLATFORM', 'BUTTON', 5102030,
'firstleg:inbound:preload-view', '查看预装清单', NULL, NULL, 160, 'ACTIVE', 1, 0),
(5102094, 0, 'PLATFORM', 'BUTTON', 5102030,
'platform:firstleg:inbound:loading:edit', '维护装柜证据', NULL, NULL, 170, 'ACTIVE', 1, 0),
(5102095, 0, 'PLATFORM', 'BUTTON', 5102030,
'platform:firstleg:inbound:loading:view', '查看装柜证据', NULL, NULL, 180, 'ACTIVE', 1, 0),
(5102096, 0, 'PLATFORM', 'BUTTON', 5102030,
'platform:firstleg:inbound:loading:complete', '完成装柜', NULL, NULL, 190, 'ACTIVE', 1, 0),
(5102097, 0, 'PLATFORM', 'BUTTON', 5102030,
'platform:firstleg:inbound:delivery:complete', '完成派送', NULL, NULL, 200, 'ACTIVE', 1, 0),
(5102098, 0, 'PLATFORM', 'BUTTON', 5102030,
'platform:firstleg:inbound:picking:edit', '执行拣货', NULL, NULL, 210, 'ACTIVE', 1, 0),
(5102099, 0, 'PLATFORM', 'BUTTON', 5102030,
'platform:firstleg:inbound:picking:view', '查看拣货任务', NULL, NULL, 220, 'ACTIVE', 1, 0)
ON DUPLICATE KEY UPDATE
permission_type = VALUES(permission_type), parent_id = VALUES(parent_id),
permission_name = VALUES(permission_name), route_path = VALUES(route_path),
http_method = VALUES(http_method), sort_no = VALUES(sort_no),
status = 'ACTIVE', built_in = 1, deleted = 0;
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
SELECT 4000001, id, 0
FROM iam_permission
WHERE tenant_id = 0 AND app_code = 'PLATFORM'
AND permission_code IN (
'firstleg:inbound:split',
'firstleg:inbound:preload',
'firstleg:inbound:preload-view',
'platform:firstleg:inbound:loading:edit',
'platform:firstleg:inbound:loading:view',
'platform:firstleg:inbound:loading:complete',
'platform:firstleg:inbound:delivery:complete',
'platform:firstleg:inbound:picking:edit',
'platform:firstleg:inbound:picking:view'
)
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
DROP PROCEDURE IF EXISTS unified_assert_final_extensions;
DELIMITER $$
CREATE PROCEDURE unified_assert_final_extensions()
BEGIN
IF (SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = 'baiyue_firstleg'
AND table_name IN ('fl_preload_plan', 'fl_split_operation',
'fl_loading_evidence', 'fl_loading_operation',
'fl_loading_pick_task')) <> 5 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:三期分柜、预装或装柜表未完整创建';
END IF;
IF (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_preload_plan'
AND column_name IN ('id', 'tenant_id', 'inbound_id', 'status', 'version',
'items_json', 'remark', 'created_by', 'created_by_name',
'updated_by', 'updated_by_name', 'created_at', 'updated_at')) <> 13
OR (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_split_operation'
AND column_name IN ('id', 'tenant_id', 'inbound_id', 'operator_id', 'operation',
'idempotency_key', 'status', 'result_json', 'created_at')) <> 9
OR (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_evidence'
AND column_name IN ('id', 'tenant_id', 'inbound_id', 'evidence_type', 'file_id',
'file_name', 'seal_no', 'remark', 'version', 'created_by',
'created_by_name', 'updated_by', 'updated_by_name',
'created_at', 'updated_at')) <> 15
OR (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_operation'
AND column_name IN ('id', 'tenant_id', 'inbound_id', 'operator_id', 'operation',
'idempotency_key', 'request_hash', 'status', 'result_json',
'created_at', 'completed_at')) <> 11
OR (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'
AND column_name IN ('id', 'tenant_id', 'inbound_id', 'status', 'version',
'expected_count', 'picked_count', 'remark', 'updated_by',
'updated_by_name', 'created_at', 'updated_at')) <> 12 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:三期分柜、预装或装柜关键字段未完整创建';
END IF;
IF (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg'
AND table_name IN ('fl_preload_plan', 'fl_split_operation', 'fl_loading_operation')
AND column_name IN ('items_json', 'result_json') AND data_type = 'json') <> 3
OR (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_order'
AND column_name = 'process_status' AND character_maximum_length >= 24) <> 1 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:三期 JSON 字段或入库状态字段类型不兼容';
END IF;
IF (SELECT COUNT(DISTINCT CONCAT(table_name, '.', index_name))
FROM information_schema.statistics
WHERE table_schema = 'baiyue_firstleg'
AND index_name IN ('uk_preload_tenant_inbound', 'idx_preload_status',
'uk_split_operation', 'idx_split_operation_time',
'uk_loading_evidence_type', 'idx_loading_evidence_time',
'uk_loading_operation', 'idx_loading_operation_time',
'uk_loading_pick_tenant_inbound', 'idx_loading_pick_status')) <> 10 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:三期分柜、预装或装柜索引未完整创建';
END IF;
IF (SELECT COUNT(*) FROM information_schema.table_constraints
WHERE constraint_schema = 'baiyue_firstleg' AND constraint_type = 'FOREIGN KEY'
AND table_name IN ('fl_preload_plan', 'fl_split_operation',
'fl_loading_evidence', 'fl_loading_operation',
'fl_loading_pick_task')) <> 5 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:三期操作表入库单外键未完整创建';
END IF;
IF (SELECT COUNT(DISTINCT permission_code) FROM baiyue_iam.iam_permission
WHERE tenant_id = 0 AND app_code = 'PLATFORM' AND status = 'ACTIVE' AND deleted = 0
AND permission_code IN (
'firstleg:inbound:split', 'firstleg:inbound:preload',
'firstleg:inbound:preload-view',
'platform:firstleg:inbound:loading:edit',
'platform:firstleg:inbound:loading:view',
'platform:firstleg:inbound:loading:complete',
'platform:firstleg:inbound:delivery:complete',
'platform:firstleg:inbound:picking:edit',
'platform:firstleg:inbound:picking:view')) <> 9 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:三期分柜、预装或装柜权限未完整创建';
END IF;
IF (SELECT COUNT(DISTINCT rp.permission_id)
FROM baiyue_iam.iam_role_permission rp
JOIN baiyue_iam.iam_permission p ON p.id = rp.permission_id
WHERE rp.role_id = 4000001 AND rp.tenant_id = 0
AND p.permission_code IN (
'firstleg:inbound:split', 'firstleg:inbound:preload',
'firstleg:inbound:preload-view',
'platform:firstleg:inbound:loading:edit',
'platform:firstleg:inbound:loading:view',
'platform:firstleg:inbound:loading:complete',
'platform:firstleg:inbound:delivery:complete',
'platform:firstleg:inbound:picking:edit',
'platform:firstleg:inbound:picking:view')) <> 9 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:平台系统管理员未获得完整三期操作权限';
END IF;
IF (SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = 'baiyue_firstleg'
AND table_name IN ('fl_fee_item', 'fl_inbound_fee_detail')) <> 2 THEN
@@ -4693,4 +4936,4 @@ SELECT COUNT(*) AS unresolved_third_party_calls
WHERE tenant_id = 0;
SELECT 'PASS' AS migration_status,
'统一增量全部执行完成,平台公共资源归0、Warehouse父子回填、费用及可靠审计结构校验通过' AS message;
'统一数据库脚本执行完成,三期出仓、费用、公共资源及可靠审计结构校验通过' AS message;
@@ -22,15 +22,18 @@ public final class PalletRequests {
@NotNull @Min(1)
@Schema(description = "所属仓库ID", example = "190000000000000002", requiredMode = Schema.RequiredMode.REQUIRED)
Long warehouseId,
@NotBlank @Size(max = 50)
@Schema(description = "仓库内唯一托盘编码", example = "TP-001", requiredMode = Schema.RequiredMode.REQUIRED)
String palletCode,
@NotBlank @Size(max = 32)
@Schema(description = "托盘类型", example = "木托盘", requiredMode = Schema.RequiredMode.REQUIRED)
String palletType,
@NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 12, fraction = 6)
@Schema(description = "托盘容积,单位m³", example = "1.200000", requiredMode = Schema.RequiredMode.REQUIRED)
BigDecimal capacityCbm,
@NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 12, fraction = 3)
@Schema(description = "托盘重量,单位kg", example = "8.000", requiredMode = Schema.RequiredMode.REQUIRED)
BigDecimal weightKg,
@NotBlank @Size(max = 32)
@Schema(description = "托盘颜色", example = "黄色", requiredMode = Schema.RequiredMode.REQUIRED)
String color,
@Size(max = 500)
@Schema(description = "备注", example = "标准川字托盘")
String remark
@@ -64,6 +67,8 @@ public final class PalletRequests {
String palletCode,
String palletType,
BigDecimal capacityCbm,
BigDecimal weightKg,
String color,
String remark,
LocalDateTime createdAt,
LocalDateTime updatedAt
@@ -11,7 +11,7 @@ import lombok.Setter;
import java.math.BigDecimal;
/**
* 托盘主数据实体。状态同时承载可用、预占、占用和停用四种资源状态。
* 托盘主数据实体。状态仅供内部预占、占用流程使用,不属于平台维护字段。
*/
@Getter
@Setter
@@ -37,6 +37,12 @@ public class PalletEntity extends BaseEntity {
@Schema(description = "托盘容积,单位m³", example = "1.200000")
private BigDecimal capacityCbm;
@Schema(description = "托盘重量,单位kg", example = "8.000")
private BigDecimal weightKg;
@Schema(description = "托盘颜色", example = "黄色")
private String color;
@Schema(description = "资源状态", allowableValues = {"AVAILABLE", "RESERVED", "OCCUPIED", "DISABLED"})
private String status;
@@ -13,16 +13,21 @@ import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** 托盘持久化仓储;平台分页支持跨租户筛选,资源操作始终包含租户和仓库边界。 */
@Repository
public class PalletRepository {
private static final Pattern PALLET_CODE = Pattern.compile("TP-(\\d+)");
@Resource
private PalletMapper mapper;
public PageResult<PalletEntity> page(Long tenantId, PalletRequests.PageQuery query) {
LambdaQueryWrapper<PalletEntity> wrapper = new LambdaQueryWrapper<PalletEntity>()
.eq(tenantId != null, PalletEntity::getTenantId, tenantId)
.eq(PalletEntity::getDeleted, false)
.eq(query.warehouseId() != null, PalletEntity::getWarehouseId, query.warehouseId())
.like(StringUtils.hasText(query.palletCode()), PalletEntity::getPalletCode, query.palletCode())
.eq(StringUtils.hasText(query.palletType()), PalletEntity::getPalletType, query.palletType())
@@ -36,6 +41,7 @@ public class PalletRepository {
return mapper.selectOne(new LambdaQueryWrapper<PalletEntity>()
.eq(PalletEntity::getTenantId, tenantId)
.eq(PalletEntity::getWarehouseId, warehouseId)
.eq(PalletEntity::getDeleted, false)
.eq(PalletEntity::getId, id));
}
@@ -44,6 +50,7 @@ public class PalletRepository {
.eq(PalletEntity::getTenantId, tenantId)
.eq(PalletEntity::getWarehouseId, warehouseId)
.eq(PalletEntity::getPalletCode, code)
.eq(PalletEntity::getDeleted, false)
.ne(excludedId != null, PalletEntity::getId, excludedId)) > 0;
}
@@ -52,6 +59,7 @@ public class PalletRepository {
return mapper.selectList(new LambdaQueryWrapper<PalletEntity>()
.eq(PalletEntity::getTenantId, tenantId)
.eq(PalletEntity::getWarehouseId, warehouseId)
.eq(PalletEntity::getDeleted, false)
.eq(PalletEntity::getStatus, "AVAILABLE")
.eq(StringUtils.hasText(palletType), PalletEntity::getPalletType, palletType)
.eq(capacityCbm != null, PalletEntity::getCapacityCbm, capacityCbm)
@@ -59,6 +67,23 @@ public class PalletRepository {
.last("LIMIT " + limit));
}
public String nextCode(long tenantId, long warehouseId) {
int maxSequence = mapper.selectList(new LambdaQueryWrapper<PalletEntity>()
.select(PalletEntity::getPalletCode)
.eq(PalletEntity::getTenantId, tenantId)
.eq(PalletEntity::getWarehouseId, warehouseId)
.likeRight(PalletEntity::getPalletCode, "TP-")
.eq(PalletEntity::getDeleted, false))
.stream()
.map(PalletEntity::getPalletCode)
.map(PALLET_CODE::matcher)
.filter(Matcher::matches)
.mapToInt(matcher -> Integer.parseInt(matcher.group(1)))
.max()
.orElse(0);
return "TP-%03d".formatted(maxSequence + 1);
}
public void insert(PalletEntity entity) {
mapper.insert(entity);
}
@@ -35,10 +35,10 @@ public class PalletApplicationService {
@BusinessAudit(event = "PALLET_CREATE", resourceType = "PALLET", description = "新增托盘", captureBefore = false)
public Long create(long tenantId, PalletRequests.SaveRequest request) {
requireWarehouse(tenantId, request.warehouseId());
ensureCodeUnique(tenantId, request.warehouseId(), request.palletCode(), null);
PalletEntity entity = new PalletEntity();
entity.setTenantId(tenantId);
entity.setWarehouseId(request.warehouseId());
entity.setPalletCode(repository.nextCode(tenantId, request.warehouseId()));
copy(entity, request);
entity.setStatus("AVAILABLE");
try {
@@ -56,7 +56,6 @@ public class PalletApplicationService {
throw new BizException(ResultCode.FORBIDDEN, "请求仓库与托盘所属仓库不一致");
}
PalletEntity entity = owned(tenantId, warehouseId, id);
ensureCodeUnique(tenantId, warehouseId, request.palletCode(), id);
copy(entity, request);
try {
if (repository.save(entity) != 1) {
@@ -90,22 +89,18 @@ public class PalletApplicationService {
}
}
private void ensureCodeUnique(long tenantId, long warehouseId, String code, Long excludedId) {
if (repository.existsCode(tenantId, warehouseId, code, excludedId)) {
throw new BizException(ResultCode.CONFLICT, "托盘编码已存在");
}
}
private static void copy(PalletEntity entity, PalletRequests.SaveRequest request) {
entity.setPalletCode(request.palletCode());
entity.setPalletType(request.palletType());
entity.setCapacityCbm(request.capacityCbm());
entity.setWeightKg(request.weightKg());
entity.setColor(request.color());
entity.setRemark(request.remark());
}
private static PalletRequests.View view(PalletEntity entity) {
return new PalletRequests.View(entity.getId(), entity.getTenantId(), entity.getWarehouseId(),
entity.getPalletCode(), entity.getPalletType(), entity.getCapacityCbm(), entity.getRemark(),
entity.getPalletCode(), entity.getPalletType(), entity.getCapacityCbm(), entity.getWeightKg(),
entity.getColor(), entity.getRemark(),
entity.getCreatedAt(), entity.getUpdatedAt());
}
}
@@ -3,6 +3,18 @@
SET NAMES utf8mb4;
USE baiyue_warehouse;
-- 托盘展示字段增量:重量、颜色;平台接口不暴露内部状态。
SET @sql = IF((SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'wh_pallet' AND column_name = 'weight_kg') = 0,
'ALTER TABLE wh_pallet ADD COLUMN weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''托盘重量kg'' AFTER capacity_cbm',
'SELECT 1');
PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement;
SET @sql = IF((SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'wh_pallet' AND column_name = 'color') = 0,
'ALTER TABLE wh_pallet ADD COLUMN color VARCHAR(32) NOT NULL DEFAULT '''' COMMENT ''托盘颜色'' AFTER weight_kg',
'SELECT 1');
PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement;
-- 活动预占必须同时保证“一托一位”;A0 共享只放宽库位,不放宽托盘唯一性。
SET @schema_name = DATABASE();
DROP PROCEDURE IF EXISTS add_pallet_reservation_column_if_missing;
@@ -15,6 +15,8 @@ CREATE TABLE IF NOT EXISTS wh_pallet (
pallet_type VARCHAR(32) NOT NULL COMMENT '托盘类型',
pallet_name VARCHAR(100) NULL COMMENT '托盘名称',
capacity_cbm DECIMAL(18,6) NOT NULL COMMENT '托盘可用容积CBM',
weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '托盘重量kg',
color VARCHAR(32) NOT NULL DEFAULT '' COMMENT '托盘颜色',
status VARCHAR(20) NOT NULL DEFAULT 'AVAILABLE' COMMENT '内部状态:AVAILABLE、RESERVED或OCCUPIED',
remark VARCHAR(500) NULL COMMENT '备注',
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
@@ -11,6 +11,13 @@ INSERT INTO wh_service_capability(id,tenant_id,warehouse_id,capability_code,capa
VALUES(190000000000000021,0,190000000000000001,'RECEIVING','头程收货',1,'16:00:00','工作日16点前预约可当日收货')
ON DUPLICATE KEY UPDATE tenant_id=0,enabled=1,cutoff_time=VALUES(cutoff_time);
-- 二期托盘初始化数据。编码由平台维护接口自动生成,种子固定为 TP-001、TP-002。
INSERT INTO wh_pallet(id,tenant_id,warehouse_id,pallet_code,pallet_type,capacity_cbm,weight_kg,color,status,remark,created_by,updated_by)
VALUES
(190000000000000101,0,190000000000000001,'TP-001','木托盘',1.200000,8.000,'黄色','AVAILABLE','无备注',0,0),
(190000000000000102,0,190000000000000001,'TP-002','胶托盘',2.400000,12.000,'黑色','AVAILABLE','无备注',0,0)
ON DUPLICATE KEY UPDATE tenant_id=0,warehouse_id=VALUES(warehouse_id),pallet_type=VALUES(pallet_type),capacity_cbm=VALUES(capacity_cbm),weight_kg=VALUES(weight_kg),color=VALUES(color),remark=VALUES(remark);
-- 一期联调快递公司主数据,供平台维护和订单履约回显验证。
INSERT INTO wh_express_company(id,tenant_id,company_code,company_name,service_phone,website,tracking_url_template,status,remark)
VALUES(192000000000000001,0,'SF','顺丰速运','95338','https://www.sf-express.com','https://www.sf-express.com/chn/sc/dynamic_function/waybill/#search/bill-number/{trackingNo}','ACTIVE','平台公共快递公司样本')