Kaynağa Gözat

更新mini-auth服务和订单管理组件

- 添加微信模板消息发送功能
- 优化Redis工具类
- 更新订单管理界面

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
yourname 1 ay önce
ebeveyn
işleme
505777a891

+ 3 - 1
packages/core-module-mt/auth-module-mt/src/routes/index.mt.ts

@@ -8,6 +8,7 @@ import updateMeRoute from './update-me.route.mt';
 import logoutRoute from './logout.route.mt';
 import ssoVerifyRoute from './sso-verify.route.mt';
 import phoneDecryptRoute from './phone-decrypt.route.mt';
+import sendTemplateMessageRoute from './send-template-message.route.mt';
 
 // 创建统一的路由应用
 const authRoutes = new OpenAPIHono<AuthContext>()
@@ -18,7 +19,8 @@ const authRoutes = new OpenAPIHono<AuthContext>()
   .route('/', updateMeRoute)
   .route('/', logoutRoute)
   .route('/', ssoVerifyRoute)
-  .route('/', phoneDecryptRoute);
+  .route('/', phoneDecryptRoute)
+  .route('/', sendTemplateMessageRoute);
 
 export { authRoutes };
 export default authRoutes;

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

@@ -0,0 +1,111 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { MiniAuthService } from '../services/index.mt';
+import { AppDataSource } from '@d8d/shared-utils';
+import { ErrorSchema } from '@d8d/shared-utils';
+
+// 微信模板消息请求Schema
+const SendTemplateMessageSchema = z.object({
+  openid: z.string().min(1, 'openid不能为空'),
+  templateId: z.string().min(1, '模板ID不能为空'),
+  page: z.string().optional().default('pages/index/index'),
+  data: z.record(z.string(), z.object({
+    value: z.string()
+  })),
+  miniprogramState: z.enum(['developer', 'trial', 'formal']).optional().default('formal'),
+  tenantId: z.number().optional()
+});
+
+// 微信模板消息响应Schema
+const SendTemplateMessageResponseSchema = z.object({
+  success: z.boolean(),
+  message: z.string(),
+  data: z.any().optional(),
+  error: z.any().optional()
+});
+
+const sendTemplateMessageRoute = createRoute({
+  method: 'post',
+  path: '/send-template-message',
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: SendTemplateMessageSchema
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '微信模板消息发送成功',
+      content: {
+        'application/json': {
+          schema: SendTemplateMessageResponseSchema
+        }
+      }
+    },
+    400: {
+      description: '参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono().openapi(sendTemplateMessageRoute, async (c) => {
+  try {
+    const miniAuthService = new MiniAuthService(AppDataSource);
+    const { openid, templateId, page, data, miniprogramState, tenantId } = c.req.valid('json');
+
+    console.debug('收到微信模板消息发送请求:', {
+      openid,
+      templateId,
+      page,
+      dataKeys: Object.keys(data),
+      miniprogramState,
+      tenantId
+    });
+
+    // 调用服务发送模板消息
+    const result = await miniAuthService.sendTemplateMessage({
+      openid,
+      templateId,
+      page,
+      data: data as Record<string, { value: string }>,
+      miniprogramState,
+      tenantId
+    });
+
+    return c.json({
+      success: true,
+      message: '微信模板消息发送成功',
+      data: result
+    }, 200);
+
+  } catch (error) {
+    console.error('发送微信模板消息失败:', error);
+
+    const errorMessage = error instanceof Error ? error.message : '发送微信模板消息失败';
+    const errorCode = (error as any)?.code || 500;
+
+    return c.json({
+      success: false,
+      message: errorMessage,
+      error: error instanceof Error ? error.stack : error
+    }, errorCode);
+  }
+});
+
+export default app;

+ 158 - 0
packages/core-module-mt/auth-module-mt/src/services/mini-auth.service.mt.ts

@@ -216,4 +216,162 @@ export class MiniAuthService {
       }
     }
   }
