Files
soul/开发文档/5、接口/提现功能完整技术文档.md
卡若 afc2376e96 v1.19 全面改版:VIP会员系统、我的收益、创业老板排行、阅读量排序
- 后端: users表新增VIP字段, 4个VIP API (purchase/status/profile/members)
- 后端: hot接口改按user_tracks阅读量排序
- 后端: orders表支持vip产品类型, migrate新增vip_fields迁移
- 小程序「我的」: 推广中心改为我的收益, 头像VIP标识, VIP入口卡片
- 小程序「我的」: 最近阅读显示真实章节名称
- 小程序首页: 去掉内容概览, 新增创业老板排行(4列网格)
- 小程序首页: 精选推荐从hot接口获取, goToRead增加track记录
- 新增页面: VIP详情页, 会员详情页
- 开发文档精简为10个标准目录, 创建SKILL.md, 需求日志规范化

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 14:07:41 +08:00

1034 lines
26 KiB
Markdown
Raw 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.

# 提现功能技术文档微信支付API集成
## 文档说明
本文档专注于**微信支付商家转账到零钱API**的集成方法,包括:
- 微信支付官方API文档
- 签名生成算法
- 加密解密算法
- 完整代码实现
- 测试验证方法
**适用场景**:实现用户提现功能,将资金从商户号转账到用户微信零钱。
---
## 目录
1. [业务场景](#业务场景)
2. [微信支付官方API文档](#微信支付官方api文档)
3. [前置准备](#前置准备)
4. [API集成](#api集成)
5. [签名算法](#签名算法)
6. [加密解密](#加密解密)
7. [代码实现](#代码实现)
8. [测试验证](#测试验证)
---
## 业务场景
### 典型流程
```
用户申请提现
系统审核通过
调用微信支付【商家转账到零钱API】
微信返回处理中(PROCESSING)
微信异步处理(7-15秒)
微信【主动回调】通知转账结果
系统接收回调,验签、解密
更新提现状态
用户确认收款
```
### 关键步骤
1. **发起转账**调用微信API发起转账
2. **接收回调**:接收微信异步通知
3. **验证签名**:验证回调的真实性
4. **解密数据**:解密回调中的加密数据
5. **查询状态**:主动查询转账状态
---
## 微信支付官方API文档
### 核心API
| API名称 | 官方文档地址 |
|--------|------------|
| 🔥 **商家转账到零钱** | https://pay.weixin.qq.com/doc/v3/merchant/4012716434 |
| 📋 **查询转账单(商户单号)** | https://pay.weixin.qq.com/doc/v3/merchant/4012716456 |
| 📋 **查询转账单(微信单号)** | https://pay.weixin.qq.com/doc/v3/merchant/4012716457 |
| 🔐 **签名生成与验证** | https://pay.weixin.qq.com/doc/v3/merchant/4013053249 |
| 🔒 **敏感信息加密** | https://pay.weixin.qq.com/doc/v3/merchant/4012070130 |
| 🔓 **回调通知解密** | https://pay.weixin.qq.com/doc/v3/merchant/4012071382 |
| 📝 **转账场景报备** | https://pay.weixin.qq.com/doc/v3/merchant/4012716437 |
| ❌ **错误码查询** | https://pay.weixin.qq.com/doc/v3/merchant/4012070193 |
| 📜 **平台证书管理** | https://pay.weixin.qq.com/doc/v3/merchant/4012154180 |
### 开发指引
- **API V3 开发总览**https://pay.weixin.qq.com/doc/v3/merchant/4012065168
- **PHP SDK 使用**https://pay.weixin.qq.com/doc/v3/merchant/4012076511
---
## 前置准备
### 1. 获取配置信息
登录微信商户平台https://pay.weixin.qq.com
| 配置项 | 说明 | 获取路径 |
|-------|------|---------|
| **商户号(mch_id)** | 微信支付商户号 | 账户中心 → 商户信息 |
| **APIv3密钥(api_v3_key)** | 32字节密钥用于加密解密 | 账户中心 → API安全 → 设置APIv3密钥 |
| **商户私钥(apiclient_key.pem)** | 用于请求签名 | 账户中心 → API安全 → 申请证书 |
| **证书序列号(cert_serial_no)** | 商户证书标识 | 从证书文件提取 |
| **平台证书(wechat_pay_pub_key)** | 用于验证回调签名 | 下载或通过API获取 |
| **小程序AppId** | 小程序标识 | 小程序管理后台 |
### 2. 提取证书序列号
**使用OpenSSL命令**
```bash
openssl x509 -in apiclient_cert.pem -noout -serial
```
输出:
```
serial=4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5
```
**使用PHP**
```php
<?php
$certContent = file_get_contents('apiclient_cert.pem');
$certData = openssl_x509_parse($certContent);
echo strtoupper(dechex($certData['serialNumber']));
?>
```
### 3. 配置IP白名单
路径:微信商户平台 → 账户中心 → API安全 → IP配置
添加服务器公网IP地址。
**获取服务器IP**
```bash
curl ifconfig.me
```
### 4. 配置转账场景
路径:微信商户平台 → 产品中心 → 商家转账到零钱 → 前往功能
可选场景:
- **1000**:现金营销
- **1005**:营销活动
### 5. 环境要求
- PHP >= 7.0
- OpenSSL 扩展(必须)
- cURL 扩展(必须)
- JSON 扩展(必须)
- TLS 1.2+
**检查环境**
```bash
php -v
php -m | grep openssl
php -m | grep curl
```
---
## API集成
### 1. 商家转账到零钱API
#### 基本信息
- **接口地址**`https://api.mch.weixin.qq.com/v3/transfer/batches`
- **请求方法**POST
- **Content-Type**application/json
#### 请求头
```
Authorization: WECHATPAY2-SHA256-RSA2048 mchid="商户号",nonce_str="随机字符串",signature="签名",timestamp="时间戳",serial_no="证书序列号"
Content-Type: application/json
Accept: application/json
User-Agent: YourApp/1.0
```
#### 请求参数
```json
{
"appid": "wx6489c26045912fe1",
"out_batch_no": "BATCH202601291234567890",
"batch_name": "提现",
"batch_remark": "用户提现",
"total_amount": 5000,
"total_num": 1,
"transfer_detail_list": [
{
"out_detail_no": "TX202601291234567890",
"transfer_amount": 5000,
"transfer_remark": "提现",
"openid": "odq3g5IOG-Z1WLpbeG_amUme8EZk"
}
],
"transfer_scene_id": "1005",
"transfer_scene_report_infos": [
{
"info_type": "岗位类型",
"info_content": "兼职人员"
},
{
"info_type": "报酬说明",
"info_content": "当日兼职费"
}
]
}
```
**参数说明**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| appid | string | 是 | 小程序AppId |
| out_batch_no | string | 是 | 商户批次单号,商户下唯一 |
| batch_name | string | 是 | 批次名称 |
| batch_remark | string | 是 | 批次备注 |
| total_amount | integer | 是 | 转账总金额,单位:**分** |
| total_num | integer | 是 | 转账总笔数 |
| transfer_detail_list | array | 是 | 转账明细列表 |
| transfer_scene_id | string | 是 | 转账场景ID1000或1005 |
| transfer_scene_report_infos | array | 否 | 场景报备信息 |
**transfer_detail_list说明**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| out_detail_no | string | 是 | 商户明细单号 |
| transfer_amount | integer | 是 | 转账金额,单位:**分** |
| transfer_remark | string | 是 | 转账备注 |
| openid | string | 是 | 收款用户OpenId |
**场景报备信息场景ID=1005**
```json
[
{
"info_type": "岗位类型",
"info_content": "兼职人员"
},
{
"info_type": "报酬说明",
"info_content": "当日兼职费"
}
]
```
**重要**
- `info_type` 必须是固定值
- 金额单位是**分**`元 * 100`
#### 响应数据
**成功响应**
```json
{
"out_batch_no": "BATCH202601291234567890",
"batch_id": "1030000071100999991182020050700019480001",
"create_time": "2026-01-29T12:30:00+08:00",
"batch_status": "PROCESSING"
}
```
**字段说明**
| 字段 | 说明 |
|------|------|
| out_batch_no | 商户批次单号 |
| batch_id | 微信批次单号 |
| create_time | 批次创建时间 |
| batch_status | 批次状态PROCESSING/SUCCESS/FAIL |
**失败响应**
```json
{
"code": "PARAM_ERROR",
"message": "参数错误"
}
```
### 2. 查询转账单API
#### 按商户单号查询
**接口地址**
```
GET https://api.mch.weixin.qq.com/v3/transfer/batches/batch-id/{batch_id}/details/detail-id/{detail_id}
```
**路径参数**
- `batch_id`商户批次单号需URL编码
- `detail_id`商户明细单号需URL编码
**示例**
```
GET /v3/transfer/batches/batch-id/BATCH202601291234567890/details/detail-id/TX202601291234567890
```
**响应示例**
```json
{
"mchid": "1318592501",
"out_batch_no": "BATCH202601291234567890",
"batch_id": "1030000071100999991182020050700019480001",
"out_detail_no": "TX202601291234567890",
"detail_id": "1040000071100999991182020050700019500100",
"detail_status": "SUCCESS",
"transfer_amount": 5000,
"transfer_remark": "提现",
"openid": "odq3g5IOG-Z1WLpbeG_amUme8EZk",
"initiate_time": "2026-01-29T12:30:00+08:00",
"update_time": "2026-01-29T12:30:15+08:00"
}
```
**状态说明**
| detail_status | 说明 |
|--------------|------|
| PROCESSING | 转账中 |
| SUCCESS | 转账成功 |
| FAIL | 转账失败 |
### 3. 转账结果通知(回调)
#### 回调触发
当转账状态变更时,微信支付会主动向配置的 `notify_url` 发送POST请求。
#### 回调请求头
```
Wechatpay-Signature: 签名值
Wechatpay-Timestamp: 1769653396
Wechatpay-Nonce: R0PDA5lOV3IMrBjrvbCH5U4L3Lb0gg8L
Wechatpay-Serial: 642B2B33557205BA79A1CFF08EA2A2478D67BD63
Wechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048
Content-Type: application/json
```
#### 回调请求体(加密)
```json
{
"id": "cb29e425-ca17-59fb-8045-8e5b58917154",
"create_time": "2026-01-29T10:23:11+08:00",
"resource_type": "encrypt-resource",
"event_type": "MCHTRANSFER.BILL.FINISHED",
"summary": "商家转账单据终态通知",
"resource": {
"original_type": "mch_payment",
"algorithm": "AEAD_AES_256_GCM",
"ciphertext": "加密的数据...",
"associated_data": "mch_payment",
"nonce": "随机字符串"
}
}
```
#### 解密后的数据
```json
{
"mch_id": "1318592501",
"out_bill_no": "TX202601291234567890",
"transfer_bill_no": "1330000114850082601290057112302122",
"transfer_amount": 5000,
"state": "SUCCESS",
"openid": "odq3g5IOG-Z1WLpbeG_amUme8EZk",
"create_time": "2026-01-29T12:30:00+08:00",
"update_time": "2026-01-29T12:30:15+08:00"
}
```
**state状态说明**
| state | 说明 |
|-------|------|
| PROCESSING | 转账中 |
| SUCCESS | 转账成功 |
| FAIL | 转账失败 |
| WAIT_USER_CONFIRM | 待用户确认 |
| TRANSFERING | 正在转账 |
#### 回调响应
处理完成后,返回给微信:
```json
{
"code": "SUCCESS"
}
```
---
## 签名算法
### 1. 签名生成(请求签名)
#### 签名串格式
```
请求方法\n
请求URL路径\n
请求时间戳\n
随机字符串\n
请求报文主体\n
```
**示例**
```
POST
/v3/transfer/batches
1234567890
RandomString123456
{"appid":"wx6489c26045912fe1"}
```
**重要**:每部分末尾都有 `\n` 换行符。
#### 签名步骤
1. 构建签名串
2. 使用商户私钥进行SHA256withRSA签名
3. 对签名结果进行Base64编码
#### PHP实现
```php
function buildSignature($method, $url, $timestamp, $nonce, $body, $privateKeyPath) {
// 1. 构建签名串
$signStr = $method . "\n"
. $url . "\n"
. $timestamp . "\n"
. $nonce . "\n"
. $body . "\n";
// 2. 加载私钥
$privateKeyContent = file_get_contents($privateKeyPath);
$privateKeyResource = openssl_pkey_get_private($privateKeyContent);
// 3. 使用私钥签名
openssl_sign($signStr, $signature, $privateKeyResource, 'sha256WithRSAEncryption');
// 4. Base64编码
return base64_encode($signature);
}
```
#### 构建Authorization头
```php
function buildAuthorization($mchId, $timestamp, $nonce, $signature, $serialNo) {
return sprintf(
'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%d",serial_no="%s"',
$mchId,
$nonce,
$signature,
$timestamp,
$serialNo
);
}
```
### 2. 签名验证(回调验签)
#### 验签串格式
```
时间戳\n
随机字符串\n
请求报文主体\n
```
**示例**
```
1769653396
R0PDA5lOV3IMrBjrvbCH5U4L3Lb0gg8L
{"id":"cb29e425-ca17-59fb-8045-8e5b58917154",...}
```
#### PHP实现
```php
function verifySignature($timestamp, $nonce, $body, $signature, $publicKeyPath) {
// 1. 构建验签串
$verifyStr = $timestamp . "\n"
. $nonce . "\n"
. $body . "\n";
// 2. Base64解码签名
$signatureDecode = base64_decode($signature);
// 3. 加载平台公钥
$publicKeyContent = file_get_contents($publicKeyPath);
$publicKeyResource = openssl_pkey_get_public($publicKeyContent);
// 4. 验证签名
$result = openssl_verify(
$verifyStr,
$signatureDecode,
$publicKeyResource,
'sha256WithRSAEncryption'
);
return $result === 1; // 1表示验证成功
}
```
**重要**:验签使用的是**微信支付平台公钥**,不是商户私钥!
---
## 加密解密
### 回调数据解密
#### 算法信息
- **算法**AEAD_AES_256_GCM
- **密钥**APIv3密钥32字节
- **密文格式**:实际密文 + 认证标签16字节
#### 解密步骤
1. 提取加密数据ciphertext、nonce、associated_data
2. Base64解码密文
3. 分离密文和认证标签最后16字节
4. 使用AES-256-GCM解密
5. 解析JSON数据
#### PHP实现
```php
function decryptCallbackData($ciphertext, $nonce, $associatedData, $apiV3Key) {
// 1. 检查APIv3密钥长度必须32字节
if (strlen($apiV3Key) !== 32) {
throw new Exception('APIv3密钥长度必须为32字节');
}
// 2. Base64解码密文
$ciphertextDecoded = base64_decode($ciphertext);
// 3. 分离密文和认证标签
$authTag = substr($ciphertextDecoded, -16);
$ctext = substr($ciphertextDecoded, 0, -16);
// 4. 使用AES-256-GCM解密
$decrypted = openssl_decrypt(
$ctext, // 密文
'aes-256-gcm', // 算法
$apiV3Key, // 密钥
OPENSSL_RAW_DATA, // 选项
$nonce, // 随机串
$authTag, // 认证标签
$associatedData // 附加数据
);
if ($decrypted === false) {
throw new Exception('解密失败');
}
// 5. 解析JSON
return json_decode($decrypted, true);
}
```
**使用示例**
```php
$resource = $callbackData['resource'];
$decrypted = decryptCallbackData(
$resource['ciphertext'],
$resource['nonce'],
$resource['associated_data'],
'wx3e31b068be59ddc131b068be59ddc2' // APIv3密钥
);
```
---
## 代码实现
### 完整的微信支付转账类
```php
<?php
class WechatPayTransfer
{
private $mchId;
private $appId;
private $apiV3Key;
private $privateKey;
private $certSerialNo;
public function __construct($config)
{
$this->mchId = $config['mch_id'];
$this->appId = $config['app_id'];
$this->apiV3Key = $config['api_v3_key'];
$this->certSerialNo = $config['cert_serial_no'];
// 加载私钥
$privateKeyContent = file_get_contents($config['private_key']);
$this->privateKey = openssl_pkey_get_private($privateKeyContent);
}
/**
* 发起转账
*/
public function createTransfer($params)
{
$url = '/v3/transfer/batches';
$method = 'POST';
// 构建请求数据
$data = [
'appid' => $this->appId,
'out_batch_no' => 'BATCH' . date('YmdHis') . mt_rand(1000, 9999),
'batch_name' => $params['batch_name'] ?? '提现',
'batch_remark' => $params['batch_remark'] ?? '用户提现',
'total_amount' => $params['transfer_amount'],
'total_num' => 1,
'transfer_detail_list' => [
[
'out_detail_no' => $params['out_detail_no'],
'transfer_amount' => $params['transfer_amount'],
'transfer_remark' => $params['transfer_remark'],
'openid' => $params['openid'],
]
],
'transfer_scene_id' => $params['transfer_scene_id'] ?? '1005',
];
// 添加场景报备信息
if (!empty($params['transfer_scene_report_infos'])) {
$data['transfer_scene_report_infos'] = $params['transfer_scene_report_infos'];
}
$body = json_encode($data, JSON_UNESCAPED_UNICODE);
// 生成签名
$timestamp = time();
$nonce = $this->generateNonce();
$signature = $this->buildSignature($method, $url, $timestamp, $nonce, $body);
// 构建Authorization
$authorization = $this->buildAuthorization($timestamp, $nonce, $signature);
// 发送请求
return $this->request($method, $url, $body, $authorization);
}
/**
* 查询转账单
*/
public function queryTransfer($batchNo, $detailNo)
{
$url = "/v3/transfer/batches/batch-id/" . urlencode($batchNo)
. "/details/detail-id/" . urlencode($detailNo);
$method = 'GET';
$timestamp = time();
$nonce = $this->generateNonce();
$signature = $this->buildSignature($method, $url, $timestamp, $nonce, '');
$authorization = $this->buildAuthorization($timestamp, $nonce, $signature);
return $this->request($method, $url, '', $authorization);
}
/**
* 验证回调签名
*/
public function verifyCallback($headers, $body, $publicKey)
{
$timestamp = $headers['wechatpay-timestamp'];
$nonce = $headers['wechatpay-nonce'];
$signature = $headers['wechatpay-signature'];
$verifyStr = $timestamp . "\n" . $nonce . "\n" . $body . "\n";
$signatureDecode = base64_decode($signature);
$publicKeyContent = file_get_contents($publicKey);
$publicKeyResource = openssl_pkey_get_public($publicKeyContent);
$result = openssl_verify($verifyStr, $signatureDecode, $publicKeyResource, 'sha256WithRSAEncryption');
return $result === 1;
}
/**
* 解密回调数据
*/
public function decryptCallbackResource($resource)
{
$ciphertext = $resource['ciphertext'];
$nonce = $resource['nonce'];
$associatedData = $resource['associated_data'];
if (strlen($this->apiV3Key) !== 32) {
throw new \Exception('APIv3密钥长度必须为32字节');
}
$ciphertextDecoded = base64_decode($ciphertext);
$authTag = substr($ciphertextDecoded, -16);
$ctext = substr($ciphertextDecoded, 0, -16);
$decrypted = openssl_decrypt(
$ctext,
'aes-256-gcm',
$this->apiV3Key,
OPENSSL_RAW_DATA,
$nonce,
$authTag,
$associatedData
);
if ($decrypted === false) {
throw new \Exception('解密失败');
}
return json_decode($decrypted, true);
}
/**
* 生成签名
*/
private function buildSignature($method, $url, $timestamp, $nonce, $body)
{
$signStr = $method . "\n"
. $url . "\n"
. $timestamp . "\n"
. $nonce . "\n"
. $body . "\n";
openssl_sign($signStr, $signature, $this->privateKey, 'sha256WithRSAEncryption');
return base64_encode($signature);
}
/**
* 构建Authorization头
*/
private function buildAuthorization($timestamp, $nonce, $signature)
{
return sprintf(
'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%d",serial_no="%s"',
$this->mchId,
$nonce,
$signature,
$timestamp,
$this->certSerialNo
);
}
/**
* 生成随机字符串
*/
private function generateNonce($length = 32)
{
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$nonce = '';
for ($i = 0; $i < $length; $i++) {
$nonce .= $chars[mt_rand(0, strlen($chars) - 1)];
}
return $nonce;
}
/**
* 发送HTTP请求
*/
private function request($method, $url, $body, $authorization)
{
$fullUrl = 'https://api.mch.weixin.qq.com' . $url;
$headers = [
'Authorization: ' . $authorization,
'Content-Type: application/json',
'Accept: application/json',
'User-Agent: YourApp/1.0'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $fullUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$result = json_decode($response, true);
if ($httpCode >= 200 && $httpCode < 300) {
return ['success' => true, 'data' => $result];
} else {
return [
'success' => false,
'error_code' => $result['code'] ?? 'UNKNOWN',
'error_msg' => $result['message'] ?? '未知错误'
];
}
}
}
```
### 使用示例
#### 1. 发起转账
```php
// 初始化配置
$config = [
'mch_id' => '1318592501',
'app_id' => 'wx6489c26045912fe1',
'api_v3_key' => 'wx3e31b068be59ddc131b068be59ddc2',
'private_key' => '/path/to/apiclient_key.pem',
'cert_serial_no' => '4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5',
];
$wechatPay = new WechatPayTransfer($config);
// 发起转账
$result = $wechatPay->createTransfer([
'out_detail_no' => 'TX' . date('YmdHis') . mt_rand(1000, 9999),
'transfer_amount' => 5000, // 50元 = 5000分
'transfer_remark' => '提现',
'openid' => 'odq3g5IOG-Z1WLpbeG_amUme8EZk',
'transfer_scene_id' => '1005',
'transfer_scene_report_infos' => [
['info_type' => '岗位类型', 'info_content' => '兼职人员'],
['info_type' => '报酬说明', 'info_content' => '当日兼职费'],
],
]);
if ($result['success']) {
echo "转账成功: " . json_encode($result['data']);
} else {
echo "转账失败: " . $result['error_msg'];
}
```
#### 2. 查询转账单
```php
$result = $wechatPay->queryTransfer('BATCH202601291234567890', 'TX202601291234567890');
if ($result['success']) {
echo "状态: " . $result['data']['detail_status'];
} else {
echo "查询失败: " . $result['error_msg'];
}
```
#### 3. 处理回调
```php
// 接收回调
$headers = [
'wechatpay-signature' => $_SERVER['HTTP_WECHATPAY_SIGNATURE'],
'wechatpay-timestamp' => $_SERVER['HTTP_WECHATPAY_TIMESTAMP'],
'wechatpay-nonce' => $_SERVER['HTTP_WECHATPAY_NONCE'],
'wechatpay-serial' => $_SERVER['HTTP_WECHATPAY_SERIAL'],
];
$body = file_get_contents('php://input');
$callbackData = json_decode($body, true);
// 验证签名
$verified = $wechatPay->verifyCallback($headers, $body, '/path/to/wechat_pay_pub_key.pem');
if ($verified) {
// 解密数据
$decrypted = $wechatPay->decryptCallbackResource($callbackData['resource']);
// 处理转账结果
if ($decrypted['state'] === 'SUCCESS') {
echo "转账成功: " . $decrypted['out_bill_no'];
}
// 返回成功
echo json_encode(['code' => 'SUCCESS']);
} else {
echo json_encode(['code' => 'FAIL', 'message' => '签名验证失败']);
}
```
---
## 测试验证
### 1. 签名生成测试
```php
$method = 'POST';
$url = '/v3/transfer/batches';
$timestamp = time();
$nonce = 'RandomString123456';
$body = '{"appid":"wx6489c26045912fe1"}';
$signature = buildSignature($method, $url, $timestamp, $nonce, $body, 'apiclient_key.pem');
echo "签名: " . $signature . "\n";
```
### 2. 小额转账测试
```php
// 测试金额0.01元 = 1分
$result = $wechatPay->createTransfer([
'out_detail_no' => 'TEST' . time(),
'transfer_amount' => 1, // 1分
'transfer_remark' => '测试',
'openid' => 'test_openid',
'transfer_scene_id' => '1005',
'transfer_scene_report_infos' => [
['info_type' => '岗位类型', 'info_content' => '测试'],
['info_type' => '报酬说明', 'info_content' => '测试'],
],
]);
```
### 3. 解密测试
```php
$resource = [
'ciphertext' => 'xxx',
'nonce' => 'xxx',
'associated_data' => 'mch_payment',
];
try {
$decrypted = decryptCallbackData(
$resource['ciphertext'],
$resource['nonce'],
$resource['associated_data'],
'wx3e31b068be59ddc131b068be59ddc2'
);
print_r($decrypted);
} catch (Exception $e) {
echo "解密失败: " . $e->getMessage();
}
```
### 4. 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|---------|
| 签名验证失败 | 证书序列号错误 | 重新提取证书序列号 |
| IP白名单错误 | 服务器IP未配置 | 添加到微信商户平台 |
| 解密失败 | APIv3密钥错误 | 检查密钥长度32字节 |
| 场景报备错误 | info_type不正确 | 使用固定值 |
| 余额不足 | 商户号余额不足 | 充值商户号 |
---
## 附录
### A. 错误码对照表
https://pay.weixin.qq.com/doc/v3/merchant/4012070193
| 错误码 | 说明 | 处理建议 |
|-------|------|---------|
| PARAM_ERROR | 参数错误 | 检查请求参数格式 |
| NOTENOUGH | 商户余额不足 | 充值商户号 |
| INVALID_REQUEST | 不符合业务规则 | 检查业务逻辑 |
| SYSTEM_ERROR | 系统错误 | 稍后重试 |
| FREQUENCY_LIMITED | 频率限制 | 降低请求频率 |
| APPID_MCHID_NOT_MATCH | appid和mch_id不匹配 | 检查配置 |
### B. 转账状态说明
| 状态 | 说明 | 处理方式 |
|------|------|---------|
| PROCESSING | 转账中 | 等待回调或主动查询 |
| SUCCESS | 转账成功 | 完成流程 |
| FAIL | 转账失败 | 检查失败原因 |
| WAIT_USER_CONFIRM | 待用户确认 | 等待用户操作 |
| TRANSFERING | 正在转账 | 等待处理完成 |
### C. 开发工具
- **Postman**API测试工具
- **OpenSSL**:证书和密钥管理
- **微信支付调试工具**https://pay.weixin.qq.com/
---
**文档版本**v3.0纯微信支付API版
**更新时间**2026-01-29
**适用场景**:微信支付商家转账到零钱功能集成
---
## 总结
本文档提供了微信支付转账功能的完整集成方案:
**3个核心API**
- 发起转账:`POST /v3/transfer/batches`
- 查询转账:`GET /v3/transfer/batches/batch-id/{batch_id}/details/detail-id/{detail_id}`
- 接收回调:异步通知
**3个核心算法**
- 签名生成SHA256withRSA + Base64
- 签名验证:使用平台公钥
- 数据解密AEAD_AES_256_GCM
**完整代码实现**
- WechatPayTransfer类可直接使用
- 包含发起转账、查询、验签、解密全部功能
根据本文档可以快速集成微信支付转账功能。