Browse Source

✨ feat(payment-success): 新增微信发货通知订阅功能

- 在支付成功页面添加订阅发货通知按钮,引导用户订阅微信模板消息
- 新增用户实体字段 hasSubscribedDeliveryNotice 用于记录订阅状态
- 实现用户订阅状态检查、记录和更新逻辑,支持数据库持久化
- 在订单管理后台发货时检查用户订阅状态,仅向已订阅用户发送微信通知
- 优化微信模板消息发送服务,添加调试日志和租户ID支持
- 新增用户订阅状态更新API路由,支持PATCH请求更新订阅状态
- 在温馨提示中添加订阅发货通知的相关说明

🐛 fix(mini-auth): 修复微信模板消息发送配置获取逻辑

- 优化系统配置获取错误处理,添加try-catch块
- 添加调试日志输出时间戳,便于问题排查
- 修复配置获取失败时的错误处理逻辑

📝 docs(schemas): 更新用户和订单相关Schema文档

- 在UserSchemaMt和UpdateUserDtoMt中添加hasSubscribedDeliveryNotice字段定义
- 在OrderSchema和OrderListSchema中添加用户订阅状态字段
- 更新API文档描述,明确字段用途和示例值

♻️ refactor(order-management): 重构微信通知发送逻辑

- 提取微信日期时间格式化函数formatWechatDate
- 优化通知发送前的用户订阅状态检查逻辑
- 根据通知结果记录不同级别的日志(debug/warn)
- 添加租户ID参数支持,确保多租户环境正确发送通知
yourname 1 month ago
parent
commit
0ff26d2fdc

+ 222 - 2
mini/src/pages/payment-success/index.tsx

@@ -4,9 +4,11 @@
  */
  */
 
 
 import Taro, { useRouter } from '@tarojs/taro'
 import Taro, { useRouter } from '@tarojs/taro'
+import React, { useState, useEffect } from 'react'
 import { View, Text } from '@tarojs/components'
 import { View, Text } from '@tarojs/components'
-import { useQuery } from '@tanstack/react-query'
-import { orderClient } from '@/api'
+import { useQuery,useMutation } from '@tanstack/react-query'
+import { orderClient,userClient } from '@/api'
+import { useAuth } from '@/utils/auth'
 import { Navbar } from '@/components/ui/navbar'
 import { Navbar } from '@/components/ui/navbar'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 import dayjs from 'dayjs'
 import dayjs from 'dayjs'
