# 微信小程序支付集成文档

## 概述

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

## 一、核心文件结构

```
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`

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

**响应数据：**
```typescript
{
  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`

**请求参数：**
```typescript
{
  payment_method: 'wechat',           // 支付方式：wechat | wallet
  app_type: 'miniapp'                 // 应用类型（微信支付必传）
}
```

**响应数据：**
```typescript
{
  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()` 调起支付：

```typescript
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` 调起支付

**判断逻辑：**
```typescript
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）

```typescript
/**
 * 微信支付工具类
 */
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）

```typescript
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）

```typescript
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）

```typescript
/** 创建订单 - 请求 */
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` 参数格式：**
```typescript
if (!payConfig.package || !payConfig.package.startsWith('prepay_id=')) {
  // 参数无效，停止支付
  return;
}
```

### 5.2 支付结果判断

```typescript
// 成功
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 日志记录

关键节点必须记录日志：
- 支付参数（脱敏）
- 支付成功/失败
- 参数验证失败
- 异常情况

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

### 5.5 Loading状态管理

```typescript
// 创建订单前
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接口（`createOrder`、`payOrder`）
- [ ] 定义类型文件（`api.ts`）
- [ ] 实现业务逻辑（参考 `createOrderAndPay`）
- [ ] 添加 Logger 日志工具（可选）
- [ ] 处理支付回调（成功、失败、取消）
- [ ] 添加错误处理逻辑

## 八、注意事项

1. **安全性**
   - 日志中敏感信息（pay_sign）要脱敏
   - 永远不要在前端生成支付签名
   - 所有支付参数由后端返回

2. **用户体验**
   - 支付过程中显示 Loading 提示
   - 调起支付前必须关闭 Loading
   - 区分取消和失败，给出不同提示
   - 支付失败时保留订单，允许用户重试

3. **异常处理**
   - 参数验证失败时友好提示
   - 网络错误时允许重试
   - 订单创建中（1008）引导到订单页
   - 所有异常都要记录日志

4. **测试要点**
   - 正常支付流程
   - 用户取消支付
   - 网络异常
   - 参数异常
   - 强充订单流程
   - 普通订单流程

---

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