Просмотр исходного кода

✨ feat(feie-printer): 添加鉴权中间件并优化打印任务处理

- 添加@d8d/auth-module-mt依赖并集成鉴权中间件到飞鹅打印机路由
- 优化打印任务服务:减少最大重试次数为1次,添加打印状态检查防止重复执行
- 处理飞鹅API订单号重复错误,自动标记为打印成功
- 扩展订单信息查询,包含备注字段并添加到打印模板
- 优化打印内容生成:添加字符串长度限制函数,防止打印内容过长
- 统一模板变量替换逻辑,支持多种占位符格式({key}、{ key }、{{key}}等)
- 修复{remark}变量替换问题,添加健壮的清理逻辑
- 更新订单管理UI,启用支付成功触发按钮并优化模板变量处理
- 添加多个测试文件验证修复效果和健壮性
yourname 1 месяц назад
Родитель
Сommit
9853b44411

+ 1 - 0
packages/feie-printer-module-mt/package.json

@@ -56,6 +56,7 @@
     "@d8d/shared-crud": "workspace:*",
     "@d8d/shared-crud": "workspace:*",
     "@d8d/orders-module-mt": "workspace:*",
     "@d8d/orders-module-mt": "workspace:*",
     "@d8d/tenant-module-mt": "workspace:*",
     "@d8d/tenant-module-mt": "workspace:*",
+    "@d8d/auth-module-mt": "workspace:*",
     "@hono/zod-openapi": "^1.0.2",
     "@hono/zod-openapi": "^1.0.2",
     "typeorm": "^0.3.20",
     "typeorm": "^0.3.20",
     "zod": "^4.1.12",
     "zod": "^4.1.12",

+ 4 - 0
packages/feie-printer-module-mt/src/routes/feie.routes.ts

@@ -1,6 +1,7 @@
 import { OpenAPIHono } from '@hono/zod-openapi';
 import { OpenAPIHono } from '@hono/zod-openapi';
 import { DataSource, In } from 'typeorm';
 import { DataSource, In } from 'typeorm';
 import { AuthContext } from '@d8d/shared-types';
 import { AuthContext } from '@d8d/shared-types';
+import { authMiddleware } from '@d8d/auth-module-mt';
 import { PrinterService } from '../services/printer.service';
 import { PrinterService } from '../services/printer.service';
 import { PrintTaskService } from '../services/print-task.service';
 import { PrintTaskService } from '../services/print-task.service';
 import { DelaySchedulerService } from '../services/delay-scheduler.service';
 import { DelaySchedulerService } from '../services/delay-scheduler.service';
