@@ -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 = ' 481784CF56E872D9D8122BAC64E6BC739080358 2'
# 星阙(xq) 生产微信支付 — club 表为空时的兜底(禁止回落 settings 旧商户 1746545364 )
XQ_WX_PAY = {
' mch_id ' : ' 174784657 2 ' ,
' 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