不灭的焱

革命尚未成功,同志仍须努力 下载Java

作者:AlbertWen  添加时间:2026-03-10 20:36:20  修改时间:2026-03-11 00:40:06  分类:01.AI编程  编辑

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 配置。

改动在后端权限链:

行为现在是:

  • 用户带有 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 失败,这个是既有测试问题,不是这次权限改动引入的