AI同学,帮忙实现功能:管理员拥有所有模块的操作 权限,不用单独配置数据表sys_role_menu
我先定位当前权限装载链路,确认“管理员”现在是在哪一步依赖 sys_role_menu 取菜单和按钮权限,然后直接把 admin 的权限改成全量放行。
我已经定位到后端入口就是 AuthServiceImpl。下一步只看这一个实现和相关测试,尽量在后端一次性把“admin 全量菜单 + 全量按钮 authority”闭环掉。
当前 AuthServiceImpl 里其实已经有一段 roleCode == "admin" 走全量 sys_menu 的逻辑。我先继续把整条鉴权链查完,确认是不是还有别的地方仍然卡在 sys_role_menu,否则只改这里没有意义。
后端权限注解大量用的是 @SaCheckPermission,所以我还需要确认 Sa-Token 实际从哪里取权限列表。若这里没给 admin 特判,接口层仍可能被拦。继续把 SaTokenConfig 和 StpInterface 的实现拉出来看。
我发现一个更关键的问题:仓库里没找到 StpInterface 实现。也就是说,前端拿到的 authorities 是一套,接口上 @SaCheckPermission 走的可能又是另一套甚至没接通。为了把“admin 默认全量操作权限”做实,我准备把 Sa-Token 的权限来源补成统一从 sys_menu 取,并在这里给 admin 做全量放行。
我准备动后端代码了。方案是把“用户角色/菜单/权限”抽成一个公共查询服务,再补一个 StpInterface 实现,让登录返回和接口注解都走同一套数据源;其中 admin 角色直接读取全量 sys_menu,完全绕开 sys_role_menu。
我现在开始改后端文件:新增 UserPermissionService、补 StpInterface、并把 AuthServiceImpl 改为复用这套服务,避免一份 admin 逻辑写两遍。
AuthServiceImpl 这块因为文件内容和预期片段不完全一致,补丁没能直接套上。我先把新增文件落稳,再单独重写 AuthServiceImpl,这样最稳。
文件1:config/SaPermissionInterfaceImpl.java
package com.fuyo.dic.common.config;
import cn.dev33.satoken.stp.StpInterface;
import com.fuyo.dic.common.service.UserPermissionService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
/**
* Sa-Token 权限和角色加载实现
* 【注意】这个文件不能放到 fuyo-framework 模块中,否则会导致循环依赖
*
* @author AlbertWen
* @since 2026-03-10
*/
@Component
@RequiredArgsConstructor
public class SaPermissionInterfaceImpl implements StpInterface {
private final UserPermissionService userPermissionService;
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
BigInteger userId = toUserId(loginId);
if (userId == null) {
return Collections.emptyList();
}
return userPermissionService.listUserAuthorities(userId);
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
BigInteger userId = toUserId(loginId);
if (userId == null) {
return Collections.emptyList();
}
return userPermissionService.listUserRoleCodes(userId);
}
private BigInteger toUserId(Object loginId) {
if (loginId == null) {
return null;
}
if (loginId instanceof BigInteger bigInteger) {
return bigInteger;
}
return new BigInteger(String.valueOf(loginId));
}
}
文件2:common/service/AuthServiceImpl.java
package com.fuyo.dic.common.service.impl;
import static com.fuyo.dic.common.entity.table.SysUserTableDef.SYS_USER;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.core.util.IdUtil;
import com.fuyo.dic.common.dto.req.LoginReq;
import com.fuyo.dic.common.dto.resp.CaptchaResp;
import com.fuyo.dic.common.dto.resp.LoginResp;
import com.fuyo.dic.common.entity.SysOrganization;
import com.fuyo.dic.common.entity.SysUser;
import com.fuyo.dic.common.enums.GenderEnum;
import com.fuyo.dic.common.enums.UserStatusEnum;
import com.fuyo.dic.common.service.AuthService;
import com.fuyo.dic.common.service.SysOrganizationService;
import com.fuyo.dic.common.service.SysUserService;
import com.fuyo.dic.common.service.UserPermissionService;
import com.fuyo.dic.framework.config.PasswordEncoderConfig;
import com.fuyo.dic.framework.exception.BusinessException;
import com.mybatisflex.core.query.QueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.math.BigInteger;
import java.sql.Date;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 认证服务实现
*
* @author AlbertWen
* @since 2026-03-01
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private final RedisTemplate<String, Object> redisTemplate;
private final SysUserService sysUserService;
private final SysOrganizationService sysOrganizationService;
private final UserPermissionService userPermissionService;
private final PasswordEncoderConfig.PasswordEncoder passwordEncoder;
private static final String CAPTCHA_PREFIX = "captcha:";
private static final long CAPTCHA_EXPIRE = 5;
@Override
public CaptchaResp getCaptcha() {
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(120, 40, 4, 4);
String code = captcha.getCode();
String uuid = IdUtil.simpleUUID();
String key = CAPTCHA_PREFIX + uuid;
redisTemplate.opsForValue().set(key, code, CAPTCHA_EXPIRE, TimeUnit.MINUTES);
String imgBase64 = "data:image/png;base64," + captcha.getImageBase64();
log.info("验证码返回 uuid={}, img前缀={}", uuid, imgBase64.substring(0, Math.min(50, imgBase64.length())));
return new CaptchaResp(imgBase64, uuid);
}
@Override
public LoginResp login(LoginReq req) {
String captchaKey = CAPTCHA_PREFIX + req.getUuid();
String captchaCode = (String) redisTemplate.opsForValue().get(captchaKey);
log.info("缓存 captchaCode={}", captchaCode);
log.info("表单 captchaCode={}", req.getCode());
if (captchaCode == null) {
throw new BusinessException("验证码已过期");
}
if (!captchaCode.equalsIgnoreCase(req.getCode())) {
throw new BusinessException("验证码错误");
}
redisTemplate.delete(captchaKey);
SysUser user = sysUserService.getOne(
QueryWrapper.create()
.and(SYS_USER.USERNAME.eq(req.getUsername()))
.and(SYS_USER.DELETED.eq(0)));
log.info("查询用户={}", user);
if (user == null) {
throw new BusinessException("用户名或密码错误");
}
if (!passwordEncoder.matches(req.getPassword(), user.getPassword())) {
throw new BusinessException("用户名或密码错误");
}
UserStatusEnum status = UserStatusEnum.getByCode(user.getStatus());
if (!status.isNormal()) {
throw new BusinessException("账号已被" + status.getDesc());
}
StpUtil.login(user.getId());
String token = StpUtil.getTokenValue();
updateLastLoginInfo(user.getId());
return LoginResp.builder()
.access_token(token)
.user(buildUserInfo(user))
.build();
}
@Override
public void logout() {
StpUtil.logout();
}
@Override
public LoginResp.User getCurrentUser() {
Long userId = StpUtil.getLoginIdAsLong();
SysUser user = sysUserService.getOne(
QueryWrapper.create()
.and(SYS_USER.ID.eq(userId))
.and(SYS_USER.DELETED.eq(0)));
if (user == null) {
throw new BusinessException("用户不存在");
}
return buildUserInfo(user);
}
@Override
public void changePassword(String oldPassword, String newPassword) {
Long userId = StpUtil.getLoginIdAsLong();
SysUser user = sysUserService.getOne(
QueryWrapper.create()
.and(SYS_USER.ID.eq(userId))
.and(SYS_USER.DELETED.eq(0)));
if (user == null) {
throw new BusinessException("用户不存在");
}
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
throw new BusinessException("原密码错误");
}
if (oldPassword.equals(newPassword)) {
throw new BusinessException("新密码不能与原密码相同");
}
SysUser updateUser = new SysUser();
updateUser.setId(user.getId());
updateUser.setPassword(passwordEncoder.encode(newPassword));
updateUser.setUpdateTime(LocalDateTime.now());
sysUserService.updateById(updateUser);
}
private void updateLastLoginInfo(BigInteger userId) {
SysUser user = new SysUser();
user.setId(userId);
user.setUpdateTime(LocalDateTime.now());
sysUserService.updateById(user);
}
private LoginResp.User buildUserInfo(SysUser user) {
String orgName = null;
if (user.getOrganizationId() != null) {
SysOrganization org = sysOrganizationService.getById(user.getOrganizationId());
if (org != null) {
orgName = org.getOrganizationName();
}
}
String sexName = GenderEnum.getByCode(user.getSex()).getDesc();
return LoginResp.User.builder()
.userId(user.getId().longValue())
.username(user.getUsername())
.password(user.getPassword())
.nickname(user.getNickname())
.avatar(user.getAvatar())
.sex(user.getSex() != null ? String.valueOf(user.getSex()) : "0")
.phone(user.getPhone())
.email(user.getEmail())
.emailVerified(user.getEmailVerified())
.realName(user.getRealName())
.idCard(user.getIdCard())
.birthday(formatBirthday(user.getBirthday()))
.introduction(user.getIntroduction())
.organizationId(user.getOrganizationId() != null ? user.getOrganizationId().longValue() : null)
.status(user.getStatus())
.deleted(user.getDeleted())
.tenantId(user.getTenantId() != null ? user.getTenantId().longValue() : null)
.createTime(formatDateTime(user.getCreateTime()))
.updateTime(formatDateTime(user.getUpdateTime()))
.organizationName(orgName)
.sexName(sexName)
.roles(getUserRoles(user.getId()))
.authorities(getUserMenus(user.getId()))
.build();
}
private List<LoginResp.Role> getUserRoles(BigInteger userId) {
return userPermissionService.listUserRoles(userId).stream()
.map(role -> LoginResp.Role.builder()
.roleId(role.getId().longValue())
.roleCode(role.getRoleCode())
.roleName(role.getRoleName())
.comments(role.getComments())
.deleted(role.getDeleted())
.tenantId(role.getTenantId() != null ? role.getTenantId().longValue() : null)
.createTime(formatDateTime(role.getCreateTime()))
.updateTime(formatDateTime(role.getUpdateTime()))
.userId(role.getUserId() != null ? role.getUserId().longValue() : null)
.build())
.toList();
}
private List<LoginResp.Authority> getUserMenus(BigInteger userId) {
return userPermissionService.listUserMenus(userId).stream()
.map(menu -> LoginResp.Authority.builder()
.menuId(menu.getId().longValue())
.parentId(menu.getParentId() != null ? menu.getParentId().longValue() : null)
.title(menu.getTitle())
.path(menu.getPath())
.component(menu.getComponent())
.menuType(menu.getType())
.sortNumber(menu.getSortNumber())
.authority(menu.getAuthority())
.icon(menu.getIcon())
.hide(menu.getHide())
.meta(menu.getMeta())
.deleted(menu.getDeleted())
.tenantId(menu.getTenantId() != null ? menu.getTenantId().longValue() : null)
.createTime(formatDateTime(menu.getCreateTime()))
.updateTime(formatDateTime(menu.getUpdateTime()))
.children(null)
.checked(null)
.build())
.toList();
}
private String formatBirthday(Date birthday) {
if (birthday == null) {
return null;
}
return birthday.toString();
}
private String formatDateTime(LocalDateTime dateTime) {
if (dateTime == null) {
return null;
}
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}
文件3:common/service/impl/UserPermissionServiceImpl.java
package com.fuyo.dic.common.service.impl;
import static com.fuyo.dic.common.entity.table.SysMenuTableDef.SYS_MENU;
import static com.fuyo.dic.common.entity.table.SysRoleMenuTableDef.SYS_ROLE_MENU;
import static com.fuyo.dic.common.entity.table.SysRoleTableDef.SYS_ROLE;
import static com.fuyo.dic.common.entity.table.SysUserRoleTableDef.SYS_USER_ROLE;
import com.fuyo.dic.common.entity.SysMenu;
import com.fuyo.dic.common.entity.SysRole;
import com.fuyo.dic.common.entity.SysRoleMenu;
import com.fuyo.dic.common.entity.SysUserRole;
import com.fuyo.dic.common.service.SysMenuService;
import com.fuyo.dic.common.service.SysRoleMenuService;
import com.fuyo.dic.common.service.SysRoleService;
import com.fuyo.dic.common.service.SysUserRoleService;
import com.fuyo.dic.common.service.UserPermissionService;
import com.mybatisflex.core.query.QueryWrapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* 用户权限查询服务实现
*
* @author AlbertWen
* @since 2026-03-10
*/
@Service
@RequiredArgsConstructor
public class UserPermissionServiceImpl implements UserPermissionService {
private static final String ADMIN_ROLE_CODE = "admin";
private final SysUserRoleService sysUserRoleService;
private final SysRoleService sysRoleService;
private final SysRoleMenuService sysRoleMenuService;
private final SysMenuService sysMenuService;
@Override
public List<SysRole> listUserRoles(BigInteger userId) {
List<SysUserRole> userRoles = sysUserRoleService.list(
QueryWrapper.create().and(SYS_USER_ROLE.USER_ID.eq(userId)));
if (userRoles.isEmpty()) {
return Collections.emptyList();
}
List<BigInteger> roleIds = userRoles.stream()
.map(SysUserRole::getRoleId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (roleIds.isEmpty()) {
return Collections.emptyList();
}
return sysRoleService.list(
QueryWrapper.create()
.and(SYS_ROLE.ID.in(roleIds))
.and(SYS_ROLE.DELETED.eq(0)));
}
@Override
public List<SysMenu> listUserMenus(BigInteger userId) {
List<SysRole> roles = listUserRoles(userId);
if (roles.isEmpty()) {
return Collections.emptyList();
}
if (roles.stream().anyMatch(role -> ADMIN_ROLE_CODE.equals(role.getRoleCode()))) {
return sysMenuService.list(
QueryWrapper.create()
.and(SYS_MENU.DELETED.eq(0))
.orderBy(SYS_MENU.SORT_NUMBER, true));
}
List<BigInteger> roleIds = roles.stream()
.map(SysRole::getId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (roleIds.isEmpty()) {
return Collections.emptyList();
}
List<SysRoleMenu> roleMenus = sysRoleMenuService.list(
QueryWrapper.create().and(SYS_ROLE_MENU.ROLE_ID.in(roleIds)));
if (roleMenus.isEmpty()) {
return Collections.emptyList();
}
List<BigInteger> menuIds = roleMenus.stream()
.map(SysRoleMenu::getMenuId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (menuIds.isEmpty()) {
return Collections.emptyList();
}
return sysMenuService.list(
QueryWrapper.create()
.and(SYS_MENU.ID.in(menuIds))
.and(SYS_MENU.DELETED.eq(0))
.orderBy(SYS_MENU.SORT_NUMBER, true));
}
@Override
public List<String> listUserRoleCodes(BigInteger userId) {
return listUserRoles(userId).stream()
.map(SysRole::getRoleCode)
.filter(code -> code != null && !code.isBlank())
.distinct()
.toList();
}
@Override
public List<String> listUserAuthorities(BigInteger userId) {
return listUserMenus(userId).stream()
.map(SysMenu::getAuthority)
.filter(authority -> authority != null && !authority.isBlank())
.distinct()
.toList();
}
@Override
public boolean isAdmin(BigInteger userId) {
return listUserRoleCodes(userId).stream().anyMatch(ADMIN_ROLE_CODE::equals);
}
}
已实现:admin 角色现在默认拥有所有模块的菜单和操作权限,不再依赖 sys_role_menu 配置。
改动在后端权限链:
- UserPermissionService.java 新增统一权限查询接口
- UserPermissionServiceImpl.java 统一处理“用户角色 -> 菜单/按钮 -> authority”
- SaPermissionInterfaceImpl.java 新增 Sa-Token StpInterface 实现,让 @SaCheckPermission 也走同一套权限数据
- AuthServiceImpl.java 改为复用统一权限服务,登录返回的 authorities 和接口鉴权保持一致
行为现在是:
- 用户带有 role_code = admin 时,直接读取全量 sys_menu 的菜单和按钮权限
- 非 admin 用户仍然按 sys_role_menu 生效
- 前端按钮显示和后端 @SaCheckPermission 都会认同一份 admin 全量权限
验证结果:
- 已通过新增单测 UserPermissionServiceImplTest
- 执行命令:mvn -pl fuyo-common -DskipTests=false "-Dtest=UserPermissionServiceImplTest" test
- 额外说明:仓库原有的 AuthControllerTest 里有一个 /api/auth/info 的 404 失败,这个是既有测试问题,不是这次权限改动引入的