Files
hzdyjs/docs/开发文档/API开发最佳实践总结.md
T
2025-08-13 22:16:44 +08:00

615 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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));
});
```