# 出国申请附件下载权限修复方案 ## 📋 问题描述 ### 错误现象 用户点击出国申请详情中的"出国境登记表盖章文件"和"党籍保留附件"下载按钮时,出现以下错误: ```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