Files
hzdyjs/.cursor/cursorrules-backend.md
T
zlwuandCursor 1d67506451 fix: 显式声明接口参数名并优化移动端若干页面
生产环境未保留方法参数名时 Spring 无法绑定 @RequestParam/@PathVariable,补齐注解名称并在 Maven 编译中强制 -parameters;同时优化走访、勤工助学、请假和宿舍点名页交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 11:48:08 +08:00

2056 lines
63 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Cursor Rules - 后端开发规范
## 适用范围
- **项目目录**: `backend/` (基于 RuoYi-Vue-Plus + Spring Boot 3.4.6)
- **Java 版本**: JDK 17/21
- **后端服务**: http://localhost:8080
---
# 后端开发规范 (backend/)
## 1. 技术栈规范
### 核心技术
- **框架**: Spring Boot 3.4.6
- **Java 版本**: JDK 17/21
- **Web 容器**: Undertow
- **ORM 框架**: MyBatis-Plus 3.5.12
- **数据库**: MySQL/Oracle/PostgreSQL/SQLServer (多数据库支持)
- **缓存**: Redis 5-7 + Redisson 3.45.1
- **权限认证**: Sa-Token 1.42.0 + JWT
- **任务调度**: SnailJob 1.5.0
- **工作流**: Warm-Flow 1.7.3
## 2. 项目结构规范
### 后端模块结构
```
backend/
├── ruoyi-admin/ # 管理后台启动模块
├── ruoyi-common/ # 公共模块组
│ ├── ruoyi-common-core/ # 核心模块
│ ├── ruoyi-common-mybatis/ # MyBatis配置
│ ├── ruoyi-common-redis/ # Redis配置
│ ├── ruoyi-common-satoken/ # Sa-Token配置
│ └── ... (其他公共模块)
├── ruoyi-modules/ # 业务模块组
│ ├── ruoyi-system/ # 系统管理模块
│ ├── ruoyi-demo/ # 示例模块
│ ├── ruoyi-hq/ # 后勤管理模块 (hq)
│ ├── ruoyi-rs/ # 人事管理模块 (rs)
│ ├── ruoyi-cw/ # 财务管理模块 (cw)
│ ├── ruoyi-aqbw/ # 安全保卫管理模块 (aqbw)
│ ├── ruoyi-zsjy/ # 招生就业管理模块 (zsjy)
│ ├── ruoyi-xg/ # 学工管理模块 (xg)
│ └── ... (其他业务模块)
├── ruoyi-extend/ # 扩展模块组
└── script/ # 脚本和SQL
```
## 🔴 模块工程管理规范(强制性,2025-01-23新增)
### 📦 模块工程创建规范
**⚠️ 基于ruoyi-hq工程实践制定,后续所有新模块必须遵循此规范!**
#### 🏗️ 模块命名和位置规范
**模块位置**: `backend/ruoyi-modules/ruoyi-{模块简称}/`
**已规划模块列表**:
- ✅ `ruoyi-hq` - 后勤管理模块 (已完成)
- 🚧 `ruoyi-rs` - 人事管理模块
- 🚧 `ruoyi-cw` - 财务管理模块
- 🚧 `ruoyi-aqbw` - 安全保卫管理模块
- 🚧 `ruoyi-zsjy` - 招生就业管理模块
- 🚧 `ruoyi-xg` - 学工管理模块
#### 📋 Maven配置规范
**pom.xml标准模板**:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi-modules</artifactId>
<groupId>org.dromara</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-{模块简称}</artifactId>
<description>
{模块名称}管理模块
</description>
<dependencies>
<!-- 通用工具 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-doc</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-log</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-security</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-excel</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-idempotent</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-tenant</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-social</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-ratelimiter</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mail</artifactId>
</dependency>
<!-- 系统模块依赖 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-system</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-job</artifactId>
</dependency>
<!-- 代码生成-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- demo模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-demo</artifactId>
</dependency>
<!-- 工作流模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-workflow</artifactId>
</dependency>
</dependencies>
</project>
```
#### 🏛️ 包结构规范
**Java包结构**: `org.dromara.{模块简称}`
**标准目录结构**:
```
ruoyi-{模块简称}/
├── pom.xml
└── src/main/
├── java/org/dromara/{模块简称}/
│ ├── controller/ # REST API控制器层
│ ├── service/ # 业务服务接口层
│ │ └── impl/ # 业务服务实现层
│ ├── mapper/ # 数据访问层接口
│ └── domain/ # 数据对象层
│ ├── [Entity].java # 实体类 (继承TenantEntity)
│ ├── bo/ # 业务对象 (Business Object)
│ └── vo/ # 视图对象 (View Object)
└── resources/
└── mapper/ # MyBatis XML映射文件
```
#### 🎯 代码规范要求
**Entity实体类**:
```java
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("表名")
public class EntityName extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id")
private Long id;
// 业务字段...
}
```
**Service接口**:
```java
public interface IEntityService {
EntityVo queryById(Long id);
TableDataInfo<EntityVo> queryPageList(EntityBo bo, PageQuery pageQuery);
List<EntityVo> queryList(EntityBo bo);
Boolean insertByBo(EntityBo bo);
Boolean updateByBo(EntityBo bo);
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
}
```
**Controller控制器**:
```java
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/{模块简称}/entity")
public class EntityController extends BaseController {
private final IEntityService entityService;
@SaCheckPermission("{模块简称}:entity:list")
@GetMapping("/list")
public TableDataInfo<EntityVo> list(EntityBo bo, PageQuery pageQuery) {
return entityService.queryPageList(bo, pageQuery);
}
// 其他标准接口...
}
```
**Mapper接口**:
```java
public interface EntityMapper extends BaseMapperPlus<Entity, EntityVo> {
// 自定义查询方法...
}
```
#### 🔐 权限命名规范
**权限编码格式**: `{模块简称}:{功能}:{操作}`
**示例**:
- `hq:repair:list` - 后勤模块报修列表查询权限
- `rs:employee:add` - 人事模块员工新增权限
- `cw:budget:export` - 财务模块预算导出权限
#### 📚 模块开发最佳实践
**1. 遵循ruoyi-hq标准模式**:
- 继承TenantEntity支持多租户
- 使用@SaCheckPermission进行权限控制
- 遵循TableDataInfo分页响应格式
- 使用MapstructUtils进行对象转换
**2. 代码质量要求**:
- 使用Lombok减少样板代码
- 完整的中文注释和文档
- 统一的异常处理机制
- 完善的参数校验
**3. 模块间解耦**:
- 模块间通过Service接口交互
- 避免直接依赖其他模块的内部实现
- 共用功能提取到common模块
**4. 数据库设计**:
- 必须遵循数据库规范基础字段
- 支持逻辑删除和多租户
- 合理的索引设计
- 完整的数据字典配置
#### 🚨 模块创建检查清单
**创建新模块前必须检查**:
- [ ] 模块名称符合规范?(`ruoyi-{简称}`)
- [ ] Maven配置是否正确?
- [ ] 包结构是否遵循标准?
- [ ] 是否包含完整的分层架构?
- [ ] 权限编码是否符合规范?
- [ ] 是否遵循ruoyi-hq的最佳实践?
- [ ] 数据库设计是否符合规范?
- [ ] 是否添加了相关文档?
## 3. 开发规范
### 代码规范
- **严格遵守**: Alibaba Java 开发手册
- **命名规范**:
- 类名: PascalCase (如: UserController)
- 方法名: camelCase (如: getUserList)
- 常量: UPPER_SNAKE_CASE (如: MAX_RETRY_COUNT)
- 包名: 全小写 (如: org.dromara.system)
### 🔴 数据库规范(强制性)
- **⚠️ 超级重要**: 所有数据库操作必须严格遵循 `docs/设计文档/database/mysql-database-standard.md` 规范
- **现有数据库模型**: 参考 `docs/设计文档/database/hzdyjs_xg.sql` 中的完整表结构
- **环境配置**: 开发环境数据库信息见 `docs/设计文档/database/database_msg.md`
#### 🚨 强制性核心规范
1. **所有业务表必须包含标准基础字段**
2. **统一使用雪花 ID (bigint(20)) 作为主键**
3. **强制使用逻辑删除,禁止物理删除**
4. **所有表必须支持多租户**
5. **完整的中文注释是必须的**
6. **禁止使用数据库外键约束**,关联关系通过应用层维护
#### 🔴 表命名规范(强制性)
**⚠️ 超级重要:所有数据库表必须按业务模块使用统一前缀命名!**
**模块表名前缀规范**:
- **后勤管理**:`hq_` 开头 (如:`hq_repair`, `hq_maintenance`)
- **人事管理**:`rs_` 开头 (如:`rs_employee`, `rs_department`)
- **财务管理**:`cw_` 开头 (如:`cw_budget`, `cw_expense`)
- **学工管理**:`xg_` 开头 (如:`xg_student`, `xg_dormitory`)
- **招生就业**:`zsjy_` 开头 (如:`zsjy_application`, `zsjy_job`)
- **安全保卫**:`aqbw_` 开头 (如:`aqbw_security`, `aqbw_patrol`)
- **系统公共**:`common_` 开头 (如:`common_dict`, `common_config`)
**命名示例**:
```sql
-- 后勤管理模块表
CREATE TABLE `hq_repair` (
`id` bigint(20) NOT NULL COMMENT '报修ID',
-- 其他字段...
) COMMENT='报修管理表';
-- 人事管理模块表
CREATE TABLE `rs_employee` (
`id` bigint(20) NOT NULL COMMENT '员工ID',
-- 其他字段...
) COMMENT='员工信息表';
-- 财务管理模块表
CREATE TABLE `cw_budget` (
`id` bigint(20) NOT NULL COMMENT '预算ID',
-- 其他字段...
) COMMENT='预算管理表';
-- 学工管理模块表
CREATE TABLE `xg_student` (
`id` bigint(20) NOT NULL COMMENT '学生ID',
-- 其他字段...
) COMMENT='学生信息表';
-- 招生就业模块表
CREATE TABLE `zsjy_application` (
`id` bigint(20) NOT NULL COMMENT '申请ID',
-- 其他字段...
) COMMENT='招生申请表';
-- 安全保卫模块表
CREATE TABLE `aqbw_security` (
`id` bigint(20) NOT NULL COMMENT '安全记录ID',
-- 其他字段...
) COMMENT='安全管理表';
-- 系统公共模块表
CREATE TABLE `common_dict` (
`id` bigint(20) NOT NULL COMMENT '字典ID',
-- 其他字段...
) COMMENT='数据字典表';
```
**强制性要求**:
- ✅ 新建表时必须使用对应模块前缀
- ✅ 表名采用下划线命名法(snake_case)
- ✅ 表名必须是英文,禁止使用中文或拼音
- ✅ 表注释必须用中文清楚说明表的用途
- ❌ 禁止跨模块混用前缀(如人事模块不能创建 cw_ 开头的表)
- ❌ 禁止使用无前缀的表名(系统核心表除外)
**实体类命名对应**:
```java
// 表名: hq_repair -> 实体类: HqRepair
@TableName("hq_repair")
public class HqRepair extends TenantEntity {
// ...
}
// 表名: rs_employee -> 实体类: RsEmployee
@TableName("rs_employee")
public class RsEmployee extends TenantEntity {
// ...
}
// 表名: cw_budget -> 实体类: CwBudget
@TableName("cw_budget")
public class CwBudget extends TenantEntity {
// ...
}
```
#### 🔴 强制性基础字段模板
```sql
-- 所有业务表必须包含以下字段(顺序和类型必须完全一致)
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0代表存在 1代表删除)',
-- 业务字段在此处添加
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_create_time` (`create_time`)
```
#### 🔴 AI 开发强制要求
当使用 AI 进行数据库开发时:
1. **必须先阅读** `docs/设计文档/database/mysql-database-standard.md` 规范
2. **必须使用标准模板**,不得随意修改基础字段
3. **必须保持与现有表结构的一致性**
4. **建表后必须立即更新** `hzdyjs_xg.sql` 文件
5. **必须在变更记录中记录** 数据库变更
### 🔴 API 接口规范(强制性)
**⚠️ 超级重要:所有 API 接口必须严格遵循以下统一规范!**
#### 🔐 请求头规范(强制性)
**所有需要认证的接口必须在请求头中包含 Token,格式如下:**
```json
{
"Content-Type": "application/json;charset=utf-8",
"Authorization": "Bearer {access_token}",
"clientid": "e5cd7e4891bf95d1d19206ce24a7b32e",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"referer": "http://localhost/login?redirect=%252Findex"
}
```
**请求头字段说明**:
- **Authorization**: `Bearer ` + Token 值(⚠️ 注意 Bearer 后面有一个空格)
- **clientid**: 客户端 ID,用于权限验证
- **Content-Type**: 请求内容类型,必须为 application/json;charset=utf-8
- **Accept-Language**: 语言设置,用于国际化
- **referer**: 来源页面,用于安全验证
**Token 格式强制要求**:
- ✅ 正确:`"Authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."`
- ❌ 错误:`"Authorization": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."`(缺少 Bearer 前缀)
- ❌ 错误:`"Authorization": "Bearer eyJ0eXAi..."`(Bearer 后面没有空格)
#### 📊 统一响应格式(必须遵循)
**基础响应格式(分页列表)**:
```json
{
"total": 7,
"rows": [],
"code": 200,
"msg": "查询成功"
}
```
#### 🎯 响应格式详细说明
**1. 分页列表响应**
```json
{
"total": 100,
"rows": [
{
"id": 1,
"name": "示例数据1",
"createTime": "2025-06-30 12:00:00"
},
{
"id": 2,
"name": "示例数据2",
"createTime": "2025-06-30 12:01:00"
}
],
"code": 200,
"msg": "查询成功"
}
```
**2. 非分页列表响应**
```json
{
"code": 200,
"msg": "查询成功",
"data": [
{
"id": 1,
"name": "示例数据1"
},
{
"id": 2,
"name": "示例数据2"
}
]
}
```
**3. 单条数据响应**
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"id": 1,
"name": "示例数据",
"createTime": "2025-06-30 12:00:00"
}
}
```
**4. 操作成功响应(无数据)**
```json
{
"code": 200,
"msg": "操作成功",
"data": null
}
```
**5. 错误响应**
```json
{
"code": 400,
"msg": "参数错误:用户名不能为空",
"data": null
}
```
#### 📋 状态码规范
**成功状态码**:
- `200` - 操作成功
- `201` - 创建成功
**客户端错误(4xx)**:
- `400` - 请求参数错误
- `401` - 未认证/认证失败
- `403` - 无权限访问
- `404` - 资源不存在
- `409` - 数据冲突
**服务端错误(5xx)**:
- `500` - 服务器内部错误
- `502` - 网关错误
- `503` - 服务不可用
#### 🚨 接口开发强制要求
**1. Controller 返回格式**
```java
// ✅ 正确 - 分页接口直接返回TableDataInfo
@GetMapping("/list")
public TableDataInfo<UserVo> list(UserBo bo, PageQuery pageQuery) {
TableDataInfo<UserVo> result = userService.queryPageList(bo, pageQuery);
return result;
}
// ✅ 正确 - 非分页接口使用R类型包装
@GetMapping("/options")
public R<List<UserVo>> options() {
List<UserVo> list = userService.selectUserOptions();
return R.ok(list);
}
// ❌ 错误 - 分页接口使用R类型包装
@GetMapping("/list")
public R<TableDataInfo<UserVo>> list(UserBo bo, PageQuery pageQuery) {
TableDataInfo<UserVo> result = userService.queryPageList(bo, pageQuery);
return R.ok(result); // 这会产生多层嵌套
}
```
**2. 分页接口规范**
```java
// 分页查询直接返回 TableDataInfo 数据
@GetMapping("/list")
public TableDataInfo<UserVo> list(UserBo bo, PageQuery pageQuery) {
TableDataInfo<UserVo> result = userService.queryPageList(bo, pageQuery);
return result;
}
// 返回格式:
// {
// "total": 100,
// "rows": [...],
// "code": 200,
// "msg": "查询成功"
// }
```
**3. 非分页列表接口**
```java
// 非分页列表查询(下拉选项等)
@GetMapping("/options")
public R<List<UserVo>> options() {
List<UserVo> list = userService.selectUserOptions();
return R.ok(list);
}
// 返回格式:
// {
// "code": 200,
// "msg": "查询成功",
// "data": [...]
// }
```
**4. 单条数据接口**
```java
// 单条数据查询
@GetMapping("/{id}")
public R<UserVo> getInfo(@PathVariable Long id) {
UserVo user = userService.queryById(id);
return R.ok(user);
}
// 返回格式:
// {
// "code": 200,
// "msg": "操作成功",
// "data": {...}
// }
```
**5. 操作类接口**
```java
// 新增/修改/删除操作
@PostMapping
public R<Void> add(@RequestBody UserBo bo) {
boolean result = userService.insertByBo(bo);
return result ? R.ok() : R.fail("操作失败");
}
// 成功返回格式:
// {
// "code": 200,
// "msg": "操作成功",
// "data": null
// }
// 失败返回格式:
// {
// "code": 500,
// "msg": "操作失败",
// "data": null
// }
```
#### 🔍 接口命名规范
**RESTful 风格**:
- `GET /api/users` - 查询用户列表
- `GET /api/users/{id}` - 查询单个用户
- `POST /api/users` - 新增用户
- `PUT /api/users` - 修改用户(传完整对象)
- `PATCH /api/users/{id}` - 部分更新用户
- `DELETE /api/users/{id}` - 删除用户
**特殊功能接口**:
- `GET /api/users/options` - 获取用户下拉选项
- `GET /api/users/statistics` - 获取用户统计数据
- `POST /api/users/batch` - 批量操作用户
- `POST /api/users/{id}/enable` - 启用用户
- `POST /api/users/{id}/disable` - 禁用用户
#### 🛡️ 权限控制规范
```java
// 每个接口必须添加权限注解
@SaCheckPermission("system:user:list")
@GetMapping("/list")
public R<TableDataInfo<UserVo>> list(UserBo bo, PageQuery pageQuery) {
// 接口实现
}
```
#### 📝 参数验证规范
```java
// 使用Bean Validation进行参数校验
@PostMapping
public R<Void> add(@Validated(AddGroup.class) @RequestBody UserBo bo) {
// 接口实现
}
// VO类中的验证注解
public class UserBo {
@NotBlank(message = "用户名不能为空", groups = {AddGroup.class, EditGroup.class})
@Size(max = 30, message = "用户名长度不能超过30个字符")
private String userName;
@NotBlank(message = "手机号不能为空", groups = {AddGroup.class, EditGroup.class})
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}
```
#### 🔴 请求参数名必须显式声明(强制性)
**背景**:生产环境若未使用 `-parameters` 编译,Spring 无法通过反射获取方法参数名,会报错:
`Name for argument of type [java.lang.Long] not specified, and parameter name information not available via reflection.`
**强制要求**:`@RequestParam` / `@PathVariable` 必须写明参数名,禁止依赖编译器保留参数名。
```java
// ✅ 正确
@PutMapping("/authUser/selectAll")
public R<Void> selectAuthUserAll(@RequestParam("roleId") Long roleId,
@RequestParam("userIds") Long[] userIds) { ... }
@GetMapping("/{roleId}")
public R<SysRoleVo> getInfo(@PathVariable("roleId") Long roleId) { ... }
// ❌ 错误(本地 IDEA 可能正常,生产易 500)
public R<Void> selectAuthUserAll(Long roleId, Long[] userIds) { ... }
public R<SysRoleVo> getInfo(@PathVariable Long roleId) { ... }
```
父 POM 已配置 `<parameters>true</parameters>` 与 `-parameters` 作为兜底,但接口代码仍必须显式声明参数名。
#### 🚨 接口开发检查清单
**每个新接口必须检查**:
- [ ] 是否在请求头中正确使用了 Bearer Token 格式?
- [ ] 分页接口是否直接返回 TableDataInfo 格式?
- [ ] 非分页接口是否使用 R 类型包装返回结果?
- [ ] 是否遵循了统一的响应格式规范?
- [ ] 是否添加了@SaCheckPermission 权限注解?
- [ ] 是否添加了@Operation 接口描述?
- [ ] 是否进行了参数校验?
- [ ] `@RequestParam`/`@PathVariable` 是否显式声明了参数名?
- [ ] 是否符合 RESTful 命名规范?
- [ ] 是否包含了必要的请求头(clientid、Content-Type 等)?
- [ ] 是否在 API 文档中记录了接口信息?
#### 💥 违规后果
**如果发现接口不符合统一规范,必须立即修改!不符合规范的接口不允许合并到主分支。**
### 🔴 API 开发强制性工作流程
#### 💡 基于场地预约 API 开发实践的标准流程
**⚠️ 本流程基于场地预约管理系统的成功开发经验制定,必须严格遵循!**
#### 步骤 1:需求分析和文档创建
**在编写任何代码之前必须完成**:
- 📋 **创建需求文档**:`docs/需求文档/[模块名]/` 详细的业务需求说明
- 🏗️ **业务模块文档**:`docs/业务模块/[模块名]/README.md` 技术架构说明
- 🗄️ **数据库设计**:完整的表结构、字典数据、权限配置设计
**参考示例**:
- `docs/需求文档/后勤场地预约需求/场地预约系统需求说明.md`
- `docs/业务模块/venue-reservation/README.md`
#### 步骤 2:数据库初始化(必须先行)
**在编写业务代码之前必须完成数据库设计**:
- 🗄️ **数据表创建**:`docs/设计文档/database/[模块名]-module.sql`
- 📊 **字典数据配置**:`docs/设计文档/database/[模块名]-data.sql`
- 🔐 **菜单权限配置**:`docs/设计文档/database/[模块名]-menu.sql`
- 🧪 **示例数据插入**:便于开发和测试
**参考示例**:
```sql
-- 04-venue-reservation-module.sql (数据表)
-- 04-venue-reservation-data.sql (字典数据)
-- 04-venue-reservation-menu.sql (菜单权限)
```
#### 步骤 3:后端模块开发
**完整的 ruoyi 模块开发结构**:
```
ruoyi-[模块名]/
├── pom.xml # 模块依赖配置
├── src/main/java/org/dromara/[模块名]/
│ ├── domain/ # 实体类
│ │ ├── [Entity].java # 数据库实体(继承TenantEntity)
│ │ ├── bo/[EntityBo].java # 业务对象(含验证注解)
│ │ └── vo/[EntityVo].java # 视图对象(含Excel注解)
│ ├── mapper/ # 数据访问层
│ │ ├── [Entity]Mapper.java # Mapper接口
│ │ └── xml/[Entity]Mapper.xml # SQL映射文件
│ ├── service/ # 业务逻辑层
│ │ ├── I[Entity]Service.java # 服务接口
│ │ └── impl/[Entity]ServiceImpl.java # 服务实现
│ └── controller/ # 控制器层
│ └── [Entity]Controller.java # REST API控制器
└── src/main/resources/
└── mapper/[Entity]Mapper.xml # MyBatis映射文件
```
**开发要点**:
- ✅ **实体类**:必须继承`TenantEntity`,包含完整注释
- ✅ **业务对象**:添加`@Validated`参数校验注解
- ✅ **视图对象**:支持 Excel 导出,添加`@ExcelProperty`注解
- ✅ **权限控制**:每个接口必须添加`@SaCheckPermission`注解
- ✅ **响应格式**:分页用`TableDataInfo`,非分页用`R`类型
#### 步骤 4:API 接口规范实现
**16 个标准 API 接口设计模式**(基于场地预约实践):
**基础管理接口(6 个)**:
```java
@GetMapping("/list") // 分页查询列表
@GetMapping("/{id}") // 查询详情
@PostMapping // 新增
@PutMapping // 修改
@DeleteMapping("/{ids}") // 删除
@GetMapping("/available") // 获取可用选项
```
**业务功能接口(4 个)**:
```java
@GetMapping("/my") // 查询我的数据
@GetMapping("/pending") // 查询待处理数据
@GetMapping("/conflict") // 业务规则检查
@PostMapping("/approve") // 审批操作
```
**辅助功能接口(6 个)**:
```java
@PostMapping("/export") // 导出Excel
@PostMapping("/import") // 导入Excel
@GetMapping("/statistics") // 统计数据
@PostMapping("/batch") // 批量操作
@PostMapping("/{id}/enable") // 启用操作
@PostMapping("/{id}/disable") // 禁用操作
```
#### 步骤 5:立即更新 API 文档(强制性)
**⚠️ 代码写完后立即执行,不得延后!**
- 📖 **创建模块 API 文档**:`docs/开发文档/API/[模块名]管理API.md`
- 📋 **按标准模板编写**:参考`docs/开发文档/API/API文档模板.md`
- 🔗 **更新文档总览**:在`docs/开发文档/API/API文档总览.md`中添加导航
- ✅ **包含完整信息**:16 个接口的详细参数、响应、权限、业务规则
**参考示例**:
- `docs/开发文档/API/场地预约管理API.md` (400+行完整文档)
#### 步骤 6:创建自动化测试脚本
**完整的 Python 测试脚本开发**:
- 🐍 **测试脚本**:`test/[模块名]API测试脚本.py`
- 🔐 **AES+RSA 加密登录**:严格按照`cursorrules-api-test.md`规范
- 🧪 **完整功能覆盖**:测试所有 16 个 API 接口
- 📋 **业务规则测试**:验证时间冲突、权限控制等
- 🧹 **自动数据清理**:测试完成后清理测试数据
**测试脚本标准结构**:
```python
class [Module]APITest:
def __init__(self): # 初始化和配置
def login(self): # AES+RSA双重加密登录
def test_[entity]_management(self): # 基础管理功能测试
def test_[business]_features(self): # 业务功能测试
def test_business_rules(self): # 业务规则验证
def cleanup_test_data(self): # 测试数据清理
def run_all_tests(self): # 执行完整测试
```
#### 步骤 7:模块集成和配置
**模块集成到主项目**:
- 📦 **添加模块依赖**:在`ruoyi-modules/pom.xml`中添加子模块
- 🔗 **集成到启动模块**:在`ruoyi-admin/pom.xml`中添加依赖
- ⚙️ **版本管理**:在根`pom.xml`的`dependencyManagement`中配置版本
#### 步骤 8:记录变更日志(强制性)
**详细记录开发过程**:
- 📝 **变更记录**:`docs/变更管理/code-adjust-record-YYYY-MM-DD-{username}.md`
- ⏰ **按时间记录**:需求分析 → 数据库 → 后端开发 → 文档 → 测试的完整流程
- 📊 **包含统计**:文件数量、接口数量、测试覆盖率等
#### 步骤 9:验证和测试
**完整的验证流程**:
- 🖥️ **后端服务启动**:确保服务正常运行
- 🧪 **API 功能测试**:运行自动化测试脚本
- 📖 **文档准确性验证**:确保文档示例与实际响应一致
- 🔐 **权限控制测试**:验证接口权限和数据权限
#### 🚨 违规后果
**如果发现以下问题,该代码变更将被视为不完整:**
- ❌ 数据库变更未创建对应 SQL 脚本
- ❌ API 接口变更未同步更新文档
- ❌ 缺少自动化测试脚本
- ❌ 未记录详细的变更日志
- ❌ 模块未正确集成到主项目
#### 📊 开发完成标准
**一个完整的 API 模块开发必须包含**:
- ✅ 完整的需求和技术文档(2-3 个文档)
- ✅ 数据库脚本和权限配置(3 个 SQL 文件)
- ✅ 完整的后端模块代码(15-20 个 Java 文件)
- ✅ 详细的 API 接口文档(400+行)
- ✅ 自动化测试脚本(450+行)
- ✅ 完整的变更记录日志
- ✅ 模块成功集成和功能验证
#### 💡 最佳实践总结
**基于场地预约 API 开发的成功经验**:
1. **先文档后代码**:需求分析完成后再开始编码
2. **数据库优先**:表结构确定后再开发业务逻辑
3. **标准化接口**:16 个接口模式覆盖 90%的业务场景
4. **文档同步更新**:代码完成立即更新文档
5. **完整测试验证**:自动化测试确保质量
6. **规范化集成**:严格按照项目架构规范集成
## 4. 🔐 菜单界面和接口权限配置规范
### 🚨 权限配置核心原理
**⚠️ 超级重要:每个菜单界面和相关接口都必须在`sys_menu`表中配置对应的权限数据!**
#### 权限配置核心概念
1. **菜单权限**:控制用户是否能看到某个菜单项
2. **按钮权限**:控制用户是否能看到和使用某个功能按钮
3. **接口权限**:通过 Sa-Token 注解控制用户是否能调用某个 API 接口
4. **角色权限**:通过`sys_role_menu`表关联,控制哪个角色拥有哪些权限
#### 权限验证机制
```
用户登录 → 获取用户角色 → 查询角色权限(sys_role_menu) → 验证接口权限(@SaCheckPermission) → 允许/拒绝访问
```
### 🔴 sys_menu 表结构和权限配置
#### sys_menu 表核心字段说明
```sql
-- sys_menu表核心字段
`menu_id` bigint(20) 菜单ID(主键)
`menu_name` varchar(50) 菜单名称
`parent_id` bigint(20) 父菜单ID(0表示一级菜单)
`order_num` int(4) 显示顺序
`path` varchar(200) 路由地址
`component` varchar(255) 组件路径
`menu_type` char(1) 菜单类型(M目录 C菜单 F按钮)
`visible` char(1) 菜单状态(0显示 1隐藏)
`status` char(1) 菜单状态(0正常 1停用)
`perms` varchar(100) 权限标识(如:system:user:list)
`icon` varchar(100) 菜单图标
```
#### 权限标识(perms)命名规范
```
格式:[模块名]:[功能名]:[操作名]
示例:
- pm:repairOrder:list # 报修工单列表查询
- pm:repairOrder:add # 报修工单新增
- pm:repairOrder:edit # 报修工单修改
- pm:repairOrder:remove # 报修工单删除
- pm:repairOrder:statistics # 报修统计数据
- pm:category:list # 维修分类列表
- pm:category:options # 维修分类下拉选项
```
### 🔧 权限配置标准工作流程
#### 📋 新增功能权限配置流程
**步骤 1:分析权限需求**
```
1. 确定主菜单名称和路径
2. 列出所有功能按钮(新增、修改、删除、查询等)
3. 列出所有API接口及对应权限
4. 确定权限标识命名
```
**步骤 2:权限配置 SQL 编写**
使用 UPSERT 逻辑(先判断后操作):
```sql
-- 权限配置标准模板(UPSERT逻辑)
-- ==========================================
-- [模块名]权限配置脚本
-- 功能:为[模块名]添加完整权限配置
-- 时间:YYYY-MM-DD HH:mm:ss
-- 说明:使用UPSERT逻辑,避免重复插入
-- ==========================================
-- 第一步:检查并插入主菜单
INSERT INTO sys_menu (
menu_id, menu_name, parent_id, order_num, path, component,
query_param, is_frame, is_cache, menu_type, visible, status,
perms, icon, create_by, create_time, update_by, update_time, remark
)
SELECT
2100, '物业管理', 0, 6, 'pm', NULL,
'', 1, 0, 'M', '0', '0',
'', 'tool', 1, now(), 1, now(), '物业管理模块'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2100);
-- 第二步:检查并插入子菜单
INSERT INTO sys_menu (
menu_id, menu_name, parent_id, order_num, path, component,
query_param, is_frame, is_cache, menu_type, visible, status,
perms, icon, create_by, create_time, update_by, update_time, remark
)
SELECT
2110, '报修管理', 2100, 1, 'repairOrder', 'pm/repairOrder/index',
'', 1, 0, 'C', '0', '0',
'pm:repairOrder:list', 'form', 1, now(), 1, now(), '报修工单管理'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2110);
-- 第三步:批量插入功能权限(如果不存在)
INSERT INTO sys_menu (
menu_id, menu_name, parent_id, order_num, path, component,
query_param, is_frame, is_cache, menu_type, visible, status,
perms, icon, create_by, create_time, update_by, update_time, remark
)
SELECT * FROM (
SELECT 2111, '查询', 2110, 1, '', '', '', 1, 0, 'F', '0', '0', 'pm:repairOrder:query', '#', 1, now(), 1, now(), '查询权限' UNION ALL
SELECT 2112, '新增', 2110, 2, '', '', '', 1, 0, 'F', '0', '0', 'pm:repairOrder:add', '#', 1, now(), 1, now(), '新增权限' UNION ALL
SELECT 2113, '修改', 2110, 3, '', '', '', 1, 0, 'F', '0', '0', 'pm:repairOrder:edit', '#', 1, now(), 1, now(), '修改权限'
) AS temp
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = temp.menu_id);
-- 第四步:为admin角色分配权限(如果未分配)
INSERT IGNORE INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id FROM sys_menu WHERE menu_id BETWEEN 2100 AND 2199;
```
**步骤 3:更新现有权限**
```sql
-- 更新现有权限信息(如果需要修改)
UPDATE sys_menu
SET
menu_name = '新的菜单名称',
path = '新的路径',
perms = '新的权限标识',
update_time = now(),
update_by = 1,
remark = '权限更新说明'
WHERE menu_id = 2110 AND EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2110);
```
### 🔄 权限配置操作规范
#### 🚨 强制性检查清单
**每次新增功能时必须检查**:
- [ ] 是否需要新增主菜单?
- [ ] 是否需要新增子菜单?
- [ ] 列出所有需要的功能权限(CRUD 等)
- [ ] 列出所有 API 接口及权限标识
- [ ] 确认权限标识命名符合规范
- [ ] 权限配置是否使用 UPSERT 逻辑?
- [ ] 是否为相关角色分配了权限?
#### 🔍 权限配置验证方法
**验证 SQL 模板**:
```sql
-- 1. 验证权限是否已正确配置
SELECT menu_id, menu_name, perms, menu_type
FROM sys_menu
WHERE perms LIKE 'pm:%'
ORDER BY menu_id;
-- 2. 验证角色权限分配
SELECT rm.role_id, r.role_name, m.menu_name, m.perms
FROM sys_role_menu rm
JOIN sys_role r ON rm.role_id = r.role_id
JOIN sys_menu m ON rm.menu_id = m.menu_id
WHERE m.perms LIKE 'pm:%'
ORDER BY rm.role_id, m.menu_id;
-- 3. 验证用户权限(通过角色)
SELECT u.user_name, r.role_name, m.menu_name, m.perms
FROM sys_user u
JOIN sys_user_role ur ON u.user_id = ur.user_id
JOIN sys_role r ON ur.role_id = r.role_id
JOIN sys_role_menu rm ON r.role_id = rm.role_id
JOIN sys_menu m ON rm.menu_id = m.menu_id
WHERE u.user_name = 'admin' AND m.perms LIKE 'pm:%'
ORDER BY m.menu_id;
```
### 🎯 Controller 权限注解规范
#### 标准权限注解使用
```java
@RestController
@RequestMapping("/pm/repairOrder")
@Api(tags = "报修工单管理")
public class PmRepairOrderController extends BaseController {
// 列表查询 - 对应菜单权限
@SaCheckPermission("pm:repairOrder:list")
@GetMapping("/list")
@Operation(summary = "查询报修工单列表")
public TableDataInfo<PmRepairOrderVo> list(PmRepairOrderBo bo, PageQuery pageQuery) {
return repairOrderService.queryPageList(bo, pageQuery);
}
// 详情查询 - 对应按钮权限
@SaCheckPermission("pm:repairOrder:query")
@GetMapping("/{id}")
@Operation(summary = "获取报修工单详细信息")
public R<PmRepairOrderVo> getInfo(@PathVariable Long id) {
return R.ok(repairOrderService.queryById(id));
}
// 新增功能 - 对应按钮权限
@SaCheckPermission("pm:repairOrder:add")
@PostMapping
@Operation(summary = "新增报修工单")
public R<Void> add(@RequestBody PmRepairOrderBo bo) {
return toAjax(repairOrderService.insertByBo(bo));
}
// 修改功能 - 对应按钮权限
@SaCheckPermission("pm:repairOrder:edit")
@PutMapping
@Operation(summary = "修改报修工单")
public R<Void> edit(@RequestBody PmRepairOrderBo bo) {
return toAjax(repairOrderService.updateByBo(bo));
}
// 删除功能 - 对应按钮权限
@SaCheckPermission("pm:repairOrder:remove")
@DeleteMapping("/{ids}")
@Operation(summary = "删除报修工单")
public R<Void> remove(@PathVariable Long[] ids) {
return toAjax(repairOrderService.deleteWithValidByIds(List.of(ids)));
}
// 统计数据 - 对应功能权限
@SaCheckPermission("pm:repairOrder:statistics")
@GetMapping("/statistics")
@Operation(summary = "获取报修统计数据")
public R<Map<String, Object>> statistics() {
return R.ok(repairOrderService.getStatistics());
}
// 下拉选项 - 对应功能权限
@SaCheckPermission("pm:repairOrder:generateOrderNo")
@PostMapping("/generateOrderNo")
@Operation(summary = "生成工单编号")
public R<String> generateOrderNo() {
return R.ok(repairOrderService.generateOrderNo());
}
}
```
#### 权限注解检查清单
- [ ] 每个 Controller 方法都有@SaCheckPermission 注解
- [ ] 权限标识与 sys_menu 表中的 perms 字段一致
- [ ] 权限标识命名符合[模块]:[功能]:[操作]规范
- [ ] 所有权限标识都已在 sys_menu 表中配置
### 🖥️ 前端权限控制规范
#### Vue 页面权限控制
```vue
<template>
<div class="app-container">
<!-- 功能按钮权限控制 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
v-hasPermi="['pm:repairOrder:add']"
type="primary"
plain
icon="Plus"
@click="handleAdd"
>
新增
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-hasPermi="['pm:repairOrder:edit']"
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
>
修改
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-hasPermi="['pm:repairOrder:remove']"
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
>
删除
</el-button>
</el-col>
</el-row>
<!-- 表格操作列权限控制 -->
<el-table-column label="操作" align="center" width="160">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button
v-hasPermi="['pm:repairOrder:edit']"
link
type="primary"
icon="Edit"
@click="handleUpdate(scope.row)"
></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
v-hasPermi="['pm:repairOrder:remove']"
link
type="danger"
icon="Delete"
@click="handleDelete(scope.row)"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</div>
</template>
```
#### API 调用权限检查
```typescript
// api调用前的权限检查(可选)
import { checkPermi } from '@/utils/permission';
// 在调用API前检查权限
const handleAdd = () => {
if (!checkPermi(['pm:repairOrder:add'])) {
ElMessage.error('您没有新增权限');
return;
}
// 执行新增操作
router.push('/pm/repairOrder/add');
};
const handleEdit = (row: any) => {
if (!checkPermi(['pm:repairOrder:edit'])) {
ElMessage.error('您没有修改权限');
return;
}
// 执行修改操作
router.push(`/pm/repairOrder/edit/${row.id}`);
};
```
### 📋 权限配置标准模板
#### 完整权限配置脚本模板
```sql
-- ==========================================
-- [模块名称]权限配置脚本
-- 功能:为[模块名称]添加完整权限配置
-- 时间:YYYY-MM-DD HH:mm:ss (⚠️必须填写真实时间!)
-- 说明:使用UPSERT逻辑,支持重复执行
-- ==========================================
-- 清理旧数据(谨慎使用,仅在测试环境)
-- DELETE FROM sys_role_menu WHERE menu_id BETWEEN [起始ID] AND [结束ID];
-- DELETE FROM sys_menu WHERE menu_id BETWEEN [起始ID] AND [结束ID];
-- 第一部分:主菜单配置
INSERT INTO sys_menu (
menu_id, menu_name, parent_id, order_num, path, component,
query_param, is_frame, is_cache, menu_type, visible, status,
perms, icon, create_by, create_time, update_by, update_time, remark
)
SELECT
[主菜单ID], '[主菜单名称]', 0, [排序号], '[路径]', NULL,
'', 1, 0, 'M', '0', '0',
'', '[图标]', 1, now(), 1, now(), '[模块描述]'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = [主菜单ID]);
-- 第二部分:子菜单配置
INSERT INTO sys_menu (
menu_id, menu_name, parent_id, order_num, path, component,
query_param, is_frame, is_cache, menu_type, visible, status,
perms, icon, create_by, create_time, update_by, update_time, remark
)
SELECT
[子菜单ID], '[子菜单名称]', [主菜单ID], [排序号], '[路径]', '[组件路径]',
'', 1, 0, 'C', '0', '0',
'[模块]:[功能]:list', '[图标]', 1, now(), 1, now(), '[功能描述]'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = [子菜单ID]);
-- 第三部分:功能权限配置(批量插入)
INSERT INTO sys_menu (
menu_id, menu_name, parent_id, order_num, path, component,
query_param, is_frame, is_cache, menu_type, visible, status,
perms, icon, create_by, create_time, update_by, update_time, remark
)
SELECT * FROM (
-- 基础CRUD权限
SELECT [权限ID1], '[功能名称]查询', [子菜单ID], 1, '', '', '', 1, 0, 'F', '0', '0', '[模块]:[功能]:query', '#', 1, now(), 1, now(), '查询权限' UNION ALL
SELECT [权限ID2], '[功能名称]新增', [子菜单ID], 2, '', '', '', 1, 0, 'F', '0', '0', '[模块]:[功能]:add', '#', 1, now(), 1, now(), '新增权限' UNION ALL
SELECT [权限ID3], '[功能名称]修改', [子菜单ID], 3, '', '', '', 1, 0, 'F', '0', '0', '[模块]:[功能]:edit', '#', 1, now(), 1, now(), '修改权限' UNION ALL
SELECT [权限ID4], '[功能名称]删除', [子菜单ID], 4, '', '', '', 1, 0, 'F', '0', '0', '[模块]:[功能]:remove', '#', 1, now(), 1, now(), '删除权限' UNION ALL
-- 特殊功能权限
SELECT [权限ID5], '[特殊功能名称]', [子菜单ID], 5, '', '', '', 1, 0, 'F', '0', '0', '[模块]:[功能]:[特殊操作]', '#', 1, now(), 1, now(), '[特殊功能描述]'
) AS temp
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = temp.menu_id);
-- 第四部分:权限分配(为admin角色分配权限)
INSERT IGNORE INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id FROM sys_menu WHERE menu_id BETWEEN [起始ID] AND [结束ID];
-- 第五部分:验证配置结果
SELECT '权限配置验证' as message,
COUNT(*) as menu_count,
(SELECT COUNT(*) FROM sys_role_menu WHERE role_id = 1 AND menu_id BETWEEN [起始ID] AND [结束ID]) as role_permission_count
FROM sys_menu
WHERE menu_id BETWEEN [起始ID] AND [结束ID];
```
### 🧪 权限配置测试和验证
#### 权限测试标准流程
```
1. **权限配置验证**
- 检查sys_menu表中权限记录
- 检查sys_role_menu表中角色权限分配
- 验证权限标识命名规范
↓
2. **后端接口测试**
- 使用Token调用接口
- 验证权限认证是否正常
- 测试无权限用户访问是否被拒绝
↓
3. **前端界面测试**
- 登录系统查看菜单是否显示
- 检查功能按钮是否根据权限显示/隐藏
- 验证无权限用户的界面表现
↓
4. **权限缓存测试**
- 重启服务后权限是否正常
- 重新登录后权限是否正确加载
- 权限变更后是否需要重新登录
```
#### 权限测试脚本模板
```javascript
// 权限验证测试脚本
async function testPermissions(token, permissions) {
const results = [];
for (const perm of permissions) {
try {
const response = await callAPIWithToken(token, perm.endpoint, perm.data, perm.method);
results.push({
permission: perm.name,
endpoint: perm.endpoint,
success: response.success,
status: response.status,
message: response.success ? '权限验证通过' : '权限验证失败',
});
} catch (error) {
results.push({
permission: perm.name,
endpoint: perm.endpoint,
success: false,
status: 'ERROR',
message: error.message,
});
}
}
return results;
}
// 使用示例
const permissionTests = [
{ name: 'pm:repairOrder:list', endpoint: '/pm/repairOrder/list', method: 'GET', data: null },
{ name: 'pm:repairOrder:add', endpoint: '/pm/repairOrder', method: 'POST', data: {} },
{
name: 'pm:repairOrder:statistics',
endpoint: '/pm/repairOrder/statistics',
method: 'GET',
data: null,
},
];
// 执行权限测试
testPermissions(token, permissionTests).then((results) => {
console.log('权限测试结果:', results);
});
```
### 🚨 权限配置强制要求总结
#### 必须遵循的规范
1. **⚠️ 每个新功能都必须配置权限**:菜单权限 + 按钮权限 + 接口权限
2. **⚠️ 使用 UPSERT 逻辑**:权限配置脚本必须支持重复执行
3. **⚠️ 权限标识规范**:严格按照[模块]:[功能]:[操作]格式命名
4. **⚠️ Controller 注解必须**:每个接口方法都要有@SaCheckPermission 注解
5. **⚠️ 前端权限控制**:功能按钮必须使用 v-hasPermi 指令控制
6. **⚠️ 权限测试验证**:配置完成后必须进行完整的权限测试
7. **⚠️ 文档同步更新**:权限配置必须在 API 文档中说明
#### 违规检查清单
- [ ] 新功能是否缺少权限配置?
- [ ] Controller 方法是否缺少权限注解?
- [ ] 权限标识命名是否符合规范?
- [ ] 前端按钮是否有权限控制?
- [ ] 权限配置是否支持重复执行?
- [ ] 是否进行了权限功能测试?
- [ ] API 文档是否更新了权限信息?
### 🔴 VO 类基础字段规范(强制性)
**⚠️ 超级重要:为了项目一致性,所有 VO 类必须包含统一的基础字段!**
#### 🎯 规范背景
基于实际开发中发现的问题,不同查询方式对 VO 类字段的要求存在差异:
| 查询方式 | 基础字段处理 | VO 类要求 | 示例 |
| ------------------------- | -------------- | -------------- | ---------------------- |
| **MyBatis-Plus 自动查询** | 不包含继承字段 | 无需基础字段 | `HqSatCategoryVo` |
| **自定义 SQL 查询** | 明确包含字段 | 必须有对应字段 | `HqVenueReservationVo` |
**问题案例**:
```
org.mybatis.spring.MyBatisSystemException:
Could not set property 'createBy' of 'class org.dromara.hq.domain.vo.HqVenueReservationVo'
with value '1' Cause: There is no setter for property named 'createBy'
```
#### 🚨 强制性统一规范
**为了避免开发过程中的不一致问题,所有 VO 类必须包含完整的基础字段!**
#### 📋 标准 VO 类基础字段模板
```java
/**
* 标准VO类模板
*
* @author [作者]
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = [Entity].class)
public class [Entity]Vo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
// ========== 业务字段 ==========
/**
* 主键ID
*/
@ExcelProperty(value = "主键ID")
private Long id;
// ... 其他业务字段 ...
// ========== 基础字段(强制性,所有VO类必须包含) ==========
/**
* 创建者
*/
private Long createBy;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ExcelProperty(value = "创建时间")
private Date createTime;
/**
* 更新者
*/
private Long updateBy;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
```
#### 🛠️ 基础字段说明
| 字段 | 类型 | 必需 | 说明 | JSON 格式化 | Excel 导出 |
| -------------- | ------ | ---- | --------- | ----------- | ---------- |
| **createBy** | `Long` | ✅ | 创建者 ID | 否 | 否 |
| **createTime** | `Date` | ✅ | 创建时间 | ✅ | ✅ |
| **updateBy** | `Long` | ✅ | 更新者 ID | 否 | 否 |
| **updateTime** | `Date` | ✅ | 更新时间 | ✅ | 否 |
#### 📝 字段使用注解规范
```java
// 时间字段必须使用JSON格式化注解
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
// 创建时间可选择性导出到Excel
@ExcelProperty(value = "创建时间")
private Date createTime;
// 更新时间通常不导出到Excel
private Date updateTime;
```
#### 🚨 强制性要求
1. **⚠️ 所有 VO 类必须包含四个基础字段**:createBy、createTime、updateBy、updateTime
2. **⚠️ 字段类型必须统一**:createBy/updateBy 为 Long,createTime/updateTime 为 Date
3. **⚠️ 时间字段必须使用 JSON 格式化注解**:`@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")`
4. **⚠️ 新生成的 VO 类必须检查基础字段完整性**
5. **⚠️ 修复现有 VO 类时必须补全缺失的基础字段**
#### 🔍 VO 类检查清单
**新增 VO 类检查**:
- [ ] 是否包含 createBy 字段?
- [ ] 是否包含 createTime 字段?
- [ ] 是否包含 updateBy 字段?
- [ ] 是否包含 updateTime 字段?
- [ ] 时间字段是否有 JSON 格式化注解?
- [ ] 字段类型是否正确(Long/Date)?
**现有 VO 类修复**:
- [ ] 检查 MyBatis 映射错误日志中提到的字段
- [ ] 补全缺失的基础字段
- [ ] 重新构建模块 jar 包
- [ ] 触发热重启验证修复效果
#### 💡 最佳实践
**1. 代码生成器配置**
```java
// 在代码生成模板中包含基础字段
// generator/template/vo.java.vm
/**
* 创建者
*/
private Long createBy;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ExcelProperty(value = "创建时间")
private Date createTime;
/**
* 更新者
*/
private Long updateBy;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
```
**2. 开发流程规范**
```
新增VO类 → 使用标准模板 → 包含基础字段 → 编译测试
修复VO类 → 补全缺失字段 → 重新构建 → 热重启验证
```
**3. 问题预防措施**
- 定期检查 VO 类与 Entity 类的字段对应关系
- MyBatis 映射错误要及时检查字段定义
- 使用 IDE 插件自动检查 VO 类完整性
- 在代码评审中重点检查 VO 类基础字段
#### 🎯 修复现有问题的标准流程
**当遇到 MyBatis 映射错误时**:
```bash
# 1. 分析错误日志,确认缺失字段
grep "Could not set property" logs/error.log
# 2. 修复VO类,添加缺失字段
# 编辑对应的VO类文件
# 3. 重新构建模块
cd ruoyi-modules/[模块名]
mvn clean install -U -Dmaven.test.skip=true
# 4. 重新构建主应用
cd ../../ruoyi-admin
mvn clean compile -U -Dmaven.test.skip=true
# 5. 触发热重启
echo "reload" > src/main/resources/.reloadtrigger
```
#### 📊 实际修复案例总结
**案例 1:HqVenueInfoVo**
```java
// 问题:缺少createBy、updateBy、updateTime字段
// 修复:添加标准基础字段
// 结果:场地管理界面正常加载
```
**案例 2:HqVenueReservationVo**
```java
// 问题:缺少createBy、updateBy、updateTime字段
// 修复:添加标准基础字段
// 结果:场地预约界面正常加载
```
**案例 3:HqSatCategoryVo**
```java
// 情况:使用MyBatis-Plus自动查询,无需基础字段
// 建议:为了一致性,仍建议添加基础字段
// 优势:支持未来可能的自定义SQL查询
```
### 📋 VO 类规范违规检查
**开发阶段检查**:
- 新增 VO 类时未包含基础字段
- 时间字段未使用 JSON 格式化注解
- 字段类型不符合规范(如用 String 代替 Date)
**测试阶段检查**:
- MyBatis 映射错误日志中提到字段缺失
- Excel 导出功能异常
- 时间格式显示不正确
**生产阶段检查**:
- 接口返回数据中基础字段为空
- 前端显示创建时间/更新时间异常
- 数据审计功能不完整
### 🎯 规范总结
**统一 VO 类基础字段规范的目标**:
1. **🔄 兼容性**:无论使用 MyBatis-Plus 自动查询还是自定义 SQL,都能正常工作
2. **📊 一致性**:所有 VO 类具有统一的基础字段结构
3. **🚀 可维护性**:减少因字段缺失导致的运行时错误
4. **📈 可扩展性**:支持未来可能的查询方式变更
**⚠️ 记住:宁可多加字段保证兼容性,也不要因为字段缺失导致系统异常!**
### 🔴 MyBatis-Plus 自定义 SQL 规范(强制性)
**⚠️ 基于实际开发中遇到的 SQL 拼接错误,制定强制性规范!**
#### 🎯 常见问题
**SQL 拼接错误案例**:
```xml
<!-- ❌ 错误写法 -->
<select id="selectPage">
SELECT * FROM table_name
<where>
and deleted = 0
${ew.sqlSegment}
</where>
order by create_time desc
</select>
```
**生成的错误 SQL**:
```sql
WHERE and deleted = 0 AND (applicant_id = ?) ORDER BY create_time DESC order by create_time desc
```
#### 🚨 强制性规范
**1. `${ew.sqlSegment}`使用规范**
```xml
<!-- ✅ 正确写法 -->
<select id="selectPage">
SELECT * FROM table_name
<where>
deleted = 0
${ew.sqlSegment}
</where>
</select>
```
**要点**:
- ✅ `${ew.sqlSegment}`内容**不包含前置 AND**
- ✅ 固定条件和动态条件之间**必须显式添加 AND**
- ✅ 使用`<if>`标签进行安全的条件拼接
- ✅ 使用`<where>`标签自动处理 WHERE 关键字
**2. ORDER BY 重复问题**
```java
// Java代码中设置排序(推荐)
LambdaQueryWrapper<Entity> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(Entity::getCreateTime);
```
```xml
<!-- XML中不要重复设置ORDER BY -->
<select id="selectPage">
SELECT * FROM table_name
<where>
deleted = 0
${ew.sqlSegment}
</where>
<!-- ❌ 不要在这里再写 order by -->
</select>
```
**3. 标准 Mapper.xml 模板**
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.[module].mapper.[Entity]Mapper">
<!-- 结果映射(包含基础字段) -->
<resultMap type="org.dromara.[module].domain.vo.[Entity]Vo" id="[Entity]Result">
<result property="id" column="id"/>
<!-- 业务字段映射 -->
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<!-- 基础查询SQL -->
<sql id="select[Entity]Vo">
select id, [业务字段], create_by, create_time, update_by, update_time
from [table_name]
</sql>
<!-- 分页查询(标准模板) -->
<select id="select[Entity]Page" resultMap="[Entity]Result">
<include refid="select[Entity]Vo"/>
<where>
deleted = 0
<if test="ew != null and ew.sqlSegment != null and ew.sqlSegment != ''">
AND ${ew.sqlSegment}
</if>
</where>
</select>
</mapper>
```
#### 🔍 SQL 拼接检查清单
**开发阶段检查**:
- [ ] `${ew.sqlSegment}`前是否误加了`AND`?
- [ ] 是否存在重复的 ORDER BY 子句?
- [ ] `<where>`标签是否正确使用?
- [ ] VO 类是否包含完整的基础字段?
**测试阶段检查**:
- [ ] 分页查询是否正常工作?
- [ ] SQL 日志中是否有语法错误?
- [ ] 条件查询是否按预期执行?
#### 🛠️ 调试和排错
**启用 MyBatis SQL 日志**:
```yaml
# application-dev.yml
logging:
level:
org.dromara.[module].mapper: debug
```
**SQL 错误快速定位**:
```bash
# 1. 查看错误日志
grep "Failed to process, Error SQL" logs/error.log
# 2. 检查生成的SQL结构
# 关注WHERE子句和ORDER BY的拼接
# 3. 验证Mapper.xml中的拼接逻辑
```
#### 💡 最佳实践
**1. 统一的 Mapper 方法命名**:
```java
// 分页查询方法
Page<EntityVo> selectEntityPage(Page<Entity> page, @Param("ew") LambdaQueryWrapper<Entity> lqw);
// 列表查询方法
List<EntityVo> selectEntityList(@Param("ew") LambdaQueryWrapper<Entity> lqw);
```
**2. Service 中的条件构造**:
```java
private LambdaQueryWrapper<Entity> buildQueryWrapper(EntityBo bo) {
LambdaQueryWrapper<Entity> lqw = Wrappers.lambdaQuery();
// 添加查询条件
lqw.eq(ObjectUtil.isNotNull(bo.getId()), Entity::getId, bo.getId());
// 设置排序(统一在Java代码中设置)
lqw.orderByDesc(Entity::getCreateTime);
return lqw;
}
```
**3. 错误预防措施**:
- 代码生成器模板中使用标准的 SQL 拼接格式
- 代码评审时重点检查 Mapper.xml 的 SQL 拼接
- 单元测试中验证生成的 SQL 语句
- 使用 IDE 插件检查 XML 语法
#### 🚨 强制性要求总结
1. **⚠️ 必须在固定条件和`${ew.sqlSegment}`之间显式添加 AND**
2. **⚠️ 必须使用`<if>`标签安全拼接`${ew.sqlSegment}`**
3. **⚠️ 禁止在 XML 和 Java 代码中重复设置 ORDER BY**
4. **⚠️ 必须使用`<where>`标签处理条件拼接**
5. **⚠️ 必须在 VO 类中包含完整的基础字段**
6. **⚠️ 必须在测试中验证 SQL 拼接结果**
**违规处理**:不符合规范的 Mapper.xml 将导致运行时 SQL 错误,必须立即修复!
**⚠️ 记住:宁可多加字段保证兼容性,也不要因为字段缺失导致系统异常!**
## 5. 标准代码模板
### Controller 模板
```java
@RestController
@RequestMapping("/system/user")
@Api(tags = "用户管理")
public class SysUserController extends BaseController {
@Autowired
private ISysUserService userService;
@SaCheckPermission("system:user:list")
@GetMapping("/list")
@Operation(summary = "查询用户列表")
public TableDataInfo<SysUserVo> list(SysUserBo user, PageQuery pageQuery) {
return userService.queryPageList(user, pageQuery);
}
}
```
### Service 模板
```java
@Service
public class SysUserServiceImpl implements ISysUserService {
@Autowired
private SysUserMapper userMapper;
@Override
public TableDataInfo<SysUserVo> queryPageList(SysUserBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<SysUser> lqw = buildQueryWrapper(bo);
Page<SysUserVo> result = userMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
}
```
### Mapper 模板
```java
@Mapper
public interface SysUserMapper extends BaseMapperPlus<SysUser, SysUserVo> {
/**
* 查询用户列表
*/
List<SysUserVo> selectUserList(SysUserBo bo);
/**
* 根据条件分页查询用户列表
*/
Page<SysUserVo> selectUserPage(Page<SysUser> page, @Param("bo") SysUserBo bo);
}
```
### 🗑️ 逻辑删除规则(全局强制,2025-07-11新增)
> **适用范围**:所有业务表、所有MyBatis-Plus实体、所有Service删除操作
>
> **设计目标**:统一软删除实现,避免物理删除带来的数据丢失,保证查询接口自动过滤已删除数据。
1. **数据库字段**
- 字段名:`deleted`
- 字段类型:`tinyint(1)`
- 默认值:`0`(存在)
- 含义:`0`-存在,`1`-已删除
2. **实体类注解**
```java
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic(value = "0", delval = "1")
private Integer deleted;
```
- `@TableLogic` 由 MyBatis-Plus 提供,可自动将 `DELETE` SQL 转换为
```sql
UPDATE table SET deleted = 1 WHERE id = ?
```
- 同时在所有自动生成的 `SELECT` 语句中追加条件 `deleted = 0`,无需显式编写过滤条件。
3. **删除操作代码规范**
- Service 层推荐使用 MyBatis-Plus 的 `deleteById` / `deleteBatchIds` 等方法即可触发逻辑删除。
- 如需自定义删除 SQL,**必须**包含 `deleted = 0` 过滤并在删除时设置 `deleted = 1`。
- Controller 层仍按业务语义暴露 `DELETE /{id}` 接口,无需额外逻辑,保持与物理删除一致的调用方式。
4. **查询操作规范**
- 默认情况下,MyBatis-Plus 已自动附加 `deleted = 0` 条件。编写自定义 SQL 时,**必须**手动加上 `AND deleted = 0`。
- 对外返回的分页/列表接口,不得包含 `deleted = 1` 的数据。
5. **数据恢复**
- 若业务需要"恢复已删除数据",可单独提供接口,将 `deleted` 字段重新置回 `0`。
> 📌 **注意**:逻辑删除是项目级强制要求,任何物理删除(`DELETE FROM ...`)将被代码审查拒绝。
#### 🔴 AI 开发强制要求
当使用 AI 进行数据库开发时:
1. **必须先阅读** `docs/设计文档/database/mysql-database-standard.md` 规范
2. **必须使用标准模板**,不得随意修改基础字段
3. **必须保持与现有表结构的一致性**
4. **建表后必须立即更新** `hzdyjs_xg.sql` 文件
5. **必须在变更记录中记录** 数据库变更