小程序前端仍传 wechat;后台可切换 wechat/fubei,支持配置多套付呗并退款按原通道路由。 Co-authored-by: Cursor <cursoragent@cursor.com>
242 lines
9.8 KiB
Python
242 lines
9.8 KiB
Python
"""俱乐部支付通道后台管理(付呗配置 CRUD + 选用 wechat/fubei)。"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
|
||
from rest_framework.parsers import JSONParser
|
||
from rest_framework.permissions import IsAuthenticated
|
||
from rest_framework.response import Response
|
||
from rest_framework.views import APIView
|
||
|
||
from backend.utils import verify_kefu_permission
|
||
from jituan.constants import (
|
||
CLUB_ID_DEFAULT,
|
||
MINI_PAY_CHANNEL_FUBEI,
|
||
MINI_PAY_CHANNEL_WECHAT,
|
||
PAY_CHANNEL_FUBEI_WX_MINI,
|
||
)
|
||
from jituan.models import Club, ClubPaymentChannel
|
||
from jituan.services.club_payment_channel import (
|
||
encrypt_secret_field,
|
||
invalidate_club_payment_cache,
|
||
)
|
||
from jituan.services.mini_pay_router import get_club_mini_pay_channel, set_club_mini_pay_channel
|
||
from jituan.services.permissions import can_manage_admin_assignments
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _mask_secret(plain: str) -> str:
|
||
s = (plain or '').strip()
|
||
if not s:
|
||
return ''
|
||
if len(s) <= 8:
|
||
return '****'
|
||
return s[:4] + '****' + s[-4:]
|
||
|
||
|
||
def _serialize_channel(row: ClubPaymentChannel, reveal_secret: bool = False) -> dict:
|
||
from jituan.services.club_payment_channel import decrypt_secret_field
|
||
secret = decrypt_secret_field(row.api_key_enc) if row.api_key_enc else ''
|
||
extra = dict(row.extra_json or {})
|
||
return {
|
||
'id': row.id,
|
||
'club_id': row.club_id,
|
||
'channel': row.channel,
|
||
'name': row.name,
|
||
'enabled': row.enabled,
|
||
'is_default': row.is_default,
|
||
'sort_order': row.sort_order,
|
||
'sandbox': row.sandbox,
|
||
'app_id': row.app_id,
|
||
'mch_id': row.mch_id,
|
||
'mini_app_id': row.mini_app_id,
|
||
'store_id': extra.get('store_id') or extra.get('fubei_store_id') or '',
|
||
'merchant_id': extra.get('merchant_id') or row.mch_id or '',
|
||
'api_key_masked': _mask_secret(secret),
|
||
'api_key': secret if reveal_secret else '',
|
||
'api_key_configured': bool(secret),
|
||
'notify_url': row.notify_url,
|
||
'gateway_url': row.gateway_url,
|
||
'remark': row.remark,
|
||
'extra_json': extra,
|
||
}
|
||
|
||
|
||
class ClubPaymentChannelManageView(APIView):
|
||
"""POST /jituan/houtai/payment-channel-manage
|
||
|
||
actions:
|
||
- get: 当前俱乐部 mini_pay_channel + 付呗通道列表
|
||
- set_mini_pay_channel: {mini_pay_channel: wechat|fubei}
|
||
- save_fubei: 新增或更新付呗配置(按 id 或 name)
|
||
- set_default / enable / disable
|
||
- delete
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username = (request.data.get('phone') or request.data.get('username') or '').strip()
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少账号'}, status=401)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
if not can_manage_admin_assignments(request.user):
|
||
return Response({'code': 403, 'msg': '仅集团超管可管理支付通道'}, status=403)
|
||
|
||
action = (request.data.get('action') or 'get').strip()
|
||
club_id = (request.data.get('club_id') or '').strip() or CLUB_ID_DEFAULT
|
||
club = Club.query.filter(club_id=club_id).first()
|
||
if not club:
|
||
return Response({'code': 1, 'msg': f'俱乐部 {club_id} 不存在'})
|
||
|
||
if action == 'get':
|
||
rows = ClubPaymentChannel.query.filter(
|
||
club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI,
|
||
).order_by('-is_default', 'sort_order', 'id')
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'club_id': club_id,
|
||
'mini_pay_channel': get_club_mini_pay_channel(club_id),
|
||
'fubei_channels': [_serialize_channel(r) for r in rows],
|
||
},
|
||
})
|
||
|
||
if action == 'set_mini_pay_channel':
|
||
ch = (request.data.get('mini_pay_channel') or '').strip().lower()
|
||
try:
|
||
set_club_mini_pay_channel(club_id, ch)
|
||
except ValueError as exc:
|
||
return Response({'code': 400, 'msg': str(exc)})
|
||
if ch == MINI_PAY_CHANNEL_FUBEI:
|
||
fb = ClubPaymentChannel.query.filter(
|
||
club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI, enabled=1,
|
||
).first()
|
||
if not fb:
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '已切换为付呗,但尚未配置付呗通道,请先保存付呗配置',
|
||
'data': {'mini_pay_channel': ch},
|
||
'warning': True,
|
||
})
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '已切换小程序收款通道',
|
||
'data': {'mini_pay_channel': ch},
|
||
})
|
||
|
||
if action == 'save_fubei':
|
||
return self._save_fubei(request, club_id)
|
||
|
||
if action in ('enable', 'disable', 'set_default', 'delete'):
|
||
row_id = request.data.get('id')
|
||
row = ClubPaymentChannel.query.filter(
|
||
id=row_id, club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI,
|
||
).first()
|
||
if not row:
|
||
return Response({'code': 1, 'msg': '配置不存在'})
|
||
if action == 'delete':
|
||
row.delete()
|
||
invalidate_club_payment_cache(club_id, PAY_CHANNEL_FUBEI_WX_MINI)
|
||
return Response({'code': 0, 'msg': '已删除'})
|
||
if action == 'enable':
|
||
row.enabled = 1
|
||
row.save(update_fields=['enabled'])
|
||
elif action == 'disable':
|
||
row.enabled = 0
|
||
row.save(update_fields=['enabled'])
|
||
elif action == 'set_default':
|
||
ClubPaymentChannel.objects.filter(
|
||
club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI, is_default=True,
|
||
).exclude(id=row.id).update(is_default=False)
|
||
row.is_default = True
|
||
row.enabled = 1
|
||
row.save(update_fields=['is_default', 'enabled'])
|
||
invalidate_club_payment_cache(club_id, PAY_CHANNEL_FUBEI_WX_MINI)
|
||
return Response({'code': 0, 'msg': 'ok', 'data': _serialize_channel(row)})
|
||
|
||
return Response({'code': 400, 'msg': f'未知 action: {action}'})
|
||
except Exception as exc:
|
||
logger.error('payment-channel-manage 异常: %s', exc, exc_info=True)
|
||
return Response({'code': 500, 'msg': str(exc)})
|
||
|
||
def _save_fubei(self, request, club_id: str):
|
||
name = (request.data.get('name') or '主付呗').strip() or '主付呗'
|
||
row_id = request.data.get('id')
|
||
app_id = (request.data.get('app_id') or '').strip()
|
||
api_key = request.data.get('api_key')
|
||
store_id = request.data.get('store_id')
|
||
merchant_id = (request.data.get('merchant_id') or request.data.get('mch_id') or '').strip()
|
||
mini_app_id = (request.data.get('mini_app_id') or '').strip()
|
||
remark = (request.data.get('remark') or '').strip()
|
||
|
||
if not app_id:
|
||
return Response({'code': 400, 'msg': '请填写付呗开放平台 app_id'})
|
||
if store_id in (None, ''):
|
||
return Response({'code': 400, 'msg': '请填写门店 store_id'})
|
||
|
||
row = None
|
||
if row_id:
|
||
row = ClubPaymentChannel.query.filter(
|
||
id=row_id, club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI,
|
||
).first()
|
||
if not row:
|
||
row = ClubPaymentChannel.query.filter(
|
||
club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI, name=name,
|
||
).first()
|
||
creating = row is None
|
||
if creating:
|
||
row = ClubPaymentChannel(
|
||
club_id=club_id,
|
||
channel=PAY_CHANNEL_FUBEI_WX_MINI,
|
||
name=name,
|
||
enabled=1,
|
||
is_default=True,
|
||
)
|
||
|
||
row.app_id = app_id
|
||
row.mch_id = merchant_id
|
||
row.mini_app_id = mini_app_id
|
||
row.remark = remark
|
||
if api_key is not None and str(api_key).strip():
|
||
row.api_key_enc = encrypt_secret_field(str(api_key).strip())
|
||
elif creating:
|
||
return Response({'code': 400, 'msg': '新建付呗配置必须填写接口密钥'})
|
||
|
||
extra = dict(row.extra_json or {})
|
||
try:
|
||
extra['store_id'] = int(store_id)
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': 'store_id 必须是数字'})
|
||
if merchant_id:
|
||
extra['merchant_id'] = merchant_id
|
||
row.extra_json = extra
|
||
|
||
# 设为默认时取消同通道其它默认
|
||
if request.data.get('is_default', True):
|
||
ClubPaymentChannel.objects.filter(
|
||
club_id=club_id, channel=PAY_CHANNEL_FUBEI_WX_MINI, is_default=True,
|
||
).exclude(id=getattr(row, 'id', None) or 0).update(is_default=False)
|
||
row.is_default = True
|
||
row.enabled = 1
|
||
|
||
row.save()
|
||
invalidate_club_payment_cache(club_id, PAY_CHANNEL_FUBEI_WX_MINI)
|
||
|
||
# 可选:保存后同时切到付呗
|
||
if request.data.get('activate'):
|
||
set_club_mini_pay_channel(club_id, MINI_PAY_CHANNEL_FUBEI)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '付呗配置已保存',
|
||
'data': {
|
||
'channel': _serialize_channel(row),
|
||
'mini_pay_channel': get_club_mini_pay_channel(club_id),
|
||
},
|
||
})
|