Commit d043ba58 by Lizh

调整用户登录,用户添加等功能

parent d0367d25
......@@ -15,4 +15,5 @@
CLAUDE.md
docs
plans
logs
package com.jomalls.custom.app.constant;
/**
* @Author: Lizh
* @Date: 2026/7/15 15:27
* @Description: 角色常量
* @Version: 1.0
*/
public class RoleConstants {
/**
* 超级管理员admin 名称
*/
public static final String ADMIN_USER = "admin";
/**
* 角色状态
*/
public static final String STATUS_NORMAL = "0";
}
......@@ -111,4 +111,25 @@ public class UserConstants {
*/
public static final int PASSWORD_MIN_LENGTH = 5;
public static final int PASSWORD_MAX_LENGTH = 20;
/**
* 初始化密码长度
*/
public static final int INIT_PASSWORD_LENGTH = 8;
/**
* 黑名单key
*/
public static final String BLACK_IP_KEY = "sys.login.blackIPList";
/**
* 是否修改过初始化密码
*/
public static final String INIT_PWD_MODIFY_KEY = "sys.account.initPasswordModify";
/**
* 密码过期
*/
public static final String PWD_VALIDATE_KEY = "sys.account.passwordValidateDays";
}
......@@ -10,6 +10,7 @@ import lombok.Getter;
*/
@Getter
public enum SysUserStatusEnums {
NORMAL("0", "正常"),
DELETED("2", "已被删除"),
DISABLED("1", "已被停用");
......
......@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jomalls.custom.app.dto.system.*;
import com.jomalls.custom.app.vo.system.SysDeptVO;
import com.jomalls.custom.app.vo.system.SysRoleVO;
import com.jomalls.custom.app.vo.system.SysUserVO;
import java.util.List;
......@@ -112,12 +113,21 @@ public interface SysUserService {
void updatePwd(SysUserUpdatePwdDTO dto);
/**
* 查询用户已分配的角色ID列表
* 查询用户详情(仅返回用户 VO,不含角色/岗位)
*
* @param userId 用户ID
* @return 角色ID列表
* @return 用户 VO
*/
List<Long> getAuthRoleList(Long userId);
SysUserVO getById(Long userId);
/**
* 查询用户已分配的角色列表(与 custom-back selectRolesByUserId + selectRoleAll 对齐)
* <p>返回全部角色,已分配给用户的角色设置 flag=true
*
* @param userId 用户ID
* @return 角色 VO 列表(含 flag 标识)
*/
List<SysRoleVO> getAuthRoleList(Long userId);
/**
* 批量插入用户角色关联
......
......@@ -9,10 +9,13 @@ import com.jomalls.custom.app.utils.BeanMapper;
import com.jomalls.custom.app.vo.system.SysDeptVO;
import com.jomalls.custom.dal.entity.SysDeptEntity;
import com.jomalls.custom.domain.service.system.SysDeptDomainService;
import com.jomalls.custom.security.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
......@@ -69,8 +72,18 @@ public class SysDeptServiceImpl implements SysDeptService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void add(SysDeptSaveDTO dto) {
SysDeptEntity dept = BeanMapper.mapper().convert(dto, SysDeptEntity.class);
// 检查部门名称唯一性
long count = sysDeptDomainService.count(
new LambdaQueryWrapper<SysDeptEntity>()
.eq(SysDeptEntity::getDeptName, dept.getDeptName())
.eq(SysDeptEntity::getParentId, dept.getParentId() != null ? dept.getParentId() : 0L)
.ne(dept.getDeptId() != null, SysDeptEntity::getDeptId, dept.getDeptId() != null ? dept.getDeptId() : -1L));
if (count > 0) {
throw new ServiceException("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
// 设置祖级列表
if (dept.getParentId() == null || dept.getParentId() == 0L) {
dept.setAncestors("0");
......@@ -79,8 +92,14 @@ public class SysDeptServiceImpl implements SysDeptService {
if (parentDept == null) {
throw new ServiceException("父部门不存在");
}
dept.setAncestors(parentDept.getAncestors() + "," + dept.getParentId());
if ("1".equals(parentDept.getStatus())) {
throw new ServiceException("父部门已停用,不允许新增子部门");
}
dept.setAncestors(parentDept.getAncestors() != null ? parentDept.getAncestors() + "," + parentDept.getDeptId() : String.valueOf(parentDept.getDeptId()));
}
// 设置审计字段
dept.setCreateBy(SecurityUtils.getUsername());
dept.setCreateTime(LocalDateTime.now());
boolean saved = sysDeptDomainService.save(dept);
if (saved) {
log.info("新增部门成功: deptName={}", dept.getDeptName());
......@@ -88,13 +107,85 @@ public class SysDeptServiceImpl implements SysDeptService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void edit(SysDeptUpdateDTO dto) {
SysDeptEntity dept = BeanMapper.mapper().convert(dto, SysDeptEntity.class);
// 检查部门名称唯一性(排除自身)
long count = sysDeptDomainService.count(
new LambdaQueryWrapper<SysDeptEntity>()
.eq(SysDeptEntity::getDeptName, dept.getDeptName())
.eq(SysDeptEntity::getParentId, dept.getParentId() != null ? dept.getParentId() : 0L)
.ne(dept.getDeptId() != null, SysDeptEntity::getDeptId, dept.getDeptId() != null ? dept.getDeptId() : -1L));
if (count > 0) {
throw new ServiceException("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
// 获取旧部门信息
SysDeptEntity oldDept = sysDeptDomainService.getById(dept.getDeptId());
if (oldDept == null) {
throw new ServiceException("部门不存在");
}
String oldAncestors = oldDept.getAncestors();
// 计算新的祖级列表
boolean parentChanged = dept.getParentId() != null && !dept.getParentId().equals(oldDept.getParentId());
if (parentChanged) {
String newAncestors;
if (dept.getParentId() == 0L) {
newAncestors = "0";
} else {
SysDeptEntity newParent = sysDeptDomainService.getById(dept.getParentId());
if (newParent == null) {
throw new ServiceException("父部门不存在");
}
if ("1".equals(newParent.getStatus())) {
throw new ServiceException("父部门已停用,不允许移动子部门");
}
newAncestors = newParent.getAncestors() + "," + newParent.getDeptId();
}
dept.setAncestors(newAncestors);
} else {
// 父部门未变更,保持原有祖级列表
dept.setAncestors(oldAncestors);
}
// 设置审计字段
dept.setUpdateBy(SecurityUtils.getUsername());
dept.setUpdateTime(LocalDateTime.now());
// 更新部门
sysDeptDomainService.updateById(dept);
// 更新子部门 ancestors(仅父部门变更时)
if (parentChanged) {
String oldPrefix = oldAncestors + "," + dept.getDeptId();
String newPrefix = dept.getAncestors() + "," + dept.getDeptId();
List<SysDeptEntity> descendants = sysDeptDomainService.list(
new LambdaQueryWrapper<SysDeptEntity>()
.likeRight(SysDeptEntity::getAncestors, oldPrefix));
for (SysDeptEntity descendant : descendants) {
descendant.setAncestors(
descendant.getAncestors().replace(oldPrefix, newPrefix));
sysDeptDomainService.updateById(descendant);
}
}
// 如果部门状态启用,更新父部门链状态为正常
if ("0".equals(dept.getStatus()) && dept.getAncestors() != null) {
String[] ancestorIds = dept.getAncestors().split(",");
for (String ancestorId : ancestorIds) {
if (ancestorId.isEmpty() || "0".equals(ancestorId)) continue;
SysDeptEntity ancestor = new SysDeptEntity();
ancestor.setDeptId(Long.parseLong(ancestorId));
ancestor.setStatus("0");
sysDeptDomainService.updateById(ancestor);
}
}
log.info("修改部门成功: deptId={}", dept.getDeptId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void remove(Long deptId) {
// 检查是否有子部门
if (sysDeptDomainService.hasChildByDeptId(deptId)) {
......
......@@ -21,12 +21,15 @@ import com.jomalls.custom.domain.service.system.SysUserDomainService;
import com.jomalls.custom.security.LoginUser;
import com.jomalls.custom.security.SecurityUtils;
import com.jomalls.custom.security.TokenHandle;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.Nullable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.*;
import java.util.stream.Collectors;
......@@ -52,7 +55,7 @@ public class SysLoginServiceImpl implements SysLoginService {
private final StringRedisTemplate stringRedisTemplate;
/**
* 校验验证码(与 custom-back SysLoginService.validateCaptcha 对齐)
* 校验验证码
*
* @param username 用户名
* @param code 用户输入的验证码
......@@ -66,7 +69,7 @@ public class SysLoginServiceImpl implements SysLoginService {
}
// 验证码未开启则不校验
if (!StringUtils.hasText(uuid)) {
sysLogininforService.recordLogininfor(username, "1", "验证码参数缺失");
sysLogininforService.recordLogininfor(username, UserConstants.EXCEPTION, "验证码参数缺失");
throw new ServiceException("验证码参数缺失");
}
if (stringRedisTemplate == null) {
......@@ -75,44 +78,44 @@ public class SysLoginServiceImpl implements SysLoginService {
String verifyKey = "captcha_codes:" + uuid;
String captcha = stringRedisTemplate.opsForValue().get(verifyKey);
if (captcha == null) {
sysLogininforService.recordLogininfor(username, "1", "验证码已失效");
sysLogininforService.recordLogininfor(username, UserConstants.EXCEPTION, "验证码已失效");
throw new ServiceException("验证码已失效");
}
// 删除 Redis 中的验证码(一次性使用)
stringRedisTemplate.delete(verifyKey);
if (!captcha.equalsIgnoreCase(code)) {
sysLogininforService.recordLogininfor(username, "1", "验证码错误");
sysLogininforService.recordLogininfor(username, UserConstants.EXCEPTION, "验证码错误");
throw new ServiceException("验证码错误");
}
}
/**
* 登录前置校验(与 custom-back SysLoginService.loginPreCheck 对齐)
* 登录前置校验
* <p>
* 校验项:用户名/密码长度、IP 黑名单
*/
private void loginPreCheck(String username, String password) {
// 用户名或密码为空
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
sysLogininforService.recordLogininfor(username, "1", "用户名或密码不能为空");
sysLogininforService.recordLogininfor(username, UserConstants.EXCEPTION, "用户名或密码不能为空");
throw new ServiceException("用户名或密码不能为空");
}
// 密码长度校验(与 custom-back PASSWORD_MIN_LENGTH=5, PASSWORD_MAX_LENGTH=20 对齐)
// 密码长度校验
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
sysLogininforService.recordLogininfor(username, "1", "密码长度不在允许范围内");
sysLogininforService.recordLogininfor(username, UserConstants.EXCEPTION, "密码长度不在允许范围内");
throw new ServiceException("密码长度不正确");
}
// 用户名长度校验(与 custom-back USERNAME_MIN_LENGTH=2, USERNAME_MAX_LENGTH=20 对齐)
// 用户名长度校验
if (username.length() < UserConstants.USERNAME_MIN_LENGTH || username.length() > UserConstants.USERNAME_MAX_LENGTH) {
sysLogininforService.recordLogininfor(username, "1", "用户名长度不在允许范围内");
sysLogininforService.recordLogininfor(username, UserConstants.EXCEPTION, "用户名长度不在允许范围内");
throw new ServiceException("用户名长度不正确");
}
// IP 黑名单校验(与 custom-back 对齐)
String blackIpList = sysConfigService.selectConfigByKey("sys.login.blackIPList");
String blackIpList = sysConfigService.selectConfigByKey(UserConstants.BLACK_IP_KEY);
if (StringUtils.hasText(blackIpList)) {
String currentIp = getCurrentIp();
if (isIpMatched(blackIpList, currentIp)) {
sysLogininforService.recordLogininfor(username, "1", "IP 已被列入黑名单");
sysLogininforService.recordLogininfor(username, UserConstants.EXCEPTION, "IP 已被列入黑名单");
throw new ServiceException("IP 已被列入黑名单,请联系管理员");
}
}
......@@ -120,7 +123,7 @@ public class SysLoginServiceImpl implements SysLoginService {
@Override
public String login(String username, String password, String code, String uuid) {
// 验证码校验(与 custom-back validateCaptcha 对齐)
// 验证码校验
validateCaptcha(username, code, uuid);
// 登录前置校验
......@@ -130,10 +133,8 @@ public class SysLoginServiceImpl implements SysLoginService {
sysPasswordService.checkLocked(username);
// 1. 查询用户
SysUserEntity user = sysUserDomainService.getOne(
new LambdaQueryWrapper<SysUserEntity>()
.eq(SysUserEntity::getUserName, username)
);
SysUserEntity user = sysUserDomainService.getOne(new LambdaQueryWrapper<SysUserEntity>()
.eq(SysUserEntity::getUserName, username));
if (user == null) {
log.warn("登录失败:用户[{}]不存在", username);
throw new ServiceException("用户不存在");
......@@ -147,13 +148,13 @@ public class SysLoginServiceImpl implements SysLoginService {
throw new ServiceException("用户已被停用");
}
// 2. 验证密码(MD5,与 custom-back 兼容
// 2. 验证密码(MD5)
if (!passwordEncoder.matches(password, user.getPassword())) {
log.warn("用户[{}]密码验证失败", username);
// 记录登录失败重试次数
sysPasswordService.recordLoginFailure(username);
// 记录登录日志
sysLogininforService.recordLogininfor(username, "1", "密码错误");
sysLogininforService.recordLogininfor(username, UserConstants.EXCEPTION, "密码错误");
throw new ServiceException("密码错误");
}
......@@ -162,7 +163,7 @@ public class SysLoginServiceImpl implements SysLoginService {
// 4. 密码正确,清除重试计数并记录登录日志
sysPasswordService.clearLoginRecord(username);
sysLogininforService.recordLogininfor(username, "0", "登录成功");
sysLogininforService.recordLogininfor(username, UserConstants.NORMAL, "登录成功");
// 5. 构建 LoginUser
LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), username);
......@@ -185,7 +186,7 @@ public class SysLoginServiceImpl implements SysLoginService {
}
/**
* 记录登录信息(登录IP和登录时间,与 custom-back recordLoginInfo 对齐
* 记录登录信息(登录IP和登录时间)
*/
private void recordLoginInfo(SysUserEntity user) {
try {
......@@ -239,15 +240,15 @@ public class SysLoginServiceImpl implements SysLoginService {
}
result.setPermissions(dbPermissions);
// 初始密码修改状态(与 custom-back SysLoginController.getInfo 对齐)
// 初始密码修改状态
// 与 custom-back initPasswordIsModify 对齐:initPasswordModify == 1 且 pwdUpdateDate == null
String initPwdModify = sysConfigService.selectConfigByKey("sys.account.initPasswordModify");
String initPwdModify = sysConfigService.selectConfigByKey(UserConstants.INIT_PWD_MODIFY_KEY);
result.setIsDefaultModifyPwd("1".equals(initPwdModify) && user.getPwdUpdateDate() == null);
// 密码过期状态
// 读取 sys.account.passwordValidateDays 配置,大于 0 时检查是否超期
boolean passwordExpired = false;
Integer passwordValidateDays = Convert.toInt(sysConfigService.selectConfigByKey("sys.account.passwordValidateDays"));
Integer passwordValidateDays = Convert.toInt(sysConfigService.selectConfigByKey(UserConstants.PWD_VALIDATE_KEY));
if (passwordValidateDays != null && passwordValidateDays > 0) {
if (user.getPwdUpdateDate() == null) {
// 如果从未修改过初始密码,直接提醒过期
......@@ -280,14 +281,14 @@ public class SysLoginServiceImpl implements SysLoginService {
}
/**
* 构建前端路由菜单(与 custom-back SysMenuService.buildMenus 完全对齐)
* 构建前端路由菜单
*/
private List<RouterVO> buildRouterTree(Long parentId, List<SysMenuEntity> allMenus) {
List<SysMenuEntity> menus = allMenus.stream()
.filter(menu -> !"F".equals(menu.getMenuType()))
.filter(menu -> menu.getParentId().equals(parentId))
.sorted(Comparator.comparingInt(m -> m.getOrderNum() != null ? m.getOrderNum() : 0))
.collect(Collectors.toList());
.toList();
List<RouterVO> routers = new ArrayList<>();
for (SysMenuEntity menu : menus) {
......@@ -337,16 +338,18 @@ public class SysLoginServiceImpl implements SysLoginService {
return routers;
}
// ========== 路由构建辅助方法(与 custom-back SysMenuService 对齐) ==========
private String getRouteName(SysMenuEntity menu) {
if (isMenuFrame(menu)) return "";
if (isMenuFrame(menu)) {
return "";
}
return getRouteName(menu.getRouteName(), menu.getPath());
}
private String getRouteName(String name, String path) {
String routerName = StringUtils.hasText(name) ? name : path;
if (!StringUtils.hasText(routerName)) return "";
if (!StringUtils.hasText(routerName)) {
return "";
}
return routerName.substring(0, 1).toUpperCase() + (routerName.length() > 1 ? routerName.substring(1) : "");
}
......@@ -392,7 +395,9 @@ public class SysLoginServiceImpl implements SysLoginService {
}
private String innerLinkReplaceEach(String path) {
if (path == null) return "";
if (path == null) {
return "";
}
return path.replace("http://", "").replace("https://", "")
.replace("www.", "").replace(".", "/").replace(":", "/");
}
......@@ -415,7 +420,16 @@ public class SysLoginServiceImpl implements SysLoginService {
org.springframework.web.context.request.ServletRequestAttributes attributes =
(org.springframework.web.context.request.ServletRequestAttributes) org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
if (attributes != null) {
jakarta.servlet.http.HttpServletRequest request = attributes.getRequest();
return getString(attributes);
}
} catch (Exception e) {
log.debug("获取客户端 IP 失败: {}", e.getMessage());
}
return "";
}
private static String getString(ServletRequestAttributes attributes) {
HttpServletRequest request = attributes.getRequest();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
......@@ -428,11 +442,6 @@ public class SysLoginServiceImpl implements SysLoginService {
}
return ip;
}
} catch (Exception e) {
log.debug("获取客户端 IP 失败: {}", e.getMessage());
}
return "";
}
/**
* 检查 IP 是否匹配黑名单(与 custom-back IpUtils.isMatchedIp 对齐)
......
......@@ -9,12 +9,16 @@ import com.jomalls.custom.app.utils.BeanMapper;
import com.jomalls.custom.app.vo.system.SysMenuVO;
import com.jomalls.custom.app.vo.system.TreeSelectVO;
import com.jomalls.custom.dal.entity.SysMenuEntity;
import com.jomalls.custom.dal.entity.SysRoleEntity;
import com.jomalls.custom.dal.entity.SysRoleMenuEntity;
import com.jomalls.custom.domain.service.system.SysMenuDomainService;
import com.jomalls.custom.domain.service.system.SysRoleDomainService;
import com.jomalls.custom.domain.service.system.SysRoleMenuDomainService;
import com.jomalls.custom.security.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.*;
......@@ -32,6 +36,7 @@ public class SysMenuServiceImpl implements SysMenuService {
private final SysMenuDomainService sysMenuDomainService;
private final SysRoleMenuDomainService sysRoleMenuDomainService;
private final SysRoleDomainService sysRoleDomainService;
@Override
public List<SysMenuVO> selectMenuList(SysMenuVO menu) {
......@@ -70,23 +75,67 @@ public class SysMenuServiceImpl implements SysMenuService {
List<Long> checkedKeys = roleMenus.stream()
.map(SysRoleMenuEntity::getMenuId)
.collect(Collectors.toList());
// 如果菜单树选择项不关联显示(menuCheckStrictly=false),
// 则包含直接分配的菜单及其所有子菜单的ID
SysRoleEntity role = sysRoleDomainService.getById(roleId);
if (role != null && role.getMenuCheckStrictly() != null && role.getMenuCheckStrictly() == 0) {
Set<Long> allCheckedKeys = new HashSet<>(checkedKeys);
List<SysMenuEntity> allMenus = sysMenuDomainService.list();
for (Long menuId : checkedKeys) {
addAllDescendantIds(menuId, allMenus, allCheckedKeys);
}
checkedKeys = new ArrayList<>(allCheckedKeys);
}
Map<String, Object> result = new HashMap<>();
result.put("menus", menus);
result.put("checkedKeys", checkedKeys);
return result;
}
/**
* 递归添加所有子菜单ID
*/
private void addAllDescendantIds(Long parentId, List<SysMenuEntity> allMenus, Set<Long> result) {
for (SysMenuEntity menu : allMenus) {
if (parentId.equals(menu.getParentId())) {
result.add(menu.getMenuId());
addAllDescendantIds(menu.getMenuId(), allMenus, result);
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void add(SysMenuSaveDTO dto) {
SysMenuEntity menu = BeanMapper.mapper().convert(dto, SysMenuEntity.class);
// 检查菜单名称唯一性(同级菜单下名称不能重复)
long nameCount = sysMenuDomainService.count(
new LambdaQueryWrapper<SysMenuEntity>()
.eq(SysMenuEntity::getMenuName, menu.getMenuName())
.eq(menu.getParentId() != null, SysMenuEntity::getParentId, menu.getParentId()));
if (nameCount > 0) {
throw new ServiceException("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
}
menu.setCreateBy(SecurityUtils.getUsername());
menu.setCreateTime(new Date());
sysMenuDomainService.save(menu);
log.info("新增菜单成功: menuName={}", menu.getMenuName());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void edit(SysMenuUpdateDTO dto) {
SysMenuEntity menu = BeanMapper.mapper().convert(dto, SysMenuEntity.class);
// 检查菜单名称唯一性(排除自身)
long nameCount = sysMenuDomainService.count(
new LambdaQueryWrapper<SysMenuEntity>()
.eq(SysMenuEntity::getMenuName, menu.getMenuName())
.eq(menu.getParentId() != null, SysMenuEntity::getParentId, menu.getParentId())
.ne(SysMenuEntity::getMenuId, menu.getMenuId()));
if (nameCount > 0) {
throw new ServiceException("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
}
menu.setUpdateBy(SecurityUtils.getUsername());
menu.setUpdateTime(new Date());
sysMenuDomainService.updateById(menu);
log.info("修改菜单成功: menuId={}", menu.getMenuId());
......@@ -107,6 +156,7 @@ public class SysMenuServiceImpl implements SysMenuService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void remove(Long menuId) {
// 检查是否有子菜单(controller 层已做检查,此处双重保障)
long childCount = sysMenuDomainService.count(
......@@ -121,8 +171,6 @@ public class SysMenuServiceImpl implements SysMenuService {
log.info("删除菜单成功: menuId={}", menuId);
}
// ==================== 私有辅助方法 ====================
/**
* 构建菜单树选择列表
*/
......
......@@ -11,7 +11,7 @@ import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 密码服务(密码重试锁定,与 custom-back SysPasswordService 对齐
* 密码服务(密码重试锁定)
*
* @author Lizh
*/
......
......@@ -10,12 +10,18 @@ import com.jomalls.custom.app.service.system.SysPostService;
import com.jomalls.custom.app.utils.BeanMapper;
import com.jomalls.custom.app.vo.system.SysPostVO;
import com.jomalls.custom.dal.entity.SysPostEntity;
import com.jomalls.custom.dal.entity.SysUserPostEntity;
import com.jomalls.custom.domain.service.system.SysPostDomainService;
import com.jomalls.custom.domain.service.system.SysUserPostDomainService;
import com.jomalls.custom.security.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
/**
......@@ -29,10 +35,14 @@ import java.util.List;
public class SysPostServiceImpl implements SysPostService {
private final SysPostDomainService sysPostDomainService;
private final SysUserPostDomainService sysUserPostDomainService;
@Override
public List<SysPostVO> listAllPosts() {
List<SysPostEntity> entities = sysPostDomainService.list();
// 与 custom-back selectPostAll 对齐:返回全部岗位(无过滤),按 postSort 排序
List<SysPostEntity> entities = sysPostDomainService.list(
new LambdaQueryWrapper<SysPostEntity>()
.orderByAsc(SysPostEntity::getPostSort));
return BeanMapper.mapper().convertList(entities, SysPostVO.class);
}
......@@ -61,6 +71,7 @@ public class SysPostServiceImpl implements SysPostService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void add(SysPostSaveDTO dto) {
SysPostEntity post = BeanMapper.mapper().convert(dto, SysPostEntity.class);
// 检查岗位编码唯一性
......@@ -69,6 +80,14 @@ public class SysPostServiceImpl implements SysPostService {
if (count > 0) {
throw new ServiceException("岗位编码已存在");
}
// 检查岗位名称唯一性
long nameCount = sysPostDomainService.count(
new LambdaQueryWrapper<SysPostEntity>().eq(SysPostEntity::getPostName, post.getPostName()));
if (nameCount > 0) {
throw new ServiceException("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在");
}
post.setCreateBy(SecurityUtils.getUsername());
post.setCreateTime(LocalDateTime.now());
boolean saved = sysPostDomainService.save(post);
if (saved) {
log.info("新增岗位成功: postName={}", post.getPostName());
......@@ -76,6 +95,7 @@ public class SysPostServiceImpl implements SysPostService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void edit(SysPostUpdateDTO dto) {
SysPostEntity post = BeanMapper.mapper().convert(dto, SysPostEntity.class);
// 检查岗位编码唯一性(排除自身)
......@@ -86,13 +106,31 @@ public class SysPostServiceImpl implements SysPostService {
if (count > 0) {
throw new ServiceException("岗位编码已存在");
}
// 检查岗位名称唯一性(排除自身)
long nameCount = sysPostDomainService.count(
new LambdaQueryWrapper<SysPostEntity>()
.eq(SysPostEntity::getPostName, post.getPostName())
.ne(post.getPostId() != null, SysPostEntity::getPostId, post.getPostId() != null ? post.getPostId() : -1L));
if (nameCount > 0) {
throw new ServiceException("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在");
}
post.setUpdateBy(SecurityUtils.getUsername());
post.setUpdateTime(LocalDateTime.now());
sysPostDomainService.updateById(post);
log.info("修改岗位成功: postId={}", post.getPostId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void remove(Long[] postIds) {
for (Long postId : postIds) {
// 检查岗位下是否有用户关联(有用户关联则不允许删除)
long userCount = sysUserPostDomainService.count(
new LambdaQueryWrapper<SysUserPostEntity>().eq(SysUserPostEntity::getPostId, postId));
if (userCount > 0) {
SysPostEntity post = sysPostDomainService.getById(postId);
throw new ServiceException("岗位'" + (post != null ? post.getPostName() : postId) + "'已分配用户,不能删除");
}
sysPostDomainService.removeById(postId);
}
log.info("批量删除岗位成功: count={}", postIds.length);
......
......@@ -3,6 +3,7 @@ package com.jomalls.custom.app.service.system.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jomalls.custom.app.constant.RoleConstants;
import com.jomalls.custom.app.dto.system.SysRoleDataScopeDTO;
import com.jomalls.custom.app.dto.system.SysRoleSaveDTO;
import com.jomalls.custom.app.dto.system.SysRoleStatusDTO;
......@@ -15,9 +16,11 @@ import com.jomalls.custom.app.vo.system.SysRoleVO;
import com.jomalls.custom.app.vo.system.SysUserVO;
import com.jomalls.custom.dal.entity.*;
import com.jomalls.custom.domain.service.system.*;
import com.jomalls.custom.security.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.*;
......@@ -42,7 +45,11 @@ public class SysRoleServiceImpl implements SysRoleService {
@Override
public List<SysRoleVO> listAllRoles() {
List<SysRoleEntity> entities = sysRoleDomainService.list();
// 过滤 delFlag='0',按 roleSort 排序
List<SysRoleEntity> entities = sysRoleDomainService.list(
new LambdaQueryWrapper<SysRoleEntity>()
.eq(SysRoleEntity::getDelFlag, RoleConstants.STATUS_NORMAL)
.orderByAsc(SysRoleEntity::getRoleSort));
return BeanMapper.mapper().convertList(entities, SysRoleVO.class);
}
......@@ -58,7 +65,7 @@ public class SysRoleServiceImpl implements SysRoleService {
if (StringUtils.hasText(role.getStatus())) {
wrapper.eq(SysRoleEntity::getStatus, role.getStatus());
}
wrapper.eq(SysRoleEntity::getDelFlag, "0");
wrapper.eq(SysRoleEntity::getDelFlag, RoleConstants.STATUS_NORMAL);
wrapper.orderByAsc(SysRoleEntity::getRoleSort);
IPage<SysRoleEntity> entityPage = sysRoleDomainService.page(
new Page<>(page.getCurrent(), page.getSize()), wrapper);
......@@ -72,6 +79,7 @@ public class SysRoleServiceImpl implements SysRoleService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void add(SysRoleSaveDTO dto) {
SysRoleEntity role = BeanMapper.mapper().convert(dto, SysRoleEntity.class);
// 检查角色名称唯一性
......@@ -86,10 +94,14 @@ public class SysRoleServiceImpl implements SysRoleService {
if (countByKey > 0) {
throw new ServiceException("角色权限字符串已存在");
}
role.setCreateBy(SecurityUtils.getUsername());
role.setCreateTime(new Date());
// 保存角色
sysRoleDomainService.save(role);
Long roleId = role.getRoleId();
// 更新角色权限字符串为包含角色ID的格式(始终覆盖)
role.setRoleKey("role_" + roleId);
sysRoleDomainService.updateById(role);
// 保存角色菜单关联
if (dto.getMenuIds() != null) {
for (Long menuId : dto.getMenuIds()) {
......@@ -103,8 +115,14 @@ public class SysRoleServiceImpl implements SysRoleService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void edit(SysRoleUpdateDTO dto) {
SysRoleEntity role = BeanMapper.mapper().convert(dto, SysRoleEntity.class);
// 检查是否允许操作超级管理员角色
SysRoleEntity existingRole = sysRoleDomainService.getById(role.getRoleId());
if (existingRole != null) {
checkRoleAllowed(existingRole);
}
// 检查角色名称唯一性(排除自身)
long countByName = sysRoleDomainService.count(
new LambdaQueryWrapper<SysRoleEntity>()
......@@ -121,6 +139,7 @@ public class SysRoleServiceImpl implements SysRoleService {
if (countByKey > 0) {
throw new ServiceException("角色权限字符串已存在");
}
role.setUpdateBy(SecurityUtils.getUsername());
role.setUpdateTime(new Date());
// 更新角色
sysRoleDomainService.updateById(role);
......@@ -139,10 +158,18 @@ public class SysRoleServiceImpl implements SysRoleService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void dataScope(SysRoleDataScopeDTO dto) {
SysRoleEntity role = new SysRoleEntity();
role.setRoleId(dto.getRoleId());
// 检查是否允许操作超级管理员角色
SysRoleEntity existingRole = sysRoleDomainService.getById(dto.getRoleId());
if (existingRole != null) {
checkRoleAllowed(existingRole);
}
role.setDataScope(dto.getDataScope());
role.setUpdateBy(SecurityUtils.getUsername());
role.setUpdateTime(new Date());
// 更新角色信息
sysRoleDomainService.updateById(role);
// 删除旧的部门关联
......@@ -166,23 +193,35 @@ public class SysRoleServiceImpl implements SysRoleService {
if (role == null) {
throw new ServiceException("角色不存在");
}
checkRoleAllowed(role);
role.setStatus(dto.getStatus());
role.setUpdateBy(SecurityUtils.getUsername());
role.setUpdateTime(new Date());
sysRoleDomainService.updateById(role);
log.info("角色状态已修改: roleId={}, status={}", dto.getRoleId(), dto.getStatus());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void remove(Long[] roleIds) {
for (Long roleId : roleIds) {
// 检查是否允许操作超级管理员角色
SysRoleEntity role = sysRoleDomainService.getById(roleId);
if (role != null) {
checkRoleAllowed(role);
}
// 检查是否有用户关联(有用户关联则不允许删除)
long userCount = sysUserRoleDomainService.count(
new LambdaQueryWrapper<SysUserRoleEntity>().eq(SysUserRoleEntity::getRoleId, roleId));
if (userCount > 0) {
throw new ServiceException("角色'" + (role != null ? role.getRoleName() : roleId) + "'已分配用户,不能删除");
}
// 删除角色菜单关联
sysRoleMenuDomainService.remove(
new LambdaQueryWrapper<SysRoleMenuEntity>().eq(SysRoleMenuEntity::getRoleId, roleId));
// 删除角色部门关联
sysRoleDeptDomainService.remove(
new LambdaQueryWrapper<SysRoleDeptEntity>().eq(SysRoleDeptEntity::getRoleId, roleId));
// 删除用户角色关联
sysUserRoleDomainService.remove(
new LambdaQueryWrapper<SysUserRoleEntity>().eq(SysUserRoleEntity::getRoleId, roleId));
// 删除角色
sysRoleDomainService.removeById(roleId);
}
......@@ -274,4 +313,13 @@ public class SysRoleServiceImpl implements SysRoleService {
List<SysDeptEntity> tree = sysDeptDomainService.buildDeptTree(allDepts);
return BeanMapper.mapper().convertList(tree, SysDeptVO.class);
}
/**
* 检查是否允许操作超级管理员角色
*/
private void checkRoleAllowed(SysRoleEntity role) {
if (role.getRoleId() != null && (role.getRoleId() == 1L || "admin".equals(role.getRoleKey()))) {
throw new ServiceException("不允许操作超级管理员角色");
}
}
}
......@@ -3,11 +3,19 @@ package com.jomalls.custom.app.service.system.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jomalls.custom.app.constant.RoleConstants;
import com.jomalls.custom.app.constant.SecurityConstants;
import com.jomalls.custom.app.constant.UserConstants;
import com.jomalls.custom.app.dto.system.*;
import com.jomalls.custom.app.enums.SysUserStatusEnums;
import com.jomalls.custom.app.exception.ServiceException;
import com.jomalls.custom.app.service.system.SysRoleService;
import com.jomalls.custom.app.service.system.SysUserService;
import com.jomalls.custom.app.utils.BeanMapper;
import com.jomalls.custom.app.utils.CharsetKit;
import com.jomalls.custom.app.utils.UniqueId;
import com.jomalls.custom.app.vo.system.SysDeptVO;
import com.jomalls.custom.app.vo.system.SysRoleVO;
import com.jomalls.custom.app.vo.system.SysUserVO;
import com.jomalls.custom.dal.entity.*;
import com.jomalls.custom.domain.service.system.*;
......@@ -15,8 +23,10 @@ import com.jomalls.custom.security.LoginUser;
import com.jomalls.custom.security.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.*;
......@@ -36,6 +46,7 @@ public class SysUserServiceImpl implements SysUserService {
private final SysUserRoleDomainService sysUserRoleDomainService;
private final SysUserPostDomainService sysUserPostDomainService;
private final SysRoleDomainService sysRoleDomainService;
private final SysRoleService sysRoleService;
private final SysDeptDomainService sysDeptDomainService;
private final SysPostDomainService sysPostDomainService;
private final PasswordEncoder passwordEncoder;
......@@ -55,7 +66,7 @@ public class SysUserServiceImpl implements SysUserService {
if (user.getDeptId() != null) {
wrapper.eq(SysUserEntity::getDeptId, user.getDeptId());
}
wrapper.eq(SysUserEntity::getDelFlag, "0");
wrapper.eq(SysUserEntity::getDelFlag, SysUserStatusEnums.NORMAL.getCode());
wrapper.orderByAsc(SysUserEntity::getUserId);
IPage<SysUserEntity> entityPage = sysUserDomainService.page(
new Page<>(page.getCurrent(), page.getSize()), wrapper);
......@@ -70,76 +81,139 @@ public class SysUserServiceImpl implements SysUserService {
}
// Entity → VO
SysUserVO userVO = BeanMapper.mapper().convert(user, SysUserVO.class);
// 查询角色列表
List<SysRoleEntity> roles = sysRoleDomainService.selectRoleListByUserId(userId);
// 查询角色ID列表
List<SysUserRoleEntity> userRoles = sysUserRoleDomainService.list(
new LambdaQueryWrapper<SysUserRoleEntity>().eq(SysUserRoleEntity::getUserId, userId));
List<Long> roleIds = userRoles.stream().map(SysUserRoleEntity::getRoleId).toList();
// 查询岗位ID列表
List<SysUserPostEntity> userPosts = sysUserPostDomainService.list(
new LambdaQueryWrapper<SysUserPostEntity>().eq(SysUserPostEntity::getUserId, userId));
List<Long> postIds = userPosts.stream()
.map(SysUserPostEntity::getPostId)
.collect(Collectors.toList());
List<Long> postIds = userPosts.stream().map(SysUserPostEntity::getPostId).toList();
Map<String, Object> result = new HashMap<>();
result.put("user", userVO);
result.put("roles", roles != null ? roles : Collections.emptyList());
result.put("roleIds", roleIds);
result.put("postIds", postIds);
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String add(SysUserSaveDTO dto) {
// DTO → Entity
SysUserEntity user = BeanMapper.mapper().convert(dto, SysUserEntity.class);
// 检查用户名唯一性
if (!StringUtils.hasText(user.getUserName())) {
throw new ServiceException("新增用户'" + user.getUserName() + "'失败,登录账号不能为空");
}
if (CharsetKit.containsChinese(user.getUserName())) {
throw new ServiceException("新增用户'" + user.getUserName() + "'失败,登录账号请使用英文字符");
}
// 用户名校验
long count = sysUserDomainService.count(
new LambdaQueryWrapper<SysUserEntity>().eq(SysUserEntity::getUserName, user.getUserName()));
if (count > 0) {
throw new ServiceException("用户账号已存在");
throw new ServiceException("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
}
// 如果未提供密码,设置默认密码
// 手机号唯一性校验
if (StringUtils.hasText(user.getPhonenumber())) {
long phoneCount = sysUserDomainService.count(
new LambdaQueryWrapper<SysUserEntity>().eq(SysUserEntity::getPhonenumber, user.getPhonenumber()));
if (phoneCount > 0) {
throw new ServiceException("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
}
}
// 邮箱唯一性校验
if (StringUtils.hasText(user.getEmail())) {
long emailCount = sysUserDomainService.count(
new LambdaQueryWrapper<SysUserEntity>().eq(SysUserEntity::getEmail, user.getEmail()));
if (emailCount > 0) {
throw new ServiceException("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
}
}
// 生成随机密码
String originalPassword = user.getPassword();
if (!StringUtils.hasText(originalPassword)) {
originalPassword = "123456";
user.setPassword(originalPassword);
originalPassword = UniqueId.generateUniqueId(UserConstants.INIT_PASSWORD_LENGTH);
}
// 密码加密
user.setPassword(passwordEncoder.encode(user.getPassword()));
// MD5 加密
user.setPassword(passwordEncoder.encode(originalPassword));
user.setCreateTime(new Date());
user.setCreateBy(SecurityUtils.getUsername());
// 保存用户
sysUserDomainService.save(user);
Long userId = user.getUserId();
// 保存用户角色关联
if (dto.getRoleIds() != null) {
// 批量插入用户角色关联
if (dto.getRoleIds() != null && dto.getRoleIds().length > 0) {
List<SysUserRoleEntity> roleList = new ArrayList<>(dto.getRoleIds().length);
for (Long roleId : dto.getRoleIds()) {
SysUserRoleEntity userRole = new SysUserRoleEntity();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
sysUserRoleDomainService.save(userRole);
SysUserRoleEntity ur = new SysUserRoleEntity();
ur.setUserId(userId);
ur.setRoleId(roleId);
roleList.add(ur);
}
sysUserRoleDomainService.saveBatch(roleList);
}
// 保存用户岗位关联
if (dto.getPostIds() != null) {
// 批量插入用户岗位关联
if (dto.getPostIds() != null && dto.getPostIds().length > 0) {
List<SysUserPostEntity> postList = new ArrayList<>(dto.getPostIds().length);
for (Long postId : dto.getPostIds()) {
SysUserPostEntity userPost = new SysUserPostEntity();
userPost.setUserId(userId);
userPost.setPostId(postId);
sysUserPostDomainService.save(userPost);
SysUserPostEntity up = new SysUserPostEntity();
up.setUserId(userId);
up.setPostId(postId);
postList.add(up);
}
sysUserPostDomainService.saveBatch(postList);
}
log.info("新增用户成功: userName={}", user.getUserName());
return originalPassword;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void edit(SysUserUpdateDTO dto) {
// 检查用户是否存在
SysUserEntity existing = sysUserDomainService.getById(dto.getUserId());
if (existing == null) {
throw new ServiceException("用户不存在");
}
checkUserAllowed(existing);
// DTO → Entity
SysUserEntity user = BeanMapper.mapper().convert(dto, SysUserEntity.class);
// 用户名校验(排除自身)
if (StringUtils.hasText(user.getUserName())) {
long nameCount = sysUserDomainService.count(
new LambdaQueryWrapper<SysUserEntity>()
.eq(SysUserEntity::getUserName, user.getUserName())
.ne(SysUserEntity::getUserId, existing.getUserId()));
if (nameCount > 0) {
throw new ServiceException("修改用户'" + user.getUserName() + "'失败,登录账号已存在");
}
}
// 手机号唯一性校验(排除自身)
if (StringUtils.hasText(user.getPhonenumber())) {
long phoneCount = sysUserDomainService.count(
new LambdaQueryWrapper<SysUserEntity>()
.eq(SysUserEntity::getPhonenumber, user.getPhonenumber())
.ne(SysUserEntity::getUserId, existing.getUserId()));
if (phoneCount > 0) {
throw new ServiceException("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
}
}
// 邮箱唯一性校验(排除自身)
if (StringUtils.hasText(user.getEmail())) {
long emailCount = sysUserDomainService.count(
new LambdaQueryWrapper<SysUserEntity>()
.eq(SysUserEntity::getEmail, user.getEmail())
.ne(SysUserEntity::getUserId, existing.getUserId()));
if (emailCount > 0) {
throw new ServiceException("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在");
}
}
// 更新用户信息
user.setUpdateBy(SecurityUtils.getUsername());
user.setUpdateTime(new Date());
......@@ -170,15 +244,27 @@ public class SysUserServiceImpl implements SysUserService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void remove(Long[] userIds) {
for (Long userId : userIds) {
SysUserEntity user = sysUserDomainService.getById(userId);
if (user != null) {
// 逻辑删除
checkUserAllowed(user);
}
}
// 批量删除用户-角色关联
sysUserRoleDomainService.remove(
new LambdaQueryWrapper<SysUserRoleEntity>().in(SysUserRoleEntity::getUserId, Arrays.asList(userIds)));
// 批量删除用户-岗位关联
sysUserPostDomainService.remove(
new LambdaQueryWrapper<SysUserPostEntity>().in(SysUserPostEntity::getUserId, Arrays.asList(userIds)));
// 逻辑删除用户
for (Long userId : userIds) {
SysUserEntity user = new SysUserEntity();
user.setUserId(userId);
user.setDelFlag("1");
sysUserDomainService.updateById(user);
}
}
log.info("批量删除用户成功: count={}", userIds.length);
}
......@@ -188,6 +274,7 @@ public class SysUserServiceImpl implements SysUserService {
if (user == null) {
throw new ServiceException("用户不存在");
}
checkUserAllowed(user);
user.setPassword(passwordEncoder.encode(dto.getPassword()));
sysUserDomainService.updateById(user);
log.info("用户密码已重置: userId={}", dto.getUserId());
......@@ -199,6 +286,7 @@ public class SysUserServiceImpl implements SysUserService {
if (user == null) {
throw new ServiceException("用户不存在");
}
checkUserAllowed(user);
user.setStatus(dto.getStatus());
sysUserDomainService.updateById(user);
log.info("用户状态已修改: userId={}, status={}", dto.getUserId(), dto.getStatus());
......@@ -238,20 +326,16 @@ public class SysUserServiceImpl implements SysUserService {
}
List<SysUserPostEntity> userPosts = sysUserPostDomainService.list(
new LambdaQueryWrapper<SysUserPostEntity>().eq(SysUserPostEntity::getUserId, loginUser.getUserId()));
if (userPosts == null || userPosts.isEmpty()) {
if (CollectionUtils.isEmpty(userPosts)) {
return "";
}
List<Long> postIds = userPosts.stream()
.map(SysUserPostEntity::getPostId)
.collect(Collectors.toList());
List<Long> postIds = userPosts.stream().map(SysUserPostEntity::getPostId).toList();
List<SysPostEntity> posts = sysPostDomainService.listByIds(postIds);
if (posts == null || posts.isEmpty()) {
if (CollectionUtils.isEmpty(posts)) {
return "";
}
return posts.stream()
.map(SysPostEntity::getPostName)
.filter(Objects::nonNull)
.collect(Collectors.joining(", "));
return posts.stream().map(SysPostEntity::getPostName)
.filter(Objects::nonNull).collect(Collectors.joining(", "));
}
@Override
......@@ -307,12 +391,28 @@ public class SysUserServiceImpl implements SysUserService {
}
@Override
public List<Long> getAuthRoleList(Long userId) {
public SysUserVO getById(Long userId) {
SysUserEntity entity = sysUserDomainService.getById(userId);
return BeanMapper.mapper().convert(entity, SysUserVO.class);
}
@Override
public List<SysRoleVO> getAuthRoleList(Long userId) {
// 返回全部角色,已分配的角色设置 flag=true
List<SysRoleVO> allRoles = sysRoleService.listAllRoles();
// 查询用户已分配的角色ID
List<SysUserRoleEntity> userRoles = sysUserRoleDomainService.list(
new LambdaQueryWrapper<SysUserRoleEntity>().eq(SysUserRoleEntity::getUserId, userId));
return userRoles.stream()
.map(SysUserRoleEntity::getRoleId)
.collect(Collectors.toList());
Set<Long> assignedRoleIds = userRoles.stream().map(SysUserRoleEntity::getRoleId).collect(Collectors.toSet());
// 设置 flag:已分配的角色标记为 true
for (SysRoleVO role : allRoles) {
role.setFlag(assignedRoleIds.contains(role.getRoleId()));
}
// admin 用户看到全部角色,非 admin 不显示 admin 角色
if (!SecurityConstants.ADMIN_USER_ID.equals(userId)) {
allRoles = allRoles.stream().filter(r -> !RoleConstants.ADMIN_USER.equals(r.getRoleKey())).collect(Collectors.toList());
}
return allRoles;
}
@Override
......@@ -336,4 +436,13 @@ public class SysUserServiceImpl implements SysUserService {
List<SysDeptEntity> tree = sysDeptDomainService.buildDeptTree(allDepts);
return BeanMapper.mapper().convertList(tree, SysDeptVO.class);
}
/**
* 检查是否允许操作超级管理员用户
*/
private void checkUserAllowed(SysUserEntity user) {
if (user.getUserId() != null && SecurityConstants.ADMIN_USER_ID.equals(user.getUserId())) {
throw new ServiceException("不允许操作超级管理员用户");
}
}
}
......@@ -86,4 +86,26 @@ public class CharsetKit {
public static String systemCharset() {
return Charset.defaultCharset().name();
}
public static boolean containsChinese(String str) {
if (str == null || str.isEmpty()) {
return false;
}
// 中文字符的 Unicode 范围
// 基本汉字:4E00-9FA5
// 扩展A:3400-4DBF
// 扩展B-F:20000-2EBEF
for (char c : str.toCharArray()) {
// 最常用的汉字范围
if (c >= 0x4E00 && c <= 0x9FA5) {
return true;
}
// 如果需要包含扩展汉字,加上下面的判断
//if ((c >= 0x3400 && c <= 0x4DBF) || (c >= 0x20000 && c <= 0x2EBEF)) {
// return true;
//}
}
return false;
}
}
package com.jomalls.custom.app.utils;
import java.util.UUID;
/**
* @Author: Lizh
* @Date: 2026/7/15 14:36
* @Description: 唯一ID
* @Version: 1.0
*/
public class UniqueId {
private static final int START_INDEX = 1;
/**
* 生成UUID
* @param length
* @return
*/
public static String generateUniqueId(int length) {
return UUID.randomUUID().toString().replace("-", "").substring(START_INDEX, length);
}
}
......@@ -73,4 +73,8 @@ public class SysRoleVO implements Serializable {
@JsonProperty("deptIds")
@Schema(description = "部门ID列表")
private Long[] deptIds;
@JsonProperty("flag")
@Schema(description = "用户是否已分配该角色")
private Boolean flag;
}
......@@ -15,10 +15,9 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Token 验证处理服务
......@@ -252,7 +251,7 @@ public class TokenHandle {
redisKey,
JSON.toJSONString(loginUser),
expireTime * 60L,
java.util.concurrent.TimeUnit.SECONDS
TimeUnit.SECONDS
);
}
......@@ -269,19 +268,21 @@ public class TokenHandle {
*/
private String buildJwtCompatibly(String uuid, LoginUser loginUser) {
try {
java.util.Base64.Encoder encoder = java.util.Base64.getUrlEncoder().withoutPadding();
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
log.info(JSON.toJSONString(loginUser));
// Header: {"alg":"HS512"}
String header = "{\"alg\":\"HS512\"}";
String headerBase64 = encoder.encodeToString(
header.getBytes(java.nio.charset.StandardCharsets.UTF_8));
header.getBytes(StandardCharsets.UTF_8));
// Payload: 仅含 UUID + 基本标识,不含 permissions/deptId(全量信息在 Redis
java.util.LinkedHashMap<String, Object> claims = new java.util.LinkedHashMap<>();
// Payload:基本标识 + deptId + permissions(前端从 JWT 解析 sysUser 信息
LinkedHashMap<String, Object> claims = new LinkedHashMap<>();
claims.put("login_user_key", uuid);
claims.put("sub", loginUser.getUsername());
claims.put("account", loginUser.getUsername());
claims.put("id", loginUser.getUserId());
claims.put("deptId", loginUser.getDeptId());
claims.put("permissions", loginUser.getPermissions());
// JWT 24 小时硬过期(与 custom-back TOKEN_TIMEOUT = 86400000L 一致)
claims.put("exp", (System.currentTimeMillis() + 86400000L) / 1000);
......@@ -343,16 +344,12 @@ public class TokenHandle {
*/
public void refreshToken(LoginUser loginUser) {
if (stringRedisTemplate != null && loginUser != null
&& org.springframework.util.StringUtils.hasText(loginUser.getToken())) {
&& StringUtils.hasText(loginUser.getToken())) {
String redisKey = REDIS_TOKEN_PREFIX + loginUser.getToken();
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(System.currentTimeMillis() + expireTime * 60 * 1000L);
stringRedisTemplate.opsForValue().set(
redisKey,
JSON.toJSONString(loginUser),
expireTime * 60L,
java.util.concurrent.TimeUnit.SECONDS
);
stringRedisTemplate.opsForValue().set(redisKey, JSON.toJSONString(loginUser),
expireTime * 60L, java.util.concurrent.TimeUnit.SECONDS);
}
}
......
......@@ -62,18 +62,19 @@
AND ur.user_id = #{userId}
</select>
<!-- 根据用户ID查询菜单树 -->
<!-- 根据用户ID查询菜单树(与 custom-back 完全对齐:JOIN 方式,含 role.status 过滤) -->
<select id="selectMenuTreeByUserId" resultType="com.jomalls.custom.dal.entity.SysMenuEntity">
SELECT DISTINCT m.* FROM sys_menu m
<if test="userId != 1">
LEFT JOIN sys_role_menu rm ON m.menu_id = rm.menu_id
LEFT JOIN sys_user_role ur ON rm.role_id = ur.role_id
LEFT JOIN sys_role ro ON ur.role_id = ro.role_id
</if>
WHERE m.menu_type IN ('M', 'C')
AND m.status = '0'
<if test="userId != 1">
AND m.menu_id IN (
SELECT rm.menu_id FROM sys_role_menu rm
WHERE rm.role_id IN (
SELECT ur.role_id FROM sys_user_role ur WHERE ur.user_id = #{userId}
)
)
AND ur.user_id = #{userId}
AND ro.status = '0'
</if>
ORDER BY m.parent_id, m.order_num
</select>
......
......@@ -91,8 +91,7 @@ public class CommonExceptionHandlerAdvice {
@ExceptionHandler(ServiceException.class)
public ResponseEntity<R<Object>> handleServiceException(ServiceException e) {
log.debug("[ 业务异常 ] code={}, {}", e.getCode(), e.getMessage(), e);
HttpStatus httpStatus = CodeEnum.MAP.getOrDefault(e.getCode(), HttpStatus.OK);
return ResponseEntity.status(httpStatus).body(R.fail(e.getCode(), e.getMessage()));
return ResponseEntity.status(HttpStatus.OK).body(R.fail(e.getCode(), e.getMessage()));
}
/**
......@@ -101,7 +100,7 @@ public class CommonExceptionHandlerAdvice {
@ExceptionHandler(RemoteServiceException.class)
public ResponseEntity<R<Object>> handleRemoteServiceException(RemoteServiceException e) {
log.debug("[ 远程服务异常 ] status={}, {}", e.getStatusCode(), e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
return ResponseEntity.status(HttpStatus.OK)
.body(R.fail(e.getStatusCode(), e.getMessage()));
}
......@@ -111,7 +110,7 @@ public class CommonExceptionHandlerAdvice {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<R<Object>> handleRuntimeException(Exception e) {
log.debug("[ 运行时异常 ] {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
return ResponseEntity.status(HttpStatus.OK)
.body(R.fail(e.getMessage()));
}
......@@ -121,7 +120,7 @@ public class CommonExceptionHandlerAdvice {
@ExceptionHandler(Exception.class)
public ResponseEntity<R<Object>> handleException(Exception e) {
log.debug("[ 未捕获异常 ] {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
return ResponseEntity.status(HttpStatus.OK)
.body(R.fail(CodeEnum.FAIL.getCode(), e.getMessage()));
}
}
......@@ -3,12 +3,17 @@ package com.jomalls.custom.webapp.controller.system;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jomalls.custom.app.annotation.RequiresPermissions;
import com.jomalls.custom.app.constant.SecurityConstants;
import com.jomalls.custom.app.dto.system.*;
import com.jomalls.custom.app.service.system.SysRoleService;
import com.jomalls.custom.app.vo.system.SysDeptVO;
import com.jomalls.custom.app.vo.system.SysRoleVO;
import com.jomalls.custom.app.vo.system.SysUserVO;
import com.jomalls.custom.dal.entity.SysMenuEntity;
import com.jomalls.custom.domain.service.system.SysMenuDomainService;
import com.jomalls.custom.security.LoginUser;
import com.jomalls.custom.security.SecurityUtils;
import com.jomalls.custom.security.TokenHandle;
import com.jomalls.custom.app.exception.ServiceException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
......@@ -16,8 +21,10 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 角色管理 Controller
......@@ -32,6 +39,8 @@ import java.util.Map;
public class SysRoleController {
private final SysRoleService sysRoleService;
private final SysMenuDomainService sysMenuDomainService;
private final TokenHandle tokenHandle;
@Operation(summary = "查询角色列表(无权限要求,用于下拉框)")
@GetMapping("/optionselect")
......@@ -69,11 +78,15 @@ public class SysRoleController {
@PutMapping
public void edit(@RequestBody SysRoleUpdateDTO dto) {
sysRoleService.edit(dto);
// 刷新 LoginUser 缓存(非管理员用户的权限可能发生变化)
if (SecurityUtils.getUserId() != null && !SecurityUtils.getUserId().equals(1L)) {
// TODO: 如果当前用户所属角色被修改,需要刷新 token 缓存中的 permissions
// 在 Redis TokenStore 场景下,应清除或刷新对应用户的缓存权限
log.debug("角色更新完成,需要刷新相关用户的LoginUser缓存: roleId={}", dto.getRoleId());
// 角色修改后,刷新缓存用户权限
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUserId() != null && SecurityConstants.ADMIN_USER_ID.equals(loginUser.getUserId())) {
// 重新查询权限
Set<String> perms = new HashSet<>();
List<String> permList = sysMenuDomainService.selectMenuPermsByUserId(loginUser.getUserId());
if (permList != null) perms.addAll(permList);
loginUser.setPermissions(perms);
tokenHandle.refreshToken(loginUser);
}
}
......
......@@ -3,11 +3,14 @@ package com.jomalls.custom.webapp.controller.system;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jomalls.custom.app.annotation.RequiresPermissions;
import com.jomalls.custom.app.constant.RoleConstants;
import com.jomalls.custom.app.constant.SecurityConstants;
import com.jomalls.custom.app.dto.system.*;
import com.jomalls.custom.app.service.system.SysPostService;
import com.jomalls.custom.app.service.system.SysRoleService;
import com.jomalls.custom.app.service.system.SysUserService;
import com.jomalls.custom.app.vo.system.SysDeptVO;
import com.jomalls.custom.app.vo.system.SysRoleVO;
import com.jomalls.custom.app.vo.system.SysUserVO;
import com.jomalls.custom.security.SecurityUtils;
import com.jomalls.custom.app.exception.ServiceException;
......@@ -19,10 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
/**
* 用户管理 Controller
......@@ -55,15 +55,18 @@ public class SysUserController {
@Operation(summary = "查询用户详情(含角色、岗位)")
@RequiresPermissions("system:user:query")
@GetMapping(value = {"/", "/{userId}"})
public R<Object> getInfo(@PathVariable(value = "userId", required = false) Long userId) {
public R<Object> getInfo(@PathVariable(required = false) Long userId) {
R<Object> result = R.ok();
if (userId != null) {
Map<String, Object> info = sysUserService.getInfo(userId);
result.put("data", info.get("user"));
result.put("postIds", info.get("postIds"));
result.put("roleIds", info.get("roleIds") != null ? info.get("roleIds") : info.get("roles"));
result.put("roleIds", info.get("roleIds"));
}
result.put("roles", sysRoleService.listAllRoles());
// admin 用户看到全部角色,非 admin 不显示 admin 角色
List<SysRoleVO> roles = sysRoleService.listAllRoles();
result.put("roles", (SecurityConstants.ADMIN_USER_ID.equals(userId)) ? roles :
roles.stream().filter(r -> !RoleConstants.ADMIN_USER.equals(r.getRoleKey())).toList());
result.put("posts", sysPostService.listAllPosts());
return result;
}
......@@ -124,21 +127,26 @@ public class SysUserController {
@Operation(summary = "更新当前用户头像")
@PostMapping("/profile/avatar")
public R<Object> updateAvatar(@RequestParam("avatarfile") MultipartFile file) {
public Map<String, String> updateAvatar(@RequestParam("avatarfile") MultipartFile file) {
// TODO: 实际头像上传处理(保存文件到服务器/OSS),此处模拟返回
log.info("上传头像文件: {}", file.getOriginalFilename());
String avatarUrl = "/profile/avatar/" + file.getOriginalFilename();
sysUserService.updateAvatar(avatarUrl);
return R.ok().put("imgUrl", avatarUrl);
Map<String, String> result = new HashMap<>();
result.put("avatarUrl", avatarUrl);
return result;
}
@Operation(summary = "查询用户已分配的角色ID列表")
@Operation(summary = "查询用户已分配的角色(含 user 对象和角色列表)")
@RequiresPermissions("system:user:query")
@GetMapping("/authRole/{userId}")
public Map<String, Object> authRole(@PathVariable Long userId) {
// 返回 user + 全部角色列表(含 flag 标识已分配)
Map<String, Object> result = new java.util.HashMap<>();
result.put("userId", userId);
result.put("roleIds", sysUserService.getAuthRoleList(userId));
SysUserVO user = sysUserService.getById(userId);
result.put("user", user);
// admin 用户看到全部角色,非 admin 不显示 admin 角色
result.put("roles", sysUserService.getAuthRoleList(userId));
return result;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment