3377 lines
136 KiB
Python
3377 lines
136 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 utils.chat_utils import subscribe_merchant_link_chat
|
||
|
||
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 HaibaoPeizhiView(APIView):
|
||
"""小程序推广海报背景配置(按俱乐部)。"""
|
||
permission_classes = [AllowAny]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
from jituan.services.display_config import _display_club_id
|
||
from jituan.services.miniapp_assets import get_poster_payload
|
||
|
||
club_id = _display_club_id(request)
|
||
poster = get_poster_payload(club_id)
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'success',
|
||
'data': {
|
||
'club_id': club_id,
|
||
'poster': poster,
|
||
},
|
||
})
|
||
except Exception as e:
|
||
logger.error('获取海报配置失败: %s', 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 '',
|
||
})
|
||
|
||
# 获取聊天相关标识(新配对群优先,旧单保留 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': '',
|
||
'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:
|
||
from jituan.services.club_context import resolve_effective_club_id
|
||
from utils.weixin_token import get_club_mini_access_token, is_weixin_token_invalid
|
||
|
||
club_id = resolve_effective_club_id(request, current_user)
|
||
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}")
|
||
|
||
# 按俱乐部使用对应小程序 appid 的 access_token
|
||
access_token = get_club_mini_access_token(club_id)
|
||
if not access_token:
|
||
logger.error("获取 access_token 失败 club_id=%s", club_id)
|
||
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]}... club={club_id}")
|
||
|
||
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}")
|
||
|
||
def _request_qr(token):
|
||
try:
|
||
return requests.post(
|
||
f'https://api.weixin.qq.com/wxa/getwxacode?access_token={token}',
|
||
json=post_data, timeout=10,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"请求微信接口异常: {str(e)}", exc_info=True)
|
||
return None
|
||
|
||
wx_resp = _request_qr(access_token)
|
||
if wx_resp is None:
|
||
return Response(
|
||
{'code': 500, 'message': '调用微信服务失败', 'data': None},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|
||
if wx_resp.headers.get('content-type', '').startswith('application/json'):
|
||
try:
|
||
err = wx_resp.json()
|
||
if is_weixin_token_invalid(err.get('errcode'), err.get('errmsg', '')):
|
||
access_token = get_club_mini_access_token(club_id, force_refresh=True)
|
||
if access_token:
|
||
wx_resp = _request_qr(access_token)
|
||
except Exception:
|
||
pass
|
||
|
||
logger.info(f"微信接口响应状态码: {wx_resp.status_code}")
|
||
|
||
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/{club_id}/{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
|
||
from jituan.services.club_context import resolve_effective_club_id
|
||
from utils.weixin_token import get_club_mini_access_token
|
||
|
||
club_id = resolve_effective_club_id(request, current_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(按俱乐部对应小程序)
|
||
access_token = get_club_mini_access_token(club_id, 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 失效,强制刷新后重试 club=%s', club_id)
|
||
access_token = get_club_mini_access_token(club_id, 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/{club_id}/{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 |