暂存提交
This commit is contained in:
+4
-1
@@ -15,6 +15,7 @@
|
||||
| 商户成员增改、启停、重置密码 | `MerchantTenantController.java` | `MemberService.java`,账号协作看 `client/AuthAccountClient.java` |
|
||||
| 平台创建企业与入驻审核 | `controller/PlatformTenantController.java` | `MerchantOnboardingService.java`、`TenantService.java` |
|
||||
| 平台企业客户分页 | `controller/PlatformTenantController.java` | `TenantService.pageMerchants`、`TenantRepository.pageMerchants`;仅返回 `tenant_type=MERCHANT` |
|
||||
| 平台企业客户档案 | `controller/PlatformTenantController.java` | 一期维护销售负责人、账期、企业登记资料、联系微信、注册地址和内部备注;响应返回入驻与审批时间,不承载运单、财务或套餐统计 |
|
||||
| 平台内部用户 | `controller/ProfileResourceController.java` | `PlatformInternalUserService.java`;账号创建、显示名、启停和重置密码经 `AuthAccountClient` 编排 |
|
||||
| 企业地址与资质元数据 | `controller/ProfileResourceController.java` | `ProfileResourceService.java`、`ProfileResourceRepository.java` |
|
||||
| 角色、权限、成员角色、角色授权 | `controller/RbacController.java` | `RbacService.java`、`RbacRepository.java` |
|
||||
@@ -28,16 +29,18 @@
|
||||
|
||||
## 修改与验证
|
||||
- 平台客户档案维护使用 `PlatformTenantController` 的列表、详情、新建、编辑、启停接口;详情和维护仅接受 `tenant_type=MERCHANT`。PENDING/REJECTED 只能由审核流程变更,不得由启停接口绕过。
|
||||
- 平台客户列表使用 `platform:customer:list` 并脱敏电话、邮箱;完整详情、平台地址和平台资质只读查询统一使用独立的 `platform:customer:view`,不得复用 `platform:customer:review`。
|
||||
- 权限资源的 code/type 不可变;只有 `built_in=0` 的自定义资源可编辑、启停或删除,删除前必须无子节点且无角色绑定。
|
||||
|
||||
- `tenant_id` 是商户数据隔离核心;平台端与商户端访问域、角色和权限不可混用。
|
||||
- 按钮隐藏不能代替后端 permission code / permissionIds 校验;权限快照变化需同步检查 Auth 与 Gateway Claim 消费。
|
||||
- 成员新增携带角色、编辑时变更或清空角色,必须额外具有 `merchant:member:grant-role`;仅资料编辑且角色集合不变时不要求该权限。
|
||||
- `/api/{app}/v1/iam/authorization/permissions` 是登录初始化接口,只校验已认证应用域和有效成员身份;禁止增加 `role:list` 等业务权限前置条件。
|
||||
- 平台企业客户分页仅查询商户租户;分页参数类型错误返回 400,范围或状态约束错误返回 422。
|
||||
- 成员账号操作跨 IAM/Auth,保持幂等与补偿;资质上传跨 IAM/File,IAM 不保存文件内容。
|
||||
- 平台内部用户的公开主键是 `membershipId`;列表必须回显 `roleIds`,密码只允许通过 DTO 转交 Auth,禁止进入 IAM 实体、日志或审计快照。
|
||||
- 平台内部用户角色分配使用 `PUT /api/platform/v1/iam/internal-users/{membershipId}/roles`,仅允许平台租户下已启用的平台角色;空 `roleIds` 表示清空。
|
||||
- 商户成员新增、编辑采用字段级授权:基础资料分别校验 `merchant:member:create`、`merchant:member:edit`;创建时非空角色、编辑时角色集合发生任何变化(含空数组清空)还必须校验 `merchant:member:grant-role`。详情和列表实时回显当前 `roleIds`,比较和写入均限定当前租户。
|
||||
- 企业 `ACTIVE` 只表示平台审核已通过;下单资格还必须至少存在一份 `APPROVED` 资质,因此允许“已激活但暂不可下单”的中间业务状态。
|
||||
- SQL 增量保持可重复部署顺序,修改 Entity 时同步检查 Mapper、Repository、DTO 与种子数据。`phase1-platform-master-data-and-permission-management.sql` 必须在 `phase1-admin-rbac.sql` 和 `rbac-role-management-v2.sql` 之后执行;它标记既有系统资源并补齐平台客户、快递公司、往来单位与权限资源管理权限。
|
||||
- 存量库新增一期客户档案字段时执行 `phase1-customer-profile.sql`;该脚本只扩展 `iam_tenant`,可重复执行,不得把运单数、财务金额、套餐计费、代客登录或物理删除能力并入 IAM。
|
||||
- 从父目录运行 `mvn -pl iam -am test`,再按影响执行一期认证/IAM API 回归。
|
||||
|
||||
@@ -27,3 +27,9 @@ Merge the packages under `src/main/java` into `D:/baiyue/iam/src/main/java`; exe
|
||||
| GET/POST | `/api/{app}/v1/iam/permissions` | PLATFORM/MERCHANT | Permission tree / 权限树 |
|
||||
|
||||
OpenAPI is synchronized in `openapi/iam-v1.yaml`.
|
||||
|
||||
## 商户成员角色字段级授权
|
||||
|
||||
- 新增、编辑成员基础资料分别需要 `merchant:member:create`、`merchant:member:edit`。
|
||||
- 新增请求的 `roleIds` 非空,或编辑请求的完整目标角色集合与当前集合不同时,还必须具有 `merchant:member:grant-role`。
|
||||
- `roleIds: []` 表示清空角色,不能绕过授权;编辑时角色集合未变化则不额外拒绝。成员详情和列表均实时返回当前租户下的 `roleIds`。
|
||||
|
||||
@@ -56,6 +56,7 @@ 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")));
|
||||
}
|
||||
@@ -63,6 +64,7 @@ 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));
|
||||
@@ -86,6 +88,7 @@ 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,8 +43,10 @@ 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 = "403", description = "需要平台端权限"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "422", description = "分页范围或状态无效")
|
||||
})
|
||||
public ApiResponse<PageResult<TenantResponse>> page(
|
||||
@RequestParam(defaultValue = "1") @Min(1) long page,
|
||||
@@ -58,8 +60,8 @@ public class PlatformTenantController {
|
||||
}
|
||||
|
||||
@GetMapping("/tenants/{tenantId:\\d+}")
|
||||
@PreAuthorize("hasAuthority('platform:customer:list')")
|
||||
@Operation(summary = "查询企业客户档案")
|
||||
@PreAuthorize("hasAuthority('platform:customer:view')")
|
||||
@Operation(summary = "查询企业客户档案", description = "返回企业基础资料、销售负责人、账期、注册地址以及入驻和审批时间。")
|
||||
public ApiResponse<TenantResponse> tenant(@PathVariable long tenantId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
@@ -71,7 +73,9 @@ 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 = "403", description = "Platform scope required / 需要平台端权限域")
|
||||
@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) {
|
||||
@@ -81,7 +85,7 @@ public class PlatformTenantController {
|
||||
|
||||
@PutMapping("/tenants/{tenantId:\\d+}")
|
||||
@PreAuthorize("hasAuthority('platform:customer:edit')")
|
||||
@Operation(summary = "编辑企业客户档案")
|
||||
@Operation(summary = "编辑企业客户档案", description = "维护一期客户档案字段;统一社会信用代码和生命周期状态不由该接口修改。")
|
||||
public ApiResponse<TenantResponse> updateTenant(@PathVariable long tenantId,
|
||||
@Valid @RequestBody UpdateTenantRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
@@ -116,10 +120,11 @@ 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 = "403", description = "Platform scope required / 需要平台端权限")
|
||||
@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 / 审核已完成")
|
||||
})
|
||||
public ApiResponse<Void> approve(@Parameter(description = "Review ticket ID / 审核单 ID", required = true, example = "190000000000000001") @PathVariable long id,
|
||||
@Valid @RequestBody ReviewRequest request,
|
||||
@RequestBody ReviewRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
service.review(id, user.userId(), true, request.comment());
|
||||
@@ -131,10 +136,11 @@ 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 = "403", description = "Platform scope required / 需要平台端权限")
|
||||
@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 / 审核已完成")
|
||||
})
|
||||
public ApiResponse<Void> reject(@Parameter(description = "Review ticket ID / 审核单 ID", required = true, example = "190000000000000001") @PathVariable long id,
|
||||
@Valid @RequestBody ReviewRequest request,
|
||||
@RequestBody ReviewRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
service.review(id, user.userId(), false, request.comment());
|
||||
|
||||
@@ -43,6 +43,7 @@ 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");
|
||||
@@ -100,6 +101,7 @@ 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));
|
||||
@@ -113,11 +115,21 @@ public class ProfileResourceController {
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/v1/iam/tenants/{tenantId}/qualifications")
|
||||
@PreAuthorize("hasAuthority('platform:customer:review')")
|
||||
@PreAuthorize("hasAuthority('platform:customer:view')")
|
||||
@Operation(summary = "查询企业客户资质", description = "只读查询企业客户资质元数据,不授予审核能力。")
|
||||
public ApiResponse<List<IamTenantQualification>> platformQualifications(
|
||||
@PathVariable long tenantId, @AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.qualifications(tenantId));
|
||||
return ApiResponse.success(service.platformQualifications(tenantId));
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/v1/iam/tenants/{tenantId}/addresses")
|
||||
@PreAuthorize("hasAuthority('platform:customer:view')")
|
||||
@Operation(summary = "查询企业客户地址", description = "只读查询企业客户地址,不开放平台端写入。")
|
||||
public ApiResponse<List<IamTenantAddress>> platformAddresses(
|
||||
@PathVariable long tenantId, @AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.platformAddresses(tenantId));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,10 +2,19 @@ package com.baiyue.iam.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Digits;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.PastOrPresent;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 租户生命周期数据契约集合,定义入驻、资料更新、审核请求和租户响应。
|
||||
*/
|
||||
@@ -19,7 +28,16 @@ public final class TenantDtos {
|
||||
@Schema(description = "18-character unified social credit code / 18 位统一社会信用代码", example = "91440300MA5EXAMPLE") @NotBlank @Pattern(regexp = "[0-9A-Z]{18}") String unifiedSocialCreditCode,
|
||||
@Schema(description = "Primary contact / 联系人", example = "张三") @NotBlank String contactName,
|
||||
@Schema(description = "Contact phone / 联系电话", example = "13800138000") @NotBlank String contactPhone,
|
||||
@Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") @Email String contactEmail
|
||||
@Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") @Email String contactEmail,
|
||||
@Size(max = 64) String contactWechat,
|
||||
@Size(max = 64) String salesOwnerName,
|
||||
@Min(0) @Max(365) Integer paymentTermDays,
|
||||
@Size(max = 64) String enterpriseType,
|
||||
@PastOrPresent LocalDate establishedDate,
|
||||
@DecimalMin("0.00") @Digits(integer = 16, fraction = 2) BigDecimal registeredCapital,
|
||||
@Pattern(regexp = "[A-Z]{3}") String registeredCapitalCurrency,
|
||||
@Size(max = 500) String registeredAddress,
|
||||
@Size(max = 1000) String internalRemark
|
||||
) { }
|
||||
|
||||
@Schema(name = "TenantProfileUpdateRequest", description = "Enterprise profile update / 企业资料更新")
|
||||
@@ -38,7 +56,16 @@ public final class TenantDtos {
|
||||
@NotBlank String legalName,
|
||||
@NotBlank String contactName,
|
||||
@NotBlank String contactPhone,
|
||||
@Email String contactEmail
|
||||
@Email String contactEmail,
|
||||
@Size(max = 64) String contactWechat,
|
||||
@Size(max = 64) String salesOwnerName,
|
||||
@Min(0) @Max(365) Integer paymentTermDays,
|
||||
@Size(max = 64) String enterpriseType,
|
||||
@PastOrPresent LocalDate establishedDate,
|
||||
@DecimalMin("0.00") @Digits(integer = 16, fraction = 2) BigDecimal registeredCapital,
|
||||
@Pattern(regexp = "[A-Z]{3}") String registeredCapitalCurrency,
|
||||
@Size(max = 500) String registeredAddress,
|
||||
@Size(max = 1000) String internalRemark
|
||||
) { }
|
||||
|
||||
@Schema(name = "TenantStatusChangeRequest", description = "Platform controlled merchant tenant status")
|
||||
@@ -48,12 +75,15 @@ public final class TenantDtos {
|
||||
|
||||
@Schema(name = "TenantReviewRequest", description = "Enterprise review comment / 企业审核意见")
|
||||
public record ReviewRequest(
|
||||
@Schema(description = "Approval or rejection comment / 审核意见", example = "资料完整,审核通过")
|
||||
@Size(max = 500, message = "审核意见不能超过 500 个字符") String comment
|
||||
@Schema(description = "Approval or rejection comment / 审核意见", example = "资料完整,审核通过") String comment
|
||||
) { }
|
||||
|
||||
@Schema(name = "TenantResponse", description = "Enterprise tenant summary / 企业租户概要")
|
||||
public record TenantResponse(Long id, String tenantCode, String tenantName, String status,
|
||||
String legalName, String unifiedSocialCreditCode, String contactName,
|
||||
String contactPhone, String contactEmail) { }
|
||||
String contactPhone, String contactEmail, String contactWechat,
|
||||
String salesOwnerName, Integer paymentTermDays, String enterpriseType,
|
||||
LocalDate establishedDate, BigDecimal registeredCapital,
|
||||
String registeredCapitalCurrency, String registeredAddress,
|
||||
String internalRemark, LocalDateTime createdAt, LocalDateTime approvedAt) { }
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import lombok.Setter;
|
||||
import com.baiyue.common.core.model.BaseEntity;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
@@ -25,6 +27,15 @@ public class IamTenant extends BaseEntity {
|
||||
private String contactName;
|
||||
private String contactPhone;
|
||||
private String contactEmail;
|
||||
private String contactWechat;
|
||||
private String salesOwnerName;
|
||||
private Integer paymentTermDays;
|
||||
private String enterpriseType;
|
||||
private LocalDate establishedDate;
|
||||
private BigDecimal registeredCapital;
|
||||
private String registeredCapitalCurrency;
|
||||
private String registeredAddress;
|
||||
private String internalRemark;
|
||||
private LocalDateTime approvedAt;
|
||||
private Long approvedBy;
|
||||
private String rejectReason;
|
||||
|
||||
@@ -6,12 +6,10 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -43,22 +41,7 @@ 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); }
|
||||
|
||||
/**
|
||||
* 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 void saveReview(IamTenantReview review) { reviewMapper.updateById(review); }
|
||||
|
||||
public List<IamTenantReview> findPendingReviews() {
|
||||
return reviewMapper.selectList(new LambdaQueryWrapper<IamTenantReview>()
|
||||
@@ -78,7 +61,8 @@ public class TenantRepository {
|
||||
.or().like(IamTenant::getLegalName, keyword)
|
||||
.or().like(IamTenant::getUnifiedSocialCreditCode, keyword)
|
||||
.or().like(IamTenant::getContactName, keyword)
|
||||
.or().like(IamTenant::getContactPhone, keyword));
|
||||
.or().like(IamTenant::getContactPhone, keyword)
|
||||
.or().like(IamTenant::getSalesOwnerName, keyword));
|
||||
}
|
||||
return PageResult.from(tenantMapper.selectPage(new Page<>(page, size), wrapper));
|
||||
}
|
||||
|
||||
@@ -200,19 +200,11 @@ public class MemberService {
|
||||
private <T> ApiResponse<T> call(RemoteCall<T> action) {
|
||||
try {
|
||||
ApiResponse<T> result = action.call();
|
||||
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);
|
||||
}
|
||||
if (result == null || !result.successful()) throw unavailable();
|
||||
return result;
|
||||
} catch (FeignException exception) {
|
||||
throw unavailable();
|
||||
} catch (BizException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
if (exception.status() == 409) throw new BizException(ResultCode.CONFLICT, "登录账号已存在");
|
||||
if (exception.status() == 404) throw new BizException(ResultCode.NOT_FOUND, "认证账号不存在");
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,19 +321,11 @@ public class PlatformInternalUserService {
|
||||
private <T> ApiResponse<T> call(RemoteCall<T> action) {
|
||||
try {
|
||||
ApiResponse<T> result = action.call();
|
||||
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);
|
||||
}
|
||||
if (result == null || !result.successful()) throw unavailable();
|
||||
return result;
|
||||
} catch (FeignException exception) {
|
||||
throw unavailable();
|
||||
} catch (BizException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
if (exception.status() == 409) throw new BizException(ResultCode.CONFLICT, "登录账号已存在");
|
||||
if (exception.status() == 404) throw new BizException(ResultCode.NOT_FOUND, "认证账号不存在");
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,16 @@ public class ProfileResourceService {
|
||||
|
||||
public List<IamTenantQualification> qualifications(long tenant) { return repository.findQualifications(tenant); }
|
||||
|
||||
public List<IamTenantAddress> platformAddresses(long tenantId) {
|
||||
requireMerchant(tenantId);
|
||||
return repository.findAddresses(tenantId);
|
||||
}
|
||||
|
||||
public List<IamTenantQualification> platformQualifications(long tenantId) {
|
||||
requireMerchant(tenantId);
|
||||
return repository.findQualifications(tenantId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "TENANT_QUALIFICATION_SUBMIT", resourceType = "TENANT_QUALIFICATION",
|
||||
resourceId = "#request.tenantId", description = "提交企业资质", captureAfter = false)
|
||||
@@ -84,4 +94,12 @@ public class ProfileResourceService {
|
||||
x.setCertificateNo(certificateNo); x.setDocumentUrl(url); x.setStatus("PENDING");
|
||||
repository.insertQualification(x); return x;
|
||||
}
|
||||
|
||||
private void requireMerchant(long tenantId) {
|
||||
IamTenant tenant = tenantRepository.findTenant(tenantId);
|
||||
if (tenant == null || tenant.getId() == null || tenant.getId().longValue() != tenantId
|
||||
|| !"MERCHANT".equals(tenant.getTenantType())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Merchant tenant not found / 企业客户不存在");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,15 @@ public class TenantService {
|
||||
tenant.setContactName(request.contactName());
|
||||
tenant.setContactPhone(request.contactPhone());
|
||||
tenant.setContactEmail(request.contactEmail());
|
||||
tenant.setContactWechat(request.contactWechat());
|
||||
tenant.setSalesOwnerName(request.salesOwnerName());
|
||||
tenant.setPaymentTermDays(request.paymentTermDays());
|
||||
tenant.setEnterpriseType(request.enterpriseType());
|
||||
tenant.setEstablishedDate(request.establishedDate());
|
||||
tenant.setRegisteredCapital(request.registeredCapital());
|
||||
tenant.setRegisteredCapitalCurrency(request.registeredCapitalCurrency());
|
||||
tenant.setRegisteredAddress(request.registeredAddress());
|
||||
tenant.setInternalRemark(request.internalRemark());
|
||||
repository.insertTenant(tenant);
|
||||
|
||||
IamTenantReview review = new IamTenantReview();
|
||||
@@ -77,7 +86,7 @@ public class TenantService {
|
||||
|
||||
public PageResult<TenantResponse> pageMerchants(long page, long size, String keyword, String status) {
|
||||
PageResult<IamTenant> source = repository.pageMerchants(page, size, keyword, status);
|
||||
return new PageResult<>(source.records().stream().map(this::response).toList(),
|
||||
return new PageResult<>(source.records().stream().map(this::summaryResponse).toList(),
|
||||
source.page(), source.size(), source.total(), source.pages());
|
||||
}
|
||||
|
||||
@@ -99,11 +108,11 @@ public class TenantService {
|
||||
throw new BizException(ResultCode.CONFLICT, "Review already completed / 审核已完成");
|
||||
}
|
||||
|
||||
LocalDateTime reviewedAt = LocalDateTime.now();
|
||||
if (!repository.completeReview(reviewId, approve ? "APPROVED" : "REJECTED",
|
||||
reviewerId, comment, reviewedAt)) {
|
||||
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);
|
||||
|
||||
IamTenant tenant = repository.findTenant(review.getTenantId());
|
||||
if (tenant == null) {
|
||||
@@ -144,7 +153,7 @@ public class TenantService {
|
||||
if (tenant == null || !tenantIdEquals(tenant, tenantId)) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Tenant not found / 企业不存在");
|
||||
}
|
||||
return response(tenant);
|
||||
return merchantVisibleResponse(tenant);
|
||||
}
|
||||
|
||||
/** 平台端查询客户档案:仅能查商户租户,不暴露平台租户。 */
|
||||
@@ -162,6 +171,15 @@ public class TenantService {
|
||||
tenant.setContactName(request.contactName());
|
||||
tenant.setContactPhone(request.contactPhone());
|
||||
tenant.setContactEmail(request.contactEmail());
|
||||
tenant.setContactWechat(request.contactWechat());
|
||||
tenant.setSalesOwnerName(request.salesOwnerName());
|
||||
tenant.setPaymentTermDays(request.paymentTermDays());
|
||||
tenant.setEnterpriseType(request.enterpriseType());
|
||||
tenant.setEstablishedDate(request.establishedDate());
|
||||
tenant.setRegisteredCapital(request.registeredCapital());
|
||||
tenant.setRegisteredCapitalCurrency(request.registeredCapitalCurrency());
|
||||
tenant.setRegisteredAddress(request.registeredAddress());
|
||||
tenant.setInternalRemark(request.internalRemark());
|
||||
repository.saveTenant(tenant);
|
||||
return response(tenant);
|
||||
}
|
||||
@@ -194,7 +212,7 @@ public class TenantService {
|
||||
tenant.setContactPhone(request.contactPhone());
|
||||
tenant.setContactEmail(request.contactEmail());
|
||||
repository.saveTenant(tenant);
|
||||
return response(tenant);
|
||||
return merchantVisibleResponse(tenant);
|
||||
}
|
||||
|
||||
private boolean tenantIdEquals(IamTenant tenant, long id) {
|
||||
@@ -213,7 +231,44 @@ public class TenantService {
|
||||
return new TenantResponse(
|
||||
tenant.getId(), tenant.getTenantCode(), tenant.getTenantName(), tenant.getStatus(),
|
||||
tenant.getLegalName(), tenant.getUnifiedSocialCreditCode(), tenant.getContactName(),
|
||||
tenant.getContactPhone(), tenant.getContactEmail()
|
||||
tenant.getContactPhone(), tenant.getContactEmail(), tenant.getContactWechat(),
|
||||
tenant.getSalesOwnerName(), tenant.getPaymentTermDays(), tenant.getEnterpriseType(),
|
||||
tenant.getEstablishedDate(), tenant.getRegisteredCapital(), tenant.getRegisteredCapitalCurrency(),
|
||||
tenant.getRegisteredAddress(), tenant.getInternalRemark(), tenant.getCreatedAt(), tenant.getApprovedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private TenantResponse summaryResponse(IamTenant tenant) {
|
||||
TenantResponse response = response(tenant);
|
||||
return new TenantResponse(response.id(), response.tenantCode(), response.tenantName(), response.status(),
|
||||
response.legalName(), response.unifiedSocialCreditCode(), response.contactName(),
|
||||
maskPhone(response.contactPhone()), maskEmail(response.contactEmail()), response.contactWechat(),
|
||||
response.salesOwnerName(), response.paymentTermDays(), response.enterpriseType(),
|
||||
response.establishedDate(), response.registeredCapital(), response.registeredCapitalCurrency(),
|
||||
response.registeredAddress(), response.internalRemark(), response.createdAt(), response.approvedAt());
|
||||
}
|
||||
|
||||
private TenantResponse merchantVisibleResponse(IamTenant tenant) {
|
||||
TenantResponse response = response(tenant);
|
||||
return new TenantResponse(response.id(), response.tenantCode(), response.tenantName(), response.status(),
|
||||
response.legalName(), response.unifiedSocialCreditCode(), response.contactName(),
|
||||
response.contactPhone(), response.contactEmail(), response.contactWechat(), response.salesOwnerName(),
|
||||
response.paymentTermDays(), response.enterpriseType(), response.establishedDate(),
|
||||
response.registeredCapital(), response.registeredCapitalCurrency(), response.registeredAddress(),
|
||||
null, response.createdAt(), response.approvedAt());
|
||||
}
|
||||
|
||||
private String maskPhone(String phone) {
|
||||
if (phone == null || phone.isBlank()) return phone;
|
||||
if (phone.length() <= 4) return "****";
|
||||
int prefix = Math.min(3, phone.length() - 4);
|
||||
return phone.substring(0, prefix) + "****" + phone.substring(phone.length() - 4);
|
||||
}
|
||||
|
||||
private String maskEmail(String email) {
|
||||
if (email == null || email.isBlank()) return email;
|
||||
int separator = email.indexOf('@');
|
||||
if (separator <= 0) return "***";
|
||||
return email.substring(0, 1) + "***" + email.substring(separator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,15 @@ CREATE TABLE IF NOT EXISTS iam_tenant (
|
||||
contact_name VARCHAR(64) NULL COMMENT '企业联系人',
|
||||
contact_phone VARCHAR(32) NULL COMMENT '联系电话',
|
||||
contact_email VARCHAR(128) NULL COMMENT '联系邮箱',
|
||||
contact_wechat VARCHAR(64) NULL COMMENT '联系微信',
|
||||
sales_owner_name VARCHAR(64) NULL COMMENT '销售负责人姓名',
|
||||
payment_term_days SMALLINT UNSIGNED NULL COMMENT '账期天数',
|
||||
enterprise_type VARCHAR(64) NULL COMMENT '企业类型',
|
||||
established_date DATE NULL COMMENT '成立日期',
|
||||
registered_capital DECIMAL(18,2) NULL COMMENT '注册资本',
|
||||
registered_capital_currency CHAR(3) NULL COMMENT '注册资本币种 ISO 4217',
|
||||
registered_address VARCHAR(500) NULL COMMENT '注册地址文本',
|
||||
internal_remark VARCHAR(1000) NULL COMMENT '平台内部备注',
|
||||
approved_at DATETIME(3) NULL COMMENT '审核通过时间',
|
||||
approved_by BIGINT NULL COMMENT '审核人ID',
|
||||
reject_reason VARCHAR(500) NULL COMMENT '驳回原因',
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
-- 一期平台客户档案字段增量迁移。可在同一数据库重复执行。
|
||||
USE baiyue_iam;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
SET @ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'iam_tenant' AND column_name = 'contact_wechat'),
|
||||
'SELECT 1', 'ALTER TABLE iam_tenant ADD COLUMN contact_wechat VARCHAR(64) NULL COMMENT ''联系微信'' AFTER contact_email');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'iam_tenant' AND column_name = 'sales_owner_name'),
|
||||
'SELECT 1', 'ALTER TABLE iam_tenant ADD COLUMN sales_owner_name VARCHAR(64) NULL COMMENT ''销售负责人姓名'' AFTER contact_wechat');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'iam_tenant' AND column_name = 'payment_term_days'),
|
||||
'SELECT 1', 'ALTER TABLE iam_tenant ADD COLUMN payment_term_days SMALLINT UNSIGNED NULL COMMENT ''账期天数'' AFTER sales_owner_name');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'iam_tenant' AND column_name = 'enterprise_type'),
|
||||
'SELECT 1', 'ALTER TABLE iam_tenant ADD COLUMN enterprise_type VARCHAR(64) NULL COMMENT ''企业类型'' AFTER payment_term_days');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'iam_tenant' AND column_name = 'established_date'),
|
||||
'SELECT 1', 'ALTER TABLE iam_tenant ADD COLUMN established_date DATE NULL COMMENT ''成立日期'' AFTER enterprise_type');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'iam_tenant' AND column_name = 'registered_capital'),
|
||||
'SELECT 1', 'ALTER TABLE iam_tenant ADD COLUMN registered_capital DECIMAL(18,2) NULL COMMENT ''注册资本'' AFTER established_date');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'iam_tenant' AND column_name = 'registered_capital_currency'),
|
||||
'SELECT 1', 'ALTER TABLE iam_tenant ADD COLUMN registered_capital_currency CHAR(3) NULL COMMENT ''注册资本币种 ISO 4217'' AFTER registered_capital');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'iam_tenant' AND column_name = 'registered_address'),
|
||||
'SELECT 1', 'ALTER TABLE iam_tenant ADD COLUMN registered_address VARCHAR(500) NULL COMMENT ''注册地址文本'' AFTER registered_capital_currency');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'iam_tenant' AND column_name = 'internal_remark'),
|
||||
'SELECT 1', 'ALTER TABLE iam_tenant ADD COLUMN internal_remark VARCHAR(1000) NULL COMMENT ''平台内部备注'' AFTER registered_address');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@@ -22,6 +22,7 @@ INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name,
|
||||
sort_no, status, built_in, deleted)
|
||||
VALUES
|
||||
(5100014, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:view', '查看企业客户档案', 15, 'ACTIVE', 1, 0),
|
||||
(5100012, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:edit', '编辑企业客户', 20, 'ACTIVE', 1, 0),
|
||||
(5100013, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:status', '启停企业客户', 30, 'ACTIVE', 1, 0),
|
||||
(5101015, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:grant-role', '分配内部用户角色', 50, 'ACTIVE', 1, 0),
|
||||
@@ -50,3 +51,18 @@ WHERE tenant_id = 0 AND app_code = 'PLATFORM'
|
||||
'platform:counterparty:list', 'platform:counterparty:create', 'platform:counterparty:edit', 'platform:counterparty:status'
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
-- 详情查看从列表和审核权限中拆分;升级时保留既有运营角色的查询能力。
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT DISTINCT existing_grant.role_id, view_permission.id, existing_grant.tenant_id
|
||||
FROM iam_role_permission existing_grant
|
||||
JOIN iam_permission existing_permission
|
||||
ON existing_permission.id = existing_grant.permission_id
|
||||
JOIN iam_permission view_permission
|
||||
ON view_permission.tenant_id = 0
|
||||
AND view_permission.app_code = 'PLATFORM'
|
||||
AND view_permission.permission_code = 'platform:customer:view'
|
||||
WHERE existing_permission.tenant_id = 0
|
||||
AND existing_permission.app_code = 'PLATFORM'
|
||||
AND existing_permission.permission_code IN ('platform:customer:list', 'platform:customer:review')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Baiyue IAM API / 百悦身份与权限 API
|
||||
description: 业务成功和失败均使用 HTTP 200;仅响应体 code 为字符串 "200" 时表示成功,其他 code 表示失败。鉴权和协议层异常仍使用真实 HTTP 状态码。Tenant, member, role, menu, page, button and API permission management.
|
||||
description: Tenant, member, role, menu, page, button and API permission management. / 租户、成员、角色、菜单、页面、按钮和 API 权限管理。
|
||||
version: 1.0.1
|
||||
servers:
|
||||
- url: /
|
||||
@@ -21,7 +21,7 @@ paths:
|
||||
get:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: 查询企业客户列表
|
||||
description: 分页查询商户企业客户,需要 `platform:customer:list` 权限。
|
||||
description: 分页查询商户企业客户,需要 `platform:customer:list` 权限;联系电话和邮箱仅返回脱敏摘要。
|
||||
parameters:
|
||||
- {name: page, in: query, required: false, schema: {type: integer, minimum: 1, default: 1}}
|
||||
- {name: size, in: query, required: false, schema: {type: integer, minimum: 1, maximum: 200, default: 20}}
|
||||
@@ -33,8 +33,10 @@ 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 / 创建企业租户
|
||||
@@ -45,8 +47,10 @@ paths:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/RegisterRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'200': {$ref: '#/components/responses/TenantSuccess'}
|
||||
'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 资源]
|
||||
@@ -66,9 +70,11 @@ paths:
|
||||
get:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: 查询企业客户档案
|
||||
description: 需要独立的 `platform:customer:view` 权限,返回完整联系方式。
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'200': {$ref: '#/components/responses/TenantSuccess'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
put:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: 编辑企业客户档案
|
||||
@@ -78,8 +84,40 @@ paths:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/UpdateTenantRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'200': {$ref: '#/components/responses/TenantSuccess'}
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/api/platform/v1/iam/tenants/{tenantId}/addresses:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TenantId'}
|
||||
get:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: 查询企业客户地址
|
||||
description: 需要 `platform:customer:view` 权限;只读接口,不提供平台端地址写入。
|
||||
responses:
|
||||
'200':
|
||||
description: 企业客户地址查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/TenantAddressListApiResponse'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/api/platform/v1/iam/tenants/{tenantId}/qualifications:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TenantId'}
|
||||
get:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: 查询企业客户资质
|
||||
description: 需要 `platform:customer:view` 权限;只读查询资质元数据,不授予审核权限。
|
||||
responses:
|
||||
'200':
|
||||
description: 企业客户资质查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/TenantQualificationListApiResponse'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/api/platform/v1/iam/tenants/{tenantId}/status:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TenantId'}
|
||||
@@ -92,8 +130,11 @@ paths:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ChangeTenantStatusRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'200': {$ref: '#/components/responses/TenantSuccess'}
|
||||
'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 资源]
|
||||
@@ -130,6 +171,8 @@ 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 / 平台租户]
|
||||
@@ -145,6 +188,8 @@ 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 资源]
|
||||
@@ -167,7 +212,9 @@ 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'}
|
||||
@@ -181,7 +228,10 @@ 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'}
|
||||
@@ -195,7 +245,9 @@ 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'}
|
||||
@@ -209,7 +261,9 @@ 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'}
|
||||
@@ -228,7 +282,9 @@ 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 / 商户企业]
|
||||
@@ -238,6 +294,7 @@ 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 / 更新当前企业资料
|
||||
@@ -248,7 +305,9 @@ 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 / 商户企业]
|
||||
@@ -259,7 +318,7 @@ paths:
|
||||
post:
|
||||
tags: [Merchant Tenant / 商户企业]
|
||||
summary: Add enterprise member / 新增企业成员
|
||||
description: Attach an existing IAM user to the current tenant. / 将已有 IAM 用户加入当前企业。
|
||||
description: 新增基础资料需要 merchant:member:create;roleIds 非空时还必须具有 merchant:member:grant-role,否则返回 403/A0403。空数组表示不分配角色。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -267,7 +326,38 @@ 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/members/{membershipId}:
|
||||
parameters:
|
||||
- name: membershipId
|
||||
in: path
|
||||
required: true
|
||||
description: 当前租户内的成员关系 ID
|
||||
schema: {type: string, pattern: '^\d+$'}
|
||||
get:
|
||||
tags: [Merchant Tenant / 商户企业]
|
||||
summary: 查询成员详情及当前角色
|
||||
description: 返回当前租户成员资料,roleIds 为服务端实时查询的当前角色集合;跨租户成员按不存在处理。
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
put:
|
||||
tags: [Merchant Tenant / 商户企业]
|
||||
summary: 编辑成员资料及角色
|
||||
description: 基础资料编辑需要 merchant:member:edit。仅当 roleIds 与当前角色集合实际不同时额外要求 merchant:member:grant-role;传空数组清空角色也属于角色变更。角色集合未变化时不误拒绝。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/UpdateMemberRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/merchant/v1/iam/tenant/addresses:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
@@ -285,6 +375,7 @@ 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:
|
||||
@@ -313,7 +404,9 @@ 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]
|
||||
@@ -328,6 +421,7 @@ 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:
|
||||
@@ -349,7 +443,9 @@ 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]
|
||||
@@ -364,6 +460,7 @@ 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:
|
||||
@@ -380,7 +477,9 @@ 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: 删除未绑定的自定义权限资源
|
||||
@@ -388,6 +487,7 @@ 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'}
|
||||
@@ -402,7 +502,9 @@ 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:
|
||||
@@ -435,7 +537,7 @@ components:
|
||||
in: path
|
||||
required: true
|
||||
description: 企业客户租户 ID
|
||||
schema: {type: integer, format: int64, minimum: 1}
|
||||
schema: {type: string, pattern: '^\d+$'}
|
||||
PermissionId:
|
||||
name: permissionId
|
||||
in: path
|
||||
@@ -444,14 +546,19 @@ components:
|
||||
schema: {type: integer, format: int64}
|
||||
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'}}}}
|
||||
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: '200', description: 200 表示成功,其他值表示业务失败}
|
||||
code: {type: string, example: '00000'}
|
||||
message: {type: string, example: Success}
|
||||
data: {}
|
||||
requestId: {type: string}
|
||||
@@ -480,6 +587,12 @@ components:
|
||||
size: {type: integer, format: int64, minimum: 1}
|
||||
total: {type: integer, format: int64, minimum: 0}
|
||||
pages: {type: integer, format: int64, minimum: 0}
|
||||
TenantApiResponse:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/ApiResponse'}
|
||||
- type: object
|
||||
properties:
|
||||
data: {$ref: '#/components/schemas/TenantResponse'}
|
||||
InternalUserListApiResponse:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/ApiResponse'}
|
||||
@@ -496,7 +609,7 @@ components:
|
||||
type: object
|
||||
required: [id, tenantCode, tenantName, status]
|
||||
properties:
|
||||
id: {type: integer, format: int64}
|
||||
id: {type: string, pattern: '^\d+$', example: '190000000000000001'}
|
||||
tenantCode: {type: string}
|
||||
tenantName: {type: string}
|
||||
status: {type: string, enum: [PROFILE_REQUIRED, PENDING, ACTIVE, REJECTED, DISABLED]}
|
||||
@@ -505,6 +618,58 @@ components:
|
||||
contactName: {type: string}
|
||||
contactPhone: {type: string}
|
||||
contactEmail: {type: string, format: email}
|
||||
contactWechat: {type: string, maxLength: 64}
|
||||
salesOwnerName: {type: string, maxLength: 64}
|
||||
paymentTermDays: {type: integer, minimum: 0, maximum: 365}
|
||||
enterpriseType: {type: string, maxLength: 64}
|
||||
establishedDate: {type: string, format: date}
|
||||
registeredCapital: {type: number, format: decimal, minimum: 0}
|
||||
registeredCapitalCurrency: {type: string, pattern: '^[A-Z]{3}$', example: CNY}
|
||||
registeredAddress: {type: string, maxLength: 500}
|
||||
internalRemark: {type: string, maxLength: 1000, description: 仅平台端可见}
|
||||
createdAt: {type: string, format: date-time, description: 入驻时间}
|
||||
approvedAt: {type: string, format: date-time, description: 审批通过时间}
|
||||
TenantAddressListApiResponse:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/ApiResponse'}
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items: {$ref: '#/components/schemas/TenantAddress'}
|
||||
TenantQualificationListApiResponse:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/ApiResponse'}
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items: {$ref: '#/components/schemas/TenantQualification'}
|
||||
TenantAddress:
|
||||
type: object
|
||||
properties:
|
||||
id: {type: string, pattern: '^\d+$'}
|
||||
tenantId: {type: string, pattern: '^\d+$'}
|
||||
addressType: {type: string}
|
||||
countryCode: {type: string}
|
||||
province: {type: string}
|
||||
city: {type: string}
|
||||
district: {type: string}
|
||||
detail: {type: string}
|
||||
postalCode: {type: string}
|
||||
isDefault: {type: boolean}
|
||||
TenantQualification:
|
||||
type: object
|
||||
properties:
|
||||
id: {type: string, pattern: '^\d+$'}
|
||||
tenantId: {type: string, pattern: '^\d+$'}
|
||||
qualificationType: {type: string}
|
||||
certificateNo: {type: string}
|
||||
documentUrl: {type: string}
|
||||
validFrom: {type: string, format: date}
|
||||
validTo: {type: string, format: date}
|
||||
status: {type: string}
|
||||
reviewComment: {type: string}
|
||||
RegisterRequest:
|
||||
type: object
|
||||
required: [tenantName, legalName, unifiedSocialCreditCode, contactName, contactPhone]
|
||||
@@ -515,6 +680,15 @@ components:
|
||||
contactName: {type: string, example: 李四}
|
||||
contactPhone: {type: string, example: '13800138000'}
|
||||
contactEmail: {type: string, format: email, example: contact@example.com}
|
||||
contactWechat: {type: string, maxLength: 64}
|
||||
salesOwnerName: {type: string, maxLength: 64}
|
||||
paymentTermDays: {type: integer, minimum: 0, maximum: 365}
|
||||
enterpriseType: {type: string, maxLength: 64}
|
||||
establishedDate: {type: string, format: date}
|
||||
registeredCapital: {type: number, format: decimal, minimum: 0}
|
||||
registeredCapitalCurrency: {type: string, pattern: '^[A-Z]{3}$', example: CNY}
|
||||
registeredAddress: {type: string, maxLength: 500}
|
||||
internalRemark: {type: string, maxLength: 1000}
|
||||
UpdateProfileRequest:
|
||||
type: object
|
||||
required: [tenantName, legalName, contactName, contactPhone]
|
||||
@@ -533,6 +707,15 @@ components:
|
||||
contactName: {type: string}
|
||||
contactPhone: {type: string}
|
||||
contactEmail: {type: string, format: email}
|
||||
contactWechat: {type: string, maxLength: 64}
|
||||
salesOwnerName: {type: string, maxLength: 64}
|
||||
paymentTermDays: {type: integer, minimum: 0, maximum: 365}
|
||||
enterpriseType: {type: string, maxLength: 64}
|
||||
establishedDate: {type: string, format: date}
|
||||
registeredCapital: {type: number, format: decimal, minimum: 0}
|
||||
registeredCapitalCurrency: {type: string, pattern: '^[A-Z]{3}$'}
|
||||
registeredAddress: {type: string, maxLength: 500}
|
||||
internalRemark: {type: string, maxLength: 1000}
|
||||
ChangeTenantStatusRequest:
|
||||
type: object
|
||||
required: [status]
|
||||
@@ -541,7 +724,7 @@ components:
|
||||
ReviewRequest:
|
||||
type: object
|
||||
properties:
|
||||
comment: {type: string, maxLength: 500, description: Review comment or rejection reason / 审核意见或驳回原因}
|
||||
comment: {type: string, description: Review comment or rejection reason / 审核意见或驳回原因}
|
||||
CreateInternalUserRequest:
|
||||
type: object
|
||||
required: [displayName]
|
||||
@@ -603,9 +786,25 @@ components:
|
||||
displayName: {type: string, maxLength: 64}
|
||||
phone: {type: string, maxLength: 32}
|
||||
email: {type: string, format: email, maxLength: 128}
|
||||
memberType: {type: string, enum: [OWNER, ADMIN, EMPLOYEE]}
|
||||
memberType: {type: string, enum: [EMPLOYEE]}
|
||||
memberNo: {type: string}
|
||||
roleIds: {type: array, items: {type: integer, format: int64}}
|
||||
roleIds:
|
||||
type: array
|
||||
description: 初始角色 ID;非空时额外要求 merchant:member:grant-role
|
||||
items: {type: string, pattern: '^\d+$'}
|
||||
UpdateMemberRequest:
|
||||
type: object
|
||||
required: [displayName, memberType]
|
||||
properties:
|
||||
displayName: {type: string, maxLength: 64}
|
||||
phone: {type: string, maxLength: 32}
|
||||
email: {type: string, format: email, maxLength: 128}
|
||||
memberType: {type: string, enum: [EMPLOYEE]}
|
||||
memberNo: {type: string, maxLength: 64}
|
||||
roleIds:
|
||||
type: array
|
||||
description: 完整目标角色集合;空数组表示清空,集合变化时额外要求 merchant:member:grant-role
|
||||
items: {type: string, pattern: '^\d+$'}
|
||||
AddressRequest:
|
||||
type: object
|
||||
required: [addressType, detail]
|
||||
|
||||
+1
-3
@@ -1,7 +1,6 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -11,10 +10,9 @@ import java.lang.reflect.Method;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
@DisplayName("权限快照接口契约")
|
||||
class AuthorizationControllerContractTest {
|
||||
@Test
|
||||
void currentPermissionsEndpointDoesNotRequireRoleManagementPermission() throws Exception {
|
||||
void 当前权限接口不应依赖角色管理权限() throws Exception {
|
||||
Method method = AuthorizationController.class.getMethod("permissions", String.class,
|
||||
AuthenticatedUser.class);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
@@ -10,10 +9,9 @@ import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("IAM OpenAPI 契约")
|
||||
class IamOpenApiContractTest {
|
||||
@Test
|
||||
void openApiDeclaresPermissionPaginationAndInternalUserResponseSchema() throws IOException {
|
||||
void OpenApi应声明权限分页和内部用户的真实响应结构() throws IOException {
|
||||
String document;
|
||||
try (var stream = getClass().getResourceAsStream("/openapi/iam-v1.yaml")) {
|
||||
assertThat(stream).isNotNull();
|
||||
@@ -24,13 +22,29 @@ class IamOpenApiContractTest {
|
||||
.contains("/api/merchant/v1/iam/authorization/permissions:")
|
||||
.contains("PermissionCodesApiResponse:")
|
||||
.contains("TenantPageApiResponse:")
|
||||
.contains("TenantApiResponse:")
|
||||
.contains("TenantSuccess:")
|
||||
.contains("salesOwnerName:")
|
||||
.contains("paymentTermDays:")
|
||||
.contains("registeredCapitalCurrency:")
|
||||
.contains("createdAt: {type: string, format: date-time, description: 入驻时间}")
|
||||
.contains("id: {type: string, pattern: '^\\d+$'")
|
||||
.contains("/api/platform/v1/iam/tenants/{tenantId}/addresses:")
|
||||
.contains("/api/platform/v1/iam/tenants/{tenantId}/qualifications:")
|
||||
.contains("platform:customer:view")
|
||||
.contains("TenantAddressListApiResponse:")
|
||||
.contains("TenantQualificationListApiResponse:")
|
||||
.contains("InternalUserListApiResponse:")
|
||||
.contains("/api/platform/v1/iam/internal-users/{membershipId}/roles:")
|
||||
.contains("AssignInternalUserRolesRequest:")
|
||||
.contains("/api/merchant/v1/iam/members/{membershipId}:")
|
||||
.contains("仅当 roleIds 与当前角色集合实际不同时额外要求 merchant:member:grant-role")
|
||||
.contains("传空数组清空角色也属于角色变更")
|
||||
.contains("roleIds 为服务端实时查询的当前角色集合")
|
||||
.contains("UpdateMemberRequest:")
|
||||
.contains("membershipId: {type: integer, format: int64}")
|
||||
.contains("roleIds: {type: array, items: {type: integer, format: int64}}")
|
||||
.contains("example: '200'")
|
||||
.doesNotContain("'422': {$ref: '#/components/responses/UnprocessableEntity'}");
|
||||
.contains("'422': {$ref: '#/components/responses/UnprocessableEntity'}");
|
||||
|
||||
Map<String, Object> openApi = new Yaml().load(document);
|
||||
assertThat(openApi).containsEntry("openapi", "3.0.3");
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class PlatformCustomerResourceContractTest {
|
||||
|
||||
@Test
|
||||
void 平台地址和资质只读查询统一使用客户查看权限() throws Exception {
|
||||
assertReadOnlyViewContract("platformAddresses",
|
||||
"/api/platform/v1/iam/tenants/{tenantId}/addresses");
|
||||
assertReadOnlyViewContract("platformQualifications",
|
||||
"/api/platform/v1/iam/tenants/{tenantId}/qualifications");
|
||||
}
|
||||
|
||||
private void assertReadOnlyViewContract(String methodName, String path) throws Exception {
|
||||
Method method = ProfileResourceController.class.getMethod(
|
||||
methodName, long.class, AuthenticatedUser.class);
|
||||
assertEquals(path, method.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("hasAuthority('platform:customer:view')",
|
||||
method.getAnnotation(PreAuthorize.class).value());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -10,11 +9,10 @@ import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("平台内部用户接口契约")
|
||||
class PlatformInternalUserContractTest {
|
||||
|
||||
@Test
|
||||
void internalUserListRequiresListPermission() throws Exception {
|
||||
void 内部用户列表必须校验列表权限() throws Exception {
|
||||
Method method = ProfileResourceController.class.getMethod("internalUsers",
|
||||
com.baiyue.common.security.AuthenticatedUser.class);
|
||||
|
||||
@@ -25,7 +23,7 @@ class PlatformInternalUserContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformUserWriteEndpointsAddressMembershipId() {
|
||||
void 平台用户写接口以成员关系主键寻址() {
|
||||
assertMapping("updateInternalUser", "/api/platform/v1/iam/internal-users/{membershipId:\\d+}");
|
||||
assertMapping("changeInternalUserStatus",
|
||||
"/api/platform/v1/iam/internal-users/{membershipId:\\d+}/status");
|
||||
@@ -36,7 +34,7 @@ class PlatformInternalUserContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void tenantProfileReadRequiresViewPermission() throws Exception {
|
||||
void 企业资料查看必须校验查看权限() throws Exception {
|
||||
Method method = MerchantTenantController.class.getMethod("profile",
|
||||
com.baiyue.common.security.AuthenticatedUser.class);
|
||||
assertEquals("hasAuthority('merchant:profile:view')",
|
||||
|
||||
+7
-16
@@ -5,9 +5,6 @@ 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;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -21,10 +18,9 @@ import java.util.stream.Collectors;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@DisplayName("平台客户接口契约")
|
||||
class PlatformTenantControllerContractTest {
|
||||
@Test
|
||||
void merchantPageEndpointDeclaresPermissionPaginationConstraintsAndUnifiedBusinessStatus() throws Exception {
|
||||
void 客户分页接口声明权限分页约束和错误状态() throws Exception {
|
||||
Method method = PlatformTenantController.class.getMethod("page", long.class, long.class,
|
||||
String.class, String.class, AuthenticatedUser.class);
|
||||
|
||||
@@ -40,11 +36,11 @@ 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", "401", "403"), statuses);
|
||||
assertEquals(Set.of("200", "400", "401", "403", "422"), statuses);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pendingMerchantListRequiresMerchantApprovalPermission() throws Exception {
|
||||
void 待审列表必须校验客户审核权限() throws Exception {
|
||||
Method method = PlatformTenantController.class.getMethod("reviews", AuthenticatedUser.class);
|
||||
|
||||
assertEquals("hasAuthority('platform:customer:review')",
|
||||
@@ -52,16 +48,11 @@ class PlatformTenantControllerContractTest {
|
||||
}
|
||||
|
||||
@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");
|
||||
void 客户详情必须使用独立查看权限() throws Exception {
|
||||
Method method = PlatformTenantController.class.getMethod("tenant", long.class, AuthenticatedUser.class);
|
||||
|
||||
assertNotNull(annotation(approve, 1, Valid.class));
|
||||
assertNotNull(annotation(reject, 1, Valid.class));
|
||||
assertEquals(500, comment.getAnnotation(Size.class).max());
|
||||
assertEquals("hasAuthority('platform:customer:view')",
|
||||
method.getAnnotation(PreAuthorize.class).value());
|
||||
}
|
||||
|
||||
private <T extends Annotation> T annotation(Method method, int parameter, Class<T> type) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
@@ -11,7 +10,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@DisplayName("企业资质上传接口契约")
|
||||
class QualificationUploadContractTest {
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CustomerProfileMigrationContractTest {
|
||||
@Test
|
||||
void 客户档案迁移覆盖全部字段并通过元数据判断保持可重复执行() throws IOException {
|
||||
String sql;
|
||||
try (var stream = getClass().getResourceAsStream("/db/phase1-customer-profile.sql")) {
|
||||
assertThat(stream).isNotNull();
|
||||
sql = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
assertThat(sql)
|
||||
.contains("USE baiyue_iam;")
|
||||
.contains("SET NAMES utf8mb4;")
|
||||
.contains("information_schema.columns")
|
||||
.contains("contact_wechat")
|
||||
.contains("sales_owner_name")
|
||||
.contains("payment_term_days")
|
||||
.contains("enterprise_type")
|
||||
.contains("established_date")
|
||||
.contains("registered_capital")
|
||||
.contains("registered_capital_currency")
|
||||
.contains("registered_address")
|
||||
.contains("internal_remark");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 统一测试环境入口包含客户字段查看权限和执行后核验() throws IOException {
|
||||
Path repositoryRoot = Path.of(System.getProperty("maven.multiModuleProjectDirectory", ".."));
|
||||
Path migration = repositoryRoot.resolve("deploy/sql/2026-07-31一期回归测试环境增量迁移.sql");
|
||||
Path readme = repositoryRoot.resolve("deploy/sql/README.md");
|
||||
|
||||
assertThat(migration).exists();
|
||||
assertThat(readme).exists();
|
||||
String sql = Files.readString(migration, StandardCharsets.UTF_8);
|
||||
String instructions = Files.readString(readme, StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CALL regression_assert_table('iam_tenant')")
|
||||
.contains("CREATE PROCEDURE regression_add_iam_column")
|
||||
.contains("platform:customer:view")
|
||||
.contains("'IAM客户档案字段' AS check_item")
|
||||
.contains("9 AS expected_count")
|
||||
.contains("44 AS expected_count")
|
||||
.contains("MIGRATION_COMPLETED");
|
||||
assertThat(instructions)
|
||||
.contains("phase1-platform-master-data-and-permission-management.sql")
|
||||
.contains("phase1-customer-profile.sql")
|
||||
.contains("测试环境只执行统一入口");
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.dto.AuthorizationDtos.SnapshotRequest;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.iam.mapper.AuthorizationQueryMapper;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
@@ -17,7 +16,6 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@DisplayName("权限快照服务")
|
||||
class AuthorizationSnapshotServiceTest {
|
||||
@Test
|
||||
void loadsRolesAndPermissionsForActiveMerchantIdentity() {
|
||||
@@ -84,7 +82,7 @@ class AuthorizationSnapshotServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyRoleAndPermissionQueriesDoNotTriggerServerError() {
|
||||
void 空角色和权限查询结果不应触发五百错误() {
|
||||
AuthorizationQueryMapper mapper = mock(AuthorizationQueryMapper.class);
|
||||
when(mapper.countActiveIdentity(42L, 0L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 0L, 9L, "PLATFORM")).thenReturn(null);
|
||||
|
||||
@@ -11,7 +11,6 @@ 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.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
@@ -21,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@DisplayName("商户成员服务")
|
||||
class MemberServiceTest {
|
||||
private final MemberRepository repository = mock(MemberRepository.class);
|
||||
private final IamRoleMapper roleMapper = mock(IamRoleMapper.class);
|
||||
@@ -39,7 +37,7 @@ class MemberServiceTest {
|
||||
when(repository.findRoleIds(100L, 20L)).thenReturn(List.of(50L));
|
||||
}
|
||||
|
||||
@Test void memberDetailsReturnContactInformationAndExistingRoles() {
|
||||
@Test void 成员详情返回联系方式和已有角色() {
|
||||
user.setPhone("13800138000"); user.setEmail("member@example.com");
|
||||
MemberResponse result = service.detail(100L, 20L);
|
||||
assertEquals("测试成员", result.displayName());
|
||||
@@ -48,12 +46,12 @@ class MemberServiceTest {
|
||||
verifyNoInteractions(auth);
|
||||
}
|
||||
|
||||
@Test void crossTenantLookupIsRejected() {
|
||||
@Test void 跨租户查询被拒绝() {
|
||||
when(repository.findByTenantAndId(200L, 20L)).thenReturn(null);
|
||||
assertThrows(BizException.class, () -> service.detail(200L, 20L));
|
||||
}
|
||||
|
||||
@Test void tenantOwnerCannotBeDisabled() {
|
||||
@Test void 企业所有者不可停用() {
|
||||
member.setMemberType("OWNER");
|
||||
BizException exception = assertThrows(BizException.class, () -> service.changeStatus(100L, 20L,
|
||||
new ChangeMemberStatusRequest("DISABLED"), 20L));
|
||||
@@ -61,7 +59,7 @@ class MemberServiceTest {
|
||||
verify(auth, never()).status(anyString(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test void disablingMemberSynchronizesAuthAccountAndPersistsLocalStatus() {
|
||||
@Test void 停用成员同步认证账号并保存本地状态() {
|
||||
when(auth.status(anyString(), eq(40L), any())).thenReturn(ApiResponse.success(null));
|
||||
MemberResponse result = service.changeStatus(100L, 20L,
|
||||
new ChangeMemberStatusRequest("DISABLED"), 99L);
|
||||
@@ -70,28 +68,14 @@ class MemberServiceTest {
|
||||
verify(repository).saveUser(user);
|
||||
}
|
||||
|
||||
@Test void authServiceFailureDoesNotPersistLocalStatus() {
|
||||
@Test void 认证服务失败时不得写入本地状态() {
|
||||
when(auth.status(anyString(), eq(40L), any())).thenThrow(new RuntimeException("网络失败"));
|
||||
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,
|
||||
assertThrows(RuntimeException.class, () -> service.changeStatus(100L, 20L,
|
||||
new ChangeMemberStatusRequest("DISABLED"), 99L));
|
||||
|
||||
assertEquals(ResultCode.NOT_FOUND, exception.getResultCode());
|
||||
assertEquals("认证账号不存在", exception.getMessage());
|
||||
verify(repository, never()).save(member);
|
||||
}
|
||||
|
||||
@Test void tenantOwnerCannotBeDowngradedOrHaveRolesChangedThroughMemberUpdate() {
|
||||
@Test void 企业所有者不能通过成员编辑接口降级或变更角色() {
|
||||
member.setMemberType("OWNER");
|
||||
UpdateMemberRequest request = new UpdateMemberRequest("所有者", null, null,
|
||||
"EMPLOYEE", null, List.of());
|
||||
@@ -101,7 +85,7 @@ class MemberServiceTest {
|
||||
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
|
||||
}
|
||||
|
||||
@Test void createWithRolesRequiresGrantRolePermission() {
|
||||
@Test void 新增成员携带非空角色但无授权权限时返回403且不创建账号() {
|
||||
CreateMemberRequest request = new CreateMemberRequest("member01", "Password1", "成员",
|
||||
null, null, "EMPLOYEE", null, List.of(50L));
|
||||
|
||||
@@ -113,7 +97,7 @@ class MemberServiceTest {
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test void profileOnlyUpdateAllowsUnchangedRolesWithoutGrantPermission() {
|
||||
@Test void 编辑资料且角色集合未变化时不要求角色授权权限() {
|
||||
IamRole role = activeMerchantRole(50L, 100L);
|
||||
when(roleMapper.selectById(50L)).thenReturn(role);
|
||||
UpdateMemberRequest request = new UpdateMemberRequest("新姓名", "13800138000", null,
|
||||
@@ -126,7 +110,7 @@ class MemberServiceTest {
|
||||
verify(repository).replaceRoles(100L, 20L, List.of(50L));
|
||||
}
|
||||
|
||||
@Test void roleChangeRequiresGrantRolePermission() {
|
||||
@Test void 编辑成员变更角色但无授权权限时返回403() {
|
||||
UpdateMemberRequest request = new UpdateMemberRequest("测试成员", null, null,
|
||||
"EMPLOYEE", null, List.of(60L));
|
||||
|
||||
@@ -138,7 +122,7 @@ class MemberServiceTest {
|
||||
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
|
||||
}
|
||||
|
||||
@Test void clearingRolesAlsoRequiresGrantRolePermission() {
|
||||
@Test void 空集合清空已有角色同样要求角色授权权限() {
|
||||
UpdateMemberRequest request = new UpdateMemberRequest("测试成员", null, null,
|
||||
"EMPLOYEE", null, List.of());
|
||||
|
||||
@@ -149,7 +133,7 @@ class MemberServiceTest {
|
||||
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
|
||||
}
|
||||
|
||||
@Test void grantRolePermissionAllowsClearingRoles() {
|
||||
@Test void 具有角色授权权限时允许清空已有角色() {
|
||||
UpdateMemberRequest request = new UpdateMemberRequest("测试成员", null, null,
|
||||
"EMPLOYEE", null, List.of());
|
||||
|
||||
@@ -159,7 +143,7 @@ class MemberServiceTest {
|
||||
verify(repository).replaceRoles(100L, 20L, List.of());
|
||||
}
|
||||
|
||||
@Test void crossTenantUpdateReturnsNotFoundBeforeReadingRoles() {
|
||||
@Test void 跨租户编辑先按目标租户查成员并返回404() {
|
||||
when(repository.findByTenantAndId(200L, 20L)).thenReturn(null);
|
||||
UpdateMemberRequest request = new UpdateMemberRequest("测试成员", null, null,
|
||||
"EMPLOYEE", null, List.of(50L));
|
||||
@@ -178,7 +162,7 @@ class MemberServiceTest {
|
||||
return role;
|
||||
}
|
||||
|
||||
@Test void tenantOwnerPasswordCannotBeResetByMemberAdministrator() {
|
||||
@Test void 企业所有者密码不能被成员管理员重置() {
|
||||
member.setMemberType("OWNER");
|
||||
|
||||
assertThrows(BizException.class, () -> service.resetPassword(100L, 20L,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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.*;
|
||||
@@ -11,7 +10,6 @@ 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.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
@@ -21,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@DisplayName("平台内部用户服务")
|
||||
class PlatformInternalUserServiceTest {
|
||||
private final MemberRepository repository = mock(MemberRepository.class);
|
||||
private final IamRoleMapper roleMapper = mock(IamRoleMapper.class);
|
||||
@@ -48,7 +45,7 @@ class PlatformInternalUserServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void listReturnsMembershipIdContactInformationAndCurrentRoles() {
|
||||
void 列表返回成员主键联系方式和当前角色() {
|
||||
when(repository.findByTenant(0L)).thenReturn(List.of(membership));
|
||||
|
||||
InternalUserResponse result = service.list().get(0);
|
||||
@@ -60,30 +57,7 @@ class PlatformInternalUserServiceTest {
|
||||
}
|
||||
|
||||
@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() {
|
||||
void 新建平台用户在Auth创建平台账号并写入IAM成员() {
|
||||
IamRole role = new IamRole();
|
||||
role.setId(40L); role.setTenantId(0L); role.setAppCode("PLATFORM"); role.setStatus("ACTIVE");
|
||||
when(roleMapper.selectById(40L)).thenReturn(role);
|
||||
@@ -107,7 +81,7 @@ class PlatformInternalUserServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void localCreationFailureCompensatesAuthAccountWithoutRecordingPassword() {
|
||||
void 本地创建失败会补偿认证账号且不记录密码() {
|
||||
when(auth.create(anyString(), any())).thenReturn(ApiResponse.success(
|
||||
new AuthAccountClient.AccountSummary(99L, "new.operator", "新运营", "ACTIVE")));
|
||||
when(auth.compensate(anyString(), anyLong())).thenReturn(ApiResponse.success(null));
|
||||
@@ -121,7 +95,7 @@ class PlatformInternalUserServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void cannotDisableCurrentlyAuthenticatedPlatformMember() {
|
||||
void 不能停用当前登录的平台成员() {
|
||||
assertThrows(BizException.class, () -> service.changeStatus(30L,
|
||||
new ChangeInternalUserStatusRequest("DISABLED"), 30L));
|
||||
|
||||
@@ -129,7 +103,7 @@ class PlatformInternalUserServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void localStatusPersistenceFailureRestoresAuthAndAvoidsFurtherRemoteLookupsAfterSuccess() {
|
||||
void 状态本地写入失败时恢复Auth且成功后不再远程查询() {
|
||||
when(auth.status(anyString(), eq(10L), any())).thenReturn(ApiResponse.success(null));
|
||||
doThrow(new RuntimeException("IAM 写入失败")).when(repository).saveUser(user);
|
||||
|
||||
@@ -144,7 +118,7 @@ class PlatformInternalUserServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusCompensationRestoresOriginalAuthStatusInsteadOfOldIamStatus() {
|
||||
void 状态补偿应恢复Auth原状态而不是IAM旧状态() {
|
||||
when(auth.summary("internal-token", 10L)).thenReturn(ApiResponse.success(
|
||||
new AuthAccountClient.AccountSummary(10L, "operator", "平台运营", "DISABLED")));
|
||||
when(auth.status(anyString(), eq(10L), any())).thenReturn(ApiResponse.success(null));
|
||||
@@ -160,7 +134,7 @@ class PlatformInternalUserServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetPasswordDelegatesOnlyToAuthWithoutWritingIam() {
|
||||
void 重置密码只转交Auth不写入IAM() {
|
||||
when(auth.reset(anyString(), eq(10L), any())).thenReturn(ApiResponse.success(null));
|
||||
|
||||
service.resetPassword(30L, new ResetInternalUserPasswordRequest("ResetPass1"));
|
||||
@@ -171,7 +145,7 @@ class PlatformInternalUserServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void roleAssignmentAcceptsEnabledPlatformRolesOnlyAndReturnsLatestView() {
|
||||
void 角色分配只接受平台已启用角色并返回最新回显() {
|
||||
IamRole role = new IamRole();
|
||||
role.setId(41L); role.setTenantId(0L); role.setAppCode("PLATFORM"); role.setStatus("ACTIVE");
|
||||
when(roleMapper.selectById(41L)).thenReturn(role);
|
||||
@@ -184,7 +158,7 @@ class PlatformInternalUserServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void preventsAssigningMerchantRolesToPlatformUser() {
|
||||
void 禁止给平台用户分配商户角色() {
|
||||
IamRole merchantRole = new IamRole();
|
||||
merchantRole.setId(42L); merchantRole.setTenantId(10001L);
|
||||
merchantRole.setAppCode("MERCHANT"); merchantRole.setStatus("ACTIVE");
|
||||
|
||||
@@ -16,7 +16,6 @@ import com.baiyue.iam.entity.IamRole;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
import com.baiyue.iam.repository.RbacRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -32,7 +31,6 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("角色与权限服务")
|
||||
class RbacServiceTest {
|
||||
@Mock private RbacRepository repository;
|
||||
@Mock private MemberRepository memberRepository;
|
||||
@@ -46,7 +44,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void roleListReturnsCurrentPermissionsForSafeView() {
|
||||
void 角色列表返回当前权限用于安全回显() {
|
||||
IamRole role = role(11L, 100L, "MERCHANT", "CUSTOM");
|
||||
when(repository.findRoles(100L, "MERCHANT")).thenReturn(List.of(role));
|
||||
when(repository.findRolePermissionIds(100L, 11L)).thenReturn(Set.of(21L, 22L));
|
||||
@@ -58,7 +56,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void permissionListPrefersCurrentTenantAndRemovesDuplicateGlobalTemplatesWithSameCode() {
|
||||
void 权限列表同码时优先当前租户并去除全局模板重复项() {
|
||||
IamPermission global = permission(21L, 0L, "merchant:member:list");
|
||||
IamPermission tenant = permission(22L, 100L, "merchant:member:list");
|
||||
IamPermission fallback = permission(23L, 0L, "merchant:member:status");
|
||||
@@ -73,7 +71,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void crossTenantRoleAccessIsRejected() {
|
||||
void 跨租户角色访问被拒绝() {
|
||||
when(repository.findRole(11L, 200L, "MERCHANT")).thenReturn(null);
|
||||
|
||||
BizException exception = assertThrows(BizException.class,
|
||||
@@ -83,7 +81,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void systemRoleCannotBeDeleted() {
|
||||
void 系统角色禁止删除() {
|
||||
when(repository.findRole(11L, 0L, "PLATFORM"))
|
||||
.thenReturn(role(11L, 0L, "PLATFORM", "SYSTEM"));
|
||||
|
||||
@@ -95,7 +93,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void customRoleAssignedToMembersCannotBeDeleted() {
|
||||
void 已分配成员的自定义角色禁止删除() {
|
||||
when(repository.findRole(11L, 100L, "MERCHANT"))
|
||||
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
|
||||
when(repository.isRoleAssigned(100L, 11L)).thenReturn(true);
|
||||
@@ -108,7 +106,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyPermissionSetExplicitlyClearsAuthorization() {
|
||||
void 空权限集合可以明确清空授权() {
|
||||
when(repository.findRole(11L, 100L, "MERCHANT"))
|
||||
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
|
||||
|
||||
@@ -118,7 +116,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyRoleSetExplicitlyClearsMemberAuthorization() {
|
||||
void 空角色集合可以明确清空成员授权() {
|
||||
IamMembership membership = new IamMembership();
|
||||
membership.setId(31L);
|
||||
membership.setTenantId(100L);
|
||||
@@ -130,7 +128,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void memberRoleViewReturnsRolesFromCurrentApplicationDomainOnly() {
|
||||
void 成员角色回显只返回当前应用域角色() {
|
||||
IamMembership membership = new IamMembership();
|
||||
membership.setId(31L);
|
||||
membership.setTenantId(100L);
|
||||
@@ -146,7 +144,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateRoleCodeWithinTenantAndApplicationReturnsConflict() {
|
||||
void 同一租户同一应用角色编码重复时返回冲突() {
|
||||
when(repository.existsRoleCode(100L, "MERCHANT", "ORDER_ADMIN", null)).thenReturn(true);
|
||||
|
||||
BizException exception = assertThrows(BizException.class, () -> service.createRole(
|
||||
@@ -157,7 +155,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void disabledRoleCannotBeAssignedToMember() {
|
||||
void 停用角色不得继续分配给成员() {
|
||||
IamMembership membership = new IamMembership();
|
||||
membership.setId(31L);
|
||||
membership.setTenantId(100L);
|
||||
@@ -174,7 +172,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void customRoleCanBeUpdatedAndDisabledWhileRetainingPermissionView() {
|
||||
void 自定义角色可以编辑和停用并保持权限回显() {
|
||||
IamRole role = role(11L, 100L, "MERCHANT", "CUSTOM");
|
||||
when(repository.findRole(11L, 100L, "MERCHANT")).thenReturn(role);
|
||||
when(repository.findRolePermissionIds(100L, 11L)).thenReturn(Set.of(21L));
|
||||
@@ -191,7 +189,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unreferencedCustomRoleCanBeDeletedSafely() {
|
||||
void 未使用的自定义角色可以安全删除() {
|
||||
when(repository.findRole(11L, 100L, "MERCHANT"))
|
||||
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
|
||||
when(repository.isRoleAssigned(100L, 11L)).thenReturn(false);
|
||||
@@ -202,7 +200,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void tenantOwnerRolesCannotBeChangedThroughRegularMemberAuthorizationEndpoint() {
|
||||
void 企业所有者角色不能通过普通成员授权接口修改() {
|
||||
IamMembership owner = new IamMembership();
|
||||
owner.setId(31L);
|
||||
owner.setTenantId(100L);
|
||||
@@ -217,7 +215,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void systemRolePermissionsCannotBeChangedThroughRegularAuthorizationEndpoint() {
|
||||
void 系统角色权限不能通过普通授权接口修改() {
|
||||
when(repository.findRole(11L, 0L, "PLATFORM"))
|
||||
.thenReturn(role(11L, 0L, "PLATFORM", "SYSTEM"));
|
||||
|
||||
@@ -229,7 +227,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void tenantOwnerSystemRoleCannotBeAssignedToRegularMember() {
|
||||
void 企业所有者系统角色不能分配给普通成员() {
|
||||
IamMembership member = new IamMembership();
|
||||
member.setId(31L);
|
||||
member.setTenantId(100L);
|
||||
@@ -247,7 +245,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicatePermissionCodeReturnsStableConflictWithoutDatabaseWrite() {
|
||||
void 权限编码重复时返回稳定冲突且不写数据库() {
|
||||
when(repository.existsPermissionCode(100L, "MERCHANT", "merchant:role:test"))
|
||||
.thenReturn(true);
|
||||
CreatePermissionRequest request = new CreatePermissionRequest(
|
||||
@@ -261,7 +259,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void builtInSystemPermissionCannotBeUpdatedDisabledOrDeleted() {
|
||||
void 系统预置权限不允许编辑启停或删除() {
|
||||
IamPermission builtIn = permission(21L, 0L, "platform:permission:list");
|
||||
builtIn.setAppCode("PLATFORM");
|
||||
builtIn.setBuiltIn(true);
|
||||
@@ -276,7 +274,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void customPermissionCanBeUpdatedAndToggledWithoutChangingCode() {
|
||||
void 自定义权限能编辑和启停但保持编码不变() {
|
||||
IamPermission custom = permission(21L, 100L, "merchant:custom:edit");
|
||||
when(repository.findOwnedPermission(21L, 100L, "MERCHANT")).thenReturn(custom);
|
||||
|
||||
@@ -292,7 +290,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void permissionResourceWithChildrenOrRoleBindingsRejectsDeletion() {
|
||||
void 带子节点或角色绑定的权限资源拒绝删除() {
|
||||
IamPermission custom = permission(21L, 100L, "merchant:custom:edit");
|
||||
when(repository.findOwnedPermission(21L, 100L, "MERCHANT")).thenReturn(custom);
|
||||
when(repository.hasPermissionChildren(100L, "MERCHANT", 21L)).thenReturn(true);
|
||||
@@ -310,7 +308,7 @@ class RbacServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unreferencedCustomPermissionCanBeDeleted() {
|
||||
void 无引用的自定义权限可以删除() {
|
||||
IamPermission custom = permission(21L, 100L, "merchant:custom:edit");
|
||||
when(repository.findOwnedPermission(21L, 100L, "MERCHANT")).thenReturn(custom);
|
||||
when(repository.hasPermissionChildren(100L, "MERCHANT", 21L)).thenReturn(false);
|
||||
|
||||
@@ -2,27 +2,26 @@ 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;
|
||||
import com.baiyue.iam.repository.ProfileResourceRepository;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
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;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
|
||||
@DisplayName("企业租户服务")
|
||||
class TenantServiceTest {
|
||||
|
||||
@Test
|
||||
@@ -41,6 +40,11 @@ class TenantServiceTest {
|
||||
tenant.setContactName("Alice");
|
||||
tenant.setContactPhone("13800138000");
|
||||
tenant.setContactEmail("alice@example.com");
|
||||
tenant.setSalesOwnerName("Sales Zhang");
|
||||
tenant.setPaymentTermDays(30);
|
||||
tenant.setEnterpriseType("LIMITED_COMPANY");
|
||||
tenant.setCreatedAt(LocalDateTime.of(2026, 7, 1, 9, 30));
|
||||
tenant.setApprovedAt(LocalDateTime.of(2026, 7, 2, 10, 0));
|
||||
when(repository.pageMerchants(1, 10, "Example", "ACTIVE"))
|
||||
.thenReturn(new PageResult<>(List.of(tenant), 1, 10, 1, 1));
|
||||
|
||||
@@ -49,6 +53,12 @@ class TenantServiceTest {
|
||||
assertEquals(1, result.total());
|
||||
assertEquals("TEN10001", result.records().get(0).tenantCode());
|
||||
assertEquals("Alice", result.records().get(0).contactName());
|
||||
assertEquals("Sales Zhang", result.records().get(0).salesOwnerName());
|
||||
assertEquals(30, result.records().get(0).paymentTermDays());
|
||||
assertEquals(LocalDateTime.of(2026, 7, 1, 9, 30), result.records().get(0).createdAt());
|
||||
assertEquals(LocalDateTime.of(2026, 7, 2, 10, 0), result.records().get(0).approvedAt());
|
||||
assertEquals("138****8000", result.records().get(0).contactPhone());
|
||||
assertEquals("a***@example.com", result.records().get(0).contactEmail());
|
||||
verify(repository).pageMerchants(1, 10, "Example", "ACTIVE");
|
||||
}
|
||||
|
||||
@@ -61,10 +71,21 @@ class TenantServiceTest {
|
||||
when(repository.findTenant(10001L)).thenReturn(tenant);
|
||||
|
||||
var updated = service.updateMerchant(10001L,
|
||||
new UpdateTenantRequest("New name", "New legal", "Alice", "13900139000", "alice@example.com"));
|
||||
new UpdateTenantRequest("New name", "New legal", "Alice", "13900139000", "alice@example.com",
|
||||
"alice_wechat", "Sales Zhang", 45, "LIMITED_COMPANY", LocalDate.of(2020, 1, 2),
|
||||
new BigDecimal("5000000.00"), "CNY", "深圳市南山区示例路 1 号", "一期重点客户"));
|
||||
var disabled = service.changeMerchantStatus(10001L, new ChangeTenantStatusRequest("DISABLED"));
|
||||
|
||||
assertEquals("New name", updated.tenantName());
|
||||
assertEquals("alice_wechat", updated.contactWechat());
|
||||
assertEquals("Sales Zhang", updated.salesOwnerName());
|
||||
assertEquals(45, updated.paymentTermDays());
|
||||
assertEquals("LIMITED_COMPANY", updated.enterpriseType());
|
||||
assertEquals(LocalDate.of(2020, 1, 2), updated.establishedDate());
|
||||
assertEquals(new BigDecimal("5000000.00"), updated.registeredCapital());
|
||||
assertEquals("CNY", updated.registeredCapitalCurrency());
|
||||
assertEquals("深圳市南山区示例路 1 号", updated.registeredAddress());
|
||||
assertEquals("一期重点客户", updated.internalRemark());
|
||||
assertEquals("DISABLED", disabled.status());
|
||||
verify(repository, org.mockito.Mockito.times(2)).saveTenant(tenant);
|
||||
}
|
||||
@@ -83,7 +104,7 @@ class TenantServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeTenantStillRequiresApprovedQualificationToPlaceOrder() {
|
||||
void 企业激活不等于具备下单资格仍需已审核资质() {
|
||||
TenantRepository repository = mock(TenantRepository.class);
|
||||
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
||||
TenantService service = new TenantService();
|
||||
@@ -100,59 +121,6 @@ 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