Merge remote-tracking branch 'origin/test' into dev

# Conflicts:
#	deploy/sql/头程数据库统一增量-MySQL8.sql
This commit is contained in:
2026-09-03 11:55:46 +08:00
12 changed files with 230 additions and 27 deletions
@@ -566,6 +566,7 @@ CREATE TABLE IF NOT EXISTS ord_order (
destination_warehouse_name VARCHAR(128) NOT NULL COMMENT '目的仓名称快照',
expected_ship_date DATE NULL COMMENT '预计发运日期',
expected_arrival_at DATETIME(3) NULL COMMENT '客户申报预计到货时间',
estimated_volume_cbm DECIMAL(18,4) NULL COMMENT '平台运营预估体积(立方米),独立于客户下单及实测体积',
customer_remark VARCHAR(1000) NULL COMMENT '客户可见备注',
pickup_method VARCHAR(24) NULL COMMENT '取货方式',
requested_pickup_at DATETIME(3) NULL COMMENT '客户要求平台上门提货的预约时间',
@@ -14976,12 +14977,9 @@ CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'version',
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'warehouse_id', '所属仓库ID');
-- END GENERATED NINE-SCHEMA COMMENT UPGRADE CALLS
CALL apply_queued_comments();
DROP PROCEDURE IF EXISTS apply_queued_comments;
DROP PROCEDURE IF EXISTS apply_one_table_comments;
DROP PROCEDURE IF EXISTS queue_column_comment;
DROP PROCEDURE IF EXISTS queue_table_comment;
DROP TEMPORARY TABLE IF EXISTS tmp_column_comment_target;
DROP TEMPORARY TABLE IF EXISTS tmp_table_comment_target;
-- 后续还会创建整柜报关和同柜组最新基线,保留本会话内的结构保真注释升级器并清空首批队列。
TRUNCATE TABLE tmp_column_comment_target;
TRUNCATE TABLE tmp_table_comment_target;
-- 三期统一最终总断言:稳定箱号、单箱测量、墨家收件箱、客户尺寸和装柜权限。
USE baiyue_firstleg;
@@ -17194,6 +17192,50 @@ CREATE TABLE IF NOT EXISTS fl_dropship_pallet (
CONSTRAINT chk_dropship_pallet_status CHECK (pallet_status IN ('OPEN','COMPLETED'))
) ENGINE=InnoDB COMMENT='一件代发合托形成的可打印托盘';
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','id','柜次证据记录ID');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','file_id','File服务文件ID');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','file_name','文件名称快照');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','seal_no','带锁码照对应锁码');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','remark','证据备注');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','status','证据状态:ACTIVE有效、VOID作废');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','void_reason','作废原因');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','voided_by','作废操作人ID');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','voided_by_name','作废操作人名称快照');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','voided_at','作废时间');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','version','乐观锁版本号');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','created_by','创建操作人ID');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','created_by_name','创建操作人名称快照');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','updated_by','最后修改操作人ID');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','updated_by_name','最后修改操作人名称快照');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','created_at','创建时间');
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','updated_at','最后更新时间');
CALL queue_column_comment('baiyue_firstleg','fl_order_declaration_event','consumed_at','事件消费完成时间');
CALL queue_column_comment('baiyue_firstleg','fl_order_declaration_snapshot','created_at','快照创建时间');
CALL queue_column_comment('baiyue_firstleg','fl_order_declaration_snapshot','updated_at','快照更新时间');
SET @unified_ddl = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='baiyue_order' AND TABLE_NAME='ord_order' AND COLUMN_NAME='estimated_volume_cbm') = 0,
'ALTER TABLE baiyue_order.ord_order ADD COLUMN estimated_volume_cbm DECIMAL(18,4) NULL COMMENT ''平台运营预估体积(立方米),独立于客户下单及实测体积'' AFTER expected_arrival_at',
'SELECT 1'
);
PREPARE unified_stmt FROM @unified_ddl; EXECUTE unified_stmt; DEALLOCATE PREPARE unified_stmt;
CALL queue_column_comment('baiyue_order','ord_order','estimated_volume_cbm','平台运营预估体积(立方米),独立于客户下单及实测体积');
CALL queue_column_comment('baiyue_order','ord_customer_address','created_by','创建人ID');
CALL queue_column_comment('baiyue_order','ord_customer_address','created_at','创建时间');
CALL queue_column_comment('baiyue_order','ord_customer_address','updated_by','最后修改人ID');
CALL queue_column_comment('baiyue_order','ord_customer_address','updated_at','更新时间');
CALL queue_column_comment('baiyue_order','ord_customer_address','deleted','逻辑删除标记:0未删除、1已删除');
CALL queue_column_comment('baiyue_order','ord_customer_address','version','乐观锁版本号');
CALL apply_queued_comments();
DROP PROCEDURE IF EXISTS apply_queued_comments;
DROP PROCEDURE IF EXISTS apply_one_table_comments;
DROP PROCEDURE IF EXISTS queue_column_comment;
DROP PROCEDURE IF EXISTS queue_table_comment;
DROP TEMPORARY TABLE IF EXISTS tmp_column_comment_target;
DROP TEMPORARY TABLE IF EXISTS tmp_table_comment_target;
CREATE TABLE IF NOT EXISTS fl_dropship_pallet_carton (
id BIGINT UNSIGNED NOT NULL COMMENT '托盘箱明细ID',
task_id BIGINT UNSIGNED NOT NULL COMMENT '所属合托任务ID',
@@ -78,6 +78,22 @@ public class PlatformUnifiedOrderController {
user.username(), request));
}
@PutMapping("/{orderId}/planning")
@PreAuthorize("hasAuthority('platform:order:declaration:edit')")
@BusinessAudit(event = "ORDER_PLANNING_UPDATE", resourceType = "FIRST_LEG_ORDER",
resourceId = "#orderId", tenantId = "#tenantId", description = "平台修改预计到货时间和预估体积")
@Operation(summary = "版本化修改订单运营预估",
description = "只修改预计到货时间和平台预估体积;客户下单体积与仓库实测体积保持独立。")
public ApiResponse<OrderDeclarationDtos.UpdateResponse> updatePlanning(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long tenantId,
@PathVariable long orderId,
@Valid @RequestBody OrderDeclarationDtos.PlanningUpdateRequest request) {
long scopedTenant = OrderScopeGuard.platform(user, tenantId);
return ApiResponse.success(declarationUpdates.updatePlanning(scopedTenant, orderId, user.userId(),
user.username(), request));
}
@GetMapping("/{orderId}/fulfillment-tree")
@PreAuthorize("hasAuthority('platform:order:list')")
@Operation(summary = "父子订单与柜次数量摘要")
@@ -22,11 +22,19 @@ public final class OrderDeclarationDtos {
@NotNull @Min(0) Integer expectedVersion,
@Size(max = 1000) String customerRemark,
LocalDateTime expectedArrivalAt,
@DecimalMin("0.0001") BigDecimal estimatedVolumeCbm,
@Size(max = 64) String channelCode,
@Size(max = 128) String channelName,
@NotEmpty @Size(max = 500) List<@Valid Cargo> cargoes
) {}
/** 订单列表只修改运营预估;服务端复用当前完整申报快照,避免浏览器拼装货物明细。 */
public record PlanningUpdateRequest(
@NotNull @Min(0) Integer expectedVersion,
LocalDateTime expectedArrivalAt,
@DecimalMin("0.0001") BigDecimal estimatedVolumeCbm
) {}
/** itemNo 是订单内稳定业务行号;不以货物数据库 ID 作为编辑契约。 */
public record Cargo(
@NotNull @Min(1) @Max(500) Integer itemNo,
@@ -12,7 +12,7 @@ public final class UnifiedOrderResponse {
private UnifiedOrderResponse() {}
public record Row(
Long orderId, Long tenantId, String orderNo, String mainStatus, String nextAction,
Long orderId, Integer orderVersion, Long tenantId, String orderNo, String mainStatus, String nextAction,
OrderRelationSummary relation,
OrderStatus internalOrderStatus,
Long customerId, String customerName, String channelCode, String channelName, Boolean channelChanged,
@@ -20,7 +20,8 @@ public final class UnifiedOrderResponse {
Long destinationWarehouseId, String destinationWarehouseName,
Long inboundId, Integer inboundVersion, String inboundCode, LocalDate expectedShipDate,
Integer totalPackageCount, String recommendedPalletType, String locationCodes,
BigDecimal expectedVolumeCbm, BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm,
BigDecimal estimatedVolumeCbm, BigDecimal expectedVolumeCbm,
BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm,
LocalDateTime expectedArrivalAt, String feeReviewStatus,
String companyName, String businessType, String pickupMethod,
String taxMode, String containerMode, Boolean customsInspection,
@@ -1,5 +1,6 @@
package com.baiyue.order.entity;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.Setter;
import com.baiyue.common.core.model.BaseEntity;
@@ -32,6 +33,7 @@ public class OrderEntity extends BaseEntity {
private String destinationWarehouseName;
private LocalDate expectedShipDate;
private LocalDateTime expectedArrivalAt;
private BigDecimal estimatedVolumeCbm;
private String customerRemark;
private String pickupMethod;
private LocalDateTime requestedPickupAt;
@@ -56,6 +56,7 @@ public class OrderRepository {
}
public int updateDeclarationHeader(long tenantId, long orderId, int expectedVersion, long operatorId,
String customerRemark, LocalDateTime expectedArrivalAt,
java.math.BigDecimal estimatedVolumeCbm,
String channelCode, String channelName, boolean channelChanged) {
return orderMapper.update(null, new LambdaUpdateWrapper<OrderEntity>()
.eq(OrderEntity::getTenantId, tenantId)
@@ -64,6 +65,7 @@ public class OrderRepository {
.eq(OrderEntity::getDeleted, false)
.set(OrderEntity::getCustomerRemark, customerRemark)
.set(OrderEntity::getExpectedArrivalAt, expectedArrivalAt)
.set(OrderEntity::getEstimatedVolumeCbm, estimatedVolumeCbm)
.set(OrderEntity::getChannelCode, channelCode)
.set(OrderEntity::getChannelName, channelName)
.set(OrderEntity::getChannelChanged, channelChanged)
@@ -39,6 +39,45 @@ public class OrderDeclarationUpdateService {
@Resource private OrderFulfillmentChannelChangeGuard fulfillmentChannelGuard;
@Resource private ObjectMapper json;
@Transactional
public OrderDeclarationDtos.UpdateResponse updatePlanning(long tenantId, long orderId, long operatorId,
String operatorName,
OrderDeclarationDtos.PlanningUpdateRequest planning) {
OrderEntity order = repository.findOwned(tenantId, orderId);
if (order == null) throw new BizException(ResultCode.NOT_FOUND, "订单不存在");
if (!EDITABLE.contains(order.getStatus())) {
throw new BizException(ResultCode.CONFLICT, "订单已进入不可逆履约状态,不能修改运营预估");
}
if (!Objects.equals(order.getVersion(), planning.expectedVersion())) {
throw new BizException(ResultCode.CONFLICT, "订单版本已变化,请刷新列表后重试");
}
List<OrderDeclarationDtos.FieldChange> changes = new ArrayList<>();
change(changes, null, "expectedArrivalAt", order.getExpectedArrivalAt(), planning.expectedArrivalAt());
change(changes, null, "estimatedVolumeCbm", order.getEstimatedVolumeCbm(), planning.estimatedVolumeCbm());
if (changes.isEmpty()) throw new BizException(ResultCode.CONFLICT, "运营预估没有发生变化");
boolean channelChanged = Boolean.TRUE.equals(order.getChannelChanged());
if (repository.updateDeclarationHeader(tenantId, orderId, planning.expectedVersion(), operatorId,
order.getCustomerRemark(), planning.expectedArrivalAt(), planning.estimatedVolumeCbm(),
order.getChannelCode(), order.getChannelName(), channelChanged) != 1) {
throw new BizException(ResultCode.CONFLICT, "订单版本已变化,请刷新列表后重试");
}
int version = planning.expectedVersion() + 1;
recordPlanningHistory(tenantId, orderId, operatorId, operatorName, order.getStatus(), changes.size());
if (order.getStatus() == OrderStatus.APPROVED) {
List<OrderDeclarationDtos.Cargo> cargoes = repository.findCargo(tenantId, orderId).stream()
.sorted(Comparator.comparing(OrderCargoEntity::getItemNo))
.map(this::cargoSnapshot)
.toList();
writeOutbox(order, operatorId, new OrderDeclarationDtos.UpdateRequest(
planning.expectedVersion(), order.getCustomerRemark(), planning.expectedArrivalAt(),
planning.estimatedVolumeCbm(), order.getChannelCode(), order.getChannelName(), cargoes),
order.getChannelCode(), order.getChannelName(), channelChanged, changes, version);
}
return new OrderDeclarationDtos.UpdateResponse(order.getOrderNo(), order.getInboundCode(),
planning.expectedVersion(), version, channelChanged, channelChanged, List.copyOf(changes));
}
@Transactional
public OrderDeclarationDtos.UpdateResponse update(long tenantId, long orderId, long operatorId,
String operatorName,
@@ -83,6 +122,7 @@ public class OrderDeclarationUpdateService {
List<OrderDeclarationDtos.FieldChange> changes = new ArrayList<>();
change(changes, null, "customerRemark", order.getCustomerRemark(), request.customerRemark());
change(changes, null, "expectedArrivalAt", order.getExpectedArrivalAt(), request.expectedArrivalAt());
change(changes, null, "estimatedVolumeCbm", order.getEstimatedVolumeCbm(), request.estimatedVolumeCbm());
change(changes, null, "channelCode", order.getChannelCode(), channelCode);
change(changes, null, "channelName", order.getChannelName(), channelName);
for (OrderDeclarationDtos.Cargo update : request.cargoes()) {
@@ -94,7 +134,8 @@ public class OrderDeclarationUpdateService {
boolean changedBefore = Boolean.TRUE.equals(order.getChannelChanged());
boolean channelChanged = changedBefore || channelChangedNow;
if (repository.updateDeclarationHeader(tenantId, orderId, request.expectedVersion(), operatorId,
request.customerRemark(), request.expectedArrivalAt(), channelCode, channelName, channelChanged) != 1) {
request.customerRemark(), request.expectedArrivalAt(), request.estimatedVolumeCbm(),
channelCode, channelName, channelChanged) != 1) {
throw new BizException(ResultCode.CONFLICT, "订单版本已变化,请刷新详情后重试");
}
for (OrderDeclarationDtos.Cargo update : request.cargoes()) {
@@ -129,6 +170,15 @@ public class OrderDeclarationUpdateService {
change(changes,item,"brand",before.getBrand(),after.brand()); change(changes,item,"cargoRemark",before.getCargoRemark(),after.cargoRemark());
}
private OrderDeclarationDtos.Cargo cargoSnapshot(OrderCargoEntity value) {
return new OrderDeclarationDtos.Cargo(value.getItemNo(), value.getCargoName(), value.getCargoNameEn(),
value.getSkuNo(), value.getModel(), value.getPackageCount(), value.getTotalQuantity(),
value.getLengthCm(), value.getWidthCm(), value.getHeightCm(), value.getGrossWeightKg(),
value.getVolumeCbm(), value.getNetWeightKg(), value.getHsCode(), value.getCustomsHsCode(),
value.getUnitsPerPackage(), value.getUnitDeclaredValue(), value.getDeclaredValue(),
value.getCurrency(), value.getMaterial(), value.getProductUsage(), value.getBrand(), value.getCargoRemark());
}
private void apply(OrderCargoEntity value, OrderDeclarationDtos.Cargo source, long operatorId) {
value.setCargoName(source.cargoName()); value.setCargoNameEn(source.cargoNameEn()); value.setSkuNo(source.skuNo()); value.setModel(source.model());
value.setPackageCount(source.packageCount()); value.setTotalQuantity(source.totalQuantity()); value.setLengthCm(source.lengthCm()); value.setWidthCm(source.widthCm()); value.setHeightCm(source.heightCm());
@@ -146,6 +196,15 @@ public class OrderDeclarationUpdateService {
repository.insertHistory(history);
}
private void recordPlanningHistory(long tenantId, long orderId, long operatorId, String operatorName,
OrderStatus status, int changedFields) {
OrderStatusHistoryEntity history = new OrderStatusHistoryEntity();
history.setTenantId(tenantId); history.setOrderId(orderId); history.setFromStatus(status); history.setToStatus(status);
history.setAction("PLANNING_UPDATE"); history.setReason("平台修改运营预估,共 " + changedFields + " 个字段");
history.setOperatorId(operatorId); history.setOperatorName(operatorName); history.setOccurredAt(LocalDateTime.now());
repository.insertHistory(history);
}
private void writeOutbox(OrderEntity order, long operatorId, OrderDeclarationDtos.UpdateRequest request,
String channelCode, String channelName, boolean channelChanged,
List<OrderDeclarationDtos.FieldChange> changes, int version) {
@@ -280,15 +280,16 @@ public class UnifiedOrderQueryService {
UnifiedOrderResponse.CoLoadGroupSummary coLoadGroup) {
if (summary == null) {
String mainStatus = orderMainStatus(order.getStatus());
return new UnifiedOrderResponse.Row(order.getId(), order.getTenantId(), order.getOrderNo(), mainStatus,
return new UnifiedOrderResponse.Row(order.getId(), order.getVersion(), order.getTenantId(), order.getOrderNo(), mainStatus,
order.getStatus() == OrderStatus.SUBMITTED ? "APPROVE_ORDER" : null,
relation(order, childCounts),
order.getStatus(), order.getCustomerId(), order.getCustomerName(),
order.getChannelCode(), order.getChannelName(), Boolean.TRUE.equals(order.getChannelChanged()), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(),
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), null, null,
order.getInboundCode(),
order.getExpectedShipDate(), totalPackageCount(cargo), null, null, volumeCbm(cargo), null, null,
null, null, null, null, order.getPickupMethod(), order.getTaxMode(), order.getContainerMode(),
order.getExpectedShipDate(), totalPackageCount(cargo), null, null,
order.getEstimatedVolumeCbm(), volumeCbm(cargo), null, null,
order.getExpectedArrivalAt(), null, null, null, order.getPickupMethod(), order.getTaxMode(), order.getContainerMode(),
order.getCustomsInspection(), order.getDestinationType(), order.getAmazonWarehouseCode(),
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),
@@ -300,12 +301,13 @@ public class UnifiedOrderQueryService {
boolean pendingReview = order.getStatus() == OrderStatus.SUBMITTED;
String mainStatus = pendingReview ? "PENDING_REVIEW" : summary.mainStatus();
String nextAction = pendingReview ? "APPROVE_ORDER" : summary.nextAction();
return new UnifiedOrderResponse.Row(order.getId(), order.getTenantId(), order.getOrderNo(),
return new UnifiedOrderResponse.Row(order.getId(), order.getVersion(), order.getTenantId(), order.getOrderNo(),
mainStatus, nextAction, relation(order, childCounts), order.getStatus(),
order.getCustomerId(), order.getCustomerName(),
order.getChannelCode(), order.getChannelName(), Boolean.TRUE.equals(order.getChannelChanged()), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(),
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), inbound.id(), inbound.version(), inbound.inboundCode(),
order.getExpectedShipDate(), inbound.totalPackageCount() != null ? inbound.totalPackageCount() : totalPackageCount(cargo), null, inbound.locationCodes(), volumeCbm(cargo),
order.getExpectedShipDate(), inbound.totalPackageCount() != null ? inbound.totalPackageCount() : totalPackageCount(cargo), null, inbound.locationCodes(),
order.getEstimatedVolumeCbm(), volumeCbm(cargo),
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(),
order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(),
inbound.companyName(), inbound.businessType(), order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
@@ -324,14 +326,15 @@ public class UnifiedOrderQueryService {
CargoSummary cargo,
UnifiedOrderResponse.CoLoadGroupSummary coLoadGroup) {
var inbound = summary.inbound();
return new UnifiedOrderResponse.Row(inbound.orderId(), inbound.tenantId(), inbound.orderNo(),
return new UnifiedOrderResponse.Row(inbound.orderId(), order == null ? null : order.getVersion(), inbound.tenantId(), inbound.orderNo(),
summary.mainStatus(), summary.nextAction(), relation(order, childCounts),
parseOrderStatus(inbound.orderStatusSnapshot()),
inbound.customerId(), inbound.customerName(), inbound.channelCode(), inbound.channelName(),
order != null && Boolean.TRUE.equals(order.getChannelChanged()), inbound.warehouseId(),
inbound.warehouseName(), inbound.deliveryWarehouseId(), inbound.deliveryWarehouseName(),
inbound.id(), inbound.version(), inbound.inboundCode(), null,
inbound.totalPackageCount() != null ? inbound.totalPackageCount() : totalPackageCount(cargo), null, inbound.locationCodes(), volumeCbm(cargo),
inbound.totalPackageCount() != null ? inbound.totalPackageCount() : totalPackageCount(cargo), null, inbound.locationCodes(),
order == null ? null : order.getEstimatedVolumeCbm(), volumeCbm(cargo),
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(),
order != null && order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(), inbound.companyName(),
inbound.businessType(), order == null ? null : order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
+1
View File
@@ -20,6 +20,7 @@ CREATE TABLE IF NOT EXISTS ord_order (
destination_warehouse_name VARCHAR(128) NOT NULL COMMENT '目的仓名称快照',
expected_ship_date DATE NULL COMMENT '预计发运日期',
expected_arrival_at DATETIME(3) NULL COMMENT '客户申报预计到货时间',
estimated_volume_cbm DECIMAL(18,4) NULL COMMENT '平台运营预估体积(立方米),独立于客户下单及实测体积',
customer_remark VARCHAR(1000) NULL COMMENT '客户可见备注',
pickup_method VARCHAR(24) NULL COMMENT '取货方式',
requested_pickup_at DATETIME(3) NULL COMMENT '客户要求平台上门提货的预约时间',
+26 -1
View File
@@ -231,6 +231,21 @@ paths:
responses:
'200': {description: 成功返回新版本、渠道历史标记和字段级前后值;版本冲突、渠道与不可逆履约或已确认费用不兼容时响应体业务码为 A0409}
'403': {description: 无权限或非平台身份}
/api/platform/v1/orders/{orderId}/planning:
put:
summary: 版本化修改订单运营预估
description: 需要 platform:order:declaration:edit;只修改预计到货时间与平台预估体积,客户下单和仓库实测体积不受影响。
parameters:
- $ref: '#/components/parameters/UnifiedOrderId'
- $ref: '#/components/parameters/UnifiedTenantId'
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/OrderPlanningUpdateRequest'}
responses:
'200': {description: 成功返回新订单版本;版本冲突时响应体业务码为 A0409}
'403': {description: 无权限或非平台身份}
/api/merchant/v1/orders:
get:
summary: 客户统一订单列表
@@ -585,9 +600,17 @@ components:
expectedVersion: {type: integer, minimum: 0}
customerRemark: {type: string, nullable: true, maxLength: 1000}
expectedArrivalAt: {type: string, format: date-time, nullable: true}
estimatedVolumeCbm: {type: number, minimum: 0.0001, nullable: true, description: 平台运营预估体积,独立于客户下单货物体积与仓库实测体积}
channelCode: {type: string, maxLength: 64, description: 与 channelName 同时传;变更时重新校验价格渠道,入仓码保持不变}
channelName: {type: string, maxLength: 128}
cargoes: {type: array, minItems: 1, maxItems: 500, items: {$ref: '#/components/schemas/OrderDeclarationCargo'}}
OrderPlanningUpdateRequest:
type: object
required: [expectedVersion]
properties:
expectedVersion: {type: integer, minimum: 0}
expectedArrivalAt: {type: string, format: date-time, nullable: true}
estimatedVolumeCbm: {type: number, minimum: 0.0001, nullable: true}
OrderDeclarationCargo:
type: object
required: [itemNo, cargoName, packageCount, totalQuantity, lengthCm, widthCm, heightCm, grossWeightKg, volumeCbm]
@@ -668,6 +691,7 @@ components:
type: object
properties:
orderId: {type: integer, format: int64}
orderVersion: {type: integer, nullable: true, description: 列表行编辑使用的订单乐观锁版本,不作为业务编号展示}
tenantId: {type: integer, format: int64}
orderNo: {type: string}
mainStatus: {$ref: '#/components/schemas/UnifiedMainStatus'}
@@ -690,7 +714,8 @@ components:
totalPackageCount: {type: integer, nullable: true, description: 无入仓事实时为当前订单有效货物箱数合计;存在入仓业务单元后为该单元履约总箱数,拆分后不以父单原始箱数覆盖}
recommendedPalletType: {type: string, nullable: true, description: 当前无列表级权威事实时返回 null,不得返回固定托盘类型}
locationCodes: {type: string, nullable: true}
expectedVolumeCbm: {type: number, nullable: true, description: Order 客户申报货物体积合计,不得与实测体积混用}
estimatedVolumeCbm: {type: number, nullable: true, description: 平台运营填写的预估体积,不得与客户下单或实测体积混用}
expectedVolumeCbm: {type: number, nullable: true, description: Order 客户下单货物体积合计,不得与平台预估或实测体积混用}
actualGrossWeightKg: {type: number, nullable: true, description: 仅 Firstleg 实测毛重;无实测时返回 null}
actualVolumeCbm: {type: number, nullable: true, description: 仅 Firstleg 实测体积;无实测时返回 null}
expectedArrivalAt: {type: string, format: date-time, nullable: true}
@@ -29,7 +29,7 @@ class OrderDeclarationUpdateContractTest {
assertThat(OrderDeclarationDtos.UpdateRequest.class.getRecordComponents())
.extracting(component -> component.getName())
.containsExactly("expectedVersion", "customerRemark", "expectedArrivalAt",
"channelCode", "channelName", "cargoes")
"estimatedVolumeCbm", "channelCode", "channelName", "cargoes")
.doesNotContain("receivingWarehouseId", "receivingWarehouseName",
"destinationWarehouseId", "destinationWarehouseName", "inboundCode", "orderNo");
assertThat(OrderDeclarationDtos.Cargo.class.getRecordComponents())
@@ -40,17 +40,33 @@ class OrderDeclarationUpdateContractTest {
"measurementReviewStatus", "feeReviewStatus");
}
@Test
void planningEndpointOnlyAcceptsVersionArrivalAndOperatorEstimate() throws Exception {
var method = PlatformUnifiedOrderController.class.getDeclaredMethod("updatePlanning",
com.baiyue.common.security.AuthenticatedUser.class, long.class, long.class,
OrderDeclarationDtos.PlanningUpdateRequest.class);
assertThat(method.getAnnotation(PutMapping.class).value()).containsExactly("/{orderId}/planning");
assertThat(method.getAnnotation(PreAuthorize.class).value())
.isEqualTo("hasAuthority('platform:order:declaration:edit')");
assertThat(OrderDeclarationDtos.PlanningUpdateRequest.class.getRecordComponents())
.extracting(component -> component.getName())
.containsExactly("expectedVersion", "expectedArrivalAt", "estimatedVolumeCbm");
}
@Test
void openApiSeparatesDeclaredAndMeasuredFacts() throws Exception {
String source = new ClassPathResource("openapi/order-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
Map<String, Object> document = map(new Yaml().load(source));
assertThat(map(document.get("paths")))
.containsKey("/api/platform/v1/orders/{orderId}/declaration");
.containsKeys("/api/platform/v1/orders/{orderId}/declaration",
"/api/platform/v1/orders/{orderId}/planning");
Map<String, Object> schemas = map(map(document.get("components")).get("schemas"));
Map<String, Object> row = map(map(schemas.get("UnifiedOrderRow")).get("properties"));
assertThat(row).containsKeys("expectedVolumeCbm", "actualVolumeCbm", "expectedArrivalAt", "channelChanged");
assertThat(row).containsKeys("orderVersion", "estimatedVolumeCbm", "expectedVolumeCbm",
"actualVolumeCbm", "expectedArrivalAt", "channelChanged");
assertThat(map(row.get("actualVolumeCbm")).get("description").toString()).contains("仅 Firstleg 实测");
assertThat(map(row.get("estimatedVolumeCbm")).get("description").toString()).contains("平台运营");
assertThat(source).contains("ORDER_DECLARATION_UPDATED_V1", "platform:order:declaration:edit", "A0409",
"/internal/firstleg/v1/inbounds/order-channel-changes/validate",
"/internal/firstleg/v1/pricing/order-channel-changes/validate",
@@ -43,7 +43,7 @@ class OrderDeclarationUpdateServiceTest {
when(repository.findOwned(100L, 200L)).thenReturn(order);
when(repository.findCargo(100L, 200L)).thenReturn(List.of(cargo));
when(repository.updateDeclarationHeader(eq(100L), eq(200L), eq(3), eq(700L),
any(), any(), anyString(), anyString(), anyBoolean())).thenReturn(1);
any(), any(), any(), anyString(), anyString(), anyBoolean())).thenReturn(1);
when(repository.updateDeclarationCargo(eq(100L), eq(200L), any())).thenReturn(1);
var request = request(3, "NEW", "新渠道", "新备注");
@@ -54,7 +54,7 @@ class OrderDeclarationUpdateServiceTest {
assertThat(response.version()).isEqualTo(4);
assertThat(response.channelChanged()).isTrue();
assertThat(response.changes()).extracting(OrderDeclarationDtos.FieldChange::field)
.contains("channelCode", "channelName", "customerRemark", "expectedArrivalAt",
.contains("channelCode", "channelName", "customerRemark", "expectedArrivalAt", "estimatedVolumeCbm",
"cargoName", "packageCount", "totalQuantity", "lengthCm", "grossWeightKg");
verify(pricingChannelGuard).requireAvailable(100L, 31L, "NEW", "FBA");
verify(repository).insertHistory(any());
@@ -71,7 +71,7 @@ class OrderDeclarationUpdateServiceTest {
request(3, "OLD", "旧渠道", "备注")))
.isInstanceOf(BizException.class).hasMessageContaining("版本已变化");
verify(repository, never()).updateDeclarationHeader(anyLong(), anyLong(), anyInt(), anyLong(),
any(), any(), any(), any(), anyBoolean());
any(), any(), any(), any(), any(), anyBoolean());
}
@Test
@@ -80,7 +80,7 @@ class OrderDeclarationUpdateServiceTest {
when(repository.findOwned(100L, 200L)).thenReturn(order);
when(repository.findCargo(100L, 200L)).thenReturn(List.of(cargo()));
when(repository.updateDeclarationHeader(anyLong(), anyLong(), anyInt(), anyLong(), any(), any(),
any(), any(), anyBoolean())).thenReturn(1);
any(), any(), any(), anyBoolean())).thenReturn(1);
when(repository.updateDeclarationCargo(anyLong(), anyLong(), any())).thenReturn(1);
when(json.writeValueAsString(any())).thenReturn("{\"schemaVersion\":1}");
@@ -98,7 +98,7 @@ class OrderDeclarationUpdateServiceTest {
when(repository.findOwned(100L, 200L)).thenReturn(order);
when(repository.findCargo(100L, 200L)).thenReturn(List.of(cargo()));
when(repository.updateDeclarationHeader(anyLong(), anyLong(), anyInt(), anyLong(), any(), any(),
any(), any(), anyBoolean())).thenReturn(1);
any(), any(), any(), anyBoolean())).thenReturn(1);
when(repository.updateDeclarationCargo(anyLong(), anyLong(), any())).thenReturn(1);
when(json.writeValueAsString(any())).thenReturn("{\"schemaVersion\":1}");
@@ -125,7 +125,34 @@ class OrderDeclarationUpdateServiceTest {
verify(fulfillmentChannelGuard).requireEligible(order, 4L, "NEW");
verify(pricingChannelGuard, never()).requireOrderChannelChangeEligible(any(), anyLong(), anyString(), anyString());
verify(repository, never()).updateDeclarationHeader(anyLong(), anyLong(), anyInt(), anyLong(),
any(), any(), any(), any(), anyBoolean());
any(), any(), any(), any(), any(), anyBoolean());
}
@Test
void updatesPlanningFieldsWithCurrentDeclarationSnapshotAndVersion() {
OrderEntity order = order(OrderStatus.SUBMITTED, 3);
order.setExpectedArrivalAt(LocalDateTime.of(2026, 9, 10, 8, 30));
order.setEstimatedVolumeCbm(new BigDecimal("0.25"));
OrderCargoEntity cargo = cargo();
when(repository.findOwned(100L, 200L)).thenReturn(order);
when(repository.findCargo(100L, 200L)).thenReturn(List.of(cargo));
when(repository.updateDeclarationHeader(eq(100L), eq(200L), eq(3), eq(700L),
eq("旧备注"), eq(LocalDateTime.of(2026, 9, 12, 16, 0)), eq(new BigDecimal("0.50")),
eq("OLD"), eq("旧渠道"), eq(false))).thenReturn(1);
when(repository.updateDeclarationCargo(eq(100L), eq(200L), any())).thenReturn(1);
var response = service.updatePlanning(100L, 200L, 700L, "平台操作员",
new OrderDeclarationDtos.PlanningUpdateRequest(3,
LocalDateTime.of(2026, 9, 12, 16, 0), new BigDecimal("0.50")));
assertThat(response.version()).isEqualTo(4);
assertThat(response.changes()).extracting(OrderDeclarationDtos.FieldChange::field)
.containsExactlyInAnyOrder("expectedArrivalAt", "estimatedVolumeCbm");
verify(pricingChannelGuard, never()).requireAvailable(anyLong(), anyLong(), anyString(), anyString());
verify(fulfillmentChannelGuard, never()).requireEligible(any(), anyLong(), anyString());
verify(repository, never()).updateDeclarationCargo(anyLong(), anyLong(), any());
assertThat(cargo.getCargoName()).isEqualTo("旧品名");
assertThat(cargo.getPackageCount()).isEqualTo(10);
}
private OrderEntity order(OrderStatus status, int version) {
@@ -156,6 +183,7 @@ class OrderDeclarationUpdateServiceTest {
"8518", "851821", 10, new BigDecimal("5"), new BigDecimal("1000"),
"USD", "塑料", "音响", "Brand", "新备注");
return new OrderDeclarationDtos.UpdateRequest(version, remark,
LocalDateTime.of(2026, 9, 15, 10, 30), channelCode, channelName, List.of(cargo));
LocalDateTime.of(2026, 9, 15, 10, 30), new BigDecimal("2.5"),
channelCode, channelName, List.of(cargo));
}
}