diff --git a/AGENTS.md b/AGENTS.md index 6e0afca0b..e5aaf68af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,9 +25,11 @@ | 订单、货物、快照、状态、幂等、Outbox | `order/` | `order/AGENTS.md`;统一列表由 `UnifiedOrderQueryService` 组合交易与履约投影,仍固定显示 `PENDING_REVIEW` 并提供审核动作:Order 为 `SUBMITTED` 时必须优先返回 `PENDING_REVIEW/APPROVE_ORDER`,不得被 Firstleg 准备中状态覆盖 | | 客户头程订单中文批量导入 | `order/` | `MerchantBatchImportService.java`、`MerchantWarehouseCatalogClient.java`、`MerchantPricingCatalogClient.java`、`order-v1.yaml`;模板和导入都按当前客户读取动态地址、全部有效仓库与渠道,最多 50 行,只创建草稿,支持 `PARTIAL/ATOMIC` | | 平台修改客户原始申报、履约渠道变更与跨服务同步 | `order/`、`firstleg/` | Order `OrderDeclarationUpdateService.java`、`OrderInboundOutboxPublisher.java`、`order-v1.yaml`;Firstleg `OrderDeclarationSyncService.java`、`OrderDeclarationSyncMapper.java`、`firstleg-v1.yaml`。只更新申报快照;渠道变化先失败关闭校验履约可逆性与重新计价,成功后由版本化 `ORDER_DECLARATION_UPDATED_V1` 幂等同步,不覆盖实测、箱码或已确认费用 | +| 平台订单批量审核预览 | `order/` | `PlatformFirstLegOrderController.java` 的 `/batch-review-preview`、`BatchOrderReviewPreviewService.java`、`BatchOrderReviewPreviewResponse.java`、`order-v1.yaml`;一次请求固定用订单与货物两次批量查询返回同序快照,不允许前端逐票请求详情,逐项资格以 `canApprove/blockReason` 为准 | | 履约分配、入库、测量、箱规算法、审核、FBA 计费 | `firstleg/` | `firstleg/AGENTS.md`;客户四档箱规与 `ADD/MULTIPLY` 默认算法从 `CustomerDimensionConfigService` 进入,单箱测量和最终计价分别由 `CustomerDimensionWeightingService`、`FbaPricingService` 保存并消费服务端快照 | | 入库扣货、解除、履约门禁与凭证 | `firstleg/`、`file/` | Firstleg `InboundHoldService.java`、`InboundHoldGuard.java`、`InboundHoldEvidenceService.java`;File 业务类型 `INBOUND_HOLD_EVIDENCE`。扣货阻断物理履约写入,已发生实物作业后禁止手工切换,命令按入库版本和幂等键执行 | | 称重确认、整票库位块与 PDA 资源结算 | `firstleg/`、`warehouse/` | Firstleg `MeasurementLocationOccupationService.java`、`InboundMeasurementService.java`、`MeasurementSubmissionService.java`、`WarehousePalletLocationClient.java`;Warehouse `LocationBlockService.java` 与 `location-block-v1.yaml`。称重确认尽力建块、提交时再次补齐,库位指引失败不阻断测量主流程;部分拣货整段转移给余量子单,整票拣完才整块释放,A0 共享资源按 `reservationId` 区分 | +| 客户专属价格簿、柜级阶梯运费与受控调价 | `firstleg/` | `PlatformPriceBookService.java`、`ActivePriceBookEstimator.java`、`FbaPricingService.java`、`pricing/ContainerTierAmountAllocator.java`、`InboundFeeDetailService.java`、`fba-pricing-v1.yaml`。`customerId=0` 为公共报价,正数为客户专属版本;计价优先专属 ACTIVE、缺失才回退公共 ACTIVE。同柜区域重量阶梯按实际成员计费量聚合命中并守恒分摊,装柜变化只重算未锁定客户费用;人工计费量调整限基准上下 10%,私人仓尾程派送费只绑定 `PRIVATE` 入库单;一件代发 `WAIT_PALLETIZE` 允许在预装确认前试算,写费用时必须保留托盘化状态 | | Firstleg 内部模块化与依赖基线 | `firstleg/` | `firstleg/docs/firstleg-internal-modularization-baseline.md`、`firstleg/scripts/analyze-firstleg-dependencies.mjs`;价格簿查询和客户统计按端口/适配器切片,不改变服务与数据库边界 | | Mapper 新增、移动、Bean 缺失与扫描遗漏 | 目标持久化服务 | 所有 Mapper 显式 `@Mapper`,启动类按服务根包注解过滤扫描;先运行 `node scripts/mapper-scan-contract.mjs`,再按目标模块执行 `test-compile/test`,不得等部署启动发现 | | 脚本管理、全量验证、测试造数与诊断 | 仓库根 `scripts/`;服务专属脚本留在 `/scripts/` | 先读 `scripts/AGENTS.md` 和 `scripts/README.md`;Admin HTTP 内部模块继续读 `scripts/admin-test-env-modules/README.md`。新增入口必须登记用途、目标环境、数据副作用和报告位置,并通过 `scripts/script-documentation-contract.test.mjs` | @@ -52,11 +54,11 @@ | 海外 API 路由、海外 Token、Audience/JWKS | `gateway-overseas/` | `gateway-overseas/AGENTS.md` | | 海外 IAM 状态与权限版本投影 | `lastleg-service/security/` | `lastleg-service/AGENTS.md`;内部入口 `/internal/lastleg/security/v1/events`、`/validate`;消费三类 IAM 安全事件并为海外 Gateway 提供 fail-closed 校验,投影表归 `baiyue_lastleg` | | 尾程订单、海外入仓计划、换标、库存、采购、本土卡车 | `lastleg-service/` | `lastleg-service/AGENTS.md`,尾程订单控制从 `LastlegOrderController.java`、`LastlegOrderCommandService.java` 进入,再按 `inboundplan/`、`relabel/`、`inventory/`、`procurement/`、`trucking/` 定位 | -| 尾程订单控制与柜级入仓计划转换 | `lastleg-service/` | `orders/container-summaries` 按 `sourceContainerBookingId` 跨客户聚合,分页前排除全部入仓码均已生成有效独立计划的柜,详情返回全柜数据;`inbound-plans/convert-container` 锁定同柜订单,每票生成独立计划,整批原子且幂等。单票完整入库计划进入 `InboundPlanService.java`,暂扣/解除和固定八行人工节点进入 `LastlegOrderCommandService.java`;命令按订单版本、客户范围和幂等键执行,暂扣中的订单不得进入排车或出库 | +| 尾程订单控制、柜级预览与入仓计划转换 | `lastleg-service/`、`warehouse-overseas-service/` | `orders/container-summaries` 按 `sourceContainerBookingId` 跨客户聚合并分页返回全部稳定来源柜,已完成计划转换的柜仍用于接柜仓查询与修改,生成资格由 `allPlansReady/pendingPlanCount` 表达,详情返回全柜数据。`inbound-plans/container-preview` 由 `InboundPlanService.java` 返回服务端分组、托数、阻断项及快照版本/哈希;`ContainerInboundPlanConversionCoordinator.java` 校验同一快照并以唯一 Saga 先向 `WarehousePalletBatchService.java` 批量预留真实托盘,再本地原子生成剩余独立计划并确认资源,失败只补偿未确认资源,同柜重放不得新建第二批。暂扣/解除和固定八行人工节点进入 `LastlegOrderCommandService.java`;命令按订单版本、客户范围和幂等键执行,暂扣中的订单不得进入排车或出库 | | 海外入仓分页、FBA 预约、PDA 柜号签收与入仓 | `lastleg-service/` | 平台入口 `inboundplan/controller/InboundPlanController.java`、`InboundPlanQueryMapper.java`、`InboundFbaAppointmentService.java`;PDA 公网入口 `inboundplan/pda/controller/ReceivingContainerController.java`、`ReceivingContainerService.java`,旧 planNo/allocationId Controller 已移除 | | 海外仓排车查询、候选与智能预排 | `lastleg-service/` | `dispatch/controller/DispatchQueryController.java`、`SmartDispatchController.java`;草稿柜、托盘锁和预排归 `dispatch/`,只读排车和预排创建独立开关,不自动开放编辑、取消、发车、PDA 出库或派送 | | 海外排车执行、PDA 出库、派送与一件代发 | `lastleg-service/` | `dispatch/controller/DispatchController.java`、`dispatch/pda/controller/DispatchPdaController.java`、`delivery/controller/DeliveryTransferController.java`、`OutboundPdaController.java`、`MerchantDeliveryController.java`,一件代发库存归 `dropship/`;统一经 `gateway-overseas/`,设备端正式入口继续查 `../apps/pda-overseas-uniapp/AGENTS.md` | -| 海外异常退回与客户回访 | `lastleg-service/` | `exceptionorder/`、`customervisit/`;平台异常建档和关系解析、派送退回入仓、派送完成/状态、回访资料与附件均以 Lastleg 自身客户范围和状态为准 | +| 海外异常退回、库存恢复与客户回访 | `lastleg-service/`、`file/` | `exceptionorder/`、`inventory/command/`、`customervisit/`;异常按版本和幂等键执行 `PENDING → PROCESSING → COMPLETED`,完成证据固定归 `OVERSEAS_EXCEPTION_EVIDENCE`。派送退回明细只能来自原出库拣货事实,退回海外仓时校验真实目标托盘/库位并以 `RETURN_STOCK_IN` 恢复库存后,原子完成异常单与派送 `RETURNED`;回访仍以 Lastleg 客户范围和派送状态为准 | | 一件代发 SKU/箱唛与暂扣跨区协同 | `order/`、`firstleg/`、`common-event-contract/`、`lastleg-service/` | Order `InternalOrderCargoController` 提供强制 SKU;Firstleg `InboundCartonLabelService` 冻结 SKU、`LoadingWorkPalletService` 维护装柜工作托盘并消费 `OverseasInboundOrderDetainedChangedV1`;Lastleg `dropship/` 建立库存并发布暂扣变更 | | 客户海外仓贴标、转运、废弃与退货换标 | `lastleg-service/` | `relabel/`、`transfer/`、`discard/` 的 Merchant Controller/Service;Client 消费端 `../apps/client-web/src/modules/overseas-warehouse/`,统一经 `gateway-overseas/` | | 海外仓 PDA 盘点、移库、托盘、换标与查询 | `lastleg-service/`、`warehouse-overseas-service/` | Lastleg `pda/stocktake/`、`pda/move/`、`pda/warehouse/`、`palletops/`、`relabel/pda/`;契约 `lastleg-service/src/main/resources/openapi/overseas-pda-operations-v1.yaml`;物理托盘流转与补偿进入 Warehouse `InternalPdaPalletTransitionController`,设备端入口是否开放以 `../apps/pda-overseas-uniapp/AGENTS.md` 为准 | diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index f3281f667..d4dd91b6b 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -5065,6 +5065,10 @@ CREATE TABLE IF NOT EXISTS fl_container_booking ( actual_port_arrival_at DATETIME(3) NULL COMMENT '实际到港时间', estimated_warehouse_arrival_at DATETIME(3) NULL COMMENT '预计到仓时间', actual_warehouse_arrival_at DATETIME(3) NULL COMMENT '实际到仓时间', + pickup_code VARCHAR(64) NULL COMMENT 'PU数字码;按字符串保存以保留前导零', + pickup_code_updated_by BIGINT UNSIGNED NULL COMMENT 'PU码最后填写人ID', + pickup_code_updated_by_name VARCHAR(64) NULL COMMENT 'PU码最后填写人名称快照', + pickup_code_updated_at DATETIME(3) NULL COMMENT 'PU码最后填写时间', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', @@ -5084,6 +5088,7 @@ CREATE TABLE IF NOT EXISTS fl_container_booking ( CONSTRAINT chk_container_booking_record_status CHECK (record_status IN ('ACTIVE','MERGED')), CONSTRAINT chk_container_booking_capacity CHECK (capacity_cbm IS NULL OR capacity_cbm > 0), CONSTRAINT chk_container_booking_weight CHECK (max_weight_kg IS NULL OR max_weight_kg > 0), + CONSTRAINT chk_container_booking_pickup_code CHECK (pickup_code IS NULL OR pickup_code REGEXP '^[0-9]{1,64}$'), CONSTRAINT chk_container_booking_iso6346 CHECK (container_no IS NULL OR container_no REGEXP '^[A-Z]{3}[UJZ][0-9]{7}$') ) ENGINE=InnoDB COMMENT='平台货柜预订资料;预装状态唯一来源为装柜计划'; @@ -5277,7 +5282,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item ( CREATE TABLE IF NOT EXISTS fl_container_document ( id BIGINT UNSIGNED NOT NULL COMMENT '货柜文件关联ID(雪花ID)', container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID', - document_type VARCHAR(32) NOT NULL COMMENT '文件类型:DESTINATION_CLEARANCE目的港清关、DELIVERY_ORDER提货单、PICKUP_ORDER取货单', + document_type VARCHAR(32) NOT NULL COMMENT '文件类型:DESTINATION_CLEARANCE目的港清关、DELIVERY_ORDER提货单', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件ID,不建立跨库外键', file_name VARCHAR(255) NOT NULL COMMENT '文件名称快照', file_hash VARCHAR(64) NULL COMMENT 'File服务SHA-256快照', @@ -5297,8 +5302,8 @@ CREATE TABLE IF NOT EXISTS fl_container_document ( CONSTRAINT fk_container_document_booking FOREIGN KEY (container_booking_id) REFERENCES fl_container_booking (id), CONSTRAINT chk_container_document_type - CHECK (document_type IN ('DESTINATION_CLEARANCE', 'DELIVERY_ORDER', 'PICKUP_ORDER')) -) ENGINE=InnoDB COMMENT='平台货柜清关、DO与PU文件关联'; + CHECK (document_type IN ('DESTINATION_CLEARANCE', 'DELIVERY_ORDER')) +) ENGINE=InnoDB COMMENT='平台货柜清关与DO文件关联'; CREATE TABLE IF NOT EXISTS fl_loading_plan_history ( id BIGINT UNSIGNED NOT NULL COMMENT '装柜操作历史ID(雪花ID)', @@ -6120,7 +6125,7 @@ BEGIN SELECT MAX(plan_status) INTO current_plan_status FROM fl_loading_plan WHERE container_booking_id = NEW.container_booking_id; IF current_plan_status = 'COMPLETED' - AND NEW.document_type NOT IN ('DESTINATION_CLEARANCE', 'DELIVERY_ORDER', 'PICKUP_ORDER') THEN + AND NEW.document_type NOT IN ('DESTINATION_CLEARANCE', 'DELIVERY_ORDER') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '装柜完成后货柜文件不可新增'; END IF; END$$ @@ -6136,7 +6141,7 @@ BEGIN SELECT MAX(plan_status) INTO current_plan_status FROM fl_loading_plan WHERE container_booking_id = OLD.container_booking_id; IF current_plan_status = 'COMPLETED' - AND OLD.document_type NOT IN ('DESTINATION_CLEARANCE', 'DELIVERY_ORDER', 'PICKUP_ORDER') THEN + AND OLD.document_type NOT IN ('DESTINATION_CLEARANCE', 'DELIVERY_ORDER') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '装柜完成后货柜文件不可修改'; END IF; END$$ @@ -12631,7 +12636,7 @@ CALL queue_table_comment('baiyue_firstleg', 'fl_carrier_schedule_node_fact', ' CALL queue_table_comment('baiyue_firstleg', 'fl_carrier_schedule_exception_fact', '船节点异常发生与解除不可变事实'); CALL queue_table_comment('baiyue_firstleg', 'fl_consumed_event', '头程消息消费幂等表:记录已处理事件并按事件ID去重,防止RocketMQ重复投递造成重复建单;不是消息发布表'); CALL queue_table_comment('baiyue_firstleg', 'fl_container_booking', '平台货柜预订资料;预装状态唯一来源为装柜计划'); -CALL queue_table_comment('baiyue_firstleg', 'fl_container_document', '平台货柜清关、DO与PU文件关联'); +CALL queue_table_comment('baiyue_firstleg', 'fl_container_document', '平台货柜清关与DO文件关联'); CALL queue_table_comment('baiyue_firstleg', 'fl_container_fee_detail', '柜次级人工应收应付费用来源;审核通过且参与结算后同步Finance'); CALL queue_table_comment('baiyue_firstleg', 'fl_customer_charge_rule', '客户头程计费规则版本'); CALL queue_table_comment('baiyue_firstleg', 'fl_customer_dimension_config', '客户固定四箱规尺寸配置'); @@ -13351,7 +13356,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'warehouse_ CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'warehouse_name', '装柜仓库名称快照'); CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'container_booking_id', '货柜预订ID'); CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'created_at', '创建时间'); -CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'document_type', '文件类型:DESTINATION_CLEARANCE目的港清关、DELIVERY_ORDER提货单、PICKUP_ORDER取货单'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'document_type', '文件类型:DESTINATION_CLEARANCE目的港清关、DELIVERY_ORDER提货单'); CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'file_id', 'File服务文件ID,不建立跨库外键'); CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'file_name', '文件名称快照'); CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'file_url', '文件访问地址快照'); @@ -16203,7 +16208,7 @@ BEGIN END IF; END$$ DELIMITER ; --- 货柜目的港清关与DO/PU采用轻量文件事实;出口报关由customs-v1整柜聚合唯一承载。 +-- 货柜目的港清关与DO采用轻量文件事实;PU为货柜主表数字码;出口报关由customs-v1整柜聚合唯一承载。 USE baiyue_firstleg; DROP PROCEDURE IF EXISTS phase3_simple_customs_add_column; DELIMITER $$ @@ -16223,6 +16228,32 @@ CALL phase3_simple_customs_add_column('completed_by_name', "VARCHAR(64) NULL COM CALL phase3_simple_customs_add_column('completed_at', "DATETIME(3) NULL COMMENT '一键完成时间' AFTER completed_by_name"); DROP PROCEDURE phase3_simple_customs_add_column; +DROP PROCEDURE IF EXISTS phase3_pickup_code_add_column; +DELIMITER $$ +CREATE PROCEDURE phase3_pickup_code_add_column(IN p_name VARCHAR(64), IN p_definition TEXT) +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema='baiyue_firstleg' AND table_name='fl_container_booking' + AND column_name=p_name) THEN + SET @ddl=CONCAT('ALTER TABLE fl_container_booking ADD COLUMN ',p_name,' ',p_definition); + PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement; + END IF; +END$$ +DELIMITER ; +CALL phase3_pickup_code_add_column('pickup_code', "VARCHAR(64) NULL COMMENT 'PU数字码;按字符串保存以保留前导零' AFTER actual_warehouse_arrival_at"); +CALL phase3_pickup_code_add_column('pickup_code_updated_by', "BIGINT UNSIGNED NULL COMMENT 'PU码最后填写人ID' AFTER pickup_code"); +CALL phase3_pickup_code_add_column('pickup_code_updated_by_name', "VARCHAR(64) NULL COMMENT 'PU码最后填写人名称快照' AFTER pickup_code_updated_by"); +CALL phase3_pickup_code_add_column('pickup_code_updated_at', "DATETIME(3) NULL COMMENT 'PU码最后填写时间' AFTER pickup_code_updated_by_name"); +DROP PROCEDURE phase3_pickup_code_add_column; + +SET @has_pickup_code_check=(SELECT COUNT(*) FROM information_schema.table_constraints + WHERE table_schema='baiyue_firstleg' AND table_name='fl_container_booking' + AND constraint_name='chk_container_booking_pickup_code' AND constraint_type='CHECK'); +SET @ddl=IF(@has_pickup_code_check=0, + "ALTER TABLE fl_container_booking ADD CONSTRAINT chk_container_booking_pickup_code CHECK (pickup_code IS NULL OR pickup_code REGEXP '^[0-9]{1,64}$')", + 'SELECT 1'); +PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement; + SET @has_simple_customs_check = (SELECT COUNT(*) FROM information_schema.table_constraints WHERE table_schema='baiyue_firstleg' AND table_name='fl_container_document' AND constraint_name='chk_container_document_type' AND constraint_type='CHECK'); @@ -16230,7 +16261,7 @@ SET @ddl = IF(@has_simple_customs_check=0, 'SELECT 1', 'ALTER TABLE fl_container_document DROP CHECK chk_container_document_type'); PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement; ALTER TABLE fl_container_document ADD CONSTRAINT chk_container_document_type - CHECK (document_type IN ('DESTINATION_CLEARANCE','DELIVERY_ORDER','PICKUP_ORDER')); + CHECK (document_type IN ('DESTINATION_CLEARANCE','DELIVERY_ORDER')); DROP TRIGGER IF EXISTS trg_container_document_before_insert; DELIMITER $$ @@ -16238,7 +16269,7 @@ CREATE TRIGGER trg_container_document_before_insert BEFORE INSERT ON fl_containe BEGIN DECLARE current_plan_status VARCHAR(24); SELECT MAX(plan_status) INTO current_plan_status FROM fl_loading_plan WHERE container_booking_id=NEW.container_booking_id; - IF current_plan_status='COMPLETED' AND NEW.document_type NOT IN ('DESTINATION_CLEARANCE','DELIVERY_ORDER','PICKUP_ORDER') THEN + IF current_plan_status='COMPLETED' AND NEW.document_type NOT IN ('DESTINATION_CLEARANCE','DELIVERY_ORDER') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='装柜完成后货柜物流文件不可新增'; END IF; END$$ @@ -16249,7 +16280,7 @@ CREATE TRIGGER trg_container_document_before_update BEFORE UPDATE ON fl_containe BEGIN DECLARE current_plan_status VARCHAR(24); SELECT MAX(plan_status) INTO current_plan_status FROM fl_loading_plan WHERE container_booking_id=OLD.container_booking_id; - IF current_plan_status='COMPLETED' AND OLD.document_type NOT IN ('DESTINATION_CLEARANCE','DELIVERY_ORDER','PICKUP_ORDER') THEN + IF current_plan_status='COMPLETED' AND OLD.document_type NOT IN ('DESTINATION_CLEARANCE','DELIVERY_ORDER') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='装柜完成后货柜物流文件不可修改'; END IF; END$$ diff --git a/firstleg/AGENTS.md b/firstleg/AGENTS.md index 28c98fb0e..ee9eb076b 100644 --- a/firstleg/AGENTS.md +++ b/firstleg/AGENTS.md @@ -24,7 +24,8 @@ | 审核与初始计价 | `controller/PlatformInboundMeasurementReviewController.java`、`service/MeasurementReviewService.java`、`service/InitialInboundPricingService.java`、`dto/MeasurementApprovalResponse.java` | | 客户四档箱规与默认算法 | `controller/PlatformCustomerDimensionConfigController.java`、`service/CustomerDimensionConfigService.java`、`repository/CustomerDimensionConfigRepository.java`;固定 `SMALL/MEDIUM/LARGE/EXTRA_LARGE`,默认算法只允许 `ADD/MULTIPLY` | | FBA 主数据/计费与客户规则 | `controller/PlatformFbaMasterDataController.java`、`PlatformFbaPricingController.java`、`service/FbaPricingService.java`、`repository/FbaPricingRepository.java`;客户规则可保存多个版本,但每个客户仅一条默认规则,计算请求可提交已确认的重量/体积选择值 | -| 入库费用明细 | `controller/PlatformInboundFeeDetailController.java`、`service/InboundFeeDetailService.java`、`dto/InboundFeeDetailDtos.java`;Admin 消费端见 `../../apps/admin-web/src/modules/unified-orders/api/inboundFeeDetails.ts` | +| 客户专属价格簿、柜级阶梯运费与调价边界 | `controller/PlatformPriceBookController.java`、`service/PlatformPriceBookService.java`、`pricing/ActivePriceBookEstimator.java`、`service/FbaPricingService.java`、`pricing/ContainerTierAmountAllocator.java`;公共价格簿固定 `customerId=0`,客户专属版本按客户独立发布与替换,计价优先专属 ACTIVE、缺失才回退公共 ACTIVE。`REGION_WEIGHT_TIER` 对同柜同客户有效成员聚合实际计费量命中一个阶梯,再按成员计费量守恒分摊;装柜成员变化调用 `recalculateUnlockedContainerCustomerFees`,已确认/已发送或客户已确认的版本不得改写。人工调整以系统基准量为中心限制上下 10%(含边界),保存版本、原因和二次确认;一件代发 `WAIT_PALLETIZE` 可在预装确认前试算,`markInboundCalculated` 只更新费用并保留该托盘化状态 | +| 入库费用明细 | `controller/PlatformInboundFeeDetailController.java`、`service/InboundFeeDetailService.java`、`dto/InboundFeeDetailDtos.java`;`PRIVATE_LAST_MILE_DELIVERY` 是独立私人仓尾程派送费,只允许 `destinationType=PRIVATE`,按稳定费用代码校验而不按中文名判断;Admin 消费端见 `../../apps/admin-web/src/modules/unified-orders/api/inboundFeeDetails.ts` | | 入库/柜次直接已付快照与凭证校验 | `service/FeePaymentSnapshotValidator.java`、`client/FeePaymentVoucherFileClient.java`;写入入口分别为 `InboundFeeDetailService.java`、`ContainerFeeDetailService.java`,File 业务类型固定 `FINANCE_VOUCHER` | | 客户运营统计/在途柜量 | `customerstatistics/controller/PlatformCustomerTransitStatisticsController.java`、`customerstatistics/service/CustomerTransitStatisticsService.java`;契约见 `src/main/resources/openapi/customer-statistics-v1.yaml` | | 费用项目与价格簿 | `controller/PlatformFeeItemController.java`、`PlatformPriceBookController.java`、`MerchantPriceBookController.java` | diff --git a/lastleg-service/AGENTS.md b/lastleg-service/AGENTS.md index fc7ed7694..79b94491d 100644 --- a/lastleg-service/AGENTS.md +++ b/lastleg-service/AGENTS.md @@ -12,7 +12,7 @@ | 功能 | 入口 | |---|---| | 内部模块化基线与依赖分析 | `docs/lastleg-internal-modularization-baseline.md`、`scripts/analyze-lastleg-dependencies.mjs`、`scripts/analyze-lastleg-dependencies.test.mjs`、`src/test/resources/architecture/lastleg-slice-dependencies.json`;只约束本服务内部包依赖,不改变部署单元或数据库边界 | -| 事件接收、平台订单列表/详情/导出、柜级进度、目的仓调整 | `src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java`;`/api/platform/v1/last-leg/orders/container-summaries` 按 `sourceContainerBookingId` 服务端聚合全柜订单、入仓码和有效计划,分页前排除全部入仓码均已生成有效独立计划的柜,`/{sourceContainerBookingId}` 返回柜内全部订单/入仓码/计划状态且不受订单分页截断;`plannedArrivalAt` 只取装柜完成事件 `estimatedWarehouseArrivalAt` 冻结到 `ll_order.estimated_warehouse_at` 的正式事实,按其升序、null 最后、稳定柜 ID 排序;目的仓调整仅接受国内 Warehouse Service 平台公共 `ACTIVE + FBA` 主档,写入前由 `service/FbaWarehouseValidator.java` 经内部只读目录校验 ID/编码/名称一致 | +| 事件接收、平台订单列表/详情/导出、柜级进度、目的仓调整 | `src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java`;`/api/platform/v1/last-leg/orders/container-summaries` 按 `sourceContainerBookingId` 服务端聚合全柜订单、入仓码和有效计划并分页返回全部稳定来源柜,已完成计划转换的柜仍保留用于接柜仓查询与修改,生成资格只由 `allPlansReady/pendingPlanCount` 表达;`/{sourceContainerBookingId}` 返回柜内全部订单/入仓码/计划状态且不受订单分页截断;`plannedArrivalAt` 只取装柜完成事件 `estimatedWarehouseArrivalAt` 冻结到 `ll_order.estimated_warehouse_at` 的正式事实,按其升序、null 最后、稳定柜 ID 排序;目的仓调整仅接受国内 Warehouse Service 平台公共 `ACTIVE + FBA` 主档,写入前由 `service/FbaWarehouseValidator.java` 经内部只读目录校验 ID/编码/名称一致 | | 尾程订单完整计划、暂扣与人工节点 | `controller/LastlegOrderController.java`、`service/LastlegOrderCommandService.java`、`inboundplan/service/InboundPlanService.java`、`dto/LastlegOrderCommandDtos.java`;单票计划写入校验订单版本、客户范围、订单冻结入仓码、稳定接柜仓 ID、当前仓库主档与幂等键;接柜仓编码/名称以该 ID 当前目录快照为准,不要求订单残留旧展示快照。暂扣/解除写不可变历史并发布 `InboundOrderDetainedOutboxService`,已拣货、已发运或已有出库单时禁止新增暂扣;暂扣期间不得进入排车或出库。人工节点必须一次提交固定八行完整快照且至少一个时间,首次保存后永久使用 `MANUAL` 来源,不得回退柜级节点或反向修改头程船节点 | | 客户尾程订单列表与详情 | `controller/MerchantLastlegOrderController.java`;客户范围只取认证身份,稳定类型/状态见 `model/LastlegOrderType.java`、`LastlegOrderStatus.java` | | 装柜完成事件消费 | `service/LoadingCompletedConsumer.java`;当前生产信封为 `FIRST_LEG_LOADING_PLAN_COMPLETED` V2,消费端冻结 `destinationType`:FBA 可按箱数且不生成逐箱作业账,OVERSEAS 必须逐箱校验并生成箱级事实;历史缺字段事件仅按明确 FBA 单号或目的仓与接柜仓一致性安全推断,歧义直接拒绝 | @@ -23,10 +23,10 @@ | 独立尾程入仓单快照 | `entity/LastlegInboundOrder.java`、`mapper/LastlegInboundOrderMapper.java`;按客户与来源入仓标识冻结 inboundCode、来源/根订单及目的仓,不以尾程订单行代替入仓单聚合 | | 内部令牌保护 | `security/InternalServiceTokenGuard.java` | | IAM 海外安全投影与实时校验 | 内部入口 `security/controller/LastlegSecurityController.java`,固定 `/internal/lastleg/security/v1/events`、`/validate` 且复用 `InternalServiceTokenGuard`;投影/校验 `security/service/`,DTO `security/dto/`,表固定 `ll_security_event_inbox`、`ll_security_projection`;MySQL 投影缺失、落后或不可用一律 fail-closed,Redis 仅为提交后可重建镜像;业务切片不得直接依赖投影内部类,边界由 `SecurityProjectionArchitectureTest` 与 Node 合同锁定 | -| 入仓计划分页、转换与详情 | 平台 `inboundplan/controller/InboundPlanController.java`,客户只读 `inboundplan/controller/MerchantInboundPlanController.java`;共用 `inboundplan/dto/InboundPlanRequests.java`、`InboundPlanListResponse.java`、`inboundplan/mapper/InboundPlanQueryMapper.java`、`inboundplan/service/InboundPlanService.java`;客户入口固定 `/api/merchant/v1/last-leg/inbound-plans`,范围只取 Token;整柜转换固定为 `/convert-container`,锁定同一 `sourceContainerBookingId` 后每票独立生成计划,整批原子且幂等 | +| 入仓计划分页、柜级预览、转换与详情 | 平台 `inboundplan/controller/InboundPlanController.java`,客户只读 `inboundplan/controller/MerchantInboundPlanController.java`;共用 `inboundplan/dto/InboundPlanRequests.java`、`InboundPlanListResponse.java`、`ContainerInboundPlanPreviewResponse.java`、`inboundplan/mapper/InboundPlanQueryMapper.java`、`inboundplan/service/InboundPlanService.java`;客户入口固定 `/api/merchant/v1/last-leg/inbound-plans`,范围只取 Token。`/container-preview` 按全柜返回 FBA/一件代发/私人地址分组、托数、就绪、阻断项及 `snapshotVersion/snapshotHash`;`ContainerInboundPlanConversionCoordinator.java` 用唯一柜级 Saga 锁柜重算,先经 `WarehouseResourceGateway` 批量预留真实托盘,再本地原子生成剩余独立计划并确认资源,本地失败只补偿未确认资源,远端确认失败的重放只重试确认,同柜不同幂等键也不得产生第二批资源 | | 托盘分配算法 | `inboundplan/service/PalletAllocator.java` | | FBA 预约创建、详情与编辑 | `InboundPlanController.java` 的 `/fba-appointments/**`、`inboundplan/service/InboundFbaAppointmentService.java`、`BatchFbaAppointmentRequest.java`、`UpdateFbaAppointmentRequest.java` | -| PDA 入仓、实收、上架与完成 | `inboundplan/pda/controller/ReceivingContainerController.java`、`inboundplan/pda/service/ReceivingContainerService.java`、内部编排 `InboundPdaWorkflowService.java`;公网入口固定为 `/api/pda/v1/last-leg/receiving-containers/**`,`/warehouse-profile` 只按 Token 绑定仓解析共享仓库中文名称与编码,旧 planNo/allocationId Controller 已移除 | +| PDA 入仓、实收、上架与完成 | `inboundplan/pda/controller/ReceivingContainerController.java`、`inboundplan/pda/service/ReceivingContainerService.java`、内部编排 `InboundPdaWorkflowService.java`;公网入口固定为 `/api/pda/v1/last-leg/receiving-containers/**`,`/warehouse-profile` 只按 Token 绑定仓解析共享仓库中文名称与编码,旧 planNo/allocationId Controller 已移除。多计划同柜返回计划级 `assigned/mergeAllowed/urgent/mergeHint`,合托只作现场可选提示;首扫确认已预留的真实 pallet/location/lock 绑定,不得重新分配计划资源 | | PDA 卸柜照片上传、归属校验与预览 | `ReceivingContainerController.java` 的 `/current/unload-photos/files`、`/current/unload-photos/files/{fileId}/download`,网关 `inboundplan/pda/gateway/UnloadPhotoFileGateway.java`、`HttpUnloadPhotoFileGateway.java`;File 类型固定 `OVERSEAS_UNLOAD_PHOTO`,下载只允许当前会话已签收的 fileId 并返回 `no-store` | | 卸柜照片与海外卸柜完成 Outbox | `ReceivingContainerController.java`、`ReceivingContainerService.java`、`inboundplan/pda/service/InboundUnloadCompletedOutboxService.java`、`entity/LastlegEventOutbox.java`、`mapper/LastlegEventOutboxMapper.java`;多计划同柜只能在全柜完成后调用 `publishContainerCompleted`,按来源柜预订 ID 幂等发布一次,完成时刻与拍照时刻分离;事件模型见 `../common-event-contract/AGENTS.md` | | 尾程暂扣状态回传 | `service/InboundOrderDetainedOutboxService.java`;暂扣与解除暂扣在订单更新事务内按 `sourceInboundId` 写入海外到国内 Outbox,国内按来源入仓单绑定更新 | diff --git a/order/AGENTS.md b/order/AGENTS.md index e9beb0c3d..848bcfcee 100644 --- a/order/AGENTS.md +++ b/order/AGENTS.md @@ -12,6 +12,7 @@ | 目标 | 入口 | |---|---| | 平台订单 API | `controller/PlatformFirstLegOrderController.java` | +| 平台批量审核预览 | `controller/PlatformFirstLegOrderController.java` 的 `/batch-review-preview`、`service/BatchOrderReviewPreviewService.java`、`dto/BatchOrderReviewPreviewResponse.java`;按目标客户和请求顺序一次返回订单、中文货物摘要、渠道、目的仓及逐项 `canApprove/blockReason`,实现固定两次批量查询,禁止前端逐票读取详情 | | 统一订单查询、审核优先状态与稳定渠道编码 | `service/UnifiedOrderQueryService.java`、`dto/UnifiedOrderResponse.java`、`src/main/resources/openapi/order-v1.yaml`;`channelCode` 直接来自 Order/Firstleg 权威快照,只用于业务校验,不得由 `channelName` 推导。Order 为 `SUBMITTED` 时即使已有 Firstleg 入库投影,统一主状态/下一动作仍固定为 `PENDING_REVIEW/APPROVE_ORDER` | | Firstleg 智能装柜待审核候选 | `controller/InternalPendingReviewOrderController.java`、`service/PendingReviewOrderQueryService.java`、`dto/PendingReviewOrderDtos.java`、`repository/OrderRepository.java`;内部入口 `/internal/order/v1/pending-review-orders` 只接受服务令牌,只分页返回未删除的 `SUBMITTED` 头程订单并固定投影为 `PENDING_REVIEW`,按 `Asia/Shanghai` 自然日过滤、`created_at DESC, id DESC` 稳定排序,不读取或修复 Firstleg 投影 | | 平台版本化修改客户原始申报 | `controller/PlatformUnifiedOrderController.java` 的 `PUT /api/platform/v1/orders/{orderId}/declaration`、`service/OrderDeclarationUpdateService.java`、`dto/OrderDeclarationDtos.java`;权限 `platform:order:declaration:edit`,货物以订单内 `itemNo` 定位并保持行集合完整,契约 `src/main/resources/openapi/order-v1.yaml` | @@ -23,7 +24,7 @@ | 下单计价渠道校验 | `service/OrderPricingChannelGuard.java`、`client/FirstlegPricingClient.java` | | 下单仓库主数据守卫 | `service/OrderMasterDataGuard.java`、`client/WarehouseClient.java`;校验公共收货仓、目的仓类型、状态、地址与收货能力,并保存服务端快照 | | 统一订单七状态与操作映射 | `../../docs/统一订单主状态与操作契约.md`;`LOADED` 硬门禁和 `DELIVERED` 可信回传同时核对国内货柜履约需求确认表与开工门禁 | -| 货物内部查询与箱唛绑定来源 | `controller/InternalOrderCargoController.java` 向 Firstleg 提供箱唛生成的 SKU 快照来源;`CargoLabelBinding.skuNo` 对 `ONE_CLICK_DROPSHIP` 为强制字段。Firstleg 消费端为 `../firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java`,变更两端 DTO 必须同步。 | +| 货物内部查询与箱唛绑定来源 | `controller/InternalOrderCargoController.java` 向 Firstleg 批量提供箱唛生成的 SKU、尺寸与货件快照;`CargoLabelBinding.skuNo` 对 `ONE_CLICK_DROPSHIP` 为强制字段,`shipmentId` 只取对应货物行,不回退订单顶层字段。Firstleg 消费端为 `../firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java`,变更两端 DTO 必须同步。 | | 智能装柜待审核候选内部查询 | `controller/InternalPendingReviewOrderController.java`、`service/PendingReviewOrderQueryService.java`、`dto/PendingReviewOrderDtos.java`;Firstleg 消费端为 `../firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java`,契约 `src/main/resources/openapi/order-v1.yaml` | | 快照/Outbox | `service/`、`repository/`、`event/` | | DTO/持久化 | `dto/`、`entity/`、`mapper/`、`repository/` | diff --git a/scripts/agents-index-contract.ps1 b/scripts/agents-index-contract.ps1 index 298e953aa..8263d0e9b 100644 --- a/scripts/agents-index-contract.ps1 +++ b/scripts/agents-index-contract.ps1 @@ -128,11 +128,13 @@ Assert-Contains $testTaskRegistry 'projectId `c64729ff-6710-43c6-8ef2-d84e9666cc Assert-Contains $testTaskRegistry 'C:\Users\BY-OY\Desktop\code\chongchong-global`' '测试登记工作目录' Assert-Contains $testTaskRegistry '已重新启用测试环境总控调度' '测试登记启用状态' Assert-Contains $testTaskRegistry '`01a05b10-a5fc-7d51-8203-c671f3a823ad`' '测试环境全流程总控' -Assert-Contains $testTaskRegistry '`01a046ea-cfc4-73a1-85f0-c69c714bff22`' '测试环境海外接柜仓总控' -Assert-Contains $testTaskRegistry '| 海外接柜后端 |' '测试环境海外接柜后端专项' -Assert-Contains $testTaskRegistry '| 海外接柜Admin前端 |' '测试环境海外接柜 Admin 专项' -Assert-Contains $testTaskRegistry '| 海外接柜Client前端 |' '测试环境海外接柜 Client 专项' -Assert-Contains $testTaskRegistry '| 海外接柜PDA前端 |' '测试环境海外接柜 PDA 专项' +Assert-Contains $testTaskRegistry '| 【总控】测试环境全流程 |' '测试环境唯一总控名称' +Assert-Contains $testTaskRegistry '`01a046ea-cfc4-73a1-85f0-c69c714bff22`' '测试环境海外仓全流程测试专项' +Assert-Contains $testTaskRegistry '| 【测试】海外仓全流程 |' '测试环境海外仓全流程测试专项名称' +Assert-Contains $testTaskRegistry '| 【前端】Admin全流程 |' '测试环境 Admin 前端专项' +Assert-Contains $testTaskRegistry '| 【前端】Client全流程 |' '测试环境 Client 前端专项' +Assert-Contains $testTaskRegistry '| 【前端】海外PDA |' '测试环境海外 PDA 专项' +Assert-Contains $testTaskRegistry '| 【后端4】海外接柜 |' '测试环境海外接柜后端专项' Assert-NotContains $testTaskRegistry '01a0194a-259c-7171-8967-074731a27618' '测试登记历史总控' Assert-NotContains $testTaskRegistry '01a06c0b-0515-7c60-b329-77342b54e666' '测试登记当前开发总控' Assert-NotContains $testTaskRegistry '6f874ec3-d3b2-49bd-8625-327f4d685f98' '测试登记开发 projectId' @@ -172,7 +174,7 @@ Assert-Contains $rootAgents '尾程订单控制与柜级入仓计划转换' '根 Assert-Contains $adminAgents '目的港码头拖车与还空预警' 'Admin 目的港码头拖车路由' Assert-Contains $adminAgents '尾程订单控制与柜级入仓计划转换' 'Admin 尾程订单控制路由' Assert-Contains $backendAgents '目的港码头拖车与还空预警' '后端目的港码头拖车路由' -Assert-Contains $backendAgents '尾程订单控制与柜级入仓计划转换' '后端尾程订单控制路由' +Assert-Contains $backendAgents '尾程订单控制、柜级预览与入仓计划转换' '后端尾程订单控制路由' $testTaskIds = @([regex]::Matches($testTaskRegistry, '(?m)^\|\s*`([0-9a-f-]{36})`\s*\|') | ForEach-Object { $_.Groups[1].Value }) $devTaskIds = @([regex]::Matches($devTaskRegistry, '(?m)^\|\s*`([0-9a-f-]{36})`\s*\|') | ForEach-Object { $_.Groups[1].Value }) foreach ($sharedTaskId in @($testTaskIds | Where-Object { $devTaskIds -contains $_ } | Select-Object -Unique)) { @@ -263,6 +265,48 @@ Assert-Contains $unifiedOrderQueryService 'order.getStatus() == OrderStatus.SUBM Assert-Contains $unifiedOrderQueryService 'pendingReview ? "PENDING_REVIEW" : summary.mainStatus()' 'Order 待审核主状态优先' Assert-Contains $unifiedOrderQueryService 'pendingReview ? "APPROVE_ORDER" : summary.nextAction()' 'Order 待审核动作优先' +# 批量审核、客户专属价格簿和柜级阶梯计价已经成为稳定跨端能力,必须从根表直达且锁住服务端权威规则。 +$batchReviewPreviewService = Read-RequiredText (Join-Path $backendRoot 'order/src/main/java/com/baiyue/order/service/BatchOrderReviewPreviewService.java') +$fbaPricingOpenApi = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/resources/openapi/fba-pricing-v1.yaml') +$fbaPricingService = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java') +$inboundFeeDetailService = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java') +$adminUnifiedOrdersApi = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/unified-orders/api/unifiedOrders.ts') +$adminCarrierRatesView = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue') +Assert-Contains $rootAgents '头程订单、批量审核预览与入仓码关系' '根批量审核预览路由' +Assert-Contains $adminAgents '订单列表、批量审核预览、详情、商品图片与入库费用' 'Admin 批量审核预览路由' +Assert-Contains $backendAgents '平台订单批量审核预览' '后端批量审核预览路由' +Assert-Contains $orderAgents '平台批量审核预览' 'Order 批量审核预览入口' +Assert-Contains $orderOpenApi '/api/platform/v1/orders/first-leg/batch-review-preview:' 'Order 批量审核预览 OpenAPI' +Assert-Contains $batchReviewPreviewService '固定两次批量查询生成审核弹窗快照' 'Order 批量审核禁止逐票查询' +Assert-Contains $adminUnifiedOrdersApi '/platform/v1/orders/first-leg/batch-review-preview' 'Admin 批量审核预览 API' +Assert-Contains $rootAgents '客户专属报价、柜级阶梯运费与费用调整' '根客户专属报价路由' +Assert-Contains $adminAgents '客户专属价格簿、柜级阶梯运费与受控调价' 'Admin 客户专属报价路由' +Assert-Contains $backendAgents '客户专属价格簿、柜级阶梯运费与受控调价' '后端客户专属报价路由' +Assert-Contains $firstlegAgents '客户专属价格簿、柜级阶梯运费与调价边界' 'Firstleg 客户专属报价入口' +Assert-Contains $fbaPricingOpenApi '计价时优先使用客户专属 ACTIVE 版本,不存在时回退公共 ACTIVE 版本' '价格簿作用域优先级' +Assert-Contains $fbaPricingOpenApi '计费量只能在系统原计费量上下10%范围内(含边界)' '计费量调整边界' +Assert-Contains $fbaPricingService 'recalculateUnlockedContainerCustomerFees' '柜级阶梯费用重算入口' +Assert-Contains $firstlegAgents '一件代发 `WAIT_PALLETIZE` 可在预装确认前试算' 'Firstleg 一件代发预装前计价边界' +Assert-Contains $fbaPricingService '"WAIT_PALLETIZE"' '一件代发托盘化状态可计价' +Assert-Contains $fbaPricingService 'preserveProcessStatusDuringPricing' '计价保留履约状态入口' +Assert-Contains $inboundFeeDetailService 'PRIVATE_LAST_MILE_DELIVERY_FEE_CODE' '私人仓尾程派送费稳定代码' +Assert-Contains $adminCarrierRatesView 'customerId' 'Admin 价格簿客户作用域' +Assert-Path 'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/pricing/ContainerTierAmountAllocator.java' + +# 海外异常处理必须贯通证据、原出库来源和库存恢复,不能退化为仅修改派送状态。 +$exceptionOrderService = Read-RequiredText (Join-Path $backendRoot 'lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/service/ExceptionOrderService.java') +$inventoryCommandService = Read-RequiredText (Join-Path $backendRoot 'lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/service/InventoryCommandService.java') +Assert-Contains $rootAgents '退回海外仓时先把实收恢复到指定真实托盘/库位' '根海外异常库存恢复路由' +Assert-Contains $adminAgents '库存恢复、异常完成与派送 `RETURNED` 由服务端原子推进' 'Admin 海外异常原子边界' +Assert-Contains $backendAgents '海外异常退回、库存恢复与客户回访' '后端海外异常库存恢复路由' +Assert-Contains $lastlegAgents 'ExceptionEvidenceFileGateway.java' 'Lastleg 异常证据入口' +Assert-Contains $lastlegAgents '`RETURN_STOCK_IN` 流水' 'Lastleg 退回库存流水边界' +Assert-Contains $fileAgents 'businessType=OVERSEAS_EXCEPTION_EVIDENCE' 'File 海外异常证据归属' +Assert-Contains $exceptionOrderService 'restoreReturnedInventory' '海外异常退回库存恢复调用' +Assert-Contains $exceptionOrderService 'delivery.setStatus("RETURNED")' '海外异常完成派送状态' +Assert-Contains $inventoryCommandService 'ReturnedInventoryRestoreRequest' '海外退回库存命令入口' +Assert-Path 'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/gateway/ExceptionEvidenceFileGateway.java' + # 智能装柜已扩展到四态候选、Order 待审核补齐、提前占位与未拣整批移除;费用确认和会话恢复也必须可定向命中。 $pendingReviewOrderService = Read-RequiredText (Join-Path $backendRoot 'order/src/main/java/com/baiyue/order/service/PendingReviewOrderQueryService.java') $merchantInboundBillingService = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/java/com/baiyue/firstleg/service/MerchantInboundBillingService.java') @@ -391,7 +435,7 @@ Assert-Contains $firstlegAgents '按卸柜完成时间加 72 小时' 'Firstleg Assert-Contains $terminalDrayageOpenApi '/api/platform/v1/terminal-drayages/{containerBookingId}/fleet:' '码头拖车车队命令 OpenAPI' Assert-Contains $terminalDrayageOpenApi '/api/platform/v1/terminal-drayages/{containerBookingId}/empty-return:' '码头拖车还柜命令 OpenAPI' Assert-Contains $terminalDrayageService 'EMPTY_RETURN_WARNING_HOURS = 72' '码头拖车 72 小时预警实现' -Assert-Contains $terminalDrayageService 'context.unloadingCompletedAt().plusHours(EMPTY_RETURN_WARNING_HOURS)' '码头拖车权威截止时间实现' +Assert-Contains $terminalDrayageService 'context.base().unloadingCompletedAt().plusHours(EMPTY_RETURN_WARNING_HOURS)' '码头拖车权威截止时间实现' Assert-Contains $adminTerminalDrayages 'allowedActions' 'Admin 码头拖车服务端动作消费' Assert-Contains $lastlegOrderCommandService '必须提交完整8行人工时间快照' '尾程人工节点完整快照实现' Assert-Contains $lastlegOrderCommandService '尾程订单已进入出库流程,不能新增暂扣' '尾程暂扣出库门禁实现' @@ -675,28 +719,47 @@ Assert-Contains $productImageController "hasAuthority('platform:order:list')" ' Assert-Contains $receivingController '/current/unload-photos/files/{fileId}/download' '海外 PDA 签收照片预览' Assert-Contains $receivingController 'CacheControl.noStore()' '海外 PDA 签收照片缓存边界' -# 尾程订单按来源柜展示服务端计划进度,整柜转换每票独立计划且整批原子。 +# 尾程订单按来源柜展示服务端计划进度;整柜确认必须绑定预览快照并编排真实托盘批次。 $lastlegBusinessOpenApi = Read-RequiredText (Join-Path $backendRoot 'lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml') $lastlegAgents = Read-RequiredText (Join-Path $backendRoot 'lastleg-service/AGENTS.md') $adminLastMileApi = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/overseas-warehouse/api/lastMileOrders.ts') +$containerConversionCoordinator = Read-RequiredText (Join-Path $backendRoot 'lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinator.java') +$warehousePalletBatchService = Read-RequiredText (Join-Path $backendRoot 'warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehousePalletBatchService.java') +$overseasWarehouseAgents = Read-RequiredText (Join-Path $backendRoot 'warehouse-overseas-service/AGENTS.md') +$overseasPdaReceivingAdapter = Read-RequiredText (Join-Path $workspaceRoot 'apps/pda-overseas-uniapp/src/api/receiving-adapter.js') Assert-Contains $lastlegBusinessOpenApi '/api/platform/v1/last-leg/orders/container-summaries:' 'Lastleg 柜级摘要契约' Assert-Contains $lastlegBusinessOpenApi '/api/platform/v1/last-leg/inbound-plans/convert-container:' 'Lastleg 整柜计划转换契约' +Assert-Contains $lastlegBusinessOpenApi '/api/platform/v1/last-leg/inbound-plans/container-preview:' 'Lastleg 整柜计划预览契约' +Assert-Contains $lastlegBusinessOpenApi 'snapshotVersion' 'Lastleg 整柜计划快照版本' +Assert-Contains $lastlegBusinessOpenApi 'snapshotHash' 'Lastleg 整柜计划快照哈希' Assert-Contains $lastlegBusinessOpenApi 'sourceContainerBookingId' 'Lastleg 稳定来源柜身份' Assert-Contains $adminAgents '/orders/container-summaries' 'Admin 柜级摘要目标入口' Assert-Contains $rootAgents '尾程订单控制与柜级入仓计划转换' '根柜级计划入口' -Assert-Contains $backendAgents '分页前排除全部入仓码均已生成有效独立计划的柜' '后端柜级计划待处理筛选' -Assert-Contains $adminAgents '返回仍有待生成计划入仓码的柜' 'Admin 柜级计划待处理筛选' -Assert-Contains $lastlegAgents '分页前排除全部入仓码均已生成有效独立计划的柜' 'Lastleg 柜级计划待处理筛选' -Assert-Contains $lastlegBusinessOpenApi '仅返回仍有入仓码待生成有效独立计划的柜' 'Lastleg 柜级计划待处理 OpenAPI' +Assert-Contains $backendAgents '分页返回全部稳定来源柜' '后端全柜工作台范围' +Assert-Contains $adminAgents '服务端分页返回全部稳定来源柜' 'Admin 全柜工作台范围' +Assert-Contains $lastlegAgents '分页返回全部稳定来源柜' 'Lastleg 全柜工作台范围' +Assert-Contains $lastlegBusinessOpenApi '返回全部稳定来源柜' 'Lastleg 全柜工作台 OpenAPI' Assert-Contains $adminAgents 'ACTIVE + FBA' 'Admin FBA 权威仓候选' Assert-Contains $adminAgents '/inbound-plans/convert-container' 'Admin 整柜转换目标入口' +Assert-Contains $adminAgents '/inbound-plans/container-preview' 'Admin 整柜预览目标入口' +Assert-Contains $adminAgents '同一 `snapshotVersion/snapshotHash` 原样提交' 'Admin 整柜快照确认边界' Assert-Contains $adminLastMileApi '/orders/container-summaries' 'Admin 柜级摘要真实调用' +Assert-Contains $adminLastMileApi '/platform/v1/last-leg/inbound-plans/container-preview' 'Admin 柜级预览真实调用' Assert-NotContains $adminLastMileApi '/orders/pending-containers' 'Admin 旧柜级摘要调用' Assert-Contains $lastlegBusinessOpenApi 'ContainerPlanProgressSummary' 'Lastleg 柜级计划进度摘要模型' Assert-Contains $lastlegBusinessOpenApi 'ContainerPlanProgressDetail' 'Lastleg 柜级计划进度详情模型' Assert-Contains $lastlegBusinessOpenApi 'allPlansReady' 'Lastleg 全柜计划就绪事实' Assert-Contains $lastlegBusinessOpenApi 'plannedArrivalAt' 'Lastleg 计划到仓冻结事实' Assert-Contains $lastlegAgents 'service/FbaWarehouseValidator.java' 'Lastleg FBA 权威仓校验入口' +Assert-Contains $lastlegAgents 'ContainerInboundPlanConversionCoordinator.java' 'Lastleg 柜级计划 Saga 入口' +Assert-Contains $containerConversionCoordinator 'warehouse.ensurePalletBatch' 'Lastleg 托盘批次预留' +Assert-Contains $containerConversionCoordinator 'warehouse.confirmPalletBatch' 'Lastleg 托盘批次确认' +Assert-Contains $containerConversionCoordinator 'warehouse.compensatePalletBatch' 'Lastleg 托盘批次补偿' +Assert-Contains $warehousePalletBatchService 'Only a RESERVED pallet batch can be confirmed' '海外资源批次确认状态门禁' +Assert-Contains $overseasWarehouseAgents '同柜重放不得产生第二批资源' '海外资源批次幂等边界' +Assert-Contains $overseasPdaAgents 'assigned/mergeAllowed/urgent/mergeHint' '海外 PDA 计划标志索引' +Assert-Contains $overseasPdaAgents '不在设备端重新分配' '海外 PDA 预留托盘边界' +Assert-Contains $overseasPdaReceivingAdapter 'mergeAllowed: value.mergeAllowed === true' '海外 PDA 合托可选事实适配' # 国内 PDA 当前以页面、API 和作业状态合同覆盖部分拣货及库位不足反馈。 Assert-Contains $pdaAgents 'tests/page-contract.test.mjs' '国内 PDA 页面合同' @@ -1079,5 +1142,5 @@ if ($failures.Count -gt 0) { exit 1 } -Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖快速表唯一性、部署单元、开发/测试任务登记隔离、前端模块、Client active 路由、客户中文批量下单、箱规算法与测量计费、称重整票库位块与暂估箱数校准、全局智能装柜组分页、客户账单入仓码联查、导出退避任务重试、脚本目录治理、Finance 扩展 HTTP 模块、柜费跨客户原子分摊、海外费用跨区入池与直接已付隔离、企业联系信息与登录标识解耦、选填法人身份证独立上传、待审核订单状态优先、订单申报版本同步与渠道门禁、装柜证据内容哈希重放、业务编号与内部 ID 隔离、装柜清单生命周期与真实柜号后补、订舱级拖车与柜级证据、海外仓全流程范围、柜级待处理计划筛选与范围漂移、仓库主档唯一权威、订舱候选与柜量语义、整柜出仓与船节点单调性、Gateway 覆盖、PDA 页面/运行配置/独立更新/语言预留/能力开关、人工分段验收链、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口和排除目录。" -ForegroundColor Green +Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖快速表唯一性、部署单元、开发/测试任务登记隔离、前端模块、Client active 路由、客户中文批量下单、箱规算法与测量计费、一件代发预装前计价、称重整票库位块与暂估箱数校准、全局智能装柜组分页、客户账单入仓码联查、导出退避任务重试、脚本目录治理、Finance 扩展 HTTP 模块、柜费跨客户原子分摊、海外费用跨区入池与直接已付隔离、企业联系信息与登录标识解耦、选填法人身份证独立上传、待审核订单状态优先、订单申报版本同步与渠道门禁、装柜证据内容哈希重放、业务编号与内部 ID 隔离、装柜清单生命周期与真实柜号后补、订舱级拖车与柜级证据、海外仓全流程范围、柜级全柜工作台、计划快照与托盘 Saga、海外 PDA 多计划标志及范围漂移、仓库主档唯一权威、订舱候选与柜量语义、整柜出仓与船节点单调性、Gateway 覆盖、PDA 页面/运行配置/独立更新/语言预留/能力开关、人工分段验收链、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口和排除目录。" -ForegroundColor Green exit 0 diff --git a/scripts/create-pending-review-orders-contract.test.mjs b/scripts/create-pending-review-orders-contract.test.mjs index 5f3744f75..764b20d91 100644 --- a/scripts/create-pending-review-orders-contract.test.mjs +++ b/scripts/create-pending-review-orders-contract.test.mjs @@ -60,6 +60,7 @@ test('builds an order payload owned by the fixed profile', () => { senderLocation: { province: '浙江省', city: '义乌市', district: '稠城街道', postalCode: '322000' }, }, productImageFileId: '33', + pickupAddressId: '55', }) assert.equal(payload.receivingWarehouseId, '11') assert.equal(payload.destinationWarehouseId, '22') @@ -67,11 +68,14 @@ test('builds an order payload owned by the fixed profile', () => { assert.equal(payload.customerName, '固定测试客户') assert.equal(payload.cargoes.length, 1) assert.equal(payload.cargoes[0].productImageFileId, '33') + assert.equal(payload.cargoes[0].woodenProduct, false) assert.equal(payload.tradeTerm, 'EXW') assert.equal(payload.pickupMethod, 'SELF_TRUCK') assert.equal(payload.taxMode, 'TAX_REFUND') assert.equal(payload.customsInspection, true) assert.equal(payload.sender, null) + assert.equal(payload.senderAddressId, null) + assert.equal(payload.requestedPickupAt, null) assert.equal(payload.cargoes[0].cargoName, PRODUCT_CATALOG[0].cargoName) assert.equal(payload.cargoes[0].packageCount, 7) assert.equal(payload.cargoes[0].totalQuantity, PRODUCT_CATALOG[0].unitsPerPackage * 7) @@ -79,6 +83,27 @@ test('builds an order payload owned by the fixed profile', () => { assert.equal(payload.cargoes[0].declaredValue, PRODUCT_CATALOG[0].unitDeclaredValue * PRODUCT_CATALOG[0].unitsPerPackage * 7) }) +test('uses the selected customer pickup address and appointment for pickup orders', () => { + const payload = buildOrderPayload({ + marker: 'PENDING-PICKUP', index: 1, + profile: { id: '44', tenantName: '固定测试客户' }, + master: { + receiving: { id: '11', warehouseName: '国内仓' }, + destination: { id: '22', warehouseName: 'ONT8', warehouseCode: 'ONT8', countryCode: 'US' }, + channel: { channelCode: 'SEA-US', channelName: '美西海运' }, pricingTaxMode: 'TAX_INCLUDED', + }, + product: PRODUCT_CATALOG[0], packageCount: 1, productImageFileId: '33', pickupAddressId: '55', + options: { + tradeTerm: 'FOB', pickupMethod: 'PICKUP', taxMode: 'BUY_DECLARATION', + customsInspection: false, expectedShipDays: 7, + senderLocation: { province: '广东省', city: '深圳市', district: '南山区', postalCode: '518000' }, + }, + }) + assert.equal(payload.sender, null) + assert.equal(payload.senderAddressId, '55') + assert.match(payload.requestedPickupAt, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/) +}) + test('randomizes only supported basic-order fields', () => { for (let index = 0; index < 100; index += 1) { const options = randomOrderOptions() diff --git a/scripts/create-pending-review-orders.mjs b/scripts/create-pending-review-orders.mjs index 31dada7ce..9dc46dad4 100644 --- a/scripts/create-pending-review-orders.mjs +++ b/scripts/create-pending-review-orders.mjs @@ -247,7 +247,39 @@ async function uploadProductImage(gateway, timeoutMs, token, marker) { return data.fileId } -export function buildOrderPayload({ marker, index, profile, master, product, packageCount, options = randomOrderOptions(), productImageFileId }) { +async function ensurePickupAddress(gateway, timeoutMs, token, profile, marker) { + const listResponse = await request(gateway, timeoutMs, 'GET', + '/api/merchant/v1/orders/first-leg/addresses?addressType=PICKUP', { token }) + const existing = items(requireSuccess(listResponse, 'Query pickup addresses')) + .find((address) => address?.id) + if (existing) return String(existing.id) + + const location = pickRandom(SENDER_LOCATIONS) + const createResponse = await request(gateway, timeoutMs, 'POST', + '/api/merchant/v1/orders/first-leg/addresses', { + token, + body: { + addressType: 'PICKUP', + addressName: '待审核订单测试取货地址', + companyName: profile.tenantName || `固定客户-${marker}`, + contactName: '订单测试联系人', + phone: '13800138000', + email: 'pending-order@example.com', + countryCode: 'CN', + province: location.province, + city: location.city, + district: location.district, + addressLine: `${marker} 固定客户测试地址`, + postalCode: location.postalCode, + defaultAddress: true, + }, + }) + const addressId = String(requireSuccess(createResponse, 'Create pickup address') ?? '') + if (!addressId) throw new Error('Create pickup address did not return address ID') + return addressId +} + +export function buildOrderPayload({ marker, index, profile, master, product, packageCount, options = randomOrderOptions(), productImageFileId, pickupAddressId }) { const sequence = String(index).padStart(3, '0') const shipmentId = `SHIP-${sequence}-${marker}`.slice(0, 64) const referenceId = `REF-${sequence}-${marker}`.slice(0, 64) @@ -282,18 +314,10 @@ export function buildOrderPayload({ marker, index, profile, master, product, pac amazonWarehouseCode: master.destination.warehouseCode, shipmentId, referenceId, - sender: options.pickupMethod === 'PICKUP' ? { - companyName, - contactName: '订单测试联系人', - phone: '13800138000', - email: 'pending-order@example.com', - countryCode: 'CN', - province: options.senderLocation.province, - city: options.senderLocation.city, - district: options.senderLocation.district, - addressLine: `${marker} 固定客户测试地址`, - postalCode: options.senderLocation.postalCode, - } : null, + requestedPickupAt: options.pickupMethod === 'PICKUP' + ? new Date(Date.now() + 7 * 86_400_000).toISOString().slice(0, 19) : null, + senderAddressId: options.pickupMethod === 'PICKUP' ? pickupAddressId : null, + sender: null, receiver: null, cargoes: [{ skuNo: `SKU-${sequence}-${marker}`.slice(0, 64), @@ -325,6 +349,7 @@ export function buildOrderPayload({ marker, index, profile, master, product, pac referenceId, upcNo: `${Date.now()}${sequence}`.slice(-12), productImageFileId, + woodenProduct: false, dangerousGoods: false, cargoRemark: `待审核订单货物 ${sequence} ${marker}`, }], @@ -383,6 +408,7 @@ export async function main(argv = process.argv.slice(2)) { const routePool = await loadPricedRoutes(gateway, timeoutMs, token) const productImageFileId = await uploadProductImage(gateway, timeoutMs, token, marker) + const pickupAddressId = await ensurePickupAddress(gateway, timeoutMs, token, profile, marker) const randomizedProducts = shuffled(PRODUCT_CATALOG) const sameChannel = args['same-channel'] === true const randomizedRoutes = sameChannel ? selectSameChannelRoutes(routePool) : diversifyRoutes(routePool) @@ -392,7 +418,7 @@ export async function main(argv = process.argv.slice(2)) { const product = randomizedProducts[(index - 1) % randomizedProducts.length] const packageCount = randomInt(1, 31) const options = randomOrderOptions() - const payload = buildOrderPayload({ marker, index, profile, master, product, packageCount, options, productImageFileId }) + const payload = buildOrderPayload({ marker, index, profile, master, product, packageCount, options, productImageFileId, pickupAddressId }) const createResponse = await request(gateway, timeoutMs, 'POST', '/api/merchant/v1/orders/first-leg', { token, diff --git a/warehouse-overseas-service/AGENTS.md b/warehouse-overseas-service/AGENTS.md index 47622f276..9fb3d57f7 100644 --- a/warehouse-overseas-service/AGENTS.md +++ b/warehouse-overseas-service/AGENTS.md @@ -15,7 +15,7 @@ | 内部资源锁定、释放、报废 | `controller/InternalWarehouseResourceLockController.java` | | PDA 托盘状态流转与补偿 | `controller/InternalPdaPalletTransitionController.java`、`service/WarehousePalletTransitionService.java`、`mapper/WarehousePalletTransitionMapper.java`;内部前缀 `/internal/v1/pda-pallet-transitions` | | 托盘解析与业务绑定 | `controller/InternalWarehousePalletController.java`、`dto/WarehousePalletResolveResponse.java`、`entity/WarehousePalletBindingEntity.java` | -| 柜级入库计划批量托盘 | `controller/InternalWarehousePalletBatchController.java`、`service/WarehousePalletBatchService.java`、`warehouse-pallet-batch-internal-v1.yaml`;先预留,Lastleg 本地成功后确认,失败时仅补偿未确认资源 | +| 柜级入库计划批量托盘 | `controller/InternalWarehousePalletBatchController.java`、`service/WarehousePalletBatchService.java`、`warehouse-pallet-batch-internal-v1.yaml`;以稳定批次键幂等执行 `ensure-reserved → confirm`,只从可预留真实托盘/库位建立逐项资源锁;Lastleg 本地计划事务失败时仅补偿仍为 `RESERVED` 的未确认资源,已 `CONFIRMED` 批次不得回退,同柜重放不得产生第二批资源 | | 仓储资源服务 | `service/WarehouseResourceService.java`、`WarehouseResourceLockService.java` | | 内部服务令牌 | `security/InternalServiceTokenGuard.java` | | 数据库基线与中文注释契约 | `src/main/resources/db/warehouse.sql`、`src/test/java/com/baiyue/warehouse/schema/SchemaChineseCommentContractTest.java` |