1021 lines
26 KiB
Markdown
1021 lines
26 KiB
Markdown
# 提现功能技术文档(微信支付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
|
||
|
||
|
||
---
|
||
|
||
## 前置准备
|
||
|
||
### 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**:营销活动
|
||
|
||
**检查环境**:
|
||
|
||
|
||
---
|
||
|
||
## 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 | 是 | 转账场景ID:1000或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类(可直接使用)
|
||
- 包含发起转账、查询、验签、解密全部功能
|
||
|
||
根据本文档可以快速集成微信支付转账功能。
|