本文档供 AI 写代码时遵循,描述框架封装位置和编码规范。
openspec/
├── backend/ 后端 Spring Boot 项目
└── frontend/ 前端 Vue3 项目
com.aijiuyi.admin/
├── AijiuyiAdminApplication.java 启动类
├── common/
│ ├── annotation/
│ │ ├── Log.java 操作日志注解
│ │ └── NoAuth.java 免鉴权注解
│ ├── aspect/
│ │ └── LogAspect.java 日志 AOP 切面(拦截所有 Controller)
│ ├── config/
│ │ ├── AsyncConfig.java 异步线程池配置
│ │ ├── AuthProperties.java 鉴权白名单配置属性
│ │ ├── MybatisPlusConfig.java MP 分页插件
│ │ ├── RedisConfig.java Redis 序列化配置
│ │ └── WebMvcConfig.java 拦截器 + 跨域配置
│ ├── constant/
│ │ └── ResultCode.java ★ 所有错误码和错误信息集中定义
│ ├── context/
│ │ └── RequestContext.java ThreadLocal(requestId/userId/userName)
│ ├── entity/
│ │ ├── Result.java 统一接口返回对象
│ │ └── SysLog.java 操作日志实体
│ ├── enums/
│ │ └── OperationType.java 操作类型枚举(查询/新增/更新/删除/其他)
│ ├── exception/
│ │ ├── BusinessException.java 业务异常类
│ │ └── GlobalExceptionHandler.java 全局异常处理器
│ ├── handler/
│ │ └── MyMetaObjectHandler.java MP 时间自动填充
│ ├── interceptor/
│ │ └── TokenInterceptor.java Token 鉴权拦截器
│ ├── service/
│ │ ├── SysLogService.java 日志 Service 接口
│ │ └── impl/SysLogServiceImpl.java 日志 Service 实现(异步保存)
│ └── util/
│ ├── IpUtil.java IP 获取工具
│ ├── LogUtil.java ★ 统一日志工具(自动携带 requestId)
│ ├── RedisUtil.java ★ Redis 统一操作工具
│ └── TokenUtil.java JWT Token 工具
├── controller/ Controller 层(业务)
│ ├── dto/ 请求参数 DTO
│ └── AuthController.java 示例:认证接口
├── entity/ 业务实体类
│ └── SysUser.java
├── mapper/ MyBatis Plus Mapper
│ ├── SysLogMapper.java
│ └── SysUserMapper.java
└── service/ 业务 Service
├── SysUserService.java
└── impl/SysUserServiceImpl.java
| 文件 | 说明 |
|---|---|
application.yml |
共享配置:端口、Jackson、MyBatis Plus、鉴权白名单 |
application-dev.yml |
开发环境:DB、Redis、日志(DEBUG级别,打印SQL) |
application-prod.yml |
生产环境:DB、Redis、日志(INFO级别,不打印SQL) |
切换环境:修改 application.yml 中的 spring.profiles.active
所有 Controller 必须返回 Result<T>,禁止直接返回业务对象。
// ✅ 正确
return Result.success(data);
return Result.error(ResultCode.USER_NOT_FOUND);
// ❌ 错误
return user;
所有错误码和错误信息必须在 ResultCode.java 中定义,禁止在其他地方硬编码。
// 在 ResultCode.java 新增
MY_ERROR(2001, "自定义错误信息"),
// 在业务代码中使用
throw new BusinessException(ResultCode.MY_ERROR);
使用 LogUtil 工具类,禁止直接用 @Slf4j 或 LoggerFactory(否则 requestId 不自动注入)。
// ✅ 正确(自动带 requestId)
LogUtil.info(MyService.class, "处理完成: userId={}", userId);
LogUtil.error(MyService.class, "保存失败", e);
// ❌ 错误
log.info("处理完成");
所有 Controller 方法上都应添加 @Log 注解,明确接口说明和操作类型。
@GetMapping("/list")
@Log(value = "查询用户列表", module = "用户管理", operationType = OperationType.QUERY)
public Result<List<SysUser>> list() { ... }
两种方式,推荐用注解(@NoAuth),也可在 application.yml 的 auth.white-list 中配置。
// 方式一:注解(推荐,精确到方法)
@PostMapping("/login")
@NoAuth
public Result<?> login(...) { ... }
// 方式二:配置文件(适合整批免鉴权路径)
# application.yml
auth:
white-list:
- /api/auth/login
只能用 MyBatis Plus,禁止手写 SQL(简单查询);复杂查询写在 mapper/ 目录对应的 XML 文件中。
// ✅ 简单查询用 Lambda
userMapper.selectOne(
new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUsername, username)
);
// ✅ 也可用 ServiceImpl 提供的 lambdaQuery
SysUser user = lambdaQuery().eq(SysUser::getUsername, username).one();
// ✅ 复杂查询写 XML(放 resources/mapper/XxxMapper.xml)
@Data
@TableName("表名")
public class XxxEntity {
@TableId(type = IdType.AUTO)
private Long id;
// 逻辑删除字段
@TableLogic
private Integer deleted;
// 自动填充时间
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
在 Service/Controller 中通过 RequestContext 获取当前登录用户,无需额外传参。
Long userId = RequestContext.getUserId();
String userName = RequestContext.getUserName();
String requestId = RequestContext.getRequestId();
统一使用 RedisUtil,禁止直接注入 RedisTemplate。
@Autowired
private RedisUtil redisUtil;
redisUtil.set("key", value, 3600); // 设置1小时过期
redisUtil.get("key"); // 获取
redisUtil.del("key"); // 删除
redisUtil.expire("key", 7200); // 修改过期时间
所有方法(包括私有方法)必须有中文注释,格式参考:
/**
* 根据用户名查询用户信息
*
* @param username 用户名
* @return 用户对象,不存在返回 null
*/
public SysUser findByUsername(String username) { ... }
frontend/src/
├── api/
│ ├── index.js API 统一导出入口
│ └── auth.js 认证模块 API(示例)
│ └── xxx.js 新业务模块 API(按模块建文件)
├── components/
│ ├── ExcelImport.vue Excel 导入组件
│ └── ExcelExport.vue Excel 导出按钮组件
├── layout/
│ └── index.vue 主布局(侧边栏 + 顶栏 + 内容区)
├── router/
│ └── index.js 路由配置(含登录守卫)
├── store/
│ └── user.js 用户状态 Store(Pinia)
├── utils/
│ ├── excel.js Excel 导入导出工具函数
│ ├── request.js Axios 封装(自动 Token + 统一错误处理)
│ └── storage.js localStorage 封装(Token/用户信息)
└── views/
├── login/index.vue 登录页
├── dashboard/index.vue 控制台
└── 404.vue 404 页面
每个业务模块创建独立的 API 文件,在 api/index.js 统一导出。
// 新建 src/api/user.js
import request from '@/utils/request'
export function getUserList(params) {
return request.get('/user/list', { params })
}
// 在 api/index.js 追加
export * as userApi from './user'
// 页面中使用
import { userApi } from '@/api'
const res = await userApi.getUserList({ page: 1, size: 10 })
需要登录的页面都放在 Layout 的 children 下,meta.requiresAuth = true(默认)。
不需要登录的页面配置 meta: { requiresAuth: false }。
// router/index.js 中添加子路由
{
path: 'user',
name: 'User',
component: () => import('@/views/user/index.vue'),
meta: { requiresAuth: true, title: '用户管理', icon: 'User' }
}
meta.icon 的值是 Element Plus 图标名(会自动显示在左侧菜单)。
<ExcelExport
:headers="[
{ label: '姓名', prop: 'name' },
{ label: '手机号', prop: 'phone' }
]"
:data="tableData"
file-name="用户列表"
/>
<ExcelImport
:on-import="handleImport"
button-text="导入用户"
@success="onImportSuccess"
/>
// handleImport 接收解析后的数组,执行业务逻辑(如调用后端接口批量保存)
async function handleImport(data) {
await userApi.batchImport(data)
}
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
userStore.nickname // 用户昵称
userStore.username // 用户名
userStore.avatar // 头像
userStore.isLoggedIn // 是否已登录
await userStore.logout() // 退出登录
url: jdbc:mysql://localhost:3306/aijiuyi_admin
账号: root
密码: root
字符集: utf8mb4
执行 backend/src/main/resources/sql/init.sql,创建所有表结构和初始数据。
默认管理员账号:
adminadmin123地址: localhost:6379
密码: 无(如有密码,在 application-dev.yml 的 spring.redis.password 中配置)
数据库: 0
cd backend
mvn spring-boot:run
# 或用 IDE 运行 AijiuyiAdminApplication.java
# 服务地址:http://localhost:8080/api
cd frontend
nvm use 22 # 切换到 Node.js 22
npm run dev # 开发服务器:http://localhost:3000
npm run build # 构建生产包
HTTP 请求
↓
TokenInterceptor(生成 requestId,写入 ThreadLocal + MDC)
↓
Token 验证(白名单/NoAuth 注解跳过)
↓
Controller 方法
↓
LogAspect(AOP 环绕,记录请求参数/结果/耗时)
↓
Service → Mapper(所有日志自动带 requestId,含 SQL 日志)
↓
返回 Result<T>
↓
LogAspect 异步保存操作日志到 sys_log 表
↓
TokenInterceptor afterCompletion(清理 ThreadLocal + MDC)
所有涉及数据库删除都是软删除