优化
This commit is contained in:
@@ -4,6 +4,8 @@ import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** auth-service 内部账号管理客户端。 */
|
||||
@FeignClient(name = "auth-service", contextId = "authAccountClient",
|
||||
url = "${baiyue.clients.auth.url:}", path = "/internal/auth/accounts")
|
||||
@@ -18,18 +20,30 @@ public interface AuthAccountClient {
|
||||
@PathVariable Long id, @RequestBody ProfileRequest request);
|
||||
@PutMapping("/{id}/password") ApiResponse<Void> reset(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable Long id, @RequestBody PasswordRequest request);
|
||||
@PutMapping("/{id}/scope") ApiResponse<Void> scope(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable Long id, @RequestBody ScopeRequest request);
|
||||
@DeleteMapping("/{id}/compensation") ApiResponse<Void> compensate(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable Long id);
|
||||
|
||||
record CreateAccountRequest(Long accountId, String username, String password, String displayName,
|
||||
Long tenantId, Long membershipId, String appCode) {
|
||||
Long tenantId, Long membershipId, String appCode, String accountStatus) {
|
||||
public CreateAccountRequest(Long accountId, String username, String password, String displayName,
|
||||
Long tenantId, Long membershipId) {
|
||||
this(accountId, username, password, displayName, tenantId, membershipId, "MERCHANT");
|
||||
this(accountId, username, password, displayName, tenantId, membershipId, "MERCHANT", "ACTIVE");
|
||||
}
|
||||
public CreateAccountRequest(Long accountId, String username, String password, String displayName,
|
||||
Long tenantId, Long membershipId, String appCode) {
|
||||
this(accountId, username, password, displayName, tenantId, membershipId, appCode, "ACTIVE");
|
||||
}
|
||||
}
|
||||
record AccountSummary(Long accountId, String username, String displayName, String status,
|
||||
LocalDateTime lastLoginAt, Long tenantId, Long membershipId, String appCode) {
|
||||
public AccountSummary(Long accountId, String username, String displayName, String status) {
|
||||
this(accountId, username, displayName, status, null, null, null, null);
|
||||
}
|
||||
}
|
||||
record AccountSummary(Long accountId, String username, String displayName, String status) { }
|
||||
record StatusRequest(String status) { }
|
||||
record ProfileRequest(String displayName) { }
|
||||
record PasswordRequest(String newPassword) { }
|
||||
record ScopeRequest(Long tenantId, Long membershipId, String appCode) { }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.iam.dto.PlatformUserManagementDtos.*;
|
||||
import com.baiyue.iam.service.PlatformUserManagementService;
|
||||
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 org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 原型“用户管理”的平台统一入口,覆盖平台与企业租户账号。 */
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/iam/users")
|
||||
@Tag(name = "Platform user administration / 平台用户管理")
|
||||
public class PlatformUserManagementController {
|
||||
private final PlatformUserManagementService service;
|
||||
|
||||
public PlatformUserManagementController(PlatformUserManagementService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:user:list')")
|
||||
@Operation(summary = "统一查询平台及企业租户用户")
|
||||
public ApiResponse<List<UserResponse>> list(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.list());
|
||||
}
|
||||
|
||||
@GetMapping("/options")
|
||||
@PreAuthorize("hasAuthority('platform:user:list')")
|
||||
@Operation(summary = "查询用户管理可选租户和角色")
|
||||
public ApiResponse<UserOptions> options(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.options());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('platform:user:create') and hasAuthority('platform:user:grant-role')")
|
||||
@Operation(summary = "新增平台或企业租户用户")
|
||||
public ApiResponse<UserResponse> create(@Valid @RequestBody CreateUserRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.create(request));
|
||||
}
|
||||
|
||||
@PutMapping("/{membershipId:\\d+}")
|
||||
@PreAuthorize("hasAuthority('platform:user:edit')")
|
||||
@Operation(summary = "编辑用户资料、租户、角色、状态和密码")
|
||||
public ApiResponse<UserResponse> update(@PathVariable Long membershipId,
|
||||
@Valid @RequestBody UpdateUserRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.update(membershipId, request, user.permissions()));
|
||||
}
|
||||
|
||||
@PutMapping("/{membershipId:\\d+}/status")
|
||||
@PreAuthorize("hasAuthority('platform:user:status')")
|
||||
@Operation(summary = "启用或停用用户")
|
||||
public ApiResponse<UserResponse> status(@PathVariable Long membershipId,
|
||||
@Valid @RequestBody ChangeStatusRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.changeStatus(membershipId, request, user.membershipId()));
|
||||
}
|
||||
|
||||
@PutMapping("/{membershipId:\\d+}/password")
|
||||
@PreAuthorize("hasAuthority('platform:user:reset-password')")
|
||||
@Operation(summary = "重置用户密码")
|
||||
public ApiResponse<Void> password(@PathVariable Long membershipId,
|
||||
@Valid @RequestBody ResetPasswordRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
service.resetPassword(membershipId, request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.baiyue.iam.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 平台统一用户管理契约,覆盖平台账号与企业租户账号。 */
|
||||
public final class PlatformUserManagementDtos {
|
||||
private PlatformUserManagementDtos() { }
|
||||
|
||||
public record CreateUserRequest(
|
||||
@NotBlank @Pattern(regexp = "^[A-Za-z0-9_.@-]{4,64}$") String username,
|
||||
@NotBlank @Size(max = 64) String displayName,
|
||||
@NotBlank @Pattern(regexp = "^1\\d{10}$", message = "手机号格式不正确") String phone,
|
||||
@Email @Size(max = 128) String email,
|
||||
@NotNull Long tenantId,
|
||||
@NotNull List<Long> roleIds,
|
||||
@NotBlank @Size(min = 8, max = 64)
|
||||
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password,
|
||||
@NotBlank @Pattern(regexp = "ACTIVE|DISABLED") String status) { }
|
||||
|
||||
public record UpdateUserRequest(
|
||||
@NotBlank @Size(max = 64) String displayName,
|
||||
@NotBlank @Pattern(regexp = "^1\\d{10}$", message = "手机号格式不正确") String phone,
|
||||
@Email @Size(max = 128) String email,
|
||||
@NotNull Long tenantId,
|
||||
@NotNull List<Long> roleIds) { }
|
||||
|
||||
public record ChangeStatusRequest(@NotBlank @Pattern(regexp = "ACTIVE|DISABLED") String status) { }
|
||||
public record ResetPasswordRequest(@NotBlank @Size(min = 8, max = 64)
|
||||
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字")
|
||||
String newPassword) { }
|
||||
|
||||
public record UserResponse(Long membershipId, Long userId, Long authUserId, String username,
|
||||
String displayName, String phone, String email, Long tenantId,
|
||||
String tenantName, String memberType, String memberNo, String status,
|
||||
List<Long> roleIds, List<String> roleNames, LocalDateTime lastLoginAt,
|
||||
boolean protectedAccount) { }
|
||||
|
||||
public record TenantOption(Long id, String name, String status) { }
|
||||
public record RoleOption(Long id, Long tenantId, String name, String status) { }
|
||||
public record UserOptions(List<TenantOption> tenants, List<RoleOption> roles) { }
|
||||
}
|
||||
@@ -51,7 +51,13 @@ public final class TenantDtos {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "TenantStatusChangeRequest", description = "Platform controlled merchant tenant status")
|
||||
public record ChangeTenantStatusRequest(
|
||||
|
||||
@@ -30,6 +30,10 @@ 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 IamMembership findById(Long id) { return membershipMapper.selectById(id); }
|
||||
public IamMembership findByTenantAndId(long tenantId, Long id) {
|
||||
return membershipMapper.selectOne(new LambdaQueryWrapper<IamMembership>()
|
||||
|
||||
@@ -33,6 +33,11 @@ public class TenantRepository {
|
||||
}
|
||||
|
||||
public IamTenant findTenant(Long tenantId) { return tenantMapper.selectById(tenantId); }
|
||||
public List<IamTenant> listMerchants() {
|
||||
return tenantMapper.selectList(new LambdaQueryWrapper<IamTenant>()
|
||||
.eq(IamTenant::getTenantType, "MERCHANT")
|
||||
.orderByAsc(IamTenant::getTenantName));
|
||||
}
|
||||
public IamTenantReview findReview(Long reviewId) { return reviewMapper.selectById(reviewId); }
|
||||
public IamTenantReview findPendingReviewByTenant(Long tenantId) {
|
||||
return reviewMapper.selectOne(new LambdaQueryWrapper<IamTenantReview>()
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
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.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;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 平台管理员跨租户维护登录用户;凭据仍只由 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,
|
||||
@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()
|
||||
.collect(Collectors.toMap(IamRole::getId, Function.identity()));
|
||||
return members.findAll().stream().map(membership -> response(membership, tenantMap, roleMap)).toList();
|
||||
}
|
||||
|
||||
@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()
|
||||
.map(value -> new RoleOption(value.getId(), value.getTenantId(), value.getRoleName(), value.getStatus()))
|
||||
.toList();
|
||||
return new UserOptions(List.copyOf(tenantOptions), roleOptions);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PLATFORM_USER_CREATE", resourceType = "MEMBERSHIP",
|
||||
resourceId = "#request.username", description = "平台新增用户", captureAfter = false)
|
||||
public UserResponse create(CreateUserRequest request) {
|
||||
long tenantId = request.tenantId();
|
||||
validateTenant(tenantId);
|
||||
List<Long> roleIds = validateRoles(tenantId, request.roleIds());
|
||||
long authUserId = IdWorker.getId();
|
||||
long userId = IdWorker.getId();
|
||||
long membershipId = IdWorker.getId();
|
||||
String appCode = appCode(tenantId);
|
||||
call(() -> authClient.create(internalToken, new AuthAccountClient.CreateAccountRequest(
|
||||
authUserId, request.username().trim(), request.password(), request.displayName().trim(),
|
||||
tenantId, membershipId, appCode, request.status())));
|
||||
try {
|
||||
IamUser user = new IamUser();
|
||||
user.setId(userId);
|
||||
user.setAuthUserId(authUserId);
|
||||
user.setUserType(userType(tenantId));
|
||||
user.setDisplayName(request.displayName().trim());
|
||||
user.setPhone(request.phone().trim());
|
||||
user.setEmail(trimToNull(request.email()));
|
||||
user.setStatus(request.status());
|
||||
members.insertUser(user);
|
||||
|
||||
IamMembership membership = new IamMembership();
|
||||
membership.setId(membershipId);
|
||||
membership.setTenantId(tenantId);
|
||||
membership.setUserId(userId);
|
||||
membership.setMemberType("EMPLOYEE");
|
||||
membership.setMemberNo(memberNo(tenantId, membershipId));
|
||||
membership.setStatus(request.status());
|
||||
membership.setJoinedAt(LocalDateTime.now());
|
||||
membership.setLeftAt("DISABLED".equals(request.status()) ? LocalDateTime.now() : null);
|
||||
members.insert(membership);
|
||||
members.replaceRoles(tenantId, membershipId, roleIds);
|
||||
return response(membership);
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensateCreate(authUserId, localFailure);
|
||||
throw localFailure;
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@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);
|
||||
IamUser user = requiredUser(membership.getUserId());
|
||||
long oldTenantId = membership.getTenantId();
|
||||
long newTenantId = request.tenantId();
|
||||
validateTenant(newTenantId);
|
||||
List<Long> roleIds = validateRoles(newTenantId, request.roleIds());
|
||||
List<Long> oldRoleIds = members.findRoleIds(oldTenantId, membershipId);
|
||||
if (isProtectedPlatformAdmin(membership, oldRoleIds)
|
||||
&& (newTenantId != PLATFORM_TENANT_ID || !containsPlatformAdminRole(roleIds))) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "系统管理员不能更换租户或移除超级管理员角色");
|
||||
}
|
||||
if ((oldTenantId != newTenantId || !new java.util.HashSet<>(oldRoleIds).equals(new java.util.HashSet<>(roleIds)))
|
||||
&& (operatorPermissions == null || !operatorPermissions.contains("platform:user:grant-role"))) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "变更用户所属租户或角色需要分配角色权限");
|
||||
}
|
||||
AuthAccountClient.AccountSummary oldAccount = account(user.getAuthUserId());
|
||||
boolean profileChanged = !Objects.equals(user.getDisplayName(), request.displayName().trim());
|
||||
boolean scopeChanged = oldTenantId != newTenantId;
|
||||
|
||||
if (profileChanged) call(() -> authClient.profile(internalToken, user.getAuthUserId(),
|
||||
new AuthAccountClient.ProfileRequest(request.displayName().trim())));
|
||||
if (scopeChanged) call(() -> authClient.scope(internalToken, user.getAuthUserId(),
|
||||
new AuthAccountClient.ScopeRequest(newTenantId, membershipId, appCode(newTenantId))));
|
||||
try {
|
||||
user.setDisplayName(request.displayName().trim());
|
||||
user.setPhone(request.phone().trim());
|
||||
user.setEmail(trimToNull(request.email()));
|
||||
user.setUserType(userType(newTenantId));
|
||||
membership.setTenantId(newTenantId);
|
||||
if (scopeChanged && members.existsMemberNo(newTenantId, membership.getMemberNo(), membershipId)) {
|
||||
membership.setMemberNo(memberNo(newTenantId, membershipId));
|
||||
}
|
||||
members.saveUser(user);
|
||||
members.save(membership);
|
||||
if (scopeChanged) members.replaceRoles(oldTenantId, membershipId, List.of());
|
||||
members.replaceRoles(newTenantId, membershipId, roleIds);
|
||||
} catch (RuntimeException localFailure) {
|
||||
compensateUpdate(user.getAuthUserId(), membershipId, oldTenantId, oldAccount,
|
||||
profileChanged, scopeChanged, localFailure);
|
||||
throw localFailure;
|
||||
}
|
||||
return response(membership);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@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);
|
||||
if (membershipId.equals(operatorMembershipId) && "DISABLED".equals(request.status())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "不能停用当前登录用户");
|
||||
}
|
||||
IamUser user = requiredUser(membership.getUserId());
|
||||
if ("DISABLED".equals(request.status())
|
||||
&& isProtectedPlatformAdmin(membership,
|
||||
members.findRoleIds(membership.getTenantId(), membershipId))) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "系统管理员不能被禁用");
|
||||
}
|
||||
AuthAccountClient.AccountSummary oldAccount = account(user.getAuthUserId());
|
||||
call(() -> authClient.status(internalToken, user.getAuthUserId(),
|
||||
new AuthAccountClient.StatusRequest(request.status())));
|
||||
try {
|
||||
membership.setStatus(request.status());
|
||||
membership.setLeftAt("DISABLED".equals(request.status()) ? LocalDateTime.now() : null);
|
||||
user.setStatus(request.status());
|
||||
members.saveUser(user);
|
||||
members.save(membership);
|
||||
} catch (RuntimeException localFailure) {
|
||||
call(() -> authClient.status(internalToken, user.getAuthUserId(),
|
||||
new AuthAccountClient.StatusRequest(oldAccount.status())));
|
||||
throw localFailure;
|
||||
}
|
||||
return response(membership);
|
||||
}
|
||||
|
||||
@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());
|
||||
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()
|
||||
.collect(Collectors.toMap(IamRole::getId, Function.identity()));
|
||||
return response(membership, tenantMap, roleMap);
|
||||
}
|
||||
|
||||
private UserResponse response(IamMembership membership, Map<Long, IamTenant> tenantMap,
|
||||
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() : "未知租户";
|
||||
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() {
|
||||
return roleMapper.selectList(new LambdaQueryWrapper<IamRole>()
|
||||
.in(IamRole::getAppCode, "PLATFORM", "MERCHANT")
|
||||
.orderByAsc(IamRole::getTenantId).orderByAsc(IamRole::getRoleName));
|
||||
}
|
||||
|
||||
private List<Long> validateRoles(long tenantId, List<Long> requested) {
|
||||
List<Long> roleIds = List.copyOf(new LinkedHashSet<>(requested == null ? List.of() : requested));
|
||||
if (roleIds.isEmpty()) throw new BizException(ResultCode.BAD_REQUEST, "请选择角色");
|
||||
for (Long roleId : roleIds) {
|
||||
IamRole role = roleMapper.selectById(roleId);
|
||||
if (role == null || !Objects.equals(role.getTenantId(), tenantId)
|
||||
|| !appCode(tenantId).equals(role.getAppCode()) || !"ACTIVE".equals(role.getStatus())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "角色必须属于所选租户且处于启用状态");
|
||||
}
|
||||
}
|
||||
return roleIds;
|
||||
}
|
||||
|
||||
private boolean isProtectedPlatformAdmin(IamMembership membership, List<Long> roleIds) {
|
||||
return membership.getTenantId() == PLATFORM_TENANT_ID && containsPlatformAdminRole(roleIds);
|
||||
}
|
||||
|
||||
private boolean containsPlatformAdminRole(List<Long> roleIds) {
|
||||
return roleIds.stream().map(roleMapper::selectById).filter(Objects::nonNull)
|
||||
.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 IamMembership required(Long membershipId) {
|
||||
IamMembership membership = members.findById(membershipId);
|
||||
if (membership == null) 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, "用户档案不存在");
|
||||
return user;
|
||||
}
|
||||
|
||||
private AuthAccountClient.AccountSummary account(Long authUserId) {
|
||||
ApiResponse<AuthAccountClient.AccountSummary> result = call(() -> authClient.summary(internalToken, authUserId));
|
||||
if (result.data() == null) throw unavailable();
|
||||
return result.data();
|
||||
}
|
||||
|
||||
private String appCode(long tenantId) { return tenantId == PLATFORM_TENANT_ID ? "PLATFORM" : "MERCHANT"; }
|
||||
private String userType(long tenantId) { return tenantId == PLATFORM_TENANT_ID ? "INTERNAL" : "ENTERPRISE"; }
|
||||
private String memberNo(long tenantId, long membershipId) {
|
||||
return (tenantId == PLATFORM_TENANT_ID ? "PLATFORM-" : "MEMBER-") + membershipId;
|
||||
}
|
||||
private String trimToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
|
||||
private void compensateCreate(long authUserId, RuntimeException localFailure) {
|
||||
try {
|
||||
call(() -> authClient.compensate(internalToken, authUserId));
|
||||
} catch (RuntimeException compensationFailure) {
|
||||
log.error("统一用户创建失败且认证账号补偿失败,authUserId={}", authUserId, compensationFailure);
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,
|
||||
"用户创建未完成,认证账号补偿失败,请按 authUserId=" + authUserId + " 修复一致性");
|
||||
}
|
||||
}
|
||||
|
||||
private void compensateUpdate(long authUserId, long membershipId, long oldTenantId,
|
||||
AuthAccountClient.AccountSummary oldAccount, boolean profileChanged,
|
||||
boolean scopeChanged, RuntimeException localFailure) {
|
||||
try {
|
||||
if (scopeChanged) call(() -> authClient.scope(internalToken, authUserId,
|
||||
new AuthAccountClient.ScopeRequest(oldTenantId, membershipId, appCode(oldTenantId))));
|
||||
if (profileChanged) call(() -> authClient.profile(internalToken, authUserId,
|
||||
new AuthAccountClient.ProfileRequest(oldAccount.displayName())));
|
||||
} catch (RuntimeException compensationFailure) {
|
||||
log.error("统一用户编辑失败且认证账号补偿失败,authUserId={}", authUserId, compensationFailure);
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,
|
||||
"用户编辑未完成,认证账号补偿失败,请按 authUserId=" + authUserId + " 修复一致性");
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
throw new BizException(code, result.message() == null || result.message().isBlank()
|
||||
? code.message() : result.message());
|
||||
}
|
||||
return result;
|
||||
} catch (FeignException exception) {
|
||||
throw unavailable();
|
||||
} catch (BizException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
private BizException unavailable() {
|
||||
return new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用");
|
||||
}
|
||||
@FunctionalInterface private interface RemoteCall<T> { ApiResponse<T> call(); }
|
||||
}
|
||||
@@ -93,6 +93,10 @@ VALUES
|
||||
(5100021, 0, 'PLATFORM', 'BUTTON', 5100020, 'platform:customer:approve', '审核通过', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5100022, 0, 'PLATFORM', 'BUTTON', 5100020, 'platform:customer:reject', '审核驳回', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5101011, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:create', '新增用户', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5101012, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:edit', '编辑用户', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5101013, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:status', '启停用户', NULL, NULL, 30, 'ACTIVE', 0),
|
||||
(5101014, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:reset-password', '重置密码', NULL, NULL, 40, 'ACTIVE', 0),
|
||||
(5101015, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:grant-role', '分配用户角色', NULL, NULL, 50, 'ACTIVE', 0),
|
||||
(5101021, 0, 'PLATFORM', 'BUTTON', 5101020, 'platform:role:create', '新增角色', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5101022, 0, 'PLATFORM', 'BUTTON', 5101020, 'platform:role:grant', '角色授权', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5101031, 0, 'PLATFORM', 'BUTTON', 5101030, 'platform:permission:create', '新增菜单按钮', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
@@ -113,9 +117,11 @@ VALUES
|
||||
(5103021, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:create', '新增快递公司', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5103022, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:edit', '编辑快递公司', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5103023, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:status', '启用停用快递公司', NULL, NULL, 30, 'ACTIVE', 0),
|
||||
(5103024, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:import', '批量导入快递公司', NULL, NULL, 40, 'ACTIVE', 0),
|
||||
(5103031, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:create', '新增往来单位', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5103032, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:edit', '编辑往来单位', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5103033, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:status', '启用停用往来单位', NULL, NULL, 30, 'ACTIVE', 0)
|
||||
(5103033, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:status', '启用停用往来单位', NULL, NULL, 30, 'ACTIVE', 0),
|
||||
(5103034, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:import', '批量导入往来单位', NULL, NULL, 40, 'ACTIVE', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_type = VALUES(permission_type), parent_id = VALUES(parent_id),
|
||||
permission_name = VALUES(permission_name), route_path = VALUES(route_path),
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
-- 平台客户档案原型字段补齐,可重复执行。
|
||||
USE baiyue_iam;
|
||||
|
||||
SET @add_customer_columns = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'iam_tenant' AND column_name = 'service_package'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE iam_tenant ADD COLUMN service_package VARCHAR(64) NULL COMMENT ''客户服务套餐'' AFTER contact_email, ADD COLUMN credit_days INT NOT NULL DEFAULT 0 COMMENT ''账期天数'' AFTER service_package, ADD COLUMN sales_person VARCHAR(64) NULL COMMENT ''负责销售人员'' AFTER credit_days, ADD COLUMN business_address VARCHAR(255) NULL COMMENT ''企业经营地址'' AFTER sales_person, ADD COLUMN remark VARCHAR(500) NULL COMMENT ''平台客户备注'' AFTER business_address'
|
||||
);
|
||||
PREPARE add_customer_columns_stmt FROM @add_customer_columns;
|
||||
EXECUTE add_customer_columns_stmt;
|
||||
DEALLOCATE PREPARE add_customer_columns_stmt;
|
||||
@@ -31,6 +31,7 @@ VALUES
|
||||
(5103021, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:create', '新增快递公司', 10, 'ACTIVE', 1, 0),
|
||||
(5103022, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:edit', '编辑快递公司', 20, 'ACTIVE', 1, 0),
|
||||
(5103023, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:status', '启停快递公司', 30, 'ACTIVE', 1, 0),
|
||||
(5103024, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:import', '批量导入快递公司', 40, 'ACTIVE', 1, 0),
|
||||
(5103031, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:create', '新增往来单位', 10, 'ACTIVE', 1, 0),
|
||||
(5103032, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:edit', '编辑往来单位', 20, 'ACTIVE', 1, 0),
|
||||
(5103033, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:status', '启停往来单位', 30, 'ACTIVE', 1, 0)
|
||||
@@ -46,7 +47,7 @@ WHERE tenant_id = 0 AND app_code = 'PLATFORM'
|
||||
AND permission_code IN (
|
||||
'platform:customer:edit', 'platform:customer:status', 'platform:user:grant-role',
|
||||
'platform:permission:edit', 'platform:permission:status', 'platform:permission:delete',
|
||||
'platform:express:list', 'platform:express:create', 'platform:express:edit', 'platform:express:status',
|
||||
'platform:express:list', 'platform:express:create', 'platform:express:edit', 'platform:express:status', 'platform:express:import',
|
||||
'platform:counterparty:list', 'platform:counterparty:create', 'platform:counterparty:edit', 'platform:counterparty:status'
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
@@ -145,6 +145,73 @@ paths:
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/platform/v1/iam/users:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 统一查询平台及企业租户用户
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
post:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 新增平台或企业租户用户
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/CreatePlatformManagedUserRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/platform/v1/iam/users/options:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 查询用户管理可选租户和角色
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/platform/v1/iam/users/{membershipId}:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/MembershipId'}
|
||||
put:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 编辑用户资料、所属租户及角色
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/UpdatePlatformManagedUserRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/platform/v1/iam/users/{membershipId}/status:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/MembershipId'}
|
||||
put:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 启用或停用平台及企业租户用户
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ChangeInternalUserStatusRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/platform/v1/iam/users/{membershipId}/password:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/MembershipId'}
|
||||
put:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 重置平台及企业租户用户密码
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ResetInternalUserPasswordRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/platform/v1/iam/internal-users:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
@@ -505,6 +572,12 @@ 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}
|
||||
RegisterRequest:
|
||||
type: object
|
||||
required: [tenantName, legalName, unifiedSocialCreditCode, contactName, contactPhone]
|
||||
@@ -515,6 +588,11 @@ 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}
|
||||
UpdateProfileRequest:
|
||||
type: object
|
||||
required: [tenantName, legalName, contactName, contactPhone]
|
||||
@@ -533,6 +611,11 @@ 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}
|
||||
ChangeTenantStatusRequest:
|
||||
type: object
|
||||
required: [status]
|
||||
@@ -542,6 +625,27 @@ components:
|
||||
type: object
|
||||
properties:
|
||||
comment: {type: string, maxLength: 500, description: Review comment or rejection reason / 审核意见或驳回原因}
|
||||
CreatePlatformManagedUserRequest:
|
||||
type: object
|
||||
required: [username, displayName, phone, tenantId, roleIds, password, status]
|
||||
properties:
|
||||
username: {type: string, minLength: 4, maxLength: 64}
|
||||
displayName: {type: string, maxLength: 64}
|
||||
phone: {type: string, pattern: '^1\\d{10}$'}
|
||||
email: {type: string, format: email, maxLength: 128}
|
||||
tenantId: {type: integer, format: int64}
|
||||
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]}
|
||||
UpdatePlatformManagedUserRequest:
|
||||
type: object
|
||||
required: [displayName, phone, tenantId, roleIds]
|
||||
properties:
|
||||
displayName: {type: string, maxLength: 64}
|
||||
phone: {type: string, pattern: '^1\\d{10}$'}
|
||||
email: {type: string, format: email, maxLength: 128}
|
||||
tenantId: {type: integer, format: int64}
|
||||
roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}}
|
||||
CreateInternalUserRequest:
|
||||
type: object
|
||||
required: [displayName]
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@@ -35,6 +36,33 @@ class PlatformInternalUserContractTest {
|
||||
"/api/platform/v1/iam/internal-users/{membershipId:\\d+}/roles");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedPlatformUserEndpointsKeepIndependentPermissions() throws Exception {
|
||||
Method list = PlatformUserManagementController.class.getMethod("list",
|
||||
com.baiyue.common.security.AuthenticatedUser.class);
|
||||
Method create = PlatformUserManagementController.class.getMethod("create",
|
||||
com.baiyue.iam.dto.PlatformUserManagementDtos.CreateUserRequest.class,
|
||||
com.baiyue.common.security.AuthenticatedUser.class);
|
||||
Method update = PlatformUserManagementController.class.getMethod("update", Long.class,
|
||||
com.baiyue.iam.dto.PlatformUserManagementDtos.UpdateUserRequest.class,
|
||||
com.baiyue.common.security.AuthenticatedUser.class);
|
||||
Method status = PlatformUserManagementController.class.getMethod("status", Long.class,
|
||||
com.baiyue.iam.dto.PlatformUserManagementDtos.ChangeStatusRequest.class,
|
||||
com.baiyue.common.security.AuthenticatedUser.class);
|
||||
Method password = PlatformUserManagementController.class.getMethod("password", Long.class,
|
||||
com.baiyue.iam.dto.PlatformUserManagementDtos.ResetPasswordRequest.class,
|
||||
com.baiyue.common.security.AuthenticatedUser.class);
|
||||
|
||||
assertEquals("hasAuthority('platform:user:list')", list.getAnnotation(PreAuthorize.class).value());
|
||||
assertEquals("hasAuthority('platform:user:create') and hasAuthority('platform:user:grant-role')",
|
||||
create.getAnnotation(PreAuthorize.class).value());
|
||||
assertEquals("hasAuthority('platform:user:edit')", update.getAnnotation(PreAuthorize.class).value());
|
||||
assertEquals("hasAuthority('platform:user:status')", status.getAnnotation(PreAuthorize.class).value());
|
||||
assertEquals("hasAuthority('platform:user:reset-password')", password.getAnnotation(PreAuthorize.class).value());
|
||||
assertNotNull(create.getAnnotation(PostMapping.class));
|
||||
assertNotNull(update.getAnnotation(PutMapping.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void tenantProfileReadRequiresViewPermission() throws Exception {
|
||||
Method method = MerchantTenantController.class.getMethod("profile",
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
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 org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class PlatformUserManagementServiceTest {
|
||||
private final MemberRepository members = mock(MemberRepository.class);
|
||||
private final TenantRepository tenants = mock(TenantRepository.class);
|
||||
private final IamRoleMapper roles = mock(IamRoleMapper.class);
|
||||
private final AuthAccountClient auth = mock(AuthAccountClient.class);
|
||||
private final PlatformUserManagementService service =
|
||||
new PlatformUserManagementService(members, tenants, roles, auth, "internal-token");
|
||||
private IamTenant merchant;
|
||||
private IamRole merchantRole;
|
||||
private IamMembership membership;
|
||||
private IamUser user;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
merchant = new IamTenant();
|
||||
merchant.setId(10001L); merchant.setTenantType("MERCHANT"); merchant.setTenantName("华远贸易");
|
||||
merchant.setStatus("ACTIVE");
|
||||
merchantRole = new IamRole();
|
||||
merchantRole.setId(40001L); merchantRole.setTenantId(10001L); merchantRole.setAppCode("MERCHANT");
|
||||
merchantRole.setRoleName("租户管理员"); merchantRole.setStatus("ACTIVE");
|
||||
membership = new IamMembership();
|
||||
membership.setId(30L); membership.setTenantId(10001L); membership.setUserId(20L);
|
||||
membership.setMemberType("EMPLOYEE"); membership.setMemberNo("MEMBER-30"); membership.setStatus("ACTIVE");
|
||||
user = new IamUser();
|
||||
user.setId(20L); user.setAuthUserId(10L); user.setUserType("ENTERPRISE");
|
||||
user.setDisplayName("张伟"); user.setPhone("13800138000"); user.setStatus("ACTIVE");
|
||||
|
||||
when(tenants.findTenant(10001L)).thenReturn(merchant);
|
||||
when(tenants.listMerchants()).thenReturn(List.of(merchant));
|
||||
when(roles.selectById(40001L)).thenReturn(merchantRole);
|
||||
when(roles.selectList(any())).thenReturn(List.of(merchantRole));
|
||||
when(members.findById(30L)).thenReturn(membership);
|
||||
when(members.findUser(20L)).thenReturn(user);
|
||||
when(members.findRoleIds(10001L, 30L)).thenReturn(List.of(40001L));
|
||||
when(auth.summary("internal-token", 10L)).thenReturn(ApiResponse.success(
|
||||
new AuthAccountClient.AccountSummary(10L, "zhangwei", "张伟", "ACTIVE",
|
||||
LocalDateTime.of(2026, 6, 1, 10, 30), 10001L, 30L, "MERCHANT")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listReturnsTenantRoleAndLastLoginForMerchantUser() {
|
||||
when(members.findAll()).thenReturn(List.of(membership));
|
||||
|
||||
UserResponse response = service.list().get(0);
|
||||
|
||||
assertEquals("zhangwei", response.username());
|
||||
assertEquals("华远贸易", response.tenantName());
|
||||
assertEquals(List.of("租户管理员"), response.roleNames());
|
||||
assertEquals(LocalDateTime.of(2026, 6, 1, 10, 30), response.lastLoginAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createMerchantUserCreatesDisabledMerchantAccountAndMembership() {
|
||||
when(auth.create(anyString(), any())).thenReturn(ApiResponse.success(
|
||||
new AuthAccountClient.AccountSummary(99L, "newuser", "新用户", "DISABLED")));
|
||||
when(auth.summary(anyString(), anyLong())).thenReturn(ApiResponse.success(
|
||||
new AuthAccountClient.AccountSummary(99L, "newuser", "新用户", "DISABLED")));
|
||||
when(members.findUser(anyLong())).thenReturn(user);
|
||||
CreateUserRequest request = new CreateUserRequest("newuser", "新用户", "13900139000",
|
||||
"new@example.com", 10001L, List.of(40001L), "Password1", "DISABLED");
|
||||
|
||||
service.create(request);
|
||||
|
||||
ArgumentCaptor<AuthAccountClient.CreateAccountRequest> account =
|
||||
ArgumentCaptor.forClass(AuthAccountClient.CreateAccountRequest.class);
|
||||
verify(auth).create(eq("internal-token"), account.capture());
|
||||
assertEquals("MERCHANT", account.getValue().appCode());
|
||||
assertEquals("DISABLED", account.getValue().accountStatus());
|
||||
verify(members).insertUser(argThat(value -> "ENTERPRISE".equals(value.getUserType())));
|
||||
verify(members).insert(argThat(value -> value.getTenantId() == 10001L
|
||||
&& "DISABLED".equals(value.getStatus())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void tenantMoveUpdatesAuthScopeAndRolesWhenOperatorCanGrantRoles() {
|
||||
IamTenant target = new IamTenant();
|
||||
target.setId(10002L); target.setTenantType("MERCHANT"); target.setTenantName("深圳速达"); target.setStatus("ACTIVE");
|
||||
IamRole targetRole = new IamRole();
|
||||
targetRole.setId(40002L); targetRole.setTenantId(10002L); targetRole.setAppCode("MERCHANT");
|
||||
targetRole.setRoleName("操作员"); targetRole.setStatus("ACTIVE");
|
||||
when(tenants.findTenant(10002L)).thenReturn(target);
|
||||
when(tenants.listMerchants()).thenReturn(List.of(merchant, target));
|
||||
when(roles.selectById(40002L)).thenReturn(targetRole);
|
||||
when(roles.selectList(any())).thenReturn(List.of(merchantRole, targetRole));
|
||||
when(auth.scope(anyString(), anyLong(), any())).thenReturn(ApiResponse.success(null));
|
||||
when(auth.profile(anyString(), anyLong(), any())).thenReturn(ApiResponse.success(null));
|
||||
UpdateUserRequest request = new UpdateUserRequest("张伟新", "13800138000", null,
|
||||
10002L, List.of(40002L));
|
||||
|
||||
UserResponse response = service.update(30L, request, Set.of("platform:user:grant-role"));
|
||||
|
||||
verify(auth).scope(eq("internal-token"), eq(10L),
|
||||
argThat(value -> value.tenantId() == 10002L && "MERCHANT".equals(value.appCode())));
|
||||
verify(members).replaceRoles(10001L, 30L, List.of());
|
||||
verify(members).replaceRoles(10002L, 30L, List.of(40002L));
|
||||
assertEquals(10002L, response.tenantId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void tenantOrRoleChangeRequiresGrantRolePermission() {
|
||||
IamRole otherRole = new IamRole();
|
||||
otherRole.setId(40003L); otherRole.setTenantId(10001L); otherRole.setAppCode("MERCHANT");
|
||||
otherRole.setRoleName("操作员"); otherRole.setStatus("ACTIVE");
|
||||
when(roles.selectById(40003L)).thenReturn(otherRole);
|
||||
UpdateUserRequest request = new UpdateUserRequest("张伟", "13800138000", null,
|
||||
10001L, List.of(40003L));
|
||||
|
||||
assertThrows(BizException.class, () -> service.update(30L, request, Set.of()));
|
||||
verify(auth, never()).scope(anyString(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentPlatformOperatorCannotDisableSelf() {
|
||||
assertThrows(BizException.class,
|
||||
() -> service.changeStatus(30L, new ChangeStatusRequest("DISABLED"), 30L));
|
||||
verify(auth, never()).status(anyString(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void builtInPlatformAdministratorCannotBeDisabled() {
|
||||
membership.setTenantId(0L);
|
||||
IamRole platformAdmin = new IamRole();
|
||||
platformAdmin.setId(40000L); platformAdmin.setTenantId(0L); platformAdmin.setAppCode("PLATFORM");
|
||||
platformAdmin.setRoleCode("PLATFORM_ADMIN"); platformAdmin.setRoleName("平台超级管理员");
|
||||
platformAdmin.setStatus("ACTIVE");
|
||||
when(roles.selectById(40000L)).thenReturn(platformAdmin);
|
||||
when(members.findRoleIds(0L, 30L)).thenReturn(List.of(40000L));
|
||||
|
||||
assertThrows(BizException.class,
|
||||
() -> service.changeStatus(30L, new ChangeStatusRequest("DISABLED"), 99L));
|
||||
verify(auth, never()).status(anyString(), anyLong(), any());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user