xzj 提现/查单与 xq 同读 settings 私钥目录;AppID 仍各用 club 表小程序。 Co-authored-by: Cursor <cursoragent@cursor.com>
115 lines
5.5 KiB
Python
115 lines
5.5 KiB
Python
"""诊断微信 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}'))
|