"""诊断微信 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 或 xzj') 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 : 加载成功') from cryptography import x509 from cryptography.hazmat.backends import default_backend tested = [] for fname in sorted(os.listdir(cert_dir)): if not fname.lower().endswith('.pem'): continue if fname.lower() in ('apiclient_key.pem',): continue cert_path = os.path.join(cert_dir, fname) tested.append(fname) try: with open(cert_path, 'rb') as f: data = f.read() if b'PRIVATE KEY' in data: self.stdout.write(f' {fname}: 是私钥,跳过') continue cert = x509.load_pem_x509_certificate(data, default_backend()) serial = format(cert.serial_number, 'X').upper() matched = _public_keys_match(private_key, cert.public_key()) self.stdout.write( f' {fname}: X509证书 serial={serial} 与私钥匹配={matched}' ) except Exception as exc: self.stdout.write(f' {fname}: 非X509商户证书 ({exc})') if 'apiclient_cert.pem' not in tested: self.stdout.write(self.style.ERROR( '\n缺少 apiclient_cert.pem!' '\n请到微信商户平台(1747846572) → 账户中心 → API安全 → 申请API证书' '\n下载证书包,将 apiclient_cert.pem 放到目录:' f'\n {cert_dir}' '\n当前目录只有 apiclient_key.pem,没有商户证书,所以签名必然失败。' '\npub_key.pem 是微信支付平台公钥,不能代替 apiclient_cert.pem。' )) except Exception as exc: self.stdout.write(self.style.ERROR(f'私钥/证书检查失败: {exc}')) # 试探请求:查一个不存在的单号,仅验证签名是否通过(404=签名OK,401 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}'))