init: UserBaseService (backend + deploy + scripts)
ci-userbase-service / build-and-push (push) Successful in 40s

This commit is contained in:
chenxing
2026-08-10 14:50:22 +08:00
commit 4ec19486e0
234 changed files with 15604 additions and 0 deletions
@@ -0,0 +1,279 @@
package com.yunhai.userbase.service.impl;
import com.bestsch.utils.DException;
import com.yunhai.userbase.enums.IEnum;
import com.yunhai.userbase.enums.StatusEnum;
import com.yunhai.userbase.enums.SysRoleEnum;
import com.yunhai.userbase.enums.UserTypeEnum;
import com.yunhai.userbase.entity.SysUser;
import com.yunhai.userbase.entity.SysUserGroupRel;
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.repository.SysGroupRepository;
import com.yunhai.userbase.repository.SysUserAppRoleRelRepository;
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
import com.yunhai.userbase.repository.SysUserRepository;
import com.yunhai.userbase.service.ISysGroupService;
import com.yunhai.userbase.service.ISysUserService;
import com.yunhai.userbase.utils.ValidatorUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
import javax.transaction.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author chenx
* @date 2026/7/20 16:14
* @description 系统用户服务实现
*/
@Service
public class SysUserService implements ISysUserService {
/** 主归属标识:1=本职主学校/部门 */
private static final int IS_MAIN = 1;
/** 系统内置超级管理员账号(与 sys_user_init.sql 种子账号保持一致) */
private static final Set<String> SUPER_ADMIN_ACCOUNTS =
new HashSet<>(Arrays.asList("yunhaisch", "yunhaisch2", "yunhaisch3"));
@Autowired
private SysUserRepository sysUserRepository;
@Autowired
private SysUserGroupRelRepository sysUserGroupRelRepository;
@Autowired
private SysUserAppRoleRelRepository sysUserAppRoleRelRepository;
@Autowired
private SysGroupRepository sysGroupRepository;
@Autowired
private ISysGroupService sysGroupService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
@Transactional
public void add(SysUserRequest request) {
if (sysUserRepository.countByAccount(request.getAccount()) > 0) {
throw new DException("账号已存在");
}
if (!sysGroupRepository.existsById(request.getGroupId())) {
throw new DException("所属分组不存在");
}
validateProfile(request);
String encodedPassword = validateAndEncodePassword(request.getPassword(), request.getConfirmPassword());
SysUser entity = new SysUser();
entity.setAccount(request.getAccount());
entity.setPassword(encodedPassword);
entity.setRealName(request.getRealName());
entity.setPhone(request.getPhone());
entity.setBirthday(request.getBirthday());
entity.setIdcard(request.getIdcard());
entity.setProfilePicture(request.getProfilePicture());
entity.setRoles(request.getRoles());
// 用户类型推导:超级管理员账号固定为 1;其余含 ADMIN(7) 角色 -> 2 学校管理员;默认 0 普通用户
entity.setType(resolveUserType(entity.getAccount(), entity.getRoles()));
// disabled 用枚举存储:不传时默认 NORMAL(正常/未禁用)
entity.setDisabled(request.getDisabled() != null ? resolveStatus(request.getDisabled()) : StatusEnum.NORMAL);
entity.setExpired(request.getExpired() != null ? request.getExpired() : false);
sysUserRepository.save(entity);
// 写入组用户关联
SysUserGroupRel rel = new SysUserGroupRel();
rel.setUserId(entity.getId());
rel.setGroupId(request.getGroupId());
rel.setIsMain(IS_MAIN);
rel.setCreateTime(LocalDateTime.now());
sysUserGroupRelRepository.save(rel);
}
@Override
@Transactional
public void edit(Integer id, SysUserRequest request) {
if (sysUserRepository.countByAccountAndIdNot(request.getAccount(), id) > 0) {
throw new DException("账号已被其他用户使用");
}
SysUser entity = sysUserRepository.findById(id)
.orElseThrow(() -> new DException("系统用户不存在"));
validateProfile(request);
entity.setAccount(request.getAccount());
// 密码非空才修改:需两次一致 + 弱密码校验 + BCrypt 加密
if (StringUtils.isNotBlank(request.getPassword())) {
entity.setPassword(validateAndEncodePassword(request.getPassword(), request.getConfirmPassword()));
}
entity.setRealName(request.getRealName());
entity.setPhone(request.getPhone());
entity.setBirthday(request.getBirthday());
entity.setIdcard(request.getIdcard());
entity.setProfilePicture(request.getProfilePicture());
entity.setRoles(request.getRoles());
// 用户类型推导:仅当本次传入了角色时才重算,避免编辑时未传角色把 type 误重置为 0
if (request.getRoles() != null) {
entity.setType(resolveUserType(entity.getAccount(), entity.getRoles()));
}
if (request.getDisabled() != null) {
entity.setDisabled(resolveStatus(request.getDisabled()));
}
if (request.getExpired() != null) {
entity.setExpired(request.getExpired());
}
sysUserRepository.save(entity);
}
@Override
@Transactional
public void delete(Integer id) {
if (!sysUserRepository.existsById(id)) {
throw new DException("系统用户不存在");
}
// 先清理组用户关联、用户-角色绑定,再删除用户
sysUserGroupRelRepository.deleteByUserId(id);
// AI-FIX(角色授权): 删除用户时级联清理用户-业务角色绑定(逻辑外键无物理 FK,避免残留孤儿数据)
sysUserAppRoleRelRepository.deleteByUserIdBulk(id);
sysUserRepository.deleteById(id);
}
@Override
@Transactional
public void changeStatus(Integer id, boolean disabled) {
SysUser entity = sysUserRepository.findById(id)
.orElseThrow(() -> new DException("系统用户不存在"));
// 实体 disabled 字段为 StatusEnum(0=正常/未禁用,1=禁用):boolean 直接映射枚举
entity.setDisabled(disabled ? StatusEnum.DISABLED : StatusEnum.NORMAL);
sysUserRepository.save(entity);
}
@Override
public Page<SysUserVo> list(SysUserQuery query, Pageable pageable) {
Specification<SysUser> spec = buildSpecification(query);
return sysUserRepository.findAll(spec, pageable).map(this::convertToVo);
}
// ===================== 私有工具 =====================
/**
* 组装用户列表查询条件:组范围(含子组)+ 关键字(账号/姓名/身份证模糊、手机号精确)+ 启用状态。<br>
*/
private Specification<SysUser> buildSpecification(SysUserQuery query) {
return (root, cbQuery, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// 组范围过滤:用户ID 属于目标组集合下的关联记录
if (query.getGroupId() != null) {
List<Integer> groupIds = resolveGroupIds(query.getGroupId(), query.getWithSub());
Subquery<Integer> sub = cbQuery.subquery(Integer.class);
Root<SysUserGroupRel> relRoot = sub.from(SysUserGroupRel.class);
sub.select(relRoot.get("userId"));
sub.where(relRoot.get("groupId").in(groupIds));
predicates.add(root.get("id").in(sub));
}
// 关键字:账号/姓名/身份证模糊,手机号精确
if (StringUtils.isNotBlank(query.getName())) {
String keyword = query.getName().trim();
String like = "%" + keyword + "%";
Predicate byAccount = cb.like(root.get("account"), like);
Predicate byRealName = cb.like(root.get("realName"), like);
Predicate byIdcard = cb.like(root.get("idcard"), like);
Predicate byPhone = cb.equal(root.get("phone"), keyword);
predicates.add(cb.or(byAccount, byRealName, byIdcard, byPhone));
}
if (query.getDisabled() != null) {
// 实体 disabled 为 StatusEnum,查询条件需用枚举值比较(按 key 解析)
predicates.add(cb.equal(root.get("disabled"), IEnum.getByKey(StatusEnum.class, query.getDisabled())));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
/** 依据 withSub 决定组范围:1=含所有子孙组,否则仅当前组。 */
private List<Integer> resolveGroupIds(Integer groupId, Integer withSub) {
if (withSub != null && withSub == 1) {
return sysGroupService.listSelfAndDescendantIds(groupId);
}
List<Integer> single = new ArrayList<>();
single.add(groupId);
return single;
}
/** 校验手机号与身份证(有值才校验)。 */
private void validateProfile(SysUserRequest request) {
if (StringUtils.isNotBlank(request.getPhone()) && !ValidatorUtil.isPhone(request.getPhone())) {
throw new DException("手机号格式不正确");
}
if (StringUtils.isNotBlank(request.getIdcard()) && !ValidatorUtil.isIdCard(request.getIdcard())) {
throw new DException("身份证号不合法");
}
}
/**
* 校验两次密码一致性与弱密码规则,通过后返回 BCrypt 密文。<br>
*/
private String validateAndEncodePassword(String password, String confirmPassword) {
if (!StringUtils.equals(password, confirmPassword)) {
throw new DException("两次输入的密码不一致");
}
String weakReason = ValidatorUtil.checkPasswordStrength(password);
if (weakReason != null) {
throw new DException(weakReason);
}
return passwordEncoder.encode(password);
}
private SysUserVo convertToVo(SysUser entity) {
SysUserVo vo = new SysUserVo();
BeanUtils.copyProperties(entity, vo);
// disabled 为枚举,需要单独映射为 key(与 GroupTypeEnum 的 type 处理一致)
vo.setDisabled(entity.getDisabled() == null ? null : entity.getDisabled().getKey());
// type 为枚举,同样映射为 key
vo.setType(entity.getType() == null ? null : entity.getType().getKey());
return vo;
}
/**
* 推导用户类型(见 {@link UserTypeEnum}):
* - 内置超级管理员账号 -> 1 超级管理员;
* - 其余账号若拥有 ADMIN(7) 角色 -> 2 学校管理员;
* - 其他 -> 0 普通用户。
*/
private UserTypeEnum resolveUserType(String account, List<Integer> roles) {
if (account != null && SUPER_ADMIN_ACCOUNTS.contains(account)) {
return UserTypeEnum.SUPER_ADMIN;
}
if (roles != null && roles.contains(SysRoleEnum.ADMIN.getKey())) {
return UserTypeEnum.SCHOOL_ADMIN;
}
return UserTypeEnum.NORMAL;
}
/** 校验并返回用户状态枚举;非法状态抛异常。 */
private StatusEnum resolveStatus(Integer status) {
StatusEnum statusEnum = IEnum.getByKey(StatusEnum.class, status);
if (statusEnum == null) {
throw new DException("用户状态不存在");
}
return statusEnum;
}
}