13036 lines
531 KiB
Python
13036 lines
531 KiB
Python
import os
|
||
import sys
|
||
import json
|
||
import re
|
||
import time
|
||
import uuid
|
||
import random
|
||
import string
|
||
import requests
|
||
import hashlib
|
||
import traceback
|
||
import decimal
|
||
import jwt
|
||
import ipaddress
|
||
import xmltodict
|
||
from datetime import datetime, timedelta
|
||
from decimal import Decimal
|
||
import defusedxml.ElementTree as ET
|
||
|
||
from django.conf import settings
|
||
from django.db import models, transaction, IntegrityError, DatabaseError, connection
|
||
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
|
||
from django.core.cache import cache
|
||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from django.shortcuts import get_object_or_404
|
||
from django.utils import timezone
|
||
from django.http import HttpResponse
|
||
from django.views import View
|
||
from django.views.decorators.csrf import csrf_exempt
|
||
from django.contrib.auth.hashers import make_password
|
||
|
||
from rest_framework.views import APIView
|
||
from rest_framework.response import Response
|
||
from rest_framework import permissions, status
|
||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||
from rest_framework.throttling import AnonRateThrottle
|
||
from rest_framework_simplejwt.tokens import RefreshToken
|
||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||
|
||
from gvsdsdk.fluent import db, func, FQ
|
||
|
||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||
from utils.fadan_utils import check_fadan_qiangdan_eligible
|
||
from utils.wechat_v3 import verify_wechat_sign, decrypt_callback_ciphertext
|
||
|
||
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||
from users.fadan_fenhong_utils import process_fadan_fenhong
|
||
|
||
from orders.utils import (
|
||
update_daily_payout,
|
||
update_daily_income, settle_shangjia_order_guanshi_fenhong
|
||
)
|
||
from backend.utils import (
|
||
update_dashou_daily_by_action, update_guanshi_daily_by_action,
|
||
update_shangjia_daily, update_zuzhang_daily_by_action,
|
||
verify_kefu_permission
|
||
)
|
||
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
|
||
|
||
from .models import (
|
||
UserBoss, UserDashou, UserShangjia, UserGuanshi,
|
||
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
|
||
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
|
||
RankingRecord
|
||
)
|
||
from users.business_models import User
|
||
from .tixian_shenhe_services import (
|
||
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
|
||
mark_transfer_success, release_collect_quota_for_record,
|
||
close_audit_wechat_failed, query_wechat_transfer_bill,
|
||
check_shijidaozhang_within_limit,
|
||
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
|
||
)
|
||
from .tixian_shenhe_views import process_audit_collect
|
||
|
||
from orders.models import (
|
||
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
|
||
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
|
||
Penalty, PenaltyAppealImage
|
||
)
|
||
from products.models import (
|
||
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
|
||
)
|
||
from config.models import Qunpeizhi, Szjilu
|
||
from backend.models import MerchantDailyStats
|
||
from rank.models import (
|
||
KaohePayTemp, Chenghao, ShenheJilu,
|
||
KaoheCishuFeiyong, YonghuChenghao
|
||
)
|
||
from users.models import UserShenheguan
|
||
from products.utils import update_shangpin_daily_stat
|
||
from shop.utils import update_dianpu_daily_stat
|
||
from rank.utils import create_shenhe_jilu_from_temp
|
||
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# 获取用户代理信息(用于更详细的追踪)
|
||
def huoquYonghuDaili(request):
|
||
"""
|
||
获取用户浏览器/设备信息
|
||
"""
|
||
user_agent = request.META.get('HTTP_USER_AGENT', '')
|
||
|
||
# 这里可以添加更复杂的用户代理解析
|
||
# 为简化,我们只返回原始字符串的前100个字符
|
||
return user_agent[:100] if user_agent else ''
|
||
|
||
|
||
|
||
|
||
|
||
def huoquKehuduanIP(request):
|
||
"""
|
||
强化版:安全、兼容地获取客户端真实IP地址
|
||
"""
|
||
ip = None
|
||
|
||
# 1. 从标准头部获取IP链
|
||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||
if x_forwarded_for:
|
||
# 按逗号分割,取第一个(即最原始的客户端IP)
|
||
ip = x_forwarded_for.split(',')[0].strip()
|
||
|
||
# 2. 如果上面没有,则用Nginx传递的X-Real-IP
|
||
if not ip:
|
||
ip = request.META.get('HTTP_X_REAL_IP')
|
||
|
||
# 3. 最后降级到连接地址
|
||
if not ip:
|
||
ip = request.META.get('REMOTE_ADDR')
|
||
|
||
# ===== 核心:清洗和验证IP格式 =====
|
||
if ip:
|
||
# 情况A: 处理IPv4映射的IPv6地址 (::ffff:192.168.1.1)
|
||
if ip.startswith('::ffff:'):
|
||
ip = ip[7:]
|
||
|
||
# 情况B: 处理带端口的IPv4地址 (223.91.110.123:62417)
|
||
# 这是导致您记录“三段”的最可能原因!
|
||
# 如果IP字符串中有冒号,且最后一个冒号前的部分是IPv4,则分离端口。
|
||
if ':' in ip and '.' in ip:
|
||
# 尝试按冒号分割,取第一部分
|
||
possible_ip = ip.rsplit(':', 1)[0] # 从右边分割一次
|
||
# 验证它是否是有效的IPv4地址
|
||
try:
|
||
ipaddress.IPv4Address(possible_ip) # 如果通过验证,说明格式正确
|
||
ip = possible_ip
|
||
except (ipaddress.AddressValueError, ValueError):
|
||
# 如果验证失败,说明可能不是IPv4带端口,保持原样或进行其他处理
|
||
pass
|
||
|
||
# 情况C: 验证最终结果是否为有效IPv4(可选,但推荐)
|
||
try:
|
||
# 此步骤会抛出异常,如果ip变量此时仍是“223.91.110.”这种格式
|
||
ipaddress.IPv4Address(ip)
|
||
except (ipaddress.AddressValueError, ValueError):
|
||
# 记录一个警告日志,并将IP标记为无效或存为原始字符串
|
||
print(f"警告:获取到无效或非标准IP地址格式: {ip}")
|
||
# 您可以选择在此处 return None 或存储原始字符串以供排查
|
||
|
||
return ip
|
||
|
||
|
||
|
||
|
||
class WechatMiniProgramLoginView(APIView):
|
||
"""
|
||
微信小程序登录接口
|
||
"""
|
||
throttle_classes = [AnonRateThrottle]
|
||
permission_classes = [AllowAny]
|
||
|
||
def post(self, request):
|
||
try:
|
||
code = request.data.get('code', '').strip()
|
||
if not code:
|
||
return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 获取微信openid和unionid
|
||
wechat_data = self.get_wechat_openid(code)
|
||
if not wechat_data or 'openid' not in wechat_data:
|
||
error_msg = wechat_data.get('errmsg', '微信授权失败')
|
||
return Response({'code': 2, 'msg': f'微信登录失败: {error_msg}', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
openid = wechat_data['openid']
|
||
session_key = wechat_data.get('session_key', '')
|
||
unionid = wechat_data.get('unionid', '') # 🆕【新增】获取unionid
|
||
|
||
# 获取客户端真实IP
|
||
kehuduan_ip = self.huoquKehuduanIP(request)
|
||
|
||
with transaction.atomic():
|
||
# 创建或获取用户
|
||
user_main, created = User.objects.select_for_update().get_or_create(
|
||
OpenID=openid,
|
||
defaults={'UserUID': self.shengchengYonghuID(), 'UserName': openid}
|
||
)
|
||
|
||
# 保存unionid
|
||
if unionid and unionid != user_main.UnionID:
|
||
user_main.UnionID = unionid
|
||
|
||
user_main.IP = kehuduan_ip
|
||
user_main.UserLastLoginDate = timezone.now()
|
||
user_main.save()
|
||
|
||
if created:
|
||
UserBoss.query.create(user=user_main, nickname='板板大人')
|
||
# 新用户默认分配消费者角色
|
||
from users.business_models import UserRole, Role
|
||
normal_role = Role.objects.filter(RoleName='消费者').first()
|
||
if normal_role:
|
||
UserRole.objects.create(
|
||
UserRoleUUID=uuid.uuid4().bytes,
|
||
UserUUID=user_main.UserUUID,
|
||
RoleUUID=normal_role.RoleUUID,
|
||
CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
|
||
)
|
||
|
||
# 检查扩展表
|
||
dashou_status = 0
|
||
shangjia_status = 0
|
||
guanshi_status = 0
|
||
boss_nickname = '微信用户'
|
||
|
||
try:
|
||
boss_profile = UserBoss.query.get(user=user_main)
|
||
boss_nickname = boss_profile.nickname if boss_profile.nickname else '微信用户'
|
||
except UserBoss.DoesNotExist:
|
||
pass
|
||
|
||
try:
|
||
UserDashou.query.get(user=user_main)
|
||
dashou_status = 1
|
||
except UserDashou.DoesNotExist:
|
||
dashou_status = None
|
||
|
||
try:
|
||
UserShangjia.query.get(user=user_main)
|
||
shangjia_status = 1
|
||
except UserShangjia.DoesNotExist:
|
||
shangjia_status = None
|
||
|
||
try:
|
||
UserGuanshi.query.get(user=user_main)
|
||
guanshi_status = 1
|
||
except UserGuanshi.DoesNotExist:
|
||
guanshi_status = None
|
||
|
||
|
||
try:
|
||
UserZuzhang.query.get(user=user_main)
|
||
zuzhang_status = 1
|
||
except UserZuzhang.DoesNotExist:
|
||
zuzhang_status = None
|
||
|
||
|
||
# 在检查 zuzhang 扩展表之后,插入以下代码
|
||
try:
|
||
kg = UserShenheguan.query.get(user=user_main)
|
||
kaoheguan_status = 1 if kg.zhuangtai == 1 else 0
|
||
except UserShenheguan.DoesNotExist:
|
||
kaoheguan_status = 0
|
||
|
||
# 生成JWT token
|
||
refresh = RefreshToken.for_user(user_main)
|
||
token = str(refresh.access_token)
|
||
|
||
# 计算订单数量
|
||
order_counts = self.jisuanOrderShuliang(user_main.UserUID)
|
||
|
||
# 获取群配置
|
||
group_info = self.huoquQunPeizhi()
|
||
|
||
# 返回数据
|
||
response_data = {
|
||
'token': token,
|
||
'nicheng': boss_nickname,
|
||
'uid': user_main.UserUID,
|
||
'touxiang': user_main.Avatar or '',
|
||
'shangjiastatus': shangjia_status,
|
||
'dashoustatus': dashou_status,
|
||
'guanshistatus': guanshi_status,
|
||
'zuzhangstatus': zuzhang_status,
|
||
'kaoheguanstatus': kaoheguan_status,
|
||
'dingdantiaoshu': order_counts,
|
||
'dashouqun': group_info.get('dashouqun', ''),
|
||
'dashouqunid': group_info.get('dashouqunid', ''),
|
||
'guanshiqun': group_info.get('guanshiqun', ''),
|
||
'guanshiqunid': group_info.get('guanshiqunid', '')
|
||
}
|
||
|
||
return Response({'code': 0, 'msg': '登录成功', 'data': response_data})
|
||
|
||
except Exception as e:
|
||
error_openid = locals().get('openid', 'N/A')
|
||
logger.error(f"微信登录异常 - openid: {error_openid}, 错误类型: {type(e).__name__}, 错误详情: {str(e)}",
|
||
exc_info=True)
|
||
|
||
return Response({'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def get_wechat_openid(self, code):
|
||
"""
|
||
调用微信接口获取openid和unionid
|
||
"""
|
||
try:
|
||
appid = getattr(settings, 'WEIXIN_APPID', '')
|
||
secret = getattr(settings, 'WEIXIN_SECRET', '')
|
||
|
||
if not appid or not secret:
|
||
raise ValueError('微信配置未设置')
|
||
|
||
url = 'https://api.weixin.qq.com/sns/jscode2session'
|
||
params = {'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code'}
|
||
|
||
response = requests.get(url, params=params, timeout=10)
|
||
result = response.json()
|
||
|
||
if 'openid' in result:
|
||
return {'openid': result['openid'], 'session_key': result.get('session_key', ''),
|
||
'unionid': result.get('unionid', '')}
|
||
else:
|
||
errcode = result.get('errcode', 'unknown')
|
||
errmsg = result.get('errmsg', '未知错误')
|
||
return {'errmsg': f'[{errcode}]{errmsg}'}
|
||
|
||
except requests.exceptions.Timeout:
|
||
return {'errmsg': '请求微信接口超时'}
|
||
except Exception as e:
|
||
return {'errmsg': f'请求微信接口异常: {str(e)}'}
|
||
|
||
def shengchengYonghuID(self):
|
||
"""
|
||
生成7位数字用户ID
|
||
"""
|
||
for _ in range(10):
|
||
timestamp_part = str(int(time.time()))[-5:].zfill(5)
|
||
random_part = str(random.randint(0, 99)).zfill(2)
|
||
user_id = timestamp_part + random_part
|
||
|
||
if len(user_id) == 7 and user_id.isdigit():
|
||
if not User.query.filter(UserUID=user_id).exists():
|
||
return user_id
|
||
|
||
raise Exception('生成用户ID失败')
|
||
|
||
def huoquKehuduanIP(self, request):
|
||
"""
|
||
获取客户端真实IP地址
|
||
"""
|
||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||
if x_forwarded_for:
|
||
ip = x_forwarded_for.split(',')[0].strip()
|
||
if ip:
|
||
return ip
|
||
|
||
ip = request.META.get('REMOTE_ADDR', '')
|
||
return ip if ip else '0.0.0.0'
|
||
|
||
def jisuanOrderShuliang(self, yonghuid):
|
||
"""
|
||
统计订单数量
|
||
"""
|
||
try:
|
||
if not PlatformOrderExt.query.filter(BossID=yonghuid).exists():
|
||
return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0}
|
||
|
||
dingdan_ids = PlatformOrderExt.query.filter(BossID=yonghuid).values_list('Order__id', flat=True)
|
||
|
||
results = Order.query.filter(id__in=dingdan_ids).aggregate(
|
||
daifuwu=Count(Case(When(Q(Status=1) | Q(Status=7), then=1), output_field=IntegerField())),
|
||
fuwuzhong=Count(Case(When(Status=2, then=1), output_field=IntegerField())),
|
||
yiwancheng=Count(Case(When(Status=3, then=1), output_field=IntegerField())),
|
||
yituikuan=Count(Case(When(Status=5, then=1), output_field=IntegerField()))
|
||
)
|
||
|
||
return {
|
||
'daifuwu': results['daifuwu'] or 0,
|
||
'fuwuzhong': results['fuwuzhong'] or 0,
|
||
'yiwancheng': results['yiwancheng'] or 0,
|
||
'yituikuan': results['yituikuan'] or 0
|
||
}
|
||
|
||
except Exception:
|
||
return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0}
|
||
|
||
def huoquQunPeizhi(self):
|
||
"""
|
||
获取群配置信息
|
||
"""
|
||
try:
|
||
qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
|
||
|
||
result = {
|
||
'dashouqun': '',
|
||
'dashouqunid': '',
|
||
'guanshiqun': '',
|
||
'guanshiqunid': ''
|
||
}
|
||
|
||
for config in qun_configs:
|
||
if config.id == 1:
|
||
result['dashouqun'] = config.GroupContent or ''
|
||
result['dashouqunid'] = config.GroupID or ''
|
||
elif config.id == 2:
|
||
result['guanshiqun'] = config.GroupContent or ''
|
||
result['guanshiqunid'] = config.GroupID or ''
|
||
|
||
return result
|
||
|
||
except Exception:
|
||
return {
|
||
'dashouqun': '',
|
||
'dashouqunid': '',
|
||
'guanshiqun': '',
|
||
'guanshiqunid': ''
|
||
}
|
||
|
||
# views/weixin_official.py - 完整文件
|
||
class WeixinOfficialCallbackView(APIView):
|
||
"""
|
||
微信服务号回调接口
|
||
处理用户关注、取消关注等事件
|
||
核心:有关注事件就存,不管有没有UnionID
|
||
"""
|
||
throttle_classes = []
|
||
permission_classes = [AllowAny]
|
||
authentication_classes = []
|
||
|
||
def get(self, request):
|
||
"""验证服务器配置(微信官方要求)"""
|
||
try:
|
||
signature = request.GET.get('signature', '')
|
||
timestamp = request.GET.get('timestamp', '')
|
||
nonce = request.GET.get('nonce', '')
|
||
echostr = request.GET.get('echostr', '')
|
||
|
||
token = getattr(settings, 'WEIXIN_OFFICIAL_TOKEN', '')
|
||
|
||
if not token:
|
||
logger.error("服务号Token未配置")
|
||
return HttpResponse('Token not configured', status=500)
|
||
|
||
# 验证签名
|
||
tmp_list = sorted([token, timestamp, nonce])
|
||
tmp_str = ''.join(tmp_list)
|
||
hash_str = hashlib.sha1(tmp_str.encode()).hexdigest()
|
||
|
||
if hash_str == signature:
|
||
return HttpResponse(echostr)
|
||
else:
|
||
logger.error(f"签名验证失败")
|
||
return HttpResponse('Signature verification failed', status=403)
|
||
|
||
except Exception as e:
|
||
logger.error(f"验证服务器异常: {str(e)}")
|
||
return HttpResponse('Internal Server Error', status=500)
|
||
|
||
def post(self, request):
|
||
"""处理事件推送(关注、取关)"""
|
||
try:
|
||
xml_str = request.body.decode('utf-8')
|
||
logger.info(f"收到服务号事件,原始数据(前200字符): {xml_str[:200]}")
|
||
|
||
root = ET.fromstring(xml_str)
|
||
|
||
msg_type = root.find('MsgType').text
|
||
from_user = root.find('FromUserName').text # 服务号OpenID
|
||
|
||
if msg_type == 'event':
|
||
event = root.find('Event').text
|
||
|
||
if event == 'subscribe':
|
||
return self.handle_subscribe_event(root, from_user)
|
||
elif event == 'unsubscribe':
|
||
return self.handle_unsubscribe_event(root, from_user)
|
||
else:
|
||
# 其他事件(如菜单点击)直接返回success
|
||
return HttpResponse('success')
|
||
|
||
# 非事件消息也直接返回success
|
||
return HttpResponse('success')
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理服务号事件异常: {str(e)}", exc_info=True)
|
||
# 即使异常也返回success,避免微信服务器重试
|
||
return HttpResponse('success')
|
||
|
||
def handle_subscribe_event(self, root, from_user):
|
||
"""处理用户关注事件 - 简化版:有就存,没有拉倒"""
|
||
try:
|
||
# 尝试获取UnionID(有就有,没有也没关系)
|
||
unionid_elem = root.find('UnionId')
|
||
unionid = unionid_elem.text if unionid_elem is not None else None
|
||
|
||
logger.info(f"用户关注,OpenID: {from_user}, UnionID: {unionid}")
|
||
|
||
# 核心逻辑:更新或创建记录
|
||
obj, created = OfficialAccountUser.query.update_or_create(
|
||
official_openid=from_user,
|
||
defaults={
|
||
'unionid': unionid,
|
||
'is_subscribed': True,
|
||
'subscribe_time': timezone.now()
|
||
}
|
||
)
|
||
|
||
action = "新建" if created else "更新"
|
||
logger.info(f"{action}用户记录成功: OpenID={from_user}")
|
||
|
||
# 回复欢迎消息(可选)
|
||
return self.generate_text_response(root, "感谢关注星阙网络服务!平台有新订单时您会收到通知。")
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理关注事件异常: {str(e)}", exc_info=True)
|
||
# 依然返回success,不让微信重试
|
||
return HttpResponse('success')
|
||
|
||
def handle_unsubscribe_event(self, root, from_user):
|
||
"""处理用户取消关注事件 - 简化版:标记为未订阅"""
|
||
try:
|
||
updated_count = OfficialAccountUser.query.filter(
|
||
official_openid=from_user
|
||
).update(is_subscribed=False)
|
||
|
||
if updated_count > 0:
|
||
logger.info(f"用户取消关注,已标记为未订阅: {from_user}")
|
||
else:
|
||
logger.info(f"用户取消关注,但记录不存在(无需处理): {from_user}")
|
||
|
||
return HttpResponse('success')
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理取消关注事件异常: {str(e)}", exc_info=True)
|
||
return HttpResponse('success')
|
||
|
||
def generate_text_response(self, root, content):
|
||
"""生成文本消息响应"""
|
||
try:
|
||
to_user = root.find('ToUserName').text
|
||
from_user = root.find('FromUserName').text
|
||
response_xml = f"""<xml>
|
||
<ToUserName><![CDATA[{from_user}]]></ToUserName>
|
||
<FromUserName><![CDATA[{to_user}]]></FromUserName>
|
||
<CreateTime>{int(time.time())}</CreateTime>
|
||
<MsgType><![CDATA[text]]></MsgType>
|
||
<Content><![CDATA[{content}]]></Content>
|
||
</xml>"""
|
||
return HttpResponse(response_xml, content_type='application/xml')
|
||
|
||
except Exception as e:
|
||
logger.error(f"生成回复消息异常: {str(e)}")
|
||
# 即使生成回复失败,也返回一个空的success
|
||
return HttpResponse('success')
|
||
|
||
|
||
|
||
|
||
# yonghu/views.py
|
||
|
||
class UserInfoUpdateView(APIView):
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
# 进程内缓存,防止同一code被重复调用微信(单进程安全,多进程小概率重复,但用户极少可忽略)
|
||
_phone_cache = {}
|
||
|
||
def post(self, request):
|
||
try:
|
||
user = request.user
|
||
boss = self._get_or_create_boss(user)
|
||
result = {}
|
||
|
||
# 头像
|
||
av = request.FILES.get('avatar')
|
||
if av:
|
||
ok, msg = validate_image(av)
|
||
if not ok:
|
||
return Response({'code': 1, 'msg': msg, 'data': None}, status=400)
|
||
url = self._upload_avatar(user, av)
|
||
if url:
|
||
result['touxiang'] = url
|
||
user.Avatar = url
|
||
else:
|
||
return Response({'code': 2, 'msg': '头像上传失败', 'data': None}, status=500)
|
||
|
||
# 昵称
|
||
nick = request.data.get('nicheng')
|
||
if nick:
|
||
nick = str(nick).strip()
|
||
if len(nick) > 20:
|
||
return Response({'code': 3, 'msg': '昵称过长', 'data': None}, status=400)
|
||
boss.nickname = nick
|
||
result['nicheng'] = nick
|
||
|
||
# 手机号 —— 极简可靠版
|
||
code = request.data.get('shoujihao_code')
|
||
if code:
|
||
phone = self._get_phone_safe(code)
|
||
if phone is None:
|
||
return Response({'code': 6, 'msg': '手机号获取失败,请重新授权', 'data': None}, status=400)
|
||
user.Phone = phone
|
||
user.PhoneVerified = True
|
||
result['phone'] = phone
|
||
|
||
if not result:
|
||
return Response({'code': 5, 'msg': '无更新', 'data': None}, status=400)
|
||
|
||
with transaction.atomic():
|
||
user.save()
|
||
boss.save()
|
||
return Response({'code': 0, 'msg': '更新成功', 'data': result})
|
||
|
||
except Exception as e:
|
||
logger.exception("系统异常")
|
||
return Response({'code': 99, 'msg': str(e), 'data': None}, status=500)
|
||
|
||
# ---------- 安全手机号(内存去重) ----------
|
||
def _get_phone_safe(self, code):
|
||
# 如果已经处理过这个code,直接返回结果
|
||
if code in self._phone_cache:
|
||
cached = self._phone_cache[code]
|
||
return cached if cached != '__FAIL__' else None
|
||
|
||
# 标记处理中(简单防重入)
|
||
self._phone_cache[code] = '__PROCESSING__'
|
||
try:
|
||
phone = self._call_wechat(code)
|
||
self._phone_cache[code] = phone
|
||
return phone
|
||
except Exception as e:
|
||
logger.error(f"微信调用失败: {str(e)}")
|
||
self._phone_cache[code] = '__FAIL__'
|
||
return None
|
||
|
||
# ---------- 微信接口调用(每次全新token) ----------
|
||
def _call_wechat(self, code):
|
||
token = self._get_new_token()
|
||
url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
|
||
resp = requests.post(url, json={'code': code}, timeout=10)
|
||
data = resp.json()
|
||
errcode = data.get('errcode')
|
||
if errcode == 0:
|
||
return data['phone_info']['purePhoneNumber']
|
||
# 即使刚拿的token也可能意外失效,重试一次
|
||
if errcode == 40001:
|
||
token = self._get_new_token()
|
||
url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
|
||
resp = requests.post(url, json={'code': code}, timeout=10)
|
||
data = resp.json()
|
||
if data.get('errcode') == 0:
|
||
return data['phone_info']['purePhoneNumber']
|
||
raise Exception(f"微信错误(errcode={errcode}): {data.get('errmsg')}")
|
||
|
||
# ---------- 获取全新 access_token(不缓存,每次调微信) ----------
|
||
def _get_new_token(self):
|
||
appid = getattr(settings, 'WEIXIN_APPID', '')
|
||
secret = getattr(settings, 'WEIXIN_SECRET', '')
|
||
if not appid or not secret:
|
||
raise Exception('缺少微信配置')
|
||
resp = requests.get('https://api.weixin.qq.com/cgi-bin/token', params={
|
||
'grant_type': 'client_credential',
|
||
'appid': appid,
|
||
'secret': secret
|
||
})
|
||
data = resp.json()
|
||
token = data.get('access_token')
|
||
if token:
|
||
return token
|
||
raise Exception(f"获取access_token失败: {data}")
|
||
|
||
# ---------- 工具 ----------
|
||
def _get_or_create_boss(self, user):
|
||
try:
|
||
return UserBoss.query.get(user=user)
|
||
except UserBoss.DoesNotExist:
|
||
return UserBoss.query.create(user=user, nickname='微信用户')
|
||
|
||
def _upload_avatar(self, user, file):
|
||
try:
|
||
uid = user.UserUID
|
||
if not uid: return None
|
||
old = user.Avatar
|
||
if old: delete_from_oss(old)
|
||
ext = os.path.splitext(file.name)[1] or '.jpg'
|
||
name = f"{uuid.uuid4().hex}{ext}"
|
||
path = f"avatar/{uid}/{name}"
|
||
return path if upload_to_oss(file, path) else None
|
||
except:
|
||
return None
|
||
|
||
|
||
|
||
# yonghu/views.py 或 yonghu/api_views.py
|
||
|
||
class DashouXinxiAPIView(APIView):
|
||
"""
|
||
获取打手信息接口
|
||
请求方式:POST
|
||
认证方式:JWT Token
|
||
返回:打手基本信息、资产信息、会员信息,金牌打手额外返回专属字段
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 1. 获取当前登录用户
|
||
current_user = request.user
|
||
yonghuid = current_user.UserUID
|
||
|
||
# 2. 查询打手扩展表
|
||
try:
|
||
dashou_profile = current_user.DashouProfile
|
||
except UserDashou.DoesNotExist:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '您不是打手身份',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 3. 构建基础信息响应(原有字段完全保留)
|
||
data = {
|
||
'dashounicheng': dashou_profile.nicheng or '',
|
||
'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
|
||
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
|
||
'dashouzhuangtai': dashou_profile.zhuangtai,
|
||
'yongjin': str(dashou_profile.yue) if dashou_profile.yue is not None else '0.00',
|
||
'zonge': str(dashou_profile.zonge) if dashou_profile.zonge is not None else '0.00',
|
||
'yajin': str(dashou_profile.yajin) if dashou_profile.yajin is not None else '0.00',
|
||
'jiedanZongliang':dashou_profile.jiedanzongliang if dashou_profile.jiedanzongliang is not None else '0',
|
||
'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00',
|
||
'chengjiaoliang': dashou_profile.chengjiaozongliang,
|
||
'jifen': dashou_profile.jifen,
|
||
'chenghao': dashou_profile.chenghao or '',
|
||
}
|
||
|
||
# 4. 查询会员信息(原有逻辑)
|
||
huiyuan_list = []
|
||
try:
|
||
huiyuan_records = Huiyuangoumai.query.filter(
|
||
yonghu_id=yonghuid
|
||
).select_related()
|
||
|
||
for record in huiyuan_records:
|
||
# 检查会员是否过期
|
||
is_expired = record.jiance_shifou_daoqi()
|
||
|
||
# 只返回未过期的会员(huiyuan_zhuangtai = 1)
|
||
if record.huiyuan_zhuangtai == 1:
|
||
huiyuan_list.append({
|
||
'huiyuanid': record.huiyuan_id,
|
||
'huiyuanming': record.jieshao or f"会员{record.huiyuan_id}",
|
||
'daoqi': record.daoqi_time
|
||
})
|
||
except Exception as e:
|
||
# 会员信息查询失败不影响主要功能,返回空列表
|
||
huiyuan_list = []
|
||
|
||
data['clumber'] = huiyuan_list
|
||
|
||
# 5. 如果是金牌打手,添加专属字段
|
||
if dashou_profile.chenghao == "金牌打手":
|
||
data.update({
|
||
'jiedanZongliang': dashou_profile.jiedanzongliang, # 接单总量
|
||
'jinriJiedan': str(dashou_profile.jinrishouyi) if dashou_profile.jinrishouyi is not None else '0.00', # 今日接单金额
|
||
'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', # 最近提现金额(今日已提现)
|
||
})
|
||
|
||
# 6. 返回成功响应
|
||
return Response({
|
||
'code': 200,
|
||
'message': '获取成功',
|
||
'data': data
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
# 生产环境应使用 logging 记录错误,而不是 print
|
||
# logger.error(f"获取打手信息失败: {e}", exc_info=True)
|
||
return Response({
|
||
'code': 500,
|
||
'message': f'服务器内部错误: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
class HuoQuYaoQingRenView(APIView):
|
||
"""
|
||
获取当前打手的邀请人信息
|
||
POST /yonghu/hqwdyqr
|
||
Headers: Authorization: Bearer <token>
|
||
返回邀请人的用户ID、昵称(来自老板扩展表)、头像相对URL
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
current_user = request.user # 当前登录用户
|
||
yonghuid = current_user.UserUID # 7位用户ID
|
||
|
||
# 1. 校验打手身份及账号状态(只检查账号状态,不检查接单状态)
|
||
try:
|
||
dashou = UserDashou.query.get(user=current_user)
|
||
if dashou.zhanghaozhuangtai != 1:
|
||
return Response({
|
||
'code': 403,
|
||
'message': '您的账号已被封禁,无法查看邀请人',
|
||
'data': None
|
||
})
|
||
except UserDashou.DoesNotExist:
|
||
return Response({
|
||
'code': 404,
|
||
'message': '您还不是打手,无法查看邀请人',
|
||
'data': None
|
||
})
|
||
|
||
# 2. 获取邀请人ID
|
||
yaoqingren_id = dashou.yaoqingren
|
||
if not yaoqingren_id:
|
||
return Response({
|
||
'code': 404,
|
||
'message': '您没有邀请人',
|
||
'data': None
|
||
})
|
||
|
||
# 3. 查询邀请人主表(头像)
|
||
try:
|
||
inviter_main = User.query.get(UserUID=yaoqingren_id)
|
||
touxiang = inviter_main.Avatar or ''
|
||
except User.DoesNotExist:
|
||
return Response({
|
||
'code': 404,
|
||
'message': '邀请人信息不存在',
|
||
'data': None
|
||
})
|
||
|
||
# 4. 查询老板扩展表(昵称),如果没有则使用空字符串
|
||
nicheng = ''
|
||
try:
|
||
boss = UserBoss.query.get(user=inviter_main)
|
||
nicheng = boss.nickname or ''
|
||
except UserBoss.DoesNotExist:
|
||
pass # 邀请人可能不是老板,昵称留空
|
||
|
||
response_data = {
|
||
'uid': yaoqingren_id,
|
||
'nicheng': nicheng,
|
||
'touxiang': touxiang # 相对路径,前端自行拼接
|
||
}
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'message': '获取成功',
|
||
'data': response_data
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f'获取邀请人失败: {str(e)}', exc_info=True)
|
||
return Response({
|
||
'code': 500,
|
||
'message': '系统繁忙,请稍后重试',
|
||
'data': None
|
||
})
|
||
|
||
|
||
|
||
|
||
# yonghu/views.py 或 yonghu/api_views.py
|
||
|
||
class ZaixianZhuangtaiAPIView(APIView):
|
||
"""
|
||
更改打手在线状态接口
|
||
请求方式:POST
|
||
认证方式:JWT Token
|
||
请求参数:status(0=离线,1=在线)
|
||
返回:更新后的状态信息
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 1. 获取请求参数
|
||
new_status = request.data.get('status')
|
||
|
||
if new_status is None:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '缺少状态参数',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 转换为整数,确保是0或1
|
||
try:
|
||
new_status = int(new_status)
|
||
if new_status not in [0, 1]:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '状态参数必须是0或1',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
except ValueError:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '状态参数必须是整数',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 2. 获取当前登录用户
|
||
current_user = request.user
|
||
yonghuid = current_user.UserUID
|
||
|
||
|
||
|
||
# 3. 查询打手扩展表
|
||
try:
|
||
dashou_profile = current_user.DashouProfile
|
||
except UserDashou.DoesNotExist:
|
||
|
||
return Response({
|
||
'code': 400,
|
||
'message': '您不是打手身份',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 4. 检查账号状态(是否被封禁)
|
||
if dashou_profile.zhanghaozhuangtai != 1:
|
||
|
||
return Response({
|
||
'code': 403,
|
||
'message': '账号状态异常,无法更改在线状态',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 5. 更新在线状态
|
||
old_status = dashou_profile.zaixianzhuangtai
|
||
|
||
# 如果状态没有变化,直接返回成功
|
||
if old_status == new_status:
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'message': '状态未变化',
|
||
'data': {
|
||
'zaixianzhuangtai': new_status,
|
||
'status_text': '在线' if new_status == 1 else '离线'
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
# 更新状态
|
||
dashou_profile.zaixianzhuangtai = new_status
|
||
dashou_profile.save()
|
||
|
||
|
||
|
||
# 6. 返回成功响应
|
||
return Response({
|
||
'code': 200,
|
||
'message': '状态更新成功',
|
||
'data': {
|
||
'zaixianzhuangtai': new_status,
|
||
'status_text': '在线' if new_status == 1 else '离线'
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
|
||
return Response({
|
||
'code': 500,
|
||
'message': f'服务器内部错误: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
class GuanshiXinxiView(APIView):
|
||
"""
|
||
获取管事信息接口
|
||
路径:/yonghu/guanshixinxi
|
||
方法:POST
|
||
权限:JWT认证
|
||
功能:获取当前管事的详细信息,包括基本信息、分红信息以及已充值打手数量
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 获取当前用户(通过JWT认证)
|
||
current_user = request.user
|
||
|
||
# 查询管事扩展表
|
||
try:
|
||
guanshi_profile = UserGuanshi.query.get(user=current_user)
|
||
|
||
# 🔥【新增】查询已充值打手数量(从分红表中统计)
|
||
# Gsfenhong 表中 guanshi 字段存储管事ID(yonghuid)
|
||
yichongzhi_count = Gsfenhong.query.filter(guanshi=current_user.UserUID).count()
|
||
|
||
# 返回管事信息
|
||
return Response({
|
||
'code': 200,
|
||
'msg': '获取成功',
|
||
'data': {
|
||
'gszhstatus': guanshi_profile.zhuangtai, # 管事账号状态
|
||
'yaoqingzongshu': guanshi_profile.yaogingshuliang, # 邀请打手总数
|
||
'fenyongzonge': float(guanshi_profile.chongzhifenrun), # 充值分佣总额
|
||
'fenyongtixian': float(guanshi_profile.yue), # 可提现余额
|
||
'yichongzhiDashou': yichongzhi_count, # 已充值打手数量
|
||
}
|
||
})
|
||
|
||
except UserGuanshi.DoesNotExist:
|
||
# 用户不是管事,返回错误
|
||
return Response({
|
||
'code': 400,
|
||
'msg': '您还不是管事,请先注册',
|
||
'data': None
|
||
}, status=400)
|
||
|
||
except Exception as e:
|
||
# 捕获所有异常,返回错误信息并记录日志
|
||
logger.error(f"获取管事信息异常: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 500,
|
||
'msg': f'获取信息失败: {str(e)}',
|
||
'data': None
|
||
}, status=500)
|
||
|
||
|
||
|
||
|
||
# yonghu/views.py
|
||
class ShangJiaXinXiView(APIView):
|
||
"""
|
||
获取商家信息接口 (POST /yonghu/shangjiaxinxi)
|
||
权限:需JWT认证
|
||
返回:商家基本信息 + 称号列表 + 今日/本月统计
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 获取商家扩展资料
|
||
shangjia = request.user.ShopProfile
|
||
except ObjectDoesNotExist:
|
||
return Response({
|
||
'code': 4001,
|
||
'msg': '您还未注册成为商家或商家资料不存在',
|
||
'data': None
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
user = request.user
|
||
yonghuid = user.UserUID
|
||
today = timezone.now().date()
|
||
this_month_start = today.replace(day=1)
|
||
|
||
# ------------------- 1. 从 UserShangjia 获取基础字段 -------------------
|
||
response_data = {
|
||
'sjzhzhuangtai': shangjia.zhuangtai, # 商家账号状态
|
||
'fadanzong': shangjia.fabu, # 发单总量(累计)
|
||
'tuikuanzong': shangjia.tuikuan, # 退款总量(累计)
|
||
'sjyue': float(shangjia.yue), # 商家余额
|
||
# 'riliushui' 和 'yueliushui' 将从统计表计算,不再使用原字段
|
||
}
|
||
|
||
# ------------------- 2. 获取称号列表(仅限商家称号) -------------------
|
||
chenghao_list = []
|
||
# 查询当前用户的所有称号关联
|
||
yonghu_chenghao_qs = YonghuChenghao.query.filter(yonghu=user).select_related('chenghao')
|
||
for yc in yonghu_chenghao_qs:
|
||
chenghao_obj = yc.chenghao
|
||
# 只保留类型为“商家”的称号
|
||
if chenghao_obj.leixing == 'shangjia':
|
||
chenghao_list.append({
|
||
'mingcheng': chenghao_obj.mingcheng,
|
||
'texiao_json': chenghao_obj.texiao_miaoshu, # 前端需要的特效描述
|
||
# 如果还需要其他字段(如图标URL)可在此添加,打手端有 texiao_json
|
||
})
|
||
response_data['chenghao_list'] = chenghao_list
|
||
|
||
# ------------------- 3. 从 MerchantDailyStats 获取今日统计 -------------------
|
||
try:
|
||
today_stat = MerchantDailyStats.query.get(MerchantID=yonghuid, Date=today)
|
||
jinri_paifa_dingdan = today_stat.AssignedOrderCount
|
||
jinri_paifa_jine = float(today_stat.AssignedAmount)
|
||
jinri_tuikuan_dingdan = today_stat.RefundOrderCount
|
||
except MerchantDailyStats.DoesNotExist:
|
||
jinri_paifa_dingdan = 0
|
||
jinri_paifa_jine = 0.00
|
||
jinri_tuikuan_dingdan = 0
|
||
|
||
response_data['jinripaidan'] = jinri_paifa_dingdan # 今日派单数(新增)
|
||
response_data['jinrituikuan'] = jinri_tuikuan_dingdan # 今日退款数(新增)
|
||
response_data['riliushui'] = jinri_paifa_jine # 今日流水(派发总金额)
|
||
|
||
# ------------------- 4. 从 MerchantDailyStats 获取本月统计 -------------------
|
||
month_stats = MerchantDailyStats.query.filter(
|
||
MerchantID=yonghuid,
|
||
Date__gte=this_month_start,
|
||
Date__lte=today
|
||
).aggregate(
|
||
total_paifa_jine=Sum('AssignedAmount'),
|
||
total_paifa_dingdan=Sum('AssignedOrderCount')
|
||
)
|
||
yue_paifa_jine = float(month_stats.get('total_paifa_jine') or 0.00)
|
||
# yue_paifa_dingdan = month_stats.get('total_paifa_dingdan') or 0 # 如果前端不需要可不加
|
||
|
||
response_data['yueliushui'] = yue_paifa_jine # 今月流水(派发总金额)
|
||
|
||
# (可选)如果前端还需要今月派单总数,取消下面注释
|
||
# response_data['jinyuepaidan'] = yue_paifa_dingdan
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'msg': '获取商家信息成功',
|
||
'data': response_data
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class BossBiaoshiView(APIView):
|
||
"""
|
||
获取用户的GoEasy标识 - 极简实现
|
||
POST /yonghu/bossbiaoshi
|
||
需要JWT Token认证
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 直接从request.user获取用户
|
||
user = request.user
|
||
|
||
# 2. 获取yonghuid(确保用户已登录)
|
||
if not user or not hasattr(user, 'UserUID'):
|
||
return Response({
|
||
'code': 401,
|
||
'msg': '用户未认证或用户信息不完整'
|
||
}, status=401)
|
||
|
||
# 3. 生成标识:Boss + yonghuid(与前端约定)
|
||
boss_biaoshi = f"Boss{user.UserUID}"
|
||
|
||
# 4. 返回给前端
|
||
return Response({
|
||
'code': 200,
|
||
'msg': '获取成功',
|
||
'data': {
|
||
'boss_biaoshi': boss_biaoshi,
|
||
'yonghuid': user.UserUID,
|
||
'user_type': user.UserType
|
||
}
|
||
})
|
||
|
||
|
||
"""
|
||
阿龙电竞陪玩平台 - 邀请码相关API接口
|
||
"""
|
||
|
||
class YaoqingmaView(APIView):
|
||
"""
|
||
获取或生成管事邀请码
|
||
访问路径: /api/yonghu/yaoqingma/
|
||
请求方法: GET
|
||
权限要求: JWT Token认证通过的用户
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
"""
|
||
获取当前用户的邀请码
|
||
逻辑:
|
||
1. 通过JWT获取当前用户 (request.user)
|
||
2. 反向查询UserGuanshi扩展表
|
||
3. 验证管事状态(zhuangtai=1)
|
||
4. 已有邀请码直接返回,没有则生成并保存
|
||
"""
|
||
# 获取当前认证用户
|
||
current_user = request.user
|
||
|
||
# 1. 使用反向查询获取管事扩展信息 (性能最优方式)
|
||
try:
|
||
guanshi_profile = current_user.GuanshiProfile
|
||
except UserGuanshi.DoesNotExist:
|
||
# 用户不是管事,没有扩展表记录
|
||
return Response({
|
||
'code': 403,
|
||
'message': '当前用户不是管事身份,无法生成邀请码',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 2. 验证管事状态 (zhuangtai 必须等于 1)
|
||
if guanshi_profile.zhuangtai != 1:
|
||
return Response({
|
||
'code': 403,
|
||
'message': '管事账号状态异常,无法生成邀请码',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 3. 检查是否已存在邀请码
|
||
existing_yaoqingma = guanshi_profile.yaoqingma
|
||
|
||
# 如果存在且非空,直接返回
|
||
if existing_yaoqingma and existing_yaoqingma.strip() != '':
|
||
return Response({
|
||
'code': 200,
|
||
'message': '成功获取现有邀请码',
|
||
'data': {
|
||
'yaoqingma': existing_yaoqingma,
|
||
'is_new': False # 标记是否为新生成的
|
||
}
|
||
})
|
||
|
||
# 4. 生成新的邀请码 (使用用户ID字符串)
|
||
# 注意: 这里使用yonghuid,不是主键id,如你要求的"123456"这种形式
|
||
yonghuid_str = str(current_user.UserUID)
|
||
new_yaoqingma = CreateInvitationCode(yonghuid_str)
|
||
|
||
# 5. 验证生成的邀请码格式
|
||
if not VerifyInvitationCode(new_yaoqingma):
|
||
return Response({
|
||
'code': 500,
|
||
'message': '邀请码生成失败,格式验证错误',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
# 6. 保存到数据库 (使用事务确保数据一致性)
|
||
try:
|
||
with transaction.atomic():
|
||
guanshi_profile.yaoqingma = new_yaoqingma
|
||
guanshi_profile.save()
|
||
except Exception as e:
|
||
# 这里可以记录日志
|
||
return Response({
|
||
'code': 500,
|
||
'message': f'保存邀请码到数据库失败: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
# 7. 返回新生成的邀请码
|
||
return Response({
|
||
'code': 200,
|
||
'message': '成功生成新的邀请码',
|
||
'data': {
|
||
'yaoqingma': new_yaoqingma,
|
||
'is_new': True # 标记是新生成的
|
||
}
|
||
})
|
||
|
||
|
||
|
||
"""
|
||
阿龙电竞陪玩平台 - 打手注册及相关API接口
|
||
"""
|
||
|
||
|
||
class DashouZhuceView(APIView):
|
||
"""
|
||
打手注册接口 (通过管事邀请码)
|
||
访问路径: /api/yonghu/dashouzhuce/
|
||
请求方法: POST
|
||
权限要求: JWT Token认证通过的用户
|
||
请求体: { "inviteCode": "AbC123..." }
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 获取并验证邀请码参数
|
||
yaoqingma = request.data.get('inviteCode')
|
||
if not yaoqingma:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '邀请码不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
yaoqingma = yaoqingma.strip()
|
||
if len(yaoqingma) > 100:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '邀请码长度超过限制',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
current_user = request.user
|
||
# 注意:这里request.user是User实例
|
||
|
||
# 2. 检查用户是否已是打手 (使用反向查询,性能最优)
|
||
try:
|
||
dashou_profile = current_user.DashouProfile
|
||
# 用户已是打手,直接返回现有信息
|
||
return self.fanhuiXianyouDashouXinxi(dashou_profile, current_user)
|
||
except UserDashou.DoesNotExist:
|
||
# 用户不是打手,继续注册流程
|
||
pass
|
||
|
||
# 3. 根据邀请码查找对应的管事
|
||
try:
|
||
# 使用select_related一次性获取管事及其关联的主表用户信息,性能最高
|
||
guanshi_profile = UserGuanshi.query.select_related('user').get(yaoqingma=yaoqingma)
|
||
except UserGuanshi.DoesNotExist:
|
||
return Response({
|
||
'code': 404,
|
||
'message': '邀请码无效或不存在',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 4. 验证管事状态
|
||
|
||
# 5. 核心:在数据库事务中创建用户的所有身份
|
||
try:
|
||
with transaction.atomic():
|
||
# 获取管事的用户ID (yonghuid)
|
||
guanshi_yonghuid = guanshi_profile.user.UserUID
|
||
|
||
# 5.1 创建打手扩展表
|
||
dashou_profile = UserDashou.query.create(
|
||
user=current_user,
|
||
nicheng='大手子', # 默认昵称
|
||
chenghao='普通大手', # 默认称号
|
||
yaoqingren=guanshi_yonghuid, # 设置邀请人ID
|
||
# 其他字段全部使用模型定义的默认值
|
||
)
|
||
|
||
# 5.2 创建商家扩展表
|
||
try:
|
||
boss_profile = current_user.BossProfile
|
||
if boss_profile.nickname:
|
||
boss_nickname = boss_profile.nickname
|
||
except:
|
||
pass
|
||
|
||
|
||
# 5.3 创建管事扩展表
|
||
|
||
# 5.4 更新主表的用户类型 (如果需要,可以设为'dashou',或保持原逻辑)
|
||
# current_user.UserType = 'dashou' # 根据你的业务逻辑决定
|
||
# current_user.save()
|
||
|
||
# 5.5 更新原管事的邀请数量 (使用行锁防止并发问题)
|
||
# 重新查询并锁定该行记录
|
||
locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
|
||
locked_guanshi.yaogingshuliang += 1
|
||
locked_guanshi.save()
|
||
|
||
# ========== 新增:管事每日统计(邀请打手) ==========
|
||
try:
|
||
update_guanshi_daily_by_action(
|
||
yonghuid=guanshi_yonghuid,
|
||
action=1, # 1 = 邀请打手
|
||
amount=Decimal('0.00')
|
||
)
|
||
logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}")
|
||
except Exception as e:
|
||
logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}")
|
||
|
||
except Exception as e:
|
||
|
||
# 事务会自动回滚
|
||
# 这里可以记录更详细的日志
|
||
return Response({
|
||
'code': 500,
|
||
'message': f'注册过程中发生系统错误: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
# 6. 注册成功,返回信息 (新注册用户没有会员记录,clumber返回空列表)
|
||
fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=True)
|
||
return Response({
|
||
'code': 200,
|
||
'message': '注册成功!已开通打手、管事身份。',
|
||
'data': fanhui_data
|
||
})
|
||
|
||
def fanhuiXianyouDashouXinxi(self, dashou_profile, current_user):
|
||
"""处理已是打手的用户,返回其现有信息"""
|
||
fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=False)
|
||
return Response({
|
||
'code': 200,
|
||
'message': '您已是打手,返回当前信息。',
|
||
'data': fanhui_data
|
||
})
|
||
|
||
def zhuangbeiFanhuiShuju(self, dashou_profile, current_user, is_new=False):
|
||
"""准备返回给前端的打手数据,字段名严格匹配前端"""
|
||
|
||
|
||
|
||
# 构建基础信息
|
||
data = {
|
||
'dashounicheng': dashou_profile.nicheng,
|
||
'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
|
||
'yongjin': str(dashou_profile.yue), # Decimal转字符串
|
||
'zonge': str(dashou_profile.zonge),
|
||
'yajin': str(dashou_profile.yajin),
|
||
'chenghao': dashou_profile.chenghao,
|
||
'jinfen': dashou_profile.jifen,
|
||
'chengjiaoliang': dashou_profile.chengjiaozongliang,
|
||
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
|
||
'dashouzhuangtai': dashou_profile.zhuangtai,
|
||
}
|
||
|
||
# 查询并处理会员列表 (clumber)
|
||
# 使用当前用户的yonghuid进行查询
|
||
huiyuan_goumai_list = []
|
||
if not is_new: # 如果是新用户,直接返回空列表,无需查询
|
||
goumai_records = Huiyuangoumai.query.filter(yonghu_id=current_user.UserUID)
|
||
for record in goumai_records:
|
||
# 调用方法检查是否到期,此方法内部会更新状态
|
||
shifou_daoqi = record.jiance_shifou_daoqi()
|
||
huiyuan_goumai_list.append({
|
||
'huiyuan_id': record.huiyuan_id,
|
||
'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
|
||
'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
|
||
'shifou_daoqi': shifou_daoqi,
|
||
'jieshao': record.jieshao
|
||
})
|
||
|
||
data['clumber'] = huiyuan_goumai_list
|
||
return data
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class GuanshiYaoqingDashouListView(APIView):
|
||
"""
|
||
管事获取已邀请打手列表(支持筛选、搜索、分页)
|
||
POST /api/yonghu/gshqyqds/
|
||
Headers: Authorization: Bearer <token>
|
||
Body:
|
||
{
|
||
"page": 1, // 页码,从1开始
|
||
"pageSize": 30, // 每页条数,最大100
|
||
"keyword": "", // 搜索关键词(用户ID或昵称)
|
||
"zhanghaozhuangtai": 1, // 账号状态:1=正常,2=封禁(可选)
|
||
"zaixianzhuangtai": 1 // 在线状态:1=在线,2=离线(可选)
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
current_user = request.user
|
||
guanshi_yonghuid = current_user.UserUID
|
||
|
||
# 1. 分页参数
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('pageSize', 30))
|
||
if page < 1 or page_size < 1:
|
||
return Response({'code': 400, 'message': '分页参数必须大于0'}, status=400)
|
||
if page_size > 100:
|
||
page_size = 100
|
||
|
||
# 2. 筛选参数
|
||
keyword = request.data.get('keyword', '').strip()
|
||
zhanghaozhuangtai = request.data.get('zhanghaozhuangtai') # 可选:1=正常,2=封禁
|
||
zaixianzhuangtai = request.data.get('zaixianzhuangtai') # 可选:1=在线,2=离线
|
||
|
||
# 3. 基础查询:当前管事邀请的所有打手
|
||
base_qs = UserDashou.query.select_related('user').filter(
|
||
yaoqingren=guanshi_yonghuid
|
||
)
|
||
|
||
# 4. 全局统计(不受筛选影响,直接基于 base_qs)
|
||
total_all = base_qs.count()
|
||
zaixian_all = base_qs.filter(zaixianzhuangtai=1).count()
|
||
zhengchang_all = base_qs.filter(zhanghaozhuangtai=1).count()
|
||
fengjin_all = total_all - zhengchang_all
|
||
|
||
# 5. 应用筛选条件
|
||
if keyword:
|
||
base_qs = base_qs.filter(
|
||
Q(nicheng__icontains=keyword) | Q(user__UserUID__icontains=keyword)
|
||
)
|
||
if zhanghaozhuangtai is not None:
|
||
try:
|
||
val = int(zhanghaozhuangtai)
|
||
if val == 1:
|
||
base_qs = base_qs.filter(zhanghaozhuangtai=1)
|
||
else:
|
||
base_qs = base_qs.exclude(zhanghaozhuangtai=1)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
if zaixianzhuangtai is not None:
|
||
try:
|
||
val = int(zaixianzhuangtai)
|
||
if val == 1:
|
||
base_qs = base_qs.filter(zaixianzhuangtai=1)
|
||
else:
|
||
base_qs = base_qs.exclude(zaixianzhuangtai=1)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# 6. 排序和分页
|
||
base_qs = base_qs.order_by('-CreateTime')
|
||
paginator = Paginator(base_qs, page_size)
|
||
try:
|
||
page_obj = paginator.page(page)
|
||
except EmptyPage:
|
||
page_obj = []
|
||
has_more = False
|
||
filtered_total = paginator.count if paginator.count else 0
|
||
else:
|
||
has_more = page_obj.has_next()
|
||
filtered_total = paginator.count
|
||
|
||
# 7. 构建打手列表
|
||
dashou_list = []
|
||
for dashou in page_obj:
|
||
user_main = dashou.user
|
||
dashou_list.append({
|
||
'uid': user_main.UserUID,
|
||
'nicheng': dashou.nicheng or '打手',
|
||
'zaixianzhuangtai': dashou.zaixianzhuangtai,
|
||
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
|
||
'touxiang': user_main.Avatar or '',
|
||
'jiedanzongliang': dashou.jiedanzongliang,
|
||
'chengjiaozongliang': dashou.chengjiaozongliang,
|
||
'tuikuanliang': dashou.tuikuanliang,
|
||
'yue': str(dashou.zonge),
|
||
})
|
||
|
||
# 8. 获取管事邀请总数(来自管事扩展表)
|
||
try:
|
||
yaoqingzongshu = current_user.GuanshiProfile.yaogingshuliang
|
||
except Exception:
|
||
yaoqingzongshu = total_all
|
||
|
||
response_data = {
|
||
'dashouList': dashou_list,
|
||
'yaoqingzongshu': yaoqingzongshu, # 邀请总数(全局)
|
||
'totalCount': filtered_total, # 当前筛选条件下的总数
|
||
'zaixianCount': zaixian_all, # 全局在线
|
||
'zhengchangCount': zhengchang_all, # 全局正常
|
||
'fengjinCount': fengjin_all, # 全局封禁
|
||
'currentPage': page,
|
||
'pageSize': page_size,
|
||
'hasMore': has_more,
|
||
}
|
||
|
||
return Response({'code': 200, 'message': '获取成功', 'data': response_data})
|
||
|
||
except ValueError:
|
||
return Response({'code': 400, 'message': '分页参数格式错误'}, status=400)
|
||
except Exception as e:
|
||
logger.error(f'获取打手列表失败: {str(e)}', exc_info=True)
|
||
return Response({'code': 500, 'message': '系统错误'}, status=500)
|
||
|
||
|
||
|
||
|
||
|
||
class TixianXinxiHuoquView(APIView):
|
||
"""
|
||
获取用户提现收款信息接口
|
||
返回用户已设置的手机号、收款码、收款账号
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
"""
|
||
处理获取收款信息的POST请求
|
||
直接返回用户当前的收款设置
|
||
"""
|
||
try:
|
||
# 从request.user获取当前用户
|
||
user_main = request.user
|
||
|
||
# 🔥【修改开始 - 使用Decimal,字段名是Rate】
|
||
# 查询打手提现费率 (Platform='5')
|
||
dashou_rate_obj = CommissionRate.query.filter(Platform='5').first()
|
||
dashou_rate = decimal.Decimal(str(dashou_rate_obj.Rate)) if dashou_rate_obj else decimal.Decimal('0.00')
|
||
|
||
# 查询管事提现费率 (Platform='6')
|
||
guanshi_rate_obj = CommissionRate.query.filter(Platform='6').first()
|
||
guanshi_rate = decimal.Decimal(str(guanshi_rate_obj.Rate)) if guanshi_rate_obj else decimal.Decimal('0.00')
|
||
# 🔥【修改结束】
|
||
|
||
# 构建返回数据
|
||
response_data = {
|
||
'txdianhua': user_main.Phone if user_main.Phone else '',
|
||
'txtupian': user_main.PaymentQRCode if user_main.PaymentQRCode else '',
|
||
'txzh': user_main.PaymentAccount if user_main.PaymentAccount else '',
|
||
# 🔥【修改开始】
|
||
'dashou_rate': dashou_rate, # 打手提现费率
|
||
'guanshi_rate': guanshi_rate, # 管事提现费率
|
||
# 🔥【修改结束】
|
||
}
|
||
|
||
# 返回成功响应
|
||
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 ShoukuanXinxiShangchuanView(APIView):
|
||
"""
|
||
上传用户收款信息接口
|
||
支持同时上传手机号、收款账号、收款码图片
|
||
使用multipart/form-data格式
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
parser_classes = (MultiPartParser,FormParser,JSONParser)
|
||
|
||
def post(self, request):
|
||
"""
|
||
处理上传收款信息的POST请求
|
||
验证并保存用户的收款设置
|
||
"""
|
||
try:
|
||
# 获取当前用户
|
||
user_main = request.user
|
||
yonghuid = user_main.UserUID
|
||
|
||
# 准备响应数据
|
||
response_data = {}
|
||
|
||
# 1. 验证必填字段:手机号必须要有
|
||
txdianhua = request.data.get('txdianhua', '').strip()
|
||
if not txdianhua:
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '收款人电话不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证手机号格式(11位数字)
|
||
if not txdianhua.isdigit() or len(txdianhua) != 11:
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '请输入11位有效的手机号码',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 2. 获取收款账号(可能为空)
|
||
txzh = request.data.get('txzh', '').strip()
|
||
|
||
# 3. 处理收款码图片上传(如果有)
|
||
txtupian_file = request.FILES.get('file') # 注意:前端upload.js使用'file'作为字段名
|
||
txtupian_url = ''
|
||
|
||
if txtupian_file:
|
||
# 验证图片
|
||
is_valid, error_msg = validate_image(txtupian_file)
|
||
if not is_valid:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': f'图片验证失败: {error_msg}',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 上传收款码图片
|
||
txtupian_url = self.handle_shoukuanma_upload(user_main, txtupian_file)
|
||
if txtupian_url:
|
||
response_data['txtupian'] = txtupian_url
|
||
else:
|
||
return Response({
|
||
'code': 4,
|
||
'msg': '收款码上传失败',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
else:
|
||
response_data['txtupian'] = ''
|
||
|
||
# 4. 验证收款账号和收款码至少有一个
|
||
if not txzh and not txtupian_url:
|
||
return Response({
|
||
'code': 5,
|
||
'msg': '请填写收款账号或上传收款码',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 5. 保存所有信息到数据库
|
||
with transaction.atomic():
|
||
user_main.Phone = txdianhua
|
||
user_main.PaymentAccount = txzh if txzh else ''
|
||
|
||
# 只有上传了图片才更新zhifu字段
|
||
if txtupian_url:
|
||
user_main.PaymentQRCode = txtupian_url
|
||
|
||
user_main.save()
|
||
|
||
# 6. 构建完整的返回数据
|
||
response_data['txdianhua'] = txdianhua
|
||
response_data['txzh'] = txzh if txzh else ''
|
||
|
||
# 7. 返回成功响应
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '收款信息设置成功',
|
||
'data': response_data
|
||
})
|
||
|
||
except Exception as e:
|
||
traceback.print_exc() # 打印详细错误信息到控制台
|
||
return Response({
|
||
'code': 99,
|
||
'msg': f'系统错误: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def handle_shoukuanma_upload(self, user_main, image_file):
|
||
"""
|
||
处理收款码图片上传到腾讯云COS
|
||
存储路径: tixian/{yonghuid}/{yonghuid}.扩展名
|
||
"""
|
||
try:
|
||
yonghuid = user_main.UserUID
|
||
if not yonghuid:
|
||
return None
|
||
|
||
# 获取旧收款码URL
|
||
old_shoukuanma_url = user_main.PaymentQRCode
|
||
|
||
# 生成文件扩展名
|
||
file_name = image_file.name
|
||
file_extension = os.path.splitext(file_name)[1].lower()
|
||
if not file_extension:
|
||
file_extension = '.jpg'
|
||
|
||
# 构建OSS文件路径: tixian/{yonghuid}/{yonghuid}{扩展名}
|
||
# 使用用户ID作为文件名,确保同一用户多次上传会覆盖
|
||
oss_file_name = f"{yonghuid}{file_extension}"
|
||
oss_file_path = f"tixian/{yonghuid}/{oss_file_name}"
|
||
|
||
# 删除旧收款码(如果存在)
|
||
if old_shoukuanma_url and old_shoukuanma_url.strip():
|
||
delete_from_oss(old_shoukuanma_url)
|
||
|
||
# 上传新收款码到OSS
|
||
new_shoukuanma_url = upload_to_oss(image_file, oss_file_path)
|
||
|
||
if new_shoukuanma_url:
|
||
# 返回相对路径(从tixian文件夹开始)
|
||
return oss_file_path
|
||
else:
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"处理收款码上传失败: {e}")
|
||
return None
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# ========== 订单状态常量(请根据您的实际业务修改)==========
|
||
# 假设状态值:已完成=9,已退款=10(示例,实际以您的项目为准)
|
||
ORDER_STATUS_COMPLETED = 3 # 订单已完成
|
||
ORDER_STATUS_REFUNDED = 5 # 订单已退款
|
||
ORDER_STATUS_FAIL = 6
|
||
# 进行中状态(不可提现)例如:待接单、已接单、进行中等,您需要定义排除列表
|
||
FORBIDDEN_STATUS_FOR_WITHDRAW = [1, 2, 3, 4, 5, 6, 7, 8] # 除已完成和已退款外的所有状态
|
||
|
||
|
||
class TixianShenqingView(APIView):
|
||
"""
|
||
用户提现申请接口(完整版)
|
||
支持类型:
|
||
1-打手佣金提现
|
||
2-管事分红提现
|
||
3-组长分红提现
|
||
4-审核官分佣提现
|
||
5-打手押金提现(退出平台)
|
||
6-商家余额提现(退出平台)
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user_main = request.user
|
||
yonghuid = user_main.UserUID
|
||
|
||
# 1. 获取并验证参数
|
||
leixing = request.data.get('leixing')
|
||
jine = request.data.get('jine')
|
||
fangshi = request.data.get('fangshi')
|
||
txskfs = request.data.get('txskfs', 0)
|
||
|
||
# 罚单检查(共用)
|
||
fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2)
|
||
if not fadan_ok:
|
||
return Response({'code': 400, 'msg': fadan_msg})
|
||
|
||
# 参数完整性
|
||
missing = []
|
||
if leixing is None: missing.append('leixing')
|
||
if jine is None: missing.append('jine')
|
||
if fangshi is None: missing.append('fangshi')
|
||
if missing:
|
||
return Response({'code': 1, 'msg': f'缺少参数: {", ".join(missing)}'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
leixing = int(leixing)
|
||
jine = decimal.Decimal(str(jine))
|
||
fangshi = int(fangshi)
|
||
txskfs = int(txskfs)
|
||
except (ValueError, TypeError, decimal.InvalidOperation) as e:
|
||
return Response({'code': 2, 'msg': f'参数格式错误: {str(e)}'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if jine <= decimal.Decimal('0'):
|
||
return Response({'code': 3, 'msg': '提现金额必须大于0'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 支持的提现类型(1-6)
|
||
if leixing not in [1, 2, 3, 4, 5, 6]:
|
||
return Response({'code': 4, 'msg': '无效的提现类型'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if fangshi not in [1, 2]:
|
||
return Response({'code': 5, 'msg': '无效的收款方式,只能是1(微信)或2(支付宝)'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 2. 获取对应费率及手续费
|
||
current_rate = decimal.Decimal('0.00')
|
||
rate_key = {
|
||
1: '5', # 打手佣金
|
||
2: '6', # 管事分红
|
||
3: '8', # 组长分红
|
||
4: '9', # 审核官分佣
|
||
5: '11', # 打手押金提现
|
||
6: '10', # 商家余额提现
|
||
}.get(leixing)
|
||
|
||
if rate_key:
|
||
rate_obj = CommissionRate.query.filter(Platform=rate_key).first()
|
||
if rate_obj:
|
||
current_rate = decimal.Decimal(str(rate_obj.Rate))
|
||
else:
|
||
return Response({'code': 41, 'msg': f'提现费率未配置(类型{leixing})'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
shouxufei = jine * current_rate
|
||
shijidaozhang = jine - shouxufei
|
||
if shijidaozhang <= decimal.Decimal('0'):
|
||
return Response({'code': 6, 'msg': '提现金额过低,扣除手续费后无实际到账'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 3. 根据提现类型执行校验和扣款
|
||
nicheng = ''
|
||
if leixing == 1:
|
||
result = self._check_dashou_commission_withdraw(user_main, jine)
|
||
if result['code'] != 0:
|
||
return Response(result, status=status.HTTP_400_BAD_REQUEST)
|
||
nicheng = result['data']['nicheng']
|
||
# 扣款(佣金)
|
||
with transaction.atomic():
|
||
profile = UserDashou.objects.select_for_update().get(user=user_main)
|
||
if profile.yue < jine:
|
||
return Response({'code': 13, 'msg': '余额不足'})
|
||
profile.yue = F('yue') - jine
|
||
profile.save()
|
||
elif leixing == 2:
|
||
result = self._check_guanshi_dividend_withdraw(user_main, jine)
|
||
if result['code'] != 0:
|
||
return Response(result, status=status.HTTP_400_BAD_REQUEST)
|
||
with transaction.atomic():
|
||
profile = UserGuanshi.objects.select_for_update().get(user=user_main)
|
||
if profile.yue < jine:
|
||
return Response({'code': 22, 'msg': '余额不足'})
|
||
profile.yue = F('yue') - jine
|
||
profile.save()
|
||
nicheng = "管事用户"
|
||
elif leixing == 3:
|
||
result = self._check_zuzhang_withdraw(user_main, jine)
|
||
if result['code'] != 0:
|
||
return Response(result, status=status.HTTP_400_BAD_REQUEST)
|
||
with transaction.atomic():
|
||
profile = UserZuzhang.objects.select_for_update().get(user=user_main)
|
||
if profile.ketixian_jine < jine:
|
||
return Response({'code': 32, 'msg': '可提现金额不足'})
|
||
profile.ketixian_jine = F('ketixian_jine') - jine
|
||
profile.save()
|
||
nicheng = user_main.nicheng if hasattr(user_main, 'nicheng') else ''
|
||
elif leixing == 4:
|
||
result = self._check_shenheguan_withdraw(user_main, jine)
|
||
if result['code'] != 0:
|
||
return Response(result, status=status.HTTP_400_BAD_REQUEST)
|
||
with transaction.atomic():
|
||
profile = UserShenheguan.objects.select_for_update().get(user=user_main)
|
||
if profile.yue < jine:
|
||
return Response({'code': 42, 'msg': '可提现金额不足'})
|
||
profile.yue = F('yue') - jine
|
||
profile.save()
|
||
nicheng = "审核官"
|
||
elif leixing == 5:
|
||
# 打手押金提现(退出)
|
||
result = self._check_dashou_yajin_withdraw(user_main, jine)
|
||
if result['code'] != 0:
|
||
return Response(result, status=status.HTTP_400_BAD_REQUEST)
|
||
with transaction.atomic():
|
||
profile = UserDashou.objects.select_for_update().get(user=user_main)
|
||
if profile.yajin < jine:
|
||
return Response({'code': 53, 'msg': '押金余额不足'})
|
||
profile.yajin = F('yajin') - jine
|
||
# 押金提现后封禁打手账号
|
||
profile.zhanghaozhuangtai = 0 # 0表示禁用
|
||
profile.save()
|
||
nicheng = profile.nicheng or ''
|
||
elif leixing == 6:
|
||
# 商家余额提现(退出)
|
||
result = self._check_shangjia_withdraw(user_main, jine)
|
||
if result['code'] != 0:
|
||
return Response(result, status=status.HTTP_400_BAD_REQUEST)
|
||
with transaction.atomic():
|
||
profile = UserShangjia.objects.select_for_update().get(user=user_main)
|
||
if profile.yue < jine:
|
||
return Response({'code': 63, 'msg': '商家余额不足'})
|
||
profile.yue = F('yue') - jine
|
||
# 注意:是否封禁商家账号?根据需求,用户未明确要求封禁,暂时只扣款,可后续扩展。
|
||
profile.save()
|
||
nicheng = profile.nicheng or ''
|
||
|
||
# 4. 创建提现记录(先扣款后创建,确保资金安全)
|
||
try:
|
||
avatar = user_main.Avatar or ''
|
||
phone = user_main.Phone or ''
|
||
zhifu = user_main.PaymentQRCode or ''
|
||
skzhanghao = user_main.PaymentAccount or ''
|
||
tixian_record = Tixianjilu.query.create(
|
||
yonghuid=yonghuid,
|
||
avatar=avatar,
|
||
phone=phone,
|
||
nicheng=nicheng,
|
||
leixing=leixing,
|
||
zhifu=zhifu,
|
||
skzhanghao=skzhanghao,
|
||
jine=shijidaozhang, # 记录实际到账金额
|
||
zhuangtai=1, # 审核中
|
||
fangshi=fangshi,
|
||
shenheid=None,
|
||
bhliyou='',
|
||
)
|
||
except Exception as e:
|
||
return Response({'code': 99, 'msg': f'创建提现记录失败: {str(e)}'},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
# 5. 返回成功响应
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '提现申请提交成功',
|
||
'data': {
|
||
'tixian_id': tixian_record.id,
|
||
'leixing': leixing,
|
||
'jine': str(jine.quantize(decimal.Decimal('0.01'))),
|
||
'fangshi': fangshi,
|
||
'zhuangtai': 1,
|
||
'CreateTime': tixian_record.CreateTime.strftime('%Y-%m-%d %H:%M:%S'),
|
||
'current_rate': str(current_rate.quantize(decimal.Decimal('0.0000'))),
|
||
'shouxufei': str(shouxufei.quantize(decimal.Decimal('0.01'))),
|
||
'shijidaozhang': str(shijidaozhang.quantize(decimal.Decimal('0.01'))),
|
||
'tip': f'本次提现手续费率{current_rate * 100}%,手续费{shouxufei}元,实际到账{shijidaozhang}元'
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
traceback.print_exc()
|
||
return Response({'code': 99, 'msg': f'系统错误: {str(e)}'},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
# ==================== 校验函数 ====================
|
||
|
||
def _check_dashou_commission_withdraw(self, user_main, jine):
|
||
"""打手佣金提现校验"""
|
||
try:
|
||
dashou = UserDashou.query.get(user=user_main)
|
||
except ObjectDoesNotExist:
|
||
return {'code': 10, 'msg': '您不是打手身份,无法提现佣金'}
|
||
|
||
if dashou.zhanghaozhuangtai != 1:
|
||
return {'code': 11, 'msg': '打手账号已被封禁,无法提现'}
|
||
if dashou.zhuangtai != 1:
|
||
return {'code': 12, 'msg': '您有订单进行中,请完成后提现'}
|
||
if dashou.jifen != 10:
|
||
return {'code': 15, 'msg': '积分不足10分,请补充积分后提现'}
|
||
if dashou.yue < jine:
|
||
return {'code': 13, 'msg': f'余额不足,当前余额: {dashou.yue}'}
|
||
|
||
return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
|
||
|
||
def _check_guanshi_dividend_withdraw(self, user_main, jine):
|
||
"""管事分红提现校验"""
|
||
try:
|
||
guanshi = UserGuanshi.query.get(user=user_main)
|
||
except ObjectDoesNotExist:
|
||
return {'code': 20, 'msg': '您不是管事身份,无法提现分红'}
|
||
if guanshi.zhuangtai != 1:
|
||
return {'code': 21, 'msg': '管事账号已被封禁,无法提现'}
|
||
if guanshi.yue < jine:
|
||
return {'code': 22, 'msg': f'余额不足,当前余额: {guanshi.yue}'}
|
||
return {'code': 0, 'msg': 'ok', 'data': {}}
|
||
|
||
def _check_zuzhang_withdraw(self, user_main, jine):
|
||
"""组长分红提现校验"""
|
||
try:
|
||
zuzhang = UserZuzhang.query.get(user=user_main)
|
||
except ObjectDoesNotExist:
|
||
return {'code': 30, 'msg': '您不是组长身份,无法提现组长分红'}
|
||
if zuzhang.zhuangtai != 1:
|
||
return {'code': 31, 'msg': '组长账号已被封禁,无法提现'}
|
||
if zuzhang.ketixian_jine < jine:
|
||
return {'code': 32, 'msg': f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}'}
|
||
return {'code': 0, 'msg': 'ok', 'data': {}}
|
||
|
||
def _check_shenheguan_withdraw(self, user_main, jine):
|
||
"""审核官分佣提现校验"""
|
||
try:
|
||
shenheguan = UserShenheguan.query.get(user=user_main)
|
||
except ObjectDoesNotExist:
|
||
return {'code': 40, 'msg': '您不是审核官身份,无法提现分佣'}
|
||
if shenheguan.zhuangtai != 1:
|
||
return {'code': 41, 'msg': '审核官账号已被封禁,无法提现'}
|
||
if shenheguan.yue < jine:
|
||
return {'code': 42, 'msg': f'可提现金额不足,当前可提: {shenheguan.yue}'}
|
||
return {'code': 0, 'msg': 'ok', 'data': {}}
|
||
|
||
|
||
def _check_shangjia_withdraw(self, user_main, jine):
|
||
"""
|
||
商家余额提现校验(退出平台)
|
||
包含:订单、自身罚单、申请的罚单检查
|
||
"""
|
||
try:
|
||
shangjia = UserShangjia.query.get(user=user_main)
|
||
except ObjectDoesNotExist:
|
||
return {'code': 60, 'msg': '您不是商家身份,无法提现商家余额'}
|
||
|
||
if shangjia.zhuangtai != 1:
|
||
return {'code': 61, 'msg': '商家账号已被禁用,无法提现'}
|
||
if shangjia.yue < jine:
|
||
return {'code': 62, 'msg': f'商家余额不足,当前余额: {shangjia.yue}'}
|
||
|
||
# 订单检查(使用 Q 取反)
|
||
unfinished_orders = MerchantOrderExt.query.filter(
|
||
MerchantID=user_main.UserUID,
|
||
).exclude(
|
||
Order__Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED,ORDER_STATUS_FAIL]
|
||
).exists()
|
||
if unfinished_orders:
|
||
return {'code': 63, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提现余额'}
|
||
|
||
# 自身被处罚的罚单检查(状态不是已缴纳(2)和已驳回(4))
|
||
self_fadan = Penalty.query.filter(
|
||
PenalizedUserID=user_main.UserUID,
|
||
).exclude(
|
||
Status__in=[2, 4]
|
||
).exists()
|
||
if self_fadan:
|
||
return {'code': 64, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'}
|
||
|
||
# 申请的对他人处罚罚单检查(状态不是已缴纳(2)和已驳回(4))
|
||
applied_fadan = Penalty.query.filter(
|
||
ApplicantID=user_main.UserUID,
|
||
).exclude(
|
||
Status__in=[2, 4]
|
||
).exists()
|
||
if applied_fadan:
|
||
return {'code': 65, 'msg': '您发起的对其他用户的处罚尚未处理完毕(待缴纳/申诉中),请等待完结'}
|
||
|
||
return {'code': 0, 'msg': 'ok', 'data': {'nicheng': shangjia.nicheng or ''}}
|
||
|
||
def _check_dashou_yajin_withdraw(self, user_main, jine):
|
||
"""
|
||
打手押金提现校验(退出平台)
|
||
要求:无进行中的订单,无未结算订单,自身罚单需已处理
|
||
"""
|
||
try:
|
||
dashou = UserDashou.query.get(user=user_main)
|
||
except ObjectDoesNotExist:
|
||
return {'code': 50, 'msg': '您不是打手身份,无法提取押金'}
|
||
|
||
if dashou.zhanghaozhuangtai != 1:
|
||
return {'code': 51, 'msg': '打手账号已禁用,无法提取押金'}
|
||
if dashou.zhuangtai != 1:
|
||
return {'code': 52, 'msg': '您有订单进行中,请完成后提取押金'}
|
||
if dashou.yajin < jine:
|
||
return {'code': 53, 'msg': f'押金余额不足,当前押金: {dashou.yajin}'}
|
||
|
||
# 订单检查(使用 exclude + __in)
|
||
unfinished_orders = Order.query.filter(
|
||
PlayerID=user_main.UserUID,
|
||
).exclude(
|
||
Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]
|
||
).exists()
|
||
if unfinished_orders:
|
||
return {'code': 54, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提取押金'}
|
||
|
||
# 自身罚单检查
|
||
self_fadan = Penalty.query.filter(
|
||
PenalizedUserID=user_main.UserUID,
|
||
).exclude(
|
||
Status__in=[2, 4]
|
||
).exists()
|
||
if self_fadan:
|
||
return {'code': 55, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'}
|
||
|
||
return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class TixianJiluHuoquViewV2(APIView):
|
||
"""
|
||
提现记录获取接口
|
||
mode=1:手动提现记录(默认,兼容旧版)
|
||
mode=2:自动提现审核记录(含 shenhe_jilu_id / shenhe_danhao,支持 zhuangtai 筛选)
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user_main = request.user
|
||
yonghuid = user_main.UserUID
|
||
page = request.data.get('page', 1)
|
||
limit = request.data.get('limit', 50)
|
||
mode = int(request.data.get('mode', 1) or 1)
|
||
zhuangtai_filter = int(request.data.get('zhuangtai', 0) or 0)
|
||
|
||
try:
|
||
page = int(page)
|
||
limit = int(limit)
|
||
if page < 1 or limit < 1:
|
||
return Response({'code': 1, 'msg': '参数错误', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 1, 'msg': '参数格式错误', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if limit > 100:
|
||
limit = 100
|
||
offset = (page - 1) * limit
|
||
|
||
qs = Tixianjilu.query.filter(yonghuid=yonghuid)
|
||
if mode == 2:
|
||
# 自动提现:含新审核关联记录;zhuangtai>0 时按状态筛选
|
||
if zhuangtai_filter > 0:
|
||
qs = qs.filter(zhuangtai=zhuangtai_filter)
|
||
qs = qs.order_by('-CreateTime')
|
||
|
||
records_queryset = qs.values(
|
||
'id', 'leixing', 'jine', 'zhuangtai', 'CreateTime', 'fangshi',
|
||
'phone', 'skzhanghao', 'zhifu', 'bhliyou',
|
||
'shenhe_jilu_id', 'shenhe_danhao',
|
||
)[offset:offset + limit + 1]
|
||
|
||
records_list = list(records_queryset)
|
||
has_more = len(records_list) > limit
|
||
records_to_return = records_list[:limit] if has_more else records_list
|
||
|
||
# 批量取审核表 fail_reason
|
||
audit_ids = [r['shenhe_jilu_id'] for r in records_to_return if r.get('shenhe_jilu_id')]
|
||
audit_map = {}
|
||
if audit_ids:
|
||
for a in TixianShenheJilu.query.filter(id__in=audit_ids).values(
|
||
'id', 'fail_reason', 'shenqing_jine', 'shenhe_danhao'
|
||
):
|
||
audit_map[a['id']] = a
|
||
|
||
# 批量取自动打款失败原因
|
||
danhao_list = [r['shenhe_danhao'] for r in records_to_return if r.get('shenhe_danhao')]
|
||
auto_fail_map = {}
|
||
if danhao_list:
|
||
for ar in TixianAutoRecord.query.filter(
|
||
shenhe_danhao__in=danhao_list, zhuangtai=2
|
||
).order_by('-CreateTime').values('shenhe_danhao', 'fail_reason'):
|
||
if ar['shenhe_danhao'] not in auto_fail_map:
|
||
auto_fail_map[ar['shenhe_danhao']] = ar['fail_reason']
|
||
|
||
formatted_records = []
|
||
for record in records_to_return:
|
||
audit = audit_map.get(record.get('shenhe_jilu_id')) if record.get('shenhe_jilu_id') else None
|
||
fail_reason = ''
|
||
if audit and audit.get('fail_reason'):
|
||
fail_reason = audit['fail_reason']
|
||
elif record.get('shenhe_danhao'):
|
||
fail_reason = auto_fail_map.get(record['shenhe_danhao'], '') or ''
|
||
|
||
jine_val = float(record['jine']) if record['jine'] else 0.00
|
||
if mode == 2 and audit and audit.get('shenqing_jine') is not None:
|
||
jine_val = float(audit['shenqing_jine'])
|
||
|
||
shenhe_danhao_val = record.get('shenhe_danhao') or (audit.get('shenhe_danhao') if audit else '')
|
||
formatted_records.append({
|
||
'id': record['id'],
|
||
'tixianjilu_id': record['id'],
|
||
'tixian_id': record['id'],
|
||
'leixing': record['leixing'],
|
||
'jine': jine_val,
|
||
'zhuangtai': record['zhuangtai'],
|
||
'create': record['CreateTime'].strftime('%Y-%m-%d %H:%M:%S'),
|
||
'fangshi': record['fangshi'],
|
||
'txdianhua': record['phone'] or '',
|
||
'txzh': record['skzhanghao'] or '',
|
||
'txtupian': record['zhifu'] or '',
|
||
'shibaiyuanyin': record['bhliyou'] or '',
|
||
'bhliyou': record['bhliyou'] or '',
|
||
'fail_reason': fail_reason,
|
||
'shenhe_jilu_id': record.get('shenhe_jilu_id'),
|
||
'shenhe_danhao': shenhe_danhao_val,
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '获取成功',
|
||
'data': {
|
||
'list': formatted_records,
|
||
'has_more': has_more,
|
||
'page': page,
|
||
'limit': limit,
|
||
'current_count': len(formatted_records),
|
||
},
|
||
})
|
||
|
||
except Exception as e:
|
||
return Response({
|
||
'code': 99,
|
||
'msg': f'系统错误: {str(e)}',
|
||
'data': None,
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
# yonghu/views.py
|
||
class AdminLoginView(APIView):
|
||
"""
|
||
管理员登录接口
|
||
路径:/yonghu/adlog/
|
||
方法:POST
|
||
字段:zhanghao(账号), mima(密码), ip(二次验证码)
|
||
"""
|
||
|
||
# 限制匿名用户访问频率,防止暴力破解
|
||
throttle_classes = [AnonRateThrottle]
|
||
# 允许任何人访问(登录接口不需要认证)
|
||
permission_classes = [AllowAny]
|
||
|
||
def post(self, request):
|
||
"""
|
||
处理管理员登录请求
|
||
安全策略:任何错误都返回401,不透露具体信息
|
||
"""
|
||
try:
|
||
# 1. 获取客户端真实IP和用户代理信息
|
||
kehuduan_ip = huoquKehuduanIP(request)
|
||
yonghu_daili = huoquYonghuDaili(request)
|
||
|
||
|
||
# 2. 获取前端传递的数据
|
||
data = request.data
|
||
|
||
# 检查必需字段是否存在
|
||
bixu_ziduan = ['zhanghao', 'mima', 'ip']
|
||
for ziduan in bixu_ziduan:
|
||
if ziduan not in data:
|
||
# 字段缺失,返回401
|
||
return Response(
|
||
{'code': 401, 'msg': '登录失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 提取并清理数据
|
||
zhanghao = data.get('zhanghao', '').strip()
|
||
mima = data.get('mima', '').strip()
|
||
erci_yanzhengma = data.get('ip', '').strip() # 二次验证码(前端叫ip)
|
||
|
||
# 简单验证字段非空
|
||
if not zhanghao or not mima or not erci_yanzhengma:
|
||
return Response(
|
||
{'code': 401, 'msg': '登录失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 3. 使用事务确保数据一致性
|
||
with transaction.atomic():
|
||
# 【优化点1】使用select_related一次性获取管理员扩展表
|
||
# 只查询管理员类型的用户(密码用 CheckPassword 验证,不能在 ORM 中直接匹配哈希)
|
||
user = User.query.filter(
|
||
Phone=zhanghao, # 手机号作为账号
|
||
AdminProfile__isnull=False # 必须是管理员类型
|
||
).select_related('AdminProfile').first()
|
||
|
||
# 如果用户不存在或不是管理员,返回401
|
||
if not user:
|
||
return Response(
|
||
{'code': 401, 'msg': '登录失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 验证密码(bcrypt 哈希验证)
|
||
if not user.CheckPassword(mima):
|
||
return Response(
|
||
{'code': 401, 'msg': '登录失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 【优化点2】检查管理员扩展表是否存在
|
||
if not hasattr(user, 'AdminProfile'):
|
||
return Response(
|
||
{'code': 401, 'msg': '登录失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 【优化点3】验证二次密码(管理员扩展表中的password字段)
|
||
admin_profile = user.AdminProfile
|
||
if admin_profile.password != erci_yanzhengma:
|
||
return Response(
|
||
{'code': 401, 'msg': '登录失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 4. 更新用户登录信息
|
||
# 获取当前时间
|
||
now_time = timezone.now()
|
||
|
||
# 处理IP地址长度问题
|
||
# 如果原ip字段长度不够,截取前11个字符(保持兼容性)
|
||
|
||
cunchu_ip = kehuduan_ip
|
||
#print(cunchu_ip)
|
||
|
||
# 更新用户信息
|
||
user.IP = cunchu_ip # 存储客户端IP
|
||
user.UserLastLoginDate = now_time # 更新最后登录时间
|
||
user.save()
|
||
|
||
# 【优化点4】生成JWT token(使用和微信登录相同的方式)
|
||
# 使用djangorestframework-simplejwt生成token
|
||
refresh = RefreshToken.for_user(user)
|
||
token = str(refresh.access_token)
|
||
|
||
# 5. 准备返回数据
|
||
# 返回用户基本信息(可根据需要调整)
|
||
response_data = {
|
||
'token': token,
|
||
'yonghuid': user.UserUID,
|
||
'phone': user.Phone or '',
|
||
'user_type': user.UserType,
|
||
'last_login_time': now_time.strftime('%Y-%m-%d %H:%M:%S') if now_time else '',
|
||
}
|
||
|
||
# 可以添加管理员特定信息
|
||
if hasattr(user, 'AdminProfile'):
|
||
response_data['wechat'] = user.AdminProfile.wechat or ''
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '登录成功',
|
||
'data': response_data
|
||
})
|
||
|
||
except Exception as e:
|
||
# 【安全点】任何异常都返回401,不暴露具体错误信息
|
||
# 记录日志用于排查问题,但前端只收到通用错误信息
|
||
# 这里可以添加日志记录,比如:
|
||
logging.error(f'管理员登录异常: {str(e)}', exc_info=True)
|
||
|
||
return Response(
|
||
{'code': 401, 'msg': '登录失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
|
||
# yonghu/views.py
|
||
class AdminFinancialDataView(APIView):
|
||
"""
|
||
管理员财务数据接口
|
||
路径:/yonghu/adhq/
|
||
方法:POST
|
||
认证:JWT Token
|
||
权限:仅管理员
|
||
返回:收支记录数据
|
||
"""
|
||
|
||
# 使用JWT认证
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
"""
|
||
处理管理员财务数据请求
|
||
安全策略:任何错误都返回401,不透露具体信息
|
||
"""
|
||
try:
|
||
# 1. 获取前端传递的数据
|
||
data = request.data
|
||
|
||
# 检查必需字段
|
||
if 'zhanghao' not in data:
|
||
return Response(
|
||
{'code': 401, 'msg': '请求参数错误', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
qianzhanghao = data.get('zhanghao', '').strip()
|
||
|
||
# 简单验证字段非空
|
||
if not qianzhanghao:
|
||
return Response(
|
||
{'code': 401, 'msg': '请求参数错误', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 2. 获取当前登录用户(JWT认证后,request.user就是User实例)
|
||
dangqianyonghu = request.user
|
||
|
||
# 3. 验证用户类型是否为admin
|
||
if not hasattr(dangqianyonghu, 'UserType') or dangqianyonghu.UserType != 'admin':
|
||
return Response(
|
||
{'code': 401, 'msg': '权限不足', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 4. 验证前端传递的账号与当前用户的phone字段是否一致
|
||
if not dangqianyonghu.Phone or dangqianyonghu.Phone != qianzhanghao:
|
||
return Response(
|
||
{'code': 401, 'msg': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 5. 验证管理员扩展表是否存在(反向查询)
|
||
# 使用高效的ORM查询,避免额外的数据库查询
|
||
try:
|
||
# 使用select_related一次性获取管理员扩展表
|
||
# UserType 是 @property 非数据库字段,用 AdminProfile__isnull=False 替代
|
||
yonghu_xiangxi = User.query.select_related('AdminProfile').get(
|
||
pk=dangqianyonghu.pk,
|
||
AdminProfile__isnull=False
|
||
)
|
||
|
||
# 检查管理员扩展表是否存在
|
||
if not hasattr(yonghu_xiangxi, 'AdminProfile'):
|
||
return Response(
|
||
{'code': 401, 'msg': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
except User.DoesNotExist:
|
||
return Response(
|
||
{'code': 401, 'msg': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 6. 查询收支记录表(Szjilu)
|
||
# 使用高效的ORM查询,获取最新的一条记录
|
||
# 注意:根据你的业务需求,这里获取最新的一条记录
|
||
# 如果需要汇总多条记录,可以使用aggregate进行聚合查询
|
||
|
||
# 方法1:获取最新的一条记录(假设表中只有一条汇总记录)
|
||
shouzhijilu = Szjilu.query.order_by('-CreateTime').first()
|
||
|
||
# 方法2:如果表中可能有多条记录,需要汇总,可以使用以下方式:
|
||
# from django.db.models Sum
|
||
# zonghe = Szjilu.query.aggregate(
|
||
# zongliushui=Sum('TotalFlow'),
|
||
# zongshouyi=Sum('TotalIncome'),
|
||
# zongzhichu=Sum('TotalExpense'),
|
||
# DailyFlow=Sum('DailyFlow'),
|
||
# DailyExpense=Sum('DailyExpense')
|
||
# )
|
||
|
||
# 准备返回数据
|
||
if shouzhijilu:
|
||
# 有记录的情况
|
||
huizongshuju = {
|
||
'zongliushui': float(shouzhijilu.TotalFlow or 0),
|
||
'zongshouyi': float(shouzhijilu.TotalIncome or 0),
|
||
'zongzhichu': float(shouzhijilu.TotalExpense or 0),
|
||
'jrls': float(shouzhijilu.DailyFlow or 0),
|
||
'jrzc': float(shouzhijilu.DailyExpense or 0)
|
||
}
|
||
else:
|
||
# 没有记录的情况,返回默认值
|
||
huizongshuju = {
|
||
'zongliushui': 0.00,
|
||
'zongshouyi': 0.00,
|
||
'zongzhichu': 0.00,
|
||
'jrls': 0.00,
|
||
'jrzc': 0.00
|
||
}
|
||
|
||
# 7. 返回成功响应
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '获取成功',
|
||
'data': huizongshuju
|
||
})
|
||
|
||
except Exception as e:
|
||
# 【安全策略】任何异常都返回401,不暴露具体错误信息
|
||
# 这里可以记录日志,但不返回给前端
|
||
# logging.error(f'管理员财务数据接口异常: {str(e)}', exc_info=True)
|
||
|
||
return Response(
|
||
{'code': 401, 'msg': '获取失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# shangpin/views.py
|
||
class AdGetOrderTypes(APIView):
|
||
"""
|
||
后台获取订单类型列表接口
|
||
前端调用地址: /shangpin/getOrderTypes
|
||
前端需要字段: id, biaoti, tupian_url
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 1. 获取参数
|
||
qianzhanghao = request.data.get('zhanghao', '').strip()
|
||
|
||
if not qianzhanghao:
|
||
return Response(
|
||
{'code': 400, 'message': '参数不完整', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 2. 身份验证(按你说的方式)
|
||
dangqianyonghu = request.user
|
||
|
||
# 验证手机号
|
||
if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
|
||
return Response(
|
||
{'code': 400, 'message': '参数不完整', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 验证管理员身份
|
||
if dangqianyonghu.UserType != 'admin':
|
||
return Response(
|
||
{'code': 400, 'message': '参数不完整', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 3. 验证管理员扩展表
|
||
try:
|
||
guanliyuan = dangqianyonghu.AdminProfile
|
||
except AttributeError:
|
||
return Response(
|
||
{'code': 400, 'message': '参数不完整', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 4. 查询订单类型(商品类型)表
|
||
# 注意:这里需要根据你实际的模型名调整
|
||
# 如果模型名不是ShangpinType,请告诉我正确的模型名
|
||
|
||
# 假设模型名是ShangpinLeixing(因为我之前看到你有商品类型表)
|
||
# 你需要告诉我确切的模型名
|
||
try:
|
||
# 请替换为你的实际模型名
|
||
# 只查询前端需要的三个字段
|
||
shangpin_types = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url')
|
||
|
||
# 构建响应数据
|
||
type_list = [
|
||
{
|
||
'id': item.id,
|
||
'biaoti': item.jieshao or '',
|
||
'tupian_url': item.tupian_url or ''
|
||
}
|
||
for item in shangpin_types
|
||
]
|
||
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'message': '获取成功',
|
||
'data': type_list
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"查询商品类型表失败: {str(e)}")
|
||
# 如果表不存在,返回空数组
|
||
return Response({
|
||
'code': 0,
|
||
'message': '获取成功',
|
||
'data': []
|
||
})
|
||
|
||
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
|
||
)
|
||
|
||
|
||
|
||
|
||
|
||
# yonghu/views.py 或 dingdan/views.py
|
||
class AdGetOrderList(APIView):
|
||
"""
|
||
后台订单列表获取接口
|
||
前端调用地址: /yonghu/adddhq
|
||
前端需要字段: dingdan_id, jieshao, zhuangtai, leixing, fadanpingtai, jiage, tupian_url, fadanshijian
|
||
统计字段: stats.platform.total, stats.platform.completed, stats.merchant.total, stats.merchant.completed
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 1. 获取参数(严格按前端传的字段名)
|
||
qianzhanghao = request.data.get('zhanghao', '').strip()
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('pageSize', 30))
|
||
|
||
# 搜索相关参数
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
|
||
# 筛选相关参数(严格按前端字段名)
|
||
fadanpingtai = request.data.get('fadanpingtai') # 前端叫 fadanpingtai
|
||
leixing = request.data.get('leixing') # 前端叫 leixing
|
||
zhuangtai = request.data.get('zhuangtai', '') # 前端叫 zhuangtai
|
||
|
||
if not qianzhanghao:
|
||
return Response(
|
||
{'code': 400, 'message': '参数不完整', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 2. 身份验证(与你说的完全一样)
|
||
dangqianyonghu = request.user
|
||
|
||
# 验证手机号
|
||
if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
|
||
return Response(
|
||
{'code': 400, 'message': '参数不完整', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 验证管理员身份
|
||
if dangqianyonghu.UserType != 'admin':
|
||
return Response(
|
||
{'code': 400, 'message': '参数不完整', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 3. 验证管理员扩展表
|
||
try:
|
||
guanliyuan = dangqianyonghu.AdminProfile
|
||
except AttributeError:
|
||
return Response(
|
||
{'code': 400, 'message': '参数不完整', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 5. 构建查询条件(严格按前端字段名映射到数据库字段)
|
||
query_conditions = Q()
|
||
|
||
# 判断是否为搜索模式
|
||
is_search_mode = bool(dingdan_id)
|
||
|
||
if is_search_mode:
|
||
# 搜索模式:只按订单ID查询
|
||
query_conditions &= Q(OrderID=dingdan_id)
|
||
else:
|
||
# 筛选模式
|
||
|
||
# 发单平台筛选(前端: fadanpingtai -> 数据库: Platform)
|
||
if fadanpingtai is not None:
|
||
try:
|
||
fadanpingtai_int = int(fadanpingtai)
|
||
if fadanpingtai_int in [1, 2]:
|
||
query_conditions &= Q(Platform=fadanpingtai_int)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# 订单类型筛选(前端: leixing -> 数据库: ProductTypeID)
|
||
if leixing is not None:
|
||
try:
|
||
leixing_int = int(leixing)
|
||
query_conditions &= Q(ProductTypeID=leixing_int)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# 订单状态筛选(前端: zhuangtai -> 数据库: Status)
|
||
if zhuangtai and zhuangtai != 'all':
|
||
try:
|
||
# 处理多个状态,如 "1,7"
|
||
status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
|
||
if status_list:
|
||
query_conditions &= Q(Status__in=status_list)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# 6. 获取统计数据(平台订单和商家订单)
|
||
# 注意:这里统计所有订单,不考虑筛选条件
|
||
try:
|
||
# 平台订单统计 (Platform=1)
|
||
platform_total = Order.query.filter(Platform=1).count()
|
||
platform_completed = Order.query.filter(Platform=1, Status=3).count()
|
||
|
||
# 商家订单统计 (Platform=2)
|
||
merchant_total = Order.query.filter(Platform=2).count()
|
||
merchant_completed = Order.query.filter(Platform=2, Status=3).count()
|
||
|
||
platform_stats = {
|
||
'total': platform_total,
|
||
'completed': platform_completed
|
||
}
|
||
merchant_stats = {
|
||
'total': merchant_total,
|
||
'completed': merchant_completed
|
||
}
|
||
except Exception as e:
|
||
logger.warning(f"统计数据查询失败: {str(e)}")
|
||
platform_stats = {'total': 0, 'completed': 0}
|
||
merchant_stats = {'total': 0, 'completed': 0}
|
||
|
||
# 7. 执行分页查询(只查询主表,不查扩展表)
|
||
start_time = time.time()
|
||
|
||
# 计算分页
|
||
offset = (page - 1) * page_size
|
||
|
||
# 查询符合条件的订单总数
|
||
total_count = Order.query.filter(query_conditions).count()
|
||
|
||
# 查询当前页的数据(只查询主表,只查询前端需要的字段)
|
||
dingdan_list = Order.query.filter(query_conditions).only(
|
||
'OrderID', 'Description', 'Status', 'ProductTypeID',
|
||
'Platform', 'Amount', 'ImageURL', 'CreateTime'
|
||
).order_by('-CreateTime')[offset:offset + page_size]
|
||
|
||
query_time = time.time() - start_time
|
||
logger.info(f"订单查询完成,耗时: {query_time:.3f}s,查询条数: {len(dingdan_list)}")
|
||
|
||
# 8. 构建响应数据(严格按前端需要的字段)
|
||
result_list = []
|
||
|
||
for dingdan in dingdan_list:
|
||
# 注意:这里字段名必须与前端完全一致
|
||
order_data = {
|
||
'dingdan_id': dingdan.OrderID or '',
|
||
'jieshao': dingdan.Description or '',
|
||
'zhuangtai': dingdan.Status or 1,
|
||
'leixing': dingdan.ProductTypeID or 0, # 前端需要 leixing
|
||
'fadanpingtai': dingdan.Platform or 1, # 前端需要 fadanpingtai
|
||
'jiage': float(dingdan.Amount) if dingdan.Amount else 0.00, # 价格对应 Amount 字段
|
||
'tupian_url': dingdan.ImageURL or '', # 图片对应 ImageURL 字段
|
||
'fadanshijian': dingdan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan.CreateTime else ''
|
||
}
|
||
result_list.append(order_data)
|
||
|
||
# 9. 判断是否有更多数据
|
||
current_loaded_count = offset + len(result_list)
|
||
has_more = len(result_list) >= page_size and current_loaded_count < total_count
|
||
|
||
# 10. 构建完整响应(严格按前端需要的结构)
|
||
response_data = {
|
||
'stats': {
|
||
'platform': platform_stats,
|
||
'merchant': merchant_stats
|
||
},
|
||
'list': result_list,
|
||
'hasMore': has_more,
|
||
'total': total_count,
|
||
'currentPage': page,
|
||
'pageSize': page_size
|
||
}
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'message': '获取成功',
|
||
'data': response_data
|
||
})
|
||
|
||
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
|
||
)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class AdYaoQingDaShou(APIView):
|
||
"""
|
||
后台获取邀请码接口(用于邀请打手)
|
||
权限: JWT Token认证 + 管理员身份验证
|
||
前端调用: POST /yonghu/adyqds
|
||
参数: {
|
||
"zhanghao": "管理员账号"
|
||
}
|
||
返回: {
|
||
"code": 0,
|
||
"message": "成功",
|
||
"data": {
|
||
"yaoqingma": "邀请码"
|
||
}
|
||
}
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 1. 获取前端参数
|
||
qianzhanghao = request.data.get('zhanghao', '').strip()
|
||
|
||
if not qianzhanghao:
|
||
return Response(
|
||
{'code': 400, 'message': '参数不完整', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 2. JWT Token身份验证
|
||
dangqianyonghu = request.user
|
||
|
||
# 3. 验证手机号是否匹配
|
||
if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 4. 验证用户类型是否为管理员
|
||
if dangqianyonghu.UserType != 'admin':
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 5. 验证管理员扩展表是否存在
|
||
try:
|
||
guanliyuan = dangqianyonghu.AdminProfile
|
||
except AttributeError:
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 6. 获取用户ID(yonghuid),如果获取不到就用'123456'
|
||
yonghuid_str = getattr(dangqianyonghu, 'yonghuid', '123456')
|
||
|
||
# 7. 尝试获取或创建管事扩展表
|
||
try:
|
||
# 尝试获取现有的管事扩展表
|
||
guanshi_profile = dangqianyonghu.GuanshiProfile
|
||
except AttributeError:
|
||
# 管事扩展表不存在,创建一个
|
||
try:
|
||
# 创建默认的管事扩展表
|
||
guanshi_profile = UserGuanshi.query.create(
|
||
user=dangqianyonghu,
|
||
yaoqingma='', # 暂时为空,后面生成
|
||
dianhua='', # 默认空
|
||
wechat='', # 默认空
|
||
yaogingshuliang=0, # 邀请数量为0
|
||
zhuangtai=1, # 正常状态
|
||
jinrichongzhi=0, # 今日充值0
|
||
jinyuechongzhi=0, # 今月充值0
|
||
chongzhifenrun=0, # 充值分佣0
|
||
yue=0 # 余额0
|
||
)
|
||
#logger.info(f"为管理员{dangqianyonghu.UserUID}创建了管事扩展表")
|
||
except Exception as e:
|
||
logger.error(f"创建管事扩展表失败: {str(e)}")
|
||
return Response(
|
||
{'code': 500, 'message': '创建用户信息失败', 'data': None},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|
||
# 8. 检查是否已有邀请码
|
||
existing_yaoqingma = guanshi_profile.yaoqingma
|
||
|
||
if existing_yaoqingma and existing_yaoqingma.strip():
|
||
# 已有邀请码,直接返回
|
||
return Response({
|
||
'code': 0,
|
||
'message': '成功获取邀请码',
|
||
'data': {
|
||
'yaoqingma': existing_yaoqingma
|
||
}
|
||
})
|
||
|
||
# 9. 生成新的邀请码
|
||
try:
|
||
# 生成邀请码
|
||
new_yaoqingma = CreateInvitationCode(str(yonghuid_str))
|
||
|
||
# 验证邀请码格式
|
||
if not VerifyInvitationCode(new_yaoqingma):
|
||
raise Exception('邀请码格式验证失败')
|
||
|
||
except ImportError:
|
||
# 如果导入失败,使用简单方法生成邀请码
|
||
timestamp = str(int(time.time()))
|
||
random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
||
new_yaoqingma = f"GL{timestamp[-6:]}{random_str}"
|
||
|
||
logger.warning(f"使用简单方法生成邀请码: {new_yaoqingma}")
|
||
except Exception as e:
|
||
logger.error(f"生成邀请码失败: {str(e)}")
|
||
return Response(
|
||
{'code': 500, 'message': '生成邀请码失败', 'data': None},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|
||
# 10. 保存邀请码到数据库
|
||
try:
|
||
with transaction.atomic():
|
||
guanshi_profile.yaoqingma = new_yaoqingma
|
||
guanshi_profile.save()
|
||
|
||
logger.info(f"管理员{dangqianyonghu.UserUID}生成邀请码: {new_yaoqingma}")
|
||
except Exception as e:
|
||
logger.error(f"保存邀请码失败: {str(e)}")
|
||
return Response(
|
||
{'code': 500, 'message': '保存邀请码失败', 'data': None},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|
||
# 11. 返回成功响应
|
||
return Response({
|
||
'code': 0,
|
||
'message': '成功生成邀请码',
|
||
'data': {
|
||
'yaoqingma': new_yaoqingma
|
||
}
|
||
})
|
||
|
||
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
|
||
)
|
||
|
||
|
||
|
||
|
||
class AdGuanLiYongHu(APIView):
|
||
"""
|
||
后台用户管理列表接口
|
||
权限: JWT Token认证 + 管理员身份验证
|
||
前端调用: POST /yonghu/adgl
|
||
参数: {
|
||
"zhanghao": "管理员账号",
|
||
"user_type": "用户类型(1-4)",
|
||
"keyword": "搜索关键词(可选)",
|
||
"page": "页码(从1开始)",
|
||
"page_size": "每页数量"
|
||
}
|
||
返回: {
|
||
"code": 0,
|
||
"message": "成功",
|
||
"data": {
|
||
"list": [用户列表],
|
||
"stats": {"1": 总数, "2": 总数, "3": 总数, "4": 总数},
|
||
"has_more": true/false
|
||
}
|
||
}
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 1. 获取前端参数
|
||
qianzhanghao = request.data.get('zhanghao', '').strip()
|
||
user_type = request.data.get('user_type')
|
||
keyword = request.data.get('keyword', '').strip()
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 30))
|
||
|
||
# 验证参数
|
||
if not qianzhanghao or not user_type:
|
||
return Response(
|
||
{'code': 400, 'message': '参数不完整', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 2. JWT Token身份验证
|
||
dangqianyonghu = request.user
|
||
|
||
# 3. 验证手机号是否匹配
|
||
if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 4. 验证用户类型是否为管理员
|
||
if dangqianyonghu.UserType != 'admin':
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 5. 验证管理员扩展表是否存在
|
||
try:
|
||
guanliyuan = dangqianyonghu.AdminProfile
|
||
except AttributeError:
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 6. 根据用户类型选择查询的模型
|
||
user_type_int = int(user_type)
|
||
|
||
if user_type_int not in [1, 2, 3, 4]:
|
||
return Response(
|
||
{'code': 400, 'message': '用户类型错误', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 7. 统计四种用户类型的总数
|
||
stats = self._get_user_stats()
|
||
|
||
# 8. 根据用户类型查询用户列表
|
||
user_list, has_more = self._query_users_by_type(
|
||
user_type_int, keyword, page, page_size
|
||
)
|
||
|
||
# 9. 构建返回数据
|
||
return Response({
|
||
'code': 0,
|
||
'message': '成功',
|
||
'data': {
|
||
'list': user_list,
|
||
'stats': stats,
|
||
'has_more': has_more
|
||
}
|
||
})
|
||
|
||
except ValueError:
|
||
logger.error("参数类型错误")
|
||
return Response(
|
||
{'code': 400, 'message': '参数类型错误', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
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_user_stats(self):
|
||
"""
|
||
获取四种用户类型的统计数量
|
||
"""
|
||
try:
|
||
|
||
stats = {
|
||
'1': UserBoss.query.count(),
|
||
'2': UserDashou.query.count(),
|
||
'3': UserGuanshi.query.count(),
|
||
'4': UserShangjia.query.count()
|
||
}
|
||
|
||
return stats
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取用户统计失败: {str(e)}")
|
||
return {'1': 0, '2': 0, '3': 0, '4': 0}
|
||
|
||
def _query_users_by_type(self, user_type_int, keyword, page, page_size):
|
||
"""
|
||
根据用户类型查询用户列表
|
||
返回: (用户列表, 是否还有更多数据)
|
||
"""
|
||
try:
|
||
# 计算偏移量
|
||
offset = (page - 1) * page_size
|
||
|
||
if user_type_int == 1:
|
||
return self._query_boss_users(keyword, offset, page_size)
|
||
elif user_type_int == 2:
|
||
return self._query_dashou_users(keyword, offset, page_size)
|
||
elif user_type_int == 3:
|
||
return self._query_guanshi_users(keyword, offset, page_size)
|
||
elif user_type_int == 4:
|
||
return self._query_shangjia_users(keyword, offset, page_size)
|
||
else:
|
||
return [], False
|
||
|
||
except Exception as e:
|
||
logger.error(f"查询用户列表失败: {str(e)}")
|
||
return [], False
|
||
|
||
def _query_boss_users(self, keyword, offset, limit):
|
||
"""查询普通用户(老板)"""
|
||
try:
|
||
queryset = UserBoss.query.select_related('user')
|
||
|
||
if keyword:
|
||
queryset = queryset.filter(
|
||
Q(user__UserUID__icontains=keyword) |
|
||
Q(nickname__icontains=keyword)
|
||
)
|
||
|
||
# 获取总数
|
||
total_count = queryset.count()
|
||
|
||
# 获取当前页的数据
|
||
users = list(queryset.order_by('-CreateTime')[offset:offset + limit])
|
||
|
||
# 计算是否有更多数据
|
||
current_count = len(users)
|
||
has_more = (offset + current_count) < total_count
|
||
|
||
|
||
|
||
# 构建返回数据
|
||
user_list = []
|
||
for boss in users:
|
||
user_main = boss.user
|
||
|
||
user_data = {
|
||
'yonghuid': user_main.UserUID,
|
||
'avatar': user_main.Avatar or '',
|
||
'nicheng': boss.nickname or '',
|
||
}
|
||
|
||
user_list.append(user_data)
|
||
|
||
return user_list, has_more
|
||
|
||
except Exception as e:
|
||
logger.error(f"查询老板用户失败: {str(e)}")
|
||
return [], False
|
||
|
||
def _query_dashou_users(self, keyword, offset, limit):
|
||
"""查询打手用户"""
|
||
try:
|
||
queryset = UserDashou.query.select_related('user')
|
||
|
||
if keyword:
|
||
queryset = queryset.filter(
|
||
Q(user__UserUID__icontains=keyword) |
|
||
Q(nicheng__icontains=keyword) |
|
||
Q(chenghao__icontains=keyword)
|
||
)
|
||
|
||
# 获取总数
|
||
total_count = queryset.count()
|
||
|
||
# 获取当前页的数据
|
||
dashou_list = list(queryset.order_by('-CreateTime')[offset:offset + limit])
|
||
|
||
# 计算是否有更多数据
|
||
current_count = len(dashou_list)
|
||
has_more = (offset + current_count) < total_count
|
||
|
||
|
||
|
||
# 构建返回数据
|
||
user_list = []
|
||
for dashou in dashou_list:
|
||
user_main = dashou.user
|
||
|
||
user_data = {
|
||
'yonghuid': user_main.UserUID,
|
||
'avatar': user_main.Avatar or '',
|
||
'zaixianzhuangtai': dashou.zaixianzhuangtai,
|
||
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
|
||
'nicheng': dashou.nicheng or '',
|
||
'zhuangtai': dashou.zhuangtai,
|
||
}
|
||
|
||
user_list.append(user_data)
|
||
|
||
return user_list, has_more
|
||
|
||
except Exception as e:
|
||
logger.error(f"查询打手用户失败: {str(e)}")
|
||
return [], False
|
||
|
||
def _query_guanshi_users(self, keyword, offset, limit):
|
||
"""查询管事用户"""
|
||
try:
|
||
queryset = UserGuanshi.query.select_related('user')
|
||
|
||
if keyword:
|
||
queryset = queryset.filter(
|
||
Q(user__UserUID__icontains=keyword)
|
||
)
|
||
|
||
# 获取总数
|
||
total_count = queryset.count()
|
||
|
||
# 获取当前页的数据
|
||
guanshi_list = list(queryset.order_by('-CreateTime')[offset:offset + limit])
|
||
|
||
# 计算是否有更多数据
|
||
current_count = len(guanshi_list)
|
||
has_more = (offset + current_count) < total_count
|
||
|
||
|
||
|
||
# 构建返回数据
|
||
user_list = []
|
||
for guanshi in guanshi_list:
|
||
user_main = guanshi.user
|
||
|
||
user_data = {
|
||
'yonghuid': user_main.UserUID,
|
||
'avatar': user_main.Avatar or '',
|
||
'zhanghaozhuangtai': guanshi.zhuangtai,
|
||
'nicheng': f'管事{user_main.UserUID}',
|
||
}
|
||
|
||
user_list.append(user_data)
|
||
|
||
return user_list, has_more
|
||
|
||
except Exception as e:
|
||
logger.error(f"查询管事用户失败: {str(e)}")
|
||
return [], False
|
||
|
||
def _query_shangjia_users(self, keyword, offset, limit):
|
||
"""查询商家用户"""
|
||
try:
|
||
queryset = UserShangjia.query.select_related('user')
|
||
|
||
if keyword:
|
||
queryset = queryset.filter(
|
||
Q(user__UserUID__icontains=keyword) |
|
||
Q(nicheng__icontains=keyword)
|
||
)
|
||
|
||
# 获取总数
|
||
total_count = queryset.count()
|
||
|
||
# 获取当前页的数据
|
||
shangjia_list = list(queryset.order_by('-CreateTime')[offset:offset + limit])
|
||
|
||
# 计算是否有更多数据
|
||
current_count = len(shangjia_list)
|
||
has_more = (offset + current_count) < total_count
|
||
|
||
|
||
|
||
# 构建返回数据
|
||
user_list = []
|
||
for shangjia in shangjia_list:
|
||
user_main = shangjia.user
|
||
|
||
user_data = {
|
||
'yonghuid': user_main.UserUID,
|
||
'avatar': user_main.Avatar or '',
|
||
'zhanghaozhuangtai': shangjia.zhuangtai,
|
||
'nicheng': shangjia.nicheng or '',
|
||
}
|
||
|
||
user_list.append(user_data)
|
||
|
||
return user_list, has_more
|
||
|
||
except Exception as e:
|
||
logger.error(f"查询商家用户失败: {str(e)}")
|
||
return [], False
|
||
|
||
|
||
|
||
|
||
|
||
class CfGuanLi(APIView):
|
||
"""
|
||
后台处罚管理列表接口
|
||
权限: JWT Token认证 + 管理员身份验证
|
||
前端调用: POST /yonghu/cfgl
|
||
参数: {
|
||
"zhanghao": "管理员账号",
|
||
"qingqiu_tongji": true/false, # 是否只请求统计信息
|
||
"sqzhuangtai": 0 或 [1,2], # 处罚申请状态筛选(可选)
|
||
"page": "页码(从1开始)", # 分页参数
|
||
"page_size": "每页数量" # 分页参数
|
||
}
|
||
返回: {
|
||
"code": 0,
|
||
"message": "成功",
|
||
"data": {
|
||
"list": [处罚记录列表],
|
||
"zongshu": 总数,
|
||
"daichuli": 待处理数量,
|
||
"yichuli": 已处理数量,
|
||
"haiyougengduo": true/false
|
||
}
|
||
}
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 1. 获取前端参数
|
||
qianzhanghao = request.data.get('zhanghao', '').strip()
|
||
qingqiu_tongji = request.data.get('qingqiu_tongji', False)
|
||
sqzhuangtai = request.data.get('sqzhuangtai')
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 30))
|
||
|
||
# 验证参数
|
||
if not qianzhanghao:
|
||
return Response(
|
||
{'code': 400, 'message': '参数不完整', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 2. JWT Token身份验证
|
||
dangqianyonghu = request.user
|
||
|
||
# 3. 验证手机号是否匹配
|
||
if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 4. 验证用户类型是否为管理员
|
||
if dangqianyonghu.UserType != 'admin':
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 5. 验证管理员扩展表是否存在
|
||
try:
|
||
guanliyuan = dangqianyonghu.AdminProfile
|
||
except AttributeError:
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 7. 获取统计信息(总是需要统计)
|
||
zongshu = PenaltyRecord.query.count()
|
||
daichuli = PenaltyRecord.query.filter(ApplyStatus__in=[0, 3]).count()
|
||
yichuli = PenaltyRecord.query.filter(ApplyStatus__in=[1, 2]).count()
|
||
|
||
# 8. 如果只请求统计信息,直接返回
|
||
if qingqiu_tongji:
|
||
return Response({
|
||
'code': 0,
|
||
'message': '成功',
|
||
'data': {
|
||
'zongshu': zongshu,
|
||
'daichuli': daichuli,
|
||
'yichuli': yichuli
|
||
}
|
||
})
|
||
|
||
# 9. 构建查询集
|
||
queryset = PenaltyRecord.query.all()
|
||
|
||
# 10. 根据状态筛选
|
||
if sqzhuangtai is not None:
|
||
if isinstance(sqzhuangtai, list):
|
||
# 如果是数组,表示多个状态
|
||
queryset = queryset.filter(ApplyStatus__in=sqzhuangtai)
|
||
else:
|
||
# 单个状态
|
||
queryset = queryset.filter(ApplyStatus=sqzhuangtai)
|
||
|
||
# 11. 获取总数(用于分页计算)
|
||
total_count = queryset.count()
|
||
|
||
# 12. 计算分页偏移量
|
||
offset = (page - 1) * page_size
|
||
|
||
# 13. 分页查询
|
||
chufa_list = list(queryset.order_by('-CreateTime')[offset:offset + page_size])
|
||
|
||
# 🔴【新增】高效获取图片信息
|
||
if chufa_list:
|
||
# 收集所有需要查询的组合
|
||
dingdan_ids = []
|
||
yonghu_combinations = [] # 保存 (dingdan_id, yonghuid) 组合
|
||
|
||
for chufa in chufa_list:
|
||
if chufa.OrderID:
|
||
dingdan_ids.append(chufa.OrderID)
|
||
# 商家图片组合
|
||
if chufa.ApplicantID:
|
||
yonghu_combinations.append((chufa.OrderID, chufa.ApplicantID))
|
||
# 打手图片组合
|
||
if chufa.PlayerID:
|
||
yonghu_combinations.append((chufa.OrderID, chufa.PlayerID))
|
||
|
||
# 去重
|
||
dingdan_ids = list(set(dingdan_ids))
|
||
yonghu_combinations = list(set(yonghu_combinations))
|
||
|
||
# 🔴【新增】批量查询图片表(高效查询,避免循环)
|
||
tupian_mapping = {}
|
||
if yonghu_combinations:
|
||
# 构建查询条件
|
||
query_conditions = Q()
|
||
for dingdan_id, yonghuid in yonghu_combinations:
|
||
query_conditions |= Q(OrderID=dingdan_id, UserID=yonghuid)
|
||
|
||
# 批量查询
|
||
tupian_queryset = PenaltyEvidenceImage.query.filter(query_conditions).values(
|
||
'OrderID', 'UserID', 'ImageURL'
|
||
)
|
||
|
||
# 构建映射:key为"OrderID_UserID",value为图片URL列表
|
||
for item in tupian_queryset:
|
||
key = f"{item['OrderID']}_{item['UserID']}"
|
||
if key not in tupian_mapping:
|
||
tupian_mapping[key] = []
|
||
if item['ImageURL']:
|
||
tupian_mapping[key].append(item['ImageURL'])
|
||
else:
|
||
tupian_mapping = {}
|
||
|
||
# 🔴【修改】14. 构建返回数据 - 添加图片和申诉理由字段
|
||
chufa_data = []
|
||
for chufa in chufa_list:
|
||
# 🔴【新增】获取商家证据图片
|
||
zhengju_tupian = []
|
||
if chufa.OrderID and chufa.ApplicantID:
|
||
key = f"{chufa.OrderID}_{chufa.ApplicantID}"
|
||
zhengju_tupian = tupian_mapping.get(key, [])
|
||
|
||
# 🔴【新增】获取打手申诉图片
|
||
shensu_tupian = []
|
||
if chufa.OrderID and chufa.PlayerID:
|
||
key = f"{chufa.OrderID}_{chufa.PlayerID}"
|
||
shensu_tupian = tupian_mapping.get(key, [])
|
||
|
||
chufa_data.append({
|
||
'dashouid': chufa.PlayerID,
|
||
'qingqiuid': chufa.ApplicantID,
|
||
'chuliid': chufa.ProcessorID,
|
||
'cfliyou': chufa.PenaltyReason,
|
||
'sqzhuangtai': chufa.ApplyStatus,
|
||
'bhliyou': chufa.RejectReason,
|
||
'jifen': chufa.DeductedPoints,
|
||
'dingdan_id': chufa.OrderID,
|
||
'CreateTime': chufa.CreateTime,
|
||
'UpdateTime': chufa.UpdateTime,
|
||
# 🔴【新增】申诉理由字段
|
||
'ssliyou': chufa.AppealReason or '',
|
||
# 🔴【新增】商家证据图片
|
||
'zhengju_tupian': zhengju_tupian,
|
||
# 🔴【新增】打手申诉图片
|
||
'shensu_tupian': shensu_tupian,
|
||
})
|
||
|
||
# 15. 计算是否还有更多数据
|
||
current_count = len(chufa_list)
|
||
haiyougengduo = (offset + current_count) < total_count
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'message': '成功',
|
||
'data': {
|
||
'list': chufa_data,
|
||
'zongshu': zongshu,
|
||
'daichuli': daichuli,
|
||
'yichuli': yichuli,
|
||
'haiyougengduo': haiyougengduo
|
||
}
|
||
})
|
||
|
||
except ValueError:
|
||
return Response(
|
||
{'code': 400, 'message': '参数类型错误', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
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
|
||
)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class AdTongYiChuFa(APIView):
|
||
"""
|
||
后台同意/驳回处罚接口(已修复重复记录问题)
|
||
权限: JWT Token认证 + 管理员身份验证
|
||
前端调用: POST /yonghu/adtycf
|
||
参数: {
|
||
"zhanghao": "管理员账号",
|
||
"dingdan_id": "订单ID",
|
||
"caozuo": 1或2, # 1=同意处罚,2=拒绝处罚
|
||
"bohui_liyou": "拒绝理由" # 拒绝处罚时必填
|
||
}
|
||
返回: {
|
||
"code": 0,
|
||
"message": "成功",
|
||
"data": null
|
||
}
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 1. 获取前端参数
|
||
qianzhanghao = request.data.get('zhanghao', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
caozuo = request.data.get('caozuo') # 1=同意,2=拒绝
|
||
bohui_liyou = request.data.get('bohui_liyou', '').strip()
|
||
|
||
# 验证参数
|
||
if not qianzhanghao or not dingdan_id or caozuo is None:
|
||
return Response(
|
||
{'code': 400, 'message': '参数不完整', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 验证操作类型
|
||
if caozuo not in [1, 2]:
|
||
return Response(
|
||
{'code': 400, 'message': '操作类型错误', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 如果是拒绝处罚,需要拒绝理由
|
||
if caozuo == 2 and not bohui_liyou:
|
||
return Response(
|
||
{'code': 400, 'message': '拒绝处罚需要填写理由', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 2. JWT Token身份验证
|
||
dangqianyonghu = request.user
|
||
|
||
# 3. 验证手机号是否匹配
|
||
if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 4. 验证用户类型是否为管理员
|
||
if dangqianyonghu.UserType != 'admin':
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 5. 验证管理员扩展表是否存在
|
||
try:
|
||
guanliyuan = dangqianyonghu.AdminProfile
|
||
except AttributeError:
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 6. 使用事务保证数据一致性
|
||
with transaction.atomic():
|
||
# 8. 🔴 修复:查询处罚记录(处理重复记录问题)
|
||
# 使用 filter().first() 而不是 get(),以防出现多条记录
|
||
chufajilu = PenaltyRecord.query.filter(
|
||
OrderID=dingdan_id,
|
||
ApplyStatus__in=[0, 3] # 🔴【修改】包括待处理和申诉中状态
|
||
).order_by('-CreateTime').first() # 取最新的一条
|
||
|
||
if not chufajilu:
|
||
# 如果没有待处理的处罚记录,检查是否有已处理的
|
||
exists_processed = PenaltyRecord.query.filter(OrderID=dingdan_id).exists()
|
||
if exists_processed:
|
||
return Response(
|
||
{'code': 400, 'message': '该处罚记录已处理', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
else:
|
||
return Response(
|
||
{'code': 404, 'message': '处罚记录不存在', 'data': None},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# 9. 验证处罚记录状态(必须是待处理或申诉中状态才能处理)
|
||
if chufajilu.ApplyStatus not in [0, 3]: # 🔴【修改】包括待处理(0)和申诉中(3)
|
||
return Response(
|
||
{'code': 400, 'message': '该处罚记录已处理', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 10. 🔴 修复:检查是否有重复的待处理/申诉中记录,并记录警告
|
||
duplicate_count = PenaltyRecord.query.filter(
|
||
OrderID=dingdan_id,
|
||
ApplyStatus__in=[0, 3] # 🔴【修改】包括待处理和申诉中
|
||
).count()
|
||
|
||
if duplicate_count > 1:
|
||
# 记录警告日志
|
||
logger.warning(f"订单ID {dingdan_id} 存在 {duplicate_count} 条待处理/申诉中处罚记录,处理最新的一条")
|
||
|
||
# 🔴 可选:将其他重复的待处理/申诉中记录标记为已拒绝
|
||
# 防止用户重复提交相同订单的处罚申请
|
||
duplicate_records = PenaltyRecord.query.filter(
|
||
OrderID=dingdan_id,
|
||
ApplyStatus__in=[0, 3] # 🔴【修改】包括待处理和申诉中
|
||
).exclude(id=chufajilu.id)
|
||
|
||
for dup in duplicate_records:
|
||
dup.ApplyStatus = 2 # 已拒绝
|
||
dup.ProcessorID = qianzhanghao
|
||
dup.RejectReason = "系统自动拒绝:存在重复处罚记录"
|
||
dup.save()
|
||
|
||
# 11. 根据操作类型处理
|
||
if caozuo == 1:
|
||
# 同意处罚 - 不扣分,只改状态
|
||
# 11.1 检查打手是否存在
|
||
if not chufajilu.PlayerID:
|
||
return Response(
|
||
{'code': 400, 'message': '被处罚打手ID不存在', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 11.2 查询打手扩展表(只是验证存在性)
|
||
try:
|
||
dashou = UserDashou.query.get(user__UserUID=chufajilu.PlayerID)
|
||
except UserDashou.DoesNotExist:
|
||
return Response(
|
||
{'code': 404, 'message': '被处罚打手不存在', 'data': None},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# 🔴【修改】11.3 同意处罚时不扣分(因为申请时已经扣了),只更新状态
|
||
chufajilu.ApplyStatus = 1 # 已处罚
|
||
chufajilu.ProcessorID = qianzhanghao # 处理人ID为管理员账号
|
||
chufajilu.save()
|
||
|
||
# 11.4 更新商家订单扩展表
|
||
try:
|
||
dingdan = Order.query.get(OrderID=dingdan_id)
|
||
if hasattr(dingdan, 'shangjia_kuozhan'):
|
||
shangjia_kuozhan = dingdan.shangjia_kuozhan
|
||
shangjia_kuozhan.PenaltyApplyStatus = 1 # 已处罚
|
||
shangjia_kuozhan.save()
|
||
except (Order.DoesNotExist, AttributeError):
|
||
# 如果订单或扩展表不存在,继续执行,不中断
|
||
pass
|
||
|
||
logger.info(f"同意处罚成功:订单ID={dingdan_id},打手ID={chufajilu.PlayerID}")
|
||
|
||
elif caozuo == 2:
|
||
# 拒绝处罚 - 需要返还积分(加分)
|
||
# 🔴【新增】11.5 返还打手积分(加5分)
|
||
if chufajilu.DeductedPoints > 0:
|
||
try:
|
||
dashou = UserDashou.query.get(user__UserUID=chufajilu.PlayerID)
|
||
|
||
# 🔴【新增】安全验证:必须积分小于等于5分才允许加分
|
||
if dashou.jifen <= 5: # 判断积分是否小于等于5分
|
||
dashou.jifen += chufajilu.DeductedPoints # 加5分
|
||
dashou.save()
|
||
logger.info(
|
||
f"拒绝处罚,给打手{dashou.user.UserUID}返还积分{chufajilu.DeductedPoints}分,当前积分{dashou.jifen}")
|
||
else:
|
||
# 积分已超过5分,可能是异常情况,记录警告但不中断处理
|
||
logger.warning(
|
||
f"拒绝处罚时打手积分异常:打手ID={chufajilu.PlayerID},当前积分{dashou.jifen}已超过5分,不再加分")
|
||
except UserDashou.DoesNotExist:
|
||
# 打手不存在,记录错误,但继续处理状态
|
||
logger.error(f"拒绝处罚时打手不存在:打手ID={chufajilu.PlayerID}")
|
||
|
||
# 11.6 更新处罚记录
|
||
chufajilu.ApplyStatus = 2 # 已拒绝
|
||
chufajilu.ProcessorID = qianzhanghao # 处理人ID为管理员账号
|
||
chufajilu.RejectReason = bohui_liyou # 拒绝理由
|
||
chufajilu.save()
|
||
|
||
# 11.7 更新商家订单扩展表
|
||
try:
|
||
dingdan = Order.query.get(OrderID=dingdan_id)
|
||
if hasattr(dingdan, 'shangjia_kuozhan'):
|
||
shangjia_kuozhan = dingdan.shangjia_kuozhan
|
||
shangjia_kuozhan.PenaltyApplyStatus = 2 # 已拒绝
|
||
shangjia_kuozhan.RejectReason = bohui_liyou # 拒绝理由
|
||
shangjia_kuozhan.save()
|
||
except (Order.DoesNotExist, AttributeError):
|
||
# 如果订单或扩展表不存在,继续执行,不中断
|
||
pass
|
||
|
||
logger.info(f"拒绝处罚成功:订单ID={dingdan_id},拒绝理由={bohui_liyou}")
|
||
|
||
# 12. 返回成功响应
|
||
return Response({
|
||
'code': 0,
|
||
'message': '处理成功',
|
||
'data': None
|
||
})
|
||
|
||
except ValueError:
|
||
return Response(
|
||
{'code': 400, 'message': '参数类型错误', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
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
|
||
)
|
||
|
||
|
||
|
||
|
||
# yonghu/views.py
|
||
class AdckyhxqView(APIView):
|
||
"""
|
||
获取用户详情接口
|
||
前端需要传递: zhanghao, uid, user_type
|
||
返回用户信息和会员列表
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# ==================== 管理员身份验证 ====================
|
||
# 1. 验证JWT Token
|
||
if not request.user.is_authenticated:
|
||
return Response({
|
||
'code': 401,
|
||
'message': '未认证',
|
||
'data': None
|
||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 验证用户类型为管理员
|
||
if request.user.UserType != 'admin':
|
||
return Response({
|
||
'code': 403,
|
||
'message': '权限不足',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 3. 验证管理员扩展表存在
|
||
admin_profile = getattr(request.user, 'AdminProfile', None)
|
||
if not admin_profile:
|
||
return Response({
|
||
'code': 403,
|
||
'message': '管理员信息不完整',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 4. 验证管理员账号(phone)与前端传递的zhanghao一致
|
||
zhanghao = request.data.get('zhanghao')
|
||
if not zhanghao or request.user.Phone != zhanghao:
|
||
return Response({
|
||
'code': 401,
|
||
'message': '账号验证失败',
|
||
'data': None
|
||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||
# ==================== 身份验证结束 ====================
|
||
|
||
# 获取业务参数
|
||
uid = request.data.get('uid') # 用户ID (yonghuid)
|
||
user_type = request.data.get('user_type') # 用户类型 (1,2,3,4)
|
||
|
||
if not uid or not user_type:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '参数缺失',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证用户类型
|
||
try:
|
||
user_type = int(user_type)
|
||
if user_type not in [1, 2, 3, 4]:
|
||
raise ValueError
|
||
except ValueError:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '用户类型无效',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 查询用户主表 - 使用select_related优化查询
|
||
user_main = User.query.filter(UserUID=uid).first()
|
||
if not user_main:
|
||
return Response({
|
||
'code': 404,
|
||
'message': '用户不存在',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 构建响应数据
|
||
response_data = {
|
||
'user_info': {},
|
||
'member_list': [],
|
||
'all_member_list': []
|
||
}
|
||
|
||
# 通用字段(所有用户都有)
|
||
user_info = {
|
||
'yonghuid': user_main.UserUID,
|
||
'avatar': user_main.Avatar,
|
||
'phone': user_main.Phone,
|
||
'ip': user_main.IP,
|
||
'CreateTime': user_main.UserCreateTime,
|
||
'UpdateTime': user_main.UserCreateTime,
|
||
'user_type': user_type
|
||
}
|
||
|
||
# 根据用户类型查询扩展信息
|
||
if user_type == 1: # 老板
|
||
# 使用select_related预取boss_profile
|
||
user_main = User.query.filter(UserUID=uid).select_related('BossProfile').first()
|
||
if getattr(user_main, 'BossProfile', None):
|
||
boss_profile = user_main.BossProfile
|
||
user_info.update({
|
||
'nickname': boss_profile.nickname,
|
||
'zonge': boss_profile.zonge,
|
||
'alldingdan': boss_profile.alldingdan,
|
||
'alltui': boss_profile.alltui,
|
||
'CreateTime_boss': boss_profile.CreateTime
|
||
})
|
||
|
||
elif user_type == 2: # 打手
|
||
# 使用select_related预取dashou_profile
|
||
user_main = User.query.filter(UserUID=uid).select_related('DashouProfile').first()
|
||
if getattr(user_main, 'DashouProfile', None):
|
||
dashou_profile = user_main.DashouProfile
|
||
|
||
# 构建打手信息
|
||
user_info.update({
|
||
'nicheng': dashou_profile.nicheng,
|
||
'chenghao': dashou_profile.chenghao,
|
||
'zhuangtai': dashou_profile.zhuangtai,
|
||
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
|
||
'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
|
||
'jieshao': dashou_profile.jieshao,
|
||
'jiedanzongliang': dashou_profile.jiedanzongliang,
|
||
'chengjiaozongliang': dashou_profile.chengjiaozongliang,
|
||
'tuikuanliang': dashou_profile.tuikuanliang,
|
||
'yue': dashou_profile.yue, # 打手可提现余额
|
||
'zonge': dashou_profile.zonge, # 打手赚取总额
|
||
'dianhua': dashou_profile.dianhua,
|
||
'wechat': dashou_profile.wechat,
|
||
'yaoqingren': dashou_profile.yaoqingren,
|
||
'jinrijiedan': dashou_profile.jinrijiedan,
|
||
'jinrishouyi': dashou_profile.jinrishouyi,
|
||
'jinyuejiedan': dashou_profile.jinyuejiedan,
|
||
'jinyueshouyi': dashou_profile.jinyueshouyi,
|
||
'jifen': dashou_profile.jifen,
|
||
'yajin': dashou_profile.yajin,
|
||
'CreateTime_dashou': dashou_profile.CreateTime
|
||
})
|
||
|
||
# 查询打手的会员信息(只返回未过期的)
|
||
# 使用select_related预取huiyuan信息(如果有关联的话)
|
||
member_records = Huiyuangoumai.query.filter(
|
||
yonghu_id=uid
|
||
).order_by('-CreateTime')
|
||
|
||
member_list = []
|
||
for record in member_records:
|
||
# 调用方法检查是否过期,并更新状态
|
||
is_expired = record.jiance_shifou_daoqi()
|
||
|
||
if not is_expired: # 只返回未过期的
|
||
# 查询会员详情
|
||
huiyuan = Huiyuan.query.filter(
|
||
huiyuan_id=record.huiyuan_id
|
||
).first()
|
||
|
||
if huiyuan:
|
||
member_list.append({
|
||
'huiyuan_id': record.huiyuan_id,
|
||
'huiyuan_name': huiyuan.jieshao,
|
||
'daoqi_time': record.daoqi_time,
|
||
'is_active': record.huiyuan_zhuangtai == 1,
|
||
'CreateTime': record.CreateTime
|
||
})
|
||
|
||
response_data['member_list'] = member_list
|
||
|
||
elif user_type == 3: # 管事
|
||
# 使用select_related预取guanshi_profile
|
||
user_main = User.query.filter(UserUID=uid).select_related('GuanshiProfile').first()
|
||
if getattr(user_main, 'GuanshiProfile', None):
|
||
guanshi_profile = user_main.GuanshiProfile
|
||
user_info.update({
|
||
'nicheng': user_main.Phone, # 管事昵称用手机号
|
||
'yaoqingma': guanshi_profile.yaoqingma,
|
||
'dianhua': guanshi_profile.dianhua,
|
||
'wechat': guanshi_profile.wechat,
|
||
'yaogingshuliang': guanshi_profile.yaogingshuliang,
|
||
'zhuangtai': guanshi_profile.zhuangtai,
|
||
'jinrichongzhi': guanshi_profile.jinrichongzhi,
|
||
'jinyuechongzhi': guanshi_profile.jinyuechongzhi,
|
||
'chongzhifenrun': guanshi_profile.chongzhifenrun,
|
||
'yue': guanshi_profile.yue, # 管事可提现余额
|
||
'CreateTime_guanshi': guanshi_profile.CreateTime
|
||
})
|
||
|
||
elif user_type == 4: # 商家
|
||
# 使用select_related预取shop_profile
|
||
user_main = User.query.filter(UserUID=uid).select_related('ShopProfile').first()
|
||
if getattr(user_main, 'ShopProfile', None):
|
||
shop_profile = user_main.ShopProfile
|
||
user_info.update({
|
||
'nicheng': shop_profile.nicheng,
|
||
'zhuangtai': shop_profile.zhuangtai,
|
||
'dianhua': shop_profile.dianhua,
|
||
'wechat': shop_profile.wechat,
|
||
'fabu': shop_profile.fabu,
|
||
'tuikuan': shop_profile.tuikuan,
|
||
'yue': shop_profile.yue, # 商家余额
|
||
'chengjiao': shop_profile.chengjiao,
|
||
'jinridingdan': shop_profile.jinridingdan,
|
||
'jinriliushui': shop_profile.jinriliushui,
|
||
'jinyuedingdan': shop_profile.jinyuedingdan,
|
||
'jinyueliushui': shop_profile.jinyueliushui,
|
||
'CreateTime_shop': shop_profile.CreateTime
|
||
})
|
||
|
||
# 获取所有会员列表(用于前端下拉选择)
|
||
all_huiyuan = Huiyuan.query.all().order_by('jiage')
|
||
all_member_list = [
|
||
{
|
||
'id': huiyuan.huiyuan_id,
|
||
'nicheng': huiyuan.jieshao,
|
||
'jiage': huiyuan.jiage,
|
||
'jieshao': huiyuan.jtjieshao
|
||
}
|
||
for huiyuan in all_huiyuan
|
||
]
|
||
|
||
response_data['user_info'] = user_info
|
||
response_data['all_member_list'] = all_member_list
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'message': '获取成功',
|
||
'data': response_data
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
return Response({
|
||
'code': 500,
|
||
'message': f'获取用户详情失败: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
class AdcjxgView(APIView):
|
||
"""
|
||
修改用户信息接口
|
||
前端需要传递: zhanghao, uid, user_type, 以及其他修改的字段
|
||
支持打手会员添加功能
|
||
敏感字段修改会记录到修改记录表
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# ==================== 管理员身份验证 ====================
|
||
if not request.user.is_authenticated:
|
||
return Response({
|
||
'code': 401,
|
||
'message': '未认证',
|
||
'data': None
|
||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if request.user.UserType != 'admin':
|
||
return Response({
|
||
'code': 403,
|
||
'message': '权限不足',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
admin_profile = getattr(request.user, 'AdminProfile', None)
|
||
if not admin_profile:
|
||
return Response({
|
||
'code': 403,
|
||
'message': '管理员信息不完整',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
zhanghao = request.data.get('zhanghao')
|
||
if not zhanghao or request.user.Phone != zhanghao:
|
||
return Response({
|
||
'code': 401,
|
||
'message': '账号验证失败',
|
||
'data': None
|
||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||
# ==================== 身份验证结束 ====================
|
||
|
||
# 获取业务参数
|
||
uid = request.data.get('uid')
|
||
user_type = request.data.get('user_type')
|
||
|
||
if not uid or not user_type:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '参数缺失',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
user_type = int(user_type)
|
||
if user_type not in [1, 2, 3, 4]:
|
||
raise ValueError
|
||
except ValueError:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '用户类型无效',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 查询用户主表
|
||
user_main = User.query.filter(UserUID=uid).first()
|
||
if not user_main:
|
||
return Response({
|
||
'code': 404,
|
||
'message': '用户不存在',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 使用事务确保数据一致性
|
||
with transaction.atomic():
|
||
# 存储修改记录的数据
|
||
modify_record_data = {
|
||
'yonghuid': uid,
|
||
'xiugaiid': zhanghao,
|
||
'leixing': user_type,
|
||
'qitashuoming': '',
|
||
'CreateTime': timezone.now()
|
||
}
|
||
|
||
# 标记是否有敏感字段被修改
|
||
has_sensitive_change = False
|
||
|
||
if user_type == 1: # 老板 - 不允许修改
|
||
return Response({
|
||
'code': 403,
|
||
'message': '老板信息不允许修改',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
elif user_type == 2: # 打手
|
||
# 获取或创建打手扩展记录
|
||
dashou_profile, created = UserDashou.query.get_or_create(
|
||
user=user_main,
|
||
defaults={'nicheng': user_main.Phone or f'打手{uid}'}
|
||
)
|
||
|
||
# 记录敏感字段的旧值
|
||
old_yue = dashou_profile.yue # 打手可提现余额
|
||
old_zonge = dashou_profile.zonge # 打手提现金额
|
||
old_jifen = dashou_profile.jifen # 打手积分
|
||
old_yajin = dashou_profile.yajin # 打手押金
|
||
|
||
# 需要更新的字段列表
|
||
update_fields = [
|
||
'chenghao', 'dianhua', 'yue', 'zonge', 'jifen',
|
||
'yajin', 'yaoqingren', 'jieshao', 'zhuangtai',
|
||
'zaixianzhuangtai', 'zhanghaozhuangtai'
|
||
]
|
||
|
||
# 记录被修改的字段
|
||
modified_fields = []
|
||
|
||
for field in update_fields:
|
||
if field in request.data:
|
||
new_value = request.data.get(field)
|
||
old_value = getattr(dashou_profile, field)
|
||
|
||
# 检查值是否改变
|
||
if str(new_value) != str(old_value):
|
||
# 处理特殊字段类型
|
||
if field in ['yue', 'zonge', 'yajin']:
|
||
new_value = Decimal(str(new_value))
|
||
setattr(dashou_profile, field, new_value)
|
||
elif field in ['jifen']:
|
||
new_value = int(new_value)
|
||
setattr(dashou_profile, field, new_value)
|
||
elif field in ['zhuangtai', 'zaixianzhuangtai', 'zhanghaozhuangtai']:
|
||
new_value = int(new_value)
|
||
setattr(dashou_profile, field, new_value)
|
||
else:
|
||
setattr(dashou_profile, field, new_value)
|
||
|
||
modified_fields.append(field)
|
||
|
||
# 记录敏感字段修改
|
||
if field == 'yue':
|
||
has_sensitive_change = True
|
||
modify_record_data['xiugaitijiao'] = new_value # 使用xiugaitijiao字段记录打手余额修改
|
||
modify_record_data['xiugaitijiaoq'] = old_yue
|
||
elif field == 'zonge':
|
||
has_sensitive_change = True
|
||
# zonge字段在修改记录表中可能没有直接对应,可以记录在备注中
|
||
modify_record_data['qitashuoming'] += f' 打手总额从{old_zonge}修改为{new_value}'
|
||
elif field == 'jifen':
|
||
has_sensitive_change = True
|
||
modify_record_data['jifen'] = new_value
|
||
modify_record_data['yjifen'] = old_jifen
|
||
elif field == 'yajin':
|
||
has_sensitive_change = True
|
||
modify_record_data['yajin'] = new_value
|
||
modify_record_data['yyajin'] = old_yajin
|
||
|
||
# 保存打手信息
|
||
dashou_profile.save()
|
||
|
||
# 记录被修改的非敏感字段
|
||
non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'zonge', 'jifen', 'yajin']]
|
||
if non_sensitive_fields:
|
||
modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
|
||
|
||
elif user_type == 3: # 管事
|
||
# 获取或创建管事扩展记录
|
||
guanshi_profile, created = UserGuanshi.query.get_or_create(
|
||
user=user_main,
|
||
defaults={'yaoqingma': f'GS{uid[-4:]}'}
|
||
)
|
||
|
||
# 记录敏感字段的旧值
|
||
old_yue = guanshi_profile.yue # 管事可提现余额
|
||
|
||
update_fields = [
|
||
'yaogingshuliang', 'chongzhifenrun', 'yue',
|
||
'zhuangtai', 'dianhua', 'wechat'
|
||
]
|
||
|
||
modified_fields = []
|
||
|
||
for field in update_fields:
|
||
if field in request.data:
|
||
new_value = request.data.get(field)
|
||
old_value = getattr(guanshi_profile, field)
|
||
|
||
if str(new_value) != str(old_value):
|
||
if field in ['chongzhifenrun', 'yue']:
|
||
new_value = Decimal(str(new_value))
|
||
setattr(guanshi_profile, field, new_value)
|
||
elif field in ['yaogingshuliang']:
|
||
new_value = int(new_value)
|
||
setattr(guanshi_profile, field, new_value)
|
||
elif field in ['zhuangtai']:
|
||
new_value = int(new_value)
|
||
setattr(guanshi_profile, field, new_value)
|
||
else:
|
||
setattr(guanshi_profile, field, new_value)
|
||
|
||
modified_fields.append(field)
|
||
|
||
# 记录敏感字段修改
|
||
if field == 'yue':
|
||
has_sensitive_change = True
|
||
modify_record_data['guanshiyue'] = new_value
|
||
modify_record_data['guanshiyueq'] = old_yue
|
||
elif field == 'chongzhifenrun':
|
||
has_sensitive_change = True
|
||
modify_record_data['qitashuoming'] += f' 管事分佣从{old_value}修改为{new_value}'
|
||
|
||
guanshi_profile.save()
|
||
|
||
# 记录非敏感字段
|
||
non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'chongzhifenrun']]
|
||
if non_sensitive_fields:
|
||
modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
|
||
|
||
elif user_type == 4: # 商家
|
||
# 获取或创建商家扩展记录
|
||
shop_profile, created = UserShangjia.query.get_or_create(
|
||
user=user_main,
|
||
defaults={'nicheng': user_main.Phone or f'商家{uid}'}
|
||
)
|
||
|
||
# 记录敏感字段的旧值
|
||
old_yue = shop_profile.yue # 商家余额
|
||
|
||
update_fields = ['yue', 'zhuangtai', 'dianhua', 'wechat']
|
||
|
||
modified_fields = []
|
||
|
||
for field in update_fields:
|
||
if field in request.data:
|
||
new_value = request.data.get(field)
|
||
old_value = getattr(shop_profile, field)
|
||
|
||
if str(new_value) != str(old_value):
|
||
if field == 'yue':
|
||
new_value = Decimal(str(new_value))
|
||
setattr(shop_profile, field, new_value)
|
||
elif field == 'zhuangtai':
|
||
new_value = int(new_value)
|
||
setattr(shop_profile, field, new_value)
|
||
else:
|
||
setattr(shop_profile, field, new_value)
|
||
|
||
modified_fields.append(field)
|
||
|
||
# 记录敏感字段修改
|
||
if field == 'yue':
|
||
has_sensitive_change = True
|
||
modify_record_data['shangjiayue'] = new_value
|
||
modify_record_data['shangjiayueq'] = old_yue
|
||
|
||
shop_profile.save()
|
||
|
||
# 记录非敏感字段
|
||
non_sensitive_fields = [f for f in modified_fields if f != 'yue']
|
||
if non_sensitive_fields:
|
||
modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
|
||
|
||
# 处理打手会员添加(只有打手可以添加会员)
|
||
|
||
# 🔥🔥🔥 后端修改:支持正负数会员天数
|
||
# 修改会员处理逻辑部分
|
||
|
||
if user_type == 2 and 'huiyuan_id' in request.data and 'days' in request.data:
|
||
huiyuan_id = request.data.get('huiyuan_id')
|
||
days = int(request.data.get('days', 0))
|
||
|
||
# 🔥 修改:允许正负数,但不能为0
|
||
if huiyuan_id and days != 0:
|
||
# 🔥 修改:检查绝对值范围
|
||
if abs(days) < 1 or abs(days) > 10000:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '会员天数必须在1-10000之间(正数增加,负数减少)',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 检查会员是否存在
|
||
huiyuan = Huiyuan.query.filter(huiyuan_id=huiyuan_id).first()
|
||
if not huiyuan:
|
||
return Response({
|
||
'code': 404,
|
||
'message': '会员不存在',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 检查是否已购买该会员
|
||
existing_record = Huiyuangoumai.query.filter(
|
||
yonghu_id=uid,
|
||
huiyuan_id=huiyuan_id
|
||
).first()
|
||
|
||
if existing_record:
|
||
# 🔥🔥🔥 重要修改:支持减少天数
|
||
# 计算新的到期时间
|
||
new_daoqi_time = existing_record.daoqi_time + timedelta(days=days)
|
||
|
||
# 🔥 安全验证:减少天数后不能小于当前时间前90天(防止过度减少)
|
||
# 这个限制可以根据业务调整
|
||
min_allowed_time = timezone.now() - timedelta(days=90)
|
||
if new_daoqi_time < min_allowed_time:
|
||
return Response({
|
||
'code': 400,
|
||
'message': f'减少天数过多,调整后到期时间不能早于 {(min_allowed_time + timedelta(days=90)).strftime("%Y-%m-%d")}',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
existing_record.daoqi_time = new_daoqi_time
|
||
existing_record.save()
|
||
|
||
# 记录会员操作
|
||
has_sensitive_change = True
|
||
modify_record_data['huiyuants'] = days
|
||
modify_record_data['huiyuan_id'] = huiyuan_id
|
||
|
||
if days > 0:
|
||
modify_record_data['qitashuoming'] += f' 续费会员 {huiyuan.jieshao} {days}天'
|
||
# 只有增加天数时才增加购买次数
|
||
huiyuan.goumai_cishu += 1
|
||
huiyuan.save()
|
||
else:
|
||
modify_record_data['qitashuoming'] += f' 减少会员 {huiyuan.jieshao} {abs(days)}天'
|
||
# 减少天数时不增加购买次数
|
||
|
||
else:
|
||
# 🔥 修改:新购买时只能增加天数
|
||
if days > 0:
|
||
daoqi_time = timezone.now() + timedelta(days=days)
|
||
new_record = Huiyuangoumai.query.create(
|
||
huiyuan_id=huiyuan_id,
|
||
yonghu_id=uid,
|
||
jieshao=huiyuan.jieshao,
|
||
daoqi_time=daoqi_time
|
||
)
|
||
|
||
# 记录会员购买
|
||
has_sensitive_change = True
|
||
modify_record_data['huiyuants'] = days
|
||
modify_record_data['huiyuan_id'] = huiyuan_id
|
||
modify_record_data['qitashuoming'] += f' 购买会员 {huiyuan.jieshao} {days}天'
|
||
|
||
# 更新会员购买次数
|
||
huiyuan.goumai_cishu += 1
|
||
huiyuan.save()
|
||
else:
|
||
# 🔥 不能为不存在的会员减少天数
|
||
return Response({
|
||
'code': 400,
|
||
'message': '用户未购买该会员,无法减少天数',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# 如果有敏感字段被修改或会员操作,创建修改记录
|
||
if has_sensitive_change or 'huiyuan_id' in request.data:
|
||
# 创建修改记录
|
||
Xiugaijilu.query.create(**modify_record_data)
|
||
elif modify_record_data.get('qitashuoming', '').strip():
|
||
# 只有非敏感字段修改,也记录但qitashuoming不同
|
||
Xiugaijilu.query.create(**modify_record_data)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'message': '修改成功',
|
||
'data': None
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
return Response({
|
||
'code': 500,
|
||
'message': f'修改失败: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
# 管理员认证装饰器
|
||
def admin_required(view_func):
|
||
"""管理员权限验证装饰器"""
|
||
|
||
def wrapped_view(self, request, *args, **kwargs):
|
||
try:
|
||
# 1. 验证JWT Token
|
||
if not request.user.is_authenticated:
|
||
return Response({
|
||
'code': 401,
|
||
'message': '未认证',
|
||
'data': None
|
||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 验证用户类型为管理员
|
||
if request.user.UserType != 'admin':
|
||
return Response({
|
||
'code': 403,
|
||
'message': '权限不足',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 3. 验证管理员扩展表存在
|
||
admin_profile = getattr(request.user, 'AdminProfile', None)
|
||
if not admin_profile:
|
||
return Response({
|
||
'code': 403,
|
||
'message': '管理员信息不完整',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 4. 验证管理员账号(phone)与前端传递的zhanghao一致
|
||
zhanghao = request.data.get('zhanghao')
|
||
if not zhanghao or request.user.Phone != zhanghao:
|
||
return Response({
|
||
'code': 401,
|
||
'message': '账号验证失败',
|
||
'data': None
|
||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
return view_func(self, request, *args, **kwargs)
|
||
except Exception as e:
|
||
return Response({
|
||
'code': 500,
|
||
'message': f'权限验证异常: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
return wrapped_view
|
||
|
||
|
||
class AddtxshView(APIView):
|
||
"""
|
||
获取提现审核列表接口
|
||
与用户管理页面保持一致的分页逻辑
|
||
支持按用户ID搜索,排序改为按申请时间升序(最早申请排最前)
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@admin_required
|
||
def post(self, request):
|
||
try:
|
||
# 获取参数
|
||
zhanghao = request.data.get('zhanghao')
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 5))
|
||
zhuangtai = int(request.data.get('zhuangtai', 1))
|
||
leixing = int(request.data.get('leixing', 1))
|
||
fangshi = request.data.get('fangshi')
|
||
|
||
# 🔴【新增】获取搜索参数(用户ID)
|
||
search_uid = request.data.get('search_uid', '').strip()
|
||
|
||
if page < 1:
|
||
page = 1
|
||
|
||
# 计算偏移量(与用户管理页面完全一致)
|
||
offset = (page - 1) * page_size
|
||
|
||
# 构建查询条件
|
||
query = Q()
|
||
|
||
# 状态筛选
|
||
if zhuangtai == 1:
|
||
query &= Q(zhuangtai=1)
|
||
elif zhuangtai == 2:
|
||
query &= Q(zhuangtai__in=[2, 3])
|
||
|
||
# 类型筛选
|
||
query &= Q(leixing=leixing)
|
||
|
||
# 收款方式筛选(可选)
|
||
if fangshi:
|
||
query &= Q(fangshi=int(fangshi))
|
||
|
||
# 🔴【新增】如果搜索了用户ID,添加条件
|
||
if search_uid:
|
||
query &= Q(yonghuid=search_uid)
|
||
|
||
# 🔴【修改】排序改为按申请时间升序(最早申请排最前)
|
||
queryset = Tixianjilu.query.filter(query).order_by('CreateTime')
|
||
|
||
# 🔴【修改】分页逻辑:如果是搜索模式,不分页,返回所有匹配记录
|
||
if search_uid:
|
||
records = list(queryset)
|
||
total_count = len(records)
|
||
has_more = False # 搜索模式没有更多数据
|
||
else:
|
||
total_count = queryset.count()
|
||
records = list(queryset[offset:offset + page_size])
|
||
current_count = len(records)
|
||
has_more = (offset + current_count) < total_count
|
||
|
||
# 获取状态统计(统计与搜索无关,依然按筛选条件统计)
|
||
status_counts = {
|
||
'awaiting': Tixianjilu.query.filter(
|
||
zhuangtai=1,
|
||
leixing=leixing
|
||
).count(),
|
||
'processed': Tixianjilu.query.filter(
|
||
zhuangtai__in=[2, 3],
|
||
leixing=leixing
|
||
).count()
|
||
}
|
||
|
||
# 构建响应数据
|
||
withdrawal_list = []
|
||
for record in records:
|
||
withdrawal_list.append({
|
||
'id': record.id,
|
||
'yonghuid': record.yonghuid,
|
||
'avatar': record.avatar,
|
||
'phone': record.phone,
|
||
'nicheng': record.nicheng,
|
||
'leixing': record.leixing,
|
||
'zhifu': record.zhifu,
|
||
'skzhanghao': record.skzhanghao,
|
||
'jine': str(record.jine) if record.jine else '0.00',
|
||
'zhuangtai': record.zhuangtai,
|
||
'fangshi': record.fangshi,
|
||
'shenheid': record.shenheid,
|
||
'bhliyou': record.bhliyou,
|
||
'CreateTime': record.CreateTime,
|
||
'UpdateTime': record.UpdateTime
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'message': '获取成功',
|
||
'data': {
|
||
'list': withdrawal_list,
|
||
'total_count': total_count,
|
||
'status_counts': status_counts,
|
||
'has_more': has_more
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
return Response({
|
||
'code': 500,
|
||
'message': f'获取提现列表失败: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
|
||
class AdtixianqkView(APIView):
|
||
"""
|
||
处理提现申请接口(已更新:添加收支记录表更新)
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@admin_required # 假设你有这个装饰器
|
||
def post(self, request):
|
||
try:
|
||
zhanghao = request.data.get('zhanghao')
|
||
tixian_id = request.data.get('tixian_id')
|
||
result = int(request.data.get('result', 0))
|
||
reason = request.data.get('reason', '')
|
||
|
||
if not tixian_id:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '提现记录ID不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if result not in [2, 3]:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '操作结果无效',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 如果是拒绝,需要理由
|
||
if result == 3 and not reason:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '拒绝提现需要提供理由',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 获取提现记录
|
||
try:
|
||
withdrawal = Tixianjilu.query.get(id=tixian_id)
|
||
except Tixianjilu.DoesNotExist:
|
||
return Response({
|
||
'code': 404,
|
||
'message': '提现记录不存在',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 检查状态,只有待审核的可以处理
|
||
if withdrawal.zhuangtai != 1:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '该提现申请已被处理',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 使用事务确保数据一致性
|
||
with transaction.atomic():
|
||
# 更新提现记录
|
||
withdrawal.zhuangtai = result
|
||
withdrawal.shenheid = request.user.UserUID
|
||
|
||
if result == 3: # 拒绝
|
||
withdrawal.bhliyou = reason
|
||
|
||
withdrawal.UpdateTime = timezone.now()
|
||
withdrawal.save()
|
||
|
||
# 🟢 新增:如果同意提现(result=2),更新收支记录表
|
||
if result == 2:
|
||
try:
|
||
# 查询收支记录表,id=1的记录
|
||
szjilu_record = Szjilu.query.get(id=1)
|
||
|
||
# 获取提现金额
|
||
tixian_jine = withdrawal.jine # 假设提现记录模型中有jine字段
|
||
|
||
update_daily_payout(tixian_jine)
|
||
|
||
# 更新收支记录
|
||
szjilu_record.TotalIncome -= tixian_jine # 总收益减去提现金额
|
||
szjilu_record.TotalExpense += tixian_jine # 总支出加上提现金额
|
||
szjilu_record.DailyExpense += tixian_jine # 今日支出加上提现金额
|
||
szjilu_record.UpdateTime = timezone.now()
|
||
szjilu_record.save()
|
||
|
||
except Szjilu.DoesNotExist:
|
||
# 如果收支记录表不存在id=1的记录,创建一个
|
||
szjilu_record = Szjilu.query.create(
|
||
id=1,
|
||
TotalIncome=-tixian_jine if result == 2 else 0,
|
||
TotalExpense=tixian_jine if result == 2 else 0,
|
||
DailyExpense=tixian_jine if result == 2 else 0
|
||
)
|
||
except Exception as e:
|
||
# 如果更新收支记录失败,抛出异常,事务会回滚
|
||
raise Exception(f"更新收支记录失败: {str(e)}")
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'message': '处理成功',
|
||
'data': {
|
||
'id': withdrawal.id,
|
||
'zhuangtai': withdrawal.zhuangtai,
|
||
'shenheid': withdrawal.shenheid,
|
||
'UpdateTime': withdrawal.UpdateTime
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
return Response({
|
||
'code': 500,
|
||
'message': f'处理提现失败: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
|
||
# views.py (yonghu应用)
|
||
class DashouZiliaoHuoquView(APIView):
|
||
"""
|
||
打手资料获取接口
|
||
URL: /yonghu/dszjxxhq
|
||
方法: POST
|
||
认证: JWT Token
|
||
返回: 打手扩展表信息
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 使用select_related高效查询,避免N+1问题
|
||
dashou = UserDashou.query.select_related('user').get(user=request.user)
|
||
|
||
# 构建返回数据
|
||
data = {
|
||
'nicheng': dashou.nicheng if dashou.nicheng else '',
|
||
'jieshao': dashou.jieshao if dashou.jieshao else '',
|
||
'dianhua': dashou.dianhua if dashou.dianhua else '',
|
||
'wechat': dashou.wechat if dashou.wechat else '',
|
||
'uid': request.user.UserUID, # 从主表获取UID
|
||
'avatar': request.user.Avatar if request.user.Avatar else '', # 头像相对URL
|
||
}
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'msg': 'success',
|
||
'data': data
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except UserDashou.DoesNotExist:
|
||
logger.warning(f'打手扩展表不存在: user_id={request.user.id}')
|
||
return Response({
|
||
'code': 404,
|
||
'msg': '打手信息不存在'
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
except Exception as e:
|
||
logger.error(f'获取打手资料异常: {str(e)}', exc_info=True)
|
||
return Response({
|
||
'code': 500,
|
||
'msg': '服务器内部错误'
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
|
||
# views.py (yonghu应用)
|
||
|
||
class DashouGengxinView(APIView):
|
||
"""
|
||
打手信息更新接口
|
||
URL: /yonghu/dsgxxx
|
||
方法: POST
|
||
认证: JWT Token
|
||
参数: nicheng(昵称), dianhua(电话), wechat(微信), jieshao(介绍)
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 获取请求数据
|
||
nicheng = request.data.get('nicheng', '').strip()
|
||
dianhua = request.data.get('dianhua', '').strip()
|
||
wechat = request.data.get('wechat', '').strip()
|
||
jieshao = request.data.get('jieshao', '').strip()
|
||
|
||
# 数据验证
|
||
validation_errors = self.validate_data(nicheng, dianhua, wechat, jieshao)
|
||
if validation_errors:
|
||
return Response({
|
||
'code': 400,
|
||
'msg': validation_errors
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 获取打手扩展表
|
||
try:
|
||
dashou = UserDashou.query.get(user=request.user)
|
||
except UserDashou.DoesNotExist:
|
||
return Response({
|
||
'code': 404,
|
||
'msg': '打手信息不存在'
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 更新数据
|
||
update_fields = []
|
||
|
||
if nicheng is not None:
|
||
dashou.nicheng = nicheng if nicheng != '' else None
|
||
update_fields.append('nicheng')
|
||
|
||
if dianhua is not None:
|
||
dashou.dianhua = dianhua if dianhua != '' else None
|
||
update_fields.append('dianhua')
|
||
|
||
if wechat is not None:
|
||
dashou.wechat = wechat if wechat != '' else None
|
||
update_fields.append('wechat')
|
||
|
||
if jieshao is not None:
|
||
dashou.jieshao = jieshao if jieshao != '' else None
|
||
update_fields.append('jieshao')
|
||
|
||
# 保存更新(只更新修改的字段)
|
||
if update_fields:
|
||
dashou.save(update_fields=update_fields)
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'msg': '更新成功',
|
||
'data': {
|
||
'nicheng': dashou.nicheng or '',
|
||
'dianhua': dashou.dianhua or '',
|
||
'wechat': dashou.wechat or '',
|
||
'jieshao': dashou.jieshao or ''
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
logger.error(f'更新打手信息异常: {str(e)}', exc_info=True)
|
||
return Response({
|
||
'code': 500,
|
||
'msg': '服务器内部错误'
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def validate_data(self, nicheng, dianhua, wechat, jieshao):
|
||
"""数据验证方法"""
|
||
errors = []
|
||
|
||
# 1. 昵称验证(必填)
|
||
if not nicheng:
|
||
errors.append('昵称不能为空')
|
||
elif len(nicheng) > 20:
|
||
errors.append('昵称不能超过20个字符')
|
||
|
||
# 2. 手机号验证(非必填,但填写时必须正确)
|
||
if dianhua:
|
||
# 手机号正则:1开头,11位数字
|
||
phone_pattern = r'^1[3-9]\d{9}$'
|
||
if not re.match(phone_pattern, dianhua):
|
||
errors.append('手机号格式不正确')
|
||
|
||
# 3. 微信号验证(非必填)
|
||
if wechat and len(wechat) > 30:
|
||
errors.append('微信号不能超过30个字符')
|
||
|
||
# 4. 个人介绍验证
|
||
if jieshao and len(jieshao) > 200:
|
||
errors.append('个人介绍不能超过200个字符')
|
||
|
||
return '; '.join(errors) if errors else None
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class ShangjiaZhuceView(APIView):
|
||
"""
|
||
商家注册接口 (通过管理员邀请码)
|
||
访问路径: /api/yonghu/shangjiahuce/
|
||
请求方法: POST
|
||
权限要求: JWT Token认证通过的用户
|
||
请求体: { "inviteCode": "管理员邀请码" }
|
||
|
||
返回字段说明:
|
||
- code: 状态码(200成功,其他失败)
|
||
- message: 提示信息
|
||
- data: 商家信息数据,包含以下字段(与前端商家页面完全一致):
|
||
- nicheng: 商家昵称
|
||
- sjyue: 商家余额(字符串,保留两位小数)
|
||
- fabu: 发布订单总数
|
||
- chengjiao: 成交订单总数
|
||
- tuikuan: 退款订单总数
|
||
- jinridingdan: 今日订单
|
||
- jinriliushui: 今日流水(字符串)
|
||
- jinyuedingdan: 今月订单
|
||
- jinyueliushui: 今月流水(字符串)
|
||
- zhuangtai: 商家状态(1正常)
|
||
- dianhua: 商家电话
|
||
- wechat: 商家微信
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 获取并验证邀请码参数
|
||
yaoqingma = request.data.get('inviteCode')
|
||
if not yaoqingma:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '邀请码不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
yaoqingma = yaoqingma.strip()
|
||
if len(yaoqingma) > 100:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '邀请码长度超过限制',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
current_user = request.user
|
||
|
||
# 2. 检查用户是否已是商家
|
||
try:
|
||
shangjia_profile = current_user.ShopProfile
|
||
# 用户已是商家,直接返回现有信息
|
||
return self.fanhuiXianyouShangjiaXinxi(shangjia_profile, current_user)
|
||
except UserShangjia.DoesNotExist:
|
||
# 用户不是商家,继续注册流程
|
||
pass
|
||
|
||
# 3. 根据邀请码查找对应的管理员
|
||
try:
|
||
# 从管理员表查找邀请码
|
||
admin_profile = AdminProfile.query.select_related('user').get(yaoqingma=yaoqingma)
|
||
except AdminProfile.DoesNotExist:
|
||
return Response({
|
||
'code': 404,
|
||
'message': '管理员邀请码无效或不存在',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 4. 核心:在数据库事务中创建商家身份
|
||
try:
|
||
with transaction.atomic():
|
||
# 4.1 查询老板扩展表获取老板昵称
|
||
boss_nickname = '普通商家'
|
||
try:
|
||
boss_profile = current_user.BossProfile
|
||
if boss_profile.nickname and boss_profile.nickname.strip():
|
||
boss_nickname = boss_profile.nickname.strip()
|
||
except UserBoss.DoesNotExist:
|
||
# 老板扩展表不存在,使用默认值
|
||
pass
|
||
|
||
# 4.2 创建商家扩展表
|
||
# 使用老板昵称作为商家昵称的基础
|
||
shangjia_nicheng = boss_nickname
|
||
|
||
# 检查昵称是否已存在(安全考虑)
|
||
if UserShangjia.query.filter(nicheng=shangjia_nicheng).exists():
|
||
# 如果存在,添加随机数确保唯一
|
||
shangjia_nicheng = f"{boss_nickname}{random.randint(1000, 9999)}"
|
||
|
||
# 4.3 创建商家记录
|
||
shop_profile = UserShangjia.query.create(
|
||
user=current_user,
|
||
nicheng=shangjia_nicheng,
|
||
zhuangtai=1, # 正常状态
|
||
yue=0.00, # 商家余额
|
||
fabu=0, # 发布订单总数
|
||
tuikuan=0, # 退款订单总数
|
||
chengjiao=0, # 成交订单总数
|
||
jinridingdan=0, # 今日订单
|
||
jinriliushui=0.00, # 今日流水
|
||
jinyuedingdan=0, # 今月订单
|
||
jinyueliushui=0.00, # 今月流水
|
||
dianhua='', # 电话
|
||
wechat='', # 微信
|
||
)
|
||
|
||
except Exception as e:
|
||
# 事务会自动回滚
|
||
error_detail = traceback.format_exc()
|
||
#print(f"商家注册错误详情: {error_detail}")
|
||
|
||
return Response({
|
||
'code': 500,
|
||
'message': f'商家注册过程中发生系统错误: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
# 5. 注册成功,返回商家信息
|
||
fanhui_data = self.zhuangbeiFanhuiShuju(shop_profile, current_user)
|
||
return Response({
|
||
'code': 200,
|
||
'message': '商家注册成功!',
|
||
'data': fanhui_data
|
||
})
|
||
|
||
def fanhuiXianyouShangjiaXinxi(self, shangjia_profile, current_user):
|
||
"""处理已是商家的用户,返回其现有信息"""
|
||
fanhui_data = self.zhuangbeiFanhuiShuju(shangjia_profile, current_user)
|
||
return Response({
|
||
'code': 200,
|
||
'message': '您已是商家,返回当前信息。',
|
||
'data': fanhui_data
|
||
})
|
||
|
||
def zhuangbeiFanhuiShuju(self, shangjia_profile, current_user):
|
||
"""
|
||
准备返回给前端的商家数据
|
||
字段名必须与前端商家页面完全一致
|
||
"""
|
||
data = {
|
||
# 🟢 商家基本信息
|
||
'nicheng': shangjia_profile.nicheng, # 商家昵称
|
||
'sjyue': format(shangjia_profile.yue, '.2f'), # 商家余额,保留两位小数
|
||
'fabu': shangjia_profile.fabu, # 发布订单总数
|
||
'chengjiao': shangjia_profile.chengjiao, # 成交订单总数
|
||
'tuikuan': shangjia_profile.tuikuan, # 退款订单总数
|
||
'jinridingdan': shangjia_profile.jinridingdan, # 今日订单
|
||
'jinriliushui': format(shangjia_profile.jinriliushui, '.2f'), # 今日流水
|
||
'jinyuedingdan': shangjia_profile.jinyuedingdan, # 今月订单
|
||
'jinyueliushui': format(shangjia_profile.jinyueliushui, '.2f'), # 今月流水
|
||
'zhuangtai': shangjia_profile.zhuangtai, # 商家状态
|
||
'dianhua': shangjia_profile.dianhua or '', # 电话
|
||
'wechat': shangjia_profile.wechat or '', # 微信
|
||
|
||
# 🟢 可能需要的主表信息
|
||
'uid': current_user.UserUID, # 用户ID
|
||
}
|
||
return data
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# yonghu/ranking_views.py
|
||
"""
|
||
排行榜查询接口 - 管理员专用
|
||
安全认证:JWT Token + 管理员权限验证
|
||
"""
|
||
|
||
# yonghu/ranking_views.py - 修改PaihangbangGuanliQueryView的post方法参数接收
|
||
class PaihangbangGuanliQueryView(APIView):
|
||
"""
|
||
排行榜查询接口 - 管理员专用
|
||
安全要求:
|
||
1. JWT Token认证
|
||
2. 管理员权限验证
|
||
3. 参数严格验证
|
||
4. 返回统一格式
|
||
"""
|
||
|
||
# JWT Token认证
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@admin_required
|
||
def post(self, request):
|
||
"""
|
||
POST请求查询排行榜数据
|
||
参数:
|
||
- zhanghao: 管理员账号
|
||
- shenfen: 用户身份(必须,2=打手,3=管事,4=商家)
|
||
- zhouqi: 周期类型(必须,1=日榜,2=月榜)
|
||
- date: 查询日期(必须,格式:YYYY-MM-DD)
|
||
|
||
返回格式:
|
||
{
|
||
"code": 0, # 0=成功,非0=失败
|
||
"message": "成功",
|
||
"data": {
|
||
"list": [...], # 排行榜数据列表
|
||
"query_info": {...} # 查询信息
|
||
}
|
||
}
|
||
"""
|
||
try:
|
||
# 1. 获取请求数据
|
||
data = request.data
|
||
zhanghao = data.get('zhanghao')
|
||
shenfen = data.get('shenfen')
|
||
zhouqi = data.get('zhouqi')
|
||
date_str = data.get('date')
|
||
|
||
# 2. 参数验证
|
||
if not zhanghao:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '管理员账号不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if not shenfen:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '用户身份不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if not zhouqi:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '周期类型不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if not date_str:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '查询日期不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 3. 参数类型和范围验证
|
||
try:
|
||
zhouqi_int = int(zhouqi)
|
||
shenfen_int = int(shenfen)
|
||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||
except ValueError as e:
|
||
return Response({
|
||
'code': 400,
|
||
'message': f'参数格式错误: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证周期类型范围
|
||
if zhouqi_int not in [1, 2]:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '周期类型必须为1(日榜)或2(月榜)',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证用户身份范围
|
||
if shenfen_int not in [2, 3, 4]:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '用户身份必须为2(打手)、3(管事)或4(商家)',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 4. 构建查询条件
|
||
query = Q(
|
||
zhouqi=zhouqi_int,
|
||
tongji_date=date_obj,
|
||
shenfen=shenfen_int
|
||
)
|
||
|
||
# 5. 执行查询(按排名升序)
|
||
records = RankingRecord.query.filter(query).order_by('paiming')
|
||
|
||
# 6. 格式化返回数据
|
||
data_list = []
|
||
for record in records:
|
||
# 根据用户身份确定指标值单位
|
||
zhizhi_danwei = self._get_zhizhi_danwei(shenfen_int)
|
||
|
||
data_list.append({
|
||
'paiming': record.paiming,
|
||
'yonghuid': record.yonghuid,
|
||
'nicheng': record.nicheng,
|
||
'avatar': record.avatar or '',
|
||
'zhizhi': float(record.zhizhi),
|
||
'zhizhi_danwei': zhizhi_danwei,
|
||
})
|
||
|
||
# 7. 返回成功响应
|
||
response_data = {
|
||
'code': 0,
|
||
'message': '查询成功',
|
||
'data': {
|
||
'list': data_list,
|
||
'total_count': len(data_list),
|
||
'query_info': {
|
||
'zhouqi': zhouqi_int,
|
||
'zhouqi_text': '日榜' if zhouqi_int == 1 else '月榜',
|
||
'shenfen': shenfen_int,
|
||
'shenfen_text': self._get_shenfen_text(shenfen_int),
|
||
'date': date_str,
|
||
}
|
||
}
|
||
}
|
||
|
||
return Response(response_data, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
logger.error(f"管理员查询排行榜失败: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 500,
|
||
'message': f'查询排行榜失败: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def _get_shenfen_text(self, shenfen):
|
||
"""获取身份文本"""
|
||
shenfen_map = {2: '打手', 3: '管事', 4: '商家'}
|
||
return shenfen_map.get(shenfen, '未知')
|
||
|
||
def _get_zhizhi_danwei(self, shenfen):
|
||
"""获取指标值单位"""
|
||
danwei_map = {2: '元', 3: '人', 4: '元'}
|
||
return danwei_map.get(shenfen, '')
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# yonghu/ranking_views.py - 添加以下代码
|
||
class PaihangbangRiqiliebiaoView(APIView):
|
||
"""
|
||
获取排行榜可查询日期列表 - 管理员专用
|
||
返回当前排行榜表中存在的日期(日榜和月榜分开)
|
||
"""
|
||
|
||
# JWT Token认证
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@admin_required
|
||
def post(self, request):
|
||
"""
|
||
POST请求获取可查询日期列表
|
||
参数:
|
||
- zhanghao: 管理员账号(用于验证)
|
||
|
||
返回格式:
|
||
{
|
||
"code": 0,
|
||
"message": "成功",
|
||
"data": {
|
||
"ribang_dates": ["2024-01-15", "2024-01-14", ...], # 日榜日期列表
|
||
"yuebang_dates": ["2024-01-01", "2023-12-01", ...], # 月榜日期列表
|
||
}
|
||
}
|
||
"""
|
||
try:
|
||
# 1. 获取请求数据
|
||
data = request.data
|
||
zhanghao = data.get('zhanghao')
|
||
|
||
if not zhanghao:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '管理员账号不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 2. 查询日榜日期(最近3个月内的)
|
||
three_months_ago = timezone.now().date() - timedelta(days=90)
|
||
|
||
ribang_dates = RankingRecord.query.filter(
|
||
zhouqi=1, # 日榜
|
||
tongji_date__gte=three_months_ago # 只查询最近3个月
|
||
).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date')
|
||
|
||
# 3. 查询月榜日期(最近12个月内的)
|
||
twelve_months_ago = timezone.now().date() - timedelta(days=365)
|
||
|
||
yuebang_dates = RankingRecord.query.filter(
|
||
zhouqi=2, # 月榜
|
||
tongji_date__gte=twelve_months_ago # 只查询最近12个月
|
||
).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date')
|
||
|
||
# 4. 格式化日期
|
||
ribang_date_list = [date.strftime('%Y-%m-%d') for date in ribang_dates]
|
||
yuebang_date_list = [date.strftime('%Y-%m-%d') for date in yuebang_dates]
|
||
|
||
# 5. 返回成功响应
|
||
response_data = {
|
||
'code': 0,
|
||
'message': '获取成功',
|
||
'data': {
|
||
'ribang_dates': ribang_date_list,
|
||
'yuebang_dates': yuebang_date_list,
|
||
'ribang_count': len(ribang_date_list),
|
||
'yuebang_count': len(yuebang_date_list),
|
||
}
|
||
}
|
||
|
||
return Response(response_data, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取排行榜日期列表失败: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 500,
|
||
'message': f'获取日期列表失败: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class WechatLoginAndDashouRegisterView(APIView):
|
||
"""
|
||
微信登录并注册打手一体化接口
|
||
访问路径: /api/yonghu/wdlyhdl
|
||
请求方法: POST
|
||
权限要求: 无需登录
|
||
请求体: { "code": "微信登录code", "inviteCode": "邀请码" }
|
||
|
||
功能: 用户扫码进入但未登录时,一次性完成微信登录和打手注册
|
||
"""
|
||
|
||
throttle_classes = [AnonRateThrottle]
|
||
permission_classes = [AllowAny]
|
||
|
||
def post(self, request):
|
||
"""
|
||
处理微信登录并注册打手请求
|
||
"""
|
||
try:
|
||
# 1. 获取并验证参数
|
||
code = request.data.get('code', '').strip()
|
||
yaoqingma = request.data.get('inviteCode', '').strip()
|
||
|
||
if not code:
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '微信授权码不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if not yaoqingma:
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '邀请码不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if len(yaoqingma) > 100:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': '邀请码长度超过限制',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 2. 获取微信openid
|
||
wechat_data = self.get_wechat_openid(code)
|
||
if not wechat_data or 'openid' not in wechat_data:
|
||
error_msg = wechat_data.get('errmsg', '微信授权失败')
|
||
return Response({
|
||
'code': 4,
|
||
'msg': f'微信登录失败: {error_msg}',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
openid = wechat_data['openid']
|
||
|
||
# 3. 获取客户端真实IP
|
||
kehuduan_ip = self.huoquKehuduanIP(request)
|
||
|
||
# 4. 开始数据库事务(确保登录和注册的原子性)
|
||
with transaction.atomic():
|
||
# 4.1 创建或获取用户(登录逻辑)
|
||
user_main, created = User.objects.select_for_update().get_or_create(
|
||
OpenID=openid,
|
||
defaults={
|
||
'UserUID': self.shengchengYonghuID(),
|
||
'UserName': openid,
|
||
}
|
||
)
|
||
|
||
# 更新用户IP和最后登录时间
|
||
cunchu_ip = kehuduan_ip
|
||
user_main.IP = cunchu_ip
|
||
user_main.UserLastLoginDate = timezone.now()
|
||
user_main.save()
|
||
|
||
if created:
|
||
# 新用户:创建老板扩展表
|
||
UserBoss.query.create(user=user_main, nickname='微信用户')
|
||
|
||
# 4.2 验证邀请码对应的管事(注册逻辑)
|
||
try:
|
||
# 使用select_related获取管事及其关联的主表用户信息
|
||
guanshi_profile = UserGuanshi.query.select_related('user').get(yaoqingma=yaoqingma)
|
||
except UserGuanshi.DoesNotExist:
|
||
# 邀请码无效,但用户已登录,可以返回登录成功但注册失败
|
||
# 这里我们继续执行,但只返回登录信息
|
||
return self.fanhuiZhihouDengluXinxi(user_main, yaoqingma_wuxiao=True)
|
||
|
||
# 验证管事状态
|
||
|
||
# 4.3 检查用户是否已是打手
|
||
dashou_exists = UserDashou.query.filter(user=user_main).exists()
|
||
if dashou_exists:
|
||
# 用户已是打手,直接返回信息
|
||
dashou_profile = UserDashou.query.get(user=user_main)
|
||
return self.fanhuiDengluZhuceshuju(user_main, dashou_profile)
|
||
|
||
# 4.4 核心:为用户创建所有身份(打手、商家、管事)
|
||
guanshi_yonghuid = guanshi_profile.user.UserUID
|
||
|
||
# 创建打手扩展表
|
||
dashou_profile = UserDashou.query.create(
|
||
user=user_main,
|
||
nicheng='大神',
|
||
chenghao='普通大神',
|
||
yaoqingren=guanshi_yonghuid,
|
||
# 其他字段使用模型默认值
|
||
)
|
||
|
||
# 创建商家扩展表
|
||
|
||
# 创建管事扩展表
|
||
|
||
# 更新原管事的邀请数量
|
||
locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
|
||
locked_guanshi.yaogingshuliang += 1
|
||
locked_guanshi.save()
|
||
|
||
# ========== 新增:管事每日统计(邀请打手) ==========
|
||
try:
|
||
update_guanshi_daily_by_action(
|
||
yonghuid=guanshi_yonghuid,
|
||
action=1, # 1 = 邀请打手
|
||
amount=Decimal('0.00')
|
||
)
|
||
logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}")
|
||
except Exception as e:
|
||
logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}")
|
||
|
||
# 5. 生成JWT token
|
||
refresh = RefreshToken.for_user(user_main)
|
||
token = str(refresh.access_token)
|
||
|
||
# 6. 准备返回数据
|
||
response_data = self.zhuangbeiFanhuiShuju(user_main, dashou_profile, token)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '登录并注册成功!已开通打手、管事等身份。',
|
||
'data': response_data
|
||
})
|
||
|
||
except Exception as e:
|
||
# 记录错误日志
|
||
error_openid = locals().get('openid', 'N/A')
|
||
logger.error(
|
||
f"微信登录注册一体化接口异常 - openid: {error_openid}, "
|
||
f"错误类型: {type(e).__name__}, 错误详情: {str(e)}",
|
||
exc_info=True
|
||
)
|
||
|
||
# 返回友好错误信息
|
||
return Response({
|
||
'code': 99,
|
||
'msg': '系统繁忙,请稍后重试',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def get_wechat_openid(self, code):
|
||
"""
|
||
调用微信接口获取openid
|
||
"""
|
||
try:
|
||
appid = getattr(settings, 'WEIXIN_APPID', '')
|
||
secret = getattr(settings, 'WEIXIN_SECRET', '')
|
||
|
||
if not appid or not secret:
|
||
raise ValueError('微信配置未设置')
|
||
|
||
url = 'https://api.weixin.qq.com/sns/jscode2session'
|
||
params = {
|
||
'appid': appid,
|
||
'secret': secret,
|
||
'js_code': code,
|
||
'grant_type': 'authorization_code'
|
||
}
|
||
|
||
response = requests.get(url, params=params, timeout=10)
|
||
result = response.json()
|
||
|
||
if 'openid' in result:
|
||
return result
|
||
else:
|
||
errcode = result.get('errcode', 'unknown')
|
||
errmsg = result.get('errmsg', '未知错误')
|
||
return {'errmsg': f'[{errcode}]{errmsg}'}
|
||
|
||
except requests.exceptions.Timeout:
|
||
return {'errmsg': '请求微信接口超时'}
|
||
except Exception as e:
|
||
return {'errmsg': f'请求微信接口异常: {str(e)}'}
|
||
|
||
def shengchengYonghuID(self):
|
||
"""
|
||
生成7位数字用户ID
|
||
"""
|
||
for _ in range(10):
|
||
timestamp_part = str(int(time.time()))[-5:].zfill(5)
|
||
random_part = str(random.randint(0, 99)).zfill(2)
|
||
user_id = timestamp_part + random_part
|
||
|
||
if len(user_id) == 7 and user_id.isdigit():
|
||
if not User.query.filter(UserUID=user_id).exists():
|
||
return user_id
|
||
|
||
raise Exception('生成用户ID失败')
|
||
|
||
def huoquKehuduanIP(self, request):
|
||
"""
|
||
获取客户端真实IP地址
|
||
"""
|
||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||
if x_forwarded_for:
|
||
ip = x_forwarded_for.split(',')[0].strip()
|
||
if ip:
|
||
return ip
|
||
|
||
ip = request.META.get('REMOTE_ADDR', '')
|
||
return ip if ip else '0.0.0.0'
|
||
|
||
def huoquQunPeizhi(self):
|
||
"""
|
||
获取群配置信息
|
||
"""
|
||
try:
|
||
qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
|
||
|
||
result = {
|
||
'dashouqun': '',
|
||
'dashouqunid': '',
|
||
'guanshiqun': '',
|
||
'guanshiqunid': ''
|
||
}
|
||
|
||
for config in qun_configs:
|
||
if config.id == 1:
|
||
result['dashouqun'] = config.GroupContent or ''
|
||
result['dashouqunid'] = config.GroupID or ''
|
||
elif config.id == 2:
|
||
result['guanshiqun'] = config.GroupContent or ''
|
||
result['guanshiqunid'] = config.GroupID or ''
|
||
|
||
return result
|
||
|
||
except Exception:
|
||
return {
|
||
'dashouqun': '',
|
||
'dashouqunid': '',
|
||
'guanshiqun': '',
|
||
'guanshiqunid': ''
|
||
}
|
||
|
||
def fanhuiZhihouDengluXinxi(self, user_main, yaoqingma_wuxiao=False, guanshi_jinyong=False):
|
||
"""
|
||
返回只登录成功的信息(当邀请码无效或管事禁用时)
|
||
"""
|
||
# 获取老板昵称
|
||
boss_nickname = '微信用户'
|
||
try:
|
||
boss_profile = UserBoss.query.get(user=user_main)
|
||
if boss_profile.nickname:
|
||
boss_nickname = boss_profile.nickname
|
||
except UserBoss.DoesNotExist:
|
||
pass
|
||
|
||
# 检查各身份状态
|
||
dashou_status = 1 if UserDashou.query.filter(user=user_main).exists() else 0
|
||
shangjia_status = 1 if UserShangjia.query.filter(user=user_main).exists() else 0
|
||
guanshi_status = 1 if UserGuanshi.query.filter(user=user_main).exists() else 0
|
||
|
||
# 生成token
|
||
refresh = RefreshToken.for_user(user_main)
|
||
token = str(refresh.access_token)
|
||
|
||
# 获取群配置
|
||
group_info = self.huoquQunPeizhi()
|
||
|
||
# 构建返回数据
|
||
response_data = {
|
||
'token': token,
|
||
'nicheng': boss_nickname,
|
||
'uid': user_main.UserUID,
|
||
'touxiang': user_main.Avatar or '',
|
||
'shangjiastatus': shangjia_status,
|
||
'dashoustatus': dashou_status,
|
||
'guanshistatus': guanshi_status,
|
||
# 🟢 注意:这里没有返回dingdantiaoshu(订单数量),与前端需求一致
|
||
'dashouqun': group_info.get('dashouqun', ''),
|
||
'dashouqunid': group_info.get('dashouqunid', ''),
|
||
'guanshiqun': group_info.get('guanshiqun', ''),
|
||
'guanshiqunid': group_info.get('guanshiqunid', '')
|
||
}
|
||
|
||
# 根据情况设置返回消息
|
||
if yaoqingma_wuxiao:
|
||
msg = '登录成功,但邀请码无效'
|
||
elif guanshi_jinyong:
|
||
msg = '登录成功,但邀请码对应的管事账号已被禁用'
|
||
else:
|
||
msg = '登录成功'
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': msg,
|
||
'data': response_data
|
||
})
|
||
|
||
def fanhuiDengluZhuceshuju(self, user_main, dashou_profile):
|
||
"""
|
||
返回登录并注册的数据(用户已是打手的情况)
|
||
"""
|
||
# 获取老板昵称
|
||
boss_nickname = '微信用户'
|
||
try:
|
||
boss_profile = UserBoss.query.get(user=user_main)
|
||
if boss_profile.nickname:
|
||
boss_nickname = boss_profile.nickname
|
||
except UserBoss.DoesNotExist:
|
||
pass
|
||
|
||
# 检查各身份状态(这里用户已是打手,所以都是1)
|
||
dashou_status = 1
|
||
shangjia_status = 1 if UserShangjia.query.filter(user=user_main).exists() else 1 # 确保是1
|
||
guanshi_status = 1 if UserGuanshi.query.filter(user=user_main).exists() else 1 # 确保是1
|
||
|
||
# 生成token
|
||
refresh = RefreshToken.for_user(user_main)
|
||
token = str(refresh.access_token)
|
||
|
||
# 获取群配置
|
||
group_info = self.huoquQunPeizhi()
|
||
|
||
# 准备打手信息
|
||
dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile)
|
||
|
||
# 合并所有数据
|
||
response_data = {
|
||
'token': token,
|
||
'nicheng': boss_nickname,
|
||
'uid': user_main.UserUID,
|
||
'touxiang': user_main.Avatar or '',
|
||
'shangjiastatus': shangjia_status,
|
||
'dashoustatus': dashou_status,
|
||
'guanshistatus': guanshi_status,
|
||
'dashouqun': group_info.get('dashouqun', ''),
|
||
'dashouqunid': group_info.get('dashouqunid', ''),
|
||
'guanshiqun': group_info.get('guanshiqun', ''),
|
||
'guanshiqunid': group_info.get('guanshiqunid', ''),
|
||
# 合并打手信息
|
||
**dashou_data
|
||
}
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '您已是打手,登录成功',
|
||
'data': response_data
|
||
})
|
||
|
||
def zhuangbeiFanhuiShuju(self, user_main, dashou_profile, token):
|
||
"""
|
||
准备完整的返回数据(登录+注册成功的情况)
|
||
"""
|
||
# 获取老板昵称
|
||
boss_nickname = '微信用户'
|
||
try:
|
||
boss_profile = UserBoss.query.get(user=user_main)
|
||
if boss_profile.nickname:
|
||
boss_nickname = boss_profile.nickname
|
||
except UserBoss.DoesNotExist:
|
||
pass
|
||
|
||
# 身份状态(刚注册成功,应该都是1)
|
||
dashou_status = 1
|
||
shangjia_status = 1
|
||
guanshi_status = 1
|
||
|
||
# 获取群配置
|
||
group_info = self.huoquQunPeizhi()
|
||
|
||
# 准备打手信息
|
||
dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile)
|
||
|
||
# 合并所有数据
|
||
response_data = {
|
||
'token': token,
|
||
'nicheng': boss_nickname,
|
||
'uid': user_main.UserUID,
|
||
'touxiang': user_main.Avatar or '',
|
||
'shangjiastatus': shangjia_status,
|
||
'dashoustatus': dashou_status,
|
||
'guanshistatus': guanshi_status,
|
||
'dashouqun': group_info.get('dashouqun', ''),
|
||
'dashouqunid': group_info.get('dashouqunid', ''),
|
||
'guanshiqun': group_info.get('guanshiqun', ''),
|
||
'guanshiqunid': group_info.get('guanshiqunid', ''),
|
||
# 合并打手信息
|
||
**dashou_data
|
||
}
|
||
|
||
return response_data
|
||
|
||
def zhuangbeiDashouFanhuiShuju(self, dashou_profile):
|
||
"""
|
||
准备打手返回数据(字段名严格匹配前端)
|
||
"""
|
||
# 构建基础信息
|
||
data = {
|
||
'dashounicheng': dashou_profile.nicheng,
|
||
'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
|
||
'yongjin': str(dashou_profile.yue), # Decimal转字符串
|
||
'zonge': str(dashou_profile.zonge),
|
||
'yajin': str(dashou_profile.yajin),
|
||
'chenghao': dashou_profile.chenghao,
|
||
'jinfen': dashou_profile.jifen,
|
||
'chengjiaoliang': dashou_profile.chengjiaozongliang,
|
||
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
|
||
'dashouzhuangtai': dashou_profile.zhuangtai,
|
||
}
|
||
|
||
# 查询并处理会员列表 (clumber)
|
||
# 使用当前用户的yonghuid进行查询
|
||
huiyuan_goumai_list = []
|
||
goumai_records = Huiyuangoumai.query.filter(yonghu_id=dashou_profile.user.UserUID)
|
||
for record in goumai_records:
|
||
# 调用方法检查是否到期,此方法内部会更新状态
|
||
shifou_daoqi = record.jiance_shifou_daoqi()
|
||
huiyuan_goumai_list.append({
|
||
'huiyuan_id': record.huiyuan_id,
|
||
'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
|
||
'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
|
||
'shifou_daoqi': shifou_daoqi,
|
||
'jieshao': record.jieshao
|
||
})
|
||
|
||
data['clumber'] = huiyuan_goumai_list
|
||
return data
|
||
|
||
class YonghuTixianShenheXiugaiView(APIView):
|
||
"""
|
||
用户修改审核中提现申请接口
|
||
访问路径: /api/yonghu/yonghutxshxg
|
||
请求方法: POST
|
||
权限要求: JWT Token认证通过的用户
|
||
请求体: multipart/form-data 格式
|
||
支持字段: tixian_id, fangshi, txdianhua, txzh, file (图片文件)
|
||
|
||
功能: 允许用户修改审核中提现申请的收款方式、电话、账号、收款码等
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = (MultiPartParser, FormParser, JSONParser)
|
||
|
||
def post(self, request):
|
||
"""
|
||
处理用户修改审核中提现申请的POST请求
|
||
"""
|
||
try:
|
||
# 1. 获取当前用户
|
||
user_main = request.user
|
||
current_yonghuid = user_main.UserUID
|
||
|
||
# 2. 验证必填字段:提现记录ID
|
||
tixian_id = request.data.get('tixian_id')
|
||
if not tixian_id:
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '提现记录ID不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 3. 查询提现记录(使用select_for_update确保事务安全)
|
||
with transaction.atomic():
|
||
try:
|
||
tixian_record = Tixianjilu.objects.select_for_update().get(
|
||
id=tixian_id,
|
||
yonghuid=current_yonghuid # 确保只能修改自己的记录
|
||
)
|
||
except Tixianjilu.DoesNotExist:
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '提现记录不存在或无权修改',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 4. 验证提现记录状态(必须是审核中)
|
||
if tixian_record.zhuangtai != 1:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': '只有审核中的提现申请可以修改',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 5. 获取并验证提现方式
|
||
fangshi = request.data.get('fangshi')
|
||
if fangshi:
|
||
fangshi = int(fangshi)
|
||
if fangshi not in [1, 2]:
|
||
return Response({
|
||
'code': 4,
|
||
'msg': '提现方式无效,请选择1(微信)或2(支付宝)',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 检查提现类型与方式的匹配(如果前端提供了leixing)
|
||
leixing = request.data.get('leixing')
|
||
if leixing:
|
||
leixing = int(leixing)
|
||
# 这里可以根据业务逻辑添加类型与方式的验证
|
||
# 例如:某些类型只能使用某些方式
|
||
|
||
# 6. 验证手机号(必填)
|
||
txdianhua = request.data.get('txdianhua', '').strip()
|
||
if not txdianhua:
|
||
return Response({
|
||
'code': 5,
|
||
'msg': '收款人电话不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证手机号格式
|
||
if not txdianhua.isdigit() or len(txdianhua) != 11:
|
||
return Response({
|
||
'code': 6,
|
||
'msg': '请输入11位有效的手机号码',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 7. 获取收款账号(可选)
|
||
txzh = request.data.get('txzh', '').strip()
|
||
|
||
# 8. 处理收款码图片上传(如果有)
|
||
txtupian_file = request.FILES.get('file')
|
||
new_zhifu_url = None
|
||
old_zhifu_url = tixian_record.zhifu
|
||
|
||
if txtupian_file:
|
||
# 验证图片
|
||
is_valid, error_msg = validate_image(txtupian_file)
|
||
if not is_valid:
|
||
return Response({
|
||
'code': 7,
|
||
'msg': f'图片验证失败: {error_msg}',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 上传收款码图片
|
||
new_zhifu_url = self.handle_shoukuanma_upload(
|
||
user_main,
|
||
txtupian_file,
|
||
old_zhifu_url,
|
||
tixian_record.leixing
|
||
)
|
||
|
||
if not new_zhifu_url:
|
||
return Response({
|
||
'code': 8,
|
||
'msg': '收款码上传失败',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
# 9. 验证收款信息(账号和图片至少有一个)
|
||
if not txzh and not (new_zhifu_url or old_zhifu_url):
|
||
return Response({
|
||
'code': 9,
|
||
'msg': '请填写收款账号或上传收款码',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 10. 更新提现记录
|
||
update_fields = []
|
||
|
||
# 更新提现方式(如果有)
|
||
if fangshi:
|
||
tixian_record.fangshi = fangshi
|
||
update_fields.append('fangshi')
|
||
|
||
# 更新手机号(必填)
|
||
tixian_record.phone = txdianhua
|
||
update_fields.append('phone')
|
||
|
||
# 更新收款账号(如果有)
|
||
if txzh is not None: # 注意:空字符串也要更新
|
||
tixian_record.skzhanghao = txzh
|
||
update_fields.append('skzhanghao')
|
||
|
||
# 更新收款码图片(如果有新图片)
|
||
if new_zhifu_url:
|
||
tixian_record.zhifu = new_zhifu_url
|
||
update_fields.append('zhifu')
|
||
|
||
# 保存更新
|
||
if update_fields:
|
||
tixian_record.save(update_fields=update_fields)
|
||
|
||
# 11. 构建返回数据
|
||
response_data = {
|
||
'tixian_id': tixian_record.id,
|
||
'fangshi': tixian_record.fangshi,
|
||
'txdianhua': tixian_record.phone,
|
||
'txzh': tixian_record.skzhanghao or '',
|
||
'txtupian': tixian_record.zhifu or '' # 返回相对URL
|
||
}
|
||
|
||
# 12. 返回成功响应
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '提现申请修改成功',
|
||
'data': response_data
|
||
})
|
||
|
||
except Exception as e:
|
||
# 记录详细错误日志
|
||
logger.error(
|
||
f"修改提现申请异常 - 用户ID: {getattr(user_main, 'yonghuid', 'N/A')}, "
|
||
f"提现ID: {tixian_id}, "
|
||
f"错误类型: {type(e).__name__}, "
|
||
f"错误详情: {str(e)}",
|
||
exc_info=True
|
||
)
|
||
|
||
# 返回友好错误信息
|
||
return Response({
|
||
'code': 99,
|
||
'msg': '系统繁忙,请稍后重试',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def handle_shoukuanma_upload(self, user_main, image_file, old_zhifu_url, leixing=None):
|
||
"""
|
||
处理收款码图片上传到腾讯云COS
|
||
存储路径: tixian/{yonghuid}/{timestamp_uuid}.扩展名
|
||
|
||
参数:
|
||
- user_main: 当前用户对象
|
||
- image_file: 图片文件对象
|
||
- old_zhifu_url: 旧收款码URL(用于删除)
|
||
- leixing: 提现类型(可选,用于文件夹分类)
|
||
|
||
返回: 新的图片相对路径
|
||
"""
|
||
try:
|
||
yonghuid = user_main.UserUID
|
||
if not yonghuid:
|
||
return None
|
||
|
||
# 删除旧收款码(如果存在且不是空字符串)
|
||
if old_zhifu_url and old_zhifu_url.strip():
|
||
delete_from_oss(old_zhifu_url)
|
||
|
||
# 获取文件扩展名
|
||
file_name = image_file.name
|
||
file_extension = os.path.splitext(file_name)[1].lower()
|
||
if not file_extension:
|
||
file_extension = '.jpg'
|
||
|
||
# 生成唯一文件名(使用时间戳+UUID确保不重复)
|
||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
||
unique_id = str(uuid.uuid4())[:8] # 取UUID前8位
|
||
unique_file_name = f"{timestamp}_{unique_id}{file_extension}"
|
||
|
||
# 根据提现类型构建文件夹路径(如果提供了leixing)
|
||
if leixing:
|
||
# leixing=1:打手佣金, leixing=2:管事分红
|
||
type_folder = 'dashou' if leixing == 1 else 'guanshi'
|
||
oss_file_path = f"tixian/{type_folder}/{yonghuid}/{unique_file_name}"
|
||
else:
|
||
# 默认路径
|
||
oss_file_path = f"tixian/{yonghuid}/{unique_file_name}"
|
||
|
||
# 上传新收款码到OSS
|
||
new_shoukuanma_url = upload_to_oss(image_file, oss_file_path)
|
||
|
||
if new_shoukuanma_url:
|
||
# 返回相对路径(从tixian文件夹开始)
|
||
return oss_file_path
|
||
else:
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"处理收款码上传失败: {e}")
|
||
return None
|
||
|
||
|
||
# views.py
|
||
class DashouPermission:
|
||
"""
|
||
打手权限验证类
|
||
确保用户是打手且账号状态正常
|
||
"""
|
||
|
||
def has_permission(self, request, view):
|
||
user = request.user
|
||
|
||
# 检查用户是否已认证
|
||
if not user.is_authenticated:
|
||
return False
|
||
|
||
try:
|
||
# 通过反向关系获取打手扩展信息
|
||
dashou_profile = user.DashouProfile
|
||
|
||
# 检查账号状态是否为1(正常)
|
||
if dashou_profile.zhanghaozhuangtai != 1:
|
||
return False
|
||
|
||
return True
|
||
except UserDashou.DoesNotExist:
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"打手权限验证异常: {e}")
|
||
return False
|
||
|
||
|
||
|
||
|
||
class ChufaJiluHuoquView(APIView):
|
||
"""
|
||
打手获取处罚记录接口
|
||
路径: /yonghu/dshqcfjl
|
||
方法: POST
|
||
权限: JWT Token + 打手权限验证
|
||
"""
|
||
permission_classes = [IsAuthenticated, DashouPermission]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 获取当前用户信息
|
||
user_main = request.user
|
||
yonghuid = user_main.UserUID
|
||
|
||
# 通过反向关系获取打手扩展信息
|
||
try:
|
||
dashou_profile = user_main.DashouProfile
|
||
# 再次确认账号状态
|
||
if dashou_profile.zhanghaozhuangtai != 1:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': '账号已被封禁,无法查看处罚记录',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
except UserDashou.DoesNotExist:
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '用户不是打手身份',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 判断请求类型:统计信息 or 记录列表
|
||
qingqiu_tongji = request.data.get('qingqiu_tongji', False)
|
||
|
||
if qingqiu_tongji:
|
||
# 返回统计信息
|
||
return self.get_tongji_info(yonghuid)
|
||
else:
|
||
# 返回处罚记录列表
|
||
return self.get_chufa_list(yonghuid, request.data)
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取处罚记录异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 99,
|
||
'msg': '系统繁忙,请稍后重试',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def get_tongji_info(self, yonghuid):
|
||
"""
|
||
获取统计信息(总记录、待处理、已处理)
|
||
"""
|
||
try:
|
||
# 使用单个查询获取所有统计信息
|
||
stats = PenaltyRecord.query.filter(PlayerID=yonghuid).aggregate(
|
||
# 总记录数
|
||
zongshu=Count('id'),
|
||
|
||
# 待处理数:状态为0(待处罚)和3(申诉中)
|
||
daichuli=Count(
|
||
Case(
|
||
When(Q(ApplyStatus=0) | Q(ApplyStatus=3), then=1),
|
||
output_field=IntegerField()
|
||
)
|
||
),
|
||
|
||
# 已处理数:状态为1(已处罚)和2(已驳回)
|
||
yichuli=Count(
|
||
Case(
|
||
When(Q(ApplyStatus=1) | Q(ApplyStatus=2), then=1),
|
||
output_field=IntegerField()
|
||
)
|
||
)
|
||
)
|
||
|
||
# 如果查询结果为None,则设置为0
|
||
tongji_data = {
|
||
'zongshu': stats.get('zongshu', 0) or 0,
|
||
'daichuli': stats.get('daichuli', 0) or 0,
|
||
'yichuli': stats.get('yichuli', 0) or 0
|
||
}
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '获取统计信息成功',
|
||
'data': tongji_data
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}")
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '获取统计信息失败',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def get_chufa_list(self, yonghuid, request_data):
|
||
"""
|
||
获取处罚记录列表(高性能批量查询)
|
||
🔴 只修改图片查询逻辑,字段保持原样
|
||
"""
|
||
try:
|
||
# 获取分页参数
|
||
page = int(request_data.get('page', 1))
|
||
page_size = int(request_data.get('page_size', 10))
|
||
|
||
# 限制最大每页数量,防止恶意请求
|
||
if page_size > 50:
|
||
page_size = 50
|
||
if page_size < 1:
|
||
page_size = 10
|
||
|
||
# 获取状态筛选参数
|
||
zhuangtai = request_data.get('zhuangtai', 0)
|
||
|
||
# 🔴 构建查询条件 - 打手端根据PlayerID查询
|
||
queryset = PenaltyRecord.query.filter(PlayerID=yonghuid)
|
||
|
||
if zhuangtai == 0:
|
||
# 待处理:状态为0(待处罚)和3(申诉中)
|
||
queryset = queryset.filter(Q(ApplyStatus=0) | Q(ApplyStatus=3))
|
||
elif zhuangtai == 1:
|
||
# 已处理:状态为1(已处罚)和2(已驳回)
|
||
queryset = queryset.filter(Q(ApplyStatus=1) | Q(ApplyStatus=2))
|
||
else:
|
||
# 默认返回所有状态
|
||
pass
|
||
|
||
# 按创建时间倒序排列(最新在前)
|
||
queryset = queryset.order_by('-CreateTime')
|
||
|
||
# 使用游标分页,避免传统分页的性能问题
|
||
paginator = Paginator(queryset, page_size)
|
||
|
||
try:
|
||
page_obj = paginator.page(page)
|
||
except:
|
||
# 页码超出范围,返回空列表
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '获取成功',
|
||
'data': {
|
||
'list': [],
|
||
'has_more': False,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
'current_count': 0
|
||
}
|
||
})
|
||
|
||
# 获取当前页的记录
|
||
chufa_records = list(page_obj)
|
||
|
||
if not chufa_records:
|
||
# 没有数据直接返回
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '获取成功',
|
||
'data': {
|
||
'list': [],
|
||
'has_more': False,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
'current_count': 0
|
||
}
|
||
})
|
||
|
||
# 🔴【核心修复】收集所有需要查询图片的组合
|
||
dingdan_ids = []
|
||
shangjia_ids = []
|
||
|
||
for record in chufa_records:
|
||
if record.OrderID:
|
||
dingdan_ids.append(record.OrderID)
|
||
if record.ApplicantID: # 商家ID
|
||
shangjia_ids.append(record.ApplicantID)
|
||
|
||
# 去重
|
||
dingdan_ids = list(set(dingdan_ids))
|
||
shangjia_ids = list(set(shangjia_ids))
|
||
|
||
# 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误)
|
||
# 1. 查询证据图片(商家上传的)
|
||
zhengju_mapping = {}
|
||
if dingdan_ids and shangjia_ids:
|
||
# 构建Q对象:OrderID在列表中 AND UserID在商家ID列表中
|
||
zhengju_q = Q(OrderID__in=dingdan_ids, UserID__in=shangjia_ids)
|
||
zhengju_queryset = PenaltyEvidenceImage.query.filter(zhengju_q).values('OrderID', 'UserID', 'ImageURL')
|
||
|
||
for item in zhengju_queryset:
|
||
key = f"{item['OrderID']}_{item['UserID']}"
|
||
if key not in zhengju_mapping:
|
||
zhengju_mapping[key] = []
|
||
if item['ImageURL']:
|
||
zhengju_mapping[key].append(item['ImageURL'])
|
||
|
||
# 2. 查询申诉图片(打手上传的)
|
||
shensu_mapping = {}
|
||
if dingdan_ids:
|
||
# 构建Q对象:OrderID在列表中 AND UserID=当前打手ID
|
||
shensu_q = Q(OrderID__in=dingdan_ids, UserID=yonghuid)
|
||
shensu_queryset = PenaltyEvidenceImage.query.filter(shensu_q).values('OrderID', 'UserID', 'ImageURL')
|
||
|
||
for item in shensu_queryset:
|
||
key = f"{item['OrderID']}_{item['UserID']}"
|
||
if key not in shensu_mapping:
|
||
shensu_mapping[key] = []
|
||
if item['ImageURL']:
|
||
shensu_mapping[key].append(item['ImageURL'])
|
||
|
||
# 🔴【核心修复】组装返回数据 - 字段保持原样!
|
||
chufa_list = []
|
||
for record in chufa_records:
|
||
dingdan_id = record.OrderID
|
||
shangjia_id = record.ApplicantID
|
||
|
||
# 获取证据图片(商家上传的)
|
||
zhengju_tupian = []
|
||
if dingdan_id and shangjia_id:
|
||
key = f"{dingdan_id}_{shangjia_id}"
|
||
zhengju_tupian = zhengju_mapping.get(key, [])
|
||
|
||
# 获取申诉图片(打手上传的)
|
||
shensu_tupian = []
|
||
if dingdan_id:
|
||
key = f"{dingdan_id}_{yonghuid}"
|
||
shensu_tupian = shensu_mapping.get(key, [])
|
||
|
||
# 🔴【字段保持原样!】与前端完全对应!
|
||
chufa_data = {
|
||
'id': record.id,
|
||
'dingdan_id': record.OrderID or '',
|
||
'qingqiuid': record.ApplicantID or '', # 商家ID
|
||
'cfliyou': record.PenaltyReason or '',
|
||
'sqzhuangtai': record.ApplyStatus or 0,
|
||
'ssliyou': record.AppealReason or '',
|
||
'jifen': record.DeductedPoints or 0,
|
||
'CreateTime': record.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if record.CreateTime else '',
|
||
'UpdateTime': record.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if record.UpdateTime else '',
|
||
'zhengju_tupian': zhengju_tupian, # 商家上传的证据图片
|
||
'shensu_tupian': shensu_tupian, # 打手上传的申诉图片
|
||
}
|
||
chufa_list.append(chufa_data)
|
||
|
||
# 判断是否还有更多数据
|
||
has_more = page_obj.has_next()
|
||
|
||
# 构建响应数据
|
||
response_data = {
|
||
'list': chufa_list,
|
||
'has_more': has_more,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
'current_count': len(chufa_list)
|
||
}
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '获取成功',
|
||
'data': response_data
|
||
})
|
||
|
||
except ValueError as e:
|
||
logger.error(f"参数错误 - 用户ID: {yonghuid}, 错误: {str(e)}")
|
||
return Response({
|
||
'code': 4,
|
||
'msg': '参数格式错误',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
except Exception as e:
|
||
logger.error(f"获取处罚记录列表异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 5,
|
||
'msg': '获取记录列表失败',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
|
||
class DashouShensuView(APIView):
|
||
"""
|
||
打手申诉接口
|
||
路径: /yonghu/dscfss
|
||
方法: POST
|
||
权限: JWT Token + 打手权限验证
|
||
功能: 打手对处罚进行申诉
|
||
"""
|
||
permission_classes = [IsAuthenticated, DashouPermission]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 获取当前用户信息
|
||
user_main = request.user
|
||
yonghuid = user_main.UserUID
|
||
|
||
# 🔴🆕【关键】验证打手账号状态
|
||
try:
|
||
dashou_profile = user_main.DashouProfile
|
||
if dashou_profile.zhanghaozhuangtai != 1:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': '账号已被封禁,无法申诉',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
except UserDashou.DoesNotExist:
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '用户不是打手身份',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 🔴🆕【关键】获取请求参数
|
||
chufa_id = request.data.get('chufa_id') # 处罚记录ID
|
||
shensu_liyou = request.data.get('shensu_liyou', '').strip() # 申诉理由
|
||
shensu_tupian_urls = request.data.get('shensu_tupian_urls', []) # 申诉图片URL数组
|
||
|
||
# 验证必填字段
|
||
if not chufa_id:
|
||
return Response({
|
||
'code': 6,
|
||
'msg': '处罚记录ID不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if not shensu_liyou:
|
||
return Response({
|
||
'code': 7,
|
||
'msg': '申诉理由不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证申诉理由长度
|
||
if len(shensu_liyou) > 500:
|
||
return Response({
|
||
'code': 8,
|
||
'msg': '申诉理由不能超过500字',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证图片数量
|
||
if len(shensu_tupian_urls) > 9:
|
||
return Response({
|
||
'code': 9,
|
||
'msg': '最多只能上传9张图片',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 🔴🆕【关键】使用事务确保数据一致性
|
||
|
||
with transaction.atomic():
|
||
# 1. 查询处罚记录
|
||
try:
|
||
chufa_record = PenaltyRecord.objects.select_for_update().get(
|
||
id=chufa_id,
|
||
PlayerID=yonghuid # 确保只能申诉自己的处罚记录
|
||
)
|
||
except PenaltyRecord.DoesNotExist:
|
||
return Response({
|
||
'code': 10,
|
||
'msg': '处罚记录不存在或无权申诉',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 2. 验证处罚记录状态(必须是待处理状态0)
|
||
if chufa_record.ApplyStatus != 0:
|
||
return Response({
|
||
'code': 11,
|
||
'msg': '该处罚记录已处理,无法申诉',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 3. 验证订单是否存在(可选,根据需求决定)
|
||
# 这里可以根据OrderID去查询订单表,但根据需求描述,我们已经有了处罚记录
|
||
# 处罚记录中包含了OrderID,如果OrderID不存在,说明订单可能被删除
|
||
# 但根据业务逻辑,有处罚记录就应该有订单,所以这里可以跳过这个检查
|
||
|
||
# 4. 检查是否已有申诉记录(防止重复申诉)
|
||
if chufa_record.AppealReason or chufa_record.ApplyStatus == 3:
|
||
# 检查是否已经有申诉图片
|
||
existing_shensu_tupian = PenaltyEvidenceImage.query.filter(
|
||
OrderID=chufa_record.OrderID,
|
||
UserID=yonghuid
|
||
).exists()
|
||
|
||
if existing_shensu_tupian:
|
||
return Response({
|
||
'code': 12,
|
||
'msg': '您已经提交过申诉,请等待处理结果',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 5. 更新处罚记录
|
||
chufa_record.AppealReason = shensu_liyou
|
||
chufa_record.ApplyStatus = 3 # 状态改为申诉中
|
||
chufa_record.save()
|
||
|
||
# 6. 保存申诉图片(如果有)
|
||
saved_tupian_urls = []
|
||
if shensu_tupian_urls:
|
||
for tupian_url in shensu_tupian_urls:
|
||
if tupian_url: # 确保URL不为空
|
||
chufa_tupian = PenaltyEvidenceImage.query.create(
|
||
OrderID=chufa_record.OrderID,
|
||
UserID=yonghuid,
|
||
ImageURL=tupian_url
|
||
)
|
||
saved_tupian_urls.append(tupian_url)
|
||
|
||
# 7. 构建返回数据
|
||
response_data = {
|
||
'chufa_id': chufa_record.id,
|
||
'dingdan_id': chufa_record.OrderID or '',
|
||
'shensu_liyou': shensu_liyou,
|
||
'shensu_tupian_urls': saved_tupian_urls,
|
||
'sqzhuangtai': chufa_record.ApplyStatus,
|
||
'UpdateTime': chufa_record.UpdateTime.strftime(
|
||
'%Y-%m-%d %H:%M:%S') if chufa_record.UpdateTime else ''
|
||
}
|
||
|
||
# 记录操作日志
|
||
logger.info(f"打手申诉成功 - 用户ID: {yonghuid}, 处罚ID: {chufa_id}, 订单ID: {chufa_record.OrderID}")
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '申诉提交成功',
|
||
'data': response_data
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"打手申诉异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 99,
|
||
'msg': '系统繁忙,请稍后重试',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class ShangjiaChufaJiluHuoquView(APIView):
|
||
"""
|
||
商家获取处罚记录接口
|
||
路径: /yonghu/sjcfjlhq
|
||
方法: POST
|
||
权限: JWT Token + 商家权限验证
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 获取当前用户信息
|
||
user_main = request.user
|
||
yonghuid = user_main.UserUID
|
||
|
||
# 通过反向关系获取商家扩展信息
|
||
try:
|
||
shangjia_profile = user_main.ShopProfile
|
||
# 再次确认账号状态
|
||
if shangjia_profile.zhuangtai != 1:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': '商家账号状态异常,无法查看处罚记录',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
except UserShangjia.DoesNotExist:
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '用户不是商家身份',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 判断请求类型
|
||
qingqiu_tongji = request.data.get('qingqiu_tongji', False)
|
||
|
||
if qingqiu_tongji:
|
||
return self.get_tongji_info(yonghuid)
|
||
else:
|
||
return self.get_chufa_list(yonghuid, request.data)
|
||
|
||
except Exception as e:
|
||
logger.error(f"商家获取处罚记录异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 99,
|
||
'msg': '系统繁忙,请稍后重试',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def get_tongji_info(self, yonghuid):
|
||
"""
|
||
获取统计信息
|
||
"""
|
||
try:
|
||
# 🔴 查询条件:ApplicantID = 当前商家ID
|
||
stats = PenaltyRecord.query.filter(ApplicantID=yonghuid).aggregate(
|
||
zongshu=Count('id'),
|
||
daichuli=Count(
|
||
Case(
|
||
When(Q(ApplyStatus=0) | Q(ApplyStatus=3), then=1),
|
||
output_field=IntegerField()
|
||
)
|
||
),
|
||
yichuli=Count(
|
||
Case(
|
||
When(Q(ApplyStatus=1) | Q(ApplyStatus=2), then=1),
|
||
output_field=IntegerField()
|
||
)
|
||
)
|
||
)
|
||
|
||
tongji_data = {
|
||
'zongshu': stats.get('zongshu', 0) or 0,
|
||
'daichuli': stats.get('daichuli', 0) or 0,
|
||
'yichuli': stats.get('yichuli', 0) or 0
|
||
}
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '获取统计信息成功',
|
||
'data': tongji_data
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"商家获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}")
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '获取统计信息失败',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def get_chufa_list(self, yonghuid, request_data):
|
||
"""
|
||
获取处罚记录列表(商家端)
|
||
🔴 只修改图片查询逻辑,字段保持原样
|
||
"""
|
||
try:
|
||
# 获取分页参数
|
||
page = int(request_data.get('page', 1))
|
||
page_size = int(request_data.get('page_size', 10))
|
||
|
||
if page_size > 50:
|
||
page_size = 50
|
||
if page_size < 1:
|
||
page_size = 10
|
||
|
||
# 获取状态筛选参数
|
||
zhuangtai = request_data.get('zhuangtai', 0)
|
||
|
||
# 🔴 查询条件:ApplicantID = 当前商家ID
|
||
queryset = PenaltyRecord.query.filter(ApplicantID=yonghuid)
|
||
|
||
if zhuangtai == 0:
|
||
queryset = queryset.filter(Q(ApplyStatus=0) | Q(ApplyStatus=3))
|
||
elif zhuangtai == 1:
|
||
queryset = queryset.filter(Q(ApplyStatus=1) | Q(ApplyStatus=2))
|
||
else:
|
||
pass
|
||
|
||
# 按创建时间倒序排列
|
||
queryset = queryset.order_by('-CreateTime')
|
||
|
||
# 分页逻辑
|
||
paginator = Paginator(queryset, page_size)
|
||
|
||
try:
|
||
page_obj = paginator.page(page)
|
||
except:
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '获取成功',
|
||
'data': {
|
||
'list': [],
|
||
'has_more': False,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
'current_count': 0
|
||
}
|
||
})
|
||
|
||
chufa_records = list(page_obj)
|
||
|
||
if not chufa_records:
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '获取成功',
|
||
'data': {
|
||
'list': [],
|
||
'has_more': False,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
'current_count': 0
|
||
}
|
||
})
|
||
|
||
# 🔴【核心修复】收集所有需要查询图片的组合
|
||
dingdan_ids = []
|
||
dashou_ids = []
|
||
|
||
for record in chufa_records:
|
||
if record.OrderID:
|
||
dingdan_ids.append(record.OrderID)
|
||
if record.PlayerID: # 打手ID
|
||
dashou_ids.append(record.PlayerID)
|
||
|
||
# 去重
|
||
dingdan_ids = list(set(dingdan_ids))
|
||
dashou_ids = list(set(dashou_ids))
|
||
|
||
# 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误)
|
||
# 1. 查询证据图片(商家自己上传的)
|
||
zhengju_mapping = {}
|
||
if dingdan_ids:
|
||
# 构建Q对象:OrderID在列表中 AND UserID=当前商家ID
|
||
zhengju_q = Q(OrderID__in=dingdan_ids, UserID=yonghuid)
|
||
zhengju_queryset = PenaltyEvidenceImage.query.filter(zhengju_q).values('OrderID', 'UserID', 'ImageURL')
|
||
|
||
for item in zhengju_queryset:
|
||
key = f"{item['OrderID']}_{item['UserID']}"
|
||
if key not in zhengju_mapping:
|
||
zhengju_mapping[key] = []
|
||
if item['ImageURL']:
|
||
zhengju_mapping[key].append(item['ImageURL'])
|
||
|
||
# 2. 查询申诉图片(打手上传的)
|
||
shensu_mapping = {}
|
||
if dingdan_ids and dashou_ids:
|
||
# 构建Q对象:OrderID在列表中 AND UserID在打手ID列表中
|
||
shensu_q = Q(OrderID__in=dingdan_ids, UserID__in=dashou_ids)
|
||
shensu_queryset = PenaltyEvidenceImage.query.filter(shensu_q).values('OrderID', 'UserID', 'ImageURL')
|
||
|
||
for item in shensu_queryset:
|
||
key = f"{item['OrderID']}_{item['UserID']}"
|
||
if key not in shensu_mapping:
|
||
shensu_mapping[key] = []
|
||
if item['ImageURL']:
|
||
shensu_mapping[key].append(item['ImageURL'])
|
||
|
||
# 🔴【核心修复】组装返回数据 - 字段保持原样!
|
||
chufa_list = []
|
||
for record in chufa_records:
|
||
dingdan_id = record.OrderID
|
||
dashou_id = record.PlayerID
|
||
|
||
# 获取证据图片(商家自己上传的)
|
||
zhengju_tupian = []
|
||
if dingdan_id:
|
||
key = f"{dingdan_id}_{yonghuid}"
|
||
zhengju_tupian = zhengju_mapping.get(key, [])
|
||
|
||
# 获取申诉图片(打手上传的)
|
||
shensu_tupian = []
|
||
if dingdan_id and dashou_id:
|
||
key = f"{dingdan_id}_{dashou_id}"
|
||
shensu_tupian = shensu_mapping.get(key, [])
|
||
|
||
# 🔴【字段保持原样!】与前端完全对应!
|
||
chufa_data = {
|
||
'id': record.id,
|
||
'dingdan_id': record.OrderID or '',
|
||
'qingqiuid': record.PlayerID or '', # 打手ID
|
||
'cfliyou': record.PenaltyReason or '',
|
||
'sqzhuangtai': record.ApplyStatus or 0,
|
||
'ssliyou': record.AppealReason or '',
|
||
'jifen': record.DeductedPoints or 0,
|
||
'CreateTime': record.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if record.CreateTime else '',
|
||
'UpdateTime': record.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if record.UpdateTime else '',
|
||
'zhengju_tupian': zhengju_tupian, # 商家自己上传的证据图片
|
||
'shensu_tupian': shensu_tupian, # 打手上传的申诉图片
|
||
}
|
||
chufa_list.append(chufa_data)
|
||
|
||
# 判断是否还有更多数据
|
||
has_more = page_obj.has_next()
|
||
|
||
response_data = {
|
||
'list': chufa_list,
|
||
'has_more': has_more,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
'current_count': len(chufa_list)
|
||
}
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '获取成功',
|
||
'data': response_data
|
||
})
|
||
|
||
except ValueError as e:
|
||
logger.error(f"商家端参数错误 - 用户ID: {yonghuid}, 错误: {str(e)}")
|
||
return Response({
|
||
'code': 4,
|
||
'msg': '参数格式错误',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
except Exception as e:
|
||
logger.error(f"商家获取处罚记录列表异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 5,
|
||
'msg': '获取记录列表失败',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KefuLoginView(APIView):
|
||
"""
|
||
客服/管理员登录接口(支持手机号重复)
|
||
请求:POST /yonghu/kefujinru
|
||
请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"}
|
||
返回:
|
||
code=0 成功,data包含token,nicheng,yonghuid
|
||
code=1 失败(msg区分:用户不存在/密码错误/二级密码错误)
|
||
code=4 账号被封禁
|
||
|
||
管理员(IsSuperuser 或 UserType='admin')无需 erjimima 和 KefuProfile。
|
||
"""
|
||
throttle_classes = [AnonRateThrottle] # 限制匿名请求频率
|
||
permission_classes = [AllowAny] # 任何人都可访问
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
phone = request.data.get('phone', '').strip()
|
||
password = request.data.get('password', '')
|
||
erjimima = request.data.get('erjimima', '')
|
||
|
||
if not phone or not password:
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '请填写手机号和密码',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 2. 获取客户端真实IP
|
||
client_ip = self.get_client_ip(request)
|
||
|
||
# 3. 查询该手机号下所有账号(客服 + 管理员)
|
||
# 先查客服(有 KefuProfile),再查管理员(IsSuperuser 或 UserType='admin')
|
||
user_mains = list(
|
||
User.query.filter(
|
||
Phone=phone,
|
||
KefuProfile__isnull=False,
|
||
).select_related('KefuProfile')
|
||
)
|
||
|
||
# 如果没有客服账号,尝试查找管理员账号
|
||
if not user_mains:
|
||
admin_candidates = User.query.filter(Phone=phone)
|
||
for admin_user in admin_candidates:
|
||
if admin_user.IsSuperuser or admin_user.UserType == 'admin':
|
||
user_mains = [admin_user]
|
||
break
|
||
|
||
if not user_mains:
|
||
logger.warning(f"登录失败,用户不存在: {phone}, IP: {client_ip}")
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '用户不存在',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 4. 遍历所有匹配的账号,验证密码和二级密码
|
||
target_user = None
|
||
target_kefu = None
|
||
password_matched = False
|
||
|
||
for user in user_mains:
|
||
# 验证主密码(bcrypt 哈希验证)
|
||
if not user.CheckPassword(password):
|
||
continue
|
||
password_matched = True
|
||
|
||
# 管理员:跳过二级密码和客服扩展表检查
|
||
is_admin = user.IsSuperuser or user.UserType == 'admin'
|
||
if is_admin:
|
||
target_user = user
|
||
target_kefu = None
|
||
break
|
||
|
||
# 客服:验证二级密码
|
||
kefu = user.KefuProfile
|
||
stored_erji = kefu.erjimima or ''
|
||
if stored_erji.startswith(('$2b$', '$2a$')) and len(stored_erji) == 60:
|
||
import bcrypt as _bcrypt
|
||
if not _bcrypt.checkpw(erjimima.encode('utf-8'), stored_erji.encode('utf-8')):
|
||
continue
|
||
elif stored_erji != erjimima:
|
||
continue
|
||
|
||
# 如果账号被禁用,记录但继续查找
|
||
if kefu.zhuangtai != 1:
|
||
if target_user is None:
|
||
target_user = user
|
||
target_kefu = kefu
|
||
continue
|
||
|
||
# 找到完全匹配且状态正常的账号,立即使用
|
||
target_user = user
|
||
target_kefu = kefu
|
||
break
|
||
else:
|
||
# 循环结束未找到正常账号
|
||
if target_user and target_kefu:
|
||
# 只有被禁用的账号匹配,返回封禁提示
|
||
logger.warning(f"登录失败,客服账号已禁用: {phone}, IP: {client_ip}")
|
||
return Response({
|
||
'code': 4,
|
||
'msg': '账号已被封禁',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
elif password_matched:
|
||
# 密码正确但二级密码错误
|
||
logger.warning(f"登录失败,二级密码错误: {phone}, IP: {client_ip}")
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '二级密码错误',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
else:
|
||
# 密码错误
|
||
logger.warning(f"登录失败,密码错误: {phone}, IP: {client_ip}")
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '密码错误',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 5. 更新IP和最后登录时间
|
||
target_user.IP = client_ip
|
||
target_user.UserLastLoginDate = timezone.now()
|
||
target_user.save(update_fields=['IP', 'UserLastLoginDate'])
|
||
|
||
# 6. 生成JWT token
|
||
refresh = RefreshToken.for_user(target_user)
|
||
token = str(refresh.access_token)
|
||
|
||
# 7. 返回成功数据(管理员无 KefuProfile,nicheng 用 UserName 或 Phone 兜底)
|
||
nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone)
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '登录成功',
|
||
'data': {
|
||
'token': token,
|
||
'nicheng': nicheng,
|
||
'yonghuid': target_user.UserUID,
|
||
}
|
||
})
|
||
|
||
def get_client_ip(self, request):
|
||
"""
|
||
获取客户端真实IP
|
||
"""
|
||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||
if x_forwarded_for:
|
||
ip = x_forwarded_for.split(',')[0].strip()
|
||
if ip:
|
||
return ip
|
||
return request.META.get('REMOTE_ADDR', '0.0.0.0')
|
||
|
||
|
||
|
||
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class KefuStatsView(APIView):
|
||
"""
|
||
客服统计数据接口
|
||
请求:POST /yonghu/kfjrhqzl
|
||
请求体:{"phone": "13800138000"}
|
||
认证:必须携带有效的 JWT token(放在 Authorization 头)
|
||
返回:
|
||
code=0 成功,data 包含 jinrichuli, jinyuechuli, zongchuli
|
||
验证失败统一返回 401,不透露具体原因
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated] # 必须登录
|
||
|
||
|
||
def post(self, request):
|
||
# 1. 获取请求中的 phone
|
||
phone = request.data.get('phone', '').strip()
|
||
if not phone:
|
||
# 缺少参数,但按约定返回 401(可以改为 400,但为了统一,用401)
|
||
logger.warning(f"统计接口缺少 phone 参数,用户: {request.user.UserUID}")
|
||
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 获取当前登录用户(从 JWT 中解析)
|
||
current_user = request.user
|
||
|
||
# 3. 验证前端传来的 phone 是否与当前用户的 phone 一致
|
||
if current_user.Phone != phone:
|
||
# 不一致,可能 token 被冒用或参数错误
|
||
logger.warning(f"统计接口 phone 不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
|
||
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 4. 验证用户类型是否为客服
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
logger.warning(f"统计接口用户类型非客服: {current_user.UserUID}")
|
||
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 5. 尝试获取客服扩展表数据(管理员跳过)
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
if is_admin:
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'jinrichuli': 0,
|
||
'jinyuechuli': 0,
|
||
'zongchuli': 0,
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
try:
|
||
kefu_profile = current_user.KefuProfile # OneToOneField 反向关系
|
||
except UserKefu.DoesNotExist:
|
||
logger.warning(f"统计接口客服扩展表不存在: {current_user.UserUID}")
|
||
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 6. 返回统计数据(字段名与前端约定一致)
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'jinrichuli': kefu_profile.jinrichuli,
|
||
'jinyuechuli': kefu_profile.jinyuechuli,
|
||
'zongchuli': kefu_profile.zongchuli,
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KefuGetOrderTypesView(APIView):
|
||
"""
|
||
客服获取订单类型列表接口(平台订单用)
|
||
请求:POST /yonghu/kfhqptddlx
|
||
参数:{"phone": "13800138000"}
|
||
认证:JWT
|
||
返回:code=0 + 类型列表
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
phone = request.data.get('phone', '').strip()
|
||
if not phone:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
current_user = request.user
|
||
|
||
# 2. 验证手机号一致性
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
logger.warning(f"手机号不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 3. 验证用户类型及客服状态
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 管理员跳过客服扩展表检查
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
if not is_admin:
|
||
try:
|
||
kefu_profile = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if kefu_profile.zhuangtai != 1:
|
||
logger.warning(f"客服账号已被禁用: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 4. 查询订单类型(商品类型)
|
||
try:
|
||
# 只查询需要的字段,减少数据传输
|
||
types_qs = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url')
|
||
type_list = [
|
||
{
|
||
'id': t.id,
|
||
'biaoti': t.jieshao or '', # 假设商品类型的标题字段为 jieshao
|
||
'tupian_url': t.tupian_url or ''
|
||
}
|
||
for t in types_qs
|
||
]
|
||
return Response({'code': 0, 'data': type_list})
|
||
except Exception as e:
|
||
logger.error(f"查询商品类型表失败: {str(e)}")
|
||
# 表不存在或查询失败,返回空列表
|
||
return Response({'code': 0, 'data': []})
|
||
|
||
|
||
|
||
|
||
|
||
class KefuGetOrderListView(APIView):
|
||
"""
|
||
客服获取平台订单列表接口(发单平台=1,非跨平台)
|
||
需要权限:002ab(平台订单管理)
|
||
请求参数:
|
||
username: 客服账号(用于权限校验)
|
||
page: 页码
|
||
pageSize: 每页条数
|
||
dingdan_id: 订单ID(精确搜索)
|
||
laoban_id: 老板ID(搜索平台扩展表中的老板)
|
||
dashou_id: 打手ID(搜索主表中的接单打手)
|
||
leixing: 商品类型ID
|
||
zhuangtai: 状态(多个用逗号分隔,如 "1,7")
|
||
date: 日期(YYYY-MM-DD),筛选订单创建时间
|
||
nicheng: 游戏昵称(订单主表的 nicheng 字段)
|
||
jieshao: 订单介绍(订单主表的 jieshao 字段)
|
||
clkf: 处理客服(订单主表的 clkf 字段)
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
start_total = time.time()
|
||
|
||
# 1. 获取前端参数
|
||
username = request.data.get('phone', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 3. 检查平台订单管理权限(002ab)
|
||
if '002ab' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
|
||
|
||
# 4. 获取分页和筛选参数
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('pageSize', 20))
|
||
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
laoban_id = request.data.get('laoban_id', '').strip()
|
||
dashou_id = request.data.get('dashou_id', '').strip()
|
||
leixing = request.data.get('leixing')
|
||
zhuangtai = request.data.get('zhuangtai', '')
|
||
date = request.data.get('date', '').strip()
|
||
# 新增筛选条件
|
||
nicheng = request.data.get('nicheng', '').strip()
|
||
jieshao = request.data.get('jieshao', '').strip()
|
||
clkf = request.data.get('clkf', '').strip()
|
||
|
||
# ---------- 查询条件 ----------
|
||
# 基础条件:平台发单 + 非跨平台
|
||
base_q = Q(Platform=1)
|
||
|
||
if dingdan_id:
|
||
q_conditions = base_q & Q(OrderID=dingdan_id)
|
||
else:
|
||
q_conditions = base_q
|
||
if leixing is not None:
|
||
try:
|
||
leixing_int = int(leixing)
|
||
q_conditions &= Q(ProductTypeID=leixing_int)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
if zhuangtai and zhuangtai != 'all':
|
||
try:
|
||
status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
|
||
if status_list:
|
||
q_conditions &= Q(Status__in=status_list)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
if dashou_id:
|
||
q_conditions &= Q(PlayerID=dashou_id)
|
||
if laoban_id:
|
||
q_conditions &= Q(pingtai_kuozhan__BossID=laoban_id)
|
||
if date:
|
||
q_conditions &= Q(CreateTime__date=date)
|
||
# 新增筛选条件
|
||
if nicheng:
|
||
q_conditions &= Q(Nickname__icontains=nicheng)
|
||
if jieshao:
|
||
q_conditions &= Q(Description__icontains=jieshao)
|
||
if clkf:
|
||
q_conditions &= Q(AssignedCS__icontains=clkf)
|
||
|
||
# ---------- 统计(仅按发单平台,非跨平台) ----------
|
||
stats = Order.query.filter(Platform=1).aggregate(
|
||
total_orders=Count('id'),
|
||
completed_orders=Count('id', filter=Q(Status=3)),
|
||
refund_orders=Count('id', filter=Q(Status=5)),
|
||
pending_orders=Count('id', filter=Q(Status__in=[4,8]))
|
||
)
|
||
|
||
# ---------- 状态计数(所有状态都返回,非跨平台) ----------
|
||
status_counts = {
|
||
'all': Order.query.filter(Platform=1).count(),
|
||
'1,7': Order.query.filter(Platform=1, Status__in=[1,7]).count(),
|
||
'2': Order.query.filter(Platform=1, Status=2).count(),
|
||
'8': Order.query.filter(Platform=1, Status=8).count(),
|
||
'3': Order.query.filter(Platform=1, Status=3).count(),
|
||
'4': Order.query.filter(Platform=1, Status=4).count(),
|
||
'5': Order.query.filter(Platform=1, Status=5).count(),
|
||
'6': Order.query.filter(Platform=1, Status=6).count(),
|
||
}
|
||
|
||
# ---------- 分页查询 ----------
|
||
total_count = Order.query.filter(q_conditions).count()
|
||
|
||
orders_qs = Order.query.filter(q_conditions).select_related(
|
||
'pingtai_kuozhan'
|
||
).only(
|
||
'OrderID', 'Description', 'Status', 'ProductTypeID', 'Platform',
|
||
'Amount', 'ImageURL', 'CreateTime', 'Nickname', 'PlayerID',
|
||
'pingtai_kuozhan__BossID'
|
||
).order_by('-CreateTime')[(page-1)*page_size : page*page_size]
|
||
|
||
# ---------- 获取老板昵称 ----------
|
||
laoban_ids = [o.pingtai_kuozhan.BossID for o in orders_qs if hasattr(o, 'pingtai_kuozhan') and o.pingtai_kuozhan.BossID]
|
||
nickname_map = {}
|
||
if laoban_ids:
|
||
users = User.query.filter(UserUID__in=laoban_ids).select_related('BossProfile').only(
|
||
'UserUID', 'BossProfile__nickname'
|
||
)
|
||
for user in users:
|
||
nickname_map[user.UserUID] = (user.BossProfile.nickname or '') if getattr(user, 'BossProfile', None) else ''
|
||
|
||
result_list = []
|
||
for order in orders_qs:
|
||
ext = getattr(order, 'pingtai_kuozhan', None)
|
||
laoban_id = ext.BossID if ext else ''
|
||
youxi_nicheng = nickname_map.get(laoban_id, order.Nickname or '')
|
||
result_list.append({
|
||
'dingdan_id': order.OrderID or '',
|
||
'jieshao': order.Description or '',
|
||
'zhuangtai': order.Status,
|
||
'leixing': order.ProductTypeID,
|
||
'fadanpingtai': order.Platform,
|
||
'jiage': float(order.Amount) if order.Amount else 0.00,
|
||
'tupian_url': order.ImageURL or '',
|
||
'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
|
||
'youxi_nicheng': youxi_nicheng
|
||
})
|
||
|
||
response_data = {
|
||
'stats': {
|
||
'totalOrders': stats['total_orders'] or 0,
|
||
'completedOrders': stats['completed_orders'] or 0,
|
||
'refundOrders': stats['refund_orders'] or 0,
|
||
'pendingOrders': stats['pending_orders'] or 0
|
||
},
|
||
'statusCounts': status_counts,
|
||
'list': result_list,
|
||
'total': total_count,
|
||
'page': page,
|
||
'pageSize': page_size
|
||
}
|
||
|
||
logger.info(f"平台订单接口总耗时: {time.time()-start_total:.3f}s")
|
||
return Response({'code': 0, 'data': response_data})
|
||
|
||
class KefuGetShangjiaOrderListView(APIView):
|
||
"""
|
||
客服获取商家订单列表接口(发单平台=2,非跨平台)
|
||
需要权限:002ac(商家订单管理)—— 如不区分可使用 002ab
|
||
请求参数:
|
||
username: 客服账号(用于权限校验)
|
||
page: 页码
|
||
pageSize: 每页条数
|
||
dingdan_id: 订单ID(精确搜索)
|
||
shangjia_id: 商家ID(搜索商家扩展表中的商家ID)
|
||
dashou_id: 打手ID(搜索主表中的接单打手)
|
||
leixing: 商品类型ID
|
||
zhuangtai: 状态(多个用逗号分隔,如 "1,7")
|
||
date: 日期(YYYY-MM-DD),筛选订单创建时间
|
||
nicheng: 游戏昵称(订单主表的 nicheng 字段)
|
||
jieshao: 订单介绍(订单主表的 jieshao 字段)
|
||
clkf: 处理客服(订单主表的 clkf 字段)
|
||
shangjia_nicheng: 商家昵称(商家扩展表中的昵称)
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
start_total = time.time()
|
||
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 3. 检查商家订单管理权限(使用 002ac,如果没有可改用 002ab)
|
||
if '002ac' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看商家订单列表'})
|
||
|
||
# 4. 获取分页和筛选参数
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('pageSize', 20))
|
||
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
shangjia_id = request.data.get('shangjia_id', '').strip()
|
||
dashou_id = request.data.get('dashou_id', '').strip()
|
||
leixing = request.data.get('leixing')
|
||
zhuangtai = request.data.get('zhuangtai', '')
|
||
date = request.data.get('date', '').strip()
|
||
# 新增筛选条件
|
||
nicheng = request.data.get('nicheng', '').strip()
|
||
jieshao = request.data.get('jieshao', '').strip()
|
||
clkf = request.data.get('clkf', '').strip()
|
||
shangjia_nicheng = request.data.get('shangjia_nicheng', '').strip()
|
||
|
||
# ---------- 查询条件 ----------
|
||
# 基础条件:商家发单 + 非跨平台
|
||
base_q = Q(Platform=2)
|
||
|
||
if dingdan_id:
|
||
q_conditions = base_q & Q(OrderID=dingdan_id)
|
||
else:
|
||
q_conditions = base_q
|
||
if leixing is not None:
|
||
try:
|
||
leixing_int = int(leixing)
|
||
q_conditions &= Q(ProductTypeID=leixing_int)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
if zhuangtai and zhuangtai != 'all':
|
||
try:
|
||
status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
|
||
if status_list:
|
||
q_conditions &= Q(Status__in=status_list)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
if dashou_id:
|
||
q_conditions &= Q(PlayerID=dashou_id)
|
||
if shangjia_id:
|
||
q_conditions &= Q(shangjia_kuozhan__MerchantID=shangjia_id)
|
||
if date:
|
||
q_conditions &= Q(CreateTime__date=date)
|
||
if nicheng:
|
||
q_conditions &= Q(Nickname__icontains=nicheng)
|
||
if jieshao:
|
||
q_conditions &= Q(Description__icontains=jieshao)
|
||
if clkf:
|
||
q_conditions &= Q(AssignedCS__icontains=clkf)
|
||
if shangjia_nicheng:
|
||
# 通过商家扩展表关联 UserShangjia 的昵称
|
||
q_conditions &= Q(shangjia_kuozhan__MerchantID__in=User.query.filter(
|
||
ShopProfile__nicheng__icontains=shangjia_nicheng
|
||
).values_list('UserUID', flat=True))
|
||
|
||
# ---------- 统计(仅按发单平台,非跨平台) ----------
|
||
stats = Order.query.filter(Platform=2).aggregate(
|
||
total_orders=Count('id'),
|
||
completed_orders=Count('id', filter=Q(Status=3)),
|
||
refund_orders=Count('id', filter=Q(Status=5)),
|
||
pending_orders=Count('id', filter=Q(Status__in=[4,8]))
|
||
)
|
||
|
||
# ---------- 状态计数(所有状态都返回,非跨平台) ----------
|
||
status_counts = {
|
||
'all': Order.query.filter(Platform=2).count(),
|
||
'1,7': Order.query.filter(Platform=2, Status__in=[1,7]).count(),
|
||
'2': Order.query.filter(Platform=2, Status=2).count(),
|
||
'8': Order.query.filter(Platform=2, Status=8).count(),
|
||
'3': Order.query.filter(Platform=2, Status=3).count(),
|
||
'4': Order.query.filter(Platform=2, Status=4).count(),
|
||
'5': Order.query.filter(Platform=2, Status=5).count(),
|
||
'6': Order.query.filter(Platform=2, Status=6).count(),
|
||
}
|
||
|
||
# ---------- 分页查询 ----------
|
||
total_count = Order.query.filter(q_conditions).count()
|
||
|
||
orders_qs = Order.query.filter(q_conditions).select_related(
|
||
'shangjia_kuozhan'
|
||
).only(
|
||
'OrderID', 'Description', 'Status', 'ProductTypeID', 'Platform',
|
||
'Amount', 'ImageURL', 'CreateTime', 'Nickname', 'PlayerID',
|
||
'shangjia_kuozhan__MerchantID'
|
||
).order_by('-CreateTime')[(page-1)*page_size : page*page_size]
|
||
|
||
# ---------- 获取商家昵称 ----------
|
||
shangjia_ids = [o.shangjia_kuozhan.MerchantID for o in orders_qs if hasattr(o, 'shangjia_kuozhan') and o.shangjia_kuozhan.MerchantID]
|
||
nickname_map = {}
|
||
if shangjia_ids:
|
||
users = User.query.filter(UserUID__in=shangjia_ids).select_related('ShopProfile').only(
|
||
'UserUID', 'ShopProfile__nicheng'
|
||
)
|
||
for user in users:
|
||
nickname_map[user.UserUID] = user.ShopProfile.nicheng or '' if hasattr(user, 'ShopProfile') else ''
|
||
|
||
result_list = []
|
||
for order in orders_qs:
|
||
ext = getattr(order, 'shangjia_kuozhan', None)
|
||
shangjia_id = ext.MerchantID if ext else ''
|
||
youxi_nicheng = order.Nickname or '' # 商家订单的玩家昵称
|
||
result_list.append({
|
||
'dingdan_id': order.OrderID or '',
|
||
'jieshao': order.Description or '',
|
||
'zhuangtai': order.Status,
|
||
'leixing': order.ProductTypeID,
|
||
'fadanpingtai': order.Platform,
|
||
'jiage': float(order.Amount) if order.Amount else 0.00,
|
||
'tupian_url': order.ImageURL or '',
|
||
'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
|
||
'youxi_nicheng': youxi_nicheng
|
||
})
|
||
|
||
response_data = {
|
||
'stats': {
|
||
'totalOrders': stats['total_orders'] or 0,
|
||
'completedOrders': stats['completed_orders'] or 0,
|
||
'refundOrders': stats['refund_orders'] or 0,
|
||
'pendingOrders': stats['pending_orders'] or 0
|
||
},
|
||
'statusCounts': status_counts,
|
||
'list': result_list,
|
||
'total': total_count,
|
||
'page': page,
|
||
'pageSize': page_size
|
||
}
|
||
|
||
logger.info(f"商家订单接口总耗时: {time.time()-start_total:.3f}s")
|
||
return Response({'code': 0, 'data': response_data})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KefuChangeDashouView(APIView):
|
||
"""
|
||
客服更换打手接口
|
||
请求:POST /yonghu/kefughds
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"dingdan_id": "订单ID",
|
||
"new_dashou_id": "新打手ID"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 成功
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
phone = request.data.get('phone', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
new_dashou_id = request.data.get('new_dashou_id', '').strip()
|
||
|
||
if not phone or not dingdan_id or not new_dashou_id:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
logger.warning(f"手机号不匹配: {phone}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if kefu.zhuangtai != 1:
|
||
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 3. 查询订单
|
||
try:
|
||
order = Order.query.get(OrderID=dingdan_id)
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 4. 验证订单状态是否允许更换打手(进行中2 或 结算中8)
|
||
if order.Status not in [2, 8]:
|
||
return Response({'code': 400, 'msg': '订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 5. 查询新打手
|
||
try:
|
||
new_dashou_user = User.query.get(UserUID=new_dashou_id)
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 6. 验证打手扩展表
|
||
try:
|
||
new_dashou_profile = new_dashou_user.DashouProfile
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手信息不完整'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 7. 验证打手状态(打手状态和账号状态必须为1)
|
||
if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1:
|
||
return Response({'code': 400, 'msg': '打手状态不允许接单'}, status=status.HTTP_400_BAD_REQUEST)
|
||
# 8. 验证打手积分是否足够(至少5分)
|
||
if new_dashou_profile.jifen < 5:
|
||
return Response({'code': 400, 'msg': '打手积分不足,无法接单'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 8. 根据订单的抢单要求类型进行额外验证
|
||
yaoqiuleixing = order.GrabRequirement
|
||
|
||
if yaoqiuleixing == 1: # 会员抢单
|
||
huiyuan_id = order.MembershipID
|
||
if not huiyuan_id:
|
||
return Response({'code': 400, 'msg': '订单会员ID为空'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
# 查询打手的会员购买记录
|
||
huiyuan_goumai = Huiyuangoumai.query.get(
|
||
huiyuan_id=huiyuan_id,
|
||
yonghu_id=new_dashou_id # 假设 yonghu_id 字段存储用户ID
|
||
)
|
||
except Huiyuangoumai.DoesNotExist:
|
||
return Response({'code': 400, 'msg': '打手未购买该订单要求的会员'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 检查会员是否过期(调用模型方法)
|
||
is_expired = huiyuan_goumai.jiance_shifou_daoqi()
|
||
if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1:
|
||
return Response({'code': 400, 'msg': '打手的会员已过期'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
elif yaoqiuleixing == 2: # 押金抢单
|
||
order_yajin = order.CommissionReq # 假设订单的 yongjin 字段存储押金要求
|
||
if order_yajin is None:
|
||
return Response({'code': 400, 'msg': '订单押金为空'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if new_dashou_profile.yajin < order_yajin:
|
||
return Response({'code': 400, 'msg': '打手押金不足'}, status=status.HTTP_400_BAD_REQUEST)
|
||
else:
|
||
return Response({'code': 400, 'msg': '订单抢单要求类型错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 9. 所有验证通过,开始更换打手(使用事务)
|
||
with transaction.atomic():
|
||
# 9.1 更新新打手扩展表
|
||
new_dashou_profile.zhuangtai = 0 # 设为接单中
|
||
new_dashou_profile.jiedanzongliang += 1
|
||
new_dashou_profile.jinrijiedan += 1
|
||
new_dashou_profile.jinyuejiedan += 1
|
||
new_dashou_profile.save()
|
||
|
||
# 9.2 处理原打手(如果存在)
|
||
old_dashou_id = order.PlayerID
|
||
if old_dashou_id:
|
||
try:
|
||
old_dashou_user = User.query.get(UserUID=old_dashou_id)
|
||
old_dashou_profile = old_dashou_user.DashouProfile
|
||
old_dashou_profile.zhuangtai = 1 # 恢复空闲
|
||
old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1(业务逻辑)
|
||
old_dashou_profile.save()
|
||
logger.info(f"原打手 {old_dashou_id} 状态已更新")
|
||
except (User.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"原打手 {old_dashou_id} 不存在或信息不完整,跳过更新")
|
||
except Exception as e:
|
||
logger.error(f"更新原打手时异常: {str(e)}", exc_info=True)
|
||
|
||
# 9.3 更新订单
|
||
order.PlayerID = new_dashou_id
|
||
order.Status = 2 # 进行中
|
||
order.AssignedCS = current_user.UserUID # 记录处理客服ID
|
||
order.save()
|
||
|
||
return Response({'code': 0, 'msg': '更换打手成功'})
|
||
|
||
|
||
|
||
|
||
|
||
class KefuRecoverOrderView(APIView):
|
||
"""
|
||
客服恢复订单接口(仅状态9可恢复)
|
||
请求:POST /yonghu/kefuhf
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"dingdan_id": "订单ID"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 成功
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
phone = request.data.get('phone', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
|
||
if not phone or not dingdan_id:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if kefu.zhuangtai != 1:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 3. 查询订单
|
||
try:
|
||
order = Order.query.get(OrderID=dingdan_id)
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 4. 验证订单状态是否为9(未付款)
|
||
if order.Status != 9:
|
||
return Response({'code': 400, 'msg': '订单状态不允许恢复'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 5. 根据是否有指定ID决定新状态
|
||
if order.AssignedID:
|
||
new_status = 7 # 指定中
|
||
else:
|
||
new_status = 1 # 下单中
|
||
|
||
# 6. 更新订单
|
||
order.Status = new_status
|
||
order.AssignedCS = current_user.UserUID
|
||
order.save()
|
||
|
||
return Response({'code': 0, 'msg': '恢复成功', 'data': {'new_status': new_status}})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# dingdan/views.py
|
||
class KefuPlatformRefundView(APIView):
|
||
"""
|
||
客服平台订单退款接口(调用微信支付)
|
||
请求:POST /yonghu/kefupttk
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"dingdan_id": "订单ID",
|
||
"tuikuan_liyou": "退款理由(可选)"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 成功,code=其他 失败
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
phone = request.data.get('phone', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
|
||
|
||
if not phone or not dingdan_id:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
logger.warning(f"手机号不匹配: {phone}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if kefu.zhuangtai != 1:
|
||
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 3. 查询订单并加锁
|
||
try:
|
||
with transaction.atomic():
|
||
order = Order.query.select_related('pingtai_kuozhan').select_for_update().get(OrderID=dingdan_id)
|
||
|
||
# 4. 验证平台订单
|
||
if order.Platform != 1:
|
||
return Response({'code': 400, 'msg': '该订单不是平台订单'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 5. 验证状态允许退款(根据模型,状态值:1下单中,2进行中,3已完成,4退款审核,5已退款,6退款失败,7指定中,8结算中)
|
||
allowed_statuses = [1, 2, 4, 7, 8] # 允许退款的已支付或处理中状态
|
||
if order.Status not in allowed_statuses:
|
||
return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 6. 获取微信交易号(如果存在)
|
||
wechat_transaction_id = getattr(order, 'WechatTransactionID', None)
|
||
|
||
# 7. 调用微信退款接口
|
||
refund_result = self.call_wechat_refund(
|
||
dingdan_id=order.OrderID,
|
||
jine=order.Amount,
|
||
transaction_id=wechat_transaction_id
|
||
)
|
||
|
||
if refund_result['code'] != 0:
|
||
# 微信退款失败,直接返回错误,数据库不更新
|
||
logger.error(f"微信退款失败,订单 {dingdan_id},原因:{refund_result['msg']}")
|
||
return Response({'code': 400, 'msg': refund_result['msg']}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 8. 更新订单状态为5(已退款)
|
||
order.Status = 5
|
||
order.AssignedCS = current_user.Phone # 记录处理客服手机号
|
||
if tuikuan_liyou:
|
||
order.RefundReason = tuikuan_liyou
|
||
order.save()
|
||
|
||
# 9. 更新打手数据
|
||
jiedan_dashou_id = order.PlayerID
|
||
if jiedan_dashou_id:
|
||
try:
|
||
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
|
||
dashou_profile = dashou_user.DashouProfile
|
||
dashou_profile.tuikuanliang += 1
|
||
dashou_profile.zhuangtai = 1 # 打手状态设为空闲
|
||
dashou_profile.save()
|
||
logger.info(f"平台退款:打手 {jiedan_dashou_id} 退款订单+1")
|
||
except (User.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"平台退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
|
||
|
||
# ========== 新增:商品和店铺每日统计 ==========
|
||
try:
|
||
# 商品每日统计(只要有商品ID就执行)
|
||
if order.ProductID:
|
||
# 从扩展表获取已计算好的收益值(下单时已存储)
|
||
pingtai_shouyi = Decimal('0.00')
|
||
dianpu_shouyi = Decimal('0.00')
|
||
if hasattr(order, 'pingtai_kuozhan'):
|
||
pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
|
||
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
|
||
|
||
update_shangpin_daily_stat(
|
||
shangpin_id=order.ProductID,
|
||
caozuo_leixing=3, # 3 = 退款
|
||
dingdan_jiage=order.Amount,
|
||
pingtai_shouyi=pingtai_shouyi,
|
||
dianpu_zongshouyi=dianpu_shouyi
|
||
)
|
||
logger.info(f"商品统计更新完成,商品ID: {order.ProductID}")
|
||
|
||
# 店铺每日统计(仅当存在店铺ID时执行)
|
||
dianpu_id = None
|
||
if hasattr(order, 'pingtai_kuozhan'):
|
||
dianpu_id = order.pingtai_kuozhan.ShopID
|
||
if dianpu_id:
|
||
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
|
||
update_dianpu_daily_stat(
|
||
dianpu_id=dianpu_id,
|
||
caozuo_leixing=3, # 3 = 退款
|
||
dingdan_jiage=order.Amount,
|
||
dianpu_shouyi=dianpu_shouyi
|
||
)
|
||
logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
|
||
except Exception as e:
|
||
logger.error(f"商品/店铺统计更新失败: {e}")
|
||
logger.error(traceback.format_exc())
|
||
# 统计失败不影响主流程
|
||
# =============================================
|
||
|
||
# 10. 更新退款记录表(假设存在 RefundRecord 模型,字段需根据实际调整)
|
||
|
||
#更新支出记录
|
||
update_daily_payout(order.Amount)
|
||
|
||
|
||
try:
|
||
refund_record, created = RefundRecord.query.get_or_create(
|
||
OrderID=dingdan_id,
|
||
defaults={
|
||
'PlayerID': jiedan_dashou_id or '',
|
||
'ApplicantID': '',
|
||
'ProcessorID': phone,
|
||
'Reason': tuikuan_liyou or '客服退款',
|
||
'ApplyStatus': 1, # 成功
|
||
'Amount': order.Amount,
|
||
'PlayerCommission': order.PlayerCommission or 0,
|
||
'Description': order.Description or '',
|
||
'Remark': '客服平台退款',
|
||
'Nickname': order.Nickname or '',
|
||
}
|
||
)
|
||
if not created:
|
||
refund_record.ApplyStatus = 1
|
||
refund_record.ProcessorID = phone
|
||
if tuikuan_liyou:
|
||
refund_record.Reason = tuikuan_liyou
|
||
refund_record.save()
|
||
except Exception as e:
|
||
logger.error(f"平台退款更新退款记录异常: {str(e)}", exc_info=True)
|
||
|
||
# 11. 更新客服统计数据
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
# 🔥 新增:调用打手每日统计(成交)
|
||
try:
|
||
update_dashou_daily_by_action(
|
||
yonghuid=order.PlayerID,
|
||
amount=order.PlayerCommission,
|
||
action=3 # 3 表示退款(退款)
|
||
)
|
||
logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
|
||
except Exception as e:
|
||
logger.error(f"打手每日统计更新失败: {e}")
|
||
# 不影响主流程
|
||
|
||
logger.info(f"平台退款成功,订单 {dingdan_id},退款单号 {refund_result.get('refund_id')}")
|
||
return Response({'code': 0, 'msg': '退款成功'})
|
||
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
except Exception as e:
|
||
logger.error(f"平台退款接口异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def call_wechat_refund(self, dingdan_id, jine, transaction_id=None):
|
||
"""
|
||
调用微信支付退款接口
|
||
:param dingdan_id: 商户订单号(我们的订单ID)
|
||
:param jine: 退款金额
|
||
:param transaction_id: 微信支付订单号(优先使用)
|
||
:return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'}
|
||
"""
|
||
# 获取配置
|
||
appid = getattr(settings, 'WEIXIN_APPID', '')
|
||
mch_id = getattr(settings, 'WEIXIN_MCHID', '')
|
||
key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '')
|
||
cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '')
|
||
key_path = getattr(settings, 'WEIXIN_KEY_PATH', '')
|
||
|
||
if not all([appid, mch_id, key, cert_path, key_path]):
|
||
missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v]
|
||
logger.error(f"微信支付配置缺失: {missing}")
|
||
return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'}
|
||
|
||
# 生成退款单号
|
||
out_refund_no = self.generate_refund_no(dingdan_id)
|
||
total_fee = int(float(jine) * 100)
|
||
refund_fee = total_fee
|
||
refund_desc = '客服退款'
|
||
|
||
# 构造请求参数
|
||
params = {
|
||
'appid': appid,
|
||
'mch_id': mch_id,
|
||
'nonce_str': self.generate_nonce_str(),
|
||
'out_refund_no': out_refund_no,
|
||
'total_fee': total_fee,
|
||
'refund_fee': refund_fee,
|
||
'refund_desc': refund_desc,
|
||
}
|
||
if transaction_id:
|
||
params['transaction_id'] = transaction_id
|
||
else:
|
||
params['out_trade_no'] = dingdan_id # 使用订单ID作为商户订单号
|
||
|
||
# 生成签名
|
||
sorted_keys = sorted(params.keys())
|
||
stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys])
|
||
stringSignTemp = f"{stringA}&key={key}"
|
||
sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
|
||
params['sign'] = sign
|
||
|
||
xml_data = self.dict_to_xml(params)
|
||
url = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
|
||
|
||
try:
|
||
response = requests.post(
|
||
url,
|
||
data=xml_data,
|
||
cert=(cert_path, key_path),
|
||
timeout=10
|
||
)
|
||
result = xmltodict.parse(response.content)['xml']
|
||
logger.info(f"微信退款响应: {result}")
|
||
except Exception as e:
|
||
logger.error(f"微信退款请求异常: {str(e)}", exc_info=True)
|
||
return {'code': 500, 'msg': '微信退款请求异常'}
|
||
|
||
if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
|
||
return {'code': 0, 'msg': 'success', 'refund_id': out_refund_no}
|
||
else:
|
||
err_msg = result.get('err_code_des', result.get('return_msg', '未知错误'))
|
||
logger.warning(f"微信退款失败: {err_msg}")
|
||
return {'code': 400, 'msg': err_msg}
|
||
|
||
def generate_nonce_str(self):
|
||
return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||
|
||
def generate_refund_no(self, dingdan_id):
|
||
timestamp = str(int(time.time()))
|
||
rand = ''.join(random.choices('0123456789', k=4))
|
||
return f"{dingdan_id}REF{timestamp}{rand}"
|
||
|
||
def dict_to_xml(self, data):
|
||
xml = ['<xml>']
|
||
for k, v in data.items():
|
||
xml.append(f'<{k}>{v}</{k}>')
|
||
xml.append('</xml>')
|
||
return ''.join(xml)
|
||
|
||
|
||
|
||
|
||
|
||
# dingdan/views.py
|
||
class KefuRejectRefundView(APIView):
|
||
"""
|
||
客服拒绝退款接口
|
||
请求:POST /yonghu/kefujjtk
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"dingdan_id": "订单ID",
|
||
"jujue_liyou": "拒绝理由(可选)"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 成功
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
phone = request.data.get('phone', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
jujue_liyou = request.data.get('jujue_liyou', '').strip()
|
||
|
||
if not phone or not dingdan_id:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if kefu.zhuangtai != 1:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 3. 查询订单并加锁
|
||
try:
|
||
with transaction.atomic():
|
||
order = Order.objects.select_for_update().get(OrderID=dingdan_id)
|
||
|
||
# 4. 验证订单状态为退款审核中(4)
|
||
if order.Status != 4:
|
||
return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 获取必要字段
|
||
jiedan_dashou_id = order.PlayerID
|
||
dashou_fencheng = order.PlayerCommission
|
||
fadan_pingtai = order.Platform
|
||
|
||
# 5. 验证打手分成金额是否存在
|
||
if dashou_fencheng is None:
|
||
return Response({'code': 400, 'msg': '订单缺少打手分成金额'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 6. 更新订单状态为退款失败(6)
|
||
order.Status = 6
|
||
order.AssignedCS = current_user.Phone # 记录处理客服手机号
|
||
if jujue_liyou:
|
||
order.RefundReason = jujue_liyou
|
||
order.save()
|
||
|
||
# 7. 给打手结算(返还分成)
|
||
if jiedan_dashou_id:
|
||
try:
|
||
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
|
||
dashou_profile = dashou_user.DashouProfile
|
||
# 增加打手相关统计数据
|
||
dashou_profile.chengjiaozongliang += 1
|
||
dashou_profile.yue += dashou_fencheng
|
||
dashou_profile.zonge += dashou_fencheng
|
||
dashou_profile.jinrishouyi += dashou_fencheng
|
||
dashou_profile.jinyueshouyi += dashou_fencheng
|
||
dashou_profile.save()
|
||
logger.info(f"拒绝退款:打手 {jiedan_dashou_id} 获得分成 {dashou_fencheng}")
|
||
except (User.DoesNotExist, AttributeError):
|
||
logger.warning(f"拒绝退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算")
|
||
|
||
# 8. 如果是商家发单,更新商家成交订单数量
|
||
if fadan_pingtai == 2:
|
||
try:
|
||
# 获取商家订单扩展表
|
||
shangjia_kuozhan = order.shangjia_kuozhan
|
||
shangjia_id = shangjia_kuozhan.MerchantID
|
||
if shangjia_id:
|
||
shangjia_user = User.query.get(UserUID=shangjia_id)
|
||
shangjia_profile = shangjia_user.ShopProfile
|
||
shangjia_profile.chengjiao += 1
|
||
shangjia_profile.save()
|
||
logger.info(f"拒绝退款:商家 {shangjia_id} 成交订单+1")
|
||
|
||
# ✅ 新方法(行为驱动,更安全)
|
||
update_shangjia_daily(
|
||
yonghuid=shangjia_id,
|
||
amount=Decimal(str(order.Amount)),
|
||
action=2 # 2 = 结算
|
||
)
|
||
except (ObjectDoesNotExist, User.DoesNotExist, AttributeError):
|
||
logger.warning(f"拒绝退款:商家信息不存在,跳过商家更新")
|
||
|
||
# 9. 更新退款记录表
|
||
try:
|
||
refund_record = RefundRecord.query.get(OrderID=dingdan_id)
|
||
refund_record.ApplyStatus = 2 # 2表示拒绝
|
||
refund_record.ProcessorID = phone
|
||
refund_record.RejectReason = jujue_liyou # 记录驳回理由(假设模型有此字段)
|
||
refund_record.save()
|
||
logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}")
|
||
except RefundRecord.DoesNotExist:
|
||
# 如果退款记录不存在,可以创建一条记录(可选)
|
||
RefundRecord.query.create(
|
||
OrderID=dingdan_id,
|
||
PlayerID=jiedan_dashou_id or '',
|
||
ApplicantID='',
|
||
ProcessorID=phone,
|
||
Reason=jujue_liyou or '客服拒绝退款',
|
||
ApplyStatus=2,
|
||
Amount=order.Amount,
|
||
PlayerCommission=dashou_fencheng or 0,
|
||
Description=order.Description or '',
|
||
Remark='客服拒绝退款',
|
||
Nickname=order.Nickname or '',
|
||
RejectReason=jujue_liyou or '',
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True)
|
||
|
||
# 🔥 新增:调用打手每日统计(成交)
|
||
try:
|
||
update_dashou_daily_by_action(
|
||
yonghuid=order.PlayerID,
|
||
amount=dashou_fencheng,
|
||
action=2 # 2 表示成交(结算)
|
||
)
|
||
logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
|
||
except Exception as e:
|
||
logger.error(f"打手每日统计更新失败: {e}")
|
||
# 不影响主流程
|
||
|
||
# 10. 更新客服统计数据
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
return Response({'code': 0, 'msg': '拒绝退款成功'})
|
||
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
except Exception as e:
|
||
logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KefuMerchantRefundView(APIView):
|
||
"""
|
||
客服商家订单退款接口(发单平台=2)
|
||
请求:POST /yonghu/kefusjtk
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"dingdan_id": "订单ID",
|
||
"tuikuan_liyou": "退款理由(可选)"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 成功
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
phone = request.data.get('phone', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
|
||
|
||
if not phone or not dingdan_id:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 客服身份验证(同前,省略以节省篇幅,但实际必须完整)
|
||
current_user = request.user
|
||
# ... 验证
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if kefu.zhuangtai != 1:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
|
||
# 查询订单,预加载商家扩展表
|
||
try:
|
||
order = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id)
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 验证商家订单
|
||
if order.Platform != 2:
|
||
return Response({'code': 400, 'msg': '该订单不是商家订单'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证状态允许退款
|
||
allowed_statuses = [1, 7, 2, 8, 4]
|
||
if order.Status not in allowed_statuses:
|
||
return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 获取必要数据
|
||
jiedan_dashou_id = order.PlayerID
|
||
jine = order.Amount
|
||
if jine is None:
|
||
return Response({'code': 400, 'msg': '订单金额缺失'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
shangjia_kuozhan = order.shangjia_kuozhan
|
||
shangjia_id = shangjia_kuozhan.MerchantID
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 400, 'msg': '订单商家信息不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
with transaction.atomic():
|
||
# 更新订单
|
||
order.Status = 5
|
||
order.AssignedCS = current_user.Phone
|
||
if tuikuan_liyou:
|
||
order.RefundReason = tuikuan_liyou
|
||
order.save()
|
||
|
||
# 更新打手
|
||
if jiedan_dashou_id:
|
||
try:
|
||
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
|
||
dashou_profile = dashou_user.DashouProfile
|
||
dashou_profile.tuikuanliang += 1
|
||
dashou_profile.zhuangtai = 1
|
||
dashou_profile.save()
|
||
except (User.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"商家退款:打手 {jiedan_dashou_id} 不存在,跳过更新")
|
||
|
||
# 更新商家
|
||
try:
|
||
shangjia_user = User.query.get(UserUID=shangjia_id)
|
||
shangjia_profile = shangjia_user.ShopProfile
|
||
shangjia_profile.tuikuan += 1
|
||
shangjia_profile.yue += jine
|
||
shangjia_profile.save()
|
||
except (User.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"商家退款:商家 {shangjia_id} 不存在,跳过更新")
|
||
|
||
# 更新退款记录表
|
||
try:
|
||
refund_record = RefundRecord.query.get(OrderID=dingdan_id)
|
||
refund_record.ApplyStatus = 1
|
||
refund_record.ProcessorID = phone
|
||
refund_record.save()
|
||
except RefundRecord.DoesNotExist:
|
||
pass
|
||
|
||
# 更新客服统计
|
||
update_shangjia_daily(
|
||
yonghuid=shangjia_id,
|
||
amount=Decimal(str(order.Amount)),
|
||
action=3 # 3 = 退款
|
||
)
|
||
|
||
|
||
# 调用打手每日统计(成交)
|
||
try:
|
||
update_dashou_daily_by_action(
|
||
yonghuid=order.PlayerID,
|
||
amount=order.PlayerCommission,
|
||
action=3 # 3 表示退款(退款)
|
||
)
|
||
logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
|
||
except Exception as e:
|
||
logger.error(f"打手每日统计更新失败: {e}")
|
||
# 不影响主流程
|
||
|
||
|
||
|
||
# 管理员无客服扩展表,跳过计数器更新
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
if not is_admin:
|
||
kefu = current_user.KefuProfile
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
return Response({'code': 0, 'msg': '退款成功'})
|
||
|
||
|
||
|
||
class KefuRejectSettlementView(APIView):
|
||
"""
|
||
客服拒绝结算接口(仅平台订单,状态2/8)
|
||
请求:POST /yonghu/kefujjjs
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"dingdan_id": "订单ID",
|
||
"tkly": "拒绝结算理由(必填)"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 成功,其他失败
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
phone = request.data.get('phone', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
tkly = request.data.get('tkly', '').strip()
|
||
|
||
if not phone or not dingdan_id or not tkly:
|
||
return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
logger.warning(f"手机号不匹配: {phone}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if kefu.zhuangtai != 1:
|
||
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 3. 查询订单,预加载平台扩展表
|
||
try:
|
||
order = Order.query.select_related('pingtai_kuozhan').get(OrderID=dingdan_id)
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 4. 校验订单条件:平台发单(1)且状态为2或8
|
||
if order.Platform != 1 or order.Status not in [2, 8]:
|
||
return Response({'code': 400, 'msg': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
jiedan_dashou_id = order.PlayerID
|
||
if not jiedan_dashou_id:
|
||
return Response({'code': 400, 'msg': '订单暂无接单打手'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 5. 使用事务更新
|
||
with transaction.atomic():
|
||
# 5.1 更新订单主表
|
||
order.Status = 5 # 已退款
|
||
order.RefundReason = tkly # 拒绝理由
|
||
order.AssignedCS = phone # 记录客服手机号
|
||
order.save()
|
||
|
||
# 5.2 更新平台订单扩展表:标记为拒绝结算(假设字段 jjjs_biaoshi)
|
||
# 注意:需要确保 PlatformOrderExt 模型中已添加 jjjs_biaoshi 字段,IntegerField,默认0
|
||
if hasattr(order, 'pingtai_kuozhan') and order.pingtai_kuozhan:
|
||
pingtai_ext = order.pingtai_kuozhan
|
||
pingtai_ext.RejectSettlementFlag = 1 # 1表示拒绝结算
|
||
pingtai_ext.save()
|
||
else:
|
||
# 理论上应该存在,但若不存在则记录日志
|
||
logger.warning(f"订单 {dingdan_id} 缺少平台扩展表,无法标记拒绝结算")
|
||
|
||
# 5.3 更新打手扩展表
|
||
try:
|
||
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
|
||
dashou_profile = dashou_user.DashouProfile
|
||
dashou_profile.zhuangtai = 1 # 打手状态设为空闲
|
||
dashou_profile.tuikuanliang += 1 # 退款订单总量+1
|
||
dashou_profile.save()
|
||
logger.info(f"拒绝结算:打手 {jiedan_dashou_id} 退款订单+1,状态空闲")
|
||
except (User.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"拒绝结算:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
|
||
except Exception as e:
|
||
logger.error(f"拒绝结算更新打手异常: {str(e)}", exc_info=True)
|
||
|
||
# 5.4 更新客服扩展表统计数据
|
||
|
||
|
||
# 🔥 新增:调用打手每日统计(成交)
|
||
try:
|
||
update_dashou_daily_by_action(
|
||
yonghuid=order.PlayerID,
|
||
amount=order.PlayerCommission,
|
||
action=3 # 3 表示退款(退款)
|
||
)
|
||
logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
|
||
except Exception as e:
|
||
logger.error(f"打手每日统计更新失败: {e}")
|
||
# 不影响主流程
|
||
|
||
|
||
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
return Response({'code': 0, 'msg': '拒绝结算成功'})
|
||
|
||
|
||
|
||
class KefuTransferHallView(APIView):
|
||
"""
|
||
客服恢复大厅(转移大厅)接口
|
||
请求:POST /yonghu/kefuzydt
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"dingdan_id": "订单ID"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 成功
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
phone = request.data.get('phone', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
|
||
if not phone or not dingdan_id:
|
||
return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 客服身份验证(同前,略,但必须完整实现)
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
if kefu.zhuangtai != 1:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 3. 查询订单
|
||
try:
|
||
order = Order.query.get(OrderID=dingdan_id)
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 4. 校验订单状态(允许状态:2,8,4)
|
||
if order.Status not in [2, 8, 4]:
|
||
return Response({'code': 400, 'msg': '订单状态不允许转移大厅'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
|
||
if order.PlayerID:
|
||
UserDashou.query.filter(user__UserUID=order.PlayerID).update(zhuangtai=1)
|
||
else:
|
||
pass
|
||
|
||
# 5. 根据是否有指定ID决定新状态
|
||
if order.AssignedID:
|
||
new_status = 7 # 指定中
|
||
else:
|
||
new_status = 1 # 下单中
|
||
|
||
# 6. 使用事务更新
|
||
with transaction.atomic():
|
||
# 清空接单打手ID,更新状态,记录客服
|
||
order.PlayerID = None
|
||
order.Status = new_status
|
||
order.AssignedCS = phone
|
||
order.save()
|
||
|
||
return Response({'code': 0, 'msg': '转移大厅成功'})
|
||
|
||
|
||
class KefuCancelDesignationView(APIView):
|
||
"""
|
||
客服取消指定接口(仅状态7)
|
||
请求:POST /yonghu/kefuqxzd
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"dingdan_id": "订单ID"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 成功
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
phone = request.data.get('phone', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
|
||
if not phone or not dingdan_id:
|
||
return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 客服身份验证(同前,略,但必须完整实现)
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
if kefu.zhuangtai != 1:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 3. 查询订单
|
||
try:
|
||
order = Order.query.get(OrderID=dingdan_id)
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 4. 校验订单状态为7(指定中)
|
||
|
||
if order.Status != 7:
|
||
print(999)
|
||
return Response({'code': 400, 'msg': '仅状态为指定中(7)的订单可取消指定'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 5. 使用事务更新
|
||
with transaction.atomic():
|
||
# 状态改为1(下单中),清空指定ID,记录客服
|
||
order.Status = 1
|
||
order.AssignedID = None
|
||
order.AssignedCS = phone
|
||
order.save()
|
||
|
||
return Response({'code': 0, 'msg': '取消指定成功'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KefuGetDashouListView(APIView):
|
||
"""
|
||
客服获取打手列表接口(仅打手类型)
|
||
请求:POST /yonghu/kefuhqdslb
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"keyword": "搜索关键词(可选,搜索用户ID或昵称)",
|
||
"page": "页码(从1开始)",
|
||
"page_size": "每页数量"
|
||
}
|
||
认证:JWT
|
||
返回:{
|
||
"code": 0,
|
||
"data": {
|
||
"list": [打手对象],
|
||
"has_more": true/false
|
||
}
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
phone = request.data.get('zhanghao', '').strip()
|
||
keyword = request.data.get('keyword', '').strip()
|
||
try:
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 20))
|
||
except ValueError:
|
||
return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if not phone:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
logger.warning(f"手机号不匹配: {phone}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if kefu.zhuangtai != 1:
|
||
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 3. 构建查询条件(只查询打手)
|
||
# 通过 UserDashou 扩展表关联 User
|
||
queryset = UserDashou.query.select_related('user').all()
|
||
|
||
if keyword:
|
||
# 搜索用户ID或打手昵称
|
||
queryset = queryset.filter(
|
||
Q(user__UserUID__icontains=keyword) |
|
||
Q(nicheng__icontains=keyword)
|
||
)
|
||
|
||
# 4. 计算总数并分页
|
||
total = queryset.count()
|
||
offset = (page - 1) * page_size
|
||
dashou_list = queryset.order_by('-CreateTime')[offset:offset + page_size]
|
||
|
||
# 5. 构建返回数据(只取需要的字段)
|
||
result = []
|
||
for dashou in dashou_list:
|
||
user = dashou.user
|
||
result.append({
|
||
'yonghuid': user.UserUID,
|
||
'avatar': user.Avatar or '',
|
||
'zaixianzhuangtai': dashou.zaixianzhuangtai,
|
||
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
|
||
'nicheng': dashou.nicheng or '',
|
||
'zhuangtai': dashou.zhuangtai,
|
||
})
|
||
|
||
# 6. 判断是否有更多数据
|
||
has_more = (offset + len(dashou_list)) < total
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': result,
|
||
'has_more': has_more
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KefuGetDashouDetailView(APIView):
|
||
"""
|
||
客服获取打手详情接口
|
||
请求:POST /yonghu/kefuhqdsxq
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"uid": "打手ID (yonghuid)"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 + 打手信息 + 会员列表
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
phone = request.data.get('phone', '').strip()
|
||
uid = request.data.get('uid', '').strip()
|
||
|
||
if not phone or not uid:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
logger.warning(f"手机号不匹配: {phone}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if kefu.zhuangtai != 1:
|
||
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 3. 查询打手主表和扩展表
|
||
try:
|
||
# 使用 select_related 预加载 dashou_profile,减少数据库查询
|
||
user_main = User.query.select_related('DashouProfile').get(
|
||
UserUID=uid
|
||
)
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 验证用户类型是否为打手(可选,但建议验证)
|
||
# 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册)
|
||
try:
|
||
dashou = user_main.DashouProfile
|
||
except ObjectDoesNotExist:
|
||
# 用户存在但未注册打手身份,返回基本信息
|
||
user_info = {
|
||
'yonghuid': user_main.UserUID,
|
||
'avatar': user_main.Avatar or '',
|
||
'zaixianzhuangtai': 0,
|
||
'zhanghaozhuangtai': 0,
|
||
'zhuangtai': 0,
|
||
'nicheng': '',
|
||
'chenghao': '',
|
||
'dianhua': '',
|
||
'wechat': '',
|
||
'phone': user_main.Phone or '',
|
||
'CreateTime': user_main.UserCreateTime,
|
||
'ip': user_main.IP or '',
|
||
'jiedanzongliang': 0,
|
||
'chengjiaozongliang': 0,
|
||
'tuikuanliang': 0,
|
||
'jinrijiedan': 0,
|
||
'yue': 0.00,
|
||
'zonge': 0.00,
|
||
'jifen': 0,
|
||
'yajin': 0.00,
|
||
'jinrishouyi': 0.00,
|
||
'jinyueshouyi': 0.00,
|
||
'yaoqingren': '',
|
||
'jieshao': '',
|
||
}
|
||
member_list = []
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'user_info': user_info,
|
||
'member_list': member_list
|
||
}
|
||
})
|
||
|
||
# 4. 构建打手信息
|
||
user_info = {
|
||
'yonghuid': user_main.UserUID,
|
||
'avatar': user_main.Avatar or '',
|
||
'zaixianzhuangtai': dashou.zaixianzhuangtai,
|
||
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
|
||
'zhuangtai': dashou.zhuangtai,
|
||
'nicheng': dashou.nicheng or '',
|
||
'chenghao': dashou.chenghao or '',
|
||
'dianhua': dashou.dianhua or '',
|
||
'wechat': dashou.wechat or '',
|
||
'phone': user_main.Phone or '',
|
||
'CreateTime': user_main.UserCreateTime,
|
||
'ip': user_main.IP or '',
|
||
'jiedanzongliang': dashou.jiedanzongliang,
|
||
'chengjiaozongliang': dashou.chengjiaozongliang,
|
||
'tuikuanliang': dashou.tuikuanliang,
|
||
'jinrijiedan': dashou.jinrijiedan,
|
||
'yue': float(dashou.yue) if dashou.yue else 0.00,
|
||
'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
|
||
'jifen': dashou.jifen,
|
||
'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
|
||
'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
|
||
'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
|
||
'yaoqingren': dashou.yaoqingren or '',
|
||
'jieshao': dashou.jieshao or '',
|
||
}
|
||
|
||
# 5. 查询打手会员信息(仅未过期)
|
||
member_records = Huiyuangoumai.query.filter(
|
||
yonghu_id=uid
|
||
).order_by('-CreateTime')
|
||
|
||
member_list = []
|
||
for record in member_records:
|
||
# 调用模型方法检查是否过期
|
||
if hasattr(record, 'jiance_shifou_daoqi'):
|
||
is_expired = record.jiance_shifou_daoqi()
|
||
else:
|
||
# 如果方法不存在,简单判断
|
||
is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
|
||
|
||
if not is_expired:
|
||
# 获取会员详情
|
||
try:
|
||
huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id)
|
||
huiyuan_name = huiyuan.jieshao
|
||
except Huiyuan.DoesNotExist:
|
||
huiyuan_name = '未知会员'
|
||
|
||
member_list.append({
|
||
'id': record.id,
|
||
'huiyuan_name': huiyuan_name,
|
||
'daoqi_time': record.daoqi_time,
|
||
'is_active': record.huiyuan_zhuangtai == 1,
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'user_info': user_info,
|
||
'member_list': member_list
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
class KefuUpdateDashouView(APIView):
|
||
permission_classes = [IsAuthenticated]
|
||
"""
|
||
客服修改打手信息接口
|
||
支持操作:
|
||
- jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb)
|
||
- jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb)
|
||
- jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd)
|
||
- gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee)
|
||
- gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee)
|
||
- gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee)
|
||
- jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg)
|
||
"""
|
||
def post(self, request):
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
dashou_id = request.data.get('dashou_id', '').strip()
|
||
caozuo = request.data.get('caozuo', '').strip()
|
||
value = request.data.get('value') # 金额、积分、天数等
|
||
huiyuan_id = request.data.get('huiyuan_id', '').strip() # 会员ID(加/减会员时使用)
|
||
|
||
if not username or not dashou_id or not caozuo:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 权限校验(统一方法)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 查询打手是否存在
|
||
try:
|
||
dashou_user = User.query.get(UserUID=dashou_id)
|
||
dashou_profile = dashou_user.DashouProfile
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手不存在'})
|
||
except UserDashou.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手扩展信息缺失'})
|
||
|
||
# 4. 根据操作类型执行(事务内)
|
||
with transaction.atomic():
|
||
try:
|
||
# 记录修改前的值(用于日志)
|
||
before = {
|
||
'yue': dashou_profile.yue,
|
||
'yajin': dashou_profile.yajin,
|
||
'jifen': dashou_profile.jifen,
|
||
'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
|
||
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
|
||
'zhuangtai': dashou_profile.zhuangtai,
|
||
}
|
||
|
||
# ---------- 余额操作 ----------
|
||
if caozuo == 'jian_yue':
|
||
if '001aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限减打手余额'})
|
||
amount = Decimal(str(value))
|
||
if dashou_profile.yue < amount:
|
||
return Response({'code': 400, 'msg': '余额不足'})
|
||
dashou_profile.yue -= amount
|
||
dashou_profile.save()
|
||
after_yue = dashou_profile.yue
|
||
# 记录日志
|
||
Xiugaijilu.query.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=2, # 2=打手
|
||
xiugaitijiao=after_yue,
|
||
xiugaitijiaoq=before['yue'],
|
||
)
|
||
return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after_yue)}})
|
||
|
||
elif caozuo == 'jia_yue':
|
||
if '001bb' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限加打手余额'})
|
||
amount = Decimal(str(value))
|
||
dashou_profile.yue += amount
|
||
dashou_profile.save()
|
||
after_yue = dashou_profile.yue
|
||
Xiugaijilu.query.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=2,
|
||
xiugaitijiao=after_yue,
|
||
xiugaitijiaoq=before['yue'],
|
||
)
|
||
return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after_yue)}})
|
||
|
||
# ---------- 押金操作 ----------
|
||
elif caozuo == 'jian_yajin':
|
||
if '001aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限减打手押金'})
|
||
amount = Decimal(str(value))
|
||
if dashou_profile.yajin < amount:
|
||
return Response({'code': 400, 'msg': '押金不足'})
|
||
dashou_profile.yajin -= amount
|
||
dashou_profile.save()
|
||
after_yajin = dashou_profile.yajin
|
||
Xiugaijilu.query.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=2,
|
||
yajin=after_yajin,
|
||
yyajin=before['yajin'],
|
||
)
|
||
return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after_yajin)}})
|
||
|
||
elif caozuo == 'jia_yajin':
|
||
if '001bb' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限加打手押金'})
|
||
amount = Decimal(str(value))
|
||
dashou_profile.yajin += amount
|
||
dashou_profile.save()
|
||
after_yajin = dashou_profile.yajin
|
||
Xiugaijilu.query.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=2,
|
||
yajin=after_yajin,
|
||
yyajin=before['yajin'],
|
||
)
|
||
return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after_yajin)}})
|
||
|
||
# ---------- 积分操作 ----------
|
||
elif caozuo == 'jian_jifen':
|
||
if '001cc' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限减打手积分'})
|
||
jifen = int(value)
|
||
if dashou_profile.jifen < jifen:
|
||
return Response({'code': 400, 'msg': '积分不足'})
|
||
dashou_profile.jifen -= jifen
|
||
dashou_profile.save()
|
||
after_jifen = dashou_profile.jifen
|
||
Xiugaijilu.query.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=2,
|
||
jifen=after_jifen,
|
||
yjifen=before['jifen'],
|
||
)
|
||
return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after_jifen}})
|
||
|
||
elif caozuo == 'jia_jifen':
|
||
if '001dd' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限加打手积分'})
|
||
jifen = int(value)
|
||
dashou_profile.jifen += jifen
|
||
dashou_profile.save()
|
||
after_jifen = dashou_profile.jifen
|
||
Xiugaijilu.query.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=2,
|
||
jifen=after_jifen,
|
||
yjifen=before['jifen'],
|
||
)
|
||
return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after_jifen}})
|
||
|
||
# ---------- 状态修改 ----------
|
||
elif caozuo == 'gai_zhanghaozhuangtai':
|
||
if '001ee' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限修改打手账号状态'})
|
||
new_status = int(value)
|
||
if new_status not in [0, 1]:
|
||
return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'})
|
||
dashou_profile.zhanghaozhuangtai = new_status
|
||
dashou_profile.save()
|
||
Xiugaijilu.query.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=2,
|
||
qitashuoming=f'修改账号状态为{new_status}',
|
||
)
|
||
return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}})
|
||
|
||
elif caozuo == 'gai_zaixianzhuangtai':
|
||
if '001ee' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限修改打手在线状态'})
|
||
new_status = int(value)
|
||
if new_status not in [0, 1]:
|
||
return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'})
|
||
dashou_profile.zaixianzhuangtai = new_status
|
||
dashou_profile.save()
|
||
Xiugaijilu.query.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=2,
|
||
qitashuoming=f'修改在线状态为{new_status}',
|
||
)
|
||
return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}})
|
||
|
||
elif caozuo == 'gai_zhuangtai':
|
||
if '001ee' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限修改打手接单状态'})
|
||
new_status = int(value)
|
||
if new_status not in [0, 1]:
|
||
return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'})
|
||
dashou_profile.zhuangtai = new_status
|
||
dashou_profile.save()
|
||
Xiugaijilu.query.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=2,
|
||
qitashuoming=f'修改接单状态为{new_status}',
|
||
)
|
||
return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}})
|
||
|
||
# ---------- 会员操作 ----------
|
||
elif caozuo == 'jia_huiyuan':
|
||
if '001ff' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限给打手加会员'})
|
||
if not huiyuan_id:
|
||
return Response({'code': 400, 'msg': '缺少会员ID'})
|
||
days = int(value) if value else 30 # 默认加30天
|
||
try:
|
||
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
|
||
except Huiyuan.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '会员类型不存在'})
|
||
# 获取或创建购买记录
|
||
record, created = Huiyuangoumai.objects.select_for_update().get_or_create(
|
||
yonghu_id=dashou_id,
|
||
huiyuan_id=huiyuan_id,
|
||
defaults={
|
||
'huiyuan_zhuangtai': 1,
|
||
'jieshao': huiyuan.jieshao,
|
||
'daoqi_time': timezone.now() + timedelta(days=days),
|
||
}
|
||
)
|
||
if not created:
|
||
# 延长有效期
|
||
new_daoqi = record.daoqi_time + timedelta(days=days)
|
||
record.daoqi_time = new_daoqi
|
||
record.huiyuan_zhuangtai = 1
|
||
record.save()
|
||
# 记录修改日志
|
||
Xiugaijilu.query.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=2,
|
||
huiyuants=days,
|
||
huiyuan_id=huiyuan_id,
|
||
qitashuoming=f'加会员 {huiyuan_id},增加{days}天',
|
||
)
|
||
return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
|
||
|
||
elif caozuo == 'jian_huiyuan':
|
||
if '001gg' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限给打手减会员'})
|
||
if not huiyuan_id:
|
||
return Response({'code': 400, 'msg': '缺少会员ID'})
|
||
try:
|
||
record = Huiyuangoumai.query.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id)
|
||
except Huiyuangoumai.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '该打手未购买此会员'})
|
||
# 删除会员记录(或设置为过期,根据业务要求)
|
||
record.delete()
|
||
Xiugaijilu.query.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=2,
|
||
huiyuan_id=huiyuan_id,
|
||
qitashuoming=f'移除会员 {huiyuan_id}',
|
||
)
|
||
return Response({'code': 0, 'msg': '减会员成功'})
|
||
|
||
else:
|
||
return Response({'code': 400, 'msg': '未知操作类型'})
|
||
|
||
except Exception as e:
|
||
logger.error(f"修改打手信息异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KefuPunishmentListView(APIView):
|
||
"""
|
||
客服获取处罚记录列表接口
|
||
请求:POST /yonghu/kefu_cfgl
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"status": [0,3] 或 [1,2], # 状态数组
|
||
"dingdan_id": "订单ID(可选)",
|
||
"dashouid": "打手ID(可选)",
|
||
"page": 1,
|
||
"page_size": 20
|
||
}
|
||
认证:JWT
|
||
返回:{
|
||
"code": 0,
|
||
"data": {
|
||
"list": [处罚记录],
|
||
"total": 总数,
|
||
"stats": {
|
||
"zongshu": 总记录数,
|
||
"daichuli": 待处理数,
|
||
"yichuli": 已处理数
|
||
}
|
||
}
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
phone = request.data.get('phone', '').strip()
|
||
status_list = request.data.get('status')
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
dashouid = request.data.get('dashouid', '').strip()
|
||
try:
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 20))
|
||
except ValueError:
|
||
return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if not phone:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
# 1. 获取前端参数
|
||
username = request.data.get('phone', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 3. 检查平台订单管理权限(002ab)
|
||
if '005aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
|
||
|
||
# 3. 获取统计信息(不分页)
|
||
stats = {
|
||
'zongshu': PenaltyRecord.query.count(),
|
||
'daichuli': PenaltyRecord.query.filter(ApplyStatus__in=[0, 3]).count(),
|
||
'yichuli': PenaltyRecord.query.filter(ApplyStatus__in=[1, 2]).count(),
|
||
}
|
||
|
||
# 4. 构建查询条件
|
||
queryset = PenaltyRecord.query.all()
|
||
|
||
if status_list and isinstance(status_list, list):
|
||
queryset = queryset.filter(ApplyStatus__in=status_list)
|
||
|
||
if dingdan_id:
|
||
queryset = queryset.filter(OrderID=dingdan_id)
|
||
|
||
if dashouid:
|
||
queryset = queryset.filter(PlayerID=dashouid)
|
||
|
||
# 5. 分页
|
||
total = queryset.count()
|
||
offset = (page - 1) * page_size
|
||
records = queryset.order_by('-CreateTime')[offset:offset + page_size]
|
||
|
||
# 6. 构建返回列表
|
||
list_data = []
|
||
for item in records:
|
||
list_data.append({
|
||
'dingdan_id': item.OrderID,
|
||
'dashouid': item.PlayerID,
|
||
'qingqiuid': item.ApplicantID,
|
||
'cfliyou': item.PenaltyReason,
|
||
'sqzhuangtai': item.ApplyStatus,
|
||
'CreateTime': item.CreateTime,
|
||
'UpdateTime': item.UpdateTime,
|
||
# 其他字段按需添加
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': list_data,
|
||
'total': total,
|
||
'stats': stats
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KefuGetOrderDetailView(APIView):
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
phone = request.data.get('phone', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
|
||
if not phone or not dingdan_id:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 获取当前用户
|
||
current_user = request.user
|
||
|
||
# 验证手机号
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 验证用户类型
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 客服扩展表
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
if kefu.zhuangtai != 1:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
except Exception as e:
|
||
logger.exception(f"[kefuhqddxq] 获取客服扩展表异常 user={getattr(current_user, 'yonghuid', '?')}")
|
||
return Response({'code': 500, 'msg': '服务器错误'}, status=500)
|
||
|
||
# 3. 查询订单
|
||
try:
|
||
dingdan_obj = Order.query.select_related(
|
||
'pingtai_kuozhan', 'shangjia_kuozhan'
|
||
).get(OrderID=dingdan_id)
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'}, status=404)
|
||
except Exception as e:
|
||
logger.exception(f"[kefuhqddxq] 订单查询异常 dingdan_id={dingdan_id}")
|
||
return Response({'code': 500, 'msg': '服务器错误'}, status=500)
|
||
|
||
# 4. 构建基础数据
|
||
try:
|
||
response_data = {
|
||
'dingdan_id': dingdan_obj.OrderID or '',
|
||
'zhuangtai': dingdan_obj.Status or 0,
|
||
'fadanpingtai': dingdan_obj.Platform or 0,
|
||
'jiage': float(dingdan_obj.Amount) if dingdan_obj.Amount else 0.00,
|
||
'dashou_fencheng': float(dingdan_obj.PlayerCommission) if dingdan_obj.PlayerCommission else 0.00,
|
||
'jiedan_dashou_id': dingdan_obj.PlayerID or '',
|
||
'dashou_liuyan': dingdan_obj.PlayerRemark or '',
|
||
'zhiding_id': dingdan_obj.AssignedID or '',
|
||
'shangpin_id': dingdan_obj.ProductID or '',
|
||
'leixing': dingdan_obj.ProductTypeID or 0,
|
||
'tupian_url': dingdan_obj.ImageURL or '',
|
||
'jieshao': dingdan_obj.Description or '',
|
||
'beizhu': dingdan_obj.Remark or '',
|
||
'tkly': dingdan_obj.RefundReason or '',
|
||
'nicheng': dingdan_obj.Nickname or '',
|
||
'clkf': dingdan_obj.AssignedCS or '',
|
||
'chuangjianshijian': dingdan_obj.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.CreateTime else '',
|
||
'genggaishijian': dingdan_obj.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.UpdateTime else '',
|
||
}
|
||
except Exception as e:
|
||
logger.exception(f"[kefuhqddxq] 构建基础数据异常 dingdan_id={dingdan_id}")
|
||
return Response({'code': 500, 'msg': '服务器错误'}, status=500)
|
||
|
||
# 5. 扩展表数据
|
||
try:
|
||
fadan_pingtai = dingdan_obj.Platform
|
||
if fadan_pingtai == 1:
|
||
if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan:
|
||
pingtai = dingdan_obj.pingtai_kuozhan
|
||
response_data['pingtai_kuozhan'] = {
|
||
'laoban_id': pingtai.BossID or '',
|
||
'laoban_pingjia': pingtai.BossReview or '',
|
||
}
|
||
else:
|
||
response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''}
|
||
elif fadan_pingtai == 2:
|
||
if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan:
|
||
shangjia = dingdan_obj.shangjia_kuozhan
|
||
response_data['shangjia_kuozhan'] = {
|
||
'shangjia_id': shangjia.MerchantID or '',
|
||
'sjnicheng': shangjia.MerchantNickname or '',
|
||
'shangjia_pingjia': shangjia.MerchantReview or '',
|
||
'cfliyou': shangjia.PenaltyReason or '',
|
||
'sqzhuangtai': shangjia.PenaltyApplyStatus if shangjia.PenaltyApplyStatus is not None else 0,
|
||
'bhliyou': shangjia.RejectReason or '',
|
||
}
|
||
else:
|
||
response_data['shangjia_kuozhan'] = {
|
||
'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '',
|
||
'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': ''
|
||
}
|
||
except Exception as e:
|
||
logger.exception(f"[kefuhqddxq] 扩展数据异常 dingdan_id={dingdan_id}")
|
||
return Response({'code': 500, 'msg': '服务器错误'}, status=500)
|
||
|
||
# 6. 打手图片
|
||
try:
|
||
dashou_images = PlayerDeliveryImage.query.filter(OrderID=dingdan_id).values_list('ImageURL', flat=True)
|
||
response_data['dashou_images'] = [img for img in dashou_images if img]
|
||
except Exception as e:
|
||
logger.exception(f"[kefuhqddxq] 打手图片查询异常 dingdan_id={dingdan_id}")
|
||
response_data['dashou_images'] = []
|
||
|
||
# 7. 退款理由
|
||
try:
|
||
tuikuan_record = RefundRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first()
|
||
response_data['tuikuan_liyou'] = tuikuan_record.Reason if tuikuan_record and tuikuan_record.Reason else ''
|
||
except Exception as e:
|
||
logger.exception(f"[kefuhqddxq] 退款理由查询异常 dingdan_id={dingdan_id}")
|
||
response_data['tuikuan_liyou'] = ''
|
||
|
||
return Response({'code': 0, 'data': response_data})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KefuPunishmentDetailView(APIView):
|
||
"""
|
||
客服获取处罚详情接口
|
||
请求:POST /yonghu/kefu_cf_detail
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"dingdan_id": "订单ID"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 + 处罚记录详情(含图片)
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
phone = request.data.get('phone', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
|
||
if not phone or not dingdan_id:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 客服身份验证
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
logger.warning(f"手机号不匹配: {phone}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if kefu.zhuangtai != 1:
|
||
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 查询处罚记录(按创建时间倒序取最新一条)
|
||
try:
|
||
record = PenaltyRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first()
|
||
if not record:
|
||
return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
except Exception as e:
|
||
logger.error(f"查询处罚记录失败: {e}")
|
||
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
# 查询商家证据图片
|
||
zhengju_images = []
|
||
shensu_images = []
|
||
try:
|
||
# 商家证据图片:上传者为请求人 (ApplicantID)
|
||
zhengju_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.ApplicantID)
|
||
zhengju_images = [item.ImageURL for item in zhengju_qs if item.ImageURL]
|
||
|
||
# 打手申诉图片:上传者为打手 (PlayerID)
|
||
shensu_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.PlayerID)
|
||
shensu_images = [item.ImageURL for item in shensu_qs if item.ImageURL]
|
||
except Exception as e:
|
||
logger.error(f"查询图片失败: {e}")
|
||
|
||
# 构建返回数据
|
||
data = {
|
||
'dingdan_id': record.OrderID,
|
||
'dashouid': record.PlayerID,
|
||
'qingqiuid': record.ApplicantID,
|
||
'chuliid': record.ProcessorID,
|
||
'cfliyou': record.PenaltyReason,
|
||
'sqzhuangtai': record.ApplyStatus,
|
||
'bhliyou': record.RejectReason,
|
||
'jifen': record.DeductedPoints,
|
||
'ssliyou': record.AppealReason or '',
|
||
'zhengju_tupian': zhengju_images,
|
||
'shensu_tupian': shensu_images,
|
||
'CreateTime': record.CreateTime,
|
||
'UpdateTime': record.UpdateTime,
|
||
}
|
||
|
||
return Response({'code': 0, 'data': data})
|
||
|
||
|
||
|
||
|
||
class KefuPunishmentActionView(APIView):
|
||
"""
|
||
客服同意/驳回处罚接口
|
||
请求:POST /yonghu/kefu_cf_action
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"dingdan_id": "订单ID",
|
||
"action": 1(同意) / 2(驳回),
|
||
"reject_reason": "驳回理由(驳回时必填)"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 成功
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
phone = request.data.get('phone', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
action = request.data.get('action')
|
||
reject_reason = request.data.get('reject_reason', '').strip()
|
||
|
||
if not phone or not dingdan_id or action not in [1, 2]:
|
||
return Response({'code': 401, 'msg': '参数错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if action == 2 and not reject_reason:
|
||
return Response({'code': 400, 'msg': '驳回理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 客服身份验证(完整验证,此处略)
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
if kefu.zhuangtai != 1:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 查询待处理的处罚记录(状态0或3)
|
||
try:
|
||
record = PenaltyRecord.query.filter(
|
||
OrderID=dingdan_id,
|
||
ApplyStatus__in=[0, 3]
|
||
).order_by('-CreateTime').first()
|
||
if not record:
|
||
exists = PenaltyRecord.query.filter(OrderID=dingdan_id).exists()
|
||
if exists:
|
||
return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST)
|
||
else:
|
||
return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
except Exception as e:
|
||
logger.error(f"查询处罚记录失败: {e}")
|
||
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
with transaction.atomic():
|
||
# 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回
|
||
duplicate_records = PenaltyRecord.query.filter(
|
||
OrderID=dingdan_id,
|
||
ApplyStatus__in=[0, 3]
|
||
).exclude(id=record.id)
|
||
for dup in duplicate_records:
|
||
dup.ApplyStatus = 2
|
||
dup.ProcessorID = phone
|
||
dup.RejectReason = "系统自动驳回:存在重复处罚记录"
|
||
dup.save()
|
||
logger.info(f"自动驳回重复处罚记录 ID={dup.id}")
|
||
|
||
if action == 1: # 同意
|
||
# 可选验证打手存在
|
||
if record.PlayerID:
|
||
try:
|
||
dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
|
||
except UserDashou.DoesNotExist:
|
||
return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
record.ApplyStatus = 1
|
||
record.ProcessorID = phone
|
||
record.save()
|
||
|
||
# 更新商家订单扩展表
|
||
try:
|
||
dingdan = Order.query.get(OrderID=dingdan_id)
|
||
if hasattr(dingdan, 'shangjia_kuozhan'):
|
||
dingdan.shangjia_kuozhan.PenaltyApplyStatus = 1
|
||
dingdan.shangjia_kuozhan.save()
|
||
except Exception as e:
|
||
logger.warning(f"更新商家扩展表失败: {e}")
|
||
|
||
elif action == 2: # 驳回
|
||
# 返还积分
|
||
if record.DeductedPoints > 0 and record.PlayerID:
|
||
try:
|
||
dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
|
||
if dashou.jifen <= 1000: # 安全限制
|
||
dashou.jifen += record.DeductedPoints
|
||
dashou.save()
|
||
logger.info(f"驳回处罚,打手{record.PlayerID}返还积分{record.DeductedPoints}")
|
||
else:
|
||
logger.warning(f"打手积分异常({dashou.jifen}),不再返还")
|
||
except UserDashou.DoesNotExist:
|
||
logger.warning(f"打手{record.PlayerID}不存在,无法返还积分")
|
||
|
||
record.ApplyStatus = 2
|
||
record.ProcessorID = phone
|
||
record.RejectReason = reject_reason
|
||
record.save()
|
||
|
||
try:
|
||
dingdan = Order.query.get(OrderID=dingdan_id)
|
||
if hasattr(dingdan, 'shangjia_kuozhan'):
|
||
dingdan.shangjia_kuozhan.PenaltyApplyStatus = 2
|
||
dingdan.shangjia_kuozhan.RejectReason = reject_reason
|
||
dingdan.shangjia_kuozhan.save()
|
||
except Exception as e:
|
||
logger.warning(f"更新商家扩展表失败: {e}")
|
||
|
||
return Response({'code': 0, 'msg': '处理成功'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KefuForceCompleteView(APIView):
|
||
"""
|
||
客服强制结单接口(仅本地订单)
|
||
请求:POST /yonghu/kefuqiangzhijiedan
|
||
参数:{"phone": "客服手机号", "dingdan_id": "订单ID"}
|
||
认证:JWT,仅限状态正常的客服
|
||
逻辑:
|
||
1. 验证客服身份及状态
|
||
2. 查询订单并加锁(事务内)
|
||
3. 校验订单状态是否为8(结算中)
|
||
4. 校验订单必须有接单打手且打手分成金额有效
|
||
5. 给打手增加余额、成交统计、今日/本月收益
|
||
6. 如果是商家发单,更新商家成交订单数量
|
||
7. 更新订单状态为3(已完成)
|
||
8. 更新客服处理统计
|
||
9. 返回成功
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
phone = request.data.get('phone', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
|
||
if not phone or not dingdan_id:
|
||
return Response({'code': 401, 'msg': '参数缺失'})
|
||
|
||
# 1. 客服身份验证
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
return Response({'code': 401, 'msg': '认证失败'})
|
||
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
return Response({'code': 401, 'msg': '认证失败'})
|
||
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 401, 'msg': '认证失败'})
|
||
|
||
if kefu.zhuangtai != 1:
|
||
return Response({'code': 401, 'msg': '客服状态异常'})
|
||
|
||
# 2. 订单处理(事务内)
|
||
try:
|
||
with transaction.atomic():
|
||
order = Order.objects.select_for_update().get(OrderID=dingdan_id)
|
||
|
||
# 3. 校验订单状态
|
||
if order.Status != 8:
|
||
return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.Status}'})
|
||
|
||
# 4. 校验必要字段
|
||
dashou_id = order.PlayerID
|
||
dashou_fencheng = order.PlayerCommission
|
||
if not dashou_id:
|
||
return Response({'code': 400, 'msg': '订单无接单打手'})
|
||
if not dashou_fencheng or dashou_fencheng <= 0:
|
||
return Response({'code': 400, 'msg': '打手分成金额无效'})
|
||
|
||
# 5. 给打手结算
|
||
try:
|
||
dashou_user = User.query.get(UserUID=dashou_id)
|
||
dashou_profile = dashou_user.DashouProfile
|
||
# 原子更新(避免并发重复加)
|
||
dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1
|
||
dashou_profile.yue = F('yue') + dashou_fencheng
|
||
dashou_profile.zonge = F('zonge') + dashou_fencheng
|
||
dashou_profile.jinrishouyi = F('jinrishouyi') + dashou_fencheng
|
||
dashou_profile.jinyueshouyi = F('jinyueshouyi') + dashou_fencheng
|
||
dashou_profile.save(update_fields=[
|
||
'chengjiaozongliang', 'yue', 'zonge',
|
||
'jinrishouyi', 'jinyueshouyi'
|
||
])
|
||
except (User.DoesNotExist, AttributeError):
|
||
return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'})
|
||
|
||
# 6. 如果是商家发单,更新商家成交订单数量
|
||
if order.Platform == 2:
|
||
try:
|
||
# 通过 MerchantOrderExt 获取商家ID
|
||
shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan'
|
||
if shangjia_ext and shangjia_ext.shangjia_id:
|
||
shangjia_user = User.query.get(UserUID=shangjia_ext.shangjia_id)
|
||
shangjia_profile = shangjia_user.ShopProfile
|
||
shangjia_profile.chengjiao = F('chengjiao') + 1
|
||
shangjia_profile.save(update_fields=['chengjiao'])
|
||
|
||
# (行为驱动,更安全)
|
||
update_shangjia_daily(
|
||
yonghuid=shangjia_ext.shangjia_id,
|
||
amount=Decimal(str(order.Amount)),
|
||
action=2 # 2 = 结算
|
||
)
|
||
|
||
except ObjectDoesNotExist:
|
||
# 没有商家扩展记录则忽略
|
||
pass
|
||
|
||
# 7. 更新订单状态
|
||
order.Status = 3
|
||
order.AssignedCS = phone
|
||
order.save(update_fields=['zhuangtai', 'clkf'])
|
||
|
||
|
||
if order.Platform == 1:
|
||
|
||
# ========== 新增:商品和店铺每日统计 ==========
|
||
try:
|
||
# 商品每日统计(只要有商品ID就执行)
|
||
if order.ProductID:
|
||
# 从扩展表获取已计算好的收益值(下单时已存储)
|
||
pingtai_shouyi = Decimal('0.00')
|
||
dianpu_shouyi = Decimal('0.00')
|
||
if hasattr(order, 'pingtai_kuozhan'):
|
||
pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
|
||
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
|
||
|
||
update_shangpin_daily_stat(
|
||
shangpin_id=order.ProductID,
|
||
caozuo_leixing=2, # 2 = 结单
|
||
dingdan_jiage=order.Amount,
|
||
pingtai_shouyi=pingtai_shouyi,
|
||
dianpu_zongshouyi=dianpu_shouyi
|
||
)
|
||
logger.info(f"商品统计更新完成,商品ID: {order.ProductID}")
|
||
|
||
# 店铺每日统计(仅当存在店铺ID时执行)
|
||
dianpu_id = None
|
||
if hasattr(order, 'pingtai_kuozhan'):
|
||
dianpu_id = order.pingtai_kuozhan.ShopID
|
||
if dianpu_id:
|
||
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
|
||
update_dianpu_daily_stat(
|
||
dianpu_id=dianpu_id,
|
||
caozuo_leixing=2, # 2 = 结单
|
||
dingdan_jiage=order.Amount,
|
||
dianpu_shouyi=dianpu_shouyi
|
||
)
|
||
logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
|
||
except Exception as e:
|
||
logger.error(f"商品/店铺统计更新失败: {e}")
|
||
logger.error(traceback.format_exc())
|
||
# 统计失败不影响主流程
|
||
# =============================================
|
||
|
||
# 🔥 新增:调用打手每日统计(成交)
|
||
try:
|
||
update_dashou_daily_by_action(
|
||
yonghuid=order.PlayerID,
|
||
amount=dashou_fencheng,
|
||
action=2 # 2 表示成交(结算)
|
||
)
|
||
logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
|
||
except Exception as e:
|
||
logger.error(f"打手每日统计更新失败: {e}")
|
||
# 不影响主流程
|
||
|
||
# 商家订单管事分红
|
||
if order.Platform == 2:
|
||
settle_shangjia_order_guanshi_fenhong(order, dashou_id)
|
||
|
||
# 8. 更新客服统计
|
||
kefu.jinrichuli = F('jinrichuli') + 1
|
||
kefu.jinyuechuli = F('jinyuechuli') + 1
|
||
kefu.zongchuli = F('zongchuli') + 1
|
||
kefu.save(update_fields=['jinrichuli', 'jinyuechuli', 'zongchuli'])
|
||
|
||
return Response({'code': 0, 'msg': '强制结单成功'})
|
||
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
except Exception as e:
|
||
logger.error(f"强制结单异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
kefu_txsh_list_logger = logging.getLogger('yonghu.kefu_txsh_list')
|
||
if not kefu_txsh_list_logger.handlers:
|
||
_console_handler = logging.StreamHandler(sys.stderr)
|
||
_console_handler.setFormatter(logging.Formatter(
|
||
'[%(asctime)s] %(levelname)s %(name)s: %(message)s'
|
||
))
|
||
kefu_txsh_list_logger.addHandler(_console_handler)
|
||
kefu_txsh_list_logger.setLevel(logging.DEBUG)
|
||
kefu_txsh_list_logger.propagate = False
|
||
|
||
_TIXIAN_LIST_BASE_FIELDS = (
|
||
'id', 'yonghuid', 'avatar', 'phone', 'nicheng', 'leixing',
|
||
'zhifu', 'skzhanghao', 'jine', 'zhuangtai', 'fangshi',
|
||
'shenheid', 'bhliyou', 'CreateTime', 'UpdateTime',
|
||
)
|
||
_TIXIAN_LIST_AUDIT_FIELDS = ('shenhe_jilu_id', 'shenhe_danhao')
|
||
|
||
|
||
class KefuWithdrawListView(APIView):
|
||
"""
|
||
客服获取提现审核列表接口
|
||
请求:POST /yonghu/kefu_txsh_list
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"page": 1,
|
||
"page_size": 20,
|
||
"status": 1(待审核)/2(已处理),
|
||
"type": 1(打手佣金)/2(管事分红)/3(组长分红)/4(审核官分佣)/5(打手押金)/6(商家余额), 可选
|
||
"payment": 1(微信)/2(支付宝) 可选,
|
||
"search_uid": "用户ID搜索" 可选,
|
||
"date": "YYYY-MM-DD" 可选,
|
||
"min_amount": 0, # 最小提现金额(元)可选
|
||
"max_amount": 999999, # 最大提现金额(元)可选
|
||
"dakuan_mode": 1 # 打款方式:1手动打款 2自动打款,可选
|
||
}
|
||
认证:JWT
|
||
返回:{
|
||
"code": 0,
|
||
"data": {
|
||
"list": [提现记录],
|
||
"total": 总数,
|
||
"stats": {
|
||
"total": 总记录数,
|
||
"awaiting": 待审核数,
|
||
"processed": 已处理数,
|
||
"total_amount": "总金额(元)"
|
||
}
|
||
}
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def _safe_request_params(self, request):
|
||
try:
|
||
return dict(request.data)
|
||
except Exception:
|
||
return {'raw': str(request.data)}
|
||
|
||
def _shenhe_jilu_field_available(self):
|
||
"""探测 shenhe_jilu_id 字段是否已在数据库中就绪"""
|
||
try:
|
||
Tixianjilu.query.values_list('shenhe_jilu_id', flat=True).first()
|
||
return True
|
||
except DatabaseError as e:
|
||
kefu_txsh_list_logger.error(
|
||
'kefu_txsh_list: shenhe_jilu_id 字段不可用,将按旧表结构查询: %s', e,
|
||
exc_info=True,
|
||
)
|
||
return False
|
||
|
||
def _list_queryset(self, q, shenhe_field_ok):
|
||
"""字段未迁移时 only 旧字段,避免 SELECT 不存在的列导致 500"""
|
||
fields = _TIXIAN_LIST_BASE_FIELDS + (
|
||
_TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else ()
|
||
)
|
||
return Tixianjilu.query.filter(q).only(*fields)
|
||
|
||
def _resolve_row_dakuan_mode(self, item, audit_mode_map, shenhe_field_ok):
|
||
"""无关联审核或审核表未标记自动 → 默认手动(1)"""
|
||
if not shenhe_field_ok:
|
||
return 1
|
||
sid = getattr(item, 'shenhe_jilu_id', None)
|
||
if not sid:
|
||
return 1
|
||
if audit_mode_map.get(sid) == 2:
|
||
return 2
|
||
return 1
|
||
|
||
def post(self, request):
|
||
dakuan_mode_filter = request.data.get('dakuan_mode')
|
||
req_params = self._safe_request_params(request)
|
||
kefu_txsh_list_logger.info('kefu_txsh_list 请求开始 params=%s', req_params)
|
||
|
||
try:
|
||
try:
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 20))
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if page < 1:
|
||
page = 1
|
||
if page_size < 1 or page_size > 100:
|
||
page_size = min(max(page_size, 1), 100)
|
||
|
||
status_filter = request.data.get('status')
|
||
type_filter = request.data.get('type')
|
||
payment_filter = request.data.get('payment')
|
||
search_uid = (request.data.get('search_uid') or '').strip()
|
||
date_filter = (request.data.get('date') or '').strip()
|
||
min_amount = request.data.get('min_amount')
|
||
max_amount = request.data.get('max_amount')
|
||
|
||
phone = (request.data.get('phone') or '').strip()
|
||
if not phone:
|
||
return Response({'code': 401, 'msg': '缺少客服手机号'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
kefu, permissions = verify_kefu_permission(request, phone)
|
||
if kefu is None:
|
||
return permissions
|
||
if '005bb' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
|
||
|
||
q = Q()
|
||
|
||
try:
|
||
status_val = int(status_filter) if status_filter is not None and str(status_filter).strip() != '' else None
|
||
except (TypeError, ValueError):
|
||
status_val = None
|
||
|
||
try:
|
||
dakuan_mode_val = int(dakuan_mode_filter) if dakuan_mode_filter is not None and str(dakuan_mode_filter).strip() != '' else None
|
||
except (TypeError, ValueError):
|
||
dakuan_mode_val = None
|
||
|
||
# 未传打款方式参数 → 默认手动打款(1)
|
||
effective_dakuan = dakuan_mode_val if dakuan_mode_val in (1, 2) else 1
|
||
|
||
if status_val == 1:
|
||
q &= Q(zhuangtai=1)
|
||
elif status_val == 2:
|
||
if effective_dakuan == 2:
|
||
q &= Q(zhuangtai__in=[2, 3, 4, 5, 6])
|
||
else:
|
||
q &= Q(zhuangtai__in=[2, 3])
|
||
|
||
if type_filter is not None and str(type_filter).strip() != '':
|
||
try:
|
||
type_val = int(type_filter)
|
||
if type_val in [1, 2, 3, 4, 5, 6]:
|
||
q &= Q(leixing=type_val)
|
||
except (TypeError, ValueError):
|
||
pass
|
||
|
||
if payment_filter is not None and str(payment_filter).strip() != '':
|
||
try:
|
||
payment_val = int(payment_filter)
|
||
if payment_val in [1, 2]:
|
||
q &= Q(fangshi=payment_val)
|
||
except (TypeError, ValueError):
|
||
pass
|
||
|
||
if search_uid:
|
||
q &= Q(yonghuid=search_uid)
|
||
|
||
if date_filter:
|
||
q &= Q(UpdateTime__date=date_filter)
|
||
|
||
if min_amount is not None and str(min_amount).strip() != '':
|
||
try:
|
||
q &= Q(jine__gte=float(min_amount))
|
||
except (TypeError, ValueError):
|
||
pass
|
||
if max_amount is not None and str(max_amount).strip() != '':
|
||
try:
|
||
q &= Q(jine__lte=float(max_amount))
|
||
except (TypeError, ValueError):
|
||
pass
|
||
|
||
shenhe_field_ok = self._shenhe_jilu_field_available()
|
||
if shenhe_field_ok:
|
||
if effective_dakuan == 2:
|
||
q &= Q(shenhe_jilu_id__isnull=False)
|
||
else:
|
||
q &= Q(shenhe_jilu_id__isnull=True)
|
||
elif effective_dakuan == 2:
|
||
kefu_txsh_list_logger.warning('kefu_txsh_list: 自动打款筛选不可用,返回空列表')
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': [],
|
||
'total': 0,
|
||
'stats': {'total': 0, 'awaiting': 0, 'processed': 0, 'total_amount': '0.00'},
|
||
},
|
||
})
|
||
|
||
processed_statuses = [2, 3, 4, 5, 6] if effective_dakuan == 2 else [2, 3]
|
||
|
||
base_qs = self._list_queryset(q, shenhe_field_ok)
|
||
total_count = base_qs.count()
|
||
awaiting_count = self._list_queryset(q & Q(zhuangtai=1), shenhe_field_ok).count()
|
||
processed_count = self._list_queryset(
|
||
q & Q(zhuangtai__in=processed_statuses), shenhe_field_ok,
|
||
).count()
|
||
agg = base_qs.aggregate(total=Sum('jine'))
|
||
total_amount = agg.get('total') or 0
|
||
|
||
stats = {
|
||
'total': total_count,
|
||
'awaiting': awaiting_count,
|
||
'processed': processed_count,
|
||
'total_amount': str(round(float(total_amount), 2)),
|
||
}
|
||
|
||
queryset = base_qs.order_by('CreateTime')
|
||
total = queryset.count()
|
||
records = list(queryset[(page - 1) * page_size: page * page_size])
|
||
|
||
audit_ids = [
|
||
getattr(item, 'shenhe_jilu_id', None)
|
||
for item in records if getattr(item, 'shenhe_jilu_id', None)
|
||
]
|
||
audit_meta_map = {}
|
||
if audit_ids:
|
||
try:
|
||
audit_meta_map = load_audit_meta_map(audit_ids)
|
||
except Exception as e:
|
||
kefu_txsh_list_logger.error(
|
||
'kefu_txsh_list: 查询审核表信息失败: %s', e,
|
||
exc_info=True,
|
||
)
|
||
|
||
audit_mode_map = {
|
||
aid: meta.get('dakuan_mode', 2) for aid, meta in audit_meta_map.items()
|
||
}
|
||
list_data = []
|
||
for item in records:
|
||
shenhe_jilu_id = getattr(item, 'shenhe_jilu_id', None) if shenhe_field_ok else None
|
||
shenhe_danhao = (getattr(item, 'shenhe_danhao', None) or '') if shenhe_field_ok else ''
|
||
if shenhe_jilu_id and not shenhe_danhao:
|
||
shenhe_danhao = audit_meta_map.get(shenhe_jilu_id, {}).get('shenhe_danhao', '')
|
||
list_data.append({
|
||
'id': item.id,
|
||
'shenhe_jilu_id': shenhe_jilu_id,
|
||
'shenhe_danhao': shenhe_danhao,
|
||
'yonghuid': item.yonghuid,
|
||
'avatar': item.avatar or '',
|
||
'phone': item.phone or '',
|
||
'nicheng': item.nicheng or '',
|
||
'leixing': item.leixing,
|
||
'zhifu': item.zhifu or '',
|
||
'skzhanghao': item.skzhanghao or '',
|
||
'jine': str(item.jine) if item.jine is not None else '0.00',
|
||
'zhuangtai': item.zhuangtai,
|
||
'fangshi': item.fangshi,
|
||
'dakuan_mode': self._resolve_row_dakuan_mode(item, audit_mode_map, shenhe_field_ok),
|
||
'shenheid': item.shenheid or '',
|
||
'bhliyou': item.bhliyou or '',
|
||
'CreateTime': item.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if item.CreateTime else '',
|
||
'UpdateTime': item.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if item.UpdateTime else '',
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': list_data,
|
||
'total': total,
|
||
'stats': stats,
|
||
},
|
||
})
|
||
|
||
except DatabaseError as e:
|
||
kefu_txsh_list_logger.error(
|
||
'kefu_txsh_list 数据库错误 dakuan_mode=%s params=%s err=%s',
|
||
dakuan_mode_filter, req_params, e,
|
||
exc_info=True,
|
||
)
|
||
return Response({
|
||
'code': 500,
|
||
'msg': '数据库查询失败,请确认已执行 python manage.py migrate yonghu',
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
except Exception as e:
|
||
kefu_txsh_list_logger.error(
|
||
'kefu_txsh_list 接口异常 dakuan_mode=%s params=%s err=%s',
|
||
dakuan_mode_filter, req_params, e,
|
||
exc_info=True,
|
||
)
|
||
return Response({
|
||
'code': 500,
|
||
'msg': '获取提现列表失败',
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
|
||
|
||
class KefuWithdrawDetailView(APIView):
|
||
"""
|
||
客服获取提现详情接口
|
||
请求:POST /yonghu/kefu_txsh_detail
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"tixian_id": "提现记录ID"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 + 提现记录详情 + 用户扩展信息
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
phone = request.data.get('phone', '').strip()
|
||
tixian_id = request.data.get('tixian_id')
|
||
|
||
if not phone or not tixian_id:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 客服身份验证
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
logger.warning(f"手机号不匹配: {phone}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if kefu.zhuangtai != 1:
|
||
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 查询提现记录
|
||
try:
|
||
record = Tixianjilu.query.get(id=tixian_id)
|
||
except Tixianjilu.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 查询用户主表
|
||
try:
|
||
user = User.query.get(UserUID=record.yonghuid)
|
||
except User.DoesNotExist:
|
||
user = None
|
||
|
||
# 构建基础返回数据(字段必须与前端一致)
|
||
data = {
|
||
'id': record.id,
|
||
'yonghuid': record.yonghuid,
|
||
'avatar': record.avatar or '',
|
||
'phone': record.phone or '',
|
||
'nicheng': record.nicheng or '',
|
||
'leixing': record.leixing, # 1打手 2管事
|
||
'zhifu': record.zhifu or '',
|
||
'skzhanghao': record.skzhanghao or '',
|
||
'jine': str(record.jine) if record.jine else '0.00',
|
||
'zhuangtai': record.zhuangtai,
|
||
'fangshi': record.fangshi,
|
||
'shenheid': record.shenheid or '',
|
||
'bhliyou': record.bhliyou or '',
|
||
'CreateTime': record.CreateTime,
|
||
'UpdateTime': record.UpdateTime,
|
||
}
|
||
|
||
# 根据提现类型添加扩展信息
|
||
if user:
|
||
if record.leixing == 1: # 打手
|
||
try:
|
||
dashou = UserDashou.query.get(user=user)
|
||
# 计算成交率和退款率
|
||
total = dashou.jiedanzongliang or 0
|
||
completed = dashou.chengjiaozongliang or 0
|
||
refund = dashou.tuikuanliang or 0
|
||
completion_rate = round((completed / total * 100) if total > 0 else 0)
|
||
refund_rate = round((refund / completed * 100) if completed > 0 else 0)
|
||
|
||
data['dashouInfo'] = {
|
||
'zhuangtai': dashou.zhuangtai, # 工作状态
|
||
'zaixianzhuangtai': dashou.zaixianzhuangtai, # 在线状态
|
||
'zhanghaozhuangtai': dashou.zhanghaozhuangtai, # 账号状态
|
||
'yue': float(dashou.yue) if dashou.yue else 0.00, # 可提现余额
|
||
'zonge': float(dashou.zonge) if dashou.zonge else 0.00, # 接单总额
|
||
'jifen': dashou.jifen, # 积分
|
||
'jiedanzongliang': dashou.jiedanzongliang, # 接单总量
|
||
'chengjiaozongliang': dashou.chengjiaozongliang, # 成交总量
|
||
'tuikuanliang': dashou.tuikuanliang, # 退款总量
|
||
'completionRate': completion_rate,
|
||
'refundRate': refund_rate,
|
||
}
|
||
except ObjectDoesNotExist:
|
||
data['dashouInfo'] = None
|
||
elif record.leixing == 2: # 管事
|
||
try:
|
||
guanshi = UserGuanshi.query.get(user=user)
|
||
data['guanshiInfo'] = {
|
||
'zhuangtai': guanshi.zhuangtai, # 账号状态
|
||
'yue': float(guanshi.yue) if guanshi.yue else 0.00,
|
||
'yaogingshuliang': guanshi.yaogingshuliang, # 邀请打手总数
|
||
'jinrichongzhi': guanshi.jinrichongzhi, # 今日充值
|
||
'jinyuechongzhi': guanshi.jinyuechongzhi, # 今月充值
|
||
'chongzhifenrun': float(guanshi.chongzhifenrun) if guanshi.chongzhifenrun else 0.00,
|
||
}
|
||
except ObjectDoesNotExist:
|
||
data['guanshiInfo'] = None
|
||
|
||
return Response({'code': 0, 'data': data})
|
||
|
||
|
||
class KefuWithdrawActionView(APIView):
|
||
"""
|
||
客服处理提现接口(同意/拒绝)—— 仅此接口处理后台审核打款
|
||
POST /yonghu/kefu_txsh_action
|
||
|
||
单条:{ phone, action, tixian_id, yonghuid, leixing, reason? }
|
||
批量:{ phone, action, batch_list: [{tixian_id, yonghuid, leixing}, ...], reason? }
|
||
|
||
自动打款(dakuan_mode=2):同意→6待收款;拒绝→5+驳回原因+退还可到账金额(shijidaozhang),手续费不退,不更新平台收支/每日统计
|
||
手动打款(dakuan_mode=1):原逻辑不变
|
||
状态要求:Tixianjilu.zhuangtai=1 且 TixianShenheJilu.zhuangtai=1(审核中),其他状态一律拒绝
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def _verify_kefu(self, request, phone):
|
||
if not phone:
|
||
return None, Response({'code': 401, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST)
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
# 管理员跳过客服扩展表检查
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
if kefu.zhuangtai != 1:
|
||
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
return current_user, None
|
||
|
||
def _parse_items(self, request):
|
||
"""解析单条或 batch_list,返回 [{tixian_id, yonghuid, leixing}]"""
|
||
batch_list = request.data.get('batch_list')
|
||
if batch_list is not None:
|
||
if not isinstance(batch_list, list) or len(batch_list) == 0:
|
||
return None, Response({'code': 400, 'msg': 'batch_list 不能为空'}, status=status.HTTP_400_BAD_REQUEST)
|
||
items = []
|
||
for idx, row in enumerate(batch_list):
|
||
if not isinstance(row, dict):
|
||
return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 格式错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||
tid = row.get('tixian_id')
|
||
yid = (row.get('yonghuid') or '').strip()
|
||
lx = row.get('leixing')
|
||
if not tid or not yid or lx is None:
|
||
return None, Response(
|
||
{'code': 400, 'msg': f'batch_list[{idx}] 缺少 tixian_id / yonghuid / leixing'},
|
||
status=status.HTTP_400_BAD_REQUEST,
|
||
)
|
||
try:
|
||
items.append({
|
||
'tixian_id': int(tid),
|
||
'yonghuid': yid,
|
||
'leixing': int(lx),
|
||
})
|
||
except (TypeError, ValueError):
|
||
return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||
return items, None
|
||
|
||
tixian_id = request.data.get('tixian_id')
|
||
if not tixian_id:
|
||
return None, Response({'code': 400, 'msg': '提现记录ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
|
||
yonghuid = (request.data.get('yonghuid') or '').strip()
|
||
leixing = request.data.get('leixing')
|
||
try:
|
||
item = {'tixian_id': int(tixian_id), 'yonghuid': yonghuid, 'leixing': int(leixing) if leixing is not None else None}
|
||
except (TypeError, ValueError):
|
||
return None, Response({'code': 400, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||
return [item], None
|
||
|
||
def _process_auto_item(self, item, action, reason, kefu_user):
|
||
"""自动打款单条处理(须在 transaction.atomic 内调用)"""
|
||
tixian_id = item['tixian_id']
|
||
req_yonghuid = item['yonghuid']
|
||
req_leixing = item['leixing']
|
||
|
||
if req_leixing not in [1, 2, 3, 4, 5, 6]:
|
||
raise ValueError(f'提现记录{tixian_id}:提现类型无效')
|
||
|
||
try:
|
||
tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
|
||
except Tixianjilu.DoesNotExist:
|
||
raise ValueError(f'提现记录{tixian_id}不存在')
|
||
|
||
if tixian.yonghuid != req_yonghuid:
|
||
raise ValueError(f'提现记录{tixian_id}:用户ID不匹配')
|
||
if tixian.leixing != req_leixing:
|
||
raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
|
||
if tixian.zhuangtai != 1:
|
||
raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
|
||
if not tixian.shenhe_jilu_id:
|
||
raise ValueError(f'提现记录{tixian_id}:非自动打款审核记录')
|
||
|
||
try:
|
||
audit = TixianShenheJilu.objects.select_for_update().get(id=tixian.shenhe_jilu_id)
|
||
except TixianShenheJilu.DoesNotExist:
|
||
raise ValueError(f'提现记录{tixian_id}:审核记录不存在')
|
||
|
||
if audit.yonghuid != req_yonghuid:
|
||
raise ValueError(f'提现记录{tixian_id}:审核记录用户ID不匹配')
|
||
if audit.leixing != req_leixing:
|
||
raise ValueError(f'提现记录{tixian_id}:审核记录提现类型不匹配')
|
||
if audit.zhuangtai != 1:
|
||
raise ValueError(f'提现记录{tixian_id}:审核记录非审核中状态')
|
||
|
||
try:
|
||
user = User.objects.select_for_update().get(UserUID=req_yonghuid)
|
||
except User.DoesNotExist:
|
||
raise ValueError(f'用户{req_yonghuid}不存在')
|
||
|
||
if action == 1:
|
||
limit_ok, limit_msg = check_shijidaozhang_within_limit(audit.shijidaozhang)
|
||
if not limit_ok:
|
||
sync_audit_and_jilu_status(
|
||
audit, 5,
|
||
bhliyou=limit_msg,
|
||
bo_hui_ren_id=kefu_user.UserUID,
|
||
)
|
||
refund_balance(user, audit.leixing, audit.shijidaozhang)
|
||
tixian.shenheid = kefu_user.UserUID
|
||
tixian.bhliyou = limit_msg
|
||
tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
|
||
raise ValueError(f'提现记录{tixian_id}:{limit_msg},已自动驳回并退款')
|
||
sync_audit_and_jilu_status(audit, 6, shenhe_ren_id=kefu_user.UserUID)
|
||
tixian.shenheid = kefu_user.UserUID
|
||
tixian.save(update_fields=['shenheid', 'UpdateTime'])
|
||
else:
|
||
sync_audit_and_jilu_status(
|
||
audit, 5,
|
||
bhliyou=reason,
|
||
bo_hui_ren_id=kefu_user.UserUID,
|
||
)
|
||
# 退还可到账金额,申请时扣的 shenqing_jine 中的手续费(shouxufei)不退
|
||
refund_balance(user, audit.leixing, audit.shijidaozhang)
|
||
tixian.shenheid = kefu_user.UserUID
|
||
tixian.bhliyou = reason
|
||
tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
|
||
|
||
def _process_manual_item(self, item, action, reason, phone):
|
||
"""手动打款单条处理(原逻辑,须在 transaction.atomic 内调用)"""
|
||
tixian_id = item['tixian_id']
|
||
try:
|
||
tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
|
||
except Tixianjilu.DoesNotExist:
|
||
raise ValueError(f'提现记录{tixian_id}不存在')
|
||
|
||
req_yonghuid = item.get('yonghuid')
|
||
req_leixing = item.get('leixing')
|
||
if req_yonghuid and tixian.yonghuid != req_yonghuid:
|
||
raise ValueError(f'提现记录{tixian_id}:用户ID不匹配')
|
||
if req_leixing is not None and tixian.leixing != req_leixing:
|
||
raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
|
||
if tixian.zhuangtai != 1:
|
||
raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
|
||
|
||
if getattr(tixian, 'shenhe_jilu_id', None):
|
||
raise ValueError(f'提现记录{tixian_id}:自动打款请走自动流程')
|
||
|
||
try:
|
||
user = User.query.get(UserUID=tixian.yonghuid)
|
||
except User.DoesNotExist:
|
||
raise ValueError(f'用户{tixian.yonghuid}不存在')
|
||
|
||
if action == 1:
|
||
tixian.zhuangtai = 2
|
||
try:
|
||
update_daily_payout(tixian.jine)
|
||
szjilu, _ = Szjilu.query.get_or_create(id=1)
|
||
szjilu.TotalIncome -= tixian.jine
|
||
szjilu.TotalExpense += tixian.jine
|
||
szjilu.DailyExpense += tixian.jine
|
||
szjilu.UpdateTime = timezone.now()
|
||
szjilu.save()
|
||
except Exception as e:
|
||
logger.error(f'更新收支记录失败: {e}')
|
||
else:
|
||
try:
|
||
refund_balance(user, tixian.leixing, tixian.jine)
|
||
except ValueError as e:
|
||
raise ValueError(str(e))
|
||
tixian.zhuangtai = 3
|
||
tixian.bhliyou = reason
|
||
|
||
tixian.shenheid = phone
|
||
tixian.UpdateTime = timezone.now()
|
||
tixian.save()
|
||
|
||
def post(self, request):
|
||
logger.info(f'接收到提现处理请求,数据: {request.data}')
|
||
|
||
phone = request.data.get('phone', '').strip()
|
||
action_raw = request.data.get('action')
|
||
reason = request.data.get('reason', '').strip()
|
||
|
||
try:
|
||
action = int(action_raw)
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': 'action 参数必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
|
||
if action not in [1, 2]:
|
||
return Response({'code': 400, 'msg': 'action 必须为1(同意)或2(拒绝)'}, status=status.HTTP_400_BAD_REQUEST)
|
||
if action == 2 and not reason:
|
||
return Response({'code': 400, 'msg': '拒绝提现时理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
kefu_user, err_resp = self._verify_kefu(request, phone)
|
||
if err_resp:
|
||
return err_resp
|
||
|
||
items, err_resp = self._parse_items(request)
|
||
if err_resp:
|
||
return err_resp
|
||
|
||
is_batch = isinstance(request.data.get('batch_list'), list) and len(request.data.get('batch_list')) > 0
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
for item in items:
|
||
tixian_id = item['tixian_id']
|
||
try:
|
||
tixian_probe = Tixianjilu.objects.select_for_update().get(id=tixian_id)
|
||
except Tixianjilu.DoesNotExist:
|
||
raise ValueError(f'提现记录{tixian_id}不存在')
|
||
|
||
# 有关联审核记录(shenhe_jilu_id)即为 zddksh 自动打款,不依赖 dakuan_mode 字段
|
||
is_auto = bool(getattr(tixian_probe, 'shenhe_jilu_id', None))
|
||
|
||
if is_auto:
|
||
self._process_auto_item(item, action, reason, kefu_user)
|
||
else:
|
||
if is_batch:
|
||
raise ValueError(f'提现记录{tixian_id}:批量操作仅支持自动打款记录')
|
||
self._process_manual_item(item, action, reason, phone)
|
||
|
||
except ValueError as e:
|
||
return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||
except Exception as e:
|
||
logger.error(f'处理提现事务失败: {e}', exc_info=True)
|
||
return Response({'code': 500, 'msg': f'处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
msg = '批量处理成功' if is_batch and len(items) > 1 else '处理成功'
|
||
return Response({'code': 0, 'msg': msg, 'data': {'count': len(items)}})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# dingdan/views.py
|
||
class KefuPunishView(APIView):
|
||
"""
|
||
客服处罚打手接口
|
||
请求:POST /yonghu/kefuchufa
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"dingdan_id": "订单ID",
|
||
"reason": "处罚原因(可选)"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 成功
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
phone = request.data.get('phone', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
reason = request.data.get('reason', '').strip()
|
||
|
||
if not phone or not dingdan_id:
|
||
return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
if getattr(current_user, 'Phone', '') != phone:
|
||
logger.warning(f"手机号不匹配: {phone}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if current_user.UserType not in ('kefu', 'admin'):
|
||
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||
kefu = None
|
||
if not is_admin:
|
||
try:
|
||
kefu = current_user.KefuProfile
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if kefu.zhuangtai != 1:
|
||
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 3. 查询订单
|
||
try:
|
||
order = Order.query.get(OrderID=dingdan_id)
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 4. 获取打手ID
|
||
dashou_id = order.PlayerID
|
||
if not dashou_id:
|
||
return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 5. 查询打手
|
||
try:
|
||
dashou_user = User.query.get(UserUID=dashou_id)
|
||
dashou = dashou_user.DashouProfile
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 400, 'msg': '该用户不是打手'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 6. 使用事务
|
||
with transaction.atomic():
|
||
# 创建处罚记录(积分扣除5分)
|
||
jifen = 5
|
||
chufa = PenaltyRecord.query.create(
|
||
PlayerID=dashou_id,
|
||
ApplicantID=phone, # 客服手机号作为请求人
|
||
PenaltyReason=reason or '',
|
||
ApplyStatus=0, # 待处理
|
||
DeductedPoints=jifen,
|
||
OrderID=dingdan_id,
|
||
)
|
||
|
||
# 扣除打手积分
|
||
if dashou.jifen >= jifen:
|
||
dashou.jifen -= jifen
|
||
else:
|
||
dashou.jifen = 0
|
||
dashou.save()
|
||
|
||
# 可选:更新订单的 clkf 字段记录处理客服
|
||
order.AssignedCS = phone
|
||
order.save()
|
||
|
||
return Response({'code': 0, 'msg': '处罚成功'})
|
||
|
||
|
||
|
||
class AdKftjView(APIView):
|
||
"""
|
||
添加客服接口
|
||
权限:管理员JWT
|
||
请求:POST /yonghu/adkftj
|
||
参数:{
|
||
"zhanghao": "管理员账号",
|
||
"phone": "客服手机号(必填)",
|
||
"password": "密码(必填)",
|
||
"erjimima": "二级密码(必填)",
|
||
"nicheng": "昵称(可选)"
|
||
}
|
||
返回:code=0
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 参数
|
||
zhanghao = request.data.get('zhanghao', '').strip()
|
||
phone = request.data.get('phone', '').strip()
|
||
password = request.data.get('password', '').strip()
|
||
erjimima = request.data.get('erjimima', '').strip()
|
||
nicheng = request.data.get('nicheng', '').strip()
|
||
|
||
if not all([zhanghao, phone, password, erjimima]):
|
||
return Response({'code': 400, 'message': '手机号、密码、二级密码不能为空'})
|
||
|
||
# 2. 管理员验证(同前)
|
||
current_user = request.user
|
||
if current_user.Phone != zhanghao or current_user.UserType != 'admin':
|
||
return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
try:
|
||
admin_profile = current_user.AdminProfile
|
||
except:
|
||
return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 3. 检查手机号是否已被使用
|
||
if User.query.filter(Phone=phone).exists():
|
||
return Response({'code': 400, 'message': '该手机号已被注册'})
|
||
|
||
# 4. 生成唯一 yonghuid
|
||
def generate_yonghuid():
|
||
for _ in range(10):
|
||
timestamp = str(int(time.time()))[-5:]
|
||
rand = str(random.randint(0, 99)).zfill(2)
|
||
uid = timestamp + rand
|
||
if len(uid) == 7 and not User.query.filter(UserUID=uid).exists():
|
||
return uid
|
||
raise Exception('无法生成唯一用户ID')
|
||
|
||
with transaction.atomic():
|
||
# 创建主表用户
|
||
user = User.query.create(
|
||
UserUID=generate_yonghuid(),
|
||
UserName=f'kefu_{phone}',
|
||
Phone=phone,
|
||
OpenID=f'kefu_{phone}' # 临时openid
|
||
)
|
||
user.SetPassword(password)
|
||
user.save(update_fields=['UserPassword'])
|
||
# 创建客服扩展表
|
||
import bcrypt as _bcrypt
|
||
erjimima_hashed = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
|
||
kefu = UserKefu.query.create(
|
||
user=user,
|
||
nicheng=nicheng or f'客服{user.UserUID}',
|
||
erjimima=erjimima_hashed,
|
||
zhuangtai=1,
|
||
jinrichuli=0,
|
||
jinyuechuli=0,
|
||
zongchuli=0
|
||
)
|
||
|
||
return Response({'code': 0, 'message': '添加成功'})
|
||
|
||
|
||
|
||
class AdKfglView(APIView):
|
||
"""
|
||
客服管理列表接口
|
||
权限:管理员JWT
|
||
请求:POST /yonghu/adkfgl
|
||
参数:{
|
||
"zhanghao": "管理员账号",
|
||
"keyword": "搜索关键词(可选,匹配客服ID或昵称)"
|
||
}
|
||
返回:{
|
||
"code": 0,
|
||
"data": {
|
||
"list": [客服对象...]
|
||
}
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 参数获取
|
||
zhanghao = request.data.get('zhanghao', '').strip()
|
||
keyword = request.data.get('keyword', '').strip()
|
||
|
||
if not zhanghao:
|
||
return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 2. 管理员身份验证
|
||
current_user = request.user
|
||
if not hasattr(current_user, 'Phone') or str(current_user.Phone) != zhanghao:
|
||
return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
if current_user.UserType != 'admin':
|
||
return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
try:
|
||
admin_profile = current_user.AdminProfile
|
||
except AttributeError:
|
||
return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 3. 查询所有客服(通过扩展表关联主表)
|
||
queryset = UserKefu.query.select_related('user').all()
|
||
|
||
if keyword:
|
||
queryset = queryset.filter(
|
||
#Q(user__UserUID__icontains=keyword) |
|
||
Q(nicheng__icontains=keyword) |
|
||
Q(user__Phone__icontains=keyword)
|
||
)
|
||
|
||
# 4. 构建返回数据
|
||
kefu_list = []
|
||
for kefu in queryset:
|
||
user = kefu.user
|
||
kefu_list.append({
|
||
'yonghuid': user.Phone,
|
||
'phone': user.Phone or '',
|
||
'nicheng': kefu.nicheng or '',
|
||
'zhuangtai': kefu.zhuangtai,
|
||
'jinrichuli': kefu.jinrichuli,
|
||
'jinyuechuli': kefu.jinyuechuli,
|
||
'zongchuli': kefu.zongchuli,
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {'list': kefu_list}
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
class AdKfxgView(APIView):
|
||
"""
|
||
修改客服信息接口
|
||
权限:管理员JWT
|
||
请求:POST /yonghu/adkfxg
|
||
参数:{
|
||
"zhanghao": "管理员账号",
|
||
"uid": "客服ID (yonghuid)",
|
||
"nicheng": "新昵称(可选)",
|
||
"phone": "新手机号(可选)",
|
||
"password": "新密码(可选)",
|
||
"erjimima": "新二级密码(可选)",
|
||
"zhuangtai": 0/1(可选)
|
||
}
|
||
返回:code=0
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
zhanghao = request.data.get('zhanghao', '').strip()
|
||
uid = request.data.get('uid', '').strip()
|
||
|
||
if not zhanghao or not uid:
|
||
return Response({'code': 400, 'message': '参数不完整'})
|
||
|
||
# 管理员验证
|
||
current_user = request.user
|
||
if current_user.Phone != zhanghao or current_user.UserType != 'admin':
|
||
return Response({'code': 401, 'message': '身份验证失败'})
|
||
try:
|
||
admin_profile = current_user.AdminProfile
|
||
except:
|
||
return Response({'code': 401, 'message': '身份验证失败'})
|
||
|
||
# 查询客服
|
||
try:
|
||
user = User.query.get(Phone=uid)
|
||
kefu = user.KefuProfile
|
||
except (User.DoesNotExist, UserKefu.DoesNotExist):
|
||
return Response({'code': 404, 'message': '客服不存在'})
|
||
|
||
# 更新数据
|
||
with transaction.atomic():
|
||
# 更新主表字段
|
||
if 'phone' in request.data:
|
||
new_phone = request.data.get('phone').strip()
|
||
if new_phone and User.query.filter(Phone=new_phone).exclude(UserUID=uid).exists():
|
||
return Response({'code': 400, 'message': '手机号已被使用'})
|
||
user.Phone = new_phone
|
||
if 'password' in request.data:
|
||
user.UserPassword = request.data['password']
|
||
user.save()
|
||
|
||
# 更新扩展表
|
||
if 'nicheng' in request.data:
|
||
kefu.nicheng = request.data['nicheng']
|
||
if 'erjimima' in request.data:
|
||
import bcrypt as _bcrypt
|
||
new_erji = request.data['erjimima']
|
||
kefu.erjimima = _bcrypt.hashpw(new_erji.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
|
||
if 'zhuangtai' in request.data:
|
||
kefu.zhuangtai = int(request.data['zhuangtai'])
|
||
kefu.save()
|
||
|
||
return Response({'code': 0, 'message': '修改成功'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class WechatLoginAndGuanshiRegisterView(APIView):
|
||
"""
|
||
未登录用户微信登录+管事注册一体化接口
|
||
路径: /api/yonghu/zuzhangyqmzc
|
||
方法: POST
|
||
权限: 允许所有
|
||
请求体: { "code": "微信登录code", "inviteCode": "组长邀请码" }
|
||
成功返回 code: 200,数据同 GuanshiRegisterView
|
||
"""
|
||
permission_classes = [AllowAny]
|
||
|
||
def post(self, request):
|
||
code = request.data.get('code', '').strip()
|
||
yaoqingma = request.data.get('inviteCode', '').strip()
|
||
|
||
if not code:
|
||
return Response({'code': 400, 'msg': '微信授权码不能为空', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if not yaoqingma:
|
||
return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if len(yaoqingma) > 100:
|
||
return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
wechat_data = self._get_wechat_openid(code)
|
||
if not wechat_data or 'openid' not in wechat_data:
|
||
error_msg = wechat_data.get('errmsg', '微信授权失败')
|
||
return Response({'code': 400, 'msg': f'微信登录失败: {error_msg}', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
openid = wechat_data['openid']
|
||
kehuduan_ip = self._huoqu_kehuduan_ip(request)
|
||
|
||
with transaction.atomic():
|
||
user_main, created = User.objects.select_for_update().get_or_create(
|
||
OpenID=openid,
|
||
defaults={
|
||
'UserUID': self._shengcheng_yonghu_id(),
|
||
'UserName': openid,
|
||
}
|
||
)
|
||
user_main.IP = kehuduan_ip
|
||
user_main.UserLastLoginDate = timezone.now()
|
||
user_main.save()
|
||
|
||
if created:
|
||
UserBoss.query.create(user=user_main, nickname='微信用户')
|
||
|
||
if hasattr(user_main, 'GuanshiProfile'):
|
||
return self._fanhui_xianyou_guanshi_info(user_main)
|
||
|
||
try:
|
||
zuzhang = UserZuzhang.query.select_related('user').get(yaoqingma=yaoqingma)
|
||
except UserZuzhang.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None},
|
||
status=status.HTTP_404_NOT_FOUND)
|
||
|
||
if zuzhang.zhuangtai != 1:
|
||
return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None},
|
||
status=status.HTTP_403_FORBIDDEN)
|
||
|
||
zuzhang_yonghuid = zuzhang.user.UserUID
|
||
|
||
guanshi_yaoqingma = CreateInvitationCode(str(user_main.UserUID))
|
||
guanshi_profile = UserGuanshi.query.create(
|
||
user=user_main,
|
||
yaoqingma=guanshi_yaoqingma,
|
||
yaoqingren=zuzhang_yonghuid,
|
||
)
|
||
|
||
UserZuzhang.query.filter(pk=zuzhang.pk).update(
|
||
yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
|
||
)
|
||
|
||
# ========== 新增:组长每日统计(邀请管事 action=1) ==========
|
||
try:
|
||
update_zuzhang_daily_by_action(
|
||
yonghuid=zuzhang_yonghuid,
|
||
action=1 # 1 = 邀请管事
|
||
)
|
||
logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}")
|
||
except Exception as e:
|
||
logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}")
|
||
|
||
|
||
if not hasattr(user_main, 'DashouProfile'):
|
||
dashou_profile = UserDashou.query.create(
|
||
user=user_main,
|
||
nicheng='大神',
|
||
chenghao='普通大神',
|
||
yaoqingren=zuzhang_yonghuid,
|
||
)
|
||
else:
|
||
dashou_profile = user_main.DashouProfile
|
||
|
||
refresh = RefreshToken.for_user(user_main)
|
||
token = str(refresh.access_token)
|
||
|
||
response_data = self._zhuangbei_fanhui_shuju(
|
||
user_main, dashou_profile, guanshi_profile, token
|
||
)
|
||
|
||
return Response({'code': 0, 'msg': '登录并注册成功!', 'data': response_data})
|
||
|
||
# ---------- 辅助方法(与接口1完全相同,完整列出以保证完整性) ----------
|
||
|
||
def _get_wechat_openid(self, code):
|
||
try:
|
||
appid = getattr(settings, 'WEIXIN_APPID', '')
|
||
secret = getattr(settings, 'WEIXIN_SECRET', '')
|
||
if not appid or not secret:
|
||
raise ValueError('微信配置未设置')
|
||
url = 'https://api.weixin.qq.com/sns/jscode2session'
|
||
params = {
|
||
'appid': appid,
|
||
'secret': secret,
|
||
'js_code': code,
|
||
'grant_type': 'authorization_code'
|
||
}
|
||
response = requests.get(url, params=params, timeout=10)
|
||
result = response.json()
|
||
if 'openid' in result:
|
||
return result
|
||
else:
|
||
errcode = result.get('errcode', 'unknown')
|
||
errmsg = result.get('errmsg', '未知错误')
|
||
return {'errmsg': f'[{errcode}]{errmsg}'}
|
||
except requests.exceptions.Timeout:
|
||
logger.error('请求微信接口超时')
|
||
return {'errmsg': '请求微信接口超时'}
|
||
except Exception as e:
|
||
logger.error(f'请求微信接口异常: {e}')
|
||
return {'errmsg': f'请求微信接口异常: {str(e)}'}
|
||
|
||
def _shengcheng_yonghu_id(self):
|
||
for _ in range(10):
|
||
timestamp_part = str(int(time.time()))[-5:].zfill(5)
|
||
random_part = str(random.randint(0, 99)).zfill(2)
|
||
user_id = timestamp_part + random_part
|
||
if len(user_id) == 7 and user_id.isdigit():
|
||
if not User.query.filter(UserUID=user_id).exists():
|
||
return user_id
|
||
raise Exception('生成用户ID失败')
|
||
|
||
def _huoqu_kehuduan_ip(self, request):
|
||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||
if x_forwarded_for:
|
||
ip = x_forwarded_for.split(',')[0].strip()
|
||
if ip:
|
||
return ip
|
||
ip = request.META.get('REMOTE_ADDR', '')
|
||
return ip if ip else '0.0.0.0'
|
||
|
||
def _huoqu_qun_peizhi(self):
|
||
try:
|
||
qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
|
||
result = {
|
||
'dashouqun': '',
|
||
'dashouqunid': '',
|
||
'guanshiqun': '',
|
||
'guanshiqunid': ''
|
||
}
|
||
for config in qun_configs:
|
||
if config.id == 1:
|
||
result['dashouqun'] = config.GroupContent or ''
|
||
result['dashouqunid'] = config.GroupID or ''
|
||
elif config.id == 2:
|
||
result['guanshiqun'] = config.GroupContent or ''
|
||
result['guanshiqunid'] = config.GroupID or ''
|
||
return result
|
||
except Exception as e:
|
||
logger.error(f"获取群配置失败: {e}")
|
||
return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''}
|
||
|
||
def _zhuangbei_dashou_fanhui(self, dashou_profile):
|
||
data = {
|
||
'dashounicheng': dashou_profile.nicheng,
|
||
'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
|
||
'yongjin': str(dashou_profile.yue),
|
||
'zonge': str(dashou_profile.zonge),
|
||
'yajin': str(dashou_profile.yajin),
|
||
'chenghao': dashou_profile.chenghao,
|
||
'jinfen': dashou_profile.jifen,
|
||
'chengjiaoliang': dashou_profile.chengjiaozongliang,
|
||
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
|
||
'dashouzhuangtai': dashou_profile.zhuangtai,
|
||
}
|
||
huiyuan_list = []
|
||
try:
|
||
records = Huiyuangoumai.query.filter(yonghu_id=dashou_profile.user.UserUID)
|
||
for record in records:
|
||
shifou_daoqi = record.jiance_shifou_daoqi()
|
||
huiyuan_list.append({
|
||
'huiyuan_id': record.huiyuan_id,
|
||
'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
|
||
'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
|
||
'shifou_daoqi': shifou_daoqi,
|
||
'jieshao': record.jieshao
|
||
})
|
||
except Exception as e:
|
||
logger.warning(f"获取会员列表失败: {e}")
|
||
data['clumber'] = huiyuan_list
|
||
return data
|
||
|
||
def _fanhui_xianyou_guanshi_info(self, user_main):
|
||
guanshi_profile = user_main.GuanshiProfile
|
||
dashou_profile = getattr(user_main, 'DashouProfile', None)
|
||
group_info = self._huoqu_qun_peizhi()
|
||
dashou_status = 1 if dashou_profile else 0
|
||
shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
|
||
guanshi_status = 1
|
||
try:
|
||
nicheng = user_main.BossProfile.nickname or '微信用户'
|
||
except:
|
||
nicheng = '微信用户'
|
||
refresh = RefreshToken.for_user(user_main)
|
||
token = str(refresh.access_token)
|
||
data = {
|
||
'token': token,
|
||
'nicheng': nicheng,
|
||
'uid': user_main.UserUID,
|
||
'touxiang': user_main.Avatar or '',
|
||
'shangjiastatus': shangjia_status,
|
||
'dashoustatus': dashou_status,
|
||
'guanshistatus': guanshi_status,
|
||
**group_info,
|
||
}
|
||
guanshi_data = {
|
||
'gszhstatus': guanshi_profile.zhuangtai,
|
||
'yaoqingzongshu': guanshi_profile.yaogingshuliang,
|
||
'fenyongzonge': str(guanshi_profile.chongzhifenrun),
|
||
'fenyongtixian': str(guanshi_profile.yue),
|
||
'yichongzhiDashou': guanshi_profile.jinrichongzhi,
|
||
}
|
||
data.update(guanshi_data)
|
||
if dashou_profile:
|
||
dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
|
||
data.update(dashou_data)
|
||
else:
|
||
data.update({
|
||
'dashounicheng': '',
|
||
'zhanghaostatus': 0,
|
||
'yongjin': '0.00',
|
||
'zonge': '0.00',
|
||
'yajin': '0.00',
|
||
'chenghao': '',
|
||
'jinfen': 0,
|
||
'chengjiaoliang': 0,
|
||
'zaixianzhuangtai': 0,
|
||
'dashouzhuangtai': 0,
|
||
'clumber': []
|
||
})
|
||
return Response({'code': 0, 'msg': '您已是管事', 'data': data})
|
||
|
||
def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token):
|
||
try:
|
||
nicheng = user_main.BossProfile.nickname or '微信用户'
|
||
except:
|
||
nicheng = '微信用户'
|
||
dashou_status = 1
|
||
shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
|
||
guanshi_status = 1
|
||
group_info = self._huoqu_qun_peizhi()
|
||
data = {
|
||
'token': token,
|
||
'nicheng': nicheng,
|
||
'uid': user_main.UserUID,
|
||
'touxiang': user_main.Avatar or '',
|
||
'shangjiastatus': shangjia_status,
|
||
'dashoustatus': dashou_status,
|
||
'guanshistatus': guanshi_status,
|
||
**group_info,
|
||
}
|
||
guanshi_data = {
|
||
'gszhstatus': guanshi_profile.zhuangtai,
|
||
'yaoqingzongshu': guanshi_profile.yaogingshuliang,
|
||
'fenyongzonge': str(guanshi_profile.chongzhifenrun),
|
||
'fenyongtixian': str(guanshi_profile.yue),
|
||
'yichongzhiDashou': guanshi_profile.jinrichongzhi,
|
||
}
|
||
data.update(guanshi_data)
|
||
dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
|
||
data.update(dashou_data)
|
||
return data
|
||
|
||
|
||
|
||
|
||
class GuanshiRegisterView(APIView):
|
||
"""
|
||
已登录用户注册管事接口
|
||
路径: /api/yonghu/guanshizhuce
|
||
方法: POST
|
||
权限: JWT认证
|
||
请求体: { "inviteCode": "组长邀请码" }
|
||
成功返回 code: 200
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 获取并验证邀请码
|
||
yaoqingma = request.data.get('inviteCode', '').strip()
|
||
if not yaoqingma:
|
||
return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if len(yaoqingma) > 100:
|
||
return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
current_user = request.user
|
||
|
||
# 2. 检查用户是否已是管事
|
||
if hasattr(current_user, 'GuanshiProfile'):
|
||
# 用户已是管事,直接返回现有信息
|
||
return self._fanhui_xianyou_guanshi_info(current_user)
|
||
|
||
# 3. 验证邀请码对应的组长
|
||
try:
|
||
zuzhang = UserZuzhang.query.select_related('user').get(yaoqingma=yaoqingma)
|
||
except UserZuzhang.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None},
|
||
status=status.HTTP_404_NOT_FOUND)
|
||
|
||
if zuzhang.zhuangtai != 1:
|
||
return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None},
|
||
status=status.HTTP_403_FORBIDDEN)
|
||
|
||
zuzhang_yonghuid = zuzhang.user.UserUID
|
||
|
||
# 4. 事务内创建管事实体和打手(如果需要)
|
||
with transaction.atomic():
|
||
# 创建管事扩展表
|
||
guanshi_yaoqingma = CreateInvitationCode(str(current_user.UserUID))
|
||
guanshi_profile = UserGuanshi.query.create(
|
||
user=current_user,
|
||
yaoqingma=guanshi_yaoqingma,
|
||
yaoqingren=zuzhang_yonghuid,
|
||
)
|
||
|
||
# 更新组长的邀请总数
|
||
UserZuzhang.query.filter(pk=zuzhang.pk).update(
|
||
yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
|
||
)
|
||
|
||
# ========== 新增:组长每日统计(邀请管事 action=1) ==========
|
||
try:
|
||
update_zuzhang_daily_by_action(
|
||
yonghuid=zuzhang_yonghuid,
|
||
action=1 # 1 = 邀请管事
|
||
)
|
||
logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}")
|
||
except Exception as e:
|
||
logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}")
|
||
|
||
# 检查用户是否已是打手,如果不是则创建
|
||
if not hasattr(current_user, 'DashouProfile'):
|
||
dashou_profile = UserDashou.query.create(
|
||
user=current_user,
|
||
nicheng='大手子',
|
||
chenghao='普通大手',
|
||
yaoqingren=zuzhang_yonghuid,
|
||
)
|
||
else:
|
||
dashou_profile = current_user.DashouProfile
|
||
|
||
# 5. 生成新token
|
||
refresh = RefreshToken.for_user(current_user)
|
||
token = str(refresh.access_token)
|
||
|
||
# 6. 准备返回数据(包含所有字段)
|
||
response_data = self._zhuangbei_fanhui_shuju(
|
||
current_user, dashou_profile, guanshi_profile, token
|
||
)
|
||
|
||
return Response({'code': 200, 'msg': '注册成功!', 'data': response_data})
|
||
|
||
# ---------- 辅助方法 ----------
|
||
|
||
def _huoqu_qun_peizhi(self):
|
||
"""获取群配置信息"""
|
||
try:
|
||
qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
|
||
result = {
|
||
'dashouqun': '',
|
||
'dashouqunid': '',
|
||
'guanshiqun': '',
|
||
'guanshiqunid': ''
|
||
}
|
||
for config in qun_configs:
|
||
if config.id == 1:
|
||
result['dashouqun'] = config.GroupContent or ''
|
||
result['dashouqunid'] = config.GroupID or ''
|
||
elif config.id == 2:
|
||
result['guanshiqun'] = config.GroupContent or ''
|
||
result['guanshiqunid'] = config.GroupID or ''
|
||
return result
|
||
except Exception as e:
|
||
logger.error(f"获取群配置失败: {e}")
|
||
return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''}
|
||
|
||
def _zhuangbei_dashou_fanhui(self, dashou_profile):
|
||
"""组装打手信息(字段名与前端完全匹配)"""
|
||
data = {
|
||
'dashounicheng': dashou_profile.nicheng,
|
||
'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
|
||
'yongjin': str(dashou_profile.yue),
|
||
'zonge': str(dashou_profile.zonge),
|
||
'yajin': str(dashou_profile.yajin),
|
||
'chenghao': dashou_profile.chenghao,
|
||
'jinfen': dashou_profile.jifen,
|
||
'chengjiaoliang': dashou_profile.chengjiaozongliang,
|
||
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
|
||
'dashouzhuangtai': dashou_profile.zhuangtai,
|
||
}
|
||
|
||
# 会员列表
|
||
huiyuan_list = []
|
||
try:
|
||
records = Huiyuangoumai.query.filter(yonghu_id=dashou_profile.user.UserUID)
|
||
for record in records:
|
||
shifou_daoqi = record.jiance_shifou_daoqi()
|
||
huiyuan_list.append({
|
||
'huiyuan_id': record.huiyuan_id,
|
||
'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
|
||
'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
|
||
'shifou_daoqi': shifou_daoqi,
|
||
'jieshao': record.jieshao
|
||
})
|
||
except Exception as e:
|
||
logger.warning(f"获取会员列表失败: {e}")
|
||
data['clumber'] = huiyuan_list
|
||
return data
|
||
|
||
def _fanhui_xianyou_guanshi_info(self, user_main):
|
||
"""用户已是管事时的返回"""
|
||
guanshi_profile = user_main.GuanshiProfile
|
||
dashou_profile = getattr(user_main, 'DashouProfile', None)
|
||
|
||
group_info = self._huoqu_qun_peizhi()
|
||
|
||
dashou_status = 1 if dashou_profile else 0
|
||
shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
|
||
guanshi_status = 1
|
||
|
||
try:
|
||
nicheng = user_main.BossProfile.nickname or '微信用户'
|
||
except:
|
||
nicheng = '微信用户'
|
||
|
||
refresh = RefreshToken.for_user(user_main)
|
||
token = str(refresh.access_token)
|
||
|
||
data = {
|
||
'token': token,
|
||
'nicheng': nicheng,
|
||
'uid': user_main.UserUID,
|
||
'touxiang': user_main.Avatar or '',
|
||
'shangjiastatus': shangjia_status,
|
||
'dashoustatus': dashou_status,
|
||
'guanshistatus': guanshi_status,
|
||
**group_info,
|
||
}
|
||
|
||
# 管事信息
|
||
guanshi_data = {
|
||
'gszhstatus': guanshi_profile.zhuangtai,
|
||
'yaoqingzongshu': guanshi_profile.yaogingshuliang,
|
||
'fenyongzonge': str(guanshi_profile.chongzhifenrun),
|
||
'fenyongtixian': str(guanshi_profile.yue),
|
||
'yichongzhiDashou': guanshi_profile.jinrichongzhi,
|
||
}
|
||
data.update(guanshi_data)
|
||
|
||
if dashou_profile:
|
||
dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
|
||
data.update(dashou_data)
|
||
else:
|
||
data.update({
|
||
'dashounicheng': '',
|
||
'zhanghaostatus': 0,
|
||
'yongjin': '0.00',
|
||
'zonge': '0.00',
|
||
'yajin': '0.00',
|
||
'chenghao': '',
|
||
'jinfen': 0,
|
||
'chengjiaoliang': 0,
|
||
'zaixianzhuangtai': 0,
|
||
'dashouzhuangtai': 0,
|
||
'clumber': []
|
||
})
|
||
|
||
return Response({'code': 200, 'msg': '您已是管事', 'data': data})
|
||
|
||
def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token):
|
||
"""组装完整返回数据(新注册)"""
|
||
try:
|
||
nicheng = user_main.BossProfile.nickname or '微信用户'
|
||
except:
|
||
nicheng = '微信用户'
|
||
|
||
dashou_status = 1
|
||
shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
|
||
guanshi_status = 1
|
||
|
||
group_info = self._huoqu_qun_peizhi()
|
||
|
||
data = {
|
||
'token': token,
|
||
'nicheng': nicheng,
|
||
'uid': user_main.UserUID,
|
||
'touxiang': user_main.Avatar or '',
|
||
'shangjiastatus': shangjia_status,
|
||
'dashoustatus': dashou_status,
|
||
'guanshistatus': guanshi_status,
|
||
**group_info,
|
||
}
|
||
|
||
# 管事信息
|
||
guanshi_data = {
|
||
'gszhstatus': guanshi_profile.zhuangtai,
|
||
'yaoqingzongshu': guanshi_profile.yaogingshuliang,
|
||
'fenyongzonge': str(guanshi_profile.chongzhifenrun),
|
||
'fenyongtixian': str(guanshi_profile.yue),
|
||
'yichongzhiDashou': guanshi_profile.jinrichongzhi,
|
||
}
|
||
data.update(guanshi_data)
|
||
|
||
dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
|
||
data.update(dashou_data)
|
||
|
||
return data
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class ZuzhangZhuceView(APIView):
|
||
"""
|
||
组长注册/激活接口
|
||
POST /yonghu/zuzhangzhuce
|
||
请求体: {"inviteCode": "xxx"}
|
||
返回:
|
||
{
|
||
"code": 200,
|
||
"msg": "success",
|
||
"data": {
|
||
"ketixian": "0.00", # 可提现金额
|
||
"guanshiCount": 0, # 邀请的管事数量
|
||
"fenhongZonge": "0.00" # 分红总额
|
||
}
|
||
}
|
||
或错误码
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
# 硬编码激活码(注意转义特殊字符)
|
||
ACTIVATION_CODE = "237huwehdw77e777eyw224fr"
|
||
|
||
def post(self, request):
|
||
user = request.user
|
||
invite_code = request.data.get('inviteCode', '').strip()
|
||
|
||
if not invite_code:
|
||
return Response({'code': 400, 'msg': '邀请码不能为空'}, status=400)
|
||
|
||
# 检查是否已是组长
|
||
try:
|
||
zuzhang = UserZuzhang.query.select_related('user').get(user=user)
|
||
# 已注册,直接返回数据
|
||
data = self._build_response_data(zuzhang, user)
|
||
return Response({'code':200, 'msg': '已注册', 'data': data})
|
||
except UserZuzhang.DoesNotExist:
|
||
pass
|
||
|
||
# 比对激活码
|
||
if invite_code != self.ACTIVATION_CODE:
|
||
return Response({'code': 400, 'msg': '邀请码无效'}, status=400)
|
||
|
||
# 生成唯一组长邀请码:时间戳 + 用户ID后6位 + 随机字符
|
||
yaoqingma = self._generate_unique_yaoqingma(user.UserUID)
|
||
|
||
# 创建组长扩展记录
|
||
try:
|
||
with transaction.atomic():
|
||
zuzhang = UserZuzhang.query.create(
|
||
user=user,
|
||
yaoqingma=yaoqingma,
|
||
fenyong_zonge=0.00,
|
||
ketixian_jine=0.00,
|
||
yaoqing_zongshu=0,
|
||
jinri_fenyong=0.00,
|
||
jinyue_fenyong=0.00,
|
||
zhuangtai=1,
|
||
jinri_tixian=0.00,
|
||
ewai_tixian_xiane=0.00,
|
||
kaioi_ewai_tixian=False,
|
||
kaioi_ewai_fenhong=False,
|
||
ewai_fenhong_jine=0.00
|
||
)
|
||
except IntegrityError:
|
||
# 极低概率重复,重试一次
|
||
yaoqingma = self._generate_unique_yaoqingma(user.UserUID, retry=True)
|
||
zuzhang = UserZuzhang.query.create(
|
||
user=user,
|
||
yaoqingma=yaoqingma,
|
||
# 其他字段默认同上
|
||
fenyong_zonge=0.00,
|
||
ketixian_jine=0.00,
|
||
yaoqing_zongshu=0,
|
||
jinri_fenyong=0.00,
|
||
jinyue_fenyong=0.00,
|
||
zhuangtai=1,
|
||
jinri_tixian=0.00,
|
||
ewai_tixian_xiane=0.00,
|
||
kaioi_ewai_tixian=False,
|
||
kaioi_ewai_fenhong=False,
|
||
ewai_fenhong_jine=0.00
|
||
)
|
||
|
||
data = self._build_response_data(zuzhang, user)
|
||
return Response({'code':200, 'msg': '注册成功', 'data': data})
|
||
|
||
def _generate_unique_yaoqingma(self, yonghuid, retry=False):
|
||
"""生成唯一邀请码:时间戳+用户ID后6位+随机2位字符"""
|
||
timestamp = str(int(time.time()))[-6:] # 取后6位
|
||
uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0')
|
||
rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2))
|
||
base = f"Z{timestamp}{uid_part}{rand_str}"
|
||
# 确保唯一性(若已存在则追加随机数,最多尝试3次)
|
||
for _ in range(3):
|
||
if not UserZuzhang.query.filter(yaoqingma=base).exists():
|
||
return base
|
||
base = base + random.choice(string.digits)
|
||
# 极端情况,再附加时间戳毫秒
|
||
millis = str(time.time()).replace('.', '')[-4:]
|
||
return base + millis
|
||
|
||
def _build_response_data(self, zuzhang, user):
|
||
"""组装前端所需数据"""
|
||
# 计算邀请的管事数量(通过UserGuanshi表的yaoqingren字段关联到user的yonghuid)
|
||
guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count()
|
||
return {
|
||
'ketixian': str(zuzhang.ketixian_jine), # 可提现余额
|
||
'guanshiCount': guanshi_count, # 邀请管事数
|
||
'fenhongZonge': str(zuzhang.fenyong_zonge) # 分红总额
|
||
}
|
||
|
||
|
||
|
||
class ZuzhangXinxiView(APIView):
|
||
"""
|
||
组长信息查询接口
|
||
POST /yonghu/zuzhangxinxi
|
||
请求体: 无
|
||
返回: 同注册接口成功返回格式
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
user = request.user
|
||
try:
|
||
zuzhang = UserZuzhang.query.get(user=user)
|
||
except UserZuzhang.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不是组长'}, status=404)
|
||
|
||
guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count()
|
||
data = {
|
||
'ketixian': str(zuzhang.ketixian_jine),
|
||
'guanshiCount': guanshi_count,
|
||
'fenhongZonge': str(zuzhang.fenyong_zonge)
|
||
}
|
||
return Response({'code': 200, 'msg': 'success', 'data': data})
|
||
|
||
|
||
|
||
|
||
|
||
# apps/yonghu/views_dashou_jianquan.py
|
||
"""
|
||
打手 IM 消息权限鉴权接口
|
||
URL: /yonghu/dshqltqx
|
||
方法: POST
|
||
认证: JWT Token (request.user)
|
||
|
||
鉴权逻辑(满足任意一条即通过):
|
||
1. 押金 >= 5 元 → 通过
|
||
2. 拥有未过期会员 → 通过
|
||
3. 存在订单状态为 2 或 8 的订单(作为接单打手) → 通过
|
||
以上均不满足 → 拒绝
|
||
"""
|
||
|
||
|
||
class DashouJianquanView(APIView):
|
||
"""打手消息权限鉴权"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# ========== 1. 通过 JWT 获取当前登录用户 ==========
|
||
user = request.user # User 实例
|
||
|
||
# ========== 2. 检查是否为已注册打手 ==========
|
||
try:
|
||
dashou = user.DashouProfile # OneToOneField 反向查询
|
||
except UserDashou.DoesNotExist:
|
||
return Response({
|
||
'code': 0,
|
||
'allow': 0,
|
||
'msg': '您尚未注册打手身份,请先注册'
|
||
})
|
||
|
||
# ========== 3. 条件一:押金 >= 5 元 ==========
|
||
if dashou.yajin and dashou.yajin >= 5:
|
||
return Response({
|
||
'code': 0,
|
||
'allow': 1,
|
||
'msg': '鉴权通过(押金满足条件)'
|
||
})
|
||
|
||
# ========== 4. 条件二:存在未过期会员 ==========
|
||
# 高效查询:利用 yonghu_id + huiyuan_zhuangtai 复合索引
|
||
huiyuan_list = Huiyuangoumai.query.filter(
|
||
yonghu_id=user.UserUID,
|
||
huiyuan_zhuangtai=1 # 只查状态为"可用"的,减少内存遍历
|
||
).only('id', 'daoqi_time', 'huiyuan_zhuangtai')
|
||
|
||
for hy in huiyuan_list:
|
||
if not hy.jiance_shifou_daoqi(): # 未过期返回 False
|
||
return Response({
|
||
'code': 0,
|
||
'allow': 1,
|
||
'msg': '鉴权通过(会员未过期)'
|
||
})
|
||
|
||
# ========== 5. 条件三:存在订单状态为 2 或 8 的订单 ==========
|
||
# 利用 jiedan_dashou_id + zhuangtai 复合索引,只查 1 条
|
||
has_valid_order = Order.query.filter(
|
||
PlayerID=user.UserUID,
|
||
Status__in=[2, 8]
|
||
).only('id').exists() # exists() 高性能判断,只发 SELECT 1 LIMIT 1
|
||
|
||
if has_valid_order:
|
||
return Response({
|
||
'code': 0,
|
||
'allow': 1,
|
||
'msg': '鉴权通过(存在有效订单)'
|
||
})
|
||
|
||
# ========== 6. 以上均不满足 → 拒绝 ==========
|
||
return Response({
|
||
'code': 0,
|
||
'allow': 0,
|
||
'msg': '消息权限不足,请充值会员或联系管理员'
|
||
})
|
||
|
||
|
||
|
||
# apps/yonghu/views_laoban_jianquan.py
|
||
"""
|
||
老板(普通用户) IM 消息权限鉴权接口
|
||
URL: /yonghu/lbhqltqx
|
||
方法: POST
|
||
认证: JWT Token (request.user)
|
||
|
||
鉴权逻辑:
|
||
查询平台订单扩展表(PlatformOrderExt),
|
||
通过关联的订单主表(Order)查找该老板的订单,
|
||
只要存在任意一条订单状态在 1~8 范围内 → 通过
|
||
"""
|
||
|
||
|
||
|
||
class LaobanJianquanView(APIView):
|
||
"""老板消息权限鉴权"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# ========== 1. 通过 JWT 获取当前登录用户 ==========
|
||
user = request.user # User 实例
|
||
|
||
# ========== 2. 高效查询平台订单扩展表 ==========
|
||
# 利用 PlatformOrderExt.laoban_id 索引 + Order.zhuangtai 索引
|
||
# 只查 1 条,用 exists() 实现最高性能
|
||
has_valid_order = PlatformOrderExt.query.filter(
|
||
laoban_id=user.UserUID
|
||
).filter(
|
||
dingdan__zhuangtai__in=[1, 2, 3, 4, 5, 6, 7, 8]
|
||
).only('id').exists()
|
||
|
||
if has_valid_order:
|
||
return Response({
|
||
'code': 0,
|
||
'allow': 1,
|
||
'msg': '鉴权通过'
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'allow': 0,
|
||
'msg': '您暂无订单记录,消息功能暂不可用'
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# views.py (在处罚相关app的views中)
|
||
class FaKuanJiFenTongJiView(APIView):
|
||
"""
|
||
获取罚款与积分处罚统计总览
|
||
路径: POST /yonghu/cffktjhq
|
||
"""
|
||
permission_classes = [IsAuthenticated, DashouPermission]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user_main = request.user
|
||
yonghuid = user_main.UserUID
|
||
|
||
# ================= 罚款统计 =================
|
||
fadan_stats = Penalty.query.filter(PenalizedUserID=yonghuid).aggregate(
|
||
total=Count('id'),
|
||
daijiaona=Count('id', filter=Q(Status=1)),
|
||
shensuzhong=Count('id', filter=Q(Status=3)),
|
||
yijiaona=Count('id', filter=Q(Status=2)),
|
||
yibohui=Count('id', filter=Q(Status=4)),
|
||
)
|
||
|
||
# ================= 积分处罚统计 =================
|
||
jifen_stats = PenaltyRecord.query.filter(PlayerID=yonghuid).aggregate(
|
||
total=Count('id'),
|
||
daichuli=Count('id', filter=Q(ApplyStatus=0) | Q(ApplyStatus=3)),
|
||
yichuli=Count('id', filter=Q(ApplyStatus=1) | Q(ApplyStatus=2)),
|
||
# 细分
|
||
daichuli_0=Count('id', filter=Q(ApplyStatus=0)),
|
||
shensuzhong_3=Count('id', filter=Q(ApplyStatus=3)),
|
||
yichufa_1=Count('id', filter=Q(ApplyStatus=1)),
|
||
yibohui_2=Count('id', filter=Q(ApplyStatus=2)),
|
||
)
|
||
|
||
data = {
|
||
"fakuan": {
|
||
"total": fadan_stats['total'],
|
||
"daijiaona": fadan_stats['daijiaona'],
|
||
"shensuzhong": fadan_stats['shensuzhong'],
|
||
"yijiaona": fadan_stats['yijiaona'],
|
||
"yibohui": fadan_stats['yibohui'],
|
||
},
|
||
"jifen": {
|
||
"total": jifen_stats['total'],
|
||
"daichuli": jifen_stats['daichuli'],
|
||
"yichuli": jifen_stats['yichuli'],
|
||
"daichuli_0": jifen_stats['daichuli_0'],
|
||
"shensuzhong_3": jifen_stats['shensuzhong_3'],
|
||
"yichufa_1": jifen_stats['yichufa_1'],
|
||
"yibohui_2": jifen_stats['yibohui_2'],
|
||
}
|
||
}
|
||
|
||
return Response({'code': 0, 'msg': '获取统计成功', 'data': data})
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取统计异常 {yonghuid}: {e}", exc_info=True)
|
||
return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
|
||
|
||
class DaShouFaKuanLieBiaoView(APIView):
|
||
"""
|
||
打手罚款列表接口
|
||
路径:POST /yonghu/dsfklbhq
|
||
参数:
|
||
page: 页码(默认1)
|
||
page_size: 每页条数(默认10,最大50)
|
||
zhuangtai: 状态筛选(1待缴纳/2已缴纳/3申诉中/4申诉成功,0或不传表示全部)
|
||
sousuo_dingdan_id: 订单号模糊搜索
|
||
返回:{
|
||
list: [{
|
||
id, beichufa_id, guanliandingdan_id, chufaliyou, fakuanjine, zhuangtai,
|
||
yingxiang_qiangdan, shensuliyou, bohuiliyou,
|
||
zhengju_tupian: [...], // 证据图片(yongtu=1)
|
||
shensu_tupian: [...], // 申诉图片(yongtu=2)
|
||
CreateTime, UpdateTime
|
||
}],
|
||
has_more, page, page_size
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated, DashouPermission]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user_main = request.user
|
||
yonghuid = user_main.UserUID
|
||
|
||
# 获取分页参数
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 10))
|
||
if page_size > 50:
|
||
page_size = 50
|
||
if page_size < 1:
|
||
page_size = 10
|
||
|
||
# 状态筛选
|
||
zhuangtai = request.data.get('zhuangtai')
|
||
# 订单号搜索
|
||
sousuo_dingdan_id = request.data.get('sousuo_dingdan_id', '').strip()
|
||
|
||
# 基础查询:当前用户的罚单
|
||
qs = Penalty.query.filter(PenalizedUserID=yonghuid)
|
||
|
||
# 状态筛选
|
||
if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]:
|
||
qs = qs.filter(Status=int(zhuangtai))
|
||
|
||
# 订单号模糊搜索
|
||
if sousuo_dingdan_id:
|
||
qs = qs.filter(RelatedOrderID__icontains=sousuo_dingdan_id)
|
||
|
||
# 按创建时间倒序
|
||
qs = qs.order_by('-CreateTime')
|
||
|
||
# 分页
|
||
paginator = Paginator(qs, page_size)
|
||
try:
|
||
page_obj = paginator.page(page)
|
||
except Exception:
|
||
return Response({
|
||
'code': 0, 'msg': '获取成功',
|
||
'data': {'list': [], 'has_more': False, 'page': page, 'page_size': page_size}
|
||
})
|
||
|
||
records = list(page_obj)
|
||
fadan_ids = [r.id for r in records]
|
||
|
||
# 批量获取图片,按用途分组
|
||
zhengju_map = {} # key: fadan_id, value: [url, ...]
|
||
shensu_map = {}
|
||
if fadan_ids:
|
||
tupians = PenaltyAppealImage.query.filter(
|
||
Penalty_id__in=fadan_ids
|
||
).values('Penalty_id', 'ImageURL', 'Purpose')
|
||
for t in tupians:
|
||
fid = t['Penalty_id']
|
||
url = t['ImageURL']
|
||
if t['Purpose'] == 1:
|
||
zhengju_map.setdefault(fid, []).append(url)
|
||
else:
|
||
shensu_map.setdefault(fid, []).append(url)
|
||
|
||
# 组装返回数据
|
||
chufa_list = []
|
||
for r in records:
|
||
rid = r.id
|
||
item = {
|
||
'id': rid,
|
||
'beichufa_id': r.PenalizedUserID,
|
||
'guanliandingdan_id': r.RelatedOrderID or '',
|
||
'chufaliyou': r.Reason or '',
|
||
'fakuanjine': str(r.FineAmount),
|
||
'zhuangtai': r.Status,
|
||
'yingxiang_qiangdan': r.AffectsGrabbing,
|
||
'shensuliyou': r.AppealReason or '',
|
||
'bohuiliyou': r.RejectReason or '',
|
||
'zhengju_tupian': zhengju_map.get(rid, []), # 证据图片
|
||
'shensu_tupian': shensu_map.get(rid, []), # 申诉图片
|
||
'CreateTime': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||
'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '',
|
||
}
|
||
chufa_list.append(item)
|
||
|
||
has_more = page_obj.has_next()
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '获取成功',
|
||
'data': {
|
||
'list': chufa_list,
|
||
'has_more': has_more,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取罚款列表异常 {yonghuid if 'yonghuid' in locals() else ''}: {e}", exc_info=True)
|
||
return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
|
||
|
||
|
||
class FaKuanShenSuView(APIView):
|
||
"""
|
||
罚款申诉提交/修改接口
|
||
路径:POST /yonghu/fkss
|
||
参数:
|
||
fadan_id: 罚单ID
|
||
shensu_liyou: 申诉理由
|
||
shensu_tupian_urls: 图片相对URL数组(可空数组,但至少要有理由)
|
||
逻辑:
|
||
- 仅待缴纳状态且无驳回理由的罚单可申诉
|
||
- 允许对已有申诉进行修改:先删除旧申诉图片(yongtu=2),再添加新图片
|
||
"""
|
||
permission_classes = [IsAuthenticated, DashouPermission]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user_main = request.user
|
||
yonghuid = user_main.UserUID
|
||
fadan_id = request.data.get('fadan_id')
|
||
shensu_liyou = request.data.get('shensu_liyou', '').strip()
|
||
tupian_urls = request.data.get('shensu_tupian_urls', [])
|
||
|
||
if not fadan_id:
|
||
return Response({'code': 400, 'msg': '缺少罚款ID'}, status=400)
|
||
if not shensu_liyou:
|
||
return Response({'code': 400, 'msg': '请输入申诉理由'}, status=400)
|
||
|
||
# 查询罚单,确保属于当前打手
|
||
try:
|
||
fadan = Penalty.query.get(id=fadan_id, PenalizedUserID=yonghuid)
|
||
except Penalty.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '罚单不存在或无权操作'}, status=404)
|
||
|
||
# 只允许待缴纳状态(状态1)且没有驳回理由的罚单申诉
|
||
if fadan.Status != 1:
|
||
return Response({'code': 400, 'msg': '罚单状态不允许申诉'}, status=400)
|
||
if fadan.RejectReason:
|
||
return Response({'code': 400, 'msg': '该罚单申诉已被驳回,不可再次申诉'}, status=400)
|
||
|
||
# 更新罚单状态为申诉中,并记录理由
|
||
fadan.Status = 3
|
||
fadan.AppealReason = shensu_liyou
|
||
fadan.save()
|
||
|
||
# 删除旧的申诉图片(yongtu=2),仅删除打手自己上传的申诉图
|
||
PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=2).delete()
|
||
|
||
# 插入新上传的申诉图片
|
||
for url in tupian_urls:
|
||
PenaltyAppealImage.query.create(
|
||
Penalty=fadan,
|
||
PenalizedUserID=yonghuid,
|
||
ImageURL=url,
|
||
Purpose=2
|
||
)
|
||
|
||
return Response({'code': 0, 'msg': '申诉提交成功'})
|
||
|
||
except Exception as e:
|
||
logger.error(f"罚款申诉异常 {yonghuid if 'yonghuid' in locals() else ''}: {e}", exc_info=True)
|
||
return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# 需要替换为你自己项目中的导入路径
|
||
|
||
# XIAOCHENGXU LOGGER?
|
||
# ==================== 1. 罚款支付下单接口 ====================
|
||
|
||
class FaKuanPayView(APIView):
|
||
"""
|
||
罚款微信支付下单接口(完全照抄 YajinGoumai)
|
||
路径:POST /yonghu/fkjn
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
fadan_id = request.data.get('fadan_id')
|
||
if not fadan_id:
|
||
return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
fadan = Penalty.query.get(id=fadan_id, PenalizedUserID=request.user.UserUID)
|
||
except Penalty.DoesNotExist:
|
||
return Response({'code': 404, 'message': '罚单不存在或不属于您'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
if fadan.Status not in [1, 3]:
|
||
return Response({'code': 400, 'message': '罚单当前状态不可支付'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
jine = float(fadan.FineAmount)
|
||
|
||
timestamp_ms = int(time.time() * 1000)
|
||
timestamp_ns = int(time.time_ns() // 1000) % 1000000
|
||
random_num = random.randint(0, 99)
|
||
dingdanid = f"FK{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
|
||
|
||
chongzhi_jilu = Czjilu.query.create(
|
||
dingdan_id=dingdanid,
|
||
zhuangtai=9,
|
||
yonghuid=request.user.UserUID,
|
||
jine=jine,
|
||
leixing=4,
|
||
shuoming=settings.FAKUAN_PAY_DESCRIPTION
|
||
)
|
||
|
||
try:
|
||
pay_params = self.generate_wechat_pay_params(
|
||
dingdanid=dingdanid,
|
||
jine=jine,
|
||
openid=request.user.OpenID,
|
||
pay_type='fakuan'
|
||
)
|
||
except Exception as e:
|
||
chongzhi_jilu.delete()
|
||
logger.error(f"生成微信支付参数失败: {str(e)}")
|
||
return Response({
|
||
'code': 500,
|
||
'message': f'支付参数生成失败: {str(e)}'
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'message': '支付参数生成成功',
|
||
'payParams': pay_params,
|
||
'dingdanid': dingdanid
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
logger.error(f"罚款支付下单接口异常: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 500,
|
||
'message': '服务器内部错误,请稍后重试'
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
|
||
APPID = settings.WEIXIN_APPID
|
||
MCHID = settings.WEIXIN_MCHID
|
||
KEY = settings.WEIXIN_SHANGHUMIYAO
|
||
|
||
if pay_type == 'fakuan':
|
||
PAY_DESCRIPTION = settings.FAKUAN_PAY_DESCRIPTION
|
||
NOTIFY_URL = settings.FAKUAN_PAY_NOTIFY_URL # 回调地址在这里
|
||
else:
|
||
raise Exception('未知的支付类型')
|
||
|
||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||
total_fee = int(jine * 100)
|
||
trade_type = 'JSAPI'
|
||
user_ip = self.get_client_ip()
|
||
|
||
# 第一次签名
|
||
sign_data = {
|
||
'appid': APPID,
|
||
'mch_id': MCHID,
|
||
'nonce_str': nonce_str,
|
||
'body': PAY_DESCRIPTION,
|
||
'out_trade_no': dingdanid,
|
||
'total_fee': total_fee,
|
||
'spbill_create_ip': user_ip,
|
||
'notify_url': NOTIFY_URL, # 关键:把回调地址传入
|
||
'trade_type': trade_type,
|
||
'openid': openid
|
||
}
|
||
|
||
sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
|
||
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
|
||
sign_string += f'&key={KEY}'
|
||
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
|
||
|
||
# 构建 XML,这里必须包含 notify_url
|
||
xml_data = f"""<xml>
|
||
<appid>{APPID}</appid>
|
||
<body>{PAY_DESCRIPTION}</body>
|
||
<mch_id>{MCHID}</mch_id>
|
||
<nonce_str>{nonce_str}</nonce_str>
|
||
<notify_url>{NOTIFY_URL}</notify_url>
|
||
<openid>{openid}</openid>
|
||
<out_trade_no>{dingdanid}</out_trade_no>
|
||
<spbill_create_ip>{user_ip}</spbill_create_ip>
|
||
<total_fee>{total_fee}</total_fee>
|
||
<trade_type>{trade_type}</trade_type>
|
||
<sign>{sign}</sign>
|
||
</xml>"""
|
||
|
||
response = requests.post(
|
||
'https://api.mch.weixin.qq.com/pay/unifiedorder',
|
||
data=xml_data.encode('utf-8'),
|
||
headers={'Content-Type': 'application/xml'},
|
||
timeout=10
|
||
)
|
||
|
||
root = ET.fromstring(response.content)
|
||
return_code = root.find('return_code').text
|
||
result_code = root.find('result_code').text
|
||
|
||
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
|
||
prepay_id = root.find('prepay_id').text
|
||
timestamp = str(int(time.time()))
|
||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||
package = f'prepay_id={prepay_id}'
|
||
sign_type = 'MD5'
|
||
|
||
pay_sign_data = {
|
||
'appId': APPID,
|
||
'timeStamp': timestamp,
|
||
'nonceStr': nonce_str,
|
||
'package': package,
|
||
'signType': sign_type
|
||
}
|
||
|
||
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
|
||
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
|
||
pay_sign_string += f'&key={KEY}'
|
||
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
|
||
|
||
return {
|
||
'appId': APPID,
|
||
'timeStamp': timestamp,
|
||
'nonceStr': nonce_str,
|
||
'package': package,
|
||
'signType': sign_type,
|
||
'paySign': pay_sign
|
||
}
|
||
else:
|
||
err_code = root.find('err_code')
|
||
err_code_des = root.find('err_code_des')
|
||
error_details = f"return_code: {return_code}"
|
||
if err_code is not None:
|
||
error_details += f", err_code: {err_code.text}"
|
||
if err_code_des is not None:
|
||
error_details += f", err_code_des: {err_code_des.text}"
|
||
raise Exception(f"微信支付下单失败: {error_details}")
|
||
|
||
def get_client_ip(self):
|
||
request = self.request
|
||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||
if x_forwarded_for:
|
||
ip = x_forwarded_for.split(',')[0]
|
||
else:
|
||
ip = request.META.get('REMOTE_ADDR')
|
||
return ip if ip else '127.0.0.1'
|
||
|
||
|
||
# ==================== 2. 罚款支付回调接口 ====================
|
||
class FaKuanHuitiaoView(View):
|
||
|
||
"""
|
||
罚款支付回调接口(严格借鉴 YajinHuitiao 接口,参数一个不少)
|
||
路径:POST /yonghu/fk_huitiao
|
||
"""
|
||
def post(self, request):
|
||
try:
|
||
# 1. 解析微信回调XML数据
|
||
request_body = request.body
|
||
logger.info(f"收到微信支付回调,原始数据: {request_body.decode('utf-8')}")
|
||
|
||
root = ET.fromstring(request_body)
|
||
|
||
# 获取关键字段
|
||
return_code = root.find('return_code').text
|
||
result_code = root.find('result_code').text
|
||
out_trade_no = root.find('out_trade_no').text # 订单ID
|
||
transaction_id = root.find('transaction_id').text # 微信交易号
|
||
total_fee = int(root.find('total_fee').text) # 支付金额(分)
|
||
|
||
logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分")
|
||
|
||
# 2. 验证基本返回状态
|
||
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
|
||
logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}")
|
||
return self.wechat_response('FAIL', '支付失败')
|
||
|
||
# 3. 验证签名(确保是微信发来的)
|
||
if not self.verify_signature(request_body):
|
||
logger.error(f"签名验证失败: {out_trade_no}")
|
||
return self.wechat_response('FAIL', '签名验证失败')
|
||
|
||
# 4. 查询订单
|
||
try:
|
||
order = Czjilu.query.get(dingdan_id=out_trade_no)
|
||
logger.info(f"找到订单: {order.OrderID}, 状态: {order.Status}, 金额: {order.Amount}")
|
||
except Czjilu.DoesNotExist:
|
||
logger.error(f"订单不存在: {out_trade_no}")
|
||
return self.wechat_response('FAIL', '订单不存在')
|
||
|
||
# 5. 检查订单状态是否为未支付(9)
|
||
if order.Status != 9:
|
||
logger.warning(f"订单状态不是未支付: {order.OrderID}, 当前状态: {order.Status}")
|
||
# 已处理过的订单直接返回成功,避免微信重复回调
|
||
return self.wechat_response('SUCCESS', 'OK')
|
||
|
||
# 6. 验证金额(微信返回的是分,需要转换)
|
||
wechat_amount_yuan = total_fee / 100 # 转换为元
|
||
order_amount_yuan = float(order.Amount) # 订单金额(元)
|
||
|
||
# 允许1分钱的误差(浮点数比较)
|
||
if abs(wechat_amount_yuan - order_amount_yuan) > 0.01:
|
||
logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
|
||
return self.wechat_response('FAIL', '金额不一致')
|
||
|
||
# 7. 更新订单状态为已支付(3)
|
||
order.Status = 3
|
||
order.save(update_fields=['zhuangtai'])
|
||
logger.info(f"订单状态更新成功: {order.OrderID} -> 3")
|
||
|
||
# 8. 更新罚单状态为已缴纳(2)
|
||
try:
|
||
# 根据订单中的用户ID和金额匹配待缴纳的罚单
|
||
fadan = Penalty.query.filter(
|
||
PenalizedUserID=order.yonghuid,
|
||
FineAmount=order.Amount,
|
||
Status__in=[1, 3]
|
||
).first()
|
||
if fadan:
|
||
fadan.Status = 2 # 已缴纳
|
||
fadan.save(update_fields=['Status'])
|
||
logger.info(f"罚单 {fadan.id} 已自动标记为已缴纳")
|
||
|
||
success, msg = process_fadan_fenhong(fadan.id)
|
||
if not success:
|
||
logger.error(f"罚单 {fadan.id} 分红处理失败: {msg}")
|
||
else:
|
||
logger.info(f"罚单 {fadan.id} 分红处理完成: {msg}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"更新罚单状态失败: {str(e)}", exc_info=True)
|
||
|
||
# 9. 更新收支记录表
|
||
try:
|
||
|
||
# 获取订单金额(确保是Decimal类型)
|
||
jine_decimal = Decimal(str(order.Amount)) if not isinstance(order.Amount, Decimal) else order.Amount
|
||
|
||
update_daily_income(jine_decimal)
|
||
|
||
# 查询或创建ID=1的收支记录
|
||
szjilu, created = Szjilu.query.get_or_create(
|
||
id=1,
|
||
defaults={
|
||
'TotalIncome': Decimal('0.00'),
|
||
'TotalFlow': Decimal('0.00'),
|
||
'TotalExpense': Decimal('0.00'),
|
||
'DailyExpense': Decimal('0.00'),
|
||
'DailyFlow': Decimal('0.00')
|
||
}
|
||
)
|
||
|
||
# 更新三个字段
|
||
szjilu.TotalIncome += jine_decimal # 总收益
|
||
szjilu.TotalFlow += jine_decimal # 总流水
|
||
szjilu.DailyFlow += jine_decimal # 今日流水
|
||
|
||
szjilu.save()
|
||
|
||
logger.info(f"收支记录更新成功: 订单{order.OrderID}, 金额{jine_decimal}元")
|
||
|
||
except Exception as e:
|
||
# 收支记录更新失败不影响主流程,只记录日志
|
||
logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
|
||
|
||
# 10. 返回成功响应给微信
|
||
logger.info(f"罚款支付回调处理完成: {out_trade_no}")
|
||
return self.wechat_response('SUCCESS', 'OK')
|
||
|
||
except ET.ParseError as e:
|
||
logger.error(f"XML解析失败: {str(e)}")
|
||
return self.wechat_response('FAIL', '数据格式错误')
|
||
except Exception as e:
|
||
logger.error(f"罚款支付回调处理异常: {str(e)}", exc_info=True)
|
||
# 返回FAIL让微信重试
|
||
return self.wechat_response('FAIL', '处理异常')
|
||
|
||
def verify_signature(self, xml_data):
|
||
"""验证微信支付签名(完全照搬 YajinHuitiao 的 verify_signature)"""
|
||
try:
|
||
root = ET.fromstring(xml_data)
|
||
sign = root.find('sign').text
|
||
|
||
if not sign:
|
||
logger.error("签名字段不存在")
|
||
return False
|
||
|
||
# 获取所有参数并排除sign
|
||
params = {}
|
||
for child in root:
|
||
if child.tag != 'sign' and child.text is not None:
|
||
params[child.tag] = child.text
|
||
|
||
# 按照参数名ASCII码从小到大排序
|
||
sorted_params = sorted(params.items(), key=lambda x: x[0])
|
||
|
||
# 拼接字符串
|
||
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
|
||
sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
|
||
|
||
# 计算签名
|
||
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
|
||
|
||
is_valid = sign == calculated_sign
|
||
if not is_valid:
|
||
logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}")
|
||
|
||
return is_valid
|
||
|
||
except Exception as e:
|
||
logger.error(f"验证签名异常: {str(e)}")
|
||
return False
|
||
|
||
def wechat_response(self, return_code, return_msg):
|
||
"""返回微信支付回调响应(完全照搬 YajinHuitiao 的 wechat_response)"""
|
||
xml_response = f"""<xml>
|
||
<return_code><![CDATA[{return_code}]]></return_code>
|
||
<return_msg><![CDATA[{return_msg}]]></return_msg>
|
||
</xml>"""
|
||
|
||
logger.info(f"返回微信响应: {return_code} - {return_msg}")
|
||
return HttpResponse(xml_response, content_type='application/xml')
|
||
|
||
|
||
# ==================== 3. 支付轮询接口 ====================
|
||
class FaKuanPayPollView(APIView):
|
||
"""罚款支付轮询接口(借鉴 YajinLunxun)"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
dingdanid = request.data.get('dingdanid')
|
||
if not dingdanid:
|
||
return Response({'code': 400, 'message': '订单ID不能为空'})
|
||
|
||
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
|
||
if order.Status == 3:
|
||
return Response({'code': 200, 'message': '支付成功'})
|
||
else:
|
||
return Response({'code': 400, 'message': '订单尚未支付'})
|
||
except Czjilu.DoesNotExist:
|
||
return Response({'code': 400, 'message': '订单不存在'})
|
||
except Exception as e:
|
||
logger.error(f"轮询异常: {str(e)}")
|
||
return Response({'code': 500, 'message': '服务器内部错误'})
|
||
|
||
|
||
# ==================== 4. 支付失败处理接口 ====================
|
||
class FaKuanPayFailView(APIView):
|
||
"""罚款支付失败处理接口(借鉴 YajinShibai)"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
dingdanid = request.data.get('dingdanid')
|
||
if not dingdanid:
|
||
return Response({'code': 400, 'message': '订单ID不能为空'})
|
||
|
||
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
|
||
if order.Status == 9: # 未支付
|
||
order.delete()
|
||
logger.info(f"用户{request.user.UserUID}删除未支付订单: {dingdanid}")
|
||
return Response({'code': 200, 'message': '订单已删除'})
|
||
else:
|
||
return Response({'code': 400, 'message': '订单状态不是未支付,无法删除'})
|
||
except Czjilu.DoesNotExist:
|
||
return Response({'code': 400, 'message': '订单不存在'})
|
||
except Exception as e:
|
||
logger.error(f"支付失败处理异常: {str(e)}")
|
||
return Response({'code': 500, 'message': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KaohePayView(APIView):
|
||
"""
|
||
考核支付下单接口
|
||
路径:POST /yonghu/khzf
|
||
参数:
|
||
tag_id: 标签ID
|
||
game_nick: 游戏昵称
|
||
remark: 备注
|
||
reapply: 是否再次申请 (true/false)
|
||
jilu_id: 再次申请时的原记录ID(reapply=true时必传)
|
||
shenheguan_id: 指定考核官ID(可选)
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user = request.user
|
||
tag_id = request.data.get('tag_id')
|
||
game_nick = request.data.get('game_nick', '').strip()
|
||
remark = request.data.get('remark', '').strip()
|
||
reapply = request.data.get('reapply', False)
|
||
jilu_id = request.data.get('jilu_id') if reapply else None
|
||
shenheguan_id = request.data.get('shenheguan_id', '').strip()
|
||
|
||
logger.info(f"用户 {user.UserUID} 发起考核支付申请,标签ID: {tag_id}, 再次申请: {reapply}")
|
||
|
||
if not tag_id or not game_nick:
|
||
logger.warning(f"用户 {user.UserUID} 参数不完整: tag_id={tag_id}, game_nick={game_nick}")
|
||
return Response({'code': 400, 'message': '标签ID和游戏昵称不能为空'})
|
||
|
||
# 1. 检查是否有未支付的考核订单
|
||
"""unpaid_order = Czjilu.query.filter(
|
||
UserUID=user.UserUID,
|
||
leixing=5,
|
||
zhuangtai=9
|
||
).exists()
|
||
if unpaid_order:
|
||
logger.warning(f"用户 {user.UserUID} 存在未支付的考核订单")
|
||
return Response({'code': 400, 'message': '您有未支付的考核订单,请先完成支付或取消'})"""
|
||
|
||
# 2. 验证标签
|
||
try:
|
||
chenghao = Chenghao.query.get(id=tag_id)
|
||
logger.info(f"标签验证通过: {chenghao.mingcheng}")
|
||
except Chenghao.DoesNotExist:
|
||
logger.error(f"标签不存在: tag_id={tag_id}")
|
||
return Response({'code': 404, 'message': '标签不存在'})
|
||
|
||
# 3. 检查用户是否已拥有该标签
|
||
if YonghuChenghao.query.filter(yonghu=user, chenghao=chenghao).exists():
|
||
logger.warning(f"用户 {user.UserUID} 已拥有标签 {chenghao.mingcheng}")
|
||
return Response({'code': 400, 'message': '您已拥有该标签,无需重复考核'})
|
||
|
||
# 4. 检查是否有未完成的申请
|
||
if ShenheJilu.query.filter(
|
||
shenqingren_id=user.UserUID,
|
||
chenghao=chenghao,
|
||
zhuangtai__in=[0, 3] # 审核中、待审核
|
||
).exists():
|
||
logger.warning(f"用户 {user.UserUID} 有未完成的考核申请")
|
||
return Response({'code': 400, 'message': '您有未完成的考核申请,请等待结果'})
|
||
|
||
# 5. 计算考核次数和费用
|
||
if reapply and jilu_id:
|
||
last = ShenheJilu.query.filter(
|
||
jilu_id=jilu_id,
|
||
shenqingren_id=user.UserUID,
|
||
zhuangtai=2
|
||
).first()
|
||
if not last:
|
||
logger.warning(f"用户 {user.UserUID} 再次申请失败:原记录不存在或状态不是未通过,jilu_id={jilu_id}")
|
||
return Response({'code': 400, 'message': '原记录不存在或状态不是未通过'})
|
||
new_cishu = last.cishu + 1
|
||
logger.info(f"再次申请,原记录ID: {jilu_id},新次数: {new_cishu}")
|
||
else:
|
||
max_cishu = ShenheJilu.query.filter(
|
||
shenqingren_id=user.UserUID,
|
||
chenghao=chenghao,
|
||
zhuangtai__in=[1, 2]
|
||
).aggregate(max=models.Max('cishu'))['max'] or 0
|
||
new_cishu = max_cishu + 1
|
||
logger.info(f"首次申请,计算次数: {new_cishu}")
|
||
|
||
# 6. 获取本次费用
|
||
fee = get_tag_fee(chenghao, new_cishu)
|
||
logger.info(f"本次考核费用: {fee}元,次数: {new_cishu}")
|
||
|
||
if fee <= 0:
|
||
logger.warning(f"费用为0,应使用免费申请接口,tag_id={tag_id}, cishu={new_cishu}")
|
||
return Response({'code': 400, 'message': '本次费用为0,请直接使用免费申请接口'})
|
||
|
||
# 7. 生成订单ID
|
||
timestamp_ms = int(time.time() * 1000)
|
||
timestamp_ns = int(time.time_ns() // 1000) % 1000000
|
||
random_num = random.randint(0, 99)
|
||
dingdanid = f"KH{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
|
||
logger.info(f"生成订单ID: {dingdanid}")
|
||
|
||
# 8. 创建支付订单(Czjilu)
|
||
order = Czjilu.query.create(
|
||
dingdan_id=dingdanid,
|
||
zhuangtai=9, # 未支付
|
||
yonghuid=user.UserUID,
|
||
jine=float(fee),
|
||
leixing=5, # 5-考核支付
|
||
shuoming=settings.KAOHE_PAY_DESCRIPTION
|
||
)
|
||
logger.info(f"创建支付订单成功: {dingdanid}")
|
||
|
||
# 9. 保存考核支付临时上下文(用于回调时取业务参数)
|
||
try:
|
||
KaohePayTemp.query.create(
|
||
dingdan_id=dingdanid,
|
||
yonghuid=user.UserUID,
|
||
tag_id=tag_id,
|
||
game_nick=game_nick,
|
||
remark=remark,
|
||
reapply=reapply,
|
||
jilu_id=jilu_id if reapply else None,
|
||
shenheguan_id=shenheguan_id,
|
||
cishu=new_cishu,
|
||
fee=fee,
|
||
)
|
||
logger.info(f"保存临时上下文成功: {dingdanid}")
|
||
except Exception as e:
|
||
logger.error(f"保存临时上下文失败: {str(e)}", exc_info=True)
|
||
# 临时表保存失败应该回滚订单
|
||
order.delete()
|
||
return Response({'code': 500, 'message': '系统异常,请稍后重试'})
|
||
|
||
# 10. 构造 attach 参数(只传订单ID,确保不超127字节)
|
||
attach_str = json.dumps({'oid': dingdanid}, ensure_ascii=False)
|
||
logger.info(f"attach内容: {attach_str},长度: {len(attach_str.encode('utf-8'))}字节")
|
||
|
||
# 11. 生成微信支付参数
|
||
try:
|
||
pay_params = self.generate_wechat_pay_params(
|
||
dingdanid=dingdanid,
|
||
jine=float(fee),
|
||
openid=user.OpenID,
|
||
attach=attach_str,
|
||
pay_type='kaohe'
|
||
)
|
||
logger.info(f"微信支付参数生成成功: {dingdanid}")
|
||
except Exception as e:
|
||
order.delete() # 下单失败,删除订单记录
|
||
# 同时删除临时表记录
|
||
try:
|
||
KaohePayTemp.query.filter(dingdan_id=dingdanid).delete()
|
||
except Exception as del_e:
|
||
logger.error(f"删除临时表记录失败: {str(del_e)}")
|
||
logger.error(f"生成微信支付参数失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'message': f'支付参数生成失败: {str(e)}'})
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'message': '支付参数生成成功',
|
||
'payParams': pay_params,
|
||
'dingdanid': dingdanid
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"考核支付下单异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'message': '服务器内部错误'})
|
||
|
||
def generate_wechat_pay_params(self, dingdanid, jine, openid, attach, pay_type):
|
||
"""生成微信JSAPI支付参数"""
|
||
APPID = settings.WEIXIN_APPID
|
||
MCHID = settings.WEIXIN_MCHID
|
||
KEY = settings.WEIXIN_SHANGHUMIYAO
|
||
|
||
if pay_type == 'kaohe':
|
||
PAY_DESCRIPTION = settings.KAOHE_PAY_DESCRIPTION
|
||
NOTIFY_URL = settings.KAOHE_PAY_NOTIFY_URL
|
||
else:
|
||
raise Exception('未知支付类型')
|
||
|
||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||
total_fee = int(jine * 100)
|
||
trade_type = 'JSAPI'
|
||
user_ip = self.get_client_ip()
|
||
|
||
logger.info(f"微信支付下单参数: out_trade_no={dingdanid}, total_fee={total_fee}, openid={openid}, ip={user_ip}")
|
||
|
||
# 第一次签名(统一下单)
|
||
sign_data = {
|
||
'appid': APPID,
|
||
'mch_id': MCHID,
|
||
'nonce_str': nonce_str,
|
||
'body': PAY_DESCRIPTION,
|
||
'out_trade_no': dingdanid,
|
||
'total_fee': total_fee,
|
||
'spbill_create_ip': user_ip,
|
||
'notify_url': NOTIFY_URL,
|
||
'trade_type': trade_type,
|
||
'openid': openid,
|
||
'attach': attach,
|
||
}
|
||
|
||
sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
|
||
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
|
||
sign_string += f'&key={KEY}'
|
||
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
|
||
|
||
# 构建XML请求体
|
||
xml_data = f"""<xml>
|
||
<appid>{APPID}</appid>
|
||
<body>{PAY_DESCRIPTION}</body>
|
||
<mch_id>{MCHID}</mch_id>
|
||
<nonce_str>{nonce_str}</nonce_str>
|
||
<notify_url>{NOTIFY_URL}</notify_url>
|
||
<openid>{openid}</openid>
|
||
<out_trade_no>{dingdanid}</out_trade_no>
|
||
<spbill_create_ip>{user_ip}</spbill_create_ip>
|
||
<total_fee>{total_fee}</total_fee>
|
||
<trade_type>{trade_type}</trade_type>
|
||
<attach><![CDATA[{attach}]]></attach>
|
||
<sign>{sign}</sign>
|
||
</xml>"""
|
||
|
||
logger.info(f"微信支付请求XML: {xml_data[:500]}...") # 只打印前500字符
|
||
|
||
response = requests.post(
|
||
'https://api.mch.weixin.qq.com/pay/unifiedorder',
|
||
data=xml_data.encode('utf-8'),
|
||
headers={'Content-Type': 'application/xml'},
|
||
timeout=10
|
||
)
|
||
|
||
logger.info(f"微信支付响应: {response.text}")
|
||
|
||
root = ET.fromstring(response.content)
|
||
|
||
# 安全获取return_code
|
||
return_code_node = root.find('return_code')
|
||
if return_code_node is None or return_code_node.text is None:
|
||
logger.error(f"微信返回缺少return_code节点,原始内容: {response.text}")
|
||
raise Exception('微信支付接口异常,缺少return_code')
|
||
return_code = return_code_node.text
|
||
|
||
# 安全获取result_code
|
||
result_code_node = root.find('result_code')
|
||
if result_code_node is None or result_code_node.text is None:
|
||
logger.error(f"微信返回缺少result_code节点,原始内容: {response.text}")
|
||
raise Exception('微信支付接口异常,缺少result_code')
|
||
result_code = result_code_node.text
|
||
|
||
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
|
||
prepay_id_node = root.find('prepay_id')
|
||
if prepay_id_node is None or prepay_id_node.text is None:
|
||
logger.error(f"统一下单成功但未返回prepay_id,原始内容: {response.text}")
|
||
raise Exception('微信支付接口异常,缺少prepay_id')
|
||
prepay_id = prepay_id_node.text
|
||
|
||
timestamp = str(int(time.time()))
|
||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||
package = f'prepay_id={prepay_id}'
|
||
sign_type = 'MD5'
|
||
|
||
# 第二次签名(前端调起支付用)
|
||
pay_sign_data = {
|
||
'appId': APPID,
|
||
'timeStamp': timestamp,
|
||
'nonceStr': nonce_str,
|
||
'package': package,
|
||
'signType': sign_type,
|
||
}
|
||
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
|
||
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
|
||
pay_sign_string += f'&key={KEY}'
|
||
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
|
||
|
||
logger.info(f"微信支付预下单成功,prepay_id={prepay_id}")
|
||
|
||
return {
|
||
'appId': APPID,
|
||
'timeStamp': timestamp,
|
||
'nonceStr': nonce_str,
|
||
'package': package,
|
||
'signType': sign_type,
|
||
'paySign': pay_sign,
|
||
}
|
||
else:
|
||
# 安全获取错误信息
|
||
err_code_node = root.find('err_code')
|
||
err_code = err_code_node.text if err_code_node is not None else '未知错误码'
|
||
err_code_des_node = root.find('err_code_des')
|
||
err_code_des = err_code_des_node.text if err_code_des_node is not None else '未知错误描述'
|
||
error_details = f"return_code: {return_code}, result_code: {result_code}, err_code: {err_code}, err_code_des: {err_code_des}"
|
||
logger.error(f"微信支付下单失败: {error_details}")
|
||
raise Exception(f"微信支付下单失败: {error_details}")
|
||
|
||
def get_client_ip(self):
|
||
"""获取客户端IP"""
|
||
request = self.request
|
||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||
if x_forwarded_for:
|
||
ip = x_forwarded_for.split(',')[0]
|
||
else:
|
||
ip = request.META.get('REMOTE_ADDR', '127.0.0.1')
|
||
return ip if ip else '127.0.0.1'
|
||
|
||
|
||
class KaohePayCallbackView(View):
|
||
"""
|
||
考核支付回调接口
|
||
路径:/yonghu/kh_huitiao
|
||
"""
|
||
|
||
def post(self, request):
|
||
try:
|
||
request_body = request.body
|
||
logger.info(f"收到考核支付回调: {request_body.decode('utf-8')}")
|
||
|
||
root = ET.fromstring(request_body)
|
||
|
||
# 安全获取return_code
|
||
return_code_node = root.find('return_code')
|
||
if return_code_node is None or return_code_node.text is None:
|
||
logger.error(f"微信回调XML缺少return_code节点,原始内容: {request_body.decode('utf-8')}")
|
||
return self.wechat_response('FAIL', '数据格式错误')
|
||
return_code = return_code_node.text
|
||
|
||
# 安全获取result_code
|
||
result_code_node = root.find('result_code')
|
||
if result_code_node is None or result_code_node.text is None:
|
||
logger.error(f"微信回调XML缺少result_code节点,原始内容: {request_body.decode('utf-8')}")
|
||
return self.wechat_response('FAIL', '数据格式错误')
|
||
result_code = result_code_node.text
|
||
|
||
# 安全获取out_trade_no
|
||
out_trade_no_node = root.find('out_trade_no')
|
||
if out_trade_no_node is None or out_trade_no_node.text is None:
|
||
logger.error(f"微信回调XML缺少out_trade_no节点,原始内容: {request_body.decode('utf-8')}")
|
||
return self.wechat_response('FAIL', '数据格式错误')
|
||
out_trade_no = out_trade_no_node.text
|
||
|
||
# 安全获取transaction_id
|
||
transaction_id_node = root.find('transaction_id')
|
||
if transaction_id_node is None or transaction_id_node.text is None:
|
||
logger.error(f"微信回调XML缺少transaction_id节点,原始内容: {request_body.decode('utf-8')}")
|
||
return self.wechat_response('FAIL', '数据格式错误')
|
||
transaction_id = transaction_id_node.text
|
||
|
||
# 安全获取total_fee
|
||
total_fee_node = root.find('total_fee')
|
||
if total_fee_node is None or total_fee_node.text is None:
|
||
logger.error(f"微信回调XML缺少total_fee节点,原始内容: {request_body.decode('utf-8')}")
|
||
return self.wechat_response('FAIL', '数据格式错误')
|
||
total_fee = int(total_fee_node.text)
|
||
|
||
logger.info(
|
||
f"回调参数解析成功: out_trade_no={out_trade_no}, transaction_id={transaction_id}, total_fee={total_fee}")
|
||
|
||
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
|
||
logger.warning(f"支付状态异常: return_code={return_code}, result_code={result_code}")
|
||
return self.wechat_response('FAIL', '支付失败')
|
||
|
||
# 验证签名
|
||
if not self.verify_signature(request_body):
|
||
logger.error(f"签名验证失败,订单号: {out_trade_no}")
|
||
return self.wechat_response('FAIL', '签名验证失败')
|
||
logger.info(f"签名验证成功,订单号: {out_trade_no}")
|
||
|
||
# 查找订单
|
||
try:
|
||
order = Czjilu.query.get(dingdan_id=out_trade_no, leixing=5)
|
||
logger.info(f"找到订单: {out_trade_no}, 当前状态: {order.Status}")
|
||
except Czjilu.DoesNotExist:
|
||
logger.error(f"考核订单不存在: {out_trade_no}")
|
||
return self.wechat_response('FAIL', '订单不存在')
|
||
|
||
if order.Status != 9:
|
||
logger.warning(f"订单状态非未支付: {order.Status}, 订单号: {out_trade_no}")
|
||
# 已支付的订单直接返回成功,避免微信重复回调
|
||
if order.Status == 3:
|
||
logger.info(f"订单已支付,直接返回成功: {out_trade_no}")
|
||
return self.wechat_response('SUCCESS', 'OK')
|
||
return self.wechat_response('FAIL', '订单状态异常')
|
||
|
||
# 验证金额
|
||
wechat_yuan = total_fee / 100
|
||
if abs(wechat_yuan - float(order.Amount)) > 0.01:
|
||
logger.error(f"金额不一致: 订单金额={order.Amount}, 回调金额={wechat_yuan}, 订单号: {out_trade_no}")
|
||
return self.wechat_response('FAIL', '金额不一致')
|
||
logger.info(f"金额验证通过: {wechat_yuan}元")
|
||
|
||
# 更新订单状态为已支付
|
||
order.Status = 3
|
||
order.save()
|
||
logger.info(f"订单状态更新为已支付: {out_trade_no}")
|
||
|
||
# 从临时表获取业务参数并创建审核记录
|
||
try:
|
||
temp = KaohePayTemp.query.get(dingdan_id=out_trade_no, yonghuid=order.yonghuid)
|
||
logger.info(f"找到临时表记录: {out_trade_no}, 用户ID: {order.yonghuid}")
|
||
|
||
user = User.query.get(UserUID=order.yonghuid)
|
||
logger.info(f"获取用户信息成功: {user.UserUID}")
|
||
|
||
success, msg, record = create_shenhe_jilu_from_temp(user, temp)
|
||
if success:
|
||
logger.info(f"创建审核记录成功: jilu_id={record.jilu_id}, 订单号: {out_trade_no}")
|
||
else:
|
||
logger.error(f"创建审核记录失败: {msg}, 订单号: {out_trade_no}")
|
||
# 订单已支付但记录创建失败,需要标记订单异常,人工介入
|
||
order.Status = 10 # 10表示支付成功但业务处理失败
|
||
order.save()
|
||
logger.error(f"订单状态标记为异常(10): {out_trade_no}")
|
||
|
||
except KaohePayTemp.DoesNotExist:
|
||
logger.error(f"临时表无记录,订单号: {out_trade_no}, 用户ID: {order.yonghuid}")
|
||
# 尝试从attach解析(兼容旧数据)
|
||
attach_element = root.find('attach')
|
||
if attach_element is not None and attach_element.text:
|
||
try:
|
||
attach_data = json.loads(attach_element.text)
|
||
logger.info(f"从attach解析数据成功: {attach_data}")
|
||
# 这里可以保留旧逻辑,但建议只记录日志
|
||
except Exception as e:
|
||
logger.error(f"解析attach失败: {str(e)}")
|
||
except Exception as e:
|
||
logger.error(f"处理临时表或创建审核记录异常: {str(e)}", exc_info=True)
|
||
# 标记订单异常
|
||
order.Status = 10
|
||
order.save()
|
||
logger.error(f"订单状态标记为异常(10): {out_trade_no}")
|
||
|
||
# 更新收支记录
|
||
try:
|
||
jine_decimal = Decimal(str(order.Amount))
|
||
update_daily_income(jine_decimal)
|
||
logger.info(f"更新每日收入成功: {jine_decimal}元")
|
||
|
||
szjilu, created = Szjilu.query.get_or_create(
|
||
id=1,
|
||
defaults={
|
||
'TotalIncome': Decimal('0.00'),
|
||
'TotalFlow': Decimal('0.00'),
|
||
'TotalExpense': Decimal('0.00'),
|
||
'DailyExpense': Decimal('0.00'),
|
||
'DailyFlow': Decimal('0.00'),
|
||
}
|
||
)
|
||
szjilu.TotalIncome += jine_decimal
|
||
szjilu.TotalFlow += jine_decimal
|
||
szjilu.DailyFlow += jine_decimal
|
||
szjilu.save()
|
||
logger.info(f"更新收支记录成功: zongsy={szjilu.TotalIncome}")
|
||
except Exception as e:
|
||
logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
|
||
|
||
logger.info(f"考核支付回调处理完成,订单号: {out_trade_no}")
|
||
return self.wechat_response('SUCCESS', 'OK')
|
||
|
||
except ET.ParseError as e:
|
||
logger.error(f"XML解析失败: {str(e)}, 原始数据: {request.body.decode('utf-8')}")
|
||
return self.wechat_response('FAIL', '数据格式错误')
|
||
except Exception as e:
|
||
logger.error(f"考核支付回调异常: {str(e)}", exc_info=True)
|
||
return self.wechat_response('FAIL', '处理异常')
|
||
|
||
def verify_signature(self, xml_data):
|
||
"""验证微信支付签名"""
|
||
try:
|
||
root = ET.fromstring(xml_data)
|
||
sign_node = root.find('sign')
|
||
if sign_node is None or sign_node.text is None:
|
||
logger.error("微信回调XML缺少sign节点")
|
||
return False
|
||
sign = sign_node.text
|
||
|
||
params = {}
|
||
for child in root:
|
||
if child.tag != 'sign' and child.text is not None:
|
||
params[child.tag] = child.text
|
||
|
||
sorted_params = sorted(params.items(), key=lambda x: x[0])
|
||
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
|
||
sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
|
||
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
|
||
|
||
result = sign == calculated_sign
|
||
if not result:
|
||
logger.error(f"签名验证失败: 原签名={sign}, 计算签名={calculated_sign}")
|
||
return result
|
||
except Exception as e:
|
||
logger.error(f"签名验证异常: {str(e)}", exc_info=True)
|
||
return False
|
||
|
||
def wechat_response(self, return_code, return_msg):
|
||
"""构造微信回调响应XML"""
|
||
xml = f"""<xml>
|
||
<return_code><![CDATA[{return_code}]]></return_code>
|
||
<return_msg><![CDATA[{return_msg}]]></return_msg>
|
||
</xml>"""
|
||
logger.info(f"返回微信响应: {xml}")
|
||
return HttpResponse(xml, content_type='application/xml')
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KaohePayPollView(APIView):
|
||
"""考核支付轮询"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
dingdanid = request.data.get('dingdanid')
|
||
if not dingdanid:
|
||
return Response({'code': 400, 'message': '订单ID不能为空'})
|
||
try:
|
||
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5)
|
||
if order.Status == 3:
|
||
return Response({'code': 200, 'message': '支付成功'})
|
||
return Response({'code': 400, 'message': '订单尚未支付'})
|
||
except Czjilu.DoesNotExist:
|
||
return Response({'code': 400, 'message': '订单不存在'})
|
||
|
||
|
||
class KaohePayFailView(APIView):
|
||
"""考核支付失败处理"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
dingdanid = request.data.get('dingdanid')
|
||
if not dingdanid:
|
||
return Response({'code': 400, 'message': '订单ID不能为空'})
|
||
try:
|
||
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5)
|
||
if order.Status == 9:
|
||
order.delete()
|
||
return Response({'code': 200, 'message': '订单已删除'})
|
||
return Response({'code': 400, 'message': '订单状态无法删除'})
|
||
except Czjilu.DoesNotExist:
|
||
return Response({'code': 400, 'message': '订单不存在'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class TixianAssetView(APIView):
|
||
"""
|
||
获取用户所有可提现资产及费率
|
||
请求方式:POST
|
||
认证:JWT Token
|
||
返回:
|
||
{
|
||
"code": 200,
|
||
"msg": "success",
|
||
"data": {
|
||
"dashou_yue": "123.45",
|
||
"dashou_yajin": "500.00",
|
||
"shangjia_yue": "0.00",
|
||
"guanshi_fenyong": "67.89",
|
||
"zuzhang_fenyong": "0.00",
|
||
"kaoheguan_fenyong": "0.00",
|
||
"dashou_rate": 0.08,
|
||
"dashou_yajin_rate": 0.05,
|
||
"shangjia_rate": 0.06,
|
||
"guanshi_rate": 0.08,
|
||
"zuzhang_rate": 0.05,
|
||
"kaoheguan_rate": 0.07
|
||
}
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
user = request.user
|
||
# 初始化返回数据,默认全部为0
|
||
data = {
|
||
'dashou_yue': '0.00',
|
||
'dashou_yajin': '0.00',
|
||
'shangjia_yue': '0.00',
|
||
'guanshi_fenyong': '0.00',
|
||
'zuzhang_fenyong': '0.00',
|
||
'kaoheguan_fenyong': '0.00',
|
||
'dashou_rate': 0.00,
|
||
'dashou_yajin_rate': 0.00,
|
||
'shangjia_rate': 0.00,
|
||
'guanshi_rate': 0.00,
|
||
'zuzhang_rate': 0.00,
|
||
'kaoheguan_rate': 0.00,
|
||
}
|
||
|
||
# ------------------- 打手资产 -------------------
|
||
try:
|
||
dashou = user.DashouProfile
|
||
data['dashou_yue'] = str(dashou.yue)
|
||
data['dashou_yajin'] = str(dashou.yajin)
|
||
except ObjectDoesNotExist:
|
||
pass
|
||
|
||
# ------------------- 商家资产 -------------------
|
||
try:
|
||
shangjia = user.ShopProfile
|
||
data['shangjia_yue'] = str(shangjia.yue)
|
||
except ObjectDoesNotExist:
|
||
pass
|
||
|
||
# ------------------- 管事资产 -------------------
|
||
try:
|
||
guanshi = user.GuanshiProfile
|
||
data['guanshi_fenyong'] = str(guanshi.yue)
|
||
except ObjectDoesNotExist:
|
||
pass
|
||
|
||
# ------------------- 组长资产 -------------------
|
||
try:
|
||
zuzhang = user.ZuzhangProfile
|
||
data['zuzhang_fenyong'] = str(zuzhang.ketixian_jine)
|
||
except ObjectDoesNotExist:
|
||
pass
|
||
|
||
# ------------------- 审核官(考核官)资产 -------------------
|
||
try:
|
||
kaoheguan = user.ShenheguanProfile
|
||
data['kaoheguan_fenyong'] = str(kaoheguan.yue)
|
||
except ObjectDoesNotExist:
|
||
pass
|
||
|
||
# ------------------- 费率查询(从 CommissionRate 获取) -------------------
|
||
# 打手提现费率 (类型 '5')
|
||
lilu_obj = CommissionRate.query.filter(Platform='5').first()
|
||
data['dashou_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00
|
||
|
||
# 打手保证金提现费率 (类型 '11')
|
||
lilu_obj = CommissionRate.query.filter(Platform='11').first()
|
||
data['dashou_yajin_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00
|
||
|
||
# 商家提现费率 (类型 '10')
|
||
lilu_obj = CommissionRate.query.filter(Platform='10').first()
|
||
data['shangjia_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00
|
||
|
||
# 管事提现费率 (类型 '6')
|
||
lilu_obj = CommissionRate.query.filter(Platform='6').first()
|
||
data['guanshi_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00
|
||
|
||
# 组长提现费率 (类型 '8')
|
||
lilu_obj = CommissionRate.query.filter(Platform='8').first()
|
||
data['zuzhang_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00
|
||
|
||
# 审核官(考核官)提现费率 (类型 '9')
|
||
lilu_obj = CommissionRate.query.filter(Platform='9').first()
|
||
data['kaoheguan_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'msg': '获取成功',
|
||
'data': data
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
|
||
|
||
|
||
|
||
class GuanshiContactView(APIView):
|
||
"""
|
||
管事获取自己的邀请人(组长)信息
|
||
路径: /api/yonghu/guanshi_contact
|
||
方法: POST
|
||
权限: JWT认证
|
||
请求体: 无(仅需携带合法token)
|
||
成功返回:
|
||
code: 200,
|
||
data: {
|
||
uid: "1234567", // 组长用户ID
|
||
nicheng: "张三", // 组长昵称
|
||
touxiang: "avatar/xxx.jpg" // 组长头像相对路径(前端拼接完整URL)
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 获取当前登录用户(必须已通过JWT认证)
|
||
current_user = request.user # 类型: User
|
||
|
||
# 2. 校验是否为管事身份(存在管事扩展表记录)
|
||
try:
|
||
guanshi_profile = current_user.GuanshiProfile
|
||
except ObjectDoesNotExist:
|
||
return Response({
|
||
'code': 403,
|
||
'msg': '您还不是管事,无法获取组长信息',
|
||
'data': None
|
||
})
|
||
|
||
# 3. 获取邀请人ID(管事扩展表中的 yaoqingren 字段存储的是邀请人的 yonghuid)
|
||
inviter_yonghuid = guanshi_profile.yaoqingren
|
||
if not inviter_yonghuid:
|
||
return Response({
|
||
'code': 404,
|
||
'msg': '未找到邀请人信息',
|
||
'data': None
|
||
})
|
||
|
||
# 4. 根据邀请人用户ID查询用户主表
|
||
try:
|
||
inviter_user = User.query.get(UserUID=inviter_yonghuid)
|
||
except User.DoesNotExist:
|
||
logger.error(f"邀请人用户不存在: yonghuid={inviter_yonghuid}")
|
||
return Response({
|
||
'code': 404,
|
||
'msg': '邀请人账号异常',
|
||
'data': None
|
||
})
|
||
|
||
# 5. 获取邀请人昵称(从老板扩展表取,若不存在则给默认值)
|
||
try:
|
||
inviter_nicheng = inviter_user.BossProfile.nickname
|
||
except ObjectDoesNotExist:
|
||
inviter_nicheng = '组长' # 兜底昵称
|
||
|
||
# 6. 构造返回数据
|
||
response_data = {
|
||
'uid': inviter_user.UserUID,
|
||
'nicheng': inviter_nicheng,
|
||
'touxiang': inviter_user.Avatar or '' # 头像相对路径
|
||
}
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'msg': '获取成功',
|
||
'data': response_data
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
logger = logging.getLogger(__name__) # 恢复区间?
|
||
|
||
def generate_tixian_id():
|
||
"""生成唯一提现订单号:TX + 时间戳(13位) + 4位随机数"""
|
||
timestamp = str(int(time.time() * 1000))
|
||
rand = ''.join(random.choices(string.digits, k=4))
|
||
return f'TX{timestamp}{rand}'
|
||
|
||
|
||
class TixianShenqingV3View(APIView):
|
||
"""
|
||
自动提现收款接口(新流程第二步)
|
||
POST /yonghu/tixiansq
|
||
必须传 shenhe_danhao;审核状态须为 6=待收款;申请时已扣款,本接口不再扣款
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
shenhe_danhao = (request.data.get('shenhe_danhao') or '').strip()
|
||
if shenhe_danhao:
|
||
return process_audit_collect(request)
|
||
return Response({
|
||
'code': 7,
|
||
'msg': '缺少审核单号,请先提交提现申请(/yonghu/zddksh)并等待审核通过后再收款',
|
||
})
|
||
|
||
|
||
|
||
class TixianCallbackV3View(APIView):
|
||
"""
|
||
微信转账结果回调接口(最终状态通知)
|
||
处理微信异步发送的转账成功/失败结果
|
||
"""
|
||
permission_classes = [] # 无需认证,签名验证保证安全
|
||
|
||
def post(self, request):
|
||
headers = request.headers
|
||
body = request.body.decode('utf-8')
|
||
|
||
# 1. 验证签名
|
||
if not verify_wechat_sign(headers, body):
|
||
return Response({'code': 'FAIL', 'message': '签名验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 解密数据
|
||
try:
|
||
data = json.loads(body)
|
||
resource = data.get('resource')
|
||
if not resource:
|
||
return Response({'code': 'FAIL', 'message': 'resource不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
associated_data = resource.get('associated_data', '')
|
||
nonce = resource.get('nonce')
|
||
ciphertext = resource.get('ciphertext')
|
||
if not nonce or not ciphertext:
|
||
return Response({'code': 'FAIL', 'message': '解密字段缺失'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
plaintext = decrypt_callback_ciphertext(associated_data, nonce, ciphertext)
|
||
callback_data = json.loads(plaintext)
|
||
except Exception as e:
|
||
logger.error(f"回调解密失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 'FAIL', 'message': f'解密失败: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 3. 获取关键信息
|
||
out_bill_no = callback_data.get('out_bill_no') # 商户提现单号(即 tixian_id)
|
||
transfer_status = callback_data.get('transfer_status') # SUCCESS / FAIL
|
||
wechat_transfer_no = callback_data.get('transfer_no') # 微信转账单号
|
||
fail_reason = callback_data.get('fail_reason', '')
|
||
|
||
if not out_bill_no:
|
||
return Response({'code': 'FAIL', 'message': 'out_bill_no缺失'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 4. 幂等处理:只更新状态为0(处理中)的记录,已最终状态不再处理
|
||
with transaction.atomic():
|
||
try:
|
||
auto_record = TixianAutoRecord.objects.select_for_update().get(tixian_id=out_bill_no)
|
||
except TixianAutoRecord.DoesNotExist:
|
||
# 记录不存在,视为已处理,返回成功
|
||
return Response({'code': 'SUCCESS', 'message': 'ok'})
|
||
|
||
# 如果已经是最新状态,直接返回成功(幂等)
|
||
if auto_record.zhuangtai != 0:
|
||
return Response({'code': 'SUCCESS', 'message': 'already processed'})
|
||
|
||
if transfer_status == 'SUCCESS':
|
||
mark_transfer_success(
|
||
auto_record,
|
||
wechat_transfer_no=wechat_transfer_no,
|
||
with_accounting=True,
|
||
)
|
||
|
||
else: # transfer_status == 'FAIL'
|
||
auto_record.zhuangtai = 2
|
||
auto_record.fail_reason = fail_reason or '微信转账失败'
|
||
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
|
||
release_collect_quota_for_record(auto_record)
|
||
|
||
if not auto_record.shenhe_danhao:
|
||
logger.error('回调失败但打款记录无审核单号 bill=%s', out_bill_no)
|
||
else:
|
||
try:
|
||
audit = TixianShenheJilu.objects.select_for_update().get(
|
||
shenhe_danhao=auto_record.shenhe_danhao
|
||
)
|
||
close_audit_wechat_failed(
|
||
audit, None, auto_record.fail_reason,
|
||
)
|
||
except TixianShenheJilu.DoesNotExist:
|
||
logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}')
|
||
|
||
return Response({'code': 'SUCCESS', 'message': 'ok'})
|
||
|
||
|
||
|
||
class TixianQueRenAutoView(APIView):
|
||
"""
|
||
用户确认提现结果
|
||
POST /yonghu/tixianqr
|
||
以微信官方查单状态为准:SUCCESS 落库成功;FAIL/CANCELLED 关闭审核单+退款;进行中则提示等待
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
tixian_id = request.data.get('tixian_id')
|
||
result = request.data.get('result') # 1成功 0失败/取消
|
||
|
||
if not tixian_id or result is None:
|
||
return Response({'code': 1, 'msg': '缺少参数'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
result = int(result)
|
||
except ValueError:
|
||
return Response({'code': 2, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if result not in [0, 1]:
|
||
return Response({'code': 3, 'msg': 'result值无效'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
user_main = request.user
|
||
|
||
with transaction.atomic():
|
||
try:
|
||
auto_record = TixianAutoRecord.objects.select_for_update().get(
|
||
tixian_id=tixian_id,
|
||
yonghuid=user_main.UserUID,
|
||
)
|
||
except TixianAutoRecord.DoesNotExist:
|
||
return Response({'code': 4, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
if result == 1:
|
||
if auto_record.zhuangtai == 1:
|
||
return Response({'code': 0, 'msg': '提现成功', 'data': None})
|
||
|
||
wx_data = query_wechat_transfer_bill(tixian_id)
|
||
if wx_data is None:
|
||
return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
|
||
|
||
if wx_data.get('_not_found'):
|
||
return Response({'code': 12, 'msg': '收款处理中,请稍后再试'})
|
||
|
||
state = (wx_data.get('state') or '').upper()
|
||
if state in WX_STATE_SUCCESS:
|
||
mark_transfer_success(
|
||
auto_record,
|
||
wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'),
|
||
with_accounting=True,
|
||
)
|
||
return Response({'code': 0, 'msg': '提现成功', 'data': None})
|
||
|
||
if state in WX_STATE_FAIL:
|
||
fail_reason = wx_data.get('fail_reason') or wx_data.get('close_reason') or state
|
||
auto_record.zhuangtai = 2
|
||
auto_record.fail_reason = str(fail_reason)[:500]
|
||
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
|
||
release_collect_quota_for_record(auto_record)
|
||
if auto_record.shenhe_danhao:
|
||
audit = TixianShenheJilu.objects.select_for_update().get(
|
||
shenhe_danhao=auto_record.shenhe_danhao,
|
||
)
|
||
close_audit_wechat_failed(audit, None, auto_record.fail_reason)
|
||
return Response({'code': 400, 'msg': '微信已确认转账失败,该笔提现已关闭,请联系客服处理'})
|
||
|
||
if state in WX_STATE_WAIT or state in WX_STATE_CANCELING:
|
||
return Response({'code': 12, 'msg': '收款处理中,请在微信中完成或稍后再试'})
|
||
|
||
return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
|
||
|
||
# 用户报告取消/失败:以微信查单为准,不本地盲目标记
|
||
if auto_record.zhuangtai == 2:
|
||
return Response({'code': 0, 'msg': '提现已取消', 'data': None})
|
||
if auto_record.zhuangtai == 1:
|
||
return Response({'code': 0, 'msg': '提现成功', 'data': None})
|
||
|
||
wx_data = query_wechat_transfer_bill(tixian_id)
|
||
if wx_data is None:
|
||
return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试,切勿重复操作'})
|
||
|
||
if wx_data.get('_not_found'):
|
||
return Response({'code': 12, 'msg': '收款处理中,请稍后再试'})
|
||
|
||
state = (wx_data.get('state') or '').upper()
|
||
if state in WX_STATE_SUCCESS:
|
||
mark_transfer_success(
|
||
auto_record,
|
||
wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'),
|
||
with_accounting=True,
|
||
)
|
||
return Response({'code': 0, 'msg': '提现成功', 'data': None})
|
||
|
||
if state in WX_STATE_FAIL:
|
||
fail_reason = wx_data.get('fail_reason') or wx_data.get('close_reason') or '用户取消收款'
|
||
auto_record.zhuangtai = 2
|
||
auto_record.fail_reason = str(fail_reason)[:500]
|
||
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
|
||
release_collect_quota_for_record(auto_record)
|
||
if auto_record.shenhe_danhao:
|
||
audit = TixianShenheJilu.objects.select_for_update().get(
|
||
shenhe_danhao=auto_record.shenhe_danhao,
|
||
)
|
||
close_audit_wechat_failed(audit, None, auto_record.fail_reason)
|
||
return Response({'code': 0, 'msg': '提现已关闭,请联系客服处理'})
|
||
|
||
if state in WX_STATE_WAIT or state in WX_STATE_CANCELING:
|
||
return Response({'code': 12, 'msg': '收款仍在进行中,请在微信中操作或稍后再试'})
|
||
|
||
return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
|
||
|
||
except Exception as e:
|
||
logger.error(f'提现确认异常: {str(e)}', exc_info=True)
|
||
return Response({'code': 99, 'msg': '系统错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|