refactor(api): 统一 HTTP 200 响应契约

This commit is contained in:
2026-08-01 10:17:20 +08:00
parent f2e8935dbc
commit cad6c37af4
45 changed files with 689 additions and 289 deletions
@@ -56,7 +56,6 @@ public class MerchantTenantController {
@GetMapping("/tenant/profile")
@PreAuthorize("hasAuthority('merchant:profile:view')")
@Operation(summary = "Get current enterprise profile / 查询当前企业资料", description = "Tenant ID is taken only from the authenticated merchant JWT. / 租户 ID 仅从已认证商户 JWT 获取。")
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Profile returned / 查询成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Tenant not found / 企业不存在")})
public ApiResponse<TenantResponse> profile(@AuthenticationPrincipal AuthenticatedUser user) {
return ApiResponse.success(tenantService.profile(TenantGuard.tenant(user, "MERCHANT")));
}
@@ -64,7 +63,6 @@ public class MerchantTenantController {
@PutMapping("/tenant/profile")
@PreAuthorize("hasAuthority('merchant:profile:edit')")
@Operation(summary = "Update current enterprise profile / 更新当前企业资料", description = "Update basic profile fields within the authenticated tenant. / 在当前认证租户内更新企业基础资料。")
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Profile updated / 更新成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid profile / 资料不合法")})
public ApiResponse<TenantResponse> update(@Valid @RequestBody UpdateProfileRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
return ApiResponse.success(tenantService.update(TenantGuard.tenant(user, "MERCHANT"), request));
@@ -88,7 +86,6 @@ public class MerchantTenantController {
@PostMapping("/members")
@Operation(summary = "Add enterprise member / 新增企业成员", description = "Create a membership under the current JWT tenant. / 在当前 JWT 租户下创建成员关系。")
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Member created / 创建成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "Member already exists / 成员已存在")})
@PreAuthorize("hasAuthority('merchant:member:create')")
public ApiResponse<MemberResponse> addMember(@Valid @RequestBody CreateMemberRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
@@ -43,10 +43,8 @@ public class PlatformTenantController {
@Operation(summary = "查询企业列表", description = "平台端分页查询商户企业,支持企业名称、统一社会信用代码、联系人和状态筛选。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "企业列表查询成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "分页参数类型无效"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "需要登录"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "需要平台端权限"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "422", description = "分页范围或状态无效")
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "需要平台端权限")
})
public ApiResponse<PageResult<TenantResponse>> page(
@RequestParam(defaultValue = "1") @Min(1) long page,
@@ -73,9 +71,7 @@ public class PlatformTenantController {
@Operation(summary = "Register enterprise tenant / 提交企业注册", description = "Create a pending merchant tenant and its registration review ticket. Platform token required. / 创建待审核商户租户及注册审核单,需要平台端令牌。")
@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 / 企业已存在")
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限域")
})
public ApiResponse<TenantResponse> register(@Valid @RequestBody RegisterRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
@@ -120,11 +116,10 @@ public class PlatformTenantController {
@Operation(summary = "Approve enterprise registration / 审核通过企业注册", description = "Approve a pending review and activate the tenant. The operation is audited. / 审核通过并激活租户,操作写入审计日志。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Approved / 审核通过"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Review not found / 审核单不存在"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "Review already completed / 审核已完成")
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限")
})
public ApiResponse<Void> approve(@Parameter(description = "Review ticket ID / 审核单 ID", required = true, example = "190000000000000001") @PathVariable long id,
@RequestBody ReviewRequest request,
@Valid @RequestBody ReviewRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
TenantGuard.tenant(user, "PLATFORM");
service.review(id, user.userId(), true, request.comment());
@@ -136,11 +131,10 @@ public class PlatformTenantController {
@Operation(summary = "Reject enterprise registration / 驳回企业注册", description = "Reject a pending review and retain the rejection comment. The operation is audited. / 驳回待审核企业并保留审核意见,操作写入审计日志。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Rejected / 已驳回"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Review not found / 审核单不存在"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "Review already completed / 审核已完成")
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限")
})
public ApiResponse<Void> reject(@Parameter(description = "Review ticket ID / 审核单 ID", required = true, example = "190000000000000001") @PathVariable long id,
@RequestBody ReviewRequest request,
@Valid @RequestBody ReviewRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
TenantGuard.tenant(user, "PLATFORM");
service.review(id, user.userId(), false, request.comment());
@@ -43,7 +43,6 @@ public class ProfileResourceController {
@PostMapping("/api/platform/v1/iam/internal-users")
@PreAuthorize("hasAuthority('platform:user:create')")
@Operation(summary = "Create platform internal user / 新增平台内部用户", description = "Create an IAM profile linked to an auth-service account. / 创建关联统一认证账号的 IAM 用户资料。")
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "User created / 创建成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid user profile / 用户资料不合法")})
public ApiResponse<InternalUserResponse> addUser(@Valid @RequestBody CreateInternalUserRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
TenantGuard.tenant(user, "PLATFORM");
@@ -101,7 +100,6 @@ public class ProfileResourceController {
@PostMapping("/api/merchant/v1/iam/tenant/addresses")
@PreAuthorize("hasAuthority('merchant:profile:edit')")
@Operation(summary = "Add enterprise address / 新增企业地址", description = "Add an operating, registered or return address to the current tenant. / 为当前租户新增经营、注册地址或退货地址。")
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Address created / 创建成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid address / 地址不合法")})
public ApiResponse<IamTenantAddress> addAddress(@Valid @RequestBody AddressRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
return ApiResponse.success(service.addAddress(TenantGuard.tenant(user, "MERCHANT"), request));
@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
/**
* 租户生命周期数据契约集合,定义入驻、资料更新、审核请求和租户响应。
@@ -47,7 +48,8 @@ public final class TenantDtos {
@Schema(name = "TenantReviewRequest", description = "Enterprise review comment / 企业审核意见")
public record ReviewRequest(
@Schema(description = "Approval or rejection comment / 审核意见", example = "资料完整,审核通过") String comment
@Schema(description = "Approval or rejection comment / 审核意见", example = "资料完整,审核通过")
@Size(max = 500, message = "审核意见不能超过 500 个字符") String comment
) { }
@Schema(name = "TenantResponse", description = "Enterprise tenant summary / 企业租户概要")
@@ -6,10 +6,12 @@ import com.baiyue.iam.entity.IamTenantReview;
import com.baiyue.iam.mapper.IamTenantMapper;
import com.baiyue.iam.mapper.IamTenantReviewMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
/**
@@ -41,7 +43,22 @@ public class TenantRepository {
public void insertTenant(IamTenant tenant) { tenantMapper.insert(tenant); }
public void saveTenant(IamTenant tenant) { tenantMapper.updateById(tenant); }
public void insertReview(IamTenantReview review) { reviewMapper.insert(review); }
public void saveReview(IamTenantReview review) { reviewMapper.updateById(review); }
/**
* Atomically completes a pending review. The status predicate makes duplicate or concurrent
* approval requests fail instead of overwriting the first review decision.
*/
public boolean completeReview(Long reviewId, String status, Long reviewerId, String comment,
LocalDateTime reviewedAt) {
int affected = reviewMapper.update(null, new LambdaUpdateWrapper<IamTenantReview>()
.eq(IamTenantReview::getId, reviewId)
.eq(IamTenantReview::getStatus, "PENDING")
.set(IamTenantReview::getStatus, status)
.set(IamTenantReview::getReviewerId, reviewerId)
.set(IamTenantReview::getReviewComment, comment)
.set(IamTenantReview::getReviewedAt, reviewedAt));
return affected == 1;
}
public List<IamTenantReview> findPendingReviews() {
return reviewMapper.selectList(new LambdaQueryWrapper<IamTenantReview>()
@@ -200,11 +200,19 @@ public class MemberService {
private <T> ApiResponse<T> call(RemoteCall<T> action) {
try {
ApiResponse<T> result = action.call();
if (result == null || !result.successful()) throw unavailable();
if (result == null) throw unavailable();
if (!result.successful()) {
ResultCode code = ResultCode.of(result.code());
String message = result.message() == null || result.message().isBlank()
? code.message() : result.message();
throw new BizException(code, message);
}
return result;
} catch (FeignException exception) {
if (exception.status() == 409) throw new BizException(ResultCode.CONFLICT, "登录账号已存在");
if (exception.status() == 404) throw new BizException(ResultCode.NOT_FOUND, "认证账号不存在");
throw unavailable();
} catch (BizException exception) {
throw exception;
} catch (RuntimeException exception) {
throw unavailable();
}
}
@@ -321,11 +321,19 @@ public class PlatformInternalUserService {
private <T> ApiResponse<T> call(RemoteCall<T> action) {
try {
ApiResponse<T> result = action.call();
if (result == null || !result.successful()) throw unavailable();
if (result == null) throw unavailable();
if (!result.successful()) {
ResultCode code = ResultCode.of(result.code());
String message = result.message() == null || result.message().isBlank()
? code.message() : result.message();
throw new BizException(code, message);
}
return result;
} catch (FeignException exception) {
if (exception.status() == 409) throw new BizException(ResultCode.CONFLICT, "登录账号已存在");
if (exception.status() == 404) throw new BizException(ResultCode.NOT_FOUND, "认证账号不存在");
throw unavailable();
} catch (BizException exception) {
throw exception;
} catch (RuntimeException exception) {
throw unavailable();
}
}
@@ -99,11 +99,11 @@ public class TenantService {
throw new BizException(ResultCode.CONFLICT, "Review already completed / 审核已完成");
}
review.setStatus(approve ? "APPROVED" : "REJECTED");
review.setReviewerId(reviewerId);
review.setReviewComment(comment);
review.setReviewedAt(LocalDateTime.now());
repository.saveReview(review);
LocalDateTime reviewedAt = LocalDateTime.now();
if (!repository.completeReview(reviewId, approve ? "APPROVED" : "REJECTED",
reviewerId, comment, reviewedAt)) {
throw new BizException(ResultCode.CONFLICT, "Review already completed / 审核已完成");
}
IamTenant tenant = repository.findTenant(review.getTenantId());
if (tenant == null) {
+3 -49
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Baiyue IAM API / 百悦身份与权限 API
description: Tenant, member, role, menu, page, button and API permission management. / 租户、成员、角色、菜单、页面、按钮和 API 权限管理。
description: 业务成功和失败均使用 HTTP 200;仅响应体 code 为字符串 "200" 时表示成功,其他 code 表示失败。鉴权和协议层异常仍使用真实 HTTP 状态码。Tenant, member, role, menu, page, button and API permission management.
version: 1.0.1
servers:
- url: /
@@ -33,10 +33,8 @@ paths:
content:
application/json:
schema: {$ref: '#/components/schemas/TenantPageApiResponse'}
'400': {$ref: '#/components/responses/BadRequest'}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
'422': {$ref: '#/components/responses/UnprocessableEntity'}
post:
tags: [Platform Tenant / 平台租户]
summary: Register enterprise tenant / 创建企业租户
@@ -48,9 +46,7 @@ paths:
schema: {$ref: '#/components/schemas/RegisterRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
/api/platform/v1/iam/authorization/permissions:
get:
tags: [IAM Resources / IAM 资源]
@@ -73,7 +69,6 @@ paths:
responses:
'200': {$ref: '#/components/responses/Success'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
put:
tags: [Platform Tenant / 平台租户]
summary: 编辑企业客户档案
@@ -84,9 +79,7 @@ paths:
schema: {$ref: '#/components/schemas/UpdateTenantRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
/api/platform/v1/iam/tenants/{tenantId}/status:
parameters:
- {$ref: '#/components/parameters/TenantId'}
@@ -100,10 +93,7 @@ paths:
schema: {$ref: '#/components/schemas/ChangeTenantStatusRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/api/merchant/v1/iam/authorization/permissions:
get:
tags: [IAM Resources / IAM 资源]
@@ -140,8 +130,6 @@ paths:
responses:
'200': {$ref: '#/components/responses/Success'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/api/platform/v1/iam/reviews/{id}/reject:
post:
tags: [Platform Tenant / 平台租户]
@@ -157,8 +145,6 @@ paths:
responses:
'200': {$ref: '#/components/responses/Success'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/api/platform/v1/iam/internal-users:
get:
tags: [IAM Resources / IAM 资源]
@@ -181,9 +167,7 @@ paths:
schema: {$ref: '#/components/schemas/CreateInternalUserRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
/api/platform/v1/iam/internal-users/{membershipId}:
parameters:
- {$ref: '#/components/parameters/MembershipId'}
@@ -197,10 +181,7 @@ paths:
schema: {$ref: '#/components/schemas/UpdateInternalUserRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/api/platform/v1/iam/internal-users/{membershipId}/status:
parameters:
- {$ref: '#/components/parameters/MembershipId'}
@@ -214,9 +195,7 @@ paths:
schema: {$ref: '#/components/schemas/ChangeInternalUserStatusRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
/api/platform/v1/iam/internal-users/{membershipId}/password:
parameters:
- {$ref: '#/components/parameters/MembershipId'}
@@ -230,9 +209,7 @@ paths:
schema: {$ref: '#/components/schemas/ResetInternalUserPasswordRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
/api/platform/v1/iam/internal-users/{membershipId}/roles:
parameters:
- {$ref: '#/components/parameters/MembershipId'}
@@ -251,9 +228,7 @@ paths:
content:
application/json:
schema: {$ref: '#/components/schemas/InternalUserApiResponse'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
/api/merchant/v1/iam/tenant/profile:
get:
tags: [Merchant Tenant / 商户企业]
@@ -263,7 +238,6 @@ paths:
'200': {$ref: '#/components/responses/Success'}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
put:
tags: [Merchant Tenant / 商户企业]
summary: Update current tenant profile / 更新当前企业资料
@@ -274,9 +248,7 @@ paths:
schema: {$ref: '#/components/schemas/UpdateProfileRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
/api/merchant/v1/iam/members:
get:
tags: [Merchant Tenant / 商户企业]
@@ -295,9 +267,7 @@ paths:
schema: {$ref: '#/components/schemas/CreateMemberRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
/api/merchant/v1/iam/tenant/addresses:
get:
tags: [IAM Resources / IAM 资源]
@@ -315,7 +285,6 @@ paths:
schema: {$ref: '#/components/schemas/AddressRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
/api/merchant/v1/iam/tenant/qualifications:
get:
@@ -344,9 +313,7 @@ paths:
schema: {$ref: '#/components/schemas/CreateRoleRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
/api/{app}/v1/iam/members/roles:
put:
tags: [RBAC]
@@ -361,7 +328,6 @@ paths:
schema: {$ref: '#/components/schemas/AssignRolesRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
/api/{app}/v1/iam/permissions:
parameters:
@@ -383,9 +349,7 @@ paths:
schema: {$ref: '#/components/schemas/CreatePermissionRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
/api/{app}/v1/iam/roles/permissions:
put:
tags: [RBAC]
@@ -400,7 +364,6 @@ paths:
schema: {$ref: '#/components/schemas/GrantPermissionsRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
/api/{app}/v1/iam/permissions/{permissionId}:
parameters:
@@ -417,9 +380,7 @@ paths:
schema: {$ref: '#/components/schemas/UpdatePermissionRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
delete:
tags: [RBAC]
summary: 删除未绑定的自定义权限资源
@@ -427,7 +388,6 @@ paths:
responses:
'200': {$ref: '#/components/responses/Success'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
/api/{app}/v1/iam/permissions/{permissionId}/status:
parameters:
- {$ref: '#/components/parameters/App'}
@@ -442,9 +402,7 @@ paths:
schema: {$ref: '#/components/schemas/ChangePermissionStatusRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
components:
securitySchemes:
bearerAuth:
@@ -486,18 +444,14 @@ components:
schema: {type: integer, format: int64}
responses:
Success: {description: Operation succeeded / 操作成功, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
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 or tenant scope rejected / 权限或租户范围不匹配, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
NotFound: {description: Resource not found / 资源不存在, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
Conflict: {description: Resource conflict / 资源冲突, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
UnprocessableEntity: {description: 参数校验失败, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
schemas:
ApiResponse:
type: object
required: [code, message, timestamp]
properties:
code: {type: string, example: '00000'}
code: {type: string, example: '200', description: 200 表示成功,其他值表示业务失败}
message: {type: string, example: Success}
data: {}
requestId: {type: string}
@@ -587,7 +541,7 @@ components:
ReviewRequest:
type: object
properties:
comment: {type: string, description: Review comment or rejection reason / 审核意见或驳回原因}
comment: {type: string, maxLength: 500, description: Review comment or rejection reason / 审核意见或驳回原因}
CreateInternalUserRequest:
type: object
required: [displayName]
@@ -29,7 +29,8 @@ class IamOpenApiContractTest {
.contains("AssignInternalUserRolesRequest:")
.contains("membershipId: {type: integer, format: int64}")
.contains("roleIds: {type: array, items: {type: integer, format: int64}}")
.contains("'422': {$ref: '#/components/responses/UnprocessableEntity'}");
.contains("example: '200'")
.doesNotContain("'422': {$ref: '#/components/responses/UnprocessableEntity'}");
Map<String, Object> openApi = new Yaml().load(document);
assertThat(openApi).containsEntry("openapi", "3.0.3");
@@ -5,6 +5,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import jakarta.validation.Valid;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -22,7 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
@DisplayName("平台客户接口契约")
class PlatformTenantControllerContractTest {
@Test
void merchantPageEndpointDeclaresPermissionPaginationConstraintsAndErrorStatuses() throws Exception {
void merchantPageEndpointDeclaresPermissionPaginationConstraintsAndUnifiedBusinessStatus() throws Exception {
Method method = PlatformTenantController.class.getMethod("page", long.class, long.class,
String.class, String.class, AuthenticatedUser.class);
@@ -38,7 +40,7 @@ class PlatformTenantControllerContractTest {
Set<String> statuses = Arrays.stream(method.getAnnotation(ApiResponses.class).value())
.map(io.swagger.v3.oas.annotations.responses.ApiResponse::responseCode)
.collect(Collectors.toSet());
assertEquals(Set.of("200", "400", "401", "403", "422"), statuses);
assertEquals(Set.of("200", "401", "403"), statuses);
}
@Test
@@ -49,6 +51,19 @@ class PlatformTenantControllerContractTest {
method.getAnnotation(PreAuthorize.class).value());
}
@Test
void reviewRequestsAreValidatedAndCommentMatchesDatabaseLimit() throws Exception {
Method approve = PlatformTenantController.class.getMethod("approve", long.class,
com.baiyue.iam.dto.TenantDtos.ReviewRequest.class, AuthenticatedUser.class);
Method reject = PlatformTenantController.class.getMethod("reject", long.class,
com.baiyue.iam.dto.TenantDtos.ReviewRequest.class, AuthenticatedUser.class);
Method comment = com.baiyue.iam.dto.TenantDtos.ReviewRequest.class.getMethod("comment");
assertNotNull(annotation(approve, 1, Valid.class));
assertNotNull(annotation(reject, 1, Valid.class));
assertEquals(500, comment.getAnnotation(Size.class).max());
}
private <T extends Annotation> T annotation(Method method, int parameter, Class<T> type) {
return method.getParameters()[parameter].getAnnotation(type);
}
@@ -72,8 +72,22 @@ class MemberServiceTest {
@Test void authServiceFailureDoesNotPersistLocalStatus() {
when(auth.status(anyString(), eq(40L), any())).thenThrow(new RuntimeException("网络失败"));
assertThrows(RuntimeException.class, () -> service.changeStatus(100L, 20L,
BizException exception = assertThrows(BizException.class,
() -> service.changeStatus(100L, 20L,
new ChangeMemberStatusRequest("DISABLED"), 99L));
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
verify(repository, never()).save(member);
}
@Test void authHttp200BusinessFailureKeepsBusinessCodeAndDoesNotPersistLocalStatus() {
when(auth.status(anyString(), eq(40L), any())).thenReturn(
ApiResponse.<Void>failure(ResultCode.NOT_FOUND, "认证账号不存在", null, null));
BizException exception = assertThrows(BizException.class, () -> service.changeStatus(100L, 20L,
new ChangeMemberStatusRequest("DISABLED"), 99L));
assertEquals(ResultCode.NOT_FOUND, exception.getResultCode());
assertEquals("认证账号不存在", exception.getMessage());
verify(repository, never()).save(member);
}
@@ -1,6 +1,7 @@
package com.baiyue.iam.service;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.iam.client.AuthAccountClient;
import com.baiyue.iam.dto.PlatformInternalUserDtos.*;
@@ -58,6 +59,29 @@ class PlatformInternalUserServiceTest {
assertEquals(List.of(40L), result.roleIds());
}
@Test
void authHttp200BusinessFailureKeepsBusinessCode() {
when(repository.findByTenant(0L)).thenReturn(List.of(membership));
when(auth.summary("internal-token", 10L)).thenReturn(
ApiResponse.<AuthAccountClient.AccountSummary>failure(
ResultCode.NOT_FOUND, "认证账号不存在", null, null));
BizException exception = assertThrows(BizException.class, service::list);
assertEquals(ResultCode.NOT_FOUND, exception.getResultCode());
assertEquals("认证账号不存在", exception.getMessage());
}
@Test
void authDecodeOrTransportFailureMapsToServiceUnavailable() {
when(repository.findByTenant(0L)).thenReturn(List.of(membership));
when(auth.summary("internal-token", 10L)).thenThrow(new RuntimeException("decode failed"));
BizException exception = assertThrows(BizException.class, service::list);
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
}
@Test
void createPlatformUserCreatesPlatformAccountInAuthAndMembershipInIam() {
IamRole role = new IamRole();
@@ -2,6 +2,7 @@ package com.baiyue.iam.service;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.iam.entity.IamTenant;
import com.baiyue.iam.entity.IamTenantReview;
import com.baiyue.iam.dto.TenantDtos.ChangeTenantStatusRequest;
import com.baiyue.iam.dto.TenantDtos.UpdateTenantRequest;
import com.baiyue.iam.repository.TenantRepository;
@@ -15,6 +16,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.baiyue.common.core.exception.BizException;
@@ -98,6 +100,59 @@ class TenantServiceTest {
assertEquals(true, withQualification.eligible());
}
@Test
void reviewCompletesPendingTicketWithConditionalUpdate() {
TenantRepository repository = mock(TenantRepository.class);
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
TenantService service = new TenantService();
ReflectionTestUtils.setField(service, "repository", repository);
ReflectionTestUtils.setField(service, "profileRepository", profileRepository);
IamTenantReview review = pendingReview(90001L, 10001L);
IamTenant tenant = merchant(10001L, "PENDING");
when(repository.findReview(90001L)).thenReturn(review);
when(repository.completeReview(org.mockito.ArgumentMatchers.eq(90001L),
org.mockito.ArgumentMatchers.eq("APPROVED"), org.mockito.ArgumentMatchers.eq(70001L),
org.mockito.ArgumentMatchers.eq("ok"), org.mockito.ArgumentMatchers.any()))
.thenReturn(true);
when(repository.findTenant(10001L)).thenReturn(tenant);
when(profileRepository.findQualifications(10001L)).thenReturn(List.of());
service.review(90001L, 70001L, true, "ok");
assertEquals("ACTIVE", tenant.getStatus());
assertEquals(70001L, tenant.getApprovedBy());
verify(repository).saveTenant(tenant);
}
@Test
void duplicateOrConcurrentReviewIsRejectedBeforeTenantMutation() {
TenantRepository repository = mock(TenantRepository.class);
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
TenantService service = new TenantService();
ReflectionTestUtils.setField(service, "repository", repository);
ReflectionTestUtils.setField(service, "profileRepository", profileRepository);
when(repository.findReview(90001L)).thenReturn(pendingReview(90001L, 10001L));
when(repository.completeReview(org.mockito.ArgumentMatchers.eq(90001L),
org.mockito.ArgumentMatchers.eq("REJECTED"), org.mockito.ArgumentMatchers.eq(70001L),
org.mockito.ArgumentMatchers.eq("no"), org.mockito.ArgumentMatchers.any()))
.thenReturn(false);
BizException exception = assertThrows(BizException.class,
() -> service.review(90001L, 70001L, false, "no"));
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
verify(repository, never()).findTenant(org.mockito.ArgumentMatchers.anyLong());
verify(repository, never()).saveTenant(org.mockito.ArgumentMatchers.any());
}
private IamTenantReview pendingReview(long id, long tenantId) {
IamTenantReview review = new IamTenantReview();
review.setId(id);
review.setTenantId(tenantId);
review.setStatus("PENDING");
return review;
}
private IamTenant merchant(long id, String status) {
IamTenant tenant = new IamTenant();
tenant.setId(id);