首页 > 教程攻略 > ai教程 >我让 AI 从零做了一个用户权限系统,这是全过程记录

我让 AI 从零做了一个用户权限系统,这是全过程记录

来源:互联网 时间:2026-07-11 07:22:21

从零搭建一个用户权限系统,AI全流程实录

从一个刚初始化完的空白仓库开始,让AI从头搭出一套完整的RBAC权限系统。前端用Vue,后端Ja va 17 + Spring Boot 3,数据库MySQL,认证走SaToken。需求清单包括:用户登录、用户管理、角色管理、权限分配、操作审计日志。

我让 AI 从零做了一个用户权限系统,这是全过程记录

放在以前,这套活儿得手动写上好几天:设计表结构、写实体类、Service层、Controller层、单元测试,然后code review、改bug。这次想换个做法——让Claude从需求分析到代码生成完整走一遍,顺便验证一下之前文章里总结的规则管理方法,在实际项目里到底好不好用。

第一阶段:先搞清楚要做什么

接手这种需求,一上来就让AI写代码是大忌。超过3个文件需要改动的任务,先上/plan让它规划方案——这是之前反复踩坑后得出的经验。改到一半发现方向不对,整个上下文废了只能重新开始,这种情况至少遇到过两次。

让Claude先读了项目现有代码结构,然后给出需求:

/plan 这是一个AI开发一个rbac系统示例,现在处于刚刚初始化仓库,还没进行任何开发的状态。前端可以使用vue,后端使用ja va,版本使用17,数据库使用mysql。增加用户登录、用户管理与权限管理功能。RBAC 权限系统,要求:
- 支持用户多角色分配
- 基于角色的细粒度权限控制
- 权限校验用 AOP 拦截,不在每个方法里手写
- 角色和权限变更必须记录审计日志
- 数据库用 MySQL

AI给出的方案对比了三种实现路径:

  1. 基于角色的RBAC

    ——用户关联角色,角色关联权限点
  2. 基于策略的策略模式

    ——每个权限定义一个策略类
  3. 基于表达式语言的SpEL

    ——用Spring Expression Language写权限表达式

最终选了第一种。RBAC是标准方案,SaToken原生支持角色和权限判断,团队接手也容易。后两种更灵活,但学习成本和维护成本都上去了。

方案确认后,Claude把它拆成了按依赖关系排序的任务清单:

1. 设计数据模型(User-Role-Permission 五张关系表)
2. 写实体类和Mapper(MyBatis-Plus)
3. 写Service层(权限校验、角色分配)
4. 写AOP拦截器(基于注解的权限校验)
5. 写Controller(用户/角色/权限CRUD)
6. 写审计日志
7. 写全局异常处理

这份任务清单,就是后面整个开发过程的"施工图"。

第二阶段:定规则

这次没有搞三层规则分层,只用一个CLAUDE.md。原因很简单——这个权限模块是一次性开发任务,不是要长期维护的独立子系统。专门为它建一个L3文件,后期维护成本大于收益。CLAUDE.md里的核心规则:

## 技术栈
- Ja va 17, Spring Boot 3.x
- MyBatis-Plus 3.5.x, SaToken 1.38.x
- Hutool 5.8.x, Lombok 1.18.x
## 代码规范
- 使用 @RequirePermission 注解 + AOP 做权限校验
- 禁止在 Controller 方法体内写权限判断逻辑
- 使用 BusinessException 抛业务异常
- 统一用 ApiResponse 包装返回结果
## 查询规范
- 使用 MyBatis-Plus LambdaQueryWrapper
- 禁止手写 SQL 字符串拼接

核心就一条:你不说清楚,AI就按它的训练数据来。而它的训练数据是全网代码的平均值,不是你团队的规范。

第三阶段:开始写代码

数据模型

先跑/init让Claude确认项目结构,然后从任务1开始。RBAC的标准数据模型是5张表:

┌─────────┐┌──────────────┐ ┌─────────┐
│t_user │ ────▶│ t_user_role│◀────│ t_role│
│(用户)││(用户-角色)│ │(角色)│
└─────────┘└──────────────┘ └────┬────┘
                                  │
                          ┌─────────────────┐
                          │t_role_permission│
                          │ (角色-权限)│
                          └─────────────────┘
                                  │
                          ┌──────┴─────┐
                          │t_permission│
                          │ (权限)│
                          └────────────┘

Claude生成的实体类用的是MyBatis-Plus注解,不是JPA——这也是提前在CLAUDE.md里说清楚技术栈的好处:

