zh_drawing.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. 'use strict';
  2. const axios = require('axios');
  3. const FILES_PATH = '/api/v1/files';
  4. const FILE_ACCESS_PATH = '/api/v1/files/access';
  5. const DEFAULT_TIMEOUT = 120000;
  6. /**
  7. * 纵横图纸接口客户端。
  8. *
  9. * 推荐传入 app.config.pdfDrawing:
  10. * new ZhDrawing(ctx.app.config.pdfDrawing)
  11. */
  12. class ZhDrawing {
  13. /**
  14. * @param {Object} config 接口配置
  15. * @param {String} config.baseUrl 纵横图纸访问地址
  16. * @param {String} config.token 外部 API 访问凭证
  17. * @param {Number} [config.timeout=120000] 请求超时时间,单位毫秒
  18. * @param {Object} [httpClient=axios] HTTP 客户端,主要用于单元测试注入
  19. */
  20. constructor(config = {}, httpClient = axios) {
  21. if (typeof config.baseUrl !== 'string' || !config.baseUrl.trim()) {
  22. throw new TypeError('pdfDrawing.baseUrl 不能为空');
  23. }
  24. if (typeof config.token !== 'string' || !config.token.trim()) {
  25. throw new TypeError('pdfDrawing.token 不能为空');
  26. }
  27. if (!httpClient || typeof httpClient.post !== 'function') {
  28. throw new TypeError('httpClient 必须提供 post 方法');
  29. }
  30. this.baseUrl = config.baseUrl.trim().replace(/\/+$/, '');
  31. this.token = config.token.trim();
  32. this.timeout = Number(config.timeout) > 0 ? Number(config.timeout) : DEFAULT_TIMEOUT;
  33. this.httpClient = httpClient;
  34. }
  35. /**
  36. * 读取 PDF 图纸数据,返回图纸 ID 与访问地址等信息。
  37. *
  38. * @param {String} file 可下载的 PDF 文件地址
  39. * @param {String} [filename] 文件名称;传入后以该名称为准
  40. * @return {Promise<Object>} 接口响应数据
  41. */
  42. async createFile(file, filename) {
  43. const payload = {
  44. file: this._requiredString(file, 'file'),
  45. };
  46. const normalizedFilename = this._optionalString(filename, 'filename');
  47. if (normalizedFilename) {
  48. payload.filename = normalizedFilename;
  49. }
  50. return this._post(FILES_PATH, payload);
  51. }
  52. /**
  53. * 为指定图纸签发短期可编辑访问凭证。
  54. *
  55. * @param {String} fileId 图纸 ID
  56. * @return {Promise<Object>} 接口响应数据
  57. */
  58. async createEditableAccess(fileId) {
  59. const payload = {
  60. file_id: this._requiredString(fileId, 'fileId'),
  61. };
  62. return this._post(FILE_ACCESS_PATH, payload);
  63. }
  64. /** 按传入文件顺序查找包含图表号的首个文件;未找到返回 { file_id: null }。 */
  65. async findDrawing(fileIds, drawingNumber) {
  66. if (!Array.isArray(fileIds) || !fileIds.length || fileIds.length > 100) {
  67. throw new TypeError('fileIds 必须为包含 1–100 个文件 ID 的数组');
  68. }
  69. return this._post('/api/v1/drawings/find', {
  70. file_id: fileIds.map(id => this._requiredString(id, 'fileId')),
  71. drawing_number: this._requiredString(drawingNumber, 'drawingNumber'),
  72. });
  73. }
  74. /** 标记删除图纸,立即停止访问,保留30天后后台清理;调用方应先取得用户确认。 */
  75. async deleteFile(fileId) {
  76. return this._post('/api/v1/files/delete', {
  77. file_id: this._requiredString(fileId, 'fileId'),
  78. });
  79. }
  80. /**
  81. * 将接口返回的相对 document_path 转换为可直接访问的完整地址。
  82. *
  83. * @param {String} documentPath 接口返回的 document_path
  84. * @return {String} 完整访问地址
  85. */
  86. resolveDocumentUrl(documentPath) {
  87. const path = this._requiredString(documentPath, 'documentPath');
  88. if (/^https?:\/\//i.test(path)) {
  89. return path;
  90. }
  91. return `${this.baseUrl}/${path.replace(/^\/+/, '')}`;
  92. }
  93. async _post(path, payload) {
  94. try {
  95. const response = await this.httpClient.post(
  96. `${this.baseUrl}${path}`,
  97. Object.assign({}, payload, { token: this.token }),
  98. {
  99. headers: {
  100. 'Content-Type': 'application/json',
  101. },
  102. timeout: this.timeout,
  103. }
  104. );
  105. return response.data;
  106. } catch (error) {
  107. throw this._normalizeError(error);
  108. }
  109. }
  110. _normalizeError(error) {
  111. const response = error && error.response;
  112. const responseData = response && response.data;
  113. const detail = responseData && responseData.detail;
  114. let message = error && error.message ? error.message : '纵横图纸接口请求失败';
  115. if (typeof detail === 'string' && detail) {
  116. message = detail;
  117. } else if (detail !== undefined) {
  118. try {
  119. message = JSON.stringify(detail);
  120. } catch (jsonError) {
  121. message = '纵横图纸接口请求失败';
  122. }
  123. }
  124. const normalizedError = new Error(message);
  125. normalizedError.name = 'ZhDrawingError';
  126. normalizedError.status = response && response.status;
  127. normalizedError.detail = detail;
  128. normalizedError.code = error && error.code;
  129. normalizedError.originalError = error;
  130. return normalizedError;
  131. }
  132. _requiredString(value, fieldName) {
  133. if (typeof value !== 'string' || !value.trim()) {
  134. throw new TypeError(`${fieldName} 不能为空`);
  135. }
  136. return value.trim();
  137. }
  138. _optionalString(value, fieldName) {
  139. if (value === undefined || value === null || value === '') {
  140. return '';
  141. }
  142. if (typeof value !== 'string') {
  143. throw new TypeError(`${fieldName} 必须是字符串`);
  144. }
  145. return value.trim();
  146. }
  147. }
  148. module.exports = ZhDrawing;