615 lines
19 KiB
Markdown
615 lines
19 KiB
Markdown
# API 开发最佳实践总结
|
||
|
||
**基于场地预约管理系统开发实践**
|
||
|
||
## 📖 概述
|
||
|
||
本文档总结了场地预约管理系统的完整 API 开发实践,形成了一套标准化的 API 开发流程和规范,为后续 API 模块开发提供参考。
|
||
|
||
**实践基础**:
|
||
|
||
- 项目:校园管理系统 (hzdyjs_xg)
|
||
- 模块:场地预约管理系统
|
||
- 开发时间:2025-07-01
|
||
- 技术栈:Spring Boot 3.4.6 + MyBatis-Plus + Sa-Token
|
||
|
||
## 🎯 开发成果总览
|
||
|
||
### 完整交付物
|
||
|
||
| 类型 | 数量 | 文件/功能 | 质量标准 |
|
||
| ---------- | --------- | ------------------ | ------------ |
|
||
| 需求文档 | 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 模块** |
|
||
|
||
## 🚀 标准开发流程
|
||
|
||
### 阶段一:需求分析和文档创建(必须先行)
|
||
|
||
#### 1. 业务需求文档
|
||
|
||
```
|
||
docs/需求文档/[模块名]/
|
||
├── [模块名]系统需求说明.md # 详细业务需求
|
||
└── README.md # 需求概览
|
||
```
|
||
|
||
**内容要求**:
|
||
|
||
- 项目概述和目标
|
||
- 详细业务流程分析
|
||
- 功能需求规格说明
|
||
- 数据模型设计
|
||
- 权限设计方案
|
||
- 验收标准定义
|
||
|
||
#### 2. 技术架构文档
|
||
|
||
```
|
||
docs/业务模块/[模块名]/
|
||
└── README.md # 技术架构说明
|
||
```
|
||
|
||
### 阶段二:数据库设计(代码开发前置条件)
|
||
|
||
#### 3. 数据库脚本创建
|
||
|
||
```
|
||
docs/设计文档/database/
|
||
├── XX-[模块名]-module.sql # 数据表结构
|
||
├── XX-[模块名]-data.sql # 字典数据
|
||
└── XX-[模块名]-menu.sql # 菜单权限
|
||
```
|
||
|
||
**标准内容**:
|
||
|
||
- **数据表**:2 个表(主实体+业务实体),包含标准基础字段
|
||
- **字典数据**:2 个类型+6-9 条数据
|
||
- **菜单权限**:12 个权限节点(目录+菜单+按钮)
|
||
- **示例数据**:5-8 条测试数据
|
||
|
||
#### 4. 数据库执行验证
|
||
|
||
- 在测试环境执行 SQL 脚本
|
||
- 验证所有表结构、字典、权限、示例数据
|
||
- 确保数据库初始化完成
|
||
|
||
### 阶段三:后端模块开发
|
||
|
||
#### 5. ruoyi 模块标准结构
|
||
|
||
```
|
||
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. 16 个标准 API 接口模式
|
||
|
||
**基础管理接口(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` - 导出 Excel
|
||
- `POST /[module]/[business]/import` - 导入 Excel
|
||
- `GET /[module]/[business]/statistics` - 统计数据
|
||
- `POST /[module]/[business]/batch` - 批量操作
|
||
- `POST /[module]/[business]/{id}/enable` - 启用操作
|
||
- `POST /[module]/[business]/{id}/disable` - 禁用操作
|
||
|
||
#### 7. 模块集成配置
|
||
|
||
- `ruoyi-modules/pom.xml` - 添加子模块
|
||
- `ruoyi-admin/pom.xml` - 添加依赖
|
||
- `backend/pom.xml` - 版本管理配置
|
||
|
||
### 阶段四:API 文档编写(代码完成后立即执行)
|
||
|
||
#### 8. API 文档创建
|
||
|
||
```
|
||
docs/开发文档/API/
|
||
├── [模块名]管理API.md # 完整API文档(400+行)
|
||
├── API文档总览.md # 更新导航索引
|
||
└── API文档模板.md # 标准模板
|
||
```
|
||
|
||
**标准文档结构**:
|
||
|
||
- 📖 概述和核心功能
|
||
- 🏢 主实体管理 API(6 个接口)
|
||
- 📅 业务实体管理 API(10 个接口)
|
||
- 📋 数据字典
|
||
- 🔐 权限配置
|
||
- 📏 业务规则
|
||
- ❌ 错误码说明
|
||
|
||
### 阶段五:自动化测试脚本开发
|
||
|
||
#### 9. Python 测试脚本
|
||
|
||
```
|
||
test/
|
||
└── [模块名]API测试脚本.py # 自动化测试脚本(450+行)
|
||
```
|
||
|
||
**标准脚本结构**:
|
||
|
||
- 强制 AES+RSA 双重加密登录
|
||
- 服务可用性检测
|
||
- 16 个 API 接口完整测试
|
||
- 业务规则验证测试
|
||
- 自动数据清理
|
||
|
||
### 阶段六:变更记录和验证
|
||
|
||
#### 10. 变更记录文档
|
||
|
||
```
|
||
docs/变更管理/
|
||
└── code-adjust-record-YYYY-MM-DD-{username}.md
|
||
```
|
||
|
||
#### 11. 完整功能验证
|
||
|
||
- 后端服务启动测试
|
||
- API 接口功能测试
|
||
- 文档准确性验证
|
||
- 权限控制测试
|
||
|
||
## 🔧 技术实现要点
|
||
|
||
### 数据库设计规范
|
||
|
||
#### 强制性基础字段
|
||
|
||
```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代表删除)',
|
||
```
|
||
|
||
#### 业务字段设计原则
|
||
|
||
- 雪花 ID 作为主键
|
||
- 逻辑删除,禁止物理删除
|
||
- 多租户支持
|
||
- 完整的中文注释
|
||
- 合理的索引设计
|
||
|
||
### 后端代码规范
|
||
|
||
#### 实体类设计
|
||
|
||
```java
|
||
// 继承TenantEntity,包含完整注释
|
||
@TableName("table_name")
|
||
public class Entity extends TenantEntity {
|
||
@TableId("id")
|
||
private Long id;
|
||
|
||
@TableField("field_name")
|
||
private String fieldName;
|
||
}
|
||
```
|
||
|
||
#### 业务对象设计
|
||
|
||
```java
|
||
// 添加验证注解
|
||
public class EntityBo {
|
||
@NotBlank(message = "字段不能为空", groups = {AddGroup.class, EditGroup.class})
|
||
@Size(max = 100, message = "字段长度不能超过100个字符")
|
||
private String fieldName;
|
||
}
|
||
```
|
||
|
||
#### 视图对象设计
|
||
|
||
```java
|
||
// 支持Excel导出
|
||
public class EntityVo {
|
||
@ExcelProperty(value = "字段名称")
|
||
private String fieldName;
|
||
}
|
||
```
|
||
|
||
#### 权限控制
|
||
|
||
```java
|
||
// 每个接口必须添加权限注解
|
||
@SaCheckPermission("module:entity:list")
|
||
@GetMapping("/list")
|
||
public TableDataInfo<EntityVo> list(EntityBo bo, PageQuery pageQuery) {
|
||
// 接口实现
|
||
}
|
||
```
|
||
|
||
#### 响应格式规范
|
||
|
||
```java
|
||
// 分页接口直接返回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());
|
||
}
|
||
```
|
||
|
||
### API 测试脚本规范
|
||
|
||
#### 强制加密登录
|
||
|
||
```python
|
||
# 强制依赖检查,缺少加密库直接退出
|
||
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)
|
||
```
|
||
|
||
#### 标准测试流程
|
||
|
||
```python
|
||
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 模块开发必须包含**:
|
||
|
||
- ✅ **完整的需求和技术文档**(2-3 个文档)
|
||
- ✅ **数据库脚本和权限配置**(3 个 SQL 文件)
|
||
- ✅ **完整的后端模块代码**(15-20 个 Java 文件)
|
||
- ✅ **详细的 API 接口文档**(400+行)
|
||
- ✅ **自动化测试脚本**(450+行)
|
||
- ✅ **完整的变更记录日志**
|
||
- ✅ **模块成功集成和功能验证**
|
||
|
||
### 文档质量标准
|
||
|
||
- **完整性**:16 个标准接口的完整文档
|
||
- **准确性**:所有参数和响应与实际代码一致
|
||
- **详细性**:包含完整的业务规则和错误码说明
|
||
- **规范性**:使用统一的文档模板和格式
|
||
- **可测试性**:提供真实的请求响应示例
|
||
|
||
### 代码质量标准
|
||
|
||
- **架构规范**:严格遵循 RuoYi-Vue-Plus 架构规范
|
||
- **完整功能**:涵盖管理和业务的全流程
|
||
- **权限控制**:实现接口权限和数据权限控制
|
||
- **业务规则**:包含完整的业务验证逻辑
|
||
- **可扩展性**:支持后续功能扩展和优化
|
||
|
||
## 💡 最佳实践经验
|
||
|
||
### 开发流程优化
|
||
|
||
1. **先文档后代码**:需求分析完成后再开始编码
|
||
2. **数据库优先**:表结构确定后再开发业务逻辑
|
||
3. **标准化接口**:16 个接口模式覆盖 90%的业务场景
|
||
4. **文档同步更新**:代码完成立即更新文档
|
||
5. **完整测试验证**:自动化测试确保质量
|
||
6. **规范化集成**:严格按照项目架构规范集成
|
||
|
||
### 常见问题和解决方案
|
||
|
||
#### 1. 版本配置问题
|
||
|
||
**问题**:新模块版本号不一致导致编译失败
|
||
**解决**:在根 pom.xml 的 dependencyManagement 中配置版本管理
|
||
|
||
#### 2. Excel 注解错误
|
||
|
||
**问题**:使用了错误的 Excel 注解包
|
||
**解决**:使用`cn.idev.excel.annotation.ExcelProperty`
|
||
|
||
#### 3. 登录认证问题
|
||
|
||
**问题**:API 测试脚本登录失败
|
||
**解决**:强制使用 AES+RSA 双重加密登录,确保加密库正确安装
|
||
|
||
#### 4. 文档不同步问题
|
||
|
||
**问题**:代码变更后文档未更新
|
||
**解决**:建立强制性文档同步更新规则,代码完成后立即更新文档
|
||
|
||
#### 5. MyBatis-Plus 实体字段映射问题 (2025-08-13 新增)
|
||
|
||
**问题**:SQL 错误 "Unknown column 'parent_name' in 'field list'"
|
||
**原因**:实体类中包含数据库表不存在的字段,MyBatis-Plus 自动生成 SQL 时包含了这些字段
|
||
**典型场景**:实体类中有用于显示的冗余字段(如 parentName),但数据库表中没有对应字段
|
||
|
||
**解决方案**:
|
||
|
||
```java
|
||
/**
|
||
* 父部门名称(冗余字段,用于显示,不持久化到数据库)
|
||
*/
|
||
@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...
|
||
```
|
||
|
||
### 性能优化建议
|
||
|
||
1. **数据库查询优化**:合理使用索引,避免全表扫描
|
||
2. **分页查询优化**:使用 PageHelper 进行高效分页
|
||
3. **缓存策略**:对频繁查询的字典数据使用 Redis 缓存
|
||
4. **接口响应优化**:减少不必要的字段传输
|
||
|
||
## 📁 文件下载最佳实践 (2025-08-02 新增)
|
||
|
||
### 核心问题解决
|
||
|
||
基于OSS文件下载功能的修复经验,总结文件下载相关的最佳实践。
|
||
|
||
#### 1. 响应头设置规范
|
||
|
||
**🔴 关键点:二进制文件下载不要设置charset**
|
||
|
||
```java
|
||
// ❌ 错误方式 - 会导致下载失败
|
||
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");
|
||
```
|
||
|
||
#### 2. Content-Length 正确设置
|
||
|
||
```java
|
||
// ❌ 可能失效的方法引用
|
||
storage.download(fileName, response.getOutputStream(), response::setContentLengthLong);
|
||
|
||
// ✅ 确保安全的设置方式
|
||
storage.download(fileName, response.getOutputStream(), contentLength -> {
|
||
if (contentLength != null && contentLength > 0) {
|
||
response.setContentLengthLong(contentLength);
|
||
}
|
||
});
|
||
```
|
||
|
||
#### 3. 输出流处理
|
||
|
||
```java
|
||
try {
|
||
// 文件下载逻辑
|
||
storage.download(fileName, response.getOutputStream(), contentLengthConsumer);
|
||
|
||
// 🔴 关键:必须手动刷新输出流
|
||
response.getOutputStream().flush();
|
||
|
||
} catch (Exception e) {
|
||
throw new ServiceException("文件下载失败: " + e.getMessage());
|
||
}
|
||
```
|
||
|
||
#### 4. 完整的文件下载模板
|
||
|
||
```java
|
||
@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());
|
||
}
|
||
}
|
||
```
|
||
|
||
### 常见问题排查
|
||
|
||
#### 问题1:文件传输完成但客户端未收到
|
||
- **原因**:Content-Type包含charset或缺少flush()
|
||
- **解决**:移除charset,添加手动flush
|
||
|
||
#### 问题2:下载的文件损坏
|
||
- **原因**:Content-Length设置错误
|
||
- **解决**:使用lambda表达式确保Content-Length正确设置
|
||
|
||
#### 问题3:浏览器缓存导致下载异常
|
||
- **原因**:缺少缓存控制头
|
||
- **解决**:设置no-cache相关响应头
|
||
|
||
### 测试验证标准
|
||
|
||
1. **Postman测试**:能正确下载文件
|
||
2. **浏览器测试**:点击下载链接正常
|
||
3. **文件完整性**:下载文件与原文件MD5一致
|
||
4. **不同文件类型**:图片、文档、压缩包都能正常下载
|
||
5. **权限控制**:无权限用户被正确拒绝
|
||
|
||
## 🔄 持续改进
|
||
|
||
### 规范迭代
|
||
|
||
本最佳实践将根据后续项目开发经验持续优化:
|
||
|
||
1. **新技术集成**:随着技术栈升级调整开发规范
|
||
2. **流程优化**:根据开发效率反馈优化工作流程
|
||
3. **质量提升**:根据测试反馈完善质量标准
|
||
4. **工具改进**:开发更多自动化工具提升效率
|
||
|
||
### 应用推广
|
||
|
||
**适用场景**:
|
||
|
||
- 校园管理系统的其他业务模块
|
||
- 类似的管理信息系统
|
||
- Spring Boot + MyBatis-Plus 架构的项目
|
||
|
||
**推广方式**:
|
||
|
||
- 作为新团队成员的培训材料
|
||
- 作为代码评审的质量标准
|
||
- 作为项目规划的参考模板
|
||
|
||
## 📚 参考文档
|
||
|
||
- **后端开发规范**:`.cursor/cursorrules-backend.md`
|
||
- **API 测试规范**:`.cursor/cursorrules-api-test.md`
|
||
- **文档管理规范**:`.cursor/cursorrules-docs.md`
|
||
- **实际案例参考**:
|
||
- `docs/开发文档/API/场地预约管理API.md`
|
||
- `test/场地预约API测试脚本.py`
|
||
- `docs/变更管理/code-adjust-record-2025-07-01-wuzilong.md`
|
||
|
||
---
|
||
|
||
**总结**:本最佳实践基于场地预约管理系统的成功开发经验,形成了一套完整的 API 开发标准流程,为后续 API 模块开发提供了可复制的标准模板和质量保证体系。
|
||
|
||
**维护人员**:开发团队
|
||
**最后更新**:2025-08-02
|
||
**文档版本**:v1.1 (新增文件下载最佳实践)
|
||
|
||
## 前端开发最佳实践
|
||
|
||
### 🎯 组件开发规范
|
||
|
||
#### ImagePreview图片预览组件
|
||
- **认证图片加载**: 通过axios获取blob数据,确保认证信息传递
|
||
- **内存管理**: 使用`onBeforeUnmount`清理blob URLs,防止内存泄漏
|
||
- **缓存优化**: 实现blob URL缓存机制,提升性能
|
||
|
||
**核心实现**:
|
||
```javascript
|
||
// 认证图片加载
|
||
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));
|
||
});
|
||
```
|