Merge remote-tracking branch 'origin/test' into dev
# Conflicts: # deploy/sql/头程数据库统一增量-MySQL8.sql
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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 '客户要求平台上门提货的预约时间',
|
||||
|
||||
@@ -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}
|
||||
|
||||
+19
-3
@@ -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",
|
||||
|
||||
+35
-7
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user