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

627 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 微信小程序支付集成文档
## 概述
本文档总结了套餐订购模块中的微信支付完整实现流程,可复用到其他项目中。
## 一、核心文件结构
```
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
**适用范围:** 微信小程序支付