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

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

63 KiB
Raw Permalink Blame History

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 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实体类:

@Data
@EqualsAndHashCode(callSuper = true)
@TableName("表名")
public class EntityName extends TenantEntity {
    @Serial
    private static final long serialVersionUID = 1L;
    
    @TableId(value = "id")
    private Long id;
    
    // 业务字段...
}

Service接口:

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控制器:

@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接口:

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)

命名示例:

-- 后勤管理模块表
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_ 开头的表)
  • ❌ 禁止使用无前缀的表名(系统核心表除外)

实体类命名对应:

// 表名: 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 {
    // ...
}

🔴 强制性基础字段模板

-- 所有业务表必须包含以下字段(顺序和类型必须完全一致)
`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,格式如下:

{
    "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 后面没有空格)

📊 统一响应格式(必须遵循)

基础响应格式(分页列表):

{
    "total": 7,
    "rows": [],
    "code": 200,
    "msg": "查询成功"
}

🎯 响应格式详细说明

1. 分页列表响应

{
    "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. 非分页列表响应

{
    "code": 200,
    "msg": "查询成功",
    "data": [
        {
            "id": 1,
            "name": "示例数据1"
        },
        {
            "id": 2,
            "name": "示例数据2"
        }
    ]
}

3. 单条数据响应

{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "id": 1,
        "name": "示例数据",
        "createTime": "2025-06-30 12:00:00"
    }
}

4. 操作成功响应(无数据)

{
    "code": 200,
    "msg": "操作成功",
    "data": null
}

5. 错误响应

{
    "code": 400,
    "msg": "参数错误:用户名不能为空",
    "data": null
}

📋 状态码规范

成功状态码:

  • 200 - 操作成功
  • 201 - 创建成功

客户端错误(4xx):

  • 400 - 请求参数错误
  • 401 - 未认证/认证失败
  • 403 - 无权限访问
  • 404 - 资源不存在
  • 409 - 数据冲突

服务端错误(5xx):

  • 500 - 服务器内部错误
  • 502 - 网关错误
  • 503 - 服务不可用

🚨 接口开发强制要求

1. Controller 返回格式

// ✅ 正确 - 分页接口直接返回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. 分页接口规范

// 分页查询直接返回 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. 非分页列表接口

// 非分页列表查询(下拉选项等)
@GetMapping("/options")
public R<List<UserVo>> options() {
    List<UserVo> list = userService.selectUserOptions();
    return R.ok(list);
}

// 返回格式:
// {
//   "code": 200,
//   "msg": "查询成功",
//   "data": [...]
// }

4. 单条数据接口

// 单条数据查询
@GetMapping("/{id}")
public R<UserVo> getInfo(@PathVariable Long id) {
    UserVo user = userService.queryById(id);
    return R.ok(user);
}

// 返回格式:
// {
//   "code": 200,
//   "msg": "操作成功",
//   "data": {...}
// }

5. 操作类接口

// 新增/修改/删除操作
@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 - 禁用用户

🛡️ 权限控制规范

// 每个接口必须添加权限注解
@SaCheckPermission("system:user:list")
@GetMapping("/list")
public R<TableDataInfo<UserVo>> list(UserBo bo, PageQuery pageQuery) {
    // 接口实现
}

📝 参数验证规范

// 使用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 必须写明参数名,禁止依赖编译器保留参数名。

// ✅ 正确
@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
  • 🧪 示例数据插入:便于开发和测试

参考示例:

-- 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 个):

@GetMapping("/list")           // 分页查询列表
@GetMapping("/{id}")          // 查询详情
@PostMapping                  // 新增
@PutMapping                   // 修改
@DeleteMapping("/{ids}")      // 删除
@GetMapping("/available")     // 获取可用选项

业务功能接口(4 个):

@GetMapping("/my")            // 查询我的数据
@GetMapping("/pending")       // 查询待处理数据
@GetMapping("/conflict")      // 业务规则检查
@PostMapping("/approve")      // 审批操作

辅助功能接口(6 个):

@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 接口
  • 📋 业务规则测试:验证时间冲突、权限控制等
  • 🧹 自动数据清理:测试完成后清理测试数据

测试脚本标准结构:

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 表核心字段说明

-- 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 逻辑(先判断后操作):

-- 权限配置标准模板(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:更新现有权限

-- 更新现有权限信息(如果需要修改)
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 模板:

-- 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 权限注解规范

标准权限注解使用

@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 页面权限控制

<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 调用权限检查

// 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}`);
};

