fix: 会员支付打印微信完整错误并支持配置 V2 商户密钥

- 统一下单 XML 安全解析,日志输出 raw_response 与 err_code_des
- 下单前打印 club/appid/mch_id/key_source 诊断
- 后台 club-manage 可保存 config_json.mch_key(V2 MD5 密钥,非 api_v3_key)
- 未配 mch_key 时告警:回落全局密钥易导致签名错误

回退: git reset --hard backup-before-wx-v2-diag-20260710

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-10 20:05:00 +08:00
parent e0fa112da6
commit 3660fa4cc7
3 changed files with 142 additions and 59 deletions

View File

@@ -10,6 +10,26 @@ from jituan.models import Club
logger = logging.getLogger(__name__)
def _xml_text(root, tag, default=''):
node = root.find(tag)
if node is None or node.text is None:
return default
return node.text
def _resolve_v2_mch_key(cfg_json, fallback_key):
"""V2 JSAPI 统一下单用的商户 API 密钥32 位 MD5 密钥,不是 api_v3_key"""
cfg_json = cfg_json or {}
for field in ('mch_key', 'shanghumiyao', 'WEIXIN_SHANGHUMIYAO'):
val = (cfg_json.get(field) or '').strip()
if val:
return val, f'club.config_json.{field}'
fb = (fallback_key or '').strip()
if fb:
return fb, 'settings.WEIXIN_SHANGHUMIYAO'
return '', 'missing'
def get_wechat_v2_config(club_id=None):
"""
小程序 JSAPI 支付 V2 配置。
@@ -30,20 +50,86 @@ def get_wechat_v2_config(club_id=None):
return fallback
cfg_json = club.config_json or {}
mch_key = (
cfg_json.get('mch_key')
or cfg_json.get('shanghumiyao')
or cfg_json.get('WEIXIN_SHANGHUMIYAO')
or fallback['key']
)
mch_key, key_source = _resolve_v2_mch_key(cfg_json, fallback['key'])
appid = club.pay_app_id or club.wx_appid or fallback['appid']
mch_id = club.mch_id or fallback['mch_id']
if mch_id and key_source == 'settings.WEIXIN_SHANGHUMIYAO':
logger.warning(
'[WX_V2_CONFIG] club=%s 已配置 mch_id=%s 但未配置 V2 商户密钥 config_json.mch_key'
'当前回落全局 WEIXIN_SHANGHUMIYAO与 api_v3_key 不是同一个密钥,易导致签名错误)',
cid, mch_id,
)
return {
'appid': club.pay_app_id or club.wx_appid or fallback['appid'],
'mch_id': club.mch_id or fallback['mch_id'],
'appid': appid,
'mch_id': mch_id,
'key': mch_key,
'club_id': cid,
'_key_source': key_source,
}
def log_wechat_v2_pay_diag(pay_cfg, *, scene='', openid='', order_hint=''):
"""下单前诊断日志(不打印完整密钥)。"""
oid = (openid or '')
oid_preview = (oid[:6] + '...' + oid[-4:]) if len(oid) > 10 else oid or '(empty)'
key = pay_cfg.get('key') or ''
logger.warning(
'[WX_V2_PAY_DIAG] scene=%s club=%s appid=%s mch_id=%s key_len=%d key_source=%s openid=%s hint=%s',
scene or '-',
pay_cfg.get('club_id'),
pay_cfg.get('appid'),
pay_cfg.get('mch_id'),
len(key),
pay_cfg.get('_key_source', '?'),
oid_preview,
order_hint or '-',
)
def parse_wechat_v2_unifiedorder_response(content, *, log_prefix='WX_UNIFIED'):
"""
解析微信 V2 统一下单 XML失败时记录完整原始响应并抛出可读异常。
成功返回 {'prepay_id': '...'}。
"""
import defusedxml.ElementTree as ET
raw = (
content.decode('utf-8', errors='replace')
if isinstance(content, (bytes, bytearray))
else str(content or '')
)
logger.warning('[%s] raw_response=%s', log_prefix, raw)
try:
root = ET.fromstring(content)
except Exception as exc:
raise Exception(f'微信支付返回非 XML: {raw[:800]}') from exc
return_code = _xml_text(root, 'return_code')
result_code = _xml_text(root, 'result_code')
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
prepay_id = _xml_text(root, 'prepay_id')
if not prepay_id:
raise Exception(f'微信支付成功但缺少 prepay_id: {raw}')
return {'prepay_id': prepay_id}
parts = []
if return_code:
parts.append(f'return_code={return_code}')
if result_code:
parts.append(f'result_code={result_code}')
for tag in ('return_msg', 'err_code', 'err_code_des'):
val = _xml_text(root, tag)
if val:
parts.append(f'{tag}={val}')
if not parts:
parts.append(f'raw={raw[:800]}')
raise Exception(f'微信支付下单失败: {", ".join(parts)}')
def verify_wechat_v2_signature(xml_data, club_id=None):
"""验证微信 V2 支付回调 MD5 签名。"""
try:

View File

@@ -443,9 +443,11 @@ class ClubManageView(APIView):
'sort_order': club.sort_order,
}
if full:
cfg_json = club.config_json or {}
base.update({
'wx_secret': club.wx_secret,
'api_v3_key': club.api_v3_key,
'mch_key': cfg_json.get('mch_key') or cfg_json.get('shanghumiyao') or '',
'cert_serial_no': club.cert_serial_no,
'private_key_path': club.private_key_path,
'platform_cert_dir': club.platform_cert_dir,
@@ -494,6 +496,12 @@ class ClubManageView(APIView):
if field in request.data:
setattr(club, field, request.data[field])
update_fields.append(field)
if 'mch_key' in request.data:
cfg = dict(club.config_json or {})
cfg['mch_key'] = (request.data.get('mch_key') or '').strip()
club.config_json = cfg
if 'config_json' not in update_fields:
update_fields.append('config_json')
if update_fields:
club.save(update_fields=update_fields)
return Response({