This commit is contained in:
2026-08-04 14:54:57 +08:00
29 changed files with 664 additions and 178 deletions
@@ -72,7 +72,7 @@ public class MerchantTenantController {
@PostMapping("/tenant/qualifications/submit-review")
@PreAuthorize("hasAuthority('merchant:qualification:upload')")
@Operation(summary = "提交当前企业资质审核", description = "校验四类必填资质齐全后创建审核单。上传或替换文件不会自动送审。")
@Operation(summary = "提交当前企业资质审核", description = "校验营业执照和法人身份证齐全后创建审核单;货代资质证书和银行开户许可证选填。上传或替换文件不会自动送审。")
public ApiResponse<TenantResponse> submitQualificationReview(@AuthenticationPrincipal AuthenticatedUser user) {
return ApiResponse.success(tenantService.submitMerchantReview(TenantGuard.tenant(user, "MERCHANT")));
}
@@ -1,6 +1,7 @@
package com.baiyue.iam.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.iam.dto.PlatformUserManagementDtos.*;
import com.baiyue.iam.service.PlatformUserManagementService;
@@ -8,15 +9,18 @@ import com.baiyue.iam.service.TenantGuard;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/** 原型“用户管理”的平台统一入口,覆盖平台与企业租户账号。 */
/** Admin 内部用户管理入口,只允许维护平台租户账号。 */
@RestController
@RequestMapping("/api/platform/v1/iam/users")
@Validated
@Tag(name = "Platform user administration / 平台用户管理")
public class PlatformUserManagementController {
private final PlatformUserManagementService service;
@@ -27,15 +31,20 @@ public class PlatformUserManagementController {
@GetMapping
@PreAuthorize("hasAuthority('platform:user:list')")
@Operation(summary = "统一查询平台及企业租户用户")
public ApiResponse<List<UserResponse>> list(@AuthenticationPrincipal AuthenticatedUser user) {
@Operation(summary = "分页查询平台内部用户")
public ApiResponse<PageResult<UserResponse>> list(
@RequestParam(defaultValue = "1") @Min(1) long page,
@RequestParam(defaultValue = "20") @Min(1) @Max(200) long size,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) @Pattern(regexp = "ACTIVE|DISABLED") String status,
@AuthenticationPrincipal AuthenticatedUser user) {
TenantGuard.tenant(user, "PLATFORM");
return ApiResponse.success(service.list());
return ApiResponse.success(service.page(page, size, keyword, status));
}
@GetMapping("/options")
@PreAuthorize("hasAuthority('platform:user:list')")
@Operation(summary = "查询用户管理可选租户和角色")
@Operation(summary = "查询平台内部用户可选平台角色")
public ApiResponse<UserOptions> options(@AuthenticationPrincipal AuthenticatedUser user) {
TenantGuard.tenant(user, "PLATFORM");
return ApiResponse.success(service.options());
@@ -43,7 +52,7 @@ public class PlatformUserManagementController {
@PostMapping
@PreAuthorize("hasAuthority('platform:user:create') and hasAuthority('platform:user:grant-role')")
@Operation(summary = "新增平台或企业租户用户")
@Operation(summary = "新增平台内部用户")
public ApiResponse<UserResponse> create(@Valid @RequestBody CreateUserRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
TenantGuard.tenant(user, "PLATFORM");
@@ -52,7 +61,7 @@ public class PlatformUserManagementController {
@PutMapping("/{membershipId:\\d+}")
@PreAuthorize("hasAuthority('platform:user:edit')")
@Operation(summary = "编辑用户资料、租户、角色、状态和密码")
@Operation(summary = "编辑平台内部用户资料和角色")
public ApiResponse<UserResponse> update(@PathVariable Long membershipId,
@Valid @RequestBody UpdateUserRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
@@ -9,7 +9,7 @@ import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.List;
/** 平台统一用户管理契约,覆盖平台账号与企业租户账号。 */
/** Admin 平台内部用户管理契约;tenantId 保留用于兼容既有请求,但只能为 0。 */
public final class PlatformUserManagementDtos {
private PlatformUserManagementDtos() { }
@@ -1,5 +1,6 @@
package com.baiyue.iam.repository;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.iam.entity.IamMembership;
import com.baiyue.iam.entity.IamMembershipRole;
import com.baiyue.iam.entity.IamUser;
@@ -8,7 +9,9 @@ import com.baiyue.iam.mapper.IamMembershipRoleMapper;
import com.baiyue.iam.mapper.IamUserMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import java.util.List;
@@ -30,9 +33,27 @@ public class MemberRepository {
return membershipMapper.selectList(new LambdaQueryWrapper<IamMembership>()
.eq(IamMembership::getTenantId, tenantId).orderByDesc(IamMembership::getJoinedAt));
}
public List<IamMembership> findAll() {
return membershipMapper.selectList(new LambdaQueryWrapper<IamMembership>()
.orderByDesc(IamMembership::getJoinedAt));
public PageResult<IamMembership> pageByTenant(long tenantId, long page, long size,
String keyword, String status) {
LambdaQueryWrapper<IamMembership> wrapper = new LambdaQueryWrapper<IamMembership>()
.eq(IamMembership::getTenantId, tenantId)
.eq(StringUtils.hasText(status), IamMembership::getStatus, status)
.orderByDesc(IamMembership::getJoinedAt);
if (StringUtils.hasText(keyword)) {
String normalizedKeyword = keyword.trim();
List<Long> matchingUserIds = userMapper.selectList(new LambdaQueryWrapper<IamUser>()
.select(IamUser::getId)
.in(IamUser::getUserType, "INTERNAL", "PLATFORM")
.and(query -> query.like(IamUser::getDisplayName, normalizedKeyword)
.or().like(IamUser::getPhone, normalizedKeyword)
.or().like(IamUser::getEmail, normalizedKeyword)))
.stream().map(IamUser::getId).toList();
wrapper.and(query -> {
query.like(IamMembership::getMemberNo, normalizedKeyword);
if (!matchingUserIds.isEmpty()) query.or().in(IamMembership::getUserId, matchingUserIds);
});
}
return PageResult.from(membershipMapper.selectPage(new Page<>(page, size), wrapper));
}
public IamMembership findById(Long id) { return membershipMapper.selectById(id); }
public IamMembership findByTenantAndId(long tenantId, Long id) {
@@ -2,17 +2,16 @@ package com.baiyue.iam.service;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
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.PlatformUserManagementDtos.*;
import com.baiyue.iam.entity.IamMembership;
import com.baiyue.iam.entity.IamRole;
import com.baiyue.iam.entity.IamTenant;
import com.baiyue.iam.entity.IamUser;
import com.baiyue.iam.mapper.IamRoleMapper;
import com.baiyue.iam.repository.MemberRepository;
import com.baiyue.iam.repository.TenantRepository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import feign.FeignException;
@@ -31,45 +30,41 @@ import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/** 平台管理员跨租户维护登录用户;凭据仍只由 auth-service 保存。 */
/** 平台管理员维护内部登录用户;凭据仍只由 auth-service 保存。 */
@Service
public class PlatformUserManagementService {
private static final long PLATFORM_TENANT_ID = 0L;
private static final Logger log = LoggerFactory.getLogger(PlatformUserManagementService.class);
private final MemberRepository members;
private final TenantRepository tenants;
private final IamRoleMapper roleMapper;
private final AuthAccountClient authClient;
private final String internalToken;
public PlatformUserManagementService(MemberRepository members, TenantRepository tenants,
IamRoleMapper roleMapper, AuthAccountClient authClient,
public PlatformUserManagementService(MemberRepository members, IamRoleMapper roleMapper,
AuthAccountClient authClient,
@Value("${baiyue.security.internal-service-token}") String internalToken) {
this.members = members;
this.tenants = tenants;
this.roleMapper = roleMapper;
this.authClient = authClient;
this.internalToken = internalToken;
}
@Transactional(readOnly = true)
public List<UserResponse> list() {
Map<Long, IamTenant> tenantMap = tenants.listMerchants().stream()
.collect(Collectors.toMap(IamTenant::getId, Function.identity()));
Map<Long, IamRole> roleMap = allRoles().stream()
public PageResult<UserResponse> page(long page, long size, String keyword, String status) {
PageResult<IamMembership> source = members.pageByTenant(
PLATFORM_TENANT_ID, page, size, keyword, status);
Map<Long, IamRole> roleMap = platformRoles().stream()
.collect(Collectors.toMap(IamRole::getId, Function.identity()));
return members.findAll().stream().map(membership -> response(membership, tenantMap, roleMap)).toList();
return new PageResult<>(source.records().stream()
.map(membership -> response(membership, roleMap)).toList(),
source.page(), source.size(), source.total(), source.pages());
}
@Transactional(readOnly = true)
public UserOptions options() {
List<TenantOption> tenantOptions = new java.util.ArrayList<>();
tenantOptions.add(new TenantOption(PLATFORM_TENANT_ID, "平台", "ACTIVE"));
tenants.listMerchants().stream()
.map(value -> new TenantOption(value.getId(), value.getTenantName(), value.getStatus()))
.forEach(tenantOptions::add);
List<RoleOption> roleOptions = allRoles().stream()
List<TenantOption> tenantOptions = List.of(new TenantOption(PLATFORM_TENANT_ID, "平台", "ACTIVE"));
List<RoleOption> roleOptions = platformRoles().stream()
.map(value -> new RoleOption(value.getId(), value.getTenantId(), value.getRoleName(), value.getStatus()))
.toList();
return new UserOptions(List.copyOf(tenantOptions), roleOptions);
@@ -80,7 +75,7 @@ public class PlatformUserManagementService {
resourceId = "#request.username", description = "平台新增用户", captureAfter = false)
public UserResponse create(CreateUserRequest request) {
long tenantId = request.tenantId();
validateTenant(tenantId);
requirePlatformTenant(tenantId);
List<Long> roleIds = validateRoles(tenantId, request.roleIds());
long authUserId = IdWorker.getId();
long userId = IdWorker.getId();
@@ -122,11 +117,11 @@ public class PlatformUserManagementService {
@BusinessAudit(event = "PLATFORM_USER_UPDATE", resourceType = "MEMBERSHIP",
resourceId = "#membershipId", description = "平台编辑用户", captureAfter = false)
public UserResponse update(Long membershipId, UpdateUserRequest request, Set<String> operatorPermissions) {
IamMembership membership = required(membershipId);
IamMembership membership = requiredPlatformMembership(membershipId);
IamUser user = requiredUser(membership.getUserId());
long oldTenantId = membership.getTenantId();
long newTenantId = request.tenantId();
validateTenant(newTenantId);
requirePlatformTenant(newTenantId);
List<Long> roleIds = validateRoles(newTenantId, request.roleIds());
List<Long> oldRoleIds = members.findRoleIds(oldTenantId, membershipId);
if (isProtectedPlatformAdmin(membership, oldRoleIds)
@@ -170,7 +165,7 @@ public class PlatformUserManagementService {
@BusinessAudit(event = "PLATFORM_USER_STATUS_CHANGE", resourceType = "MEMBERSHIP",
resourceId = "#membershipId", description = "平台启停用户", captureAfter = false)
public UserResponse changeStatus(Long membershipId, ChangeStatusRequest request, Long operatorMembershipId) {
IamMembership membership = required(membershipId);
IamMembership membership = requiredPlatformMembership(membershipId);
if (membershipId.equals(operatorMembershipId) && "DISABLED".equals(request.status())) {
throw new BizException(ResultCode.BAD_REQUEST, "不能停用当前登录用户");
}
@@ -200,39 +195,35 @@ public class PlatformUserManagementService {
@BusinessAudit(event = "PLATFORM_USER_PASSWORD_RESET", resourceType = "MEMBERSHIP",
resourceId = "#membershipId", description = "平台重置用户密码", captureAfter = false)
public void resetPassword(Long membershipId, ResetPasswordRequest request) {
IamUser user = requiredUser(required(membershipId).getUserId());
IamUser user = requiredUser(requiredPlatformMembership(membershipId).getUserId());
call(() -> authClient.reset(internalToken, user.getAuthUserId(),
new AuthAccountClient.PasswordRequest(request.newPassword())));
}
private UserResponse response(IamMembership membership) {
Map<Long, IamTenant> tenantMap = tenants.listMerchants().stream()
.collect(Collectors.toMap(IamTenant::getId, Function.identity()));
Map<Long, IamRole> roleMap = allRoles().stream()
Map<Long, IamRole> roleMap = platformRoles().stream()
.collect(Collectors.toMap(IamRole::getId, Function.identity()));
return response(membership, tenantMap, roleMap);
return response(membership, roleMap);
}
private UserResponse response(IamMembership membership, Map<Long, IamTenant> tenantMap,
Map<Long, IamRole> roleMap) {
private UserResponse response(IamMembership membership, Map<Long, IamRole> roleMap) {
IamUser user = requiredUser(membership.getUserId());
AuthAccountClient.AccountSummary account = account(user.getAuthUserId());
List<Long> roleIds = members.findRoleIds(membership.getTenantId(), membership.getId());
List<String> roleNames = roleIds.stream().map(roleMap::get).filter(Objects::nonNull)
.map(IamRole::getRoleName).toList();
String tenantName = membership.getTenantId() == PLATFORM_TENANT_ID ? "平台"
: tenantMap.containsKey(membership.getTenantId())
? tenantMap.get(membership.getTenantId()).getTenantName() : "未知租户";
String tenantName = "平台";
return new UserResponse(membership.getId(), user.getId(), user.getAuthUserId(), account.username(),
user.getDisplayName(), user.getPhone(), user.getEmail(), membership.getTenantId(), tenantName,
membership.getMemberType(), membership.getMemberNo(), membership.getStatus(), roleIds,
roleNames, account.lastLoginAt(), isProtectedPlatformAdmin(membership, roleIds));
}
private List<IamRole> allRoles() {
private List<IamRole> platformRoles() {
return roleMapper.selectList(new LambdaQueryWrapper<IamRole>()
.in(IamRole::getAppCode, "PLATFORM", "MERCHANT")
.orderByAsc(IamRole::getTenantId).orderByAsc(IamRole::getRoleName));
.eq(IamRole::getTenantId, PLATFORM_TENANT_ID)
.eq(IamRole::getAppCode, "PLATFORM")
.orderByAsc(IamRole::getRoleName));
}
private List<Long> validateRoles(long tenantId, List<Long> requested) {
@@ -257,11 +248,9 @@ public class PlatformUserManagementService {
.anyMatch(role -> "PLATFORM_ADMIN".equals(role.getRoleCode()));
}
private void validateTenant(long tenantId) {
if (tenantId == PLATFORM_TENANT_ID) return;
IamTenant tenant = tenants.findTenant(tenantId);
if (tenant == null || !"MERCHANT".equals(tenant.getTenantType()) || !"ACTIVE".equals(tenant.getStatus())) {
throw new BizException(ResultCode.BAD_REQUEST, "只能选择正常状态的企业租户");
private void requirePlatformTenant(long tenantId) {
if (tenantId != PLATFORM_TENANT_ID) {
throw new BizException(ResultCode.FORBIDDEN, "内部用户只能属于平台租户");
}
}
@@ -271,6 +260,14 @@ public class PlatformUserManagementService {
return membership;
}
private IamMembership requiredPlatformMembership(Long membershipId) {
IamMembership membership = required(membershipId);
if (membership.getTenantId() != PLATFORM_TENANT_ID) {
throw new BizException(ResultCode.NOT_FOUND, "平台内部用户不存在");
}
return membership;
}
private IamUser requiredUser(Long userId) {
IamUser user = members.findUser(userId);
if (user == null) throw new BizException(ResultCode.NOT_FOUND, "用户档案不存在");
@@ -296,7 +293,7 @@ public class PlatformUserManagementService {
try {
call(() -> authClient.compensate(internalToken, authUserId));
} catch (RuntimeException compensationFailure) {
log.error("统一用户创建失败且认证账号补偿失败,authUserId={}", authUserId, compensationFailure);
log.error("内部用户创建失败且认证账号补偿失败,authUserId={}", authUserId, compensationFailure);
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,
"用户创建未完成,认证账号补偿失败,请按 authUserId=" + authUserId + " 修复一致性");
}
@@ -311,7 +308,7 @@ public class PlatformUserManagementService {
if (profileChanged) call(() -> authClient.profile(internalToken, authUserId,
new AuthAccountClient.ProfileRequest(oldAccount.displayName())));
} catch (RuntimeException compensationFailure) {
log.error("统一用户编辑失败且认证账号补偿失败,authUserId={}", authUserId, compensationFailure);
log.error("内部用户编辑失败且认证账号补偿失败,authUserId={}", authUserId, compensationFailure);
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,
"用户编辑未完成,认证账号补偿失败,请按 authUserId=" + authUserId + " 修复一致性");
}
@@ -35,7 +35,7 @@ import java.util.Set;
@Service
public class TenantService {
private static final Set<String> REQUIRED_QUALIFICATION_TYPES = Set.of(
"BUSINESS_LICENSE", "LEGAL_PERSON_ID", "FREIGHT_FORWARDER_CERTIFICATE", "BANK_ACCOUNT_LICENSE");
"BUSINESS_LICENSE", "LEGAL_PERSON_ID");
@Resource
private TenantRepository repository;
@Resource
@@ -273,7 +273,7 @@ public class TenantService {
var qualifications = profileRepository.findQualifications(tenantId);
if (!hasAllPendingQualifications(qualifications)) {
throw new BizException(ResultCode.BAD_REQUEST,
"Business license, legal-person ID, freight-forwarder certificate and bank account license are required / 营业执照、法人身份证、货代资质证书和银行开户许可证均为必填资质");
"Business license and legal-person ID are required / 营业执照和法人身份证为必填资质");
}
IamTenantReview review = new IamTenantReview();
review.setId(IdWorker.getId()); review.setTenantId(tenantId);
+53 -10
View File
@@ -210,13 +210,22 @@ paths:
/api/platform/v1/iam/users:
get:
tags: [IAM Resources / IAM 资源]
summary: 统一查询平台及企业租户用户
summary: 分页查询平台内部用户
parameters:
- {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
- {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 200, default: 20}}
- {name: keyword, in: query, schema: {type: string}, description: 匹配姓名、手机号、邮箱或成员编号}
- {name: status, in: query, schema: {type: string, enum: [ACTIVE, DISABLED]}}
responses:
'200': {$ref: '#/components/responses/Success'}
'200':
description: 仅返回 tenant_id=0 的平台内部用户分页
content:
application/json:
schema: {$ref: '#/components/schemas/PlatformManagedUserPageApiResponse'}
'403': {$ref: '#/components/responses/Forbidden'}
post:
tags: [IAM Resources / IAM 资源]
summary: 新增平台或企业租户用户
summary: 新增平台内部用户
requestBody:
required: true
content:
@@ -228,7 +237,7 @@ paths:
/api/platform/v1/iam/users/options:
get:
tags: [IAM Resources / IAM 资源]
summary: 查询用户管理可选租户和角色
summary: 查询平台租户和 PLATFORM 角色选项
responses:
'200': {$ref: '#/components/responses/Success'}
'403': {$ref: '#/components/responses/Forbidden'}
@@ -237,7 +246,7 @@ paths:
- {$ref: '#/components/parameters/MembershipId'}
put:
tags: [IAM Resources / IAM 资源]
summary: 编辑用户资料、所属租户及角色
summary: 编辑平台内部用户资料及角色
requestBody:
required: true
content:
@@ -251,7 +260,7 @@ paths:
- {$ref: '#/components/parameters/MembershipId'}
put:
tags: [IAM Resources / IAM 资源]
summary: 启用或停用平台及企业租户用户
summary: 启用或停用平台内部用户
requestBody:
required: true
content:
@@ -265,7 +274,7 @@ paths:
- {$ref: '#/components/parameters/MembershipId'}
put:
tags: [IAM Resources / IAM 资源]
summary: 重置平台及企业租户用户密码
summary: 重置平台内部用户密码
requestBody:
required: true
content:
@@ -472,7 +481,7 @@ paths:
post:
tags: [IAM Resources / IAM 资源]
summary: Submit enterprise qualifications for review / 提交企业资质审核
description: Uploading or replacing a qualification keeps the tenant in draft state. This endpoint creates the review only after all four required qualification types have been provided. / 上传或替换资质不会自动送审;四类必填资质齐全后由此接口创建审核单。
description: Uploading or replacing a qualification keeps the tenant in draft state. This endpoint creates the review after the business license and legal-person ID have been provided; the freight-forwarder certificate and bank account license are optional. / 上传或替换资质不会自动送审;营业执照和法人身份证齐全后由此接口创建审核单,货代资质证书和银行开户许可证选填。
responses:
'200': {$ref: '#/components/responses/TenantSuccess'}
'400': {$ref: '#/components/responses/BadRequest'}
@@ -748,6 +757,20 @@ components:
- type: object
properties:
data: {$ref: '#/components/schemas/InternalUserResponse'}
PlatformManagedUserPageApiResponse:
allOf:
- {$ref: '#/components/schemas/ApiResponse'}
- type: object
properties:
data:
type: object
required: [records, page, size, total, pages]
properties:
records: {type: array, items: {$ref: '#/components/schemas/PlatformManagedUserResponse'}}
page: {type: integer, minimum: 1}
size: {type: integer, minimum: 1}
total: {type: integer, minimum: 0}
pages: {type: integer, minimum: 0}
TenantResponse:
type: object
required: [id, tenantCode, tenantName, status, servicePackage]
@@ -883,7 +906,7 @@ components:
displayName: {type: string, maxLength: 64}
phone: {type: string, pattern: '^1\\d{10}$'}
email: {type: string, format: email, maxLength: 128}
tenantId: {type: integer, format: int64}
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台租户 0}
roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}}
password: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true}
status: {type: string, enum: [ACTIVE, DISABLED]}
@@ -894,7 +917,7 @@ components:
displayName: {type: string, maxLength: 64}
phone: {type: string, pattern: '^1\\d{10}$'}
email: {type: string, format: email, maxLength: 128}
tenantId: {type: integer, format: int64}
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台租户 0}
roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}}
CreateInternalUserRequest:
type: object
@@ -948,6 +971,26 @@ components:
memberNo: {type: string}
status: {type: string, enum: [ACTIVE, DISABLED]}
roleIds: {type: array, items: {type: integer, format: int64}}
PlatformManagedUserResponse:
type: object
required: [membershipId, userId, authUserId, username, displayName, tenantId, tenantName, memberType, status, roleIds, roleNames, protectedAccount]
properties:
membershipId: {type: string, pattern: '^\d+$'}
userId: {type: string, pattern: '^\d+$'}
authUserId: {type: string, pattern: '^\d+$'}
username: {type: string}
displayName: {type: string}
phone: {type: string}
email: {type: string, format: email}
tenantId: {type: string, enum: ['0']}
tenantName: {type: string, enum: [平台]}
memberType: {type: string, enum: [EMPLOYEE]}
memberNo: {type: string}
status: {type: string, enum: [ACTIVE, DISABLED]}
roleIds: {type: array, items: {type: string, pattern: '^\d+$'}}
roleNames: {type: array, items: {type: string}}
lastLoginAt: {type: string, format: date-time, nullable: true}
protectedAccount: {type: boolean}
CreateMemberRequest:
type: object
required: [username, password, displayName, memberType]