
简介本资源是一套面向PHP开发者与微信支付接入工程师的实战型代码包聚焦小程序用户提交记录后商家后台自动审核并打款至微信零钱的核心业务场景解决企业付款到零钱v2密钥与微信商户转账到零钱v3密钥双版本接口的落地难题。压缩包共3个文件2个PHP核心控制器文件分别实现v2/v3接口调用逻辑1个说明文档梳理密钥配置、签名规则与调用要点总大小仅4KB轻量易集成适合中高级开发者快速复用或对照调试。已有1318人学习下载资源直接提取自真实项目提供可运行的Demo级代码结构、关键参数封装示例及v2/v3迁移注意事项尤其适合正在对接微信打款功能、需规避签名失效或接口废弃风险的商户系统开发人员。1. 微信商户付款到零钱v2密钥版和v3密钥版不是“升级替换”而是两套并行、互不兼容的独立通道——选错版本连签名都验不过你手头有一份「微信商户付款到微信用户零钱」的源码包标题里同时标着 v2密钥版 和 v3密钥版。别急着解压运行——这不是一个项目两个分支而是两条完全不同的技术路径v2 基于 MD5 API 密钥签名走的是 2014 年上线的老支付网关v3 基于 RSA-SHA256 平台证书验签对接的是 2020 年起强制推行的新开放平台。我见过太多团队在测试环境跑通 v2上线后切 v3 时卡在「INVALID_SIGNATURE」整整三天不是代码写错了是根本没意识到 v3 要求你先在商户平台下载平台证书、用私钥生成请求签名、再用公钥验签响应体——而 v2 根本不碰证书。这个源码包的价值不在于它“有两套”而在于它把两套最常翻车的落地细节全摊开了v2 的 key 拼接顺序玄学、v3 的证书加载时机黑匣子、回调验签时 timestamp 和 nonce_str 的毫秒级对齐陷阱。适合正在接入微信零钱打款、已开通「企业付款」权限、但被文档绕晕的后端工程师——尤其适合那些刚被运营催着“今天必须打出第一笔测试款”的人。2. v2密钥版MD5签名不是拼接完就完事key位置、字段顺序、空值处理三处不一致签名必挂v2 接口https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers表面简单实则处处是坑。它的签名逻辑是将所有非空参数按字段名 ASCII 升序排序拼成key1value1key2value2key3value3keykey字符串再对整个字符串做 MD5。但“非空”“ASCII 升序”“key 的位置”这三点微信官方文档写得极其模糊而源码包里的 v2 实现正是踩过全部血泪坑后沉淀下来的。2.1 参数组装与签名生成必须严格遵循字段名 ASCII 排序且 key 必须放在末尾微信 v2 签名要求所有参与签名的字段除sign外按字段名字典序升序排列且keyxxx必须追加在最后。注意partner_trade_no、amount、check_name这些字段名本身是小写但mch_appid、mchid是小写nonce_str是小写re_user_name是小写——它们的 ASCII 值决定了顺序。常见错误是把mchid放最前因为觉得它是商户号但amounta比mchidm靠前所以amount必须排第一。# python 示例v2 签名生成函数关键逻辑 import hashlib import urllib.parse def generate_v2_sign(params: dict, api_key: str) - str: # 1. 过滤空值值为 None、、0 都要剔除注意0 是数字不是空字符串但微信要求剔除 filtered {k: v for k, v in params.items() if v is not None and v ! and v ! 0} # 2. 按 key 字典序升序排序ASCII sorted_items sorted(filtered.items(), keylambda x: x[0]) # 3. 拼接 kvkv...keyxxx sign_str .join([f{k}{v} for k, v in sorted_items]) fkey{api_key} # 4. MD5 大写 return hashlib.md5(sign_str.encode(utf-8)).hexdigest().upper()提示params字典传入前务必确保amount是整数单位分partner_trade_no是纯数字或字母组合不能含下划线openid是标准微信 openid32位小写字母数字。check_name必须是FORCE_CHECK或NO_CHECK不能是force_check大小写敏感。2.2 请求构造与 XML 封装CDATA 包裹、编码、换行符一个都不能错v2 接口只认application/xml且要求 body 是标准 XML。微信对 XML 格式极其苛刻所有业务字段值必须用![CDATA[]]包裹XML 声明必须是?xml version1.0 encodingUTF-8?标签必须严格闭合xml根节点内不能有多余换行或空格。# 构造 v2 请求 XML body def build_v2_xml(params: dict, sign: str) - str: xml_parts [?xml version1.0 encodingUTF-8?, xml] for k, v in params.items(): # 所有值必须 CDATA 包裹包括数字 xml_parts.append(f{k}![CDATA[{v}]]/{k}) xml_parts.append(fsign![CDATA[{sign}]]/sign) xml_parts.append(/xml) return .join(xml_parts) # 使用示例 params { mch_appid: wx1234567890abcdef, mchid: 1234567890, nonce_str: 5K8264ILTKCH16CQ2502SI8ZNMTM67VS, partner_trade_no: 20240520100001, openid: oAbcDefGhIjKlMnOpQrStUvWxYz, check_name: FORCE_CHECK, re_user_name: 张三, amount: 100, # 1元 100分 desc: 测试打款, spbill_create_ip: 127.0.0.1 } sign generate_v2_sign(params, your_api_key_here) xml_body build_v2_xml(params, sign) # 发送请求requests 库 import requests response requests.post( https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers, dataxml_body.encode(utf-8), headers{Content-Type: application/xml} )参数说明spbill_create_ip必须是真实发起请求的服务器公网 IP不能是 127.0.0.1 或内网地址否则返回INVALID_REQUESTdesc不能超过 30 个字符re_user_name若含中文XML 编码必须为 UTF-8且?xml ...?声明中encodingUTF-8必须显式写出。3. v3密钥版RSA-SHA256 不是“换个算法”而是整套通信模型重构——证书、签名、验签、回调四步缺一不可v3 接口https://api.mch.weixin.qq.com/v3/pay/transfer/batches彻底抛弃了 v2 的 MD5 key 模式改用基于 X.509 证书的非对称加密体系。它不是“v2 的升级版”而是一套新协议你用私钥签名请求微信用你的公钥验签微信用平台私钥签名响应你用平台公钥验签所有敏感字段如金额、收款人必须 AES-256-GCM 加密回调通知也必须验签解密。源码包里的 v3 实现核心价值在于把这四步的“证书加载时机”“签名头构造”“响应体解密”“回调验签链”全部拆解成了可调试的模块。3.1 平台证书加载与私钥准备证书不是“下载完放文件夹就行”必须解析出公钥用于验签v3 要求你从微信商户平台下载「平台证书」.pem文件该文件本质是 PEM 格式的 X.509 证书里面包含微信的公钥。你不能直接拿.pem去验签必须用 OpenSSL 或 Python 的cryptography库从中提取出公钥对象。同时你的 APIv3 密钥即你在商户平台设置的 32 位字符串仅用于 AES 加密不参与 RSA 签名——RSA 签名用的是你自己的私钥.pem或.p12格式。# python 示例加载平台证书并提取公钥用于验签微信响应 from cryptography import x509 from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa def load_platform_public_key(cert_pem_path: str): with open(cert_pem_path, rb) as f: cert_data f.read() cert x509.load_pem_x509_certificate(cert_data) return cert.public_key() # 加载你的私钥用于签名请求 def load_merchant_private_key(key_pem_path: str, key_password: bytes None): with open(key_pem_path, rb) as f: key_data f.read() return serialization.load_pem_private_key( key_data, passwordkey_password, backenddefault_backend() )注意微信平台证书有有效期通常 2 年且会轮换。源码包中应包含自动检测证书过期、触发重新下载的逻辑通过调用GET /v3/certificates接口。不要硬编码证书路径生产环境必须支持热更新。3.2 请求签名与 Authorization 头构造timestamp、nonce_str、body 三者哈希必须严格一致v3 签名头Authorization: WECHATPAY2-SHA256-RSA2048 ...的生成是最大难点。它不是对参数签名而是对「HTTP 方法 换行符 请求路径 换行符 时间戳 换行符 随机串 换行符 请求体 SHA256」这一整段字符串做 RSA-SHA256 签名。其中timestamp是当前 Unix 时间戳秒级非毫秒nonce_str是 32 位随机字符串字母数字body是原始 JSON 字符串无空格、无换行、键名小写三者拼接时每个换行符\n都是 LF\x0A不能是 CRLF\x0D\x0A。# v3 签名头生成简化版实际需 base64 编码签名 import time import secrets import hashlib import base64 from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives import hashes def generate_v3_authorization( method: str, url_path: str, timestamp: int, nonce_str: str, body: str, private_key ): # 1. 构造签名原串 message f{method}\n{url_path}\n{timestamp}\n{nonce_str}\n{body}\n # 2. 计算 body 的 SHA256注意body 是原始 JSON 字符串未格式化 body_hash hashlib.sha256(body.encode(utf-8)).digest() # 3. 对 message 做 RSA-SHA256 签名 signature private_key.sign( message.encode(utf-8), padding.PKCS1v15(), hashes.SHA256() ) # 4. 构造 Authorization 头实际需 base64 编码 signature auth_header ( fWECHATPAY2-SHA256-RSA2048 fmchid{mchid}, fnonce_str{nonce_str}, fsignature{base64.b64encode(signature).decode()}, ftimestamp{timestamp}, fserial_no{serial_no} # serial_no 是你证书的序列号需从证书中读取 ) return auth_header关键点serial_no不是你自己设的而是你上传到微信的 API 证书的序列号可在证书详情里看到或用openssl x509 -in apiclient_cert.pem -noout -serial查看。漏填或填错serial_no微信直接返回401 Unauthorized。4. v2 与 v3 的核心差异避坑5 条血泪经验每一条都让团队少加班 4 小时这两套方案不是“选哪个更好”而是“在什么场景下必须用哪个”。很多团队以为 v3 是“新标准所以必须切”结果发现 v2 的到账速度更快、失败率更低也有团队死磕 v2直到微信发邮件通知“v2 接口将于 X 月下线”才仓促迁移。以下是我在 12 个微信打款项目中踩出的 5 条硬核避坑指南4.1 现象v2 接口返回FAILresult_codeFAILerr_codeSIGN_ERROR原因签名字符串中混入了空格、制表符、不可见 Unicode 字符或key没放在末尾或amount传了字符串100而非整数100v2 要求所有数字字段为整型字符串会被当作空值过滤。解决打印出最终拼接的sign_str用repr()查看是否含\x00、\t、\r确认params字典中amount、mchid等字段是int类型用sorted(..., keylambda x: x[0])强制 ASCII 排序勿用dict.keys()默认顺序。4.2 现象v3 接口返回401 Unauthorized响应体为空原因Authorization头中的serial_no错误填了商户号、API 密钥、或旧证书序列号或timestamp与微信服务器时间偏差超过 300 秒微信校验时间戳容错为 ±5 分钟或nonce_str重复使用v3 要求每次请求唯一。解决用openssl x509 -in apiclient_cert.pem -noout -serial精确获取序列号请求前同步服务器时间ntpdate -s time.windows.comnonce_str必须用secrets.token_urlsafe(24)生成禁止用uuid.uuid4().hex长度不够且含-。4.3 现象v3 成功创建批次但查询明细返回{code:PARAM_ERROR,message:invalid request}原因查询接口GET /v3/pay/transfer/batches/{batch_id}/details?offset0limit20的offset和limit参数必须是字符串0和20不能是整数0和20微信 v3 API 对 query 参数类型极其敏感。解决所有 query 参数统一转为字符串URL 拼接时用urllib.parse.urlencode({offset: 0, limit: 20})。4.4 现象v2 打款成功但用户零钱未到账微信账单显示“企业付款”状态为“处理中”超 24 小时原因spbill_create_ip填了内网 IP如192.168.x.x、10.x.x.x或127.0.0.1微信风控系统拦截。解决在发起请求的服务器上执行curl ifconfig.me获取真实出口 IP并在商户平台「产品中心 企业付款 IP 白名单」中添加该 IP支持 CIDR如203.208.60.0/24。4.5 现象v3 回调通知验签失败WechatPay-Signature头存在但验签报InvalidSignature原因回调体是 gzip 压缩的微信默认开启但你的服务未解压就直接验签或回调体 JSON 中的resource.algorithm字段值为AEAD_AES_256_GCM但你的解密逻辑用了 AES-CBC或resource.nonce和resource.ciphertext拼接时多加了换行符。解决收到回调后先检查Content-Encoding: gzip头用gzip.decompress()解压再按微信文档解密流程aes_key hmac.new(api_v3_key.encode(), (associated_data nonce).encode(), hashlib.sha256).digest()[:32]然后 AES-256-GCM 解密。5. 生产环境必须做的三件事证书轮换监控、异步结果轮询、失败自动重试策略源码包给你的是“能跑通”但生产环境要的是“不出事”。我在线上跑了 3 年微信打款总结出三条铁律不监控证书等于裸奔不轮询结果等于盲打不设计重试等于放弃 SLA。5.1 平台证书自动轮换用定时任务每 24 小时检查证书有效期过期前 7 天强制刷新微信平台证书有效期 2 年但会在到期前 30 天开始推送新证书。如果你不主动轮换旧证书过期那一刻所有 v3 接口瞬间 500。源码包中必须包含一个独立脚本每天凌晨 2 点执行# check_cert.sh #!/bin/bash CERT_PATH/opt/wechat/certs/platform_cert.pem DAYS_LEFT$(openssl x509 -in $CERT_PATH -noout -daysuntilexpire 2/dev/null | awk {print $1}) if [ $DAYS_LEFT -lt 7 ]; then echo Certificate expires in $DAYS_LEFT days, fetching new one... # 调用 v3 接口 GET /v3/certificates 获取新证书 curl -X GET \ -H Authorization: Bearer $(generate_access_token) \ https://api.mch.weixin.qq.com/v3/certificates \ -o /tmp/new_cert.pem # 验证新证书有效性 if openssl x509 -in /tmp/new_cert.pem -noout -subject /dev/null 21; then mv /tmp/new_cert.pem $CERT_PATH systemctl reload wechat-pay-service echo Cert updated successfully fi fi提示generate_access_token是用你的商户号、APIv3 密钥、私钥生成的短期 token有效期 8 小时必须缓存并自动续期。不要在每次请求时都生成新 token。5.2 异步结果轮询v3 批次创建后必须用固定间隔轮询GET /v3/pay/transfer/batches/{batch_id}直到batch_status变为FINISHEDv3 打款是异步的。你调POST /v3/pay/transfer/batches只是提交申请真正打款成功与否要看batch_status字段。微信不保证回调 100% 到达网络抖动、你的服务宕机都会丢所以必须轮询。我的策略是创建后立即查一次 → 3 秒后查 → 10 秒后查 → 30 秒后查 → 1 分钟后查 → 后续每 5 分钟查一次最多查 24 小时。# 轮询函数带指数退避 import time import random def poll_batch_status(batch_id: str, max_retries: int 288): # 24小时 * 5分钟/次 for i in range(max_retries): try: resp requests.get( fhttps://api.mch.weixin.qq.com/v3/pay/transfer/batches/{batch_id}, headersget_v3_auth_headers() # 包含 Authorization 头 ) if resp.status_code 200: data resp.json() status data.get(batch_status) if status FINISHED: return data elif status in [PROCESSING, PENDING]: # 指数退避第1次3秒第2次10秒第3次30秒之后固定60秒 wait_time [3, 10, 30] [60] * (max_retries - 3) time.sleep(wait_time[min(i, len(wait_time)-1)]) continue else: raise Exception(fBatch failed: {status}) except Exception as e: print(fPoll failed: {e}) time.sleep(60) raise TimeoutError(Batch polling timeout)5.3 失败自动重试对NETWORK_ERROR、SYSTEMERROR等临时错误必须实现带退避的重试但对BALANCE_NOT_ENOUGH等业务错误绝不重试微信接口错误分两类可重试网络超时、系统繁忙和不可重试余额不足、用户 openid 错误。源码包的重试逻辑必须精准区分错误码类型是否重试最大重试次数退避策略NETWORK_ERROR临时✅3指数退避1s, 2s, 4sSYSTEMERROR临时✅3指数退避BALANCE_NOT_ENOUGH业务❌0记录告警人工介入INVALID_REQUEST业务❌0检查参数修复代码INVALID_SIGNATURE配置❌0检查证书、密钥、时间# 重试装饰器仅针对临时错误 import functools import time def wechat_retry(max_tries3, backoff_factor1): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for i in range(max_tries): try: return func(*args, **kwargs) except WechatTemporaryError as e: if i max_tries - 1: raise wait backoff_factor * (2 ** i) random.uniform(0, 1) time.sleep(wait) return None return wrapper return decorator # 在调用 v2/v3 接口的函数上加装饰器 wechat_retry(max_tries3, backoff_factor1) def transfer_to_wallet_v2(params): # v2 打款逻辑 pass我的习惯所有打款请求必须记录完整日志请求参数、响应体、耗时、错误码日志级别设为INFO并接入 ELK 做关键词告警如err_code:.*、result_code:FAIL。曾经靠这条规则在凌晨 3 点发现某台服务器 NTP 同步失效timestamp偏差 302 秒导致 v3 批量失败——提前 4 小时止损。希望帮到你。本文还有配套的精品资源点击获取