"""俱乐部支付通道后台管理(付呗配置 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.admin_context 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), }, })