@@ -73,6 +74,9 @@ async function getFeieApiConfig(tenantId: number, dataSource: DataSource): Promi
 export function createFeieRoutes(dataSource: DataSource) {
 export function createFeieRoutes(dataSource: DataSource) {
   const app = new OpenAPIHono<AuthContext>();
   const app = new OpenAPIHono<AuthContext>();
 
 
+  // 添加鉴权中间件
+  app.use('*', authMiddleware);
+
   // 初始化服务(使用空配置,实际配置在路由处理中动态获取)
   // 初始化服务(使用空配置,实际配置在路由处理中动态获取)
   const printTaskService = new PrintTaskService(dataSource, {
   const printTaskService = new PrintTaskService(dataSource, {
     baseUrl: 'https://api.feieyun.cn/Api/Open/',
     baseUrl: 'https://api.feieyun.cn/Api/Open/',

+ 28 - 2
packages/feie-printer-module-mt/src/services/print-task.service.ts

@@ -131,8 +131,8 @@ export class PrintTaskService extends GenericCrudService<FeiePrintTaskMt> {
    * 获取最大重试次数
    * 获取最大重试次数
    */
    */
   private async getMaxRetries(tenantId: number): Promise<number> {
   private async getMaxRetries(tenantId: number): Promise<number> {
-    // 使用固定值3
-    return 3;
+    // 使用固定值1
+    return 1;
   }
   }
 
 
   /**
   /**
@@ -156,6 +156,12 @@ export class PrintTaskService extends GenericCrudService<FeiePrintTaskMt> {
       throw new Error('打印任务已完成');
       throw new Error('打印任务已完成');
     }
     }
 
 
+    // 检查任务是否已经在打印中(防止重复执行)
+    if (task.printStatus === PrintStatus.PRINTING) {
+      console.warn(`[租户${tenantId}] 打印任务 ${taskId} 已经在打印中,跳过重复执行`);
+      return task;
+    }
+
     // 更新状态为打印中
     // 更新状态为打印中
     await this.update(task.id, {
     await this.update(task.id, {
       printStatus: PrintStatus.PRINTING,
       printStatus: PrintStatus.PRINTING,
@@ -186,6 +192,26 @@ export class PrintTaskService extends GenericCrudService<FeiePrintTaskMt> {
       // 处理打印失败
       // 处理打印失败
       const errorMessage = error instanceof Error ? error.message : '打印失败';
       const errorMessage = error instanceof Error ? error.message : '打印失败';
 
 
+      // 检查是否是订单重复错误(飞鹅API错误代码 -6)
+      // 如果订单号重复,说明飞鹅那边已经打印成功,只是本地不知道
+      if (errorMessage.includes('错误代码: -6') || errorMessage.includes('订单号重复')) {
+        console.log(`[租户${tenantId}] 打印任务 ${taskId} 订单号重复,飞鹅API已打印,标记为成功`);
+
+        // 更新任务状态为成功(因为飞鹅那边已经打印了)
+        const updatedTask = await this.update(task.id, {
+          printStatus: PrintStatus.SUCCESS,
+          printedAt: new Date(),
+          errorMessage: '订单号重复,飞鹅API已打印',
+          retryCount: task.retryCount + 1
+        });
+
+        if (!updatedTask) {
+          throw new Error('更新打印任务状态失败');
+        }
+
+        return updatedTask;
+      }
+
       // 获取配置的最大重试次数
       // 获取配置的最大重试次数
       const maxRetries = await this.getMaxRetries(tenantId);
       const maxRetries = await this.getMaxRetries(tenantId);
 
 

+ 84 - 18
packages/feie-printer-module-mt/src/services/print-trigger.service.ts

@@ -212,6 +212,7 @@ export class PrintTriggerService {
     state: number;
     state: number;
     payState: number;
     payState: number;
     createdAt: Date;
     createdAt: Date;
+    remark: string | null;
   } | null> {
   } | null> {
     try {
     try {
       const order = await this.orderRepository.findOne({
       const order = await this.orderRepository.findOne({
@@ -242,7 +243,8 @@ export class PrintTriggerService {
         recevierName: order.recevierName,
         recevierName: order.recevierName,
         state: order.state,
         state: order.state,
         payState: order.payState,
         payState: order.payState,
-        createdAt: order.createdAt
+        createdAt: order.createdAt,
+        remark: order.remark
       };
       };
     } catch (error) {
     } catch (error) {
       console.error(`[租户${tenantId}] 获取完整订单信息失败,订单ID: ${orderId}:`, error);
       console.error(`[租户${tenantId}] 获取完整订单信息失败,订单ID: ${orderId}:`, error);
@@ -301,6 +303,9 @@ export class PrintTriggerService {
 订单状态: {orderStatus}
 订单状态: {orderStatus}
 支付状态: {payStatus}
 支付状态: {payStatus}
 <BR>
 <BR>
+<B>订单备注</B>
+{remark}
+<BR>
 <C>感谢您的惠顾!</C>
 <C>感谢您的惠顾!</C>
 <BR>
 <BR>
 <QR>{orderNo}</QR>
 <QR>{orderNo}</QR>
@@ -330,6 +335,9 @@ export class PrintTriggerService {
 订单状态: {orderStatus}
 订单状态: {orderStatus}
 支付状态: {payStatus}
 支付状态: {payStatus}
 <BR>
 <BR>
+<B>订单备注</B>
+{remark}
+<BR>
 <C>感谢您的惠顾!</C>
 <C>感谢您的惠顾!</C>
 <BR>
 <BR>
 <QR>{orderNo}</QR>
 <QR>{orderNo}</QR>
@@ -360,6 +368,7 @@ export class PrintTriggerService {
       state: number;
       state: number;
       payState: number;
       payState: number;
       createdAt: Date;
       createdAt: Date;
+      remark: string | null;
     }
     }
   ): Promise<string> {
   ): Promise<string> {
     const {
     const {
@@ -373,9 +382,44 @@ export class PrintTriggerService {
       recevierName,
       recevierName,
       state,
       state,
       payState,
       payState,
-      createdAt
+      createdAt,
+      remark
     } = orderInfo;
     } = orderInfo;
 
 
+    // 字符串长度限制函数(按中文字符计算)
+    const limitStringLength = (str: string, maxLength: number): string => {
+      if (!str || str.length <= maxLength) {
+        return str;
+      }
+
+      // 计算字符长度(中文字符算1个长度)
+      let length = 0;
+      let result = '';
+
+      for (const char of str) {
+        // 中文字符范围判断
+        const charCode = char.charCodeAt(0);
+        if (charCode >= 0x4E00 && charCode <= 0x9FFF) {
+          length += 1; // 中文字符
+        } else {
+          length += 0.5; // 英文字符和数字算半个长度
+        }
+
+        if (length <= maxLength) {
+          result += char;
+        } else {
+          break;
+        }
+      }
+
+      // 如果被截断,添加省略号
+      if (result.length < str.length) {
+        result += '...';
+      }
+
+      return result;
+    };
+
     try {
     try {
       // 1. 获取打印模板
       // 1. 获取打印模板
       const template = await this.getPrintTemplate(tenantId);
       const template = await this.getPrintTemplate(tenantId);
@@ -411,26 +455,32 @@ export class PrintTriggerService {
 
 
       const variables = {
       const variables = {
         orderNo,
         orderNo,
-        orderTime: new Date(createdAt).toLocaleString('zh-CN'),
+        orderTime: new Date(createdAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
         receiverName: recevierName || '客户',
         receiverName: recevierName || '客户',
         receiverPhone: receiverMobile || '未提供',
         receiverPhone: receiverMobile || '未提供',
         phone: receiverMobile || '未提供', // 兼容变量
         phone: receiverMobile || '未提供', // 兼容变量
         address: address || '未提供地址',
         address: address || '未提供地址',
-        goodsList: items.map(item => {
-          const itemName = item.name || '未命名商品';
-          const itemPrice = typeof item.price === 'number' ? item.price : parseFloat(item.price as any) || 0;
-          const itemQuantity = typeof item.quantity === 'number' ? item.quantity : parseInt(item.quantity as any, 10) || 0;
-          const itemTotal = itemPrice * itemQuantity;
-          return `${itemName} × ${itemQuantity} = ¥${itemTotal.toFixed(2)}`;
-        }).join('\n') || '暂无商品信息',
-        totalAmount: `¥${safeAmount.toFixed(2)}`,
-        freightAmount: `¥${safeFreightAmount.toFixed(2)}`,
-        payAmount: `¥${safePayAmount.toFixed(2)}`,
+        goodsList: (() => {
+          const goodsLines = items.map(item => {
+            const itemName = item.name || '未命名商品';
+            const itemPrice = typeof item.price === 'number' ? item.price : parseFloat(item.price as any) || 0;
+            const itemQuantity = typeof item.quantity === 'number' ? item.quantity : parseInt(item.quantity as any, 10) || 0;
+            const itemTotal = itemPrice * itemQuantity;
+            return `${itemName}  ${itemPrice} × ${itemQuantity} = ${itemTotal.toFixed(2)}`;
+          }).join('\n') || '暂无商品信息';
+
+          // 限制商品列表总长度在500字以内
+          return limitStringLength(goodsLines, 2000);
+        })(),
+        totalAmount: `${safeAmount.toFixed(2)}`,
+        freightAmount: `${safeFreightAmount.toFixed(2)}`,
+        payAmount: `${safePayAmount.toFixed(2)}`,
         orderStatus: getOrderStatusLabel(state),
         orderStatus: getOrderStatusLabel(state),
-        payStatus: getPayStatusLabel(payState)
+        payStatus: getPayStatusLabel(payState),
+        remark: remark || '无备注'
       };
       };
 
 
-      // 3. 替换模板变量
+      // 替换模板变量
       let content = template;
       let content = template;
       for (const [key, value] of Object.entries(variables)) {
       for (const [key, value] of Object.entries(variables)) {
         content = content.replace(new RegExp(`{${key}}`, 'g'), value);
         content = content.replace(new RegExp(`{${key}}`, 'g'), value);
@@ -445,7 +495,7 @@ export class PrintTriggerService {
         '<CB>订单小票</CB><BR>',
         '<CB>订单小票</CB><BR>',
         '------------------------<BR>',
         '------------------------<BR>',
         `<B>订单号:</B>${orderNo}<BR>`,
         `<B>订单号:</B>${orderNo}<BR>`,
-        `<B>下单时间:</B>${new Date(createdAt).toLocaleString('zh-CN')}<BR>`,
+        `<B>下单时间:</B>${new Date(createdAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}<BR>`,
         `<B>收货人:</B>${recevierName || '客户'}<BR>`,
         `<B>收货人:</B>${recevierName || '客户'}<BR>`,
         `<B>联系电话:</B>${receiverMobile || '未提供'}<BR>`,
         `<B>联系电话:</B>${receiverMobile || '未提供'}<BR>`,
         `<B>地址:</B>${address || '未提供地址'}<BR>`,
         `<B>地址:</B>${address || '未提供地址'}<BR>`,
@@ -454,15 +504,27 @@ export class PrintTriggerService {
       ];
       ];
 
 
       // 添加商品明细
       // 添加商品明细
+      const goodsDetails: string[] = [];
       items.forEach(item => {
       items.forEach(item => {
         const itemPrice = typeof item.price === 'number' ? item.price : parseFloat(item.price as any) || 0;
         const itemPrice = typeof item.price === 'number' ? item.price : parseFloat(item.price as any) || 0;
         const itemQuantity = typeof item.quantity === 'number' ? item.quantity : parseInt(item.quantity as any, 10) || 0;
         const itemQuantity = typeof item.quantity === 'number' ? item.quantity : parseInt(item.quantity as any, 10) || 0;
         const itemTotal = itemPrice * itemQuantity;
         const itemTotal = itemPrice * itemQuantity;
         const itemName = item.name || '未命名商品';
         const itemName = item.name || '未命名商品';
-        lines.push(`${itemName} x${itemQuantity}<BR>`);
-        lines.push(`  ¥${itemPrice.toFixed(2)} x ${itemQuantity} = ¥${itemTotal.toFixed(2)}<BR>`);
+        goodsDetails.push(`${itemName} x${itemQuantity}<BR>`);
+        goodsDetails.push(`  ¥${itemPrice.toFixed(2)} x ${itemQuantity} = ¥${itemTotal.toFixed(2)}<BR>`);
       });
       });
 
 
+      // 限制商品明细总长度在100字以内
+      const goodsDetailsStr = goodsDetails.join('');
+      const limitedGoodsDetails = limitStringLength(goodsDetailsStr, 2000);
+
+      // 将限制后的商品明细添加到lines中
+      if (limitedGoodsDetails) {
+        lines.push(limitedGoodsDetails);
+      } else {
+        lines.push('暂无商品信息<BR>');
+      }
+
       // 添加总计
       // 添加总计
       const safeAmount = typeof amount === 'number' ? amount : parseFloat(amount as any) || 0;
       const safeAmount = typeof amount === 'number' ? amount : parseFloat(amount as any) || 0;
       const safePayAmount = typeof payAmount === 'number' ? payAmount : parseFloat(payAmount as any) || 0;
       const safePayAmount = typeof payAmount === 'number' ? payAmount : parseFloat(payAmount as any) || 0;
@@ -473,6 +535,10 @@ export class PrintTriggerService {
       lines.push(`<B>运费:</B>¥${safeFreightAmount.toFixed(2)}<BR>`);
       lines.push(`<B>运费:</B>¥${safeFreightAmount.toFixed(2)}<BR>`);
       lines.push(`<B>实付金额:</B>¥${safePayAmount.toFixed(2)}<BR>`);
       lines.push(`<B>实付金额:</B>¥${safePayAmount.toFixed(2)}<BR>`);
       lines.push('------------------------<BR>');
       lines.push('------------------------<BR>');
+      // 添加备注信息
+      const displayRemark = remark || '无备注';
+      lines.push(`<B>订单备注:</B>${displayRemark}<BR>`);
+      lines.push('------------------------<BR>');
       lines.push('<B>感谢您的惠顾!</B><BR>');
       lines.push('<B>感谢您的惠顾!</B><BR>');
       lines.push('<QR>https://example.com/order/' + orderNo + '</QR><BR>');
       lines.push('<QR>https://example.com/order/' + orderNo + '</QR><BR>');
 
 

+ 45 - 9
packages/order-management-ui-mt/src/components/OrderManagement.tsx

@@ -1190,6 +1190,9 @@ const sendDeliverySuccessNotification = async (order: OrderResponse, deliveryDat
 订单状态: {orderStatus}
 订单状态: {orderStatus}
 支付状态: {payStatus}
 支付状态: {payStatus}
 <BR>
 <BR>
+<B>订单备注</B>
+{remark}
+<BR>
 <C>感谢您的惠顾!</C>
 <C>感谢您的惠顾!</C>
 <BR>
 <BR>
 <QR>{orderNo}</QR>
 <QR>{orderNo}</QR>
@@ -1205,19 +1208,52 @@ const sendDeliverySuccessNotification = async (order: OrderResponse, deliveryDat
       phone: order.receiverMobile || '-', // 添加phone变量,兼容两种模板格式
       phone: order.receiverMobile || '-', // 添加phone变量,兼容两种模板格式
       address: order.address || '-',
       address: order.address || '-',
       goodsList: order.orderGoods?.map(item =>
       goodsList: order.orderGoods?.map(item =>
-        `${item.goodsName} × ${item.num} = ¥${(item.price * item.num).toFixed(2)}`
+        `${item.goodsName}  ${item.price} × ${item.num} = ${(item.price * item.num).toFixed(2)}`
       ).join('\n') || '暂无商品信息',
       ).join('\n') || '暂无商品信息',
-      totalAmount: `¥${order.amount.toFixed(2)}`,
-      freightAmount: `¥${order.freightAmount.toFixed(2)}`,
-      payAmount: `¥${order.payAmount.toFixed(2)}`,
+      totalAmount: `${order.amount.toFixed(2)}`,
+      freightAmount: `${order.freightAmount.toFixed(2)}`,
+      payAmount: `${order.payAmount.toFixed(2)}`,
       orderStatus: orderStatusMap[order.state as keyof typeof orderStatusMap]?.label || '未知',
       orderStatus: orderStatusMap[order.state as keyof typeof orderStatusMap]?.label || '未知',
-      payStatus: payStatusMap[order.payState as keyof typeof payStatusMap]?.label || '未知'
+      payStatus: payStatusMap[order.payState as keyof typeof payStatusMap]?.label || '未知',
+      remark: order.remark || '无备注'
     };
     };
 
 
-    // 替换模板变量
+    // 替换模板变量 - 使用更健壮的替换方法
     let content = template;
     let content = template;
+
+    // 方法1: 标准替换
     for (const [key, value] of Object.entries(variables)) {
     for (const [key, value] of Object.entries(variables)) {
-      content = content.replace(new RegExp(`{${key}}`, 'g'), value);
+      // 处理多种格式的占位符:{key}、{ key }、{{key}}等
+      const patterns = [
+        `{${key}}`,           // 标准格式
+        `{ ${key} }`,         // 有空格
+        `{{${key}}}`,         // 双大括号
+        `{{ ${key} }}`,       // 双大括号有空格
+        `{${key} }`,          // 右空格
+        `{ ${key}}`,          // 左空格
+      ];
+
+      for (const pattern of patterns) {
+        if (content.includes(pattern)) {
+          content = content.split(pattern).join(value);
+        }
+      }
+    }
+
+    // 方法2: 清理未替换的变量(特别是remark)
+    // 使用正则表达式匹配各种格式的{remark}
+    const remarkPatterns = [
+      /\{remark\}/g,
+      /\{\s*remark\s*\}/g,
+      /\{\{remark\}\}/g,
+      /\{\{\s*remark\s*\}\}/g,
+    ];
+
+    for (const pattern of remarkPatterns) {
+      if (pattern.test(content)) {
+        const safeRemark = variables.remark || '无备注';
+        content = content.replace(pattern, safeRemark);
+      }
     }
     }
 
 
     return content.trim();
     return content.trim();
@@ -1532,7 +1568,7 @@ const sendDeliverySuccessNotification = async (order: OrderResponse, deliveryDat
                             <Printer className="h-4 w-4" />
                             <Printer className="h-4 w-4" />
                           )}
                           )}
                         </Button>
                         </Button>
-                        {/* <Button
+                        <Button
                           variant="ghost"
                           variant="ghost"
                           size="icon"
                           size="icon"
                           onClick={() => handleTriggerPaymentSuccess(order)}
                           onClick={() => handleTriggerPaymentSuccess(order)}
@@ -1545,7 +1581,7 @@ const sendDeliverySuccessNotification = async (order: OrderResponse, deliveryDat
                           ) : (
                           ) : (
                             <Play className="h-4 w-4" />
                             <Play className="h-4 w-4" />
                           )}
                           )}
-                        </Button> */}
+                        </Button> 
 
 
                         <Button
                         <Button
                           variant="ghost"
                           variant="ghost"

+ 3 - 0
pnpm-lock.yaml

@@ -1902,6 +1902,9 @@ importers:
 
 
   packages/feie-printer-module-mt:
   packages/feie-printer-module-mt:
     dependencies:
     dependencies:
+      '@d8d/auth-module-mt':
+        specifier: workspace:*
+        version: link:../auth-module-mt
       '@d8d/orders-module-mt':
       '@d8d/orders-module-mt':
         specifier: workspace:*
         specifier: workspace:*
         version: link:../orders-module-mt
         version: link:../orders-module-mt

+ 107 - 0
test-actual-fix.js

@@ -0,0 +1,107 @@
+// 实际修复测试 - 模拟真实场景
+console.log('=== 实际修复测试 ===\n');
+
+// 模拟实际打印内容(从数据库查询的结果)
+const actualPrintedContent = `订单号: ORD20251210112844577127
+时间: 2025-12-10 19:28:44
+商品: 洗护产品6 × 1 = ¥0.01
+合计: ¥0.01
+备注: {remark}
+收货人姓名: 1
+地址: 1
+联系电话: 13100000000`;
+
+console.log('实际打印内容:');
+console.log(actualPrintedContent);
+console.log('\n是否包含 {remark}:', actualPrintedContent.includes('{remark}'));
+
+// 修复逻辑测试
+function testFix() {
+  console.log('\n=== 测试修复逻辑 ===');
+
+  // 场景1: 模板是 备注: {remark}
+  const template1 = `订单号: {orderNo}
+时间: {orderTime}
+商品: {goodsList}
+合计: {payAmount}
+备注: {remark}
+收货人姓名: {receiverName}
+地址: {address}
+联系电话: {receiverPhone}`;
+
+  // 场景2: 模板是 备注1: {remark}(数据库中的)
+  const template2 = `订单号: {orderNo}
+时间: {orderTime}
+商品: {goodsList}
+合计: {totalAmount}
+备注1: {remark}
+收货人姓名: {receiverName}
+地址: {address}
+联系电话: {phone}`;
+
+  const variables = {
+    orderNo: 'TEST123',
+    orderTime: '2025-12-10 12:00:00',
+    goodsList: '测试商品 × 1 = ¥1.00',
+    payAmount: '¥1.00',
+    totalAmount: '¥1.00',
+    remark: '测试备注内容',
+    receiverName: '张三',
+    address: '测试地址',
+    receiverPhone: '13800138000',
+    phone: '13800138000'
+  };
+
+  // 测试修复后的替换逻辑
+  function applyFix(template, vars) {
+    let content = template;
+
+    // 1. 标准替换
+    for (const [key, value] of Object.entries(vars)) {
+      const safeValue = value != null ? String(value) : '无';
+      const placeholder = `{${key}}`;
+      if (content.includes(placeholder)) {
+        content = content.split(placeholder).join(safeValue);
+      }
+    }
+
+    // 2. 清理未替换的 {remark}
+    const remarkPatterns = [
+      /\{remark\}/g,
+      /\{\s*remark\s*\}/g,
+    ];
+
+    for (const pattern of remarkPatterns) {
+      if (pattern.test(content)) {
+        const safeRemark = vars.remark != null ? String(vars.remark) : '无';
+        content = content.replace(pattern, safeRemark);
+      }
+    }
+
+    return content;
+  }
+
+  console.log('\n1. 模板: 备注: {remark}');
+  const result1 = applyFix(template1, variables);
+  console.log('结果:', result1.includes('{remark}') ? '❌ 仍有 {remark}' : '✅ 已替换');
+  console.log(result1);
+
+  console.log('\n2. 模板: 备注1: {remark}');
+  const result2 = applyFix(template2, variables);
+  console.log('结果:', result2.includes('{remark}') ? '❌ 仍有 {remark}' : '✅ 已替换');
+  console.log(result2);
+
+  console.log('\n3. 空备注测试');
+  const emptyVars = { ...variables, remark: null };
+  const result3 = applyFix(template1, emptyVars);
+  console.log('结果:', result3.includes('{remark}') ? '❌ 仍有 {remark}' : '✅ 已替换');
+  console.log(result3);
+}
+
+testFix();
+
+console.log('\n\n=== 建议操作 ===');
+console.log('1. 重启服务清除缓存');
+console.log('2. 更新数据库模板为正确格式');
+console.log('3. 创建一个测试订单验证修复效果');
+console.log('4. 检查是否有其他代码修改模板');

+ 138 - 0
test-final-fix.js

@@ -0,0 +1,138 @@
+// 最终修复测试
+console.log('=== 最终修复测试 ===\n');
+
+// 模拟修复后的替换逻辑
+function finalRobustReplace(template, variables, tenantId = 1) {
+  let content = template;
+  console.log(`[租户${tenantId}] 模板变量替换前:`, {
+    templateContainsRemark: template.includes('{remark}'),
+    templatePreview: template.substring(0, 100) + (template.length > 100 ? '...' : '')
+  });
+
+  // 使用更健壮的替换方法,处理各种格式的占位符
+  for (const [key, value] of Object.entries(variables)) {
+    // 处理多种格式的占位符:{key}、{ key }、{{key}}等
+    const patterns = [
+      `{${key}}`,           // 标准格式
+      `{ ${key} }`,         // 有空格
+      `{{${key}}}`,         // 双大括号
+      `{{ ${key} }}`,       // 双大括号有空格
+      `{${key} }`,          // 右空格
+      `{ ${key}}`,          // 左空格
+    ];
+
+    // 确保替换值不是null或undefined
+    const safeValue = value != null ? String(value) : '无备注';
+
+    for (const pattern of patterns) {
+      if (content.includes(pattern)) {
+        content = content.split(pattern).join(safeValue);
+        console.log(`[租户${tenantId}] 替换变量 ${key} (模式: ${pattern}): ${safeValue}`);
+      }
+    }
+  }
+
+  // 清理未替换的变量(特别是remark)
+  // 使用正则表达式匹配各种格式的{remark}
+  const remarkPatterns = [
+    /\{remark\}/g,
+    /\{\s*remark\s*\}/g,
+    /\{\{remark\}\}/g,
+    /\{\{\s*remark\s*\}\}/g,
+  ];
+
+  let hasUnreplacedRemark = false;
+  for (const pattern of remarkPatterns) {
+    if (pattern.test(content)) {
+      hasUnreplacedRemark = true;
+      const safeRemark = variables.remark != null ? String(variables.remark) : '无备注';
+      console.log(`[租户${tenantId}] 模板中仍有未替换的remark占位符(模式: ${pattern.source}),使用默认值替换`);
+      content = content.replace(pattern, safeRemark);
+    }
+  }
+
+  // 记录是否有未替换的remark
+  if (hasUnreplacedRemark) {
+    console.log(`[租户${tenantId}] 清理了未替换的remark占位符`);
+  }
+
+  console.log(`[租户${tenantId}] 模板变量替换后,是否还有{remark}:`, content.includes('{remark}') || content.includes('{ remark }'));
+
+  return content;
+}
+
+// 测试实际场景
+console.log('\n=== 实际场景测试 ===');
+
+// 场景1: 数据库模板(备注1: {remark})
+const dbTemplate = `订单号: {orderNo}
+时间: {orderTime}
+商品: {goodsList}
+合计: {totalAmount}
+备注1: {remark}
+收货人姓名: {receiverName}
+地址: {address}
+联系电话: {phone}`;
+
+// 场景2: 实际打印模板(备注: {remark})
+const printTemplate = `订单号: {orderNo}
+时间: {orderTime}
+商品: {goodsList}
+合计: {payAmount}
+备注: {remark}
+收货人姓名: {receiverName}
+地址: {address}
+联系电话: {receiverPhone}`;
+
+const testVariables = {
+  orderNo: 'ORD20251210112844577127',
+  orderTime: '2025-12-10 19:28:44',
+  goodsList: '洗护产品6 × 1 = ¥0.01',
+  totalAmount: '¥0.01',
+  payAmount: '¥0.01',
+  remark: '测试备注',
+  receiverName: '1',
+  address: '1',
+  phone: '13100000000',
+  receiverPhone: '13100000000'
+};
+
+console.log('\n1. 数据库模板测试:');
+const result1 = finalRobustReplace(dbTemplate, testVariables);
+console.log('\n结果:');
+console.log(result1);
+
+console.log('\n2. 打印模板测试:');
+const result2 = finalRobustReplace(printTemplate, testVariables);
+console.log('\n结果:');
+console.log(result2);
+
+console.log('\n3. 空备注测试:');
+const emptyVars = { ...testVariables, remark: null };
+const result3 = finalRobustReplace(printTemplate, emptyVars);
+console.log('\n结果:');
+console.log(result3);
+
+console.log('\n4. 空字符串备注测试:');
+const emptyStrVars = { ...testVariables, remark: '' };
+const result4 = finalRobustReplace(printTemplate, emptyStrVars);
+console.log('\n结果:');
+console.log(result4);
+
+console.log('\n5. 字符串"null"备注测试:');
+const nullStrVars = { ...testVariables, remark: 'null' };
+const result5 = finalRobustReplace(printTemplate, nullStrVars);
+console.log('\n结果:');
+console.log(result5);
+
+// 检查实际数据库中的模板
+console.log('\n\n=== 数据库模板分析 ===');
+console.log('数据库模板:', dbTemplate);
+console.log('实际打印模板:', printTemplate);
+console.log('\n差异分析:');
+console.log('1. 数据库: 备注1: {remark}');
+console.log('2. 打印:   备注: {remark}');
+console.log('\n可能的原因:');
+console.log('1. 模板被代码修改了');
+console.log('2. 有缓存机制');
+console.log('3. 模板配置被更新了但代码用了旧版本');

+ 142 - 0
test-fix-remark.js

@@ -0,0 +1,142 @@
+// 测试修复后的 {remark} 替换逻辑
+console.log('=== 测试修复后的 {remark} 替换逻辑 ===\n');
+
+// 模拟数据库中的模板(实际配置)
+const databaseTemplate = `订单号: {orderNo}
+时间: {orderTime}
+商品: {goodsList}
+合计: {totalAmount}
+备注1: {remark}
+收货人姓名: {receiverName}
+地址: {address}
+联系电话: {phone}`;
+
+// 模拟打印内容中的模板(实际打印出来的)
+const printedTemplate = `订单号: {orderNo}
+时间: {orderTime}
+商品: {goodsList}
+合计: {payAmount}
+备注: {remark}
+收货人姓名: {receiverName}
+地址: {address}
+联系电话: {receiverPhone}`;
+
+// 模拟变量
+const variables = {
+  orderNo: 'ORD20251210112844577127',
+  orderTime: '2025-12-10 19:28:44',
+  goodsList: '洗护产品6 × 1 = ¥0.01',
+  totalAmount: '¥0.01',
+  payAmount: '¥0.01',
+  remark: '测试备注',
+  receiverName: '1',
+  address: '1',
+  phone: '13100000000',
+  receiverPhone: '13100000000'
+};
+
+// 修复后的替换逻辑
+function robustReplace(template, variables, tenantId = 1) {
+  let content = template;
+  console.log(`[租户${tenantId}] 模板变量替换前:`, {
+    templateContainsRemark: template.includes('{remark}'),
+    templatePreview: template.substring(0, 100) + (template.length > 100 ? '...' : '')
+  });
+
+  // 使用更健壮的替换方法,处理各种格式的占位符
+  for (const [key, value] of Object.entries(variables)) {
+    // 处理多种格式的占位符:{key}、{ key }、{{key}}等
+    const patterns = [
+      `{${key}}`,           // 标准格式
+      `{ ${key} }`,         // 有空格
+      `{{${key}}}`,         // 双大括号
+      `{{ ${key} }}`,       // 双大括号有空格
+      `{${key} }`,          // 右空格
+      `{ ${key}}`,          // 左空格
+    ];
+
+    for (const pattern of patterns) {
+      if (content.includes(pattern)) {
+        content = content.split(pattern).join(value);
+        console.log(`[租户${tenantId}] 替换变量 ${key} (模式: ${pattern}): ${value}`);
+      }
+    }
+  }
+
+  // 清理未替换的变量(特别是remark)
+  // 使用正则表达式匹配各种格式的{remark}
+  const remarkPatterns = [
+    /\{remark\}/g,
+    /\{\s*remark\s*\}/g,
+    /\{\{remark\}\}/g,
+    /\{\{\s*remark\s*\}\}/g,
+  ];
+
+  let hasUnreplacedRemark = false;
+  for (const pattern of remarkPatterns) {
+    if (pattern.test(content)) {
+      hasUnreplacedRemark = true;
+      console.log(`[租户${tenantId}] 模板中仍有未替换的remark占位符(模式: ${pattern.source}),使用默认值替换`);
+      content = content.replace(pattern, variables.remark || '无备注');
+    }
+  }
+
+  // 记录是否有未替换的remark
+  if (hasUnreplacedRemark) {
+    console.log(`[租户${tenantId}] 清理了未替换的remark占位符`);
+  }
+
+  console.log(`[租户${tenantId}] 模板变量替换后,是否还有{remark}:`, content.includes('{remark}') || content.includes('{ remark }'));
+
+  return content;
+}
+
+// 测试1: 数据库模板
+console.log('\n=== 测试1: 数据库模板 (备注1: {remark}) ===');
+const result1 = robustReplace(databaseTemplate, variables);
+console.log('\n替换结果:');
+console.log(result1);
+
+// 测试2: 打印模板
+console.log('\n\n=== 测试2: 打印模板 (备注: {remark}) ===');
+const result2 = robustReplace(printedTemplate, variables);
+console.log('\n替换结果:');
+console.log(result2);
+
+// 测试3: 空备注
+console.log('\n\n=== 测试3: 空备注测试 ===');
+const emptyVariables = {
+  ...variables,
+  remark: null
+};
+const result3 = robustReplace(printedTemplate, emptyVariables);
+console.log('\n替换结果:');
+console.log(result3);
+
+// 测试4: 有空格的情况
+console.log('\n\n=== 测试4: 有空格的情况 ===');
+const spacedTemplate = `订单号: { orderNo }
+时间: {orderTime }
+商品: { goodsList }
+合计: {totalAmount }
+备注: { remark }
+收货人姓名: {receiverName}
+地址: { address }
+联系电话: { phone }`;
+const result4 = robustReplace(spacedTemplate, variables);
+console.log('\n替换结果:');
+console.log(result4);
+
+// 测试5: 双大括号情况
+console.log('\n\n=== 测试5: 双大括号情况 ===');
+const doubleBraceTemplate = `订单号: {{orderNo}}
+时间: {{orderTime}}
+商品: {{goodsList}}
+合计: {{totalAmount}}
+备注: {{remark}}
+收货人姓名: {{receiverName}}
+地址: {{address}}
+联系电话: {{phone}}`;
+const result5 = robustReplace(doubleBraceTemplate, variables);
+console.log('\n替换结果:');
+console.log(result5);

+ 128 - 0
test-remark-replace.js

@@ -0,0 +1,128 @@
+// 测试 {remark} 替换问题
+console.log('=== 测试 {remark} 替换问题 ===\n');
+
+// 模拟打印模板
+const template1 = `订单号: {orderNo}
+时间: {orderTime}
+商品: {goodsList}
+合计: {payAmount}
+备注: {remark}
+收货人姓名: {receiverName}
+地址: {address}
+联系电话: {receiverPhone}`;
+
+const template2 = `订单号: {orderNo}
+时间: {orderTime}
+商品: {goodsList}
+合计: {payAmount}
+备注: { remark }  // 有空格
+收货人姓名: {receiverName}
+地址: {address}
+联系电话: {receiverPhone}`;
+
+const template3 = `订单号: {orderNo}
+时间: {orderTime}
+商品: {goodsList}
+合计: {payAmount}
+备注: {{remark}}  // 双大括号
+收货人姓名: {receiverName}
+地址: {address}
+联系电话: {receiverPhone}`;
+
+// 模拟变量
+const variables = {
+  orderNo: 'ORD20251210112844577127',
+  orderTime: '2025-12-10 19:28:44',
+  goodsList: '洗护产品6 × 1 = ¥0.01',
+  payAmount: '¥0.01',
+  remark: '测试备注内容',
+  receiverName: '1',
+  address: '1',
+  receiverPhone: '13100000000'
+};
+
+// 测试不同的替换方法
+function testReplaceMethod(name, template, variables) {
+  console.log(`\n=== 测试方法: ${name} ===`);
+  console.log('原始模板:');
+  console.log(template);
+
+  let content = template;
+
+  // 方法1: 当前代码中的方法
+  if (name === '当前代码方法') {
+    for (const [key, value] of Object.entries(variables)) {
+      const placeholder = `{${key}}`;
+      if (content.includes(placeholder)) {
+        content = content.split(placeholder).join(value);
+      }
+    }
+  }
+
+  // 方法2: 正则表达式替换
+  if (name === '正则表达式方法') {
+    for (const [key, value] of Object.entries(variables)) {
+      const regex = new RegExp(`\\{${key}\\}`, 'g');
+      content = content.replace(regex, value);
+    }
+  }
+
+  // 方法3: 处理有空格的情况
+  if (name === '处理空格方法') {
+    for (const [key, value] of Object.entries(variables)) {
+      // 处理 {key} 和 { key } 两种情况
+      const regex1 = new RegExp(`\\{${key}\\}`, 'g');
+      const regex2 = new RegExp(`\\{\\s*${key}\\s*\\}`, 'g');
+      content = content.replace(regex1, value).replace(regex2, value);
+    }
+  }
+
+  console.log('\n替换后内容:');
+  console.log(content);
+
+  // 检查是否还有未替换的 {remark}
+  if (content.includes('{remark}') || content.includes('{ remark }') || content.includes('{{remark}}')) {
+    console.log('❌ 仍有未替换的 remark 占位符');
+  } else {
+    console.log('✅ 所有 remark 占位符已替换');
+  }
+}
+
+// 运行测试
+testReplaceMethod('当前代码方法', template1, variables);
+testReplaceMethod('正则表达式方法', template2, variables);
+testReplaceMethod('处理空格方法', template3, variables);
+
+// 测试空备注情况
+console.log('\n\n=== 测试空备注情况 ===');
+const emptyRemarkVariables = {
+  ...variables,
+  remark: null
+};
+
+const emptyRemarkVariables2 = {
+  ...variables,
+  remark: ''
+};
+
+console.log('1. remark = null:');
+let content1 = template1;
+for (const [key, value] of Object.entries(emptyRemarkVariables)) {
+  const placeholder = `{${key}}`;
+  if (content1.includes(placeholder)) {
+    const replaceValue = value || '无备注';
+    content1 = content1.split(placeholder).join(replaceValue);
+  }
+}
+console.log(content1);
+
+console.log('\n2. remark = "":');
+let content2 = template1;
+for (const [key, value] of Object.entries(emptyRemarkVariables2)) {
+  const placeholder = `{${key}}`;
+  if (content2.includes(placeholder)) {
+    const replaceValue = value || '无备注';
+    content2 = content2.split(placeholder).join(replaceValue);
+  }
+}
+console.log(content2);