This commit is contained in:
2026-08-10 12:46:12 +08:00
parent fc554273b9
commit 986e97acb6
14 changed files with 198 additions and 13 deletions
@@ -39,7 +39,12 @@ public final class OrderSplitDtos {
) {}
public record Child(Long orderId, String orderNo, Integer splitSequence,
String splitRole, Integer packageCount) {}
String splitRole, Integer packageCount, Long sourceOrderId,
Long loadingPlanId, Long containerBookingId) {
public Child(Long orderId, String orderNo, Integer splitSequence, String splitRole, Integer packageCount) {
this(orderId, orderNo, splitSequence, splitRole, packageCount, null, null, null);
}
}
public record Tree(Long parentOrderId, String parentOrderNo, Integer parentVersion,
List<Child> children, Integer allocatedPackageCount,
@@ -15,6 +15,7 @@ public class OrderFulfillmentSplitEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private Long tenantId;
private Long parentOrderId;
private Long sourceOrderId;
private Long childOrderId;
private Long loadingPlanId;
private Long containerBookingId;
@@ -36,6 +36,8 @@ public class OrderFulfillmentSplitService {
return response(req, splits.lockOrder(req.tenantId(), req.parentOrderId()), replay);
}
OrderEntity parent = splits.lockOrder(req.tenantId(), req.parentOrderId());
replay = splits.findByIdempotency(req.tenantId(), req.parentOrderId(), req.idempotencyKey());
if (!replay.isEmpty()) return response(req, parent, replay);
if (parent == null) throw new BizException(ResultCode.NOT_FOUND, "父订单不存在或不属于该客户");
if (!req.parentOrderId().equals(req.sourceOrderId()) && !isDirectChild(req.sourceOrderId(), parent))
throw new BizException(ResultCode.CONFLICT, "来源订单必须是父订单或其直接子订单");
@@ -67,7 +69,7 @@ public class OrderFulfillmentSplitService {
List<OrderFulfillmentSplitEntity> rows = splits.findByParent(tenantId, parentId);
Map<Long,OrderEntity> children = new HashMap<>();
for (OrderFulfillmentSplitEntity row: rows) { OrderEntity child=orders.selectById(row.getChildOrderId()); if(child!=null&&tenantId==child.getTenantId()) children.put(child.getId(),child); }
List<OrderSplitDtos.Child> result = rows.stream().map(r -> { OrderEntity c=children.get(r.getChildOrderId()); return new OrderSplitDtos.Child(r.getChildOrderId(), c==null?null:c.getOrderNo(), c==null?null:c.getSplitSequence(), r.getSplitRole(), "LOADED".equals(r.getSplitRole())?r.getLoadedPackageCount():r.getRemainingPackageCount()); }).toList();
List<OrderSplitDtos.Child> result = rows.stream().map(r -> { OrderEntity c=children.get(r.getChildOrderId()); return new OrderSplitDtos.Child(r.getChildOrderId(), c==null?null:c.getOrderNo(), c==null?null:c.getSplitSequence(), r.getSplitRole(), "LOADED".equals(r.getSplitRole())?r.getLoadedPackageCount():r.getRemainingPackageCount(), r.getSourceOrderId(), r.getLoadingPlanId(), r.getContainerBookingId()); }).toList();
int allocated = rows.stream().filter(r -> "LOADED".equals(r.getSplitRole())).mapToInt(OrderFulfillmentSplitEntity::getLoadedPackageCount).sum();
return new OrderSplitDtos.Tree(parent.getId(), parent.getOrderNo(), parent.getVersion(), result, allocated, Math.max(0, rows.stream().mapToInt(OrderFulfillmentSplitEntity::getSourcePackageCount).findFirst().orElse(0)-allocated));
}
@@ -53,6 +53,7 @@ CREATE TABLE IF NOT EXISTS ord_order_fulfillment_split (
id BIGINT UNSIGNED NOT NULL,
tenant_id BIGINT UNSIGNED NOT NULL,
parent_order_id BIGINT UNSIGNED NOT NULL,
source_order_id BIGINT UNSIGNED NOT NULL,
child_order_id BIGINT UNSIGNED NOT NULL,
split_role VARCHAR(12) NOT NULL COMMENT 'LOADED或REMAINDER',
loading_plan_id BIGINT UNSIGNED NULL,
@@ -74,6 +75,7 @@ CREATE TABLE IF NOT EXISTS ord_order_fulfillment_split (
UNIQUE KEY uk_order_split_idempotency (tenant_id,parent_order_id,idempotency_key,split_role),
UNIQUE KEY uk_order_split_child (tenant_id,child_order_id),
KEY idx_order_split_parent (tenant_id,parent_order_id,created_at,id),
KEY idx_order_split_source (tenant_id,source_order_id,split_role),
KEY idx_order_split_container (tenant_id,container_booking_id,created_at,id),
CONSTRAINT fk_order_split_parent FOREIGN KEY(parent_order_id) REFERENCES ord_order(id),
CONSTRAINT fk_order_split_child FOREIGN KEY(child_order_id) REFERENCES ord_order(id),
@@ -114,6 +116,7 @@ END$$
DELIMITER ;
CALL phase3_order_add_column('ord_order_fulfillment_split','split_role',"VARCHAR(12) NOT NULL DEFAULT 'LOADED' COMMENT 'LOADED或REMAINDER'");
CALL phase3_order_add_column('ord_order_fulfillment_split','source_order_id','BIGINT UNSIGNED NULL COMMENT ''实际被消费的来源订单ID''');
CALL phase3_order_add_column('ord_order_fulfillment_split','request_hash',"CHAR(64) NULL COMMENT '请求内容SHA-256;存量记录允许为空'");
DROP PROCEDURE IF EXISTS phase3_order_drop_split_index;
@@ -228,6 +228,46 @@ paths:
responses:
'200': {description: 成功或业务失败,按响应体 code 判断}
'403': {description: 需要 platform:order:edit 权限}
/internal/order/v1/fulfillment-splits:
post:
summary: 创建父订单装柜拆分(Firstleg 内部)
parameters:
- $ref: '#/components/parameters/InternalToken'
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/FulfillmentSplitCreateRequest' }
responses:
'200': { description: 成功, content: { application/json: { schema: { $ref: '#/components/schemas/FulfillmentSplitResponse' } } } }
'409': { description: 数量或版本/幂等冲突 }
/internal/order/v1/fulfillment-splits/{tenantId}/{parentOrderId}:
get:
summary: 查询父子订单柜次树(Firstleg 内部)
parameters:
- $ref: '#/components/parameters/InternalToken'
- { name: tenantId, in: path, required: true, schema: { type: integer, format: int64 } }
- { name: parentOrderId, in: path, required: true, schema: { type: integer, format: int64 } }
responses:
'200': { description: 成功 }
'403': { description: 内部凭证无效 }
/api/platform/v1/orders/{orderId}/fulfillment-tree:
get:
summary: 查询父子订单与柜次数量摘要
parameters:
- { name: orderId, in: path, required: true, schema: { type: integer, format: int64 } }
- { name: tenantId, in: query, required: true, schema: { type: integer, format: int64 } }
responses: { '200': { description: 成功 }, '403': { description: 无权限 } }
/api/merchant/v1/orders/{orderId}/fulfillment-tree:
get:
summary: 查询我的父子订单与柜次数量摘要
parameters:
- { name: orderId, in: path, required: true, schema: { type: integer, format: int64 } }
responses: { '200': { description: 成功 }, '403': { description: 无权限 } }
components:
securitySchemes:
bearerAuth: