Files
hzdyjs/docs/开发文档/出国申请附件下载权限修复方案.md
T
2025-10-14 16:48:17 +08:00

12 KiB
Raw Blame History

出国申请附件下载权限修复方案

📋 问题描述

错误现象

用户点击出国申请详情中的"出国境登记表盖章文件"和"党籍保留附件"下载按钮时,出现以下错误:

<Error>
<Code>AccessDenied</Code>
<Message>Access Denied.</Message>
<Key>overseas/stamped/stamped_0_1760082853042.png</Key>
<BucketName>ruoyi</BucketName>
<Resource>/ruoyi/overseas/stamped/stamped_0_1760082853042.png</Resource>
</Error>

根本原因

  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

-- 添加盖章文件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

/**
 * 盖章文件OSS ID(用于安全下载)
 */
@ExcelIgnore
private Long stampedFileOssId;

/**
 * 党籍保留附件OSS ID(用于安全下载)
 */
@ExcelIgnore
private Long partyRetentionFileOssId;

RsOverseasApplicationVo.java

/**
 * 盖章文件OSS ID(用于安全下载)
 */
private Long stampedFileOssId;

/**
 * 党籍保留附件OSS ID(用于安全下载)
 */
private Long partyRetentionFileOssId;

3️⃣ 后端上传接口改造

改造前:使用OssClient直接上传,只返回文件URL

// 旧代码
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

// 新代码 - 使用系统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

// 添加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

@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<RsOverseasApplication>()
            .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

盖章文件下载:

/**
 * 下载盖章文件(安全下载)
 */
@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();
    }
}

党籍保留附件下载:

/**
 * 下载党籍保留附件(安全下载)
 */
@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下载

// 旧代码 - 直接访问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);
};

改造后:调用后端代理接口

// 新代码 - 通过后端代理下载(安全)
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. 执行数据库迁移

# 连接到数据库
mysql -u root -p hzdyjs_xg

# 执行SQL脚本
source docs/设计文档/database/【立即执行】出国申请表添加OSS_ID字段.sql

2. 重启后端服务

# 停止服务
# 重新编译打包
# 启动服务

3. 刷新前端

# 清除浏览器缓存
# 重新访问页面

4. 验证功能

  • 上传新的盖章文件,检查是否保存了OSS ID
  • 上传新的党籍保留附件,检查是否保存了OSS ID
  • 测试下载功能,确认可以正常下载
  • 检查下载时的权限验证是否生效

📝 注意事项

数据兼容性

⚠️ 已存在的文件处理:

  • 旧数据:没有OSS ID的记录无法通过新接口下载
  • 解决方案:
    1. 方案一:保留旧的下载方式作为降级方案
    2. 方案二:编写数据迁移脚本,为旧文件生成OSS ID
    3. 方案三(推荐):让用户重新上传文件

OSS配置检查

确保系统OSS配置正确:

# 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 ID存储和后端代理下载机制,彻底解决了出国申请附件下载权限问题。改造后的系统:

  • ✅ 安全性更高:所有下载都经过权限验证
  • ✅ 可维护性更好:使用系统统一的OSS服务
  • ✅ 可追踪性更强:所有文件访问都有日志
  • ✅ 扩展性更好:支持更多高级特性

日期:2025-01-10
作者:AI Assistant
版本:v1.0