Files
Django/jituan/services/club_payment_channel.py
XingQue a3cf7803d8 chore: 拉取远程前保留本地改动
含 kefu_base 修改及 club_payment_channel 相关文件。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 15:11:53 +08:00

245 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""俱乐部支付通道:读配置、加解密、付呗/微信/支付宝字段归一化。"""
import logging
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from django.conf import settings
from django.core.cache import cache
from gvsdsdk.payment.models import EncryptField, _decrypt_field
from jituan.constants import (
CLUB_ID_DEFAULT,
FUBEI_GATEWAY_PROD,
FUBEI_GATEWAY_SANDBOX,
PAY_CHANNEL_ALIPAY_WAP,
PAY_CHANNEL_FUBEI_WX_MINI,
PAY_CHANNEL_WECHAT_JSAPI,
PAY_METHOD_ALIPAY,
PAY_METHOD_FUBEI,
PAY_METHOD_LABELS,
PAY_METHOD_TO_CHANNEL,
PAY_METHOD_WECHAT,
)
from jituan.models import Club, ClubPaymentChannel
logger = logging.getLogger(__name__)
CACHE_TTL = 60
def encrypt_secret_field(plain: str) -> str:
if not plain:
return ''
return EncryptField(plain)
def decrypt_secret_field(encrypted: str) -> str:
if not encrypted:
return ''
return _decrypt_field(encrypted)
@dataclass
class PaymentChannelConfig:
"""归一化后的通道配置,供支付网关读取。"""
club_id: str
channel: str
config_id: int
name: str
enabled: bool
sandbox: bool
app_id: str = ''
mch_id: str = ''
api_key: str = ''
private_key: str = ''
public_key: str = ''
cert_serial_no: str = ''
private_key_path: str = ''
platform_cert_dir: str = ''
notify_url: str = ''
return_url: str = ''
gateway_url: str = ''
mini_app_id: str = ''
extra: Dict[str, Any] = field(default_factory=dict)
@property
def store_id(self):
return self.extra.get('store_id') or self.extra.get('fubei_store_id')
@property
def fubei_vendor_sn(self):
return self.extra.get('vendor_sn') or self.extra.get('fubei_vendor_sn') or self.app_id
def _cache_key(club_id: str, channel: str) -> str:
return f'club_pay_ch:{club_id}:{channel}'
def _default_notify_path(channel: str) -> str:
if channel == PAY_CHANNEL_FUBEI_WX_MINI:
return '/hqhd/dingdan/fubei-notify/'
if channel == PAY_CHANNEL_ALIPAY_WAP:
return '/hqhd/dingdan/alipay-notify/'
return '/hqhd/dingdan/pay-notify/'
def _build_base_url(club: Club) -> str:
domain = (club.h5_domain or '').strip().rstrip('/')
if domain:
return domain
return 'https://www.abas.asia'
def _resolve_notify_url(club: Club, row: ClubPaymentChannel) -> str:
if row.notify_url:
return row.notify_url.strip()
return f'{_build_base_url(club)}{_default_notify_path(row.channel)}'
def _row_to_config(club: Club, row: ClubPaymentChannel) -> PaymentChannelConfig:
extra = dict(row.extra_json or {})
gateway = (row.gateway_url or '').strip()
if not gateway and row.channel == PAY_CHANNEL_FUBEI_WX_MINI:
gateway = FUBEI_GATEWAY_SANDBOX if row.sandbox else FUBEI_GATEWAY_PROD
mini_app_id = (row.mini_app_id or club.pay_app_id or club.wx_appid or '').strip()
return PaymentChannelConfig(
club_id=row.club_id,
channel=row.channel,
config_id=row.id,
name=row.name,
enabled=row.enabled == 1,
sandbox=bool(row.sandbox),
app_id=(row.app_id or '').strip(),
mch_id=(row.mch_id or '').strip(),
api_key=decrypt_secret_field(row.api_key_enc),
private_key=decrypt_secret_field(row.private_key_enc),
public_key=decrypt_secret_field(row.public_key_enc) or (row.public_key_enc or '').strip(),
cert_serial_no=row.cert_serial_no or '',
private_key_path=row.private_key_path or '',
platform_cert_dir=row.platform_cert_dir or '',
notify_url=_resolve_notify_url(club, row),
return_url=(row.return_url or '').strip(),
gateway_url=gateway,
mini_app_id=mini_app_id,
extra=extra,
)
def get_club_payment_channel(
club_id: Optional[str],
channel: str,
*,
config_id: Optional[int] = None,
require_enabled: bool = True,
) -> Optional[PaymentChannelConfig]:
"""读取俱乐部某通道配置;优先 is_default可指定 config_id。"""
cid = (club_id or '').strip() or CLUB_ID_DEFAULT
cache_k = _cache_key(cid, channel)
if config_id is None and require_enabled:
cached = cache.get(cache_k)
if cached is not None:
return cached if cached != '__none__' else None
club = Club.query.filter(club_id=cid).first()
if not club:
logger.warning('club %s 不存在,无法读取支付通道 %s', cid, channel)
return None
qs = ClubPaymentChannel.query.filter(club_id=cid, channel=channel)
if require_enabled:
qs = qs.filter(enabled=1)
if config_id is not None:
row = qs.filter(id=config_id).first()
else:
row = qs.filter(is_default=True).order_by('sort_order', 'id').first()
if not row:
row = qs.order_by('sort_order', 'id').first()
if not row:
if config_id is None and require_enabled:
cache.set(cache_k, '__none__', CACHE_TTL)
return None
cfg = _row_to_config(club, row)
if config_id is None and require_enabled:
cache.set(cache_k, cfg, CACHE_TTL)
return cfg
def invalidate_club_payment_cache(club_id: str, channel: Optional[str] = None):
cid = (club_id or '').strip() or CLUB_ID_DEFAULT
if channel:
cache.delete(_cache_key(cid, channel))
return
for ch in (
PAY_CHANNEL_WECHAT_JSAPI,
PAY_CHANNEL_ALIPAY_WAP,
PAY_CHANNEL_FUBEI_WX_MINI,
):
cache.delete(_cache_key(cid, ch))
def get_fubei_config(club_id: Optional[str] = None) -> Optional[PaymentChannelConfig]:
"""付呗微信小程序通道配置。"""
return get_club_payment_channel(club_id, PAY_CHANNEL_FUBEI_WX_MINI)
def list_payment_methods_for_miniapp(club_id: Optional[str] = None) -> List[Dict[str, Any]]:
"""小程序下单页返回用户可见的支付方式enabled 且配置完整)。"""
cid = (club_id or '').strip() or CLUB_ID_DEFAULT
methods = []
wechat_direct = get_club_payment_channel(cid, PAY_CHANNEL_WECHAT_JSAPI)
fubei = get_fubei_config(cid)
alipay = get_club_payment_channel(cid, PAY_CHANNEL_ALIPAY_WAP)
if fubei and fubei.app_id and fubei.api_key:
methods.append({
'code': PAY_METHOD_FUBEI,
'label': PAY_METHOD_LABELS[PAY_METHOD_FUBEI],
'channel': PAY_CHANNEL_FUBEI_WX_MINI,
'sort_order': _sort_for(cid, PAY_CHANNEL_FUBEI_WX_MINI),
})
elif wechat_direct and wechat_direct.mch_id and wechat_direct.api_key:
methods.append({
'code': PAY_METHOD_WECHAT,
'label': PAY_METHOD_LABELS[PAY_METHOD_WECHAT],
'channel': PAY_CHANNEL_WECHAT_JSAPI,
'sort_order': _sort_for(cid, PAY_CHANNEL_WECHAT_JSAPI),
})
if alipay and alipay.app_id and alipay.private_key and alipay.public_key:
methods.append({
'code': PAY_METHOD_ALIPAY,
'label': PAY_METHOD_LABELS[PAY_METHOD_ALIPAY],
'channel': PAY_CHANNEL_ALIPAY_WAP,
'sort_order': _sort_for(cid, PAY_CHANNEL_ALIPAY_WAP),
})
methods.sort(key=lambda x: x.get('sort_order', 0))
return methods
def _sort_for(club_id: str, channel: str) -> int:
row = ClubPaymentChannel.query.filter(
club_id=club_id, channel=channel, enabled=1,
).order_by('-is_default', 'sort_order', 'id').first()
return row.sort_order if row else 0
def channel_for_pay_method(pay_method: str) -> Optional[str]:
return PAY_METHOD_TO_CHANNEL.get((pay_method or '').strip().lower())
# 付呗通道后台必填字段说明(文档/校验用)
FUBEI_REQUIRED_FIELDS = {
'app_id': '付呗开放平台 vendor_sn / 付呗ID',
'api_key_enc': '付呗 Secret入库前加密',
'mini_app_id': '微信小程序 AppID可与 club.wx_appid 相同;须在付呗后台关联)',
'notify_url': '异步回调,默认 /hqhd/dingdan/fubei-notify/',
'extra_json.store_id': '付呗门店 store_id服务商开户后提供统一下单必填',
}