优化
This commit is contained in:
@@ -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();
|
||||
|
||||
+27
-1
@@ -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
|
||||
|
||||
+18
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
+110
@@ -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
|
||||
|
||||
+13
@@ -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 {
|
||||
|
||||
+55
@@ -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())));
|
||||
|
||||
+25
-2
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
+24
@@ -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
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user