修复多商户轮换死锁,并加固回调解密与证书校验。

余额不足等明确错误在查单宽限 PENDING 时会卡死不换号;现确认无单后切换,停用商户仍可解回调、请求带 notify_url。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-14 03:28:42 +08:00
parent ca4b52217f
commit 537e538528
3 changed files with 121 additions and 17 deletions

View File

@@ -45,11 +45,29 @@ def row_to_wx_cfg(row) -> Dict:
}
def wx_cfg_is_complete(cfg: Optional[Dict]) -> bool:
def wx_cfg_is_complete(cfg: Optional[Dict], *, require_key_file: bool = True) -> bool:
if not cfg:
return False
required = ('MCHID', 'APPID', 'API_V3_KEY', 'PRIVATE_KEY_PATH', 'CERT_SERIAL_NO', 'TRANSFER_SCENE_ID')
return all(str(cfg.get(k) or '').strip() for k in required)
if not all(str(cfg.get(k) or '').strip() for k in required):
return False
if require_key_file:
pk = str(cfg.get('PRIVATE_KEY_PATH') or '').strip()
if not os.path.isfile(pk):
logger.warning('商户私钥文件不存在已跳过 mch_id=%s path=%s', cfg.get('MCHID'), pk)
return False
return True
def _rows_to_complete_cfgs(rows) -> List[Dict]:
result = []
for row in rows:
cfg = row_to_wx_cfg(row)
if wx_cfg_is_complete(cfg):
result.append(cfg)
else:
logger.warning('商户配置不完整已跳过 mch_id=%s id=%s', row.mch_id, row.id)
return result
def list_enabled_wx_cfgs() -> List[Dict]:
@@ -59,13 +77,7 @@ def list_enabled_wx_cfgs() -> List[Dict]:
rows = list(
WechatPayMchConfig.objects.filter(enabled=True).order_by('priority', 'id')
)
result = []
for row in rows:
cfg = row_to_wx_cfg(row)
if wx_cfg_is_complete(cfg):
result.append(cfg)
else:
logger.warning('商户配置不完整已跳过 mch_id=%s id=%s', row.mch_id, row.id)
result = _rows_to_complete_cfgs(rows)
if result:
return result
@@ -76,7 +88,29 @@ def list_enabled_wx_cfgs() -> List[Dict]:
return []
def list_wx_cfgs_for_callback() -> List[Dict]:
"""
回调验签/解密:包含已停用商户。
解密只需 API_V3_KEY验签缺平台公钥时会自动跳过该户。
"""
from peizhi.models import WechatPayMchConfig
rows = list(WechatPayMchConfig.objects.all().order_by('priority', 'id'))
result = []
for row in rows:
cfg = row_to_wx_cfg(row)
if str(cfg.get('MCHID') or '').strip() and str(cfg.get('API_V3_KEY') or '').strip():
result.append(cfg)
if result:
return result
fb = _settings_fallback_cfg()
if str(fb.get('MCHID') or '').strip() and str(fb.get('API_V3_KEY') or '').strip():
return [fb]
return []
def get_wx_cfg_by_mch_id(mch_id: str) -> Optional[Dict]:
"""按 mch_id 取配置(含停用);查单需可签名,故要求私钥文件存在。"""
mch_id = (mch_id or '').strip()
if not mch_id:
return None

View File

@@ -102,8 +102,8 @@ def verify_wechat_sign(headers, body, wx_cfg=None):
if wx_cfg:
cfgs = [wx_cfg]
else:
from utils.wechat_mch_service import list_enabled_wx_cfgs
cfgs = list_enabled_wx_cfgs()
from utils.wechat_mch_service import list_wx_cfgs_for_callback
cfgs = list_wx_cfgs_for_callback()
for cfg in cfgs:
cert_path = _resolve_platform_pub_path(cfg, serial=serial)
@@ -139,10 +139,10 @@ def decrypt_callback_with_merchants(associated_data, nonce, ciphertext):
仅对启用商户密钥做 AES 尝试(通常很少),成功后返回 (plaintext, wx_cfg)。
业务侧再用 out_bill_no 反查记录上的 mch_id对齐一致性。
"""
from utils.wechat_mch_service import list_enabled_wx_cfgs
from utils.wechat_mch_service import list_wx_cfgs_for_callback
last_err = None
for cfg in list_enabled_wx_cfgs():
for cfg in list_wx_cfgs_for_callback():
try:
text = decrypt_callback_ciphertext(associated_data, nonce, ciphertext, wx_cfg=cfg)
return text, cfg