chore: 拉取远程前保留本地改动
含 kefu_base 修改及 club_payment_channel 相关文件。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
244
jituan/services/club_payment_channel.py
Normal file
244
jituan/services/club_payment_channel.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""俱乐部支付通道:读配置、加解密、付呗/微信/支付宝字段归一化。"""
|
||||
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(服务商开户后提供,统一下单必填)',
|
||||
}
|
||||
Reference in New Issue
Block a user