This commit is contained in:
2026-08-03 13:24:06 +08:00
45 changed files with 1479 additions and 403 deletions
@@ -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,17 @@ 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.PositiveOrZero;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
@@ -22,11 +29,15 @@ public final class TenantDtos {
@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,
@Size(max = 64) String servicePackage,
@PositiveOrZero Integer creditDays,
@Size(max = 64) String salesPerson,
@Size(max = 255) String businessAddress,
@Size(max = 500) String remark
@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 / 企业资料更新")
@@ -46,18 +57,16 @@ public final class TenantDtos {
@NotBlank String contactName,
@NotBlank String contactPhone,
@Email String contactEmail,
@Size(max = 64) String servicePackage,
@PositiveOrZero Integer creditDays,
@Size(max = 64) String salesPerson,
@Size(max = 255) String businessAddress,
@Size(max = 500) String remark
) {
public UpdateTenantRequest(String tenantName, String legalName, String contactName,
String contactPhone, String contactEmail) {
this(tenantName, legalName, contactName, contactPhone, contactEmail,
null, null, null, null, null);
}
}
@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")
public record ChangeTenantStatusRequest(
@@ -66,14 +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 servicePackage,
Integer creditDays, String salesPerson, String businessAddress,
String remark, LocalDateTime createdAt) { }
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,11 +27,15 @@ public class IamTenant extends BaseEntity {
private String contactName;
private String contactPhone;
private String contactEmail;
private String servicePackage;
private Integer creditDays;
private String salesPerson;
private String businessAddress;
private String remark;
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;
/**
@@ -48,22 +46,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>()
@@ -83,7 +66,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,11 +59,15 @@ public class TenantService {
tenant.setContactName(request.contactName());
tenant.setContactPhone(request.contactPhone());
tenant.setContactEmail(request.contactEmail());
tenant.setServicePackage(request.servicePackage());
tenant.setCreditDays(request.creditDays() == null ? 0 : request.creditDays());
tenant.setSalesPerson(request.salesPerson());
tenant.setBusinessAddress(request.businessAddress());
tenant.setRemark(request.remark());
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();
@@ -82,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());
}
@@ -104,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) {
@@ -149,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);
}
/** 平台端查询客户档案:仅能查商户租户,不暴露平台租户。 */
@@ -167,11 +171,15 @@ public class TenantService {
tenant.setContactName(request.contactName());
tenant.setContactPhone(request.contactPhone());
tenant.setContactEmail(request.contactEmail());
tenant.setServicePackage(request.servicePackage());
tenant.setCreditDays(request.creditDays() == null ? 0 : request.creditDays());
tenant.setSalesPerson(request.salesPerson());
tenant.setBusinessAddress(request.businessAddress());
tenant.setRemark(request.remark());
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);
}
@@ -204,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) {
@@ -223,9 +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.getServicePackage(),
tenant.getCreditDays(), tenant.getSalesPerson(), tenant.getBusinessAddress(),
tenant.getRemark(), tenant.getCreatedAt()
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);
}
}
+9 -5
View File
@@ -16,11 +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 '联系邮箱',
service_package VARCHAR(64) NULL COMMENT '客户服务套餐',
credit_days INT NOT NULL DEFAULT 0 COMMENT '账期天数',
sales_person VARCHAR(64) NULL COMMENT '负责销售人员',
business_address VARCHAR(255) NULL COMMENT '企业经营地址',
remark VARCHAR(500) 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),
@@ -51,3 +52,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);
+211 -28
View File
@@ -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/users:
get:
tags: [IAM Resources / IAM 资源]
@@ -234,7 +279,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'}
@@ -248,7 +295,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'}
@@ -262,7 +312,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'}
@@ -276,7 +328,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'}
@@ -295,7 +349,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 / 商户企业]
@@ -305,6 +361,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 / 更新当前企业资料
@@ -315,7 +372,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 / 商户企业]
@@ -326,7 +385,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:
@@ -334,7 +393,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 资源]
@@ -352,6 +442,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:
@@ -380,7 +471,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]
@@ -395,6 +488,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:
@@ -416,7 +510,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]
@@ -431,6 +527,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:
@@ -447,7 +544,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: 删除未绑定的自定义权限资源
@@ -455,6 +554,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'}
@@ -469,7 +569,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:
@@ -502,7 +604,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
@@ -511,14 +613,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}
@@ -547,6 +654,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'}
@@ -563,7 +676,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]}
@@ -572,12 +685,58 @@ components:
contactName: {type: string}
contactPhone: {type: string}
contactEmail: {type: string, format: email}
servicePackage: {type: string, maxLength: 64}
creditDays: {type: integer, minimum: 0}
salesPerson: {type: string, maxLength: 64}
businessAddress: {type: string, maxLength: 255}
remark: {type: string, maxLength: 500}
createdAt: {type: string, format: date-time}
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]
@@ -588,11 +747,15 @@ components:
contactName: {type: string, example: 李四}
contactPhone: {type: string, example: '13800138000'}
contactEmail: {type: string, format: email, example: contact@example.com}
servicePackage: {type: string, maxLength: 64, example: 标准版}
creditDays: {type: integer, minimum: 0, example: 30}
salesPerson: {type: string, maxLength: 64, example: 李四}
businessAddress: {type: string, maxLength: 255}
remark: {type: string, maxLength: 500}
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]
@@ -611,11 +774,15 @@ components:
contactName: {type: string}
contactPhone: {type: string}
contactEmail: {type: string, format: email}
servicePackage: {type: string, maxLength: 64}
creditDays: {type: integer, minimum: 0}
salesPerson: {type: string, maxLength: 64}
businessAddress: {type: string, maxLength: 255}
remark: {type: string, maxLength: 500}
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]
@@ -707,9 +874,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,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;
@@ -11,11 +10,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);
@@ -26,7 +24,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");
@@ -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);