提交代码
This commit is contained in:
1 parent
0ed820d7ce
commit
4a2761cb0d
8 files changed
+1481
-159
No files matched your search
+145
-2
@@ -398,6 +398,122 @@ def run_all_tests(self):
|
||||
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. **权限控制**:无权限用户被正确拒绝
|
||||
|
||||
## 🔄 持续改进
|
||||
|
||||
### 规范迭代
|
||||
@@ -438,5 +554,32 @@ def run_all_tests(self):
|
||||
**总结**:本最佳实践基于场地预约管理系统的成功开发经验,形成了一套完整的 API 开发标准流程,为后续 API 模块开发提供了可复制的标准模板和质量保证体系。
|
||||
|
||||
**维护人员**:开发团队
|
||||
**最后更新**:2025-07-01
|
||||
**文档版本**:v1.0
|
||||
**最后更新**: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));
|
||||
});
|
||||
```
|
||||
Reference in new issue
Block a user