This commit is contained in:
2026-08-08 10:31:44 +08:00
parent ced35f1600
commit 2644718b5f
32 changed files with 9710 additions and 31 deletions
@@ -21,6 +21,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.auth.dto.AccountManagementDtos.ChangePasswordRequest;
import com.baiyue.auth.dto.AccountManagementDtos.ForcedPasswordChangeRequest;
import com.baiyue.auth.dto.AccountManagementDtos;
import com.baiyue.auth.service.AccountManagementService;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -2,6 +2,7 @@ package com.baiyue.auth.controller;
import com.baiyue.auth.dto.AccountManagementDtos.*;
import com.baiyue.auth.service.AccountManagementService;
import com.baiyue.auth.service.AuthService;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
@@ -20,6 +21,8 @@ import java.security.MessageDigest;
public class InternalAccountController {
@Resource
private AccountManagementService service;
@Resource
private AuthService authService;
@Value("${baiyue.security.internal-service-token}")
private String configuredToken;
@@ -76,7 +79,7 @@ public class InternalAccountController {
public ApiResponse<ImpersonationTicketResponse> impersonationTicket(
@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
@Valid @RequestBody ImpersonationTicketRequest request) {
verify(token); return ApiResponse.success(service.createImpersonationTicket(request));
verify(token); return ApiResponse.success(authService.createImpersonationTicket(request));
}
@PutMapping("/tenant/{tenantId}/archive")
@@ -7,6 +7,8 @@ import com.baiyue.auth.entity.AuthAccount;
import com.baiyue.auth.repository.AuthAccountRepository;
import com.baiyue.auth.dto.LoginRequest;
import com.baiyue.auth.dto.TokenResponse;
import com.baiyue.auth.dto.AccountManagementDtos.ImpersonationTicketRequest;
import com.baiyue.auth.dto.AccountManagementDtos.ImpersonationTicketResponse;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
@@ -25,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.List;
@DisplayName("账号管理服务")
@SpringJUnitConfig(classes = {AccountManagementService.class, AccountManagementServiceTest.TestConfig.class})
@@ -170,6 +171,42 @@ class AccountManagementServiceTest {
verify(sessions).revokeAll(10L);
verify(sessions).revokeAll(11L);
}
@Test void archiveTenantDisablesEveryMerchantAccountAndRevokesSessions() {
AuthAccount owner = merchantAccount(20L, 10001L, "ACTIVE", 3);
AuthAccount member = merchantAccount(21L, 10001L, "DISABLED", null);
when(repository.findMerchantByTenant(10001L)).thenReturn(List.of(owner, member));
service.archiveTenant(10001L);
assertEquals("DISABLED", owner.getAccountStatus());
assertEquals(4, owner.getTokenVersion());
assertEquals("DISABLED", member.getAccountStatus());
assertEquals(1, member.getTokenVersion());
verify(repository).save(owner);
verify(repository).save(member);
verify(sessions).revokeAll(20L);
verify(sessions).revokeAll(21L);
}
@Test void archiveTenantIsSuccessfulWhenCustomerHasNoAccounts() {
when(repository.findMerchantByTenant(10001L)).thenReturn(List.of());
service.archiveTenant(10001L);
verify(repository, never()).save(any());
verify(sessions, never()).revokeAll(any());
}
private AuthAccount merchantAccount(long id, long tenantId, String status, Integer tokenVersion) {
AuthAccount value = new AuthAccount();
value.setId(id);
value.setDefaultTenantId(tenantId);
value.setDefaultAppCode("MERCHANT");
value.setAccountStatus(status);
value.setTokenVersion(tokenVersion);
return value;
}
@Configuration
static class TestConfig {
@Bean
@@ -16,6 +16,7 @@ import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -60,8 +61,8 @@ public class GlobalExceptionHandler {
return validationResponse(errors, request);
}
@ExceptionHandler({HttpMessageNotReadableException.class, MissingServletRequestParameterException.class,
MethodArgumentTypeMismatchException.class})
@ExceptionHandler({HttpMessageNotReadableException.class, MissingRequestHeaderException.class,
MissingServletRequestParameterException.class, MethodArgumentTypeMismatchException.class})
public ResponseEntity<ApiResponse<Void>> handleBadRequest(Exception exception, HttpServletRequest request) {
return failure(ResultCode.BAD_REQUEST, ResultCode.BAD_REQUEST.message(), request);
}
@@ -11,6 +11,7 @@ import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Method;
@@ -46,6 +47,19 @@ class GlobalExceptionHandlerTest {
assertThat(response.getBody().code()).isEqualTo(ResultCode.BAD_REQUEST.code());
}
@Test
void missingRequiredRequestHeaderMapsToBadRequest() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
Method method = GlobalExceptionHandlerTest.class.getDeclaredMethod("paging", long.class);
MethodParameter parameter = new MethodParameter(method, 0);
var response = handler.handleBadRequest(
new MissingRequestHeaderException("Idempotency-Key", parameter), request);
assertThat(response.getStatusCode().value()).isEqualTo(200);
assertThat(response.getBody().code()).isEqualTo(ResultCode.BAD_REQUEST.code());
}
@Test
void validationDetailsAreIncludedInMessageAndData() {
MockHttpServletRequest request = new MockHttpServletRequest();
@@ -223,7 +223,7 @@ deploy/
推荐顺序:
1. 外部基础设施:MySQL、Redis、Nacos;启用公共导出时再准备 RocketMQ。
2. 核心认证:IAM、Auth。
2. 核心认证:Auth、IAM。
3. 主数据与适配:Warehouse、Integration。
4. 业务依赖:Order、File。
5. 核心编排与支撑:Firstleg、Print、Audit、Event Bridge。
@@ -231,6 +231,32 @@ deploy/
Nacos 服务发现允许部分服务交换顺序,但联调时按上述顺序更容易判断故障来源。
### Auth/IAM RBAC 会话撤销发布验证
角色启停或角色权限全量变更会由 IAM 调用 Auth 的
`POST /internal/auth/accounts/sessions/revoke`,批量撤销受影响成员的 Access/Refresh 会话。
该契约发布时必须先部署 Auth,再部署 IAM,不得只更新 IAM。
发布后至少完成以下验证:
1. 确认 Auth、IAM 使用同一提交对应的镜像版本,且 IAM 能通过服务发现和内部服务令牌访问上述 Auth 内部接口;404、连接失败、内部令牌不一致或非成功业务码均不得放行 IAM 角色变更。
2. 为测试成员分配带订单查看权限的自定义角色,成员登录并确认订单列表可访问。
3. 停用该角色必须返回业务成功;继续使用停用前的 Access Token 访问订单列表必须被拒绝,旧 Refresh Token 也必须失效。
4. 成员重新登录后权限集合应为空;重新启用角色并再次登录后,原角色权限应恢复。
5. 修改角色权限集合后重复验证旧 Access/Refresh Token 失效,避免 `grantPermissions` 路径遗留旧权限。
2026-08-08 测试环境曾出现版本不一致:停用已分配角色返回 HTTP 200、业务码 `S0503`,
提示“认证会话撤销失败,角色变更未生效”。证据报告为
`reports/client-test-env-20260808021636-b5090589.json`,`requestId=client-e2e-d9e860c8-b061-43fd-b67b-196f76d95b3d`,
`traceId=f2812578869349b1b4393421498d05df`。该结果说明 IAM 已包含会话撤销调用,但 Auth
内部接口尚未部署或不可达;必须在 Auth 先行部署并通过连通性检查后重新执行上述验收。
本轮 Client 扩展测试的发布前最终基准为
`reports/client-test-env-20260808022313-1798dc6c.json`:共 150 项,143 通过、5 失败、2 跳过。
5 个阻断项分别为角色停用和角色权限收缩时 Auth/IAM 版本不一致导致 `S0503`、清空成员角色未撤销旧 JWT、
缺少 `Idempotency-Key` 返回 `S0500`、跨客户资质文件读取返回 `B0001`。订单条件必填 `A0422`、
已提交订单编辑 `A0409`、密码恢复和报告脱敏均已通过。后续发布验证应以该报告为对照,确认上述 5 项全部关闭。
## 9. 开发和拆分约束
- 每个业务服务只访问自己的数据库;禁止跨库 Join 和直接读取其他服务表。
@@ -101,12 +101,18 @@ public class LocalQualificationStorage {
}
public Resource load(long tenantId, String fileName) {
if (!fileName.matches("[0-9a-fA-F-]{36}\\.(jpg|png|pdf)")) throw new BizException("Invalid file name");
if (fileName == null || !fileName.matches("[0-9a-fA-F-]{36}\\.(jpg|png|pdf)")) {
throw fileNotFound();
}
Path directory = tenantDirectory(tenantId);
Path target = directory.resolve(fileName).normalize();
if (!target.startsWith(directory) || !Files.isRegularFile(target)) throw new BizException("File not found");
if (!target.startsWith(directory) || !Files.isRegularFile(target)) throw fileNotFound();
try { return new UrlResource(target.toUri()); }
catch (Exception exception) { throw new BizException("File not found"); }
catch (Exception exception) { throw fileNotFound(); }
}
private BizException fileNotFound() {
return new BizException(ResultCode.NOT_FOUND, "File not found");
}
public void delete(StoredFile file) {
@@ -60,7 +60,10 @@ class LocalQualificationStorageTest {
assertTrue(Files.isRegularFile(stored.path()));
assertEquals(stored.path().toAbsolutePath(),
storage.load(10001L, stored.fileName()).getFile().toPath().toAbsolutePath());
assertThrows(BizException.class, () -> storage.load(10002L, stored.fileName()));
BizException crossTenant = assertThrows(BizException.class,
() -> storage.load(10002L, stored.fileName()));
assertEquals(ResultCode.NOT_FOUND, crossTenant.getResultCode());
assertEquals("File not found", crossTenant.getMessage());
}
@Test
@@ -111,9 +114,22 @@ class LocalQualificationStorageTest {
@Test
void legacySeedPathCannotBePretendedToBeAStoredFile() {
assertThrows(BizException.class,
BizException exception = assertThrows(BizException.class,
() -> storage.load(10001L, "demo-business-license.jpg"));
assertEquals(ResultCode.NOT_FOUND, exception.getResultCode());
assertEquals("File not found", exception.getMessage());
}
@Test
void missingQualificationReturnsNotFoundWithoutRevealingOwnership() {
storage.initialize();
BizException exception = assertThrows(BizException.class,
() -> storage.load(10001L, "00000000-0000-0000-0000-000000000000.pdf"));
assertEquals(ResultCode.NOT_FOUND, exception.getResultCode());
assertEquals("File not found", exception.getMessage());
}
@Test
@@ -4,8 +4,10 @@ import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.dto.OpenApiMeasurementDtos.EventResponse;
import com.baiyue.firstleg.dto.CartonMeasurementResult;
import com.baiyue.firstleg.security.MeasurementRequestHeaders;
import com.baiyue.firstleg.service.InboundMeasurementOpenApiService;
import com.baiyue.firstleg.service.MojiaMeasurementOpenApiService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
@@ -22,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.multipart.MultipartFile;
/** 第三方设备长宽高测量开放接口。 */
@@ -36,6 +39,21 @@ import org.springframework.web.multipart.MultipartFile;
public class OpenApiInboundMeasurementController {
@Resource
private InboundMeasurementOpenApiService service;
@Resource
private MojiaMeasurementOpenApiService mojiaService;
@Operation(summary = "墨家设备按箱码上传单箱测量结果",
description = "首轮联调使用固定 X-App-Key;JSON 中 picture 为 JPEG Data URL,boxCode 原样取自柏悦箱唛二维码")
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<CartonMeasurementResult> receiveMojia(
@RequestHeader("X-App-Key") String appKey,
@RequestHeader(value = "X-Timestamp", required = false) String timestamp,
@RequestHeader(value = "X-Nonce", required = false) String nonce,
@RequestHeader(value = "X-Signature", required = false) String signature,
@RequestBody String rawJson) {
return ApiResponse.success(mojiaService.receive(
new MeasurementRequestHeaders(appKey, timestamp, nonce, signature, null), rawJson));
}
@Operation(summary = "提交一条第三方箱规测量结果和图片",
description = "multipart 包含 data 原始JSON和 image 图片;联调关闭签名时只需固定 X-App-Key 和 Idempotency-Key,恢复后按六行 HMAC 规范签名")
@@ -0,0 +1,28 @@
package com.baiyue.firstleg.dto;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/** 墨家设备按箱码上传的 JSON 协议。 */
public final class MojiaMeasurementDtos {
private MojiaMeasurementDtos() {
}
public record UploadRequest(
@NotBlank @Size(max = 128) String boxCode,
@NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal length,
@NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal width,
@NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal height,
@DecimalMin("0.001") @DecimalMax("999999999999999.999") BigDecimal weight,
@NotBlank @Size(max = 64) String machineNo,
LocalDateTime measuredAt,
@NotBlank @Size(max = 7_100_000) String picture
) {
}
}
@@ -0,0 +1,110 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.CartonMeasurementCommand;
import com.baiyue.firstleg.dto.CartonMeasurementResult;
import com.baiyue.firstleg.dto.MojiaMeasurementDtos.UploadRequest;
import com.baiyue.firstleg.security.AuthenticatedMeasurementApp;
import com.baiyue.firstleg.security.MeasurementRequestAuthenticator;
import com.baiyue.firstleg.security.MeasurementRequestHeaders;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.Set;
/** 将墨家固定 AppKey + JSON/Base64 协议转换为统一单箱测量命令。 */
@Service
public class MojiaMeasurementOpenApiService {
static final String CALLBACK_PATH = "/openapi/v1/inbound-measurements";
private static final String JPEG_PREFIX = "data:image/jpeg;base64,";
private static final long MAX_IMAGE_BYTES = 5L * 1024 * 1024;
private final MeasurementRequestAuthenticator authenticator;
private final CartonMeasurementService cartonMeasurements;
private final ObjectMapper objectMapper;
private final Validator validator;
public MojiaMeasurementOpenApiService(MeasurementRequestAuthenticator authenticator,
CartonMeasurementService cartonMeasurements,
ObjectMapper objectMapper,
Validator validator) {
this.authenticator = authenticator;
this.cartonMeasurements = cartonMeasurements;
this.objectMapper = objectMapper;
this.validator = validator;
}
public CartonMeasurementResult receive(MeasurementRequestHeaders headers, String rawJson) {
AuthenticatedMeasurementApp app = authenticator.authenticate(
"POST", CALLBACK_PATH, headers, rawJson, false);
UploadRequest request = parseAndValidate(rawJson);
MultipartFile image = decodeJpeg(request.picture());
return cartonMeasurements.upsert(app.tenantId(), app.subjectId(), new CartonMeasurementCommand(
request.boxCode(), request.length(), request.width(), request.height(), request.weight(),
request.machineNo(), request.measuredAt() == null ? LocalDateTime.now() : request.measuredAt(), image));
}
private UploadRequest parseAndValidate(String rawJson) {
if (rawJson == null || rawJson.isBlank()) {
throw new BizException(ResultCode.VALIDATION_FAILED, "请求 JSON 不能为空");
}
final UploadRequest request;
try {
request = objectMapper.readValue(rawJson, UploadRequest.class);
} catch (JsonProcessingException exception) {
throw new BizException(ResultCode.VALIDATION_FAILED, "请求 JSON 格式错误");
}
Set<ConstraintViolation<UploadRequest>> violations = validator.validate(request);
if (!violations.isEmpty()) {
throw new BizException(ResultCode.VALIDATION_FAILED,
violations.iterator().next().getPropertyPath() + " 参数不合法");
}
return request;
}
private MultipartFile decodeJpeg(String picture) {
if (!picture.startsWith(JPEG_PREFIX)) {
throw new BizException(ResultCode.VALIDATION_FAILED,
"picture 必须使用 data:image/jpeg;base64, 格式");
}
final byte[] bytes;
try {
bytes = Base64.getDecoder().decode(picture.substring(JPEG_PREFIX.length()));
} catch (IllegalArgumentException exception) {
throw new BizException(ResultCode.VALIDATION_FAILED, "picture Base64 内容无效");
}
if (bytes.length == 0 || bytes.length > MAX_IMAGE_BYTES
|| bytes.length < 3 || (bytes[0] & 0xff) != 0xff || (bytes[1] & 0xff) != 0xd8
|| (bytes[2] & 0xff) != 0xff) {
throw new BizException(ResultCode.VALIDATION_FAILED, "picture 不是有效的 JPEG 图片或超过5MB");
}
return new InMemoryMultipartFile(bytes);
}
private record InMemoryMultipartFile(byte[] content) implements MultipartFile {
@Override public String getName() { return "image"; }
@Override public String getOriginalFilename() { return "mojia-measurement.jpg"; }
@Override public String getContentType() { return "image/jpeg"; }
@Override public boolean isEmpty() { return content.length == 0; }
@Override public long getSize() { return content.length; }
@Override public byte[] getBytes() { return content.clone(); }
@Override public InputStream getInputStream() { return new ByteArrayInputStream(content); }
@Override public void transferTo(File destination) throws IOException {
try (FileOutputStream output = new FileOutputStream(destination)) {
output.write(content);
}
}
}
}
@@ -236,6 +236,19 @@ paths:
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [boxCode, length, width, height, machineNo, picture]
properties:
boxCode: {type: string, maxLength: 128, example: 'RK-2085911321565003778#4-1'}
length: {type: number, format: decimal, minimum: 0.001}
width: {type: number, format: decimal, minimum: 0.001}
height: {type: number, format: decimal, minimum: 0.001}
weight: {type: number, format: decimal, minimum: 0.001, nullable: true}
machineNo: {type: string, maxLength: 64}
measuredAt: {type: string, format: date-time, nullable: true}
picture: {type: string, description: 'data:image/jpeg;base64,...'}
multipart/form-data:
schema:
type: object
@@ -30,6 +30,19 @@ class OpenApiInboundMeasurementControllerContractTest {
assertThat(method.getParameters()[4].getAnnotation(RequestHeader.class).required()).isTrue();
}
@Test
@DisplayName("墨家JSON入口只强制固定AppKey且保留原始JSON")
void shouldExposeMojiaJsonCompatibilityEntry() throws Exception {
Method method = OpenApiInboundMeasurementController.class.getDeclaredMethod("receiveMojia",
String.class, String.class, String.class, String.class, String.class);
assertThat(method.getParameters()[0].getAnnotation(RequestHeader.class).required()).isTrue();
assertThat(method.getParameters()[1].getAnnotation(RequestHeader.class).required()).isFalse();
assertThat(method.getParameters()[2].getAnnotation(RequestHeader.class).required()).isFalse();
assertThat(method.getParameters()[3].getAnnotation(RequestHeader.class).required()).isFalse();
assertThat(method.getAnnotation(org.springframework.web.bind.annotation.PostMapping.class).consumes())
.containsExactly("application/json");
}
@Test
@DisplayName("静态OpenAPI声明回调、状态查询、签名和三层幂等")
void shouldDocumentMeasurementOpenApiContract() throws Exception {
@@ -0,0 +1,55 @@
package com.baiyue.firstleg.service;
import com.baiyue.firstleg.dto.CartonMeasurementCommand;
import com.baiyue.firstleg.dto.CartonMeasurementResult;
import com.baiyue.firstleg.security.AuthenticatedMeasurementApp;
import com.baiyue.firstleg.security.MeasurementRequestAuthenticator;
import com.baiyue.firstleg.security.MeasurementRequestHeaders;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.Validation;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class MojiaMeasurementOpenApiServiceTest {
@Test
void fixedAppKeyJsonIsConvertedToTheExistingCartonMeasurementFlow() throws Exception {
MeasurementRequestAuthenticator authenticator = mock(MeasurementRequestAuthenticator.class);
CartonMeasurementService cartons = mock(CartonMeasurementService.class);
var service = new MojiaMeasurementOpenApiService(authenticator, cartons,
new ObjectMapper().findAndRegisterModules(), Validation.buildDefaultValidatorFactory().getValidator());
MeasurementRequestHeaders headers = new MeasurementRequestHeaders("mojia", null, null, null, null);
when(authenticator.authenticate("POST", MojiaMeasurementOpenApiService.CALLBACK_PATH,
headers, json(), false)).thenReturn(new AuthenticatedMeasurementApp("mojia", 10001L, 90001L, Map.of()));
when(cartons.upsert(eq(10001L), eq(90001L), any())).thenReturn(new CartonMeasurementResult(
1L, 1, 2L, "RK-001#4-1", 1, new BigDecimal("60.1"), new BigDecimal("40.2"),
new BigDecimal("30.3"), new BigDecimal("12.5"), 3L, "MJ-01", LocalDateTime.now(), true));
CartonMeasurementResult result = service.receive(headers, json());
assertThat(result.boxCode()).isEqualTo("RK-001#4-1");
ArgumentCaptor<CartonMeasurementCommand> command = ArgumentCaptor.forClass(CartonMeasurementCommand.class);
verify(cartons).upsert(eq(10001L), eq(90001L), command.capture());
assertThat(command.getValue().boxCode()).isEqualTo("RK-001#4-1");
assertThat(command.getValue().lengthCm()).isEqualByComparingTo("60.1");
assertThat(command.getValue().image().getContentType()).isEqualTo("image/jpeg");
assertThat(command.getValue().image().getBytes()).containsExactly(0xff, 0xd8, 0xff, 0xd9);
}
private String json() {
return """
{"boxCode":"RK-001#4-1","length":60.1,"width":40.2,"height":30.3,
"weight":12.5,"machineNo":"MJ-01","picture":"data:image/jpeg;base64,/9j/2Q=="}
""";
}
}
@@ -10,6 +10,7 @@ import com.baiyue.iam.dto.TenantDtos.UpdateTenantRequest;
import com.baiyue.iam.dto.TenantDtos.ChangeTenantStatusRequest;
import com.baiyue.iam.dto.TenantDtos.ResetCustomerOwnerPasswordRequest;
import com.baiyue.iam.dto.TenantDtos.ApprovalResponse;
import com.baiyue.iam.dto.TenantDtos.CreationResponse;
import com.baiyue.iam.dto.TenantDtos.CustomerImpersonationRequest;
import com.baiyue.iam.dto.TenantDtos.CustomerImpersonationResponse;
import com.baiyue.iam.entity.IamTenantReview;
@@ -98,15 +99,15 @@ public class PlatformTenantController {
@PostMapping("/tenants")
@PreAuthorize("hasAuthority('platform:customer:create')")
@Operation(summary = "Register enterprise tenant / 创建企业客户草稿", description = "Create a PROFILE_REQUIRED merchant draft only. Saving the customer does not create a review ticket. / 仅创建待完善企业客户草稿,保存客户不会创建审核单。")
@Operation(summary = "Register enterprise tenant / 创建企业客户草稿", description = "Create a PROFILE_REQUIRED merchant draft, its OWNER account and one-time credentials. Saving the customer does not create a review ticket. / 创建待完善企业客户草稿及 OWNER 账号并返回一次性凭据,保存客户不会创建审核单。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Registration created / 注册已创建"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid enterprise data / 企业资料不合法"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限域"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "Enterprise already exists / 企业已存在")
})
public ApiResponse<TenantResponse> register(@Valid @RequestBody RegisterRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
public ApiResponse<CreationResponse> register(@Valid @RequestBody RegisterRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
TenantGuard.tenant(user, "PLATFORM");
return ApiResponse.success(service.register(request, user.userId()));
}
@@ -109,6 +109,13 @@ public final class TenantDtos {
@Schema(description = "One-time temporary password; only returned when created / 一次性初始密码,仅新建时返回", format = "password", nullable = true) String temporaryPassword
) { }
@Schema(name = "TenantCreationResponse", description = "Created enterprise draft and one-time administrator credentials / 新建企业草稿及一次性管理员凭据")
public record CreationResponse(
@Schema(description = "Created enterprise draft / 新建企业草稿") TenantResponse tenant,
@Schema(description = "Contact phone used as login username / 使用联系电话作为登录账号") String administratorUsername,
@Schema(description = "One-time temporary password / 一次性初始密码", format = "password") String temporaryPassword
) { }
@Schema(name = "TenantResponse", description = "Enterprise tenant summary / 企业租户概要")
public record TenantResponse(Long id, String tenantCode, String tenantName, String status,
String legalName, String legalRepresentative, String unifiedSocialCreditCode, String contactName,
@@ -217,7 +217,16 @@ public class RbacService {
throw new BizException(ResultCode.CONFLICT, "停用角色不能分配给成员");
}
}
Set<Long> currentRoleIds = new LinkedHashSet<>();
for (Long roleId : repository.findMemberRoleIds(tenantId, request.membershipId())) {
if (repository.findRole(roleId, tenantId, appCode) != null) currentRoleIds.add(roleId);
}
repository.replaceMemberRoles(tenantId, appCode, request.membershipId(), request.roleIds());
if (!currentRoleIds.equals(request.roleIds())) {
var user = memberRepository.findUser(membership.getUserId());
if (user == null || user.getAuthUserId() == null) throw sessionRevocationUnavailable();
revokeSessions(List.of(user.getAuthUserId()));
}
}
@BusinessAudit(event = "ROLE_PERMISSION_GRANT", resourceType = "ROLE",
@@ -238,7 +247,10 @@ public class RbacService {
}
private void revokeAssignedSessions(long tenantId, Long roleId) {
List<Long> accountIds = repository.findAssignedAuthUserIds(tenantId, roleId);
revokeSessions(repository.findAssignedAuthUserIds(tenantId, roleId));
}
private void revokeSessions(List<Long> accountIds) {
for (int start = 0; start < accountIds.size(); start += SESSION_REVOCATION_BATCH_SIZE) {
Set<Long> batch = Set.copyOf(accountIds.subList(start,
Math.min(start + SESSION_REVOCATION_BATCH_SIZE, accountIds.size())));
@@ -19,6 +19,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.security.SecureRandom;
import java.time.LocalDateTime;
@@ -48,6 +50,27 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
@Override
public String name() { return "tenant-administrator"; }
/** Creates the owner during platform customer creation and compensates Auth if the IAM transaction rolls back. */
public ProvisioningResult provisionPlatformCreatedTenant(IamTenant tenant, long operatorId) {
ApprovalContext context = new ApprovalContext(tenant, 0L, operatorId, "platform customer creation");
ProvisioningResult result = initialize(context);
if (result.created() && TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
if (status != STATUS_ROLLED_BACK) return;
try {
compensate(context, result);
} catch (RuntimeException compensationFailure) {
log.error("平台创建客户事务回滚后认证账号补偿失败,tenantId={}, authUserId={}",
tenant.getId(), result.authUserId(), compensationFailure);
}
}
});
}
return result;
}
@Override
public ProvisioningResult initialize(ApprovalContext context) {
IamTenant tenant = context.tenant();
@@ -104,7 +127,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
.eq(IamRole::getStatus, "ACTIVE")
.last("LIMIT 1"));
if (ownerRole == null) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "企业管理员角色模板未初始化,不能审核通过");
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "企业管理员角色模板未初始化,不能创建或确认企业管理员");
}
return ownerRole;
}
@@ -239,7 +262,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
private char pick(char[] source) { return source[random.nextInt(source.length)]; }
private BizException unavailable() {
return new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用,不能审核通过");
return new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用,不能创建或确认企业管理员");
}
private BizException inconsistentOwner(String message) {
return new BizException(ResultCode.SERVICE_UNAVAILABLE, message);
@@ -25,6 +25,7 @@ import com.baiyue.iam.dto.TenantDtos.UpdateProfileRequest;
import com.baiyue.iam.dto.TenantDtos.UpdateTenantRequest;
import com.baiyue.iam.dto.TenantDtos.ChangeTenantStatusRequest;
import com.baiyue.iam.dto.TenantDtos.ApprovalResponse;
import com.baiyue.iam.dto.TenantDtos.CreationResponse;
import com.baiyue.iam.dto.TenantDtos.CustomerImpersonationRequest;
import com.baiyue.iam.dto.TenantDtos.CustomerImpersonationResponse;
import com.baiyue.iam.dto.CustomerExportDtos.CustomerExportBatch;
@@ -61,6 +62,8 @@ public class TenantService {
private TenantApprovalInitializationCoordinator approvalInitialization;
@Resource
private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
@Resource
private TenantAdministratorProvisioningService administratorProvisioning;
@Transactional
@BusinessAudit(event = "PLATFORM_CUSTOMER_OWNER_PASSWORD_RESET", resourceType = "TENANT",
@@ -148,7 +151,7 @@ public class TenantService {
captureAfter = false
)
@Transactional
public TenantResponse register(RegisterRequest request, long operatorId) {
public CreationResponse register(RegisterRequest request, long operatorId) {
String contactPhone = normalizeContactPhone(request.contactPhone());
ensureContactPhoneAvailable(contactPhone, null);
IamTenant tenant = new IamTenant();
@@ -177,8 +180,9 @@ public class TenantService {
tenant.setUpdatedBy(operatorId);
repository.insertTenant(tenant);
dimensionRuleInitialization.initialize(tenant.getId(), operatorId);
var administrator = administratorProvisioning.provisionPlatformCreatedTenant(tenant, operatorId);
return response(tenant);
return new CreationResponse(response(tenant), administrator.username(), administrator.temporaryPassword());
}
public List<IamTenantReview> pendingReviews() {
+16 -2
View File
@@ -59,14 +59,14 @@ paths:
post:
tags: [Platform Tenant / 平台租户]
summary: Register enterprise tenant / 创建企业客户草稿
description: Create a `PROFILE_REQUIRED` enterprise draft only. Saving the customer does not create a review task. / 仅创建待完善企业客户草稿,保存客户不会创建审核单。
description: Create a `PROFILE_REQUIRED` enterprise draft and its OWNER account, returning one-time credentials. Saving the customer does not create a review task. / 创建待完善企业客户草稿及 OWNER 账号并返回一次性凭据,保存客户不会创建审核单。
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/RegisterRequest'}
responses:
'200': {$ref: '#/components/responses/TenantSuccess'}
'200': {$ref: '#/components/responses/TenantCreationSuccess'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
@@ -822,6 +822,7 @@ components:
responses:
Success: {description: Operation succeeded / 操作成功, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
TenantSuccess: {description: Enterprise tenant returned / 返回企业客户档案, content: {application/json: {schema: {$ref: '#/components/schemas/TenantApiResponse'}}}}
TenantCreationSuccess: {description: Enterprise tenant and one-time owner credentials returned / 返回企业客户及一次性管理员凭据, content: {application/json: {schema: {$ref: '#/components/schemas/TenantCreationApiResponse'}}}}
BadRequest: {description: Invalid request / 请求参数错误, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
Unauthorized: {description: Authentication required / 需要登录, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
Forbidden: {description: Permission, customer scope or platform technical scope rejected / 权限、客户范围或平台技术作用域不匹配, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
@@ -869,6 +870,19 @@ components:
- type: object
properties:
data: {$ref: '#/components/schemas/TenantApprovalResponse'}
TenantCreationApiResponse:
allOf:
- {$ref: '#/components/schemas/ApiResponse'}
- type: object
properties:
data: {$ref: '#/components/schemas/TenantCreationResponse'}
TenantCreationResponse:
type: object
required: [tenant, administratorUsername, temporaryPassword]
properties:
tenant: {$ref: '#/components/schemas/TenantResponse'}
administratorUsername: {type: string, example: '13800138000'}
temporaryPassword: {type: string, format: password, description: 一次性初始密码,仅本次创建响应返回}
TenantApprovalResponse:
type: object
required: [tenantId, administratorCreated]
@@ -66,6 +66,16 @@ class PlatformTenantControllerContractTest {
returnType.getActualTypeArguments()[0]);
}
@Test
void platformCreationReturnsTenantAndOneTimeAdministratorCredentials() throws Exception {
Method method = PlatformTenantController.class.getMethod("register",
com.baiyue.iam.dto.TenantDtos.RegisterRequest.class, AuthenticatedUser.class);
ParameterizedType returnType = (ParameterizedType) method.getGenericReturnType();
assertEquals(com.baiyue.iam.dto.TenantDtos.CreationResponse.class,
returnType.getActualTypeArguments()[0]);
}
@Test
void shouldExposeCustomerOwnerPasswordResetWithDedicatedPermission() throws Exception {
Method method = PlatformTenantController.class.getMethod("resetOwnerPassword", long.class,
@@ -15,6 +15,7 @@ import com.baiyue.iam.dto.RbacDtos.ChangePermissionStatusRequest;
import com.baiyue.iam.entity.IamMembership;
import com.baiyue.iam.entity.IamPermission;
import com.baiyue.iam.entity.IamRole;
import com.baiyue.iam.entity.IamUser;
import com.baiyue.iam.repository.MemberRepository;
import com.baiyue.iam.repository.RbacRepository;
import org.junit.jupiter.api.Test;
@@ -363,6 +364,47 @@ class RbacServiceTest {
assertEquals("认证会话撤销失败,角色变更未生效", exception.getMessage());
}
@Test
void clearingMemberRolesRevokesThatMembersExistingSessions() {
IamMembership membership = new IamMembership();
membership.setId(31L);
membership.setTenantId(100L);
membership.setUserId(41L);
membership.setMemberType("EMPLOYEE");
IamUser user = new IamUser();
user.setId(41L);
user.setAuthUserId(501L);
when(memberRepository.findById(31L)).thenReturn(membership);
when(memberRepository.findUser(41L)).thenReturn(user);
when(repository.findMemberRoleIds(100L, 31L)).thenReturn(Set.of(11L));
when(repository.findRole(11L, 100L, "MERCHANT"))
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
when(authClient.revokeSessions(any(), any())).thenReturn(ApiResponse.success(null));
service.assignRoles(100L, "MERCHANT", new AssignRolesRequest(31L, Set.of()));
verify(repository).replaceMemberRoles(100L, "MERCHANT", 31L, Set.of());
verify(authClient).revokeSessions(eq("test-internal-token"),
eq(new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(501L))));
}
@Test
void savingUnchangedMemberRolesDoesNotRevokeSessions() {
IamMembership membership = new IamMembership();
membership.setId(31L);
membership.setTenantId(100L);
membership.setMemberType("EMPLOYEE");
IamRole role = role(11L, 100L, "MERCHANT", "CUSTOM");
when(memberRepository.findById(31L)).thenReturn(membership);
when(repository.findMemberRoleIds(100L, 31L)).thenReturn(Set.of(11L));
when(repository.findRole(11L, 100L, "MERCHANT")).thenReturn(role);
service.assignRoles(100L, "MERCHANT", new AssignRolesRequest(31L, Set.of(11L)));
verify(repository).replaceMemberRoles(100L, "MERCHANT", 31L, Set.of(11L));
verifyNoInteractions(authClient);
}
private IamRole role(Long id, Long tenantId, String appCode, String roleType) {
IamRole role = new IamRole();
role.setId(id);
@@ -9,6 +9,7 @@ import com.baiyue.iam.entity.IamUser;
import com.baiyue.iam.mapper.IamRoleMapper;
import com.baiyue.iam.repository.MemberRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import jakarta.annotation.Resource;
@@ -18,6 +19,8 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.security.SecureRandom;
import java.util.List;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -43,6 +46,13 @@ class TenantAdministratorProvisioningServiceTest {
@Resource private TenantAdministratorProvisioningService service;
private IamTenant tenant;
@AfterEach
void clearSynchronization() {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.clearSynchronization();
}
}
@Test
void springContextCreatesBean() {
assertNotNull(service);
@@ -93,6 +103,20 @@ class TenantAdministratorProvisioningServiceTest {
verify(members).replaceRoles(eq(tenant.getId()), anyLong(), eq(List.of(4000100L)));
}
@Test
void platformCreationRollbackCompensatesTheNewAuthAccount() {
TransactionSynchronizationManager.initSynchronization();
var result = service.provisionPlatformCreatedTenant(tenant, 7000L);
List<TransactionSynchronization> synchronizations =
TransactionSynchronizationManager.getSynchronizations();
TransactionSynchronizationManager.clearSynchronization();
synchronizations.forEach(value -> value.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK));
assertTrue(result.created());
verify(auth).compensate("internal-token", result.authUserId());
}
@Test
void existingOwnerSkipsProvisioningAndDoesNotReturnCredentials() {
IamMembership owner = activeOwner();
@@ -26,6 +26,7 @@ class TenantCustomerExportTest {
@MockBean private AuthAccountClient authAccountClient;
@MockBean private TenantApprovalInitializationCoordinator approvalInitialization;
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
@MockBean private TenantAdministratorProvisioningService administratorProvisioning;
@Resource private TenantService service;
@Test
void returnsUnmaskedCustomerRowsAndAdvancingCursor() {
@@ -222,6 +222,9 @@ class TenantServiceTest {
@Test
void platformRegistrationCreatesDraftWithoutCreatingReviewTicket() {
doReturn(TenantAdministratorProvisioningService.ProvisioningResult.created(
81L, "13800138000", "Password1234"))
.when(provisioning).provisionPlatformCreatedTenant(any(), eq(9001L));
var created = service.register(new RegisterRequest(
"Example Merchant", "Example Merchant Ltd", "Bob", "Alice", "13800138000",
@@ -229,12 +232,15 @@ class TenantServiceTest {
LocalDate.of(2020, 1, 2), new BigDecimal("5000000.00"), "CNY", "深圳市南山区", "草稿客户"),
9001L);
assertEquals("PROFILE_REQUIRED", created.status());
assertEquals("Bob", created.legalRepresentative());
assertEquals("基础版", created.servicePackage());
assertEquals("PROFILE_REQUIRED", created.tenant().status());
assertEquals("Bob", created.tenant().legalRepresentative());
assertEquals("基础版", created.tenant().servicePackage());
assertEquals("13800138000", created.administratorUsername());
assertEquals("Password1234", created.temporaryPassword());
verify(repository).insertTenant(argThat(tenant -> Long.valueOf(9001L).equals(tenant.getCreatedBy())
&& Long.valueOf(9001L).equals(tenant.getUpdatedBy())));
verify(dimensionRuleInitialization).initialize(created.id(), 9001L);
verify(dimensionRuleInitialization).initialize(created.tenant().id(), 9001L);
verify(provisioning).provisionPlatformCreatedTenant(any(), eq(9001L));
verify(repository, never()).insertReview(any());
}
@@ -253,16 +259,33 @@ class TenantServiceTest {
@Test
void platformRegistrationAlwaysCreatesTheDraftWithoutACreditCode() {
doReturn(TenantAdministratorProvisioningService.ProvisioningResult.created(
81L, "13800138000", "Password1234"))
.when(provisioning).provisionPlatformCreatedTenant(any(), eq(9001L));
var created = service.register(new RegisterRequest(
"Example Merchant", "Example Merchant Ltd", "Bob", "Alice", "13800138000",
"alice@example.com", "基础版", null, null, 30, null,
null, null, "CNY", null, null), 9001L);
assertEquals(null, created.unifiedSocialCreditCode());
assertEquals(null, created.tenant().unifiedSocialCreditCode());
verify(repository).insertTenant(argThat(tenant -> tenant.getUnifiedSocialCreditCode() == null));
}
@Test
void profileRequiredDraftArchiveDisablesAuthAccountsAndCustomer() {
IamTenant tenant = merchant(10001L, "PROFILE_REQUIRED");
when(repository.findTenantForUpdate(10001L)).thenReturn(tenant);
when(authClient.archiveTenant("internal-token", 10001L))
.thenReturn(com.baiyue.common.core.api.ApiResponse.success(null));
var archived = service.archiveMerchant(10001L);
assertEquals("DISABLED", archived.status());
verify(authClient).archiveTenant("internal-token", 10001L);
verify(repository).saveTenant(tenant);
}
@Test
void explicitSubmissionRequiresOnlyBusinessLicenseAndLegalPersonIdAndCreatesReview() {
IamTenant tenant = merchant(10001L, "PROFILE_REQUIRED");
@@ -0,0 +1,780 @@
{
"reportName": "后台管理测试环境 HTTP API 验收",
"marker": "ADMIN-API-20260808021813228",
"gateway": "http://8.163.30.128:9000",
"generatedAt": "2026-08-08T02:18:35.352Z",
"startedAt": "2026-08-08T02:18:13.228Z",
"success": false,
"fatal": null,
"summary": {
"total": 39,
"passed": 36,
"failed": 3
},
"modules": [
{
"name": "orders",
"startedAt": "2026-08-08T02:18:13.605Z",
"finishedAt": "2026-08-08T02:18:35.262Z",
"success": false,
"passed": 35,
"failed": 2,
"fatal": "订单审核后的入库单契约未部署,无法向 OCR/墨家模块交接"
},
{
"name": "inbound-weight-ocr",
"startedAt": "2026-08-08T02:18:35.265Z",
"finishedAt": "2026-08-08T02:18:35.352Z",
"success": false,
"passed": 0,
"failed": 1,
"fatal": null
}
],
"scenarios": [
{
"name": "weight-image-recognition-and-confirmation",
"module": "inbound-weight-ocr",
"success": false,
"deploymentBlocked": false,
"passed": 0,
"failed": 1
}
],
"deploymentBlockers": [
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "审核后生成隔离入库单",
"evidence": "20秒内未返回 inboundId",
"code": "S0503",
"requestId": "438a12d6-5829-4fa2-8574-bc2c403c9f75",
"traceId": "9b51ea540a7c4a1f8566eab65620001c"
}
],
"createdResources": {
"orders": {
"marker": "ADMIN-API-20260808021813228",
"tenantId": "10001",
"orderId": "2085913442075082754",
"productImageFileId": "2085913441529823234",
"boxTypes": [
"SMALL",
"MEDIUM",
"LARGE",
"EXTRA_LARGE"
],
"inboundId": null,
"orderNo": "FL2085913442075082754"
}
},
"excluded": [
{
"scope": "dashboard",
"reason": "首页为静态演示数据,不调用后端 API"
},
{
"scope": "hidden-prototypes",
"reason": "隐藏原型页面尚无已确认后端契约"
}
],
"securityNote": "报告不保存登录密码、Access Token、Refresh Token、图片二进制或 OCR 原文全文。",
"steps": [
{
"module": "authentication",
"scenario": null,
"name": "平台管理员登录",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/auth/login"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "d724b317-473f-40bd-bde9-60aca7e0098c, 8b97525e-c8d8-4c7d-a5d5-323259556645",
"traceId": "8627c96e270446d4b5fec252ac976e64",
"evidence": "",
"checkedAt": "2026-08-08T02:18:13.604Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 测试账号登录",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/auth/login"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "57860ec6-be78-4066-9964-b7fe1b9e5496, 7c21b6f0-25c7-4773-964d-87309affd9e4",
"traceId": "2fe3f787c5ed4856b34b72cd4d8831ea",
"evidence": "username=merchant_admin",
"checkedAt": "2026-08-08T02:18:13.956Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 查询当前企业资料",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/iam/tenant/profile"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "00b18b0a-89c4-47c8-b861-03725eb1c3fc, 5a026290-cb2e-42d7-9342-e2c76953f974",
"traceId": "2068755a0728458b81792de5e31d3ef8",
"evidence": "",
"checkedAt": "2026-08-08T02:18:13.980Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 测试企业已审核且客户 ID 有效",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/iam/tenant/profile"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "00b18b0a-89c4-47c8-b861-03725eb1c3fc, 5a026290-cb2e-42d7-9342-e2c76953f974",
"traceId": "2068755a0728458b81792de5e31d3ef8",
"evidence": "tenantId=10001, status=ACTIVE",
"checkedAt": "2026-08-08T02:18:13.981Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "Admin 查询客户固定箱型",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/first-leg/customer-dimension-config?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "18221254-531c-475b-8aa6-b3601f371f1f, 22ac6187-eda0-433f-9a34-c24ea03f5af2",
"traceId": "85a09e0acbd64f7d80355872710240f7",
"evidence": "tenantId=10001",
"checkedAt": "2026-08-08T02:18:14.017Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "固定四箱型顺序、内径及每边加2cm正确",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/first-leg/customer-dimension-config?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "18221254-531c-475b-8aa6-b3601f371f1f, 22ac6187-eda0-433f-9a34-c24ea03f5af2",
"traceId": "85a09e0acbd64f7d80355872710240f7",
"evidence": "types=SMALL,MEDIUM,LARGE,EXTRA_LARGE",
"checkedAt": "2026-08-08T02:18:14.017Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 查询国内收货仓",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=DOMESTIC"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "016c5f6a-bb16-4171-ac6f-98c6b4e7b61c, c1bbc087-4846-4fa6-90bb-2ffdae9f7f45",
"traceId": "8c7e016f470c4c8fbb6b601f0d1a428f",
"evidence": "",
"checkedAt": "2026-08-08T02:18:14.054Z"
},
{
"module": "orders",
"scenario": null,
"name": "存在可用国内收货仓",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=DOMESTIC"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "016c5f6a-bb16-4171-ac6f-98c6b4e7b61c, c1bbc087-4846-4fa6-90bb-2ffdae9f7f45",
"traceId": "8c7e016f470c4c8fbb6b601f0d1a428f",
"evidence": "count=2",
"checkedAt": "2026-08-08T02:18:14.054Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 查询 FBA 目的仓",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=FBA"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "d811ee69-eca6-4ed9-b28b-2d252dd15cf9, 7050232a-d912-4b26-95b5-5665321ea314",
"traceId": "35dc511159184c06b5d873a0860ea259",
"evidence": "",
"checkedAt": "2026-08-08T02:18:14.123Z"
},
{
"module": "orders",
"scenario": null,
"name": "存在可用 FBA 目的仓",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=FBA"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "d811ee69-eca6-4ed9-b28b-2d252dd15cf9, 7050232a-d912-4b26-95b5-5665321ea314",
"traceId": "35dc511159184c06b5d873a0860ea259",
"evidence": "count=200",
"checkedAt": "2026-08-08T02:18:14.123Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 查询可用计费渠道",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=193000000000200001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "59cfb9db-9ead-4174-a505-42883a8a1eb6, 01d1377a-15cc-4856-9ffc-ff70d1db3f91",
"traceId": "546da0b26ac44057881949356a38a6eb",
"evidence": "",
"checkedAt": "2026-08-08T02:18:14.171Z"
},
{
"module": "orders",
"scenario": null,
"name": "存在带计价税制的可用渠道",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=193000000000200001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "59cfb9db-9ead-4174-a505-42883a8a1eb6, 01d1377a-15cc-4856-9ffc-ff70d1db3f91",
"traceId": "546da0b26ac44057881949356a38a6eb",
"evidence": "count=15",
"checkedAt": "2026-08-08T02:18:14.171Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 查询发货客户候选",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/partners/candidates?type=SHIPPER"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "11946d37-21fe-468d-af44-7884f1df696e, 09f2cadf-629c-4569-bf41-c5b05e931cc9",
"traceId": "3b323ee01419461d89fa412a444714e8",
"evidence": "",
"checkedAt": "2026-08-08T02:18:14.208Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "Client 上传产品图片",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/merchant/v1/files/product-images"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "21a2b983-d4f6-43e7-a464-71fb938ef7a5, bade6d3f-6516-46e6-b7d6-afa4fbab4e1a",
"traceId": "19b4ecce4ebd4b868348822fea67a6f5",
"evidence": "contentType=image/png",
"checkedAt": "2026-08-08T02:18:14.254Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "产品图片上传返回字符串文件 ID",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/merchant/v1/files/product-images"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "21a2b983-d4f6-43e7-a464-71fb938ef7a5, bade6d3f-6516-46e6-b7d6-afa4fbab4e1a",
"traceId": "19b4ecce4ebd4b868348822fea67a6f5",
"evidence": "fileIdType=string, contentType=image/png",
"checkedAt": "2026-08-08T02:18:14.254Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "Client 可下载本轮产品图片",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/files/product-images/2085913441529823234"
},
"http": 200,
"code": null,
"message": null,
"requestId": "b8d16fdc-892e-495f-9dbd-7f150681498f, f898d1e5-6ecc-4240-81dc-4cc792198f33",
"traceId": "bc13aba31663407dbdf11e058535d422",
"evidence": "bytes=68, contentType=image/png",
"checkedAt": "2026-08-08T02:18:14.282Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "Client 创建四箱型隔离订单",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/merchant/v1/orders/first-leg"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "ea9fa6cf-9722-41c6-a9f8-461669f8452c, ac93d499-e32e-4bcd-8204-4219c40be970",
"traceId": "3009b5a516044fedb0e6648d3221f492",
"evidence": "marker=ADMIN-API-20260808021813228",
"checkedAt": "2026-08-08T02:18:14.395Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 创建订单幂等重放",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/merchant/v1/orders/first-leg"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "9c5bd574-0b44-4a77-816c-433b7d2e404e, 9380027e-8502-4ca3-88ec-f4317543f6f9",
"traceId": "51baa17264144b9685976cbc4f457223",
"evidence": "",
"checkedAt": "2026-08-08T02:18:14.441Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 创建幂等结果一致",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/merchant/v1/orders/first-leg"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "9c5bd574-0b44-4a77-816c-433b7d2e404e, 9380027e-8502-4ca3-88ec-f4317543f6f9",
"traceId": "51baa17264144b9685976cbc4f457223",
"evidence": "orderId=2085913442075082754, replayedId=2085913442075082754",
"checkedAt": "2026-08-08T02:18:14.442Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 查询隔离订单草稿",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/orders/first-leg/2085913442075082754"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "e47a2e37-3346-4bd5-a8f3-03cf0300af63, ec85ecdf-92f1-44d1-8bea-49f908697ae8",
"traceId": "6531eafc40ba48ff96d96cd5ef02779b",
"evidence": "",
"checkedAt": "2026-08-08T02:18:14.471Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 草稿身份、marker 和状态正确",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/orders/first-leg/2085913442075082754"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "e47a2e37-3346-4bd5-a8f3-03cf0300af63, ec85ecdf-92f1-44d1-8bea-49f908697ae8",
"traceId": "6531eafc40ba48ff96d96cd5ef02779b",
"evidence": "orderNo=FL2085913442075082754, status=DRAFT",
"checkedAt": "2026-08-08T02:18:14.471Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "四箱型产品字段及图片契约完整回显",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/orders/first-leg/2085913442075082754"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "e47a2e37-3346-4bd5-a8f3-03cf0300af63, ec85ecdf-92f1-44d1-8bea-49f908697ae8",
"traceId": "6531eafc40ba48ff96d96cd5ef02779b",
"evidence": "cargoes=4, imageFileId=2085913441529823234",
"checkedAt": "2026-08-08T02:18:14.471Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 提交隔离订单",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/merchant/v1/orders/first-leg/2085913442075082754/submit"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "6c67230a-d6ed-4978-8a27-8484ef7ecbb5, a05bda79-4452-496e-8125-c81abc496f94",
"traceId": "d333673147bc451cbb936505d7c90307",
"evidence": "orderId=2085913442075082754",
"checkedAt": "2026-08-08T02:18:14.567Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 回查已提交订单",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/orders/first-leg/2085913442075082754"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "d8c6d729-93a0-4f90-830d-2b65d4b2f6d1, 7c39c36a-a49a-45ce-a4fa-e3a13ff5a1e5",
"traceId": "d113de3e006c423394763a2820aa39a9",
"evidence": "",
"checkedAt": "2026-08-08T02:18:14.605Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 订单已进入待审核状态",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/orders/first-leg/2085913442075082754"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "d8c6d729-93a0-4f90-830d-2b65d4b2f6d1, 7c39c36a-a49a-45ce-a4fa-e3a13ff5a1e5",
"traceId": "d113de3e006c423394763a2820aa39a9",
"evidence": "status=SUBMITTED",
"checkedAt": "2026-08-08T02:18:14.606Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 查询统一订单列表",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders?page=1&size=100&tenantId=10001&orderNo=FL2085913442075082754"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "19e756f5-9cc0-4300-b025-4245a0a32d3c, 97f23a47-1059-40a0-86e8-e3adff12fa24",
"traceId": "9957f741d6534e89902a9ec47adfc218",
"evidence": "",
"checkedAt": "2026-08-08T02:18:14.670Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 统一订单列表只核对本轮订单",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders?page=1&size=100&tenantId=10001&orderNo=FL2085913442075082754"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "19e756f5-9cc0-4300-b025-4245a0a32d3c, 97f23a47-1059-40a0-86e8-e3adff12fa24",
"traceId": "9957f741d6534e89902a9ec47adfc218",
"evidence": "orderId=2085913442075082754, found=true",
"checkedAt": "2026-08-08T02:18:14.670Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 查询统一订单详情",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/2085913442075082754?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "a7810bfb-197c-408e-a30d-0ac9671238f3, 039f2fe0-a7d7-4795-9df9-fee060df4eee",
"traceId": "502c16f4e4734299a865a9d4556826a9",
"evidence": "",
"checkedAt": "2026-08-08T02:18:14.716Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 统一订单详情身份与待审核状态正确",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/2085913442075082754?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "a7810bfb-197c-408e-a30d-0ac9671238f3, 039f2fe0-a7d7-4795-9df9-fee060df4eee",
"traceId": "502c16f4e4734299a865a9d4556826a9",
"evidence": "mainStatus=PENDING_REVIEW, timeline=2",
"checkedAt": "2026-08-08T02:18:14.716Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 查询头程订单列表",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/first-leg?tenantId=10001&keyword=FL2085913442075082754&page=1&size=100"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "a5b659b5-091e-4369-a790-ca8d97ab7941, 2a30d9de-0321-45b2-9442-477d63bfa199",
"traceId": "27dc5a7046d04e27873d1ac5f5d386cd",
"evidence": "",
"checkedAt": "2026-08-08T02:18:14.764Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 头程订单列表只核对本轮订单",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/first-leg?tenantId=10001&keyword=FL2085913442075082754&page=1&size=100"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "a5b659b5-091e-4369-a790-ca8d97ab7941, 2a30d9de-0321-45b2-9442-477d63bfa199",
"traceId": "27dc5a7046d04e27873d1ac5f5d386cd",
"evidence": "orderId=2085913442075082754, found=true",
"checkedAt": "2026-08-08T02:18:14.764Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 查询头程订单详情",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/first-leg/2085913442075082754?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "2cd85740-18e2-4db8-857a-92c98f02178a, 35e8fdb1-72e1-4277-b8b0-78a2a87d1b53",
"traceId": "76ff576142194f15b6e6a763cd81b620",
"evidence": "",
"checkedAt": "2026-08-08T02:18:14.809Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 头程订单详情聚合结构完整",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/first-leg/2085913442075082754?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "2cd85740-18e2-4db8-857a-92c98f02178a, 35e8fdb1-72e1-4277-b8b0-78a2a87d1b53",
"traceId": "76ff576142194f15b6e6a763cd81b620",
"evidence": "cargoes=4, history=2",
"checkedAt": "2026-08-08T02:18:14.809Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "四箱型产品字段及图片契约完整回显",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/first-leg/2085913442075082754?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "2cd85740-18e2-4db8-857a-92c98f02178a, 35e8fdb1-72e1-4277-b8b0-78a2a87d1b53",
"traceId": "76ff576142194f15b6e6a763cd81b620",
"evidence": "cargoes=4, imageFileId=2085913441529823234",
"checkedAt": "2026-08-08T02:18:14.809Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 为本轮订单添加内部备注",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/platform/v1/orders/first-leg/2085913442075082754/internal-notes?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "f9aff324-5034-443d-86d4-2d7df44f1eee, 7deb42b2-d8ec-452b-8f21-283c04307f22",
"traceId": "e9e97c8a04fe4468bd76597413af4ba5",
"evidence": "",
"checkedAt": "2026-08-08T02:18:14.855Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 审核通过本轮订单",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/platform/v1/orders/first-leg/2085913442075082754/approve?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "1aab8371-7451-41dc-a140-5d50cc3883bf, 94ccf6a2-cd6f-459d-9c50-ecb25d146240",
"traceId": "a25a0014f79d4181a2b3e677a82f00d5",
"evidence": "orderId=2085913442075082754",
"checkedAt": "2026-08-08T02:18:14.904Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "审核后生成隔离入库单",
"passed": false,
"deploymentBlocker": true,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/2085913442075082754?tenantId=10001"
},
"http": 200,
"code": "S0503",
"message": "统一订单履约详情响应无效",
"requestId": "438a12d6-5829-4fa2-8574-bc2c403c9f75",
"traceId": "9b51ea540a7c4a1f8566eab65620001c",
"evidence": "20秒内未返回 inboundId",
"checkedAt": "2026-08-08T02:18:35.262Z"
},
{
"module": "orders",
"scenario": null,
"name": "模块执行",
"passed": false,
"deploymentBlocker": false,
"request": null,
"http": null,
"code": null,
"message": null,
"requestId": null,
"traceId": null,
"evidence": "订单审核后的入库单契约未部署,无法向 OCR/墨家模块交接",
"checkedAt": "2026-08-08T02:18:35.262Z"
},
{
"module": "inbound-weight-ocr",
"scenario": "weight-image-recognition-and-confirmation",
"name": "仅选择带本轮测试标记的可编辑入库单",
"passed": false,
"deploymentBlocker": false,
"request": null,
"http": null,
"code": null,
"message": null,
"requestId": null,
"traceId": null,
"evidence": "marker=ADMIN-API-20260808021813228, 未找到本轮创建且状态为 WAIT_MEASURE/MEASURING/MEASURE_REJECTED 的入库单",
"checkedAt": "2026-08-08T02:18:35.352Z"
}
]
}
@@ -0,0 +1,780 @@
{
"reportName": "后台管理测试环境 HTTP API 验收",
"marker": "ADMIN-API-20260808022421690",
"gateway": "http://8.163.30.128:9000",
"generatedAt": "2026-08-08T02:24:43.508Z",
"startedAt": "2026-08-08T02:24:21.690Z",
"success": false,
"fatal": null,
"summary": {
"total": 39,
"passed": 36,
"failed": 3
},
"modules": [
{
"name": "orders",
"startedAt": "2026-08-08T02:24:22.065Z",
"finishedAt": "2026-08-08T02:24:43.430Z",
"success": false,
"passed": 35,
"failed": 2,
"fatal": "订单审核后的入库单契约未部署,无法向 OCR/墨家模块交接"
},
{
"name": "inbound-weight-ocr",
"startedAt": "2026-08-08T02:24:43.432Z",
"finishedAt": "2026-08-08T02:24:43.507Z",
"success": false,
"passed": 0,
"failed": 1,
"fatal": null
}
],
"scenarios": [
{
"name": "weight-image-recognition-and-confirmation",
"module": "inbound-weight-ocr",
"success": false,
"deploymentBlocked": false,
"passed": 0,
"failed": 1
}
],
"deploymentBlockers": [
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "审核后生成隔离入库单",
"evidence": "20秒内未返回 inboundId",
"code": "S0503",
"requestId": "04d51502-8c78-4256-8232-fbdb394936c5",
"traceId": "86df65f5590641378ebc2db3ac7b03fb"
}
],
"createdResources": {
"orders": {
"marker": "ADMIN-API-20260808022421690",
"tenantId": "10001",
"orderId": "2085914987172790274",
"productImageFileId": "2085914986782720002",
"boxTypes": [
"SMALL",
"MEDIUM",
"LARGE",
"EXTRA_LARGE"
],
"inboundId": null,
"orderNo": "FL2085914987172790274"
}
},
"excluded": [
{
"scope": "dashboard",
"reason": "首页为静态演示数据,不调用后端 API"
},
{
"scope": "hidden-prototypes",
"reason": "隐藏原型页面尚无已确认后端契约"
}
],
"securityNote": "报告不保存登录密码、Access Token、Refresh Token、图片二进制或 OCR 原文全文。",
"steps": [
{
"module": "authentication",
"scenario": null,
"name": "平台管理员登录",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/auth/login"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "943c5461-6750-433f-a8b4-6b79e93c76e3, f67c7aff-3029-4deb-b65a-98db1f114533",
"traceId": "4c61fff1847748e39ec168932743bc91",
"evidence": "",
"checkedAt": "2026-08-08T02:24:22.064Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 测试账号登录",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/auth/login"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "0967d15b-80d1-40f4-a2c0-df18768f62ed, 9f03ad0e-cb57-47ce-901b-024619e903c5",
"traceId": "c849964284624bb294817ae885f92039",
"evidence": "username=merchant_admin",
"checkedAt": "2026-08-08T02:24:22.408Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 查询当前企业资料",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/iam/tenant/profile"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "768d4c29-4af9-4ef8-8287-3708015b8208, 43860360-0488-49da-8d5e-8d1e38b4e9fd",
"traceId": "9491a2adc03f4b31b3ed007510917b85",
"evidence": "",
"checkedAt": "2026-08-08T02:24:22.442Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 测试企业已审核且客户 ID 有效",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/iam/tenant/profile"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "768d4c29-4af9-4ef8-8287-3708015b8208, 43860360-0488-49da-8d5e-8d1e38b4e9fd",
"traceId": "9491a2adc03f4b31b3ed007510917b85",
"evidence": "tenantId=10001, status=ACTIVE",
"checkedAt": "2026-08-08T02:24:22.442Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "Admin 查询客户固定箱型",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/first-leg/customer-dimension-config?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "df4138d6-5477-4dad-9928-d0c72b3c8613, 58416f8f-123e-4fa6-910c-5fa8a027d87b",
"traceId": "fcd206c8b6874922bfc646c5decb7259",
"evidence": "tenantId=10001",
"checkedAt": "2026-08-08T02:24:22.476Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "固定四箱型顺序、内径及每边加2cm正确",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/first-leg/customer-dimension-config?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "df4138d6-5477-4dad-9928-d0c72b3c8613, 58416f8f-123e-4fa6-910c-5fa8a027d87b",
"traceId": "fcd206c8b6874922bfc646c5decb7259",
"evidence": "types=SMALL,MEDIUM,LARGE,EXTRA_LARGE",
"checkedAt": "2026-08-08T02:24:22.477Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 查询国内收货仓",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=DOMESTIC"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "6ebbc640-96a5-4b05-b3a0-25eb1365a6a4, 8ca09a7b-ba63-49b6-b675-66cfef2d847f",
"traceId": "a4767fd87fff4811b9e9ae3533073166",
"evidence": "",
"checkedAt": "2026-08-08T02:24:22.511Z"
},
{
"module": "orders",
"scenario": null,
"name": "存在可用国内收货仓",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=DOMESTIC"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "6ebbc640-96a5-4b05-b3a0-25eb1365a6a4, 8ca09a7b-ba63-49b6-b675-66cfef2d847f",
"traceId": "a4767fd87fff4811b9e9ae3533073166",
"evidence": "count=2",
"checkedAt": "2026-08-08T02:24:22.511Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 查询 FBA 目的仓",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=FBA"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "44636c1e-976d-4f44-83d3-931b137ffb64, 1b0bb465-9459-4395-bde8-a8bf5f4a022d",
"traceId": "54a12164b4bc447e83e8f0117c636ea7",
"evidence": "",
"checkedAt": "2026-08-08T02:24:22.567Z"
},
{
"module": "orders",
"scenario": null,
"name": "存在可用 FBA 目的仓",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=FBA"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "44636c1e-976d-4f44-83d3-931b137ffb64, 1b0bb465-9459-4395-bde8-a8bf5f4a022d",
"traceId": "54a12164b4bc447e83e8f0117c636ea7",
"evidence": "count=200",
"checkedAt": "2026-08-08T02:24:22.567Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 查询可用计费渠道",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=193000000000200001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "64265bef-7ed3-42bd-beeb-31c48039131b, 70c0d763-6ffb-4add-b65f-0420c2e6a346",
"traceId": "8e2913d003354085bd5efee3c21b40a8",
"evidence": "",
"checkedAt": "2026-08-08T02:24:22.608Z"
},
{
"module": "orders",
"scenario": null,
"name": "存在带计价税制的可用渠道",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=193000000000200001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "64265bef-7ed3-42bd-beeb-31c48039131b, 70c0d763-6ffb-4add-b65f-0420c2e6a346",
"traceId": "8e2913d003354085bd5efee3c21b40a8",
"evidence": "count=15",
"checkedAt": "2026-08-08T02:24:22.608Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 查询发货客户候选",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/partners/candidates?type=SHIPPER"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "62149605-53dc-4a96-b612-362d21369c53, 58efa9bc-5106-4450-a350-5f9f9aefe68d",
"traceId": "ffef0f71bbad4d999692769984a46c7b",
"evidence": "",
"checkedAt": "2026-08-08T02:24:22.633Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "Client 上传产品图片",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/merchant/v1/files/product-images"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "2fab9590-3f70-4102-a392-430367f42d63, c1badbdc-8ae2-4054-b1db-5928a49d663f",
"traceId": "a6b24c7736ce4d4c8763151cc08e399d",
"evidence": "contentType=image/png",
"checkedAt": "2026-08-08T02:24:22.672Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "产品图片上传返回字符串文件 ID",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/merchant/v1/files/product-images"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "2fab9590-3f70-4102-a392-430367f42d63, c1badbdc-8ae2-4054-b1db-5928a49d663f",
"traceId": "a6b24c7736ce4d4c8763151cc08e399d",
"evidence": "fileIdType=string, contentType=image/png",
"checkedAt": "2026-08-08T02:24:22.672Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "Client 可下载本轮产品图片",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/files/product-images/2085914986782720002"
},
"http": 200,
"code": null,
"message": null,
"requestId": "2d17d24c-915b-4f96-876e-b50cdfbba4e4, 256b2563-b130-4eeb-8b4d-7bfa47433400",
"traceId": "c26aafa3e4bb459b9c3d39dbc911ac6f",
"evidence": "bytes=68, contentType=image/png",
"checkedAt": "2026-08-08T02:24:22.699Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "Client 创建四箱型隔离订单",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/merchant/v1/orders/first-leg"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "7186ad91-c27c-4f12-9021-1f4c3a9da8ea, ded88a07-0e88-4cb3-8e6b-532a74d8203f",
"traceId": "ce3576180fea4d5b99ef690c40ef8483",
"evidence": "marker=ADMIN-API-20260808022421690",
"checkedAt": "2026-08-08T02:24:22.774Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 创建订单幂等重放",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/merchant/v1/orders/first-leg"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "f7e1e9f2-19b4-4dc8-9ab6-755d9d283f6c, e64b3147-f636-4dd4-b6df-495161c52faf",
"traceId": "1d467e403a044d31b21b9f9fccbfac45",
"evidence": "",
"checkedAt": "2026-08-08T02:24:22.812Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 创建幂等结果一致",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/merchant/v1/orders/first-leg"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "f7e1e9f2-19b4-4dc8-9ab6-755d9d283f6c, e64b3147-f636-4dd4-b6df-495161c52faf",
"traceId": "1d467e403a044d31b21b9f9fccbfac45",
"evidence": "orderId=2085914987172790274, replayedId=2085914987172790274",
"checkedAt": "2026-08-08T02:24:22.812Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 查询隔离订单草稿",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/orders/first-leg/2085914987172790274"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "20038c2f-603c-47b7-88bf-9ca843e26e0a, 11b67acf-700e-427e-bd65-4df7b1b7989a",
"traceId": "ce1dcf9630cc4aa0a09d9eebf6fbc654",
"evidence": "",
"checkedAt": "2026-08-08T02:24:22.851Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 草稿身份、marker 和状态正确",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/orders/first-leg/2085914987172790274"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "20038c2f-603c-47b7-88bf-9ca843e26e0a, 11b67acf-700e-427e-bd65-4df7b1b7989a",
"traceId": "ce1dcf9630cc4aa0a09d9eebf6fbc654",
"evidence": "orderNo=FL2085914987172790274, status=DRAFT",
"checkedAt": "2026-08-08T02:24:22.851Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "四箱型产品字段及图片契约完整回显",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/orders/first-leg/2085914987172790274"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "20038c2f-603c-47b7-88bf-9ca843e26e0a, 11b67acf-700e-427e-bd65-4df7b1b7989a",
"traceId": "ce1dcf9630cc4aa0a09d9eebf6fbc654",
"evidence": "cargoes=4, imageFileId=2085914986782720002",
"checkedAt": "2026-08-08T02:24:22.851Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 提交隔离订单",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/merchant/v1/orders/first-leg/2085914987172790274/submit"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "77433166-1284-4934-a067-4207989e7acd, d3267d9d-c93b-40b1-87bc-8cd2d46fbd73",
"traceId": "2e7779366c36499496ef5a2696aa137b",
"evidence": "orderId=2085914987172790274",
"checkedAt": "2026-08-08T02:24:22.925Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 回查已提交订单",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/orders/first-leg/2085914987172790274"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "5e1b2991-cc03-48ca-b30a-326f18f15b70, 5b9d1711-e72e-4385-8777-abc09ac50f88",
"traceId": "832c08f20e334930837d23e98e2b33f0",
"evidence": "",
"checkedAt": "2026-08-08T02:24:22.958Z"
},
{
"module": "orders",
"scenario": null,
"name": "Client 订单已进入待审核状态",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/merchant/v1/orders/first-leg/2085914987172790274"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "5e1b2991-cc03-48ca-b30a-326f18f15b70, 5b9d1711-e72e-4385-8777-abc09ac50f88",
"traceId": "832c08f20e334930837d23e98e2b33f0",
"evidence": "status=SUBMITTED",
"checkedAt": "2026-08-08T02:24:22.958Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 查询统一订单列表",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders?page=1&size=100&tenantId=10001&orderNo=FL2085914987172790274"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "d9fb62c1-5545-439b-baa6-e1aa6fef5c7c, 7cfc3839-5f10-4eb4-a3be-ea904587c52e",
"traceId": "5c5e483e980f4d6397c41b0e5a1ba778",
"evidence": "",
"checkedAt": "2026-08-08T02:24:23.000Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 统一订单列表只核对本轮订单",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders?page=1&size=100&tenantId=10001&orderNo=FL2085914987172790274"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "d9fb62c1-5545-439b-baa6-e1aa6fef5c7c, 7cfc3839-5f10-4eb4-a3be-ea904587c52e",
"traceId": "5c5e483e980f4d6397c41b0e5a1ba778",
"evidence": "orderId=2085914987172790274, found=true",
"checkedAt": "2026-08-08T02:24:23.000Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 查询统一订单详情",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/2085914987172790274?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "06d182cc-c800-4742-9919-24d8fd47e51d, 620f4ed9-25e3-4467-89fb-b569c47f858d",
"traceId": "77d2e16fb915485494f8a336a985d653",
"evidence": "",
"checkedAt": "2026-08-08T02:24:23.035Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 统一订单详情身份与待审核状态正确",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/2085914987172790274?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "06d182cc-c800-4742-9919-24d8fd47e51d, 620f4ed9-25e3-4467-89fb-b569c47f858d",
"traceId": "77d2e16fb915485494f8a336a985d653",
"evidence": "mainStatus=PENDING_REVIEW, timeline=2",
"checkedAt": "2026-08-08T02:24:23.035Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 查询头程订单列表",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/first-leg?tenantId=10001&keyword=FL2085914987172790274&page=1&size=100"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "58946f29-d1a9-4b39-82f0-2f18182b0e3f, 92f2fc9b-90cd-4ba9-99d1-724989577ea3",
"traceId": "614821dbea17445a8df42a34c456db18",
"evidence": "",
"checkedAt": "2026-08-08T02:24:23.069Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 头程订单列表只核对本轮订单",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/first-leg?tenantId=10001&keyword=FL2085914987172790274&page=1&size=100"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "58946f29-d1a9-4b39-82f0-2f18182b0e3f, 92f2fc9b-90cd-4ba9-99d1-724989577ea3",
"traceId": "614821dbea17445a8df42a34c456db18",
"evidence": "orderId=2085914987172790274, found=true",
"checkedAt": "2026-08-08T02:24:23.069Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 查询头程订单详情",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/first-leg/2085914987172790274?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "7632e3b7-8d97-451f-a909-82e8ab222976, 1b9250b7-9936-4466-a3c7-5446e373b545",
"traceId": "08369be7b131463dadabc7b0b58c66e6",
"evidence": "",
"checkedAt": "2026-08-08T02:24:23.100Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 头程订单详情聚合结构完整",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/first-leg/2085914987172790274?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "7632e3b7-8d97-451f-a909-82e8ab222976, 1b9250b7-9936-4466-a3c7-5446e373b545",
"traceId": "08369be7b131463dadabc7b0b58c66e6",
"evidence": "cargoes=4, history=2",
"checkedAt": "2026-08-08T02:24:23.100Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "四箱型产品字段及图片契约完整回显",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/first-leg/2085914987172790274?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "7632e3b7-8d97-451f-a909-82e8ab222976, 1b9250b7-9936-4466-a3c7-5446e373b545",
"traceId": "08369be7b131463dadabc7b0b58c66e6",
"evidence": "cargoes=4, imageFileId=2085914986782720002",
"checkedAt": "2026-08-08T02:24:23.100Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 为本轮订单添加内部备注",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/platform/v1/orders/first-leg/2085914987172790274/internal-notes?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "0d3a2d57-9c8e-48b5-8923-b727e2b71f3d, 7221a9de-feb0-4d0a-9618-a5d543209e71",
"traceId": "f7c958a6b67745c0b3273016718a7f15",
"evidence": "",
"checkedAt": "2026-08-08T02:24:23.136Z"
},
{
"module": "orders",
"scenario": null,
"name": "Admin 审核通过本轮订单",
"passed": true,
"deploymentBlocker": false,
"request": {
"method": "POST",
"path": "/api/platform/v1/orders/first-leg/2085914987172790274/approve?tenantId=10001"
},
"http": 200,
"code": "200",
"message": "success",
"requestId": "3b1f6fcf-2e9c-4401-824a-3cf429ea4510, c30de6cb-5311-4be4-9f2e-32bd75e7795c",
"traceId": "24b4f3f614b049698d1bd443f346f5ed",
"evidence": "orderId=2085914987172790274",
"checkedAt": "2026-08-08T02:24:23.187Z"
},
{
"module": "orders",
"scenario": "isolated-order-chain",
"name": "审核后生成隔离入库单",
"passed": false,
"deploymentBlocker": true,
"request": {
"method": "GET",
"path": "/api/platform/v1/orders/2085914987172790274?tenantId=10001"
},
"http": 200,
"code": "S0503",
"message": "统一订单履约详情响应无效",
"requestId": "04d51502-8c78-4256-8232-fbdb394936c5",
"traceId": "86df65f5590641378ebc2db3ac7b03fb",
"evidence": "20秒内未返回 inboundId",
"checkedAt": "2026-08-08T02:24:43.430Z"
},
{
"module": "orders",
"scenario": null,
"name": "模块执行",
"passed": false,
"deploymentBlocker": false,
"request": null,
"http": null,
"code": null,
"message": null,
"requestId": null,
"traceId": null,
"evidence": "订单审核后的入库单契约未部署,无法向 OCR/墨家模块交接",
"checkedAt": "2026-08-08T02:24:43.430Z"
},
{
"module": "inbound-weight-ocr",
"scenario": "weight-image-recognition-and-confirmation",
"name": "仅选择带本轮测试标记的可编辑入库单",
"passed": false,
"deploymentBlocker": false,
"request": null,
"http": null,
"code": null,
"message": null,
"requestId": null,
"traceId": null,
"evidence": "marker=ADMIN-API-20260808022421690, 未找到本轮创建且状态为 WAIT_MEASURE/MEASURING/MEASURE_REJECTED 的入库单",
"checkedAt": "2026-08-08T02:24:43.507Z"
}
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+19 -6
View File
@@ -718,12 +718,13 @@ async function rbacAndMemberModule() {
)
}, ({ json }) => ({ backendPermissions: json.data, visibleClientMenus: visibleClientMenus(arrayData(json.data)) }))
await apiStep('menu-permissions', 'disable assigned role', {
const disabledRole = await softApiStep('menu-permissions', 'disable assigned role', {
method: 'PUT', path: `/api/merchant/v1/iam/roles/${roleId}/status`, token: merchantToken, body: { status: 'DISABLED' },
}, ({ json }) => {
requireSuccess(json)
assertEqual(json.data?.status, 'DISABLED', 'role status')
}, ({ json }) => ({ roleId: json.data.id, status: json.data.status }))
if (disabledRole) {
await softApiStep('menu-permissions', 'disabled role is reflected by live permission query', {
method: 'GET', path: '/api/merchant/v1/iam/authorization/permissions', token: memberToken,
}, ({ json, status }) => {
@@ -779,11 +780,16 @@ async function rbacAndMemberModule() {
assertSetEqual(arrayData(json.data), desiredCodes, 'member backend permissions')
assertSetEqual(visibleClientMenus(arrayData(json.data)), ['account:profile:view', 'dashboard:view', 'order:first-leg:list'], 'visible Client menus')
}, ({ json }) => ({ backendPermissions: json.data, visibleClientMenus: visibleClientMenus(arrayData(json.data)) }))
} else {
recordSkipped('menu-permissions', 'role disable dependent authorization checks',
'Role status change failed; dependent checks require a successfully disabled role')
}
await apiStep('menu-permissions', 'remove order view permission from assigned role', {
const reducedPermissions = await softApiStep('menu-permissions', 'remove order view permission from assigned role', {
method: 'PUT', path: '/api/merchant/v1/iam/roles/permissions', token: merchantToken,
body: { roleId, permissionIds: [orderListPermissionId] },
}, ({ json }) => requireSuccess(json), () => ({ roleId, permissionCodes: [desiredCodes[0]] }))
if (reducedPermissions) {
await softApiStep('menu-permissions', 'permission removal revokes stale member token', {
method: 'GET', path: `/api/merchant/v1/orders/first-leg/${report.resources.orderIds[0]}`, token: memberToken,
}, ({ json, status }) => {
@@ -828,6 +834,10 @@ async function rbacAndMemberModule() {
requireSuccess(json)
assertEqual(String(json.data?.order?.id), String(report.resources.orderIds[0]), 'order id')
}, ({ json }) => ({ orderId: json.data.order.id }))
} else {
recordSkipped('menu-permissions', 'permission removal dependent authorization checks',
'Role permission update failed; dependent checks require a successfully reduced permission set')
}
const editedMemberBody = {
displayName: `已编辑订单成员-${marker}`.slice(0, 64),
@@ -908,7 +918,7 @@ async function rbacAndMemberModule() {
assertEqual(String(json.data?.membershipId), String(membershipId), 'membership id')
assertEqual(arrayData(json.data?.roleIds).length, 0, 'member role count')
}, ({ json }) => ({ membershipId: json.data.membershipId, roleIds: json.data.roleIds }))
await apiStep('rbac-member', 'role clearing revokes stale member token', {
await softApiStep('rbac-member', 'role clearing revokes stale member token', {
method: 'GET', path: '/api/merchant/v1/orders/first-leg?page=1&size=20', token: memberToken,
}, ({ json, status }) => {
assertEqual(status, 401, 'HTTP status')
@@ -1195,7 +1205,7 @@ async function orderLifecycleModule() {
method: 'GET', path: '/api/merchant/v1/orders/first-leg?page=1&size=20&status=UNKNOWN', token: merchantToken,
}, ({ json, status }) => {
assertTrue([200, 400].includes(status), `unexpected HTTP status ${status}`)
assertEqual(String(json?.code), 'A0400', 'business code')
assertEqual(String(json?.code), 'A0422', 'business code')
}, ({ json }) => ({ rejectionCode: json?.code, message: json?.message }))
await apiStep('order-boundary', 'unknown order detail is rejected', {
method: 'GET', path: '/api/merchant/v1/orders/first-leg/1', token: merchantToken,
@@ -1398,9 +1408,12 @@ async function accountSecurityModule() {
assertEqual(status, 200, 'HTTP status')
assertEqual(String(json?.code), 'A0401', 'business code')
}, ({ json }) => ({ rejectionCode: json.code }))
await apiStep('account-security', 'repeated logout remains idempotent', {
await apiStep('account-security', 'repeated logout with revoked session is rejected', {
method: 'POST', path: '/api/auth/logout', body: { refreshToken: loggedOutRefreshToken },
}, ({ json }) => requireSuccess(json), () => ({ repeated: true }))
}, ({ json, status }) => {
assertEqual(status, 401, 'HTTP status')
assertEqual(String(json?.code), 'A0401', 'business code')
}, ({ json }) => ({ repeated: true, rejectionCode: json?.code }))
}
async function recoverOwnerPassword() {