261 lines
15 KiB
Java
261 lines
15 KiB
Java
package com.baiyue.iam.service;
|
|
|
|
import com.baiyue.common.core.api.ApiResponse;
|
|
import com.baiyue.common.core.api.ResultCode;
|
|
import com.baiyue.common.core.exception.BizException;
|
|
import com.baiyue.common.audit.BusinessAudit;
|
|
import com.baiyue.iam.client.AuthAccountClient;
|
|
import com.baiyue.iam.dto.MemberDtos.*;
|
|
import com.baiyue.iam.entity.IamMembership;
|
|
import com.baiyue.iam.entity.IamRole;
|
|
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.baiyue.iam.dto.PlatformCustomerMemberDtos;
|
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
|
import feign.FeignException;
|
|
import jakarta.annotation.Resource;
|
|
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.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
/** 企业成员闭环服务,账号凭据归 auth-service,人员档案与租户关系归 IAM。 */
|
|
@Service
|
|
public class MemberService {
|
|
private static final String GRANT_ROLE_PERMISSION = "merchant:member:grant-role";
|
|
private static final Logger log = LoggerFactory.getLogger(MemberService.class);
|
|
@Resource
|
|
private MemberRepository repository;
|
|
@Resource
|
|
private TenantRepository tenantRepository;
|
|
@Resource
|
|
private IamRoleMapper roleMapper;
|
|
@Resource
|
|
private AuthAccountClient authClient;
|
|
@Resource
|
|
private SecurityVersionService securityVersions;
|
|
@Value("${baiyue.security.internal-service-token}")
|
|
private String internalToken;
|
|
|
|
public List<MemberResponse> list(long tenantId) {
|
|
return repository.findByTenant(tenantId).stream().map(m -> response(tenantId, m)).toList();
|
|
}
|
|
|
|
/** 平台只读查询客户成员;显式校验客户主档,并包含 OWNER 管理员。 */
|
|
public List<PlatformCustomerMemberDtos.MemberResponse> platformList(long tenantId) {
|
|
var tenant = tenantRepository.findTenant(tenantId);
|
|
if (tenant == null || !"MERCHANT".equals(tenant.getTenantType())) {
|
|
throw new BizException(ResultCode.NOT_FOUND, "企业客户不存在");
|
|
}
|
|
return repository.findByTenant(tenantId).stream().map(m -> platformResponse(tenantId, m)).toList();
|
|
}
|
|
|
|
private PlatformCustomerMemberDtos.MemberResponse platformResponse(long tenantId, IamMembership membership) {
|
|
IamUser user = requiredUser(membership.getUserId());
|
|
AuthAccountClient.AccountSummary account = accountSummary(user.getAuthUserId());
|
|
List<Long> roleIds = repository.findRoleIds(tenantId, membership.getId());
|
|
List<String> roleNames = roleIds.stream()
|
|
.map(roleMapper::selectById)
|
|
.filter(role -> role != null && "MERCHANT".equals(role.getAppCode())
|
|
&& (Long.valueOf(tenantId).equals(role.getTenantId()) || Long.valueOf(0L).equals(role.getTenantId())))
|
|
.map(IamRole::getRoleName)
|
|
.toList();
|
|
return new PlatformCustomerMemberDtos.MemberResponse(membership.getId(), user.getId(),
|
|
user.getAuthUserId(), account.username(), user.getDisplayName(), user.getPhone(), user.getEmail(),
|
|
membership.getMemberType(), membership.getMemberNo(), membership.getStatus(),
|
|
roleIds, roleNames);
|
|
}
|
|
|
|
private AuthAccountClient.AccountSummary accountSummary(Long authUserId) {
|
|
return data(call(() -> authClient.summary(internalToken, authUserId)));
|
|
}
|
|
|
|
public MemberResponse detail(long tenantId, Long membershipId) {
|
|
return response(tenantId, required(tenantId, membershipId));
|
|
}
|
|
|
|
@Transactional
|
|
@BusinessAudit(event = "MEMBER_CREATE", resourceType = "MEMBERSHIP", resourceId = "#request.username", description = "新增企业成员")
|
|
public MemberResponse create(long tenantId, CreateMemberRequest request, Set<String> operatorPermissions) {
|
|
rejectOwnerMutation(request.memberType());
|
|
requireGrantRoleIfChanged(List.of(), request.roleIds(), operatorPermissions);
|
|
validateRoles(tenantId, request.roleIds());
|
|
validateMemberNo(tenantId, request.memberNo(), null);
|
|
long authId = IdWorker.getId();
|
|
long userId = IdWorker.getId();
|
|
long membershipId = IdWorker.getId();
|
|
call(() -> authClient.create(internalToken, new AuthAccountClient.CreateAccountRequest(
|
|
authId, request.username(), request.phone(), request.password(), request.displayName(),
|
|
tenantId, membershipId, "MERCHANT", "ACTIVE")));
|
|
try {
|
|
IamUser user = new IamUser();
|
|
user.setId(userId); user.setAuthUserId(authId); user.setUserType("ENTERPRISE");
|
|
user.setDisplayName(request.displayName()); user.setPhone(request.phone()); user.setEmail(request.email());
|
|
user.setStatus("ACTIVE"); repository.insertUser(user);
|
|
|
|
IamMembership membership = new IamMembership();
|
|
membership.setId(membershipId); membership.setTenantId(tenantId); membership.setUserId(userId);
|
|
membership.setMemberType(request.memberType()); membership.setMemberNo(request.memberNo());
|
|
membership.setStatus("ACTIVE"); membership.setJoinedAt(LocalDateTime.now()); repository.insert(membership);
|
|
repository.replaceRoles(tenantId, membershipId, request.roleIds());
|
|
securityVersions.subjectChanged(authId);
|
|
return response(tenantId, membership, user);
|
|
} catch (RuntimeException exception) {
|
|
try {
|
|
authClient.compensate(internalToken, authId);
|
|
} catch (RuntimeException compensationFailure) {
|
|
log.error("成员创建失败且认证账号补偿失败,authUserId={}", authId, compensationFailure);
|
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,
|
|
"成员创建未完成,认证账号补偿失败,请按 authUserId=" + authId + " 进行一致性修复");
|
|
}
|
|
throw exception;
|
|
}
|
|
}
|
|
|
|
@Transactional
|
|
@BusinessAudit(event = "MEMBER_UPDATE", resourceType = "MEMBERSHIP", resourceId = "#membershipId", description = "编辑企业成员")
|
|
public MemberResponse update(long tenantId, Long membershipId, UpdateMemberRequest request,
|
|
Set<String> operatorPermissions) {
|
|
rejectOwnerMutation(request.memberType());
|
|
IamMembership membership = required(tenantId, membershipId);
|
|
List<Long> currentRoleIds = repository.findRoleIds(tenantId, membershipId);
|
|
requireGrantRoleIfChanged(currentRoleIds, request.roleIds(), operatorPermissions);
|
|
validateRoles(tenantId, request.roleIds());
|
|
validateMemberNo(tenantId, request.memberNo(), membershipId);
|
|
if ("OWNER".equals(membership.getMemberType())) {
|
|
throw new BizException(ResultCode.FORBIDDEN, "企业所有者不允许通过成员管理接口编辑");
|
|
}
|
|
IamUser user = requiredUser(membership.getUserId());
|
|
if (!java.util.Objects.equals(user.getPhone(), request.phone())) {
|
|
call(() -> authClient.phone(internalToken, user.getAuthUserId(),
|
|
new AuthAccountClient.PhoneRequest(request.phone())));
|
|
}
|
|
user.setDisplayName(request.displayName()); user.setPhone(request.phone()); user.setEmail(request.email());
|
|
membership.setMemberType(request.memberType()); membership.setMemberNo(request.memberNo());
|
|
repository.saveUser(user); repository.save(membership);
|
|
repository.replaceRoles(tenantId, membershipId, request.roleIds());
|
|
if (!new HashSet<>(currentRoleIds).equals(new HashSet<>(request.roleIds()))) {
|
|
call(() -> authClient.revokeSessions(internalToken,
|
|
new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(user.getAuthUserId()))));
|
|
securityVersions.subjectChanged(user.getAuthUserId());
|
|
}
|
|
return response(tenantId, membership);
|
|
}
|
|
|
|
@Transactional
|
|
@BusinessAudit(event = "MEMBER_STATUS_CHANGE", resourceType = "MEMBERSHIP", resourceId = "#membershipId", description = "启停企业成员")
|
|
public MemberResponse changeStatus(long tenantId, Long membershipId, ChangeMemberStatusRequest request,
|
|
Long operatorMembershipId) {
|
|
if (!"ACTIVE".equals(request.status()) && !"DISABLED".equals(request.status()))
|
|
throw new BizException(ResultCode.BAD_REQUEST, "成员状态只允许 ACTIVE 或 DISABLED");
|
|
IamMembership membership = required(tenantId, membershipId);
|
|
if ("OWNER".equals(membership.getMemberType()) && "DISABLED".equals(request.status()))
|
|
throw new BizException(ResultCode.FORBIDDEN, "企业所有者不能被停用");
|
|
if (membershipId.equals(operatorMembershipId) && "DISABLED".equals(request.status()))
|
|
throw new BizException(ResultCode.BAD_REQUEST, "不能停用当前登录成员");
|
|
IamUser user = requiredUser(membership.getUserId());
|
|
String oldStatus = membership.getStatus();
|
|
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()); repository.saveUser(user); repository.save(membership);
|
|
securityVersions.subjectChanged(user.getAuthUserId());
|
|
} catch (RuntimeException localFailure) {
|
|
try {
|
|
authClient.status(internalToken, user.getAuthUserId(), new AuthAccountClient.StatusRequest(oldStatus));
|
|
} catch (RuntimeException compensationFailure) {
|
|
log.error("成员状态写入失败且认证账号状态补偿失败,authUserId={}", user.getAuthUserId(), compensationFailure);
|
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,
|
|
"成员状态未完成,认证账号补偿失败,请按 authUserId=" + user.getAuthUserId() + " 进行一致性修复");
|
|
}
|
|
throw localFailure;
|
|
}
|
|
return response(tenantId, membership);
|
|
}
|
|
|
|
@BusinessAudit(event = "MEMBER_PASSWORD_RESET", resourceType = "MEMBERSHIP", resourceId = "#membershipId",
|
|
description = "管理员重置成员密码", captureBefore = false, captureAfter = false)
|
|
public void resetPassword(long tenantId, Long membershipId, ResetMemberPasswordRequest request) {
|
|
IamMembership membership = required(tenantId, membershipId);
|
|
if ("OWNER".equals(membership.getMemberType())) {
|
|
throw new BizException(ResultCode.FORBIDDEN, "企业所有者只能自行修改密码");
|
|
}
|
|
IamUser user = requiredUser(membership.getUserId());
|
|
call(() -> authClient.reset(internalToken, user.getAuthUserId(),
|
|
new AuthAccountClient.PasswordRequest(request.newPassword())));
|
|
}
|
|
|
|
private MemberResponse response(long tenantId, IamMembership membership) {
|
|
IamUser user = requiredUser(membership.getUserId());
|
|
return response(tenantId, membership, user);
|
|
}
|
|
private MemberResponse response(long tenantId, IamMembership m, IamUser u) {
|
|
AuthAccountClient.AccountSummary account = accountSummary(u.getAuthUserId());
|
|
return new MemberResponse(m.getId(), u.getId(), u.getAuthUserId(), account.username(), u.getDisplayName(),
|
|
u.getPhone(), u.getEmail(), m.getMemberType(), m.getMemberNo(), m.getStatus(), account.lastLoginAt(),
|
|
repository.findRoleIds(tenantId, m.getId()));
|
|
}
|
|
private IamMembership required(long tenantId, Long membershipId) {
|
|
IamMembership value = repository.findByTenantAndId(tenantId, membershipId);
|
|
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "成员不存在");
|
|
return value;
|
|
}
|
|
private IamUser requiredUser(Long id) {
|
|
IamUser user = repository.findUser(id);
|
|
if (user == null) throw new BizException(ResultCode.NOT_FOUND, "成员档案不存在");
|
|
return user;
|
|
}
|
|
private void validateRoles(long tenantId, List<Long> ids) {
|
|
for (Long id : ids) {
|
|
IamRole role = roleMapper.selectById(id);
|
|
if (role == null || role.getTenantId() == null || role.getTenantId() != tenantId
|
|
|| !"MERCHANT".equals(role.getAppCode()) || !"ACTIVE".equals(role.getStatus())
|
|
|| "MERCHANT_OWNER".equals(role.getRoleCode()))
|
|
throw new BizException(ResultCode.FORBIDDEN, "禁止分配其他租户、其他应用或已停用角色");
|
|
}
|
|
}
|
|
private void requireGrantRoleIfChanged(List<Long> currentRoleIds, List<Long> requestedRoleIds,
|
|
Set<String> operatorPermissions) {
|
|
if (!new HashSet<>(currentRoleIds).equals(new HashSet<>(requestedRoleIds))
|
|
&& (operatorPermissions == null || !operatorPermissions.contains(GRANT_ROLE_PERMISSION))) {
|
|
throw new BizException(ResultCode.FORBIDDEN, "变更成员角色需要 merchant:member:grant-role 权限");
|
|
}
|
|
}
|
|
private void rejectOwnerMutation(String memberType) {
|
|
if ("OWNER".equals(memberType)) {
|
|
throw new BizException(ResultCode.FORBIDDEN, "不能通过成员管理接口创建或转让企业所有者");
|
|
}
|
|
if (!"EMPLOYEE".equals(memberType)) {
|
|
throw new BizException(ResultCode.BAD_REQUEST, "成员类型只能是 EMPLOYEE");
|
|
}
|
|
}
|
|
private void validateMemberNo(long tenantId, String memberNo, Long excludedId) {
|
|
if (repository.existsMemberNo(tenantId, memberNo, excludedId))
|
|
throw new BizException(ResultCode.CONFLICT, "企业成员编号已存在");
|
|
}
|
|
private <T> ApiResponse<T> call(RemoteCall<T> action) {
|
|
try {
|
|
ApiResponse<T> result = action.call();
|
|
if (result == null || !result.successful()) throw unavailable();
|
|
return result;
|
|
} catch (FeignException exception) {
|
|
if (exception.status() == 409) throw new BizException(ResultCode.CONFLICT, "登录账号已存在");
|
|
if (exception.status() == 404) throw new BizException(ResultCode.NOT_FOUND, "认证账号不存在");
|
|
throw unavailable();
|
|
}
|
|
}
|
|
private <T> T data(ApiResponse<T> value) { return value.data(); }
|
|
private BizException unavailable() { return new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用"); }
|
|
@FunctionalInterface private interface RemoteCall<T> { ApiResponse<T> call(); }
|
|
}
|