# WPS 文档版本控制方案 ## 需求 在 fileId 不变的情况下,让 WPS 重新从服务器获取文件。 ## 解决方案 通过后端的文件信息接口返回不同的版本号,让 WPS 判断文件已更新,从而重新下载。 ## 工作原理 ### WPS 缓存判断机制 WPS 通过以下字段判断是否需要重新下载文件: 1. **version**(版本号) 2. **modify_time**(修改时间) 3. **size**(文件大小) 如果这些字段发生变化,WPS 会重新下载文件。 ### 流程 ``` 用户打开文档: ↓ WPS 调用:GET /v1/3rd/file/info?_w_fileid=28 ↓ 后端返回: { "file": { "id": "28", "name": "文档.pdf", "version": 1, // ← 版本号 "modify_time": 1735545600000, "size": 1024000 } } ↓ WPS 检查缓存: - 缓存中的版本号是 1 - 服务器返回的版本号是 1 - 版本号相同,使用缓存 ✅ 用户点击"结束编辑": ↓ 前端调用:PUT /wps/callback/v3/3rd/file/28/version ↓ 后端更新版本号:version = 2 ↓ 销毁 WPS 实例 用户重新打开文档: ↓ WPS 调用:GET /v1/3rd/file/info?_w_fileid=28 ↓ 后端返回: { "file": { "id": "28", "name": "文档.pdf", "version": 2, // ← 版本号已更新 "modify_time": 1735545700000, "size": 1024000 } } ↓ WPS 检查缓存: - 缓存中的版本号是 1 - 服务器返回的版本号是 2 - 版本号不同,重新下载 ✅ ``` ## 前端实现 ### 1. 新增 API 接口 **文件**:`src/api/system/signature.ts` ```typescript /** * 通知后端更新文档版本(用于强制 WPS 刷新) * @param documentId 文档 ID */ export function updateDocumentVersion(documentId: number | string): AxiosPromise { return request({ url: `/wps/callback/v3/3rd/file/${documentId}/version`, method: 'put' }); } ``` ### 2. 修改"结束编辑"功能 **文件**:`src/components/DocumentAuditDialog/index.vue` ```typescript const handleEndEdit = async () => { // ... 确认对话框 ... try { // 1. 调用 WPS SDK 的销毁方法 if (wpsInstance.destroy) { await wpsInstance.destroy(); } // 2. 通知后端更新文档版本 try { await updateDocumentVersion(props.document.id); console.log('[WPS] 文档版本已更新'); } catch (versionErr) { console.warn('[WPS] 更新文档版本失败(不影响主流程):', versionErr); } // 3. 清空实例 wpsInstance = null; // 4. 显示成功提示并关闭对话框 ElMessage.success('编辑已结束,文档已销毁'); setTimeout(() => { dialogVisible.value = false; }, 1000); } catch (err) { // 错误处理... } }; ``` ## 后端实现 ### 1. 数据库表结构 需要在文档表中添加版本号字段: ```sql ALTER TABLE document ADD COLUMN version INT DEFAULT 1; ``` 或者使用修改时间: ```sql ALTER TABLE document ADD COLUMN modify_time BIGINT; ``` ### 2. 更新版本接口 **接口地址**:`PUT /wps/callback/v3/3rd/file/{documentId}/version` **功能**:更新文档版本号 **实现示例**: ```java @PutMapping("/wps/callback/v3/3rd/file/{documentId}/version") public R updateDocumentVersion(@PathVariable Long documentId) { // 1. 获取文档 Document doc = documentService.getById(documentId); if (doc == null) { return R.fail("文档不存在"); } // 2. 更新版本号 doc.setVersion(doc.getVersion() + 1); doc.setModifyTime(System.currentTimeMillis()); documentService.updateById(doc); log.info("文档版本已更新: id={}, version={}", documentId, doc.getVersion()); return R.ok(); } ``` ### 3. 文件信息接口 **接口地址**:`GET /v1/3rd/file/info` **修改**:返回版本号和修改时间 **实现示例**: ```java @GetMapping("/v1/3rd/file/info") public R getFileInfo(@RequestParam("_w_fileid") String fileId) { // 1. 获取文档 Document doc = documentService.getById(fileId); if (doc == null) { return R.fail("文档不存在"); } // 2. 构建文件信息 FileInfo fileInfo = new FileInfo(); fileInfo.setId(doc.getId().toString()); fileInfo.setName(doc.getFileName()); fileInfo.setVersion(doc.getVersion()); // ← 版本号 fileInfo.setModifyTime(doc.getModifyTime()); // ← 修改时间 fileInfo.setSize(doc.getFileSize()); fileInfo.setDownloadUrl(buildDownloadUrl(doc.getOssId())); return R.ok(fileInfo); } ``` **返回格式**: ```json { "code": 200, "msg": "success", "data": { "file": { "id": "28", "name": "文档.pdf", "version": 2, "modify_time": 1735545700000, "size": 1024000, "download_url": "http://your-server.com/resource/oss/downloadWithoutPermission/123" } } } ``` ## 完整流程 ### 第一次打开文档 ``` 1. 用户打开文档 ↓ 2. WPS 调用:GET /v1/3rd/file/info?_w_fileid=28 ↓ 3. 后端返回:version=1, modify_time=1735545600000 ↓ 4. WPS 缓存中没有该文档 ↓ 5. WPS 下载文件:GET /resource/oss/downloadWithoutPermission/123 ↓ 6. WPS 缓存文件(version=1) ↓ 7. 显示文档 ``` ### 结束编辑 ``` 1. 用户点击"结束编辑" ↓ 2. 前端调用:wpsInstance.destroy() ↓ 3. 前端调用:PUT /wps/callback/v3/3rd/file/28/version ↓ 4. 后端更新:version=2, modify_time=1735545700000 ↓ 5. 前端清空实例:wpsInstance = null ↓ 6. 关闭对话框 ``` ### 重新打开文档 ``` 1. 用户重新打开文档 ↓ 2. WPS 调用:GET /v1/3rd/file/info?_w_fileid=28 ↓ 3. 后端返回:version=2, modify_time=1735545700000 ↓ 4. WPS 检查缓存: - 缓存版本:version=1 - 服务器版本:version=2 - 版本不同! ↓ 5. WPS 重新下载文件:GET /resource/oss/downloadWithoutPermission/123 ↓ 6. WPS 更新缓存(version=2) ↓ 7. 显示最新文档 ✅ ``` ## 版本号策略 ### 方案 1:递增版本号(推荐) ```java // 每次更新时递增 doc.setVersion(doc.getVersion() + 1); ``` **优势**: - ✅ 简单明了 - ✅ 易于理解 - ✅ 便于调试 **示例**: ``` 初始:version = 1 第一次更新:version = 2 第二次更新:version = 3 ``` ### 方案 2:使用时间戳 ```java // 使用当前时间戳作为版本号 doc.setVersion(System.currentTimeMillis()); ``` **优势**: - ✅ 自动递增 - ✅ 包含时间信息 - ✅ 不会重复 **示例**: ``` 初始:version = 1735545600000 第一次更新:version = 1735545700000 第二次更新:version = 1735545800000 ``` ### 方案 3:使用修改时间 ```java // 只更新 modify_time,不使用 version doc.setModifyTime(System.currentTimeMillis()); ``` **优势**: - ✅ 不需要额外字段 - ✅ 符合语义 - ✅ WPS 也会检查此字段 **注意**: - WPS 会同时检查 `version` 和 `modify_time` - 任一字段变化都会触发重新下载 ## 调试日志 ### 前端日志 ``` [WPS] 开始结束编辑,文档 ID: 28 [WPS] 调用 destroy 方法 [WPS] destroy 方法调用成功 [WPS] 通知后端更新文档版本 [WPS] 文档版本已更新 [WPS] 结束编辑成功 ``` ### 后端日志 ``` [INFO] 收到更新版本请求: documentId=28 [INFO] 当前版本: 1 [INFO] 更新后版本: 2 [INFO] 文档版本已更新: id=28, version=2 ``` ### WPS 日志 ``` [WPS] 获取文件信息: fileId=28 [WPS] 服务器版本: 2 [WPS] 缓存版本: 1 [WPS] 版本不同,重新下载文件 [WPS] 下载完成,更新缓存 ``` ## 测试场景 ### 场景 1:正常流程 ``` 1. 打开文档(version=1) 2. 编辑文档 3. 点击"结束编辑"(version 更新为 2) 4. 重新打开文档 5. 预期:WPS 重新下载文件 ``` ### 场景 2:多次编辑 ``` 1. 打开文档(version=1) 2. 结束编辑(version=2) 3. 重新打开(version=2) 4. 结束编辑(version=3) 5. 重新打开(version=3) 6. 预期:每次都重新下载 ``` ### 场景 3:版本更新失败 ``` 1. 打开文档(version=1) 2. 点击"结束编辑" 3. 版本更新接口失败(version 仍为 1) 4. 重新打开文档 5. 预期:WPS 使用缓存(因为版本号未变) ``` ## 常见问题 ### Q1: 为什么要更新版本号? **答**:让 WPS 知道文件已更新,需要重新下载。 ### Q2: 版本号必须递增吗? **答**:不必须,只要每次不同即可。但递增更易于理解和调试。 ### Q3: 如果版本更新失败怎么办? **答**:不影响主流程,只是下次打开时 WPS 会使用缓存。 ### Q4: 可以手动触发刷新吗? **答**:可以,调用版本更新接口即可。 ### Q5: 版本号会一直增长吗? **答**:是的,但这不是问题。可以定期重置或使用时间戳。 ### Q6: 需要修改数据库吗? **答**:是的,需要添加 `version` 字段或使用 `modify_time` 字段。 ## 总结 ### 核心思路 - ✅ fileId 保持不变 - ✅ 通过版本号控制缓存 - ✅ 结束编辑时更新版本号 - ✅ 重新打开时 WPS 检测到版本变化 - ✅ WPS 重新下载文件 ### 前端改动 1. 新增 `updateDocumentVersion` API 2. 在"结束编辑"时调用该 API ### 后端改动 1. 添加 `version` 字段到数据库 2. 实现版本更新接口 3. 在文件信息接口中返回版本号 ### 优势 - ✅ fileId 不变,后端逻辑简单 - ✅ 符合 WPS 的缓存机制 - ✅ 可控性强 - ✅ 易于调试 现在点击"结束编辑"后,重新打开文档时 WPS 会从你的服务器重新获取最新文件!