From 4b0b3e91720029e1ba377d7d65e333efa19eb328 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 18 Aug 2026 15:43:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/nacos/cn/order-service.yml | 4 +- config/nacos/local/order-service.yml | 4 +- deploy/sql/头程数据库统一增量-MySQL8.sql | 91 ++----- file/AGENTS.md | 3 +- finance/AGENTS.md | 2 + firstleg/AGENTS.md | 9 +- .../PlatformInboundMeasurementController.java | 4 +- .../PlatformTruckAppointmentController.java | 4 +- .../dto/CartonMeasurementCommand.java | 1 - .../firstleg/dto/CartonMeasurementResult.java | 1 - .../baiyue/firstleg/dto/ContainerDtos.java | 9 +- .../dto/InboundMeasurementRequests.java | 2 - .../dto/InboundMeasurementResponses.java | 2 - .../firstleg/dto/MeasurementBatchCommand.java | 1 - .../dto/MerchantInboundBillingResponse.java | 1 - .../firstleg/dto/MojiaMeasurementDtos.java | 1 - .../firstleg/dto/OpenApiMeasurementDtos.java | 1 - .../baiyue/firstleg/dto/SmartLoadingDtos.java | 10 +- .../firstleg/dto/TruckAppointmentDtos.java | 4 +- .../entity/InboundMeasurementItemEntity.java | 2 - .../entity/MojiaMeasurementReceiptEntity.java | 1 - .../entity/TruckAppointmentEntity.java | 1 + .../mapper/ContainerBookingMapper.java | 2 + .../mapper/InboundSplitSettlementMapper.java | 22 +- .../mapper/SmartLoadingCandidateMapper.java | 12 +- .../mapper/TruckAppointmentMapper.java | 3 +- .../PlatformOceanBookingController.java | 17 +- .../domain/OceanBookingStatus.java | 5 +- .../oceanbooking/dto/OceanBookingDtos.java | 66 +++-- .../mapper/OceanBookingMapper.java | 7 +- .../repository/OceanBookingRepository.java | 19 +- .../service/OceanBookingService.java | 216 +++++++---------- .../repository/ContainerRepository.java | 5 + .../TruckAppointmentRepository.java | 2 +- .../service/CartonMeasurementService.java | 12 +- .../firstleg/service/ContainerService.java | 44 +--- .../InboundMeasurementOpenApiService.java | 4 - .../service/InboundMeasurementService.java | 4 +- .../service/MeasurementBatchService.java | 4 - .../MerchantInboundBillingService.java | 4 +- .../MojiaMeasurementOpenApiService.java | 7 +- .../firstleg/service/SmartLoadingService.java | 18 +- .../service/TruckAppointmentService.java | 18 +- ...phase2-inbound-measurement-maintenance.sql | 8 +- .../resources/db/phase2-inbound-schema.sql | 3 - .../db/phase3-ocean-booking-schema.sql | 89 +------ .../db/phase3-truck-appointment-schema.sql | 5 +- .../main/resources/openapi/firstleg-v1.yaml | 15 +- .../resources/openapi/ocean-bookings-v1.yaml | 229 ++++-------------- .../openapi/truck-appointments-v1.yaml | 14 +- ...easurementEvidenceOpenApiContractTest.java | 2 +- .../PlatformCartonScanControllerTest.java | 8 +- ...ruckAppointmentControllerContractTest.java | 4 +- .../PdaSplitSettlementSqlContractTest.java | 9 +- ...artLoadingCandidateMapperContractTest.java | 8 + .../TruckAppointmentMapperContractTest.java | 4 +- .../service/CartonMeasurementServiceTest.java | 10 +- .../InboundMeasurementOpenApiServiceTest.java | 2 +- .../InboundMeasurementServiceTest.java | 4 +- .../service/MeasurementBatchServiceTest.java | 6 +- .../service/MeasurementReviewServiceTest.java | 1 - .../MojiaMeasurementOpenApiServiceTest.java | 4 +- .../service/SmartLoadingServiceTest.java | 2 +- .../service/TruckAppointmentServiceTest.java | 88 +++++++ order/AGENTS.md | 5 +- .../order/config/InboundCodeModeProvider.java | 2 +- order/src/main/resources/application.yml | 2 + .../config/InboundCodeModeProviderTest.java | 22 +- .../inbound-measurement.mjs | 24 +- scripts/client-phase2-test-env-acceptance.mjs | 22 +- scripts/mojia-measurement-scenario.mjs | 6 +- 71 files changed, 523 insertions(+), 724 deletions(-) diff --git a/config/nacos/cn/order-service.yml b/config/nacos/cn/order-service.yml index 49e6cb174..5e4a505b3 100644 --- a/config/nacos/cn/order-service.yml +++ b/config/nacos/cn/order-service.yml @@ -10,5 +10,5 @@ baiyue.clients: warehouse.url: ${WAREHOUSE_SERVICE_URL:} baiyue.order: - # 两套系统并跑期间统一使用旧系统入仓码;正式切换后改为 AUTO。 - inbound-code-mode: ${ORDER_INBOUND_CODE_MODE:LEGACY_MANUAL} + # 默认由新系统自动生成;仅在两套系统并跑期间显式改为 LEGACY_MANUAL。 + inbound-code-mode: ${ORDER_INBOUND_CODE_MODE:AUTO} diff --git a/config/nacos/local/order-service.yml b/config/nacos/local/order-service.yml index b6f9ec3b5..16d034278 100644 --- a/config/nacos/local/order-service.yml +++ b/config/nacos/local/order-service.yml @@ -7,5 +7,5 @@ baiyue.clients: warehouse.url: ${WAREHOUSE_SERVICE_URL:} baiyue.order: - # 两套系统并跑期间统一使用旧系统入仓码;正式切换后改为 AUTO。 - inbound-code-mode: ${ORDER_INBOUND_CODE_MODE:LEGACY_MANUAL} + # 默认由新系统自动生成;仅在两套系统并跑期间显式改为 LEGACY_MANUAL。 + inbound-code-mode: ${ORDER_INBOUND_CODE_MODE:AUTO} diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 2e3db3594..b3e15b6fb 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -8921,7 +8921,7 @@ USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_ocean_booking ( id BIGINT UNSIGNED NOT NULL COMMENT '海运订舱ID', booking_no VARCHAR(40) NOT NULL COMMENT '平台海运订舱编号', - status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订舱状态:DRAFT仅限创建事务内部、CONFIRMED已确认、RELEASED已放舱、CHANGE_PENDING船期待变更、CANCELLED已取消', + status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订舱状态:DRAFT草稿、RELEASED已发布且永久只读', customer_id BIGINT UNSIGNED NULL COMMENT '可选客户ID快照', customer_name VARCHAR(128) NULL COMMENT '可选客户名称快照', bill_of_lading_no VARCHAR(128) NULL COMMENT '提单号', @@ -8936,8 +8936,8 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking ( destination_port_code VARCHAR(32) NULL COMMENT '目的港编码快照', destination_port_name VARCHAR(128) NULL COMMENT '目的港名称快照', place_of_delivery VARCHAR(128) NULL COMMENT '最终交货地快照', - service_term VARCHAR(32) NOT NULL COMMENT '运输服务条款快照', - freight_term VARCHAR(32) NOT NULL COMMENT '海运费支付条款快照', + service_term VARCHAR(32) NULL COMMENT '运输服务条款快照;发布前必填', + freight_term VARCHAR(32) NULL COMMENT '海运费支付条款快照;发布前必填', estimated_sailing_at DATETIME(3) NULL COMMENT '当前确认预计开船时间', estimated_arrival_at DATETIME(3) NULL COMMENT '当前确认预计到港时间', pending_vessel_name VARCHAR(128) NULL COMMENT '待确认变更船名', @@ -8960,7 +8960,7 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking ( KEY idx_ocean_booking_voyage (vessel_name,voyage_no,updated_at), CONSTRAINT chk_ocean_booking_customer CHECK (customer_id > 0), CONSTRAINT chk_ocean_booking_status CHECK (status IN - ('DRAFT','CONFIRMED','RELEASED','CHANGE_PENDING','CANCELLED')), + ('DRAFT','RELEASED')), CONSTRAINT chk_ocean_booking_schedule CHECK (estimated_sailing_at IS NULL OR estimated_arrival_at IS NULL OR estimated_arrival_at >= estimated_sailing_at), CONSTRAINT chk_ocean_booking_pending_schedule CHECK @@ -8985,66 +8985,6 @@ DROP TRIGGER IF EXISTS trg_ocean_requirement_before_insert; DROP TRIGGER IF EXISTS trg_ocean_requirement_before_update; DROP TRIGGER IF EXISTS trg_ocean_requirement_before_delete; DROP TRIGGER IF EXISTS trg_ocean_history_before_delete; -DELIMITER $$ -DROP PROCEDURE IF EXISTS reconcile_ocean_booking_legacy$$ -CREATE PROCEDURE reconcile_ocean_booking_legacy() -BEGIN - DECLARE v_blank_terms INT DEFAULT 0; - DECLARE v_legacy INT DEFAULT 0; - DECLARE v_external INT DEFAULT 0; - - SELECT COUNT(*) INTO v_blank_terms - FROM fl_ocean_booking - WHERE NULLIF(TRIM(service_term),'') IS NULL - OR NULLIF(TRIM(freight_term),'') IS NULL; - IF v_blank_terms > 0 THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = '订舱结构收敛暂停:存在 service_term/freight_term 为空的真实行,请按原始业务资料补齐后重跑,禁止猜值'; - END IF; - - SELECT COUNT(*) INTO v_legacy - FROM fl_ocean_booking - WHERE status IN ('DRAFT','SUBMITTED','FAILED'); - IF v_legacy > 0 THEN - -- Tables from later phase blocks may not exist on a module-only run. Use - -- dynamic SQL so a clean initialization remains repeatable. - IF EXISTS (SELECT 1 FROM information_schema.tables - WHERE table_schema=DATABASE() AND table_name='fl_port_schedule') THEN - SET @ocean_legacy_external := 0; - SET @ocean_legacy_sql = 'SELECT COUNT(*) INTO @ocean_legacy_external FROM fl_port_schedule p JOIN fl_ocean_booking b ON b.id=p.ocean_booking_id WHERE b.status IN (''DRAFT'',''SUBMITTED'',''FAILED'')'; - PREPARE stmt FROM @ocean_legacy_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - SET v_external = v_external + COALESCE(@ocean_legacy_external,0); - END IF; - IF EXISTS (SELECT 1 FROM information_schema.tables - WHERE table_schema=DATABASE() AND table_name='fl_carrier_schedule_query') THEN - SET @ocean_legacy_external := 0; - SET @ocean_legacy_sql = 'SELECT COUNT(*) INTO @ocean_legacy_external FROM fl_carrier_schedule_query q JOIN fl_ocean_booking b ON b.id=q.ocean_booking_id WHERE b.status IN (''DRAFT'',''SUBMITTED'',''FAILED'')'; - PREPARE stmt FROM @ocean_legacy_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - SET v_external = v_external + COALESCE(@ocean_legacy_external,0); - END IF; - IF v_external > 0 THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = '订舱结构收敛暂停:旧订舱仍被港务船期/船司查询引用,请先按子表历史->主表归档顺序清理引用后重跑;不会伪造 CONFIRMED'; - END IF; - - DELETE FROM fl_ocean_booking_party - WHERE ocean_booking_id IN (SELECT id FROM fl_ocean_booking WHERE status IN ('DRAFT','SUBMITTED','FAILED')); - DELETE FROM fl_ocean_booking_container_allocation - WHERE ocean_booking_id IN (SELECT id FROM fl_ocean_booking WHERE status IN ('DRAFT','SUBMITTED','FAILED')); - DELETE FROM fl_ocean_booking_container_requirement - WHERE ocean_booking_id IN (SELECT id FROM fl_ocean_booking WHERE status IN ('DRAFT','SUBMITTED','FAILED')); - DELETE FROM fl_ocean_booking_operation - WHERE ocean_booking_id IN (SELECT id FROM fl_ocean_booking WHERE status IN ('DRAFT','SUBMITTED','FAILED')); - DELETE FROM fl_ocean_booking_status_history - WHERE ocean_booking_id IN (SELECT id FROM fl_ocean_booking WHERE status IN ('DRAFT','SUBMITTED','FAILED')); - DELETE FROM fl_ocean_booking - WHERE status IN ('DRAFT','SUBMITTED','FAILED'); - END IF; -END$$ -CALL reconcile_ocean_booking_legacy()$$ -DROP PROCEDURE reconcile_ocean_booking_legacy$$ -DELIMITER ; - -- Existing CHECK constraints are not reconciled by CREATE TABLE IF NOT EXISTS. SET @ocean_drop_status_check = ( SELECT IF(COUNT(*) > 0, @@ -9058,10 +8998,10 @@ PREPARE ocean_drop_status_stmt FROM @ocean_drop_status_check; EXECUTE ocean_drop_status_stmt; DEALLOCATE PREPARE ocean_drop_status_stmt; ALTER TABLE fl_ocean_booking - MODIFY COLUMN service_term VARCHAR(32) NOT NULL COMMENT '运输服务条款快照', - MODIFY COLUMN freight_term VARCHAR(32) NOT NULL COMMENT '海运费支付条款快照', + MODIFY COLUMN service_term VARCHAR(32) NULL COMMENT '运输服务条款快照;发布前必填', + MODIFY COLUMN freight_term VARCHAR(32) NULL COMMENT '海运费支付条款快照;发布前必填', ADD CONSTRAINT chk_ocean_booking_status CHECK - (status IN ('DRAFT','CONFIRMED','RELEASED','CHANGE_PENDING','CANCELLED')); + (status IN ('DRAFT','RELEASED')); SET @ocean_drop_failure_reason = ( SELECT IF(COUNT(*) > 0, @@ -9160,13 +9100,11 @@ DROP TRIGGER IF EXISTS trg_ocean_booking_before_update; DELIMITER $$ CREATE TRIGGER trg_ocean_booking_before_update BEFORE UPDATE ON fl_ocean_booking FOR EACH ROW BEGIN - IF OLD.status IN ('RELEASED','CANCELLED') THEN + IF OLD.status = 'RELEASED' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='已放舱或已取消订舱不可修改'; END IF; IF OLD.status <> NEW.status AND NOT ( - (OLD.status='DRAFT' AND NEW.status='CONFIRMED') - OR (OLD.status='CONFIRMED' AND NEW.status IN ('RELEASED','CHANGE_PENDING','CANCELLED')) - OR (OLD.status='CHANGE_PENDING' AND NEW.status IN ('CONFIRMED','CANCELLED'))) THEN + (OLD.status='DRAFT' AND NEW.status='RELEASED')) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海运订舱状态迁移非法'; END IF; END$$ @@ -9277,7 +9215,7 @@ CREATE TABLE IF NOT EXISTS fl_truck_appointment ( id BIGINT UNSIGNED NOT NULL COMMENT '拖车预约ID', appointment_no VARCHAR(40) NOT NULL COMMENT '平台拖车预约编号', outbound_order_id BIGINT UNSIGNED NULL COMMENT '关联头程订单逻辑ID;不跨库建外键', ocean_booking_id BIGINT UNSIGNED NULL COMMENT '关联海运订舱逻辑ID', container_booking_id BIGINT UNSIGNED NULL COMMENT '关联柜次逻辑ID;拖车完成供装柜门禁校验', loading_plan_id BIGINT UNSIGNED NULL COMMENT '关联装柜计划逻辑ID', warehouse_id BIGINT UNSIGNED NULL COMMENT '预约到仓的国内收货仓逻辑ID', customer_id BIGINT UNSIGNED NULL COMMENT '客户ID和数据隔离键', customer_name VARCHAR(128) NULL COMMENT '客户名称快照', - carrier_id BIGINT UNSIGNED NULL COMMENT '拖车服务商逻辑ID', carrier_name VARCHAR(128) NULL COMMENT '拖车服务商名称快照', fleet_contact VARCHAR(64) NULL COMMENT '车队联系人', fleet_phone VARCHAR(32) NULL COMMENT '车队联系电话', + carrier_id BIGINT UNSIGNED NULL COMMENT '拖车服务商逻辑ID', carrier_name VARCHAR(128) NULL COMMENT '拖车服务商名称快照', bill_of_lading_no VARCHAR(128) NULL COMMENT '派车提单号快照;关联RELEASED订舱时由订舱事实源冻结', fleet_contact VARCHAR(64) NULL COMMENT '车队联系人', fleet_phone VARCHAR(32) NULL COMMENT '车队联系电话', driver_name VARCHAR(64) NULL COMMENT '司机姓名', driver_phone VARCHAR(32) NULL COMMENT '司机联系电话', driver_id_card VARCHAR(32) NULL COMMENT '司机身份证号;平台内部敏感信息', vehicle_no VARCHAR(32) NULL COMMENT '车辆车牌号', store_name VARCHAR(128) NULL COMMENT '到仓装柜地点名称快照', store_contact VARCHAR(64) NULL COMMENT '到仓地点联系人快照', store_phone VARCHAR(32) NULL COMMENT '到仓地点联系电话快照', store_address VARCHAR(255) NULL COMMENT '到仓装柜地点地址快照', store_city VARCHAR(64) NULL COMMENT '到仓地点城市快照', store_zipcode VARCHAR(16) NULL COMMENT '到仓地点邮编快照', package_count INT UNSIGNED NULL COMMENT '预约货物包装件数快照', gross_weight_kg DECIMAL(18,3) NULL COMMENT '预约货物毛重,单位千克', @@ -9295,9 +9233,9 @@ CREATE TABLE IF NOT EXISTS fl_truck_appointment_history ( operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照', request_id VARCHAR(128) NULL COMMENT '请求链路中的请求ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间', PRIMARY KEY(id), KEY idx_truck_history(appointment_id,revision) ) ENGINE=InnoDB COMMENT='拖车状态与改约历史'; CREATE TABLE IF NOT EXISTS fl_truck_appointment_attachment ( - id BIGINT UNSIGNED NOT NULL COMMENT '拖车附件关系ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件逻辑ID', attachment_type VARCHAR(32) NOT NULL COMMENT '附件类型编码;由业务动作约定', + id BIGINT UNSIGNED NOT NULL COMMENT '拖车附件关系ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件逻辑ID', attachment_type VARCHAR(32) NOT NULL COMMENT '附件类型:BILL_OF_LADING提单、DISPATCH_ORDER派车单、DRIVER_DOCUMENT司机证件、VEHICLE_DOCUMENT车辆证件、OTHER其他', file_name VARCHAR(255) NULL COMMENT '上传时文件名快照', file_hash CHAR(64) NULL COMMENT '文件内容SHA-256摘要', visibility VARCHAR(24) NOT NULL DEFAULT 'INTERNAL' COMMENT '可见范围:INTERNAL仅平台内部、CLIENT客户可见', uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传操作人ID', - uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '附件上传时间', PRIMARY KEY(id), UNIQUE KEY uk_truck_attachment(appointment_id,file_id), KEY idx_truck_attachment(appointment_id) + uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '附件上传时间', PRIMARY KEY(id), UNIQUE KEY uk_truck_attachment(appointment_id,file_id), KEY idx_truck_attachment(appointment_id), CONSTRAINT chk_truck_attachment_type CHECK(attachment_type IN ('BILL_OF_LADING','DISPATCH_ORDER','DRIVER_DOCUMENT','VEHICLE_DOCUMENT','OTHER')) ) ENGINE=InnoDB COMMENT='拖车附件文件引用'; CREATE TABLE IF NOT EXISTS fl_truck_appointment_operation ( id BIGINT UNSIGNED NOT NULL COMMENT '拖车写操作幂等记录ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键', request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求', @@ -13286,7 +13224,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'remark', '平 CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'service_term', '运输服务条款快照'); CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'shipping_company_id', '船公司逻辑ID快照'); CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'shipping_company_name', '船公司名称快照'); -CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'status', '订舱状态:DRAFT仅限创建事务内部、CONFIRMED已确认、RELEASED已放舱、CHANGE_PENDING船期待变更、CANCELLED已取消'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'status', '订舱状态:DRAFT草稿、RELEASED已发布且永久只读'); CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'updated_at', '订舱记录最后更新时间'); CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'updated_by', '最后修改操作人ID'); CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'updated_by_name', '最后修改操作人姓名快照'); @@ -13541,6 +13479,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'actual_arr CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'actual_departed_at', '车辆实际离仓时间;DEPARTED后供LOADED门禁使用'); CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'actual_loading_at', '实际开始装柜时间'); CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'appointment_no', '平台拖车预约编号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'bill_of_lading_no', '派车提单号快照;关联RELEASED订舱时由订舱事实源冻结'); CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'carrier_id', '拖车服务商逻辑ID'); CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'carrier_name', '拖车服务商名称快照'); CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'container_booking_id', '关联柜次逻辑ID;拖车完成供装柜门禁校验'); @@ -13580,7 +13519,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'version', CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'volume_cbm', '预约货物体积,单位立方米'); CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'warehouse_id', '预约到仓的国内收货仓逻辑ID'); CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'appointment_id', '所属拖车预约ID'); -CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'attachment_type', '附件类型编码;由业务动作约定'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'attachment_type', '附件类型:BILL_OF_LADING提单、DISPATCH_ORDER派车单、DRIVER_DOCUMENT司机证件、VEHICLE_DOCUMENT车辆证件、OTHER其他'); CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'file_hash', '文件内容SHA-256摘要'); CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'file_id', 'File服务文件逻辑ID'); CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'file_name', '上传时文件名快照'); diff --git a/file/AGENTS.md b/file/AGENTS.md index fa856c528..47a1deec8 100644 --- a/file/AGENTS.md +++ b/file/AGENTS.md @@ -10,7 +10,7 @@ | 目标 | 入口 | |---|---| | 商户/平台资质 | `controller/QualificationFileController.java`、`PlatformQualificationFileController.java` | -| 导出中心 | `controller/PlatformExportTaskController.java`、`service/ExportStateService.java`、`mq/` | +| 导出中心 | `controller/PlatformExportTaskController.java`、`service/ExportTaskService.java`、`service/ExportStateService.java`、`service/GenericXlsxExportRunner.java`、`config/ExportDefinitionRegistry.java`、`client/ExportProviderClient.java`、`mq/` | | 测量图片/证据 | `controller/PlatformMeasurementImageController.java`、`InternalMeasurementImageController.java`、`service/MeasurementImageService.java` | | 装柜与业务文件证据 | `controller/PlatformLoadingEvidenceController.java`、`InternalLoadingEvidenceController.java`、`InternalBusinessFileController.java`、`service/InternalBusinessFileService.java` | | 存储与实体 | `service/LocalQualificationStorage.java`、`MeasurementImageStorage.java`、`entity/FileObjectEntity.java` | @@ -22,6 +22,7 @@ - 上传先写真实文件再登记 IAM 元数据;异常结果未知时保留文件供重试,禁止伪造 URL。 - 所有文件按客户、入库单、文件类型校验归属;重量图片可 OCR,第三方尺寸证据不得触发重量 OCR。 +- 公共导出只接受 `ExportDefinitionRegistry` 明确登记的 `exportType`,由 File 调用所属业务服务的内部 Provider 分批取数并生成文件;业务页面不得直连 Provider。新增或修改导出类型时同步检查业务服务 Provider、File Feign 客户端/响应上限、权限、Admin 下载中心标签与重试映射及契约测试。 - 生产环境显式配置 `FILE_STORAGE_ROOT`;磁盘故障返回 `S0503`,不得包装成业务错误。 ## 全量验收导航 diff --git a/finance/AGENTS.md b/finance/AGENTS.md index 04580abc4..8d52a573c 100644 --- a/finance/AGENTS.md +++ b/finance/AGENTS.md @@ -16,6 +16,7 @@ | 客户财务账户汇总 | `service/FinanceCustomerAccountService.java`、`client/IamCustomerClient.java`;客户主档事实仍归 IAM | | 客户对账单 | `controller/ReconciliationStatementController.java`、`service/ReconciliationStatementService.java` | | 应收/应付结算单 | `controller/FinanceSettlementController.java`、`service/FinanceSettlementService.java` | +| Finance 导出数据 Provider | `controller/FinanceExportProviderController.java`、`service/FinanceExportProviderService.java`;由 File `ExportProviderClient` 按已登记 `exportType` 分批拉取,不从 Admin 直接生成文件 | | 五页产品 OpenAPI | `../../docs/财务管理/openapi/finance-phase1-five-pages-v1.yaml`;人工财务补充契约 `finance-f1-manual-v1.yaml` | | Firstleg 费用来源事件入口 | `controller/InternalFinanceFeeSourceController.java`、`service/FinanceFeeSourceEventService.java` | | 查询模型与客户可见性 | `service/FinanceQueryService.java`、`dto/FinanceReadModels.java` | @@ -38,6 +39,7 @@ - 整柜成本分摊使用 Firstleg 返回的真实客户归属与订单体积快照;不得信任前端提交的订单归属或体积。凭证必须由 File 元数据校验客户和业务类型,依赖不可用时不得静默跳过。 - 应付费用来源、成本登记和应付结算单引用的供应商必须按费用项目映射到单一供应商类别,并通过 Warehouse 校验 ID、`ACTIVE` 状态和类别;名称快照只能取 Warehouse 返回值,不能接受自由文本替代。Warehouse 不可用、供应商停用或类别不匹配时必须失败关闭;修改该链路时同步检查事件消费、人工成本、结算创建/付款、Nacos 客户端地址和双方契约测试。 - 修改平台或客户 DTO 时同步检查 Admin `../../apps/admin-web/src/modules/finance/`、Client `../../apps/client-web/src/modules/finance/`、Gateway 路由、权限码和契约测试;客户响应不得暴露供应商成本、内部备注、平台凭证或其他客户数据。 +- 修改 Finance 导出字段、筛选或 `exportType` 时,同步检查 `FinanceExportProviderService`、File `ExportDefinitionRegistry`/`ExportProviderClient`、Admin `modules/finance/` 与 `modules/downloads/`、导出权限和双方契约测试;页面只创建异步下载任务,不绕过 File 导出中心直接拼装业务文件。 ## 验证 diff --git a/firstleg/AGENTS.md b/firstleg/AGENTS.md index b8590df96..3edc60bda 100644 --- a/firstleg/AGENTS.md +++ b/firstleg/AGENTS.md @@ -12,8 +12,8 @@ |---|---| | 履约任务/分配 | `controller/PlatformFulfillmentAssignmentController.java`、`service/FulfillmentTaskService.java` | | 入库单查询/聚合 | `controller/PlatformInboundOrderController.java`、`service/InboundOrderQueryService.java` | -| 平台测量/OCR | `controller/PlatformInboundMeasurementController.java`、`service/InboundMeasurementService.java`、`service/CartonMeasurementService.java` | -| 箱唛箱码生成与版本化 | `repository/InboundCartonLabelRepository.java`、`service/InboundCartonLabelService.java`;基线/升级 SQL 为 `src/main/resources/db/phase2-inbound-carton-label.sql`、`phase2-carton-label-versioning-migration.sql` | +| 平台测量/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`;基线/升级 SQL 为 `src/main/resources/db/phase2-inbound-carton-label.sql`、`phase2-carton-label-versioning-migration.sql` | | 墨家设备测量上传 | `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` | @@ -40,7 +40,8 @@ ## 修改约束 - 平台接口必须校验目标客户、权限、乐观锁、幂等键、历史和审计;商户查询只能使用登录态客户。 -- 第三方回传必须签名、时间窗、防重放、请求哈希,并绑定订单货物和 File 证据;不得信任外部客户字段。 +- 第三方回传必须签名、时间窗、防重放、请求哈希,并绑定订单货物和 File 证据;不得信任外部客户字段。 +- 稳定箱码映射以入库快照和 Order 货物明细为事实;订单审核创建入库单时必须幂等生成,打印接口只复用或补齐同一映射,用户是否下载箱唛不得决定箱码有效性。手工测量、墨家设备和 PDA 扫码统一通过 `InboundCartonLabelService` 校验客户、入库单、货物明细、总箱数/下标和有效批次,不得在测量时临时伪造箱码或接受跨客户、跨入库箱码。 - 测量审核通过后直接进入 `WAIT_SPLIT`,费用仅作为独立初算和人工维护结果,不得阻塞或回退订单主流程;计费量、单价和附加费调整必须版本化并由服务端重算金额。状态或 DTO 变更时逐个检查 Order/Warehouse/File/Integration 消费者。 - 报关、订舱和拖车引用的供应商必须通过 `WarehouseSupplierSnapshotService` 按 ID 向 Warehouse 校验:报关使用 `CUSTOMS`,订舱使用 `BOOKING`(不区分国内/国外 scope),拖车使用 `TRUCKING`;供应商必须为 `ACTIVE`。请求中的名称只能作为一致性提示,持久化名称必须来自 Warehouse 快照,禁止用自由文本绕过 ID、类别或启用状态校验。修改任一供应商引用 DTO、OpenAPI 或内部客户端时,必须同步检查三条业务链、Warehouse `supplier-v1.yaml`、数据库快照字段和对应契约测试。 - Firstleg 只拥有业务费用及履约事实;Finance 拥有费用入池、账期、账单、收款、成本分摊结果和财务状态。修改费用来源版本、发布条件、柜次归属或体积快照时同步检查 `../finance/src/main/java/com/baiyue/finance/client/FirstlegFeeSourceClient.java`、同目录 `FirstlegCostSnapshotClient.java` 及双方契约测试,不在 Firstleg 复制财务状态机。 @@ -48,7 +49,7 @@ ## 三期装柜与 PDA 资源结算 - 智能装柜候选只能按同一国内中转仓合柜;推荐顺序按入库时间先进先出。装柜计划状态为 `EDITING -> LOADING -> COMPLETED`,PDA 全部明细处理完成后自动完成。 -- PDA 拣货以 `fl_inbound_carton_label.box_code` 对应的稳定箱唛箱码为扫描身份;首次打印冻结箱码映射,重复打印和墨家测量复用同一箱码,版本字段只用于并发控制,不进入扫码文本。一个有效且未重复的箱码扫描表示拣出一箱,不得以 SKU、商品件数或页面展示字段替代箱码。任务下发与提交契约见 `src/main/resources/openapi/phase3-pda-convergence-v1.yaml`。 +- PDA 拣货以 `fl_inbound_carton_label.box_code` 对应的稳定箱唛箱码为扫描身份;审核建单、重复打印、手工测量和墨家测量复用同一映射,版本字段只用于并发控制,不进入扫码文本。一个有效且未重复的箱码扫描表示拣出一箱,不得以 SKU、商品件数或页面展示字段替代箱码。任务下发与提交契约见 `src/main/resources/openapi/phase3-pda-convergence-v1.yaml`。 - PDA 部分拣货生成二级子入库单时,数量必须守恒:`已拣数量 + 子单数量 = 拣货前主单可用数量`。子单只允许一层,不能继续拆分。 - `releaseMode=AUTO` 时,释放资源数为 `ceil(实际拣货箱数 × 原资源数 / 原可用箱数)`;`MANUAL` 按 `locationIds` 选择,空数组表示明确释放 0 个。当前契约中未传模式且 `locationIds=null` 时按 AUTO 处理。 - 头程快照必须保存仓库 `reservation_id`。A0 是共享临时库位,多个托盘可具有相同 `location_id`,跨服务结算、转移、释放必须使用 `reservationId`,不得把 `locationId` 当作唯一资源标识。 diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java index dd369b08e..383f8f891 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java @@ -129,7 +129,7 @@ public class PlatformInboundMeasurementController { @RequestPart("image") MultipartFile image) { requirePlatformTenant(user, tenantId); CartonMeasurementCommand command = new CartonMeasurementCommand( - request.boxCode(), request.length(), request.width(), request.height(), request.weight(), + request.boxCode(), request.length(), request.width(), request.height(), request.machineNo(), request.measuredAt(), image, request.scannedShipmentId()); return ApiResponse.success(cartonMeasurements.upsertForInbound( tenantId, user.userId(), inboundId, command)); @@ -149,7 +149,7 @@ public class PlatformInboundMeasurementController { @RequestPart("image") MultipartFile image) { requirePlatformTenant(user, tenantId); CartonMeasurementCommand command = new CartonMeasurementCommand( - request.boxCode(), request.length(), request.width(), request.height(), request.weight(), + request.boxCode(), request.length(), request.width(), request.height(), request.machineNo(), request.measuredAt(), image, request.scannedShipmentId()); return ApiResponse.success(cartonMeasurements.upsertManualForInbound( tenantId, user.userId(), inboundId, command)); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformTruckAppointmentController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformTruckAppointmentController.java index 7a5f6ec23..8dfc3ee84 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformTruckAppointmentController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformTruckAppointmentController.java @@ -1,12 +1,12 @@ package com.baiyue.firstleg.controller; -import com.baiyue.common.audit.BusinessAudit; import com.baiyue.common.core.api.*; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.firstleg.dto.TruckAppointmentDtos.*; import com.baiyue.firstleg.entity.TruckAppointmentEntity; import com.baiyue.firstleg.service.*; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; 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.*; import java.time.LocalDateTime; +import com.baiyue.common.audit.BusinessAudit; import com.baiyue.common.core.api.*; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.firstleg.dto.TruckAppointmentDtos.*; import com.baiyue.firstleg.entity.TruckAppointmentEntity; import com.baiyue.firstleg.service.*; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Positive; 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.*; import java.time.LocalDateTime; @Tag(name="拖车预约") @Validated @RestController @RequestMapping("/api/platform/v1/truck-appointments") public class PlatformTruckAppointmentController { @Resource TruckAppointmentService service; @GetMapping @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:list')") public ApiResponse> page(@AuthenticationPrincipal AuthenticatedUser u,@Valid Query q){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.page(q));} @GetMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:list')") public ApiResponse detail(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.detail(id));} @GetMapping("/{id}/history") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:list')") public ApiResponse> history(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.history(id));} @GetMapping("/{id}/attachments") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:list')") public ApiResponse> attachments(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.attachments(id));} @PostMapping("/{id}/attachments") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:edit')") public ApiResponse attach(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id,@Valid @RequestBody AttachmentRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.attach(id,r,u.userId()));} - @PostMapping(value="/{id}/attachments/upload",consumes="multipart/form-data") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:edit')") public ApiResponse upload(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id,@RequestParam(required=false) String attachmentType,@RequestPart("file") org.springframework.web.multipart.MultipartFile file){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.upload(id,attachmentType,file,u.userId()));} + @PostMapping(value="/{id}/attachments/upload",consumes="multipart/form-data") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:edit')") public ApiResponse upload(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id,@RequestParam @NotBlank @Pattern(regexp="BILL_OF_LADING|DISPATCH_ORDER|DRIVER_DOCUMENT|VEHICLE_DOCUMENT|OTHER") String attachmentType,@RequestPart("file") org.springframework.web.multipart.MultipartFile file){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.upload(id,attachmentType,file,u.userId()));} @DeleteMapping("/{id}/attachments/{attachmentId}") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:edit')") public ApiResponse deleteAttachment(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id,@PathVariable @Positive long attachmentId){FirstlegScopeGuard.platform(u);service.deleteAttachment(id,attachmentId);return ApiResponse.success(null);} @PostMapping @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:create')") @BusinessAudit(event="TRUCK_APPOINTMENT_CREATE",resourceType="TRUCK_APPOINTMENT",captureBefore=false) public ApiResponse create(@AuthenticationPrincipal AuthenticatedUser u,@Valid @RequestBody SaveRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.create(u.userId(),u.username(),r));} @PutMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:edit')") public ApiResponse edit(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable long id,@Valid @RequestBody SaveRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.edit(id,u.userId(),u.username(),r));} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/CartonMeasurementCommand.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/CartonMeasurementCommand.java index 7cc75ad55..dd3222420 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/CartonMeasurementCommand.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/CartonMeasurementCommand.java @@ -11,7 +11,6 @@ public record CartonMeasurementCommand( BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm, - BigDecimal unitWeightKg, String machineNo, LocalDateTime measuredAt, MultipartFile image, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/CartonMeasurementResult.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/CartonMeasurementResult.java index 34997f196..36c281d32 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/CartonMeasurementResult.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/CartonMeasurementResult.java @@ -14,7 +14,6 @@ public record CartonMeasurementResult( BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm, - BigDecimal unitWeightKg, Long imageFileId, String machineNo, LocalDateTime measuredAt, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerDtos.java index 61b40f304..8c21b3827 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerDtos.java @@ -100,9 +100,12 @@ public final class ContainerDtos { @NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm, @NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg) {} - @Schema(name = "ContainerOceanBookingLinkRequest", description = "将内部模拟柜原地合并为指定真实订舱柜") - public record OceanBookingLinkRequest(@NotNull @Positive Long realContainerBookingId, - @NotNull @PositiveOrZero Integer expectedVersion) {} + @Schema(name = "ContainerOceanBookingLinkRequest", description = "将现有模拟柜原地关联到已发布订舱箱型需求并补录真实柜号") + public record OceanBookingLinkRequest(@NotNull @Positive Long oceanBookingId, + @NotNull @Positive Long requirementId, @NotBlank @Size(max = 64) String containerNo, + @NotNull @PositiveOrZero Integer expectedVersion) { + @Deprecated public OceanBookingLinkRequest(Long legacyRealContainerBookingId, Integer expectedVersion) { this(null, null, null, expectedVersion); } + } @Schema(name = "ContainerOceanBookingUnlinkRequest", description = "解除空模拟柜的真实订舱关联") public record OceanBookingUnlinkRequest(@NotNull @PositiveOrZero Integer expectedVersion, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java index 2590623f5..260ac1924 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java @@ -44,7 +44,6 @@ public final class InboundMeasurementRequests { @NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal lengthCm, @NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal widthCm, @NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal heightCm, - @NotNull @DecimalMin("0.001") @Digits(integer = 15, fraction = 3) BigDecimal unitWeightKg, @Positive Long intelligentImageFileId, @Size(max = 500) String overLimitWarning, @DecimalMin("0.000") @Digits(integer = 9, fraction = 3) BigDecimal outerLengthCm, @@ -72,7 +71,6 @@ public final class InboundMeasurementRequests { @NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal length, @NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal width, @NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal height, - @DecimalMin("0.001") @Digits(integer = 15, fraction = 3) BigDecimal weight, @Size(max = 64) String scannedShipmentId, @NotBlank @Size(max = 64) String machineNo, @NotNull LocalDateTime measuredAt diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementResponses.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementResponses.java index 3c608af80..6ea5847c8 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementResponses.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementResponses.java @@ -59,9 +59,7 @@ public final class InboundMeasurementResponses { BigDecimal widthCm, BigDecimal heightCm, BigDecimal girthCm, - BigDecimal unitWeightKg, BigDecimal unitVolumeCbm, - BigDecimal totalWeightKg, BigDecimal totalVolumeCbm, BigDecimal volumeWeightKg, Long intelligentImageFileId, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/MeasurementBatchCommand.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/MeasurementBatchCommand.java index 3c3ed8dd1..653d4c315 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/MeasurementBatchCommand.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/MeasurementBatchCommand.java @@ -35,7 +35,6 @@ public record MeasurementBatchCommand( BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm, - BigDecimal unitWeightKg, Long intelligentImageFileId, String overLimitWarning, BigDecimal outerLengthCm, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/MerchantInboundBillingResponse.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/MerchantInboundBillingResponse.java index c0e3e2cca..d177903f4 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/MerchantInboundBillingResponse.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/MerchantInboundBillingResponse.java @@ -31,7 +31,6 @@ public record MerchantInboundBillingResponse(InboundOrderDetail inbound, public record MeasurementItem( Long id, String itemNo, String boxSpecification, Integer packageCount, BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm, - BigDecimal unitWeightKg, BigDecimal totalWeightKg, BigDecimal unitVolumeCbm, BigDecimal totalVolumeCbm, BigDecimal volumeWeightKg) { } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/MojiaMeasurementDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/MojiaMeasurementDtos.java index 96e61a581..a64dc7948 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/MojiaMeasurementDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/MojiaMeasurementDtos.java @@ -23,7 +23,6 @@ public final class MojiaMeasurementDtos { @NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal length, @NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal width, @NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal height, - @DecimalMin("0.001") @DecimalMax("999999999999999.999") BigDecimal weight, @NotBlank @Size(max = 64) String machineNo, LocalDateTime measuredAt, @NotBlank @Size(max = 7_100_000) String picture diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/OpenApiMeasurementDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/OpenApiMeasurementDtos.java index 249b89084..3b8277a63 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/OpenApiMeasurementDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/OpenApiMeasurementDtos.java @@ -47,7 +47,6 @@ public final class OpenApiMeasurementDtos { @NotNull @DecimalMin(value = "0", inclusive = false) @DecimalMax("999999999.999") BigDecimal widthCm, @NotNull @DecimalMin(value = "0", inclusive = false) @DecimalMax("999999999.999") BigDecimal heightCm, @NotNull @Min(1) @Max(1000000) Integer packageCount, - @DecimalMin(value = "0", inclusive = false) @DecimalMax("999999999.999") BigDecimal unitWeightKg, @NotBlank @Size(max = 64) String boxSpecification, @Size(max = 500) String remark) { } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java index 8d88fdc61..c42322654 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java @@ -3,17 +3,17 @@ import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import jakarta.validati public final class SmartLoadingDtos { private SmartLoadingDtos(){} public record Candidate(Long accountId, Long inboundId, Long inboundDetailId, Long orderId, String orderNo, String inboundCode, Long warehouseId, String channelCode, String channelName, Long destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName, String customerName, String goodsName, Integer availablePackageCount, - BigDecimal unitWeightKg, BigDecimal unitVolumeCbm) { + BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg, BigDecimal unitVolumeCbm) { public Candidate(Long accountId, Long inboundId, Long inboundDetailId, Long orderId, String orderNo, String inboundCode, Long warehouseId, String destinationWarehouseCode, String customerName, String goodsName, Integer availablePackageCount, - BigDecimal unitWeightKg, BigDecimal unitVolumeCbm) { - this(accountId,inboundId,inboundDetailId,orderId,orderNo,inboundCode,warehouseId,null,null,null,destinationWarehouseCode,null,customerName,goodsName,availablePackageCount,unitWeightKg,unitVolumeCbm); + BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg, BigDecimal unitVolumeCbm) { + this(accountId,inboundId,inboundDetailId,orderId,orderNo,inboundCode,warehouseId,null,null,null,destinationWarehouseCode,null,customerName,goodsName,availablePackageCount,unitGrossWeightKg,unitNetWeightKg,unitVolumeCbm); } public Candidate(Long accountId, Long inboundId, Long inboundDetailId, Long orderId, String orderNo, String inboundCode, Long warehouseId, Long destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName, - String customerName, String goodsName, Integer availablePackageCount, BigDecimal unitWeightKg, BigDecimal unitVolumeCbm) { + String customerName, String goodsName, Integer availablePackageCount, BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg, BigDecimal unitVolumeCbm) { this(accountId,inboundId,inboundDetailId,orderId,orderNo,inboundCode,warehouseId,null,null,destinationWarehouseId, - destinationWarehouseCode,destinationWarehouseName,customerName,goodsName,availablePackageCount,unitWeightKg,unitVolumeCbm); + destinationWarehouseCode,destinationWarehouseName,customerName,goodsName,availablePackageCount,unitGrossWeightKg,unitNetWeightKg,unitVolumeCbm); } } public record CreateRequest(@NotNull @Positive Long bookingId, @Size(max=500) String remark) {} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/TruckAppointmentDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/TruckAppointmentDtos.java index 6c61dabc5..a0a6cef2d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/TruckAppointmentDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/TruckAppointmentDtos.java @@ -2,7 +2,7 @@ package com.baiyue.firstleg.dto; import jakarta.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDateTime; public final class TruckAppointmentDtos { private TruckAppointmentDtos(){} public record Query(@Size(max=64) String keyword, com.baiyue.firstleg.domain.TruckAppointmentStatus status, @Positive Long customerId, @Positive Long containerBookingId, @Min(1) long page, @Min(1) @Max(200) long size){ public Query{if(page==0)page=1;if(size==0)size=20;} } - public record SaveRequest(@Positive Long outboundOrderId,@Positive Long oceanBookingId,@Positive Long containerBookingId,@Positive Long loadingPlanId,@Positive Long warehouseId,@NotNull @Positive Long customerId,@Size(max=128) String customerName,@NotNull @Positive Long carrierId,@Size(max=128) String carrierName,@Size(max=64) String fleetContact,@Size(max=32) String fleetPhone,@Size(max=64) String driverName,@Size(max=32) String driverPhone,@Size(max=32) String driverIdCard,@Size(max=32) String vehicleNo,@Size(max=128) String storeName,@Size(max=64) String storeContact,@Size(max=32) String storePhone,@Size(max=255) String storeAddress,@Size(max=64) String storeCity,@Size(max=16) String storeZipcode,@PositiveOrZero Integer packageCount,@DecimalMin("0") BigDecimal grossWeightKg,@DecimalMin("0") BigDecimal volumeCbm,@PositiveOrZero Integer palletCount,@Size(max=255) String poNo,@NotNull LocalDateTime plannedLoadingAt,@PositiveOrZero Integer transitMinutes,@DecimalMin("0") BigDecimal payableAmount,@DecimalMin("0") BigDecimal receivableAmount,@Size(max=8) String currency,@Size(max=1000) String remark,@NotNull @PositiveOrZero Integer expectedVersion){} + public record SaveRequest(@Positive Long outboundOrderId,@Positive Long oceanBookingId,@Positive Long containerBookingId,@Positive Long loadingPlanId,@Positive Long warehouseId,@NotNull @Positive Long customerId,@Size(max=128) String customerName,@NotNull @Positive Long carrierId,@Size(max=128) String carrierName,@Size(max=128) String billOfLadingNo,@Size(max=64) String fleetContact,@Size(max=32) String fleetPhone,@Size(max=64) String driverName,@Size(max=32) String driverPhone,@Size(max=32) String driverIdCard,@Size(max=32) String vehicleNo,@Size(max=128) String storeName,@Size(max=64) String storeContact,@Size(max=32) String storePhone,@Size(max=255) String storeAddress,@Size(max=64) String storeCity,@Size(max=16) String storeZipcode,@PositiveOrZero Integer packageCount,@DecimalMin("0") BigDecimal grossWeightKg,@DecimalMin("0") BigDecimal volumeCbm,@PositiveOrZero Integer palletCount,@Size(max=255) String poNo,@NotNull LocalDateTime plannedLoadingAt,@PositiveOrZero Integer transitMinutes,@DecimalMin("0") BigDecimal payableAmount,@DecimalMin("0") BigDecimal receivableAmount,@Size(max=8) String currency,@Size(max=1000) String remark,@NotNull @PositiveOrZero Integer expectedVersion){} public record ActionRequest(@NotNull @PositiveOrZero Integer expectedVersion,@Size(max=500) String reason){} - public record AttachmentRequest(@NotNull @Positive Long fileId,@Size(max=64) String attachmentType,@Size(max=255) String fileName,@Size(max=128) String fileHash){ } + public record AttachmentRequest(@NotNull @Positive Long fileId,@NotBlank @Pattern(regexp="BILL_OF_LADING|DISPATCH_ORDER|DRIVER_DOCUMENT|VEHICLE_DOCUMENT|OTHER") String attachmentType,@Size(max=255) String fileName,@Size(max=128) String fileHash){ } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/entity/InboundMeasurementItemEntity.java b/firstleg/src/main/java/com/baiyue/firstleg/entity/InboundMeasurementItemEntity.java index 5cd87dcf4..c012a9f85 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/entity/InboundMeasurementItemEntity.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/entity/InboundMeasurementItemEntity.java @@ -29,11 +29,9 @@ public class InboundMeasurementItemEntity { private BigDecimal widthCm; private BigDecimal heightCm; private BigDecimal girthCm; - private BigDecimal unitWeightKg; private BigDecimal unitVolumeCbm; private BigDecimal volumeRawCbm; private BigDecimal volumeCbm; - private BigDecimal totalWeightKg; private BigDecimal totalVolumeCbm; private BigDecimal volumeWeightKg; private String calculationRuleVersion; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/entity/MojiaMeasurementReceiptEntity.java b/firstleg/src/main/java/com/baiyue/firstleg/entity/MojiaMeasurementReceiptEntity.java index cae614d9c..ff341cc93 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/entity/MojiaMeasurementReceiptEntity.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/entity/MojiaMeasurementReceiptEntity.java @@ -23,7 +23,6 @@ public class MojiaMeasurementReceiptEntity { private BigDecimal lengthCm; private BigDecimal widthCm; private BigDecimal heightCm; - private BigDecimal weightKg; private String machineNo; private LocalDateTime measuredAt; private Long imageFileId; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/entity/TruckAppointmentEntity.java b/firstleg/src/main/java/com/baiyue/firstleg/entity/TruckAppointmentEntity.java index 723d8452b..6f845f881 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/entity/TruckAppointmentEntity.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/entity/TruckAppointmentEntity.java @@ -23,6 +23,7 @@ public class TruckAppointmentEntity { private String customerName; private Long carrierId; private String carrierName; + private String billOfLadingNo; private String fleetContact; private String fleetPhone; private String driverName; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java index 32a22d9e3..2d9ca082c 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java @@ -11,6 +11,8 @@ import org.apache.ibatis.annotations.Update; @Mapper public interface ContainerBookingMapper extends BaseMapper { + @Update("UPDATE fl_container_booking SET container_no=#{containerNo},ocean_booking_id=#{oceanBookingId},ocean_booking_requirement_id=#{requirementId},record_status='ACTIVE',bill_of_lading_no=#{billOfLadingNo},vessel_name=#{vesselName},voyage_no=#{voyageNo},shipping_company_id=#{shippingCompanyId},shipping_company_name=#{shippingCompanyName},departure_port_code=#{departurePortCode},departure_port_name=#{departurePortName},destination_port_code=#{destinationPortCode},destination_port_name=#{destinationPortName},channel_code=#{channelCode},channel_name=#{channelName},customer_id=#{customerId},customer_name=#{customerName},estimated_sailing_at=#{estimatedSailingAt},estimated_port_arrival_at=#{estimatedArrivalAt},updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=NOW(),version=version+1 WHERE id=#{id} AND version=#{version} AND source_type='SIMULATION' AND record_status='ACTIVE' AND ocean_booking_id IS NULL") + int linkReleasedBooking(@Param("id") long id, @Param("oceanBookingId") long oceanBookingId, @Param("requirementId") long requirementId, @Param("containerNo") String containerNo, @Param("billOfLadingNo") String billOfLadingNo, @Param("vesselName") String vesselName, @Param("voyageNo") String voyageNo, @Param("shippingCompanyId") Long shippingCompanyId, @Param("shippingCompanyName") String shippingCompanyName, @Param("departurePortCode") String departurePortCode, @Param("departurePortName") String departurePortName, @Param("destinationPortCode") String destinationPortCode, @Param("destinationPortName") String destinationPortName, @Param("channelCode") String channelCode, @Param("channelName") String channelName, @Param("customerId") Long customerId, @Param("customerName") String customerName, @Param("estimatedSailingAt") java.time.LocalDateTime estimatedSailingAt, @Param("estimatedPortArrivalAt") java.time.LocalDateTime estimatedPortArrivalAt, @Param("version") int version, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName); @Select(""" ") Page page(Page p,@Param("q") Query q); @Update("UPDATE fl_truck_appointment SET status=#{toStatus}, actual_arrived_warehouse_at=CASE WHEN #{toStatus}='ARRIVED_WAREHOUSE' THEN CURRENT_TIMESTAMP(3) ELSE actual_arrived_warehouse_at END, actual_loading_at=CASE WHEN #{toStatus}='LOADING' THEN CURRENT_TIMESTAMP(3) ELSE actual_loading_at END, actual_departed_at=CASE WHEN #{toStatus}='DEPARTED' THEN CURRENT_TIMESTAMP(3) ELSE actual_departed_at END, revision=revision+1, version=version+1, updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND status=#{fromStatus} AND version=#{version}") int transition(@Param("id") long id,@Param("fromStatus") String from,@Param("toStatus") String to,@Param("version") int version); + @Update("UPDATE fl_truck_appointment SET status='DISPATCHED',bill_of_lading_no=#{billOfLadingNo},revision=revision+1,version=version+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND status=#{fromStatus} AND version=#{version}") int dispatch(@Param("id") long id,@Param("fromStatus") String fromStatus,@Param("billOfLadingNo") String billOfLadingNo,@Param("version") int version); @Update("UPDATE fl_truck_appointment SET status='COMPLETED',actual_loading_at=COALESCE(actual_loading_at,CURRENT_TIMESTAMP(3)),revision=revision+1,version=version+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND status=#{fromStatus} AND version=#{version}") int completeLoadingFromPda(@Param("id") long id,@Param("fromStatus") String fromStatus,@Param("version") int version); @Update("UPDATE fl_truck_appointment SET planned_loading_at=#{at}, revision=revision+1, version=version+1, status='RESCHEDULED', remark=#{reason}, updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND version=#{version}") int reschedule(@Param("id") long id,@Param("at") java.time.LocalDateTime at,@Param("reason") String reason,@Param("version") int version); - @Update("UPDATE fl_truck_appointment SET outbound_order_id=#{e.outboundOrderId},ocean_booking_id=#{e.oceanBookingId},container_booking_id=#{e.containerBookingId},loading_plan_id=#{e.loadingPlanId},warehouse_id=#{e.warehouseId},customer_id=#{e.customerId},customer_name=#{e.customerName},carrier_id=#{e.carrierId},carrier_name=#{e.carrierName},fleet_contact=#{e.fleetContact},fleet_phone=#{e.fleetPhone},driver_name=#{e.driverName},driver_phone=#{e.driverPhone},driver_id_card=#{e.driverIdCard},vehicle_no=#{e.vehicleNo},store_name=#{e.storeName},store_contact=#{e.storeContact},store_phone=#{e.storePhone},store_address=#{e.storeAddress},store_city=#{e.storeCity},store_zipcode=#{e.storeZipcode},package_count=#{e.packageCount},gross_weight_kg=#{e.grossWeightKg},volume_cbm=#{e.volumeCbm},pallet_count=#{e.palletCount},po_no=#{e.poNo},planned_loading_at=#{e.plannedLoadingAt},transit_minutes=#{e.transitMinutes},payable_amount=#{e.payableAmount},receivable_amount=#{e.receivableAmount},currency=#{e.currency},remark=#{e.remark},version=version+1,revision=revision+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{e.id} AND version=#{version}") int updateEdit(@Param("e") TruckAppointmentEntity e,@Param("version") int version); + @Update("UPDATE fl_truck_appointment SET outbound_order_id=#{e.outboundOrderId},ocean_booking_id=#{e.oceanBookingId},container_booking_id=#{e.containerBookingId},loading_plan_id=#{e.loadingPlanId},warehouse_id=#{e.warehouseId},customer_id=#{e.customerId},customer_name=#{e.customerName},carrier_id=#{e.carrierId},carrier_name=#{e.carrierName},bill_of_lading_no=#{e.billOfLadingNo},fleet_contact=#{e.fleetContact},fleet_phone=#{e.fleetPhone},driver_name=#{e.driverName},driver_phone=#{e.driverPhone},driver_id_card=#{e.driverIdCard},vehicle_no=#{e.vehicleNo},store_name=#{e.storeName},store_contact=#{e.storeContact},store_phone=#{e.storePhone},store_address=#{e.storeAddress},store_city=#{e.storeCity},store_zipcode=#{e.storeZipcode},package_count=#{e.packageCount},gross_weight_kg=#{e.grossWeightKg},volume_cbm=#{e.volumeCbm},pallet_count=#{e.palletCount},po_no=#{e.poNo},planned_loading_at=#{e.plannedLoadingAt},transit_minutes=#{e.transitMinutes},payable_amount=#{e.payableAmount},receivable_amount=#{e.receivableAmount},currency=#{e.currency},remark=#{e.remark},version=version+1,revision=revision+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{e.id} AND version=#{version}") int updateEdit(@Param("e") TruckAppointmentEntity e,@Param("version") int version); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/controller/PlatformOceanBookingController.java b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/controller/PlatformOceanBookingController.java index 5acefdc3b..a6ed9d70d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/controller/PlatformOceanBookingController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/controller/PlatformOceanBookingController.java @@ -36,19 +36,16 @@ public class PlatformOceanBookingController { public ApiResponse detail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.detail(oceanBookingId)); } @PostMapping @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:create')") - @BusinessAudit(event="OCEAN_BOOKING_CREATE", resourceType="OCEAN_BOOKING", description="登记已确认海运订舱", captureBefore=false) + @BusinessAudit(event="OCEAN_BOOKING_CREATE", resourceType="OCEAN_BOOKING", description="创建海运订舱草稿", captureBefore=false) public ApiResponse create(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CreateRequest request) { validateOptionalCustomer(user, request.data().customerId()); return ApiResponse.success(service.create(user.userId(), user.username(), key, request)); } + @PutMapping("/{oceanBookingId}") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:edit')") + @BusinessAudit(event="OCEAN_BOOKING_UPDATE_DRAFT", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="编辑海运订舱草稿") + public ApiResponse updateDraft(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody UpdateRequest request) { validateOptionalCustomer(user, request.data().customerId()); return ApiResponse.success(service.updateDraft(oceanBookingId, user.userId(), user.username(), key, request)); } + @DeleteMapping("/{oceanBookingId}") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:delete')") + @BusinessAudit(event="OCEAN_BOOKING_DELETE_DRAFT", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="删除海运订舱草稿") + public ApiResponse deleteDraft(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); service.deleteDraft(oceanBookingId, user.userId(), user.username(), key, request); return ApiResponse.success(null); } @PostMapping("/{oceanBookingId}/release") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:release')") @BusinessAudit(event="OCEAN_BOOKING_RELEASE", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="确认放舱") public ApiResponse release(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ReleaseRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.release(oceanBookingId, user.userId(), user.username(), key, request)); } - @PostMapping("/{oceanBookingId}/cancel") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:cancel')") - @BusinessAudit(event="OCEAN_BOOKING_CANCEL", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="取消海运订舱") - public ApiResponse cancel(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.cancel(oceanBookingId, user.userId(), user.username(), key, request)); } - @PostMapping("/{oceanBookingId}/change-request") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:change')") - @BusinessAudit(event="OCEAN_BOOKING_CHANGE_REQUEST", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="申请变更船期") - public ApiResponse requestChange(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ChangeRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.requestChange(oceanBookingId, user.userId(), user.username(), key, request)); } - @PostMapping("/{oceanBookingId}/change-confirm") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:change')") - @BusinessAudit(event="OCEAN_BOOKING_CHANGE_CONFIRM", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="确认变更船期") - public ApiResponse confirmChange(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.confirmChange(oceanBookingId, user.userId(), user.username(), key, request)); } private static void validateOptionalCustomer(AuthenticatedUser user, Long customerId) { FirstlegScopeGuard.platform(user); if (customerId != null) FirstlegScopeGuard.platform(user, customerId); } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/domain/OceanBookingStatus.java b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/domain/OceanBookingStatus.java index 24aabee24..0e916021f 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/domain/OceanBookingStatus.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/domain/OceanBookingStatus.java @@ -2,8 +2,5 @@ package com.baiyue.firstleg.oceanbooking.domain; public enum OceanBookingStatus { DRAFT, - CONFIRMED, - RELEASED, - CHANGE_PENDING, - CANCELLED + RELEASED } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/dto/OceanBookingDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/dto/OceanBookingDtos.java index 9feb5df0d..9b663efd4 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/dto/OceanBookingDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/dto/OceanBookingDtos.java @@ -2,6 +2,7 @@ package com.baiyue.firstleg.oceanbooking.dto; import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity; import com.baiyue.firstleg.oceanbooking.entity.OceanBookingPartyEntity; +import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity; import com.baiyue.firstleg.oceanbooking.entity.OceanBookingStatusHistoryEntity; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; @@ -31,53 +32,67 @@ public final class OceanBookingDtos { @Size(max = 128) String contactName, @Size(max = 64) String contactPhone) {} - public record ContainerRequest(@NotBlank @Size(max = 64) String containerNo, - @NotBlank @Size(max = 32) String containerType, + public record ContainerRequirementRequest(@NotBlank @Size(max = 32) String containerType, @NotBlank @Size(max = 32) String containerKind, + @NotNull @Min(1) @Max(100) Integer quantity, @NotNull @jakarta.validation.constraints.DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm, @NotNull @jakarta.validation.constraints.DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg) {} + @Deprecated + public record ContainerRequest(String containerNo, String containerType, String containerKind, + BigDecimal capacityCbm, BigDecimal maxWeightKg) {} public record DraftData(@Positive Long customerId, @Size(max = 128) String customerName, - @NotBlank @Size(max = 64) String channelCode, - @NotNull @Positive Long warehouseId, - @NotBlank @Size(max = 128) String billOfLadingNo, + @Size(max = 64) String channelCode, + @Positive Long warehouseId, + @Size(max = 128) String billOfLadingNo, @Positive Long shippingCompanyId, @Size(max = 128) String shippingCompanyName, @Positive Long bookingAgentId, @Size(max = 128) String bookingAgentName, - @NotBlank @Size(max = 128) String vesselName, - @NotBlank @Size(max = 64) String voyageNo, + @Size(max = 128) String vesselName, + @Size(max = 64) String voyageNo, @Size(max = 32) String departurePortCode, @Size(max = 128) String departurePortName, @Size(max = 32) String destinationPortCode, @Size(max = 128) String destinationPortName, @Size(max = 128) String placeOfDelivery, - @NotBlank @Size(max = 32) String serviceTerm, - @NotBlank @Size(max = 32) String freightTerm, - @NotNull LocalDateTime estimatedSailingAt, - @NotNull LocalDateTime estimatedArrivalAt, + @Size(max = 32) String serviceTerm, + @Size(max = 32) String freightTerm, + LocalDateTime estimatedSailingAt, + LocalDateTime estimatedArrivalAt, @Size(max = 1000) String remark, - @Valid @NotEmpty @Size(max = 3) List parties, - @Valid @NotEmpty @Size(max = 20) List containers) {} + @Valid @Size(max = 3) List parties, + @Valid @Size(max = 20) List containerRequirements) { + public DraftData(Long customerId, String customerName, String channelCode, Long warehouseId, String billOfLadingNo, + Long shippingCompanyId, String shippingCompanyName, Long bookingAgentId, String bookingAgentName, + String vesselName, String voyageNo, String departurePortCode, String departurePortName, + String destinationPortCode, String destinationPortName, String placeOfDelivery, String serviceTerm, + String freightTerm, LocalDateTime estimatedSailingAt, LocalDateTime estimatedArrivalAt, String remark, + List parties, java.util.Collection legacyContainers) { + this(customerId, customerName, channelCode, warehouseId, billOfLadingNo, shippingCompanyId, shippingCompanyName, + bookingAgentId, bookingAgentName, vesselName, voyageNo, departurePortCode, departurePortName, destinationPortCode, + destinationPortName, placeOfDelivery, serviceTerm, freightTerm, estimatedSailingAt, estimatedArrivalAt, remark, + parties, legacyContainers == null ? List.of() : legacyContainers.stream().map(v -> v instanceof ContainerRequirementRequest x ? x : v instanceof ContainerRequest x ? new ContainerRequirementRequest(x.containerType(), x.containerKind(), 1, x.capacityCbm(), x.maxWeightKg()) : null).filter(java.util.Objects::nonNull).toList()); + } + public List containers() { return containerRequirements.stream().map(v -> new ContainerRequest(null, v.containerType(), v.containerKind(), v.capacityCbm(), v.maxWeightKg())).toList(); } + } public record CreateRequest(@NotNull @Min(0) Integer expectedVersion, @Valid @NotNull DraftData data) {} + public record UpdateRequest(@NotNull @Min(0) Integer expectedVersion, + @Valid @NotNull DraftData data) {} public record ActionRequest(@NotNull @Min(0) Integer expectedVersion, @Size(max = 1000) String reason) {} public record ReleaseRequest(@NotNull @Min(0) Integer expectedVersion, - @NotBlank @Size(max = 128) String billOfLadingNo, - @NotBlank @Size(max = 128) String vesselName, - @NotBlank @Size(max = 64) String voyageNo, - @NotNull LocalDateTime estimatedSailingAt, - @NotNull LocalDateTime estimatedArrivalAt, - @Size(max = 1000) String reason) {} - public record ChangeRequest(@NotNull @Min(0) Integer expectedVersion, - @NotBlank @Size(max = 128) String vesselName, - @NotBlank @Size(max = 64) String voyageNo, - LocalDateTime estimatedSailingAt, - LocalDateTime estimatedArrivalAt, - @NotBlank @Size(max = 1000) String reason) {} + @Size(max = 1000) String reason) { + public ReleaseRequest(int expectedVersion, String bill, String vessel, String voyage, LocalDateTime etd, LocalDateTime eta, String reason) { + this(expectedVersion, reason); + } + } + @Deprecated + public record ChangeRequest(Integer expectedVersion, String vesselName, String voyageNo, + LocalDateTime estimatedSailingAt, LocalDateTime estimatedArrivalAt, String reason) {} public record RealContainer(Long containerBookingId, String cabinetNo, String containerNo, Long requirementId, Long allocationId, String containerType, String containerKind, BigDecimal capacityCbm, BigDecimal maxWeightKg, @@ -86,6 +101,7 @@ public final class OceanBookingDtos { public record Detail(OceanBookingEntity oceanBooking, List parties, + List containerRequirements, List containers, List histories) {} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingMapper.java index e230a427a..2c8a63e2a 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingMapper.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.Query; import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; @@ -13,6 +14,10 @@ import java.time.LocalDateTime; @Mapper public interface OceanBookingMapper extends BaseMapper { + @Update("UPDATE fl_ocean_booking SET customer_id=#{value.customerId},customer_name=#{value.customerName},channel_code=#{value.channelCode},channel_name=#{value.channelName},warehouse_id=#{value.warehouseId},warehouse_name=#{value.warehouseName},bill_of_lading_no=#{value.billOfLadingNo},shipping_company_id=#{value.shippingCompanyId},shipping_company_name=#{value.shippingCompanyName},booking_agent_id=#{value.bookingAgentId},booking_agent_name=#{value.bookingAgentName},vessel_name=#{value.vesselName},voyage_no=#{value.voyageNo},departure_port_code=#{value.departurePortCode},departure_port_name=#{value.departurePortName},destination_port_code=#{value.destinationPortCode},destination_port_name=#{value.destinationPortName},place_of_delivery=#{value.placeOfDelivery},service_term=#{value.serviceTerm},freight_term=#{value.freightTerm},estimated_sailing_at=#{value.estimatedSailingAt},estimated_arrival_at=#{value.estimatedArrivalAt},remark=#{value.remark},updated_by=#{operatorId},updated_by_name=#{operatorName},version=version+1,updated_at=NOW() WHERE id=#{id} AND status='DRAFT' AND version=#{version}") + int updateDraft(@Param("id") long id, @Param("value") OceanBookingEntity value, @Param("version") int version, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName); + @Delete("DELETE FROM fl_ocean_booking WHERE id=#{id} AND status='DRAFT' AND version=#{version}") + int deleteDraft(@Param("id") long id, @Param("version") int version); @Select("""