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 .models import ( Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi, ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig ) 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 GetDynamicConfigView(APIView): permission_classes = [AllowAny] parser_classes = [JSONParser] def post(self, request): try: from jituan.services.display_config import get_tupianpeizhi_url from jituan.services.miniapp_assets import build_miniapp_assets_payload cos_oss_url = getattr(settings, 'COS_DOMAIN', '') if cos_oss_url and not cos_oss_url.endswith('/'): cos_oss_url += '/' cos_bucket = getattr(settings, 'COS_BUCKET', '') cos_region = getattr(settings, 'COS_REGION', 'ap-shanghai') goeasy_host = 'hangzhou.goeasy.io' goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '') kefu_link = getattr(settings, 'KEFU_LINK', '') kefu_enterprise_id = getattr(settings, 'KEFU_ENTERPRISE_ID', '') rule_img = get_tupianpeizhi_url(request, 1) avatar_img = get_tupianpeizhi_url(request, 2) morentouxiang = avatar_img or 'beijing/morentouxiang.jpg' dashouguize = rule_img or 'a_long/dashouguize.jpg' # 构造返回数据 data = { "code": 0, "data": { "cos": { "bucket": cos_bucket, "region": cos_region, "ossImageUrl": cos_oss_url, "uploadPathPrefix": "order/" }, "goEasy": { "host": goeasy_host, "appkey": goeasy_appkey }, "otherConfig": { "morentouxiang": morentouxiang, "dashouguize": dashouguize }, "kefu": { "link": kefu_link, "enterpriseId": kefu_enterprise_id }, "miniappAssets": build_miniapp_assets_payload(request), } } return Response(data) except Exception as e: logger.error(f"获取动态配置失败: {str(e)}", exc_info=True) return Response({"code": 500, "msg": "服务器内部错误"}, status=500) class ShangpinGonggaoView(APIView): """ 商品公告和轮播图接口 获取类型为1的公告和轮播图 """ # 应用节流(限制频率) throttle_classes = [AnonRateThrottle] # 新增:允许所有用户访问(不需要认证) permission_classes = [AllowAny] def post(self, request): """ 处理POST请求,返回公告和轮播图数据 请求: POST /api/peizhi/shangpingonggao/ 返回: { "shangpingonggao": "公告内容字符串", "shangpinlunbo": ["轮播图URL1", "轮播图URL2"] } """ try: logger.info("收到商品公告和轮播图请求") from jituan.services.display_config import get_gonggao_content, get_lunbo_urls, normalize_page_key page_key = normalize_page_key(request.data.get('page_key'), image_type=1) gonggao_content = get_gonggao_content(request, notice_type=1, page_key=page_key) lunbo_urls = get_lunbo_urls(request, page_key=page_key, image_type=1) response_data = { "shangpingonggao": gonggao_content, "shangpinlunbo": lunbo_urls, "page_key": page_key, } logger.info(f"返回数据:公告长度{len(gonggao_content)},轮播图数量{len(lunbo_urls)} page_key={page_key}") # 4. 返回响应 return Response(response_data, status=status.HTTP_200_OK) except Exception as e: # 记录异常 logger.error(f"获取商品公告和轮播图时发生错误:{str(e)}") # 返回错误响应 error_data = { "error": "服务器内部错误", "detail": "获取数据失败,请稍后重试" } return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdminConfigQueryView(APIView): """ 管理员获取系统配置接口 URL: /peizhi/adpzhq 方法: POST 权限: JWT Token认证 + 管理员权限 参数: zhanghao (管理员账号) """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: # 获取前端传递的账号 zhanghao = request.data.get('zhanghao') if not zhanghao: return Response({ 'code': 1, 'msg': '账号不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 获取当前用户(User实例) user_main = request.user # 验证账号:比较user_main的phone与前端传递的zhanghao if user_main.Phone != zhanghao: return Response({ 'code': 1, 'msg': '账号不匹配', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 验证用户类型是否为管理员 if user_main.UserType != 'admin': return Response({ 'code': 1, 'msg': '无权限', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 验证管理员扩展表是否存在 try: admin_profile = user_main.AdminProfile except AdminProfile.DoesNotExist: return Response({ 'code': 1, 'msg': '无权限', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 查询图片配置 rule_image = Tupianpeizhi.query.filter(ImageType=1).first() default_avatar = Tupianpeizhi.query.filter(ImageType=2).first() # 查询群配置 qun_configs = Qunpeizhi.query.all() qq_groups = [] for config in qun_configs: qq_groups.append({ 'peizhiid': config.GroupType, 'neirong': config.GroupContent if config.GroupContent else '', 'qunid': config.GroupID if config.GroupID else '', 'jieshao': config.Description if config.Description else '' }) # 查询利率配置 platform_rate_obj = CommissionRate.query.filter(Platform='1').first() shop_rate_obj = CommissionRate.query.filter(Platform='3').first() # 🔴【新增】打手提现抽成利率(字符5) dashou_withdraw_obj = CommissionRate.query.filter(Platform='5').first() # 🔴【新增】管事提现抽成利率(字符6) guanshi_withdraw_obj = CommissionRate.query.filter(Platform='6').first() # 构建返回数据 data = { 'images': { 'rule_image': rule_image.ImageURL if rule_image and rule_image.ImageURL else '', 'default_avatar': default_avatar.ImageURL if default_avatar and default_avatar.ImageURL else '' }, 'qq_groups': qq_groups, 'rates': { 'platform_rate': float(platform_rate_obj.Rate) if platform_rate_obj and platform_rate_obj.Rate else 0.0, 'shop_rate': float(shop_rate_obj.Rate) if shop_rate_obj and shop_rate_obj.Rate else 0.0, # 🔴【新增】打手提现抽成利率 'dashou_withdraw_rate': float( dashou_withdraw_obj.Rate) if dashou_withdraw_obj and dashou_withdraw_obj.Rate else 0.0, # 🔴【新增】管事提现抽成利率 'guanshi_withdraw_rate': float( guanshi_withdraw_obj.Rate) if guanshi_withdraw_obj and guanshi_withdraw_obj.Rate else 0.0 } } return Response({ 'code': 0, 'msg': 'success', 'data': data }) except Exception as e: return Response({ 'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdminUploadImageView(APIView): """ 管理员上传配置图片接口 URL: /peizhi/adxgtp 方法: POST 权限: JWT Token认证 + 管理员权限 参数: zhanghao, leixing (1:打手规则图片, 2:默认头像图片), image (文件) """ permission_classes = [permissions.IsAuthenticated] parser_classes = (MultiPartParser, FormParser) def post(self, request): try: zhanghao = request.data.get('zhanghao') leixing = request.data.get('leixing') image_file = request.FILES.get('image') if not zhanghao: return Response({ 'code': 1, 'msg': '账号不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not leixing: return Response({ 'code': 2, 'msg': '图片类型不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not image_file: return Response({ 'code': 3, 'msg': '请选择图片文件', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证用户权限 user_main = request.user if user_main.Phone != zhanghao: return Response({ 'code': 1, 'msg': '账号不匹配', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) if user_main.UserType != 'admin': return Response({ 'code': 1, 'msg': '无权限', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) try: admin_profile = user_main.AdminProfile except AdminProfile.DoesNotExist: return Response({ 'code': 1, 'msg': '无权限', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 验证图片类型 if leixing not in ['1', '2']: return Response({ 'code': 4, 'msg': '图片类型错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证图片文件 is_valid, error_msg = validate_image(image_file) if not is_valid: return Response({ 'code': 5, 'msg': f'图片验证失败: {error_msg}', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 根据类型确定固定相对URL和文件名 if leixing == '1': # 打手规则图片 fixed_relative_url = 'a_long/dashouguize.jpg' oss_file_name = 'dashouguize.jpg' else: # 默认头像图片 fixed_relative_url = 'a_long/morentouxiang.jpg' oss_file_name = 'morentouxiang.jpg' # 构建OSS文件路径 oss_file_path = fixed_relative_url # a_long/dashouguize.jpg 或 a_long/morentouxiang.jpg # 上传到OSS(覆盖原有文件) image_file.seek(0) # 重置文件指针 full_url = upload_to_oss(image_file, oss_file_path) if not full_url: return Response({ 'code': 6, 'msg': '图片上传到存储桶失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 更新数据库 with transaction.atomic(): # 获取或创建图片配置记录 tupian_peizhi, created = Tupianpeizhi.query.get_or_create(ImageType=int(leixing)) tupian_peizhi.ImageURL = fixed_relative_url tupian_peizhi.save() return Response({ 'code': 0, 'msg': '图片上传成功', 'data': { 'image_url': fixed_relative_url } }) except Exception as e: return Response({ 'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdminUpdateConfigView(APIView): """ 管理员更新系统配置接口(QQ群、利率)- 已修复 URL: /peizhi/adupdate 方法: POST 权限: JWT Token认证 + 管理员权限 参数: - zhanghao: 管理员账号 - qq_groups: [{"peizhiid": 1, "neirong": "", "qunid": "", "jieshao": ""}, ...] (可选) - rates: {"platform_rate": 0.1, "shop_rate": 0.2} (可选) """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: zhanghao = request.data.get('zhanghao') if not zhanghao: return Response({ 'code': 1, 'msg': '账号不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证用户权限 user_main = request.user if user_main.Phone != zhanghao: return Response({ 'code': 1, 'msg': '账号不匹配', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) if user_main.UserType != 'admin': return Response({ 'code': 1, 'msg': '无权限', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) try: admin_profile = user_main.AdminProfile except AdminProfile.DoesNotExist: return Response({ 'code': 1, 'msg': '无权限', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) response_data = {} # 🔴 修复:更新QQ群配置 - 确保根据peizhiid正确更新对应的记录 qq_groups = request.data.get('qq_groups') if qq_groups and isinstance(qq_groups, list): updated_groups = [] for group_data in qq_groups: peizhiid = group_data.get('peizhiid') if not peizhiid: continue # 🔴 修复:使用filter()确保查询到正确的记录 qun_configs = Qunpeizhi.query.filter(GroupType=peizhiid) if qun_configs.exists(): # 如果存在多个相同peizhiid的记录,取第一个(应该只有一个) qun_config = qun_configs.first() # 更新字段 neirong = group_data.get('neirong') qunid = group_data.get('qunid') jieshao = group_data.get('jieshao') if neirong is not None: qun_config.GroupContent = neirong or '' if qunid is not None: qun_config.GroupID = qunid or '' if jieshao is not None: qun_config.Description = jieshao or '' qun_config.save() else: # 如果不存在,创建新的(使用提供的peizhiid) qun_config = Qunpeizhi.query.create( GroupType=peizhiid, GroupContent=group_data.get('neirong', '') or '', GroupID=group_data.get('qunid', '') or '', Description=group_data.get('jieshao', '') or '' ) updated_groups.append({ 'peizhiid': qun_config.GroupType, 'neirong': qun_config.GroupContent, 'qunid': qun_config.GroupID, 'jieshao': qun_config.Description }) if updated_groups: response_data['qq_groups'] = updated_groups # 🔴 修复:更新利率配置 - 确保更新现有记录而不是创建新记录 rates = request.data.get('rates') if rates and isinstance(rates, dict): updated_rates = {} # 平台发单利率(类型1,fadanpingtai='1') platform_rate = rates.get('platform_rate') if platform_rate is not None: try: platform_rate_float = float(platform_rate) if 0 <= platform_rate_float <= 1: # 🔴 修复:查询现有记录,如果不存在则创建 platform_rate_obj = CommissionRate.query.filter(Platform='1').first() if platform_rate_obj: # 更新现有记录 platform_rate_obj.Rate = platform_rate_float platform_rate_obj.save() else: # 创建新记录 platform_rate_obj = CommissionRate.query.create( Rate=platform_rate_float, Platform='1' ) updated_rates['platform_rate'] = platform_rate_float else: return Response({ 'code': 3, 'msg': '平台发单利率必须在0-1之间', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except ValueError: return Response({ 'code': 4, 'msg': '平台发单利率格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 商家发单利率(类型2,fadanpingtai='2') shop_rate = rates.get('shop_rate') if shop_rate is not None: try: shop_rate_float = float(shop_rate) if 0 <= shop_rate_float <= 1: # 🔴 修复:查询现有记录,如果不存在则创建 shop_rate_obj = CommissionRate.query.filter(Platform='3').first() if shop_rate_obj: # 更新现有记录 shop_rate_obj.Rate = shop_rate_float shop_rate_obj.save() else: # 创建新记录 shop_rate_obj = CommissionRate.query.create( Rate=shop_rate_float, Platform='3' ) updated_rates['shop_rate'] = shop_rate_float else: return Response({ 'code': 5, 'msg': '商家发单利率必须在0-1之间', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except ValueError: return Response({ 'code': 6, 'msg': '商家发单利率格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 🔴【新增】打手提现抽成利率(类型5,fadanpingtai='5') dashou_withdraw_rate = rates.get('dashou_withdraw_rate') if dashou_withdraw_rate is not None: try: dashou_withdraw_rate_float = float(dashou_withdraw_rate) if 0 <= dashou_withdraw_rate_float <= 1: # 查询现有记录,如果不存在则创建 dashou_withdraw_obj = CommissionRate.query.filter(Platform='5').first() if dashou_withdraw_obj: # 更新现有记录 dashou_withdraw_obj.Rate = dashou_withdraw_rate_float dashou_withdraw_obj.save() else: # 创建新记录 dashou_withdraw_obj = CommissionRate.query.create( Rate=dashou_withdraw_rate_float, Platform='5' ) updated_rates['dashou_withdraw_rate'] = dashou_withdraw_rate_float else: return Response({ 'code': 7, # 注意:这里需要调整错误码,不要重复 'msg': '打手提现抽成利率必须在0-1之间', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except ValueError: return Response({ 'code': 8, 'msg': '打手提现抽成利率格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 🔴【新增】管事提现抽成利率(类型6,fadanpingtai='6') guanshi_withdraw_rate = rates.get('guanshi_withdraw_rate') if guanshi_withdraw_rate is not None: try: guanshi_withdraw_rate_float = float(guanshi_withdraw_rate) if 0 <= guanshi_withdraw_rate_float <= 1: # 查询现有记录,如果不存在则创建 guanshi_withdraw_obj = CommissionRate.query.filter(Platform='6').first() if guanshi_withdraw_obj: # 更新现有记录 guanshi_withdraw_obj.Rate = guanshi_withdraw_rate_float guanshi_withdraw_obj.save() else: # 创建新记录 guanshi_withdraw_obj = CommissionRate.query.create( Rate=guanshi_withdraw_rate_float, Platform='6' ) updated_rates['guanshi_withdraw_rate'] = guanshi_withdraw_rate_float else: return Response({ 'code': 9, 'msg': '管事提现抽成利率必须在0-1之间', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except ValueError: return Response({ 'code': 10, 'msg': '管事提现抽成利率格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if updated_rates: response_data['rates'] = updated_rates # 如果没有任何更新 if not response_data: return Response({ 'code': 7, 'msg': '没有需要更新的数据', 'data': None }, status=status.HTTP_400_BAD_REQUEST) return Response({ 'code': 0, 'msg': '更新成功', 'data': response_data }) except Exception as e: return Response({ 'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 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都是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. 处理查询结果(严格按照字段类型处理) 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转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) class ShangjiaMobanListView(APIView): """ 商家订单模板列表接口 POST /peizhi/sjddmblb 权限: JWT认证 """ permission_classes = [IsAuthenticated] def post(self, request): try: user = request.user yonghu_id = user.UserUID leixing_id = request.data.get('shangpinTypeId') if not leixing_id: return Response({'code': 400, 'msg': '商品类型ID不能为空', 'data': {}}, status=400) get_type = int(request.data.get('getType', 2)) page = int(request.data.get('page', 1)) page_size = int(request.data.get('pageSize', 50)) keyword = request.data.get('keyword', '') min_price = request.data.get('minPrice', '') label_id = request.data.get('labelId', None) if label_id is not None: try: label_id = int(label_id) except (ValueError, TypeError): label_id = None query = Q(UserID=yonghu_id, ProductTypeID=leixing_id) if get_type == 1: # 搜索模式 if keyword: keyword = str(keyword).strip() try: mid = int(keyword) query &= Q(id=mid) | Q(TemplateDesc__icontains=keyword) except ValueError: query &= Q(TemplateDesc__icontains=keyword) if min_price: try: query &= Q(Price__gte=Decimal(str(min_price))) except: pass if label_id: query &= Q(TitleID=label_id) mobans = ShangjiaMoban.query.filter(query).order_by('-id') formatted = [] for moban in mobans: texiao_json = '' label_name = '' if moban.TitleID: try: ch = Chenghao.query.get(id=moban.TitleID) label_name = ch.mingcheng texiao_json = ch.texiao_miaoshu or '' except Chenghao.DoesNotExist: pass formatted.append({ 'mobanId': moban.id, 'shangpinTypeId': moban.ProductTypeID, 'jieshao': moban.TemplateDesc, 'jiage': str(moban.Price), 'fabushuliang': moban.PublishedCount, 'commissionEnabled': moban.CommissionGrabEnabled, 'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None, 'labelId': moban.TitleID, 'labelName': label_name, 'texiaoJson': texiao_json, 'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if moban.CreateTime else '' }) return Response({'code': 200, 'msg': '搜索成功', 'data': {'list': formatted, 'total': len(formatted), 'hasMore': False}}) # 分页模式 queryset = ShangjiaMoban.query.filter(query).order_by('-id') paginator = Paginator(queryset, page_size) if page < 1 or page > paginator.num_pages: return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400) current_page = paginator.page(page) formatted = [] for moban in current_page.object_list: texiao_json = '' label_name = '' if moban.TitleID: try: ch = Chenghao.query.get(id=moban.TitleID) label_name = ch.mingcheng texiao_json = ch.texiao_miaoshu or '' except Chenghao.DoesNotExist: pass formatted.append({ 'mobanId': moban.id, 'shangpinTypeId': moban.ProductTypeID, 'jieshao': moban.TemplateDesc, 'jiage': str(moban.Price), 'fabushuliang': moban.PublishedCount, 'commissionEnabled': moban.CommissionGrabEnabled, 'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None, 'labelId': moban.TitleID, 'labelName': label_name, 'texiaoJson': texiao_json, 'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if moban.CreateTime else '' }) has_more = current_page.has_next() if len(formatted) < page_size: has_more = False return Response({ 'code': 200, 'msg': '获取成功', 'data': { 'list': formatted, 'total': paginator.count, 'hasMore': has_more, 'currentPage': page, 'pageSize': page_size, 'totalPages': paginator.num_pages } }) except Exception as e: logger.error(f"获取模板列表失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) class ShangjiaTianjiaMobanView(APIView): """ 商家添加订单模板接口 POST /peizhi/sjtjddmb 权限: JWT认证 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取当前商家用户ID user = request.user yonghu_id = user.UserUID # 2. 接收并清洗参数,避免类型错误 leixing_id = request.data.get('shangpinTypeId') jieshao = request.data.get('jieshao', '').strip() if request.data.get('jieshao') else '' jiage = request.data.get('jiage', '') # 佣金相关 commission_enabled = request.data.get('commissionEnabled', False) commission_value = request.data.get('commissionValue', None) # 标签ID(称号ID) label_id = request.data.get('labelId', None) # 3. 必填验证 if not leixing_id or not jieshao or not jiage: logger.warning(f"参数缺失 - leixing_id:{leixing_id}, jieshao:{jieshao}, jiage:{jiage}") return Response({ 'code': 400, 'msg': '缺少必填字段(订单类型、介绍、价格)', 'data': {} }, status=400) # 4. 价格格式验证 try: jiage_decimal = Decimal(str(jiage)) if jiage_decimal <= 0: return Response({'code': 400, 'msg': '价格必须大于0', 'data': {}}, status=400) if jiage_decimal > 10000: return Response({'code': 400, 'msg': '价格不能超过10000元', 'data': {}}, status=400) except (InvalidOperation, ValueError, TypeError): return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400) # 5. 介绍长度验证 if len(jieshao) > 100: return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400) # 6. 佣金验证(若启用) CommissionGrabEnabled = False CommissionPrice = None if commission_enabled: CommissionGrabEnabled = True if not commission_value: return Response({'code': 400, 'msg': '开启佣金时,佣金金额不能为空', 'data': {}}, status=400) try: comm_decimal = Decimal(str(commission_value)) if comm_decimal <= 0: return Response({'code': 400, 'msg': '佣金必须大于0', 'data': {}}, status=400) CommissionPrice = comm_decimal except (InvalidOperation, ValueError, TypeError): return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400) # 7. 标签ID清洗(前端可能传数字或字符串) cleaned_label_id = None if label_id is not None and str(label_id).strip(): try: cleaned_label_id = int(label_id) except (ValueError, TypeError): return Response({'code': 400, 'msg': '标签ID格式错误', 'data': {}}, status=400) # 8. 验证商品类型是否存在且可用 try: shangpin_obj = ShangpinLeixing.query.get( id=leixing_id, shenhezhuangtai=1 # 审核通过 ) except ShangpinLeixing.DoesNotExist: return Response({'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=404) # 9. 检查同一用户、同一类型下是否已存在相同介绍的模板 if ShangjiaMoban.query.filter( UserID=yonghu_id, ProductTypeID=leixing_id, TemplateDesc=jieshao ).exists(): return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400) # 10. 执行数据库创建(事务) with transaction.atomic(): moban = ShangjiaMoban.query.create( UserID=yonghu_id, ProductTypeID=leixing_id, TemplateDesc=jieshao, Price=jiage_decimal, PublishedCount=0, CommissionGrabEnabled=CommissionGrabEnabled, CommissionPrice=CommissionPrice, TitleID=cleaned_label_id ) # 11. 返回成功数据(字段与前端对齐) return Response({ 'code': 200, 'msg': '模板创建成功', 'data': { 'mobanId': moban.id, 'shangpinTypeId': leixing_id, 'jieshao': moban.TemplateDesc, 'jiage': str(moban.Price), 'fabushuliang': moban.PublishedCount, 'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S'), 'commissionEnabled': moban.CommissionGrabEnabled, 'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None, 'labelId': moban.TitleID } }) except Exception as e: # 打印完整错误堆栈到日志 logger.error(f"添加模板失败: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '服务器内部错误,添加模板失败,请联系管理员', 'data': {} }, status=500) class ShangjiaShanchuMobanView(APIView): """ 删除模板 POST /peizhi/sjddmbsc """ permission_classes = [IsAuthenticated] def post(self, request): try: user = request.user yonghu_id = user.UserUID moban_id = request.data.get('mobanId') leixing_id = request.data.get('shangpinTypeId') if not all([moban_id, leixing_id]): return Response({'code': 400, 'msg': '参数缺失', 'data': {}}, status=400) try: moban = ShangjiaMoban.query.get(id=moban_id, UserID=yonghu_id, ProductTypeID=leixing_id) except ShangjiaMoban.DoesNotExist: return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404) # 强制硬删除 moban.delete() return Response({'code': 200, 'msg': '删除成功', 'data': {'mobanId': moban_id, 'shangpinTypeId': leixing_id}}) except Exception as e: logger.error(f"删除模板失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) class ShangjiaGengxinMobanView(APIView): """ 商家更新订单模板接口 POST /peizhi/sjddmbgx 权限: JWT认证 """ permission_classes = [IsAuthenticated] def post(self, request): try: user = request.user yonghu_id = user.UserUID moban_id = request.data.get('mobanId') leixing_id = request.data.get('shangpinTypeId') new_jieshao = request.data.get('jieshao', '').strip() new_jiage = request.data.get('jiage', '').strip() # 新增字段 commission_enabled = request.data.get('commissionEnabled') commission_value = request.data.get('commissionValue') label_id = request.data.get('labelId') if not all([moban_id, leixing_id]): return Response({'code': 400, 'msg': '模板ID和商品类型ID不能为空', 'data': {}}, status=400) # 查询模板 try: moban = ShangjiaMoban.query.get( id=moban_id, UserID=yonghu_id, ProductTypeID=leixing_id ) except ShangjiaMoban.DoesNotExist: return Response({'code': 404, 'msg': '模板不存在或无权修改', 'data': {}}, status=404) update_fields = [] # 修改介绍 if new_jieshao: if len(new_jieshao) > 100: return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400) if new_jieshao != moban.TemplateDesc: moban.TemplateDesc = new_jieshao update_fields.append('TemplateDesc') # 修改价格 if new_jiage: try: jiage_decimal = Decimal(new_jiage) if jiage_decimal <= 0 or jiage_decimal > 10000: return Response({'code': 400, 'msg': '价格必须在0.1-10000元之间', 'data': {}}, status=400) if jiage_decimal != moban.Price: moban.Price = jiage_decimal update_fields.append('Price') except (InvalidOperation, ValueError, TypeError): return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400) # 修改标签 if label_id is not None: # 允许传空字符串或不传,传0表示清空 new_label = int(label_id) if label_id else None if new_label != moban.TitleID: moban.TitleID = new_label update_fields.append('TitleID') # 修改佣金设置 if commission_enabled is not None: enabled = bool(commission_enabled) if enabled: if not commission_value: return Response({'code': 400, 'msg': '佣金价格不能为空', 'data': {}}, status=400) try: comm_decimal = Decimal(commission_value) if comm_decimal < 0.1 or comm_decimal > 50: return Response({'code': 400, 'msg': '佣金需在0.1-50元之间', 'data': {}}, status=400) if moban.CommissionGrabEnabled != True or moban.CommissionPrice != comm_decimal: moban.CommissionGrabEnabled = True moban.CommissionPrice = comm_decimal update_fields.extend(['CommissionGrabEnabled', 'CommissionPrice']) except (InvalidOperation, ValueError, TypeError): return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400) else: if moban.CommissionGrabEnabled != False or moban.CommissionPrice is not None: moban.CommissionGrabEnabled = False moban.CommissionPrice = None update_fields.extend(['CommissionGrabEnabled', 'CommissionPrice']) if not update_fields: return Response({'code': 400, 'msg': '没有修改内容', 'data': {}}, status=400) # 检查介绍唯一性 if 'TemplateDesc' in update_fields: if ShangjiaMoban.query.filter( UserID=yonghu_id, ProductTypeID=leixing_id, TemplateDesc=moban.TemplateDesc ).exclude(id=moban_id).exists(): return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400) with transaction.atomic(): moban.save(update_fields=update_fields) # 返回最新数据 return Response({ 'code': 200, 'msg': '模板修改成功', 'data': { 'mobanId': moban.id, 'shangpinTypeId': moban.ProductTypeID, 'jieshao': moban.TemplateDesc, 'jiage': str(moban.Price), 'fabushuliang': moban.PublishedCount, 'commissionEnabled': moban.CommissionGrabEnabled, 'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice else None, 'labelId': moban.TitleID, 'updateTime': moban.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') } }) except Exception as e: logger = logging.getLogger(__name__) logger.error(f"修改商家模板失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) class ShangjiaGenerateLinkView(APIView): """商家生成发单链接接口(支持模板佣金/标签)""" permission_classes = [IsAuthenticated] def post(self, request): try: user = request.user # 获取参数 shangpin_type_id = request.data.get('shangpinTypeId') moban_id = request.data.get('mobanId') if not all([shangpin_type_id, moban_id]): return Response({'code': 400, 'msg': '缺少参数', 'data': {}}, status=400) try: shangpin_type_id = int(shangpin_type_id) moban_id = int(moban_id) except ValueError: return Response({'code': 400, 'msg': '参数格式错误', 'data': {}}, status=400) # 商家信息 try: shangjia = user.ShopProfile except UserShangjia.DoesNotExist: return Response({'code': 403, 'msg': '商家信息未完善', 'data': {}}, status=403) if shangjia.zhuangtai != 1: return Response({'code': 403, 'msg': '商家状态异常', 'data': {}}, status=403) # 商品类型 try: shangpin_leixing = ShangpinLeixing.query.get(id=shangpin_type_id, shenhezhuangtai=1) except ShangpinLeixing.DoesNotExist: return Response({'code': 400, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=400) # 查询模板 try: moban = ShangjiaMoban.query.get(id=moban_id, UserID=user.UserUID, ProductTypeID=shangpin_type_id) except ShangjiaMoban.DoesNotExist: return Response({'code': 400, 'msg': '模板不存在', 'data': {}}, status=400) jiage = moban.Price if jiage > shangjia.yue: return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元', 'data': {}}, status=400) # 利率 try: lilu = float(CommissionRate.query.get(Platform='3').Rate) or 1.0 except CommissionRate.DoesNotExist: lilu = 1.0 dashou_fencheng = Decimal(str(round(float(jiage) * lilu, 2))) # 抢单要求:根据模板是否启用佣金 chenghao_id = moban.TitleID if moban.CommissionGrabEnabled and moban.CommissionPrice is not None: final_yaoqiuleixing = 2 # 佣金模式 final_yongjin = moban.CommissionPrice final_huiyuan_id = '' else: final_yaoqiuleixing = shangpin_leixing.yaoqiuleixing or 1 final_yongjin = None final_huiyuan_id = shangpin_leixing.huiyuan_id or '' # 获取标签名称 label_name = '' if chenghao_id: try: label_name = Chenghao.query.get(id=chenghao_id).mingcheng except Chenghao.DoesNotExist: pass # 生成ID和Token def generate_dingdan_id(): timestamp = int(time.time() * 1000) process_id = os.getpid() % 1000 random_num = random.randint(1000, 9999) return f"SJ{timestamp}{process_id:03d}{random_num}" def generate_secure_token(dd_id, uid): timestamp = int(time.time() * 1_000_000) salt = secrets.token_hex(8) raw = f"{timestamp}:{dd_id}:{uid}:{salt}" token = hashlib.sha256(raw.encode()).hexdigest()[:24] # 确保唯一性(极小概率重复再试一次) if ShangjiaLianjie.query.filter(LinkToken=token).exists(): return generate_secure_token(dd_id, uid) return token def generate_link_url(token): base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/') return f"{base}/order/{token}" with transaction.atomic(): dingdan_id = generate_dingdan_id() secure_token = generate_secure_token(dingdan_id, user.UserUID) # 创建订单主表 dingdan = Order.query.create( OrderID=dingdan_id, Status=13, Platform=2, Amount=jiage, PlayerCommission=dashou_fencheng, ProductTypeID=shangpin_type_id, GrabRequirement=final_yaoqiuleixing, MembershipID=final_huiyuan_id, CommissionReq=final_yongjin, Description=moban.TemplateDesc[:2000], User1ID=f"Sj{user.UserUID}", ImageURL=user.Avatar or '', ) # 商家扩展表 MerchantOrderExt.query.create( Order=dingdan, MerchantID=user.UserUID, MerchantNickname=shangjia.nicheng or f"商家{user.UserUID}", ) # 关联标签 if chenghao_id: try: ch = Chenghao.query.get(id=chenghao_id) DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=ch) except Chenghao.DoesNotExist: logger.warning(f"称号{chenghao_id}不存在,跳过关联") # 更新商家余额和统计 UserShangjia.query.filter(id=shangjia.id).update( fabu=F('fabu') + 1, yue=F('yue') - jiage, jinridingdan=F('jinridingdan') + 1, jinriliushui=F('jinriliushui') + jiage, jinyuedingdan=F('jinyuedingdan') + 1, jinyueliushui=F('jinyueliushui') + jiage, ) # 生成链接并保存 link_url = generate_link_url(secure_token) lianjie_obj = ShangjiaLianjie.query.create( UserID=user.UserUID, ProductTypeID=shangpin_type_id, TemplateID=moban_id, LinkURL=link_url, LinkToken=secure_token, OrderID=dingdan_id, is_used=False, ExpireTime=timezone.now() + timezone.timedelta(days=1) ) # 更新模板发布数量 ShangjiaMoban.query.filter(id=moban_id).update(PublishedCount=F('PublishedCount') + 1) shangjia.refresh_from_db() logger.info(f"链接生成成功 - 订单ID: {dingdan_id}, 标签: {label_name}") return Response({ 'code': 200, 'msg': '链接生成成功', 'data': { 'linkUrl': link_url, 'newBalance': str(shangjia.yue), 'dingdanId': dingdan_id, 'dingdanJieshao': moban.TemplateDesc, 'jiage': str(jiage), 'labelId': chenghao_id, 'labelName': label_name, 'youxiaoShijian': lianjie_obj.ExpireTime.strftime('%Y-%m-%d %H:%M:%S'), } }) except Exception as e: logger.error(f"生成链接事务失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '系统错误,生成链接失败', 'data': {}}, status=500) class ShangjiaLianjieListView(APIView): """获取已生成的链接列表(支持筛选是否使用)""" permission_classes = [IsAuthenticated] def post(self, request): try: user = request.user yonghu_id = user.UserUID moban_id = request.data.get('mobanId') used = int(request.data.get('used', 0)) # 0未使用,1已使用 page = int(request.data.get('page', 1)) page_size = int(request.data.get('pageSize', 5)) page_size = min(page_size, 50) if not moban_id: return Response({'code': 400, 'msg': '模板ID不能为空', 'data': {}}, status=400) # 校验模板归属 try: moban = ShangjiaMoban.query.get(id=moban_id, UserID=yonghu_id) except ShangjiaMoban.DoesNotExist: return Response({'code': 404, 'msg': '模板不存在或无权查看', 'data': {}}, status=404) links = ShangjiaLianjie.query.filter( UserID=yonghu_id, TemplateID=moban_id, is_used=(used == 1) ).order_by('-CreateTime') paginator = Paginator(links, page_size) if page < 1 or page > paginator.num_pages: return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400) current_page = paginator.page(page) data_list = [] for link in current_page.object_list: data_list.append({ 'id': link.id, 'lianjie': link.LinkURL, 'LinkToken': link.LinkToken, 'is_used': link.is_used, 'dingdan_id': link.OrderID, 'ExpireTime': link.ExpireTime.strftime('%Y-%m-%d %H:%M:%S') if link.ExpireTime else '', 'CreateTime': link.CreateTime.strftime('%Y-%m-%d %H:%M:%S') }) return Response({ 'code': 200, 'msg': '获取成功', 'data': { 'list': data_list, 'total': paginator.count, 'hasMore': current_page.has_next(), 'currentPage': page, 'pageSize': page_size } }) except Exception as e: logger.error(f"获取链接列表失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) class IPUtils: """IP工具类""" @staticmethod def get_client_ip(request): """ 获取客户端真实IP,处理代理情况 优先级: X-Real-IP > X-Forwarded-For > remote_addr """ ip = None try: # 1. 尝试从 X-Real-IP 获取 x_real_ip = request.META.get('HTTP_X_REAL_IP') if x_real_ip: ip = x_real_ip.strip() logger.info(f"从 X-Real-IP 获取IP: {ip}") return ip # 2. 尝试从 X-Forwarded-For 获取(处理代理链) x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: # 可能有多个代理IP,第一个是真实IP ips = x_forwarded_for.split(',') for possible_ip in ips: possible_ip = possible_ip.strip() if possible_ip and possible_ip.lower() != 'unknown': ip = possible_ip logger.info(f"从 X-Forwarded-For 获取IP: {ip}") return ip # 3. 使用 remote_addr ip = request.META.get('REMOTE_ADDR', '') logger.info(f"从 REMOTE_ADDR 获取IP: {ip}") except Exception as e: logger.error(f"获取IP失败: {str(e)}", exc_info=True) ip = '0.0.0.0' return ip @staticmethod def validate_ip(ip): """验证IP格式是否合法""" if not ip: return False # IPv4正则 ipv4_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' # IPv6正则(简化版) ipv6_pattern = r'^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$' return bool(re.match(ipv4_pattern, ip) or re.match(ipv6_pattern, ip)) @staticmethod def is_private_ip(ip): """判断是否为内网IP""" if not ip: return False # 内网IP段 private_ranges = [ ('10.0.0.0', '10.255.255.255'), ('172.16.0.0', '172.31.255.255'), ('192.168.0.0', '192.168.255.255'), ('127.0.0.0', '127.255.255.255'), ] try: # 将IP转换为整数比较 def ip_to_int(ip): parts = ip.split('.') return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3]) ip_num = ip_to_int(ip) for start, end in private_ranges: if ip_to_int(start) <= ip_num <= ip_to_int(end): return True except: pass return False class OrderLinkThrottle(SimpleRateThrottle): """订单链接访问频率限制""" scope = 'order_link' rate = '10/minute' # 每分钟最多10次 def get_cache_key(self, request, view): # 基于IP和接口路径进行限制 ip = IPUtils.get_client_ip(request) if not ip or IPUtils.is_private_ip(ip): # 内网IP或不合法IP,使用默认 ip = 'default' return f'throttle_{view.__class__.__name__}_{ip}' class KehuGetDingdanLianjieView(APIView): """ 客户获取订单链接信息接口 路径:GET /peizhi/khhqddlj?token=xxx 返回字段包括:订单信息、打手标识、商家标识、俱乐部配置(客服链接、存储桶、GoEasy AppKey等) """ throttle_classes = [OrderLinkThrottle] permission_classes = [] # 无需认证,任何人都可访问 def get(self, request): """ 处理GET请求,获取订单链接信息 """ # 1. 记录请求开始 logger.info("=== 客户获取订单链接信息接口开始 ===") # 2. 获取客户端真实IP client_ip = IPUtils.get_client_ip(request) logger.info(f"客户端IP: {client_ip}") # 3. 验证IP合法性 if not IPUtils.validate_ip(client_ip): logger.warning(f"IP格式不合法: {client_ip}") return Response({ 'code': 400, 'msg': '网络连接异常', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 4. 获取Token参数 token = request.query_params.get('token', '') if not token: logger.warning(f"Token参数缺失 - IP: {client_ip}") return Response({ 'code': 400, 'msg': '缺少链接参数', 'data': None }, status=status.HTTP_400_BAD_REQUEST) logger.info(f"接收到Token: {token[:8]}...") # 5. 查询链接记录 try: lianjie_obj = ShangjiaLianjie.query.select_related().get(LinkToken=token) logger.info(f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.UserID}") except ShangjiaLianjie.DoesNotExist: logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}") return Response({ 'code': 404, 'msg': '链接不存在或已失效', 'data': None }, status=status.HTTP_404_NOT_FOUND) except Exception as e: logger.error(f"查询链接异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '系统错误,请稍后重试', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 6. 更新访问者IP(无论是否已使用,都记录最新访问IP) try: lianjie_obj.UserIP = client_ip lianjie_obj.save(update_fields=['UserIP']) logger.info(f"更新访问者IP成功: {client_ip}") except Exception as e: logger.warning(f"更新IP失败: {str(e)}") # 继续处理,不影响主要逻辑 # 7. 检查链接有效期 '''now = timezone.now() if lianjie_obj.ExpireTime and lianjie_obj.ExpireTime < now: logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.ExpireTime}, 当前: {now}") return Response({ 'code': 400, 'msg': '链接已过期', 'data': None }, status=status.HTTP_400_BAD_REQUEST)''' # 8. 准备返回数据 response_data = { 'is_used': lianjie_obj.is_used, 'dingdan_id': lianjie_obj.OrderID or '', 'youxiao_shijian': lianjie_obj.ExpireTime.strftime( '%Y-%m-%d %H:%M:%S') if lianjie_obj.ExpireTime else '', } # 9. 获取订单信息 if lianjie_obj.OrderID: try: # 查询订单主表 dingdan_obj = Order.query.get(OrderID=lianjie_obj.OrderID) # 获取商家昵称 shangjia_nicheng = '' try: user_main = User.query.get(UserUID=lianjie_obj.UserID) shangjia_obj = user_main.ShopProfile shangjia_nicheng = shangjia_obj.nicheng except (User.DoesNotExist, UserShangjia.DoesNotExist): logger.warning(f"商家信息不存在 - 用户ID: {lianjie_obj.UserID}") # ========== 新增:确定俱乐部配置 ========== # 默认头像(相对路径) default_avatar = 'a_long/morentouxiang.jpg' # 从 settings 获取配置 kefu_link = getattr(settings, 'KEFU_LINK', '') oss_url = getattr(settings, 'COS_DOMAIN', '') goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '') # ========== 构建订单信息 ========== dingdan_info = { 'jieshao': dingdan_obj.Description or '', 'jiage': str(dingdan_obj.Amount) if dingdan_obj.Amount is not None else '0.00', # 金额字段名是 Amount 'shangjia_mingcheng': shangjia_nicheng, 'zhuangtai': dingdan_obj.Status, # 新增配置字段 'kefu_link': kefu_link, 'oss_url': oss_url, 'default_avatar': default_avatar, 'goeasy_appkey': goeasy_appkey, # 前端用于连接GoEasy的自身用户ID(固定B前缀+商家ID) 'self_user_id': f"B{lianjie_obj.UserID}" if lianjie_obj.UserID else '', } # 如果已使用,返回已填写的信息 if lianjie_obj.is_used: dingdan_info.update({ 'youxi_nicheng': dingdan_obj.Nickname or '', 'zhiding_dashou': dingdan_obj.AssignedID or dingdan_obj.PlayerID or '', 'beizhu': dingdan_obj.Remark or '', 'waibu_dingdan_id': dingdan_obj.ExternalOrderID or '', }) # 获取聊天相关标识 # 9.1 获取接单打手ID jiedan_dashou_id = dingdan_obj.PlayerID dashou_biaoshi = f'Ds{jiedan_dashou_id}' if jiedan_dashou_id else '' # 9.2 获取商家标识 shangjia_biaoshi = '' try: shangjia_kuozhan = MerchantOrderExt.query.select_related('Order').filter( Order__OrderID=lianjie_obj.OrderID ).first() if shangjia_kuozhan and shangjia_kuozhan.MerchantID: shangjia_biaoshi = f'Sj{shangjia_kuozhan.MerchantID}' logger.info(f"生成商家标识: {shangjia_biaoshi}") else: logger.info(f"订单 {lianjie_obj.OrderID} 没有商家订单扩展信息") except Exception as e: logger.error(f"查询商家订单扩展异常: {str(e)}", exc_info=True) # 添加聊天标识 dingdan_info.update({ 'dashou_biaoshi': dashou_biaoshi, 'shangjia_biaoshi': shangjia_biaoshi, 'you_dashou': bool(dashou_biaoshi), }) response_data.update(dingdan_info) except Order.DoesNotExist: logger.error(f"订单不存在 - 订单ID: {lianjie_obj.OrderID}") response_data.update({ 'jieshao': '', 'jiage': '0.00', 'shangjia_mingcheng': '', 'zhuangtai': None, 'dashou_biaoshi': '', 'shangjia_biaoshi': '', 'you_dashou': False, 'kefu_link': '', 'oss_url': '', 'default_avatar': 'a_long/morentouxiang.jpg', 'goeasy_appkey': '', 'self_user_id': '', }) except Exception as e: logger.error(f"获取订单信息异常: {str(e)}", exc_info=True) response_data.update({ 'jieshao': '', 'jiage': '0.00', 'shangjia_mingcheng': '', 'zhuangtai': None, 'dashou_biaoshi': '', 'shangjia_biaoshi': '', 'you_dashou': False, 'kefu_link': '', 'oss_url': '', 'default_avatar': 'a_long/morentouxiang.jpg', 'goeasy_appkey': '', 'self_user_id': '', }) else: # 如果订单ID为空 logger.warning(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}") response_data.update({ 'jieshao': '', 'jiage': '0.00', 'shangjia_mingcheng': '', 'zhuangtai': None, 'dashou_biaoshi': '', 'shangjia_biaoshi': '', 'you_dashou': False, 'kefu_link': '', 'oss_url': '', 'default_avatar': 'a_long/morentouxiang.jpg', 'goeasy_appkey': '', 'self_user_id': '', }) # 10. 判断订单状态是否允许修改 if not lianjie_obj.is_used and response_data.get('zhuangtai') == 13: response_data['allow_modify'] = True else: response_data['allow_modify'] = False # 11. 记录成功日志并返回 logger.info( f"返回订单信息成功 - Token: {token[:8]}..., IP: {client_ip}, " f"是否允许修改: {response_data['allow_modify']}, " f"是否有打手: {response_data.get('you_dashou', False)}" ) return Response({ 'code': 200, 'msg': '成功', 'data': response_data }, status=status.HTTP_200_OK) class KehuTianxieDingdanView(APIView): """ 客户填写订单信息接口 路径:POST /peizhi/kehutx """ # 添加频率限制(比查询接口更严格) throttle_classes = [OrderLinkThrottle] permission_classes = [] # 无需认证 def post(self, request): """ 处理POST请求,填写订单信息 """ # 1. 记录请求开始 logger.info("=== 客户填写订单信息接口开始 ===") # 2. 获取客户端真实IP client_ip = IPUtils.get_client_ip(request) logger.info(f"客户端IP: {client_ip}") # 3. 验证IP合法性 if not IPUtils.validate_ip(client_ip): logger.warning(f"IP格式不合法: {client_ip}") return Response({ 'code': 400, 'msg': '网络连接异常', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 4. 获取请求参数 data = request.data token = data.get('token', '') youxi_nicheng = data.get('youxi_nicheng', '').strip() zhiding_dashou = data.get('zhiding_dashou', '').strip() # 可选 beizhu = data.get('beizhu', '').strip() # 可选 waibu_dingdan_id = str(data.get('waibu_dingdan_id') or data.get('waibuDingdanId') or '').strip()[:64] # 4.0 临时关闭指定功能(如需开启,请删除或注释掉下面两行) #if zhiding_dashou: #return Response({'code': 8, 'msg': '指定功能尚未开启,请不要填写指定打手ID', 'data': None}) #if beizhu: #return Response({'code': 8, 'msg': '备注功能尚未开启,请不要填写备注', 'data': None}) logger.info( f"请求参数 - Token: {token[:8]}..., 昵称: {youxi_nicheng[:10]}..., 指定打手: {zhiding_dashou}, 备注长度: {len(beizhu)}, 拼多多ID: {waibu_dingdan_id[:16] if waibu_dingdan_id else ''}") # 5. 验证必填参数 if not token: logger.warning(f"Token参数缺失 - IP: {client_ip}") return Response({ 'code': 400, 'msg': '缺少链接参数', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not youxi_nicheng: logger.warning(f"游戏昵称为空 - IP: {client_ip}") return Response({ 'code': 400, 'msg': '游戏昵称不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not waibu_dingdan_id: logger.warning(f"拼多多订单号为空 - IP: {client_ip}") return Response({ 'code': 400, 'msg': '拼多多订单号不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 6. 验证游戏昵称格式 if len(youxi_nicheng) > 50: logger.warning(f"游戏昵称过长 - IP: {client_ip}, 长度: {len(youxi_nicheng)}") return Response({ 'code': 400, 'msg': '游戏昵称不能超过50个字符', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 过滤危险字符 danger_chars = ['<', '>', "'", '"', '&', ';', '|'] for char in danger_chars: if char in youxi_nicheng: logger.warning(f"游戏昵称包含危险字符 - IP: {client_ip}, 字符: {char}") return Response({ 'code': 400, 'msg': '昵称包含非法字符', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 7. 验证指定打手ID格式(如果提供) if zhiding_dashou and len(zhiding_dashou) > 32: logger.warning(f"指定打手ID过长 - IP: {client_ip}, 长度: {len(zhiding_dashou)}") return Response({ 'code': 400, 'msg': '指定打手ID不能超过32个字符', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 8. 验证拼多多订单号 if len(waibu_dingdan_id) > 64: return Response({ 'code': 400, 'msg': '拼多多订单号不能超过64个字符', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 9. 验证备注长度 if len(beizhu) > 500: logger.warning(f"备注过长 - IP: {client_ip}, 长度: {len(beizhu)}") return Response({ 'code': 400, 'msg': '备注不能超过500个字符', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 9. 查询链接记录 try: lianjie_obj = ShangjiaLianjie.query.get(LinkToken=token) logger.info( f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.UserID}, 是否已使用: {lianjie_obj.is_used}") except ShangjiaLianjie.DoesNotExist: logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}") return Response({ 'code': 404, 'msg': '链接不存在或已失效', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 10. 检查链接有效期 now = timezone.now() if lianjie_obj.ExpireTime and lianjie_obj.ExpireTime < now: logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.ExpireTime}") return Response({ 'code': 400, 'msg': '链接已过期', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 11. 检查链接是否已使用 if lianjie_obj.is_used: logger.warning(f"链接已使用 - 链接ID: {lianjie_obj.id}, IP: {client_ip}") return Response({ 'code': 400, 'msg': '链接已被使用,无法重复提交', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 12. 验证指定打手(如果需要) zhuangtai_xin = 1 # 默认普通订单状态 if zhiding_dashou: # 验证打手是否存在且有效 try: # 先查询用户主表 dashou_user = User.query.get(UserUID=zhiding_dashou) logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}") # 通过反向关系获取打手扩展信息 try: dashou_profile = dashou_user.DashouProfile # 检查打手状态(zhuangtai) if dashou_profile.zhuangtai != 1: logger.warning(f"打手状态异常 - 打手ID: {zhiding_dashou}, 状态: {dashou_profile.zhuangtai}") return Response({ 'code': 400, 'msg': '指定打手当前无法接单', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 检查打手账号状态(zhanghaozhuangtai) if dashou_profile.zhanghaozhuangtai != 1: logger.warning( f"打手账号状态异常 - 打手ID: {zhiding_dashou}, 账号状态: {dashou_profile.zhanghaozhuangtai}") return Response({ 'code': 400, 'msg': '指定打手账号异常', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证通过,设置为指定订单状态 zhuangtai_xin = 7 # 指定打手订单状态 logger.info(f"指定打手验证通过 - 打手ID: {zhiding_dashou}, 昵称: {dashou_profile.nicheng}") except UserDashou.DoesNotExist: logger.warning(f"打手扩展信息不存在 - 用户ID: {zhiding_dashou}") return Response({ 'code': 400, 'msg': '指定打手信息不完整', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except User.DoesNotExist: logger.warning(f"指定打手用户不存在 - 用户ID: {zhiding_dashou}") return Response({ 'code': 400, 'msg': '指定打手不存在', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 13. 使用事务确保数据一致性 try: with transaction.atomic(): # 更新链接记录 lianjie_obj.UserIP = client_ip lianjie_obj.is_used = True lianjie_obj.save(update_fields=['UserIP', 'is_used']) logger.info(f"更新链接记录成功 - 链接ID: {lianjie_obj.id}, 设置为已使用") # 查询订单 if not lianjie_obj.OrderID: logger.error(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}") raise ValueError("订单ID为空") dingdan_obj = Order.query.get(OrderID=lianjie_obj.OrderID) # 检查订单状态是否为13(灰色状态) if dingdan_obj.Status != 13: logger.warning( f"订单状态不允许修改 - 订单ID: {dingdan_obj.OrderID}, 当前状态: {dingdan_obj.Status}") # 回滚事务 transaction.set_rollback(True) return Response({ 'code': 400, 'msg': '订单状态异常,无法修改', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 更新订单信息 dingdan_obj.Nickname = youxi_nicheng # 游戏昵称 dingdan_obj.Remark = beizhu if beizhu else '' # 备注 dingdan_obj.ExternalOrderID = waibu_dingdan_id dingdan_obj.Status = zhuangtai_xin # 新状态:1或7 # 如果是指定打手,设置zhiding_id if zhiding_dashou and zhuangtai_xin == 7: dingdan_obj.AssignedID = zhiding_dashou dingdan_obj.save() logger.info( f"更新订单成功 - 订单ID: {dingdan_obj.OrderID}, 新状态: {zhuangtai_xin}, 指定打手: {zhiding_dashou or '无'}") # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用) order_info = { 'dingdan_id': dingdan_obj.OrderID, 'game_type': self._get_game_type_name(dingdan_obj.ProductTypeID), 'amount': str(dingdan_obj.Amount), 'order_desc': (dingdan_obj.Description or '')[:50], } dingdan_guangbo.delay(order_info) # 瞬间返回,不阻塞请求 # 获取商家信息用于返回 try: user_main = User.query.get(UserUID=lianjie_obj.UserID) shangjia_obj = user_main.ShopProfile shangjia_mingcheng = shangjia_obj.nicheng except: shangjia_mingcheng = f"商家{lianjie_obj.UserID}" logger.warning(f"获取商家昵称失败,使用默认 - 用户ID: {lianjie_obj.UserID}") # 14. 返回成功响应 response_data = { #'dingdan_id': dingdan_obj.dingdan_id, #'CreateTime': dingdan_obj.CreateTime.strftime('%Y-%m-%d %H:%M:%S'), #'shangpin_mingcheng': dingdan_obj.jieshao[:50] if dingdan_obj.jieshao else '', # 截取前50字符 #'jiage': str(dingdan_obj.jine) if dingdan_obj.jine else '0.00', 'shangjia_mingcheng': shangjia_mingcheng, 'youxi_nicheng': youxi_nicheng, 'zhuangtai': zhuangtai_xin, 'zhuangtai_text': '普通订单' if zhuangtai_xin == 1 else '指定打手订单' } logger.info(f"订单提交成功 - 订单ID: {dingdan_obj.OrderID}, IP: {client_ip}") # 新方法(行为驱动,更安全) update_shangjia_daily( yonghuid=lianjie_obj.UserID, amount=Decimal(str(dingdan_obj.Amount)), action=1, # 1 = 派发 order_id=dingdan_obj.OrderID, ) return Response({ 'code': 200, 'msg': '提交成功', 'data': response_data }, status=status.HTTP_200_OK) except Order.DoesNotExist: logger.error(f"订单不存在 - 订单ID: {lianjie_obj.OrderID}") return Response({ 'code': 500, 'msg': '订单数据异常', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except Exception as e: logger.error(f"处理订单异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '系统错误,请稍后重试', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def _get_game_type_name(self, leixing_id): try: gt = ShangpinLeixing.query.filter(id=leixing_id).first() return gt.jieshao if gt else '游戏订单' except Exception as e: logger.error(f"获取游戏类型名称异常: {e}") return '游戏订单' def send_order_notification_async(self, dingdan, youxi_nicheng, zhiding_dashou, zhuangtai): """异步发送订单通知(客户填写订单后)""" try: # 避免阻塞主线程,使用线程异步执行 def notification_task(): try: # 获取游戏类型名称 try: leixing_obj = ShangpinLeixing.query.get(id=dingdan.ProductTypeID) game_type = leixing_obj.jieshao except: game_type = '未知游戏' # 构建订单信息 order_info = { 'dingdan_id': dingdan.OrderID, 'game_type': game_type, 'amount': str(dingdan.Amount), 'order_desc': dingdan.Description[:50] if dingdan.Description else '新订单', 'youxi_nicheng': youxi_nicheng, 'order_type': '指定订单' if zhuangtai == 7 else '普通订单', 'zhiding_dashou': zhiding_dashou or '' } # 调用微信广播发送器 sender = WeixinBroadcastSender() result = sender.broadcast_order(order_info) if result['success']: logger.info(f"【客户填写订单】微信通知发送成功: {result['msg']}") else: logger.warning(f"【客户填写订单】微信通知发送失败: {result['msg']}") except Exception as e: logger.error(f"【客户填写订单】发送通知任务异常: {str(e)}") # 启动异步线程 thread = threading.Thread(target=notification_task) thread.daemon = True thread.start() except Exception as e: logger.error(f"【客户填写订单】启动通知线程异常: {str(e)}") class GuanZhuAListView(APIView): """ 获取关注阿龙页面图片列表 POST /peizhi/gzal 需要JWT认证 返回: { "code": 0, "msg": "success", "data": { "images": ["相对URL1", "相对URL2", ...], "last_update": "2025-03-16 12:34:56", "visit_count": 123 } } """ permission_classes = [IsAuthenticated] def post(self, request): try: from jituan.services.club_context import resolve_effective_club_id club_id = resolve_effective_club_id(request, request.user) qs = Tupianpeizhi.query.filter(ImageType=3, club_id=club_id) if not qs.exists(): return Response({ 'code': 0, 'msg': 'success', 'data': { 'images': [], 'last_update': None, 'visit_count': 0 } }, status=status.HTTP_200_OK) # 获取所有图片的相对URL images = list(qs.values_list('ImageURL', flat=True)) # 获取最新更新时间(取所有记录的UpdateTime最大值) last_update = qs.aggregate(UpdateTime__max=Max('UpdateTime'))['UpdateTime__max'] last_update_str = last_update.strftime('%Y-%m-%d %H:%M:%S') if last_update else None # 原子性增加访问次数 # 先更新所有记录,再获取更新后的最大次数 qs.update(AccessCount=F('AccessCount') + 1) visit_count = qs.aggregate(access_count__max=Max('AccessCount'))['access_count__max'] or 0 return Response({ 'code': 0, 'msg': 'success', 'data': { 'images': images, 'last_update': last_update_str, 'visit_count': visit_count } }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"获取关注阿龙图片列表失败: {e}", exc_info=True) return Response({ 'code': 1, 'msg': '服务器内部错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) #不用缓存,一天2000次 class GuanshiQRCodeView(APIView): permission_classes = [permissions.IsAuthenticated] def post(self, request): logger.info("===== 开始生成管事二维码 =====") current_user = request.user logger.info(f"当前用户ID: {current_user.UserUID}") try: try: guanshi = current_user.GuanshiProfile logger.info(f"管事扩展表查询成功,状态: {guanshi.zhuangtai}") except UserGuanshi.DoesNotExist: logger.warning(f"用户 {current_user.UserUID} 不是管事身份") return Response( {'code': 403, 'message': '当前用户不是管事身份', 'data': None}, status=status.HTTP_403_FORBIDDEN ) if guanshi.zhuangtai != 1: logger.warning(f"用户 {current_user.UserUID} 管事状态异常: {guanshi.zhuangtai}") return Response( {'code': 403, 'message': '管事账号状态异常', 'data': None}, status=status.HTTP_403_FORBIDDEN ) invite_code = guanshi.yaoqingma if not invite_code: logger.info(f"用户 {current_user.UserUID} 无邀请码,开始生成") invite_code = CreateInvitationCode(str(current_user.UserUID)) if not VerifyInvitationCode(invite_code): logger.error(f"邀请码唯一性校验失败,用户: {current_user.UserUID}") return Response( {'code': 500, 'message': '邀请码生成失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) guanshi.yaoqingma = invite_code guanshi.save(update_fields=['yaoqingma']) logger.info(f"邀请码生成成功: {invite_code}") else: logger.info(f"使用已有邀请码: {invite_code}") # 直接获取新的 access_token,不缓存 access_token = self.get_fresh_access_token() if not access_token: logger.error("获取 access_token 失败") return Response( {'code': 500, 'message': '获取微信access_token失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) logger.info(f"获取到 access_token (前10位): {access_token[:10]}...") encoded_invite = urllib.parse.quote(invite_code, safe='') page_path = f'pages/dashouduan/dashouduan?inviteCode={encoded_invite}' wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}' post_data = { 'path': page_path, 'width': 430, 'auto_color': False, 'line_color': {'r': 0, 'g': 0, 'b': 0} } logger.info(f"调用微信接口生成小程序码,path: {page_path}") try: wx_resp = requests.post(wx_url, json=post_data, timeout=10) logger.info(f"微信接口响应状态码: {wx_resp.status_code}") except Exception as e: logger.error(f"请求微信接口异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '调用微信服务失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) if wx_resp.status_code != 200: error_info = {} if wx_resp.headers.get('content-type') == 'application/json': try: error_info = wx_resp.json() except: pass errmsg = error_info.get('errmsg', '未知错误') logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}") if 'errcode' in error_info: logger.error(f"微信错误码: {error_info.get('errcode')}") return Response( {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) content_type = wx_resp.headers.get('content-type', '') if 'image' not in content_type: try: error_info = wx_resp.json() errmsg = error_info.get('errmsg', '未知错误') logger.error(f"微信接口返回非图片内容: {errmsg}") return Response( {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) except: logger.error("微信接口返回未知内容,无法解析为JSON") return Response( {'code': 500, 'message': '生成二维码失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) timestamp = int(time.time()) filename = f"{current_user.UserUID}_{timestamp}.png" oss_path = f"erweima/guanshi/{filename}" file_obj = io.BytesIO(wx_resp.content) logger.info(f"开始上传图片到COS: {oss_path}") try: full_url = upload_to_oss(file_obj, oss_path) logger.info(f"上传成功,完整URL: {full_url}") except Exception as e: logger.error(f"上传到COS异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '图片上传失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) if not full_url: logger.error("upload_to_oss 返回了空 URL") return Response( {'code': 500, 'message': '图片上传失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) cos_domain = getattr(settings, 'COS_DOMAIN', '') if cos_domain and full_url.startswith(cos_domain): relative_path = full_url[len(cos_domain):].lstrip('/') else: relative_path = oss_path logger.info(f"相对路径: {relative_path}") old_url = guanshi.invite_qrcode_url if old_url: logger.info(f"删除旧二维码: {old_url}") try: delete_from_oss(old_url) except Exception as e: logger.warning(f"删除旧二维码失败(不影响主流程): {str(e)}") with transaction.atomic(): guanshi.invite_qrcode_url = relative_path guanshi.save(update_fields=['invite_qrcode_url']) logger.info("数据库更新成功") return Response({ 'code': 0, 'message': 'success', 'data': {'url': relative_path} }) except Exception as e: logger.error(f"生成二维码过程中发生未捕获异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) def get_fresh_access_token(self): """ 直接请求微信接口获取新的 access_token,不缓存,保证最新。 """ appid = getattr(settings, 'WEIXIN_APPID', '') secret = getattr(settings, 'WEIXIN_SECRET', '') if not appid or not secret: logger.error("微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置") return None url = 'https://api.weixin.qq.com/cgi-bin/token' params = { 'grant_type': 'client_credential', 'appid': appid, 'secret': secret } try: logger.info("正在请求微信接口获取最新 access_token...") resp = requests.get(url, params=params, timeout=5) data = resp.json() new_token = data.get('access_token') if new_token: logger.info("成功获取最新 access_token") return new_token else: errcode = data.get('errcode', '未知') errmsg = data.get('errmsg', '未知错误') logger.error(f"获取 access_token 失败,errcode={errcode}, errmsg={errmsg}") return None except Exception as e: logger.error(f"请求微信 access_token 接口异常: {str(e)}", exc_info=True) return None class ZuzhangHaibaoView(APIView): """ 组长专属海报二维码生成接口 路径:/peizhi/zuzhanghb 方法:POST 权限:JWT认证,仅限组长身份 返回: { "code": 0, "message": "success", "data": { "url": "erweima/zuzhang/1000001_1234567890.png", "yaoqingma": "ABC123XYZ" } } """ permission_classes = [permissions.IsAuthenticated] def post(self, request): current_user = request.user # 1. 验证组长身份 try: zuzhang = current_user.ZuzhangProfile except UserZuzhang.DoesNotExist: return Response({ 'code': 403, 'message': '当前用户不是组长身份', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 2. 验证组长账号状态 if zuzhang.zhuangtai != 1: return Response({ 'code': 403, 'message': '组长账号状态异常,无法生成海报', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 3. 获取或生成邀请码 invite_code = zuzhang.yaoqingma if not invite_code: invite_code = CreateInvitationCode(str(current_user.UserUID)) if not VerifyInvitationCode(invite_code): logger.error(f"组长邀请码生成失败,用户: {current_user.UserUID}") return Response({ 'code': 500, 'message': '邀请码生成失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) zuzhang.yaoqingma = invite_code zuzhang.save(update_fields=['yaoqingma']) # 4. 获取微信 access_token(优先 stable_token,token 失效时自动刷新重试) access_token = get_weixin_mini_access_token(force_refresh=False) if not access_token: return Response({ 'code': 500, 'message': '获取微信access_token失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # ===== 修改点:使用 A 接口(getwxacode) ===== encoded_invite = urllib.parse.quote(invite_code, safe='') page_path = f'pages/guanshiduan/guanshiduan?inviteCode={encoded_invite}' wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token) if wx_error and is_weixin_token_invalid(wx_error.get('errcode'), wx_error.get('errmsg', '')): logger.warning('组长海报二维码 token 失效,强制刷新后重试') access_token = get_weixin_mini_access_token(force_refresh=True) if not access_token: return Response({ 'code': 500, 'message': '获取微信access_token失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token) if wx_error: errmsg = wx_error.get('errmsg', '未知错误') logger.error(f"微信接口返回错误: {errmsg}") return Response({ 'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) if wx_resp is None: return Response({ 'code': 500, 'message': '调用微信服务失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 5. 上传到腾讯云 COS timestamp = int(time.time()) filename = f"{current_user.UserUID}_{timestamp}.png" oss_path = f"erweima/zuzhang/{filename}" file_obj = io.BytesIO(wx_resp.content) try: full_url = upload_to_oss(file_obj, oss_path) except Exception as e: logger.error(f"上传到COS失败: {e}") return Response({ 'code': 500, 'message': '图片上传失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) if not full_url: return Response({ 'code': 500, 'message': '图片上传失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 6. 获取相对路径 cos_domain = getattr(settings, 'COS_DOMAIN', '') if cos_domain and full_url.startswith(cos_domain): relative_path = full_url[len(cos_domain):].lstrip('/') else: relative_path = oss_path # 7. 删除旧二维码文件 old_url = zuzhang.haibao_url if old_url: delete_from_oss(old_url) # 8. 更新数据库中的 haibao_url 字段 with transaction.atomic(): zuzhang.haibao_url = relative_path zuzhang.save(update_fields=['haibao_url']) # 9. 返回邀请码和二维码相对路径 return Response({ 'code': 0, 'message': 'success', 'data': { 'url': relative_path, 'yaoqingma': invite_code } }) def _request_wx_qrcode(self, page_path, access_token): """ 调用 getwxacode 生成小程序码。 返回 (response, error_dict);成功时 error_dict 为 None。 """ wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}' post_data = { 'path': page_path, 'width': 430, 'auto_color': False, 'line_color': {'r': 0, 'g': 0, 'b': 0}, } try: wx_resp = requests.post(wx_url, json=post_data, timeout=10) except Exception as e: logger.error(f'请求微信接口失败: {e}') return None, {'errmsg': '调用微信服务失败'} if wx_resp.status_code != 200: error_info = {} if wx_resp.headers.get('content-type', '').startswith('application/json'): try: error_info = wx_resp.json() except Exception: pass return wx_resp, { 'errcode': error_info.get('errcode'), 'errmsg': error_info.get('errmsg', f'HTTP {wx_resp.status_code}'), } content_type = wx_resp.headers.get('content-type', '') if 'image' not in content_type: try: error_info = wx_resp.json() return wx_resp, { 'errcode': error_info.get('errcode'), 'errmsg': error_info.get('errmsg', '未知错误'), } except Exception: return wx_resp, {'errmsg': '微信接口返回未知内容'} return wx_resp, None class PopupConfigView(APIView): """ 获取指定页面的弹窗配置 请求格式:POST { "pageKey": "dashouzhongxin" } 返回格式:{ "code": 0, "data": { "serverTime": "...", "popups": [...] } } """ authentication_classes = [] permission_classes = [AllowAny] def post(self, request): # 获取参数 pageKey page_key = request.data.get('pageKey') if not page_key: return Response({ 'code': 400, 'msg': '缺少参数 pageKey' }, status=status.HTTP_400_BAD_REQUEST) # 3. 查询页面配置(按俱乐部) from jituan.services.display_config import get_gonggao_content from jituan.services.club_context import resolve_club_id_from_request club_id = resolve_club_id_from_request(request) try: popup_page = PopupPage.query.get(club_id=club_id, page_key=page_key, is_active=True) except PopupPage.DoesNotExist: # 该页面没有配置弹窗,返回空 return Response({ 'code': 0, 'data': { 'serverTime': timezone.now().isoformat(), 'popups': [] } }) # 4. 查询该页面下所有启用的弹窗(且在有效时间范围内) now = timezone.now() popups = PopupConfig.query.filter( page=popup_page, is_active=True, start_time__lte=now, end_time__gte=now ).prefetch_related('images') # 预加载图片,避免N+1查询 # 按 sort_order 排序(model Meta 已定义) # 5. 序列化 serializer = PopupConfigSerializer(popups, many=True) # 6. 返回数据 return Response({ 'code': 0, 'data': { 'serverTime': now.isoformat(), 'ignore_user_mute': popup_page.ignore_user_mute, # 新增 'popups': serializer.data } }) class GetWithdrawModeView(APIView): """ 获取提现模式配置 前端请求 /peizhi/hqtxzsym 返回格式: { "code": 0, "data": { "mode": 1 or 2 } } """ permission_classes = [IsAuthenticated] # JWT 认证 def post(self, request, *args, **kwargs): from jituan.services.club_context import resolve_club_id_from_request from jituan.services.club_user import get_user_club_id from jituan.services.withdraw_config import get_withdraw_mode user = getattr(request, 'user', None) club_id = get_user_club_id(user) if user and getattr(user, 'is_authenticated', False) else None if not club_id: club_id = resolve_club_id_from_request(request) mode = get_withdraw_mode(club_id) return Response({ 'code': 0, 'data': { 'mode': mode, 'club_id': club_id, } }) class CheckPhoneAuthView(APIView): """ 判断当前登录用户是否需要强制手机号认证 规则: - 已认证过手机号(PhoneVerified=True) → 不需要 - 未认证时,满足以下任一条件 → 需要认证: 打手:有会员购买记录,或押金 > 0 商家:已是商家(存在商家扩展表) 管事:已是管事(存在管事扩展表) 组长:已是组长(存在组长扩展表) 老板:有下单记录(alldingdan > 0) 子客服:已是有效商家客服(merchant_staff_member 活跃) """ permission_classes = [IsAuthenticated] def post(self, request): user = request.user if getattr(user, 'PhoneVerified', False): return Response({'code': 0, 'need_auth': False}) if self._dashou_meets_threshold(user): return Response({'code': 0, 'need_auth': True}) if self._shangjia_meets_threshold(user): return Response({'code': 0, 'need_auth': True}) if self._guanshi_meets_threshold(user): return Response({'code': 0, 'need_auth': True}) if self._zuzhang_meets_threshold(user): return Response({'code': 0, 'need_auth': True}) if self._boss_meets_threshold(user): return Response({'code': 0, 'need_auth': True}) if self._staff_meets_threshold(user): return Response({'code': 0, 'need_auth': True}) return Response({'code': 0, 'need_auth': False}) def _staff_meets_threshold(self, user): """子客服:已是有效商家客服""" try: from merchant_ops.services.authz import get_active_staff_member return get_active_staff_member(user) is not None except Exception: return False def _dashou_meets_threshold(self, user): """打手:有会员记录、押金 > 0、或有过接单/成交订单""" try: dashou = UserDashou.query.get(user=user) except UserDashou.DoesNotExist: return False if dashou.yajin and dashou.yajin > 0: return True from jituan.services.club_context import resolve_effective_club_id club_id = resolve_effective_club_id(self.request, user) if Huiyuangoumai.query.filter(yonghu_id=user.UserUID, club_id=club_id).exists(): return True if (dashou.jiedanzongliang or 0) > 0 or (dashou.chengjiaozongliang or 0) > 0: return True return Order.query.filter(PlayerID=user.UserUID).exists() def _shangjia_meets_threshold(self, user): """商家:已是商家身份""" return UserShangjia.query.filter(user=user).exists() def _guanshi_meets_threshold(self, user): """管事:已是管事身份""" return UserGuanshi.query.filter(user=user).exists() def _zuzhang_meets_threshold(self, user): """组长:已是组长身份""" return UserZuzhang.query.filter(user=user).exists() def _boss_meets_threshold(self, user): """老板:有下单记录""" try: boss = UserBoss.query.get(user=user) except UserBoss.DoesNotExist: return False return (boss.alldingdan or 0) > 0