@@ -11,19 +11,26 @@ import com.yunhai.userbase.enums.UserGroupRelStatusEnum;
|
||||
import com.yunhai.userbase.enums.UserTypeEnum;
|
||||
import com.yunhai.userbase.entity.SysUser;
|
||||
import com.yunhai.userbase.entity.SysUserAppRoleRel;
|
||||
import com.yunhai.userbase.entity.SysUserCurrentRole;
|
||||
import com.yunhai.userbase.entity.SysUserGroupRel;
|
||||
import com.yunhai.userbase.entity.SysUserRoleRel;
|
||||
import com.yunhai.userbase.model.sysuser.BatchImportResult;
|
||||
import com.yunhai.userbase.model.sysuser.SysUserGroupDeleteItem;
|
||||
import com.yunhai.userbase.model.sysuser.SysUserQuery;
|
||||
import com.yunhai.userbase.model.sysuser.SysUserRequest;
|
||||
import com.yunhai.userbase.model.sysuser.SysUserVo;
|
||||
import com.yunhai.userbase.model.sysuser.UserIdentityVo;
|
||||
import com.yunhai.userbase.repository.SysAppRoleRepository;
|
||||
import com.yunhai.userbase.repository.SysGroupRepository;
|
||||
import com.yunhai.userbase.repository.SysUserAppRoleRelRepository;
|
||||
import com.yunhai.userbase.repository.SysUserCurrentRoleRepository;
|
||||
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
|
||||
import com.yunhai.userbase.repository.SysUserRepository;
|
||||
import com.yunhai.userbase.repository.SysUserRoleRelRepository;
|
||||
import com.yunhai.userbase.service.ISysGroupService;
|
||||
import com.yunhai.userbase.service.ISysUserService;
|
||||
import com.yunhai.userbase.service.impl.IdentityService;
|
||||
import com.yunhai.userbase.service.resolve.MainGroupResolver;
|
||||
import com.yunhai.userbase.event.DataSyncEvent;
|
||||
import com.yunhai.userbase.common.context.CallerAppContext;
|
||||
import com.yunhai.userbase.model.push.PushInterfaceType;
|
||||
@@ -104,6 +111,18 @@ public class SysUserService implements ISysUserService {
|
||||
@Autowired
|
||||
private SysAppRoleRepository sysAppRoleRepository;
|
||||
|
||||
@Autowired
|
||||
private SysUserRoleRelRepository sysUserRoleRelRepository;
|
||||
|
||||
@Autowired
|
||||
private MainGroupResolver mainGroupResolver;
|
||||
|
||||
@Autowired
|
||||
private SysUserCurrentRoleRepository sysUserCurrentRoleRepository;
|
||||
|
||||
@Autowired
|
||||
private IdentityService identityService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@@ -115,7 +134,7 @@ public class SysUserService implements ISysUserService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户并写入组关联。
|
||||
* 创建用户并写入组关联与身份。
|
||||
*
|
||||
* @param enforcePasswordStrength 是否强制密码强度校验:
|
||||
* - 单条新增 = true(前端已交互式校验);
|
||||
@@ -143,9 +162,8 @@ public class SysUserService implements ISysUserService {
|
||||
entity.setEmail(request.getEmail());
|
||||
entity.setGender(request.getGender());
|
||||
entity.setProfilePicture(request.getProfilePicture());
|
||||
entity.setRoles(request.getRoles());
|
||||
// 用户类型推导:超级管理员账号固定为 1;其余含 ADMIN(7) 角色 -> 2 学校管理员;默认 0 普通用户
|
||||
entity.setType(resolveUserType(entity.getAccount(), entity.getRoles()));
|
||||
// roles 列停写(见《用户身份-学校维度落地方案》§8):身份改由 sys_user_role_rel 承载,
|
||||
// 新增用户身份仍必填(≥1),经下方 saveSchoolIdentity 落库
|
||||
// disabled 用枚举存储:不传时默认 NORMAL(正常/未禁用)
|
||||
entity.setDisabled(request.getDisabled() != null ? resolveStatus(request.getDisabled()) : StatusEnum.NORMAL);
|
||||
entity.setExpired(request.getExpired() != null ? request.getExpired() : false);
|
||||
@@ -163,6 +181,16 @@ public class SysUserService implements ISysUserService {
|
||||
rel.setCreateTime(LocalDateTime.now());
|
||||
sysUserGroupRelRepository.save(rel);
|
||||
|
||||
// 身份写入:schoolId 未传时按创建目标组解析(前端不感知解析逻辑)
|
||||
Integer schoolId = request.getSchoolId() != null
|
||||
? validateIdentitySchoolId(request.getSchoolId())
|
||||
: sysGroupService.resolveSchoolId(request.getGroupId());
|
||||
saveSchoolIdentity(saved.getId(), schoolId, request.getRoleIds());
|
||||
|
||||
// type 重算时序:先写身份再推导(数据源 = role_rel 正常态并集,超管账号特判保留)
|
||||
saved.setType(resolveUserType(saved.getAccount(), saved.getId()));
|
||||
sysUserRepository.save(saved);
|
||||
|
||||
return saved.getId();
|
||||
}
|
||||
|
||||
@@ -188,11 +216,7 @@ public class SysUserService implements ISysUserService {
|
||||
entity.setEmail(request.getEmail());
|
||||
entity.setGender(request.getGender());
|
||||
entity.setProfilePicture(request.getProfilePicture());
|
||||
entity.setRoles(request.getRoles());
|
||||
// 用户类型推导:仅当本次传入了角色时才重算,避免编辑时未传角色把 type 误重置为 0
|
||||
if (request.getRoles() != null) {
|
||||
entity.setType(resolveUserType(entity.getAccount(), entity.getRoles()));
|
||||
}
|
||||
// roles 列停写(见 §8):身份改由 role_rel 承载,type 一律在身份落库后重算(不再有 roles 守卫)
|
||||
if (request.getDisabled() != null) {
|
||||
entity.setDisabled(resolveStatus(request.getDisabled()));
|
||||
}
|
||||
@@ -202,6 +226,16 @@ public class SysUserService implements ISysUserService {
|
||||
entity.setUpdateUser(SYSTEM_USER_ID);
|
||||
sysUserRepository.save(entity);
|
||||
publishUserEvent(PushInterfaceType.UPDATE_USER, id);
|
||||
|
||||
// 身份按 schoolId 全量覆盖保存(编辑请求总是携带 {schoolId, roleIds},未修改也提交原值);
|
||||
// 未携带 schoolId 的调用(如旧版 oapi edit)跳过身份写库,保持原身份不变
|
||||
if (request.getSchoolId() != null) {
|
||||
saveSchoolIdentity(id, validateIdentitySchoolId(request.getSchoolId()), request.getRoleIds());
|
||||
}
|
||||
|
||||
// type 重算时序:身份落库之后按 role_rel 正常态并集推导(超管账号特判保留)
|
||||
entity.setType(resolveUserType(entity.getAccount(), id));
|
||||
sysUserRepository.save(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -242,9 +276,11 @@ public class SysUserService implements ISysUserService {
|
||||
for (Integer userId : userIds) {
|
||||
// 删除当前组关联
|
||||
sysUserGroupRelRepository.deleteByUserIdAndGroupId(userId, groupId);
|
||||
// 若用户已无其它组关联,则级联删除用户本身及其角色绑定
|
||||
// 若用户已无其它组关联,则级联删除用户本身、业务角色绑定,并清理身份行与当前激活身份(防幽灵身份/残留切换记录)
|
||||
if (sysUserGroupRelRepository.countByUserId(userId) == 0) {
|
||||
sysUserAppRoleRelRepository.deleteByUserIdBulk(userId);
|
||||
sysUserRoleRelRepository.deleteByUserIdBulk(userId);
|
||||
sysUserCurrentRoleRepository.deleteByUserIdBulk(userId);
|
||||
sysUserRepository.deleteById(userId);
|
||||
}
|
||||
}
|
||||
@@ -383,46 +419,66 @@ public class SysUserService implements ISysUserService {
|
||||
}
|
||||
// 源学校必须真实存在且为学校组(目标学校在 assertGroupWithinSchool 内一并校验)
|
||||
sysGroupService.assertSchoolId(fromSchoolId);
|
||||
if (fromSchoolId.equals(toSchoolId)) {
|
||||
throw new DException("原学校与目标学校不能相同");
|
||||
}
|
||||
if (!sysUserRepository.existsById(userId)) {
|
||||
throw new DException("系统用户不存在");
|
||||
}
|
||||
// 目标组必须属于目标学校子树且不等于学校根组(复用移动/删除的边界校验)
|
||||
assertGroupWithinSchool(groupId, toSchoolId);
|
||||
|
||||
boolean sameSchool = fromSchoolId.equals(toSchoolId);
|
||||
List<Integer> fromScope = sysGroupService.listSelfAndDescendantIds(fromSchoolId);
|
||||
List<Integer> toScope = sysGroupService.listSelfAndDescendantIds(toSchoolId);
|
||||
List<SysUserGroupRel> rels = sysUserGroupRelRepository.findByUserId(userId);
|
||||
|
||||
// ① 前置:用户在源学校范围必须存在至少一条「转出」状态的组关系
|
||||
boolean hasTransferOut = rels.stream().anyMatch(r ->
|
||||
r.getStatus() == UserGroupRelStatusEnum.TRANSFERRED_OUT && fromScope.contains(r.getGroupId()));
|
||||
if (!hasTransferOut) {
|
||||
// ① 前置:用户在源学校范围必须存在至少一条「转出」状态的组关系(原组由转出标记隐含携带)
|
||||
List<SysUserGroupRel> transferOutRels = rels.stream()
|
||||
.filter(r -> r.getStatus() == UserGroupRelStatusEnum.TRANSFERRED_OUT && fromScope.contains(r.getGroupId()))
|
||||
.collect(Collectors.toList());
|
||||
if (transferOutRels.isEmpty()) {
|
||||
throw new DException("该用户在原学校不存在转出状态的组关系,禁止转入");
|
||||
}
|
||||
// ② 目标学校范围不得已有组关系(转入语义为进入全新学校,避免覆盖歧义)
|
||||
boolean existsInTarget = rels.stream().anyMatch(r -> toScope.contains(r.getGroupId()));
|
||||
if (existsInTarget) {
|
||||
throw new DException("用户已在该学校存在组关系,禁止重复转入");
|
||||
|
||||
List<SysUserGroupRel> toDelete;
|
||||
int newRelIsMain;
|
||||
if (sameSchool) {
|
||||
// 同校转组:仅当原组(源校转出组)与目标组一致才拒绝;否则删除原组关系、新增目标组关系(同校其他组与他校关系保留)
|
||||
boolean sameGroup = transferOutRels.stream().anyMatch(r -> r.getGroupId().equals(groupId));
|
||||
if (sameGroup) {
|
||||
throw new DException("原学校与目标学校相同且目标组与原组相同,请指定其他目标组");
|
||||
}
|
||||
// 目标组不得已有任何关系(任意状态),避免重复/合并歧义
|
||||
boolean existsInTargetGroup = rels.stream().anyMatch(r -> r.getGroupId().equals(groupId));
|
||||
if (existsInTargetGroup) {
|
||||
throw new DException("用户已在该组存在组关系,禁止重复转入");
|
||||
}
|
||||
toDelete = transferOutRels;
|
||||
// 主归属继承原组:原组是主组则目标组为主组,否则维持非主,避免同校场景制造双主组
|
||||
newRelIsMain = transferOutRels.stream().anyMatch(r -> Objects.equals(r.getIsMain(), IS_MAIN)) ? IS_MAIN : 0;
|
||||
} else {
|
||||
// 跨校转入(原逻辑不变):目标学校范围不得已有组关系,删除源校范围全部关系,目标组设为主归属
|
||||
boolean existsInTarget = rels.stream().anyMatch(r -> toScope.contains(r.getGroupId()));
|
||||
if (existsInTarget) {
|
||||
throw new DException("用户已在该学校存在组关系,禁止重复转入");
|
||||
}
|
||||
toDelete = rels.stream()
|
||||
.filter(r -> fromScope.contains(r.getGroupId()))
|
||||
.collect(Collectors.toList());
|
||||
newRelIsMain = IS_MAIN;
|
||||
}
|
||||
// ③ 删除源学校范围内全部关系行(仅源学校,其他学校保留)
|
||||
List<SysUserGroupRel> toDelete = rels.stream()
|
||||
.filter(r -> fromScope.contains(r.getGroupId()))
|
||||
.collect(Collectors.toList());
|
||||
if (!toDelete.isEmpty()) {
|
||||
sysUserGroupRelRepository.deleteAll(toDelete);
|
||||
}
|
||||
// ④ 建立目标学校关系:目标组设为主归属,状态恢复正常(转出流程结束)
|
||||
// 建立目标组关系:状态恢复正常(转出流程结束)
|
||||
SysUserGroupRel rel = new SysUserGroupRel();
|
||||
rel.setUserId(userId);
|
||||
rel.setGroupId(groupId);
|
||||
rel.setIsMain(IS_MAIN);
|
||||
rel.setIsMain(newRelIsMain);
|
||||
rel.setStatus(UserGroupRelStatusEnum.NORMAL);
|
||||
rel.setCreateTime(LocalDateTime.now());
|
||||
sysUserGroupRelRepository.save(rel);
|
||||
// ⑤ 推送编辑用户订阅
|
||||
// 转入/转组后身份即时对齐:源校组关系已物理删除,旧校身份行清出集合(主身份为读时计算,无物化列需重指向)
|
||||
alignIdentity(userId);
|
||||
// 推送编辑用户订阅
|
||||
publishUserEvent(PushInterfaceType.UPDATE_USER, userId);
|
||||
}
|
||||
|
||||
@@ -431,12 +487,47 @@ public class SysUserService implements ISysUserService {
|
||||
// 组上下文(选中具体组):走原生 SQL 按「组内最小 sort_no」排序的专用分页,
|
||||
// 规避 MySQL 的 DISTINCT+ORDER BY 限制以及 Hibernate 不支持 ORDER BY 标量子查询的限制。
|
||||
if (query.getGroupId() != null) {
|
||||
return listByGroup(query, pageable);
|
||||
return attachMainIdentityNames(listByGroup(query, pageable));
|
||||
}
|
||||
// 根节点 / 未选组:用 Specification 过滤,按主键 id 升序(聚簇索引顺序,无 filesort)。
|
||||
Specification<SysUser> spec = buildSpecification(query);
|
||||
Page<SysUser> page = sysUserRepository.findAll(spec, pageable);
|
||||
return page.map(u -> convertToVo(u, null, null));
|
||||
return attachMainIdentityNames(page.map(u -> convertToVo(u, null, null)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按页批量带出各用户主校身份名(动态解析主身份,无 N+1)。
|
||||
* roles 列已停写(冻结历史值),列表「用户身份」列一律切此新字段展示。
|
||||
*/
|
||||
private Page<SysUserVo> attachMainIdentityNames(Page<SysUserVo> page) {
|
||||
List<SysUserVo> content = page.getContent();
|
||||
if (content.isEmpty()) {
|
||||
return page;
|
||||
}
|
||||
List<Integer> userIds = content.stream()
|
||||
.map(SysUserVo::getId)
|
||||
.collect(Collectors.toList());
|
||||
Map<Integer, SysUserRoleRel> mainRelsByUser = resolveMainRoleRelsByUser(userIds);
|
||||
for (SysUserVo vo : content) {
|
||||
SysUserRoleRel mainRel = mainRelsByUser.get(vo.getId());
|
||||
if (mainRel == null) {
|
||||
continue;
|
||||
}
|
||||
SysRoleEnum e = IEnum.getByKey(SysRoleEnum.class, mainRel.getRoleId());
|
||||
String name = e != null ? e.getValue() : null;
|
||||
if (name != null) {
|
||||
vo.setIdentityNames(name);
|
||||
}
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量解析 用户 -> 主身份行(动态解析,每用户至多 1 条)映射。
|
||||
* 供列表页主校身份名({@link #attachMainIdentityNames})与导出「用户身份」列共用。
|
||||
*/
|
||||
private Map<Integer, SysUserRoleRel> resolveMainRoleRelsByUser(List<Integer> userIds) {
|
||||
return identityService.resolveMainIdentityByUser(userIds);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -497,15 +588,142 @@ public class SysUserService implements ISysUserService {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ===================== 用户身份(SysRoleEnum × 学校) =====================
|
||||
|
||||
@Override
|
||||
public UserIdentityVo getUserIdentity(Integer userId, Integer groupId) {
|
||||
if (userId == null) {
|
||||
throw new DException("用户ID不能为空");
|
||||
}
|
||||
if (groupId == null) {
|
||||
throw new DException("组ID不能为空");
|
||||
}
|
||||
// 后端按 §3 规则解析 schoolId:同一学校多组返回同一 schoolId 与角色并集
|
||||
Integer schoolId = sysGroupService.resolveSchoolId(groupId);
|
||||
List<Integer> roleIds = sysUserRoleRelRepository
|
||||
.findByUserIdAndSchoolIdAndStatus(userId, schoolId, StatusEnum.NORMAL)
|
||||
.stream()
|
||||
.map(SysUserRoleRel::getRoleId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
UserIdentityVo vo = new UserIdentityVo();
|
||||
vo.setSchoolId(schoolId);
|
||||
vo.setRoleIds(roleIds);
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验保存身份的 schoolId:非空且必须是存在的组 id。
|
||||
*/
|
||||
private Integer validateIdentitySchoolId(Integer schoolId) {
|
||||
if (schoolId == null) {
|
||||
throw new DException("学校ID不能为空");
|
||||
}
|
||||
if (!sysGroupRepository.existsById(schoolId)) {
|
||||
throw new DException("学校ID必须为合法组ID");
|
||||
}
|
||||
return schoolId;
|
||||
}
|
||||
|
||||
/** 校验身份角色在 SysRoleEnum 内;非法角色抛异常。 */
|
||||
private void validateRoleIds(List<Integer> roleIds) {
|
||||
if (roleIds == null) {
|
||||
return;
|
||||
}
|
||||
for (Integer roleId : roleIds) {
|
||||
if (roleId == null || IEnum.getByKey(SysRoleEnum.class, roleId) == null) {
|
||||
throw new DException("用户身份不存在:" + roleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 schoolId 全量覆盖保存用户身份:删除该 (userId, schoolId) 全部行 + 按 roleIds 重建
|
||||
* (uk(user_id, role_id, school_id) 防重)。空数组/空列表语义 = 清空该校身份(幂等)。<br>
|
||||
* 保存完成后统一对齐(清理学校集合外身份行 + 激活身份滞留检查)。主身份已去字段化,不再写 is_main。
|
||||
*/
|
||||
private void saveSchoolIdentity(Integer userId, Integer schoolId, List<Integer> roleIds) {
|
||||
validateRoleIds(roleIds);
|
||||
sysUserRoleRelRepository.deleteByUserIdAndSchoolId(userId, schoolId);
|
||||
if (roleIds != null && !roleIds.isEmpty()) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<SysUserRoleRel> rows = new ArrayList<>(roleIds.size());
|
||||
// roleIds 去重,避免 uk 冲突
|
||||
for (Integer roleId : roleIds.stream().filter(Objects::nonNull).distinct()
|
||||
.collect(Collectors.toList())) {
|
||||
SysUserRoleRel rel = new SysUserRoleRel();
|
||||
rel.setUserId(userId);
|
||||
rel.setRoleId(roleId);
|
||||
rel.setSchoolId(schoolId);
|
||||
rel.setSortNo(0);
|
||||
rel.setStatus(StatusEnum.NORMAL);
|
||||
rel.setCreateTime(now);
|
||||
rel.setUpdateTime(now);
|
||||
rows.add(rel);
|
||||
}
|
||||
sysUserRoleRelRepository.saveAll(rows);
|
||||
}
|
||||
alignIdentity(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 身份统一对齐(保存后与改组三入口 add/edit/changeGroups/transferToSchool 共用):<br>
|
||||
* ① 学校集合推导:全部组关系(含「转出」标记行——软标记期保留原校身份)逐条解析 schoolId 去重;<br>
|
||||
* ② 删除不在集合内的身份行(孤儿清理:退组/转校后身份行同步清出);<br>
|
||||
* ③ 当前激活身份滞留检查:切换记录指向的学校已不在集合(退组/转学)→ 删除该记录,
|
||||
* 避免 getCurrent/boa.school 继续返回旧校(「激活身份不滞留旧校」)。<br>
|
||||
* 主身份已去字段化(读时计算),此处不再维护 is_main 物化列。
|
||||
*/
|
||||
private void alignIdentity(Integer userId) {
|
||||
Set<Integer> schoolSet = deriveSchoolSet(userId);
|
||||
|
||||
List<SysUserRoleRel> rels = sysUserRoleRelRepository.findByUserId(userId);
|
||||
List<SysUserRoleRel> remaining = new ArrayList<>(rels.size());
|
||||
for (SysUserRoleRel rel : rels) {
|
||||
if (schoolSet.contains(rel.getSchoolId())) {
|
||||
remaining.add(rel);
|
||||
} else {
|
||||
sysUserRoleRelRepository.delete(rel);
|
||||
}
|
||||
}
|
||||
if (!remaining.isEmpty()) {
|
||||
sysUserRoleRelRepository.saveAll(remaining);
|
||||
}
|
||||
|
||||
// 激活身份滞留检查:切换记录指向的学校已不在集合 → 删除(schoolId 为 null 的旧约定行不在此列)
|
||||
sysUserCurrentRoleRepository.findByUserId(userId).ifPresent(current -> {
|
||||
if (current.getSchoolId() != null && !schoolSet.contains(current.getSchoolId())) {
|
||||
sysUserCurrentRoleRepository.delete(current);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 学校集合推导(身份对齐/孤儿清理的数据基准):取该用户全部组关系逐条按 §3 解析 schoolId 去重。<br>
|
||||
* 不过滤 {@code sys_user_group_rel.status}:「转出」仅是流程标记,转入完成(transferToSchool
|
||||
* 物理删除源校关系)后才视为真正离开;标记期内保留原校身份与业务现状一致。
|
||||
*/
|
||||
private Set<Integer> deriveSchoolSet(Integer userId) {
|
||||
Set<Integer> set = new HashSet<>();
|
||||
for (SysUserGroupRel rel : sysUserGroupRelRepository.findByUserId(userId)) {
|
||||
set.add(sysGroupService.resolveSchoolId(rel.getGroupId()));
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void changeGroups(List<Integer> userIds, List<Integer> groupIds) {
|
||||
public void changeGroups(List<Integer> userIds, List<Integer> groupIds, Integer mainGroupId) {
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
throw new DException("待调整用户不能为空");
|
||||
}
|
||||
if (groupIds == null || groupIds.isEmpty()) {
|
||||
throw new DException("请至少保留一个组");
|
||||
}
|
||||
// 显式主组校验:必须落在目标组列表中(否则 400)
|
||||
if (mainGroupId != null && !groupIds.contains(mainGroupId)) {
|
||||
throw new DException("主归属组必须在目标组列表中");
|
||||
}
|
||||
|
||||
List<SysUser> users = sysUserRepository.findAllById(userIds);
|
||||
if (users.size() != userIds.size()) {
|
||||
@@ -518,7 +736,7 @@ public class SysUserService implements ISysUserService {
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (Integer userId : userIds) {
|
||||
// 覆盖式重建前先读旧关系,用于继承组内状态(转出标记):
|
||||
// 覆盖式重建前先读旧关系,用于继承组内状态(转出标记)与旧主组解析:
|
||||
// 行级继承——同组照抄旧状态(管理端重设组不误改);用户级继承——新增组在用户处于转出流程中(存在任一转出行)时继承「转出」
|
||||
List<SysUserGroupRel> oldRels = sysUserGroupRelRepository.findByUserId(userId);
|
||||
Map<Integer, UserGroupRelStatusEnum> oldStatusByGroup = new HashMap<>();
|
||||
@@ -529,14 +747,16 @@ public class SysUserService implements ISysUserService {
|
||||
inTransferFlow = true;
|
||||
}
|
||||
}
|
||||
// 主归属组三级降级:① 显式 mainGroupId → ② 旧主组 ∈ 新集合沿用 → ③ groupIds 首项
|
||||
Integer effectiveMainGroupId = decideEffectiveMainGroupId(mainGroupId, groupIds,
|
||||
mainGroupResolver.resolve(oldRels));
|
||||
sysUserGroupRelRepository.deleteByUserId(userId);
|
||||
List<SysUserGroupRel> rels = new ArrayList<>(groupIds.size());
|
||||
for (int i = 0; i < groupIds.size(); i++) {
|
||||
Integer groupId = groupIds.get(i);
|
||||
for (Integer groupId : groupIds) {
|
||||
SysUserGroupRel rel = new SysUserGroupRel();
|
||||
rel.setUserId(userId);
|
||||
rel.setGroupId(groupId);
|
||||
rel.setIsMain(i == 0 ? IS_MAIN : 0);
|
||||
rel.setIsMain(Objects.equals(groupId, effectiveMainGroupId) ? IS_MAIN : 0);
|
||||
rel.setCreateTime(now);
|
||||
rel.setStatus(oldStatusByGroup.containsKey(groupId)
|
||||
? oldStatusByGroup.get(groupId)
|
||||
@@ -544,10 +764,30 @@ public class SysUserService implements ISysUserService {
|
||||
rels.add(rel);
|
||||
}
|
||||
sysUserGroupRelRepository.saveAll(rels);
|
||||
// 改组后身份即时对齐:学校集合变化可能清掉旧校身份行,主校变更后动态主身份随之切换,
|
||||
// 消除「组已变更而激活身份仍指旧校」窗口期(跨校转学场景依赖此兜底)
|
||||
alignIdentity(userId);
|
||||
}
|
||||
publishUserEvent(PushInterfaceType.UPDATE_USER, userIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主归属组三级降级决策(纯函数,便于单测):<br>
|
||||
* ① explicitMainGroupId 非空 → 直接返回(调用方已校验 ∈ groupIds);<br>
|
||||
* ② oldMainRel 非空且其组 ∈ groupIds → 沿用旧主组(只要用户没把主组勾掉,主组不动);<br>
|
||||
* ③ 否则 → groupIds 首项(现状行为兜底)。<br>
|
||||
* groupIds 非空由调用方保证。
|
||||
*/
|
||||
static Integer decideEffectiveMainGroupId(Integer explicitMainGroupId, List<Integer> groupIds, SysUserGroupRel oldMainRel) {
|
||||
if (explicitMainGroupId != null) {
|
||||
return explicitMainGroupId;
|
||||
}
|
||||
if (oldMainRel != null && groupIds.contains(oldMainRel.getGroupId())) {
|
||||
return oldMainRel.getGroupId();
|
||||
}
|
||||
return groupIds.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void moveUserWithinSchool(Integer userId, Integer groupId, Integer schoolId) {
|
||||
@@ -580,8 +820,8 @@ public class SysUserService implements ISysUserService {
|
||||
throw new DException("目标组不属于该学校范围,禁止跨校移动");
|
||||
}
|
||||
|
||||
// 复用覆盖式改组逻辑(目标设为主组)
|
||||
changeGroups(Collections.singletonList(userId), Collections.singletonList(groupId));
|
||||
// 复用覆盖式改组逻辑(单组场景:目标组即主组)
|
||||
changeGroups(Collections.singletonList(userId), Collections.singletonList(groupId), groupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -734,6 +974,9 @@ public class SysUserService implements ISysUserService {
|
||||
// 2. 批量取每个用户的主组(isMain=1),用于填充「组名 / 组id」参考列
|
||||
Map<Integer, Integer> userMainGroup = resolveUserMainGroup(userIds);
|
||||
|
||||
// 2.1 批量取每个用户的主校身份行(动态解析主身份),用于填充「用户身份」列(roles 列已停写,不再作为真源)
|
||||
Map<Integer, SysUserRoleRel> mainRelsByUser = resolveMainRoleRelsByUser(userIds);
|
||||
|
||||
// 3. 批量取组名
|
||||
Map<Integer, String> groupNameMap = sysGroupRepository.findAllById(userMainGroup.values())
|
||||
.stream().collect(Collectors.toMap(SysGroup::getId, SysGroup::getName));
|
||||
@@ -752,7 +995,8 @@ public class SysUserService implements ISysUserService {
|
||||
row.setPhone(u.getPhone());
|
||||
row.setEmail(u.getEmail());
|
||||
row.setNature(u.getType() == null ? null : u.getType().getKey());
|
||||
row.setIdentity(u.getRoles() == null || u.getRoles().isEmpty() ? null : u.getRoles().get(0));
|
||||
SysUserRoleRel mainRel = mainRelsByUser.get(u.getId());
|
||||
row.setIdentity(mainRel == null ? null : mainRel.getRoleId());
|
||||
row.setGender(u.getGender());
|
||||
row.setBirthday(u.getBirthday() == null ? "" : u.getBirthday().toString());
|
||||
row.setUserId(u.getId());
|
||||
@@ -767,24 +1011,15 @@ public class SysUserService implements ISysUserService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量解析 用户 -> 主组(groupId) 映射:优先取 isMain=1 的组,否则取第一个关联组。
|
||||
* 批量解析 用户 -> 主组(groupId) 映射:isMain=1 取 id 最小,无主兜底 id 最小(统一走 MainGroupResolver)。
|
||||
*/
|
||||
private Map<Integer, Integer> resolveUserMainGroup(List<Integer> userIds) {
|
||||
Map<Integer, Integer> mainGroup = new HashMap<>();
|
||||
if (userIds.isEmpty()) {
|
||||
return mainGroup;
|
||||
}
|
||||
List<SysUserGroupRel> rels = sysUserGroupRelRepository.findByUserIdIn(userIds);
|
||||
Map<Integer, List<SysUserGroupRel>> byUser = rels.stream()
|
||||
.collect(Collectors.groupingBy(SysUserGroupRel::getUserId));
|
||||
for (Map.Entry<Integer, List<SysUserGroupRel>> e : byUser.entrySet()) {
|
||||
Integer gid = e.getValue().stream()
|
||||
.filter(r -> r.getIsMain() != null && r.getIsMain() == IS_MAIN)
|
||||
.map(SysUserGroupRel::getGroupId)
|
||||
.findFirst()
|
||||
.orElse(e.getValue().get(0).getGroupId());
|
||||
mainGroup.put(e.getKey(), gid);
|
||||
}
|
||||
mainGroupResolver.resolveByUserIds(userIds)
|
||||
.forEach((userId, rel) -> mainGroup.put(userId, rel.getGroupId()));
|
||||
return mainGroup;
|
||||
}
|
||||
|
||||
@@ -910,43 +1145,47 @@ public class SysUserService implements ISysUserService {
|
||||
if (userId == null) {
|
||||
throw new DException("用户ID不能为空");
|
||||
}
|
||||
List<BoaRoleInfo> result = new ArrayList<>();
|
||||
// 主归属组(学校/部门)上下文:schId/schName
|
||||
Integer mainGroupId = null;
|
||||
String mainGroupName = null;
|
||||
List<SysUserGroupRel> groupRels = sysUserGroupRelRepository.findByUserId(userId);
|
||||
for (SysUserGroupRel gr : groupRels) {
|
||||
if (gr.getIsMain() != null && gr.getIsMain() == IS_MAIN) {
|
||||
mainGroupId = gr.getGroupId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mainGroupId != null) {
|
||||
SysGroup mainGroup = sysGroupRepository.findById(mainGroupId).orElse(null);
|
||||
if (mainGroup != null) {
|
||||
mainGroupName = mainGroup.getName();
|
||||
}
|
||||
}
|
||||
// 业务角色:用户-角色关系 → 角色名称
|
||||
List<SysUserAppRoleRel> rels = sysUserAppRoleRelRepository.findByUserId(userId);
|
||||
for (SysUserAppRoleRel rel : rels) {
|
||||
if (rel.getRoleId() == null) {
|
||||
continue;
|
||||
}
|
||||
SysAppRole role = sysAppRoleRepository.findById(rel.getRoleId()).orElse(null);
|
||||
if (role == null) {
|
||||
continue;
|
||||
}
|
||||
// 数据源:sys_user_role_rel(平台身份角色权威源),内存排序 主身份优先 → sortNo → id
|
||||
// (修正:此前误读 sys_user_app_role_rel 业务权限角色 + 主归属组当学校,导致「本组」bug)
|
||||
List<SysUserRoleRel> rels = sysUserRoleRelRepository.findByUserIdAndStatus(userId, StatusEnum.NORMAL);
|
||||
SysUserRoleRel mainRel = identityService.resolveMainIdentity(userId);
|
||||
List<SysUserRoleRel> sorted = rels.stream()
|
||||
.sorted(Comparator.comparingInt((SysUserRoleRel r) ->
|
||||
mainRel != null && Objects.equals(r.getId(), mainRel.getId()) ? 0 : 1)
|
||||
.thenComparingInt(SysUserRoleRel::getSortNo)
|
||||
.thenComparingInt(SysUserRoleRel::getId))
|
||||
.collect(Collectors.toList());
|
||||
List<BoaRoleInfo> result = new ArrayList<>(sorted.size());
|
||||
for (SysUserRoleRel rel : sorted) {
|
||||
BoaRoleInfo info = new BoaRoleInfo();
|
||||
info.setRoleId(role.getId());
|
||||
info.setRoleName(role.getName());
|
||||
info.setSchId(mainGroupId);
|
||||
info.setSchName(mainGroupName);
|
||||
info.setRoleId(rel.getRoleId());
|
||||
info.setRoleName(roleName(rel.getRoleId()));
|
||||
info.setSchId(rel.getSchoolId());
|
||||
info.setSchName(groupName(rel.getSchoolId()));
|
||||
// childId/childName 家长-孩子维度本期不做,恒空
|
||||
result.add(info);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 由角色 id 反查角色名(SysRoleEnum.value);未知角色返回 null。 */
|
||||
private String roleName(Integer roleId) {
|
||||
if (roleId == null) {
|
||||
return null;
|
||||
}
|
||||
SysRoleEnum e = IEnum.getByKey(SysRoleEnum.class, roleId);
|
||||
return e != null ? e.getValue() : null;
|
||||
}
|
||||
|
||||
/** 由组 id 反查组名;未知/非正 id 返回 null。 */
|
||||
private String groupName(Integer groupId) {
|
||||
if (groupId == null || groupId <= 0) {
|
||||
return null;
|
||||
}
|
||||
SysGroup g = sysGroupRepository.findById(groupId).orElse(null);
|
||||
return g != null ? g.getName() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void changePassword(Integer userId, String oldPassword, String newPassword) {
|
||||
@@ -1011,14 +1250,17 @@ public class SysUserService implements ISysUserService {
|
||||
/**
|
||||
* 推导用户类型(见 {@link UserTypeEnum}):
|
||||
* - 内置超级管理员账号 -> 1 超级管理员;
|
||||
* - 其余账号若拥有 ADMIN(7) 角色 -> 2 学校管理员;
|
||||
* - 其余账号 sys_user_role_rel 正常态 roleId 并集含 ADMIN(7) -> 2 学校管理员;
|
||||
* - 其他 -> 0 普通用户。
|
||||
* 数据源为 role_rel(sys_user.roles 列已停写),须在身份落库之后调用。
|
||||
*/
|
||||
private UserTypeEnum resolveUserType(String account, List<Integer> roles) {
|
||||
private UserTypeEnum resolveUserType(String account, Integer userId) {
|
||||
if (account != null && SUPER_ADMIN_ACCOUNTS.contains(account)) {
|
||||
return UserTypeEnum.SUPER_ADMIN;
|
||||
}
|
||||
if (roles != null && roles.contains(SysRoleEnum.ADMIN.getKey())) {
|
||||
List<Integer> roleIds = sysUserRoleRelRepository
|
||||
.findDistinctRoleIdsByUserIdAndStatus(userId, StatusEnum.NORMAL);
|
||||
if (roleIds != null && roleIds.contains(SysRoleEnum.ADMIN.getKey())) {
|
||||
return UserTypeEnum.SCHOOL_ADMIN;
|
||||
}
|
||||
return UserTypeEnum.NORMAL;
|
||||
@@ -1090,7 +1332,8 @@ public class SysUserService implements ISysUserService {
|
||||
|
||||
Integer nature = parseInteger(getCellString(row, IDX_NATURE));
|
||||
Integer identity = parseInteger(getCellString(row, IDX_IDENTITY));
|
||||
request.setRoles(buildImportRoles(nature, identity));
|
||||
// 身份列改走身份写入:createUser 按导入目标组解析 schoolId 落 sys_user_role_rel(roles 列停写)
|
||||
request.setRoleIds(buildImportRoles(nature, identity));
|
||||
|
||||
request.setGender(parseGender(getCellString(row, IDX_GENDER)));
|
||||
request.setBirthday(parseBirthday(row.getCell(IDX_BIRTHDAY)));
|
||||
|
||||
Reference in New Issue
Block a user