+
+  /**
+   * 发送微信模板消息
+   */
+  async sendTemplateMessage(params: {
+    openid: string;
+    templateId: string;
+    page?: string;
+    data: Record<string, { value: string }>;
+    miniprogramState?: 'developer' | 'trial' | 'formal';
+    tenantId?: number;
+  }): Promise<any> {
+    const { openid, templateId, page, data, miniprogramState = 'formal', tenantId } = params;
+
+    // 获取微信小程序配置
+    let appId: string | null = null;
+    let appSecret: string | null = null;
+
+    if (tenantId !== undefined) {
+      // 从系统配置获取
+      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'];
+    }
+
+    // 如果系统配置中没有找到,回退到环境变量
+    if (!appId) {
+      appId = process.env.WX_MINI_APP_ID || null;
+    }
+    if (!appSecret) {
+      appSecret = process.env.WX_MINI_APP_SECRET || null;
+    }
+
+    if (!appId || !appSecret) {
+      throw new Error('微信小程序配置缺失');
+    }
+
+    // 获取access_token
+    const accessToken = await this.getAccessToken(appId, appSecret);
+
+    // 构建模板消息请求数据
+    const templateMessageData = {
+      touser: openid,
+      template_id: templateId,
+      page: page || 'pages/index/index',
+      data: data,
+      miniprogram_state: miniprogramState
+    };
+
+    console.debug('发送微信模板消息:', {
+      appId,
+      openid,
+      templateId,
+      page,
+      dataKeys: Object.keys(data)
+    });
+
+    // 调用微信模板消息API
+    const url = `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${accessToken}`;
+
+    try {
+      const response = await axios.post(url, templateMessageData, {
+        timeout: 10000,
+        headers: {
+          'Content-Type': 'application/json'
+        }
+      });
+
+      if (response.data.errcode && response.data.errcode !== 0) {
+        throw new Error(`微信模板消息发送失败: ${response.data.errmsg} (errcode: ${response.data.errcode})`);
+      }
+
+      console.debug('微信模板消息发送成功:', response.data);
+      return response.data;
+
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        throw new Error(`微信服务器连接失败: ${error.message}`);
+      }
+      throw error;
+    }
+  }
+
+  /**
+   * 获取微信access_token(带缓存机制)
+   */
+  private async getAccessToken(appId: string, appSecret: string): Promise<string> {
+    // 1. 首先尝试从Redis缓存获取
+    const cachedToken = await redisUtil.getWechatAccessToken(appId);
+
+    if (cachedToken) {
+      console.debug(`使用缓存的微信access_token,appId: ${appId}`);
+      return cachedToken;
+    }
+
+    console.debug(`缓存中未找到微信access_token,从API获取,appId: ${appId}`);
+
+    // 2. 缓存中没有,调用微信API获取
+    const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
+
+    try {
+      const response = await axios.get(url, { timeout: 10000 });
+
+      if (response.data.errcode) {
+        throw new Error(`获取access_token失败: ${response.data.errmsg}`);
+      }
+
+      const accessToken = response.data.access_token;
+      const expiresIn = response.data.expires_in || 7200; // 微信默认返回7200秒(2小时)
+
+      // 3. 将获取到的access_token存入Redis缓存
+      // 设置过期时间比微信返回的expires_in少100秒,确保安全
+      const cacheExpiresIn = Math.max(expiresIn - 100, 600); // 最少缓存10分钟
+      await redisUtil.setWechatAccessToken(appId, accessToken, cacheExpiresIn);
+
+      console.debug(`微信access_token获取成功并已缓存,appId: ${appId}, 过期时间: ${cacheExpiresIn}秒`);
+
+      return accessToken;
+
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        throw new Error('微信服务器连接失败,无法获取access_token');
+      }
+      throw error;
+    }
+  }
+
+  /**
+   * 强制刷新微信access_token(清除缓存并重新获取)
+   */
+  private async refreshAccessToken(appId: string, appSecret: string): Promise<string> {
+    console.debug(`强制刷新微信access_token,appId: ${appId}`);
+
+    // 1. 清除缓存
+    await redisUtil.deleteWechatAccessToken(appId);
+
+    // 2. 重新获取
+    return await this.getAccessToken(appId, appSecret);
+  }
+
+  /**
+   * 检查微信access_token缓存状态
+   */
+  private async checkAccessTokenCacheStatus(appId: string): Promise<{
+    hasCache: boolean;
+    ttl: number;
+    isValid: boolean;
+  }> {
+    const hasCache = await redisUtil.isWechatAccessTokenValid(appId);
+    const ttl = await redisUtil.getWechatAccessTokenTTL(appId);
+
+    return {
+      hasCache,
+      ttl,
+      isValid: hasCache && ttl > 60 // 剩余时间大于60秒认为有效
+    };
+  }
 }

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

@@ -1,3 +1,4 @@
+
 import { useState } from 'react';
 import { useQuery } from '@tanstack/react-query';
 import { useForm } from 'react-hook-form';
@@ -66,11 +67,142 @@ import type { InferResponseType } from 'hono/client';
 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?: {
+    id: number;
+    username: string;
+    phone: string | null;
+    openid?: string | null; // 添加openid字段
+  } | null;
+};
 type UpdateRequest = any;
