fix: V3签名从私钥配对证书自动取序列号,xq忽略club错误私钥路径

SIGN_ERROR根因是私钥与序列号不是同一套证书;扫描匹配 apiclient_cert.pem。
新增 manage.py wx_v3_pay_diag 探针验签。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-10 22:31:14 +08:00
parent 2e419b5b6e
commit b5021ee07d
3 changed files with 182 additions and 19 deletions

View File

@@ -0,0 +1,93 @@
"""诊断微信 V3 商户证书/私钥/序列号是否匹配。用法: python manage.py wx_v3_pay_diag [--club xq]"""
import os
import requests
from django.core.management.base import BaseCommand
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.wechat_pay import (
XQ_WX_PAY,
get_wechat_v3_config,
resolve_merchant_cert_serial,
_load_pem_private_key,
_public_keys_match,
)
from utils.wechat_v3 import build_authorization
class Command(BaseCommand):
help = '诊断微信 V3 提现签名:私钥、证书序列号、商户号是否一致'
def add_arguments(self, parser):
parser.add_argument('--club', default=CLUB_ID_DEFAULT, help='俱乐部 ID默认 xq')
def handle(self, *args, **options):
club_id = (options.get('club') or CLUB_ID_DEFAULT).strip()
cfg = get_wechat_v3_config(club_id)
self.stdout.write('=== 微信 V3 配置诊断 ===')
self.stdout.write(f"club_id : {cfg.get('club_id')}")
self.stdout.write(f"APPID : {cfg.get('APPID')}")
self.stdout.write(f"MCHID : {cfg.get('MCHID')} (期望 {XQ_WX_PAY['mch_id']})")
self.stdout.write(f"CERT_SERIAL : {cfg.get('CERT_SERIAL_NO')}")
self.stdout.write(f"API_V3_KEY : len={len(cfg.get('API_V3_KEY') or '')}")
key_path = cfg.get('PRIVATE_KEY_PATH') or ''
self.stdout.write(f"PRIVATE_KEY : {key_path}")
self.stdout.write(f"key_exists : {os.path.isfile(key_path)}")
cert_dir = os.path.dirname(os.path.abspath(key_path)) if key_path else ''
self.stdout.write(f"cert_dir : {cert_dir}")
if cert_dir and os.path.isdir(cert_dir):
self.stdout.write('cert_dir files:')
for name in sorted(os.listdir(cert_dir)):
if name.lower().endswith('.pem'):
self.stdout.write(f' - {name}')
serial_from_match = resolve_merchant_cert_serial(key_path, '')
self.stdout.write(f"matched_serial: {serial_from_match or '(未匹配到证书)'}")
if key_path and os.path.isfile(key_path):
try:
private_key = _load_pem_private_key(key_path)
self.stdout.write('private_key : 加载成功')
cert_path = key_path.replace('apiclient_key.pem', 'apiclient_cert.pem')
if os.path.isfile(cert_path):
from cryptography import x509
from cryptography.hazmat.backends import default_backend
with open(cert_path, 'rb') as f:
cert = x509.load_pem_x509_certificate(f.read(), default_backend())
matched = _public_keys_match(private_key, cert.public_key())
self.stdout.write(
f"apiclient_cert.pem 与私钥匹配: {matched} "
f"serial={format(cert.serial_number, 'X').upper()}"
)
else:
self.stdout.write(self.style.ERROR(
f'缺少 apiclient_cert.pem与 apiclient_key.pem 同目录): {cert_path}'
))
except Exception as exc:
self.stdout.write(self.style.ERROR(f'私钥/证书检查失败: {exc}'))
# 试探请求查一个不存在的单号仅验证签名是否通过404=签名OK401 SIGN_ERROR=签名失败)
url = 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/WX_V3_DIAG_TEST'
auth = build_authorization('GET', url, '', club_id=club_id)
try:
resp = requests.get(
url,
headers={
'Authorization': auth,
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0',
},
timeout=10,
)
self.stdout.write(f"\n探针 HTTP {resp.status_code}: {resp.text[:300]}")
if resp.status_code == 401:
self.stdout.write(self.style.ERROR(
'签名仍失败:请确认服务器 apiclient_key.pem / apiclient_cert.pem '
'是商户号 1747846572 在微信商户平台下载的那一对'
))
elif resp.status_code in (404, 200):
self.stdout.write(self.style.SUCCESS('签名已通过404/200 表示微信接受了商户签名)'))
except requests.RequestException as exc:
self.stdout.write(self.style.ERROR(f'探针请求失败: {exc}'))