2
0

print-trigger.service.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. import { DataSource } from 'typeorm';
  2. import { PrintTaskService } from './print-task.service';
  3. import { PrinterService } from './printer.service';
  4. import { DelaySchedulerService } from './delay-scheduler.service';
  5. import { FeieApiConfig, PrintType, PrintStatus, CancelReason } from '../types/feie.types';
  6. import { FeieConfigMt } from '../entities/feie-config.mt.entity';
  7. import { OrderMt } from '@d8d/orders-module-mt';
  8. /**
  9. * 打印触发服务
  10. * 负责处理订单支付成功等事件触发的打印任务
  11. */
  12. export class PrintTriggerService {
  13. private printTaskService: PrintTaskService;
  14. private printerService: PrinterService;
  15. private dataSource: DataSource;
  16. private feieConfig: FeieApiConfig;
  17. private configRepository: any;
  18. private orderRepository: any;
  19. constructor(dataSource: DataSource, feieConfig: FeieApiConfig) {
  20. this.dataSource = dataSource;
  21. this.feieConfig = feieConfig;
  22. this.printTaskService = new PrintTaskService(dataSource, feieConfig);
  23. this.printerService = new PrinterService(dataSource, feieConfig);
  24. this.configRepository = dataSource.getRepository(FeieConfigMt);
  25. this.orderRepository = dataSource.getRepository(OrderMt);
  26. }
  27. /**
  28. * 处理订单支付成功事件
  29. * @param tenantId 租户ID
  30. * @param orderId 订单ID
  31. * @param orderInfo 订单信息
  32. */
  33. async handleOrderPaymentSuccess(
  34. tenantId: number,
  35. orderId: number
  36. ): Promise<void> {
  37. try {
  38. console.debug(`[租户${tenantId}] 处理订单支付成功事件,订单ID: ${orderId}`);
  39. // 1. 获取完整的订单信息
  40. const fullOrderInfo = await this.getFullOrderInfo(tenantId, orderId);
  41. if (!fullOrderInfo) {
  42. console.warn(`[租户${tenantId}] 未找到订单信息,订单ID: ${orderId},跳过打印任务`);
  43. return;
  44. }
  45. // 2. 获取防退款延迟时间
  46. const delaySeconds = await this.getAntiRefundDelaySeconds(tenantId);
  47. // 3. 获取默认打印机
  48. const defaultPrinter = await this.printerService.getDefaultPrinter(tenantId);
  49. if (!defaultPrinter) {
  50. console.warn(`[租户${tenantId}] 未找到默认打印机,跳过打印任务`);
  51. return;
  52. }
  53. // 4. 生成打印内容
  54. const printContent = await this.generateReceiptContent(tenantId, fullOrderInfo);
  55. // 5. 创建延迟打印任务
  56. await this.printTaskService.createPrintTask(tenantId, {
  57. orderId,
  58. printerSn: defaultPrinter.printerSn,
  59. content: printContent,
  60. printType: PrintType.RECEIPT,
  61. delaySeconds
  62. });
  63. console.debug(`[租户${tenantId}] 订单支付成功打印任务已创建,订单ID: ${orderId}, 延迟时间: ${delaySeconds}秒`);
  64. // 6. 自动启动延迟任务检查
  65. // 如果延迟时间为0或负数,立即触发打印
  66. // 如果延迟时间已过(比如配置错误或系统时间问题),也立即触发
  67. if (delaySeconds <= 0) {
  68. console.debug(`[租户${tenantId}] 延迟时间为${delaySeconds}秒,立即触发打印任务检查`);
  69. try {
  70. // 动态创建DelaySchedulerService实例
  71. const delaySchedulerService = new DelaySchedulerService(this.dataSource, this.feieConfig, tenantId);
  72. // 检查并启动调度器
  73. const status = delaySchedulerService.getStatus();
  74. if (!status.isRunning) {
  75. await delaySchedulerService.start();
  76. console.debug(`[租户${tenantId}] 延迟调度器已启动`);
  77. }
  78. const result = await delaySchedulerService.triggerManualProcess(tenantId);
  79. if (result.success) {
  80. console.debug(`[租户${tenantId}] 立即触发打印成功,处理了${result.processedTasks}个任务`);
  81. } else {
  82. console.warn(`[租户${tenantId}] 立即触发打印失败: ${result.message}`);
  83. }
  84. } catch (error) {
  85. console.warn(`[租户${tenantId}] 创建延迟调度器失败:`, error);
  86. // 不抛出错误,避免影响主流程
  87. }
  88. } else {
  89. // 对于有延迟的任务,也启动调度器(如果未运行)
  90. try {
  91. console.debug(`[租户${tenantId}] 检查并启动延迟调度器...`);
  92. const delaySchedulerService = new DelaySchedulerService(this.dataSource, this.feieConfig, tenantId);
  93. // 检查调度器状态
  94. const status = delaySchedulerService.getStatus();
  95. if (!status.isRunning) {
  96. await delaySchedulerService.start();
  97. console.debug(`[租户${tenantId}] 延迟调度器已启动`);
  98. } else {
  99. console.debug(`[租户${tenantId}] 延迟调度器已在运行中`);
  100. }
  101. } catch (error) {
  102. console.warn(`[租户${tenantId}] 启动调度器失败:`, error);
  103. // 不抛出错误,避免影响主流程
  104. }
  105. }
  106. } catch (error) {
  107. console.error(`[租户${tenantId}] 处理订单支付成功事件失败,订单ID: ${orderId}:`, error);
  108. // 不抛出错误,避免影响支付流程
  109. }
  110. }
  111. /**
  112. * 处理订单退款事件
  113. * @param tenantId 租户ID
  114. * @param orderId 订单ID
  115. */
  116. async handleOrderRefund(
  117. tenantId: number,
  118. orderId: number
  119. ): Promise<void> {
  120. try {
  121. console.debug(`[租户${tenantId}] 处理订单退款事件,订单ID: ${orderId}`);
  122. // 1. 查找关联的打印任务
  123. const { tasks: printTasks } = await this.printTaskService.getPrintTasks(tenantId, {
  124. orderId,
  125. printStatus: PrintStatus.PENDING // 先查询PENDING状态的任务
  126. });
  127. if (printTasks.length === 0) {
  128. console.debug(`[租户${tenantId}] 未找到关联的打印任务,订单ID: ${orderId}`);
  129. return;
  130. }
  131. // 2. 取消所有关联的打印任务
  132. for (const task of printTasks) {
  133. await this.printTaskService.cancelPrintTask(tenantId, task.taskId, CancelReason.REFUND);
  134. console.debug(`[租户${tenantId}] 打印任务已取消,任务ID: ${task.taskId}, 订单ID: ${orderId}`);
  135. }
  136. console.debug(`[租户${tenantId}] 订单退款事件处理完成,取消 ${printTasks.length} 个打印任务`);
  137. } catch (error) {
  138. console.error(`[租户${tenantId}] 处理订单退款事件失败,订单ID: ${orderId}:`, error);
  139. // 不抛出错误,避免影响退款流程
  140. }
  141. }
  142. /**
  143. * 获取防退款延迟时间(秒)
  144. */
  145. private async getAntiRefundDelaySeconds(tenantId: number): Promise<number> {
  146. try {
  147. const delayValue = await this.getConfigValue(tenantId, 'feie.anti_refund_delay', '120');
  148. const delaySeconds = parseInt(delayValue, 10);
  149. // 验证延迟时间范围
  150. if (isNaN(delaySeconds) || delaySeconds < 0) {
  151. console.warn(`[租户${tenantId}] 无效的防退款延迟时间配置: ${delayValue},使用默认值120秒`);
  152. return 120;
  153. }
  154. // 限制最大延迟时间(例如24小时)
  155. const maxDelay = 24 * 60 * 60; // 24小时
  156. if (delaySeconds > maxDelay) {
  157. console.warn(`[租户${tenantId}] 防退款延迟时间超过最大值: ${delaySeconds}秒,限制为${maxDelay}秒`);
  158. return maxDelay;
  159. }
  160. return delaySeconds;
  161. } catch (error) {
  162. console.warn(`[租户${tenantId}] 获取防退款延迟时间失败,使用默认值120秒:`, error);
  163. return 120;
  164. }
  165. }
  166. /**
  167. * 获取完整的订单信息
  168. */
  169. private async getFullOrderInfo(
  170. tenantId: number,
  171. orderId: number
  172. ): Promise<{
  173. orderNo: string;
  174. amount: number;
  175. userId: number;
  176. items?: Array<{
  177. name: string | null;
  178. quantity: number;
  179. price: number;
  180. }>;
  181. // 新增字段
  182. payAmount: number;
  183. freightAmount: number;
  184. address: string | null;
  185. receiverMobile: string | null;
  186. recevierName: string | null;
  187. state: number;
  188. payState: number;
  189. createdAt: Date;
  190. remark: string | null;
  191. } | null> {
  192. try {
  193. const order = await this.orderRepository.findOne({
  194. where: { tenantId, id: orderId },
  195. relations: ['orderGoods'] // 关联订单商品
  196. });
  197. if (!order) {
  198. console.warn(`[租户${tenantId}] 未找到订单,订单ID: ${orderId}`);
  199. return null;
  200. }
  201. // 构建完整的订单信息
  202. return {
  203. orderNo: order.orderNo,
  204. amount: order.amount,
  205. userId: order.userId,
  206. items: order.orderGoods?.map((goods: any) => ({
  207. name: goods.goodsName,
  208. quantity: goods.num,
  209. price: goods.price
  210. })) || [],
  211. // 新增字段
  212. payAmount: order.payAmount,
  213. freightAmount: order.freightAmount,
  214. address: order.address,
  215. receiverMobile: order.receiverMobile,
  216. recevierName: order.recevierName,
  217. state: order.state,
  218. payState: order.payState,
  219. createdAt: order.createdAt,
  220. remark: order.remark
  221. };
  222. } catch (error) {
  223. console.error(`[租户${tenantId}] 获取完整订单信息失败,订单ID: ${orderId}:`, error);
  224. return null;
  225. }
  226. }
  227. /**
  228. * 获取配置值
  229. */
  230. private async getConfigValue(tenantId: number, key: string, defaultValue: string): Promise<string> {
  231. try {
  232. const config = await this.configRepository.findOne({
  233. where: { tenantId, configKey: key }
  234. });
  235. return config?.configValue || defaultValue;
  236. } catch (error) {
  237. console.warn(`[租户${tenantId}] 获取配置失败,key: ${key}:`, error);
  238. return defaultValue;
  239. }
  240. }
  241. /**
  242. * 获取打印模板
  243. */
  244. private async getPrintTemplate(tenantId: number): Promise<string> {
  245. try {
  246. const template = await this.getConfigValue(tenantId, 'feie.receipt_template', '');
  247. if (template) {
  248. return template;
  249. }
  250. // 如果没有配置模板,使用默认模板
  251. return `
  252. <CB>订单收据</CB>
  253. <BR>
  254. 订单号: {orderNo}
  255. 下单时间: {orderTime}
  256. <BR>
  257. <B>收货信息</B>
  258. 收货人: {receiverName}
  259. 联系电话: {receiverPhone}
  260. 收货地址: {address}
  261. <BR>
  262. <B>商品信息</B>
  263. {goodsList}
  264. <BR>
  265. <B>费用明细</B>
  266. 商品总额: {totalAmount}
  267. 运费: {freightAmount}
  268. 实付金额: {payAmount}
  269. <BR>
  270. <B>订单状态</B>
  271. 订单状态: {orderStatus}
  272. 支付状态: {payStatus}
  273. <BR>
  274. <B>订单备注</B>
  275. {remark}
  276. <BR>
  277. <C>感谢您的惠顾!</C>
  278. <BR>
  279. <QR>{orderNo}</QR>
  280. `;
  281. } catch (error) {
  282. console.warn(`[租户${tenantId}] 获取打印模板失败,使用默认模板:`, error);
  283. return `
  284. <CB>订单收据</CB>
  285. <BR>
  286. 订单号: {orderNo}
  287. 下单时间: {orderTime}
  288. <BR>
  289. <B>收货信息</B>
  290. 收货人: {receiverName}
  291. 联系电话: {receiverPhone}
  292. 收货地址: {address}
  293. <BR>
  294. <B>商品信息</B>
  295. {goodsList}
  296. <BR>
  297. <B>费用明细</B>
  298. 商品总额: {totalAmount}
  299. 运费: {freightAmount}
  300. 实付金额: {payAmount}
  301. <BR>
  302. <B>订单状态</B>
  303. 订单状态: {orderStatus}
  304. 支付状态: {payStatus}
  305. <BR>
  306. <B>订单备注</B>
  307. {remark}
  308. <BR>
  309. <C>感谢您的惠顾!</C>
  310. <BR>
  311. <QR>{orderNo}</QR>
  312. `;
  313. }
  314. }
  315. /**
  316. * 生成小票打印内容
  317. */
  318. private async generateReceiptContent(
  319. tenantId: number,
  320. orderInfo: {
  321. orderNo: string;
  322. amount: number;
  323. userId: number;
  324. items?: Array<{
  325. name: string | null;
  326. quantity: number;
  327. price: number;
  328. }>;
  329. // 新增字段
  330. payAmount: number;
  331. freightAmount: number;
  332. address: string | null;
  333. receiverMobile: string | null;
  334. recevierName: string | null;
  335. state: number;
  336. payState: number;
  337. createdAt: Date;
  338. remark: string | null;
  339. }
  340. ): Promise<string> {
  341. const {
  342. orderNo,
  343. amount,
  344. items = [],
  345. payAmount,
  346. freightAmount,
  347. address,
  348. receiverMobile,
  349. recevierName,
  350. state,
  351. payState,
  352. createdAt,
  353. remark
  354. } = orderInfo;
  355. // 字符串长度限制函数(按中文字符计算)
  356. const limitStringLength = (str: string, maxLength: number): string => {
  357. if (!str || str.length <= maxLength) {
  358. return str;
  359. }
  360. // 计算字符长度(中文字符算1个长度)
  361. let length = 0;
  362. let result = '';
  363. for (const char of str) {
  364. // 中文字符范围判断
  365. const charCode = char.charCodeAt(0);
  366. if (charCode >= 0x4E00 && charCode <= 0x9FFF) {
  367. length += 1; // 中文字符
  368. } else {
  369. length += 0.5; // 英文字符和数字算半个长度
  370. }
  371. if (length <= maxLength) {
  372. result += char;
  373. } else {
  374. break;
  375. }
  376. }
  377. // 如果被截断,添加省略号
  378. if (result.length < str.length) {
  379. result += '...';
  380. }
  381. return result;
  382. };
  383. try {
  384. // 1. 获取打印模板
  385. const template = await this.getPrintTemplate(tenantId);
  386. // 2. 准备模板变量
  387. // 状态映射函数
  388. const getOrderStatusLabel = (state: number): string => {
  389. const statusMap: Record<number, string> = {
  390. 0: '未发货',
  391. 1: '已发货',
  392. 2: '收货成功',
  393. 3: '已退货'
  394. };
  395. return statusMap[state] || '未知';
  396. };
  397. const getPayStatusLabel = (payState: number): string => {
  398. const payStatusMap: Record<number, string> = {
  399. 0: '未支付',
  400. 1: '支付中',
  401. 2: '支付成功',
  402. 3: '已退款',
  403. 4: '支付失败',
  404. 5: '订单关闭'
  405. };
  406. return payStatusMap[payState] || '未知';
  407. };
  408. // 确保金额是数字类型
  409. const safeAmount = typeof amount === 'number' ? amount : parseFloat(amount as any) || 0;
  410. const safePayAmount = typeof payAmount === 'number' ? payAmount : parseFloat(payAmount as any) || 0;
  411. const safeFreightAmount = typeof freightAmount === 'number' ? freightAmount : parseFloat(freightAmount as any) || 0;
  412. const variables = {
  413. orderNo,
  414. orderTime: new Date(createdAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
  415. receiverName: recevierName || '客户',
  416. receiverPhone: receiverMobile || '未提供',
  417. phone: receiverMobile || '未提供', // 兼容变量
  418. address: address || '未提供地址',
  419. goodsList: (() => {
  420. const goodsLines = items.map(item => {
  421. const itemName = item.name || '未命名商品';
  422. const itemPrice = typeof item.price === 'number' ? item.price : parseFloat(item.price as any) || 0;
  423. const itemQuantity = typeof item.quantity === 'number' ? item.quantity : parseInt(item.quantity as any, 10) || 0;
  424. const itemTotal = itemPrice * itemQuantity;
  425. return `${itemName} ${itemPrice} × ${itemQuantity} = ${itemTotal.toFixed(2)}`;
  426. }).join('\n') || '暂无商品信息';
  427. // 限制商品列表总长度在500字以内
  428. return limitStringLength(goodsLines, 2000);
  429. })(),
  430. totalAmount: `${safeAmount.toFixed(2)}`,
  431. freightAmount: `${safeFreightAmount.toFixed(2)}`,
  432. payAmount: `${safePayAmount.toFixed(2)}`,
  433. orderStatus: getOrderStatusLabel(state),
  434. payStatus: getPayStatusLabel(payState),
  435. remark: remark || '无备注'
  436. };
  437. // 替换模板变量
  438. let content = template;
  439. for (const [key, value] of Object.entries(variables)) {
  440. content = content.replace(new RegExp(`{${key}}`, 'g'), value);
  441. }
  442. return content.trim();
  443. } catch (error) {
  444. console.warn(`[租户${tenantId}] 生成打印内容失败,使用简单模板:`, error);
  445. // 失败时使用简单的回退模板
  446. const lines = [
  447. '<CB>订单小票</CB><BR>',
  448. '------------------------<BR>',
  449. `<B>订单号:</B>${orderNo}<BR>`,
  450. `<B>下单时间:</B>${new Date(createdAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}<BR>`,
  451. `<B>收货人:</B>${recevierName || '客户'}<BR>`,
  452. `<B>联系电话:</B>${receiverMobile || '未提供'}<BR>`,
  453. `<B>地址:</B>${address || '未提供地址'}<BR>`,
  454. '------------------------<BR>',
  455. '<B>商品明细:</B><BR>'
  456. ];
  457. // 添加商品明细
  458. const goodsDetails: string[] = [];
  459. items.forEach(item => {
  460. const itemPrice = typeof item.price === 'number' ? item.price : parseFloat(item.price as any) || 0;
  461. const itemQuantity = typeof item.quantity === 'number' ? item.quantity : parseInt(item.quantity as any, 10) || 0;
  462. const itemTotal = itemPrice * itemQuantity;
  463. const itemName = item.name || '未命名商品';
  464. goodsDetails.push(`${itemName} x${itemQuantity}<BR>`);
  465. goodsDetails.push(` ¥${itemPrice.toFixed(2)} x ${itemQuantity} = ¥${itemTotal.toFixed(2)}<BR>`);
  466. });
  467. // 限制商品明细总长度在100字以内
  468. const goodsDetailsStr = goodsDetails.join('');
  469. const limitedGoodsDetails = limitStringLength(goodsDetailsStr, 2000);
  470. // 将限制后的商品明细添加到lines中
  471. if (limitedGoodsDetails) {
  472. lines.push(limitedGoodsDetails);
  473. } else {
  474. lines.push('暂无商品信息<BR>');
  475. }
  476. // 添加总计
  477. const safeAmount = typeof amount === 'number' ? amount : parseFloat(amount as any) || 0;
  478. const safePayAmount = typeof payAmount === 'number' ? payAmount : parseFloat(payAmount as any) || 0;
  479. const safeFreightAmount = typeof freightAmount === 'number' ? freightAmount : parseFloat(freightAmount as any) || 0;
  480. lines.push('------------------------<BR>');
  481. lines.push(`<B>商品总额:</B>¥${safeAmount.toFixed(2)}<BR>`);
  482. lines.push(`<B>运费:</B>¥${safeFreightAmount.toFixed(2)}<BR>`);
  483. lines.push(`<B>实付金额:</B>¥${safePayAmount.toFixed(2)}<BR>`);
  484. lines.push('------------------------<BR>');
  485. // 添加备注信息
  486. const displayRemark = remark || '无备注';
  487. lines.push(`<B>订单备注:</B>${displayRemark}<BR>`);
  488. lines.push('------------------------<BR>');
  489. lines.push('<B>感谢您的惠顾!</B><BR>');
  490. lines.push('<QR>https://example.com/order/' + orderNo + '</QR><BR>');
  491. return lines.join('');
  492. }
  493. }
  494. }