Files
Django/config/views/admin_stats.py
2026-07-04 19:29:53 +08:00

719 lines
27 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.
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
# 构建返回数据
paihang_list = []
for index, guanshi in enumerate(guanshi_list):
# 获取用户主表信息
user_main = guanshi.user
# 管事的昵称需要查询老板扩展表
boss_profile = UserBoss.query.filter(user=user_main).first()
guanshi_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
# 获取头像相对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
# 构建返回数据
paihang_list = []
for index, shangjia in enumerate(shangjia_list):
# 获取用户主表信息
user_main = shangjia.user
# 商家的昵称需要查询老板扩展表
boss_profile = UserBoss.query.filter(user=user_main).first()
shangjia_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
# 获取头像相对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都是CharFieldxiugaiid也是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. 处理查询结果(严格按照字段类型处理)
jilu_list = []
for record in records:
# 获取修改者账号信息(都是字符串)
xiugaizhe_zhanghao = ''
with connection.cursor() as cursor2:
# 🔥 phone和yonghuid都是CharField用字符串查询
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
# 获取会员名称(如果有)
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转floatInteger转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)