Files
device-voice-h5/docs/微信支付集成文档.md
sexygoat f634af1d65
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 44s
fix: 套餐列表支付, 非设备显示phone
2026-04-14 11:48:03 +08:00

16 KiB
Raw Permalink Blame History

微信小程序支付集成文档

概述

本文档总结了套餐订购模块中的微信支付完整实现流程,可复用到其他项目中。

一、核心文件结构

miniprogram/
├── utils/
│   └── payment.ts          # 微信支付工具类
├── api/
│   └── order.ts            # 订单API接口
├── types/
│   └── api.ts              # API类型定义
└── pages/
    └── package/
        └── package.ts      # 套餐订购页面(业务实现)

二、支付流程

2.1 标准套餐支付流程(三步走)

1. 创建订单 → 2. 获取支付参数 → 3. 调起微信支付

第一步:创建订单

接口: POST /api/c/v1/orders/create

请求参数:

{
  app_type: 'miniapp',              // 应用类型miniapp(小程序) | official_account(公众号)
  identifier: string,                // 资产标识符设备ID/ICCID等
  package_ids: number[]              // 套餐ID列表
}

响应数据:

{
  order_type: 'package' | 'recharge',  // 订单类型
  order: {
    order_id: number,                   // 订单ID用于后续支付
    order_no: string,
    payment_status: 1,                  // 1:待支付
    total_amount: number,               // 总金额(分)
    created_at: string
  },
  pay_config?: {...}                    // 强充订单才返回见2.2
}

第二步:获取支付参数

接口: POST /api/c/v1/orders/:id/pay

请求参数:

{
  payment_method: 'wechat',           // 支付方式wechat | wallet
  app_type: 'miniapp'                 // 应用类型(微信支付必传)
}

响应数据:

{
  payment_method: 'wechat',
  pay_config: {
    app_id: string,          // 微信AppID
    timestamp: string,        // 时间戳
    nonce_str: string,        // 随机字符串
    package: string,          // 预支付ID格式prepay_id=wx...
    sign_type: string,        // 签名类型RSA/MD5/HMAC-SHA256
    pay_sign: string          // 支付签名
  }
}

第三步:调起微信支付

使用 wx.requestPayment() 调起支付:

wx.requestPayment({
  timeStamp: pay_config.timestamp,
  nonceStr: pay_config.nonce_str,
  package: pay_config.package,
  signType: pay_config.sign_type,
  paySign: pay_config.pay_sign,
  success: (res) => {
    // 支付成功
  },
  fail: (err) => {
    // 支付失败或取消
  }
})

2.2 强充订单支付流程(一步走)

当订单返回 order_type === 'recharge' 且有 pay_config 时,表示强充订单:

创建订单(直接返回支付参数) → 调起微信支付

特点:

  • 创建订单时已返回完整 pay_config
  • 无需再调用 /orders/:id/pay 接口
  • 直接使用返回的 pay_config 调起支付

判断逻辑:

if (orderResult.order_type === 'recharge' && orderResult.pay_config) {
  // 强充订单,直接支付
  await WxPayment.payWithCallback(orderResult.pay_config, callbacks);
} else {
  // 普通订单,需要获取支付参数
  const payResult = await OrderApi.payOrder(orderId, {...});
  await WxPayment.payWithCallback(payResult.pay_config, callbacks);
}

三、核心代码实现

3.1 微信支付工具类payment.ts

/**
 * 微信支付工具类
 */
import { Logger } from './logger';

/** 微信支付配置 */
export interface WxPayConfig {
  timeStamp: string;
  nonceStr: string;
  package: string;
  signType: 'RSA' | 'MD5' | 'HMAC-SHA256';
  paySign: string;
}

/** 支付结果 */
export interface PaymentResult {
  success: boolean;
  errMsg?: string;
  userCancelled?: boolean;
}

