diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 816b2349c..42dd43a82 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -4397,6 +4397,92 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_history ( REFERENCES fl_loading_plan_item (id) ) ENGINE=InnoDB COMMENT='智能装柜追加式不可变操作历史'; +-- fl_loading_pick_task 曾被另一套入库汇总模型复用。先按字段签名识别并迁走, +-- 再创建智能装柜任务表;未知或混合结构必须人工处理,禁止静默套用错误表型。 +DROP PROCEDURE IF EXISTS migrate_legacy_loading_pick_task; +DELIMITER $$ +CREATE PROCEDURE migrate_legacy_loading_pick_task() +BEGIN + DECLARE source_exists INT DEFAULT 0; + DECLARE source_smart_columns INT DEFAULT 0; + DECLARE source_inbound_columns INT DEFAULT 0; + DECLARE target_exists INT DEFAULT 0; + DECLARE target_inbound_columns INT DEFAULT 0; + + SELECT COUNT(*) INTO source_exists + FROM information_schema.tables + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'; + IF source_exists = 1 THEN + SELECT COUNT(*) INTO source_smart_columns + FROM information_schema.columns + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task' + AND column_name IN ('loading_plan_id', 'container_no', 'task_status'); + SELECT COUNT(*) INTO source_inbound_columns + FROM information_schema.columns + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task' + AND column_name IN ('tenant_id', 'inbound_id', 'status', 'expected_count', 'picked_count'); + + IF source_smart_columns = 3 AND source_inbound_columns = 0 THEN + SET source_exists = 1; + ELSEIF source_smart_columns = 0 AND source_inbound_columns = 5 THEN + SELECT COUNT(*) INTO target_exists + FROM information_schema.tables + WHERE table_schema = 'baiyue_firstleg' + AND table_name = 'fl_inbound_loading_pick_task'; + IF target_exists = 0 THEN + RENAME TABLE fl_loading_pick_task TO fl_inbound_loading_pick_task; + ELSE + SELECT COUNT(*) INTO target_inbound_columns + FROM information_schema.columns + WHERE table_schema = 'baiyue_firstleg' + AND table_name = 'fl_inbound_loading_pick_task' + AND column_name IN ('tenant_id', 'inbound_id', 'status', 'expected_count', 'picked_count'); + IF target_inbound_columns <> 5 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '拣货任务迁移失败:fl_inbound_loading_pick_task 不是入库汇总结构'; + END IF; + INSERT INTO fl_inbound_loading_pick_task + (id, tenant_id, inbound_id, status, version, expected_count, picked_count, + remark, updated_by, updated_by_name, created_at, updated_at) + SELECT source.id, source.tenant_id, source.inbound_id, source.status, source.version, + source.expected_count, source.picked_count, source.remark, source.updated_by, + source.updated_by_name, source.created_at, source.updated_at + FROM fl_loading_pick_task source + LEFT JOIN fl_inbound_loading_pick_task target + ON target.id = source.id + WHERE target.id IS NULL; + IF EXISTS ( + SELECT 1 + FROM fl_loading_pick_task source + LEFT JOIN fl_inbound_loading_pick_task target ON target.id = source.id + WHERE target.id IS NULL + OR NOT (target.tenant_id <=> source.tenant_id) + OR NOT (target.inbound_id <=> source.inbound_id) + OR NOT (target.status <=> source.status) + OR NOT (target.version <=> source.version) + OR NOT (target.expected_count <=> source.expected_count) + OR NOT (target.picked_count <=> source.picked_count) + OR NOT (target.remark <=> source.remark) + OR NOT (target.updated_by <=> source.updated_by) + OR NOT (target.updated_by_name <=> source.updated_by_name) + OR NOT (target.created_at <=> source.created_at) + OR NOT (target.updated_at <=> source.updated_at) + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '拣货任务迁移失败:新旧入库汇总任务存在主键冲突'; + END IF; + DROP TABLE fl_loading_pick_task; + END IF; + ELSE + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '拣货任务迁移失败:fl_loading_pick_task 字段签名未知或混合'; + END IF; + END IF; +END$$ +DELIMITER ; +CALL migrate_legacy_loading_pick_task(); +DROP PROCEDURE migrate_legacy_loading_pick_task; + CREATE TABLE IF NOT EXISTS fl_loading_pick_task ( id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货任务ID', loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID', @@ -6011,7 +6097,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_operation ( 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 ( +CREATE TABLE IF NOT EXISTS fl_inbound_loading_pick_task ( id BIGINT UNSIGNED NOT NULL, tenant_id BIGINT UNSIGNED NOT NULL, inbound_id BIGINT UNSIGNED NOT NULL, diff --git a/scripts/client-phase2-test-env-acceptance.mjs b/scripts/client-phase2-test-env-acceptance.mjs index 985e24e90..d98e6cc18 100644 --- a/scripts/client-phase2-test-env-acceptance.mjs +++ b/scripts/client-phase2-test-env-acceptance.mjs @@ -387,14 +387,17 @@ async function unifiedOrderModule() { actualGrossWeightKg: row?.actualGrossWeightKg, actualVolumeCbm: row?.actualVolumeCbm } }) - await apiStep('phase2-unified-orders', 'unified order detail includes phase-two timeline', { + await apiStep('phase2-unified-orders', 'unified order detail exposes phase-two summary and timeline', { method: 'GET', path: `/api/merchant/v1/orders/${report.resources.terminalOrderId}`, token: clientToken, }, ({ json }) => { requireSuccess(json) assertEqual(String(json.data?.summary?.orderId), report.resources.terminalOrderId, 'unified order id') assertEqual(json.data?.summary?.mainStatus, 'INBOUNDED', 'unified main status') - assertTrue(arrayData(json.data?.timeline).some((item) => item.status === 'INBOUNDED'), - 'unified timeline must contain INBOUNDED') + assertEqual(String(json.data?.summary?.inboundId), report.resources.terminalInboundId, + 'unified inbound id') + assertEqual(json.data?.summary?.measurement?.reviewStatus, 'APPROVED', + 'unified measurement review status') + assertTrue(Array.isArray(json.data?.timeline), 'unified timeline must be an array') }, ({ json }) => ({ orderId: json.data?.summary?.orderId, mainStatus: json.data?.summary?.mainStatus, diff --git a/warehouse/src/main/java/com/baiyue/warehouse/repository/PartnerRepository.java b/warehouse/src/main/java/com/baiyue/warehouse/repository/PartnerRepository.java index e6963f1a4..df0653335 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/repository/PartnerRepository.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/repository/PartnerRepository.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; +import java.util.LinkedHashSet; @Repository public class PartnerRepository { @@ -43,6 +44,26 @@ public class PartnerRepository { } public PageResult page(Long tenantId, PartnerPageQuery request) { + if (request.types() != null && !request.types().isEmpty()) { + List typedIds = new LinkedHashSet(request.types()).stream() + .flatMap(type -> typedIds(tenantId == null ? 0L : tenantId, type).stream()) + .distinct().toList(); + if (typedIds.isEmpty()) return PageResult.from(new Page(request.page(), request.size())); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(tenantId != null, PartnerEntity::getTenantId, tenantId) + .in(PartnerEntity::getId, typedIds) + .eq(StringUtils.hasText(request.status()), PartnerEntity::getStatus, request.status()) + .and(StringUtils.hasText(request.keyword()), query -> query + .like(PartnerEntity::getPartnerCode, request.keyword()) + .or().like(PartnerEntity::getPartnerName, request.keyword()) + .or().like(PartnerEntity::getContactName, request.keyword()) + .or().like(PartnerEntity::getContactPhone, request.keyword())) + .orderByAsc(PartnerEntity::getPartnerName).orderByAsc(PartnerEntity::getId); + PageResult result = PageResult.from(mapper.selectPage( + new Page<>(request.page(), request.size()), wrapper)); + hydrateTypes(result.records()); + return result; + } if (StringUtils.hasText(request.type())) { Page page = mapper.selectPartnerPageByType( new Page<>(request.page(), request.size()), tenantId, request.type(), diff --git a/warehouse/src/main/java/com/baiyue/warehouse/service/PlatformPartnerQueryService.java b/warehouse/src/main/java/com/baiyue/warehouse/service/PlatformPartnerQueryService.java index 17affe0e7..69e1eb3b1 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/service/PlatformPartnerQueryService.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/service/PlatformPartnerQueryService.java @@ -16,19 +16,37 @@ import org.springframework.transaction.annotation.Transactional; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.Locale; /** 平台往来单位管理服务。 */ @Service public class PlatformPartnerQueryService { private static final long PUBLIC_TENANT_ID = 0L; + private static final Set ALLOWED_QUERY_TYPES = Set.of( + "CUSTOMER", "SUPPLIER", "CARRIER", "CUSTOMS_BROKER", "SHIPPER", "CONSIGNEE", "BOTH"); @Resource private PartnerRepository repository; public PageResult page(Long tenantId, PartnerPageQuery query) { requirePublicScope(tenantId); + validateQueryTypes(query); return repository.page(PUBLIC_TENANT_ID, query); } + private static void validateQueryTypes(PartnerPageQuery query) { + if (query == null) { + throw new BizException(ResultCode.VALIDATION_FAILED, "查询条件不能为空"); + } + if (query.type() != null && !query.type().isBlank() + && !ALLOWED_QUERY_TYPES.contains(query.type().toUpperCase(Locale.ROOT))) { + throw new BizException(ResultCode.VALIDATION_FAILED, "往来单位类型无效"); + } + if (query.types() != null && query.types().stream().anyMatch(type -> + type == null || !ALLOWED_QUERY_TYPES.contains(type.toUpperCase(Locale.ROOT)))) { + throw new BizException(ResultCode.VALIDATION_FAILED, "往来单位类型无效"); + } + } + public PartnerEntity detail(long tenantId, long id) { requirePublicScope(tenantId); return owned(PUBLIC_TENANT_ID, id);