"""集团平台 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)