Files
Django/jituan/management/commands/wx_v3_pay_diag.py
XingQue 48eca8e5d0 fix: 星之界(xzj)与星阙共用商户证书路径及V3兜底配置
xzj 提现/查单与 xq 同读 settings 私钥目录;AppID 仍各用 club 表小程序。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 22:57:21 +08:00

115 lines
5.5 KiB
Python
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.
"""诊断微信 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='俱乐部 IDxq 或 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=签名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}'))