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

摘要:本文记录了作者使用 Claude AI 从零开发一个完整 RBAC 权限系统的全过程。文章详细介绍了从需求分析、方案规划、规则制定到代码实现的完整流程,重点分享了 AI 开发中的实践经验:1) 使用 /plan 命令先规划再开发,避免方向错误;2) 通过 CLAUDE.md 明确技术栈和代码规范;3) 采用三阶段开发法(需求分析→定规则→写代码);4) 实现基于注解+AOP的权限拦截器;5) 处理审计日志和异常统一化。文章还总结了 AI 开发的优缺点:AI 擅长标准模板代码(数据模型、实体类、AOP),但在技术栈对齐、业务判断和性能优化等方面仍需人工介入。最终结论是:AI 把标准活干得又快又好,但非标准的坑你得自己趟,趟完把经验写回规则文件,下次 AI 就不会再踩。

接一个需求

从零开始——一个刚初始化完的空白仓库,要让 AI 从零搭出一套完整的 RBAC 权限系统。

前端用 Vue,后端 Java 17 + Spring Boot 3,数据库 MySQL,认证用 SaToken。需求包括:用户登录、用户管理、角色管理、权限分配、操作审计日志。

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


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

接手这种需求,我不会一上来就让 AI 写代码。超过 3 个文件需要改动的任务,先用 /plan 让它规划方案——这是我之前文章里总结的经验,改到一半发现方向不对,整个上下文废了只能重新开始,这种情况我遇到过两次。

我让 Claude 读了项目现有代码结构,然后给它这个需求:

/plan 这是一个AI开发一个rbac系统示例,现在处于刚刚初始化仓库,还没进行任何开发的状态。
前端可以使用vue,后端使用java,版本使用17,数据库使用mysql。
增加用户登录、用户管理与权限管理功能。

RBAC 权限系统,要求:
- 支持用户多角色分配
- 基于角色的细粒度权限控制
- 权限校验用 AOP 拦截,不在每个方法里手写
- 角色和权限变更必须记录审计日志
- 数据库用 MySQL

它给出的方案对比了三种实现:

  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 里的核心规则:

## 技术栈
- Java 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 权限拦截器

先说 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<Role> 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<String> roleCodes = userRoleService.getUserRoleCodes(userId);
    if (roleCodes.isEmpty()) {
        return false;
    }
    // 获取用户所有角色的权限
    List<String> userPermissions = getUserPermissionCodes(userId);
    return userPermissions.contains(permissionCode);
}

然后是 getUserPermissionCodes 的具体实现:

@Override
public List<String> getUserPermissionCodes(Long userId) {
    List<Role> roles = userRoleService.getUserRoles(userId);
    if (CollUtil.isEmpty(roles)) {
        return List.of();
    }

    Set<String> permissions = new HashSet<>();
    for (Role role : roles) {
        if (role.getStatus() != 1) continue;
        List<Long> permissionIds = rolePermissionService.getPermissionIdsByRoleId(role.getId());
        if (!permissionIds.isEmpty()) {
            List<Permission> 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 次批量查权限详情)。

不过这个项目是个人演示项目,数据量小,暂时没到必须优化的程度。如果放到真实生产环境,我会让 AI 改成一次 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<Void> handleBusinessException(BusinessException e) {
        return ApiResponse.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(PermissionDeniedException.class)
    public ApiResponse<Void> 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/ai-builds-rbac,包含 RBAC 权限系统的完整实现、CLAUDE.md 规则文件,以及开发过程中的提示词记录。可以直接拉下来跑。


tangyuewei,从后端出发,用 AI 拓展到全栈的工程师。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