save.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. import request from '@/utils/request';
  2. import CryptoJS from 'crypto-js';
  3. /**
  4. * WPS 三阶段保存接口实现
  5. *
  6. * 注意:当前实现包含前端模拟版本,用于开发和测试
  7. * 生产环境应该使用真实的后端接口
  8. */
  9. // 是否使用前端模拟(开发模式)
  10. const USE_MOCK = true; // 设置为 false 使用真实后端接口
  11. // 文档信息接口
  12. export interface FileInfo {
  13. id: string;
  14. name: string;
  15. version: number;
  16. size: number;
  17. create_time: number;
  18. modify_time: number;
  19. creator_id: string;
  20. modifier_id: string;
  21. }
  22. // 准备上传参数
  23. export interface PrepareUploadParams {
  24. file_id: string;
  25. }
  26. // 准备上传返回
  27. export interface PrepareUploadResponse {
  28. digest_types: string[];
  29. }
  30. // 获取上传地址参数
  31. export interface GetUploadAddressParams {
  32. file_id: string;
  33. name: string;
  34. size: number;
  35. digest: Record<string, string>;
  36. is_manual: boolean;
  37. attachment_size?: number;
  38. content_type?: string;
  39. }
  40. // 获取上传地址返回
  41. export interface GetUploadAddressResponse {
  42. url: string;
  43. method: string;
  44. headers?: Record<string, string>;
  45. params?: Record<string, string>;
  46. send_back_params?: Record<string, string>;
  47. }
  48. // 上传完成参数
  49. export interface UploadCompleteParams {
  50. file_id: string;
  51. request: GetUploadAddressParams;
  52. response: {
  53. status_code: number;
  54. headers?: Record<string, string>;
  55. body?: string; // base64 编码
  56. };
  57. send_back_params?: Record<string, string>;
  58. }
  59. /**
  60. * 第一阶段:准备上传
  61. * 协商摘要算法
  62. */
  63. export const prepareUpload = (params: PrepareUploadParams): Promise<PrepareUploadResponse> => {
  64. if (USE_MOCK) {
  65. // 前端模拟实现
  66. return Promise.resolve({
  67. digest_types: ['sha1', 'md5', 'sha256']
  68. });
  69. }
  70. return request({
  71. url: `/v3/3rd/files/${params.file_id}/upload/prepare`,
  72. method: 'get'
  73. });
  74. };
  75. /**
  76. * 第二阶段:获取上传地址
  77. * 获取文件上传的目标地址
  78. */
  79. export const getUploadAddress = (params: GetUploadAddressParams): Promise<GetUploadAddressResponse> => {
  80. if (USE_MOCK) {
  81. // 前端模拟实现 - 使用 Blob URL
  82. return Promise.resolve({
  83. url: 'mock://upload', // 模拟上传地址
  84. method: 'PUT',
  85. headers: {},
  86. params: {},
  87. send_back_params: {
  88. file_id: params.file_id,
  89. timestamp: Date.now().toString()
  90. }
  91. });
  92. }
  93. return request({
  94. url: `/v3/3rd/files/${params.file_id}/upload/address`,
  95. method: 'post',
  96. data: {
  97. name: params.name,
  98. size: params.size,
  99. digest: params.digest,
  100. is_manual: params.is_manual,
  101. attachment_size: params.attachment_size,
  102. content_type: params.content_type
  103. }
  104. });
  105. };
  106. /**
  107. * 第三阶段:上传完成通知
  108. * 通知接入方上传已完成
  109. */
  110. export const uploadComplete = (params: UploadCompleteParams): Promise<FileInfo> => {
  111. if (USE_MOCK) {
  112. // 前端模拟实现 - 使用 localStorage 存储文件信息
  113. const fileId = params.file_id;
  114. const currentTime = Math.floor(Date.now() / 1000);
  115. // 从 localStorage 获取或创建文件记录
  116. const storageKey = `wps_file_${fileId}`;
  117. let fileRecord: any = null;
  118. try {
  119. const stored = localStorage.getItem(storageKey);
  120. if (stored) {
  121. fileRecord = JSON.parse(stored);
  122. }
  123. } catch (err) {
  124. console.warn('读取文件记录失败:', err);
  125. }
  126. // 创建或更新文件信息
  127. const version = fileRecord ? fileRecord.version + 1 : 1;
  128. const fileInfo: FileInfo = {
  129. id: fileId,
  130. name: params.request.name,
  131. version: version,
  132. size: params.request.size,
  133. create_time: fileRecord ? fileRecord.create_time : currentTime,
  134. modify_time: currentTime,
  135. creator_id: fileRecord ? fileRecord.creator_id : 'user_' + Date.now(),
  136. modifier_id: 'user_' + Date.now()
  137. };
  138. // 保存到 localStorage
  139. try {
  140. localStorage.setItem(storageKey, JSON.stringify(fileInfo));
  141. console.log('[前端模拟] 文件信息已保存到 localStorage:', fileInfo);
  142. } catch (err) {
  143. console.error('[前端模拟] 保存文件信息失败:', err);
  144. }
  145. return Promise.resolve(fileInfo);
  146. }
  147. return request({
  148. url: `/v3/3rd/files/${params.file_id}/upload/complete`,
  149. method: 'post',
  150. data: {
  151. request: params.request,
  152. response: params.response,
  153. send_back_params: params.send_back_params
  154. }
  155. });
  156. };
  157. /**
  158. * 计算文件摘要
  159. * @param file 文件 Blob 或 ArrayBuffer
  160. * @param algorithm 算法类型 md5/sha1/sha256
  161. */
  162. export const calculateDigest = async (file: Blob | ArrayBuffer, algorithm: 'md5' | 'sha1' | 'sha256'): Promise<string> => {
  163. let arrayBuffer: ArrayBuffer;
  164. if (file instanceof Blob) {
  165. arrayBuffer = await file.arrayBuffer();
  166. } else {
  167. arrayBuffer = file;
  168. }
  169. // 转换为 WordArray
  170. const wordArray = CryptoJS.lib.WordArray.create(arrayBuffer as any);
  171. // 计算摘要
  172. let hash: any;
  173. switch (algorithm) {
  174. case 'md5':
  175. hash = CryptoJS.MD5(wordArray);
  176. break;
  177. case 'sha1':
  178. hash = CryptoJS.SHA1(wordArray);
  179. break;
  180. case 'sha256':
  181. hash = CryptoJS.SHA256(wordArray);
  182. break;
  183. default:
  184. throw new Error(`不支持的算法: ${algorithm}`);
  185. }
  186. return hash.toString();
  187. };
  188. /**
  189. * 完整的三阶段保存流程
  190. * @param fileId 文件ID
  191. * @param fileName 文件名
  192. * @param fileBlob 文件内容
  193. * @param isManual 是否手动保存
  194. */
  195. export const saveFileThreePhase = async (fileId: string, fileName: string, fileBlob: Blob, isManual: boolean = false): Promise<FileInfo> => {
  196. try {
  197. // 第一阶段:准备上传,协商摘要算法
  198. console.log('[WPS保存] 第一阶段:准备上传');
  199. const prepareResult = await prepareUpload({ file_id: fileId });
  200. const digestTypes = prepareResult.digest_types || ['sha1'];
  201. console.log('[WPS保存] 支持的摘要算法:', digestTypes);
  202. // 计算文件摘要
  203. console.log('[WPS保存] 计算文件摘要...');
  204. const digest: Record<string, string> = {};
  205. for (const type of digestTypes) {
  206. if (['md5', 'sha1', 'sha256'].includes(type)) {
  207. digest[type] = await calculateDigest(fileBlob, type as any);
  208. }
  209. }
  210. console.log('[WPS保存] 文件摘要:', digest);
  211. // 第二阶段:获取上传地址
  212. console.log('[WPS保存] 第二阶段:获取上传地址');
  213. const uploadAddressParams: GetUploadAddressParams = {
  214. file_id: fileId,
  215. name: fileName,
  216. size: fileBlob.size,
  217. digest: digest,
  218. is_manual: isManual,
  219. content_type: fileBlob.type
  220. };
  221. const addressResult = await getUploadAddress(uploadAddressParams);
  222. console.log('[WPS保存] 上传地址:', addressResult.url);
  223. // 上传文件到指定地址
  224. console.log('[WPS保存] 上传文件...');
  225. let uploadResponse: Response;
  226. if (USE_MOCK && addressResult.url === 'mock://upload') {
  227. // 前端模拟 - 不实际上传,直接模拟响应
  228. console.log('[前端模拟] 跳过实际上传,使用模拟响应');
  229. uploadResponse = new Response(null, {
  230. status: 200,
  231. statusText: 'OK',
  232. headers: new Headers({
  233. 'content-type': 'application/json',
  234. 'x-mock': 'true'
  235. })
  236. });
  237. // 可选:将文件保存到 IndexedDB(用于更持久的存储)
  238. try {
  239. await saveFileToIndexedDB(fileId, fileBlob);
  240. console.log('[前端模拟] 文件已保存到 IndexedDB');
  241. } catch (err) {
  242. console.warn('[前端模拟] 保存到 IndexedDB 失败:', err);
  243. }
  244. } else {
  245. // 真实上传
  246. uploadResponse = await fetch(addressResult.url, {
  247. method: addressResult.method || 'PUT',
  248. headers: addressResult.headers || {},
  249. body: fileBlob
  250. });
  251. }
  252. console.log('[WPS保存] 上传响应状态:', uploadResponse.status);
  253. // 获取响应头
  254. const responseHeaders: Record<string, string> = {};
  255. uploadResponse.headers.forEach((value, key) => {
  256. responseHeaders[key] = value;
  257. });
  258. // 获取响应体(如果有)
  259. let responseBody: string | undefined;
  260. try {
  261. const bodyText = await uploadResponse.text();
  262. if (bodyText) {
  263. responseBody = btoa(bodyText); // base64 编码
  264. }
  265. } catch (err) {
  266. console.warn('[WPS保存] 无法读取响应体:', err);
  267. }
  268. // 第三阶段:上传完成通知
  269. console.log('[WPS保存] 第三阶段:上传完成通知');
  270. const completeParams: UploadCompleteParams = {
  271. file_id: fileId,
  272. request: uploadAddressParams,
  273. response: {
  274. status_code: uploadResponse.status,
  275. headers: responseHeaders,
  276. body: responseBody
  277. },
  278. send_back_params: addressResult.send_back_params
  279. };
  280. const fileInfo = await uploadComplete(completeParams);
  281. console.log('[WPS保存] 保存完成,文件信息:', fileInfo);
  282. return fileInfo;
  283. } catch (error) {
  284. console.error('[WPS保存] 保存失败:', error);
  285. throw error;
  286. }
  287. };
  288. /**
  289. * 前端模拟:将文件保存到 IndexedDB
  290. * 用于更持久的本地存储
  291. */
  292. const saveFileToIndexedDB = (fileId: string, fileBlob: Blob): Promise<void> => {
  293. return new Promise((resolve, reject) => {
  294. const dbName = 'WPS_Files';
  295. const storeName = 'files';
  296. const request = indexedDB.open(dbName, 1);
  297. request.onerror = () => {
  298. reject(new Error('无法打开 IndexedDB'));
  299. };
  300. request.onsuccess = (event) => {
  301. const db = (event.target as IDBOpenDBRequest).result;
  302. const transaction = db.transaction([storeName], 'readwrite');
  303. const store = transaction.objectStore(storeName);
  304. const fileRecord = {
  305. id: fileId,
  306. blob: fileBlob,
  307. timestamp: Date.now()
  308. };
  309. const putRequest = store.put(fileRecord);
  310. putRequest.onsuccess = () => {
  311. resolve();
  312. };
  313. putRequest.onerror = () => {
  314. reject(new Error('保存文件到 IndexedDB 失败'));
  315. };
  316. };
  317. request.onupgradeneeded = (event) => {
  318. const db = (event.target as IDBOpenDBRequest).result;
  319. if (!db.objectStoreNames.contains(storeName)) {
  320. db.createObjectStore(storeName, { keyPath: 'id' });
  321. }
  322. };
  323. });
  324. };
  325. /**
  326. * 前端模拟:从 IndexedDB 读取文件
  327. */
  328. export const getFileFromIndexedDB = (fileId: string): Promise<Blob | null> => {
  329. return new Promise((resolve, reject) => {
  330. const dbName = 'WPS_Files';
  331. const storeName = 'files';
  332. const request = indexedDB.open(dbName, 1);
  333. request.onerror = () => {
  334. reject(new Error('无法打开 IndexedDB'));
  335. };
  336. request.onsuccess = (event) => {
  337. const db = (event.target as IDBOpenDBRequest).result;
  338. const transaction = db.transaction([storeName], 'readonly');
  339. const store = transaction.objectStore(storeName);
  340. const getRequest = store.get(fileId);
  341. getRequest.onsuccess = () => {
  342. const result = getRequest.result;
  343. resolve(result ? result.blob : null);
  344. };
  345. getRequest.onerror = () => {
  346. reject(new Error('读取文件失败'));
  347. };
  348. };
  349. request.onupgradeneeded = (event) => {
  350. const db = (event.target as IDBOpenDBRequest).result;
  351. if (!db.objectStoreNames.contains(storeName)) {
  352. db.createObjectStore(storeName, { keyPath: 'id' });
  353. }
  354. };
  355. });
  356. };
  357. /**
  358. * 前端模拟:清除所有保存的文件
  359. */
  360. export const clearAllFiles = (): Promise<void> => {
  361. return new Promise((resolve, reject) => {
  362. // 清除 localStorage
  363. const keys = Object.keys(localStorage);
  364. keys.forEach((key) => {
  365. if (key.startsWith('wps_file_')) {
  366. localStorage.removeItem(key);
  367. }
  368. });
  369. // 清除 IndexedDB
  370. const dbName = 'WPS_Files';
  371. const request = indexedDB.deleteDatabase(dbName);
  372. request.onsuccess = () => {
  373. console.log('[前端模拟] 所有文件已清除');
  374. resolve();
  375. };
  376. request.onerror = () => {
  377. reject(new Error('清除 IndexedDB 失败'));
  378. };
  379. });
  380. };
  381. /**
  382. * 清空文档批注
  383. * @param documentId 文档ID
  384. */
  385. export const cleanDocumentComments = (documentId: string | number): Promise<any> => {
  386. return request({
  387. url: `/wps/callback/v3/3rd/clean/${documentId}`,
  388. method: 'put'
  389. });
  390. };
  391. /**
  392. * 获取文档历史版本列表
  393. * @param ossId OSS文件ID
  394. */
  395. export interface FileVersion {
  396. version: number;
  397. url: string;
  398. createTime: number;
  399. updateTime: number;
  400. }
  401. export const getFileVersionList = (ossId: string | number): Promise<{ data: FileVersion[] }> => {
  402. return request({
  403. url: '/wps/callback/v3/3rd/files/list',
  404. method: 'get',
  405. params: { ossId }
  406. });
  407. };
  408. /**
  409. * 初始化 WPS 文档
  410. * @param ossId OSS文件ID
  411. * @returns 返回当前版本号
  412. */
  413. export const initWpsDocument = (ossId: string | number): Promise<{ data: number }> => {
  414. return request({
  415. url: `/wps/callback/v3/3rd/init/${ossId}`,
  416. method: 'post'
  417. });
  418. };
  419. /**
  420. * 取消 WPS 文档编辑
  421. * @param ossId OSS文件ID
  422. */
  423. export const cancelWpsDocument = (ossId: string | number): Promise<any> => {
  424. return request({
  425. url: `/wps/callback/v3/3rd/cancel/${ossId}`,
  426. method: 'delete'
  427. });
  428. };
  429. /**
  430. * 获取最终文档信息
  431. * @param fileId 文件ID(格式:ossId_version)
  432. */
  433. export interface FinalFileInfo {
  434. ossId: number;
  435. fileName: string;
  436. originalName: string;
  437. fileSuffix: string;
  438. url: string;
  439. ext1: string;
  440. createTime: string;
  441. createBy: number;
  442. createByName: string;
  443. service: string;
  444. updateTime: string;
  445. }
  446. export const getFinalFile = (fileId: string): Promise<{ data: FinalFileInfo }> => {
  447. return request({
  448. url: '/wps/callback/v3/3rd/getFinal',
  449. method: 'get',
  450. params: { id: fileId }
  451. });
  452. };