import request from '@/utils/request'; import CryptoJS from 'crypto-js'; /** * WPS 三阶段保存接口实现 * * 注意:当前实现包含前端模拟版本,用于开发和测试 * 生产环境应该使用真实的后端接口 */ // 是否使用前端模拟(开发模式) const USE_MOCK = true; // 设置为 false 使用真实后端接口 // 文档信息接口 export interface FileInfo { id: string; name: string; version: number; size: number; create_time: number; modify_time: number; creator_id: string; modifier_id: string; } // 准备上传参数 export interface PrepareUploadParams { file_id: string; } // 准备上传返回 export interface PrepareUploadResponse { digest_types: string[]; } // 获取上传地址参数 export interface GetUploadAddressParams { file_id: string; name: string; size: number; digest: Record; is_manual: boolean; attachment_size?: number; content_type?: string; } // 获取上传地址返回 export interface GetUploadAddressResponse { url: string; method: string; headers?: Record; params?: Record; send_back_params?: Record; } // 上传完成参数 export interface UploadCompleteParams { file_id: string; request: GetUploadAddressParams; response: { status_code: number; headers?: Record; body?: string; // base64 编码 }; send_back_params?: Record; } /** * 第一阶段:准备上传 * 协商摘要算法 */ export const prepareUpload = (params: PrepareUploadParams): Promise => { if (USE_MOCK) { // 前端模拟实现 return Promise.resolve({ digest_types: ['sha1', 'md5', 'sha256'] }); } return request({ url: `/v3/3rd/files/${params.file_id}/upload/prepare`, method: 'get' }); }; /** * 第二阶段:获取上传地址 * 获取文件上传的目标地址 */ export const getUploadAddress = (params: GetUploadAddressParams): Promise => { if (USE_MOCK) { // 前端模拟实现 - 使用 Blob URL return Promise.resolve({ url: 'mock://upload', // 模拟上传地址 method: 'PUT', headers: {}, params: {}, send_back_params: { file_id: params.file_id, timestamp: Date.now().toString() } }); } return request({ url: `/v3/3rd/files/${params.file_id}/upload/address`, method: 'post', data: { name: params.name, size: params.size, digest: params.digest, is_manual: params.is_manual, attachment_size: params.attachment_size, content_type: params.content_type } }); }; /** * 第三阶段:上传完成通知 * 通知接入方上传已完成 */ export const uploadComplete = (params: UploadCompleteParams): Promise => { if (USE_MOCK) { // 前端模拟实现 - 使用 localStorage 存储文件信息 const fileId = params.file_id; const currentTime = Math.floor(Date.now() / 1000); // 从 localStorage 获取或创建文件记录 const storageKey = `wps_file_${fileId}`; let fileRecord: any = null; try { const stored = localStorage.getItem(storageKey); if (stored) { fileRecord = JSON.parse(stored); } } catch (err) { console.warn('读取文件记录失败:', err); } // 创建或更新文件信息 const version = fileRecord ? fileRecord.version + 1 : 1; const fileInfo: FileInfo = { id: fileId, name: params.request.name, version: version, size: params.request.size, create_time: fileRecord ? fileRecord.create_time : currentTime, modify_time: currentTime, creator_id: fileRecord ? fileRecord.creator_id : 'user_' + Date.now(), modifier_id: 'user_' + Date.now() }; // 保存到 localStorage try { localStorage.setItem(storageKey, JSON.stringify(fileInfo)); console.log('[前端模拟] 文件信息已保存到 localStorage:', fileInfo); } catch (err) { console.error('[前端模拟] 保存文件信息失败:', err); } return Promise.resolve(fileInfo); } return request({ url: `/v3/3rd/files/${params.file_id}/upload/complete`, method: 'post', data: { request: params.request, response: params.response, send_back_params: params.send_back_params } }); }; /** * 计算文件摘要 * @param file 文件 Blob 或 ArrayBuffer * @param algorithm 算法类型 md5/sha1/sha256 */ export const calculateDigest = async (file: Blob | ArrayBuffer, algorithm: 'md5' | 'sha1' | 'sha256'): Promise => { let arrayBuffer: ArrayBuffer; if (file instanceof Blob) { arrayBuffer = await file.arrayBuffer(); } else { arrayBuffer = file; } // 转换为 WordArray const wordArray = CryptoJS.lib.WordArray.create(arrayBuffer as any); // 计算摘要 let hash: any; switch (algorithm) { case 'md5': hash = CryptoJS.MD5(wordArray); break; case 'sha1': hash = CryptoJS.SHA1(wordArray); break; case 'sha256': hash = CryptoJS.SHA256(wordArray); break; default: throw new Error(`不支持的算法: ${algorithm}`); } return hash.toString(); }; /** * 完整的三阶段保存流程 * @param fileId 文件ID * @param fileName 文件名 * @param fileBlob 文件内容 * @param isManual 是否手动保存 */ export const saveFileThreePhase = async (fileId: string, fileName: string, fileBlob: Blob, isManual: boolean = false): Promise => { try { // 第一阶段:准备上传,协商摘要算法 console.log('[WPS保存] 第一阶段:准备上传'); const prepareResult = await prepareUpload({ file_id: fileId }); const digestTypes = prepareResult.digest_types || ['sha1']; console.log('[WPS保存] 支持的摘要算法:', digestTypes); // 计算文件摘要 console.log('[WPS保存] 计算文件摘要...'); const digest: Record = {}; for (const type of digestTypes) { if (['md5', 'sha1', 'sha256'].includes(type)) { digest[type] = await calculateDigest(fileBlob, type as any); } } console.log('[WPS保存] 文件摘要:', digest); // 第二阶段:获取上传地址 console.log('[WPS保存] 第二阶段:获取上传地址'); const uploadAddressParams: GetUploadAddressParams = { file_id: fileId, name: fileName, size: fileBlob.size, digest: digest, is_manual: isManual, content_type: fileBlob.type }; const addressResult = await getUploadAddress(uploadAddressParams); console.log('[WPS保存] 上传地址:', addressResult.url); // 上传文件到指定地址 console.log('[WPS保存] 上传文件...'); let uploadResponse: Response; if (USE_MOCK && addressResult.url === 'mock://upload') { // 前端模拟 - 不实际上传,直接模拟响应 console.log('[前端模拟] 跳过实际上传,使用模拟响应'); uploadResponse = new Response(null, { status: 200, statusText: 'OK', headers: new Headers({ 'content-type': 'application/json', 'x-mock': 'true' }) }); // 可选:将文件保存到 IndexedDB(用于更持久的存储) try { await saveFileToIndexedDB(fileId, fileBlob); console.log('[前端模拟] 文件已保存到 IndexedDB'); } catch (err) { console.warn('[前端模拟] 保存到 IndexedDB 失败:', err); } } else { // 真实上传 uploadResponse = await fetch(addressResult.url, { method: addressResult.method || 'PUT', headers: addressResult.headers || {}, body: fileBlob }); } console.log('[WPS保存] 上传响应状态:', uploadResponse.status); // 获取响应头 const responseHeaders: Record = {}; uploadResponse.headers.forEach((value, key) => { responseHeaders[key] = value; }); // 获取响应体(如果有) let responseBody: string | undefined; try { const bodyText = await uploadResponse.text(); if (bodyText) { responseBody = btoa(bodyText); // base64 编码 } } catch (err) { console.warn('[WPS保存] 无法读取响应体:', err); } // 第三阶段:上传完成通知 console.log('[WPS保存] 第三阶段:上传完成通知'); const completeParams: UploadCompleteParams = { file_id: fileId, request: uploadAddressParams, response: { status_code: uploadResponse.status, headers: responseHeaders, body: responseBody }, send_back_params: addressResult.send_back_params }; const fileInfo = await uploadComplete(completeParams); console.log('[WPS保存] 保存完成,文件信息:', fileInfo); return fileInfo; } catch (error) { console.error('[WPS保存] 保存失败:', error); throw error; } }; /** * 前端模拟:将文件保存到 IndexedDB * 用于更持久的本地存储 */ const saveFileToIndexedDB = (fileId: string, fileBlob: Blob): Promise => { return new Promise((resolve, reject) => { const dbName = 'WPS_Files'; const storeName = 'files'; const request = indexedDB.open(dbName, 1); request.onerror = () => { reject(new Error('无法打开 IndexedDB')); }; request.onsuccess = (event) => { const db = (event.target as IDBOpenDBRequest).result; const transaction = db.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); const fileRecord = { id: fileId, blob: fileBlob, timestamp: Date.now() }; const putRequest = store.put(fileRecord); putRequest.onsuccess = () => { resolve(); }; putRequest.onerror = () => { reject(new Error('保存文件到 IndexedDB 失败')); }; }; request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName, { keyPath: 'id' }); } }; }); }; /** * 前端模拟:从 IndexedDB 读取文件 */ export const getFileFromIndexedDB = (fileId: string): Promise => { return new Promise((resolve, reject) => { const dbName = 'WPS_Files'; const storeName = 'files'; const request = indexedDB.open(dbName, 1); request.onerror = () => { reject(new Error('无法打开 IndexedDB')); }; request.onsuccess = (event) => { const db = (event.target as IDBOpenDBRequest).result; const transaction = db.transaction([storeName], 'readonly'); const store = transaction.objectStore(storeName); const getRequest = store.get(fileId); getRequest.onsuccess = () => { const result = getRequest.result; resolve(result ? result.blob : null); }; getRequest.onerror = () => { reject(new Error('读取文件失败')); }; }; request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName, { keyPath: 'id' }); } }; }); }; /** * 前端模拟:清除所有保存的文件 */ export const clearAllFiles = (): Promise => { return new Promise((resolve, reject) => { // 清除 localStorage const keys = Object.keys(localStorage); keys.forEach((key) => { if (key.startsWith('wps_file_')) { localStorage.removeItem(key); } }); // 清除 IndexedDB const dbName = 'WPS_Files'; const request = indexedDB.deleteDatabase(dbName); request.onsuccess = () => { console.log('[前端模拟] 所有文件已清除'); resolve(); }; request.onerror = () => { reject(new Error('清除 IndexedDB 失败')); }; }); }; /** * 清空文档批注 * @param documentId 文档ID */ export const cleanDocumentComments = (documentId: string | number): Promise => { return request({ url: `/wps/callback/v3/3rd/clean/${documentId}`, method: 'put' }); }; /** * 获取文档历史版本列表 * @param ossId OSS文件ID */ export interface FileVersion { version: number; url: string; createTime: number; updateTime: number; } export const getFileVersionList = (ossId: string | number): Promise<{ data: FileVersion[] }> => { return request({ url: '/wps/callback/v3/3rd/files/list', method: 'get', params: { ossId } }); }; /** * 初始化 WPS 文档 * @param ossId OSS文件ID * @returns 返回当前版本号 */ export const initWpsDocument = (ossId: string | number): Promise<{ data: number }> => { return request({ url: `/wps/callback/v3/3rd/init/${ossId}`, method: 'post' }); }; /** * 取消 WPS 文档编辑 * @param ossId OSS文件ID */ export const cancelWpsDocument = (ossId: string | number): Promise => { return request({ url: `/wps/callback/v3/3rd/cancel/${ossId}`, method: 'delete' }); }; /** * 获取最终文档信息 * @param fileId 文件ID(格式:ossId_version) */ export interface FinalFileInfo { ossId: number; fileName: string; originalName: string; fileSuffix: string; url: string; ext1: string; createTime: string; createBy: number; createByName: string; service: string; updateTime: string; } export const getFinalFile = (fileId: string): Promise<{ data: FinalFileInfo }> => { return request({ url: '/wps/callback/v3/3rd/getFinal', method: 'get', params: { id: fileId } }); };