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 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 '', 'shangjia_mingcheng': shangjia_nicheng, 'zhuangtai': dingdan_obj.Status, 'jiedan_dashou_id': dingdan_obj.PlayerID or '', # 新增配置字段 '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: laoban_shouji = '' try: laoban_shouji = dingdan_obj.shangjia_kuozhan.BossPhone or '' except Exception: pass dingdan_info.update({ 'youxi_nicheng': dingdan_obj.Nickname or '', 'zhiding_dashou': dingdan_obj.AssignedID or '', 'beizhu': dingdan_obj.Remark or '', 'waibu_dingdan_id': dingdan_obj.ExternalOrderID or '', 'laoban_shouji': laoban_shouji, }) # 获取聊天相关标识(新配对群优先,旧单保留 order 群) chat_meta, _sub_ok = subscribe_merchant_link_chat(dingdan_obj, lianjie_obj.UserID) # 添加聊天标识 dingdan_info.update({ 'dashou_biaoshi': chat_meta.get('dashou_biaoshi', ''), 'shangjia_biaoshi': chat_meta.get('shangjia_biaoshi', ''), 'you_dashou': chat_meta.get('you_dashou', False), 'group_id': chat_meta.get('group_id', ''), 'is_pair_group': chat_meta.get('is_pair_group', False), 'legacy_group_id': chat_meta.get('legacy_group_id', ''), }) response_data.update(dingdan_info) except Order.DoesNotExist: logger.error(f"订单不存在 - 订单ID: {lianjie_obj.OrderID}") response_data.update({ 'jieshao': '', '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': '', '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': '', '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] laoban_shouji = str(data.get('laoban_shouji') or data.get('bossPhone') or data.get('laobanPhone') or '').strip()[:11] # 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) ok_pdd, msg_pdd = validate_pdd_order_id(waibu_dingdan_id) if not ok_pdd: logger.warning(f"拼多多订单号校验失败 - IP: {client_ip}, 值: {waibu_dingdan_id[:24]}") return Response({ 'code': 400, 'msg': msg_pdd, 'data': None }, status=status.HTTP_400_BAD_REQUEST) ok_phone, msg_phone = validate_cn_mobile(laoban_shouji, required=False) if not ok_phone: logger.warning(f"老板手机号校验失败 - IP: {client_ip}") return Response({ 'code': 400, 'msg': msg_phone, '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 '无'}") try: shangjia_ext = dingdan_obj.shangjia_kuozhan shangjia_ext.BossPhone = laoban_shouji shangjia_ext.save(update_fields=['BossPhone']) except MerchantOrderExt.DoesNotExist: logger.warning(f"商家扩展表不存在,无法保存手机号 - 订单: {dingdan_obj.OrderID}") # 构建通知数据(你的 _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)}")