基于场地预约管理系统开发实践
本文档总结了场地预约管理系统的完整 API 开发实践,形成了一套标准化的 API 开发流程和规范,为后续 API 模块开发提供参考。
实践基础:
| 类型 | 数量 | 文件/功能 | 质量标准 |
|---|---|---|---|
| 需求文档 | 2 个 | 业务需求+技术架构 | 完整详细 |
| 数据库脚本 | 3 个 | 表结构+字典+权限 | 规范标准 |
| 后端代码 | 18 个文件 | 完整 ruoyi 模块 | 符合架构规范 |
| API 接口 | 16 个 | 标准 CRUD+业务接口 | 功能完整 |
| API 文档 | 1 个 | 400+行详细文档 | 准确规范 |
| 测试脚本 | 1 个 | 450+行自动化测试 | 覆盖完整 |
| 变更记录 | 1 个 | 详细开发过程记录 | 追溯完整 |
| 阶段 | 时间投入 | 主要工作 |
|---|---|---|
| 需求分析 | 0.5 天 | 需求整理+业务设计 |
| 数据库设计 | 0.5 天 | 表结构+权限+示例数据 |
| 后端开发 | 1.5 天 | 完整 ruoyi 模块开发 |
| API 文档 | 0.5 天 | 详细 API 文档编写 |
| 测试脚本 | 0.5 天 | 自动化测试脚本 |
| 总计 | 3.5 天 | 完整 API 模块 |
docs/需求文档/[模块名]/
├── [模块名]系统需求说明.md # 详细业务需求
└── README.md # 需求概览
内容要求:
docs/业务模块/[模块名]/
└── README.md # 技术架构说明
docs/设计文档/database/
├── XX-[模块名]-module.sql # 数据表结构
├── XX-[模块名]-data.sql # 字典数据
└── XX-[模块名]-menu.sql # 菜单权限
标准内容:
ruoyi-[模块名]/
├── pom.xml # 模块依赖配置
├── src/main/java/org/dromara/[模块名]/
│ ├── domain/ # 实体类(3个类)
│ │ ├── [Entity].java # 数据库实体
│ │ ├── bo/[EntityBo].java # 业务对象
│ │ └── vo/[EntityVo].java # 视图对象
│ ├── mapper/ # 数据访问层(2个类)
│ │ └── [Entity]Mapper.java # Mapper接口
│ ├── service/ # 业务逻辑层(4个类)
│ │ ├── I[Entity]Service.java # 服务接口
│ │ └── impl/[Entity]ServiceImpl.java # 服务实现
│ └── controller/ # 控制器层(2个类)
│ └── [Entity]Controller.java # REST API控制器
└── src/main/resources/
└── mapper/[Entity]Mapper.xml # MyBatis映射文件(2个)
基础管理接口(6 个):
GET /[module]/[entity]/list - 分页查询列表GET /[module]/[entity]/{id} - 查询详情POST /[module]/[entity] - 新增PUT /[module]/[entity] - 修改DELETE /[module]/[entity]/{ids} - 删除GET /[module]/[entity]/available - 获取可用选项业务功能接口(4 个):
GET /[module]/[business]/my - 查询我的数据GET /[module]/[business]/pending - 查询待处理数据GET /[module]/[business]/conflict - 业务规则检查POST /[module]/[business]/approve - 审批操作辅助功能接口(6 个):
POST /[module]/[business]/export - 导出 ExcelPOST /[module]/[business]/import - 导入 ExcelGET /[module]/[business]/statistics - 统计数据POST /[module]/[business]/batch - 批量操作POST /[module]/[business]/{id}/enable - 启用操作POST /[module]/[business]/{id}/disable - 禁用操作ruoyi-modules/pom.xml - 添加子模块ruoyi-admin/pom.xml - 添加依赖backend/pom.xml - 版本管理配置docs/开发文档/API/
├── [模块名]管理API.md # 完整API文档(400+行)
├── API文档总览.md # 更新导航索引
└── API文档模板.md # 标准模板
标准文档结构:
test/
└── [模块名]API测试脚本.py # 自动化测试脚本(450+行)
标准脚本结构:
docs/变更管理/
└── code-adjust-record-YYYY-MM-DD-{username}.md
`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代表删除)',
// 继承TenantEntity,包含完整注释
@TableName("table_name")
public class Entity extends TenantEntity {
@TableId("id")
private Long id;
@TableField("field_name")
private String fieldName;
}
// 添加验证注解
public class EntityBo {
@NotBlank(message = "字段不能为空", groups = {AddGroup.class, EditGroup.class})
@Size(max = 100, message = "字段长度不能超过100个字符")
private String fieldName;
}
// 支持Excel导出
public class EntityVo {
@ExcelProperty(value = "字段名称")
private String fieldName;
}
// 每个接口必须添加权限注解
@SaCheckPermission("module:entity:list")
@GetMapping("/list")
public TableDataInfo<EntityVo> list(EntityBo bo, PageQuery pageQuery) {
// 接口实现
}
// 分页接口直接返回TableDataInfo
@GetMapping("/list")
public TableDataInfo<EntityVo> list(...) {
return entityService.queryPageList(bo, pageQuery);
}
// 非分页接口使用R类型
@GetMapping("/options")
public R<List<EntityVo>> options() {
return R.ok(entityService.selectOptions());
}
# 强制依赖检查,缺少加密库直接退出
try:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
CRYPTO_AVAILABLE = True
except ImportError:
print("❌ 缺少必需的加密库!")
exit(1)
def run_all_tests(self):
# 1. 检测服务可用性
if not self.get_server_info():
return False
# 2. 强制加密登录
if not self.login():
return False
try:
# 3. 执行各类测试
self.test_entity_management()
self.test_business_features()
self.test_business_rules()
finally:
# 4. 清理测试数据
self.cleanup_test_data()
一个完整的 API 模块开发必须包含:
问题:新模块版本号不一致导致编译失败 解决:在根 pom.xml 的 dependencyManagement 中配置版本管理
问题:使用了错误的 Excel 注解包
解决:使用cn.idev.excel.annotation.ExcelProperty
问题:API 测试脚本登录失败 解决:强制使用 AES+RSA 双重加密登录,确保加密库正确安装
问题:代码变更后文档未更新 解决:建立强制性文档同步更新规则,代码完成后立即更新文档
问题:SQL 错误 "Unknown column 'parent_name' in 'field list'" 原因:实体类中包含数据库表不存在的字段,MyBatis-Plus 自动生成 SQL 时包含了这些字段 典型场景:实体类中有用于显示的冗余字段(如 parentName),但数据库表中没有对应字段
解决方案:
/**
* 父部门名称(冗余字段,用于显示,不持久化到数据库)
*/
@TableField(exist = false)
private String parentName;
关键要点:
@TableField(exist = false) 注解告诉 MyBatis-Plus 该字段不存在于数据库表中import com.baomidou.mybatisplus.annotation.TableField;selectList() 等 MyBatis-Plus 方法时出现字段不存在的错误错误示例:
org.springframework.jdbc.BadSqlGrammarException:
### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Unknown column 'parent_name' in 'field list'
### SQL: SELECT dept_id, parent_id, ancestors, dept_name, order_num, leader, phone, email, status, del_flag, parent_name, dept_category, create_dept, create_by, create_time, update_by, update_time FROM sys_dept WHERE...
基于OSS文件下载功能的修复经验,总结文件下载相关的最佳实践。
🔴 关键点:二进制文件下载不要设置charset
// ❌ 错误方式 - 会导致下载失败
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8");
// ✅ 正确方式 - 纯二进制类型
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
// ✅ 必需的缓存控制头
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
// ❌ 可能失效的方法引用
storage.download(fileName, response.getOutputStream(), response::setContentLengthLong);
// ✅ 确保安全的设置方式
storage.download(fileName, response.getOutputStream(), contentLength -> {
if (contentLength != null && contentLength > 0) {
response.setContentLengthLong(contentLength);
}
});
try {
// 文件下载逻辑
storage.download(fileName, response.getOutputStream(), contentLengthConsumer);
// 🔴 关键:必须手动刷新输出流
response.getOutputStream().flush();
} catch (Exception e) {
throw new ServiceException("文件下载失败: " + e.getMessage());
}
@GetMapping("/download/{fileId}")
public void downloadFile(@PathVariable Long fileId, HttpServletResponse response) {
try {
// 1. 权限验证
if (!hasDownloadPermission(fileId)) {
throw new ServiceException("无权限下载该文件");
}
// 2. 获取文件信息
FileVo fileInfo = fileService.getById(fileId);
if (fileInfo == null) {
throw new ServiceException("文件不存在");
}
// 3. 设置响应头
FileUtils.setAttachmentResponseHeader(response, fileInfo.getOriginalName());
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
// 4. 执行下载
OssClient storage = OssFactory.instance(fileInfo.getService());
storage.download(fileInfo.getFileName(), response.getOutputStream(), contentLength -> {
if (contentLength != null && contentLength > 0) {
response.setContentLengthLong(contentLength);
}
});
// 5. 刷新输出流
response.getOutputStream().flush();
} catch (Exception e) {
log.error("文件下载失败: {}", e.getMessage(), e);
throw new ServiceException("文件下载失败: " + e.getMessage());
}
}
本最佳实践将根据后续项目开发经验持续优化:
适用场景:
推广方式:
.cursor/cursorrules-backend.md.cursor/cursorrules-api-test.md.cursor/cursorrules-docs.mddocs/开发文档/API/场地预约管理API.mdtest/场地预约API测试脚本.pydocs/变更管理/code-adjust-record-2025-07-01-wuzilong.md总结:本最佳实践基于场地预约管理系统的成功开发经验,形成了一套完整的 API 开发标准流程,为后续 API 模块开发提供了可复制的标准模板和质量保证体系。
维护人员:开发团队
最后更新:2025-08-02
文档版本:v1.1 (新增文件下载最佳实践)
onBeforeUnmount清理blob URLs,防止内存泄漏核心实现:
// 认证图片加载
const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
const response = await request({
url: imageUrl,
method: 'get',
responseType: 'blob'
});
return URL.createObjectURL(response.data);
};
// 生命周期管理
onBeforeUnmount(() => {
blobUrls.value.forEach(url => URL.revokeObjectURL(url));
});