@Data
@TableName("user_role")
public class UserRole {
    @TableId(type = IdType.AUTO)
    private Long id;
    @TableField("user_id")
    private Long userId;
    @TableField("role_id")
    private Long roleId;
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}

用MyBatis-Plus的好处是不需要操心EAGER/LAZY这些JPA的加载策略——每次查询都是主动调用,N+1问题靠手动优化查询逻辑来避免。

AOP权限拦截器

这个部分是权限系统的核心。@RequirePermission注解定义得很简洁:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequirePermission {
    /** * 权限标识,格式为 "resource:action" */
    String value();
}

切面实现:

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class PermissionAspect {
    private final PermissionService permissionService;
    @Around("@annotation(requirePermission)")
    public Object checkPermission(ProceedingJoinPoint joinPoint,
                                  RequirePermission requirePermission) throws Throwable {
        if (!StpUtil.isLogin()) {
            throw new PermissionDeniedException("未登录或登录已过期");
        }
        String permissionCode = requirePermission.value();
        long userId = StpUtil.getLoginIdAsLong();
        boolean hasPermission = permissionService.hasPermission(userId, permissionCode);
        if (!hasPermission) {
            log.warn("用户 {} 尝试访问权限 {} 被拒绝", userId, permissionCode);
            throw new PermissionDeniedException("没有权限执行此操作");
        }
        return joinPoint.proceed();
    }
}

用法就是在Controller方法上贴注解:

@RequirePermission("role:create")
@PostMapping
public ApiResponse createRole(@Valid @RequestBody CreateRoleRequest request) {
    Role role = roleService.createRole(request);
    auditLogService.logAudit(...);
    return ApiResponse.success(role);
}

这个部分Claude生成的质量很高——AOP+自定义注解是Spring生态里的标准模式,训练数据里样本多。而且用了SaToken的StpUtil.isLogin()做登录检查,说明AI读了一下项目里的依赖。

权限校验的Service层

这里是第一个值得细说的地方。AI生成的hasPermission方法:

@Override
public boolean hasPermission(Long userId, String permissionCode) {
    // 获取用户所有角色
    List roleCodes = userRoleService.getUserRoleCodes(userId);
    if (roleCodes.isEmpty()) {
        return false;
    }
    // 获取用户所有角色的权限
    List userPermissions = getUserPermissionCodes(userId);
    return userPermissions.contains(permissionCode);
}

然后是getUserPermissionCodes的具体实现:

@Override
public List getUserPermissionCodes(Long userId) {
    List roles = userRoleService.getUserRoles(userId);
    if (CollUtil.isEmpty(roles)) {
        return List.of();
    }
    Set permissions = new HashSet<>();
    for (Role role : roles) {
        if (role.getStatus() != 1) continue;
        List permissionIds = rolePermissionService.getPermissionIdsByRoleId(role.getId());
        if (!permissionIds.isEmpty()) {
            List perms = listByIds(permissionIds);
            perms.stream().filter(p -> p.getStatus() == 1)
                 .map(Permission::getCode)
                 .forEach(permissions::add);
        }
    }
    return List.copyOf(permissions);
}

这段代码其实存在N+1查询的问题。对每个角色调用getPermissionIdsByRoleId,再对每个角色调用listByIds批量查权限。用户有3个角色就是6次数据库查询(3次查权限ID+3次批量查权限详情)。不过这个项目是个人演示项目,数据量小,暂时没到必须优化的程度。如果放到真实生产环境,会改成一次JOIN查询或者用IN子句批量查——这就是之前文章里说的,AI不会主动考虑数据量增长后的性能问题。

审计日志

审计日志这块,AI最初的方案是用AOP切面自动记录。后来改成了在Controller里手动调用auditLogService.logAudit()。原因在于:AOP切面虽然看起来省事,但很难记录变更前后的具体值。比如"把用户从'普通用户'改成了'管理员'"这种信息,AOP只能拿到方法名和参数,拿不到变更前的数据。手动调用虽然多写几行,但记录的精度更高。审计日志用到的AuditLog实体:

@Data
@TableName("audit_log")
public class AuditLog {
    @TableId(type = IdType.AUTO)
    private Long id;
    @TableField("operator_id")
    private Long operatorId;
    @TableField("operator_name")
    private String operatorName;
    @TableField("action")
    private String action;
    @TableField("target_type")
    private String targetType;
    @TableField("target_id")
    private Long targetId;
    @TableField("detail")
    private String detail;
    @TableField("ip_address")
    private String ipAddress;
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}

