Files
baiyue-firstleg/iam/src/main/java/com/baiyue/iam/service/MemberService.java
T
2026-09-29 18:47:37 +08:00

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(); }
}