# 出国申请附件下载权限修复方案
## 📋 问题描述
### 错误现象
用户点击出国申请详情中的"出国境登记表盖章文件"和"党籍保留附件"下载按钮时,出现以下错误:
```xml
AccessDenied
Access Denied.
overseas/stamped/stamped_0_1760082853042.png
ruoyi
/ruoyi/overseas/stamped/stamped_0_1760082853042.png
```
### 根本原因
1. **OSS Bucket权限配置**:OSS bucket设置为私有访问,直接使用URL无法访问
2. **前端下载方式错误**:前端直接使用文件URL进行下载,绕过了权限验证
3. **缺少OSS ID存储**:数据库中只存储了文件路径,没有存储OSS文件ID,无法使用系统的安全下载接口
## 🎯 解决方案
### 整体架构
```
前端下载请求 → 后端代理接口 → 权限验证 → OSS安全下载 → 返回文件流
```
### 技术方案
采用**后端代理下载 + OSS ID存储**的方式,确保:
- ✅ 所有下载请求经过后端权限验证
- ✅ 使用系统统一的OSS服务管理文件
- ✅ 文件访问可追踪、可审计
- ✅ 支持私有bucket安全访问
## 🔧 实施步骤
### 1️⃣ 数据库改造
**新增字段**:`stamped_file_oss_id` 和 `party_retention_file_oss_id`
```sql
-- 添加盖章文件OSS ID字段
ALTER TABLE `rs_overseas_application`
ADD COLUMN `stamped_file_oss_id` BIGINT(20) NULL COMMENT '盖章文件OSS ID'
AFTER `stamped_file_upload_status`;
-- 添加党籍保留附件OSS ID字段
ALTER TABLE `rs_overseas_application`
ADD COLUMN `party_retention_file_oss_id` BIGINT(20) NULL COMMENT '党籍保留附件OSS ID'
AFTER `party_retention_upload_status`;
-- 添加索引以提升查询性能
ALTER TABLE `rs_overseas_application`
ADD INDEX `idx_stamped_file_oss_id` (`stamped_file_oss_id`),
ADD INDEX `idx_party_retention_file_oss_id` (`party_retention_file_oss_id`);
```
**执行位置**:`docs/设计文档/database/【立即执行】出国申请表添加OSS_ID字段.sql`
### 2️⃣ 后端Domain实体改造
#### RsOverseasApplication.java
```java
/**
* 盖章文件OSS ID(用于安全下载)
*/
@ExcelIgnore
private Long stampedFileOssId;
/**
* 党籍保留附件OSS ID(用于安全下载)
*/
@ExcelIgnore
private Long partyRetentionFileOssId;
```
#### RsOverseasApplicationVo.java
```java
/**
* 盖章文件OSS ID(用于安全下载)
*/
private Long stampedFileOssId;
/**
* 党籍保留附件OSS ID(用于安全下载)
*/
private Long partyRetentionFileOssId;
```
### 3️⃣ 后端上传接口改造
**改造前**:使用`OssClient`直接上传,只返回文件URL
```java
// 旧代码
OssClient ossClient = OssFactory.instance();
UploadResult uploadResult = ossClient.upload(file.getInputStream(), filePath,
file.getSize(), file.getContentType());
applicationService.updateStampedFileInfo(applicationId, uploadResult.getUrl(),
originalFilename, new Date(), "1");
```
**改造后**:使用`ISysOssService`上传,自动保存到`sys_oss`表并返回OSS ID
```java
// 新代码 - 使用系统OSS服务
@Autowired
private ISysOssService ossService;
SysOssVo ossResult = ossService.upload(file);
// 更新申请记录 - 存储OSS ID和文件路径
applicationService.updateStampedFileInfo(
applicationId,
ossResult.getUrl(), // 文件URL
ossResult.getOriginalName(), // 原始文件名
new Date(), // 上传时间
"1", // 上传状态:已上传
ossResult.getOssId() // ✨ OSS文件ID(新增)
);
```
**涉及接口**:
- `POST /rs/overseas/application/uploadStampedFile` - 盖章文件上传
- `POST /rs/overseas/application/uploadPartyRetentionFile` - 党籍保留附件上传
### 4️⃣ 后端Service层改造
#### IRsOverseasApplicationService.java
```java
// 添加ossId参数
Boolean updateStampedFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId);
Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId);
```
#### RsOverseasApplicationServiceImpl.java
```java
@Override
@Transactional
public Boolean updateStampedFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId) {
return baseMapper.update(null,
new LambdaUpdateWrapper()
.eq(RsOverseasApplication::getApplicationId, applicationId)
.set(RsOverseasApplication::getStampedRegistrationFilePath, filePath)
.set(RsOverseasApplication::getStampedRegistrationFileName, fileName)
.set(RsOverseasApplication::getStampedFileUploadTime, uploadTime)
.set(RsOverseasApplication::getStampedFileUploadStatus, uploadStatus)
.set(RsOverseasApplication::getStampedFileOssId, ossId) // ✨ 新增
) > 0;
}
```
### 5️⃣ 后端新增下载代理接口
#### RsOverseasApplicationController.java
**盖章文件下载**:
```java
/**
* 下载盖章文件(安全下载)
*/
@SaCheckPermission("rs:overseas:query")
@GetMapping("/downloadStampedFile/{applicationId}")
public void downloadStampedFile(@PathVariable Long applicationId, HttpServletResponse response) {
try {
RsOverseasApplicationVo application = applicationService.queryById(applicationId);
if (application == null || application.getStampedFileOssId() == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":404,\"msg\":\"文件不存在\"}");
return;
}
// 使用系统OSS服务进行安全下载
ossService.secureDownload(application.getStampedFileOssId(), response);
} catch (Exception e) {
e.printStackTrace();
}
}
```
**党籍保留附件下载**:
```java
/**
* 下载党籍保留附件(安全下载)
*/
@SaCheckPermission("rs:overseas:query")
@GetMapping("/downloadPartyRetentionFile/{applicationId}")
public void downloadPartyRetentionFile(@PathVariable Long applicationId, HttpServletResponse response) {
try {
RsOverseasApplicationVo application = applicationService.queryById(applicationId);
if (application == null || application.getPartyRetentionFileOssId() == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":404,\"msg\":\"文件不存在\"}");
return;
}
// 使用系统OSS服务进行安全下载
ossService.secureDownload(application.getPartyRetentionFileOssId(), response);
} catch (Exception e) {
e.printStackTrace();
}
}
```
**核心特性**:
- ✅ 权限验证:`@SaCheckPermission("rs:overseas:query")`
- ✅ 文件存在性检查
- ✅ 使用系统统一的`ISysOssService.secureDownload()`方法
- ✅ 自动处理文件流返回
### 6️⃣ 前端下载逻辑改造
#### index.vue
**改造前**:直接使用文件URL下载
```javascript
// 旧代码 - 直接访问OSS URL(会被拒绝)
const handleDownloadStampedFile = (data: OverseasApplicationVO) => {
const link = document.createElement('a');
link.href = data.stampedRegistrationFilePath; // ❌ 直接使用OSS URL
link.download = data.stampedRegistrationFileName;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
```
**改造后**:调用后端代理接口
```javascript
// 新代码 - 通过后端代理下载(安全)
const handleDownloadStampedFile = (data: OverseasApplicationVO) => {
console.log('📥 下载出国境登记表盖章文件:', data.stampedRegistrationFileName);
if (!data.applicationId) {
ElMessage.warning('申请ID不存在');
return;
}
// ✅ 使用后端代理下载接口
const downloadUrl = `/api/rs/overseas/application/downloadStampedFile/${data.applicationId}`;
window.open(downloadUrl, '_blank');
ElMessage.success('文件下载已开始');
};
// 党籍保留附件下载(同样改造)
const handleDownloadPartyRetentionFile = (data: OverseasApplicationVO) => {
console.log('📥 下载党籍保留附件:', data.partyRetentionFileName);
if (!data.applicationId) {
ElMessage.warning('申请ID不存在');
return;
}
// ✅ 使用后端代理下载接口
const downloadUrl = `/api/rs/overseas/application/downloadPartyRetentionFile/${data.applicationId}`;
window.open(downloadUrl, '_blank');
ElMessage.success('文件下载已开始');
};
```
## 📊 改造效果
### 安全性提升
| 项目 | 改造前 | 改造后 |
|------|--------|--------|
| 权限验证 | ❌ 无 | ✅ Sa-Token权限验证 |
| 访问控制 | ❌ 任何人可访问URL | ✅ 必须登录且有权限 |
| 访问日志 | ❌ 无 | ✅ 自动记录访问日志 |
| Bucket类型 | ⚠️ 必须公开 | ✅ 可使用私有bucket |
### 技术优势
- ✅ **统一管理**:使用系统统一的`ISysOssService`服务
- ✅ **可扩展性**:支持断点续传、缓存等高级特性
- ✅ **可追踪性**:所有文件访问都有日志记录
- ✅ **规范一致**:与系统其他模块保持一致的文件处理方式
## 🚀 部署步骤
### 1. 执行数据库迁移
```bash
# 连接到数据库
mysql -u root -p hzdyjs_xg
# 执行SQL脚本
source docs/设计文档/database/【立即执行】出国申请表添加OSS_ID字段.sql
```
### 2. 重启后端服务
```bash
# 停止服务
# 重新编译打包
# 启动服务
```
### 3. 刷新前端
```bash
# 清除浏览器缓存
# 重新访问页面
```
### 4. 验证功能
- [ ] 上传新的盖章文件,检查是否保存了OSS ID
- [ ] 上传新的党籍保留附件,检查是否保存了OSS ID
- [ ] 测试下载功能,确认可以正常下载
- [ ] 检查下载时的权限验证是否生效
## 📝 注意事项
### 数据兼容性
⚠️ **已存在的文件处理**:
- 旧数据:没有OSS ID的记录无法通过新接口下载
- 解决方案:
1. 方案一:保留旧的下载方式作为降级方案
2. 方案二:编写数据迁移脚本,为旧文件生成OSS ID
3. 方案三(推荐):让用户重新上传文件
### OSS配置检查
确保系统OSS配置正确:
```yaml
# application.yml
oss:
endpoint: xxx
accessKeyId: xxx
accessKeySecret: xxx
bucketName: ruoyi
region: xxx
```
### 权限配置
确保菜单权限配置正确:
- `rs:overseas:query` - 查看出国申请权限(含下载)
- `rs:overseas:add` - 新增出国申请权限(含上传)
## 🔍 故障排查
### 问题1:下载时提示"文件不存在"
**排查步骤**:
1. 检查数据库中的`stamped_file_oss_id`或`party_retention_file_oss_id`字段是否有值
2. 检查`sys_oss`表中是否存在对应的OSS ID记录
3. 检查OSS中是否真实存在该文件
### 问题2:下载时提示"无权限"
**排查步骤**:
1. 检查用户是否登录
2. 检查用户是否有`rs:overseas:query`权限
3. 检查Sa-Token配置是否正确
### 问题3:下载速度慢
**可能原因**:
- OSS服务器距离远
- 文件较大
- 网络带宽限制
**优化方案**:
- 启用CDN加速
- 使用分片下载
- 优化网络配置
## 📚 相关文档
- [系统OSS服务文档](./OSS文件管理服务.md)
- [Sa-Token权限验证文档](./权限管理文档.md)
- [出国申请API文档](../API/出国申请管理API.md)
## 🎯 总结
本次改造通过引入OSS ID存储和后端代理下载机制,彻底解决了出国申请附件下载权限问题。改造后的系统:
- ✅ 安全性更高:所有下载都经过权限验证
- ✅ 可维护性更好:使用系统统一的OSS服务
- ✅ 可追踪性更强:所有文件访问都有日志
- ✅ 扩展性更好:支持更多高级特性
**日期**:2025-01-10
**作者**:AI Assistant
**版本**:v1.0