fix(order): enforce versioned platform commands and submissions
This commit is contained in:
+3
-2
@@ -196,8 +196,9 @@ public class MerchantFirstLegOrderController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Parameter(description = "Order ID / 订单 ID", example = "190000000000000001", required = true) @PathVariable long id,
|
||||
@Parameter(description = "幂等键,同一操作者的同一请求重试时必须保持不变", required = true)
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey) {
|
||||
service.submit(OrderScopeGuard.merchant(user), user.userId(), user.username(), id, idempotencyKey);
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody OrderRequests.SubmitRequest request) {
|
||||
service.submit(OrderScopeGuard.merchant(user), user.userId(), user.username(), id, request, idempotencyKey);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
|
||||
+10
-6
@@ -122,9 +122,10 @@ public class PlatformFirstLegOrderController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Parameter(description = "目标租户 ID", required = true) @RequestParam long tenantId,
|
||||
@Parameter(description = "订单 ID", required = true) @PathVariable long id,
|
||||
@Valid @RequestBody OrderRequests.SaveRequest request) {
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody OrderRequests.PlatformOrderUpdateRequest request) {
|
||||
long scopedTenant = OrderScopeGuard.platform(user, tenantId);
|
||||
service.updateSubmittedOrder(scopedTenant, user.userId(), id, request);
|
||||
service.updateSubmittedOrder(scopedTenant, user.userId(), id, request, idempotencyKey);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@@ -141,9 +142,10 @@ public class PlatformFirstLegOrderController {
|
||||
@Parameter(description = "目标租户 ID", required = true) @RequestParam long tenantId,
|
||||
@Parameter(description = "订单 ID", required = true) @PathVariable long id,
|
||||
@Parameter(description = "提交订单幂等键,同键同请求返回首次结果", required = true)
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey) {
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody OrderRequests.SubmitRequest request) {
|
||||
long scopedTenant = OrderScopeGuard.platform(user, tenantId);
|
||||
service.submit(scopedTenant, user.userId(), user.username(), id, idempotencyKey);
|
||||
service.submit(scopedTenant, user.userId(), user.username(), id, request, idempotencyKey);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@@ -277,8 +279,10 @@ public class PlatformFirstLegOrderController {
|
||||
public ApiResponse<Void> cancel(@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.ReasonRequest request) {
|
||||
return transit(user, tenantId, id, OrderStatus.CANCELLED, request.reason());
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody OrderRequests.CancelRequest request) {
|
||||
service.cancel(OrderScopeGuard.platform(user, tenantId), user.userId(), user.username(), id, request, idempotencyKey);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "ORDER_START", resourceType = "FIRST_LEG_ORDER", resourceId = "#id", description = "Start processing / 开始处理")
|
||||
|
||||
@@ -284,6 +284,15 @@ public final class OrderRequests {
|
||||
@NotNull @Valid SaveRequest draft
|
||||
) { }
|
||||
|
||||
@Schema(name = "PlatformOrderUpdateRequest", description = "平台订单版本化幂等编辑")
|
||||
public record PlatformOrderUpdateRequest(
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotNull @Valid SaveRequest draft
|
||||
) { }
|
||||
|
||||
@Schema(name = "OrderSubmitRequest", description = "订单版本化幂等提交")
|
||||
public record SubmitRequest(@NotNull @Min(0) Integer expectedVersion) { }
|
||||
|
||||
@Schema(name = "MerchantOrderCancelRequest", description = "客户订单版本化幂等取消")
|
||||
public record CancelRequest(
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
|
||||
@@ -155,8 +155,15 @@ public class OrderApplicationService {
|
||||
* 因此不会因为该兼容能力意外获得已提交订单的编辑权限。
|
||||
*/
|
||||
@Transactional
|
||||
public void updateSubmittedOrder(long tenantId, long userId, long id, OrderRequests.SaveRequest request) {
|
||||
updateOrderData(tenantId, userId, id, request, true, null);
|
||||
public void updateSubmittedOrder(long tenantId, long userId, long id, OrderRequests.PlatformOrderUpdateRequest request,
|
||||
String idempotencyKey) {
|
||||
requireExpectedVersion(request.expectedVersion());
|
||||
if (request.draft() == null) throw new BizException(ResultCode.VALIDATION_FAILED, "订单内容不能为空");
|
||||
IdempotencyContext command = beginIdempotency(tenantId, userId, "UPDATE_SUBMITTED_ORDER", id, idempotencyKey,
|
||||
requestHash("UPDATE_SUBMITTED_ORDER", List.of(id, request)));
|
||||
if (command.replayed()) return;
|
||||
updateOrderData(tenantId, userId, id, request.draft(), true, request.expectedVersion());
|
||||
completeIdempotency(command, id);
|
||||
}
|
||||
|
||||
private void updateOrderData(long tenantId, long userId, long id, OrderRequests.SaveRequest request,
|
||||
@@ -234,7 +241,10 @@ public class OrderApplicationService {
|
||||
requirePrintableInboundCode(order);
|
||||
order.setSubmittedAt(LocalDateTime.now());
|
||||
}
|
||||
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
|
||||
if (repository.saveOrder(tenantId, order) != 1) {
|
||||
if (target == OrderStatus.SUBMITTED) throw new BizException(ResultCode.CONFLICT, "Concurrent order update");
|
||||
throw new BizException("Concurrent order update");
|
||||
}
|
||||
record(tenantId, id, from, target, target.name(), reason, userId, username);
|
||||
if (target == OrderStatus.SUBMITTED) prepareSubmittedInbound(order);
|
||||
if (target == OrderStatus.SUBMITTED || target == OrderStatus.APPROVED || target == OrderStatus.CANCELLED) {
|
||||
@@ -245,13 +255,15 @@ public class OrderApplicationService {
|
||||
|
||||
/** 提交订单的幂等入口。 */
|
||||
@Transactional
|
||||
public void submit(long tenantId, long userId, String username, long id, String idempotencyKey) {
|
||||
public void submit(long tenantId, long userId, String username, long id, OrderRequests.SubmitRequest request,
|
||||
String idempotencyKey) {
|
||||
requireExpectedVersion(request.expectedVersion());
|
||||
IdempotencyContext idempotency = beginIdempotency(tenantId, userId, "SUBMIT_ORDER", id, idempotencyKey,
|
||||
requestHash("SUBMIT_ORDER", Map.of("orderId", id)));
|
||||
requestHash("SUBMIT_ORDER", List.of(id, request)));
|
||||
if (idempotency.replayed()) {
|
||||
return;
|
||||
}
|
||||
transit(tenantId, userId, username, id, OrderStatus.SUBMITTED, null);
|
||||
transit(tenantId, userId, username, id, OrderStatus.SUBMITTED, null, request.expectedVersion());
|
||||
completeIdempotency(idempotency, id);
|
||||
}
|
||||
|
||||
|
||||
@@ -626,10 +626,15 @@ paths:
|
||||
/api/merchant/v1/orders/first-leg/{id}/submit:
|
||||
post:
|
||||
summary: 提交订单
|
||||
description: 每条货物的每箱数量、箱数和总数量均必填,且总数量必须等于每箱数量 × 箱数。
|
||||
description: 必须携带读取详情时的expectedVersion;原键同载荷重放先返回首次成功,不重复提交。改版本/订单的同键请求与过期首次提交为A0409。每条货物的每箱数量、箱数和总数量均必填,且总数量必须等于每箱数量 × 箱数。
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/OrderId'
|
||||
- $ref: '#/components/parameters/IdempotencyKey'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/OrderSubmitRequest'}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断}
|
||||
/api/platform/v1/orders/first-leg:
|
||||
@@ -750,28 +755,52 @@ paths:
|
||||
/api/platform/v1/orders/first-leg/{id}/submit:
|
||||
post:
|
||||
summary: 平台代客提交指定订单
|
||||
description: 每条货物的每箱数量、箱数和总数量均必填,且总数量必须等于每箱数量 × 箱数。
|
||||
description: 必须携带读取详情时的expectedVersion;原键同载荷重放先返回首次成功,不重复提交。改版本/订单的同键请求与过期首次提交为A0409。每条货物的每箱数量、箱数和总数量均必填,且总数量必须等于每箱数量 × 箱数。
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/OrderId'
|
||||
- $ref: '#/components/parameters/TenantId'
|
||||
- $ref: '#/components/parameters/IdempotencyKey'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/OrderSubmitRequest'}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断}
|
||||
'403': {description: 需要 platform:order:submit 权限}
|
||||
/api/platform/v1/orders/first-leg/{id}:
|
||||
put:
|
||||
summary: 平台编辑指定订单草稿
|
||||
summary: 平台版本化幂等编辑订单
|
||||
description: 仅DRAFT/REJECTED/SUBMITTED可编辑;SUBMITTED保持不回退。原键同载荷先返回首次成功,不重写明细历史或重复准备入仓。缺版本/旧平铺体拒绝,首次版本冲突或同键异载荷为A0409。
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/OrderId'
|
||||
- $ref: '#/components/parameters/TenantId'
|
||||
- $ref: '#/components/parameters/IdempotencyKey'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/FirstLegOrderDraft'}
|
||||
schema: {$ref: '#/components/schemas/PlatformOrderUpdateRequest'}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断}
|
||||
'403': {description: 需要 platform:order:edit 权限}
|
||||
/api/platform/v1/orders/first-leg/{id}/cancel:
|
||||
post:
|
||||
summary: 平台版本化幂等取消订单
|
||||
description: 复用客户取消命令的版本、原因和幂等规则;原键同载荷成功重放不重复根聚合/历史/事件,同键异载荷或新命令重复取消为A0409。
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/OrderId'
|
||||
- $ref: '#/components/parameters/TenantId'
|
||||
- $ref: '#/components/parameters/IdempotencyKey'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/MerchantOrderCancelRequest'}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断;无业务返回数据}
|
||||
'403': {description: 需要platform:order:cancel权限和平台身份}
|
||||
|
||||
/internal/order/v1/fulfillment-splits:
|
||||
post:
|
||||
summary: 创建父订单装柜拆分(Firstleg 内部)
|
||||
@@ -1279,6 +1308,17 @@ components:
|
||||
type: string
|
||||
enum: [TAX_INCLUDED, TAX_EXCLUDED]
|
||||
description: 报价包税维度;独立于订单 taxMode 的买单/退税语义
|
||||
PlatformOrderUpdateRequest:
|
||||
type: object
|
||||
required: [expectedVersion, draft]
|
||||
properties:
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
draft: {$ref: '#/components/schemas/FirstLegOrderDraft'}
|
||||
OrderSubmitRequest:
|
||||
type: object
|
||||
required: [expectedVersion]
|
||||
properties:
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
MerchantDraftUpdateRequest:
|
||||
type: object
|
||||
required: [expectedVersion, draft]
|
||||
|
||||
Reference in New Issue
Block a user