1034 lines
42 KiB
Python
1034 lines
42 KiB
Python
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 Order, MerchantOrderExt
|
||
from orders.notice_tasks import dingdan_guangbo
|
||
from orders.utils import calc_shangjia_order_fencheng
|
||
from jituan.services.club_context import resolve_club_id_from_request
|
||
|
||
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)
|
||
|
||
# 打手+管事分成(与普通派单 sjpaifa 同一套算法,打手优先)
|
||
club_id = resolve_club_id_from_request(request)
|
||
dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage, club_id=club_id)
|
||
|
||
# 抢单要求:根据模板是否启用佣金
|
||
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,
|
||
ManagerCommission=guanshi_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 '',
|
||
ClubID=club_id,
|
||
)
|
||
|
||
# 商家扩展表
|
||
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.select_related('ShopProfile').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.select_related('DashouProfile').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.select_related('ShopProfile').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)}")
|
||
|
||
|
||
|
||
|
||
|
||
|