export class WxPayment {
  /**
   * 发起微信支付
   * @param payConfig 后端返回的支付配置
   * @returns 支付结果
   */
  static async pay(payConfig: {
    app_id: string;
    timestamp: string;
    nonce_str: string;
    package: string;
    sign_type: string;
    pay_sign: string;
  }): Promise<PaymentResult> {
    return new Promise((resolve) => {
      // 1. 验证 package 参数格式
      if (!payConfig.package || !payConfig.package.startsWith('prepay_id=')) {
        Logger.error('微信支付参数错误package 格式不正确', {
          package: payConfig.package,
          expected: 'prepay_id=wx...'
        });
        resolve({
          success: false,
          errMsg: '支付参数获取失败,请稍后重试'
        });
        return;
      }

      Logger.info('发起微信支付', {
        ...payConfig,
        pay_sign: payConfig.pay_sign.substring(0, 10) + '...'  // 脱敏
      });

      // 2. 调用微信支付API
      wx.requestPayment({
        timeStamp: payConfig.timestamp,
        nonceStr: payConfig.nonce_str,
        package: payConfig.package,
        signType: payConfig.sign_type as any,
        paySign: payConfig.pay_sign,
        success: (res) => {
          Logger.info('微信支付成功', res);
          resolve({ success: true });
        },
        fail: (err) => {
          Logger.error('微信支付失败', err);

          // 判断是否为用户取消
          const userCancelled = err.errMsg?.includes('cancel') ||
                               err.errMsg?.includes('用户取消');

          resolve({
            success: false,
            errMsg: err.errMsg,
            userCancelled
          });
        }
      });
    });
  }

  /**
   * 支付并使用回调处理结果
   * @param payConfig 支付配置
   * @param callbacks 回调函数
   */
  static async payWithCallback(
    payConfig: {
      app_id: string;
      timestamp: string;
      nonce_str: string;
      package: string;
      sign_type: string;
      pay_sign: string;
    },
    callbacks?: {
      onSuccess?: () => void;
      onFail?: (errMsg: string) => void;
      onCancel?: () => void;
    }
  ): Promise<PaymentResult> {
    const result = await this.pay(payConfig);

    // 根据结果调用相应回调
    if (result.success) {
      callbacks?.onSuccess?.();
    } else if (result.userCancelled) {
      callbacks?.onCancel?.();
    } else {
      callbacks?.onFail?.(result.errMsg || '支付失败');
    }

    return result;
  }
}

3.2 订单API封装order.ts

import { Request } from '../utils/request';
import type {
  CreateOrderRequest,
  CreateOrderResponse,
  PayOrderRequest,
  PayOrderResponse
} from '../types/api';

export class OrderApi {
  /**
   * 创建订单
   * POST /api/c/v1/orders/create
   */
  static async createOrder(data: CreateOrderRequest): Promise<CreateOrderResponse> {
    return Request.post<CreateOrderResponse>(
      '/api/c/v1/orders/create',
      data
    );
  }

  /**
   * 订单支付
   * POST /api/c/v1/orders/:id/pay
   */
  static async payOrder(orderId: number, data: PayOrderRequest): Promise<PayOrderResponse> {
    return Request.post<PayOrderResponse>(
      `/api/c/v1/orders/${orderId}/pay`,
      data
    );
  }
}

3.3 业务层实现示例package.ts

import { OrderApi } from '../../api/order';
import { WxPayment } from '../../utils/payment';
import { Logger } from '../../utils/logger';

/**
 * 创建订单并支付(微信支付)
 */
