backend/ (基于 RuoYi-Vue-Plus + Spring Boot 3.4.6)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
⚠️ 基于ruoyi-hq工程实践制定,后续所有新模块必须遵循此规范!
模块位置: backend/ruoyi-modules/ruoyi-{模块简称}/
已规划模块列表:
ruoyi-hq - 后勤管理模块 (已完成)ruoyi-rs - 人事管理模块ruoyi-cw - 财务管理模块ruoyi-aqbw - 安全保卫管理模块ruoyi-zsjy - 招生就业管理模块ruoyi-xg - 学工管理模块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标准模式:
2. 代码质量要求:
3. 模块间解耦:
4. 数据库设计:
创建新模块前必须检查:
ruoyi-{简称})docs/设计文档/database/mysql-database-standard.md 规范docs/设计文档/database/hzdyjs_xg.sql 中的完整表结构docs/设计文档/database/database_msg.md⚠️ 超级重要:所有数据库表必须按业务模块使用统一前缀命名!
模块表名前缀规范:
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='数据字典表';
强制性要求:
实体类命名对应:
// 表名: 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 进行数据库开发时:
docs/设计文档/database/mysql-database-standard.md 规范hzdyjs_xg.sql 文件⚠️ 超级重要:所有 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"
}
请求头字段说明:
Bearer + Token 值(⚠️ 注意 Bearer 后面有一个空格)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 作为兜底,但接口代码仍必须显式声明参数名。
每个新接口必须检查:
@RequestParam/@PathVariable 是否显式声明了参数名?如果发现接口不符合统一规范,必须立即修改!不符合规范的接口不允许合并到主分支。
⚠️ 本流程基于场地预约管理系统的成功开发经验制定,必须严格遵循!
在编写任何代码之前必须完成:
docs/需求文档/[模块名]/ 详细的业务需求说明docs/业务模块/[模块名]/README.md 技术架构说明参考示例:
docs/需求文档/后勤场地预约需求/场地预约系统需求说明.mddocs/业务模块/venue-reservation/README.md在编写业务代码之前必须完成数据库设计:
docs/设计文档/database/[模块名]-module.sqldocs/设计文档/database/[模块名]-data.sqldocs/设计文档/database/[模块名]-menu.sql参考示例:
-- 04-venue-reservation-module.sql (数据表)
-- 04-venue-reservation-data.sql (字典数据)
-- 04-venue-reservation-menu.sql (菜单权限)
完整的 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参数校验注解@ExcelProperty注解@SaCheckPermission注解TableDataInfo,非分页用R类型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") // 禁用操作
⚠️ 代码写完后立即执行,不得延后!
docs/开发文档/API/[模块名]管理API.mddocs/开发文档/API/API文档模板.mddocs/开发文档/API/API文档总览.md中添加导航参考示例:
docs/开发文档/API/场地预约管理API.md (400+行完整文档)完整的 Python 测试脚本开发:
test/[模块名]API测试脚本.pycursorrules-api-test.md规范测试脚本标准结构:
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): # 执行完整测试
模块集成到主项目:
ruoyi-modules/pom.xml中添加子模块ruoyi-admin/pom.xml中添加依赖pom.xml的dependencyManagement中配置版本详细记录开发过程:
docs/变更管理/code-adjust-record-YYYY-MM-DD-{username}.md完整的验证流程:
如果发现以下问题,该代码变更将被视为不完整:
一个完整的 API 模块开发必须包含:
基于场地预约 API 开发的成功经验:
⚠️ 超级重要:每个菜单界面和相关接口都必须在sys_menu表中配置对应的权限数据!
sys_role_menu表关联,控制哪个角色拥有哪些权限用户登录 → 获取用户角色 → 查询角色权限(sys_role_menu) → 验证接口权限(@SaCheckPermission) → 允许/拒绝访问
-- 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) 菜单图标
格式:[模块名]:[功能名]:[操作名]
示例:
- 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);
每次新增功能时必须检查:
验证 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;
@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());
}
}
<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调用前的权限检查(可选)
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);
});
⚠️ 超级重要:为了项目一致性,所有 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类模板
*
* @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;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")新增 VO 类检查:
现有 VO 类修复:
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. 问题预防措施
当遇到 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 类基础字段规范的目标:
⚠️ 记住:宁可多加字段保证兼容性,也不要因为字段缺失导致系统异常!
⚠️ 基于实际开发中遇到的 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<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>
开发阶段检查:
${ew.sqlSegment}前是否误加了AND?<where>标签是否正确使用?测试阶段检查:
启用 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. 错误预防措施:
${ew.sqlSegment}之间显式添加 AND<if>标签安全拼接${ew.sqlSegment}<where>标签处理条件拼接违规处理:不符合规范的 Mapper.xml 将导致运行时 SQL 错误,必须立即修复!
⚠️ 记住:宁可多加字段保证兼容性,也不要因为字段缺失导致系统异常!
@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
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
public interface SysUserMapper extends BaseMapperPlus<SysUser, SysUserVo> {
/**
* 查询用户列表
*/
List<SysUserVo> selectUserList(SysUserBo bo);
/**
* 根据条件分页查询用户列表
*/
Page<SysUserVo> selectUserPage(Page<SysUser> page, @Param("bo") SysUserBo bo);
}
适用范围:所有业务表、所有MyBatis-Plus实体、所有Service删除操作
设计目标:统一软删除实现,避免物理删除带来的数据丢失,保证查询接口自动过滤已删除数据。
数据库字段
deletedtinyint(1)0(存在)0-存在,1-已删除实体类注解 ```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,无需显式编写过滤条件。
删除操作代码规范
deleteById / deleteBatchIds 等方法即可触发逻辑删除。deleted = 0 过滤并在删除时设置 deleted = 1。DELETE /{id} 接口,无需额外逻辑,保持与物理删除一致的调用方式。查询操作规范
deleted = 0 条件。编写自定义 SQL 时,必须手动加上 AND deleted = 0。deleted = 1 的数据。数据恢复
deleted 字段重新置回 0。📌 注意:逻辑删除是项目级强制要求,任何物理删除(
DELETE FROM ...)将被代码审查拒绝。
当使用 AI 进行数据库开发时:
docs/设计文档/database/mysql-database-standard.md 规范hzdyjs_xg.sql 文件