feat(jituan): 集团多俱乐部改造 — club 隔离、财务/提现/分红/展示配置
This commit is contained in:
477
jituan/views.py
Normal file
477
jituan/views.py
Normal file
@@ -0,0 +1,477 @@
|
||||
"""集团平台 API 视图。"""
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from jituan.models import Club
|
||||
from jituan.services.admin_context import build_admin_club_context
|
||||
from jituan.services.wechat_login import login_or_register_by_club
|
||||
from jituan.services.caiwu_stats import build_caiwu_payload
|
||||
from jituan.services.szxx_stats import build_szxx_payload
|
||||
from jituan.services.admin_audit import list_admin_audit_logs
|
||||
from jituan.services.user_stats import build_user_summary
|
||||
from jituan.services.finance_detail_stats import (
|
||||
build_chongzhi_finance_payload,
|
||||
build_huiyuan_bankuai_payload,
|
||||
build_huiyuan_stats_payload,
|
||||
build_order_finance_payload,
|
||||
build_order_types_payload,
|
||||
)
|
||||
from backend.utils import verify_kefu_permission
|
||||
from users.business_models import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _client_ip(request):
|
||||
xff = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if xff:
|
||||
return xff.split(',')[0].strip()
|
||||
return request.META.get('REMOTE_ADDR', '')
|
||||
|
||||
|
||||
class ClubWechatLoginView(APIView):
|
||||
"""
|
||||
多俱乐部微信小程序登录(新接口,旧 /yonghu/wechatlogin 不变)。
|
||||
POST /jituan/auth/wechat-login
|
||||
body: { code, club_id?, app_id? }
|
||||
"""
|
||||
throttle_classes = [AnonRateThrottle]
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
code = (request.data.get('code') or '').strip()
|
||||
club_id = (request.data.get('club_id') or '').strip() or None
|
||||
app_id = (request.data.get('app_id') or '').strip() or None
|
||||
|
||||
if not code:
|
||||
return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
data, err = login_or_register_by_club(
|
||||
code, club_id=club_id, app_id=app_id, client_ip=_client_ip(request),
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 2, 'msg': f'微信登录失败: {err}', 'data': None},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({'code': 0, 'msg': '登录成功', 'data': data})
|
||||
except Exception as e:
|
||||
logger.exception('ClubWechatLoginView 异常')
|
||||
return Response({'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
class ClubKefuLoginView(APIView):
|
||||
"""
|
||||
多俱乐部客服/集团后台登录(新接口,旧 /yonghu/kefujinru 不变)。
|
||||
POST /jituan/auth/kefu-login
|
||||
body: { phone, password, erjimima }
|
||||
"""
|
||||
throttle_classes = [AnonRateThrottle]
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
phone = (request.data.get('phone') or '').strip()
|
||||
password = request.data.get('password') or ''
|
||||
erjimima = request.data.get('erjimima') or ''
|
||||
|
||||
if not phone or not password:
|
||||
return Response({'code': 1, 'msg': '请填写手机号和密码', 'data': None},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
user_mains = list(
|
||||
User.query.filter(Phone=phone, KefuProfile__isnull=False).select_related('KefuProfile')
|
||||
)
|
||||
if not user_mains:
|
||||
admin_candidates = User.query.filter(Phone=phone)
|
||||
for admin_user in admin_candidates:
|
||||
if admin_user.IsSuperuser or admin_user.UserType == 'admin':
|
||||
user_mains = [admin_user]
|
||||
break
|
||||
|
||||
if not user_mains:
|
||||
return Response({'code': 1, 'msg': '用户不存在', 'data': None},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
target_user = None
|
||||
target_kefu = None
|
||||
password_matched = False
|
||||
|
||||
for user in user_mains:
|
||||
if not user.CheckPassword(password):
|
||||
continue
|
||||
password_matched = True
|
||||
is_admin = user.IsSuperuser or user.UserType == 'admin'
|
||||
if is_admin:
|
||||
target_user = user
|
||||
target_kefu = None
|
||||
break
|
||||
kefu = user.KefuProfile
|
||||
stored_erji = kefu.erjimima or ''
|
||||
if stored_erji.startswith(('$2b$', '$2a$')) and len(stored_erji) == 60:
|
||||
import bcrypt as _bcrypt
|
||||
if not _bcrypt.checkpw(erjimima.encode('utf-8'), stored_erji.encode('utf-8')):
|
||||
continue
|
||||
elif stored_erji != erjimima:
|
||||
continue
|
||||
if kefu.zhuangtai != 1:
|
||||
if target_user is None:
|
||||
target_user = user
|
||||
target_kefu = kefu
|
||||
continue
|
||||
target_user = user
|
||||
target_kefu = kefu
|
||||
break
|
||||
else:
|
||||
if target_user and target_kefu:
|
||||
return Response({'code': 4, 'msg': '账号已被封禁', 'data': None},
|
||||
status=status.HTTP_403_FORBIDDEN)
|
||||
if password_matched:
|
||||
return Response({'code': 1, 'msg': '二级密码错误', 'data': None},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({'code': 1, 'msg': '密码错误', 'data': None},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
target_user.IP = _client_ip(request)
|
||||
target_user.UserLastLoginDate = timezone.now()
|
||||
target_user.save(update_fields=['IP', 'UserLastLoginDate'])
|
||||
|
||||
refresh = RefreshToken.for_user(target_user)
|
||||
token = str(refresh.access_token)
|
||||
nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone)
|
||||
club_context = build_admin_club_context(target_user)
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '登录成功',
|
||||
'data': {
|
||||
'token': token,
|
||||
'nicheng': nicheng,
|
||||
'yonghuid': target_user.UserUID,
|
||||
'club_context': club_context,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
class ClubListView(APIView):
|
||||
"""GET /jituan/club/list — 启用中的俱乐部列表(登录前后均可读)。"""
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
clubs = Club.query.filter(status=1).order_by('sort_order', 'club_id')
|
||||
data = [
|
||||
{
|
||||
'club_id': c.club_id,
|
||||
'name': c.name,
|
||||
'wx_appid': c.wx_appid,
|
||||
'h5_domain': c.h5_domain,
|
||||
}
|
||||
for c in clubs
|
||||
]
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
|
||||
|
||||
class ClubMeContextView(APIView):
|
||||
"""GET /jituan/auth/me-context — 已登录客服刷新俱乐部上下文。"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
user = request.user
|
||||
if user.UserType not in ('kefu', 'admin') and not user.IsSuperuser:
|
||||
return Response({'code': 403, 'msg': '非后台账号', 'data': None}, status=403)
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'ok',
|
||||
'data': build_admin_club_context(user),
|
||||
})
|
||||
|
||||
|
||||
class ClubCaiwuView(APIView):
|
||||
"""
|
||||
俱乐部维度财务(新接口)。
|
||||
旧 POST /houtai/caiwu 完全不变;kefu 切换俱乐部后应调本接口。
|
||||
Header: X-Club-Id, 集团汇总 X-Club-Scope: all
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'caiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
|
||||
try:
|
||||
data = build_caiwu_payload(request)
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
except Exception:
|
||||
logger.exception('ClubCaiwuView 异常')
|
||||
return Response({'code': 99, 'msg': '统计失败', 'data': None}, status=500)
|
||||
|
||||
|
||||
class ClubSzxxView(APIView):
|
||||
"""
|
||||
俱乐部维度每日收支详细统计(新接口)。
|
||||
旧 POST /houtai/szxx 不变;参数与旧接口一致。
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'caiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
|
||||
|
||||
granularity = request.data.get('granularity', 'day')
|
||||
year = request.data.get('year')
|
||||
month = request.data.get('month')
|
||||
summary_date = request.data.get('summary_date')
|
||||
|
||||
try:
|
||||
if year is not None:
|
||||
year = int(year)
|
||||
if month is not None:
|
||||
month = int(month)
|
||||
except (ValueError, TypeError):
|
||||
return Response({'code': 1, 'msg': '年份或月份格式错误'}, status=400)
|
||||
|
||||
try:
|
||||
data, err = build_szxx_payload(request, granularity, year, month, summary_date)
|
||||
if err:
|
||||
return Response({'code': 1, 'msg': err}, status=400)
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
except Exception:
|
||||
logger.exception('ClubSzxxView 异常')
|
||||
return Response({'code': 99, 'msg': '统计失败'}, status=500)
|
||||
|
||||
|
||||
class ClubAuditLogListView(APIView):
|
||||
"""
|
||||
后台审计日志(admin_audit_log)。
|
||||
POST /jituan/houtai/audit-log
|
||||
权限:000001 或 czrz666(与操作日志一致)
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = (request.data.get('username') or request.data.get('phone') or '').strip()
|
||||
if not username:
|
||||
return Response({'code': 401, 'msg': '缺少username'})
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, username)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if '000001' not in permissions and 'czrz666' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限查看审计日志'})
|
||||
|
||||
try:
|
||||
page = int(request.data.get('page', 1))
|
||||
except (ValueError, TypeError):
|
||||
page = 1
|
||||
try:
|
||||
page_size = int(request.data.get('page_size', 20))
|
||||
except (ValueError, TypeError):
|
||||
page_size = 20
|
||||
|
||||
data = list_admin_audit_logs(
|
||||
request,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
operator_yonghuid=request.data.get('operator_yonghuid'),
|
||||
target_yonghuid=request.data.get('target_yonghuid'),
|
||||
action=request.data.get('action'),
|
||||
keyword=request.data.get('keyword'),
|
||||
)
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
|
||||
|
||||
def _parse_year_month(data):
|
||||
year = data.get('year')
|
||||
month = data.get('month')
|
||||
try:
|
||||
if year is not None:
|
||||
year = int(year)
|
||||
if month is not None:
|
||||
month = int(month)
|
||||
except (ValueError, TypeError):
|
||||
return None, None, Response({'code': 1, 'msg': '年份或月份格式错误'}, status=400)
|
||||
return year, month, None
|
||||
|
||||
|
||||
class ClubOrderTypesView(APIView):
|
||||
"""POST /jituan/houtai/cwddhqlx — 订单财务商品类型筛选。"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'dingdancaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': build_order_types_payload()})
|
||||
|
||||
|
||||
class ClubOrderFinanceView(APIView):
|
||||
"""POST /jituan/houtai/cwhqjtddsj — 订单财务详细统计(按俱乐部)。"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'dingdancaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
|
||||
|
||||
year, month, err_resp = _parse_year_month(request.data)
|
||||
if err_resp:
|
||||
return err_resp
|
||||
try:
|
||||
data, err = build_order_finance_payload(
|
||||
request,
|
||||
request.data.get('granularity', 'day'),
|
||||
year,
|
||||
month,
|
||||
request.data.get('type_id', 0),
|
||||
request.data.get('order_source', 0),
|
||||
request.data.get('summary_date'),
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 1, 'msg': err}, status=400)
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
except Exception:
|
||||
logger.exception('ClubOrderFinanceView 异常')
|
||||
return Response({'code': 99, 'msg': '统计失败'}, status=500)
|
||||
|
||||
|
||||
class ClubChongzhiFinanceView(APIView):
|
||||
"""POST /jituan/houtai/cwqtczhq — 其他充值/罚款财务统计(按俱乐部)。"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'qtczcaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
|
||||
|
||||
year, month, err_resp = _parse_year_month(request.data)
|
||||
if err_resp:
|
||||
return err_resp
|
||||
try:
|
||||
data, err = build_chongzhi_finance_payload(
|
||||
request,
|
||||
request.data.get('granularity', 'day'),
|
||||
year,
|
||||
month,
|
||||
request.data.get('types', [2, 3, 4, 5, 6]),
|
||||
request.data.get('summary_date'),
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 1, 'msg': err}, status=400)
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
except Exception:
|
||||
logger.exception('ClubChongzhiFinanceView 异常')
|
||||
return Response({'code': 99, 'msg': '统计失败'}, status=500)
|
||||
|
||||
|
||||
class ClubHuiyuanBankuaiView(APIView):
|
||||
"""POST /jituan/houtai/cwhybkhq — 板块及会员列表(附带俱乐部售价)。"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'huiyuancaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
|
||||
payload = build_huiyuan_bankuai_payload(request)
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'ok',
|
||||
'data': payload['bankuai_list'],
|
||||
'club_id': payload['club_id'],
|
||||
'scope': payload['scope'],
|
||||
})
|
||||
|
||||
|
||||
class ClubHuiyuanStatsView(APIView):
|
||||
"""POST /jituan/houtai/hybkjtsj — 会员财务详细统计(按俱乐部)。"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'huiyuancaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
|
||||
|
||||
year, month, err_resp = _parse_year_month(request.data)
|
||||
if err_resp:
|
||||
return err_resp
|
||||
try:
|
||||
data, err = build_huiyuan_stats_payload(
|
||||
request,
|
||||
request.data.get('huiyuan_id'),
|
||||
request.data.get('granularity', 'day'),
|
||||
year,
|
||||
month,
|
||||
request.data.get('include_guanshi', False),
|
||||
request.data.get('include_zuzhang', False),
|
||||
request.data.get('summary_date'),
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 1, 'msg': err}, status=400)
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
except Exception:
|
||||
logger.exception('ClubHuiyuanStatsView 异常')
|
||||
return Response({'code': 99, 'msg': '统计失败'}, status=500)
|
||||
|
||||
|
||||
class ClubUserSummaryView(APIView):
|
||||
"""
|
||||
用户管理汇总(打手/管事/商家/组长数量)。
|
||||
集团视图 X-Club-Scope: all 时额外返回 by_club 各公司拆分。
|
||||
POST /jituan/houtai/user-summary
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
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
|
||||
try:
|
||||
data = build_user_summary(request)
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
except Exception:
|
||||
logger.exception('ClubUserSummaryView 异常')
|
||||
return Response({'code': 99, 'msg': '统计失败'}, status=500)
|
||||
|
||||
|
||||
class ClubDashouRegisterView(APIView):
|
||||
"""
|
||||
打手邀请码注册(带俱乐部校验)。
|
||||
POST /jituan/auth/dashou-register body: { inviteCode }
|
||||
与 /yonghu/dashouzhuce 逻辑一致,推荐新小程序使用。
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
from users.views import DashouZhuceView
|
||||
return DashouZhuceView().post(request)
|
||||
Reference in New Issue
Block a user