async createOrderAndPay() {
  try {
    const selectedPackage = this.data.packages[this.data.selectedIdx];

    // 第一步:创建订单
    wx.showLoading({ title: '创建订单...', mask: true });

    const orderResult = await OrderApi.createOrder({
      app_type: 'miniapp',
      identifier: this.getIdentifier(),
      package_ids: [selectedPackage.package_id]
    });

    // 判断是否为强充订单
    if (orderResult.order_type === 'recharge' && orderResult.pay_config) {
      // 强充订单:直接使用返回的 pay_config 支付

      // 验证 pay_config 有效性
      if (!orderResult.pay_config.package) {
        Logger.error('强充订单支付配置无效', {
          recharge_id: orderResult.recharge?.recharge_id
        });
        wx.hideLoading();
        wx.showToast({ title: '支付参数获取失败', icon: 'none' });
        return;
      }

      wx.hideLoading();
      this.closeModal();

      // 调起支付
      await WxPayment.payWithCallback(
        orderResult.pay_config,
        {
          onSuccess: () => {
            this.handlePaySuccess();
          },
          onFail: (errMsg) => {
            wx.showToast({ title: '支付失败', icon: 'none' });
          },
          onCancel: () => {
            wx.showToast({ title: '已取消支付', icon: 'none' });
          }
        }
      );
      return;
    }

    // 普通套餐订单:需要获取支付参数
    const orderId = orderResult.order.order_id;

    // 第二步:获取支付参数
    wx.showLoading({ title: '准备支付...', mask: true });

    const payResult = await OrderApi.payOrder(orderId, {
      payment_method: 'wechat',
      app_type: 'miniapp'
    });

    Logger.info('订单支付接口返回:', {
      order_id: orderId,
      has_pay_config: !!payResult.pay_config
    });

    wx.hideLoading();
    this.closeModal();

    // 验证 pay_config
    if (!payResult.pay_config || !payResult.pay_config.package) {
      Logger.error('订单支付接口返回配置无效', {
        order_id: orderId
      });
      wx.showToast({ title: '支付参数获取失败', icon: 'none' });
      return;
    }

    // 第三步:调起微信支付
    await WxPayment.payWithCallback(
      payResult.pay_config,
      {
        onSuccess: () => {
          this.handlePaySuccess();
        },
        onFail: (errMsg) => {
          wx.showToast({ title: '支付失败', icon: 'none' });
        },
        onCancel: () => {
          wx.showToast({ title: '已取消支付', icon: 'none' });
        }
      }
    );

  } catch (error: any) {
    wx.hideLoading();
    this.closeModal();

    Logger.error('创建订单或支付失败:', error);

    // 特殊错误处理:订单正在创建中
    if (error.code === 1008) {
      wx.showModal({
        title: '提示',
        content: '订单正在创建中,是否跳转到我的订单页面进行支付?',
        confirmText: '去支付',
        success: (res) => {
          if (res.confirm) {
            wx.navigateTo({ url: '/pages/orders/orders' });
          }
        }
      });
      return;
    }

    // 其他错误提示
    const errorMsg = error.message || error.msg || '操作失败,请稍后重试';
    wx.showToast({ title: errorMsg, icon: 'none' });
  }
}

/**
 * 支付成功处理
 */
handlePaySuccess() {
  const pkg = this.data.packages[this.data.selectedIdx];
  this.setData({ selectedIdx: -1 });
  wx.showToast({
    title: `「${pkg.name}」支付成功`,
    icon: 'success'
  });
  // 可选:刷新数据、跳转页面等
}

四、类型定义api.ts

/** 创建订单 - 请求 */
export interface CreateOrderRequest {
  app_type?: 'official_account' | 'miniapp';  // 应用类型(强充必传)
  identifier: string;                          // 资产标识符
  package_ids: number[];                       // 套餐ID列表
}

/** 创建订单 - 响应 */
export interface CreateOrderResponse {
  order_type: 'package' | 'recharge';  // 订单类型
  order: {
    order_id: number;
    order_no: string;
    payment_status: 1 | 2 | 3 | 4;     // 1:待支付 2:已支付 3:已取消 4:已退款
    total_amount: number;               // 总金额(分)
    created_at: string;
  };
  pay_config?: {                        // 强充订单才返回
    app_id: string;
    timestamp: string;
    nonce_str: string;
    package: string;                    // prepay_id=wx...
    sign_type: string;
    pay_sign: string;
  } | null;
  recharge?: {                          // 充值订单信息
    recharge_id: number;
    recharge_no: string;
    amount: number;
    status: 0 | 1 | 2;                  // 0:待支付 1:已支付 2:已关闭
    auto_purchase_status: string;
  };
  linked_package_info?: {               // 强充关联套餐信息
    package_names: string[];
    total_package_amount: number;
    wallet_credit: number;
    force_recharge_amount: number;
  };
}

