feat: add editable order collaboration remarks

This commit is contained in:
2026-09-14 16:53:09 +08:00
parent 26c37deb55
commit e0af45db02
32 changed files with 356 additions and 175 deletions
@@ -579,6 +579,9 @@ CREATE TABLE IF NOT EXISTS ord_order (
expected_arrival_at DATETIME(3) NULL COMMENT '客户申报预计到货时间', expected_arrival_at DATETIME(3) NULL COMMENT '客户申报预计到货时间',
estimated_volume_cbm DECIMAL(18,4) NULL COMMENT '平台运营预估体积(立方米),独立于客户下单及实测体积', estimated_volume_cbm DECIMAL(18,4) NULL COMMENT '平台运营预估体积(立方米),独立于客户下单及实测体积',
customer_remark VARCHAR(1000) 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 '取货方式', pickup_method VARCHAR(24) NULL COMMENT '取货方式',
requested_pickup_at DATETIME(3) NULL COMMENT '客户要求平台上门提货的预约时间', requested_pickup_at DATETIME(3) NULL COMMENT '客户要求平台上门提货的预约时间',
receiving_warehouse_id BIGINT UNSIGNED NULL COMMENT '国内收货仓逻辑ID', 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) KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id)
) ENGINE=InnoDB COMMENT='头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态'; ) 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( SET @order_fulfillment_type_sql = IF(
EXISTS( EXISTS(
SELECT 1 FROM information_schema.columns 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 '平台内部备注', internal_remark VARCHAR(1000) NULL COMMENT '平台内部备注',
customer_remark VARCHAR(1000) NULL COMMENT '客户备注快照', customer_remark VARCHAR(1000) NULL COMMENT '客户备注快照',
warehouse_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_at DATETIME(3) NULL COMMENT '装柜完成后的永久冻结时间',
frozen_by BIGINT UNSIGNED NULL COMMENT '冻结操作人ID', frozen_by BIGINT UNSIGNED NULL COMMENT '冻结操作人ID',
removed_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)) CONSTRAINT chk_loading_item_deleted CHECK (deleted IN (0, 1))
) ENGINE=InnoDB COMMENT='智能装柜客户货物明细;客户归属仅在明细层'; ) 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 ( CREATE TABLE IF NOT EXISTS fl_loading_pallet_label (
id BIGINT UNSIGNED NOT NULL COMMENT '散货托盘标签ID(雪花ID)', id BIGINT UNSIGNED NOT NULL COMMENT '散货托盘标签ID(雪花ID)',
loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID', loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID',
@@ -6154,7 +6194,9 @@ BEGIN
IF current_plan_status = 'LOADING' AND EXISTS ( IF current_plan_status = 'LOADING' AND EXISTS (
SELECT 1 FROM fl_loading_pick_item SELECT 1 FROM fl_loading_pick_item
WHERE loading_plan_item_id = OLD.id AND picked_package_count > 0) THEN 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 IF NEW.package_count <> OLD.package_count OR NEW.deleted <> OLD.deleted
OR NOT (NEW.customer_name <=> OLD.customer_name) OR NOT (NEW.customer_name <=> OLD.customer_name)
OR NOT (NEW.inbound_id <=> OLD.inbound_id) OR NOT (NEW.inbound_id <=> OLD.inbound_id)
@@ -6182,14 +6224,13 @@ BEGIN
OR NOT (NEW.shipping_mark <=> OLD.shipping_mark) OR NOT (NEW.shipping_mark <=> OLD.shipping_mark)
OR NOT (NEW.load_sequence <=> OLD.load_sequence) OR NOT (NEW.load_sequence <=> OLD.load_sequence)
OR NOT (NEW.position_snapshot <=> OLD.position_snapshot) 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.customer_remark <=> OLD.customer_remark)
OR NOT (NEW.frozen_at <=> OLD.frozen_at) OR NOT (NEW.frozen_at <=> OLD.frozen_at)
OR NOT (NEW.frozen_by <=> OLD.frozen_by) OR NOT (NEW.frozen_by <=> OLD.frozen_by)
OR NOT (NEW.removed_by <=> OLD.removed_by) OR NOT (NEW.removed_by <=> OLD.removed_by)
OR NOT (NEW.removed_by_name <=> OLD.removed_by_name) OR NOT (NEW.removed_by_name <=> OLD.removed_by_name)
OR NOT (NEW.removed_at <=> OLD.removed_at) THEN 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; END IF;
ELSEIF NOT EXISTS ( ELSEIF NOT EXISTS (
SELECT 1 FROM fl_loading_pick_item SELECT 1 FROM fl_loading_pick_item
@@ -6224,7 +6265,6 @@ BEGIN
OR NOT (NEW.shipping_mark <=> OLD.shipping_mark) OR NOT (NEW.shipping_mark <=> OLD.shipping_mark)
OR NOT (NEW.load_sequence <=> OLD.load_sequence) OR NOT (NEW.load_sequence <=> OLD.load_sequence)
OR NOT (NEW.position_snapshot <=> OLD.position_snapshot) 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.customer_remark <=> OLD.customer_remark)
OR NOT (NEW.frozen_at <=> OLD.frozen_at) OR NOT (NEW.frozen_at <=> OLD.frozen_at)
OR NOT (NEW.frozen_by <=> OLD.frozen_by) 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', 'version', '乐观锁版本号');
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'volume_cbm', '本次装柜体积m3'); 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', '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', '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', 'exception_release', '是否异常放行');
CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'id', '测量审核记录ID'); 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_id', '客户逻辑ID');
CALL queue_column_comment('baiyue_order', 'ord_order', 'customer_name', '下单时客户名称快照'); 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', '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', 'customs_inspection', '是否需要商检:0否、1是');
CALL queue_column_comment('baiyue_order', 'ord_order', 'deleted', '逻辑删除标记'); CALL queue_column_comment('baiyue_order', 'ord_order', 'deleted', '逻辑删除标记');
CALL queue_column_comment('baiyue_order', 'ord_order', 'destination_country_code', '目的国家二字码'); CALL queue_column_comment('baiyue_order', 'ord_order', 'destination_country_code', '目的国家二字码');
@@ -44,12 +44,13 @@ public interface OrderCargoClient {
Integer originalPackageCount, BigDecimal originalGrossWeightKg, Integer originalPackageCount, BigDecimal originalGrossWeightKg,
BigDecimal originalVolumeCbm, String skuNo, String cargoNameEn, BigDecimal originalVolumeCbm, String skuNo, String cargoNameEn,
Integer unitsPerPackage, Integer totalQuantity, BigDecimal netWeightKg, 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, public CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
Integer originalPackageCount, BigDecimal originalGrossWeightKg, Integer originalPackageCount, BigDecimal originalGrossWeightKg,
BigDecimal originalVolumeCbm) { BigDecimal originalVolumeCbm) {
this(cargoId, orderId, itemNo, cargoName, originalPackageCount, 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, public CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
Integer originalPackageCount, BigDecimal originalGrossWeightKg, Integer originalPackageCount, BigDecimal originalGrossWeightKg,
@@ -58,7 +59,7 @@ public interface OrderCargoClient {
String cargoRemark) { String cargoRemark) {
this(cargoId, orderId, itemNo, cargoName, originalPackageCount, originalGrossWeightKg, this(cargoId, orderId, itemNo, cargoName, originalPackageCount, originalGrossWeightKg,
originalVolumeCbm, skuNo, cargoNameEn, unitsPerPackage, totalQuantity, netWeightKg, originalVolumeCbm, skuNo, cargoNameEn, unitsPerPackage, totalQuantity, netWeightKg,
cargoRemark, null, null); cargoRemark, null, null, null, null);
} }
} }
@@ -38,8 +38,8 @@ public class PlatformSmartLoadingController {
public ApiResponse<Plan> 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()));} public ApiResponse<Plan> 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')") @PatchMapping("/plans/{planId}/items/{itemId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
public ApiResponse<Plan> 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()));} public ApiResponse<Plan> 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')") @PatchMapping("/plans/{planId}/items/{itemId}/remarks") @PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
public ApiResponse<Plan> 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()));} public ApiResponse<Plan> 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')") @DeleteMapping("/plans/{planId}/items/{itemId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:delete')")
public ApiResponse<Plan> 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()));} public ApiResponse<Plan> 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')") @DeleteMapping("/plans/{planId}/unpicked-items") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:delete')")
@@ -240,6 +240,7 @@ public final class LoadingListDtos {
String internalRemark, String internalRemark,
String customerRemark, String customerRemark,
String warehouseRemark, String warehouseRemark,
String customerServiceRemark,
int loadSequence, int loadSequence,
LocalDateTime frozenAt, LocalDateTime frozenAt,
Integer itemVersion Integer itemVersion
@@ -83,7 +83,8 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
@NotNull @Min(0) Integer expectedItemVersion,@NotBlank @Size(max=128) String operationId) {} @NotNull @Min(0) Integer expectedItemVersion,@NotBlank @Size(max=128) String operationId) {}
public record RemoveUnpickedRequest(@NotNull @Min(0) Integer expectedPlanVersion, public record RemoveUnpickedRequest(@NotNull @Min(0) Integer expectedPlanVersion,
@NotBlank @Size(max=128) String operationId) {} @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, @NotNull @Min(0) Integer expectedPlanVersion, @NotNull @Min(0) Integer expectedItemVersion,
@NotBlank @Size(max=128) String operationId) {} @NotBlank @Size(max=128) String operationId) {}
public record PlanCoLoadGroup(Long groupId,String groupNo,Long lockedPlanId,Integer memberOrderCount, public record PlanCoLoadGroup(Long groupId,String groupNo,Long lockedPlanId,Integer memberOrderCount,
@@ -9,7 +9,7 @@ public class LoadingPlanItemEntity {
private String unitType; private Long palletId; private String palletNo; private Boolean indivisible; 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 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 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; private Long removedBy; private String removedByName; private LocalDateTime removedAt; @TableLogic private Boolean deleted;
@TableField(exist=false) private Long activeInboundDetailId; @TableField(exist=false) private Long activeInboundDetailId;
@TableField(exist=false) private Integer pickedPackageCount; @TableField(exist=false) private Integer pickedPackageCount;
@@ -194,7 +194,7 @@ public interface LoadingListQueryMapper {
* i.package_count / NULLIF(inbound.total_package_count,0)),0) pallet_count, * i.package_count / NULLIF(inbound.total_package_count,0)),0) pallet_count,
gross_weight_kg, net_weight_kg, volume_cbm, dimensional_weight_kg, gross_weight_kg, net_weight_kg, volume_cbm, dimensional_weight_kg,
product_quantity, units_per_carton, fba_tracking_no, shipment_no, 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 load_sequence, frozen_at, i.version AS item_version
FROM fl_loading_plan_item i FROM fl_loading_plan_item i
JOIN fl_loading_plan p ON p.id=i.loading_plan_id JOIN fl_loading_plan p ON p.id=i.loading_plan_id
@@ -457,6 +457,7 @@ public interface LoadingListQueryMapper {
private String internalRemark; private String internalRemark;
private String customerRemark; private String customerRemark;
private String warehouseRemark; private String warehouseRemark;
private String customerServiceRemark;
private Integer loadSequence; private Integer loadSequence;
private LocalDateTime frozenAt; private LocalDateTime frozenAt;
private Integer itemVersion; private Integer itemVersion;
@@ -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); int removeEditing(@Param("planId") long planId,@Param("itemId") long itemId,@Param("expectedVersion") int expectedVersion,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Update(""" @Update("""
UPDATE fl_loading_plan_item i 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 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 WHERE i.id=#{itemId} AND i.loading_plan_id=#{planId} AND i.deleted=0 AND i.frozen_at IS NULL
AND i.version=#{expectedItemVersion} 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') 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("touchedPlanVersion") int touchedPlanVersion,
@Param("expectedItemVersion") int expectedItemVersion, @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); @Param("operatorName") String operatorName);
} }
@@ -193,7 +193,7 @@ public class LoadingListRepository {
decimal(row.getGrossWeightKg()), decimal(row.getNetWeightKg()), decimal(row.getVolumeCbm()), decimal(row.getGrossWeightKg()), decimal(row.getNetWeightKg()), decimal(row.getVolumeCbm()),
decimal(row.getDimensionalWeightKg()), value(row.getProductQuantity()), decimal(row.getDimensionalWeightKg()), value(row.getProductQuantity()),
integer(row.getUnitsPerCarton()), row.getFbaTrackingNo(), row.getShipmentNo(), row.getShippingMark(), 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(); integer(row.getLoadSequence()), row.getFrozenAt(), row.getItemVersion())).toList();
} }
@@ -147,10 +147,11 @@ public class SmartLoadingRepository {
public int remove(long planId, long itemId, int version, long operatorId, String operatorName) { public int remove(long planId, long itemId, int version, long operatorId, String operatorName) {
return items.removeEditing(planId, itemId, version, operatorId, operatorName); return items.removeEditing(planId, itemId, version, operatorId, operatorName);
} }
public int updateWarehouseRemark(long planId, long itemId, int touchedPlanVersion, int expectedItemVersion, public int updateRemarks(long planId, long itemId, int touchedPlanVersion, int expectedItemVersion,
String remark, long operatorId, String operatorName) { String internalRemark, String warehouseRemark, String customerServiceRemark,
return items.updateWarehouseRemark(planId, itemId, touchedPlanVersion, expectedItemVersion, long operatorId, String operatorName) {
remark, operatorId, operatorName); return items.updateRemarks(planId, itemId, touchedPlanVersion, expectedItemVersion,
internalRemark, warehouseRemark, customerServiceRemark, operatorId, operatorName);
} }
public int startPicking(long planId, long operatorId, String operatorName) { public int startPicking(long planId, long operatorId, String operatorName) {
return plans.startPicking(planId, operatorId, operatorName); return plans.startPicking(planId, operatorId, operatorName);
@@ -1043,42 +1043,55 @@ public class SmartLoadingService {
} }
@Transactional @Transactional
@BusinessAudit(event = "SMART_LOADING_WAREHOUSE_REMARK", resourceType = "LOADING_PLAN", @BusinessAudit(event = "SMART_LOADING_REMARKS", resourceType = "LOADING_PLAN",
resourceId = "#planId", description = "修改装柜仓库备注") resourceId = "#planId", description = "修改装柜协作备注")
public Plan updateWarehouseRemark(long planId, long itemId, WarehouseRemarkRequest request, public Plan updateRemarks(long planId, long itemId, RemarksRequest request,
long operatorId, String operatorName) { long operatorId, String operatorName) {
String operation = operation(request.operationId()); String operation = operation(request.operationId());
String remark = trim(request.warehouseRemark()); String internalRemark = nullableRemark(request.internalRemark());
if (!StringUtils.hasText(remark)) remark = null; String warehouseRemark = nullableRemark(request.warehouseRemark());
String requestHash = sha256("WAREHOUSE_REMARK|" + planId + '|' + itemId + '|' String customerServiceRemark = nullableRemark(request.customerServiceRemark());
+ Objects.toString(remark, "") + '|' + request.expectedPlanVersion() + '|' String requestHash = sha256("REMARKS|" + planId + '|' + itemId + '|'
+ Objects.toString(internalRemark, "") + '|' + Objects.toString(warehouseRemark, "") + '|'
+ Objects.toString(customerServiceRemark, "") + '|' + request.expectedPlanVersion() + '|'
+ request.expectedItemVersion()); + request.expectedItemVersion());
Plan replay = replay(operation, planId, "WAREHOUSE_REMARK", itemId, requestHash); Plan replay = replay(operation, planId, "REMARKS", itemId, requestHash);
if (replay != null) return replay; if (replay != null) return replay;
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName); LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName);
LoadingPlanItemEntity item = repository.item(planId, itemId); LoadingPlanItemEntity item = repository.item(planId, itemId);
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在"); 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())); 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 { try {
touch(plan, operatorId, operatorName); touch(plan, operatorId, operatorName);
if (repository.updateWarehouseRemark(planId, itemId, request.expectedPlanVersion() + 1, if (repository.updateRemarks(planId, itemId, request.expectedPlanVersion() + 1,
request.expectedItemVersion(), remark, operatorId, operatorName) != 1) { request.expectedItemVersion(), internalRemark, warehouseRemark, customerServiceRemark,
operatorId, operatorName) != 1) {
throw conflict("装柜计划或明细已被其他人修改,请刷新后重试"); throw conflict("装柜计划或明细已被其他人修改,请刷新后重试");
} }
} catch (DataAccessException exception) { } catch (DataAccessException exception) {
if (hasSqlState(exception, "45000")) { if (hasSqlState(exception, "45000")) {
throw conflict("装柜计划或明细状态已变化,仓库备注未保存,请刷新后重试"); throw conflict("装柜计划或明细状态已变化,备注未保存,请刷新后重试");
} }
throw exception; throw exception;
} }
history(planId, itemId, "WAREHOUSE_REMARK", plan.getPlanStatus(), plan.getPlanStatus(), operation, history(planId, itemId, "REMARKS", plan.getPlanStatus(), plan.getPlanStatus(), operation,
before, json(values("warehouseRemark", remark, "requestHash", requestHash)), operatorId, operatorName); before, json(values("internalRemark", internalRemark, "warehouseRemark", warehouseRemark,
"customerServiceRemark", customerServiceRemark, "requestHash", requestHash)), operatorId, operatorName);
return view(repository.plan(planId), requireBooking(plan.getContainerBookingId())); 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 @Transactional
@BusinessAudit(event = "SMART_LOADING_ITEM_DELETE", resourceType = "LOADING_PLAN_ITEM", @BusinessAudit(event = "SMART_LOADING_ITEM_DELETE", resourceType = "LOADING_PLAN_ITEM",
resourceId = "#itemId", description = "删除智能装柜明细") resourceId = "#itemId", description = "删除智能装柜明细")
@@ -1384,7 +1397,12 @@ public class SmartLoadingService {
i.setNetWeightKg(unit(c.unitNetWeightKg()).multiply(BigDecimal.valueOf(count))); i.setNetWeightKg(unit(c.unitNetWeightKg()).multiply(BigDecimal.valueOf(count)));
i.setVolumeCbm(unit(c.unitVolumeCbm()).multiply(BigDecimal.valueOf(count))); i.setVolumeCbm(unit(c.unitVolumeCbm()).multiply(BigDecimal.valueOf(count)));
i.setPalletCount(estimatedPalletCount(c, 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) { private int estimatedPalletCount(Candidate candidate, int packageCount) {
InboundOrderEntity inbound = repository.inbound(candidate.accountId(), candidate.inboundId()); InboundOrderEntity inbound = repository.inbound(candidate.accountId(), candidate.inboundId());
@@ -1397,8 +1415,6 @@ public class SmartLoadingService {
item.setSkuCode(cargo.skuNo()); item.setSkuCode(cargo.skuNo());
item.setProductNameEn(cargo.cargoNameEn()); item.setProductNameEn(cargo.cargoNameEn());
item.setUnitsPerCarton(cargo.unitsPerPackage()); item.setUnitsPerCarton(cargo.unitsPerPackage());
item.setCustomerRemark(cargo.customerRemark());
item.setInternalRemark(cargo.latestInternalNote());
int original = cargo.originalPackageCount() == null || cargo.originalPackageCount() <= 0 int original = cargo.originalPackageCount() == null || cargo.originalPackageCount() <= 0
? boxes : cargo.originalPackageCount(); ? boxes : cargo.originalPackageCount();
if (cargo.unitsPerPackage() != null) { if (cargo.unitsPerPackage() != null) {
@@ -1419,9 +1435,7 @@ public class SmartLoadingService {
&& Objects.equals(current.getSkuCode(), desired.getSkuCode()) && Objects.equals(current.getSkuCode(), desired.getSkuCode())
&& Objects.equals(current.getProductNameEn(), desired.getProductNameEn()) && Objects.equals(current.getProductNameEn(), desired.getProductNameEn())
&& Objects.equals(current.getUnitsPerCarton(), desired.getUnitsPerCarton()) && Objects.equals(current.getUnitsPerCarton(), desired.getUnitsPerCarton())
&& Objects.equals(current.getProductQuantity(), desired.getProductQuantity()) && Objects.equals(current.getProductQuantity(), desired.getProductQuantity());
&& Objects.equals(current.getCustomerRemark(), desired.getCustomerRemark())
&& Objects.equals(current.getInternalRemark(), desired.getInternalRemark());
} }
private static boolean decimalEquals(BigDecimal left, BigDecimal right) { private static boolean decimalEquals(BigDecimal left, BigDecimal right) {
@@ -1434,7 +1448,8 @@ public class SmartLoadingService {
"palletCount", item.getPalletCount(), "skuCode", item.getSkuCode(), "palletCount", item.getPalletCount(), "skuCode", item.getSkuCode(),
"productNameEn", item.getProductNameEn(), "unitsPerCarton", item.getUnitsPerCarton(), "productNameEn", item.getProductNameEn(), "unitsPerCarton", item.getUnitsPerCarton(),
"productQuantity", item.getProductQuantity(), "customerRemark", item.getCustomerRemark(), "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) { private OrderCargoClient.CargoBinding loadCargo(Candidate candidate) {
@@ -319,6 +319,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item (
internal_remark VARCHAR(1000) NULL COMMENT '平台内部备注', internal_remark VARCHAR(1000) NULL COMMENT '平台内部备注',
customer_remark VARCHAR(1000) NULL COMMENT '客户备注快照', customer_remark VARCHAR(1000) NULL COMMENT '客户备注快照',
warehouse_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_at DATETIME(3) NULL COMMENT '装柜完成后的永久冻结时间',
frozen_by BIGINT UNSIGNED NULL COMMENT '冻结操作人ID', frozen_by BIGINT UNSIGNED NULL COMMENT '冻结操作人ID',
removed_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)) CONSTRAINT chk_loading_item_deleted CHECK (deleted IN (0, 1))
) ENGINE=InnoDB COMMENT='智能装柜客户货物明细;客户归属仅在明细层'; ) 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 ( CREATE TABLE IF NOT EXISTS fl_loading_pallet_label (
id BIGINT UNSIGNED NOT NULL COMMENT '散货托盘标签ID(雪花ID)', id BIGINT UNSIGNED NOT NULL COMMENT '散货托盘标签ID(雪花ID)',
loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID', loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID',
@@ -1036,7 +1046,9 @@ BEGIN
IF current_plan_status = 'LOADING' AND EXISTS ( IF current_plan_status = 'LOADING' AND EXISTS (
SELECT 1 FROM fl_loading_pick_item SELECT 1 FROM fl_loading_pick_item
WHERE loading_plan_item_id = OLD.id AND picked_package_count > 0) THEN 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 IF NEW.package_count <> OLD.package_count OR NEW.deleted <> OLD.deleted
OR NOT (NEW.customer_name <=> OLD.customer_name) OR NOT (NEW.customer_name <=> OLD.customer_name)
OR NOT (NEW.inbound_id <=> OLD.inbound_id) OR NOT (NEW.inbound_id <=> OLD.inbound_id)
@@ -1064,14 +1076,13 @@ BEGIN
OR NOT (NEW.shipping_mark <=> OLD.shipping_mark) OR NOT (NEW.shipping_mark <=> OLD.shipping_mark)
OR NOT (NEW.load_sequence <=> OLD.load_sequence) OR NOT (NEW.load_sequence <=> OLD.load_sequence)
OR NOT (NEW.position_snapshot <=> OLD.position_snapshot) 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.customer_remark <=> OLD.customer_remark)
OR NOT (NEW.frozen_at <=> OLD.frozen_at) OR NOT (NEW.frozen_at <=> OLD.frozen_at)
OR NOT (NEW.frozen_by <=> OLD.frozen_by) OR NOT (NEW.frozen_by <=> OLD.frozen_by)
OR NOT (NEW.removed_by <=> OLD.removed_by) OR NOT (NEW.removed_by <=> OLD.removed_by)
OR NOT (NEW.removed_by_name <=> OLD.removed_by_name) OR NOT (NEW.removed_by_name <=> OLD.removed_by_name)
OR NOT (NEW.removed_at <=> OLD.removed_at) THEN 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; END IF;
ELSEIF NOT EXISTS ( ELSEIF NOT EXISTS (
SELECT 1 FROM fl_loading_pick_item SELECT 1 FROM fl_loading_pick_item
@@ -1106,7 +1117,6 @@ BEGIN
OR NOT (NEW.shipping_mark <=> OLD.shipping_mark) OR NOT (NEW.shipping_mark <=> OLD.shipping_mark)
OR NOT (NEW.load_sequence <=> OLD.load_sequence) OR NOT (NEW.load_sequence <=> OLD.load_sequence)
OR NOT (NEW.position_snapshot <=> OLD.position_snapshot) 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.customer_remark <=> OLD.customer_remark)
OR NOT (NEW.frozen_at <=> OLD.frozen_at) OR NOT (NEW.frozen_at <=> OLD.frozen_at)
OR NOT (NEW.frozen_by <=> OLD.frozen_by) OR NOT (NEW.frozen_by <=> OLD.frozen_by)
@@ -774,20 +774,21 @@ paths:
content: {application/json: {schema: {$ref: '#/components/schemas/SmartLoadingRemoveUnpickedRequest'}}} content: {application/json: {schema: {$ref: '#/components/schemas/SmartLoadingRemoveUnpickedRequest'}}}
responses: responses:
'200': {description: 成功或业务失败,data 为最新 SmartLoadingPlan} '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: patch:
summary: 修改装柜明细仓库备注 summary: 修改装柜明细协作备注
description: >- description: >-
需要 platform:firstleg:preload:edit 权限。EDITING/LOADING 可修改或清空 warehouseRemark; 需要 platform:firstleg:preload:edit 权限。客户备注只读;EDITING/LOADING 可修改或清空
internalRemark、warehouseRemark 和 customerServiceRemark;
COMPLETED、计划/明细版本冲突返回 A0409 且事务零写入,写入历史且与 PDA final complete 使用同一并发门禁。 COMPLETED、计划/明细版本冲突返回 A0409 且事务零写入,写入历史且与 PDA final complete 使用同一并发门禁。
成功响应中的 version 是递增后的 planVersion,items[].version 是递增后的 itemVersion;调用方同时使用 成功响应中的 version 是递增后的 planVersion,items[].version 是递增后的 itemVersion;调用方同时使用
canEdit、items[].canAdjust/canRemove 与自身权限决定操作可用性。数量调整或动态增量不会覆盖已有 warehouseRemark。 canEdit、items[].canAdjust/canRemove 与自身权限决定操作可用性。数量调整或动态增量不会覆盖已有协作备注。
parameters: parameters:
- {name: planId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} - {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]*$'}} - {name: itemId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
requestBody: requestBody:
required: true required: true
content: {application/json: {schema: {$ref: '#/components/schemas/SmartLoadingWarehouseRemarkRequest'}}} content: {application/json: {schema: {$ref: '#/components/schemas/SmartLoadingRemarksRequest'}}}
responses: responses:
'200': {description: 成功或业务失败,data 为最新 SmartLoadingPlan} '200': {description: 成功或业务失败,data 为最新 SmartLoadingPlan}
/openapi/v1/inbound-measurements: /openapi/v1/inbound-measurements:
@@ -2323,11 +2324,13 @@ components:
properties: properties:
expectedPlanVersion: {type: integer, minimum: 0} expectedPlanVersion: {type: integer, minimum: 0}
operationId: {type: string, minLength: 1, maxLength: 128} operationId: {type: string, minLength: 1, maxLength: 128}
SmartLoadingWarehouseRemarkRequest: SmartLoadingRemarksRequest:
type: object type: object
required: [expectedPlanVersion, expectedItemVersion, operationId] required: [expectedPlanVersion, expectedItemVersion, operationId]
properties: properties:
internalRemark: {type: string, nullable: true, maxLength: 1000, description: 内部备注;null/空字符串均表示清空}
warehouseRemark: {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} expectedPlanVersion: {type: integer, minimum: 0}
expectedItemVersion: {type: integer, minimum: 0} expectedItemVersion: {type: integer, minimum: 0}
operationId: {type: string, minLength: 1, maxLength: 128} operationId: {type: string, minLength: 1, maxLength: 128}
@@ -2518,8 +2521,9 @@ components:
inboundCode: {type: string, nullable: true} inboundCode: {type: string, nullable: true}
customerName: {type: string, nullable: true} customerName: {type: string, nullable: true}
customerRemark: {type: string, nullable: true, readOnly: true, description: 来源为订单 customerRemark} 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 修改} warehouseRemark: {type: string, nullable: true, description: 仓库人员逐条维护,可在 EDITING/LOADING 修改}
customerServiceRemark: {type: string, nullable: true, description: 客服备注;预装阶段可修改}
packageCount: {type: integer, minimum: 1} packageCount: {type: integer, minimum: 1}
totalPackageCount: {type: integer, minimum: 1, description: 当前入仓明细可在计划内分配的总箱数,包含已用与未用箱数} totalPackageCount: {type: integer, minimum: 1, description: 当前入仓明细可在计划内分配的总箱数,包含已用与未用箱数}
remainingPackageCount: {type: integer, minimum: 0, description: 当前入仓明细尚未分配到本计划的箱数} remainingPackageCount: {type: integer, minimum: 0, description: 当前入仓明细尚未分配到本计划的箱数}
@@ -405,6 +405,7 @@ components:
internalRemark: {type: string, nullable: true, description: 内部备注} internalRemark: {type: string, nullable: true, description: 内部备注}
customerRemark: {type: string, nullable: true, description: 客户备注} customerRemark: {type: string, nullable: true, description: 客户备注}
warehouseRemark: {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: 装柜顺序} loadSequence: {type: integer, format: int32, minimum: 0, description: 装柜顺序}
frozenAt: {type: string, format: date-time, nullable: true, description: 明细冻结时间} frozenAt: {type: string, format: date-time, nullable: true, description: 明细冻结时间}
itemVersion: {type: integer, format: int32, minimum: 0, description: 明细乐观锁版本;更新仓库备注时作为 expectedItemVersion} itemVersion: {type: integer, format: int32, minimum: 0, description: 明细乐观锁版本;更新仓库备注时作为 expectedItemVersion}
@@ -54,7 +54,7 @@ class PlatformSmartLoadingControllerContractTest {
authority("removeGroup","platform:firstleg:smart-loading:delete",AuthenticatedUser.class,long.class,long.class,RemoveGroupRequest.class); 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("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("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("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); authority("removeUnpicked","platform:firstleg:smart-loading:delete",AuthenticatedUser.class,long.class,RemoveUnpickedRequest.class);
} }
@@ -77,13 +77,13 @@ class PlatformSmartLoadingControllerContractTest {
.contains("SmartLoadingAdjustRequest:") .contains("SmartLoadingAdjustRequest:")
.contains("SmartLoadingDeleteRequest:") .contains("SmartLoadingDeleteRequest:")
.contains("SmartLoadingRemoveUnpickedRequest:") .contains("SmartLoadingRemoveUnpickedRequest:")
.contains("SmartLoadingWarehouseRemarkRequest:") .contains("SmartLoadingRemarksRequest:")
.contains("/items/{itemId}/warehouse-remark:") .contains("/items/{itemId}/remarks:")
.contains("需要 platform:firstleg:preload:edit 权限") .contains("需要 platform:firstleg:preload:edit 权限")
.contains("warehouseRemark: {type: string, nullable: true, maxLength: 1000") .contains("warehouseRemark: {type: string, nullable: true, maxLength: 1000")
.contains("成功响应中的 version 是递增后的 planVersion") .contains("成功响应中的 version 是递增后的 planVersion")
.contains("items[].version 是递增后的 itemVersion") .contains("items[].version 是递增后的 itemVersion")
.contains("数量调整或动态增量不会覆盖已有 warehouseRemark") .contains("数量调整或动态增量不会覆盖已有协作备注")
.contains("柜内总容积和总重量分别允许达到柜型额定值的150%") .contains("柜内总容积和总重量分别允许达到柜型额定值的150%")
.contains("人工分配不限制订单国内仓及渠道与柜次一致") .contains("人工分配不限制订单国内仓及渠道与柜次一致")
.contains("智能推荐允许跨国内仓,但仍只选择柜次渠道") .contains("智能推荐允许跨国内仓,但仍只选择柜次渠道")
@@ -122,8 +122,8 @@ class SmartLoadingSqlContractTest {
} }
@Test @Test
@DisplayName("仓库备注使用计划先行与明细单表CAS并允许已拣明细仅改备注") @DisplayName("可编辑备注使用计划先行与明细单表CAS并允许已拣明细仅改备注")
void shouldUpdateWarehouseRemarkWithoutMultiTableTriggerConflict() throws Exception { void shouldUpdateEditableRemarksWithoutMultiTableTriggerConflict() throws Exception {
String mapper = Files.readString(Path.of( String mapper = Files.readString(Path.of(
"src/main/java/com/baiyue/firstleg/mapper/LoadingPlanItemMapper.java"), StandardCharsets.UTF_8); "src/main/java/com/baiyue/firstleg/mapper/LoadingPlanItemMapper.java"), StandardCharsets.UTF_8);
assertThat(mapper) assertThat(mapper)
@@ -133,8 +133,8 @@ class SmartLoadingSqlContractTest {
.contains("i.frozen_at IS NULL") .contains("i.frozen_at IS NULL")
.doesNotContain("UPDATE fl_loading_plan_item i JOIN fl_loading_plan p") .doesNotContain("UPDATE fl_loading_plan_item i JOIN fl_loading_plan p")
.doesNotContain("p.version=p.version+1"); .doesNotContain("p.version=p.version+1");
assertWarehouseRemarkTriggerContract(migration()); assertEditableRemarksTriggerContract(migration());
assertWarehouseRemarkTriggerContract(unifiedMigration()); assertEditableRemarksTriggerContract(unifiedMigration());
} }
private static void assertContainerLifecycleSchema(String sql) { private static void assertContainerLifecycleSchema(String sql) {
@@ -151,11 +151,13 @@ class SmartLoadingSqlContractTest {
.contains("column_name='merged_into_container_booking_id'"); .contains("column_name='merged_into_container_booking_id'");
} }
private static void assertWarehouseRemarkTriggerContract(String sql) { private static void assertEditableRemarksTriggerContract(String sql) {
assertThat(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("IF NEW.package_count <> OLD.package_count OR NEW.deleted <> OLD.deleted")
.contains("已有PDA拣货记录的装柜明细修改仓库备注时不得变更其他业务字段") .contains("已有PDA拣货记录的装柜明细修改备注时不得变更其他业务字段")
.contains("ELSEIF NOT EXISTS (") .contains("ELSEIF NOT EXISTS (")
.contains("IF OLD.frozen_at IS NOT NULL THEN") .contains("IF OLD.frozen_at IS NOT NULL THEN")
.contains("装柜完成后的明细永久冻结"); .contains("装柜完成后的明细永久冻结");
@@ -21,7 +21,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainer;
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery; import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery;
import com.baiyue.firstleg.dto.SmartLoadingDtos.Plan; import com.baiyue.firstleg.dto.SmartLoadingDtos.Plan;
import com.baiyue.firstleg.dto.SmartLoadingDtos.RecommendRequest; 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.ContainerBookingEntity;
import com.baiyue.firstleg.entity.LoadingPlanEntity; import com.baiyue.firstleg.entity.LoadingPlanEntity;
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity; 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( when(cargoClient.detail(anyString(), eq(9L), eq(30L), eq(20L))).thenReturn(ApiResponse.success(
new OrderCargoClient.CargoBinding(20L, 30L, 1, "中文品名", 10, new OrderCargoClient.CargoBinding(20L, 30L, 1, "中文品名", 10,
new BigDecimal("20"), new BigDecimal("2"), "SKU-1", "English name", new BigDecimal("20"), new BigDecimal("2"), "SKU-1", "English name",
4, 100, new BigDecimal("10"), "货物备注", "客户备注", "最新内部备注"))); 4, 100, new BigDecimal("10"), "货物备注", "客户备注", "最新内部备注", "仓库备注", "客服备注")));
doAnswer(invocation -> { doAnswer(invocation -> {
Object value = invocation.getArgument(0); Object value = invocation.getArgument(0);
if (value instanceof LoadingPlanItemEntity item) item.setId(88L); if (value instanceof LoadingPlanItemEntity item) item.setId(88L);
@@ -1288,6 +1288,8 @@ class SmartLoadingServiceTest {
assertThat(saved.getNetWeightKg()).isEqualByComparingTo("7.5"); assertThat(saved.getNetWeightKg()).isEqualByComparingTo("7.5");
assertThat(saved.getCustomerRemark()).isEqualTo("客户备注"); assertThat(saved.getCustomerRemark()).isEqualTo("客户备注");
assertThat(saved.getInternalRemark()).isEqualTo("最新内部备注"); assertThat(saved.getInternalRemark()).isEqualTo("最新内部备注");
assertThat(saved.getWarehouseRemark()).isEqualTo("仓库备注");
assertThat(saved.getCustomerServiceRemark()).isEqualTo("客服备注");
assertThat(saved.getInternalRemark()).isNotEqualTo("货物备注"); assertThat(saved.getInternalRemark()).isNotEqualTo("货物备注");
ArgumentCaptor<LoadingPlanHistoryEntity> historyCaptor = ArgumentCaptor.forClass(LoadingPlanHistoryEntity.class); ArgumentCaptor<LoadingPlanHistoryEntity> historyCaptor = ArgumentCaptor.forClass(LoadingPlanHistoryEntity.class);
@@ -1451,20 +1453,20 @@ class SmartLoadingServiceTest {
when(repository.booking(99L)).thenReturn(booking()); when(repository.booking(99L)).thenReturn(booking());
when(repository.items(1L)).thenReturn(List.of(line)); when(repository.items(1L)).thenReturn(List.of(line));
when(repository.touch(1L, 2, 7L, "u")).thenReturn(1); 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, Plan result = service.updateRemarks(1L, 8L,
new WarehouseRemarkRequest(" ", 2, 0, "warehouse-clear"), 7L, "u"); new RemarksRequest(null, " ", null, 2, 0, "warehouse-clear"), 7L, "u");
assertThat(result.status()).isEqualTo("LOADING"); assertThat(result.status()).isEqualTo("LOADING");
verify(loadingPickService).lockMutable(1L); verify(loadingPickService).lockMutable(1L);
var order = inOrder(loadingPickService, repository); var order = inOrder(loadingPickService, repository);
order.verify(loadingPickService).lockMutable(1L); order.verify(loadingPickService).lockMutable(1L);
order.verify(repository).touch(1L, 2, 7L, "u"); 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<LoadingPlanHistoryEntity> history = ArgumentCaptor.forClass(LoadingPlanHistoryEntity.class); ArgumentCaptor<LoadingPlanHistoryEntity> history = ArgumentCaptor.forClass(LoadingPlanHistoryEntity.class);
verify(repository).insert(history.capture()); verify(repository).insert(history.capture());
assertThat(history.getValue().getActionType()).isEqualTo("WAREHOUSE_REMARK"); assertThat(history.getValue().getActionType()).isEqualTo("REMARKS");
assertThat(history.getValue().getBeforeSnapshot()).contains("旧仓库备注"); assertThat(history.getValue().getBeforeSnapshot()).contains("旧仓库备注");
} }
@@ -1477,14 +1479,14 @@ class SmartLoadingServiceTest {
when(repository.booking(99L)).thenReturn(booking()); when(repository.booking(99L)).thenReturn(booking());
when(repository.items(1L)).thenReturn(List.of(line)); when(repository.items(1L)).thenReturn(List.of(line));
when(repository.touch(1L, 1, 7L, "u")).thenReturn(1); 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, Plan result = service.updateRemarks(1L, 8L,
new WarehouseRemarkRequest("待核对外箱", 1, 0, "warehouse-editing"), 7L, "u"); new RemarksRequest(null, "待核对外箱", null, 1, 0, "warehouse-editing"), 7L, "u");
assertThat(result.status()).isEqualTo("EDITING"); assertThat(result.status()).isEqualTo("EDITING");
assertThat(result.version()).isEqualTo(2); 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 @Test
@@ -1497,13 +1499,13 @@ class SmartLoadingServiceTest {
when(repository.booking(99L)).thenReturn(booking()); when(repository.booking(99L)).thenReturn(booking());
when(repository.items(1L)).thenReturn(List.of(line)); when(repository.items(1L)).thenReturn(List.of(line));
Plan result = service.updateWarehouseRemark(1L, 8L, Plan result = service.updateRemarks(1L, 8L,
new WarehouseRemarkRequest(" 待核对外箱 ", 2, 0, "warehouse-noop"), 7L, "u"); new RemarksRequest(null, " 待核对外箱 ", null, 2, 0, "warehouse-noop"), 7L, "u");
assertThat(result.version()).isEqualTo(2); assertThat(result.version()).isEqualTo(2);
verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), anyString()); verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), anyString());
verify(repository, never()).updateWarehouseRemark(anyLong(), anyLong(), anyInt(), anyInt(), verify(repository, never()).updateRemarks(anyLong(), anyLong(), anyInt(), anyInt(),
any(), anyLong(), anyString()); any(), any(), any(), anyLong(), anyString());
verify(repository, never()).insert(any(LoadingPlanHistoryEntity.class)); verify(repository, never()).insert(any(LoadingPlanHistoryEntity.class));
} }
@@ -1514,10 +1516,10 @@ class SmartLoadingServiceTest {
when(repository.plan(1L)).thenReturn(plan); when(repository.plan(1L)).thenReturn(plan);
when(repository.item(1L, 8L)).thenReturn(line); when(repository.item(1L, 8L)).thenReturn(line);
when(repository.touch(1L, 2, 7L, "u")).thenReturn(1); 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, BizException error = catchThrowableOfType(() -> service.updateRemarks(1L, 8L,
new WarehouseRemarkRequest("新备注", 2, 0, "warehouse-conflict"), 7L, "u"), BizException.class); new RemarksRequest(null, "新备注", null, 2, 0, "warehouse-conflict"), 7L, "u"), BizException.class);
assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT); assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT);
verify(repository).touch(1L, 2, 7L, "u"); verify(repository).touch(1L, 2, 7L, "u");
@@ -1531,15 +1533,15 @@ class SmartLoadingServiceTest {
when(repository.plan(1L)).thenReturn(plan); when(repository.plan(1L)).thenReturn(plan);
when(repository.item(1L, 8L)).thenReturn(line); when(repository.item(1L, 8L)).thenReturn(line);
when(repository.touch(1L, 2, 7L, "u")).thenReturn(1); 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", .thenThrow(new DataIntegrityViolationException("trigger",
new SQLException("状态冲突", "45000"))); new SQLException("状态冲突", "45000")));
BizException error = catchThrowableOfType(() -> service.updateWarehouseRemark(1L, 8L, BizException error = catchThrowableOfType(() -> service.updateRemarks(1L, 8L,
new WarehouseRemarkRequest("新备注", 2, 0, "warehouse-trigger"), 7L, "u"), BizException.class); new RemarksRequest(null, "新备注", null, 2, 0, "warehouse-trigger"), 7L, "u"), BizException.class);
assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT); assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT);
assertThat(error.getMessage()).contains("仓库备注未保存").contains("刷新后重试"); assertThat(error.getMessage()).contains("备注未保存").contains("刷新后重试");
verify(repository, never()).insert(any(LoadingPlanHistoryEntity.class)); verify(repository, never()).insert(any(LoadingPlanHistoryEntity.class));
} }
@@ -1547,46 +1549,46 @@ class SmartLoadingServiceTest {
void stalePlanVersionRejectsWarehouseRemarkBeforeItemWrite() { void stalePlanVersionRejectsWarehouseRemarkBeforeItemWrite() {
when(repository.plan(1L)).thenReturn(plan("LOADING", 3)); when(repository.plan(1L)).thenReturn(plan("LOADING", 3));
BizException error = catchThrowableOfType(() -> service.updateWarehouseRemark(1L, 8L, BizException error = catchThrowableOfType(() -> service.updateRemarks(1L, 8L,
new WarehouseRemarkRequest("新备注", 2, 0, "warehouse-stale-plan"), 7L, "u"), BizException.class); new RemarksRequest(null, "新备注", null, 2, 0, "warehouse-stale-plan"), 7L, "u"), BizException.class);
assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT); assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT);
verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), anyString()); verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), anyString());
verify(repository, never()).updateWarehouseRemark(anyLong(), anyLong(), anyInt(), anyInt(), verify(repository, never()).updateRemarks(anyLong(), anyLong(), anyInt(), anyInt(),
any(), anyLong(), anyString()); any(), any(), any(), anyLong(), anyString());
} }
@Test @Test
void completedPlanRejectsWarehouseRemarkBeforeAnyWrite() { void completedPlanRejectsWarehouseRemarkBeforeAnyWrite() {
when(repository.plan(1L)).thenReturn(plan("COMPLETED", 4)); when(repository.plan(1L)).thenReturn(plan("COMPLETED", 4));
BizException error = catchThrowableOfType(() -> service.updateWarehouseRemark(1L, 8L, BizException error = catchThrowableOfType(() -> service.updateRemarks(1L, 8L,
new WarehouseRemarkRequest("新备注", 4, 0, "warehouse-completed"), 7L, "u"), BizException.class); new RemarksRequest(null, "新备注", null, 4, 0, "warehouse-completed"), 7L, "u"), BizException.class);
assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT); assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT);
verify(loadingPickService, never()).lockMutable(anyLong()); verify(loadingPickService, never()).lockMutable(anyLong());
verify(repository, never()).item(anyLong(), anyLong()); verify(repository, never()).item(anyLong(), anyLong());
verify(repository, never()).updateWarehouseRemark(anyLong(), anyLong(), anyInt(), anyInt(), verify(repository, never()).updateRemarks(anyLong(), anyLong(), anyInt(), anyInt(),
any(), anyLong(), anyString()); any(), any(), any(), anyLong(), anyString());
verify(repository, never()).insert(any(LoadingPlanHistoryEntity.class)); verify(repository, never()).insert(any(LoadingPlanHistoryEntity.class));
} }
@Test @Test
void exactWarehouseRemarkReplayIsReadOnly() { void exactWarehouseRemarkReplayIsReadOnly() {
String requestHash = sha256("WAREHOUSE_REMARK|1|8|备注|2|0"); String requestHash = sha256("REMARKS|1|8||备注||2|0");
when(repository.operation("warehouse-replay")) 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.plan(1L)).thenReturn(plan("LOADING", 3));
when(repository.booking(99L)).thenReturn(booking()); when(repository.booking(99L)).thenReturn(booking());
when(repository.items(1L)).thenReturn(List.of(item(8, 9, 20, 1))); when(repository.items(1L)).thenReturn(List.of(item(8, 9, 20, 1)));
Plan result = service.updateWarehouseRemark(1L, 8L, Plan result = service.updateRemarks(1L, 8L,
new WarehouseRemarkRequest("备注", 2, 0, "warehouse-replay"), 7L, "u"); new RemarksRequest(null, "备注", null, 2, 0, "warehouse-replay"), 7L, "u");
assertThat(result.version()).isEqualTo(3); assertThat(result.version()).isEqualTo(3);
verify(loadingPickService, never()).lockMutable(anyLong()); verify(loadingPickService, never()).lockMutable(anyLong());
verify(repository, never()).updateWarehouseRemark(anyLong(), anyLong(), anyInt(), anyInt(), verify(repository, never()).updateRemarks(anyLong(), anyLong(), anyInt(), anyInt(),
any(), anyLong(), anyString()); any(), any(), any(), anyLong(), anyString());
} }
@Test @Test
@@ -57,14 +57,11 @@ public class InternalOrderCargoController {
} }
var order = orders.findOwned(tenantId, orderId); var order = orders.findOwned(tenantId, orderId);
if (order == null) throw new BizException(ResultCode.NOT_FOUND, "订单不存在或不属于当前客户"); 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(), return ApiResponse.success(new CargoBinding(cargo.getId(), orderId, cargo.getItemNo(),
cargo.getCargoName(), cargo.getPackageCount(), cargo.getGrossWeightKg(), cargo.getVolumeCbm(), cargo.getCargoName(), cargo.getPackageCount(), cargo.getGrossWeightKg(), cargo.getVolumeCbm(),
cargo.getSkuNo(), cargo.getCargoNameEn(), cargo.getUnitsPerPackage(), cargo.getTotalQuantity(), 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}") @GetMapping("/{tenantId}/{orderId}")
@@ -167,12 +164,13 @@ public class InternalOrderCargoController {
java.math.BigDecimal originalVolumeCbm, String skuNo, String cargoNameEn, java.math.BigDecimal originalVolumeCbm, String skuNo, String cargoNameEn,
Integer unitsPerPackage, Integer totalQuantity, Integer unitsPerPackage, Integer totalQuantity,
java.math.BigDecimal netWeightKg, String cargoRemark, 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, public CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
Integer originalPackageCount, java.math.BigDecimal originalGrossWeightKg, Integer originalPackageCount, java.math.BigDecimal originalGrossWeightKg,
java.math.BigDecimal originalVolumeCbm) { java.math.BigDecimal originalVolumeCbm) {
this(cargoId, orderId, itemNo, cargoName, originalPackageCount, 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, public CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
Integer originalPackageCount, java.math.BigDecimal originalGrossWeightKg, Integer originalPackageCount, java.math.BigDecimal originalGrossWeightKg,
@@ -181,7 +179,7 @@ public class InternalOrderCargoController {
String cargoRemark) { String cargoRemark) {
this(cargoId, orderId, itemNo, cargoName, originalPackageCount, originalGrossWeightKg, this(cargoId, orderId, itemNo, cargoName, originalPackageCount, originalGrossWeightKg,
originalVolumeCbm, skuNo, cargoNameEn, unitsPerPackage, totalQuantity, netWeightKg, originalVolumeCbm, skuNo, cargoNameEn, unitsPerPackage, totalQuantity, netWeightKg,
cargoRemark, null, null); cargoRemark, null, null, null, null);
} }
} }
@@ -282,23 +282,6 @@ public class PlatformFirstLegOrderController {
return transit(user, tenantId, id, OrderStatus.COMPLETED, null); 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<Long> 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") @PutMapping("/{id}/express")
@PreAuthorize("hasAuthority('platform:order:fulfillment')") @PreAuthorize("hasAuthority('platform:order:fulfillment')")
@@ -112,16 +112,16 @@ public class PlatformUnifiedOrderController {
return ApiResponse.success(splitService.containerSlices(containerBookingId, tenantId)); return ApiResponse.success(splitService.containerSlices(containerBookingId, tenantId));
} }
@PostMapping("/{orderId}/internal-notes") @PutMapping("/{orderId}/remarks")
@BusinessAudit(event = "ORDER_INTERNAL_NOTE", resourceType = "FIRST_LEG_ORDER", resourceId = "#orderId", description = "Add internal note / 添加内部备注") @BusinessAudit(event = "ORDER_REMARKS_UPDATE", resourceType = "FIRST_LEG_ORDER", resourceId = "#orderId", description = "更新订单协作备注")
@PreAuthorize("hasAuthority('platform:order:note')") @PreAuthorize("hasAuthority('platform:order:note')")
@Operation(summary = "添加订单内部备注", description = "添加仅平台内部可见的订单备注。") @Operation(summary = "更新订单协作备注", description = "客户备注只读;平台可更新内部备注、仓库备注和客服备注。")
public ApiResponse<Long> addInternalNote( public ApiResponse<OrderRequests.RemarksUpdateResponse> updateRemarks(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long tenantId, @RequestParam long tenantId,
@PathVariable long orderId, @PathVariable long orderId,
@Valid @RequestBody OrderRequests.NoteRequest request) { @Valid @RequestBody OrderRequests.RemarksUpdateRequest request) {
long scopedTenant = OrderScopeGuard.platform(user, tenantId); 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));
} }
} }
@@ -316,12 +316,17 @@ public final class OrderRequests {
} }
} }
@Schema(name = "OrderInternalNoteRequest", description = "Platform internal order note / 平台内部订单备注") @Schema(name = "OrderRemarksUpdateRequest", description = "平台订单协作备注更新")
public record NoteRequest( public record RemarksUpdateRequest(
@Schema(description = "Internal note content / 内部备注内容", example = "Customer confirmed the shipping schedule / 客户已确认船期", maxLength = 2000, requiredMode = Schema.RequiredMode.REQUIRED) @NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max = 2000) String content @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( public record ExpressRequest(
@NotBlank @Size(max = 64) @NotBlank @Size(max = 64)
@@ -35,7 +35,7 @@ public final class UnifiedOrderResponse {
MeasurementSummary measurement, PreloadSummary preload, MeasurementSummary measurement, PreloadSummary preload,
LoadingSummary loading, DeliverySummary delivery, TransportSummary transport, LoadingSummary loading, DeliverySummary delivery, TransportSummary transport,
CoLoadGroupSummary coLoadGroup, CoLoadGroupSummary coLoadGroup,
String customerRemark, String latestInternalNote, String customerRemark, String latestInternalNote, String warehouseRemark, String customerServiceRemark,
LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime requestedPickupAt LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime requestedPickupAt
) {} ) {}
@@ -35,6 +35,9 @@ public class OrderEntity extends BaseEntity {
private LocalDateTime expectedArrivalAt; private LocalDateTime expectedArrivalAt;
private BigDecimal estimatedVolumeCbm; private BigDecimal estimatedVolumeCbm;
private String customerRemark; private String customerRemark;
private String internalRemark;
private String warehouseRemark;
private String customerServiceRemark;
private String pickupMethod; private String pickupMethod;
private LocalDateTime requestedPickupAt; private LocalDateTime requestedPickupAt;
private Long receivingWarehouseId; private Long receivingWarehouseId;
@@ -117,6 +117,21 @@ public class OrderRepository {
.set(OrderCargoEntity::getUpdatedAt, cargo.getUpdatedAt()) .set(OrderCargoEntity::getUpdatedAt, cargo.getUpdatedAt())
.setSql("version = version + 1")); .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<OrderEntity>()
.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 insertCargo(OrderCargoEntity cargo) { cargoMapper.insert(cargo); }
public void insertParty(OrderPartyEntity party) { partyMapper.insert(party); } public void insertParty(OrderPartyEntity party) { partyMapper.insert(party); }
public void insertNote(OrderInternalNoteEntity note) { noteMapper.insert(note); } public void insertNote(OrderInternalNoteEntity note) { noteMapper.insert(note); }
@@ -268,11 +268,29 @@ public class OrderApplicationService {
return new OrderDetailResponse(order, repository.findCargo(tenantId, id), repository.findParties(tenantId, id), return new OrderDetailResponse(order, repository.findCargo(tenantId, id), repository.findParties(tenantId, id),
List.of(), repository.findHistory(tenantId, id)); List.of(), repository.findHistory(tenantId, id));
} }
public Long addNote(long tenantId, long userId, long id, String content) { @Transactional
require(tenantId, id); public OrderRequests.RemarksUpdateResponse updateRemarks(long tenantId, long userId, long id,
OrderInternalNoteEntity note = new OrderInternalNoteEntity(); OrderRequests.RemarksUpdateRequest request) {
note.setTenantId(tenantId); note.setOrderId(id); note.setContent(content); OrderEntity order = require(tenantId, id);
note.setCreatedBy(userId); note.setUpdatedBy(userId); repository.insertNote(note); return note.getId(); 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 @Transactional
@@ -11,7 +11,6 @@ import com.baiyue.order.dto.UnifiedOrderQuery;
import com.baiyue.order.dto.UnifiedOrderResponse; import com.baiyue.order.dto.UnifiedOrderResponse;
import com.baiyue.order.entity.OrderEntity; import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.entity.OrderCargoEntity; import com.baiyue.order.entity.OrderCargoEntity;
import com.baiyue.order.entity.OrderInternalNoteEntity;
import com.baiyue.order.entity.OrderStatus; import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.repository.OrderRepository; import com.baiyue.order.repository.OrderRepository;
import feign.FeignException; import feign.FeignException;
@@ -66,11 +65,10 @@ public class UnifiedOrderQueryService {
List<OrderEntity> selected = orderNos.stream().map(orderByNo::get).filter(Objects::nonNull).toList(); List<OrderEntity> selected = orderNos.stream().map(orderByNo::get).filter(Objects::nonNull).toList();
List<Long> orderIds = selected.stream().map(OrderEntity::getId).toList(); List<Long> orderIds = selected.stream().map(OrderEntity::getId).toList();
Map<Long, FirstlegUnifiedOrderClient.Summary> inboundByOrder = summaries(null, orderIds); Map<Long, FirstlegUnifiedOrderClient.Summary> inboundByOrder = summaries(null, orderIds);
Map<Long, String> latestNotes = latestNotes(null, orderIds);
Map<Long, CargoSummary> cargoSummaries = cargoSummaries(null, selected); Map<Long, CargoSummary> cargoSummaries = cargoSummaries(null, selected);
Map<Long, Integer> childCounts = childCounts(selected); Map<Long, Integer> childCounts = childCounts(selected);
return selected.stream().map(order -> row(order, inboundByOrder.get(order.getId()), 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<UnifiedOrderResponse.Row> page(Long tenantId, UnifiedOrderQuery query, private PageResult<UnifiedOrderResponse.Row> page(Long tenantId, UnifiedOrderQuery query,
@@ -89,13 +87,12 @@ public class UnifiedOrderQueryService {
Map<Long, FirstlegUnifiedOrderClient.Summary> inboundByOrder = "PENDING_REVIEW".equals(query.mainStatus()) Map<Long, FirstlegUnifiedOrderClient.Summary> inboundByOrder = "PENDING_REVIEW".equals(query.mainStatus())
? optionalSummaries(tenantId, sourceOrderIds) ? optionalSummaries(tenantId, sourceOrderIds)
: summaries(tenantId, sourceOrderIds); : summaries(tenantId, sourceOrderIds);
Map<Long, String> latestNotes = latestNotes(tenantId, source.records().stream().map(OrderEntity::getId).toList());
Map<Long, CargoSummary> cargoSummaries = cargoSummaries(tenantId, source.records()); Map<Long, CargoSummary> cargoSummaries = cargoSummaries(tenantId, source.records());
Map<Long, Integer> childCounts = childCounts(source.records()); Map<Long, Integer> childCounts = childCounts(source.records());
Map<Long, UnifiedOrderResponse.CoLoadGroupSummary> coLoadByOrder = coLoadGroups( Map<Long, UnifiedOrderResponse.CoLoadGroupSummary> coLoadByOrder = coLoadGroups(
source.records().stream().map(OrderEntity::getId).toList(), includeCoLoadGroups); source.records().stream().map(OrderEntity::getId).toList(), includeCoLoadGroups);
List<UnifiedOrderResponse.Row> rows = source.records().stream() List<UnifiedOrderResponse.Row> 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(); childCounts, cargoSummaries.get(order.getId()), coLoadByOrder.get(order.getId()))).toList();
return new PageResult<>(rows, source.page(), source.size(), source.total(), source.pages()); return new PageResult<>(rows, source.page(), source.size(), source.total(), source.pages());
} }
@@ -117,7 +114,7 @@ public class UnifiedOrderQueryService {
orders.findHistory(tenantId, orderId)); orders.findHistory(tenantId, orderId));
FirstlegUnifiedOrderClient.Detail inbound = order.getStatus() == OrderStatus.SUBMITTED FirstlegUnifiedOrderClient.Detail inbound = order.getStatus() == OrderStatus.SUBMITTED
? optionalFirstlegDetail(tenantId, orderId) : firstlegDetail(tenantId, orderId); ? 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() String creatorName = orderDetail.statusHistory().stream()
.filter(item -> "CREATE".equals(item.getAction())) .filter(item -> "CREATE".equals(item.getAction()))
.map(item -> item.getOperatorName()) .map(item -> item.getOperatorName())
@@ -166,14 +163,13 @@ public class UnifiedOrderQueryService {
List<Long> orderIds = summaries.stream().map(item -> item.inbound().orderId()).toList(); List<Long> orderIds = summaries.stream().map(item -> item.inbound().orderId()).toList();
Map<Long, OrderEntity> orderById = orders.findByIds(tenantId, orderIds).stream() Map<Long, OrderEntity> orderById = orders.findByIds(tenantId, orderIds).stream()
.collect(Collectors.toMap(OrderEntity::getId, Function.identity(), (left, right) -> left)); .collect(Collectors.toMap(OrderEntity::getId, Function.identity(), (left, right) -> left));
Map<Long, String> latestNotes = latestNotes(tenantId, orderIds);
Map<Long, CargoSummary> cargoSummaries = cargoSummaries(tenantId, List.copyOf(orderById.values())); Map<Long, CargoSummary> cargoSummaries = cargoSummaries(tenantId, List.copyOf(orderById.values()));
Map<Long, Integer> childCounts = childCounts(List.copyOf(orderById.values())); Map<Long, Integer> childCounts = childCounts(List.copyOf(orderById.values()));
Map<Long, UnifiedOrderResponse.CoLoadGroupSummary> coLoadByOrder = coLoadGroups( Map<Long, UnifiedOrderResponse.CoLoadGroupSummary> coLoadByOrder = coLoadGroups(
orderIds, includeCoLoadGroups); orderIds, includeCoLoadGroups);
List<UnifiedOrderResponse.Row> records = summaries.stream() List<UnifiedOrderResponse.Row> records = summaries.stream()
.map(summary -> row(summary, orderById.get(summary.inbound().orderId()), .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()), cargoSummaries.get(summary.inbound().orderId()),
coLoadByOrder.get(summary.inbound().orderId()))) coLoadByOrder.get(summary.inbound().orderId())))
.toList(); .toList();
@@ -233,11 +229,6 @@ public class UnifiedOrderQueryService {
return summary; return summary;
} }
private Map<Long, String> latestNotes(Long tenantId, List<Long> orderIds) {
return orders.findNotesByOrderIds(tenantId, orderIds).stream().collect(Collectors.toMap(
OrderInternalNoteEntity::getOrderId, OrderInternalNoteEntity::getContent, (latest, ignored) -> latest));
}
private Map<Long, CargoSummary> cargoSummaries(Long tenantId, List<OrderEntity> pageOrders) { private Map<Long, CargoSummary> cargoSummaries(Long tenantId, List<OrderEntity> pageOrders) {
if (pageOrders.isEmpty()) return Map.of(); if (pageOrders.isEmpty()) return Map.of();
List<Long> orderIds = pageOrders.stream().map(OrderEntity::getId).toList(); List<Long> 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(), 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), StringUtils.hasText(creatorName) ? creatorName : string(order.getCreatedBy()), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo),
null, null, null, null, null, coLoadGroup, null, null, null, null, null, coLoadGroup,
order.getCustomerRemark(), latestInternalNote, order.getCustomerRemark(), latestInternalNote, order.getWarehouseRemark(), order.getCustomerServiceRemark(),
order.getCreatedAt(), order.getUpdatedAt(), order.getRequestedPickupAt()); order.getCreatedAt(), order.getUpdatedAt(), order.getRequestedPickupAt());
} }
FirstlegUnifiedOrderClient.Inbound inbound = summary.inbound(); 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), StringUtils.hasText(creatorName) ? creatorName : inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo), measurement(inbound),
preload(summary.preload()), loading(summary.loading()), preload(summary.preload()), loading(summary.loading()),
delivery(summary.delivery()), transport(inbound), coLoadGroup, order.getCustomerRemark(), latestInternalNote, delivery(summary.delivery()), transport(inbound), coLoadGroup, order.getCustomerRemark(), latestInternalNote,
order.getWarehouseRemark(), order.getCustomerServiceRemark(),
order.getCreatedAt(), order.getUpdatedAt(), inbound.requestedPickupAt()); order.getCreatedAt(), order.getUpdatedAt(), inbound.requestedPickupAt());
} }
@@ -394,6 +386,7 @@ public class UnifiedOrderQueryService {
measurement(inbound), preload(summary.preload()), loading(summary.loading()), measurement(inbound), preload(summary.preload()), loading(summary.loading()),
delivery(summary.delivery()), transport(inbound), coLoadGroup, delivery(summary.delivery()), transport(inbound), coLoadGroup,
order == null ? null : order.getCustomerRemark(), latestInternalNote, order == null ? null : order.getCustomerRemark(), latestInternalNote,
order == null ? null : order.getWarehouseRemark(), order == null ? null : order.getCustomerServiceRemark(),
inbound.createdAt(), inbound.updatedAt(), inbound.requestedPickupAt()); inbound.createdAt(), inbound.updatedAt(), inbound.requestedPickupAt());
} }
+30
View File
@@ -22,6 +22,9 @@ CREATE TABLE IF NOT EXISTS ord_order (
expected_arrival_at DATETIME(3) NULL COMMENT '客户申报预计到货时间', expected_arrival_at DATETIME(3) NULL COMMENT '客户申报预计到货时间',
estimated_volume_cbm DECIMAL(18,4) NULL COMMENT '平台运营预估体积(立方米),独立于客户下单及实测体积', estimated_volume_cbm DECIMAL(18,4) NULL COMMENT '平台运营预估体积(立方米),独立于客户下单及实测体积',
customer_remark VARCHAR(1000) 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 '取货方式', pickup_method VARCHAR(24) NULL COMMENT '取货方式',
requested_pickup_at DATETIME(3) NULL COMMENT '客户要求平台上门提货的预约时间', requested_pickup_at DATETIME(3) NULL COMMENT '客户要求平台上门提货的预约时间',
receiving_warehouse_id BIGINT UNSIGNED NULL COMMENT '国内收货仓ID', 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) KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id)
) ENGINE=InnoDB COMMENT='头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态'; ) 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( SET @order_fulfillment_type_sql = IF(
EXISTS( EXISTS(
SELECT 1 FROM information_schema.columns SELECT 1 FROM information_schema.columns
+11 -6
View File
@@ -267,10 +267,10 @@ paths:
application/json: application/json:
schema: {$ref: '#/components/schemas/UnifiedOrderDetailEnvelope'} schema: {$ref: '#/components/schemas/UnifiedOrderDetailEnvelope'}
'403': {description: 需要平台身份和 platform:order:list 权限} '403': {description: 需要平台身份和 platform:order:list 权限}
/api/platform/v1/orders/{orderId}/internal-notes: /api/platform/v1/orders/{orderId}/remarks:
post: put:
summary: 添加订单内部备注 summary: 更新订单协作备注
description: 添加仅平台内部可见的订单备注,需要 platform:order:note 权限。 description: 客户备注只读;需要 platform:order:note 权限,可更新或清空内部备注、仓库备注和客服备注。
parameters: parameters:
- $ref: '#/components/parameters/UnifiedOrderId' - $ref: '#/components/parameters/UnifiedOrderId'
- $ref: '#/components/parameters/UnifiedTenantId' - $ref: '#/components/parameters/UnifiedTenantId'
@@ -280,9 +280,12 @@ paths:
application/json: application/json:
schema: schema:
type: object type: object
required: [content] required: [expectedVersion]
properties: 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: responses:
'200': {description: 备注添加成功或业务失败,按响应体 code 判断} '200': {description: 备注添加成功或业务失败,按响应体 code 判断}
'403': {description: 需要 platform:order:note 权限} '403': {description: 需要 platform:order:note 权限}
@@ -949,6 +952,8 @@ components:
coLoadGroup: {$ref: '#/components/schemas/UnifiedCoLoadGroupSummary'} coLoadGroup: {$ref: '#/components/schemas/UnifiedCoLoadGroupSummary'}
customerRemark: {type: string, nullable: true} customerRemark: {type: string, nullable: true}
latestInternalNote: {type: string, nullable: true, description: 客户详情不返回平台内部备注} latestInternalNote: {type: string, nullable: true, description: 客户详情不返回平台内部备注}
warehouseRemark: {type: string, nullable: true}
customerServiceRemark: {type: string, nullable: true}
createdAt: {type: string, format: date-time, nullable: true} createdAt: {type: string, format: date-time, nullable: true}
updatedAt: {type: string, format: date-time, nullable: true} updatedAt: {type: string, format: date-time, nullable: true}
UnifiedOrderRelation: UnifiedOrderRelation:
@@ -3,7 +3,6 @@ package com.baiyue.order.controller;
import com.baiyue.common.core.exception.BizException; import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.entity.OrderCargoEntity; import com.baiyue.order.entity.OrderCargoEntity;
import com.baiyue.order.entity.OrderEntity; import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.entity.OrderInternalNoteEntity;
import com.baiyue.order.repository.OrderRepository; import com.baiyue.order.repository.OrderRepository;
import com.baiyue.order.service.OrderApplicationService; import com.baiyue.order.service.OrderApplicationService;
import com.baiyue.order.service.PrintableInboundSnapshotService; import com.baiyue.order.service.PrintableInboundSnapshotService;
@@ -50,9 +49,10 @@ class InternalOrderCargoControllerTest {
cargo.setCargoRemark("内部货物备注"); cargo.setCargoRemark("内部货物备注");
when(orders.findCargoInheritedByOrder(11L, 21L, 31L)).thenReturn(cargo); when(orders.findCargoInheritedByOrder(11L, 21L, 31L)).thenReturn(cargo);
OrderEntity order = new OrderEntity(); order.setId(21L); order.setCustomerRemark("客户订单备注"); 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.findOwned(11L, 21L)).thenReturn(order);
when(orders.findNotes(11L, 21L)).thenReturn(List.of(note));
var response = controller.detail("internal-token", 11L, 21L, 31L); var response = controller.detail("internal-token", 11L, 21L, 31L);
@@ -67,6 +67,8 @@ class InternalOrderCargoControllerTest {
assertThat(response.data().cargoRemark()).isEqualTo("内部货物备注"); assertThat(response.data().cargoRemark()).isEqualTo("内部货物备注");
assertThat(response.data().customerRemark()).isEqualTo("客户订单备注"); assertThat(response.data().customerRemark()).isEqualTo("客户订单备注");
assertThat(response.data().latestInternalNote()).isEqualTo("最新内部备注"); assertThat(response.data().latestInternalNote()).isEqualTo("最新内部备注");
assertThat(response.data().warehouseRemark()).isEqualTo("仓库备注");
assertThat(response.data().customerServiceRemark()).isEqualTo("客服备注");
verify(orders).findCargoInheritedByOrder(11L, 21L, 31L); verify(orders).findCargoInheritedByOrder(11L, 21L, 31L);
} }
@@ -64,9 +64,9 @@ class OrderControllerPermissionTest {
} }
@Test @Test
void unifiedPlatformInternalNoteRequiresNotePermission() throws Exception { void unifiedPlatformRemarksRequireNotePermission() throws Exception {
assertAuthority(PlatformUnifiedOrderController.class, "addInternalNote", "hasAuthority('platform:order:note')", assertAuthority(PlatformUnifiedOrderController.class, "updateRemarks", "hasAuthority('platform:order:note')",
AuthenticatedUser.class, long.class, long.class, OrderRequests.NoteRequest.class); AuthenticatedUser.class, long.class, long.class, OrderRequests.RemarksUpdateRequest.class);
} }
private void assertAuthority(Class<?> controller, String methodName, String expected, Class<?>... parameterTypes) private void assertAuthority(Class<?> controller, String methodName, String expected, Class<?>... parameterTypes)
@@ -697,6 +697,48 @@ class OrderApplicationServiceStateTest {
.isEqualTo("HDHK26081410000"); .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, private OrderApplicationService service(OrderRepository repository, OrderEligibilityGuard eligibility,
OrderMasterDataGuard masterData) { OrderMasterDataGuard masterData) {
OrderApplicationService service = springService; OrderApplicationService service = springService;
@@ -10,7 +10,6 @@ import com.baiyue.order.dto.UnifiedOrderQuery;
import com.baiyue.order.dto.UnifiedOrderResponse; import com.baiyue.order.dto.UnifiedOrderResponse;
import com.baiyue.order.entity.OrderCargoEntity; import com.baiyue.order.entity.OrderCargoEntity;
import com.baiyue.order.entity.OrderEntity; import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.entity.OrderInternalNoteEntity;
import com.baiyue.order.entity.OrderStatusHistoryEntity; import com.baiyue.order.entity.OrderStatusHistoryEntity;
import com.baiyue.order.entity.OrderStatus; import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.repository.OrderRepository; import com.baiyue.order.repository.OrderRepository;
@@ -58,14 +57,13 @@ class UnifiedOrderQueryServiceTest {
OrderEntity order = order(OrderStatus.SUBMITTED); OrderEntity order = order(OrderStatus.SUBMITTED);
order.setInboundCode("RK-77"); order.setInboundCode("RK-77");
order.setCustomerRemark("客户要求提货前联系"); order.setCustomerRemark("客户要求提货前联系");
OrderInternalNoteEntity note = new OrderInternalNoteEntity(); order.setInternalRemark("客服已确认提货时间");
note.setOrderId(77L); order.setWarehouseRemark("仓库请优先处理");
note.setContent("客服已确认提货时间"); order.setCustomerServiceRemark("客服已回访");
when(orders.unifiedPage(eq(10001L), eq(query), eq(List.of(OrderStatus.SUBMITTED)))) when(orders.unifiedPage(eq(10001L), eq(query), eq(List.of(OrderStatus.SUBMITTED))))
.thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1)); .thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1));
when(firstleg.summaries(eq("internal-token"), any())) when(firstleg.summaries(eq("internal-token"), any()))
.thenReturn(ApiResponse.success(List.of())); .thenReturn(ApiResponse.success(List.of()));
when(orders.findNotesByOrderIds(10001L, List.of(77L))).thenReturn(List.of(note));
var result = service.page(10001L, query); var result = service.page(10001L, query);
@@ -79,6 +77,8 @@ class UnifiedOrderQueryServiceTest {
assertThat(row.currentHoldReason()).isNull(); assertThat(row.currentHoldReason()).isNull();
assertThat(row.customerRemark()).isEqualTo("客户要求提货前联系"); assertThat(row.customerRemark()).isEqualTo("客户要求提货前联系");
assertThat(row.latestInternalNote()).isEqualTo("客服已确认提货时间"); assertThat(row.latestInternalNote()).isEqualTo("客服已确认提货时间");
assertThat(row.warehouseRemark()).isEqualTo("仓库请优先处理");
assertThat(row.customerServiceRemark()).isEqualTo("客服已回访");
assertThat(row.relation()).isNull(); assertThat(row.relation()).isNull();
}); });
verify(firstleg).summaries(eq("internal-token"), any()); verify(firstleg).summaries(eq("internal-token"), any());