"""users.views.kefu_base - auto-generated by split script.""" """users.views.kefu - auto-generated by split script.""" import os import sys import json import re import time import uuid import random import string import requests import hashlib import traceback import decimal import jwt import ipaddress import xmltodict from datetime import datetime, timedelta from decimal import Decimal import defusedxml.ElementTree as ET from django.conf import settings from django.db import models, transaction, IntegrityError, DatabaseError, connection from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch from django.core.cache import cache from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.exceptions import ObjectDoesNotExist from django.shortcuts import get_object_or_404 from django.utils import timezone from django.http import HttpResponse from django.views import View from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.hashers import make_password from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import permissions, status from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.parsers import MultiPartParser, FormParser, JSONParser from rest_framework.throttling import AnonRateThrottle from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.authentication import JWTAuthentication from gvsdsdk.fluent import db, func, FQ from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss from utils.money import yuan_to_fen from utils.fadan_utils import check_fadan_qiangdan_eligible from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode from users.fadan_fenhong_utils import process_fadan_fenhong from orders.utils import ( update_daily_payout, settle_shangjia_order_guanshi_fenhong ) from backend.utils import ( update_dashou_daily_by_action, update_guanshi_daily_by_action, update_shangjia_daily, update_zuzhang_daily_by_action, verify_kefu_permission ) from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan from ..models import ( UserBoss, UserDashou, UserShangjia, UserGuanshi, UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser, TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu, RankingRecord ) from users.business_models import User from ..tixian_shenhe_services import ( load_audit_meta_map, refund_balance, sync_audit_and_jilu_status, mark_transfer_success, release_collect_quota_for_record, close_audit_wechat_failed, query_wechat_transfer_bill, check_shijidaozhang_within_limit, check_dashou_trial_blocks_commission_withdraw, WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING, ) from ..tixian_shenhe_views import process_audit_collect from orders.models import ( Order, PlatformOrderExt, MerchantOrderExt, CommissionRate, PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage, Penalty, PenaltyAppealImage ) from products.models import ( ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu ) from config.models import Qunpeizhi from backend.models import MerchantDailyStats from rank.models import ( KaohePayTemp, Chenghao, ShenheJilu, KaoheCishuFeiyong, YonghuChenghao ) from users.models import UserShenheguan from products.utils import update_shangpin_daily_stat from shop.utils import update_dianpu_daily_stat from rank.utils import create_shenhe_jilu_from_temp import logging logger = logging.getLogger(__name__) class KefuLoginView(APIView): """ 客服/管理员登录接口(支持手机号重复) 请求:POST /yonghu/kefujinru 请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"} 返回: code=0 成功,data包含token,nicheng,yonghuid code=1 失败(msg区分:用户不存在/密码错误/二级密码错误) code=4 账号被封禁 管理员(IsSuperuser 或 UserType='admin')无需 erjimima 和 KefuProfile。 """ throttle_classes = [AnonRateThrottle] # 限制匿名请求频率 permission_classes = [AllowAny] # 任何人都可访问 def post(self, request): # 1. 获取参数 phone = request.data.get('phone', '').strip() password = request.data.get('password', '') erjimima = request.data.get('erjimima', '') if not phone or not password: return Response({ 'code': 1, 'msg': '请填写手机号和密码', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 2. 获取客户端真实IP client_ip = self.get_client_ip(request) # 3. 查询该手机号下所有账号(客服 + 管理员) # 先查客服(有 KefuProfile),再查管理员(IsSuperuser 或 UserType='admin') 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: logger.warning(f"登录失败,用户不存在: {phone}, IP: {client_ip}") return Response({ 'code': 1, 'msg': '用户不存在', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 4. 遍历所有匹配的账号,验证密码和二级密码 target_user = None target_kefu = None password_matched = False for user in user_mains: # 验证主密码(bcrypt 哈希验证) 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: # 只有被禁用的账号匹配,返回封禁提示 logger.warning(f"登录失败,客服账号已禁用: {phone}, IP: {client_ip}") return Response({ 'code': 4, 'msg': '账号已被封禁', 'data': None }, status=status.HTTP_403_FORBIDDEN) elif password_matched: # 密码正确但二级密码错误 logger.warning(f"登录失败,二级密码错误: {phone}, IP: {client_ip}") return Response({ 'code': 1, 'msg': '二级密码错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) else: # 密码错误 logger.warning(f"登录失败,密码错误: {phone}, IP: {client_ip}") return Response({ 'code': 1, 'msg': '密码错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 5. 更新IP和最后登录时间 target_user.IP = client_ip target_user.UserLastLoginDate = timezone.now() target_user.save(update_fields=['IP', 'UserLastLoginDate']) # 6. 生成JWT token refresh = RefreshToken.for_user(target_user) token = str(refresh.access_token) # 7. 返回成功数据(管理员无 KefuProfile,nicheng 用 UserName 或 Phone 兜底) nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone) return Response({ 'code': 0, 'msg': '登录成功', 'data': { 'token': token, 'nicheng': nicheng, 'yonghuid': target_user.UserUID, } }) def get_client_ip(self, request): """ 获取客户端真实IP """ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: ip = x_forwarded_for.split(',')[0].strip() if ip: return ip return request.META.get('REMOTE_ADDR', '0.0.0.0') class KefuStatsView(APIView): """ 客服统计数据接口 请求:POST /yonghu/kfjrhqzl 请求体:{"phone": "13800138000"} 认证:必须携带有效的 JWT token(放在 Authorization 头) 返回: code=0 成功,data 包含 jinrichuli, jinyuechuli, zongchuli 验证失败统一返回 401,不透露具体原因 """ permission_classes = [IsAuthenticated] # 必须登录 def post(self, request): # 1. 获取请求中的 phone phone = request.data.get('phone', '').strip() if not phone: # 缺少参数,但按约定返回 401(可以改为 400,但为了统一,用401) logger.warning(f"统计接口缺少 phone 参数,用户: {request.user.UserUID}") return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 获取当前登录用户(从 JWT 中解析) current_user = request.user # 3. 验证前端传来的 phone 是否与当前用户的 phone 一致 if current_user.Phone != phone: # 不一致,可能 token 被冒用或参数错误 logger.warning(f"统计接口 phone 不匹配: 请求phone={phone}, 用户phone={current_user.Phone}") return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 4. 验证是否为后台客服账号 from jituan.services.admin_context import is_kefu_backend_account, is_system_super_admin if not is_kefu_backend_account(current_user): logger.warning(f"统计接口用户类型非客服: {current_user.UserUID}") return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 5. 尝试获取客服扩展表数据(管理员跳过) is_admin = is_system_super_admin(current_user) if is_admin: return Response({ 'code': 0, 'data': { 'jinrichuli': 0, 'jinyuechuli': 0, 'zongchuli': 0, } }, status=status.HTTP_200_OK) try: kefu_profile = current_user.KefuProfile # OneToOneField 反向关系 except UserKefu.DoesNotExist: logger.warning(f"统计接口客服扩展表不存在: {current_user.UserUID}") return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 6. 返回统计数据(字段名与前端约定一致) return Response({ 'code': 0, 'data': { 'jinrichuli': kefu_profile.jinrichuli, 'jinyuechuli': kefu_profile.jinyuechuli, 'zongchuli': kefu_profile.zongchuli, } }, status=status.HTTP_200_OK) class KefuGetOrderTypesView(APIView): """ 客服获取订单类型列表接口(平台订单用) 请求:POST /yonghu/kfhqptddlx 参数:{"phone": "13800138000"} 认证:JWT 返回:code=0 + 类型列表 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 phone = request.data.get('phone', '').strip() if not phone: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) current_user = request.user # 2. 验证手机号一致性 if getattr(current_user, 'Phone', '') != phone: logger.warning(f"手机号不匹配: 请求phone={phone}, 用户phone={current_user.Phone}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 3. 验证用户类型及客服状态 if current_user.UserType not in ('kefu', 'admin'): logger.warning(f"用户类型非客服: {current_user.UserUID}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 管理员跳过客服扩展表检查 is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser if not is_admin: try: kefu_profile = current_user.KefuProfile except ObjectDoesNotExist: logger.warning(f"客服扩展表不存在: {current_user.UserUID}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if kefu_profile.zhuangtai != 1: logger.warning(f"客服账号已被禁用: {current_user.UserUID}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 4. 查询订单类型(商品类型) try: # 只查询需要的字段,减少数据传输 types_qs = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url') type_list = [ { 'id': t.id, 'biaoti': t.jieshao or '', # 假设商品类型的标题字段为 jieshao 'tupian_url': t.tupian_url or '' } for t in types_qs ] return Response({'code': 0, 'data': type_list}) except Exception as e: logger.error(f"查询商品类型表失败: {str(e)}") # 表不存在或查询失败,返回空列表 return Response({'code': 0, 'data': []})