-type DeliveryRequest = any;
+// 发货请求类型 - 使用UpdateOrderDto的类型
+type DeliveryRequest = {
+  deliveryType: number;
+  deliveryCompany?: string | null;
+  deliveryNo?: string | null;
+  deliveryRemark?: string | null;
+  deliveryTime?: string | null;
+};
 type DeliveryResponse = any;
 
+// 微信服务消息通知配置类型 - 参考useShareAppMessage的设计模式
+interface WechatServiceMessageConfig {
+  openid: string;
+  templateId: string;
+  page?: string;
+  data: Record<string, { value: string }>;
+  miniprogramState?: 'developer' | 'trial' | 'formal';
+}
+
+// 微信服务消息通知结果类型
+interface WechatServiceMessageResult {
+  success: boolean;
+  message: string;
+  data?: any;
+  error?: any;
+}
+
+// 微信服务消息通知函数 - 参考useShareAppMessage的简洁设计模式
+const sendWechatServiceMessage = async (config: WechatServiceMessageConfig): Promise<WechatServiceMessageResult> => {
+  try {
+    console.debug('准备发送微信服务消息:', config);
+
+    // 调用后端微信API
+    const response = await fetch('/api/v1/auth/send-template-message', {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        openid: config.openid,
+        templateId: config.templateId,
+        data: config.data,
+        page: config.page || 'pages/index/index',
+        miniprogramState: config.miniprogramState || 'formal'
+      }),
+    });
+
+    if (!response.ok) {
+      const errorText = await response.text();
+      console.error('微信服务消息API调用失败:', {
+        status: response.status,
+        statusText: response.statusText,
+        error: errorText
+      });
+      return {
+        success: false,
+        message: `微信服务消息发送失败: ${response.status}`,
+        error: errorText
+      };
+    }
+
+    const result = await response.json();
+    console.debug('微信服务消息发送成功:', result);
+
+    return {
+      success: true,
+      message: '微信服务消息发送成功',
+      data: result
+    };
+
+  } catch (error) {
+    console.error('发送微信服务消息时出错:', error);
+    return {
+      success: false,
+      message: '微信服务消息发送失败',
+      error
+    };
+  }
+};
+
+// 发货成功微信通知函数 - 使用新的配置化设计
+const sendDeliverySuccessNotification = async (order: OrderResponse, deliveryData: DeliveryRequest): Promise<WechatServiceMessageResult> => {
+  // 检查是否有用户信息和openid
+  if (!order.user || !order.user.id) {
+    console.warn('订单没有用户信息,无法发送微信通知');
+    return { success: false, message: '订单没有用户信息,无法发送微信通知' };
+  }
+
+  // 检查用户是否有openid(微信小程序用户)
+  if (!order.user.openid) {
+    console.warn('用户没有绑定微信小程序,无法发送微信通知', {
+      userId: order.user.id,
+      username: order.user.username
+    });
+    return { success: false, message: '用户没有绑定微信小程序,无法发送微信通知' };
+  }
+
+  // 构建微信服务消息配置 - 参考useShareAppMessage的配置对象模式
+  const config: WechatServiceMessageConfig = {
+    openid: order.user.openid,
+    templateId: 'T00N0Wq3ECjksXSvPWUBgOUukl1TCE7PhxqeDnFPfso', // 发货成功通知模板ID
+    page: 'pages/order/detail/index', // 点击跳转到订单详情页
+    data: {
+      // 根据实际微信模板字段配置
+      character_string7: {
+        value: `订单号:${order.orderNo}`
+      },
+      date6: {
+        value: `${deliveryData.deliveryTime}`
+      },
+      amount9: {
+        value: `${order.payAmount}`
+      },
+      phrase12: {
+        value: `${deliveryData.deliveryType}`
+      },
+      thing4: {
+        value: `${deliveryData.deliveryRemark}` || '请收到货/提货后及时确认收货,2天后将自动确认收货,如有异常请及时进行交易投诉。'
+      }
+    },
+    miniprogramState: 'formal'
+  };
+
+  // 调用微信服务消息函数
+  return await sendWechatServiceMessage(config);
+};
+
 // 状态映射
 const orderStatusMap = {
   0: { label: '未发货', color: 'warning' },
@@ -197,30 +329,159 @@ export const OrderManagement = () => {
   };
 
   // 处理发货提交
+  // const handleDeliverySubmit = async (data: DeliveryRequest) => {
+  //   if (!deliveringOrder || !deliveringOrder.id) return;
+
+  //   try {
+  //     const res = await (orderClientManager.getAdminDeliveryClient() as any)[':id']['delivery']['$post']({
+  //       param: { id: deliveringOrder.id },
+  //       json: data,
+  //     });
+
+  //     if (res.status === 200) {
+  //       const result = await res.json() as DeliveryResponse;
+  //       toast.success(result.message || '发货成功');
+  //       setDeliveryModalOpen(false);
+  //       refetch();
+  //     } else {
+  //       const error = await res.json();
+  //       toast.error(error.message || '发货失败');
+  //     }
+  //   } catch (error) {
+  //     console.error('发货失败:', error);
+  //     toast.error('发货失败,请重试');
+  //   }
+  // };
+
+
+
   const handleDeliverySubmit = async (data: DeliveryRequest) => {
-    if (!deliveringOrder || !deliveringOrder.id) return;
+    if (!deliveringOrder || !deliveringOrder.id) {
+      console.error('发货失败: deliveringOrder或id为空', { deliveringOrder });
+      return;
+    }
 
     try {
-      const res = await (orderClientManager.getAdminDeliveryClient() as any)[':id']['delivery']['$post']({
+      // console.debug('发货请求数据:', {
+      //   orderId: deliveringOrder.id,
+      //   orderNo: deliveringOrder.orderNo,
+      //   data,
+      //   orderState: deliveringOrder.state,
+      //   payState: deliveringOrder.payState
+      // });
+
+      // 使用adminOrderClient的$put接口来更新订单发货信息
+      // 因为adminDeliveryRoutes没有被挂载到服务器,而adminOrderRoutes已经被正确挂载
+
+      data.deliveryTime = new Date().toISOString();
+
+      const updateData = {
+        state: 1, // 已发货状态
+        deliveryType: data.deliveryType,
+        deliveryCompany: data.deliveryCompany || null,
+        deliveryNo: data.deliveryNo || null,
+        deliveryTime: data.deliveryTime,
+        deliveryRemark: data.deliveryRemark || null,
+      };
+
+     // console.debug('更新订单数据:', updateData);
+
+      // 调用adminOrderClient的$put接口
+      const res = await (orderClientManager.getAdminOrderClient() as any)[':id']['$put']({
         param: { id: deliveringOrder.id },
-        json: data,
+        json: updateData,
       });
 
+      // console.debug('发货响应详情:', {
+      //   status: res.status,
+      //   statusText: res.statusText,
+      //   headers: Object.fromEntries(res.headers.entries()),
+      //   url: res.url
+      // });
+      
       if (res.status === 200) {
-        const result = await res.json() as DeliveryResponse;
+        let result: DeliveryResponse;
+
+        try {
+          const responseText = await res.text();
+          if (responseText.trim()) {
+            try {
+              result = JSON.parse(responseText) as DeliveryResponse;
+            } catch (parseError) {
+              console.error('成功响应JSON解析失败:', parseError);
+              result = { success: true, message: '发货成功' } as DeliveryResponse;
+            }
+          } else {
+            result = { success: true, message: '发货成功' } as DeliveryResponse;
+          }
+        } catch (error) {
+          console.error('处理成功响应失败:', error);
+          result = { success: true, message: '发货成功' } as DeliveryResponse;
+        }
+
+        console.debug('发货成功:', result);
         toast.success(result.message || '发货成功');
         setDeliveryModalOpen(false);
         refetch();
+
+        // 发货成功后发送微信服务消息通知 - 使用新的配置化设计
+        try {
+          const notificationResult = await sendDeliverySuccessNotification(deliveringOrder, data);
+          if (notificationResult.success) {
+            console.debug('微信发货通知发送成功:', notificationResult);
+          } else {
+            console.warn('微信发货通知发送失败,但发货成功:', notificationResult);
+            // 可以在这里添加额外的处理,比如记录到日志系统
+          }
+        } catch (notificationError) {
+          console.error('发送微信发货通知时发生异常:', notificationError);
+          // 不阻止发货成功,只记录错误
+        }
       } else {
-        const error = await res.json();
-        toast.error(error.message || '发货失败');
+        // 先尝试获取响应文本,避免JSON解析错误
+        let errorText = '';
+        let errorData: any = null;
+
+        try {
+          errorText = await res.text();
+          console.debug('发货失败响应文本:', errorText);
+
+          // 尝试解析为JSON
+          if (errorText.trim()) {
+            try {
+              errorData = JSON.parse(errorText);
+            } catch (parseError) {
+              console.warn('响应不是有效的JSON:', parseError);
+              errorData = { message: errorText.substring(0, 100) + '...' };
+            }
+          }
+        } catch (textError) {
+          console.error('获取响应文本失败:', textError);
+          errorData = { message: `请求失败,状态码: ${res.status}` };
+        }
+
+        console.error('发货失败响应:', {
+          status: res.status,
+          statusText: res.statusText,
+          errorData,
+          errorText: errorText.substring(0, 200),
+          orderId: deliveringOrder.id
+        });
+
+        // 显示错误消息
+        const errorMessage = errorData?.message ||
+                           errorData?.error?.message ||
+                           res.statusText ||
+                           `发货失败 (${res.status})`;
+        toast.error(errorMessage);
       }
     } catch (error) {
-      console.error('发货失败:', error);
+      console.error('发货请求异常:', error);
       toast.error('发货失败,请重试');
     }
   };
 
+
   // 处理更新订单
   const handleUpdateSubmit = async (data: UpdateRequest) => {
     if (!editingOrder || !editingOrder.id) return;

+ 93 - 0
packages/shared-utils/src/utils/redis.util.ts

@@ -153,6 +153,99 @@ class RedisUtil {
   formatSystemConfigKey(tenantId: number, configKey: string): string {
     return `system_config:${tenantId}:${configKey}`;
   }
+
+  /**
+   * 设置微信access_token缓存
+   * @param appId 微信小程序appId
+   * @param accessToken access_token值
+   * @param expiresIn 过期时间(秒),微信返回的expires_in,默认7100秒(比微信的7200秒少100秒,确保安全)
+   */
+  async setWechatAccessToken(appId: string, accessToken: string, expiresIn: number = 7100): Promise<void> {
+    const client = await this.connect();
+    const key = `wechat_access_token:${appId}`;
+    await client.set(key, accessToken, {
+      EX: expiresIn
+    });
+    console.debug(`微信access_token缓存设置成功,appId: ${appId}, 过期时间: ${expiresIn}秒`);
+  }
+
+  /**
+   * 获取微信access_token缓存
+   * @param appId 微信小程序appId
+   * @returns access_token值或null
+   */
+  async getWechatAccessToken(appId: string): Promise<string | null> {
+    const client = await this.connect();
+    const key = `wechat_access_token:${appId}`;
+    const token = await client.get(key);
+
+    if (token) {
+      console.debug(`从缓存获取微信access_token成功,appId: ${appId}`);
+    } else {
+      console.debug(`缓存中未找到微信access_token,appId: ${appId}`);
+    }
+
+    return token;
+  }
+
+  /**
+   * 删除微信access_token缓存
+   * @param appId 微信小程序appId
+   */
+  async deleteWechatAccessToken(appId: string): Promise<void> {
+    const client = await this.connect();
+    const key = `wechat_access_token:${appId}`;
+    await client.del(key);
+    console.debug(`删除微信access_token缓存成功,appId: ${appId}`);
+  }
+
+  /**
+   * 检查微信access_token缓存是否有效
+   * @param appId 微信小程序appId
+   * @returns 是否有效
+   */
+  async isWechatAccessTokenValid(appId: string): Promise<boolean> {
+    const token = await this.getWechatAccessToken(appId);
+    return !!token;
+  }
+
+  /**
+   * 获取微信access_token缓存的剩余生存时间
+   * @param appId 微信小程序appId
+   * @returns 剩余时间(秒),-1表示永不过期,-2表示键不存在
+   */
+  async getWechatAccessTokenTTL(appId: string): Promise<number> {
+    const client = await this.connect();
+    const key = `wechat_access_token:${appId}`;
+    return await client.ttl(key);
+  }
+
+  /**
+   * 清除所有微信access_token缓存
+   */
+  async clearAllWechatAccessTokens(): Promise<void> {
+    const client = await this.connect();
+    const pattern = `wechat_access_token:*`;
+
+    // 使用SCAN命令遍历匹配的键并删除
+    let cursor = 0;
+    do {
+      const result = await client.scan(cursor, {
+        MATCH: pattern,
+        COUNT: 100
+      });
+
+      cursor = result.cursor;
+      const keys = result.keys;
+
+      if (keys.length > 0) {
+        await client.del(keys);
+        console.debug(`批量删除微信access_token缓存,数量: ${keys.length}`);
+      }
+    } while (cursor !== 0);
+
+    console.debug('所有微信access_token缓存已清除');
+  }
 }
 
 export const redisUtil = RedisUtil.getInstance();