From ac8a58386c5108868eca658f7e6659cf597f4933 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Sun, 3 Aug 2025 17:38:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/TestLeaveServiceImpl.java | 9 +- ...ord-2025-01-27-fix-ossid-precision-loss.md | 103 ++++++ web/src/api/system/file.ts | 12 +- web/src/components/FileUpload/index.vue | 67 +++- web/src/components/ImageUpload/index.vue | 2 +- web/src/components/Process/approvalRecord.vue | 7 +- web/src/plugins/download.ts | 48 ++- web/src/views/hq/pm/repairOrder/index.vue | 2 +- web/src/views/xg/xsjcxx/xsqj/index.vue | 236 +++++++++++++ web/src/views/xg/xsjcxx/xsqj/leaveEdit.vue | 312 ++++++++++++++++++ 10 files changed, 765 insertions(+), 33 deletions(-) create mode 100644 docs/变更管理/code-adjust-record-2025-01-27-fix-ossid-precision-loss.md create mode 100644 web/src/views/xg/xsjcxx/xsqj/index.vue create mode 100644 web/src/views/xg/xsjcxx/xsqj/leaveEdit.vue diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/TestLeaveServiceImpl.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/TestLeaveServiceImpl.java index 3a773345..c5f7911c 100644 --- a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/TestLeaveServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/TestLeaveServiceImpl.java @@ -142,7 +142,7 @@ public class TestLeaveServiceImpl implements ITestLeaveService { * * @param processEvent 参数 */ - @EventListener(condition = "#processEvent.flowCode.startsWith('leave')") + @EventListener(condition = "#processEvent.flowCode.startsWith('leave') || #processEvent.flowCode.startsWith('hzdyjs-qj')") public void processHandler(ProcessEvent processEvent) { log.info("当前任务执行了{}", processEvent.toString()); TestLeave testLeave = baseMapper.selectById(Long.valueOf(processEvent.getBusinessId())); @@ -165,7 +165,8 @@ public class TestLeaveServiceImpl implements ITestLeaveService { /** * 执行任务创建监听 - * 示例:也可通过 @EventListener(condition = "#processTaskEvent.flowCode=='leave1'")进行判断 + * 示例:也可通过 @EventListener(condition = + * "#processTaskEvent.flowCode=='leave1'")进行判断 * 在方法中判断流程节点key * if ("xxx".equals(processTaskEvent.getNodeCode())) { * //执行业务逻辑 @@ -173,7 +174,7 @@ public class TestLeaveServiceImpl implements ITestLeaveService { * * @param processTaskEvent 参数 */ - @EventListener(condition = "#processTaskEvent.flowCode.startsWith('leave')") + @EventListener(condition = "#processTaskEvent.flowCode.startsWith('leave') || #processTaskEvent.flowCode.startsWith('hzdyjs-qj')") public void processTaskHandler(ProcessTaskEvent processTaskEvent) { log.info("当前任务创建了{}", processTaskEvent.toString()); } @@ -185,7 +186,7 @@ public class TestLeaveServiceImpl implements ITestLeaveService { * * @param processDeleteEvent 参数 */ - @EventListener(condition = "#processDeleteEvent.flowCode.startsWith('leave')") + @EventListener(condition = "#processDeleteEvent.flowCode.startsWith('leave') || #processDeleteEvent.flowCode.startsWith('hzdyjs-qj')") public void processDeleteHandler(ProcessDeleteEvent processDeleteEvent) { log.info("监听删除流程事件,当前任务执行了{}", processDeleteEvent.toString()); TestLeave testLeave = baseMapper.selectById(Long.valueOf(processDeleteEvent.getBusinessId())); diff --git a/docs/变更管理/code-adjust-record-2025-01-27-fix-ossid-precision-loss.md b/docs/变更管理/code-adjust-record-2025-01-27-fix-ossid-precision-loss.md new file mode 100644 index 00000000..4915e69d --- /dev/null +++ b/docs/变更管理/code-adjust-record-2025-01-27-fix-ossid-precision-loss.md @@ -0,0 +1,103 @@ +# 文件下载功能ossId精度丢失问题修复记录 + +**修复时间**: 2025-01-27 +**修复人员**: Cursor AI +**问题描述**: 文件下载功能中`Number(ossId)`转换导致大整数精度丢失 + +## 🔍 问题分析 + +### 问题现象 +- 前端调用文件下载API时,使用`Number(ossId)`转换文件ID +- JavaScript的`Number()`类型转换对大整数(超过2^53)会丢失精度 +- 导致传递给后端的ossId不正确,无法正确下载文件 + +### 技术原因 +- 后端使用`Long`类型(64位整数)存储文件ID +- 前端API接口定义中`ossId`参数类型为`number` +- `Number()`转换大整数时会发生精度丢失 + +## 🛠️ 修复方案 + +### 1. 修改API接口类型定义 +**文件**: `web/src/api/system/file.ts` + +```typescript +// 修改前 +export function getFileDownloadUrl(ossId: number, bucketPassword?: string, expireSeconds: number = 300) + +// 修改后 +export function getFileDownloadUrl(ossId: number | string | bigint, bucketPassword?: string, expireSeconds: number = 300) +``` + +**涉及函数**: +- `getFileDownloadUrl()` - 文件下载URL获取 +- `getFilePreviewUrl()` - 文件预览URL获取 +- `getFileAccessUrl()` - 文件访问URL获取 +- `secureDownloadFile()` - 安全文件下载 +- `securePreviewFile()` - 安全文件预览 + +### 2. 修改下载插件实现 +**文件**: `web/src/plugins/download.ts` + +```typescript +// 修改前 +const { data: downloadUrl } = await getFileDownloadUrl(Number(ossId)); + +// 修改后 +const { data: downloadUrl } = await getFileDownloadUrl(String(ossId)); +``` + +**涉及方法**: +- `oss()` - 基础文件下载方法 +- `secureOss()` - 安全文件下载方法 + +## ✅ 修复效果 + +### 解决的问题 +1. **精度丢失**: 使用`String()`转换避免大整数精度丢失 +2. **类型安全**: API接口支持多种类型输入,保持向后兼容 +3. **一致性**: 所有文件相关API接口类型定义统一 + +### 技术优势 +- **兼容性**: 支持`number`、`string`、`bigint`三种类型输入 +- **安全性**: 避免大整数精度丢失问题 +- **可维护性**: 统一的类型定义,便于后续维护 + +## 📋 验证要点 + +### 功能验证 +- [ ] 小整数文件ID下载正常 +- [ ] 大整数文件ID下载正常(超过2^53) +- [ ] 字符串类型文件ID下载正常 +- [ ] 文件预览功能正常 +- [ ] 文件访问功能正常 + +### 兼容性验证 +- [ ] 现有代码调用不受影响 +- [ ] 类型检查通过 +- [ ] 编译无错误 + +## 🔄 相关文档更新 + +### 需要同步更新的文档 +- [ ] API文档中的文件下载接口说明 +- [ ] 前端开发规范中的文件处理最佳实践 +- [ ] 测试文档中的文件下载测试用例 + +## 📝 经验总结 + +### 最佳实践 +1. **大整数处理**: 避免使用`Number()`转换可能超过2^53的整数 +2. **类型设计**: API接口设计时考虑多种输入类型的兼容性 +3. **精度安全**: 涉及ID传递时优先使用字符串类型 + +### 预防措施 +1. **代码审查**: 检查所有涉及ID转换的代码 +2. **类型检查**: 使用TypeScript严格模式进行类型检查 +3. **测试覆盖**: 增加大整数ID的测试用例 + +--- + +**修复状态**: ✅ 已完成 +**测试状态**: ⏳ 待验证 +**文档状态**: ⏳ 待更新 \ No newline at end of file diff --git a/web/src/api/system/file.ts b/web/src/api/system/file.ts index 300572ce..7fe9ed33 100644 --- a/web/src/api/system/file.ts +++ b/web/src/api/system/file.ts @@ -7,7 +7,7 @@ import request from '@/utils/request' * @param expireSeconds 过期时间(秒),默认300秒 * @returns 临时访问URL */ -export function getFileAccessUrl(ossId: number, bucketPassword?: string, expireSeconds: number = 300) { +export function getFileAccessUrl(ossId: number | string | bigint, bucketPassword?: string, expireSeconds: number = 300) { return request({ url: `/resource/file/access/${ossId}`, method: 'get', @@ -44,9 +44,9 @@ export function getBatchFileAccessUrl(ossIds: number[], bucketPassword?: string, * @param expireSeconds 过期时间(秒),默认300秒 * @returns 下载URL */ -export function getFileDownloadUrl(ossId: number, bucketPassword?: string, expireSeconds: number = 300) { +export function getFileDownloadUrl(ossId: number | string | bigint, bucketPassword?: string, expireSeconds: number = 300) { return request({ - url: `/resource/file/download/${ossId}`, + url: `/resource/oss/download/${ossId}`, method: 'get', params: { bucketPassword, @@ -62,7 +62,7 @@ export function getFileDownloadUrl(ossId: number, bucketPassword?: string, expir * @param expireSeconds 过期时间(秒),默认300秒 * @returns 预览URL */ -export function getFilePreviewUrl(ossId: number, bucketPassword?: string, expireSeconds: number = 300) { +export function getFilePreviewUrl(ossId: number | string | bigint, bucketPassword?: string, expireSeconds: number = 300) { return request({ url: `/resource/file/preview/${ossId}`, method: 'get', @@ -80,7 +80,7 @@ export function getFilePreviewUrl(ossId: number, bucketPassword?: string, expire * @param bucketPassword bucket访问密码 * @returns Promise */ -export async function secureDownloadFile(ossId: number, fileName: string, bucketPassword?: string) { +export async function secureDownloadFile(ossId: number | string | bigint, fileName: string, bucketPassword?: string) { try { // 获取临时下载URL const { data: downloadUrl } = await getFileDownloadUrl(ossId, bucketPassword) @@ -119,7 +119,7 @@ export async function secureDownloadFile(ossId: number, fileName: string, bucket * @param bucketPassword bucket访问密码 * @returns Promise 预览URL */ -export async function securePreviewFile(ossId: number, contentType?: string, bucketPassword?: string) { +export async function securePreviewFile(ossId: number | string | bigint, contentType?: string, bucketPassword?: string) { try { // 获取临时预览URL const { data: previewUrl } = await getFilePreviewUrl(ossId, bucketPassword) diff --git a/web/src/components/FileUpload/index.vue b/web/src/components/FileUpload/index.vue index 1767b1b0..0613874e 100644 --- a/web/src/components/FileUpload/index.vue +++ b/web/src/components/FileUpload/index.vue @@ -33,11 +33,12 @@
  • - - {{ getFileName(file.name) }} - + + {{ getFileName(file.name) }} +
    删除 + 下载
  • @@ -48,6 +49,8 @@ import { propTypes } from '@/utils/propTypes'; import { delOss, listByIds } from '@/api/system/oss'; import { globalHeaders } from '@/utils/request'; +import request from '@/utils/request'; +import { vLoading, ElLoading } from 'element-plus'; const props = defineProps({ modelValue: { @@ -116,6 +119,64 @@ watch( { deep: true, immediate: true } ); +/** 下载附件 */ +const handleDownloadAttachment = async (file: any) => { + let downloadUrl = file.ossId; + let fileName = file.name || 'attachment'; + + if (!downloadUrl) { + proxy?.$modal.msgError('无法获取文件下载地址'); + return; + } + + // 对于服务器文件,使用fetch获取内容强制下载 + try { + const loading = ElLoading.service({ + text: '正在下载文件...', + target: document.body + }); + + // const response = await fetch(downloadUrl); + // if (!response.ok) { + // throw new Error(`HTTP error! status: ${response.status}`); + // } + // 通过fetch获取文件内容,强制下载 + // const response = await req('/resource/oss/download/' + attachment.filePath); + const response = await request({ + url: '/resource/oss/download/' + downloadUrl, + method: 'get', + responseType: 'blob', + timeout: 300000 // 300秒超时 + }); + // 由于axios响应拦截器对blob类型响应直接返回blob数据 + // 这里response直接就是blob对象,而不是包含data属性的响应对象 + const blob = response; + + // 验证响应数据 + if (!blob) { + throw new Error('响应数据为空'); + } + + // const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + link.style.display = 'none'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + window.URL.revokeObjectURL(url); + loading?.close(); + console.log('下载服务器文件成功:', fileName); + } catch (error) { + console.error('下载文件失败:', error); + proxy?.$modal.msgError('下载文件失败,请稍后重试'); + } +}; + // 上传前校检格式和大小 const handleBeforeUpload = (file: any) => { // 校检文件类型 diff --git a/web/src/components/ImageUpload/index.vue b/web/src/components/ImageUpload/index.vue index 365b6e8e..e3020e5d 100644 --- a/web/src/components/ImageUpload/index.vue +++ b/web/src/components/ImageUpload/index.vue @@ -160,7 +160,7 @@ const getPreviewImageBlobUrl = async (imageUrl: string): Promise => { url: imageUrl, method: 'get', responseType: 'blob', - timeout: 30000 // 30秒超时 + timeout: 300000 // 300秒超时 }); // 由于request响应拦截器对blob类型响应直接返回blob数据 diff --git a/web/src/components/Process/approvalRecord.vue b/web/src/components/Process/approvalRecord.vue index 2413f08b..b50af4a4 100644 --- a/web/src/components/Process/approvalRecord.vue +++ b/web/src/components/Process/approvalRecord.vue @@ -44,7 +44,7 @@ @@ -106,8 +106,9 @@ const getIds = async (ids: string | number) => { }; /** 下载按钮操作 */ -const handleDownload = (ossId: string) => { - proxy?.$download.oss(ossId); +const handleDownload = (ossId: string, originalName: string) => { + console.log(ossId); + proxy?.$download.secureOss(ossId, originalName); }; /** diff --git a/web/src/plugins/download.ts b/web/src/plugins/download.ts index 62410adf..d36d5034 100644 --- a/web/src/plugins/download.ts +++ b/web/src/plugins/download.ts @@ -5,6 +5,7 @@ import { blobValidate } from '@/utils/ruoyi'; import { LoadingInstance } from 'element-plus/es/components/loading/src/loading.mjs'; import { globalHeaders } from '@/utils/request'; import { getFileDownloadUrl } from '@/api/system/file'; +import request from '@/utils/request'; const baseURL = import.meta.env.VITE_APP_BASE_API; let downloadLoadingInstance: LoadingInstance; @@ -12,16 +13,23 @@ export default { async oss(ossId: string | number) { downloadLoadingInstance = ElLoading.service({ text: '正在获取文件访问链接,请稍候', background: 'rgba(0, 0, 0, 0.7)' }); try { - // 使用新的安全下载方法 - const { data: downloadUrl } = await getFileDownloadUrl(Number(ossId)); + // 使用新的安全下载方法,避免Number()转换导致的大整数精度丢失 + // const { data: downloadUrl } = await getFileDownloadUrl(String(ossId)); - // 使用fetch下载文件 - const response = await fetch(downloadUrl); - if (!response.ok) { - throw new Error('下载失败'); + const response = await request({ + url: "/resource/oss/download/" + ossId, + method: 'get', + responseType: 'blob', + timeout: 300000 // 300秒超时 + }); + // 由于axios响应拦截器对blob类型响应直接返回blob数据 + // 这里response直接就是blob对象,而不是包含data属性的响应对象 + const blob = response; + + // 验证响应数据 + if (!blob) { + throw new Error('响应数据为空'); } - - const blob = await response.blob(); const filename = response.headers.get('content-disposition')?.split('filename=')[1]?.replace(/"/g, '') || `file_${ossId}`; FileSaver.saveAs(blob, decodeURIComponent(filename)); @@ -35,18 +43,27 @@ export default { // 新增:安全下载文件方法 async secureOss(ossId: string | number, fileName?: string) { + console.log(ossId, fileName); downloadLoadingInstance = ElLoading.service({ text: '正在获取文件访问链接,请稍候', background: 'rgba(0, 0, 0, 0.7)' }); try { - // 获取临时下载URL - const { data: downloadUrl } = await getFileDownloadUrl(Number(ossId)); + // 获取临时下载URL,避免Number()转换导致的大整数精度丢失 + // const { data: downloadUrl } = await getFileDownloadUrl(String(ossId)); - // 使用fetch下载文件 - const response = await fetch(downloadUrl); - if (!response.ok) { - throw new Error('下载失败'); + const response = await request({ + url: "/resource/oss/download/" + ossId, + method: 'get', + responseType: 'blob', + timeout: 300000 // 300秒超时 + }); + // 由于axios响应拦截器对blob类型响应直接返回blob数据 + // 这里response直接就是blob对象,而不是包含data属性的响应对象 + const blob = response; + + // 验证响应数据 + if (!blob) { + throw new Error('响应数据为空'); } - const blob = await response.blob(); const finalFileName = fileName || response.headers.get('content-disposition')?.split('filename=')[1]?.replace(/"/g, '') || `file_${ossId}`; FileSaver.saveAs(blob, decodeURIComponent(finalFileName)); @@ -66,6 +83,7 @@ export default { method: 'get', url: url, responseType: 'blob', + timeout: 300000 ,// 300秒超时 headers: globalHeaders() }); const isBlob = blobValidate(res.data); diff --git a/web/src/views/hq/pm/repairOrder/index.vue b/web/src/views/hq/pm/repairOrder/index.vue index 454c5fbc..0a0e20c6 100644 --- a/web/src/views/hq/pm/repairOrder/index.vue +++ b/web/src/views/hq/pm/repairOrder/index.vue @@ -2743,7 +2743,7 @@ const downloadAttachment = async (attachment: any) => { url: '/resource/oss/download/' + attachment.filePath, method: 'get', responseType: 'blob', - timeout: 30000 // 30秒超时 + timeout: 300000 // 300秒超时 }); // 由于axios响应拦截器对blob类型响应直接返回blob数据 // 这里response直接就是blob对象,而不是包含data属性的响应对象 diff --git a/web/src/views/xg/xsjcxx/xsqj/index.vue b/web/src/views/xg/xsjcxx/xsqj/index.vue new file mode 100644 index 00000000..e53b0c7e --- /dev/null +++ b/web/src/views/xg/xsjcxx/xsqj/index.vue @@ -0,0 +1,236 @@ + + + diff --git a/web/src/views/xg/xsjcxx/xsqj/leaveEdit.vue b/web/src/views/xg/xsjcxx/xsqj/leaveEdit.vue new file mode 100644 index 00000000..6290715e --- /dev/null +++ b/web/src/views/xg/xsjcxx/xsqj/leaveEdit.vue @@ -0,0 +1,312 @@ + + +