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]