import io import os import re import time import random import secrets import hashlib import threading import urllib.parse from decimal import Decimal, InvalidOperation from django.conf import settings from django.db import transaction, connection from django.db.models import Q, F, Max, Prefetch from gvsdsdk.fluent import db, func, FQ from django.core.cache import cache from django.core.paginator import Paginator from django.utils import timezone from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status, permissions from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.parsers import JSONParser, MultiPartParser, FormParser from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image from utils.weixin_broadcast import WeixinBroadcastSender from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid # from utils.ip_security import * from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode from backend.utils import update_shangjia_daily from utils.chat_utils import subscribe_merchant_link_chat from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile from ..models import ( Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi, ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig, MiniappScriptScene, MiniappScriptAutoReply, ) from users.models import ( AdminProfile, UserDashou, UserBoss, UserShangjia, UserGuanshi, UserZuzhang ) from users.business_models import User from orders.models import CommissionRate, Order, MerchantOrderExt from orders.notice_tasks import dingdan_guangbo from rank.models import Chenghao, DingdanBiaoqian from products.models import ShangpinLeixing, Huiyuangoumai from ..serializers import PopupConfigSerializer import traceback import requests import logging logger = logging.getLogger(__name__) class DashouPaihangView(APIView): """ 排行榜接口 路径:/peizhi/dsph 方法:POST 认证:JWT Token认证 请求参数: - leixing: 1=今日收入,2=今月收入 - yonghu_leixing: 1=打手,2=管事,3=商家 返回数据: - code: 0=成功,其他=失败 - msg: 提示信息 - data: { xianshi: True/False, # 是否展示真实数据 paihang_list: [ # 排行榜列表 { uid: '用户ID', touxiang: '头像相对URL', nicheng: '用户昵称', jine: 收入金额, mingci: 排名 }, ... ] } """ # =============== 可配置参数 =============== # 每次查询返回的最大记录数(可修改) ZUI_DAO_CHA_XUN_SHU_LIANG = 50 # 最少需要多少人才能展示(可修改) ZUI_SHAO_REN_SHU_YAO_QIU = 1 # 第一名最低金额要求(单位:元)(可修改) DI_YI_MING_ZUI_DI_JIN_E = Decimal('10.00') # 管事第一名最低数量要求(可修改) DI_YI_MING_ZUI_DI_SHU_LIANG = 1 # =============== 可配置参数结束 =============== permission_classes = [IsAuthenticated] def post(self, request): try: # 获取请求参数 - 修复参数类型问题 leixing_str = request.data.get('leixing', '1') yonghu_leixing_str = request.data.get('yonghu_leixing', '1') # 转换为整数 try: leixing = int(leixing_str) except (ValueError, TypeError): leixing = 1 try: yonghu_leixing = int(yonghu_leixing_str) except (ValueError, TypeError): yonghu_leixing = 1 # 参数验证 if leixing not in [1, 2]: return Response({ 'code': 400, 'msg': '参数错误:leixing必须为1(今日)或2(今月)', 'data': {'xianshi': False} }) if yonghu_leixing not in [1, 2, 3]: return Response({ 'code': 400, 'msg': '参数错误:yonghu_leixing必须为1(打手)、2(管事)或3(商家)', 'data': {'xianshi': False} }) # 根据用户类型调用不同的查询方法 if yonghu_leixing == 1: # 打手排行榜 paihang_list, xianshi_flag = self.chaxun_dashou_paihang(leixing) elif yonghu_leixing == 2: # 管事排行榜 paihang_list, xianshi_flag = self.chaxun_guanshi_paihang(leixing) else: # 商家排行榜 paihang_list, xianshi_flag = self.chaxun_shangjia_paihang(leixing) # 返回结果 return Response({ 'code': 0, 'msg': '获取成功', 'data': { 'xianshi': xianshi_flag, 'paihang_list': paihang_list } }) except Exception as e: logger.error(f"排行榜查询失败: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '服务器内部错误', 'data': {'xianshi': False} }) def chaxun_dashou_paihang(self, leixing): """ 查询打手排行榜 :param leixing: 1=今日收入,2=今月收入 :return: (排行榜列表, 是否展示标志) """ # 确定查询的字段 if leixing == 1: # 今日收入 shouyi_ziduan = 'jinrishouyi' else: # 今月收入 shouyi_ziduan = 'jinyueshouyi' # 查询条件:收入大于0,打手账号状态正常(1=正常) # 注意:打手表中的zhanghaozhuangtai字段 chaxun_tiaojian = Q(**{f'{shouyi_ziduan}__gt': 0}) & Q(zhanghaozhuangtai=1) # 查询打手扩展表,并关联用户主表 dashou_list = UserDashou.query.select_related('user').filter( chaxun_tiaojian ).order_by(f'-{shouyi_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] # 检查是否满足展示条件 if len(dashou_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU: # 人数不足,不展示 return [], False # 检查第一名金额是否达到最低要求 if leixing == 1: di_yi_ming_jine = dashou_list[0].jinrishouyi else: di_yi_ming_jine = dashou_list[0].jinyueshouyi if di_yi_ming_jine < self.DI_YI_MING_ZUI_DI_JIN_E: # 第一名金额不足,不展示 return [], False # 构建返回数据 paihang_list = [] for index, dashou in enumerate(dashou_list): # 获取用户主表信息 user_main = dashou.user # 打手的昵称直接使用打手扩展表的nicheng字段 dashou_nicheng = dashou.nicheng or '未设置昵称' # 获取头像(相对URL) touxiang_url = user_main.Avatar or '' # 获取收入金额 if leixing == 1: shouyi_jine = dashou.jinrishouyi else: shouyi_jine = dashou.jinyueshouyi paihang_list.append({ 'uid': user_main.UserUID, 'touxiang': touxiang_url, 'nicheng': dashou_nicheng, 'jine': float(shouyi_jine), # 转换为float方便前端处理 'mingci': index + 1 }) return paihang_list, True def chaxun_guanshi_paihang(self, leixing): """ 查询管事排行榜 :param leixing: 1=今日,2=今月 :return: (排行榜列表, 是否展示标志) """ # 确定查询的字段 if leixing == 1: # 今日充值打手数量 chaxun_ziduan = 'jinrichongzhi' else: # 今月充值打手数量 chaxun_ziduan = 'jinyuechongzhi' # 查询条件:数量大于0,管事状态正常(状态1为正常) # 注意:管手表中的zhuangtai字段 chaxun_tiaojian = Q(**{f'{chaxun_ziduan}__gt': 0}) & Q(zhuangtai=1) # 查询管事扩展表,并关联用户主表 guanshi_list = UserGuanshi.query.select_related('user').filter( chaxun_tiaojian ).order_by(f'-{chaxun_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] # 检查是否满足展示条件 if len(guanshi_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU: # 人数不足,不展示 return [], False # 检查第一名数量是否达到最低要求 if leixing == 1: di_yi_ming_shuliang = guanshi_list[0].jinrichongzhi else: di_yi_ming_shuliang = guanshi_list[0].jinyuechongzhi if di_yi_ming_shuliang < self.DI_YI_MING_ZUI_DI_SHU_LIANG: # 第一名数量不足,不展示 return [], False # 批量预取老板扩展表昵称 _boss_user_ids = [g.user_id for g in guanshi_list] _boss_nicheng_map = { b.user_id: b.nickname for b in UserBoss.query.filter(user_id__in=_boss_user_ids).only('user_id', 'nickname') } # 构建返回数据 paihang_list = [] for index, guanshi in enumerate(guanshi_list): # 获取用户主表信息 user_main = guanshi.user # 管事的昵称需要查询老板扩展表 guanshi_nicheng = _boss_nicheng_map.get(guanshi.user_id, '未设置昵称') # 获取头像(相对URL) touxiang_url = user_main.Avatar or '' # 获取数量 if leixing == 1: chongzhi_shuliang = guanshi.jinrichongzhi else: chongzhi_shuliang = guanshi.jinyuechongzhi paihang_list.append({ 'uid': user_main.UserUID, 'touxiang': touxiang_url, 'nicheng': guanshi_nicheng, 'jine': float(chongzhi_shuliang), # 这里jine字段实际存储的是数量 'mingci': index + 1 }) return paihang_list, True def chaxun_shangjia_paihang(self, leixing): """ 查询商家排行榜 :param leixing: 1=今日流水,2=今月流水 :return: (排行榜列表, 是否展示标志) """ # 确定查询的字段 if leixing == 1: # 今日流水 liushui_ziduan = 'jinriliushui' else: # 今月流水 liushui_ziduan = 'jinyueliushui' # 查询条件:流水大于0,商家状态正常(状态1为正常) # 注意:商家表中的zhuangtai字段 chaxun_tiaojian = Q(**{f'{liushui_ziduan}__gt': 0}) & Q(zhuangtai=1) # 查询商家扩展表,并关联用户主表 shangjia_list = UserShangjia.query.select_related('user').filter( chaxun_tiaojian ).order_by(f'-{liushui_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] # 检查是否满足展示条件 if len(shangjia_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU: # 人数不足,不展示 return [], False # 检查第一名金额是否达到最低要求 if leixing == 1: di_yi_ming_liushui = shangjia_list[0].jinriliushui else: di_yi_ming_liushui = shangjia_list[0].jinyueliushui if di_yi_ming_liushui < self.DI_YI_MING_ZUI_DI_JIN_E: # 第一名流水不足,不展示 return [], False # 批量预取老板扩展表昵称 _boss_user_ids = [s.user_id for s in shangjia_list] _boss_nicheng_map = { b.user_id: b.nickname for b in UserBoss.query.filter(user_id__in=_boss_user_ids).only('user_id', 'nickname') } # 构建返回数据 paihang_list = [] for index, shangjia in enumerate(shangjia_list): # 获取用户主表信息 user_main = shangjia.user # 商家的昵称需要查询老板扩展表 shangjia_nicheng = _boss_nicheng_map.get(shangjia.user_id, '未设置昵称') # 获取头像(相对URL) touxiang_url = user_main.Avatar or '' # 获取流水金额 if leixing == 1: liushui_jine = shangjia.jinriliushui else: liushui_jine = shangjia.jinyueliushui paihang_list.append({ 'uid': user_main.UserUID, 'touxiang': touxiang_url, 'nicheng': shangjia_nicheng, 'jine': float(liushui_jine), 'mingci': index + 1 }) return paihang_list, True class GetAdminZhanghaoView(APIView): """ 获取管理员账号列表接口 路径:/peizhi/adzhhq 方法:POST 权限:JWT Token + 管理员验证 参数: - zhanghao: 当前登录的管理员账号(字符串) """ permission_classes = [IsAuthenticated] def post(self, request): try: # 🔥 1. 身份验证(100%按表模型) user = request.user # 获取当前用户的手机号(CharField,字符串类型) current_phone = getattr(user, 'Phone', '') if not current_phone: return Response({ 'code': 401, 'message': '用户手机号信息不完整', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 检查前端传递的账号是否匹配(都是字符串) frontend_zhanghao = request.data.get('zhanghao', '').strip() if frontend_zhanghao != current_phone: return Response({ 'code': 403, 'message': '账号不匹配,无权限访问', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 🔥 2. 验证是否为管理员(user_type是CharField,值是'admin'字符串) user_type = getattr(user, 'user_type', '') if user_type != 'admin': # 检查是否有管理员扩展表 if not hasattr(user, 'AdminProfile'): return Response({ 'code': 403, 'message': '无管理员权限', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 🔥 3. 查询管理员账号列表(全部按字符串查询) with connection.cursor() as cursor: # 🔥 查询所有管理员账号(user_type='admin' 字符串) # 🔥 phone和yonghuid都是CharField,xiugaiid也是CharField sql = """ SELECT um.id, um.phone, um.yonghuid, um.UserType, COALESCE(COUNT(xjl.id), 0) as jilushuliang FROM user_main um LEFT JOIN xiugaijilu xjl ON um.phone = xjl.xiugaiid WHERE um.UserType = 'admin' GROUP BY um.id, um.phone, um.yonghuid, um.UserType ORDER BY um.CreateTime DESC """ cursor.execute(sql) results = cursor.fetchall() # 🔥 4. 处理查询结果(全部保持字符串类型) zhanghao_list = [] for row in results: zhanghao_list.append({ #'id': row[0], 'id': str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串 'zhanghao':str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串 'yonghuid': str(row[2]) if row[2] else '', # 🔥 保持字符串 'user_type': str(row[3]) if row[3] else '', # 🔥 保持字符串 'jilushuliang': int(row[4]) # 只有这个是数字 }) return Response({ 'code': 0, 'message': '获取成功', 'data': zhanghao_list }, status=status.HTTP_200_OK) except Exception as e: print(f"获取管理员账号列表失败: {str(e)}") traceback.print_exc() return Response({ 'code': 500, 'message': '系统错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class GetXiugaiJiluView(APIView): """ 获取修改记录列表接口 路径:/peizhi/adhqxg 方法:POST 权限:JWT Token + 管理员验证 参数: - zhanghao: 当前登录的管理员账号(字符串) - xiugaizhe_zhanghaoid: 选中的管理员账号ID(字符串,对应phone或yonghuid) - page: 页码(整数) - pagesize: 每页条数(整数) """ permission_classes = [IsAuthenticated] def post(self, request): try: # 🔥 1. 身份验证(100%按表模型) user = request.user # 获取当前用户的手机号(CharField,字符串) current_phone = getattr(user, 'Phone', '') if not current_phone: return Response({ 'code': 401, 'message': '用户手机号信息不完整', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 检查前端传递的账号是否匹配(字符串比较) frontend_zhanghao = request.data.get('zhanghao', '') if not isinstance(frontend_zhanghao, str): frontend_zhanghao = str(frontend_zhanghao) frontend_zhanghao = frontend_zhanghao.strip() if frontend_zhanghao != current_phone: return Response({ 'code': 403, 'message': '账号不匹配,无权限访问', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 🔥 2. 验证是否为管理员(user_type='admin'字符串) user_type = getattr(user, 'user_type', '') if user_type != 'admin': if not hasattr(user, 'AdminProfile'): return Response({ 'code': 403, 'message': '无管理员权限', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 🔥 3. 获取参数(100%按实际类型) # 🔥 xiugaizhe_zhanghaoid 是字符串(对应xiugaiid字段,CharField) xiugaizhe_zhanghaoid_param = request.data.get('xiugaizhe_zhanghaoid', '') # 🔥 强制转为字符串(因为数据库是CharField) if xiugaizhe_zhanghaoid_param is None: xiugaizhe_zhanghaoid = '' else: # 无论如何都转为字符串 xiugaizhe_zhanghaoid = str(xiugaizhe_zhanghaoid_param).strip() # 🔥 分页参数转为整数 try: page = int(request.data.get('page', 1)) except: page = 1 try: pagesize = int(request.data.get('pagesize', 10)) except: pagesize = 10 # 🔥 4. 参数验证 if not xiugaizhe_zhanghaoid: return Response({ 'code': 400, 'message': '修改者账号ID不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if page < 1: page = 1 if pagesize < 1 or pagesize > 50: pagesize = 10 # 🔥 5. 高效查询(100%按表字段类型) with connection.cursor() as cursor: # 🔥 5.1 查询总记录数(xiugaiid是CharField,用字符串查询) count_sql = """ SELECT COUNT(*) FROM xiugaijilu WHERE xiugaiid = %s """ cursor.execute(count_sql, [xiugaizhe_zhanghaoid]) total_count = cursor.fetchone()[0] # 🔥 5.2 计算分页 offset = (page - 1) * pagesize # 🔥 5.3 查询当前页数据(使用参数化查询,防止SQL注入) data_sql = """ SELECT xjl.id, xjl.yonghuid, -- CharField xjl.xiugaiid, -- CharField xjl.leixing, -- IntegerField xjl.xiugaitijiao, -- DecimalField xjl.xiugaitijiaoq, -- DecimalField xjl.jifen, -- IntegerField xjl.yjifen, -- IntegerField xjl.yajin, -- DecimalField xjl.yyajin, -- DecimalField xjl.shangjiayue, -- DecimalField xjl.shangjiayueq, -- DecimalField xjl.guanshiyue, -- DecimalField xjl.guanshiyueq, -- DecimalField xjl.huiyuants, -- IntegerField xjl.huiyuan_id, -- CharField xjl.qitashuoming, -- TextField xjl.CreateTime, -- DateTimeField um.avatar -- CharField FROM xiugaijilu xjl LEFT JOIN user_main um ON xjl.yonghuid = um.yonghuid WHERE xjl.xiugaiid = %s ORDER BY xjl.CreateTime DESC LIMIT %s OFFSET %s """ cursor.execute(data_sql, [xiugaizhe_zhanghaoid, pagesize, offset]) records = cursor.fetchall() # 🔥 6. 处理查询结果(严格按照字段类型处理) # 预查询修改者账号信息(循环外执行一次,避免循环内重复查询同一 SQL) xiugaizhe_zhanghao = '' with connection.cursor() as cursor2: cursor2.execute( "SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1", [xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid] ) xiugaizhe_info = cursor2.fetchone() if xiugaizhe_info: xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \ xiugaizhe_info[1] else xiugaizhe_zhanghaoid jilu_list = [] for record in records: # 修改者账号信息已预查询,直接使用 # 获取会员名称(如果有) huiyuan_mingzi = '' if record[15]: # huiyuan_id是CharField with connection.cursor() as cursor3: cursor3.execute( "SELECT jieshao FROM huiyuan WHERE huiyuan_id = %s LIMIT 1", [str(record[15])] # 🔥 转为字符串查询 ) huiyuan_info = cursor3.fetchone() if huiyuan_info: huiyuan_mingzi = str(huiyuan_info[0]) if huiyuan_info[0] else '' # 🔥 严格按照前端字段名映射,保持正确类型 jilu_data = { # 基础字段(字符串) 'id': record[0], 'beixiugai_yonghuid': str(record[1]) if record[1] else '', # yonghuid 'xiugaizhe_id': str(record[2]) if record[2] else '', # xiugaiid 'xiugaizhe_zhanghao': xiugaizhe_zhanghao, # 字符串 'beixiugai_yonghuleixing': record[3], # leixing是整数 # 打手字段(Decimal转float,Integer转int) 'xiugaihou_dashoutixianjine': float(record[4]) if record[4] is not None else 0.00, 'xiugaiqian_dashoutixianjine': float(record[5]) if record[5] is not None else 0.00, 'xiugaihou_dashoujifen': int(record[6]) if record[6] is not None else 0, 'xiugaiqian_dashoujifen': int(record[7]) if record[7] is not None else 0, 'xiugaihou_dashouyajin': float(record[8]) if record[8] is not None else 0.00, 'xiugaiqian_dashouyajin': float(record[9]) if record[9] is not None else 0.00, # 商家字段 'xiugaihou_shangjiayue': float(record[10]) if record[10] is not None else 0.00, 'xiugaiqian_shangjiayue': float(record[11]) if record[11] is not None else 0.00, # 管事字段 'xiugaihou_guanshiyue': float(record[12]) if record[12] is not None else 0.00, 'xiugaiqian_guanshiyue': float(record[13]) if record[13] is not None else 0.00, # 会员字段 'tianjia_huiyuantianshu': int(record[14]) if record[14] is not None else 0, 'huiyuan_id': str(record[15]) if record[15] else '', # 字符串 'huiyuan_mingzi': huiyuan_mingzi, # 字符串 # 其他字段 'qita_shuoming': str(record[16]) if record[16] else '', # 字符串 'xiugai_time': record[17].strftime('%Y-%m-%d %H:%M:%S') if record[17] else None, 'touxiang': str(record[18]) if record[18] else '' # avatar是字符串 } jilu_list.append(jilu_data) # 🔥 7. 分页控制 queried_records = len(jilu_list) has_more = False if queried_records == pagesize: total_queried = (page - 1) * pagesize + queried_records has_more = total_queried < total_count return Response({ 'code': 0, 'message': '获取成功', 'data': { 'list': jilu_list, 'zongshuliang': total_count, 'dangqian_page': page, 'page_size': pagesize, 'has_more': has_more, 'shijichaxundediaoshuliang': queried_records } }, status=status.HTTP_200_OK) except ValueError as e: print(f"参数错误: {str(e)}") return Response({ 'code': 400, 'message': '参数错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print(f"获取修改记录失败: {str(e)}") traceback.print_exc() return Response({ 'code': 500, 'message': '系统错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)