📋 权限配置标准模板

完整权限配置脚本模板

-- ==========================================
-- [模块名称]权限配置脚本
-- 功能:为[模块名称]添加完整权限配置
-- 时间: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. **权限缓存测试**
   - 重启服务后权限是否正常
   - 重新登录后权限是否正确加载
   - 权限变更后是否需要重新登录

权限测试脚本模板

// 权限验证测试脚本
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 类基础字段模板

/**
 * 标准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 ✅ 更新时间 ✅ 否

📝 字段使用注解规范

// 时间字段必须使用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. 代码生成器配置

// 在代码生成模板中包含基础字段
// 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 映射错误时:

# 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

// 问题:缺少createBy、updateBy、updateTime字段
// 修复:添加标准基础字段
// 结果:场地管理界面正常加载

案例 2:HqVenueReservationVo

// 问题:缺少createBy、updateBy、updateTime字段
// 修复:添加标准基础字段
// 结果:场地预约界面正常加载

案例 3:HqSatCategoryVo

// 情况:使用MyBatis-Plus自动查询,无需基础字段
// 建议:为了一致性,仍建议添加基础字段
// 优势:支持未来可能的自定义SQL查询

📋 VO 类规范违规检查

开发阶段检查:

  • 新增 VO 类时未包含基础字段
  • 时间字段未使用 JSON 格式化注解
  • 字段类型不符合规范(如用 String 代替 Date)

测试阶段检查:

  • MyBatis 映射错误日志中提到字段缺失
  • Excel 导出功能异常
  • 时间格式显示不正确

生产阶段检查:

  • 接口返回数据中基础字段为空
  • 前端显示创建时间/更新时间异常
  • 数据审计功能不完整

🎯 规范总结

统一 VO 类基础字段规范的目标:

  1. 🔄 兼容性:无论使用 MyBatis-Plus 自动查询还是自定义 SQL,都能正常工作
  2. 📊 一致性:所有 VO 类具有统一的基础字段结构
  3. 🚀 可维护性:减少因字段缺失导致的运行时错误
  4. 📈 可扩展性:支持未来可能的查询方式变更

⚠️ 记住:宁可多加字段保证兼容性,也不要因为字段缺失导致系统异常!

🔴 MyBatis-Plus 自定义 SQL 规范(强制性)

⚠️ 基于实际开发中遇到的 SQL 拼接错误,制定强制性规范!

🎯 常见问题

SQL 拼接错误案例:

<!-- ❌ 错误写法 -->
<select id="selectPage">
    SELECT * FROM table_name
    <where>
        and deleted = 0
        ${ew.sqlSegment}
    </where>
    order by create_time desc
</select>

生成的错误 SQL:

WHERE and deleted = 0 AND (applicant_id = ?) ORDER BY create_time DESC order by create_time desc

🚨 强制性规范

1. ${ew.sqlSegment}使用规范

<!-- ✅ 正确写法 -->
<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代码中设置排序(推荐)
LambdaQueryWrapper<Entity> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(Entity::getCreateTime);
<!-- XML中不要重复设置ORDER BY -->
<select id="selectPage">
    SELECT * FROM table_name
    <where>
        deleted = 0
        ${ew.sqlSegment}
    </where>
    <!-- ❌ 不要在这里再写 order by -->
</select>

3. 标准 Mapper.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 日志:

# application-dev.yml
logging:
    level:
        org.dromara.[module].mapper: debug

SQL 错误快速定位:

# 1. 查看错误日志
grep "Failed to process, Error SQL" logs/error.log

# 2. 检查生成的SQL结构
# 关注WHERE子句和ORDER BY的拼接

# 3. 验证Mapper.xml中的拼接逻辑

💡 最佳实践

1. 统一的 Mapper 方法命名:

// 分页查询方法
Page<EntityVo> selectEntityPage(Page<Entity> page, @Param("ew") LambdaQueryWrapper<Entity> lqw);

// 列表查询方法
List<EntityVo> selectEntityList(@Param("ew") LambdaQueryWrapper<Entity> lqw);

2. Service 中的条件构造:

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 模板

@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 模板

@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 模板

@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. 实体类注解

    /**
     * 删除标志(0代表存在 1代表删除)
     */
    @TableLogic(value = "0", delval = "1")
    private Integer deleted;
    
    • @TableLogic 由 MyBatis-Plus 提供,可自动将 DELETE 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. 必须在变更记录中记录 数据库变更