print-trigger.service.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  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. } | null> {
  191. try {
  192. const order = await this.orderRepository.findOne({
  193. where: { tenantId, id: orderId },
  194. relations: ['orderGoods'] // 关联订单商品
  195. });
  196. if (!order) {
  197. console.warn(`[租户${tenantId}] 未找到订单,订单ID: ${orderId}`);
  198. return null;
  199. }
  200. // 构建完整的订单信息
  201. return {
  202. orderNo: order.orderNo,
  203. amount: order.amount,
  204. userId: order.userId,
  205. items: order.orderGoods?.map((goods: any) => ({
  206. name: goods.goodsName,
  207. quantity: goods.num,
  208. price: goods.price
  209. })) || [],
  210. // 新增字段
  211. payAmount: order.payAmount,
  212. freightAmount: order.freightAmount,
  213. address: order.address,
  214. receiverMobile: order.receiverMobile,
  215. recevierName: order.recevierName,
  216. state: order.state,
  217. payState: order.payState,
  218. createdAt: order.createdAt
  219. };
  220. } catch (error) {
  221. console.error(`[租户${tenantId}] 获取完整订单信息失败,订单ID: ${orderId}:`, error);
  222. return null;
  223. }
  224. }
  225. /**
  226. * 获取配置值
  227. */
  228. private async getConfigValue(tenantId: number, key: string, defaultValue: string): Promise<string> {
  229. try {
  230. const config = await this.configRepository.findOne({
  231. where: { tenantId, configKey: key }
  232. });
  233. return config?.configValue || defaultValue;
  234. } catch (error) {
  235. console.warn(`[租户${tenantId}] 获取配置失败,key: ${key}:`, error);
  236. return defaultValue;
  237. }
  238. }
  239. /**
  240. * 获取打印模板
  241. */
  242. private async getPrintTemplate(tenantId: number): Promise<string> {
  243. try {
  244. const template = await this.getConfigValue(tenantId, 'feie.receipt_template', '');
  245. if (template) {
  246. return template;
  247. }
  248. // 如果没有配置模板,使用默认模板
  249. return `
  250. <CB>订单收据</CB>
  251. <BR>
  252. 订单号: {orderNo}
  253. 下单时间: {orderTime}
  254. <BR>
  255. <B>收货信息</B>
  256. 收货人: {receiverName}
  257. 联系电话: {receiverPhone}
  258. 收货地址: {address}
  259. <BR>
  260. <B>商品信息</B>
  261. {goodsList}
  262. <BR>
  263. <B>费用明细</B>
  264. 商品总额: {totalAmount}
  265. 运费: {freightAmount}
  266. 实付金额: {payAmount}
  267. <BR>
  268. <B>订单状态</B>
  269. 订单状态: {orderStatus}
  270. 支付状态: {payStatus}
  271. <BR>
  272. <C>感谢您的惠顾!</C>
  273. <BR>
  274. <QR>{orderNo}</QR>
  275. `;
  276. } catch (error) {
  277. console.warn(`[租户${tenantId}] 获取打印模板失败,使用默认模板:`, error);
  278. return `
  279. <CB>订单收据</CB>
  280. <BR>
  281. 订单号: {orderNo}
  282. 下单时间: {orderTime}
  283. <BR>
  284. <B>收货信息</B>
  285. 收货人: {receiverName}
  286. 联系电话: {receiverPhone}
  287. 收货地址: {address}
  288. <BR>
  289. <B>商品信息</B>
  290. {goodsList}
  291. <BR>
  292. <B>费用明细</B>
  293. 商品总额: {totalAmount}
  294. 运费: {freightAmount}
  295. 实付金额: {payAmount}
  296. <BR>
  297. <B>订单状态</B>
  298. 订单状态: {orderStatus}
  299. 支付状态: {payStatus}
  300. <BR>
  301. <C>感谢您的惠顾!</C>
  302. <BR>
  303. <QR>{orderNo}</QR>
  304. `;
  305. }
  306. }
  307. /**
  308. * 生成小票打印内容
  309. */
  310. private async generateReceiptContent(
  311. tenantId: number,
  312. orderInfo: {
  313. orderNo: string;
  314. amount: number;
  315. userId: number;
  316. items?: Array<{
  317. name: string | null;
  318. quantity: number;
  319. price: number;
  320. }>;
  321. // 新增字段
  322. payAmount: number;
  323. freightAmount: number;
  324. address: string | null;
  325. receiverMobile: string | null;
  326. recevierName: string | null;
  327. state: number;
  328. payState: number;
  329. createdAt: Date;
  330. }
  331. ): Promise<string> {
  332. const {
  333. orderNo,
  334. amount,
  335. items = [],
  336. payAmount,
  337. freightAmount,
  338. address,
  339. receiverMobile,
  340. recevierName,
  341. state,
  342. payState,
  343. createdAt
  344. } = orderInfo;
  345. try {
  346. // 1. 获取打印模板
  347. const template = await this.getPrintTemplate(tenantId);
  348. // 2. 准备模板变量
  349. // 状态映射函数
  350. const getOrderStatusLabel = (state: number): string => {
  351. const statusMap: Record<number, string> = {
  352. 0: '未发货',
  353. 1: '已发货',
  354. 2: '收货成功',
  355. 3: '已退货'
  356. };
  357. return statusMap[state] || '未知';
  358. };
  359. const getPayStatusLabel = (payState: number): string => {
  360. const payStatusMap: Record<number, string> = {
  361. 0: '未支付',
  362. 1: '支付中',
  363. 2: '支付成功',
  364. 3: '已退款',
  365. 4: '支付失败',
  366. 5: '订单关闭'
  367. };
  368. return payStatusMap[payState] || '未知';
  369. };
  370. // 确保金额是数字类型
  371. const safeAmount = typeof amount === 'number' ? amount : parseFloat(amount as any) || 0;
  372. const safePayAmount = typeof payAmount === 'number' ? payAmount : parseFloat(payAmount as any) || 0;
  373. const safeFreightAmount = typeof freightAmount === 'number' ? freightAmount : parseFloat(freightAmount as any) || 0;
  374. const variables = {
  375. orderNo,
  376. orderTime: new Date(createdAt).toLocaleString('zh-CN'),
  377. receiverName: recevierName || '客户',
  378. receiverPhone: receiverMobile || '未提供',
  379. phone: receiverMobile || '未提供', // 兼容变量
  380. address: address || '未提供地址',
  381. goodsList: items.map(item => {
  382. const itemName = item.name || '未命名商品';
  383. const itemPrice = typeof item.price === 'number' ? item.price : parseFloat(item.price as any) || 0;
  384. const itemQuantity = typeof item.quantity === 'number' ? item.quantity : parseInt(item.quantity as any, 10) || 0;
  385. const itemTotal = itemPrice * itemQuantity;
  386. return `${itemName} × ${itemQuantity} = ¥${itemTotal.toFixed(2)}`;
  387. }).join('\n') || '暂无商品信息',
  388. totalAmount: `¥${safeAmount.toFixed(2)}`,
  389. freightAmount: `¥${safeFreightAmount.toFixed(2)}`,
  390. payAmount: `¥${safePayAmount.toFixed(2)}`,
  391. orderStatus: getOrderStatusLabel(state),
  392. payStatus: getPayStatusLabel(payState)
  393. };
  394. // 3. 替换模板变量
  395. let content = template;
  396. for (const [key, value] of Object.entries(variables)) {
  397. content = content.replace(new RegExp(`{${key}}`, 'g'), value);
  398. }
  399. return content.trim();
  400. } catch (error) {
  401. console.warn(`[租户${tenantId}] 生成打印内容失败,使用简单模板:`, error);
  402. // 失败时使用简单的回退模板
  403. const lines = [
  404. '<CB>订单小票</CB><BR>',
  405. '------------------------<BR>',
  406. `<B>订单号:</B>${orderNo}<BR>`,
  407. `<B>下单时间:</B>${new Date(createdAt).toLocaleString('zh-CN')}<BR>`,
  408. `<B>收货人:</B>${recevierName || '客户'}<BR>`,
  409. `<B>联系电话:</B>${receiverMobile || '未提供'}<BR>`,
  410. `<B>地址:</B>${address || '未提供地址'}<BR>`,
  411. '------------------------<BR>',
  412. '<B>商品明细:</B><BR>'
  413. ];
  414. // 添加商品明细
  415. items.forEach(item => {
  416. const itemPrice = typeof item.price === 'number' ? item.price : parseFloat(item.price as any) || 0;
  417. const itemQuantity = typeof item.quantity === 'number' ? item.quantity : parseInt(item.quantity as any, 10) || 0;
  418. const itemTotal = itemPrice * itemQuantity;
  419. const itemName = item.name || '未命名商品';
  420. lines.push(`${itemName} x${itemQuantity}<BR>`);
  421. lines.push(` ¥${itemPrice.toFixed(2)} x ${itemQuantity} = ¥${itemTotal.toFixed(2)}<BR>`);
  422. });
  423. // 添加总计
  424. const safeAmount = typeof amount === 'number' ? amount : parseFloat(amount as any) || 0;
  425. const safePayAmount = typeof payAmount === 'number' ? payAmount : parseFloat(payAmount as any) || 0;
  426. const safeFreightAmount = typeof freightAmount === 'number' ? freightAmount : parseFloat(freightAmount as any) || 0;
  427. lines.push('------------------------<BR>');
  428. lines.push(`<B>商品总额:</B>¥${safeAmount.toFixed(2)}<BR>`);
  429. lines.push(`<B>运费:</B>¥${safeFreightAmount.toFixed(2)}<BR>`);
  430. lines.push(`<B>实付金额:</B>¥${safePayAmount.toFixed(2)}<BR>`);
  431. lines.push('------------------------<BR>');
  432. lines.push('<B>感谢您的惠顾!</B><BR>');
  433. lines.push('<QR>https://example.com/order/' + orderNo + '</QR><BR>');
  434. return lines.join('');
  435. }
  436. }
  437. }