From 2e419b5b6e834161570d29bad398aca88142d066 Mon Sep 17 00:00:00 2001 From: XingQue Date: Fri, 10 Jul 2026 22:29:20 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=98=9F=E9=98=99=E6=8F=90=E7=8E=B0?= =?UTF-8?q?=E6=94=B9=E7=94=A8club=E5=BA=93=E5=95=86=E6=88=B7=E5=8F=B717478?= =?UTF-8?q?46572=EF=BC=8C=E7=A6=81=E6=AD=A2=E5=9B=9E=E8=90=BD=E6=97=A71746?= =?UTF-8?q?545364?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优先读 club 表;为空时用正确 mch/api_v3_key;并从 apiclient_cert.pem 自动读序列号。 Co-authored-by: Cursor --- a_long_dianjing/settings.py | 7 +- jituan/management/commands/seed_club_xq.py | 10 +- jituan/services/wechat_pay.py | 199 +++++++++++++-------- utils/wechat_v3.py | 3 - 4 files changed, 136 insertions(+), 83 deletions(-) diff --git a/a_long_dianjing/settings.py b/a_long_dianjing/settings.py index a53318d..e07bdb4 100644 --- a/a_long_dianjing/settings.py +++ b/a_long_dianjing/settings.py @@ -375,11 +375,10 @@ ORDER_CHECK_INTERVAL = 5 # ==================== 微信支付 V3 ==================== WECHAT_PAY_V3_CONFIG = { - 'MCHID': app_secrets.WECHAT_PAY_V3_MCHID, - 'APPID': app_secrets.WECHAT_PAY_V3_APPID, - 'API_V3_KEY': app_secrets.WECHAT_PAY_V3_API_V3_KEY, + 'MCHID': '1747846572', + 'APPID': 'wx0e4be86faac4a8d1', + 'API_V3_KEY': 'dujie4637rufjd98ijuyrgcvbvbn4789', 'PRIVATE_KEY_PATH': app_secrets.WECHAT_PAY_V3_PRIVATE_KEY_PATH, - # 与服务器 apiclient_key.pem 对应;勿用 club 表错误值 'CERT_SERIAL_NO': '481784CF56E872D9D8122BAC64E6BC7390803582', 'PLATFORM_CERT_DIR': app_secrets.WECHAT_PAY_V3_PLATFORM_CERT_DIR, 'NOTIFY_URL': 'https://www.abas.asia/hqhd/yonghu/tixiancallback', diff --git a/jituan/management/commands/seed_club_xq.py b/jituan/management/commands/seed_club_xq.py index 0c12750..d45dba8 100644 --- a/jituan/management/commands/seed_club_xq.py +++ b/jituan/management/commands/seed_club_xq.py @@ -7,7 +7,7 @@ from django.core.management.base import BaseCommand from django.db import transaction from jituan.constants import CLUB_ID_DEFAULT -from jituan.services.wechat_pay import WX_V3_CERT_SERIAL_NO_OVERRIDE +from jituan.services.wechat_pay import XQ_WX_PAY, WX_V3_CERT_SERIAL_NO_OVERRIDE from jituan.models import ( Club, ClubHuiyuanPrice, @@ -42,10 +42,10 @@ class Command(BaseCommand): 'status': 1, 'wx_appid': getattr(settings, 'WEIXIN_APPID', ''), 'wx_secret': getattr(settings, 'WEIXIN_SECRET', ''), - 'mch_id': getattr(settings, 'WEIXIN_MCHID', '') or wx_v3.get('MCHID', ''), - 'pay_app_id': wx_v3.get('APPID', '') or getattr(settings, 'WEIXIN_APPID', ''), - 'mch_key': getattr(settings, 'WEIXIN_SHANGHUMIYAO', ''), - 'api_v3_key': wx_v3.get('API_V3_KEY', ''), + 'mch_id': XQ_WX_PAY['mch_id'], + 'pay_app_id': XQ_WX_PAY['wx_appid'], + 'mch_key': XQ_WX_PAY['mch_key'], + 'api_v3_key': XQ_WX_PAY['api_v3_key'], 'cert_serial_no': WX_V3_CERT_SERIAL_NO_OVERRIDE, 'private_key_path': wx_v3.get('PRIVATE_KEY_PATH', ''), 'platform_cert_dir': wx_v3.get('PLATFORM_CERT_DIR', ''), diff --git a/jituan/services/wechat_pay.py b/jituan/services/wechat_pay.py index 8c97ba3..cdd6cfd 100644 --- a/jituan/services/wechat_pay.py +++ b/jituan/services/wechat_pay.py @@ -11,9 +11,75 @@ from jituan.services.club_resolver import get_club_miniapp_appid logger = logging.getLogger(__name__) -# 临时写死:微信商户 API 证书序列号(与 apiclient_key.pem 对应) -# 后台 club 表 / settings 配置正确后可删除此覆盖 -WX_V3_CERT_SERIAL_NO_OVERRIDE = '481784CF56E872D9D8122BAC64E6BC7390803582' +# 星阙(xq) 生产微信支付 — club 表为空时的兜底(禁止回落 settings 旧商户 1746545364) +XQ_WX_PAY = { + 'mch_id': '1747846572', + 'mch_key': 'dujie4637rufjd98ijuyrgcvbvbn4789', + 'api_v3_key': 'dujie4637rufjd98ijuyrgcvbvbn4789', + 'cert_serial_no': '481784CF56E872D9D8122BAC64E6BC7390803582', + 'wx_appid': 'wx0e4be86faac4a8d1', +} + +# 兼容旧引用 +WX_V3_CERT_SERIAL_NO_OVERRIDE = XQ_WX_PAY['cert_serial_no'] + + +def _pick_club_field(club, cfg_json, *field_names): + """从 club 列或 config_json 取第一个非空值。""" + if club: + for name in field_names: + if '.' not in name: + val = (getattr(club, name, None) or '').strip() + if val: + return val + cfg_json = cfg_json or {} + for name in field_names: + key = name.split('.')[-1] if '.' in name else name + val = (cfg_json.get(key) or '').strip() + if val: + return val + return '' + + +def resolve_merchant_cert_serial(private_key_path, configured_serial=''): + """从 apiclient_cert.pem 读取与私钥匹配的证书序列号(避免手填错误)。""" + configured = (configured_serial or '').strip().upper() + if not private_key_path: + return configured + cert_dir = os.path.dirname(private_key_path) + candidates = [ + private_key_path.replace('apiclient_key.pem', 'apiclient_cert.pem'), + os.path.join(cert_dir, 'apiclient_cert.pem'), + os.path.join(cert_dir, 'wechatpay_cert.pem'), + ] + seen = set() + for path in candidates: + if not path or path in seen: + continue + seen.add(path) + if not os.path.isfile(path): + continue + try: + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + with open(path, 'rb') as f: + cert = x509.load_pem_x509_certificate(f.read(), default_backend()) + serial = format(cert.serial_number, 'X').upper() + logger.warning('[WX_V3_CONFIG] 从证书文件读取序列号 path=%s serial=%s', path, serial) + return serial + except Exception as exc: + logger.warning('[WX_V3_CONFIG] 读取证书序列号失败 path=%s err=%s', path, exc) + return configured + + +def _xq_defaults(): + """仅私钥路径/平台证书目录可回落 settings(与商户号无关)。""" + wx = dict(getattr(settings, 'WECHAT_PAY_V3_CONFIG', {}) or {}) + return { + 'PRIVATE_KEY_PATH': wx.get('PRIVATE_KEY_PATH', ''), + 'PLATFORM_CERT_DIR': wx.get('PLATFORM_CERT_DIR', ''), + 'TRANSFER_SCENE_ID': wx.get('TRANSFER_SCENE_ID', '1005'), + } def _xml_text(root, tag, default=''): @@ -23,7 +89,7 @@ def _xml_text(root, tag, default=''): return node.text -def _resolve_v2_mch_key(club, cfg_json, fallback_key): +def _resolve_v2_mch_key(club, cfg_json, fallback_key, club_id=None): """V2 JSAPI 统一下单用的商户 API 密钥(32 位 MD5 密钥,不是 api_v3_key)。""" column_key = (getattr(club, 'mch_key', None) or '').strip() if club else '' if column_key: @@ -33,6 +99,9 @@ def _resolve_v2_mch_key(club, cfg_json, fallback_key): val = (cfg_json.get(field) or '').strip() if val: return val, f'club.config_json.{field}' + cid = (club_id or '').strip() or CLUB_ID_DEFAULT + if cid == CLUB_ID_DEFAULT and XQ_WX_PAY['mch_key']: + return XQ_WX_PAY['mch_key'], 'XQ_WX_PAY.mch_key' fb = (fallback_key or '').strip() if fb: return fb, 'settings.WEIXIN_SHANGHUMIYAO' @@ -59,7 +128,7 @@ def get_wechat_v2_config(club_id=None): return fallback cfg_json = club.config_json or {} - mch_key, key_source = _resolve_v2_mch_key(club, cfg_json, fallback['key']) + mch_key, key_source = _resolve_v2_mch_key(club, cfg_json, fallback['key'], club_id=cid) mini_appid = get_club_miniapp_appid(club) wx_a = (club.wx_appid or '').strip() pay_a = (club.pay_app_id or '').strip() @@ -79,7 +148,11 @@ def get_wechat_v2_config(club_id=None): cid, ) appid = '' - mch_id = club.mch_id or fallback['mch_id'] + mch_id = ( + _pick_club_field(club, cfg_json, 'mch_id') + or (XQ_WX_PAY['mch_id'] if cid == CLUB_ID_DEFAULT else '') + or fallback['mch_id'] + ) if mch_id and key_source == 'settings.WEIXIN_SHANGHUMIYAO': logger.warning( @@ -205,19 +278,17 @@ def club_id_from_order(order_id): return CLUB_ID_DEFAULT -def _apply_v3_cert_serial_override(cfg): - cfg['CERT_SERIAL_NO'] = WX_V3_CERT_SERIAL_NO_OVERRIDE - return cfg - - -def _log_v3_config(cfg): +def _log_v3_config(cfg, *, source=''): key_path = (cfg.get('PRIVATE_KEY_PATH') or '').strip() logger.warning( - '[WX_V3_CONFIG] club=%s appid=%s mch=%s serial=%s key_path=%s key_exists=%s', + '[WX_V3_CONFIG] source=%s club=%s appid=%s mch=%s serial=%s ' + 'api_v3_key_len=%d key_path=%s key_exists=%s', + source or '-', cfg.get('club_id'), cfg.get('APPID'), cfg.get('MCHID'), cfg.get('CERT_SERIAL_NO'), + len(cfg.get('API_V3_KEY') or ''), key_path, bool(key_path and os.path.isfile(key_path)), ) @@ -226,72 +297,58 @@ def _log_v3_config(cfg): def get_wechat_v3_config(club_id=None): """ 微信 V3(转账/查单)配置。 - - 星阙(xq):证书序列号、私钥、api_v3_key、商户号强制用 settings 全局配置, - 避免 club 表错填导致「证书序列号与私钥不匹配」。仅 AppID 读 club 小程序配置。 - 其他俱乐部仍读 club 表,但序列号统一写死覆盖。 + 优先 club 数据库 → 星阙写死兜底 → 仅私钥路径/平台证书目录回落 settings。 + 禁止用 settings 里的旧商户号 1746545364。 """ cid = (club_id or '').strip() or CLUB_ID_DEFAULT - wx = dict(getattr(settings, 'WECHAT_PAY_V3_CONFIG', {}) or {}) - fallback = { - 'APPID': wx.get('APPID', ''), - 'MCHID': wx.get('MCHID', ''), - 'PRIVATE_KEY_PATH': wx.get('PRIVATE_KEY_PATH', ''), - 'CERT_SERIAL_NO': WX_V3_CERT_SERIAL_NO_OVERRIDE, - 'API_V3_KEY': wx.get('API_V3_KEY', ''), - 'PLATFORM_CERT_DIR': wx.get('PLATFORM_CERT_DIR', ''), - 'TRANSFER_SCENE_ID': wx.get('TRANSFER_SCENE_ID', '1005'), - 'club_id': cid, - } - - if cid == CLUB_ID_DEFAULT: - cfg = dict(fallback) - try: - club = Club.query.get(club_id=cid) - mini_appid = get_club_miniapp_appid(club) - if mini_appid: - cfg['APPID'] = mini_appid - except Club.DoesNotExist: - pass - _log_v3_config(cfg) - return cfg + path_defaults = _xq_defaults() + club = None + cfg_json = {} try: club = Club.query.get(club_id=cid) + cfg_json = club.config_json or {} except Club.DoesNotExist: - logger.warning('club %s 不存在,回落全局 V3 支付配置', cid) - cfg = _apply_v3_cert_serial_override(fallback) - _log_v3_config(cfg) - return cfg + logger.warning('club %s 不存在,使用星阙兜底或路径默认', cid) - cfg_json = club.config_json or {} - cfg = _apply_v3_cert_serial_override({ - 'APPID': get_club_miniapp_appid(club) or '', - 'MCHID': club.mch_id or fallback['MCHID'], - 'PRIVATE_KEY_PATH': ( - club.private_key_path - or cfg_json.get('private_key_path') - or fallback['PRIVATE_KEY_PATH'] - ), - 'CERT_SERIAL_NO': fallback['CERT_SERIAL_NO'], - 'API_V3_KEY': ( - club.api_v3_key - or cfg_json.get('api_v3_key') - or fallback['API_V3_KEY'] - ), - 'PLATFORM_CERT_DIR': ( - club.platform_cert_dir - or cfg_json.get('platform_cert_dir') - or fallback['PLATFORM_CERT_DIR'] - ), - 'TRANSFER_SCENE_ID': ( - cfg_json.get('transfer_scene_id') - or cfg_json.get('TRANSFER_SCENE_ID') - or fallback['TRANSFER_SCENE_ID'] - ), + xq = XQ_WX_PAY if cid == CLUB_ID_DEFAULT else {} + + mch_id = _pick_club_field(club, cfg_json, 'mch_id') or xq.get('mch_id', '') + api_v3_key = _pick_club_field(club, cfg_json, 'api_v3_key') or xq.get('api_v3_key', '') + cert_serial = ( + _pick_club_field(club, cfg_json, 'cert_serial_no') + or xq.get('cert_serial_no', '') + ) + private_key_path = ( + _pick_club_field(club, cfg_json, 'private_key_path') + or path_defaults['PRIVATE_KEY_PATH'] + ) + platform_cert_dir = ( + _pick_club_field(club, cfg_json, 'platform_cert_dir') + or path_defaults['PLATFORM_CERT_DIR'] + ) + transfer_scene_id = ( + _pick_club_field(club, cfg_json, 'transfer_scene_id', 'TRANSFER_SCENE_ID') + or path_defaults['TRANSFER_SCENE_ID'] + ) + + appid = get_club_miniapp_appid(club) if club else '' + if not appid and cid == CLUB_ID_DEFAULT: + appid = xq.get('wx_appid', '') + + cert_serial = resolve_merchant_cert_serial(private_key_path, cert_serial) + + cfg = { + 'APPID': appid, + 'MCHID': mch_id, + 'PRIVATE_KEY_PATH': private_key_path, + 'CERT_SERIAL_NO': cert_serial, + 'API_V3_KEY': api_v3_key, + 'PLATFORM_CERT_DIR': platform_cert_dir, + 'TRANSFER_SCENE_ID': transfer_scene_id, 'club_id': cid, - }) - _log_v3_config(cfg) + } + _log_v3_config(cfg, source='club_db' if club else 'xq_fallback') return cfg diff --git a/utils/wechat_v3.py b/utils/wechat_v3.py index e31c815..166d0fd 100644 --- a/utils/wechat_v3.py +++ b/utils/wechat_v3.py @@ -39,9 +39,6 @@ def build_authorization(method, url, body, club_id=None): private_key = load_private_key(club_id) mchid = cfg['MCHID'] serial_no = (cfg.get('CERT_SERIAL_NO') or '').strip().upper() - if not serial_no: - from jituan.services.wechat_pay import WX_V3_CERT_SERIAL_NO_OVERRIDE - serial_no = WX_V3_CERT_SERIAL_NO_OVERRIDE parsed = urlparse(url) path = parsed.path