优化
This commit is contained in:
+7
-3
@@ -10,6 +10,7 @@ import com.baiyue.firstleg.dto.ExceptionReleaseRequest;
|
||||
import com.baiyue.firstleg.dto.MeasurementReviewRequest;
|
||||
import com.baiyue.firstleg.entity.MeasureReviewRecordEntity;
|
||||
import com.baiyue.firstleg.service.MeasurementReviewService;
|
||||
import com.baiyue.firstleg.service.AutomaticInboundCompletionService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -35,6 +36,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequestMapping("/api/platform/v1/inbounds")
|
||||
public class PlatformInboundMeasurementReviewController {
|
||||
@Resource private MeasurementReviewService service;
|
||||
@Resource private AutomaticInboundCompletionService automaticCompletion;
|
||||
|
||||
@BusinessAudit(event = "INBOUND_MEASURE_APPROVE", resourceType = "INBOUND",
|
||||
resourceId = "#inboundId", description = "测量审核通过")
|
||||
@@ -50,10 +52,12 @@ public class PlatformInboundMeasurementReviewController {
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody MeasurementReviewRequest request) {
|
||||
validateScope(user, tenantId);
|
||||
// 审核服务事务在此调用返回时已提交;随后自动编排从新事务/新查询读取 WAIT_PRICING,
|
||||
// 避免在未提交的审核事务中读取旧状态。自动流程失败时保留已审核状态,原样返回业务错误,
|
||||
// 使用同一幂等键重试会从持久化阶段继续推进。
|
||||
service.approve(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request);
|
||||
// 统一订单主线要求测量审核后直接进入 WAIT_SPLIT,不把计费、库位或其他资料
|
||||
// 作为本版本的强制门禁。保留原 FeeSnapshotResponse 泛型以兼容既有客户端。
|
||||
return ApiResponse.success(null);
|
||||
return ApiResponse.success(automaticCompletion.completeApproved(
|
||||
tenantId, inboundId, user.userId(), user.username(), request.measurementId(), request.expectedVersion() + 1));
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "INBOUND_MEASURE_REJECT", resourceType = "INBOUND",
|
||||
|
||||
@@ -537,12 +537,25 @@ paths:
|
||||
/api/platform/v1/inbounds/{inboundId}/measurements/approve:
|
||||
post:
|
||||
summary: 审核通过测量
|
||||
description: 需要 firstleg:inbound:measurement-review 权限;客服审核通过后自动计费、发布费用给客户并完成入库,客户无需确认。
|
||||
description: 需要 firstleg:inbound:measurement-review 权限。审核事务提交后服务端自动执行计费、费用发布和入库编排并返回费用快照;若后续编排失败,审核结果保持已提交,使用同一 Idempotency-Key 重试将从持久化阶段继续,接口不会伪造 WAIT_SPLIT。
|
||||
parameters:
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: Idempotency-Key, in: header, required: true, schema: {type: string}}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [measurementId, expectedVersion]
|
||||
properties:
|
||||
measurementId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
reason: {type: string, maxLength: 1000}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败}
|
||||
'200': {description: 成功或业务失败;成功 data 为 FeeSnapshotResponse,nextStatus=WAIT_SPLIT}
|
||||
'409': {description: 审核已提交但自动编排尚未完成、版本冲突或费用/仓库门禁失败;不得据此推断 WAIT_SPLIT}
|
||||
/api/platform/v1/inbounds/{inboundId}/measurements/reject:
|
||||
post:
|
||||
summary: 驳回测量
|
||||
|
||||
+35
@@ -5,15 +5,21 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.ExceptionReleaseRequest;
|
||||
import com.baiyue.firstleg.dto.MeasurementReviewRequest;
|
||||
import com.baiyue.firstleg.service.AutomaticInboundCompletionService;
|
||||
import com.baiyue.firstleg.service.MeasurementReviewService;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@DisplayName("入库测量审核控制器契约")
|
||||
class InboundMeasurementReviewContractTest {
|
||||
@@ -40,11 +46,40 @@ class InboundMeasurementReviewContractTest {
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/measurements/reject:")
|
||||
.contains("firstleg:inbound:exception-release")
|
||||
.contains("Idempotency-Key")
|
||||
.contains("审核事务提交后服务端自动执行计费")
|
||||
.contains("接口不会伪造 WAIT_SPLIT")
|
||||
.contains("成功 data 为 FeeSnapshotResponse,nextStatus=WAIT_SPLIT")
|
||||
.contains("name: inboundId")
|
||||
.contains("schema: {type: string, pattern:")
|
||||
.doesNotContain("待客户确认");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("审核事务完成后才启动自动编排并返回真实费用快照")
|
||||
void shouldCompleteApprovedReviewAfterReviewTransactionReturns() {
|
||||
MeasurementReviewService review = mock(MeasurementReviewService.class);
|
||||
AutomaticInboundCompletionService completion = mock(AutomaticInboundCompletionService.class);
|
||||
PlatformInboundMeasurementReviewController controller = new PlatformInboundMeasurementReviewController();
|
||||
ReflectionTestUtils.setField(controller, "service", review);
|
||||
ReflectionTestUtils.setField(controller, "automaticCompletion", completion);
|
||||
AuthenticatedUser user = new AuthenticatedUser(9L, 0L, 0L, null,
|
||||
"客服", "PLATFORM", "WEB", Set.of("firstleg:inbound:measurement-review"));
|
||||
MeasurementReviewRequest request = new MeasurementReviewRequest(30L, 4, "通过");
|
||||
var fee = mock(com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse.class);
|
||||
when(completion.completeApproved(10001L, 88L, 9L, "客服", 30L, 5)).thenReturn(fee);
|
||||
|
||||
var first = controller.approve(user, 10001L, 88L, "approve-1", request);
|
||||
var replay = controller.approve(user, 10001L, 88L, "approve-1", request);
|
||||
|
||||
assertThat(first.data()).isSameAs(fee);
|
||||
assertThat(replay.data()).isSameAs(fee);
|
||||
var ordered = inOrder(review, completion);
|
||||
ordered.verify(review).approve(10001L, 9L, "客服", 88L, "approve-1", request);
|
||||
ordered.verify(completion).completeApproved(10001L, 88L, 9L, "客服", 30L, 5);
|
||||
ordered.verify(review).approve(10001L, 9L, "客服", 88L, "approve-1", request);
|
||||
ordered.verify(completion).completeApproved(10001L, 88L, 9L, "客服", 30L, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAuditAutomaticInboundCompletion() throws Exception {
|
||||
Method method = AutomaticInboundCompletionService.class.getDeclaredMethod("completeApproved",
|
||||
|
||||
Reference in New Issue
Block a user