|
|
@@ -12,11 +12,13 @@ export class MiniAuthService {
|
|
|
private userRepository: Repository<UserEntityMt>;
|
|
|
private fileService: FileServiceMt;
|
|
|
private systemConfigService: SystemConfigServiceMt;
|
|
|
+ private dataSource: DataSource;
|
|
|
|
|
|
constructor(dataSource: DataSource) {
|
|
|
this.userRepository = dataSource.getRepository(UserEntityMt);
|
|
|
this.fileService = new FileServiceMt(dataSource);
|
|
|
this.systemConfigService = new SystemConfigServiceMt(dataSource);
|
|
|
+ this.dataSource = dataSource;
|
|
|
}
|
|
|
|
|
|
async miniLogin(code: string, tenantId?: number): Promise<{ token: string; user: UserEntityMt; isNewUser: boolean }> {
|
|
|
@@ -221,8 +223,6 @@ export class MiniAuthService {
|
|
|
|
|
|
|
|
|
|
|
|
-
|
|
|
-
|
|
|
/**
|
|
|
* 发送微信模板消息
|
|
|
*/
|
|
|
@@ -336,6 +336,627 @@ export class MiniAuthService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据订单号查询微信支付订单信息
|
|
|
+ */
|
|
|
+ private async getWechatOrderInfo(orderNo: string, tenantId?: number): Promise<{
|
|
|
+ outTradeNo: string;
|
|
|
+ wechatTransactionId?: string;
|
|
|
+ }> {
|
|
|
+ try {
|
|
|
+ // 1. 先通过订单号查询订单,获取订单ID
|
|
|
+ const orderRepository = this.dataSource.getRepository('OrderMt');
|
|
|
+ const order = await orderRepository.findOne({
|
|
|
+ where: {
|
|
|
+ orderNo: orderNo,
|
|
|
+ ...(tenantId !== undefined ? { tenantId } : {})
|
|
|
+ },
|
|
|
+ select: ['id'] // 只选择id字段
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!order) {
|
|
|
+ throw new Error(`订单不存在: ${orderNo}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 通过订单ID查询支付记录
|
|
|
+ const paymentRepository = this.dataSource.getRepository('PaymentMtEntity');
|
|
|
+ const payment = await paymentRepository.findOne({
|
|
|
+ where: {
|
|
|
+ externalOrderId: order.id,
|
|
|
+ ...(tenantId !== undefined ? { tenantId } : {})
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!payment) {
|
|
|
+ throw new Error(`支付记录不存在,订单号: ${orderNo}, 订单ID: ${order.id}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!payment.outTradeNo) {
|
|
|
+ throw new Error(`支付记录 ${orderNo} 没有微信支付商户订单号`);
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ outTradeNo: payment.outTradeNo,
|
|
|
+ wechatTransactionId: payment.wechatTransactionId
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`查询微信支付订单信息失败:`, {
|
|
|
+ orderNo,
|
|
|
+ tenantId,
|
|
|
+ error: error instanceof Error ? error.message : '未知错误'
|
|
|
+ });
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据订单ID查询用户openid
|
|
|
+ */
|
|
|
+ private async getOpenidByOrderId(orderId: string, tenantId?: number): Promise<string> {
|
|
|
+ try {
|
|
|
+ // 动态获取订单仓库,避免循环依赖
|
|
|
+ // 注意:这里使用 any 类型来避免类型导入问题
|
|
|
+ const orderRepository = this.dataSource.getRepository('OrderMt');
|
|
|
+
|
|
|
+ // 查询订单,包含用户关联关系
|
|
|
+ const order = await orderRepository.findOne({
|
|
|
+ where: { orderNo: orderId, ...(tenantId !== undefined ? { tenantId } : {}) },
|
|
|
+ relations: ['user']
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!order) {
|
|
|
+ throw new Error(`订单不存在: ${orderId}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!order.user) {
|
|
|
+ throw new Error(`订单 ${orderId} 没有关联的用户信息`);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!order.user.openid) {
|
|
|
+ throw new Error(`订单 ${orderId} 关联的用户没有openid`);
|
|
|
+ }
|
|
|
+
|
|
|
+ return order.user.openid;
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`查询订单openid失败:`, {
|
|
|
+ orderId,
|
|
|
+ tenantId,
|
|
|
+ error: error instanceof Error ? error.message : '未知错误'
|
|
|
+ });
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发货信息录入
|
|
|
+ * 微信小程序发货信息录入API:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html
|
|
|
+ */
|
|
|
+ async uploadShippingInfo(params: {
|
|
|
+ orderId: string; // 小程序订单ID
|
|
|
+ deliveryType: number; // 发货方式:1-物流快递, 2-同城配送, 3-虚拟发货, 4-用户自提
|
|
|
+ expressInfo?: {
|
|
|
+ deliveryId: string; // 快递公司ID,通过getCompanyList获取
|
|
|
+ waybillId: string; // 快递单号
|
|
|
+ };
|
|
|
+ localDeliveryInfo?: {
|
|
|
+ deliveryName?: string; // 配送员姓名
|
|
|
+ deliveryPhone?: string; // 配送员手机号
|
|
|
+ };
|
|
|
+ isAllDelivered?: boolean; // 是否全部发货完成,默认true
|
|
|
+ tenantId?: number;
|
|
|
+ }): Promise<any> {
|
|
|
+ const { orderId, deliveryType, expressInfo, localDeliveryInfo, isAllDelivered = true, tenantId } = params;
|
|
|
+
|
|
|
+ // 参数验证
|
|
|
+ if (!orderId) {
|
|
|
+ throw new Error('订单ID不能为空');
|
|
|
+ }
|
|
|
+
|
|
|
+ const accessToken = await this.getWxAccessToken(tenantId);
|
|
|
+ //"98_eafrToobfnW67wu1fP8bmeqpL3gDnwIYE5QGkMRxyjArIF8c3w8K9bxWRq7oAy-miAJV033YRxJhEjHiNXghXL0Xv3AfTEqaJjqPUk5GVJpnnTkqDE5O7YdO2ZYJKCdABAWXP";
|
|
|
+
|
|
|
+ // 获取商户号(mchid)
|
|
|
+ let mchid: string | null = null;
|
|
|
+ if (tenantId !== undefined) {
|
|
|
+ const configKeys = ['wx.payment.merchant.id'];
|
|
|
+ const configs = await this.systemConfigService.getConfigsByKeys(configKeys, tenantId);
|
|
|
+ mchid = configs['wx.payment.merchant.id'];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果系统配置中没有找到,回退到环境变量
|
|
|
+ if (!mchid) {
|
|
|
+ mchid = process.env.WECHAT_MERCHANT_ID || null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!mchid) {
|
|
|
+ throw new Error('微信商户号配置缺失,无法使用小程序订单号发货');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询支付记录,获取微信支付订单号
|
|
|
+ const wechatOrderInfo = await this.getWechatOrderInfo(orderId, tenantId);
|
|
|
+
|
|
|
+ // 查询订单对应的用户openid
|
|
|
+ const openid = await this.getOpenidByOrderId(orderId, tenantId);
|
|
|
+
|
|
|
+ const shippingData: any = {
|
|
|
+ order_key: {
|
|
|
+ order_number_type: 1, // 1-小程序订单号
|
|
|
+ out_trade_no: wechatOrderInfo.outTradeNo, // 使用微信支付商户订单号
|
|
|
+ mchid: mchid // 添加商户号字段
|
|
|
+ },
|
|
|
+ logistics_type: deliveryType,
|
|
|
+ delivery_mode: isAllDelivered ? 1 : 2,
|
|
|
+ shipping_list: [
|
|
|
+ {
|
|
|
+ item_desc: `订单${orderId}`, // 商品描述
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ upload_time: new Date().toISOString().replace('Z', '+08:00'), // RFC 3339 格式,使用东八区时间
|
|
|
+ payer: {
|
|
|
+ openid: openid // 支付方openid,从订单获取用户openid
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 根据发货方式添加不同的信息
|
|
|
+ switch (deliveryType) {
|
|
|
+ case 1: // 物流快递
|
|
|
+ if (!expressInfo?.deliveryId || !expressInfo?.waybillId) {
|
|
|
+ throw new Error('快递发货需要提供快递公司ID和运单号');
|
|
|
+ }
|
|
|
+ shippingData.delivery_list = [
|
|
|
+ {
|
|
|
+ delivery_id: expressInfo.deliveryId,
|
|
|
+ waybill_id: expressInfo.waybillId
|
|
|
+ }
|
|
|
+ ];
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 2: // 同城配送
|
|
|
+ // if (!localDeliveryInfo?.deliveryName || !localDeliveryInfo?.deliveryPhone) {
|
|
|
+ // throw new Error('同城配送需要提供配送员姓名和手机号');
|
|
|
+ // }
|
|
|
+ // shippingData.delivery_list = [
|
|
|
+ // {
|
|
|
+ // delivery_name: localDeliveryInfo.deliveryName,
|
|
|
+ // delivery_phone: localDeliveryInfo.deliveryPhone
|
|
|
+ // }
|
|
|
+ // ];
|
|
|
+ // break;
|
|
|
+
|
|
|
+ case 3: // 虚拟发货
|
|
|
+ case 4: // 用户自提
|
|
|
+ // 无需物流,不需要额外信息
|
|
|
+ break;
|
|
|
+
|
|
|
+ default:
|
|
|
+ throw new Error(`不支持的发货方式: ${deliveryType}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const url = `https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token=${accessToken}`;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await axios.post(url, shippingData, {
|
|
|
+ timeout: 10000,
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.data.errcode && response.data.errcode !== 0) {
|
|
|
+ const errorMsg = this.getShippingErrorMsg(response.data.errcode, response.data.errmsg);
|
|
|
+ console.error(`微信发货失败: ${errorMsg}`, {
|
|
|
+ orderId,
|
|
|
+ deliveryType,
|
|
|
+ errcode: response.data.errcode,
|
|
|
+ errmsg: response.data.errmsg
|
|
|
+ });
|
|
|
+ throw new Error(errorMsg);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.debug('微信发货成功:', {
|
|
|
+ orderId,
|
|
|
+ deliveryType,
|
|
|
+ response: response.data
|
|
|
+ });
|
|
|
+ return response.data;
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发货信息录入异常:', {
|
|
|
+ orderId,
|
|
|
+ deliveryType,
|
|
|
+ error: error instanceof Error ? error.message : '未知错误',
|
|
|
+ stack: error instanceof Error ? error.stack : undefined
|
|
|
+ });
|
|
|
+
|
|
|
+ if (axios.isAxiosError(error)) {
|
|
|
+ if (error.code === 'ECONNABORTED') {
|
|
|
+ throw new Error('微信服务器连接超时,请稍后重试');
|
|
|
+ } else if (error.code === 'ENOTFOUND') {
|
|
|
+ throw new Error('无法连接到微信服务器,请检查网络');
|
|
|
+ } else {
|
|
|
+ throw new Error(`微信服务器连接失败: ${error.message}`);
|
|
|
+ }
|
|
|
+ } else if (error instanceof Error) {
|
|
|
+ // 如果是我们抛出的错误,直接抛出
|
|
|
+ throw error;
|
|
|
+ } else {
|
|
|
+ throw new Error('发货信息录入失败,请稍后重试');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ async getCompanyList(tenantId?: number): Promise<any> {
|
|
|
+
|
|
|
+ const accessToken = await this.getWxAccessToken(tenantId);
|
|
|
+
|
|
|
+ const url = `https://api.weixin.qq.com/product/delivery/get_company_list?access_token=${accessToken}`;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await axios.post(url, {}, {
|
|
|
+ timeout: 10000,
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.data.errcode && response.data.errcode !== 0) {
|
|
|
+ // 处理特定的错误码
|
|
|
+ if (response.data.errcode === 48001) {
|
|
|
+ throw new Error('API功能未授权,请确认小程序已开通微信小店功能并申请相应权限');
|
|
|
+ }
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async getIsTradeManaged(tenantId?: number): Promise<any> {
|
|
|
+
|
|
|
+ const accessToken = await this.getWxAccessToken(tenantId);
|
|
|
+
|
|
|
+ const url = `https://api.weixin.qq.com/wxa/sec/order/is_trade_managed?access_token=${accessToken}`;
|
|
|
+
|
|
|
+
|
|
|
+ // 从系统配置获取appid
|
|
|
+ let appId: string | null = null;
|
|
|
+ const configKeys = ['wx.mini.app.id', 'wx.mini.app.secret'];
|
|
|
+
|
|
|
+ if (tenantId !== undefined) {
|
|
|
+ const configs = await this.systemConfigService.getConfigsByKeys(configKeys, tenantId);
|
|
|
+ appId = configs['wx.mini.app.id'];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果系统配置中没有找到,回退到环境变量
|
|
|
+ if (!appId) {
|
|
|
+ appId = process.env.WX_MINI_APP_ID || null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查appid是否存在
|
|
|
+ if (!appId) {
|
|
|
+ throw new Error('微信小程序appid配置缺失');
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 微信API要求POST方法,并且需要在请求体中传递appid参数
|
|
|
+ const requestBody = {
|
|
|
+ appid: appId
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await axios.post(url, requestBody, {
|
|
|
+ timeout: 10000,
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.data.errcode && response.data.errcode !== 0) {
|
|
|
+ // 处理特定的错误码
|
|
|
+ if (response.data.errcode === 48001) {
|
|
|
+ throw new Error('API功能未授权,请确认小程序已开通微信小店功能并申请相应权限');
|
|
|
+ }
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // async getCompanyList(tenantId?: number): Promise<any> {
|
|
|
+ // // 使用内置的快递公司列表,避免微信API权限问题
|
|
|
+ // const builtInDeliveryCompanies = [
|
|
|
+ // { delivery_id: 'SF', delivery_name: '顺丰速运' },
|
|
|
+ // { delivery_id: 'STO', delivery_name: '申通快递' },
|
|
|
+ // { delivery_id: 'YTO', delivery_name: '圆通速递' },
|
|
|
+ // { delivery_id: 'YD', delivery_name: '韵达快递' },
|
|
|
+ // { delivery_id: 'ZTO', delivery_name: '中通快递' },
|
|
|
+ // { delivery_id: 'HTKY', delivery_name: '百世快递' },
|
|
|
+ // { delivery_id: 'YZPY', delivery_name: '邮政快递' },
|
|
|
+ // { delivery_id: 'JD', delivery_name: '京东物流' },
|
|
|
+ // { delivery_id: 'EMS', delivery_name: 'EMS' },
|
|
|
+ // { delivery_id: 'TTKDEX', delivery_name: '天天快递' },
|
|
|
+ // { delivery_id: 'DBL', delivery_name: '德邦物流' },
|
|
|
+ // { delivery_id: 'ZJS', delivery_name: '宅急送' }
|
|
|
+ // ];
|
|
|
+
|
|
|
+ // console.debug('返回内置快递公司列表,共', builtInDeliveryCompanies.length, '家');
|
|
|
+
|
|
|
+ // return {
|
|
|
+ // company_list: builtInDeliveryCompanies
|
|
|
+ // };
|
|
|
+ // }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询发货状态
|
|
|
+ * 微信小程序发货状态查询API
|
|
|
+ */
|
|
|
+ async getShippingStatus(params: {
|
|
|
+ orderId: string; // 小程序订单ID
|
|
|
+ tenantId?: number;
|
|
|
+ }): Promise<{
|
|
|
+ orderId: string;
|
|
|
+ shippingStatus: number; // 发货状态:0-未发货,1-已发货,2-已签收,3-已取消
|
|
|
+ deliveryMode?: string; // 发货方式
|
|
|
+ expressInfo?: {
|
|
|
+ deliveryId: string;
|
|
|
+ deliveryName: string;
|
|
|
+ waybillId: string;
|
|
|
+ };
|
|
|
+ localDeliveryInfo?: {
|
|
|
+ deliveryName: string;
|
|
|
+ deliveryPhone: string;
|
|
|
+ };
|
|
|
+ shippingTime?: string; // 发货时间
|
|
|
+ estimatedDeliveryTime?: string; // 预计送达时间
|
|
|
+ actualDeliveryTime?: string; // 实际送达时间
|
|
|
+ }> {
|
|
|
+ const { orderId, tenantId } = params;
|
|
|
+
|
|
|
+ if (!orderId) {
|
|
|
+ throw new Error('订单ID不能为空');
|
|
|
+ }
|
|
|
+
|
|
|
+ const accessToken = await this.getWxAccessToken(tenantId);
|
|
|
+
|
|
|
+ const queryData = {
|
|
|
+ order_key: {
|
|
|
+ order_number_type: 1, // 1-小程序订单号
|
|
|
+ out_trade_no: orderId
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const url = `https://api.weixin.qq.com/wxa/sec/order/get_shipping_info?access_token=${accessToken}`;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await axios.post(url, queryData, {
|
|
|
+ timeout: 10000,
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.data.errcode && response.data.errcode !== 0) {
|
|
|
+ const errorMsg = this.getShippingErrorMsg(response.data.errcode, response.data.errmsg);
|
|
|
+ console.error(`查询发货状态失败: ${errorMsg}`, {
|
|
|
+ orderId,
|
|
|
+ errcode: response.data.errcode,
|
|
|
+ errmsg: response.data.errmsg
|
|
|
+ });
|
|
|
+ throw new Error(errorMsg);
|
|
|
+ }
|
|
|
+
|
|
|
+ const shippingInfo = response.data.shipping_info;
|
|
|
+ const result: {
|
|
|
+ orderId: string;
|
|
|
+ shippingStatus: number;
|
|
|
+ deliveryMode?: string;
|
|
|
+ expressInfo?: {
|
|
|
+ deliveryId: string;
|
|
|
+ deliveryName: string;
|
|
|
+ waybillId: string;
|
|
|
+ };
|
|
|
+ localDeliveryInfo?: {
|
|
|
+ deliveryName: string;
|
|
|
+ deliveryPhone: string;
|
|
|
+ };
|
|
|
+ shippingTime?: string;
|
|
|
+ estimatedDeliveryTime?: string;
|
|
|
+ actualDeliveryTime?: string;
|
|
|
+ } = {
|
|
|
+ orderId,
|
|
|
+ shippingStatus: shippingInfo?.logistics_status || 0,
|
|
|
+ deliveryMode: this.getDeliveryModeFromLogisticsType(shippingInfo?.logistics_type),
|
|
|
+ shippingTime: shippingInfo?.shipping_time,
|
|
|
+ estimatedDeliveryTime: shippingInfo?.estimated_delivery_time,
|
|
|
+ actualDeliveryTime: shippingInfo?.actual_delivery_time
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理快递信息
|
|
|
+ if (shippingInfo?.delivery_list?.length > 0) {
|
|
|
+ const delivery = shippingInfo.delivery_list[0];
|
|
|
+ if (delivery.delivery_id) {
|
|
|
+ result.expressInfo = {
|
|
|
+ deliveryId: delivery.delivery_id,
|
|
|
+ deliveryName: delivery.delivery_name || this.getDeliveryCompanyName(delivery.delivery_id),
|
|
|
+ waybillId: delivery.waybill_id
|
|
|
+ };
|
|
|
+ } else if (delivery.delivery_name) {
|
|
|
+ result.localDeliveryInfo = {
|
|
|
+ deliveryName: delivery.delivery_name,
|
|
|
+ deliveryPhone: delivery.delivery_phone
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.debug('发货状态查询成功:', {
|
|
|
+ orderId,
|
|
|
+ shippingStatus: result.shippingStatus,
|
|
|
+ deliveryMode: result.deliveryMode
|
|
|
+ });
|
|
|
+
|
|
|
+ return result;
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发货状态查询异常:', {
|
|
|
+ orderId,
|
|
|
+ error: error instanceof Error ? error.message : '未知错误'
|
|
|
+ });
|
|
|
+
|
|
|
+ if (axios.isAxiosError(error)) {
|
|
|
+ throw new Error(`查询发货状态失败: ${error.message}`);
|
|
|
+ }
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据发货方式获取物流类型编码
|
|
|
+ * 微信API要求:0-无需物流,1-快递,2-同城配送
|
|
|
+ */
|
|
|
+ private getLogisticsType(deliveryMode: 'express' | 'no_logistics' | 'local_delivery'): number {
|
|
|
+ switch (deliveryMode) {
|
|
|
+ case 'no_logistics':
|
|
|
+ return 0;
|
|
|
+ case 'express':
|
|
|
+ return 1;
|
|
|
+ case 'local_delivery':
|
|
|
+ return 2;
|
|
|
+ default:
|
|
|
+ throw new Error(`不支持的发货方式: ${deliveryMode}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取发货错误信息
|
|
|
+ * 根据微信API错误码返回友好的错误信息
|
|
|
+ */
|
|
|
+ private getShippingErrorMsg(errcode: number, errmsg: string): string {
|
|
|
+ const errorMap: Record<number, string> = {
|
|
|
+ 40001: 'access_token无效或已过期',
|
|
|
+ 40003: '非法的openid',
|
|
|
+ 40013: 'appid无效',
|
|
|
+ 40029: 'code无效',
|
|
|
+ 40050: '不支持的物流类型',
|
|
|
+ 40051: '订单不存在',
|
|
|
+ 40052: '订单状态不允许发货',
|
|
|
+ 40053: '快递公司ID无效',
|
|
|
+ 40054: '运单号格式错误',
|
|
|
+ 40055: '同城配送信息不完整',
|
|
|
+ 40056: '订单已发货,不能重复发货',
|
|
|
+ 40057: '订单已取消,不能发货',
|
|
|
+ 40058: '订单已退款,不能发货',
|
|
|
+ 40059: '订单已关闭,不能发货',
|
|
|
+ 41001: '缺少access_token参数',
|
|
|
+ 41002: '缺少订单ID参数',
|
|
|
+ 41003: '缺少物流类型参数',
|
|
|
+ 41004: '缺少快递公司ID参数',
|
|
|
+ 41005: '缺少运单号参数',
|
|
|
+ 41006: '缺少同城配送员姓名',
|
|
|
+ 41007: '缺少同城配送员手机号',
|
|
|
+ 42001: 'access_token已过期',
|
|
|
+ 43001: '需要GET请求',
|
|
|
+ 43002: '需要POST请求',
|
|
|
+ 43003: '需要HTTPS请求',
|
|
|
+ 44001: '多媒体文件为空',
|
|
|
+ 45001: '多媒体文件大小超过限制',
|
|
|
+ 45002: '消息内容超过限制',
|
|
|
+ 45003: '标题字段超过限制',
|
|
|
+ 45004: '描述字段超过限制',
|
|
|
+ 45005: '链接字段超过限制',
|
|
|
+ 45006: '图片链接字段超过限制',
|
|
|
+ 45007: '语音播放时间超过限制',
|
|
|
+ 45008: '图文消息超过限制',
|
|
|
+ 45009: '接口调用超过限制',
|
|
|
+ 45010: '创建菜单个数超过限制',
|
|
|
+ 45011: 'API调用太频繁,请稍候再试',
|
|
|
+ 45015: '回复时间超过限制',
|
|
|
+ 45016: '系统分组,不允许修改',
|
|
|
+ 45017: '分组名字过长',
|
|
|
+ 45018: '分组数量超过上限',
|
|
|
+ 46001: '不存在媒体数据',
|
|
|
+ 46002: '不存在的菜单版本',
|
|
|
+ 46003: '不存在的菜单数据',
|
|
|
+ 46004: '不存在的用户',
|
|
|
+ 47001: '解析JSON/XML内容错误',
|
|
|
+ 48001: 'api功能未授权',
|
|
|
+ 50001: '用户未授权该api',
|
|
|
+ 50002: '用户受限,可能是违规后接口被封禁'
|
|
|
+ };
|
|
|
+
|
|
|
+ const friendlyMsg = errorMap[errcode] || errmsg;
|
|
|
+ return `微信发货失败: ${friendlyMsg} (错误码: ${errcode})`;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据物流类型编码获取发货方式
|
|
|
+ */
|
|
|
+ private getDeliveryModeFromLogisticsType(logisticsType?: number): string {
|
|
|
+ switch (logisticsType) {
|
|
|
+ case 0:
|
|
|
+ return 'no_logistics';
|
|
|
+ case 1:
|
|
|
+ return 'express';
|
|
|
+ case 2:
|
|
|
+ return 'local_delivery';
|
|
|
+ default:
|
|
|
+ return 'unknown';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据快递公司ID获取公司名称
|
|
|
+ */
|
|
|
+ private getDeliveryCompanyName(deliveryId: string): string {
|
|
|
+ const deliveryMap: Record<string, string> = {
|
|
|
+ 'SF': '顺丰速运',
|
|
|
+ 'STO': '申通快递',
|
|
|
+ 'YTO': '圆通速递',
|
|
|
+ 'YD': '韵达快递',
|
|
|
+ 'ZTO': '中通快递',
|
|
|
+ 'HTKY': '百世快递',
|
|
|
+ 'YZPY': '邮政快递',
|
|
|
+ 'JD': '京东物流',
|
|
|
+ 'YUNDA': '韵达快递',
|
|
|
+ 'EMS': 'EMS',
|
|
|
+ 'TTKDEX': '天天快递',
|
|
|
+ 'DBL': '德邦物流',
|
|
|
+ 'ZJS': '宅急送',
|
|
|
+ 'QFKD': '全峰快递',
|
|
|
+ 'UAPEX': '全一快递',
|
|
|
+ 'CNEX': '佳吉快运',
|
|
|
+ 'FAST': '快捷快递',
|
|
|
+ 'POSTB': '邮政小包',
|
|
|
+ 'GTO': '国通快递',
|
|
|
+ 'ANE': '安能物流'
|
|
|
+ };
|
|
|
+
|
|
|
+ return deliveryMap[deliveryId] || deliveryId;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
/**
|
|
|
* 快递公司名称到微信小店delivery_id的映射
|
|
|
* 实际项目中需要从数据库或配置中获取
|
|
|
@@ -399,25 +1020,48 @@ export class MiniAuthService {
|
|
|
throw new Error('微信小程序配置缺失');
|
|
|
}
|
|
|
|
|
|
- // 获取access_token
|
|
|
- const accessToken = await this.getAccessToken(appId, appSecret);
|
|
|
+ // 获取access_token(支持租户隔离)
|
|
|
+ const accessToken = await this.getAccessToken(appId, appSecret, tenantId);
|
|
|
return accessToken;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取微信access_token(带缓存机制)
|
|
|
+ * 获取微信access_token(带缓存机制,支持租户隔离)
|
|
|
*/
|
|
|
- private async getAccessToken(appId: string, appSecret: string): Promise<string> {
|
|
|
- // 1. 首先尝试从Redis缓存获取
|
|
|
- const cachedToken = await redisUtil.getWechatAccessToken(appId);
|
|
|
+ private async getAccessToken(appId: string, appSecret: string, tenantId?: number): Promise<string> {
|
|
|
+ // 1. 首先尝试从Redis缓存获取(支持租户隔离)
|
|
|
+ const cachedToken = await redisUtil.getWechatAccessToken(appId, tenantId);
|
|
|
|
|
|
if (cachedToken) {
|
|
|
- console.debug(`使用缓存的微信access_token,appId: ${appId}`);
|
|
|
- return cachedToken;
|
|
|
+ // 检查缓存剩余时间,如果少于60秒则认为即将过期,重新获取
|
|
|
+ const ttl = await redisUtil.getWechatAccessTokenTTL(appId, tenantId);
|
|
|
+
|
|
|
+ // ttl返回值说明:
|
|
|
+ // - 正整数:剩余时间(秒)
|
|
|
+ // - -1:键存在但没有设置过期时间(不应该发生,因为我们设置了EX)
|
|
|
+ // - -2:键不存在(不应该发生,因为我们已经获取到了token)
|
|
|
+
|
|
|
+ if (ttl > 60) {
|
|
|
+ // 剩余时间大于60秒,使用缓存
|
|
|
+ console.debug(`使用缓存的微信access_token,appId: ${appId}, 租户ID: ${tenantId || '无'}, 剩余时间: ${ttl}秒`);
|
|
|
+ return cachedToken;
|
|
|
+ } else if (ttl === -1) {
|
|
|
+ // 键存在但没有设置过期时间,删除并重新获取
|
|
|
+ console.warn(`微信access_token缓存没有设置过期时间,删除并重新获取,appId: ${appId}, 租户ID: ${tenantId || '无'}`);
|
|
|
+ await redisUtil.deleteWechatAccessToken(appId, tenantId);
|
|
|
+ } else if (ttl === -2) {
|
|
|
+ // 键不存在(虽然获取到了token,但可能被其他进程删除),重新获取
|
|
|
+ console.warn(`微信access_token缓存键不存在,重新获取,appId: ${appId}, 租户ID: ${tenantId || '无'}`);
|
|
|
+ } else {
|
|
|
+ // ttl <= 60 且 > -2,剩余时间不足60秒,认为缓存即将过期,重新获取
|
|
|
+ console.debug(`微信access_token缓存即将过期,剩余时间: ${ttl}秒,重新获取,appId: ${appId}, 租户ID: ${tenantId || '无'}`);
|
|
|
+ // 先删除即将过期的缓存
|
|
|
+ await redisUtil.deleteWechatAccessToken(appId, tenantId);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.debug(`缓存中未找到微信access_token,从API获取,appId: ${appId}, 租户ID: ${tenantId || '无'}`);
|
|
|
}
|
|
|
|
|
|
- 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}`;
|
|
|
|
|
|
@@ -431,12 +1075,12 @@ export class MiniAuthService {
|
|
|
const accessToken = response.data.access_token;
|
|
|
const expiresIn = response.data.expires_in || 7200; // 微信默认返回7200秒(2小时)
|
|
|
|
|
|
- // 3. 将获取到的access_token存入Redis缓存
|
|
|
+ // 3. 将获取到的access_token存入Redis缓存(支持租户隔离)
|
|
|
// 设置过期时间比微信返回的expires_in少100秒,确保安全
|
|
|
const cacheExpiresIn = Math.max(expiresIn - 100, 600); // 最少缓存10分钟
|
|
|
- await redisUtil.setWechatAccessToken(appId, accessToken, cacheExpiresIn);
|
|
|
+ await redisUtil.setWechatAccessToken(appId, accessToken, cacheExpiresIn, tenantId);
|
|
|
|
|
|
- console.debug(`微信access_token获取成功并已缓存,appId: ${appId}, 过期时间: ${cacheExpiresIn}秒`);
|
|
|
+ console.debug(`微信access_token获取成功并已缓存,appId: ${appId}, 租户ID: ${tenantId || '无'}, 过期时间: ${cacheExpiresIn}秒`);
|
|
|
|
|
|
return accessToken;
|
|
|
|
|
|
@@ -449,28 +1093,28 @@ export class MiniAuthService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 强制刷新微信access_token(清除缓存并重新获取)
|
|
|
+ * 强制刷新微信access_token(清除缓存并重新获取,支持租户隔离)
|
|
|
*/
|
|
|
- private async refreshAccessToken(appId: string, appSecret: string): Promise<string> {
|
|
|
- console.debug(`强制刷新微信access_token,appId: ${appId}`);
|
|
|
+ private async refreshAccessToken(appId: string, appSecret: string, tenantId?: number): Promise<string> {
|
|
|
+ console.debug(`强制刷新微信access_token,appId: ${appId}, 租户ID: ${tenantId || '无'}`);
|
|
|
|
|
|
- // 1. 清除缓存
|
|
|
- await redisUtil.deleteWechatAccessToken(appId);
|
|
|
+ // 1. 清除缓存(支持租户隔离)
|
|
|
+ await redisUtil.deleteWechatAccessToken(appId, tenantId);
|
|
|
|
|
|
- // 2. 重新获取
|
|
|
- return await this.getAccessToken(appId, appSecret);
|
|
|
+ // 2. 重新获取(支持租户隔离)
|
|
|
+ return await this.getAccessToken(appId, appSecret, tenantId);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 检查微信access_token缓存状态
|
|
|
+ * 检查微信access_token缓存状态(支持租户隔离)
|
|
|
*/
|
|
|
- private async checkAccessTokenCacheStatus(appId: string): Promise<{
|
|
|
+ private async checkAccessTokenCacheStatus(appId: string, tenantId?: number): Promise<{
|
|
|
hasCache: boolean;
|
|
|
ttl: number;
|
|
|
isValid: boolean;
|
|
|
}> {
|
|
|
- const hasCache = await redisUtil.isWechatAccessTokenValid(appId);
|
|
|
- const ttl = await redisUtil.getWechatAccessTokenTTL(appId);
|
|
|
+ const hasCache = await redisUtil.isWechatAccessTokenValid(appId, tenantId);
|
|
|
+ const ttl = await redisUtil.getWechatAccessTokenTTL(appId, tenantId);
|
|
|
|
|
|
return {
|
|
|
hasCache,
|