'use strict'; const axios = require('axios'); const FILES_PATH = '/api/v1/files'; const FILE_ACCESS_PATH = '/api/v1/files/access'; const DEFAULT_TIMEOUT = 120000; /** * 纵横图纸接口客户端。 * * 推荐传入 app.config.pdfDrawing: * new ZhDrawing(ctx.app.config.pdfDrawing) */ class ZhDrawing { /** * @param {Object} config 接口配置 * @param {String} config.baseUrl 纵横图纸访问地址 * @param {String} config.token 外部 API 访问凭证 * @param {Number} [config.timeout=120000] 请求超时时间,单位毫秒 * @param {Object} [httpClient=axios] HTTP 客户端,主要用于单元测试注入 */ constructor(config = {}, httpClient = axios) { if (typeof config.baseUrl !== 'string' || !config.baseUrl.trim()) { throw new TypeError('pdfDrawing.baseUrl 不能为空'); } if (typeof config.token !== 'string' || !config.token.trim()) { throw new TypeError('pdfDrawing.token 不能为空'); } if (!httpClient || typeof httpClient.post !== 'function') { throw new TypeError('httpClient 必须提供 post 方法'); } this.baseUrl = config.baseUrl.trim().replace(/\/+$/, ''); this.token = config.token.trim(); this.timeout = Number(config.timeout) > 0 ? Number(config.timeout) : DEFAULT_TIMEOUT; this.httpClient = httpClient; } /** * 读取 PDF 图纸数据,返回图纸 ID 与访问地址等信息。 * * @param {String} file 可下载的 PDF 文件地址 * @param {String} [filename] 文件名称;传入后以该名称为准 * @return {Promise} 接口响应数据 */ async createFile(file, filename) { const payload = { file: this._requiredString(file, 'file'), }; const normalizedFilename = this._optionalString(filename, 'filename'); if (normalizedFilename) { payload.filename = normalizedFilename; } return this._post(FILES_PATH, payload); } /** * 为指定图纸签发短期可编辑访问凭证。 * * @param {String} fileId 图纸 ID * @return {Promise} 接口响应数据 */ async createEditableAccess(fileId) { const payload = { file_id: this._requiredString(fileId, 'fileId'), }; return this._post(FILE_ACCESS_PATH, payload); } /** 按传入文件顺序查找包含图表号的首个文件;未找到返回 { file_id: null }。 */ async findDrawing(fileIds, drawingNumber) { if (!Array.isArray(fileIds) || !fileIds.length || fileIds.length > 100) { throw new TypeError('fileIds 必须为包含 1–100 个文件 ID 的数组'); } return this._post('/api/v1/drawings/find', { file_id: fileIds.map(id => this._requiredString(id, 'fileId')), drawing_number: this._requiredString(drawingNumber, 'drawingNumber'), }); } /** 标记删除图纸,立即停止访问,保留30天后后台清理;调用方应先取得用户确认。 */ async deleteFile(fileId) { return this._post('/api/v1/files/delete', { file_id: this._requiredString(fileId, 'fileId'), }); } /** * 将接口返回的相对 document_path 转换为可直接访问的完整地址。 * * @param {String} documentPath 接口返回的 document_path * @return {String} 完整访问地址 */ resolveDocumentUrl(documentPath) { const path = this._requiredString(documentPath, 'documentPath'); if (/^https?:\/\//i.test(path)) { return path; } return `${this.baseUrl}/${path.replace(/^\/+/, '')}`; } async _post(path, payload) { try { const response = await this.httpClient.post( `${this.baseUrl}${path}`, Object.assign({}, payload, { token: this.token }), { headers: { 'Content-Type': 'application/json', }, timeout: this.timeout, } ); return response.data; } catch (error) { throw this._normalizeError(error); } } _normalizeError(error) { const response = error && error.response; const responseData = response && response.data; const detail = responseData && responseData.detail; let message = error && error.message ? error.message : '纵横图纸接口请求失败'; if (typeof detail === 'string' && detail) { message = detail; } else if (detail !== undefined) { try { message = JSON.stringify(detail); } catch (jsonError) { message = '纵横图纸接口请求失败'; } } const normalizedError = new Error(message); normalizedError.name = 'ZhDrawingError'; normalizedError.status = response && response.status; normalizedError.detail = detail; normalizedError.code = error && error.code; normalizedError.originalError = error; return normalizedError; } _requiredString(value, fieldName) { if (typeof value !== 'string' || !value.trim()) { throw new TypeError(`${fieldName} 不能为空`); } return value.trim(); } _optionalString(value, fieldName) { if (value === undefined || value === null || value === '') { return ''; } if (typeof value !== 'string') { throw new TypeError(`${fieldName} 必须是字符串`); } return value.trim(); } } module.exports = ZhDrawing;