AI 从零搭建用户权限系统:完整过程复盘与经验分享
分享一次真实的开发实践:从空白仓库起步,借助 AI 从零构建了一套完整的 RBAC 权限管理系统。

技术选型为:前端 Vue,后端 Java 17 + Spring Boot 3,数据库 MySQL,权限认证采用 SaToken。核心功能涵盖用户登录、用户管理、角色管理、权限分配与操作审计日志等模块。
如果按照传统方式,这个任务通常需要数天手动完成:设计数据库表结构、编写实体类、Service 层、Controller 层、单元测试,随后还要进行代码评审和修复 Bug。这次我决定尝试全新路径——让 Claude 从需求分析到代码生成完整跑通,同时验证之前总结的规则管理方法在实际项目中的可行性。
第一阶段:明确需求与方案规划
面对这类需求,直接让 AI 编写代码往往适得其反。对于涉及多个文件改动的任务,首要步骤是使用 /plan 指令让其生成规划方案——这是此前经验的教训:中途发现方向错误导致上下文失效,不得不重新开始。两次惨痛经历后,再也不敢跳过规划阶段。
首先让 Claude 读取现有项目代码结构,随后将需求提交给它:
/plan 这是一个AI开发一个rbac系统示例,现在处于刚刚初始化仓库,还没进行任何开发的状态。前端可以使用vue,后端使用ja va,版本使用17,数据库使用mysql。增加用户登录、用户管理与权限管理功能。RBAC 权限系统,要求:- 支持用户多角色分配- 基于角色的细粒度权限控制- 权限校验用 AOP 拦截,不在每个方法里手写- 角色和权限变更必须记录审计日志- 数据库用 MySQL
AI 对比分析了三种实现方案:
- 基于经典 RBAC 模型:用户关联角色,角色关联权限点,标准方案
- 基于策略模式:每个权限定义独立策略类,灵活性高但实现成本较大
- 基于 Spring Expression Language (SpEL):通过表达式动态定义权限,灵活但复杂度较高
最终选择了第一种方案。理由充分: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 加载策略——每次查询均为主动调用,N+1 问题通过手动优化查询逻辑来规避,后续会谈到这一点。
AOP 权限拦截器
先看 AOP 部分,这是权限系统的核心。
@RequirePermission 注解定义得非常简洁:
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RequirePermission {/** * 权限标识,格式为 "resource:action" */String value();}
切面实现如下:
@Slf4j@Aspect@Component@RequiredArgsConstructorpublic 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")@PostMappingpublic 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 方法:
@Overridepublic 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 的具体实现:
@Overridepublic 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 次批量查权限详情)。
不过该项目是个人演示项目,数据量较小,暂时无需过度优化。如果用于真实生产环境,一定会要求 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 一次生成完毕、你一次评审通过,这种理想情况在实际中很少出现。
原因并不复杂: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 中手动调用后,每条审计日志的 detail 字段都能清晰描述具体操作,信息含量完全不同。
异常处理统一化
AI 生成的 Controller 中,部分地方抛出 RuntimeException,部分直接返回 null。在 CLAUDE.md 中添加“使用 BusinessException 抛业务异常”后,又编写了 GlobalExceptionHandler 进行统一处理:
@RestControllerAdvicepublic 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,禁止使用 JPA2. 权限校验用 @RequirePermission 注解 + AOP 拦截3. 统一用 BusinessException 抛业务异常4. 审计日志在 Controller 里手动调用 logAudit 记录5. 所有返回结果用 ApiResponse 包装
这些点看似理所当然(如“使用 MyBatis-Plus”“抛 BusinessException”),但若不明确说明,AI 不会主动执行。AI 并非不懂 MyBatis-Plus,而是不知道你的项目已选定技术栈,需要遵循约定。
需求越细化,修改次数越少,上下文 token 浪费也越少。这个道理与带新人的经验一致:交代任务时若不明确,新人做出来的结果必然偏离预期,进而反复沟通,双方疲惫。
几个核心判断
完整流程跑下来,真正发挥价值的环节如下:
做得好的:
- 数据模型设计——标准 RBAC 模型,AI 生成的表结构和关联关系未出现错误
- 实体类和 Mapper——MyBatis-Plus 代码是 AI 最擅长的领域,生成质量与手写无显著差异
- AOP 拦截器——标准模式,训练数据充足,一次编写正确
- 全局异常处理——RestControllerAdvice 模板代码,AI 生成得十分完整
必须由人工完成:
- 方案选择(RBAC vs 策略模式 vs SpEL)——属于架构决策,AI 只能列出选项,最终选择需要人拍板
- 技术栈对齐——AI 默认混用 JPA/MyBatis,需要人员提前明确说明
- 审计日志的记录方式——AOP 自动记录还是手动调用,这需要业务判断,而非单纯技术选择
- 异常类型的统一化——AI 会混用 RuntimeException 与自定义异常,需要人工规范
AI 将标准任务处理得又快又好,但非标准的坑仍需自己趟过。将经验写入规则文件后,下次 AI 便不会再犯。这个闭环跑通后,开发效率确实显著提升。
