exportRecordService.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. /**
  2. * Export Record Service Module
  3. *
  4. * Provides methods for export record operations:
  5. * - listExportRecords: List export records with pagination
  6. * - downloadExportRecord: Download an exported file
  7. * - deleteExportRecord: Delete an export record
  8. * - getAdminStorage: Get storage usage info (admin only)
  9. *
  10. * All methods use the configured API client and provide error normalization.
  11. *
  12. * @module services/exportRecordService
  13. */
  14. import apiClient, { getErrorMessage } from './api';
  15. import type { ApiResponse } from '../types/api';
  16. import type {
  17. ExportRecord,
  18. ListExportRecordsResponse,
  19. ExportRecordListFilters,
  20. StorageInfo,
  21. } from '../types/export';
  22. import {
  23. downloadBlob,
  24. encodeExportRecordId,
  25. getFileNameFromContentDisposition,
  26. } from '../utils/download';
  27. function isValidExportRecord(value: unknown): value is ExportRecord {
  28. if (!value || typeof value !== 'object') return false;
  29. const record = value as Partial<ExportRecord>;
  30. return (
  31. typeof record.recordId === 'string' &&
  32. record.recordId.length > 0 &&
  33. typeof record.fileName === 'string' &&
  34. typeof record.downloadUrl === 'string' &&
  35. typeof record.userId === 'string' &&
  36. typeof record.styleId === 'string' &&
  37. typeof record.fileSize === 'number' &&
  38. Number.isFinite(record.fileSize) &&
  39. record.fileSize >= 0 &&
  40. typeof record.createdAt === 'number' &&
  41. Number.isFinite(record.createdAt)
  42. );
  43. }
  44. /**
  45. * List export records with pagination
  46. *
  47. * @param filters - Filters for listing export records (userId is required)
  48. * @returns List of export records and pagination information
  49. * @throws {Error} When the API request fails
  50. */
  51. export const listExportRecords = async (
  52. filters: ExportRecordListFilters
  53. ): Promise<ListExportRecordsResponse> => {
  54. try {
  55. // Build query parameters
  56. const params: Record<string, string | number> = {
  57. userId: filters.userId,
  58. page: filters.page || 1,
  59. pageSize: filters.pageSize || 20,
  60. sortOrder: filters.sortOrder || 'desc',
  61. };
  62. const response = await apiClient.get<ApiResponse<ListExportRecordsResponse>>(
  63. '/api/v1/export/records',
  64. { params }
  65. );
  66. const data = response.data.data;
  67. if (
  68. !data ||
  69. !Array.isArray(data.records) ||
  70. !data.records.every(isValidExportRecord) ||
  71. !data.pagination ||
  72. !Number.isInteger(data.pagination.total) ||
  73. data.pagination.total < 0
  74. ) {
  75. throw new Error('导出记录响应格式无效');
  76. }
  77. // Fix any incorrect base URLs in download URLs
  78. const correctBaseURL = import.meta.env.VITE_API_BASE_URL;
  79. if (correctBaseURL && data.records) {
  80. data.records = data.records.map((record) => {
  81. if (record.downloadUrl && record.downloadUrl.startsWith('http')) {
  82. try {
  83. const urlObj = new URL(record.downloadUrl);
  84. const correctUrlObj = new URL(correctBaseURL);
  85. if (urlObj.origin !== correctUrlObj.origin) {
  86. return {
  87. ...record,
  88. downloadUrl: record.downloadUrl.replace(urlObj.origin, correctUrlObj.origin),
  89. };
  90. }
  91. } catch {
  92. // Invalid URL, skip
  93. }
  94. }
  95. return record;
  96. });
  97. }
  98. return data;
  99. } catch (error) {
  100. const message = getErrorMessage(error);
  101. throw new Error(`获取导出记录列表失败: ${message}`, { cause: error });
  102. }
  103. };
  104. /**
  105. * Download an exported file
  106. *
  107. * Opens the file in a new tab/window for download.
  108. * The backend returns a FileResponse with appropriate headers.
  109. *
  110. * @param recordId - The export record ID
  111. * @param userId - The user ID who owns the record
  112. * @throws {Error} When the download fails
  113. */
  114. export const downloadExportRecord = async (recordId: string, userId: string): Promise<void> => {
  115. try {
  116. const encodedRecordId = encodeExportRecordId(recordId);
  117. // Use apiClient to download the file (goes through axios with proper config)
  118. // This avoids mixed content warnings by using relative URLs
  119. const response = await apiClient.get(`/api/v1/export/records/${encodedRecordId}/download`, {
  120. params: { userId },
  121. responseType: 'blob', // Important: tell axios to expect binary data
  122. });
  123. // Get filename from Content-Disposition header
  124. const fileName = getFileNameFromContentDisposition(
  125. response.headers['content-disposition'],
  126. `export-${recordId}.doc`
  127. );
  128. // Create blob and download
  129. const blob = new Blob([response.data], { type: 'application/msword' });
  130. downloadBlob(blob, fileName);
  131. } catch (error) {
  132. const message = getErrorMessage(error);
  133. throw new Error(`下载文件失败: ${message}`, { cause: error });
  134. }
  135. };
  136. /**
  137. * Delete an export record
  138. *
  139. * Deletes the export record and its associated file from storage.
  140. *
  141. * @param recordId - The export record ID to delete
  142. * @param userId - The user ID who owns the record
  143. * @returns Promise that resolves when deletion is successful
  144. * @throws {Error} When the API request fails or record not found
  145. */
  146. export const deleteExportRecord = async (recordId: string, userId: string): Promise<void> => {
  147. try {
  148. const encodedRecordId = encodeExportRecordId(recordId);
  149. await apiClient.delete<ApiResponse<void>>(`/api/v1/export/records/${encodedRecordId}`, {
  150. params: { userId },
  151. });
  152. } catch (error) {
  153. const message = getErrorMessage(error);
  154. throw new Error(`删除导出记录失败: ${message}`, { cause: error });
  155. }
  156. };
  157. /**
  158. * Get storage usage information (admin only)
  159. *
  160. * Retrieves detailed storage usage statistics including:
  161. * - Total and used disk space
  162. * - Per-user storage usage
  163. * - Quota information
  164. *
  165. * @returns Storage usage information
  166. * @throws {Error} When the API request fails or user is not authorized
  167. */
  168. export const getAdminStorage = async (): Promise<StorageInfo> => {
  169. try {
  170. const response = await apiClient.get<ApiResponse<StorageInfo>>('/api/v1/admin/storage');
  171. return response.data.data;
  172. } catch (error) {
  173. const message = getErrorMessage(error);
  174. throw new Error(`获取存储信息失败: ${message}`, { cause: error });
  175. }
  176. };
  177. /**
  178. * Export record service object (alternative export pattern)
  179. * Groups all export record operations into a single namespace
  180. */
  181. export const exportRecordService = {
  182. list: listExportRecords,
  183. download: downloadExportRecord,
  184. delete: deleteExportRecord,
  185. getStorage: getAdminStorage,
  186. };
  187. export default exportRecordService;