fix(order): remove manual completion bypass of fulfillment gate

This commit is contained in:
2026-09-30 15:26:35 +08:00
parent 22584e7fde
commit 71202bce70
6 changed files with 31 additions and 16 deletions
@@ -6067,7 +6067,6 @@ INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id,
INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5102012,0,'PLATFORM','BUTTON',5102010,'platform:order:reject','订单驳回',NULL,NULL,20,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0); INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5102012,0,'PLATFORM','BUTTON',5102010,'platform:order:reject','订单驳回',NULL,NULL,20,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0);
INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5102013,0,'PLATFORM','BUTTON',5102010,'platform:order:accept','订单受理',NULL,NULL,30,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0); INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5102013,0,'PLATFORM','BUTTON',5102010,'platform:order:accept','订单受理',NULL,NULL,30,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0);
INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5102014,0,'PLATFORM','BUTTON',5102010,'platform:order:process','开始处理',NULL,NULL,40,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0); INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5102014,0,'PLATFORM','BUTTON',5102010,'platform:order:process','开始处理',NULL,NULL,40,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0);
INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5102015,0,'PLATFORM','BUTTON',5102010,'platform:order:complete','完成订单',NULL,NULL,50,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0);
INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5102016,0,'PLATFORM','BUTTON',5102010,'platform:order:cancel','取消订单',NULL,NULL,60,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0); INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5102016,0,'PLATFORM','BUTTON',5102010,'platform:order:cancel','取消订单',NULL,NULL,60,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0);
INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5102017,0,'PLATFORM','BUTTON',5102010,'platform:order:note','内部备注',NULL,NULL,70,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0); INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5102017,0,'PLATFORM','BUTTON',5102010,'platform:order:note','内部备注',NULL,NULL,70,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0);
INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5102018,0,'PLATFORM','BUTTON',5102010,'platform:order:fulfillment','登记履约快递',NULL,NULL,80,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0); INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5102018,0,'PLATFORM','BUTTON',5102010,'platform:order:fulfillment','登记履约快递',NULL,NULL,80,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0);
@@ -6449,7 +6448,6 @@ INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at)
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000001,5102012,0,CURRENT_TIMESTAMP(3)); INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000001,5102012,0,CURRENT_TIMESTAMP(3));
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000001,5102013,0,CURRENT_TIMESTAMP(3)); INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000001,5102013,0,CURRENT_TIMESTAMP(3));
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000001,5102014,0,CURRENT_TIMESTAMP(3)); INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000001,5102014,0,CURRENT_TIMESTAMP(3));
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000001,5102015,0,CURRENT_TIMESTAMP(3));
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000001,5102016,0,CURRENT_TIMESTAMP(3)); INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000001,5102016,0,CURRENT_TIMESTAMP(3));
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000001,5102017,0,CURRENT_TIMESTAMP(3)); INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000001,5102017,0,CURRENT_TIMESTAMP(3));
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000001,5102018,0,CURRENT_TIMESTAMP(3)); INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000001,5102018,0,CURRENT_TIMESTAMP(3));
@@ -6730,7 +6728,6 @@ INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at)
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000003,5102012,0,CURRENT_TIMESTAMP(3)); INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000003,5102012,0,CURRENT_TIMESTAMP(3));
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000003,5102013,0,CURRENT_TIMESTAMP(3)); INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000003,5102013,0,CURRENT_TIMESTAMP(3));
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000003,5102014,0,CURRENT_TIMESTAMP(3)); INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000003,5102014,0,CURRENT_TIMESTAMP(3));
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000003,5102015,0,CURRENT_TIMESTAMP(3));
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000003,5102016,0,CURRENT_TIMESTAMP(3)); INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000003,5102016,0,CURRENT_TIMESTAMP(3));
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000003,5102017,0,CURRENT_TIMESTAMP(3)); INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000003,5102017,0,CURRENT_TIMESTAMP(3));
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000003,5102018,0,CURRENT_TIMESTAMP(3)); INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000003,5102018,0,CURRENT_TIMESTAMP(3));
@@ -101,7 +101,6 @@ VALUES
(5102012, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:reject', '订单驳回', NULL, NULL, 20, 'ACTIVE', 0), (5102012, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:reject', '订单驳回', NULL, NULL, 20, 'ACTIVE', 0),
(5102013, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:accept', '订单受理', NULL, NULL, 30, 'ACTIVE', 0), (5102013, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:accept', '订单受理', NULL, NULL, 30, 'ACTIVE', 0),
(5102014, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:process', '开始处理', NULL, NULL, 40, 'ACTIVE', 0), (5102014, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:process', '开始处理', NULL, NULL, 40, 'ACTIVE', 0),
(5102015, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:complete', '完成订单', NULL, NULL, 50, 'ACTIVE', 0),
(5102016, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:cancel', '取消订单', NULL, NULL, 60, 'ACTIVE', 0), (5102016, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:cancel', '取消订单', NULL, NULL, 60, 'ACTIVE', 0),
(5102017, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:note', '内部备注', NULL, NULL, 70, 'ACTIVE', 0), (5102017, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:note', '内部备注', NULL, NULL, 70, 'ACTIVE', 0),
(5102018, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:fulfillment', '登记履约快递', NULL, NULL, 80, 'ACTIVE', 0), (5102018, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:fulfillment', '登记履约快递', NULL, NULL, 80, 'ACTIVE', 0),
@@ -291,16 +291,6 @@ public class PlatformFirstLegOrderController {
return transit(user, tenantId, id, OrderStatus.PROCESSING, null); return transit(user, tenantId, id, OrderStatus.PROCESSING, null);
} }
@BusinessAudit(event = "ORDER_COMPLETE", resourceType = "FIRST_LEG_ORDER", resourceId = "#id", description = "Complete order / 完成订单")
@Operation(summary = "Complete / 完成订单", description = "Marks a processing order as completed and publishes the integration event. / 将处理中订单标记为完成,并发布集成事件。")
@PostMapping("/{id}/complete")
@PreAuthorize("hasAuthority('platform:order:complete')")
public ApiResponse<Void> complete(@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) {
return transit(user, tenantId, id, OrderStatus.COMPLETED, null);
}
/** 仅履约权限可登记快递信息,商户端保持只读。 */ /** 仅履约权限可登记快递信息,商户端保持只读。 */
@PutMapping("/{id}/express") @PutMapping("/{id}/express")
@PreAuthorize("hasAuthority('platform:order:fulfillment')") @PreAuthorize("hasAuthority('platform:order:fulfillment')")
@@ -174,6 +174,9 @@ public class OrderApplicationService {
@Transactional @Transactional
public void transit(long tenantId, long userId, String username, long id, OrderStatus target, String reason, public void transit(long tenantId, long userId, String username, long id, OrderStatus target, String reason,
Integer expectedVersion) { Integer expectedVersion) {
if (target == OrderStatus.COMPLETED) {
throw new BizException(ResultCode.CONFLICT, "订单完成须由真实签收和全部有效履约分片完成后自动确认");
}
if (target == OrderStatus.CANCELLED) fulfillmentCompletion.lockCancellationRoot(tenantId, id); if (target == OrderStatus.CANCELLED) fulfillmentCompletion.lockCancellationRoot(tenantId, id);
OrderEntity order = require(tenantId, id); OrderEntity order = require(tenantId, id);
if (expectedVersion != null && !expectedVersion.equals(order.getVersion())) { if (expectedVersion != null && !expectedVersion.equals(order.getVersion())) {
@@ -205,7 +208,6 @@ public class OrderApplicationService {
requirePrintableInboundCode(order); requirePrintableInboundCode(order);
order.setSubmittedAt(LocalDateTime.now()); order.setSubmittedAt(LocalDateTime.now());
} }
if (target == OrderStatus.COMPLETED) order.setCompletedAt(LocalDateTime.now());
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update"); if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
record(tenantId, id, from, target, target.name(), reason, userId, username); record(tenantId, id, from, target, target.name(), reason, userId, username);
if (target == OrderStatus.SUBMITTED) prepareSubmittedInbound(order); if (target == OrderStatus.SUBMITTED) prepareSubmittedInbound(order);
@@ -585,7 +587,6 @@ public class OrderApplicationService {
case SUBMITTED -> target == OrderStatus.APPROVED || target == OrderStatus.REJECTED || target == OrderStatus.CANCELLED; case SUBMITTED -> target == OrderStatus.APPROVED || target == OrderStatus.REJECTED || target == OrderStatus.CANCELLED;
case APPROVED -> target == OrderStatus.ACCEPTED || target == OrderStatus.CANCELLED; case APPROVED -> target == OrderStatus.ACCEPTED || target == OrderStatus.CANCELLED;
case ACCEPTED -> target == OrderStatus.PROCESSING || target == OrderStatus.CANCELLED; case ACCEPTED -> target == OrderStatus.PROCESSING || target == OrderStatus.CANCELLED;
case PROCESSING -> target == OrderStatus.COMPLETED;
case REJECTED -> target == OrderStatus.DRAFT || target == OrderStatus.CANCELLED; case REJECTED -> target == OrderStatus.DRAFT || target == OrderStatus.CANCELLED;
default -> false; default -> false;
}; };
@@ -16,6 +16,8 @@ paths:
已完成才完成根订单;来源子单独立完成并在根锁下重新评估聚合。明确 CANCELLED 子单 已完成才完成根订单;来源子单独立完成并在根锁下重新评估聚合。明确 CANCELLED 子单
不参与完成门禁,其他未完成状态均阻断。重复事实不得重复写历史。响应明确返回来源状态 不参与完成门禁,其他未完成状态均阻断。重复事实不得重复写历史。响应明确返回来源状态
status 和履约根 rootOrderId/rootStatus;根尚未聚合完成时允许保留其原状态。 status 和履约根 rootOrderId/rootStatus;根尚未聚合完成时允许保留其原状态。
订单完成仅由此可信履约回写和取消后聚合门禁产生,普通订单状态流转禁止目标 COMPLETED,
平台不提供手工完成订单接口。
parameters: parameters:
- name: X-Internal-Service-Token - name: X-Internal-Service-Token
in: header in: header
@@ -49,6 +49,32 @@ class OrderApplicationServiceStateTest {
static class TestConfig { static class TestConfig {
@Bean ObjectMapper objectMapper() { return new ObjectMapper().findAndRegisterModules(); } @Bean ObjectMapper objectMapper() { return new ObjectMapper().findAndRegisterModules(); }
} }
@Test
void manualCompletionRejectsEveryStateAndRoleWithoutWritingFulfillmentFacts() {
for (String role : List.of("MASTER", "CHILD")) {
for (OrderStatus status : OrderStatus.values()) {
OrderRepository repository = mock(OrderRepository.class);
OrderEntity order = persistedOrder(status);
order.setOrderRole(role);
when(repository.findOwned(10001L, 1L)).thenReturn(order);
clearInvocations(repository);
OrderFulfillmentCompletionService completion = mock(OrderFulfillmentCompletionService.class);
var app = service(repository, mock(OrderEligibilityGuard.class), mock(OrderMasterDataGuard.class));
ReflectionTestUtils.setField(app, "fulfillmentCompletion", completion);
assertThatThrownBy(() -> app.transit(10001, 2, "u", 1, OrderStatus.COMPLETED, null))
.isInstanceOf(BizException.class)
.hasMessageContaining("真实签收");
assertThatThrownBy(() -> app.transit(10001, 2, "u", 1, OrderStatus.COMPLETED, null, 0))
.isInstanceOf(BizException.class)
.hasMessageContaining("真实签收");
verifyNoInteractions(repository, completion);
assertThat(order.getStatus()).isEqualTo(status);
assertThat(order.getCompletedAt()).isNull();
}
}
}
@Test @Test
void cancellationLocksRootBeforeSourceWriteAndThenAggregates() { void cancellationLocksRootBeforeSourceWriteAndThenAggregates() {
OrderRepository repository = mock(OrderRepository.class); OrderRepository repository = mock(OrderRepository.class);