/** 订单支付 - 请求 */
export interface PayOrderRequest {
  payment_method: 'wallet' | 'wechat';  // 支付方式
  app_type?: 'official_account' | 'miniapp';  // 微信支付必传
  password?: string;                    // 钱包支付必传
}

/** 订单支付 - 响应 */
export interface PayOrderResponse {
  payment_method: 'wallet' | 'wechat';
  pay_config?: {                        // 微信支付时返回
    app_id: string;
    timestamp: string;
    nonce_str: string;
    package: string;
    sign_type: string;
    pay_sign: string;
  };
}

五、关键要点

5.1 支付参数验证

必须验证 package 参数格式:

if (!payConfig.package || !payConfig.package.startsWith('prepay_id=')) {
  // 参数无效,停止支付
  return;
}

5.2 支付结果判断

// 成功
success: (res) => { /* 支付成功 */ }

// 失败/取消
fail: (err) => {
  // 判断是否用户取消
  const userCancelled = err.errMsg?.includes('cancel') ||
                       err.errMsg?.includes('用户取消');

  if (userCancelled) {
    // 用户主动取消
  } else {
    // 支付失败
  }
}

5.3 错误处理

  1. 参数验证失败 - 提示用户并停止支付
  2. 用户取消支付 - 友好提示,不作为错误
  3. 支付失败 - 提示错误信息
  4. 订单创建中1008 - 引导用户到订单页

5.4 日志记录

关键节点必须记录日志:

  • 支付参数(脱敏)
  • 支付成功/失败
  • 参数验证失败
  • 异常情况
Logger.info('发起微信支付', {
  ...payConfig,
  pay_sign: payConfig.pay_sign.substring(0, 10) + '...'  // 脱敏
});

5.5 Loading状态管理

// 创建订单前
wx.showLoading({ title: '创建订单...', mask: true });

// 获取支付参数前
wx.showLoading({ title: '准备支付...', mask: true });

// 调起支付前
wx.hideLoading();  // 必须关闭,否则遮挡支付界面

六、常见问题

Q1: 为什么有两种支付流程?

A:

  • 标准流程(三步):普通套餐订购
  • 快捷流程(一步):强充订单,后端已预创建支付参数

Q2: package 参数为什么要验证?

A: 后端可能返回空值或格式错误,直接传给微信会导致支付崩溃。必须验证格式为 prepay_id=wx...

Q3: 如何区分用户取消和支付失败?

A: 检查 errMsg 是否包含 'cancel''用户取消' 关键字。

Q4: 为什么调起支付前要 hideLoading

A: Loading 遮罩会挡住微信支付界面,导致用户无法操作。

七、快速集成清单

要在新项目中集成微信支付,需要:

  • 复制 utils/payment.ts 工具类
  • 创建订单API接口createOrderpayOrder
  • 定义类型文件(api.ts
  • 实现业务逻辑(参考 createOrderAndPay
  • 添加 Logger 日志工具(可选)
  • 处理支付回调(成功、失败、取消)
  • 添加错误处理逻辑

八、注意事项

  1. 安全性

    • 日志中敏感信息pay_sign要脱敏
    • 永远不要在前端生成支付签名
    • 所有支付参数由后端返回
  2. 用户体验

    • 支付过程中显示 Loading 提示
    • 调起支付前必须关闭 Loading
    • 区分取消和失败,给出不同提示
    • 支付失败时保留订单,允许用户重试
  3. 异常处理

    • 参数验证失败时友好提示
    • 网络错误时允许重试
    • 订单创建中1008引导到订单页
    • 所有异常都要记录日志
  4. 测试要点

    • 正常支付流程
    • 用户取消支付
    • 网络异常
    • 参数异常
    • 强充订单流程
    • 普通订单流程

文档版本: v1.0 更新日期: 2026-04-14 适用范围: 微信小程序支付