Files
Django/jituan/views_payment_channel.py
XingQue 5762f00d40 feat: 分俱乐部付呗收款通道,默认仍走微信直连
小程序前端仍传 wechat;后台可切换 wechat/fubei,支持配置多套付呗并退款按原通道路由。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 20:02:24 +08:00

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