fix: 星阙提现改用club库商户号1747846572,禁止回落旧1746545364
优先读 club 表;为空时用正确 mch/api_v3_key;并从 apiclient_cert.pem 自动读序列号。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
29e4fe3c8e
commit
8cf6d3d0ea
4 files changed
+136
-83
No files matched your search
@@ -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',
|
||||
|
||||
@@ -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', ''),
|
||||
|
||||
+128
-71
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user