Controller里调用的方式:

auditLogService.logAudit(
    StpUtil.getLoginIdAsLong(),
    StpUtil.getLoginIdAsString(),
    AuditAction.CREATE_ROLE.name(),
    "role",
    role.getId(),
    "创建角色: " + role.getName(),
    null
);

AI开发很少一把过

这篇权限模块的代码,从写完到能跑,中间改了好几个回合。这其实挺正常的——用AI写代码,一次需求描述清楚、AI一次写完、一次review通过,这种情况很少见。原因也简单:AI不会主动想边界场景。你让它"做一套RBAC权限系统",它会给你一个标准的RBAC实现,但标准实现不等于适合你的场景。

这次主要的反复集中在几个问题上:

技术栈对齐

最开始AI生成的代码混用了JPA和MyBatis-Plus两种ORM——有些实体用了@Entity,有些用了@TableName。原因是在最初的CLAUDE.md里没写清楚技术栈,AI按自己的训练数据默认选了JPA(Spring Boot教程里JPA出现频率高)。后来在CLAUDE.md里加了一条"使用MyBatis-Plus 3.5.x,禁止使用JPA",之后生成的代码就统一了。

审计日志的记录方式

上面说过了,AI一开始用AOP切面自动记录审计日志。看起来省事,但实际效果是记录的信息太粗糙——只能拿到方法名,拿不到变更前后的具体值。后来改成了Controller里手动调用auditLogService.logAudit(),虽然多写几行,但每条审计日志的detail字段都能写清楚具体做了什么操作。

异常处理统一化

AI生成的Controller里,有的地方抛RuntimeException,有的地方返回null。后来在CLAUDE.md里加了"使用BusinessException抛业务异常",同时写了GlobalExceptionHandler统一处理:

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public ApiResponse handleBusinessException(BusinessException e) {
        return ApiResponse.error(e.getCode(), e.getMessage());
    }
    @ExceptionHandler(PermissionDeniedException.class)
    public ApiResponse handlePermissionDenied(PermissionDeniedException e) {
        return ApiResponse.error(403, e.getMessage());
    }
}

需求写得越细,改的次数越少

这三个问题回头看,都不是技术难题,都是"初始需求描述不够细"导致的。所以现在养成了一个习惯:让AI写代码之前,先花10分钟把需求写到"不需要追问"的粒度。不是写大段文档,而是在对话里把关键点说清楚:

/implement 按任务清单实现权限模块,注意以下几点:
1. 使用 MyBatis-Plus,禁止使用 JPA
2. 权限校验用 @RequirePermission 注解 + AOP 拦截
3. 统一用 BusinessException 抛业务异常
4. 审计日志在 Controller 里手动调用 logAudit 记录
5. 所有返回结果用 ApiResponse 包装

这几条看起来都是废话("用MyBatis-Plus"、"抛BusinessException"谁不知道),但你不说AI就不会做。AI不是不懂MyBatis-Plus,是它不知道你的项目已经选定了技术栈,需要主动遵守。需求写得越细,改的次数越少,上下文token浪费也越少。这个道理跟带新人其实一样——你交代任务时不说清楚,新人做出来肯定不是你要的,然后反复沟通,双方都累。

写完之后的一些感受

这次全流程跑下来,AI真正帮上忙的是这些环节:

做得好的:

  • 数据模型设计——标准RBAC模型,AI生成的表结构和关联关系没出过错
  • 实体类和Mapper——MyBatis-Plus代码是AI最擅长的,生成质量和手写差不多
  • AOP拦截器——标准模式,训练数据多,一次写对
  • 全局异常处理——RestControllerAdvice模板代码,AI生成的很完整

必须人来做:

  • 方案选择(RBAC vs 策略 vs SpEL)——这是架构判断,AI只能列出选项
  • 技术栈对齐——AI默认混用JPA/MyBatis,需要人提前说清楚
  • 审计日志的记录方式——AOP自动记录还是手动调用,需要业务判断
  • 异常类型的统一化——AI会混用RuntimeException和自定义异常,需要人规范

结论是:AI把标准活干得又快又好,但非标准的坑你得自己趟。趟完把经验写回规则文件,下次AI就不会再踩。这个闭环跑顺了,开发效率确实比以前高。

本文对应的代码仓库开源在Gitee:gitee.com/tangyuewei/…,包含RBAC权限系统的完整实现、CLAUDE.md规则文件,以及开发过程中的提示词记录。可以直接拉下来跑。

相关下载