refactor(api): 统一 HTTP 200 响应契约
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
+17
-2
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user