fix: 星阙提现改用club库商户号1747846572,禁止回落旧1746545364

优先读 club 表;为空时用正确 mch/api_v3_key;并从 apiclient_cert.pem 自动读序列号。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-10 22:29:20 +08:00
parent a28dafa6b7
commit 2e419b5b6e
4 changed files with 136 additions and 83 deletions

View File

@@ -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',

View File

@@ -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', ''),

View File

@@ -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

View File

@@ -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