@@ -19,10 +21,211 @@ interface PaymentSuccessParams {
 const PaymentSuccessPage = () => {
 const PaymentSuccessPage = () => {
   // 使用useRouter钩子获取路由参数
   // 使用useRouter钩子获取路由参数
   const router = useRouter()
   const router = useRouter()
+  const { user } = useAuth()
   const params = router.params
   const params = router.params
   const orderId = params?.orderId ? parseInt(params.orderId) : 0
   const orderId = params?.orderId ? parseInt(params.orderId) : 0
   const amount = params?.amount ? parseFloat(params.amount) : 0
   const amount = params?.amount ? parseFloat(params.amount) : 0
 
 
+
+    
+ // 获取当前用户详细信息(包含订阅状态)
+ const { data: currentUser } = useQuery({
+  queryKey: ['current-user', user?.id],
+  queryFn: async () => {
+    if (!user?.id) {
+      throw new Error('用户未登录')
+    }
+    const response = await userClient[':id']['$get']({
+      param: { id: user.id }
+    })
+    if (response.status !== 200) {
+      throw new Error('获取用户信息失败')
+    }
+    return response.json()
+  },
+  enabled: !!user?.id,
+})
+
+
+  const handleSubscribe = async () => {
+    try {
+      const subscribed = await requestSubscribeMessage()
+      console.log('订阅结果:', subscribed)
+    } catch (error) {
+      console.error('订阅失败:', error)
+      Taro.showToast({
+        title: '订阅失败,请重试',
+        icon: 'none',
+        duration: 2000
+      })
+    }
+  }
+
+  // 注意:微信小程序订阅消息需要在用户交互时触发
+  // 不能在页面加载时自动调用,所以这里注释掉自动订阅
+  // 改为在用户点击按钮时触发
+
+  // useEffect(() => {
+  //   // 异步引导用户订阅发货通知,不阻塞跳转
+  //   // 只在页面首次加载时执行一次
+  //   const timer = setTimeout(() => {
+  //     requestSubscribeMessage().then(subscribed => {
+  //       console.log('订阅引导完成,用户订阅状态:', subscribed)
+  //     }).catch(error => {
+  //       console.error('订阅引导异常:', error)
+  //     })
+  //   }, 1000) // 延迟1秒执行,让用户先看到支付成功页面
+
+  //   return () => clearTimeout(timer)
+  // }, []) // 空依赖数组,确保只执行一次
+
+  
+   // 检查用户是否已经订阅过发货通知
+   const checkHasSubscribed = (): boolean => {
+    try {
+      // 优先从数据库读取订阅状态
+
+      //console.log("currentUser:",currentUser);
+
+      if (currentUser?.hasSubscribedDeliveryNotice !== undefined) {
+        return currentUser.hasSubscribedDeliveryNotice === true
+      }
+
+      // 如果数据库没有数据,回退到本地存储
+      //const subscribed = Taro.getStorageSync('hasSubscribedDeliveryNotice')
+     // return subscribed === true
+    } catch (error) {
+      console.error('检查订阅状态失败:', error)
+      return false
+    }
+  }
+
+  // 记录用户订阅状态
+  const recordSubscription = (subscribed: boolean) => {
+    try {
+      // 先记录到本地存储
+     // Taro.setStorageSync('hasSubscribedDeliveryNotice', subscribed)
+
+      // 异步更新到数据库
+      if (user?.id) {
+        updateUserSubscriptionMutation.mutate(subscribed, {
+          onSuccess: () => {
+            console.log('用户订阅状态已更新到数据库:', subscribed)
+          },
+          onError: (error) => {
+            console.error('更新用户订阅状态到数据库失败:', error)
+            // 即使数据库更新失败,本地存储仍然有效
+          }
+        })
+      }
+    } catch (error) {
+      console.error('记录订阅状态失败:', error)
+    }
+  }
+
+  // 引导用户订阅发货通知
+  const requestSubscribeMessage = async (): Promise<boolean> => {
+    try {
+      // 检查用户是否已经订阅过
+      if (checkHasSubscribed()) {
+        console.log('用户已订阅过发货通知,跳过引导')
+        return true
+      }
+
+      // 发货成功通知模板ID
+      const templateId = 'T00N0Wq3ECjksXSvPWUBgOUukl1TCE7PhxqeDnFPfso'
+
+      // 先显示一个友好的提示,让用户知道为什么要订阅
+      const modalRes = await Taro.showModal({
+        title: '订阅发货通知',
+        content: '订阅后,当订单发货时您会收到微信通知,方便您及时了解物流状态。',
+        confirmText: '立即订阅',
+        cancelText: '稍后再说'
+      })
+
+      if (modalRes.confirm) {
+        // 用户点击确认,调用微信订阅消息API
+        try {
+          const result = await Taro.requestSubscribeMessage({
+            tmplIds: [templateId],
+            entityIds:[]
+          })
+
+          console.log('订阅消息结果:', result)
+
+          // 根据用户选择处理结果
+          if (result[templateId] === 'accept') {
+            console.log('用户接受了订阅消息')
+            Taro.showToast({
+              title: '订阅成功,发货时会收到通知',
+              icon: 'success',
+              duration: 2000
+            })
+            // 记录用户已订阅
+            recordSubscription(true)
+            return true
+          } else if (result[templateId] === 'reject') {
+            console.log('用户拒绝了订阅消息')
+            Taro.showToast({
+              title: '已取消订阅',
+              icon: 'none',
+              duration: 1500
+            })
+            // 记录用户拒绝订阅
+            recordSubscription(false)
+            return false
+          } else {
+            console.log('用户未做出选择或发生错误')
+            // 如果用户没有明确选择,不记录状态,下次再询问
+            return false
+          }
+        } catch (error) {
+          console.error('订阅消息失败:', error)
+          // 订阅失败不影响主要流程
+          return false
+        }
+      } else {
+        console.log('用户选择稍后再说')
+        // 用户选择稍后再说,不记录状态,下次再询问
+        return false
+      }
+    } catch (error) {
+      console.error('订阅消息引导失败:', error)
+      // 引导失败不影响主要流程
+      return false
+    }
+  }
+
+  // 更新用户订阅状态
+  const updateUserSubscriptionMutation = useMutation({
+    mutationFn: async (subscribed: boolean) => {
+      if (!user?.id) {
+        throw new Error('用户未登录')
+      }
+
+      const updateData = {
+        hasSubscribedDeliveryNotice: subscribed
+      }
+
+      const response = await userClient[':id']['$put']({
+        param: { id: user.id },
+        json: updateData
+      })
+
+      if (response.status !== 200) {
+        throw new Error('更新用户订阅状态失败')
+      }
+      return response.json()
+    },
+    onError: (error) => {
+      console.error('更新用户订阅状态失败:', error)
+      // 即使更新失败,也不影响主要流程,只在控制台记录错误
+    }
+  })
+
+
+
+
   // 检查参数有效性
   // 检查参数有效性
   const hasValidParams = orderId > 0 && amount > 0
   const hasValidParams = orderId > 0 && amount > 0
 
 
@@ -143,6 +346,21 @@ const PaymentSuccessPage = () => {
         >
         >
           返回首页
           返回首页
         </Button>
         </Button>
+
+          {/* 订阅发货通知按钮 - 只在用户未订阅时显示 */}
+          {
+          //!checkHasSubscribed() && (
+            <Button
+              onClick={handleSubscribe}
+              className="w-full h-12"
+              variant="outline"
+              size="lg"
+            >
+              📦 订阅发货通知
+            </Button>
+         // )
+          }
+          
       </View>
       </View>
 
 
       {/* 温馨提示 */}
       {/* 温馨提示 */}
@@ -151,6 +369,8 @@ const PaymentSuccessPage = () => {
         <Text className="text-xs text-gray-600 leading-relaxed whitespace-pre-line">
         <Text className="text-xs text-gray-600 leading-relaxed whitespace-pre-line">
           • 订单详情可在订单列表中查看
           • 订单详情可在订单列表中查看
           {'\n'}
           {'\n'}
+          • 订阅发货通知后,订单发货时会收到微信提醒
+          {'\n'}
           • 如有问题请联系客服
           • 如有问题请联系客服
           {'\n'}
           {'\n'}
           • 感谢您的支持
           • 感谢您的支持

+ 4 - 4
mini/src/pages/payment/index.tsx

@@ -74,10 +74,10 @@ const PaymentPage = () => {
     enabled: !!orderId && paymentStatus === PaymentStatus.PENDING
     enabled: !!orderId && paymentStatus === PaymentStatus.PENDING
   })
   })
 
 
-  // 支付状态管理
-  const paymentStateManager = PaymentStateManager.getInstance()
-  const rateLimiter = PaymentRateLimiter.getInstance()
-
+     // 支付状态管理
+     const paymentStateManager = PaymentStateManager.getInstance()
+     const rateLimiter = PaymentRateLimiter.getInstance()
+  
   // 处理支付
   // 处理支付
   const handlePayment = async () => {
   const handlePayment = async () => {
     if (!paymentData || !orderId) {
     if (!paymentData || !orderId) {

+ 6 - 2
packages/core-module-mt/auth-module-mt/src/routes/send-template-message.route.mt.ts

@@ -69,13 +69,17 @@ const app = new OpenAPIHono().openapi(sendTemplateMessageRoute, async (c) => {
     const miniAuthService = new MiniAuthService(AppDataSource);
     const miniAuthService = new MiniAuthService(AppDataSource);
     const { openid, templateId, page, data, miniprogramState, tenantId } = c.req.valid('json');
     const { openid, templateId, page, data, miniprogramState, tenantId } = c.req.valid('json');
 
 
-    console.debug('收到微信模板消息发送请求:', {
+    // 调试输出测试
+    const timestamp = new Date().toISOString();
+
+    console.log('收到微信模板消息发送请求:', {
       openid,
       openid,
       templateId,
       templateId,
       page,
       page,
       dataKeys: Object.keys(data),
       dataKeys: Object.keys(data),
       miniprogramState,
       miniprogramState,
-      tenantId
+      tenantId,
+      timestamp
     });
     });
 
 
     // 调用服务发送模板消息
     // 调用服务发送模板消息

+ 11 - 7
packages/core-module-mt/auth-module-mt/src/services/mini-auth.service.mt.ts

@@ -5,6 +5,7 @@ import { SystemConfigServiceMt } from '@d8d/core-module-mt/system-config-module-
 import { JWTUtil, redisUtil } from '@d8d/shared-utils';
 import { JWTUtil, redisUtil } from '@d8d/shared-utils';
 import axios from 'axios';
 import axios from 'axios';
 import process from 'node:process'
 import process from 'node:process'
+import { log } from 'node:console';
 
 
 export class MiniAuthService {
 export class MiniAuthService {
   private userRepository: Repository<UserEntityMt>;
   private userRepository: Repository<UserEntityMt>;
@@ -230,8 +231,6 @@ export class MiniAuthService {
   }): Promise<any> {
   }): Promise<any> {
     const { openid, templateId, page, data, miniprogramState = 'formal', tenantId } = params;
     const { openid, templateId, page, data, miniprogramState = 'formal', tenantId } = params;
 
 
-    console.log("ssssssss");
-
     // 获取微信小程序配置
     // 获取微信小程序配置
     let appId: string | null = null;
     let appId: string | null = null;
     let appSecret: string | null = null;
     let appSecret: string | null = null;
@@ -239,12 +238,17 @@ export class MiniAuthService {
     if (tenantId !== undefined) {
     if (tenantId !== undefined) {
       // 从系统配置获取
       // 从系统配置获取
       const configKeys = ['wx.mini.app.id', 'wx.mini.app.secret'];
       const configKeys = ['wx.mini.app.id', 'wx.mini.app.secret'];
-      const configs = await this.systemConfigService.getConfigsByKeys(configKeys, tenantId);
-      appId = configs['wx.mini.app.id'];
-      appSecret = configs['wx.mini.app.secret'];
+      try {
+        const configs = await this.systemConfigService.getConfigsByKeys(configKeys, tenantId);
+        appId = configs['wx.mini.app.id'];
+        appSecret = configs['wx.mini.app.secret'];
 
 
-      console.log("appId:",appId)
-      console.log("appSecret:",appSecret)
+      } catch (error) {
+        console.error("获取系统配置失败:", error);
+        throw error;
+      }
+    } else {
+      console.debug("tenantId未提供,将使用环境变量");
     }
     }
 
 
     // 如果系统配置中没有找到,回退到环境变量
     // 如果系统配置中没有找到,回退到环境变量

+ 3 - 0
packages/core-module-mt/user-module-mt/src/entities/user.entity.mt.ts

@@ -51,6 +51,9 @@ export class UserEntityMt {
   @Column({ name: 'registration_source', type: 'varchar', length: 20, default: 'web', comment: '注册来源: web, miniapp' })
   @Column({ name: 'registration_source', type: 'varchar', length: 20, default: 'web', comment: '注册来源: web, miniapp' })
   registrationSource!: string;
   registrationSource!: string;
 
 
+  @Column({ name: 'has_subscribed_delivery_notice', type: 'boolean', default: false, comment: '是否已订阅发货通知' })
+  hasSubscribedDeliveryNotice!: boolean;
+
   @ManyToMany(() => RoleMt)
   @ManyToMany(() => RoleMt)
   @JoinTable()
   @JoinTable()
   roles!: RoleMt[];
   roles!: RoleMt[];

+ 53 - 0
packages/core-module-mt/user-module-mt/src/routes/custom.routes.mt.ts

@@ -7,6 +7,14 @@ import { parseWithAwait } from '@d8d/shared-utils';
 import { authMiddleware } from '@d8d/core-module-mt/auth-module-mt';
 import { authMiddleware } from '@d8d/core-module-mt/auth-module-mt';
 import { AuthContext } from '@d8d/shared-types';
 import { AuthContext } from '@d8d/shared-types';
 
 
+// 更新用户订阅状态请求Schema
+const UpdateUserSubscriptionSchema = z.object({
+  hasSubscribedDeliveryNotice: z.boolean().openapi({
+    example: true,
+    description: '是否已订阅发货通知'
+  })
+});
+
 // 创建多租户用户路由 - 自定义业务逻辑(密码加密等)
 // 创建多租户用户路由 - 自定义业务逻辑(密码加密等)
 const createUserRouteMt = createRoute({
 const createUserRouteMt = createRoute({
   method: 'post',
   method: 'post',
@@ -117,6 +125,51 @@ const deleteUserRouteMt = createRoute({
   }
   }
 });
 });
 
 
+// 更新用户订阅状态路由
+const updateUserSubscriptionRouteMt = createRoute({
+  method: 'patch',
+  path: '/{id}/subscription',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.coerce.number().openapi({
+        param: { name: 'id', in: 'path' },
+        example: 1,
+        description: '用户ID'
+      })
+    }),
+    body: {
+      content: {
+        'application/json': { schema: UpdateUserSubscriptionSchema }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '用户订阅状态更新成功',
+      content: {
+        'application/json': { schema: UserSchemaMt }
+      }
+    },
+    400: {
+      description: '参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '用户不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '更新用户订阅状态失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
 const app = new OpenAPIHono<AuthContext>()
 const app = new OpenAPIHono<AuthContext>()
   .openapi(createUserRouteMt, async (c) => {
   .openapi(createUserRouteMt, async (c) => {
     try {
     try {

+ 8 - 0
packages/core-module-mt/user-module-mt/src/schemas/user.schema.mt.ts

@@ -55,6 +55,10 @@ export const UserSchemaMt = z.object({
     example: 'miniapp',
     example: 'miniapp',
     description: '注册来源: web, miniapp'
     description: '注册来源: web, miniapp'
   }),
   }),
+  hasSubscribedDeliveryNotice: z.boolean().default(false).openapi({
+    example: false,
+    description: '是否已订阅发货通知'
+  }),
   isDisabled: z.nativeEnum(DisabledStatus).default(DisabledStatus.ENABLED).openapi({
   isDisabled: z.nativeEnum(DisabledStatus).default(DisabledStatus.ENABLED).openapi({
     example: DisabledStatus.ENABLED,
     example: DisabledStatus.ENABLED,
     description: '是否禁用(0:启用,1:禁用)'
     description: '是否禁用(0:启用,1:禁用)'
@@ -147,6 +151,10 @@ export const UpdateUserDtoMt = z.object({
     example: 1,
     example: 1,
     description: '头像文件ID'
     description: '头像文件ID'
   }),
   }),
+  hasSubscribedDeliveryNotice: z.boolean().optional().openapi({
+    example: true,
+    description: '是否已订阅发货通知'
+  }),
   isDisabled: z.number().int().min(0, '状态值只能是0或1').max(1, '状态值只能是0或1').optional().openapi({
   isDisabled: z.number().int().min(0, '状态值只能是0或1').max(1, '状态值只能是0或1').optional().openapi({
     example: DisabledStatus.ENABLED,
     example: DisabledStatus.ENABLED,
     description: '是否禁用(0:启用,1:禁用)'
     description: '是否禁用(0:启用,1:禁用)'

+ 74 - 7
packages/order-management-ui-mt/src/components/OrderManagement.tsx

@@ -6,6 +6,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 import { toast } from 'sonner';
 import { toast } from 'sonner';
 import { Search, Edit, Eye, Package, Truck } from 'lucide-react';
 import { Search, Edit, Eye, Package, Truck } from 'lucide-react';
+ 
 
 
 // 使用共享UI组件包的具体路径导入
 // 使用共享UI组件包的具体路径导入
 import { Button } from '@d8d/shared-ui-components/components/ui/button';
 import { Button } from '@d8d/shared-ui-components/components/ui/button';
@@ -66,6 +67,7 @@ import { adminOrderClient, orderClientManager } from '../api';
 import type { InferResponseType } from 'hono/client';
 import type { InferResponseType } from 'hono/client';
 import { UpdateOrderDto } from '@d8d/orders-module-mt/schemas';
 import { UpdateOrderDto } from '@d8d/orders-module-mt/schemas';
 
 
+
 // 类型定义
 // 类型定义
 type OrderResponse = InferResponseType<typeof adminOrderClient.index.$get, 200>['data'][0] & {
 type OrderResponse = InferResponseType<typeof adminOrderClient.index.$get, 200>['data'][0] & {
   user?: {
   user?: {
@@ -73,7 +75,9 @@ type OrderResponse = InferResponseType<typeof adminOrderClient.index.$get, 200>[
     username: string;
     username: string;
     phone: string | null;
     phone: string | null;
     openid?: string | null; // 添加openid字段
     openid?: string | null; // 添加openid字段
+    hasSubscribedDeliveryNotice?: boolean; // 添加订阅状态字段
   } | null;
   } | null;
+  tenantId?: number; // 添加tenantId字段
 };
 };
 type UpdateRequest = any;
 type UpdateRequest = any;
 // 发货请求类型 - 使用UpdateOrderDto的类型
 // 发货请求类型 - 使用UpdateOrderDto的类型
@@ -93,6 +97,7 @@ interface WechatServiceMessageConfig {
   page?: string;
   page?: string;
   data: Record<string, { value: string }>;
   data: Record<string, { value: string }>;
   miniprogramState?: 'developer' | 'trial' | 'formal';
   miniprogramState?: 'developer' | 'trial' | 'formal';
+  tenantId?: number; // 添加tenantId参数
 }
 }
 
 
 // 微信服务消息通知结果类型
 // 微信服务消息通知结果类型
@@ -119,7 +124,8 @@ const sendWechatServiceMessage = async (config: WechatServiceMessageConfig): Pro
         templateId: config.templateId,
         templateId: config.templateId,
         data: config.data,
         data: config.data,
         page: config.page || 'pages/index/index',
         page: config.page || 'pages/index/index',
-        miniprogramState: config.miniprogramState || 'formal'
+        miniprogramState: config.miniprogramState || 'formal',
+        tenantId: config.tenantId
       }),
       }),
     });
     });
 
 
@@ -138,6 +144,8 @@ const sendWechatServiceMessage = async (config: WechatServiceMessageConfig): Pro
     }
     }
 
 
     const result = await response.json();
     const result = await response.json();
+
+    
     console.debug('微信服务消息发送成功:', result);
     console.debug('微信服务消息发送成功:', result);
 
 
     return {
     return {
@@ -156,6 +164,32 @@ const sendWechatServiceMessage = async (config: WechatServiceMessageConfig): Pro
   }
   }
 };
 };
 
 
+// 格式化微信日期时间
+const formatWechatDate = (isoDateString: string | null | undefined): string => {
+  try {
+    // 如果日期字符串为空,使用当前时间
+    const date = isoDateString ? new Date(isoDateString) : new Date();
+    // 微信date类型格式:YYYY年MM月DD日 HH:mm
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    const hours = String(date.getHours()).padStart(2, '0');
+    const minutes = String(date.getMinutes()).padStart(2, '0');
+
+    return `${year}年${month}月${day}日 ${hours}:${minutes}`;
+  } catch (error) {
+    console.error('格式化微信日期失败:', error, isoDateString);
+    // 返回当前时间的格式化版本作为备用
+    const now = new Date();
+    const year = now.getFullYear();
+    const month = String(now.getMonth() + 1).padStart(2, '0');
+    const day = String(now.getDate()).padStart(2, '0');
+    const hours = String(now.getHours()).padStart(2, '0');
+    const minutes = String(now.getMinutes()).padStart(2, '0');
+    return `${year}年${month}月${day}日 ${hours}:${minutes}`;
+  }
+};
+
 // 发货成功微信通知函数 - 使用新的配置化设计
 // 发货成功微信通知函数 - 使用新的配置化设计
 const sendDeliverySuccessNotification = async (order: OrderResponse, deliveryData: DeliveryRequest): Promise<WechatServiceMessageResult> => {
 const sendDeliverySuccessNotification = async (order: OrderResponse, deliveryData: DeliveryRequest): Promise<WechatServiceMessageResult> => {
   // 检查是否有用户信息和openid
   // 检查是否有用户信息和openid
@@ -173,6 +207,26 @@ const sendDeliverySuccessNotification = async (order: OrderResponse, deliveryDat
     return { success: false, message: '用户没有绑定微信小程序,无法发送微信通知' };
     return { success: false, message: '用户没有绑定微信小程序,无法发送微信通知' };
   }
   }
 
 
+  // 检查用户是否已订阅发货通知
+  if (order.user.hasSubscribedDeliveryNotice !== true) {
+    console.debug('用户未订阅发货通知,跳过发送微信通知', {
+      userId: order.user.id,
+      username: order.user.username,
+      hasSubscribedDeliveryNotice: order.user.hasSubscribedDeliveryNotice
+    });
+    return {
+      success: false,
+      message: '用户未订阅发货通知,跳过发送微信通知',
+      data: { skipped: true, reason: 'user_not_subscribed' }
+    };
+  }
+
+  console.debug('用户已订阅发货通知,准备发送微信通知', {
+    userId: order.user.id,
+    username: order.user.username,
+    openid: order.user.openid?.substring(0, 10) + '...' // 部分隐藏openid
+  });
+
   // 构建微信服务消息配置 - 参考useShareAppMessage的配置对象模式
   // 构建微信服务消息配置 - 参考useShareAppMessage的配置对象模式
   const config: WechatServiceMessageConfig = {
   const config: WechatServiceMessageConfig = {
     openid: order.user.openid,
     openid: order.user.openid,
@@ -181,22 +235,23 @@ const sendDeliverySuccessNotification = async (order: OrderResponse, deliveryDat
     data: {
     data: {
       // 根据实际微信模板字段配置
       // 根据实际微信模板字段配置
       character_string7: {
       character_string7: {
-        value: `订单号:${order.orderNo}`
+        value: `${order.orderNo}`
       },
       },
       date6: {
       date6: {
-        value: `${deliveryData.deliveryTime}`
+        value: formatWechatDate(deliveryData.deliveryTime)
       },
       },
       amount9: {
       amount9: {
-        value: `${order.payAmount}`
+        value: `¥${order.payAmount.toFixed(2)}`
       },
       },
       phrase12: {
       phrase12: {
-        value: `${deliveryData.deliveryType}`
+        value: deliveryTypeMap[deliveryData.deliveryType as keyof typeof deliveryTypeMap]?.label || '未知'
       },
       },
       thing4: {
       thing4: {
-        value: `${deliveryData.deliveryRemark}` || '请收到货/提货后及时确认收货,2天后将自动确认收货,如有异常请及时进行交易投诉。'
+        value: (deliveryData.deliveryRemark || '请收到货/提货后及时确认收货,2天后将自动确认收货,如有异常请及时进行交易投诉。').substring(0, 20)
       }
       }
     },
     },
-    miniprogramState: 'formal'
+    miniprogramState: 'formal',
+    tenantId: order.tenantId // 从订单数据中获取tenantId
   };
   };
 
 
   // 调用微信服务消息函数
   // 调用微信服务消息函数
@@ -248,6 +303,7 @@ export const OrderManagement = () => {
   const [deliveryModalOpen, setDeliveryModalOpen] = useState(false);
   const [deliveryModalOpen, setDeliveryModalOpen] = useState(false);
   const [deliveringOrder, setDeliveringOrder] = useState<OrderResponse | null>(null);
   const [deliveringOrder, setDeliveringOrder] = useState<OrderResponse | null>(null);
 
 
+
   // 表单实例
   // 表单实例
   const updateForm = useForm<UpdateRequest>({
   const updateForm = useForm<UpdateRequest>({
     resolver: zodResolver(UpdateOrderDto),
     resolver: zodResolver(UpdateOrderDto),
@@ -264,6 +320,7 @@ export const OrderManagement = () => {
     },
     },
   });
   });
 
 
+ 
   // 数据查询 - 60秒自动刷新
   // 数据查询 - 60秒自动刷新
   const { data, isLoading, refetch } = useQuery({
   const { data, isLoading, refetch } = useQuery({
     queryKey: ['orders', searchParams],
     queryKey: ['orders', searchParams],
@@ -427,9 +484,19 @@ export const OrderManagement = () => {
         // 发货成功后发送微信服务消息通知 - 使用新的配置化设计
         // 发货成功后发送微信服务消息通知 - 使用新的配置化设计
         try {
         try {
           const notificationResult = await sendDeliverySuccessNotification(deliveringOrder, data);
           const notificationResult = await sendDeliverySuccessNotification(deliveringOrder, data);
+
+          // 根据通知结果记录不同的日志
           if (notificationResult.success) {
           if (notificationResult.success) {
             console.debug('微信发货通知发送成功:', notificationResult);
             console.debug('微信发货通知发送成功:', notificationResult);
+          } else if (notificationResult.data?.skipped) {
+            // 用户未订阅,这是正常情况,记录为debug级别
+            console.debug('用户未订阅发货通知,跳过发送:', {
+              userId: deliveringOrder.user?.id,
+              username: deliveringOrder.user?.username,
+              reason: notificationResult.data.reason
+            });
           } else {
           } else {
+            // 其他原因导致的失败,记录为warn级别
             console.warn('微信发货通知发送失败,但发货成功:', notificationResult);
             console.warn('微信发货通知发送失败,但发货成功:', notificationResult);
             // 可以在这里添加额外的处理,比如记录到日志系统
             // 可以在这里添加额外的处理,比如记录到日志系统
           }
           }

+ 8 - 0
packages/orders-module-mt/src/schemas/order.mt.schema.ts

@@ -225,6 +225,10 @@ export const OrderSchema = z.object({
     openid: z.string().nullable().optional().openapi({
     openid: z.string().nullable().optional().openapi({
       description: '微信小程序openid',
       description: '微信小程序openid',
       example: 'oABCDEFGH123456789'
       example: 'oABCDEFGH123456789'
+    }),
+    hasSubscribedDeliveryNotice: z.boolean().optional().openapi({
+      description: '是否已订阅发货通知',
+      example: true
     })
     })
   }).nullable().optional().openapi({
   }).nullable().optional().openapi({
     description: '用户信息'
     description: '用户信息'
@@ -644,6 +648,10 @@ export const OrderListSchema = z.object({
     openid: z.string().nullable().optional().openapi({
     openid: z.string().nullable().optional().openapi({
       description: '微信小程序openid',
       description: '微信小程序openid',
       example: 'oABCDEFGH123456789'
       example: 'oABCDEFGH123456789'
+    }),
+    hasSubscribedDeliveryNotice: z.boolean().optional().openapi({
+      description: '是否已订阅发货通知',
+      example: true
     })
     })
   }).nullable().optional().openapi({
   }).nullable().optional().openapi({
     description: '用户信息'
     description: '用户信息'