From e0af45db024a41549daeaa01122954ea02775e7e Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 14 Sep 2026 16:53:09 +0800 Subject: [PATCH] feat: add editable order collaboration remarks --- deploy/sql/头程数据库统一增量-MySQL8.sql | 52 ++++++++++++- .../firstleg/client/OrderCargoClient.java | 7 +- .../PlatformSmartLoadingController.java | 4 +- .../baiyue/firstleg/dto/LoadingListDtos.java | 1 + .../baiyue/firstleg/dto/SmartLoadingDtos.java | 3 +- .../entity/LoadingPlanItemEntity.java | 2 +- .../mapper/LoadingListQueryMapper.java | 3 +- .../mapper/LoadingPlanItemMapper.java | 10 ++- .../repository/LoadingListRepository.java | 2 +- .../repository/SmartLoadingRepository.java | 9 ++- .../firstleg/service/SmartLoadingService.java | 61 +++++++++------ .../db/phase3-smart-loading-schema.sql | 18 ++++- .../main/resources/openapi/firstleg-v1.yaml | 18 +++-- .../resources/openapi/loading-lists-v1.yaml | 1 + ...ormSmartLoadingControllerContractTest.java | 8 +- .../SmartLoadingSqlContractTest.java | 16 ++-- .../service/SmartLoadingServiceTest.java | 74 ++++++++++--------- .../InternalOrderCargoController.java | 14 ++-- .../PlatformFirstLegOrderController.java | 17 ----- .../PlatformUnifiedOrderController.java | 12 +-- .../com/baiyue/order/dto/OrderRequests.java | 17 +++-- .../order/dto/UnifiedOrderResponse.java | 2 +- .../com/baiyue/order/entity/OrderEntity.java | 3 + .../order/repository/OrderRepository.java | 15 ++++ .../service/OrderApplicationService.java | 28 +++++-- .../service/UnifiedOrderQueryService.java | 21 ++---- order/src/main/resources/db/order.sql | 30 ++++++++ .../src/main/resources/openapi/order-v1.yaml | 17 +++-- .../InternalOrderCargoControllerTest.java | 8 +- .../OrderControllerPermissionTest.java | 6 +- .../OrderApplicationServiceStateTest.java | 42 +++++++++++ .../service/UnifiedOrderQueryServiceTest.java | 10 +-- 32 files changed, 356 insertions(+), 175 deletions(-) diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 978490ac5..f4b6d8209 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -579,6 +579,9 @@ CREATE TABLE IF NOT EXISTS ord_order ( expected_arrival_at DATETIME(3) NULL COMMENT '客户申报预计到货时间', estimated_volume_cbm DECIMAL(18,4) NULL COMMENT '平台运营预估体积(立方米),独立于客户下单及实测体积', customer_remark VARCHAR(1000) NULL COMMENT '客户可见备注', + internal_remark VARCHAR(1000) NULL COMMENT '平台内部备注', + warehouse_remark VARCHAR(1000) NULL COMMENT '仓库备注', + customer_service_remark VARCHAR(1000) NULL COMMENT '客服备注', pickup_method VARCHAR(24) NULL COMMENT '取货方式', requested_pickup_at DATETIME(3) NULL COMMENT '客户要求平台上门提货的预约时间', receiving_warehouse_id BIGINT UNSIGNED NULL COMMENT '国内收货仓逻辑ID', @@ -618,6 +621,33 @@ CREATE TABLE IF NOT EXISTS ord_order ( KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id) ) ENGINE=InnoDB COMMENT='头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态'; +SET @order_internal_remark_sql = IF( + EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_order' AND table_name='ord_order' AND column_name='internal_remark'), + 'SELECT 1', + 'ALTER TABLE baiyue_order.ord_order ADD COLUMN internal_remark VARCHAR(1000) NULL COMMENT ''平台内部备注'' AFTER customer_remark' +); +PREPARE order_internal_remark_stmt FROM @order_internal_remark_sql; +EXECUTE order_internal_remark_stmt; +DEALLOCATE PREPARE order_internal_remark_stmt; + +SET @order_warehouse_remark_sql = IF( + EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_order' AND table_name='ord_order' AND column_name='warehouse_remark'), + 'SELECT 1', + 'ALTER TABLE baiyue_order.ord_order ADD COLUMN warehouse_remark VARCHAR(1000) NULL COMMENT ''仓库备注'' AFTER internal_remark' +); +PREPARE order_warehouse_remark_stmt FROM @order_warehouse_remark_sql; +EXECUTE order_warehouse_remark_stmt; +DEALLOCATE PREPARE order_warehouse_remark_stmt; + +SET @order_customer_service_remark_sql = IF( + EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_order' AND table_name='ord_order' AND column_name='customer_service_remark'), + 'SELECT 1', + 'ALTER TABLE baiyue_order.ord_order ADD COLUMN customer_service_remark VARCHAR(1000) NULL COMMENT ''客服备注'' AFTER warehouse_remark' +); +PREPARE order_customer_service_remark_stmt FROM @order_customer_service_remark_sql; +EXECUTE order_customer_service_remark_stmt; +DEALLOCATE PREPARE order_customer_service_remark_stmt; + SET @order_fulfillment_type_sql = IF( EXISTS( SELECT 1 FROM information_schema.columns @@ -5413,6 +5443,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item ( internal_remark VARCHAR(1000) NULL COMMENT '平台内部备注', customer_remark VARCHAR(1000) NULL COMMENT '客户备注快照', warehouse_remark VARCHAR(1000) NULL COMMENT '仓库备注快照', + customer_service_remark VARCHAR(1000) NULL COMMENT '客服备注快照', frozen_at DATETIME(3) NULL COMMENT '装柜完成后的永久冻结时间', frozen_by BIGINT UNSIGNED NULL COMMENT '冻结操作人ID', removed_by BIGINT UNSIGNED NULL COMMENT '推送前逻辑删除操作人ID', @@ -5452,6 +5483,15 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item ( CONSTRAINT chk_loading_item_deleted CHECK (deleted IN (0, 1)) ) ENGINE=InnoDB COMMENT='智能装柜客户货物明细;客户归属仅在明细层'; +SET @loading_item_customer_service_remark_sql = IF( + EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_plan_item' AND column_name='customer_service_remark'), + 'SELECT 1', + 'ALTER TABLE baiyue_firstleg.fl_loading_plan_item ADD COLUMN customer_service_remark VARCHAR(1000) NULL COMMENT ''客服备注快照'' AFTER warehouse_remark' +); +PREPARE loading_item_customer_service_remark_stmt FROM @loading_item_customer_service_remark_sql; +EXECUTE loading_item_customer_service_remark_stmt; +DEALLOCATE PREPARE loading_item_customer_service_remark_stmt; + CREATE TABLE IF NOT EXISTS fl_loading_pallet_label ( id BIGINT UNSIGNED NOT NULL COMMENT '散货托盘标签ID(雪花ID)', loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID', @@ -6154,7 +6194,9 @@ BEGIN IF current_plan_status = 'LOADING' AND EXISTS ( SELECT 1 FROM fl_loading_pick_item WHERE loading_plan_item_id = OLD.id AND picked_package_count > 0) THEN - IF NOT (NEW.warehouse_remark <=> OLD.warehouse_remark) THEN + IF NOT (NEW.internal_remark <=> OLD.internal_remark) + OR NOT (NEW.warehouse_remark <=> OLD.warehouse_remark) + OR NOT (NEW.customer_service_remark <=> OLD.customer_service_remark) THEN IF NEW.package_count <> OLD.package_count OR NEW.deleted <> OLD.deleted OR NOT (NEW.customer_name <=> OLD.customer_name) OR NOT (NEW.inbound_id <=> OLD.inbound_id) @@ -6182,14 +6224,13 @@ BEGIN OR NOT (NEW.shipping_mark <=> OLD.shipping_mark) OR NOT (NEW.load_sequence <=> OLD.load_sequence) OR NOT (NEW.position_snapshot <=> OLD.position_snapshot) - OR NOT (NEW.internal_remark <=> OLD.internal_remark) OR NOT (NEW.customer_remark <=> OLD.customer_remark) OR NOT (NEW.frozen_at <=> OLD.frozen_at) OR NOT (NEW.frozen_by <=> OLD.frozen_by) OR NOT (NEW.removed_by <=> OLD.removed_by) OR NOT (NEW.removed_by_name <=> OLD.removed_by_name) OR NOT (NEW.removed_at <=> OLD.removed_at) THEN - SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '已有PDA拣货记录的装柜明细修改仓库备注时不得变更其他业务字段'; + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '已有PDA拣货记录的装柜明细修改备注时不得变更其他业务字段'; END IF; ELSEIF NOT EXISTS ( SELECT 1 FROM fl_loading_pick_item @@ -6224,7 +6265,6 @@ BEGIN OR NOT (NEW.shipping_mark <=> OLD.shipping_mark) OR NOT (NEW.load_sequence <=> OLD.load_sequence) OR NOT (NEW.position_snapshot <=> OLD.position_snapshot) - OR NOT (NEW.internal_remark <=> OLD.internal_remark) OR NOT (NEW.customer_remark <=> OLD.customer_remark) OR NOT (NEW.frozen_at <=> OLD.frozen_at) OR NOT (NEW.frozen_by <=> OLD.frozen_by) @@ -14791,6 +14831,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'updated_by CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'version', '乐观锁版本号'); CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'volume_cbm', '本次装柜体积m3'); CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'warehouse_remark', '仓库备注快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'customer_service_remark', '客服备注快照'); CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'allow_rejected_config', '操作时Nacos异常放行开关值'); CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'exception_release', '是否异常放行'); CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'id', '测量审核记录ID'); @@ -15359,6 +15400,9 @@ CALL queue_column_comment('baiyue_order', 'ord_order', 'created_by', '创建人I CALL queue_column_comment('baiyue_order', 'ord_order', 'customer_id', '客户逻辑ID'); CALL queue_column_comment('baiyue_order', 'ord_order', 'customer_name', '下单时客户名称快照'); CALL queue_column_comment('baiyue_order', 'ord_order', 'customer_remark', '客户可见备注'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'internal_remark', '平台内部备注'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'warehouse_remark', '仓库备注'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'customer_service_remark', '客服备注'); CALL queue_column_comment('baiyue_order', 'ord_order', 'customs_inspection', '是否需要商检:0否、1是'); CALL queue_column_comment('baiyue_order', 'ord_order', 'deleted', '逻辑删除标记'); CALL queue_column_comment('baiyue_order', 'ord_order', 'destination_country_code', '目的国家二字码'); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java b/firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java index 45387e73e..7c9c3405f 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java @@ -44,12 +44,13 @@ public interface OrderCargoClient { Integer originalPackageCount, BigDecimal originalGrossWeightKg, BigDecimal originalVolumeCbm, String skuNo, String cargoNameEn, Integer unitsPerPackage, Integer totalQuantity, BigDecimal netWeightKg, - String cargoRemark, String customerRemark, String latestInternalNote) { + String cargoRemark, String customerRemark, String latestInternalNote, + String warehouseRemark, String customerServiceRemark) { public CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName, Integer originalPackageCount, BigDecimal originalGrossWeightKg, BigDecimal originalVolumeCbm) { this(cargoId, orderId, itemNo, cargoName, originalPackageCount, - originalGrossWeightKg, originalVolumeCbm, null, null, null, null, null, null, null, null); + originalGrossWeightKg, originalVolumeCbm, null, null, null, null, null, null, null, null, null, null); } public CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName, Integer originalPackageCount, BigDecimal originalGrossWeightKg, @@ -58,7 +59,7 @@ public interface OrderCargoClient { String cargoRemark) { this(cargoId, orderId, itemNo, cargoName, originalPackageCount, originalGrossWeightKg, originalVolumeCbm, skuNo, cargoNameEn, unitsPerPackage, totalQuantity, netWeightKg, - cargoRemark, null, null); + cargoRemark, null, null, null, null); } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformSmartLoadingController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformSmartLoadingController.java index b1cf08c53..09c04e9ff 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformSmartLoadingController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformSmartLoadingController.java @@ -38,8 +38,8 @@ public class PlatformSmartLoadingController { public ApiResponse recommend(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody RecommendRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.recommend(planId,r,u.userId(),u.username()));} @PatchMapping("/plans/{planId}/items/{itemId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')") public ApiResponse adjust(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long itemId,@Valid @RequestBody AdjustRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.adjust(planId,itemId,r,u.userId(),u.username()));} - @PatchMapping("/plans/{planId}/items/{itemId}/warehouse-remark") @PreAuthorize("hasAuthority('platform:firstleg:preload:edit')") - public ApiResponse warehouseRemark(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long itemId,@Valid @RequestBody WarehouseRemarkRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.updateWarehouseRemark(planId,itemId,r,u.userId(),u.username()));} + @PatchMapping("/plans/{planId}/items/{itemId}/remarks") @PreAuthorize("hasAuthority('platform:firstleg:preload:edit')") + public ApiResponse remarks(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long itemId,@Valid @RequestBody RemarksRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.updateRemarks(planId,itemId,r,u.userId(),u.username()));} @DeleteMapping("/plans/{planId}/items/{itemId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:delete')") public ApiResponse delete(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long itemId,@Valid @RequestBody DeleteRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.delete(planId,itemId,r,u.userId(),u.username()));} @DeleteMapping("/plans/{planId}/unpicked-items") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:delete')") diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java index c7611d355..3b270601c 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java @@ -240,6 +240,7 @@ public final class LoadingListDtos { String internalRemark, String customerRemark, String warehouseRemark, + String customerServiceRemark, int loadSequence, LocalDateTime frozenAt, Integer itemVersion 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 939eff487..f43568c32 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java @@ -83,7 +83,8 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){} @NotNull @Min(0) Integer expectedItemVersion,@NotBlank @Size(max=128) String operationId) {} public record RemoveUnpickedRequest(@NotNull @Min(0) Integer expectedPlanVersion, @NotBlank @Size(max=128) String operationId) {} - public record WarehouseRemarkRequest(@Size(max=1000) String warehouseRemark, + public record RemarksRequest(@Size(max=1000) String internalRemark, + @Size(max=1000) String warehouseRemark, @Size(max=1000) String customerServiceRemark, @NotNull @Min(0) Integer expectedPlanVersion, @NotNull @Min(0) Integer expectedItemVersion, @NotBlank @Size(max=128) String operationId) {} public record PlanCoLoadGroup(Long groupId,String groupNo,Long lockedPlanId,Integer memberOrderCount, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/entity/LoadingPlanItemEntity.java b/firstleg/src/main/java/com/baiyue/firstleg/entity/LoadingPlanItemEntity.java index 967b686da..363b29d52 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/entity/LoadingPlanItemEntity.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/entity/LoadingPlanItemEntity.java @@ -9,7 +9,7 @@ public class LoadingPlanItemEntity { private String unitType; private Long palletId; private String palletNo; private Boolean indivisible; private Integer palletCount; private BigDecimal grossWeightKg; private BigDecimal netWeightKg; private BigDecimal dimensionalWeightKg; private BigDecimal volumeCbm; private String fbaTrackingNo; private String shipmentNo; private String shippingMark; private Integer loadSequence; private String positionSnapshot; - private String internalRemark; private String customerRemark; private String warehouseRemark; private LocalDateTime frozenAt; private Long frozenBy; + private String internalRemark; private String customerRemark; private String warehouseRemark; private String customerServiceRemark; private LocalDateTime frozenAt; private Long frozenBy; private Long removedBy; private String removedByName; private LocalDateTime removedAt; @TableLogic private Boolean deleted; @TableField(exist=false) private Long activeInboundDetailId; @TableField(exist=false) private Integer pickedPackageCount; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java index d1fd0820c..652823fed 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java @@ -194,7 +194,7 @@ public interface LoadingListQueryMapper { * i.package_count / NULLIF(inbound.total_package_count,0)),0) pallet_count, gross_weight_kg, net_weight_kg, volume_cbm, dimensional_weight_kg, product_quantity, units_per_carton, fba_tracking_no, shipment_no, - shipping_mark, internal_remark, customer_remark, warehouse_remark, + shipping_mark, internal_remark, customer_remark, warehouse_remark, customer_service_remark, load_sequence, frozen_at, i.version AS item_version FROM fl_loading_plan_item i JOIN fl_loading_plan p ON p.id=i.loading_plan_id @@ -457,6 +457,7 @@ public interface LoadingListQueryMapper { private String internalRemark; private String customerRemark; private String warehouseRemark; + private String customerServiceRemark; private Integer loadSequence; private LocalDateTime frozenAt; private Integer itemVersion; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPlanItemMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPlanItemMapper.java index f7247eba9..0edfde3e7 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPlanItemMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPlanItemMapper.java @@ -11,7 +11,8 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstl int removeEditing(@Param("planId") long planId,@Param("itemId") long itemId,@Param("expectedVersion") int expectedVersion,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName); @Update(""" UPDATE fl_loading_plan_item i - SET i.warehouse_remark=#{remark}, i.updated_by=#{operatorId}, i.updated_by_name=#{operatorName}, + SET i.internal_remark=#{internalRemark}, i.warehouse_remark=#{warehouseRemark}, + i.customer_service_remark=#{customerServiceRemark}, i.updated_by=#{operatorId}, i.updated_by_name=#{operatorName}, i.updated_at=CURRENT_TIMESTAMP(3), i.version=i.version+1 WHERE i.id=#{itemId} AND i.loading_plan_id=#{planId} AND i.deleted=0 AND i.frozen_at IS NULL AND i.version=#{expectedItemVersion} @@ -21,9 +22,12 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstl AND p.plan_status IN ('EDITING','LOADING') ) """) - int updateWarehouseRemark(@Param("planId") long planId, @Param("itemId") long itemId, + int updateRemarks(@Param("planId") long planId, @Param("itemId") long itemId, @Param("touchedPlanVersion") int touchedPlanVersion, @Param("expectedItemVersion") int expectedItemVersion, - @Param("remark") String remark, @Param("operatorId") long operatorId, + @Param("internalRemark") String internalRemark, + @Param("warehouseRemark") String warehouseRemark, + @Param("customerServiceRemark") String customerServiceRemark, + @Param("operatorId") long operatorId, @Param("operatorName") String operatorName); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/LoadingListRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/LoadingListRepository.java index 0d4a61a4f..70c71a249 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/LoadingListRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/LoadingListRepository.java @@ -193,7 +193,7 @@ public class LoadingListRepository { decimal(row.getGrossWeightKg()), decimal(row.getNetWeightKg()), decimal(row.getVolumeCbm()), decimal(row.getDimensionalWeightKg()), value(row.getProductQuantity()), integer(row.getUnitsPerCarton()), row.getFbaTrackingNo(), row.getShipmentNo(), row.getShippingMark(), - row.getInternalRemark(), row.getCustomerRemark(), row.getWarehouseRemark(), + row.getInternalRemark(), row.getCustomerRemark(), row.getWarehouseRemark(), row.getCustomerServiceRemark(), integer(row.getLoadSequence()), row.getFrozenAt(), row.getItemVersion())).toList(); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/SmartLoadingRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/SmartLoadingRepository.java index 26b223597..ea840c3d6 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/SmartLoadingRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/SmartLoadingRepository.java @@ -147,10 +147,11 @@ public class SmartLoadingRepository { public int remove(long planId, long itemId, int version, long operatorId, String operatorName) { return items.removeEditing(planId, itemId, version, operatorId, operatorName); } - public int updateWarehouseRemark(long planId, long itemId, int touchedPlanVersion, int expectedItemVersion, - String remark, long operatorId, String operatorName) { - return items.updateWarehouseRemark(planId, itemId, touchedPlanVersion, expectedItemVersion, - remark, operatorId, operatorName); + public int updateRemarks(long planId, long itemId, int touchedPlanVersion, int expectedItemVersion, + String internalRemark, String warehouseRemark, String customerServiceRemark, + long operatorId, String operatorName) { + return items.updateRemarks(planId, itemId, touchedPlanVersion, expectedItemVersion, + internalRemark, warehouseRemark, customerServiceRemark, operatorId, operatorName); } public int startPicking(long planId, long operatorId, String operatorName) { return plans.startPicking(planId, operatorId, operatorName); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java index 1fdd914e2..8691f0797 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java @@ -1043,42 +1043,55 @@ public class SmartLoadingService { } @Transactional - @BusinessAudit(event = "SMART_LOADING_WAREHOUSE_REMARK", resourceType = "LOADING_PLAN", - resourceId = "#planId", description = "修改装柜仓库备注") - public Plan updateWarehouseRemark(long planId, long itemId, WarehouseRemarkRequest request, - long operatorId, String operatorName) { + @BusinessAudit(event = "SMART_LOADING_REMARKS", resourceType = "LOADING_PLAN", + resourceId = "#planId", description = "修改装柜协作备注") + public Plan updateRemarks(long planId, long itemId, RemarksRequest request, + long operatorId, String operatorName) { String operation = operation(request.operationId()); - String remark = trim(request.warehouseRemark()); - if (!StringUtils.hasText(remark)) remark = null; - String requestHash = sha256("WAREHOUSE_REMARK|" + planId + '|' + itemId + '|' - + Objects.toString(remark, "") + '|' + request.expectedPlanVersion() + '|' + String internalRemark = nullableRemark(request.internalRemark()); + String warehouseRemark = nullableRemark(request.warehouseRemark()); + String customerServiceRemark = nullableRemark(request.customerServiceRemark()); + String requestHash = sha256("REMARKS|" + planId + '|' + itemId + '|' + + Objects.toString(internalRemark, "") + '|' + Objects.toString(warehouseRemark, "") + '|' + + Objects.toString(customerServiceRemark, "") + '|' + request.expectedPlanVersion() + '|' + request.expectedItemVersion()); - Plan replay = replay(operation, planId, "WAREHOUSE_REMARK", itemId, requestHash); + Plan replay = replay(operation, planId, "REMARKS", itemId, requestHash); if (replay != null) return replay; LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName); LoadingPlanItemEntity item = repository.item(planId, itemId); if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在"); - if (Objects.equals(item.getWarehouseRemark(), remark)) { + if (Objects.equals(item.getInternalRemark(), internalRemark) + && Objects.equals(item.getWarehouseRemark(), warehouseRemark) + && Objects.equals(item.getCustomerServiceRemark(), customerServiceRemark)) { return view(plan, requireBooking(plan.getContainerBookingId())); } - String before = json(values("warehouseRemark", item.getWarehouseRemark(), "version", item.getVersion())); + String before = json(values("internalRemark", item.getInternalRemark(), + "warehouseRemark", item.getWarehouseRemark(), "customerServiceRemark", item.getCustomerServiceRemark(), + "version", item.getVersion())); try { touch(plan, operatorId, operatorName); - if (repository.updateWarehouseRemark(planId, itemId, request.expectedPlanVersion() + 1, - request.expectedItemVersion(), remark, operatorId, operatorName) != 1) { + if (repository.updateRemarks(planId, itemId, request.expectedPlanVersion() + 1, + request.expectedItemVersion(), internalRemark, warehouseRemark, customerServiceRemark, + operatorId, operatorName) != 1) { throw conflict("装柜计划或明细已被其他人修改,请刷新后重试"); } } catch (DataAccessException exception) { if (hasSqlState(exception, "45000")) { - throw conflict("装柜计划或明细状态已变化,仓库备注未保存,请刷新后重试"); + throw conflict("装柜计划或明细状态已变化,备注未保存,请刷新后重试"); } throw exception; } - history(planId, itemId, "WAREHOUSE_REMARK", plan.getPlanStatus(), plan.getPlanStatus(), operation, - before, json(values("warehouseRemark", remark, "requestHash", requestHash)), operatorId, operatorName); + history(planId, itemId, "REMARKS", plan.getPlanStatus(), plan.getPlanStatus(), operation, + before, json(values("internalRemark", internalRemark, "warehouseRemark", warehouseRemark, + "customerServiceRemark", customerServiceRemark, "requestHash", requestHash)), operatorId, operatorName); return view(repository.plan(planId), requireBooking(plan.getContainerBookingId())); } + private static String nullableRemark(String value) { + String remark = trim(value); + return StringUtils.hasText(remark) ? remark : null; + } + @Transactional @BusinessAudit(event = "SMART_LOADING_ITEM_DELETE", resourceType = "LOADING_PLAN_ITEM", resourceId = "#itemId", description = "删除智能装柜明细") @@ -1384,7 +1397,12 @@ public class SmartLoadingService { i.setNetWeightKg(unit(c.unitNetWeightKg()).multiply(BigDecimal.valueOf(count))); i.setVolumeCbm(unit(c.unitVolumeCbm()).multiply(BigDecimal.valueOf(count))); i.setPalletCount(estimatedPalletCount(c, count)); - applyCargo(i, cargo, count); i.setLoadSequence(seq); i.setDeleted(false); i.setCreatedBy(op); i.setCreatedByName(name); i.setUpdatedBy(op); i.setUpdatedByName(name); i.setVersion(0); return i; + applyCargo(i, cargo, count); + i.setCustomerRemark(cargo.customerRemark()); + i.setInternalRemark(cargo.latestInternalNote()); + i.setWarehouseRemark(cargo.warehouseRemark()); + i.setCustomerServiceRemark(cargo.customerServiceRemark()); + i.setLoadSequence(seq); i.setDeleted(false); i.setCreatedBy(op); i.setCreatedByName(name); i.setUpdatedBy(op); i.setUpdatedByName(name); i.setVersion(0); return i; } private int estimatedPalletCount(Candidate candidate, int packageCount) { InboundOrderEntity inbound = repository.inbound(candidate.accountId(), candidate.inboundId()); @@ -1397,8 +1415,6 @@ public class SmartLoadingService { item.setSkuCode(cargo.skuNo()); item.setProductNameEn(cargo.cargoNameEn()); item.setUnitsPerCarton(cargo.unitsPerPackage()); - item.setCustomerRemark(cargo.customerRemark()); - item.setInternalRemark(cargo.latestInternalNote()); int original = cargo.originalPackageCount() == null || cargo.originalPackageCount() <= 0 ? boxes : cargo.originalPackageCount(); if (cargo.unitsPerPackage() != null) { @@ -1419,9 +1435,7 @@ public class SmartLoadingService { && Objects.equals(current.getSkuCode(), desired.getSkuCode()) && Objects.equals(current.getProductNameEn(), desired.getProductNameEn()) && Objects.equals(current.getUnitsPerCarton(), desired.getUnitsPerCarton()) - && Objects.equals(current.getProductQuantity(), desired.getProductQuantity()) - && Objects.equals(current.getCustomerRemark(), desired.getCustomerRemark()) - && Objects.equals(current.getInternalRemark(), desired.getInternalRemark()); + && Objects.equals(current.getProductQuantity(), desired.getProductQuantity()); } private static boolean decimalEquals(BigDecimal left, BigDecimal right) { @@ -1434,7 +1448,8 @@ public class SmartLoadingService { "palletCount", item.getPalletCount(), "skuCode", item.getSkuCode(), "productNameEn", item.getProductNameEn(), "unitsPerCarton", item.getUnitsPerCarton(), "productQuantity", item.getProductQuantity(), "customerRemark", item.getCustomerRemark(), - "internalRemark", item.getInternalRemark(), "version", item.getVersion()); + "internalRemark", item.getInternalRemark(), "warehouseRemark", item.getWarehouseRemark(), + "customerServiceRemark", item.getCustomerServiceRemark(), "version", item.getVersion()); } private OrderCargoClient.CargoBinding loadCargo(Candidate candidate) { diff --git a/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql b/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql index 5d20f8dcf..dba77f523 100644 --- a/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql +++ b/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql @@ -319,6 +319,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item ( internal_remark VARCHAR(1000) NULL COMMENT '平台内部备注', customer_remark VARCHAR(1000) NULL COMMENT '客户备注快照', warehouse_remark VARCHAR(1000) NULL COMMENT '仓库备注快照', + customer_service_remark VARCHAR(1000) NULL COMMENT '客服备注快照', frozen_at DATETIME(3) NULL COMMENT '装柜完成后的永久冻结时间', frozen_by BIGINT UNSIGNED NULL COMMENT '冻结操作人ID', removed_by BIGINT UNSIGNED NULL COMMENT '推送前逻辑删除操作人ID', @@ -358,6 +359,15 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item ( CONSTRAINT chk_loading_item_deleted CHECK (deleted IN (0, 1)) ) ENGINE=InnoDB COMMENT='智能装柜客户货物明细;客户归属仅在明细层'; +SET @loading_item_customer_service_remark_sql = IF( + EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_plan_item' AND column_name='customer_service_remark'), + 'SELECT 1', + 'ALTER TABLE baiyue_firstleg.fl_loading_plan_item ADD COLUMN customer_service_remark VARCHAR(1000) NULL COMMENT ''客服备注快照'' AFTER warehouse_remark' +); +PREPARE loading_item_customer_service_remark_stmt FROM @loading_item_customer_service_remark_sql; +EXECUTE loading_item_customer_service_remark_stmt; +DEALLOCATE PREPARE loading_item_customer_service_remark_stmt; + CREATE TABLE IF NOT EXISTS fl_loading_pallet_label ( id BIGINT UNSIGNED NOT NULL COMMENT '散货托盘标签ID(雪花ID)', loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID', @@ -1036,7 +1046,9 @@ BEGIN IF current_plan_status = 'LOADING' AND EXISTS ( SELECT 1 FROM fl_loading_pick_item WHERE loading_plan_item_id = OLD.id AND picked_package_count > 0) THEN - IF NOT (NEW.warehouse_remark <=> OLD.warehouse_remark) THEN + IF NOT (NEW.internal_remark <=> OLD.internal_remark) + OR NOT (NEW.warehouse_remark <=> OLD.warehouse_remark) + OR NOT (NEW.customer_service_remark <=> OLD.customer_service_remark) THEN IF NEW.package_count <> OLD.package_count OR NEW.deleted <> OLD.deleted OR NOT (NEW.customer_name <=> OLD.customer_name) OR NOT (NEW.inbound_id <=> OLD.inbound_id) @@ -1064,14 +1076,13 @@ BEGIN OR NOT (NEW.shipping_mark <=> OLD.shipping_mark) OR NOT (NEW.load_sequence <=> OLD.load_sequence) OR NOT (NEW.position_snapshot <=> OLD.position_snapshot) - OR NOT (NEW.internal_remark <=> OLD.internal_remark) OR NOT (NEW.customer_remark <=> OLD.customer_remark) OR NOT (NEW.frozen_at <=> OLD.frozen_at) OR NOT (NEW.frozen_by <=> OLD.frozen_by) OR NOT (NEW.removed_by <=> OLD.removed_by) OR NOT (NEW.removed_by_name <=> OLD.removed_by_name) OR NOT (NEW.removed_at <=> OLD.removed_at) THEN - SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '已有PDA拣货记录的装柜明细修改仓库备注时不得变更其他业务字段'; + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '已有PDA拣货记录的装柜明细修改备注时不得变更其他业务字段'; END IF; ELSEIF NOT EXISTS ( SELECT 1 FROM fl_loading_pick_item @@ -1106,7 +1117,6 @@ BEGIN OR NOT (NEW.shipping_mark <=> OLD.shipping_mark) OR NOT (NEW.load_sequence <=> OLD.load_sequence) OR NOT (NEW.position_snapshot <=> OLD.position_snapshot) - OR NOT (NEW.internal_remark <=> OLD.internal_remark) OR NOT (NEW.customer_remark <=> OLD.customer_remark) OR NOT (NEW.frozen_at <=> OLD.frozen_at) OR NOT (NEW.frozen_by <=> OLD.frozen_by) diff --git a/firstleg/src/main/resources/openapi/firstleg-v1.yaml b/firstleg/src/main/resources/openapi/firstleg-v1.yaml index 21375b117..050f4ac2c 100644 --- a/firstleg/src/main/resources/openapi/firstleg-v1.yaml +++ b/firstleg/src/main/resources/openapi/firstleg-v1.yaml @@ -774,20 +774,21 @@ paths: content: {application/json: {schema: {$ref: '#/components/schemas/SmartLoadingRemoveUnpickedRequest'}}} responses: '200': {description: 成功或业务失败,data 为最新 SmartLoadingPlan} - /api/platform/v1/first-leg/smart-loading/plans/{planId}/items/{itemId}/warehouse-remark: + /api/platform/v1/first-leg/smart-loading/plans/{planId}/items/{itemId}/remarks: patch: - summary: 修改装柜明细仓库备注 + summary: 修改装柜明细协作备注 description: >- - 需要 platform:firstleg:preload:edit 权限。EDITING/LOADING 可修改或清空 warehouseRemark; + 需要 platform:firstleg:preload:edit 权限。客户备注只读;EDITING/LOADING 可修改或清空 + internalRemark、warehouseRemark 和 customerServiceRemark; COMPLETED、计划/明细版本冲突返回 A0409 且事务零写入,写入历史且与 PDA final complete 使用同一并发门禁。 成功响应中的 version 是递增后的 planVersion,items[].version 是递增后的 itemVersion;调用方同时使用 - canEdit、items[].canAdjust/canRemove 与自身权限决定操作可用性。数量调整或动态增量不会覆盖已有 warehouseRemark。 + canEdit、items[].canAdjust/canRemove 与自身权限决定操作可用性。数量调整或动态增量不会覆盖已有协作备注。 parameters: - {name: planId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} - {name: itemId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} requestBody: required: true - content: {application/json: {schema: {$ref: '#/components/schemas/SmartLoadingWarehouseRemarkRequest'}}} + content: {application/json: {schema: {$ref: '#/components/schemas/SmartLoadingRemarksRequest'}}} responses: '200': {description: 成功或业务失败,data 为最新 SmartLoadingPlan} /openapi/v1/inbound-measurements: @@ -2323,11 +2324,13 @@ components: properties: expectedPlanVersion: {type: integer, minimum: 0} operationId: {type: string, minLength: 1, maxLength: 128} - SmartLoadingWarehouseRemarkRequest: + SmartLoadingRemarksRequest: type: object required: [expectedPlanVersion, expectedItemVersion, operationId] properties: + internalRemark: {type: string, nullable: true, maxLength: 1000, description: 内部备注;null/空字符串均表示清空} warehouseRemark: {type: string, nullable: true, maxLength: 1000, description: 仓库备注;null/空字符串均表示清空} + customerServiceRemark: {type: string, nullable: true, maxLength: 1000, description: 客服备注;null/空字符串均表示清空} expectedPlanVersion: {type: integer, minimum: 0} expectedItemVersion: {type: integer, minimum: 0} operationId: {type: string, minLength: 1, maxLength: 128} @@ -2518,8 +2521,9 @@ components: inboundCode: {type: string, nullable: true} customerName: {type: string, nullable: true} customerRemark: {type: string, nullable: true, readOnly: true, description: 来源为订单 customerRemark} - internalRemark: {type: string, nullable: true, readOnly: true, description: 来源为订单最新内部备注,不使用 cargoRemark} + internalRemark: {type: string, nullable: true, description: 内部备注;预装阶段可修改} warehouseRemark: {type: string, nullable: true, description: 仓库人员逐条维护,可在 EDITING/LOADING 修改} + customerServiceRemark: {type: string, nullable: true, description: 客服备注;预装阶段可修改} packageCount: {type: integer, minimum: 1} totalPackageCount: {type: integer, minimum: 1, description: 当前入仓明细可在计划内分配的总箱数,包含已用与未用箱数} remainingPackageCount: {type: integer, minimum: 0, description: 当前入仓明细尚未分配到本计划的箱数} diff --git a/firstleg/src/main/resources/openapi/loading-lists-v1.yaml b/firstleg/src/main/resources/openapi/loading-lists-v1.yaml index 06cc71d0c..202690ffa 100644 --- a/firstleg/src/main/resources/openapi/loading-lists-v1.yaml +++ b/firstleg/src/main/resources/openapi/loading-lists-v1.yaml @@ -405,6 +405,7 @@ components: internalRemark: {type: string, nullable: true, description: 内部备注} customerRemark: {type: string, nullable: true, description: 客户备注} warehouseRemark: {type: string, nullable: true, description: 仓库备注} + customerServiceRemark: {type: string, nullable: true, description: 客服备注} loadSequence: {type: integer, format: int32, minimum: 0, description: 装柜顺序} frozenAt: {type: string, format: date-time, nullable: true, description: 明细冻结时间} itemVersion: {type: integer, format: int32, minimum: 0, description: 明细乐观锁版本;更新仓库备注时作为 expectedItemVersion} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformSmartLoadingControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformSmartLoadingControllerContractTest.java index a60e5844e..988875e3e 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformSmartLoadingControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformSmartLoadingControllerContractTest.java @@ -54,7 +54,7 @@ class PlatformSmartLoadingControllerContractTest { authority("removeGroup","platform:firstleg:smart-loading:delete",AuthenticatedUser.class,long.class,long.class,RemoveGroupRequest.class); authority("recommend","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,RecommendRequest.class); authority("adjust","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,long.class,AdjustRequest.class); - authority("warehouseRemark","platform:firstleg:preload:edit",AuthenticatedUser.class,long.class,long.class,WarehouseRemarkRequest.class); + authority("remarks","platform:firstleg:preload:edit",AuthenticatedUser.class,long.class,long.class,RemarksRequest.class); authority("delete","platform:firstleg:smart-loading:delete",AuthenticatedUser.class,long.class,long.class,DeleteRequest.class); authority("removeUnpicked","platform:firstleg:smart-loading:delete",AuthenticatedUser.class,long.class,RemoveUnpickedRequest.class); } @@ -77,13 +77,13 @@ class PlatformSmartLoadingControllerContractTest { .contains("SmartLoadingAdjustRequest:") .contains("SmartLoadingDeleteRequest:") .contains("SmartLoadingRemoveUnpickedRequest:") - .contains("SmartLoadingWarehouseRemarkRequest:") - .contains("/items/{itemId}/warehouse-remark:") + .contains("SmartLoadingRemarksRequest:") + .contains("/items/{itemId}/remarks:") .contains("需要 platform:firstleg:preload:edit 权限") .contains("warehouseRemark: {type: string, nullable: true, maxLength: 1000") .contains("成功响应中的 version 是递增后的 planVersion") .contains("items[].version 是递增后的 itemVersion") - .contains("数量调整或动态增量不会覆盖已有 warehouseRemark") + .contains("数量调整或动态增量不会覆盖已有协作备注") .contains("柜内总容积和总重量分别允许达到柜型额定值的150%") .contains("人工分配不限制订单国内仓及渠道与柜次一致") .contains("智能推荐允许跨国内仓,但仍只选择柜次渠道") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java index 5a024d6e5..36f5a9d45 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java @@ -122,8 +122,8 @@ class SmartLoadingSqlContractTest { } @Test - @DisplayName("仓库备注使用计划先行与明细单表CAS并允许已拣明细仅改备注") - void shouldUpdateWarehouseRemarkWithoutMultiTableTriggerConflict() throws Exception { + @DisplayName("可编辑备注使用计划先行与明细单表CAS并允许已拣明细仅改备注") + void shouldUpdateEditableRemarksWithoutMultiTableTriggerConflict() throws Exception { String mapper = Files.readString(Path.of( "src/main/java/com/baiyue/firstleg/mapper/LoadingPlanItemMapper.java"), StandardCharsets.UTF_8); assertThat(mapper) @@ -133,8 +133,8 @@ class SmartLoadingSqlContractTest { .contains("i.frozen_at IS NULL") .doesNotContain("UPDATE fl_loading_plan_item i JOIN fl_loading_plan p") .doesNotContain("p.version=p.version+1"); - assertWarehouseRemarkTriggerContract(migration()); - assertWarehouseRemarkTriggerContract(unifiedMigration()); + assertEditableRemarksTriggerContract(migration()); + assertEditableRemarksTriggerContract(unifiedMigration()); } private static void assertContainerLifecycleSchema(String sql) { @@ -151,11 +151,13 @@ class SmartLoadingSqlContractTest { .contains("column_name='merged_into_container_booking_id'"); } - private static void assertWarehouseRemarkTriggerContract(String sql) { + private static void assertEditableRemarksTriggerContract(String sql) { assertThat(sql) - .contains("IF NOT (NEW.warehouse_remark <=> OLD.warehouse_remark) THEN") + .contains("IF NOT (NEW.internal_remark <=> OLD.internal_remark)") + .contains("OR NOT (NEW.warehouse_remark <=> OLD.warehouse_remark)") + .contains("OR NOT (NEW.customer_service_remark <=> OLD.customer_service_remark) THEN") .contains("IF NEW.package_count <> OLD.package_count OR NEW.deleted <> OLD.deleted") - .contains("已有PDA拣货记录的装柜明细修改仓库备注时不得变更其他业务字段") + .contains("已有PDA拣货记录的装柜明细修改备注时不得变更其他业务字段") .contains("ELSEIF NOT EXISTS (") .contains("IF OLD.frozen_at IS NOT NULL THEN") .contains("装柜完成后的明细永久冻结"); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java index 341c5a47b..2c6b38bd5 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java @@ -21,7 +21,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainer; import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery; import com.baiyue.firstleg.dto.SmartLoadingDtos.Plan; import com.baiyue.firstleg.dto.SmartLoadingDtos.RecommendRequest; -import com.baiyue.firstleg.dto.SmartLoadingDtos.WarehouseRemarkRequest; +import com.baiyue.firstleg.dto.SmartLoadingDtos.RemarksRequest; import com.baiyue.firstleg.entity.ContainerBookingEntity; import com.baiyue.firstleg.entity.LoadingPlanEntity; import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity; @@ -1267,7 +1267,7 @@ class SmartLoadingServiceTest { when(cargoClient.detail(anyString(), eq(9L), eq(30L), eq(20L))).thenReturn(ApiResponse.success( new OrderCargoClient.CargoBinding(20L, 30L, 1, "中文品名", 10, new BigDecimal("20"), new BigDecimal("2"), "SKU-1", "English name", - 4, 100, new BigDecimal("10"), "货物备注", "客户备注", "最新内部备注"))); + 4, 100, new BigDecimal("10"), "货物备注", "客户备注", "最新内部备注", "仓库备注", "客服备注"))); doAnswer(invocation -> { Object value = invocation.getArgument(0); if (value instanceof LoadingPlanItemEntity item) item.setId(88L); @@ -1288,6 +1288,8 @@ class SmartLoadingServiceTest { assertThat(saved.getNetWeightKg()).isEqualByComparingTo("7.5"); assertThat(saved.getCustomerRemark()).isEqualTo("客户备注"); assertThat(saved.getInternalRemark()).isEqualTo("最新内部备注"); + assertThat(saved.getWarehouseRemark()).isEqualTo("仓库备注"); + assertThat(saved.getCustomerServiceRemark()).isEqualTo("客服备注"); assertThat(saved.getInternalRemark()).isNotEqualTo("货物备注"); ArgumentCaptor historyCaptor = ArgumentCaptor.forClass(LoadingPlanHistoryEntity.class); @@ -1451,20 +1453,20 @@ class SmartLoadingServiceTest { when(repository.booking(99L)).thenReturn(booking()); when(repository.items(1L)).thenReturn(List.of(line)); when(repository.touch(1L, 2, 7L, "u")).thenReturn(1); - when(repository.updateWarehouseRemark(1L, 8L, 3, 0, null, 7L, "u")).thenReturn(1); + when(repository.updateRemarks(1L, 8L, 3, 0, null, null, null, 7L, "u")).thenReturn(1); - Plan result = service.updateWarehouseRemark(1L, 8L, - new WarehouseRemarkRequest(" ", 2, 0, "warehouse-clear"), 7L, "u"); + Plan result = service.updateRemarks(1L, 8L, + new RemarksRequest(null, " ", null, 2, 0, "warehouse-clear"), 7L, "u"); assertThat(result.status()).isEqualTo("LOADING"); verify(loadingPickService).lockMutable(1L); var order = inOrder(loadingPickService, repository); order.verify(loadingPickService).lockMutable(1L); order.verify(repository).touch(1L, 2, 7L, "u"); - order.verify(repository).updateWarehouseRemark(1L, 8L, 3, 0, null, 7L, "u"); + order.verify(repository).updateRemarks(1L, 8L, 3, 0, null, null, null, 7L, "u"); ArgumentCaptor history = ArgumentCaptor.forClass(LoadingPlanHistoryEntity.class); verify(repository).insert(history.capture()); - assertThat(history.getValue().getActionType()).isEqualTo("WAREHOUSE_REMARK"); + assertThat(history.getValue().getActionType()).isEqualTo("REMARKS"); assertThat(history.getValue().getBeforeSnapshot()).contains("旧仓库备注"); } @@ -1477,14 +1479,14 @@ class SmartLoadingServiceTest { when(repository.booking(99L)).thenReturn(booking()); when(repository.items(1L)).thenReturn(List.of(line)); when(repository.touch(1L, 1, 7L, "u")).thenReturn(1); - when(repository.updateWarehouseRemark(1L, 8L, 2, 0, "待核对外箱", 7L, "u")).thenReturn(1); + when(repository.updateRemarks(1L, 8L, 2, 0, null, "待核对外箱", null, 7L, "u")).thenReturn(1); - Plan result = service.updateWarehouseRemark(1L, 8L, - new WarehouseRemarkRequest("待核对外箱", 1, 0, "warehouse-editing"), 7L, "u"); + Plan result = service.updateRemarks(1L, 8L, + new RemarksRequest(null, "待核对外箱", null, 1, 0, "warehouse-editing"), 7L, "u"); assertThat(result.status()).isEqualTo("EDITING"); assertThat(result.version()).isEqualTo(2); - verify(repository).updateWarehouseRemark(1L, 8L, 2, 0, "待核对外箱", 7L, "u"); + verify(repository).updateRemarks(1L, 8L, 2, 0, null, "待核对外箱", null, 7L, "u"); } @Test @@ -1497,13 +1499,13 @@ class SmartLoadingServiceTest { when(repository.booking(99L)).thenReturn(booking()); when(repository.items(1L)).thenReturn(List.of(line)); - Plan result = service.updateWarehouseRemark(1L, 8L, - new WarehouseRemarkRequest(" 待核对外箱 ", 2, 0, "warehouse-noop"), 7L, "u"); + Plan result = service.updateRemarks(1L, 8L, + new RemarksRequest(null, " 待核对外箱 ", null, 2, 0, "warehouse-noop"), 7L, "u"); assertThat(result.version()).isEqualTo(2); verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), anyString()); - verify(repository, never()).updateWarehouseRemark(anyLong(), anyLong(), anyInt(), anyInt(), - any(), anyLong(), anyString()); + verify(repository, never()).updateRemarks(anyLong(), anyLong(), anyInt(), anyInt(), + any(), any(), any(), anyLong(), anyString()); verify(repository, never()).insert(any(LoadingPlanHistoryEntity.class)); } @@ -1514,10 +1516,10 @@ class SmartLoadingServiceTest { when(repository.plan(1L)).thenReturn(plan); when(repository.item(1L, 8L)).thenReturn(line); when(repository.touch(1L, 2, 7L, "u")).thenReturn(1); - when(repository.updateWarehouseRemark(1L, 8L, 3, 0, "新备注", 7L, "u")).thenReturn(0); + when(repository.updateRemarks(1L, 8L, 3, 0, null, "新备注", null, 7L, "u")).thenReturn(0); - BizException error = catchThrowableOfType(() -> service.updateWarehouseRemark(1L, 8L, - new WarehouseRemarkRequest("新备注", 2, 0, "warehouse-conflict"), 7L, "u"), BizException.class); + BizException error = catchThrowableOfType(() -> service.updateRemarks(1L, 8L, + new RemarksRequest(null, "新备注", null, 2, 0, "warehouse-conflict"), 7L, "u"), BizException.class); assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT); verify(repository).touch(1L, 2, 7L, "u"); @@ -1531,15 +1533,15 @@ class SmartLoadingServiceTest { when(repository.plan(1L)).thenReturn(plan); when(repository.item(1L, 8L)).thenReturn(line); when(repository.touch(1L, 2, 7L, "u")).thenReturn(1); - when(repository.updateWarehouseRemark(1L, 8L, 3, 0, "新备注", 7L, "u")) + when(repository.updateRemarks(1L, 8L, 3, 0, null, "新备注", null, 7L, "u")) .thenThrow(new DataIntegrityViolationException("trigger", new SQLException("状态冲突", "45000"))); - BizException error = catchThrowableOfType(() -> service.updateWarehouseRemark(1L, 8L, - new WarehouseRemarkRequest("新备注", 2, 0, "warehouse-trigger"), 7L, "u"), BizException.class); + BizException error = catchThrowableOfType(() -> service.updateRemarks(1L, 8L, + new RemarksRequest(null, "新备注", null, 2, 0, "warehouse-trigger"), 7L, "u"), BizException.class); assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT); - assertThat(error.getMessage()).contains("仓库备注未保存").contains("刷新后重试"); + assertThat(error.getMessage()).contains("备注未保存").contains("刷新后重试"); verify(repository, never()).insert(any(LoadingPlanHistoryEntity.class)); } @@ -1547,46 +1549,46 @@ class SmartLoadingServiceTest { void stalePlanVersionRejectsWarehouseRemarkBeforeItemWrite() { when(repository.plan(1L)).thenReturn(plan("LOADING", 3)); - BizException error = catchThrowableOfType(() -> service.updateWarehouseRemark(1L, 8L, - new WarehouseRemarkRequest("新备注", 2, 0, "warehouse-stale-plan"), 7L, "u"), BizException.class); + BizException error = catchThrowableOfType(() -> service.updateRemarks(1L, 8L, + new RemarksRequest(null, "新备注", null, 2, 0, "warehouse-stale-plan"), 7L, "u"), BizException.class); assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT); verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), anyString()); - verify(repository, never()).updateWarehouseRemark(anyLong(), anyLong(), anyInt(), anyInt(), - any(), anyLong(), anyString()); + verify(repository, never()).updateRemarks(anyLong(), anyLong(), anyInt(), anyInt(), + any(), any(), any(), anyLong(), anyString()); } @Test void completedPlanRejectsWarehouseRemarkBeforeAnyWrite() { when(repository.plan(1L)).thenReturn(plan("COMPLETED", 4)); - BizException error = catchThrowableOfType(() -> service.updateWarehouseRemark(1L, 8L, - new WarehouseRemarkRequest("新备注", 4, 0, "warehouse-completed"), 7L, "u"), BizException.class); + BizException error = catchThrowableOfType(() -> service.updateRemarks(1L, 8L, + new RemarksRequest(null, "新备注", null, 4, 0, "warehouse-completed"), 7L, "u"), BizException.class); assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT); verify(loadingPickService, never()).lockMutable(anyLong()); verify(repository, never()).item(anyLong(), anyLong()); - verify(repository, never()).updateWarehouseRemark(anyLong(), anyLong(), anyInt(), anyInt(), - any(), anyLong(), anyString()); + verify(repository, never()).updateRemarks(anyLong(), anyLong(), anyInt(), anyInt(), + any(), any(), any(), anyLong(), anyString()); verify(repository, never()).insert(any(LoadingPlanHistoryEntity.class)); } @Test void exactWarehouseRemarkReplayIsReadOnly() { - String requestHash = sha256("WAREHOUSE_REMARK|1|8|备注|2|0"); + String requestHash = sha256("REMARKS|1|8||备注||2|0"); when(repository.operation("warehouse-replay")) - .thenReturn(history(1L, 8L, "WAREHOUSE_REMARK", requestHash)); + .thenReturn(history(1L, 8L, "REMARKS", requestHash)); when(repository.plan(1L)).thenReturn(plan("LOADING", 3)); when(repository.booking(99L)).thenReturn(booking()); when(repository.items(1L)).thenReturn(List.of(item(8, 9, 20, 1))); - Plan result = service.updateWarehouseRemark(1L, 8L, - new WarehouseRemarkRequest("备注", 2, 0, "warehouse-replay"), 7L, "u"); + Plan result = service.updateRemarks(1L, 8L, + new RemarksRequest(null, "备注", null, 2, 0, "warehouse-replay"), 7L, "u"); assertThat(result.version()).isEqualTo(3); verify(loadingPickService, never()).lockMutable(anyLong()); - verify(repository, never()).updateWarehouseRemark(anyLong(), anyLong(), anyInt(), anyInt(), - any(), anyLong(), anyString()); + verify(repository, never()).updateRemarks(anyLong(), anyLong(), anyInt(), anyInt(), + any(), any(), any(), anyLong(), anyString()); } @Test diff --git a/order/src/main/java/com/baiyue/order/controller/InternalOrderCargoController.java b/order/src/main/java/com/baiyue/order/controller/InternalOrderCargoController.java index a7d753780..fdddfec73 100644 --- a/order/src/main/java/com/baiyue/order/controller/InternalOrderCargoController.java +++ b/order/src/main/java/com/baiyue/order/controller/InternalOrderCargoController.java @@ -57,14 +57,11 @@ public class InternalOrderCargoController { } var order = orders.findOwned(tenantId, orderId); if (order == null) throw new BizException(ResultCode.NOT_FOUND, "订单不存在或不属于当前客户"); - String latestInternalNote = orders.findNotes(tenantId, orderId).stream() - .map(com.baiyue.order.entity.OrderInternalNoteEntity::getContent) - .filter(java.util.Objects::nonNull).map(String::trim).filter(value -> !value.isEmpty()) - .findFirst().orElse(null); return ApiResponse.success(new CargoBinding(cargo.getId(), orderId, cargo.getItemNo(), cargo.getCargoName(), cargo.getPackageCount(), cargo.getGrossWeightKg(), cargo.getVolumeCbm(), cargo.getSkuNo(), cargo.getCargoNameEn(), cargo.getUnitsPerPackage(), cargo.getTotalQuantity(), - cargo.getNetWeightKg(), cargo.getCargoRemark(), order.getCustomerRemark(), latestInternalNote)); + cargo.getNetWeightKg(), cargo.getCargoRemark(), order.getCustomerRemark(), order.getInternalRemark(), + order.getWarehouseRemark(), order.getCustomerServiceRemark())); } @GetMapping("/{tenantId}/{orderId}") @@ -167,12 +164,13 @@ public class InternalOrderCargoController { java.math.BigDecimal originalVolumeCbm, String skuNo, String cargoNameEn, Integer unitsPerPackage, Integer totalQuantity, java.math.BigDecimal netWeightKg, String cargoRemark, - String customerRemark, String latestInternalNote) { + String customerRemark, String latestInternalNote, + String warehouseRemark, String customerServiceRemark) { public CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName, Integer originalPackageCount, java.math.BigDecimal originalGrossWeightKg, java.math.BigDecimal originalVolumeCbm) { this(cargoId, orderId, itemNo, cargoName, originalPackageCount, - originalGrossWeightKg, originalVolumeCbm, null, null, null, null, null, null, null, null); + originalGrossWeightKg, originalVolumeCbm, null, null, null, null, null, null, null, null, null, null); } public CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName, Integer originalPackageCount, java.math.BigDecimal originalGrossWeightKg, @@ -181,7 +179,7 @@ public class InternalOrderCargoController { String cargoRemark) { this(cargoId, orderId, itemNo, cargoName, originalPackageCount, originalGrossWeightKg, originalVolumeCbm, skuNo, cargoNameEn, unitsPerPackage, totalQuantity, netWeightKg, - cargoRemark, null, null); + cargoRemark, null, null, null, null); } } diff --git a/order/src/main/java/com/baiyue/order/controller/PlatformFirstLegOrderController.java b/order/src/main/java/com/baiyue/order/controller/PlatformFirstLegOrderController.java index 3eab42d11..419b2d182 100644 --- a/order/src/main/java/com/baiyue/order/controller/PlatformFirstLegOrderController.java +++ b/order/src/main/java/com/baiyue/order/controller/PlatformFirstLegOrderController.java @@ -282,23 +282,6 @@ public class PlatformFirstLegOrderController { return transit(user, tenantId, id, OrderStatus.COMPLETED, null); } - @BusinessAudit(event = "ORDER_INTERNAL_NOTE", resourceType = "FIRST_LEG_ORDER", resourceId = "#id", description = "Add internal note / 添加内部备注") - @Operation(summary = "Add internal note / 添加内部备注", description = "Adds a platform-only internal note to the target tenant order. / 为目标租户订单添加仅平台可见的内部备注。") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Note added / 备注添加成功"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限"), - }) - @PostMapping("/{id}/internal-notes") - @PreAuthorize("hasAuthority('platform:order:note')") - public ApiResponse note(@AuthenticationPrincipal AuthenticatedUser user, - @Parameter(description = "Target tenant ID / 目标租户 ID", example = "10001", required = true) @RequestParam long tenantId, - @Parameter(description = "Order ID / 订单 ID", example = "190000000000000001", required = true) @PathVariable long id, - @Valid @RequestBody OrderRequests.NoteRequest request) { - return ApiResponse.success(service.addNote( - OrderScopeGuard.platform(user, tenantId), user.userId(), id, request.content() - )); - } - /** 仅履约权限可登记快递信息,商户端保持只读。 */ @PutMapping("/{id}/express") @PreAuthorize("hasAuthority('platform:order:fulfillment')") diff --git a/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java b/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java index 60c4b8a48..7c18df6c5 100644 --- a/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java +++ b/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java @@ -112,16 +112,16 @@ public class PlatformUnifiedOrderController { return ApiResponse.success(splitService.containerSlices(containerBookingId, tenantId)); } - @PostMapping("/{orderId}/internal-notes") - @BusinessAudit(event = "ORDER_INTERNAL_NOTE", resourceType = "FIRST_LEG_ORDER", resourceId = "#orderId", description = "Add internal note / 添加内部备注") + @PutMapping("/{orderId}/remarks") + @BusinessAudit(event = "ORDER_REMARKS_UPDATE", resourceType = "FIRST_LEG_ORDER", resourceId = "#orderId", description = "更新订单协作备注") @PreAuthorize("hasAuthority('platform:order:note')") - @Operation(summary = "添加订单内部备注", description = "添加仅平台内部可见的订单备注。") - public ApiResponse addInternalNote( + @Operation(summary = "更新订单协作备注", description = "客户备注只读;平台可更新内部备注、仓库备注和客服备注。") + public ApiResponse updateRemarks( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long orderId, - @Valid @RequestBody OrderRequests.NoteRequest request) { + @Valid @RequestBody OrderRequests.RemarksUpdateRequest request) { long scopedTenant = OrderScopeGuard.platform(user, tenantId); - return ApiResponse.success(orderService.addNote(scopedTenant, user.userId(), orderId, request.content())); + return ApiResponse.success(orderService.updateRemarks(scopedTenant, user.userId(), orderId, request)); } } diff --git a/order/src/main/java/com/baiyue/order/dto/OrderRequests.java b/order/src/main/java/com/baiyue/order/dto/OrderRequests.java index 48fa55833..068440df2 100644 --- a/order/src/main/java/com/baiyue/order/dto/OrderRequests.java +++ b/order/src/main/java/com/baiyue/order/dto/OrderRequests.java @@ -316,12 +316,17 @@ public final class OrderRequests { } } - @Schema(name = "OrderInternalNoteRequest", description = "Platform internal order note / 平台内部订单备注") - public record NoteRequest( - @Schema(description = "Internal note content / 内部备注内容", example = "Customer confirmed the shipping schedule / 客户已确认船期", maxLength = 2000, requiredMode = Schema.RequiredMode.REQUIRED) - @NotBlank @Size(max = 2000) String content - ) { - } + @Schema(name = "OrderRemarksUpdateRequest", description = "平台订单协作备注更新") + public record RemarksUpdateRequest( + @NotNull @Min(0) Integer expectedVersion, + @Size(max = 1000) String internalRemark, + @Size(max = 1000) String warehouseRemark, + @Size(max = 1000) String customerServiceRemark + ) { } + + public record RemarksUpdateResponse(Integer version, String customerRemark, + String internalRemark, String warehouseRemark, + String customerServiceRemark) { } public record ExpressRequest( @NotBlank @Size(max = 64) diff --git a/order/src/main/java/com/baiyue/order/dto/UnifiedOrderResponse.java b/order/src/main/java/com/baiyue/order/dto/UnifiedOrderResponse.java index 63de363b4..d07584cd9 100644 --- a/order/src/main/java/com/baiyue/order/dto/UnifiedOrderResponse.java +++ b/order/src/main/java/com/baiyue/order/dto/UnifiedOrderResponse.java @@ -35,7 +35,7 @@ public final class UnifiedOrderResponse { MeasurementSummary measurement, PreloadSummary preload, LoadingSummary loading, DeliverySummary delivery, TransportSummary transport, CoLoadGroupSummary coLoadGroup, - String customerRemark, String latestInternalNote, + String customerRemark, String latestInternalNote, String warehouseRemark, String customerServiceRemark, LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime requestedPickupAt ) {} diff --git a/order/src/main/java/com/baiyue/order/entity/OrderEntity.java b/order/src/main/java/com/baiyue/order/entity/OrderEntity.java index 155abaa78..ec298ab30 100644 --- a/order/src/main/java/com/baiyue/order/entity/OrderEntity.java +++ b/order/src/main/java/com/baiyue/order/entity/OrderEntity.java @@ -35,6 +35,9 @@ public class OrderEntity extends BaseEntity { private LocalDateTime expectedArrivalAt; private BigDecimal estimatedVolumeCbm; private String customerRemark; + private String internalRemark; + private String warehouseRemark; + private String customerServiceRemark; private String pickupMethod; private LocalDateTime requestedPickupAt; private Long receivingWarehouseId; diff --git a/order/src/main/java/com/baiyue/order/repository/OrderRepository.java b/order/src/main/java/com/baiyue/order/repository/OrderRepository.java index 40c1b715a..34f5670ad 100644 --- a/order/src/main/java/com/baiyue/order/repository/OrderRepository.java +++ b/order/src/main/java/com/baiyue/order/repository/OrderRepository.java @@ -117,6 +117,21 @@ public class OrderRepository { .set(OrderCargoEntity::getUpdatedAt, cargo.getUpdatedAt()) .setSql("version = version + 1")); } + + public int updateRemarks(long tenantId, long orderId, int expectedVersion, long operatorId, + String internalRemark, String warehouseRemark, String customerServiceRemark) { + return orderMapper.update(null, new LambdaUpdateWrapper() + .eq(OrderEntity::getTenantId, tenantId) + .eq(OrderEntity::getId, orderId) + .eq(OrderEntity::getVersion, expectedVersion) + .eq(OrderEntity::getDeleted, false) + .set(OrderEntity::getInternalRemark, internalRemark) + .set(OrderEntity::getWarehouseRemark, warehouseRemark) + .set(OrderEntity::getCustomerServiceRemark, customerServiceRemark) + .set(OrderEntity::getUpdatedBy, operatorId) + .set(OrderEntity::getUpdatedAt, LocalDateTime.now()) + .setSql("version = version + 1")); + } public void insertCargo(OrderCargoEntity cargo) { cargoMapper.insert(cargo); } public void insertParty(OrderPartyEntity party) { partyMapper.insert(party); } public void insertNote(OrderInternalNoteEntity note) { noteMapper.insert(note); } diff --git a/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java b/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java index 8f2a76c8b..fd6c83daf 100644 --- a/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java +++ b/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java @@ -268,11 +268,29 @@ public class OrderApplicationService { return new OrderDetailResponse(order, repository.findCargo(tenantId, id), repository.findParties(tenantId, id), List.of(), repository.findHistory(tenantId, id)); } - public Long addNote(long tenantId, long userId, long id, String content) { - require(tenantId, id); - OrderInternalNoteEntity note = new OrderInternalNoteEntity(); - note.setTenantId(tenantId); note.setOrderId(id); note.setContent(content); - note.setCreatedBy(userId); note.setUpdatedBy(userId); repository.insertNote(note); return note.getId(); + @Transactional + public OrderRequests.RemarksUpdateResponse updateRemarks(long tenantId, long userId, long id, + OrderRequests.RemarksUpdateRequest request) { + OrderEntity order = require(tenantId, id); + String internalRemark = normalizeRemark(request.internalRemark()); + String warehouseRemark = normalizeRemark(request.warehouseRemark()); + String customerServiceRemark = normalizeRemark(request.customerServiceRemark()); + if (repository.updateRemarks(tenantId, id, request.expectedVersion(), userId, + internalRemark, warehouseRemark, customerServiceRemark) != 1) { + throw new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT, "订单备注已被其他人修改,请刷新后重试"); + } + if (!java.util.Objects.equals(order.getInternalRemark(), internalRemark) && internalRemark != null) { + OrderInternalNoteEntity note = new OrderInternalNoteEntity(); + note.setTenantId(tenantId); note.setOrderId(id); note.setContent(internalRemark); + note.setCreatedBy(userId); note.setUpdatedBy(userId); repository.insertNote(note); + } + return new OrderRequests.RemarksUpdateResponse(order.getVersion() + 1, order.getCustomerRemark(), + internalRemark, warehouseRemark, customerServiceRemark); + } + + private static String normalizeRemark(String value) { + if (value == null || value.trim().isEmpty()) return null; + return value.trim(); } @Transactional diff --git a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java index 4aa72693c..5621e0ae9 100644 --- a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java +++ b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java @@ -11,7 +11,6 @@ import com.baiyue.order.dto.UnifiedOrderQuery; import com.baiyue.order.dto.UnifiedOrderResponse; import com.baiyue.order.entity.OrderEntity; import com.baiyue.order.entity.OrderCargoEntity; -import com.baiyue.order.entity.OrderInternalNoteEntity; import com.baiyue.order.entity.OrderStatus; import com.baiyue.order.repository.OrderRepository; import feign.FeignException; @@ -66,11 +65,10 @@ public class UnifiedOrderQueryService { List selected = orderNos.stream().map(orderByNo::get).filter(Objects::nonNull).toList(); List orderIds = selected.stream().map(OrderEntity::getId).toList(); Map inboundByOrder = summaries(null, orderIds); - Map latestNotes = latestNotes(null, orderIds); Map cargoSummaries = cargoSummaries(null, selected); Map childCounts = childCounts(selected); return selected.stream().map(order -> row(order, inboundByOrder.get(order.getId()), - latestNotes.get(order.getId()), childCounts, cargoSummaries.get(order.getId()), null)).toList(); + order.getInternalRemark(), childCounts, cargoSummaries.get(order.getId()), null)).toList(); } private PageResult page(Long tenantId, UnifiedOrderQuery query, @@ -89,13 +87,12 @@ public class UnifiedOrderQueryService { Map inboundByOrder = "PENDING_REVIEW".equals(query.mainStatus()) ? optionalSummaries(tenantId, sourceOrderIds) : summaries(tenantId, sourceOrderIds); - Map latestNotes = latestNotes(tenantId, source.records().stream().map(OrderEntity::getId).toList()); Map cargoSummaries = cargoSummaries(tenantId, source.records()); Map childCounts = childCounts(source.records()); Map coLoadByOrder = coLoadGroups( source.records().stream().map(OrderEntity::getId).toList(), includeCoLoadGroups); List rows = source.records().stream() - .map(order -> row(order, inboundByOrder.get(order.getId()), latestNotes.get(order.getId()), + .map(order -> row(order, inboundByOrder.get(order.getId()), order.getInternalRemark(), childCounts, cargoSummaries.get(order.getId()), coLoadByOrder.get(order.getId()))).toList(); return new PageResult<>(rows, source.page(), source.size(), source.total(), source.pages()); } @@ -117,7 +114,7 @@ public class UnifiedOrderQueryService { orders.findHistory(tenantId, orderId)); FirstlegUnifiedOrderClient.Detail inbound = order.getStatus() == OrderStatus.SUBMITTED ? optionalFirstlegDetail(tenantId, orderId) : firstlegDetail(tenantId, orderId); - String latestNote = orderDetail.internalNotes().isEmpty() ? null : orderDetail.internalNotes().get(0).getContent(); + String latestNote = order.getInternalRemark(); String creatorName = orderDetail.statusHistory().stream() .filter(item -> "CREATE".equals(item.getAction())) .map(item -> item.getOperatorName()) @@ -166,14 +163,13 @@ public class UnifiedOrderQueryService { List orderIds = summaries.stream().map(item -> item.inbound().orderId()).toList(); Map orderById = orders.findByIds(tenantId, orderIds).stream() .collect(Collectors.toMap(OrderEntity::getId, Function.identity(), (left, right) -> left)); - Map latestNotes = latestNotes(tenantId, orderIds); Map cargoSummaries = cargoSummaries(tenantId, List.copyOf(orderById.values())); Map childCounts = childCounts(List.copyOf(orderById.values())); Map coLoadByOrder = coLoadGroups( orderIds, includeCoLoadGroups); List records = summaries.stream() .map(summary -> row(summary, orderById.get(summary.inbound().orderId()), - latestNotes.get(summary.inbound().orderId()), childCounts, + orderById.get(summary.inbound().orderId()) == null ? null : orderById.get(summary.inbound().orderId()).getInternalRemark(), childCounts, cargoSummaries.get(summary.inbound().orderId()), coLoadByOrder.get(summary.inbound().orderId()))) .toList(); @@ -233,11 +229,6 @@ public class UnifiedOrderQueryService { return summary; } - private Map latestNotes(Long tenantId, List orderIds) { - return orders.findNotesByOrderIds(tenantId, orderIds).stream().collect(Collectors.toMap( - OrderInternalNoteEntity::getOrderId, OrderInternalNoteEntity::getContent, (latest, ignored) -> latest)); - } - private Map cargoSummaries(Long tenantId, List pageOrders) { if (pageOrders.isEmpty()) return Map.of(); List orderIds = pageOrders.stream().map(OrderEntity::getId).toList(); @@ -338,7 +329,7 @@ public class UnifiedOrderQueryService { null, null, null, null, null, null, null, order.getExpressCompany(), order.getExpressTrackingNo(), StringUtils.hasText(creatorName) ? creatorName : string(order.getCreatedBy()), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo), null, null, null, null, null, coLoadGroup, - order.getCustomerRemark(), latestInternalNote, + order.getCustomerRemark(), latestInternalNote, order.getWarehouseRemark(), order.getCustomerServiceRemark(), order.getCreatedAt(), order.getUpdatedAt(), order.getRequestedPickupAt()); } FirstlegUnifiedOrderClient.Inbound inbound = summary.inbound(); @@ -365,6 +356,7 @@ public class UnifiedOrderQueryService { StringUtils.hasText(creatorName) ? creatorName : inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo), measurement(inbound), preload(summary.preload()), loading(summary.loading()), delivery(summary.delivery()), transport(inbound), coLoadGroup, order.getCustomerRemark(), latestInternalNote, + order.getWarehouseRemark(), order.getCustomerServiceRemark(), order.getCreatedAt(), order.getUpdatedAt(), inbound.requestedPickupAt()); } @@ -394,6 +386,7 @@ public class UnifiedOrderQueryService { measurement(inbound), preload(summary.preload()), loading(summary.loading()), delivery(summary.delivery()), transport(inbound), coLoadGroup, order == null ? null : order.getCustomerRemark(), latestInternalNote, + order == null ? null : order.getWarehouseRemark(), order == null ? null : order.getCustomerServiceRemark(), inbound.createdAt(), inbound.updatedAt(), inbound.requestedPickupAt()); } diff --git a/order/src/main/resources/db/order.sql b/order/src/main/resources/db/order.sql index 7c3d24e80..47fd3a3bf 100644 --- a/order/src/main/resources/db/order.sql +++ b/order/src/main/resources/db/order.sql @@ -22,6 +22,9 @@ CREATE TABLE IF NOT EXISTS ord_order ( expected_arrival_at DATETIME(3) NULL COMMENT '客户申报预计到货时间', estimated_volume_cbm DECIMAL(18,4) NULL COMMENT '平台运营预估体积(立方米),独立于客户下单及实测体积', customer_remark VARCHAR(1000) NULL COMMENT '客户可见备注', + internal_remark VARCHAR(1000) NULL COMMENT '平台内部备注', + warehouse_remark VARCHAR(1000) NULL COMMENT '仓库备注', + customer_service_remark VARCHAR(1000) NULL COMMENT '客服备注', pickup_method VARCHAR(24) NULL COMMENT '取货方式', requested_pickup_at DATETIME(3) NULL COMMENT '客户要求平台上门提货的预约时间', receiving_warehouse_id BIGINT UNSIGNED NULL COMMENT '国内收货仓ID', @@ -66,6 +69,33 @@ CREATE TABLE IF NOT EXISTS ord_order ( KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id) ) ENGINE=InnoDB COMMENT='头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态'; +SET @order_internal_remark_sql = IF( + EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_order' AND table_name='ord_order' AND column_name='internal_remark'), + 'SELECT 1', + 'ALTER TABLE baiyue_order.ord_order ADD COLUMN internal_remark VARCHAR(1000) NULL COMMENT ''平台内部备注'' AFTER customer_remark' +); +PREPARE order_internal_remark_stmt FROM @order_internal_remark_sql; +EXECUTE order_internal_remark_stmt; +DEALLOCATE PREPARE order_internal_remark_stmt; + +SET @order_warehouse_remark_sql = IF( + EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_order' AND table_name='ord_order' AND column_name='warehouse_remark'), + 'SELECT 1', + 'ALTER TABLE baiyue_order.ord_order ADD COLUMN warehouse_remark VARCHAR(1000) NULL COMMENT ''仓库备注'' AFTER internal_remark' +); +PREPARE order_warehouse_remark_stmt FROM @order_warehouse_remark_sql; +EXECUTE order_warehouse_remark_stmt; +DEALLOCATE PREPARE order_warehouse_remark_stmt; + +SET @order_customer_service_remark_sql = IF( + EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_order' AND table_name='ord_order' AND column_name='customer_service_remark'), + 'SELECT 1', + 'ALTER TABLE baiyue_order.ord_order ADD COLUMN customer_service_remark VARCHAR(1000) NULL COMMENT ''客服备注'' AFTER warehouse_remark' +); +PREPARE order_customer_service_remark_stmt FROM @order_customer_service_remark_sql; +EXECUTE order_customer_service_remark_stmt; +DEALLOCATE PREPARE order_customer_service_remark_stmt; + SET @order_fulfillment_type_sql = IF( EXISTS( SELECT 1 FROM information_schema.columns diff --git a/order/src/main/resources/openapi/order-v1.yaml b/order/src/main/resources/openapi/order-v1.yaml index ad2f90bfa..382163c7a 100644 --- a/order/src/main/resources/openapi/order-v1.yaml +++ b/order/src/main/resources/openapi/order-v1.yaml @@ -267,10 +267,10 @@ paths: application/json: schema: {$ref: '#/components/schemas/UnifiedOrderDetailEnvelope'} '403': {description: 需要平台身份和 platform:order:list 权限} - /api/platform/v1/orders/{orderId}/internal-notes: - post: - summary: 添加订单内部备注 - description: 添加仅平台内部可见的订单备注,需要 platform:order:note 权限。 + /api/platform/v1/orders/{orderId}/remarks: + put: + summary: 更新订单协作备注 + description: 客户备注只读;需要 platform:order:note 权限,可更新或清空内部备注、仓库备注和客服备注。 parameters: - $ref: '#/components/parameters/UnifiedOrderId' - $ref: '#/components/parameters/UnifiedTenantId' @@ -280,9 +280,12 @@ paths: application/json: schema: type: object - required: [content] + required: [expectedVersion] properties: - content: {type: string, minLength: 1, maxLength: 2000} + expectedVersion: {type: integer, minimum: 0} + internalRemark: {type: string, nullable: true, maxLength: 1000} + warehouseRemark: {type: string, nullable: true, maxLength: 1000} + customerServiceRemark: {type: string, nullable: true, maxLength: 1000} responses: '200': {description: 备注添加成功或业务失败,按响应体 code 判断} '403': {description: 需要 platform:order:note 权限} @@ -949,6 +952,8 @@ components: coLoadGroup: {$ref: '#/components/schemas/UnifiedCoLoadGroupSummary'} customerRemark: {type: string, nullable: true} latestInternalNote: {type: string, nullable: true, description: 客户详情不返回平台内部备注} + warehouseRemark: {type: string, nullable: true} + customerServiceRemark: {type: string, nullable: true} createdAt: {type: string, format: date-time, nullable: true} updatedAt: {type: string, format: date-time, nullable: true} UnifiedOrderRelation: diff --git a/order/src/test/java/com/baiyue/order/controller/InternalOrderCargoControllerTest.java b/order/src/test/java/com/baiyue/order/controller/InternalOrderCargoControllerTest.java index f6a6486bf..99608e18e 100644 --- a/order/src/test/java/com/baiyue/order/controller/InternalOrderCargoControllerTest.java +++ b/order/src/test/java/com/baiyue/order/controller/InternalOrderCargoControllerTest.java @@ -3,7 +3,6 @@ package com.baiyue.order.controller; import com.baiyue.common.core.exception.BizException; import com.baiyue.order.entity.OrderCargoEntity; import com.baiyue.order.entity.OrderEntity; -import com.baiyue.order.entity.OrderInternalNoteEntity; import com.baiyue.order.repository.OrderRepository; import com.baiyue.order.service.OrderApplicationService; import com.baiyue.order.service.PrintableInboundSnapshotService; @@ -50,9 +49,10 @@ class InternalOrderCargoControllerTest { cargo.setCargoRemark("内部货物备注"); when(orders.findCargoInheritedByOrder(11L, 21L, 31L)).thenReturn(cargo); OrderEntity order = new OrderEntity(); order.setId(21L); order.setCustomerRemark("客户订单备注"); - OrderInternalNoteEntity note = new OrderInternalNoteEntity(); note.setContent("最新内部备注"); + order.setInternalRemark("最新内部备注"); + order.setWarehouseRemark("仓库备注"); + order.setCustomerServiceRemark("客服备注"); when(orders.findOwned(11L, 21L)).thenReturn(order); - when(orders.findNotes(11L, 21L)).thenReturn(List.of(note)); var response = controller.detail("internal-token", 11L, 21L, 31L); @@ -67,6 +67,8 @@ class InternalOrderCargoControllerTest { assertThat(response.data().cargoRemark()).isEqualTo("内部货物备注"); assertThat(response.data().customerRemark()).isEqualTo("客户订单备注"); assertThat(response.data().latestInternalNote()).isEqualTo("最新内部备注"); + assertThat(response.data().warehouseRemark()).isEqualTo("仓库备注"); + assertThat(response.data().customerServiceRemark()).isEqualTo("客服备注"); verify(orders).findCargoInheritedByOrder(11L, 21L, 31L); } diff --git a/order/src/test/java/com/baiyue/order/controller/OrderControllerPermissionTest.java b/order/src/test/java/com/baiyue/order/controller/OrderControllerPermissionTest.java index 5d116b78b..36c238443 100644 --- a/order/src/test/java/com/baiyue/order/controller/OrderControllerPermissionTest.java +++ b/order/src/test/java/com/baiyue/order/controller/OrderControllerPermissionTest.java @@ -64,9 +64,9 @@ class OrderControllerPermissionTest { } @Test - void unifiedPlatformInternalNoteRequiresNotePermission() throws Exception { - assertAuthority(PlatformUnifiedOrderController.class, "addInternalNote", "hasAuthority('platform:order:note')", - AuthenticatedUser.class, long.class, long.class, OrderRequests.NoteRequest.class); + void unifiedPlatformRemarksRequireNotePermission() throws Exception { + assertAuthority(PlatformUnifiedOrderController.class, "updateRemarks", "hasAuthority('platform:order:note')", + AuthenticatedUser.class, long.class, long.class, OrderRequests.RemarksUpdateRequest.class); } private void assertAuthority(Class controller, String methodName, String expected, Class... parameterTypes) diff --git a/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java b/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java index bf6ea159b..a92c8b645 100644 --- a/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java +++ b/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java @@ -697,6 +697,48 @@ class OrderApplicationServiceStateTest { .isEqualTo("HDHK26081410000"); } + @Test + void updatesThreeEditableRemarksAndKeepsCustomerRemarkReadOnly() { + OrderRepository repository = mock(OrderRepository.class); + OrderEntity order = new OrderEntity(); + order.setId(77L); + order.setTenantId(10001L); + order.setVersion(3); + order.setCustomerRemark("客户原始备注"); + order.setInternalRemark("旧内部备注"); + when(repository.findOwned(10001L, 77L)).thenReturn(order); + when(repository.updateRemarks(10001L, 77L, 3, 9L, "新内部备注", "仓库备注", "客服备注")).thenReturn(1); + + var response = service(repository, mock(OrderEligibilityGuard.class), mock(OrderMasterDataGuard.class)) + .updateRemarks(10001L, 9L, 77L, + new OrderRequests.RemarksUpdateRequest(3, " 新内部备注 ", "仓库备注", "客服备注")); + + assertThat(response.version()).isEqualTo(4); + assertThat(response.customerRemark()).isEqualTo("客户原始备注"); + assertThat(response.internalRemark()).isEqualTo("新内部备注"); + assertThat(response.warehouseRemark()).isEqualTo("仓库备注"); + assertThat(response.customerServiceRemark()).isEqualTo("客服备注"); + verify(repository).insertNote(argThat(note -> note.getTenantId().equals(10001L) + && note.getOrderId().equals(77L) && note.getContent().equals("新内部备注"))); + } + + @Test + void rejectsRemarkUpdateWhenOrderVersionChanged() { + OrderRepository repository = mock(OrderRepository.class); + OrderEntity order = new OrderEntity(); + order.setId(77L); + order.setTenantId(10001L); + order.setVersion(4); + when(repository.findOwned(10001L, 77L)).thenReturn(order); + when(repository.updateRemarks(anyLong(), anyLong(), anyInt(), anyLong(), any(), any(), any())).thenReturn(0); + + assertThatThrownBy(() -> service(repository, mock(OrderEligibilityGuard.class), mock(OrderMasterDataGuard.class)) + .updateRemarks(10001L, 9L, 77L, new OrderRequests.RemarksUpdateRequest(3, null, null, null))) + .isInstanceOf(BizException.class) + .hasMessageContaining("请刷新后重试"); + verify(repository, never()).insertNote(any()); + } + private OrderApplicationService service(OrderRepository repository, OrderEligibilityGuard eligibility, OrderMasterDataGuard masterData) { OrderApplicationService service = springService; diff --git a/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java b/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java index f502162aa..d7f5f484e 100644 --- a/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java +++ b/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java @@ -10,7 +10,6 @@ import com.baiyue.order.dto.UnifiedOrderQuery; import com.baiyue.order.dto.UnifiedOrderResponse; import com.baiyue.order.entity.OrderCargoEntity; import com.baiyue.order.entity.OrderEntity; -import com.baiyue.order.entity.OrderInternalNoteEntity; import com.baiyue.order.entity.OrderStatusHistoryEntity; import com.baiyue.order.entity.OrderStatus; import com.baiyue.order.repository.OrderRepository; @@ -58,14 +57,13 @@ class UnifiedOrderQueryServiceTest { OrderEntity order = order(OrderStatus.SUBMITTED); order.setInboundCode("RK-77"); order.setCustomerRemark("客户要求提货前联系"); - OrderInternalNoteEntity note = new OrderInternalNoteEntity(); - note.setOrderId(77L); - note.setContent("客服已确认提货时间"); + order.setInternalRemark("客服已确认提货时间"); + order.setWarehouseRemark("仓库请优先处理"); + order.setCustomerServiceRemark("客服已回访"); when(orders.unifiedPage(eq(10001L), eq(query), eq(List.of(OrderStatus.SUBMITTED)))) .thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1)); when(firstleg.summaries(eq("internal-token"), any())) .thenReturn(ApiResponse.success(List.of())); - when(orders.findNotesByOrderIds(10001L, List.of(77L))).thenReturn(List.of(note)); var result = service.page(10001L, query); @@ -79,6 +77,8 @@ class UnifiedOrderQueryServiceTest { assertThat(row.currentHoldReason()).isNull(); assertThat(row.customerRemark()).isEqualTo("客户要求提货前联系"); assertThat(row.latestInternalNote()).isEqualTo("客服已确认提货时间"); + assertThat(row.warehouseRemark()).isEqualTo("仓库请优先处理"); + assertThat(row.customerServiceRemark()).isEqualTo("客服已回访"); assertThat(row.relation()).isNull(); }); verify(firstleg).summaries(eq("internal-token"), any());