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

@@ -238,12 +238,20 @@ class HuiyuanGoumai(APIView):
"""生成微信支付参数(完整版,无省略)"""
# 微信支付配置 - 从settings中获取
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.wechat_pay import get_wechat_v2_config
from jituan.services.wechat_pay import get_wechat_v2_config, log_wechat_v2_pay_diag, parse_wechat_v2_unifiedorder_response
pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user))
log_wechat_v2_pay_diag(pay_cfg, scene='huiyuan', openid=openid, order_hint=dingdanid)
APPID = pay_cfg['appid']
MCHID = pay_cfg['mch_id']
KEY = pay_cfg['key']
if not all([APPID, MCHID, KEY]):
raise Exception(
f'微信支付 V2 配置不完整 club={pay_cfg.get("club_id")} '
f'appid={bool(APPID)} mch_id={bool(MCHID)} mch_key={bool(KEY)} '
f'(小程序支付需 config_json.mch_key不是 api_v3_key)'
)
# 根据支付类型获取对应的配置
if pay_type == 'yajin':
PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION
@@ -316,60 +324,41 @@ class HuiyuanGoumai(APIView):
timeout=10 # 设置超时
)
# 解析返回的XML
root = ET.fromstring(response.content)
# 解析返回的XML(失败时记录微信原始响应)
parsed = parse_wechat_v2_unifiedorder_response(
response.content, log_prefix='WX_UNIFIED_HUIYUAN',
)
prepay_id = parsed['prepay_id']
return_code = root.find('return_code').text
result_code = root.find('result_code').text
# 2. 第二次签名(小程序支付参数)
timestamp = str(int(time.time()))
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
package = f'prepay_id={prepay_id}'
sign_type = 'MD5'
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
# 获取prepay_id
prepay_id = root.find('prepay_id').text
# 生成支付签名注意这里key的大小写和字段名与第一次不同
pay_sign_data = {
'appId': APPID, # 注意这里是appId不是appid
'timeStamp': timestamp, # 注意这里是timeStamp不是timestamp
'nonceStr': nonce_str,
'package': package,
'signType': sign_type
}
# 2. 第二次签名(小程序支付参数)
timestamp = str(int(time.time()))
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
package = f'prepay_id={prepay_id}'
sign_type = 'MD5'
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
pay_sign_string += f'&key={KEY}'
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
# 生成支付签名注意这里key的大小写和字段名与第一次不同
pay_sign_data = {
'appId': APPID, # 注意这里是appId不是appid
'timeStamp': timestamp, # 注意这里是timeStamp不是timestamp
'nonceStr': nonce_str,
'package': package,
'signType': sign_type
}
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
pay_sign_string += f'&key={KEY}'
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
# 返回前端需要的支付参数
return {
'appId': APPID, # 对应前端appId
'timeStamp': timestamp, # 对应前端timeStamp
'nonceStr': nonce_str, # 对应前端nonceStr
'package': package, # 对应前端package
'signType': sign_type, # 对应前端signType
'paySign': pay_sign # 对应前端paySign
}
else:
# 获取错误信息
err_code = root.find('err_code')
err_code_des = root.find('err_code_des')
return_msg = root.find('return_msg')
error_details = f"return_code: {return_code}"
if err_code is not None:
error_details += f", err_code: {err_code.text}"
if err_code_des is not None:
error_details += f", err_code_des: {err_code_des.text}"
if return_msg is not None:
error_details += f", return_msg: {return_msg.text}"
raise Exception(f"微信支付下单失败: {error_details}")
# 返回前端需要的支付参数
return {
'appId': APPID, # 对应前端appId
'timeStamp': timestamp, # 对应前端timeStamp
'nonceStr': nonce_str, # 对应前端nonceStr
'package': package, # 对应前端package
'signType': sign_type, # 对应前端signType
'paySign': pay_sign # 对应前端paySign
}
def get_client_ip(self):
"""获取客户端IP完整版"""