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

放在以前,这套活儿得手动写上好几天:设计表结构、写实体类、Service层、Controller层、单元测试,然后code review、改bug。这次想换个做法——让Claude从需求分析到代码生成完整走一遍,顺便验证一下之前文章里总结的规则管理方法,在实际项目里到底好不好用。
第一阶段:先搞清楚要做什么
接手这种需求,一上来就让AI写代码是大忌。超过3个文件需要改动的任务,先上/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这些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规则文件,以及开发过程中的提示词记录。可以直接拉下来跑。