feie-api.service.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. import axios, { AxiosInstance } from 'axios';
  2. import { createHash } from 'crypto';
  3. import { FeieApiConfig, FeiePrinterInfo, FeiePrintRequest, FeiePrintResponse, FeiePrinterStatusResponse, FeieOrderStatusResponse, FeieAddPrinterResponse, FeieDeletePrinterResponse } from '../types/feie.types';
  4. export class FeieApiService {
  5. private client: AxiosInstance;
  6. private config: FeieApiConfig;
  7. private maxRetries: number;
  8. constructor(config: FeieApiConfig) {
  9. // 确保baseUrl有默认值
  10. const defaultBaseUrl = 'https://api.feieyun.cn/Api/Open/';
  11. this.config = {
  12. baseUrl: defaultBaseUrl,
  13. timeout: 30000, // 增加到30秒,飞鹅API有时响应较慢
  14. maxRetries: 3,
  15. ...config
  16. };
  17. this.maxRetries = this.config.maxRetries || 3;
  18. this.client = axios.create({
  19. baseURL: this.config.baseUrl,
  20. timeout: this.config.timeout,
  21. headers: {
  22. 'Content-Type': 'application/x-www-form-urlencoded'
  23. }
  24. });
  25. }
  26. /**
  27. * 生成飞鹅API签名
  28. */
  29. private generateSignature(timestamp: number): string {
  30. const content = `${this.config.user}${this.config.ukey}${timestamp}`;
  31. return createHash('sha1').update(content).digest('hex');
  32. }
  33. /**
  34. * 解析飞鹅API返回的响应(支持JSON和PHP var_dump格式)
  35. */
  36. private parseFeieResponse(responseString: string): any {
  37. try {
  38. console.debug('开始解析飞鹅API响应');
  39. // 移除多余的空白字符和换行
  40. const cleaned = responseString.trim();
  41. console.debug('清理后的响应:', cleaned.substring(0, 200) + '...');
  42. // 首先尝试解析为JSON
  43. try {
  44. const jsonResult = JSON.parse(cleaned);
  45. console.debug('成功解析为JSON格式');
  46. return jsonResult;
  47. } catch (jsonError) {
  48. console.debug('不是JSON格式,尝试解析PHP var_dump格式');
  49. }
  50. // 检查是否是var_dump格式
  51. if (!cleaned.startsWith('array(')) {
  52. console.debug('不是PHP var_dump格式');
  53. return null;
  54. }
  55. // 解析PHP var_dump格式
  56. const result: any = {};
  57. // 提取ret字段 - 支持多种格式
  58. let retMatch = cleaned.match(/\["ret"\]\s*=>\s*int\((\d+)\)/);
  59. if (!retMatch) {
  60. retMatch = cleaned.match(/\["ret"\]\s*=>\s*string\((\d+)\)\s*"(\d+)"/);
  61. if (retMatch) {
  62. result.ret = parseInt(retMatch[2], 10);
  63. }
  64. } else {
  65. result.ret = parseInt(retMatch[1], 10);
  66. }
  67. // 提取msg字段
  68. const msgMatch = cleaned.match(/\["msg"\]\s*=>\s*string\((\d+)\)\s*"([^"]*)"/);
  69. if (msgMatch) {
  70. result.msg = msgMatch[2];
  71. }
  72. // 提取data字段中的信息
  73. const dataMatch = cleaned.match(/\["data"\]\s*=>\s*array\(\d+\)\s*{([\s\S]*?)}\s*}/);
  74. if (dataMatch) {
  75. const dataContent = dataMatch[1];
  76. result.data = {};
  77. // 提取ok数组
  78. const okMatch = dataContent.match(/\["ok"\]\s*=>\s*array\(\d+\)\s*{([\s\S]*?)}/);
  79. if (okMatch) {
  80. result.data.ok = [];
  81. const okContent = okMatch[1];
  82. const okItems = okContent.match(/\[(\d+)\]\s*=>\s*string\(\d+\)\s*"([^"]*)"/g);
  83. if (okItems) {
  84. okItems.forEach(item => {
  85. const itemMatch = item.match(/string\(\d+\)\s*"([^"]*)"/);
  86. if (itemMatch) {
  87. result.data.ok.push(itemMatch[1]);
  88. }
  89. });
  90. }
  91. }
  92. // 提取no数组
  93. const noMatch = dataContent.match(/\["no"\]\s*=>\s*array\(\d+\)\s*{([\s\S]*?)}/);
  94. if (noMatch) {
  95. result.data.no = [];
  96. const noContent = noMatch[1];
  97. const noItems = noContent.match(/\[(\d+)\]\s*=>\s*string\(\d+\)\s*"([^"]*)"/g);
  98. if (noItems) {
  99. noItems.forEach(item => {
  100. const itemMatch = item.match(/string\(\d+\)\s*"([^"]*)"/);
  101. if (itemMatch) {
  102. result.data.no.push(itemMatch[1]);
  103. }
  104. });
  105. }
  106. }
  107. }
  108. // 如果没有解析到ret字段,尝试其他方式
  109. if (result.ret === undefined) {
  110. console.debug('未解析到ret字段,尝试其他方式');
  111. // 检查是否有错误信息
  112. if (cleaned.includes('错误:')) {
  113. result.ret = -1; // 假设是参数错误
  114. result.msg = '打印机添加失败';
  115. } else if (cleaned.includes('"ok"')) {
  116. result.ret = 0;
  117. result.msg = 'ok';
  118. }
  119. }
  120. console.debug('解析结果:', JSON.stringify(result, null, 2));
  121. return result;
  122. } catch (error) {
  123. console.debug('解析飞鹅API响应失败:', error);
  124. return null;
  125. }
  126. }
  127. /**
  128. * 执行API请求,支持重试
  129. */
  130. private async executeRequest<T>(endpoint: string, params: Record<string, any>): Promise<T> {
  131. const timestamp = Math.floor(Date.now() / 1000);
  132. const signature = this.generateSignature(timestamp);
  133. const requestParams = {
  134. user: this.config.user,
  135. stime: timestamp,
  136. sig: signature,
  137. apiname: endpoint, // 飞鹅API要求将接口名作为apiname参数传递
  138. debug: 1, // 添加debug参数以便查看更多错误信息
  139. ...params
  140. };
  141. // 调试日志:记录请求信息(不记录敏感信息)
  142. console.debug(`飞鹅API请求: ${endpoint}, 用户: ${this.config.user}, 时间戳: ${timestamp}`);
  143. let lastError: Error | null = null;
  144. for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
  145. try {
  146. // 飞鹅API所有请求都发送到根路径,接口名通过apiname参数指定
  147. const response = await this.client.post('', requestParams);
  148. // 调试日志:记录完整的API响应
  149. console.debug(`飞鹅API响应:`, {
  150. status: response.status,
  151. statusText: response.statusText,
  152. data: response.data,
  153. headers: response.headers
  154. });
  155. // 检查响应数据结构
  156. if (!response.data) {
  157. throw new Error(`飞鹅API响应格式错误: 响应数据为空`);
  158. }
  159. // 尝试解析响应数据,可能是字符串或对象
  160. let responseData = response.data;
  161. // 如果是字符串,使用统一的解析函数
  162. if (typeof responseData === 'string') {
  163. const parsedResponse = this.parseFeieResponse(responseData);
  164. if (parsedResponse) {
  165. responseData = parsedResponse;
  166. } else {
  167. // 检查是否是HTML错误页面
  168. if (responseData.includes('<!DOCTYPE') || responseData.includes('<html')) {
  169. throw new Error(`飞鹅API返回HTML错误页面,可能是网络或配置问题: ${responseData.substring(0, 200)}...`);
  170. }
  171. throw new Error(`飞鹅API响应格式错误: 无法解析响应数据: ${responseData.substring(0, 200)}...`);
  172. }
  173. }
  174. // 检查是否为对象
  175. if (typeof responseData !== 'object' || responseData === null) {
  176. throw new Error(`飞鹅API响应格式错误: 响应数据不是有效的对象: ${JSON.stringify(responseData)}`);
  177. }
  178. // 检查ret字段是否存在
  179. if (responseData.ret === undefined) {
  180. // 飞鹅API有时会返回不同的字段名,检查常见字段
  181. if (responseData.code !== undefined) {
  182. // 如果有code字段,将其映射到ret
  183. responseData.ret = responseData.code;
  184. } else if (responseData.error !== undefined) {
  185. // 如果有error字段,将其作为错误信息
  186. throw new Error(`飞鹅API错误: ${responseData.error}`);
  187. } else if (responseData.msg !== undefined) {
  188. // 如果只有msg字段,将其作为错误信息,并设置ret为-1(参数错误)
  189. responseData.ret = -1;
  190. console.debug('飞鹅API响应缺少ret字段,但包含msg字段,设置为参数错误:', responseData.msg);
  191. } else {
  192. throw new Error(`飞鹅API响应格式错误: 缺少ret字段,响应数据: ${JSON.stringify(responseData)}`);
  193. }
  194. }
  195. if (responseData.ret !== 0) {
  196. const errorMsg = responseData.msg || '未知错误';
  197. const errorCode = responseData.ret;
  198. // 根据错误代码提供更详细的错误信息
  199. let detailedMessage = `飞鹅API错误: ${errorMsg} (错误代码: ${errorCode})`;
  200. // 常见的飞鹅API错误代码
  201. switch (errorCode) {
  202. case -1:
  203. detailedMessage += ' - 参数错误,请检查请求参数';
  204. break;
  205. case -2:
  206. detailedMessage += ' - 签名错误,请检查用户和密钥配置';
  207. break;
  208. case -3:
  209. detailedMessage += ' - 用户或密钥错误';
  210. break;
  211. case -4:
  212. detailedMessage += ' - 打印机不存在或未授权';
  213. break;
  214. case -5:
  215. detailedMessage += ' - 打印机离线';
  216. break;
  217. case -6:
  218. detailedMessage += ' - 订单号重复';
  219. break;
  220. case -7:
  221. detailedMessage += ' - 打印内容过长';
  222. break;
  223. case -8:
  224. detailedMessage += ' - 请求频率过高';
  225. break;
  226. case -9:
  227. detailedMessage += ' - 打印机忙';
  228. break;
  229. case -10:
  230. detailedMessage += ' - 打印机缺纸';
  231. break;
  232. case -11:
  233. detailedMessage += ' - 打印机过热';
  234. break;
  235. case -12:
  236. detailedMessage += ' - 打印机故障';
  237. break;
  238. default:
  239. detailedMessage += ' - 未知API错误';
  240. }
  241. throw new Error(detailedMessage);
  242. }
  243. return responseData;
  244. } catch (error) {
  245. lastError = error as Error;
  246. // 如果是 Axios 错误,提供更详细的信息
  247. if (axios.isAxiosError(error)) {
  248. const status = error.response?.status;
  249. const data = error.response?.data;
  250. let axiosErrorMsg = `HTTP错误: ${status || '未知'}`;
  251. if (status === 400) {
  252. axiosErrorMsg += ' - 请求参数错误';
  253. if (data && typeof data === 'object') {
  254. axiosErrorMsg += `, 响应: ${JSON.stringify(data)}`;
  255. }
  256. } else if (status === 401) {
  257. axiosErrorMsg += ' - 认证失败';
  258. } else if (status === 403) {
  259. axiosErrorMsg += ' - 权限不足';
  260. } else if (status === 404) {
  261. axiosErrorMsg += ' - API端点不存在';
  262. } else if (status === 429) {
  263. axiosErrorMsg += ' - 请求频率过高';
  264. } else if (status && status >= 500) {
  265. axiosErrorMsg += ' - 服务器内部错误';
  266. }
  267. lastError = new Error(`飞鹅API请求失败: ${axiosErrorMsg}`);
  268. }
  269. if (attempt < this.maxRetries) {
  270. // 等待指数退避
  271. const delay = Math.pow(2, attempt) * 1000;
  272. await new Promise(resolve => setTimeout(resolve, delay));
  273. continue;
  274. }
  275. }
  276. }
  277. throw lastError || new Error('飞鹅API请求失败');
  278. }
  279. /**
  280. * 添加打印机
  281. */
  282. async addPrinter(printerInfo: FeiePrinterInfo): Promise<FeieAddPrinterResponse> {
  283. const { sn, key, name = '' } = printerInfo;
  284. // 飞鹅API要求格式:sn#key#remark,其中remark是备注名称
  285. const snlist = `${sn}#${key}#${name}`;
  286. return this.executeRequest<FeieAddPrinterResponse>('Open_printerAddlist', {
  287. printerContent: snlist
  288. });
  289. }
  290. /**
  291. * 删除打印机
  292. */
  293. async deletePrinter(sn: string): Promise<FeieDeletePrinterResponse> {
  294. const snlist = sn;
  295. return this.executeRequest<FeieDeletePrinterResponse>('Open_printerDelList', {
  296. snlist
  297. });
  298. }
  299. /**
  300. * 查询打印机状态
  301. */
  302. async queryPrinterStatus(sn: string): Promise<FeiePrinterStatusResponse> {
  303. return this.executeRequest<FeiePrinterStatusResponse>('Open_queryPrinterStatus', {
  304. sn
  305. });
  306. }
  307. /**
  308. * 打印小票
  309. */
  310. async printReceipt(printRequest: FeiePrintRequest): Promise<FeiePrintResponse> {
  311. const { sn, content, times = 1 } = printRequest;
  312. return this.executeRequest<FeiePrintResponse>('Open_printMsg', {
  313. sn,
  314. content,
  315. times
  316. });
  317. }
  318. /**
  319. * 查询订单打印状态
  320. */
  321. async queryOrderStatus(orderId: string): Promise<FeieOrderStatusResponse> {
  322. return this.executeRequest<FeieOrderStatusResponse>('Open_queryOrderState', {
  323. orderid: orderId
  324. });
  325. }
  326. /**
  327. * 根据时间查询订单
  328. */
  329. async queryOrdersByDate(date: string, page: number = 1): Promise<any> {
  330. return this.executeRequest<any>('Open_queryOrderInfoByDate', {
  331. date,
  332. page
  333. });
  334. }
  335. /**
  336. * 批量查询打印机状态
  337. */
  338. async batchQueryPrinterStatus(snList: string[]): Promise<FeiePrinterStatusResponse> {
  339. const snlist = snList.join('-');
  340. return this.executeRequest<FeiePrinterStatusResponse>('Open_queryPrinterStatus', {
  341. snlist
  342. });
  343. }
  344. /**
  345. * 获取打印机在线状态
  346. */
  347. async getPrinterOnlineStatus(sn: string): Promise<boolean> {
  348. try {
  349. const response = await this.queryPrinterStatus(sn);
  350. if (response.data && response.data.length > 0) {
  351. const printerStatus = response.data[0];
  352. return printerStatus.online === 1;
  353. }
  354. return false;
  355. } catch (error) {
  356. console.error('获取打印机在线状态失败:', error);
  357. return false;
  358. }
  359. }
  360. /**
  361. * 验证打印机配置
  362. */
  363. async validatePrinterConfig(sn: string, key: string): Promise<boolean> {
  364. try {
  365. console.debug(`开始验证打印机配置,SN: ${sn}, 用户: ${this.config.user}`);
  366. // 首先尝试查询打印机状态,检查打印机是否已存在
  367. try {
  368. await this.executeRequest('Open_queryPrinterStatus', {
  369. sn
  370. });
  371. console.debug(`打印机 ${sn} 已存在,配置验证通过`);
  372. return true;
  373. } catch (queryError: any) {
  374. console.debug(`查询打印机状态失败:`, queryError.message);
  375. // 如果错误代码是-4(打印机不存在),尝试添加打印机来验证密钥
  376. // 或者错误代码是undefined(可能是响应格式错误),也尝试添加验证
  377. // 或者错误信息包含"响应格式错误",也尝试添加验证
  378. if (queryError.message.includes('错误代码: -4') ||
  379. queryError.message.includes('错误代码: undefined') ||
  380. queryError.message.includes('响应格式错误')) {
  381. console.debug(`打印机 ${sn} 不存在或响应格式错误,尝试添加验证。错误信息: ${queryError.message}`);
  382. try {
  383. // 尝试添加打印机
  384. const addResponse = await this.executeRequest<any>('Open_printerAddlist', {
  385. printerContent: `${sn}#${key}#验证配置`
  386. });
  387. console.debug(`打印机添加API响应:`, JSON.stringify(addResponse, null, 2));
  388. // 检查响应结果
  389. if (addResponse.ret === 0) {
  390. // ret为0表示API调用成功
  391. // 检查data.no数组中是否包含当前打印机
  392. const isInNoArray = addResponse.data?.no?.some((item: string) =>
  393. item.includes(sn) && item.includes(key)
  394. );
  395. if (isInNoArray) {
  396. // 打印机在no数组中,检查具体错误
  397. const errorItem = addResponse.data.no.find((item: string) =>
  398. item.includes(sn) && item.includes(key)
  399. );
  400. if (errorItem && errorItem.includes('已被添加过')) {
  401. console.debug(`打印机 ${sn} 已被添加过,配置验证通过`);
  402. return true; // 打印机已存在,配置正确
  403. } else if (errorItem && errorItem.includes('设备编号和KEY不正确')) {
  404. console.debug(`打印机 ${sn} 设备编号和KEY不正确,配置验证失败`);
  405. return false; // 配置错误
  406. } else {
  407. console.debug(`打印机 ${sn} 添加失败,未知错误: ${errorItem}`);
  408. return false;
  409. }
  410. } else {
  411. // 打印机添加成功或在ok数组中
  412. console.debug(`打印机 ${sn} 添加成功,配置验证通过,开始删除测试打印机`);
  413. // 删除刚刚添加的测试打印机
  414. try {
  415. await this.executeRequest('Open_printerDelList', {
  416. snlist: sn
  417. });
  418. console.debug('删除测试打印机成功');
  419. } catch (deleteError: any) {
  420. console.debug('删除测试打印机失败,但配置验证已通过:', deleteError.message);
  421. }
  422. return true;
  423. }
  424. } else {
  425. // ret不为0,API调用失败
  426. console.debug(`打印机 ${sn} 添加失败,ret: ${addResponse.ret}, msg: ${addResponse.msg}`);
  427. return false;
  428. }
  429. } catch (addError: any) {
  430. console.debug(`打印机 ${sn} 添加失败:`, addError.message);
  431. // 如果返回-1(参数错误)、-2(签名错误)或-3(用户或密钥错误),说明配置有问题
  432. // 其他错误(如格式错误等)也认为配置有问题
  433. const isParamError = addError.message.includes('错误代码: -1') ||
  434. addError.message.includes('错误代码: -2') ||
  435. addError.message.includes('错误代码: -3');
  436. // 检查是否是响应格式错误
  437. const isFormatError = addError.message.includes('响应格式错误') ||
  438. addError.message.includes('无法解析响应数据') ||
  439. addError.message.includes('HTML错误页面');
  440. // 检查是否是超时错误
  441. const isTimeoutError = addError.message.includes('timeout') ||
  442. addError.message.includes('超时') ||
  443. addError.message.includes('Timeout');
  444. const isValid = !isParamError && !isFormatError && !isTimeoutError;
  445. console.debug(`配置验证结果: ${isValid ? '通过' : '失败'}, 参数错误: ${isParamError}, 格式错误: ${isFormatError}, 超时错误: ${isTimeoutError}`);
  446. // 如果是超时错误,可能是网络问题,建议用户检查网络连接
  447. if (isTimeoutError) {
  448. console.debug('飞鹅API请求超时,请检查网络连接或联系管理员检查飞鹅API服务状态');
  449. }
  450. return isValid;
  451. }
  452. }
  453. // 其他错误情况
  454. console.debug(`查询打印机状态失败:`, queryError.message);
  455. return false;
  456. }
  457. } catch (error) {
  458. console.error('验证打印机配置失败:', error);
  459. if (error instanceof Error) {
  460. console.error('错误详情:', error.message);
  461. }
  462. return false;
  463. }
  464. }
  465. }