# yonghu/views.py
import time
import random
import hashlib
import requests
from django.db import transaction, IntegrityError
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework_simplejwt.tokens import RefreshToken
from .models import UserMain, UserBoss, UserDashou, UserShangjia, UserGuanshi
from dingdan.models import Dingdan, DingdanPingtai, Lilubiao
from peizhi.models import Qunpeizhi # 假设群配置表在这个模块
from rest_framework.throttling import AnonRateThrottle
from rest_framework.permissions import AllowAny # 新增这行
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from django.utils import timezone
from django.db import transaction, IntegrityError
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.throttling import AnonRateThrottle
from rest_framework.permissions import AllowAny
from rest_framework_simplejwt.tokens import RefreshToken
from .models import UserMain, AdminProfile
# 获取客户端真实IP的函数
def huoquKehuduanIP(request):
"""
获取客户端的真实IP地址
考虑反向代理(如Nginx)的情况
"""
# 优先从HTTP_X_FORWARDED_FOR获取(经过代理的情况)
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
# 可能有多个IP,取第一个
ip = x_forwarded_for.split(',')[0].strip()
if ip:
return ip
# 如果没有代理,直接从REMOTE_ADDR获取
ip = request.META.get('REMOTE_ADDR', '')
# 如果还是获取不到,返回默认值
if not ip:
ip = '0.0.0.0'
return ip
# 获取用户代理信息(用于更详细的追踪)
def huoquYonghuDaili(request):
"""
获取用户浏览器/设备信息
"""
user_agent = request.META.get('HTTP_USER_AGENT', '')
# 这里可以添加更复杂的用户代理解析
# 为简化,我们只返回原始字符串的前100个字符
return user_agent[:100] if user_agent else ''
def shengcheng_yonghu_id():
"""生成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 UserMain.objects.filter(yonghuid=user_id).exists():
return user_id
raise Exception('生成用户ID失败')
def get_or_create_wechat_user(openid, user_type='normal'):
"""
按 openid 获取或创建微信用户,兼容并发注册导致的主键/唯一键冲突。
返回 (user_main, created)
"""
for attempt in range(3):
try:
with transaction.atomic():
user_main = UserMain.objects.select_for_update().filter(openid=openid).first()
if user_main:
if not user_main.yonghuid:
yonghuid = shengcheng_yonghu_id()
UserMain.objects.filter(pk=user_main.pk).update(yonghuid=yonghuid)
user_main.refresh_from_db()
return user_main, False
user_main = UserMain.objects.create(
openid=openid,
yonghuid=shengcheng_yonghu_id(),
user_type=user_type,
)
user_main.refresh_from_db()
return user_main, True
except IntegrityError:
if attempt == 2:
user_main = UserMain.objects.filter(openid=openid).first()
if user_main:
return user_main, False
raise
user_main = UserMain.objects.get(openid=openid)
return user_main, False
def update_wechat_user_login_info(user_main, ip, unionid=None):
"""更新登录信息,使用 update 避免 save() 误触发 INSERT。"""
update_fields = {
'ip': ip,
'last_login_time': timezone.now(),
}
if unionid and unionid != user_main.unionid:
update_fields['unionid'] = unionid
UserMain.objects.filter(pk=user_main.pk).update(**update_fields)
user_main.refresh_from_db()
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 = get_or_create_wechat_user(openid, user_type='normal')
update_wechat_user_login_info(user_main, kehuduan_ip, unionid=unionid or None)
if created:
UserBoss.objects.create(user=user_main, nickname='板板大人')
# 检查扩展表
dashou_status = 0
shangjia_status = 0
guanshi_status = 0
boss_nickname = '微信用户'
try:
boss_profile = UserBoss.objects.get(user=user_main)
boss_nickname = boss_profile.nickname if boss_profile.nickname else '微信用户'
except UserBoss.DoesNotExist:
pass
try:
UserDashou.objects.get(user=user_main)
dashou_status = 1
except UserDashou.DoesNotExist:
dashou_status = 0
try:
UserShangjia.objects.get(user=user_main)
shangjia_status = 1
except UserShangjia.DoesNotExist:
shangjia_status = 0
try:
UserGuanshi.objects.get(user=user_main)
guanshi_status = 1
except UserGuanshi.DoesNotExist:
guanshi_status = 0
try:
UserZuzhang.objects.get(user=user_main)
zuzhang_status = 1
except UserZuzhang.DoesNotExist:
zuzhang_status = 0
# 生成JWT token
refresh = RefreshToken.for_user(user_main)
token = str(refresh.access_token)
# 计算订单数量
order_counts = self.jisuanDingdanShuliang(user_main.yonghuid)
# 获取群配置
group_info = self.huoquQunPeizhi()
# 返回数据
response_data = {
'token': token,
'nicheng': boss_nickname,
'uid': user_main.yonghuid,
'touxiang': user_main.avatar or '',
'shangjiastatus': shangjia_status,
'dashoustatus': dashou_status,
'guanshistatus': guanshi_status,
'zuzhangstatus': zuzhang_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:
import logging
logger = logging.getLogger(__name__)
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 = wx_cfg.WEIXIN_APPID
secret = wx_cfg.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 UserMain.objects.filter(yonghuid=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 jisuanDingdanShuliang(self, yonghuid):
"""
统计订单数量
"""
try:
if not DingdanPingtai.objects.filter(laoban_id=yonghuid).exists():
return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0}
dingdan_ids = DingdanPingtai.objects.filter(laoban_id=yonghuid).values_list('dingdan__id', flat=True)
from django.db.models import Q, Count
from django.db.models import Case, When, IntegerField
results = Dingdan.objects.filter(id__in=dingdan_ids).aggregate(
daifuwu=Count(Case(When(Q(zhuangtai=1) | Q(zhuangtai=7), then=1), output_field=IntegerField())),
fuwuzhong=Count(Case(When(zhuangtai=2, then=1), output_field=IntegerField())),
yiwancheng=Count(Case(When(zhuangtai=3, then=1), output_field=IntegerField())),
yituikuan=Count(Case(When(zhuangtai=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.objects.filter(id__in=[1, 2])
result = {
'dashouqun': '',
'dashouqunid': '',
'guanshiqun': '',
'guanshiqunid': ''
}
for config in qun_configs:
if config.id == 1:
result['dashouqun'] = config.neirong or ''
result['dashouqunid'] = config.qunid or ''
elif config.id == 2:
result['guanshiqun'] = config.neirong or ''
result['guanshiqunid'] = config.qunid or ''
return result
except Exception:
return {
'dashouqun': '',
'dashouqunid': '',
'guanshiqun': '',
'guanshiqunid': ''
}
# views/weixin_official.py - 完整文件
import hashlib
import time
import logging
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from django.utils import timezone
from xml.etree import ElementTree as ET
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from .models import OfficialAccountUser
logger = logging.getLogger(__name__)
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 = wx_cfg.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.objects.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.objects.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"""
{int(time.time())}
"""
return HttpResponse(response_xml, content_type='application/xml')
except Exception as e:
logger.error(f"生成回复消息异常: {str(e)}")
# 即使生成回复失败,也返回一个空的success
return HttpResponse('success')
# yonghu/views.py
from rest_framework.parsers import MultiPartParser, FormParser,JSONParser
from rest_framework import permissions
from django.db import transaction, IntegrityError
from rest_framework.response import Response
from rest_framework import status
from rest_framework.views import APIView
from .models import UserMain, UserBoss, UserDashou, UserShangjia, UserGuanshi
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
import os
import uuid
class UserInfoUpdateView(APIView):
"""
用户个人信息修改接口
功能:
- 上传 / 更新头像
- 修改昵称
- 更新手机号(支持明文 phone 或微信 getPhoneNumber 组件返回的 shoujihao_code)
当传 shoujihao_code 时,会通过微信接口解密得到真实手机号并自动设置 shoujihao_renzheng=True
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
"""
处理 POST 请求,支持 multipart/form-data 格式
"""
try:
# ---------- 获取当前用户 ----------
user_main = request.user
# ---------- 获取或创建老板扩展表记录 ----------
try:
user_boss = UserBoss.objects.get(user=user_main)
except UserBoss.DoesNotExist:
user_boss = UserBoss.objects.create(user=user_main, nickname='微信用户')
response_data = {}
# ==================== 1. 处理头像上传 ====================
avatar_file = request.FILES.get('avatar')
if avatar_file:
# 验证图片格式 / 大小
is_valid, error_msg = validate_image(avatar_file)
if not is_valid:
return Response({
'code': 1,
'msg': f'图片验证失败: {error_msg}',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 上传头像到 OSS / COS
avatar_url = self.handle_avatar_upload(user_main, avatar_file)
if avatar_url:
response_data['touxiang'] = avatar_url
user_main.avatar = avatar_url
else:
return Response({
'code': 2,
'msg': '头像上传失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# ==================== 2. 处理昵称更新 ====================
nicheng = request.data.get('nicheng')
if nicheng is not None:
nicheng = str(nicheng).strip()
if nicheng:
if len(nicheng) > 20:
return Response({
'code': 3,
'msg': '昵称不能超过20个字符',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
user_boss.nickname = nicheng
response_data['nicheng'] = nicheng
# ==================== 3. 处理手机号更新 ====================
# 明文手机号(传统方式)
phone =None
shoujihao_code = request.data.get('shoujihao_code') # 微信手机号快速验证组件返回的临时 code
if shoujihao_code:
# 如果前端传了 shoujihao_code,则通过微信接口解密得到真实手机号
try:
phone = self._get_phone_by_code(shoujihao_code)
logger.info(f"手机号解密成功: {phone}")
except Exception as e:
logger.error(f"手机号解密失败: {str(e)}")
return Response({
'code': 6,
'msg': f'获取手机号失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if phone is not None:
phone = str(phone).strip()
if phone:
# 验证手机号格式(11 位数字)
if not phone.isdigit() or len(phone) != 11:
return Response({
'code': 4,
'msg': '请输入11位有效的手机号码',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 更新主表手机号,并标记手机号已认证
user_main.phone = phone
user_main.shoujihao_renzheng = True
response_data['phone'] = phone
# ==================== 4. 检查是否有任何更新 ====================
if not response_data:
return Response({
'code': 5,
'msg': '没有需要更新的数据',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# ==================== 5. 事务保存 ====================
with transaction.atomic():
user_main.save()
user_boss.save()
# ==================== 6. 返回成功 ====================
return Response({
'code': 0,
'msg': '更新成功',
'data': response_data
})
except Exception as e:
logger.exception("用户信息更新异常")
return Response({
'code': 99,
'msg': f'系统错误: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# ==================== 原有方法:头像上传 ====================
def handle_avatar_upload(self, user_main, avatar_file):
"""
上传头像到 OSS / COS,并返回存储的相对路径
"""
try:
yonghuid = user_main.yonghuid
if not yonghuid:
logger.warning("用户 ID 为空,无法上传头像")
return None
# 获取旧头像,用于后续删除
old_avatar_url = user_main.avatar
# 生成新文件名
file_extension = os.path.splitext(avatar_file.name)[1].lower()
if not file_extension:
file_extension = '.jpg'
unique_filename = f"{uuid.uuid4().hex}{file_extension}"
# OSS 文件路径:avatar/{yonghuid}/{filename}
avatar_folder = 'avatar'
oss_file_path = f"{avatar_folder}/{yonghuid}/{unique_filename}"
# 删除旧头像(如果存在)
if old_avatar_url and old_avatar_url.strip():
delete_from_oss(old_avatar_url)
# 上传新头像
new_avatar_url = upload_to_oss(avatar_file, oss_file_path)
if new_avatar_url:
logger.info(f"头像上传成功: {oss_file_path}")
return oss_file_path
else:
logger.error("头像上传失败,upload_to_oss 返回 None")
return None
except Exception as e:
logger.exception("处理头像上传失败")
return None
# ==================== 新增方法:微信接口相关 ====================
def _get_access_token(self):
"""
获取微信小程序全局 access_token,带缓存
access_token 有效期 7200 秒,提前 300 秒过期
"""
import requests
from django.core.cache import cache
# 尝试从缓存中读取
token = cache.get('wechat_access_token')
if token:
return token
# 从 settings 读取 AppID 和 AppSecret
appid = wx_cfg.WEIXIN_APPID or None
secret = wx_cfg.WEIXIN_SECRET or None
if not appid or not secret:
raise ValueError(
'settings.py 中缺少 WECHAT_APPID 或 WECHAT_APPSECRET,'
'请添加:\n'
"WECHAT_APPID = 'wx0e4be86faac4a8d1'\n"
"WECHAT_APPSECRET = '你的小程序密钥'"
)
url = 'https://api.weixin.qq.com/cgi-bin/token'
params = {
'grant_type': 'client_credential',
'appid': appid,
'secret': secret
}
try:
resp = requests.get(url, params=params)
data = resp.json()
except Exception as e:
raise Exception(f'请求微信 access_token 接口异常: {str(e)}')
if 'access_token' in data:
token = data['access_token']
# 缓存时间设 7100 秒,提前 5 分钟过期
cache.set('wechat_access_token', token, timeout=7100)
logger.info("成功获取微信 access_token")
return token
else:
errcode = data.get('errcode')
errmsg = data.get('errmsg')
logger.error(f"获取 access_token 失败: errcode={errcode}, errmsg={errmsg}")
raise Exception(f"获取 access_token 失败: {errmsg}")
def _get_phone_by_code(self, code):
"""
通过微信手机号快速验证组件返回的 code 换取真实手机号
接口文档:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html
"""
import requests
# 先获取有效的 access_token
access_token = self._get_access_token()
# 构建请求 URL
url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={access_token}'
# 请求体:code 必须放在 JSON 中
payload = {'code': code}
try:
resp = requests.post(url, json=payload, timeout=10)
data = resp.json()
except Exception as e:
raise Exception(f'请求微信手机号解密接口异常: {str(e)}')
errcode = data.get('errcode')
if errcode == 0:
phone_info = data.get('phone_info', {})
pure_phone = phone_info.get('purePhoneNumber')
if not pure_phone:
raise Exception('手机号为空,可能用户未授权真实手机号')
return pure_phone
else:
errmsg = data.get('errmsg')
logger.error(f"手机号解密失败: errcode={errcode}, errmsg={errmsg}")
raise Exception(f'微信接口错误 (errcode={errcode}): {errmsg}')
# yonghu/views.py 或 yonghu/api_views.py
from django.utils import timezone
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
from .models import UserMain, UserDashou
from shangpin.models import Huiyuangoumai # 需要导入会员模型
import logging
class DashouXinxiAPIView(APIView):
"""
获取打手信息接口
请求方式:POST
认证方式:JWT Token
返回:打手基本信息、资产信息、会员信息,金牌打手额外返回专属字段
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取当前登录用户
current_user = request.user
yonghuid = current_user.yonghuid
# 2. 查询打手扩展表
try:
dashou_profile = current_user.dashou_profile
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',
'chengjiaoliang': dashou_profile.chengjiaozongliang,
'jiedanZongliang': dashou_profile.jiedanzongliang if dashou_profile.jiedanzongliang is not None else '0',
'jifen': dashou_profile.jifen,
'chenghao': dashou_profile.chenghao or '',
}
# 4. 查询会员信息(原有逻辑)
huiyuan_list = []
try:
huiyuan_records = Huiyuangoumai.objects.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)
# 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.yonghuid
# 3. 查询打手扩展表
try:
dashou_profile = current_user.dashou_profile
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)
from shangpin.models import Gsfenhong # 需要导入会员模型
class GuanshiXinxiView(APIView):
"""
获取管事信息接口
路径:/yonghu/guanshixinxi
方法:POST
权限:JWT认证
功能:获取当前管事的详细信息,包括基本信息、分红信息以及已充值打手数量
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 获取当前用户(通过JWT认证)
current_user = request.user
# 查询管事扩展表
try:
guanshi_profile = UserGuanshi.objects.get(user=current_user)
# 🔥【新增】查询已充值打手数量(从分红表中统计)
# Gsfenhong 表中 guanshi 字段存储管事ID(yonghuid)
yichongzhi_count = Gsfenhong.objects.filter(guanshi=current_user.yonghuid).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:
# 捕获所有异常,返回错误信息并记录日志
import logging
logger = logging.getLogger(__name__)
logger.error(f"获取管事信息异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'msg': f'获取信息失败: {str(e)}',
'data': None
}, status=500)
'''class GuanshiXinxiView(APIView):
"""
获取管事信息接口
路径:/yonghu/guanshixinxi
方法:POST
权限:JWT认证
功能:获取当前管事的详细信息
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 获取当前用户(通过JWT认证)
current_user = request.user
# 查询管事扩展表
try:
guanshi_profile = UserGuanshi.objects.get(user=current_user)
# 返回管事信息
return Response({
'code': 200,
'msg': '获取成功',
'data': {
'gszhstatus': guanshi_profile.zhuangtai, # 管事账号状态
'yaoqingzongshu': guanshi_profile.yaogingshuliang, # 邀请打手总数
'fenyongzonge': float(guanshi_profile.chongzhifenrun), # 充值分佣总额
'fenyongtixian': float(guanshi_profile.yue), # 可提现余额
# 注意:这里不返回guanshistatus,因为这是缓存字段
# 前端需要的管事状态就是gszhstatus
}
})
except UserGuanshi.DoesNotExist:
# 用户不是管事,返回错误
return Response({
'code': 400,
'msg': '您还不是管事,请先注册',
'data': None
}, status=400)
except Exception as e:
# 捕获所有异常,返回错误信息
return Response({
'code': 500,
'msg': f'获取信息失败: {str(e)}',
'data': None
}, status=500)'''
# yonghu/views.py
from rest_framework import status
from django.core.exceptions import ObjectDoesNotExist
class ShangJiaXinXiView(APIView):
"""
获取商家信息接口 (POST /yonghu/shangjiaxinxi)
权限:需JWT认证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 关键:使用 get() 方法。如果反向关联不存在,会抛出 UserShangjia.DoesNotExist 异常。
shangjia = request.user.shop_profile
# 如果反向关系存在,但你想更显式地查询,也可以这样写(效果相同):
# shangjia = UserShangjia.objects.get(user=request.user)
except ObjectDoesNotExist:
# 捕获“不存在”异常,说明当前用户还没有商家扩展资料
return Response({
'code': 4001, # 你可以定义特定的业务错误码
'msg': '您还未注册成为商家或商家资料不存在',
'data': None
}, status=status.HTTP_200_OK) # 业务错误,HTTP状态码通常仍用200
# 构建返回给前端的数据
response_data = {
'sjzhzhuangtai': shangjia.zhuangtai, # 商家账号状态
'fadanzong': shangjia.fabu, # 发单总量
'tuikuanzong': shangjia.tuikuan, # 退款总量
'sjyue': float(shangjia.yue), # 商家余额
'riliushui': float(shangjia.jinriliushui), # 今日流水
'yueliushui': float(shangjia.jinyueliushui), # 今月流水
}
return Response({
'code': 200,
'msg': '获取商家信息成功',
'data': response_data
})
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, 'yonghuid'):
return Response({
'code': 401,
'msg': '用户未认证或用户信息不完整'
}, status=401)
# 3. 生成标识:Boss + yonghuid(与前端约定)
boss_biaoshi = f"Boss{user.yonghuid}"
# 4. 返回给前端
return Response({
'code': 200,
'msg': '获取成功',
'data': {
'boss_biaoshi': boss_biaoshi,
'yonghuid': user.yonghuid,
'user_type': user.user_type
}
})
"""
阿龙电竞陪玩平台 - 邀请码相关API接口
"""
from utils.yaoqingma_utils import chuangjianYaoqingma, yanzhengYaoqingmaWeiyixing
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.guanshi_profile
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.yonghuid)
new_yaoqingma = chuangjianYaoqingma(yonghuid_str)
# 5. 验证生成的邀请码格式
if not yanzhengYaoqingmaWeiyixing(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接口
"""
from shangpin.models import Huiyuangoumai # 导入会员购买模型
from utils.yaoqingma_utils import chuangjianYaoqingma # 导入之前写的邀请码生成算法
from shangpin.models import Huiyuangoumai # 导入会员购买模型
from utils.yaoqingma_utils import chuangjianYaoqingma # 导入之前写的邀请码生成算法
import traceback
_dashou_zhuce_logger = logging.getLogger('yonghu.dashouzhuce')
class DashouZhuceView(APIView):
"""
打手注册接口 (通过管事邀请码)
访问路径: /api/yonghu/dashouzhuce/
请求方法: POST
权限要求: JWT Token认证通过的用户
请求体: { "inviteCode": "AbC123..." }
"""
permission_classes = [IsAuthenticated]
def _log_dashou_zhuce_error(self, invite_code, yonghuid, exc):
"""500 时打印完整堆栈,便于排查(gunicorn/1panel 可抓 stderr)。"""
err_text = traceback.format_exc()
_dashou_zhuce_logger.exception(
'打手注册失败 inviteCode=%s yonghuid=%s error=%s',
invite_code, yonghuid, exc,
)
print(
f'[DashouZhuce ERROR] inviteCode={invite_code} yonghuid={yonghuid} error={exc}\n{err_text}',
flush=True,
)
def post(self, request):
yaoqingma = ''
current_user = request.user
yonghuid = getattr(current_user, 'yonghuid', '')
try:
from utils.dashou_register_service import (
create_dashou_for_user,
resolve_guanshi_for_dashou_register,
)
# 1. 解析邀请码(支持无邀请码走默认码)
yaoqingma = request.data.get('inviteCode', '')
guanshi_profile, invite_err = resolve_guanshi_for_dashou_register(yaoqingma)
if invite_err:
return Response({
'code': 404,
'message': invite_err,
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 2. 检查用户是否已是打手 (使用反向查询,性能最优)
try:
dashou_profile = current_user.dashou_profile
# 用户已是打手,直接返回现有信息
return self.fanhuiXianyouDashouXinxi(dashou_profile, current_user)
except UserDashou.DoesNotExist:
# 用户不是打手,继续注册流程
pass
# 3. 创建打手身份
dashou_profile = create_dashou_for_user(current_user, guanshi_profile)
# 4. 注册成功,返回信息
fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=True)
return Response({
'code': 200,
'message': '注册成功!已开通打手、商家、管事身份。',
'data': fanhui_data
})
except Exception as e:
self._log_dashou_zhuce_error(yaoqingma, yonghuid, e)
return Response({
'code': 500,
'message': f'注册过程中发生系统错误: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
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.objects.filter(yonghu_id=current_user.yonghuid)
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
data['dashoustatus'] = 1
data['guanshistatus'] = 1 if UserGuanshi.objects.filter(user=current_user).exists() else 0
data['shangjiastatus'] = 1 if UserShangjia.objects.filter(user=current_user).exists() else 0
return data
from django.core.paginator import Paginator, EmptyPage
class GuanshiYaoqingDashouListView(APIView):
"""
管事获取已邀请打手列表(支持筛选、搜索、分页)
POST /api/yonghu/gshqyqds/
Headers: Authorization: Bearer
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.yonghuid
# 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.objects.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__yonghuid__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('-create_time')
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.yonghuid,
'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.guanshi_profile.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)
from rest_framework.views import APIView
from rest_framework import permissions, status
from rest_framework.response import Response
from django.db import transaction, IntegrityError
from .models import UserMain, UserDashou, UserGuanshi, Tixianjilu
import os
import uuid
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
class TixianXinxiHuoquView(APIView):
"""
获取用户提现收款信息接口
返回用户已设置的手机号、收款码、收款账号
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
"""
处理获取收款信息的POST请求
直接返回用户当前的收款设置
"""
try:
# 从request.user获取当前用户
user_main = request.user
# 🔥【修改开始 - 使用Decimal,字段名是lilu】
# 查询打手提现费率 (fadanpingtai='5')
dashou_rate_obj = Lilubiao.objects.filter(fadanpingtai='5').first()
dashou_rate = decimal.Decimal(str(dashou_rate_obj.lilu)) if dashou_rate_obj else decimal.Decimal('0.00')
# 查询管事提现费率 (fadanpingtai='6')
guanshi_rate_obj = Lilubiao.objects.filter(fadanpingtai='6').first()
guanshi_rate = decimal.Decimal(str(guanshi_rate_obj.lilu)) if guanshi_rate_obj else decimal.Decimal('0.00')
# 🔥【修改结束】
# 构建返回数据
response_data = {
'txdianhua': user_main.phone if user_main.phone else '',
'txtupian': user_main.zhifu if user_main.zhifu else '',
'txzh': user_main.skzhanghao if user_main.skzhanghao 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.yonghuid
# 准备响应数据
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.skzhanghao = txzh if txzh else ''
# 只有上传了图片才更新zhifu字段
if txtupian_url:
user_main.zhifu = 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:
import traceback
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.yonghuid
if not yonghuid:
return None
# 获取旧收款码URL
old_shoukuanma_url = user_main.zhifu
# 生成文件扩展名
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
from rest_framework.views import APIView
from rest_framework import permissions, status
from rest_framework.response import Response
from django.db import transaction, IntegrityError
from django.db.models import F
import decimal
import traceback
from .models import UserMain, UserDashou, UserGuanshi, Tixianjilu
from rest_framework.views import APIView
from rest_framework import permissions, status
from rest_framework.response import Response
from django.db import transaction, IntegrityError
from django.db.models import F
import decimal
import traceback
from .models import UserMain, UserDashou, UserGuanshi, Tixianjilu,UserShenheguan
from utils.fadan_utils import check_fadan_qiangdan_eligible
# ========== 订单状态常量(请根据您的实际业务修改)==========
# 假设状态值:已完成=9,已退款=10(示例,实际以您的项目为准)
ORDER_STATUS_COMPLETED = 3 # 订单已完成
ORDER_STATUS_REFUNDED = 5 # 订单已退款
# 进行中状态(不可提现)例如:待接单、已接单、进行中等,您需要定义排除列表
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.yonghuid
# 1. 获取并验证参数
leixing = request.data.get('leixing')
jine = request.data.get('jine')
fangshi = request.data.get('fangshi')
txskfs = request.data.get('txskfs', 0)
# 提前解析 leixing:商家提现(leixing=6)不校验罚单
raw_leixing = request.data.get('leixing')
try:
leixing_preview = int(raw_leixing) if raw_leixing is not None else None
except (ValueError, TypeError):
leixing_preview = None
if leixing_preview != 6:
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 = Lilubiao.objects.filter(fadanpingtai=rate_key).first()
if rate_obj:
current_rate = decimal.Decimal(str(rate_obj.lilu))
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.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.zhifu or ''
skzhanghao = user_main.skzhanghao or ''
tixian_record = Tixianjilu.objects.create(
yonghuid=yonghuid,
avatar=avatar,
phone=phone,
nicheng=nicheng,
leixing=leixing,
zhifu=zhifu,
skzhanghao=skzhanghao,
jine=shijidaozhang,
shenqing_jine=jine,
shouxufei=shouxufei,
zhuangtai=1,
fangshi=fangshi,
dakuan_mode=1,
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,
'create_time': tixian_record.create_time.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.objects.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.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.objects.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.objects.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.objects.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': {}}
from django.db.models import Q
def _check_shangjia_withdraw(self, user_main, jine):
"""商家余额提现校验:身份、账号状态、余额。"""
try:
shangjia = UserShangjia.objects.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}'}
return {'code': 0, 'msg': 'ok', 'data': {'nicheng': shangjia.nicheng or ''}}
def _check_dashou_yajin_withdraw(self, user_main, jine):
"""
打手押金提现校验(退出平台)
要求:无进行中的订单,无未结算订单,自身罚单需已处理
"""
try:
dashou = UserDashou.objects.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 = Dingdan.objects.filter(
jiedan_dashou_id=user_main.yonghuid,
).exclude(
zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]
).exists()
if unfinished_orders:
return {'code': 54, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提取押金'}
# 自身罚单检查
self_fadan = Fadan.objects.filter(
beichufa_id=user_main.yonghuid,
).exclude(
zhuangtai__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:
from .models import TixianAutoRecord, TixianShenheJilu
user_main = request.user
yonghuid = user_main.yonghuid
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.objects.filter(yonghuid=yonghuid)
if mode == 2:
# 自动提现:含新审核关联记录;zhuangtai>0 时按状态筛选
if zhuangtai_filter > 0:
qs = qs.filter(zhuangtai=zhuangtai_filter)
qs = qs.order_by('-create_time')
records_queryset = qs.values(
'id', 'leixing', 'jine', 'zhuangtai', 'create_time', '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.objects.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.objects.filter(
shenhe_danhao__in=danhao_list, zhuangtai=2
).order_by('-create_time').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['create_time'].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
import jwt
import datetime
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一次性获取管理员扩展表
# 只查询管理员类型的用户
user = UserMain.objects.filter(
phone=zhanghao, # 手机号作为账号
password=mima, # 密码
user_type='admin' # 必须是管理员类型
).select_related('admin_profile').first()
# 如果用户不存在或不是管理员,返回401
if not user:
return Response(
{'code': 401, 'msg': '登录失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 【优化点2】检查管理员扩展表是否存在
if not hasattr(user, 'admin_profile'):
return Response(
{'code': 401, 'msg': '登录失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 【优化点3】验证二次密码(管理员扩展表中的password字段)
admin_profile = user.admin_profile
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个字符(保持兼容性)
if len(kehuduan_ip) > 11:
# 如果ip字段长度限制是11,我们截取前11个字符
# 建议在数据库中修改ip字段为50长度
cunchu_ip = kehuduan_ip[:11]
else:
cunchu_ip = kehuduan_ip
# 更新用户信息
user.ip = cunchu_ip # 存储客户端IP
user.last_login_time = 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.yonghuid,
'phone': user.phone or '',
'user_type': user.user_type,
'last_login_time': now_time.strftime('%Y-%m-%d %H:%M:%S') if now_time else '',
}
# 可以添加管理员特定信息
if hasattr(user, 'admin_profile'):
response_data['wechat'] = user.admin_profile.wechat or ''
return Response({
'code': 0,
'msg': '登录成功',
'data': response_data
})
except Exception as e:
# 【安全点】任何异常都返回401,不暴露具体错误信息
# 记录日志用于排查问题,但前端只收到通用错误信息
# 这里可以添加日志记录,比如:
# import logging
# logging.error(f'管理员登录异常: {str(e)}', exc_info=True)
return Response(
{'code': 401, 'msg': '登录失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# yonghu/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from django.db import transaction, IntegrityError
from django.core.exceptions import ObjectDoesNotExist
import json
# 导入模型
from yonghu.models import UserMain, AdminProfile
from peizhi.models import Szjilu
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就是UserMain实例)
dangqianyonghu = request.user
# 3. 验证用户类型是否为admin
if not hasattr(dangqianyonghu, 'user_type') or dangqianyonghu.user_type != '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一次性获取管理员扩展表
# 注意:这里假设user_type='admin'的用户都有admin_profile
yonghu_xiangxi = UserMain.objects.select_related('admin_profile').get(
pk=dangqianyonghu.pk,
user_type='admin'
)
# 检查管理员扩展表是否存在
if not hasattr(yonghu_xiangxi, 'admin_profile'):
return Response(
{'code': 401, 'msg': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
except UserMain.DoesNotExist:
return Response(
{'code': 401, 'msg': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 查询收支记录表(Szjilu)
# 使用高效的ORM查询,获取最新的一条记录
# 注意:根据你的业务需求,这里获取最新的一条记录
# 如果需要汇总多条记录,可以使用aggregate进行聚合查询
# 方法1:获取最新的一条记录(假设表中只有一条汇总记录)
shouzhijilu = Szjilu.objects.order_by('-create_time').first()
# 方法2:如果表中可能有多条记录,需要汇总,可以使用以下方式:
# from django.db.models import Sum
# zonghe = Szjilu.objects.aggregate(
# zongliushui=Sum('zongls'),
# zongshouyi=Sum('zongsy'),
# zongzhichu=Sum('zongzc'),
# jrls=Sum('jrls'),
# jrzc=Sum('jrzc')
# )
# 准备返回数据
if shouzhijilu:
# 有记录的情况
huizongshuju = {
'zongliushui': float(shouzhijilu.zongls or 0),
'zongshouyi': float(shouzhijilu.zongsy or 0),
'zongzhichu': float(shouzhijilu.zongzc or 0),
'jrls': float(shouzhijilu.jrls or 0),
'jrzc': float(shouzhijilu.jrzc 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,不暴露具体错误信息
# 这里可以记录日志,但不返回给前端
# import logging
# logging.error(f'管理员财务数据接口异常: {str(e)}', exc_info=True)
return Response(
{'code': 401, 'msg': '获取失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# shangpin/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
import logging
logger = logging.getLogger(__name__)
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.user_type != 'admin':
return Response(
{'code': 400, 'message': '参数不完整', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 3. 验证管理员扩展表
try:
guanliyuan = dangqianyonghu.admin_profile
except AttributeError:
return Response(
{'code': 400, 'message': '参数不完整', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 4. 查询订单类型(商品类型)表
# 注意:这里需要根据你实际的模型名调整
# 如果模型名不是ShangpinType,请告诉我正确的模型名
# 假设模型名是ShangpinLeixing(因为我之前看到你有商品类型表)
# 你需要告诉我确切的模型名
try:
# 请替换为你的实际模型名
from shangpin.models import ShangpinLeixing # 或者 ShangpinType
# 只查询前端需要的三个字段
shangpin_types = ShangpinLeixing.objects.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
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
import logging
from django.db.models import Q, Count
import time
logger = logging.getLogger(__name__)
class AdGetDingdanList(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.user_type != 'admin':
return Response(
{'code': 400, 'message': '参数不完整', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 3. 验证管理员扩展表
try:
guanliyuan = dangqianyonghu.admin_profile
except AttributeError:
return Response(
{'code': 400, 'message': '参数不完整', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 4. 导入订单模型
from dingdan.models import Dingdan # 你的订单主表模型
# 5. 构建查询条件(严格按前端字段名映射到数据库字段)
query_conditions = Q()
# 判断是否为搜索模式
is_search_mode = bool(dingdan_id)
if is_search_mode:
# 搜索模式:只按订单ID查询
query_conditions &= Q(dingdan_id=dingdan_id)
else:
# 筛选模式
# 发单平台筛选(前端: fadanpingtai -> 数据库: fadan_pingtai)
if fadanpingtai is not None:
try:
fadanpingtai_int = int(fadanpingtai)
if fadanpingtai_int in [1, 2]:
query_conditions &= Q(fadan_pingtai=fadanpingtai_int)
except (ValueError, TypeError):
pass
# 订单类型筛选(前端: leixing -> 数据库: leixing_id)
if leixing is not None:
try:
leixing_int = int(leixing)
query_conditions &= Q(leixing_id=leixing_int)
except (ValueError, TypeError):
pass
# 订单状态筛选(前端: zhuangtai -> 数据库: zhuangtai)
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(zhuangtai__in=status_list)
except (ValueError, TypeError):
pass
# 6. 获取统计数据(平台订单和商家订单)
# 注意:这里统计所有订单,不考虑筛选条件
try:
# 平台订单统计 (fadan_pingtai=1)
platform_total = Dingdan.objects.filter(fadan_pingtai=1).count()
platform_completed = Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=3).count()
# 商家订单统计 (fadan_pingtai=2)
merchant_total = Dingdan.objects.filter(fadan_pingtai=2).count()
merchant_completed = Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=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 = Dingdan.objects.filter(query_conditions).count()
# 查询当前页的数据(只查询主表,只查询前端需要的字段)
dingdan_list = Dingdan.objects.filter(query_conditions).only(
'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id',
'fadan_pingtai', 'jine', 'tupian', 'create_time'
).order_by('-create_time')[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.dingdan_id or '',
'jieshao': dingdan.jieshao or '',
'zhuangtai': dingdan.zhuangtai or 1,
'leixing': dingdan.leixing_id or 0, # 前端需要 leixing
'fadanpingtai': dingdan.fadan_pingtai or 1, # 前端需要 fadanpingtai
'jiage': float(dingdan.jine) if dingdan.jine else 0.00, # 价格对应 jine 字段
'tupian_url': dingdan.tupian or '', # 图片对应 tupian 字段
'fadanshijian': dingdan.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan.create_time 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
)
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework import status
from django.db import transaction, IntegrityError
import logging
# 获取日志器
logger = logging.getLogger(__name__)
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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.admin_profile
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.guanshi_profile
except AttributeError:
# 管事扩展表不存在,创建一个
try:
from yonghu.models import UserGuanshi
# 创建默认的管事扩展表
guanshi_profile = UserGuanshi.objects.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.yonghuid}创建了管事扩展表")
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:
# 导入邀请码生成工具
from utils.yaoqingma_generator import chuangjianYaoqingma
# 生成邀请码
new_yaoqingma = chuangjianYaoqingma(str(yonghuid_str))
# 验证邀请码格式
from utils.yaoqingma_generator import yanzhengYaoqingmaWeiyixing
if not yanzhengYaoqingmaWeiyixing(new_yaoqingma):
raise Exception('邀请码格式验证失败')
except ImportError:
# 如果导入失败,使用简单方法生成邀请码
import time
import random
import string
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.yonghuid}生成邀请码: {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
)
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework import status
from django.db.models import Q
import logging
logger = logging.getLogger(__name__)
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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.admin_profile
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:
from yonghu.models import UserBoss, UserDashou, UserGuanshi, UserShangjia
stats = {
'1': UserBoss.objects.count(),
'2': UserDashou.objects.count(),
'3': UserGuanshi.objects.count(),
'4': UserShangjia.objects.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:
from yonghu.models import UserBoss
queryset = UserBoss.objects.select_related('user')
if keyword:
queryset = queryset.filter(
Q(user__yonghuid__icontains=keyword) |
Q(nickname__icontains=keyword)
)
# 获取总数
total_count = queryset.count()
# 获取当前页的数据
users = list(queryset.order_by('-create_time')[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.yonghuid,
'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:
from yonghu.models import UserDashou
queryset = UserDashou.objects.select_related('user')
if keyword:
queryset = queryset.filter(
Q(user__yonghuid__icontains=keyword) |
Q(nicheng__icontains=keyword) |
Q(chenghao__icontains=keyword)
)
# 获取总数
total_count = queryset.count()
# 获取当前页的数据
dashou_list = list(queryset.order_by('-create_time')[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.yonghuid,
'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:
from yonghu.models import UserGuanshi
queryset = UserGuanshi.objects.select_related('user')
if keyword:
queryset = queryset.filter(
Q(user__yonghuid__icontains=keyword)
)
# 获取总数
total_count = queryset.count()
# 获取当前页的数据
guanshi_list = list(queryset.order_by('-create_time')[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.yonghuid,
'avatar': user_main.avatar or '',
'zhanghaozhuangtai': guanshi.zhuangtai,
'nicheng': f'管事{user_main.yonghuid}',
}
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:
from yonghu.models import UserShangjia
queryset = UserShangjia.objects.select_related('user')
if keyword:
queryset = queryset.filter(
Q(user__yonghuid__icontains=keyword) |
Q(nicheng__icontains=keyword)
)
# 获取总数
total_count = queryset.count()
# 获取当前页的数据
shangjia_list = list(queryset.order_by('-create_time')[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.yonghuid,
'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
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework import status
from django.db.models import Q
import logging
logger = logging.getLogger(__name__)
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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.admin_profile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 导入模型
from dingdan.models import Chufajilu
# 7. 获取统计信息(总是需要统计)
zongshu = Chufajilu.objects.count()
daichuli = Chufajilu.objects.filter(sqzhuangtai__in=[0, 3]).count()
yichuli = Chufajilu.objects.filter(sqzhuangtai__in=[1, 2]).count()
# 8. 如果只请求统计信息,直接返回
if qingqiu_tongji:
return Response({
'code': 0,
'message': '成功',
'data': {
'zongshu': zongshu,
'daichuli': daichuli,
'yichuli': yichuli
}
})
# 9. 构建查询集
queryset = Chufajilu.objects.all()
# 10. 根据状态筛选
if sqzhuangtai is not None:
if isinstance(sqzhuangtai, list):
# 如果是数组,表示多个状态
queryset = queryset.filter(sqzhuangtai__in=sqzhuangtai)
else:
# 单个状态
queryset = queryset.filter(sqzhuangtai=sqzhuangtai)
# 11. 获取总数(用于分页计算)
total_count = queryset.count()
# 12. 计算分页偏移量
offset = (page - 1) * page_size
# 13. 分页查询
chufa_list = list(queryset.order_by('-create_time')[offset:offset + page_size])
# 🔴【新增】高效获取图片信息
if chufa_list:
# 收集所有需要查询的组合
dingdan_ids = []
yonghu_combinations = [] # 保存 (dingdan_id, yonghuid) 组合
for chufa in chufa_list:
if chufa.dingdan_id:
dingdan_ids.append(chufa.dingdan_id)
# 商家图片组合
if chufa.qingqiuid:
yonghu_combinations.append((chufa.dingdan_id, chufa.qingqiuid))
# 打手图片组合
if chufa.dashouid:
yonghu_combinations.append((chufa.dingdan_id, chufa.dashouid))
# 去重
dingdan_ids = list(set(dingdan_ids))
yonghu_combinations = list(set(yonghu_combinations))
# 🔴【新增】批量查询图片表(高效查询,避免循环)
tupian_mapping = {}
if yonghu_combinations:
# 导入图片模型
from dingdan.models import Chufatupian
# 构建查询条件
from django.db.models import Q
query_conditions = Q()
for dingdan_id, yonghuid in yonghu_combinations:
query_conditions |= Q(dingdan_id=dingdan_id, yonghuid=yonghuid)
# 批量查询
tupian_queryset = Chufatupian.objects.filter(query_conditions).values(
'dingdan_id', 'yonghuid', 'tupian'
)
# 构建映射:key为"dingdan_id_yonghuid",value为图片URL列表
for item in tupian_queryset:
key = f"{item['dingdan_id']}_{item['yonghuid']}"
if key not in tupian_mapping:
tupian_mapping[key] = []
if item['tupian']:
tupian_mapping[key].append(item['tupian'])
else:
tupian_mapping = {}
# 🔴【修改】14. 构建返回数据 - 添加图片和申诉理由字段
chufa_data = []
for chufa in chufa_list:
# 🔴【新增】获取商家证据图片
zhengju_tupian = []
if chufa.dingdan_id and chufa.qingqiuid:
key = f"{chufa.dingdan_id}_{chufa.qingqiuid}"
zhengju_tupian = tupian_mapping.get(key, [])
# 🔴【新增】获取打手申诉图片
shensu_tupian = []
if chufa.dingdan_id and chufa.dashouid:
key = f"{chufa.dingdan_id}_{chufa.dashouid}"
shensu_tupian = tupian_mapping.get(key, [])
chufa_data.append({
'dashouid': chufa.dashouid,
'qingqiuid': chufa.qingqiuid,
'chuliid': chufa.chuliid,
'cfliyou': chufa.cfliyou,
'sqzhuangtai': chufa.sqzhuangtai,
'bhliyou': chufa.bhliyou,
'jifen': chufa.jifen,
'dingdan_id': chufa.dingdan_id,
'create_time': chufa.create_time,
'update_time': chufa.update_time,
# 🔴【新增】申诉理由字段
'ssliyou': chufa.ssliyou 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
)
'''from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework import status
from django.db.models import Q
import logging
logger = logging.getLogger(__name__)
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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.admin_profile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 导入模型
from dingdan.models import Chufajilu
# 7. 获取统计信息(总是需要统计)
zongshu = Chufajilu.objects.count()
daichuli = Chufajilu.objects.filter(sqzhuangtai=0).count()
yichuli = Chufajilu.objects.filter(sqzhuangtai__in=[1, 2]).count()
# 8. 如果只请求统计信息,直接返回
if qingqiu_tongji:
return Response({
'code': 0,
'message': '成功',
'data': {
'zongshu': zongshu,
'daichuli': daichuli,
'yichuli': yichuli
}
})
# 9. 构建查询集
queryset = Chufajilu.objects.all()
# 10. 根据状态筛选
if sqzhuangtai is not None:
if isinstance(sqzhuangtai, list):
# 如果是数组,表示多个状态
queryset = queryset.filter(sqzhuangtai__in=sqzhuangtai)
else:
# 单个状态
queryset = queryset.filter(sqzhuangtai=sqzhuangtai)
# 11. 获取总数(用于分页计算)
total_count = queryset.count()
# 12. 计算分页偏移量
offset = (page - 1) * page_size
# 13. 分页查询
chufa_list = list(queryset.order_by('-create_time')[offset:offset + page_size])
# 14. 构建返回数据
chufa_data = []
for chufa in chufa_list:
chufa_data.append({
'dashouid': chufa.dashouid,
'qingqiuid': chufa.qingqiuid,
'chuliid': chufa.chuliid,
'cfliyou': chufa.cfliyou,
'sqzhuangtai': chufa.sqzhuangtai,
'bhliyou': chufa.bhliyou,
'jifen': chufa.jifen,
'dingdan_id': chufa.dingdan_id,
'create_time': chufa.create_time,
'update_time': chufa.update_time
})
# 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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.admin_profile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 使用事务保证数据一致性
with transaction.atomic():
# 7. 导入模型
from dingdan.models import Chufajilu, Dingdan, DingdanShangjia
from yonghu.models import UserDashou
# 8. 🔴 修复:查询处罚记录(处理重复记录问题)
# 使用 filter().first() 而不是 get(),以防出现多条记录
chufajilu = Chufajilu.objects.filter(
dingdan_id=dingdan_id,
sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中状态
).order_by('-create_time').first() # 取最新的一条
if not chufajilu:
# 如果没有待处理的处罚记录,检查是否有已处理的
exists_processed = Chufajilu.objects.filter(dingdan_id=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.sqzhuangtai not in [0, 3]: # 🔴【修改】包括待处理(0)和申诉中(3)
return Response(
{'code': 400, 'message': '该处罚记录已处理', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 10. 🔴 修复:检查是否有重复的待处理/申诉中记录,并记录警告
duplicate_count = Chufajilu.objects.filter(
dingdan_id=dingdan_id,
sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中
).count()
if duplicate_count > 1:
# 记录警告日志
logger.warning(f"订单ID {dingdan_id} 存在 {duplicate_count} 条待处理/申诉中处罚记录,处理最新的一条")
# 🔴 可选:将其他重复的待处理/申诉中记录标记为已拒绝
# 防止用户重复提交相同订单的处罚申请
duplicate_records = Chufajilu.objects.filter(
dingdan_id=dingdan_id,
sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中
).exclude(id=chufajilu.id)
for dup in duplicate_records:
dup.sqzhuangtai = 2 # 已拒绝
dup.chuliid = qianzhanghao
dup.bhliyou = "系统自动拒绝:存在重复处罚记录"
dup.save()
# 11. 根据操作类型处理
if caozuo == 1:
# 同意处罚 - 不扣分,只改状态
# 11.1 检查打手是否存在
if not chufajilu.dashouid:
return Response(
{'code': 400, 'message': '被处罚打手ID不存在', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 11.2 查询打手扩展表(只是验证存在性)
try:
dashou = UserDashou.objects.get(user__yonghuid=chufajilu.dashouid)
except UserDashou.DoesNotExist:
return Response(
{'code': 404, 'message': '被处罚打手不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 🔴【修改】11.3 同意处罚时不扣分(因为申请时已经扣了),只更新状态
chufajilu.sqzhuangtai = 1 # 已处罚
chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号
chufajilu.save()
# 11.4 更新商家订单扩展表
try:
dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
if hasattr(dingdan, 'shangjia_kuozhan'):
shangjia_kuozhan = dingdan.shangjia_kuozhan
shangjia_kuozhan.sqzhuangtai = 1 # 已处罚
shangjia_kuozhan.save()
except (Dingdan.DoesNotExist, AttributeError):
# 如果订单或扩展表不存在,继续执行,不中断
pass
logger.info(f"同意处罚成功:订单ID={dingdan_id},打手ID={chufajilu.dashouid}")
elif caozuo == 2:
# 拒绝处罚 - 需要返还积分(加分)
# 🔴【新增】11.5 返还打手积分(加5分)
if chufajilu.jifen > 0:
try:
dashou = UserDashou.objects.get(user__yonghuid=chufajilu.dashouid)
# 🔴【新增】安全验证:必须积分小于等于5分才允许加分
if dashou.jifen <= 5: # 判断积分是否小于等于5分
dashou.jifen += chufajilu.jifen # 加5分
dashou.save()
logger.info(
f"拒绝处罚,给打手{dashou.user.yonghuid}返还积分{chufajilu.jifen}分,当前积分{dashou.jifen}")
else:
# 积分已超过5分,可能是异常情况,记录警告但不中断处理
logger.warning(
f"拒绝处罚时打手积分异常:打手ID={chufajilu.dashouid},当前积分{dashou.jifen}已超过5分,不再加分")
except UserDashou.DoesNotExist:
# 打手不存在,记录错误,但继续处理状态
logger.error(f"拒绝处罚时打手不存在:打手ID={chufajilu.dashouid}")
# 11.6 更新处罚记录
chufajilu.sqzhuangtai = 2 # 已拒绝
chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号
chufajilu.bhliyou = bohui_liyou # 拒绝理由
chufajilu.save()
# 11.7 更新商家订单扩展表
try:
dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
if hasattr(dingdan, 'shangjia_kuozhan'):
shangjia_kuozhan = dingdan.shangjia_kuozhan
shangjia_kuozhan.sqzhuangtai = 2 # 已拒绝
shangjia_kuozhan.bhliyou = bohui_liyou # 拒绝理由
shangjia_kuozhan.save()
except (Dingdan.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
)
'''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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.admin_profile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 使用事务保证数据一致性
with transaction.atomic():
# 7. 导入模型
from dingdan.models import Chufajilu, Dingdan, DingdanShangjia
from yonghu.models import UserDashou
# 8. 🔴 修复:查询处罚记录(处理重复记录问题)
# 使用 filter().first() 而不是 get(),以防出现多条记录
chufajilu = Chufajilu.objects.filter(
dingdan_id=dingdan_id,
sqzhuangtai=0 # 只查询待处理状态的记录
).order_by('-create_time').first() # 取最新的一条
if not chufajilu:
# 如果没有待处理的处罚记录,检查是否有已处理的
exists_processed = Chufajilu.objects.filter(dingdan_id=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.sqzhuangtai != 0:
return Response(
{'code': 400, 'message': '该处罚记录已处理', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 10. 🔴 修复:检查是否有重复的待处理记录,并记录警告
duplicate_count = Chufajilu.objects.filter(
dingdan_id=dingdan_id,
sqzhuangtai=0
).count()
if duplicate_count > 1:
# 记录警告日志
logger.warning(f"订单ID {dingdan_id} 存在 {duplicate_count} 条待处理处罚记录,处理最新的一条")
# 🔴 可选:将其他重复的待处理记录标记为已驳回
# 防止用户重复提交相同订单的处罚申请
duplicate_records = Chufajilu.objects.filter(
dingdan_id=dingdan_id,
sqzhuangtai=0
).exclude(id=chufajilu.id)
for dup in duplicate_records:
dup.sqzhuangtai = 2 # 已驳回
dup.chuliid = qianzhanghao
dup.bhliyou = "系统自动驳回:存在重复处罚记录"
dup.save()
# 11. 根据操作类型处理
if caozuo == 1:
# 同意处罚
# 11.1 检查打手是否存在
if not chufajilu.dashouid:
return Response(
{'code': 400, 'message': '被处罚打手ID不存在', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 11.2 查询打手扩展表
try:
dashou = UserDashou.objects.get(user__yonghuid=chufajilu.dashouid)
except UserDashou.DoesNotExist:
return Response(
{'code': 404, 'message': '被处罚打手不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 11.3 扣除打手积分(确保积分不为负数)
jifen_kouchu = chufajilu.jifen
if jifen_kouchu > 0:
# 检查积分是否足够
if dashou.jifen < jifen_kouchu:
# 如果积分不足,设置为0
dashou.jifen = 0
else:
dashou.jifen -= jifen_kouchu
dashou.save()
# 11.4 更新处罚记录
chufajilu.sqzhuangtai = 1 # 已处罚
chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号
chufajilu.save()
# 11.5 更新商家订单扩展表
try:
dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
if hasattr(dingdan, 'shangjia_kuozhan'):
shangjia_kuozhan = dingdan.shangjia_kuozhan
shangjia_kuozhan.sqzhuangtai = 1 # 已处罚
shangjia_kuozhan.save()
except (Dingdan.DoesNotExist, AttributeError):
# 如果订单或扩展表不存在,继续执行,不中断
pass
#logger.info(f"同意处罚成功:订单ID={dingdan_id},打手ID={chufajilu.dashouid},扣除积分={jifen_kouchu}")
elif caozuo == 2:
# 驳回处罚
# 11.6 更新处罚记录
chufajilu.sqzhuangtai = 2 # 已驳回
chufajilu.chuliid = qianzhanghao # 处理人ID为管理员账号
chufajilu.bhliyou = bohui_liyou # 驳回理由
chufajilu.save()
# 11.7 更新商家订单扩展表
try:
dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
if hasattr(dingdan, 'shangjia_kuozhan'):
shangjia_kuozhan = dingdan.shangjia_kuozhan
shangjia_kuozhan.sqzhuangtai = 2 # 已驳回
shangjia_kuozhan.bhliyou = bohui_liyou # 驳回理由
shangjia_kuozhan.save()
except (Dingdan.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
from django.utils import timezone
from datetime import timedelta
from decimal import Decimal
from django.db import transaction, IntegrityError
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
from .models import (
UserMain, UserBoss, UserDashou, UserShangjia, UserGuanshi,
AdminProfile, Xiugaijilu
)
from shangpin.models import Huiyuan, Huiyuangoumai
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.user_type != 'admin':
return Response({
'code': 403,
'message': '权限不足',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 3. 验证管理员扩展表存在
admin_profile = getattr(request.user, 'admin_profile', 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 = UserMain.objects.filter(yonghuid=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.yonghuid,
'avatar': user_main.avatar,
'phone': user_main.phone,
'ip': user_main.ip,
'create_time': user_main.create_time,
'update_time': user_main.update_time,
'user_type': user_type
}
# 根据用户类型查询扩展信息
if user_type == 1: # 老板
# 使用select_related预取boss_profile
user_main = UserMain.objects.filter(yonghuid=uid).select_related('boss_profile').first()
if user_main.boss_profile:
boss_profile = user_main.boss_profile
user_info.update({
'nickname': boss_profile.nickname,
'zonge': boss_profile.zonge,
'alldingdan': boss_profile.alldingdan,
'alltui': boss_profile.alltui,
'create_time_boss': boss_profile.create_time
})
elif user_type == 2: # 打手
# 使用select_related预取dashou_profile
user_main = UserMain.objects.filter(yonghuid=uid).select_related('dashou_profile').first()
if user_main.dashou_profile:
dashou_profile = user_main.dashou_profile
# 构建打手信息
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,
'create_time_dashou': dashou_profile.create_time
})
# 查询打手的会员信息(只返回未过期的)
# 使用select_related预取huiyuan信息(如果有关联的话)
member_records = Huiyuangoumai.objects.filter(
yonghu_id=uid
).order_by('-create_time')
member_list = []
for record in member_records:
# 调用方法检查是否过期,并更新状态
is_expired = record.jiance_shifou_daoqi()
if not is_expired: # 只返回未过期的
# 查询会员详情
huiyuan = Huiyuan.objects.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,
'create_time': record.create_time
})
response_data['member_list'] = member_list
elif user_type == 3: # 管事
# 使用select_related预取guanshi_profile
user_main = UserMain.objects.filter(yonghuid=uid).select_related('guanshi_profile').first()
if user_main.guanshi_profile:
guanshi_profile = user_main.guanshi_profile
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, # 管事可提现余额
'create_time_guanshi': guanshi_profile.create_time
})
elif user_type == 4: # 商家
# 使用select_related预取shop_profile
user_main = UserMain.objects.filter(yonghuid=uid).select_related('shop_profile').first()
if user_main.shop_profile:
shop_profile = user_main.shop_profile
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,
'create_time_shop': shop_profile.create_time
})
# 获取所有会员列表(用于前端下拉选择)
all_huiyuan = Huiyuan.objects.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.user_type != 'admin':
return Response({
'code': 403,
'message': '权限不足',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
admin_profile = getattr(request.user, 'admin_profile', 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 = UserMain.objects.filter(yonghuid=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': '',
'create_time': timezone.now()
}
# 标记是否有敏感字段被修改
has_sensitive_change = False
# 🔒 判断当前管理员是否有增加权限
from django.conf import settings
allow_increase = zhanghao in getattr(settings, 'ALLOW_INCREASE_ADMINS', [])
if user_type == 1: # 老板 - 不允许修改
return Response({
'code': 403,
'message': '老板信息不允许修改',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
elif user_type == 2: # 打手
# 获取或创建打手扩展记录
dashou_profile, created = UserDashou.objects.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', 'jifen', 'yajin']:
try:
# 转换为对应类型以便比较
if field in ['yue', 'zonge', 'yajin']:
new_num = Decimal(str(new_value))
old_num = Decimal(str(old_value))
else: # jifen
new_num = int(new_value)
old_num = int(old_value)
# 如果不允许增加,且新值大于旧值,则拒绝
if not allow_increase and new_num > old_num:
return Response({
'code': 403,
'message': f'您无权增加{field},只能减少或保持不变',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
except Exception:
# 类型转换失败则拒绝修改
return Response({
'code': 400,
'message': f'{field}值格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 处理特殊字段类型
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.objects.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 ['yue', 'chongzhifenrun']:
try:
new_num = Decimal(str(new_value))
old_num = Decimal(str(old_value))
if not allow_increase and new_num > old_num:
return Response({
'code': 403,
'message': f'您无权增加{field},只能减少或保持不变',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
except Exception:
return Response({
'code': 400,
'message': f'{field}值格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
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.objects.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':
try:
new_num = Decimal(str(new_value))
old_num = Decimal(str(old_value))
if not allow_increase and new_num > old_num:
return Response({
'code': 403,
'message': '您无权增加商家余额,只能减少或保持不变',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
except Exception:
return Response({
'code': 400,
'message': '余额值格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
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.objects.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.objects.filter(
yonghu_id=uid,
huiyuan_id=huiyuan_id
).first()
# 🔒 权限控制:仅允许增加的管理员可增加会员天数
if not allow_increase and days > 0:
return Response({
'code': 403,
'message': '您无权增加会员天数,只能减少或保持不变',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
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.objects.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.objects.create(**modify_record_data)
elif modify_record_data.get('qitashuoming', '').strip():
# 只有非敏感字段修改,也记录但qitashuoming不同
Xiugaijilu.objects.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)
'''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.user_type != 'admin':
return Response({
'code': 403,
'message': '权限不足',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
admin_profile = getattr(request.user, 'admin_profile', 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 = UserMain.objects.filter(yonghuid=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': '',
'create_time': 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.objects.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.objects.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.objects.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.objects.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.objects.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.objects.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.objects.create(**modify_record_data)
elif modify_record_data.get('qitashuoming', '').strip():
# 只有非敏感字段修改,也记录但qitashuoming不同
Xiugaijilu.objects.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)'''
# yonghu/views.py
from django.utils import timezone
from django.db.models import Q
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
from .models import UserMain, AdminProfile, Tixianjilu
# 管理员认证装饰器
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.user_type != 'admin':
return Response({
'code': 403,
'message': '权限不足',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 3. 验证管理员扩展表存在
admin_profile = getattr(request.user, 'admin_profile', 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
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.utils import timezone
from django.db.models import Q
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
from yonghu.models import Tixianjilu
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.objects.filter(query).order_by('create_time')
# 🔴【修改】分页逻辑:如果是搜索模式,不分页,返回所有匹配记录
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.objects.filter(
zhuangtai=1,
leixing=leixing
).count(),
'processed': Tixianjilu.objects.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,
'create_time': record.create_time,
'update_time': record.update_time
})
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 AddtxshView(APIView):
"""
获取提现审核列表接口
与用户管理页面保持一致的分页逻辑
"""
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')
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))
# 获取查询集(与用户管理页面一致)
queryset = Tixianjilu.objects.filter(query).order_by('-update_time', '-create_time')
# 获取总数
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.objects.filter(
zhuangtai=1,
leixing=leixing
).count(),
'processed': Tixianjilu.objects.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,
'create_time': record.create_time,
'update_time': record.update_time
})
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)'''
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from django.db import transaction, IntegrityError
from django.utils import timezone
from peizhi.models import Szjilu # 添加收支记录表导入
# 假设你的提现记录模型导入
# from your_app.models import Tixianjilu
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.objects.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.yonghuid
if result == 3: # 拒绝
user_main = UserMain.objects.get(yonghuid=withdrawal.yonghuid)
from yonghu.tixian_shenhe_services import refund_manual_tixianjilu
refund_manual_tixianjilu(user_main, withdrawal)
withdrawal.bhliyou = reason
withdrawal.update_time = timezone.now()
withdrawal.save()
# 🟢 新增:如果同意提现(result=2),更新收支记录表
if result == 2:
try:
tixian_jine = withdrawal.jine
szjilu_record = Szjilu.objects.get(id=1)
from dingdan.utils import update_daily_payout
from yonghu.tixian_shenhe_services import record_manual_withdraw_success_stats
update_daily_payout(tixian_jine)
user_main = UserMain.objects.get(yonghuid=withdrawal.yonghuid)
record_manual_withdraw_success_stats(user_main, withdrawal)
szjilu_record.zongsy -= tixian_jine
szjilu_record.zongzc += tixian_jine # 总支出加上提现金额
szjilu_record.jrzc += tixian_jine # 今日支出加上提现金额
szjilu_record.update_time = timezone.now()
szjilu_record.save()
except Szjilu.DoesNotExist:
# 如果收支记录表不存在id=1的记录,创建一个
szjilu_record = Szjilu.objects.create(
id=1,
zongsy=-tixian_jine if result == 2 else 0,
zongzc=tixian_jine if result == 2 else 0,
jrzc=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,
'update_time': withdrawal.update_time
}
}, 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.objects.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)
# 更新提现记录
withdrawal.zhuangtai = result
withdrawal.shenheid = request.user.yonghuid
if result == 3: # 拒绝
withdrawal.bhliyou = reason
withdrawal.update_time = timezone.now()
withdrawal.save()
return Response({
'code': 0,
'message': '处理成功',
'data': {
'id': withdrawal.id,
'zhuangtai': withdrawal.zhuangtai,
'shenheid': withdrawal.shenheid,
'update_time': withdrawal.update_time
}
}, 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应用)
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from .models import UserMain, UserDashou
import logging
logger = logging.getLogger(__name__)
class DashouZiliaoHuoquView(APIView):
"""
打手资料获取接口
URL: /yonghu/dszjxxhq
方法: POST
认证: JWT Token
返回: 打手扩展表信息
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 使用select_related高效查询,避免N+1问题
dashou = UserDashou.objects.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.yonghuid, # 从主表获取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应用)
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from .models import UserDashou
import re
import logging
logger = logging.getLogger(__name__)
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.objects.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.shop_profile
# 用户已是商家,直接返回现有信息
return self.fanhuiXianyouShangjiaXinxi(shangjia_profile, current_user)
except UserShangjia.DoesNotExist:
# 用户不是商家,继续注册流程
pass
# 3. 根据邀请码查找对应的管理员
try:
# 从管理员表查找邀请码
admin_profile = AdminProfile.objects.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.boss_profile
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.objects.filter(nicheng=shangjia_nicheng).exists():
# 如果存在,添加随机数确保唯一
import random
shangjia_nicheng = f"{boss_nickname}{random.randint(1000, 9999)}"
# 4.3 创建商家记录
shop_profile = UserShangjia.objects.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:
# 事务会自动回滚
import traceback
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.yonghuid, # 用户ID
}
return data
# yonghu/ranking_views.py
"""
排行榜查询接口 - 管理员专用
安全认证:JWT Token + 管理员权限验证
"""
from django.db.models import Q
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from datetime import datetime
from .models import RankingRecord
import logging
logger = logging.getLogger(__name__)
# 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.objects.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.objects.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.objects.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:
from utils.dashou_register_service import (
create_dashou_for_user,
ensure_guanshi_profile_for_user,
resolve_guanshi_for_dashou_register,
)
# 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)
guanshi_profile, invite_err = resolve_guanshi_for_dashou_register(yaoqingma)
if invite_err:
return Response({
'code': 2,
'msg': invite_err,
'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():
user_main, created = get_or_create_wechat_user(openid, user_type='normal')
update_wechat_user_login_info(user_main, kehuduan_ip)
if created:
# 新用户:创建老板扩展表
UserBoss.objects.create(user=user_main, nickname='微信用户')
# 验证管事状态
if guanshi_profile.zhuangtai != 1:
return self.fanhuiZhihouDengluXinxi(user_main, guanshi_jinyong=True)
# 4.3 检查用户是否已是打手
dashou_exists = UserDashou.objects.filter(user=user_main).exists()
if dashou_exists:
dashou_profile = UserDashou.objects.get(user=user_main)
return self.fanhuiDengluZhuceshuju(user_main, dashou_profile)
# 4.4 创建打手;扫码流程顺带开通管事(已存在则跳过)
dashou_profile = create_dashou_for_user(user_main, guanshi_profile)
ensure_guanshi_profile_for_user(user_main)
# 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:
# 记录错误日志
import logging
logger = logging.getLogger(__name__)
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 = wx_cfg.WEIXIN_APPID
secret = wx_cfg.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 UserMain.objects.filter(yonghuid=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.objects.filter(id__in=[1, 2])
result = {
'dashouqun': '',
'dashouqunid': '',
'guanshiqun': '',
'guanshiqunid': ''
}
for config in qun_configs:
if config.id == 1:
result['dashouqun'] = config.neirong or ''
result['dashouqunid'] = config.qunid or ''
elif config.id == 2:
result['guanshiqun'] = config.neirong or ''
result['guanshiqunid'] = config.qunid 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.objects.get(user=user_main)
if boss_profile.nickname:
boss_nickname = boss_profile.nickname
except UserBoss.DoesNotExist:
pass
# 检查各身份状态
dashou_status = 1 if UserDashou.objects.filter(user=user_main).exists() else 0
shangjia_status = 1 if UserShangjia.objects.filter(user=user_main).exists() else 0
guanshi_status = 1 if UserGuanshi.objects.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.yonghuid,
'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': 5,
'msg': msg,
'data': response_data
})
def fanhuiDengluZhuceshuju(self, user_main, dashou_profile):
"""
返回登录并注册的数据(用户已是打手的情况)
"""
# 获取老板昵称
boss_nickname = '微信用户'
try:
boss_profile = UserBoss.objects.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.objects.filter(user=user_main).exists() else 1 # 确保是1
guanshi_status = 1 if UserGuanshi.objects.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.yonghuid,
'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.objects.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.yonghuid,
'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.objects.filter(yonghu_id=dashou_profile.user.yonghuid)
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
from datetime import datetime # 🔴【关键修改】
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.yonghuid
# 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:
# 记录详细错误日志
import logging
logger = logging.getLogger(__name__)
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.yonghuid
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
from django.db.models import Q, Prefetch
from django.db import connection
from django.core.paginator import Paginator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from dingdan.models import Chufajilu, Chufatupian
from yonghu.models import UserDashou
import logging
logger = logging.getLogger(__name__)
class DashouPermission:
"""
打手权限验证类
确保用户是打手且账号状态正常
"""
def has_permission(self, request, view):
user = request.user
# 检查用户是否已认证
if not user.is_authenticated:
return False
try:
# 通过反向关系获取打手扩展信息
dashou_profile = user.dashou_profile
# 检查账号状态是否为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
from django.db.models import Q, Count, Case, When, IntegerField
logger = logging.getLogger(__name__)
class ChufaJiluHuoquView(APIView):
"""
打手获取处罚记录接口
路径: /yonghu/dshqcfjl
方法: POST
权限: JWT Token + 打手权限验证
"""
permission_classes = [IsAuthenticated, DashouPermission]
def post(self, request):
try:
# 获取当前用户信息
user_main = request.user
yonghuid = user_main.yonghuid
# 通过反向关系获取打手扩展信息
try:
dashou_profile = user_main.dashou_profile
# 再次确认账号状态
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 = Chufajilu.objects.filter(dashouid=yonghuid).aggregate(
# 总记录数
zongshu=Count('id'),
# 待处理数:状态为0(待处罚)和3(申诉中)
daichuli=Count(
Case(
When(Q(sqzhuangtai=0) | Q(sqzhuangtai=3), then=1),
output_field=IntegerField()
)
),
# 已处理数:状态为1(已处罚)和2(已驳回)
yichuli=Count(
Case(
When(Q(sqzhuangtai=1) | Q(sqzhuangtai=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)
# 🔴 构建查询条件 - 打手端根据dashouid查询
queryset = Chufajilu.objects.filter(dashouid=yonghuid)
if zhuangtai == 0:
# 待处理:状态为0(待处罚)和3(申诉中)
queryset = queryset.filter(Q(sqzhuangtai=0) | Q(sqzhuangtai=3))
elif zhuangtai == 1:
# 已处理:状态为1(已处罚)和2(已驳回)
queryset = queryset.filter(Q(sqzhuangtai=1) | Q(sqzhuangtai=2))
else:
# 默认返回所有状态
pass
# 按创建时间倒序排列(最新在前)
queryset = queryset.order_by('-create_time')
# 使用游标分页,避免传统分页的性能问题
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.dingdan_id:
dingdan_ids.append(record.dingdan_id)
if record.qingqiuid: # 商家ID
shangjia_ids.append(record.qingqiuid)
# 去重
dingdan_ids = list(set(dingdan_ids))
shangjia_ids = list(set(shangjia_ids))
# 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误)
# 1. 查询证据图片(商家上传的)
zhengju_mapping = {}
if dingdan_ids and shangjia_ids:
# 构建Q对象:dingdan_id在列表中 AND yonghuid在商家ID列表中
zhengju_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=shangjia_ids)
zhengju_queryset = Chufatupian.objects.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian')
for item in zhengju_queryset:
key = f"{item['dingdan_id']}_{item['yonghuid']}"
if key not in zhengju_mapping:
zhengju_mapping[key] = []
if item['tupian']:
zhengju_mapping[key].append(item['tupian'])
# 2. 查询申诉图片(打手上传的)
shensu_mapping = {}
if dingdan_ids:
# 构建Q对象:dingdan_id在列表中 AND yonghuid=当前打手ID
shensu_q = Q(dingdan_id__in=dingdan_ids, yonghuid=yonghuid)
shensu_queryset = Chufatupian.objects.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian')
for item in shensu_queryset:
key = f"{item['dingdan_id']}_{item['yonghuid']}"
if key not in shensu_mapping:
shensu_mapping[key] = []
if item['tupian']:
shensu_mapping[key].append(item['tupian'])
# 🔴【核心修复】组装返回数据 - 字段保持原样!
chufa_list = []
for record in chufa_records:
dingdan_id = record.dingdan_id
shangjia_id = record.qingqiuid
# 获取证据图片(商家上传的)
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.dingdan_id or '',
'qingqiuid': record.qingqiuid or '', # 商家ID
'cfliyou': record.cfliyou or '',
'sqzhuangtai': record.sqzhuangtai or 0,
'ssliyou': record.ssliyou or '',
'jifen': record.jifen or 0,
'create_time': record.create_time.strftime('%Y-%m-%d %H:%M:%S') if record.create_time else '',
'update_time': record.update_time.strftime('%Y-%m-%d %H:%M:%S') if record.update_time 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.yonghuid
# 🔴🆕【关键】验证打手账号状态
try:
dashou_profile = user_main.dashou_profile
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)
# 🔴🆕【关键】使用事务确保数据一致性
from django.db import transaction, IntegrityError
with transaction.atomic():
# 1. 查询处罚记录
try:
chufa_record = Chufajilu.objects.select_for_update().get(
id=chufa_id,
dashouid=yonghuid # 确保只能申诉自己的处罚记录
)
except Chufajilu.DoesNotExist:
return Response({
'code': 10,
'msg': '处罚记录不存在或无权申诉',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 2. 验证处罚记录状态(必须是待处理状态0)
if chufa_record.sqzhuangtai != 0:
return Response({
'code': 11,
'msg': '该处罚记录已处理,无法申诉',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 验证订单是否存在(可选,根据需求决定)
# 这里可以根据dingdan_id去查询订单表,但根据需求描述,我们已经有了处罚记录
# 处罚记录中包含了dingdan_id,如果dingdan_id不存在,说明订单可能被删除
# 但根据业务逻辑,有处罚记录就应该有订单,所以这里可以跳过这个检查
# 4. 检查是否已有申诉记录(防止重复申诉)
if chufa_record.ssliyou or chufa_record.sqzhuangtai == 3:
# 检查是否已经有申诉图片
existing_shensu_tupian = Chufatupian.objects.filter(
dingdan_id=chufa_record.dingdan_id,
yonghuid=yonghuid
).exists()
if existing_shensu_tupian:
return Response({
'code': 12,
'msg': '您已经提交过申诉,请等待处理结果',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 5. 更新处罚记录
chufa_record.ssliyou = shensu_liyou
chufa_record.sqzhuangtai = 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 = Chufatupian.objects.create(
dingdan_id=chufa_record.dingdan_id,
yonghuid=yonghuid,
tupian=tupian_url
)
saved_tupian_urls.append(tupian_url)
# 7. 构建返回数据
response_data = {
'chufa_id': chufa_record.id,
'dingdan_id': chufa_record.dingdan_id or '',
'shensu_liyou': shensu_liyou,
'shensu_tupian_urls': saved_tupian_urls,
'sqzhuangtai': chufa_record.sqzhuangtai,
'update_time': chufa_record.update_time.strftime(
'%Y-%m-%d %H:%M:%S') if chufa_record.update_time else ''
}
# 记录操作日志
logger.info(f"打手申诉成功 - 用户ID: {yonghuid}, 处罚ID: {chufa_id}, 订单ID: {chufa_record.dingdan_id}")
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.yonghuid
# 通过反向关系获取商家扩展信息
try:
shangjia_profile = user_main.shop_profile
# 再次确认账号状态
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:
# 🔴 查询条件:qingqiuid = 当前商家ID
stats = Chufajilu.objects.filter(qingqiuid=yonghuid).aggregate(
zongshu=Count('id'),
daichuli=Count(
Case(
When(Q(sqzhuangtai=0) | Q(sqzhuangtai=3), then=1),
output_field=IntegerField()
)
),
yichuli=Count(
Case(
When(Q(sqzhuangtai=1) | Q(sqzhuangtai=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)
# 🔴 查询条件:qingqiuid = 当前商家ID
queryset = Chufajilu.objects.filter(qingqiuid=yonghuid)
if zhuangtai == 0:
queryset = queryset.filter(Q(sqzhuangtai=0) | Q(sqzhuangtai=3))
elif zhuangtai == 1:
queryset = queryset.filter(Q(sqzhuangtai=1) | Q(sqzhuangtai=2))
else:
pass
# 按创建时间倒序排列
queryset = queryset.order_by('-create_time')
# 分页逻辑
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.dingdan_id:
dingdan_ids.append(record.dingdan_id)
if record.dashouid: # 打手ID
dashou_ids.append(record.dashouid)
# 去重
dingdan_ids = list(set(dingdan_ids))
dashou_ids = list(set(dashou_ids))
# 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误)
# 1. 查询证据图片(商家自己上传的)
zhengju_mapping = {}
if dingdan_ids:
# 构建Q对象:dingdan_id在列表中 AND yonghuid=当前商家ID
zhengju_q = Q(dingdan_id__in=dingdan_ids, yonghuid=yonghuid)
zhengju_queryset = Chufatupian.objects.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian')
for item in zhengju_queryset:
key = f"{item['dingdan_id']}_{item['yonghuid']}"
if key not in zhengju_mapping:
zhengju_mapping[key] = []
if item['tupian']:
zhengju_mapping[key].append(item['tupian'])
# 2. 查询申诉图片(打手上传的)
shensu_mapping = {}
if dingdan_ids and dashou_ids:
# 构建Q对象:dingdan_id在列表中 AND yonghuid在打手ID列表中
shensu_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=dashou_ids)
shensu_queryset = Chufatupian.objects.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian')
for item in shensu_queryset:
key = f"{item['dingdan_id']}_{item['yonghuid']}"
if key not in shensu_mapping:
shensu_mapping[key] = []
if item['tupian']:
shensu_mapping[key].append(item['tupian'])
# 🔴【核心修复】组装返回数据 - 字段保持原样!
chufa_list = []
for record in chufa_records:
dingdan_id = record.dingdan_id
dashou_id = record.dashouid
# 获取证据图片(商家自己上传的)
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.dingdan_id or '',
'qingqiuid': record.dashouid or '', # 打手ID
'cfliyou': record.cfliyou or '',
'sqzhuangtai': record.sqzhuangtai or 0,
'ssliyou': record.ssliyou or '',
'jifen': record.jifen or 0,
'create_time': record.create_time.strftime('%Y-%m-%d %H:%M:%S') if record.create_time else '',
'update_time': record.update_time.strftime('%Y-%m-%d %H:%M:%S') if record.update_time 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)
from .models import UserMain, UserKefu
logger = logging.getLogger(__name__)
class KefuLoginView(APIView):
"""
客服登录接口(支持手机号重复)
请求:POST /yonghu/kefujinru
请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"}
返回:
code=0 成功,data包含token,nicheng,yonghuid
code=4 账号被封禁(提示“账号已被封禁”)
其他错误统一 code=1 msg="登录失败"
"""
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 or not erjimima:
return Response({
'code': 1,
'msg': '登录失败', # 统一提示,不暴露具体原因
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 获取客户端真实IP
client_ip = self.get_client_ip(request)
# 3. 查询所有手机号匹配且类型为客服的用户
# 注意:UserMain 中需要有 user_type 字段,且值为 'kefu' 代表客服
user_mains = UserMain.objects.filter(
phone=phone,
user_type='kefu' # 只查找客服账号
).select_related('kefu_profile') # 预加载客服扩展表
if not user_mains.exists():
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
for user in user_mains:
# 验证主密码(明文比较,按您的要求)
if user.password != password:
continue
# 检查是否存在客服扩展表
try:
kefu = user.kefu_profile
except UserKefu.DoesNotExist:
continue
# 验证二级密码(明文比较)
if kefu.erjimima != 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)
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.last_login_time = timezone.now()
target_user.save(update_fields=['ip', 'last_login_time'])
# 6. 生成JWT token
refresh = RefreshToken.for_user(target_user)
token = str(refresh.access_token)
# 7. 返回成功数据
return Response({
'code': 0,
'msg': '登录成功',
'data': {
'token': token,
'nicheng': target_kefu.nicheng,
'yonghuid': target_user.yonghuid,
}
})
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.yonghuid}")
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.user_type != 'kefu':
logger.warning(f"统计接口用户类型非客服: {current_user.yonghuid}")
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 5. 尝试获取客服扩展表数据
try:
kefu_profile = current_user.kefu_profile # OneToOneField 反向关系
except UserKefu.DoesNotExist:
logger.warning(f"统计接口客服扩展表不存在: {current_user.yonghuid}")
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)
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
import logging
from django.core.exceptions import ObjectDoesNotExist
from yonghu.models import UserMain, UserKefu
from shangpin.models import ShangpinLeixing # 假设商品类型表名,请根据实际调整
logger = logging.getLogger(__name__)
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.user_type != 'kefu':
logger.warning(f"用户类型非客服: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu_profile = current_user.kefu_profile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu_profile.zhuangtai != 1:
logger.warning(f"客服账号已被禁用: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 4. 查询订单类型(商品类型)
try:
# 只查询需要的字段,减少数据传输
types_qs = ShangpinLeixing.objects.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': []})
import time
import logging
from django.db.models import Q, Count
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from yonghu.models import UserMain, UserKefu, UserBoss
from dingdan.models import Dingdan, DingdanPingtai
logger = logging.getLogger(__name__)
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from django.db.models import Q, Count
from django.core.paginator import Paginator
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(fadan_pingtai=1) & Q(is_cross=0)
if dingdan_id:
q_conditions = base_q & Q(dingdan_id=dingdan_id)
else:
q_conditions = base_q
if leixing is not None:
try:
leixing_int = int(leixing)
q_conditions &= Q(leixing_id=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(zhuangtai__in=status_list)
except (ValueError, TypeError):
pass
if dashou_id:
q_conditions &= Q(jiedan_dashou_id=dashou_id)
if laoban_id:
q_conditions &= Q(pingtai_kuozhan__laoban_id=laoban_id)
if date:
q_conditions &= Q(create_time__date=date)
# 新增筛选条件
if nicheng:
q_conditions &= Q(nicheng__icontains=nicheng)
if jieshao:
q_conditions &= Q(jieshao__icontains=jieshao)
if clkf:
q_conditions &= Q(clkf__icontains=clkf)
# ---------- 统计(仅按发单平台,非跨平台) ----------
stats = Dingdan.objects.filter(fadan_pingtai=1, is_cross=0).aggregate(
total_orders=Count('id'),
completed_orders=Count('id', filter=Q(zhuangtai=3)),
refund_orders=Count('id', filter=Q(zhuangtai=5)),
pending_orders=Count('id', filter=Q(zhuangtai__in=[4,8]))
)
# ---------- 状态计数(所有状态都返回,非跨平台) ----------
status_counts = {
'all': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0).count(),
'1,7': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai__in=[1,7]).count(),
'2': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=2).count(),
'8': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=8).count(),
'3': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=3).count(),
'4': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=4).count(),
'5': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=5).count(),
'6': Dingdan.objects.filter(fadan_pingtai=1, is_cross=0, zhuangtai=6).count(),
}
# ---------- 分页查询 ----------
total_count = Dingdan.objects.filter(q_conditions).count()
orders_qs = Dingdan.objects.filter(q_conditions).select_related(
'pingtai_kuozhan'
).only(
'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', 'fadan_pingtai',
'jine', 'tupian', 'create_time', 'nicheng', 'jiedan_dashou_id',
'pingtai_kuozhan__laoban_id'
).order_by('-create_time')[(page-1)*page_size : page*page_size]
# ---------- 获取老板昵称 ----------
laoban_ids = [o.pingtai_kuozhan.laoban_id for o in orders_qs if hasattr(o, 'pingtai_kuozhan') and o.pingtai_kuozhan.laoban_id]
nickname_map = {}
if laoban_ids:
users = UserMain.objects.filter(yonghuid__in=laoban_ids).select_related('boss_profile').only(
'yonghuid', 'boss_profile__nickname'
)
for user in users:
nickname_map[user.yonghuid] = user.boss_profile.nickname or '' if hasattr(user, 'boss_profile') else ''
result_list = []
for order in orders_qs:
ext = getattr(order, 'pingtai_kuozhan', None)
laoban_id = ext.laoban_id if ext else ''
youxi_nicheng = nickname_map.get(laoban_id, order.nicheng or '')
result_list.append({
'dingdan_id': order.dingdan_id or '',
'jieshao': order.jieshao or '',
'zhuangtai': order.zhuangtai,
'leixing': order.leixing_id,
'fadanpingtai': order.fadan_pingtai,
'jiage': float(order.jine) if order.jine else 0.00,
'tupian_url': order.tupian or '',
'fadanshijian': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time 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(fadan_pingtai=2) & Q(is_cross=0)
if dingdan_id:
q_conditions = base_q & Q(dingdan_id=dingdan_id)
else:
q_conditions = base_q
if leixing is not None:
try:
leixing_int = int(leixing)
q_conditions &= Q(leixing_id=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(zhuangtai__in=status_list)
except (ValueError, TypeError):
pass
if dashou_id:
q_conditions &= Q(jiedan_dashou_id=dashou_id)
if shangjia_id:
q_conditions &= Q(shangjia_kuozhan__shangjia_id=shangjia_id)
if date:
q_conditions &= Q(create_time__date=date)
if nicheng:
q_conditions &= Q(nicheng__icontains=nicheng)
if jieshao:
q_conditions &= Q(jieshao__icontains=jieshao)
if clkf:
q_conditions &= Q(clkf__icontains=clkf)
if shangjia_nicheng:
# 通过商家扩展表关联 UserShangjia 的昵称
q_conditions &= Q(shangjia_kuozhan__shangjia_id__in=UserMain.objects.filter(
shop_profile__nicheng__icontains=shangjia_nicheng
).values_list('yonghuid', flat=True))
# ---------- 统计(仅按发单平台,非跨平台) ----------
stats = Dingdan.objects.filter(fadan_pingtai=2, is_cross=0).aggregate(
total_orders=Count('id'),
completed_orders=Count('id', filter=Q(zhuangtai=3)),
refund_orders=Count('id', filter=Q(zhuangtai=5)),
pending_orders=Count('id', filter=Q(zhuangtai__in=[4,8]))
)
# ---------- 状态计数(所有状态都返回,非跨平台) ----------
status_counts = {
'all': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0).count(),
'1,7': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai__in=[1,7]).count(),
'2': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=2).count(),
'8': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=8).count(),
'3': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=3).count(),
'4': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=4).count(),
'5': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=5).count(),
'6': Dingdan.objects.filter(fadan_pingtai=2, is_cross=0, zhuangtai=6).count(),
}
# ---------- 分页查询 ----------
total_count = Dingdan.objects.filter(q_conditions).count()
orders_qs = Dingdan.objects.filter(q_conditions).select_related(
'shangjia_kuozhan'
).only(
'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', 'fadan_pingtai',
'jine', 'tupian', 'create_time', 'nicheng', 'jiedan_dashou_id',
'shangjia_kuozhan__shangjia_id'
).order_by('-create_time')[(page-1)*page_size : page*page_size]
# ---------- 获取商家昵称 ----------
shangjia_ids = [o.shangjia_kuozhan.shangjia_id for o in orders_qs if hasattr(o, 'shangjia_kuozhan') and o.shangjia_kuozhan.shangjia_id]
nickname_map = {}
if shangjia_ids:
users = UserMain.objects.filter(yonghuid__in=shangjia_ids).select_related('shop_profile').only(
'yonghuid', 'shop_profile__nicheng'
)
for user in users:
nickname_map[user.yonghuid] = user.shop_profile.nicheng or '' if hasattr(user, 'shop_profile') else ''
result_list = []
for order in orders_qs:
ext = getattr(order, 'shangjia_kuozhan', None)
shangjia_id = ext.shangjia_id if ext else ''
youxi_nicheng = order.nicheng or '' # 商家订单的玩家昵称
result_list.append({
'dingdan_id': order.dingdan_id or '',
'jieshao': order.jieshao or '',
'zhuangtai': order.zhuangtai,
'leixing': order.leixing_id,
'fadanpingtai': order.fadan_pingtai,
'jiage': float(order.jine) if order.jine else 0.00,
'tupian_url': order.tupian or '',
'fadanshijian': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time 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})
import time
import logging
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from yonghu.models import UserMain, UserKefu
from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia, Dashoutupian, Tuikuanjilu
logger = logging.getLogger(__name__)
class KefuGetOrderDetailView(APIView):
"""
客服获取订单详情接口
请求:POST /yonghu/kefuhqddxq
参数:{"phone": "客服手机号", "dingdan_id": "订单ID"}
认证:JWT
返回:code=0 + 订单详情数据
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
start_time = time.time()
try:
return self._build_order_detail_response(request, start_time)
except Exception as e:
logger.exception(
'客服订单详情异常 dingdan_id=%s phone=%s error=%s',
request.data.get('dingdan_id'),
request.data.get('phone'),
e,
)
return Response(
{'code': 500, 'msg': '获取订单详情失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
def _build_order_detail_response(self, request, start_time):
# 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:
logger.warning(f"手机号不匹配: {phone}")
return Response(
{'code': 401, 'msg': '认证失败'},
status=status.HTTP_401_UNAUTHORIZED
)
if current_user.user_type != 'kefu':
logger.warning(f"用户类型非客服: {current_user.yonghuid}")
return Response(
{'code': 401, 'msg': '认证失败'},
status=status.HTTP_401_UNAUTHORIZED
)
try:
kefu = current_user.kefu_profile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
return Response(
{'code': 401, 'msg': '认证失败'},
status=status.HTTP_401_UNAUTHORIZED
)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
return Response(
{'code': 401, 'msg': '认证失败'},
status=status.HTTP_401_UNAUTHORIZED
)
# 3. 查询订单
try:
dingdan_obj = Dingdan.objects.select_related(
'pingtai_kuozhan',
'shangjia_kuozhan'
).get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
logger.warning(f"订单不存在: {dingdan_id}")
return Response(
{'code': 404, 'msg': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
def _to_float(val, default=0.0):
try:
return float(val) if val is not None else default
except (TypeError, ValueError):
return default
# 4. 构建基础响应数据(主表字段)
response_data = {
'dingdan_id': dingdan_obj.dingdan_id or '',
'zhuangtai': dingdan_obj.zhuangtai or 0,
'fadanpingtai': dingdan_obj.fadan_pingtai or 0,
'jiage': _to_float(dingdan_obj.jine),
'dashou_fencheng': _to_float(dingdan_obj.dashou_fencheng),
'guanshi_fencheng': _to_float(getattr(dingdan_obj, 'guanshi_fencheng', 0)),
'guanshi_shangjia_fencheng': _to_float(getattr(dingdan_obj, 'guanshi_shangjia_fencheng', 0)),
'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '',
'dashou_liuyan': dingdan_obj.dashou_liuyan or '',
'zhiding_id': dingdan_obj.zhiding_id or '',
'shangpin_id': dingdan_obj.shangpin_id or '',
'leixing': dingdan_obj.leixing_id or 0,
'tupian_url': dingdan_obj.tupian or '',
'jieshao': dingdan_obj.jieshao or '',
'beizhu': dingdan_obj.beizhu or '',
'tkly': dingdan_obj.tkly or '',
'nicheng': dingdan_obj.nicheng or '',
'clkf': dingdan_obj.clkf or '',
'chuangjianshijian': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.create_time else '',
'genggaishijian': dingdan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.update_time else '',
}
# 5. 根据发单平台添加扩展表数据
fadan_pingtai = dingdan_obj.fadan_pingtai
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.laoban_id or '',
'laoban_pingjia': pingtai.laoban_pingjia 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.shangjia_id or '',
'sjnicheng': shangjia.sjnicheng or '',
'shangjia_pingjia': shangjia.shangjia_pingjia or '',
'cfliyou': shangjia.cfliyou or '',
'sqzhuangtai': shangjia.sqzhuangtai if shangjia.sqzhuangtai is not None else 0,
'bhliyou': shangjia.bhliyou or '',
}
else:
response_data['shangjia_kuozhan'] = {
'shangjia_id': '',
'sjnicheng': '',
'shangjia_pingjia': '',
'cfliyou': '',
'sqzhuangtai': 0,
'bhliyou': '',
}
# 6. 查询打手提交的图片
try:
dashou_images = Dashoutupian.objects.filter(
dingdan_id=dingdan_id
).values_list('tupian', flat=True)
response_data['dashou_images'] = [img for img in dashou_images if img]
except Exception as e:
logger.warning(f"查询打手图片失败: {str(e)}")
response_data['dashou_images'] = []
# 7. 查询退款理由(从退款记录表取最新一条)
try:
tuikuan_record = Tuikuanjilu.objects.filter(
dingdan_id=dingdan_id
).order_by('-create_time').first()
if tuikuan_record and tuikuan_record.liyou:
response_data['tuikuan_liyou'] = tuikuan_record.liyou
else:
response_data['tuikuan_liyou'] = ''
except Exception as e:
logger.warning(f"查询退款记录失败: {str(e)}")
response_data['tuikuan_liyou'] = ''
logger.info(f"订单详情接口耗时: {time.time()-start_time:.3f}s")
return Response({'code': 0, 'data': response_data})
import logging
from django.db import transaction, IntegrityError
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from yonghu.models import UserMain, UserKefu, UserDashou
from dingdan.models import Dingdan
from shangpin.models import Huiyuangoumai # 假设会员购买模型存在
logger = logging.getLogger(__name__)
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.user_type != 'kefu':
logger.warning(f"用户类型非客服: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单
try:
order = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 4. 验证订单状态是否允许更换打手(进行中2 或 结算中8)
if order.zhuangtai not in [2, 8]:
return Response({'code': 400, 'msg': '订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 查询新打手
try:
new_dashou_user = UserMain.objects.get(yonghuid=new_dashou_id)
except UserMain.DoesNotExist:
return Response({'code': 404, 'msg': '打手不存在'})
# 6. 验证打手扩展表
try:
new_dashou_profile = new_dashou_user.dashou_profile
except ObjectDoesNotExist:
return Response({'code': 404, 'msg': '打手信息不完整'})
# 7. 验证打手状态(打手状态和账号状态必须为1)
if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1:
return Response({'code': 400, 'msg': '打手接单中,状态不允许再次接单'})
# 8. 验证打手积分是否足够(至少5分)
if new_dashou_profile.jifen < 5:
return Response({'code': 400, 'msg': '打手积分不足,无法接单'})
# 8. 根据订单的抢单要求类型进行额外验证
yaoqiuleixing = order.yaoqiuleixing
if yaoqiuleixing == 1: # 会员抢单
huiyuan_id = order.huiyuan_id
if not huiyuan_id:
return Response({'code': 400, 'msg': '订单会员ID为空'})
try:
# 查询打手的会员购买记录
huiyuan_goumai = Huiyuangoumai.objects.get(
huiyuan_id=huiyuan_id,
yonghu_id=new_dashou_id # 假设 yonghu_id 字段存储用户ID
)
except Huiyuangoumai.DoesNotExist:
return Response({'code': 400, 'msg': '打手未购买该订单要求的会员'})
# 检查会员是否过期(调用模型方法)
is_expired = huiyuan_goumai.jiance_shifou_daoqi()
if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1:
return Response({'code': 400, 'msg': '打手的会员已过期'})
elif yaoqiuleixing == 2: # 押金抢单
order_yajin = order.yongjin # 假设订单的 yongjin 字段存储押金要求
if order_yajin is None:
return Response({'code': 400, 'msg': '订单押金为空'})
if new_dashou_profile.yajin < order_yajin:
return Response({'code': 400, 'msg': '打手押金不足'})
else:
return Response({'code': 400, 'msg': '订单抢单要求类型错误'})
# 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.jiedan_dashou_id
if old_dashou_id:
try:
old_dashou_user = UserMain.objects.get(yonghuid=old_dashou_id)
old_dashou_profile = old_dashou_user.dashou_profile
old_dashou_profile.zhuangtai = 1 # 恢复空闲
old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1(业务逻辑)
old_dashou_profile.save()
logger.info(f"原打手 {old_dashou_id} 状态已更新")
except (UserMain.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"原打手 {old_dashou_id} 不存在或信息不完整,跳过更新")
except Exception as e:
logger.error(f"更新原打手时异常: {str(e)}", exc_info=True)
# 9.3 更新订单
order.jiedan_dashou_id = new_dashou_id
order.zhuangtai = 2 # 进行中
order.clkf = current_user.yonghuid # 记录处理客服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.user_type != 'kefu':
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
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 = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
# 4. 验证订单状态是否为9(未付款)
if order.zhuangtai != 9:
return Response({'code': 400, 'msg': '订单状态不允许恢复'})
# 5. 根据是否有指定ID决定新状态
if order.zhiding_id:
new_status = 7 # 指定中
else:
new_status = 1 # 下单中
# 6. 更新订单
order.zhuangtai = new_status
order.clkf = current_user.yonghuid
order.save()
return Response({'code': 0, 'msg': '恢复成功', 'data': {'new_status': new_status}})
# dingdan/views.py
import logging
import random
import hashlib
import requests
import xmltodict
import time
from decimal import Decimal
from django.db import transaction, IntegrityError
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from yonghu.models import UserMain, UserKefu, UserDashou
from dingdan.models import Dingdan, DingdanPingtai, Tuikuanjilu # 注意:Tuikuanjilu 模型未在本次提供,但假设存在
logger = logging.getLogger(__name__)
from dingdan.utils import (
update_daily_income,
sync_order_to_partners,
update_daily_payout,)
import hmac
class KefuPlatformRefundView(APIView):
"""
虚拟支付退款接口(代币订单,使用 refund_order 接口)
业务逻辑与原 KefuPlatformRefundView 完全一致
微信 API:POST /xpay/refund_order
文档:https://developers.weixin.qq.com/minigame/dev/api-backend/open-api/virtual-payment/refund_order.html
"""
permission_classes = [permissions.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.user_type != 'kefu':
logger.warning(f"用户类型非客服: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单并加锁
try:
with transaction.atomic():
order = Dingdan.objects.select_related('pingtai_kuozhan').select_for_update().get(dingdan_id=dingdan_id)
# 4. 验证平台订单
if order.fadan_pingtai != 1:
return Response({'code': 400, 'msg': '该订单不是平台订单'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 验证状态允许退款(沿用原有白名单)
allowed_statuses = [1, 2, 4, 7, 8]
if order.zhuangtai not in allowed_statuses:
return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
# 6. 获取退款所需关键信息
jiedan_dashou_id = order.jiedan_dashou_id
# 从订单扩展表获取老板 openid(退款API需要)
laoban_openid = None
laoban_id = order.pingtai_kuozhan.laoban_id if hasattr(order, 'pingtai_kuozhan') else None
if laoban_id:
laoban_user = UserMain.objects.filter(yonghuid=laoban_id).first()
if laoban_user:
laoban_openid = getattr(laoban_user, 'openid', None)
if not laoban_openid:
return Response({'code': 400, 'msg': '未找到用户openid,无法退款'}, status=status.HTTP_400_BAD_REQUEST)
# 7. 调用虚拟支付退款接口(refund_order)
refund_result = self.call_virtual_refund(
dingdan_id=order.dingdan_id,
jine=order.jine,
openid=laoban_openid,
wechat_transaction_id=getattr(order, 'wechat_transaction_id', None)
)
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.zhuangtai = 5
order.clkf = current_user.phone
if tuikuan_liyou:
order.tkly = tuikuan_liyou
order.save()
# 9. 更新打手数据
if jiedan_dashou_id:
try:
dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
dashou_profile = dashou_user.dashou_profile
dashou_profile.tuikuanliang += 1
dashou_profile.zhuangtai = 1
dashou_profile.save()
logger.info(f"平台退款:打手 {jiedan_dashou_id} 退款订单+1")
except (UserMain.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"平台退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失")
# 10. 更新退款记录表
try:
refund_record, created = Tuikuanjilu.objects.get_or_create(
dingdan_id=dingdan_id,
defaults={
'dashouid': jiedan_dashou_id or '',
'qingqiuid': '',
'chuliid': phone,
'liyou': tuikuan_liyou or '客服退款',
'sqzhuangtai': 1,
'jine': order.jine,
'dashou_fencheng': order.dashou_fencheng or 0,
'jieshao': order.jieshao or '',
'beizhu': '客服平台退款(虚拟支付)',
'nicheng': order.nicheng or '',
}
)
if not created:
refund_record.sqzhuangtai = 1
refund_record.chuliid = phone
if tuikuan_liyou:
refund_record.liyou = tuikuan_liyou
refund_record.save()
except Exception as e:
logger.error(f"平台退款更新退款记录异常: {str(e)}", exc_info=True)
# 11. 商品和店铺每日统计
try:
if order.shangpin_id:
pingtai_shouyi = Decimal('0.00')
dianpu_shouyi = Decimal('0.00')
if hasattr(order, 'pingtai_kuozhan'):
pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00')
dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
update_shangpin_daily_stat(
shangpin_id=order.shangpin_id,
caozuo_leixing=3,
dingdan_jiage=order.jine,
pingtai_shouyi=pingtai_shouyi,
dianpu_zongshouyi=dianpu_shouyi
)
logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
dianpu_id = None
if hasattr(order, 'pingtai_kuozhan'):
dianpu_id = order.pingtai_kuozhan.dianpu_id
if dianpu_id:
dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
update_dianpu_daily_stat(
dianpu_id=dianpu_id,
caozuo_leixing=3,
dingdan_jiage=order.jine,
dianpu_shouyi=dianpu_shouyi
)
logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
except Exception as e:
logger.error(f"商品/店铺统计更新失败: {e}")
logger.error(traceback.format_exc())
# 12. 更新客服统计数据
kefu.jinrichuli += 1
kefu.jinyuechuli += 1
kefu.zongchuli += 1
kefu.save()
# 13. 更新退款统计
update_daily_payout(order.jine)
# 14. 打手每日统计(退款)
try:
from houtai.utils import update_dashou_daily_by_action
update_dashou_daily_by_action(
yonghuid=order.jiedan_dashou_id,
amount=order.dashou_fencheng,
action=3
)
logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
logger.info(f"虚拟支付退款成功,订单 {dingdan_id},退款单号 {refund_result.get('refund_id')}")
return Response({'code': 0, 'msg': '退款成功'})
except Dingdan.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_virtual_refund(self, dingdan_id, jine, openid, wechat_transaction_id=None):
"""
调用虚拟支付退款接口 /xpay/refund_order
:param dingdan_id: 商户订单号(order_id)
:param jine: 退款金额(元)
:param openid: 支付用户的 openid
:param wechat_transaction_id: 微信支付交易单号(wx_order_id),若有则优先使用
:return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'}
"""
uri = "/xpay/refund_order"
base_url = "https://api.weixin.qq.com"
appkey = settings.VP_APPKEY_SANDBOX if settings.VP_ENV == 1 else settings.VP_APPKEY
offer_id = settings.VP_OFFER_ID
# 金额:元 -> 分
refund_fee = int(float(jine) * 100)
# 退款单号:R + 时间戳 + 6位随机数,保证唯一且长度合适
refund_order_id = f"R{int(time.time())}{random.randint(100000, 999999)}"
# 用户 IP
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
user_ip = x_forwarded_for.split(',')[0].strip() if x_forwarded_for else self.request.META.get('REMOTE_ADDR', '127.0.0.1')
# 请求体构造(优先使用 wx_order_id,若有的话)
body_data = {
"openid": openid,
"env": settings.VP_ENV,
"order_id": dingdan_id, # 商户订单号
"refund_order_id": refund_order_id, # 本次退款单号
"left_fee": refund_fee, # 订单剩余可退金额(全退时等于订单金额)
"refund_fee": refund_fee, # 本次退款金额
"biz_meta": "客服退款", # 业务备注
"refund_reason": 5, # 退款原因:5 其他
"req_from": 1 # 退款来源:1 人工客服
}
# 如果有微信支付交易单号,优先传入(可选)
if wechat_transaction_id:
body_data["wx_order_id"] = wechat_transaction_id
body = json.dumps(body_data, separators=(",", ":"))
# 签名:pay_sig
msg = uri + "&" + body
pay_sig = hmac.new(appkey.encode("utf-8"), msg.encode("utf-8"), hashlib.sha256).hexdigest()
# 获取 access_token
access_token = self._get_access_token()
# 构建 URL
url = f"{base_url}{uri}?access_token={access_token}&pay_sig={pay_sig}"
# ========== 调试日志(保留,方便排查) ==========
logger.info("================== 退款调试信息 ==================")
logger.info(f"[退款] 订单号 (dingdan_id): {dingdan_id}")
logger.info(f"[退款] 微信交易号 (wx_order_id): {wechat_transaction_id}")
logger.info(f"[退款] 退款金额 (分): {refund_fee}")
logger.info(f"[退款] 退款单号: {refund_order_id}")
logger.info(f"[退款] 请求 URL: {url}")
logger.info(f"[退款] 请求 Body: {body}")
logger.info("==================================================")
headers = {"Content-Type": "application/json"}
try:
response = requests.post(url, data=body, headers=headers, timeout=10)
result = response.json()
logger.info(f"[退款] 微信返回结果: {result}")
if result.get("errcode") == 0:
return {'code': 0, 'msg': '退款成功', 'refund_id': refund_order_id}
else:
errcode = result.get('errcode')
errmsg = result.get('errmsg', '未知错误')
# 常见错误码提示
if errcode == 268490002:
errmsg = "参数错误,请检查订单号是否正确、金额是否匹配、openid是否正确"
elif errcode == 268490003:
errmsg = "签名错误,请检查 appkey 是否正确"
elif errcode == 268490004:
errmsg = "订单不存在或已退款"
logger.error(f"[退款] 退款失败: errcode={errcode}, errmsg={errmsg}")
return {'code': 400, 'msg': f"退款失败: {errmsg}"}
except requests.exceptions.RequestException as e:
logger.error(f"[退款] 网络请求异常: {e}")
return {'code': 500, 'msg': '微信退款请求网络异常'}
def _get_access_token(self):
"""获取 access_token,带缓存"""
cache_key = "virtual_payment_access_token"
access_token = cache.get(cache_key)
if not access_token:
logger.info("[退款] 缓存无 access_token,重新获取")
resp = requests.get(
"https://api.weixin.qq.com/cgi-bin/token",
params={
"grant_type": "client_credential",
"appid": wx_cfg.WEIXIN_APPID,
"secret": wx_cfg.WEIXIN_SECRET,
},
timeout=10,
)
data = resp.json()
if "access_token" in data:
access_token = data["access_token"]
cache.set(cache_key, access_token, data.get("expires_in", 7200) - 300)
logger.info(f"[退款] access_token 已更新")
else:
raise Exception(f"获取 access_token 失败: {data}")
return access_token
'''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.user_type != 'kefu':
logger.warning(f"用户类型非客服: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单并加锁
try:
with transaction.atomic():
order = Dingdan.objects.select_related('pingtai_kuozhan').select_for_update().get(dingdan_id=dingdan_id)
# 4. 验证平台订单
if order.fadan_pingtai != 1:
return Response({'code': 400, 'msg': '该订单不是平台订单'})
# 5. 验证状态允许退款(根据模型,状态值:1下单中,2进行中,3已完成,4退款审核,5已退款,6退款失败,7指定中,8结算中)
allowed_statuses = [1, 2, 4, 7, 8] # 允许退款的已支付或处理中状态
if order.zhuangtai not in allowed_statuses:
return Response({'code': 400, 'msg': '订单状态不允许退款'})
# 6. 获取微信交易号(如果存在)
wechat_transaction_id = getattr(order, 'wechat_transaction_id', None)
# 7. 调用微信退款接口
refund_result = self.call_wechat_refund(
dingdan_id=order.dingdan_id,
jine=order.jine,
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']})
# 8. 更新订单状态为5(已退款)
order.zhuangtai = 5
order.clkf = current_user.phone # 记录处理客服手机号
if tuikuan_liyou:
order.tkly = tuikuan_liyou
order.save()
# 9. 更新打手数据
jiedan_dashou_id = order.jiedan_dashou_id
if jiedan_dashou_id:
try:
dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
dashou_profile = dashou_user.dashou_profile
dashou_profile.tuikuanliang += 1
dashou_profile.zhuangtai = 1 # 打手状态设为空闲
dashou_profile.save()
logger.info(f"平台退款:打手 {jiedan_dashou_id} 退款订单+1")
except (UserMain.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"平台退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
# 10. 更新退款记录表(假设存在 Tuikuanjilu 模型,字段需根据实际调整)
#更新支出记录
from dingdan.utils import update_daily_payout
update_daily_payout(order.jine)
try:
from dingdan.models import Tuikuanjilu # 确保模型已导入
refund_record, created = Tuikuanjilu.objects.get_or_create(
dingdan_id=dingdan_id,
defaults={
'dashouid': jiedan_dashou_id or '',
'qingqiuid': '',
'chuliid': phone,
'liyou': tuikuan_liyou or '客服退款',
'sqzhuangtai': 1, # 成功
'jine': order.jine,
'dashou_fencheng': order.dashou_fencheng or 0,
'jieshao': order.jieshao or '',
'beizhu': '客服平台退款',
'nicheng': order.nicheng or '',
}
)
if not created:
refund_record.sqzhuangtai = 1
refund_record.chuliid = phone
if tuikuan_liyou:
refund_record.liyou = 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()
logger.info(f"平台退款成功,订单 {dingdan_id},退款单号 {refund_result.get('refund_id')}")
return Response({'code': 0, 'msg': '退款成功'})
except Dingdan.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': '服务器内部错误'})
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 = wx_cfg.WEIXIN_APPID
mch_id = wx_cfg.WEIXIN_MCHID
key = wx_cfg.WEIXIN_SHANGHUMIYAO
cert_path = wx_cfg.WEIXIN_CERT_PATH
key_path = wx_cfg.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):
import string
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 = ['']
for k, v in data.items():
xml.append(f'<{k}>{v}{k}>')
xml.append('')
return ''.join(xml)'''
# dingdan/views.py
import logging
from django.db import transaction, IntegrityError
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from yonghu.models import UserMain, UserKefu, UserDashou, UserShangjia
from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu
logger = logging.getLogger(__name__)
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.user_type != 'kefu':
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
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 = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
# 4. 验证订单状态为退款审核中(4)
if order.zhuangtai != 4:
return Response({'code': 400, 'msg': '订单状态不是退款审核中'})
# 获取必要字段
jiedan_dashou_id = order.jiedan_dashou_id
dashou_fencheng = order.dashou_fencheng
fadan_pingtai = order.fadan_pingtai
# 5. 验证打手分成金额是否存在
if dashou_fencheng is None:
return Response({'code': 400, 'msg': '订单缺少打手分成金额'})
# 6. 更新订单状态为退款失败(6)
order.zhuangtai = 6
order.clkf = current_user.phone # 记录处理客服手机号
if jujue_liyou:
order.tkly = jujue_liyou
order.save()
# 7. 给打手结算(返还分成)
if jiedan_dashou_id:
try:
dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
dashou_profile = dashou_user.dashou_profile
# 增加打手相关统计数据
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 (UserMain.DoesNotExist, AttributeError):
logger.warning(f"拒绝退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算")
# 8. 如果是商家发单,更新商家成交订单数量
if fadan_pingtai == 2:
try:
# 获取商家订单扩展表
shangjia_kuozhan = order.shangjia_kuozhan
shangjia_id = shangjia_kuozhan.shangjia_id
if shangjia_id:
shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
shangjia_profile = shangjia_user.shop_profile
shangjia_profile.chengjiao += 1
shangjia_profile.save()
logger.info(f"拒绝退款:商家 {shangjia_id} 成交订单+1")
except (ObjectDoesNotExist, UserMain.DoesNotExist, AttributeError):
logger.warning(f"拒绝退款:商家信息不存在,跳过商家更新")
# 9. 更新退款记录表
try:
refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id)
refund_record.sqzhuangtai = 2 # 2表示拒绝
refund_record.chuliid = phone
refund_record.bhliyou = jujue_liyou # 记录驳回理由(假设模型有此字段)
refund_record.save()
logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}")
except Tuikuanjilu.DoesNotExist:
# 如果退款记录不存在,可以创建一条记录(可选)
Tuikuanjilu.objects.create(
dingdan_id=dingdan_id,
dashouid=jiedan_dashou_id or '',
qingqiuid='',
chuliid=phone,
liyou=jujue_liyou or '客服拒绝退款',
sqzhuangtai=2,
jine=order.jine,
dashou_fencheng=dashou_fencheng or 0,
jieshao=order.jieshao or '',
beizhu='客服拒绝退款',
nicheng=order.nicheng or '',
bhliyou=jujue_liyou or '', # 假设模型有 bhliyou 字段
)
except Exception as e:
logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True)
# 10. 更新客服统计数据
kefu.jinrichuli += 1
kefu.jinyuechuli += 1
kefu.zongchuli += 1
kefu.save()
return Response({'code': 0, 'msg': '拒绝退款成功'})
except Dingdan.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)
# 2. 客服身份验证
current_user = request.user
if getattr(current_user, 'phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.user_type != 'kefu':
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
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 = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 验证商家订单
if order.fadan_pingtai != 2:
return Response({'code': 400, 'msg': '该订单不是商家订单'}, status=status.HTTP_400_BAD_REQUEST)
# 验证状态允许退款
allowed_statuses = [1, 7, 2, 8, 4]
if order.zhuangtai not in allowed_statuses:
return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
# 获取必要数据
jiedan_dashou_id = order.jiedan_dashou_id
jine = order.jine
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.shangjia_id
except ObjectDoesNotExist:
return Response({'code': 400, 'msg': '订单商家信息不存在'}, status=status.HTTP_400_BAD_REQUEST)
with transaction.atomic():
# 更新订单
order.zhuangtai = 5
order.clkf = current_user.phone
if tuikuan_liyou:
order.tkly = tuikuan_liyou
order.save()
# 更新打手
if jiedan_dashou_id:
try:
dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
dashou_profile = dashou_user.dashou_profile
dashou_profile.tuikuanliang += 1
dashou_profile.zhuangtai = 1
dashou_profile.save()
except (UserMain.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"商家退款:打手 {jiedan_dashou_id} 不存在,跳过更新")
# 更新商家
try:
shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
shangjia_profile = shangjia_user.shop_profile
shangjia_profile.tuikuan += 1
shangjia_profile.yue += jine
shangjia_profile.save()
except (UserMain.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"商家退款:商家 {shangjia_id} 不存在,跳过更新")
# 更新退款记录表
try:
refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id)
refund_record.sqzhuangtai = 1
refund_record.chuliid = phone
refund_record.save()
except Tuikuanjilu.DoesNotExist:
pass
# 更新客服统计
kefu = current_user.kefu_profile
kefu.jinrichuli += 1
kefu.jinyuechuli += 1
kefu.zongchuli += 1
kefu.save()
return Response({'code': 0, 'msg': '退款成功'})
'''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.user_type != 'kefu':
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
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 = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 验证商家订单
if order.fadan_pingtai != 2:
return Response({'code': 400, 'msg': '该订单不是商家订单'}, status=status.HTTP_400_BAD_REQUEST)
# 验证状态允许退款
allowed_statuses = [1, 7, 2, 8, 4]
if order.zhuangtai not in allowed_statuses:
return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
# 获取必要数据
jiedan_dashou_id = order.jiedan_dashou_id
jine = order.jine
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.shangjia_id
except ObjectDoesNotExist:
return Response({'code': 400, 'msg': '订单商家信息不存在'}, status=status.HTTP_400_BAD_REQUEST)
with transaction.atomic():
# 更新订单
order.zhuangtai = 5
order.clkf = current_user.phone
if tuikuan_liyou:
order.tkly = tuikuan_liyou
order.save()
# 更新打手
if jiedan_dashou_id:
try:
dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
dashou_profile = dashou_user.dashou_profile
dashou_profile.tuikuanliang += 1
dashou_profile.zhuangtai = 1
dashou_profile.save()
except (UserMain.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"商家退款:打手 {jiedan_dashou_id} 不存在,跳过更新")
# 更新商家
try:
shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
shangjia_profile = shangjia_user.shop_profile
shangjia_profile.tuikuan += 1
shangjia_profile.yue += jine
shangjia_profile.save()
except (UserMain.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"商家退款:商家 {shangjia_id} 不存在,跳过更新")
# 更新退款记录表
try:
refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id)
refund_record.sqzhuangtai = 1
refund_record.chuliid = phone
refund_record.save()
except Tuikuanjilu.DoesNotExist:
pass
# 更新客服统计
kefu = current_user.kefu_profile
kefu.jinrichuli += 1
kefu.jinyuechuli += 1
kefu.zongchuli += 1
kefu.save()
return Response({'code': 0, 'msg': '退款成功'})'''
logger = logging.getLogger(__name__)
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.user_type != 'kefu':
logger.warning(f"用户类型非客服: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单,预加载平台扩展表
try:
order = Dingdan.objects.select_related('pingtai_kuozhan').get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 4. 校验订单条件:平台发单(1)且状态为2或8
if order.fadan_pingtai != 1 or order.zhuangtai not in [2, 8]:
return Response({'code': 400, 'msg': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算'}, status=status.HTTP_400_BAD_REQUEST)
jiedan_dashou_id = order.jiedan_dashou_id
if not jiedan_dashou_id:
return Response({'code': 400, 'msg': '订单暂无接单打手'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 使用事务更新
with transaction.atomic():
# 5.1 更新订单主表
order.zhuangtai = 5 # 已退款
order.tkly = tkly # 拒绝理由
order.clkf = phone # 记录客服手机号
order.save()
# 5.2 更新平台订单扩展表:标记为拒绝结算(假设字段 jjjs_biaoshi)
# 注意:需要确保 DingdanPingtai 模型中已添加 jjjs_biaoshi 字段,IntegerField,默认0
if hasattr(order, 'pingtai_kuozhan') and order.pingtai_kuozhan:
pingtai_ext = order.pingtai_kuozhan
pingtai_ext.jjjs_biaoshi = 1 # 1表示拒绝结算
pingtai_ext.save()
else:
# 理论上应该存在,但若不存在则记录日志
logger.warning(f"订单 {dingdan_id} 缺少平台扩展表,无法标记拒绝结算")
# 5.3 更新打手扩展表
try:
dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
dashou_profile = dashou_user.dashou_profile
dashou_profile.zhuangtai = 1 # 打手状态设为空闲
dashou_profile.tuikuanliang += 1 # 退款订单总量+1
dashou_profile.save()
logger.info(f"拒绝结算:打手 {jiedan_dashou_id} 退款订单+1,状态空闲")
except (UserMain.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"拒绝结算:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
except Exception as e:
logger.error(f"拒绝结算更新打手异常: {str(e)}", exc_info=True)
# 5.4 更新客服扩展表统计数据
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.user_type != 'kefu':
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
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 = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 4. 校验订单状态(允许状态:2,8,4)
if order.zhuangtai not in [2, 8, 4]:
return Response({'code': 400, 'msg': '订单状态不允许转移大厅'})
if order.jiedan_dashou_id:
UserDashou.objects.filter(user__yonghuid=order.jiedan_dashou_id).update(zhuangtai=1)
else:
pass
# 5. 根据是否有指定ID决定新状态
if order.zhiding_id:
new_status = 7 # 指定中
else:
new_status = 1 # 下单中
# 6. 使用事务更新
with transaction.atomic():
# 清空接单打手ID,更新状态,记录客服
order.jiedan_dashou_id = None
order.zhuangtai = new_status
order.clkf = phone
order.save()
return Response({'code': 0, 'msg': '转移大厅成功'})
logger = logging.getLogger(__name__)
from dingdan.utils import update_daily_dispatch_stat
from dingdan.models import PreSettlement
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
from shangpin.utils import update_shangpin_daily_stat
from shangdian.utils import update_dianpu_daily_stat # 注意:您之前写的店铺统计方法名是 update_dianpu_daily_stat
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.user_type != 'kefu':
return Response({'code': 401, 'msg': '认证失败'})
try:
kefu = current_user.kefu_profile
except ObjectDoesNotExist:
return Response({'code': 401, 'msg': '认证失败'})
if kefu.zhuangtai != 1:
return Response({'code': 401, 'msg': '客服状态异常'})
# 2. 订单处理(事务内)
guanshi_settle_ctx = None
try:
with transaction.atomic():
order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
# 3. 校验订单状态
if order.zhuangtai != 8:
return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'})
# 4. 校验必要字段
dashou_id = order.jiedan_dashou_id
dashou_fencheng = order.dashou_fencheng
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 = UserMain.objects.get(yonghuid=dashou_id)
dashou_profile = dashou_user.dashou_profile
# 原子更新(避免并发重复加)
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 (UserMain.DoesNotExist, AttributeError):
return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'})
# 6. 如果是商家发单,更新商家成交订单数量
if order.fadan_pingtai == 2:
try:
# 通过 DingdanShangjia 获取商家ID
shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan'
if shangjia_ext and shangjia_ext.shangjia_id:
shangjia_user = UserMain.objects.get(yonghuid=shangjia_ext.shangjia_id)
shangjia_profile = shangjia_user.shop_profile
shangjia_profile.chengjiao = F('chengjiao') + 1
shangjia_profile.save(update_fields=['chengjiao'])
# ✅ 新方法(行为驱动,更安全)
from houtai.utils import update_shangjia_daily
update_shangjia_daily(
yonghuid=shangjia_ext.shangjia_id,
amount=Decimal(str(order.jine)),
action=2 # 2 = 结算
)
except ObjectDoesNotExist:
# 没有商家扩展记录则忽略
pass
# 7. 更新订单状态
order.zhuangtai = 3
order.clkf = phone
order.save(update_fields=['zhuangtai', 'clkf'])
if order.fadan_pingtai == 1:
# ========== 新增:商品和店铺每日统计 ==========
try:
# 商品每日统计(只要有商品ID就执行)
if order.shangpin_id:
# 从扩展表获取已计算好的收益值(下单时已存储)
pingtai_shouyi = Decimal('0.00')
dianpu_shouyi = Decimal('0.00')
if hasattr(order, 'pingtai_kuozhan'):
pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00')
dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
update_shangpin_daily_stat(
shangpin_id=order.shangpin_id,
caozuo_leixing=2, # 2 = 结单
dingdan_jiage=order.jine,
pingtai_shouyi=pingtai_shouyi,
dianpu_zongshouyi=dianpu_shouyi
)
logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
# 店铺每日统计(仅当存在店铺ID时执行)
dianpu_id = None
if hasattr(order, 'pingtai_kuozhan'):
dianpu_id = order.pingtai_kuozhan.dianpu_id
if dianpu_id:
dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00')
update_dianpu_daily_stat(
dianpu_id=dianpu_id,
caozuo_leixing=2, # 2 = 结单
dingdan_jiage=order.jine,
dianpu_shouyi=dianpu_shouyi
)
logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
except Exception as e:
logger.error(f"商品/店铺统计更新失败: {e}")
logger.error(traceback.format_exc())
# 统计失败不影响主流程
# =============================================
# 🔥 新增:调用打手每日统计(成交)
try:
from houtai.utils import update_dashou_daily_by_action
update_dashou_daily_by_action(
yonghuid=order.jiedan_dashou_id,
amount=dashou_fencheng,
action=2 # 2 表示成交(结算)
)
logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# 不影响主流程
if order.fadan_pingtai == 2:
guanshi_settle_ctx = (dingdan_id, 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'])
if guanshi_settle_ctx:
from dingdan.utils import settle_shangjia_order_guanshi_fenhong
settle_order = Dingdan.objects.get(dingdan_id=guanshi_settle_ctx[0])
settle_shangjia_order_guanshi_fenhong(settle_order, guanshi_settle_ctx[1])
return Response({'code': 0, 'msg': '强制结单成功'})
except Dingdan.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
except Exception as e:
logger.error(f"强制结单异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'})
'''class KefuForceCompleteView(APIView):
"""
客服强制结单接口
请求:POST /yonghu/kefuqiangzhijiedan
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID"
}
认证:JWT,仅限状态正常的客服
逻辑:
1. 验证客服身份及状态
2. 查询订单并加锁
3. 校验订单状态是否为8(结算中)
4. 更新订单状态为3(已完成)
5. 给打手增加分成(余额、收益统计)
6. 增加打手成交单量
7. 如果是商家订单,增加商家成交单量
8. 更新客服处理统计
9. 返回成功
"""
permission_classes = [permissions.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
# 校验手机号是否匹配(假设 UserMain 有 phone 字段)
if getattr(current_user, 'phone', '') != phone:
return Response(
{'code': 401, 'msg': '认证失败'},
status=status.HTTP_401_UNAUTHORIZED
)
# 校验用户类型是否为客服
if current_user.user_type != 'kefu':
return Response(
{'code': 401, 'msg': '认证失败'},
status=status.HTTP_401_UNAUTHORIZED
)
# 获取客服扩展信息
try:
kefu = current_user.kefu_profile # 假设 related_name='kefu_profile'
except ObjectDoesNotExist:
return Response(
{'code': 401, 'msg': '认证失败'},
status=status.HTTP_401_UNAUTHORIZED
)
# 校验客服状态是否为正常(1 表示正常)
if kefu.zhuangtai != 1:
return Response(
{'code': 401, 'msg': '认证失败'},
status=status.HTTP_401_UNAUTHORIZED
)
# 3. 查询订单并加锁(事务内)
try:
with transaction.atomic():
order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
# 4. 校验订单状态是否为结算中(8)
if order.zhuangtai != 8:
return Response(
{'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'},
status=status.HTTP_400_BAD_REQUEST
)
# 获取必要字段
jiedan_dashou_id = order.jiedan_dashou_id
dashou_fencheng = order.dashou_fencheng
fadan_pingtai = order.fadan_pingtai
# 5. 验证打手分成金额是否存在且有效
if dashou_fencheng is None or dashou_fencheng <= 0:
return Response(
{'code': 400, 'msg': '订单打手分成金额无效'},
status=status.HTTP_400_BAD_REQUEST
)
# 6. 更新订单状态为已完成(3)
order.zhuangtai = 3
order.clkf = current_user.phone # 记录处理客服手机号
order.save()
# 7. 给打手结算(增加分成)
if jiedan_dashou_id:
try:
dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
dashou_profile = dashou_user.dashou_profile
# 增加打手相关统计数据
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 (UserMain.DoesNotExist, AttributeError):
logger.warning(f"强制结单:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算")
# 8. 如果是商家发单(fadan_pingtai == 2),更新商家成交订单数量
if fadan_pingtai == 2:
try:
# 获取商家订单扩展表(假设存在 one-to-one 关系)
shangjia_kuozhan = order.shangjia_kuozhan
shangjia_id = shangjia_kuozhan.shangjia_id
if shangjia_id:
shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
shangjia_profile = shangjia_user.shop_profile
shangjia_profile.chengjiao += 1
shangjia_profile.save()
logger.info(f"强制结单:商家 {shangjia_id} 成交订单+1")
except (ObjectDoesNotExist, UserMain.DoesNotExist, AttributeError):
logger.warning(f"强制结单:商家信息不存在,跳过商家更新")
# 9. 更新客服统计数据
kefu.jinrichuli += 1
kefu.jinyuechuli += 1
kefu.zongchuli += 1
kefu.save()
# 事务结束,返回成功
return Response({'code': 0, 'msg': '强制结单成功'})
except Dingdan.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 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.user_type != 'kefu':
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
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 = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 4. 校验订单状态为7(指定中)
if order.zhuangtai != 7:
return Response({'code': 400, 'msg': '仅状态为指定中(7)的订单可取消指定'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 使用事务更新
with transaction.atomic():
# 状态改为1(下单中),清空指定ID,记录客服
order.zhuangtai = 1
order.zhiding_id = None
order.clkf = phone
order.save()
return Response({'code': 0, 'msg': '取消指定成功'})
import logging
from django.db.models import Q
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from yonghu.models import UserMain, UserKefu, UserDashou
logger = logging.getLogger(__name__)
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.user_type != 'kefu':
logger.warning(f"用户类型非客服: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 构建查询条件(只查询打手)
# 通过 UserDashou 扩展表关联 UserMain
queryset = UserDashou.objects.select_related('user').all()
if keyword:
# 搜索用户ID或打手昵称
queryset = queryset.filter(
Q(user__yonghuid__icontains=keyword) |
Q(nicheng__icontains=keyword)
)
# 4. 计算总数并分页
total = queryset.count()
offset = (page - 1) * page_size
dashou_list = queryset.order_by('-create_time')[offset:offset + page_size]
# 5. 构建返回数据(只取需要的字段)
result = []
for dashou in dashou_list:
user = dashou.user
result.append({
'yonghuid': user.yonghuid,
'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.user_type != 'kefu':
logger.warning(f"用户类型非客服: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询打手主表和扩展表
try:
# 使用 select_related 预加载 dashou_profile,减少数据库查询
user_main = UserMain.objects.select_related('dashou_profile').get(
yonghuid=uid
)
except UserMain.DoesNotExist:
return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
# 验证用户类型是否为打手(可选,但建议验证)
# 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册)
try:
dashou = user_main.dashou_profile
except ObjectDoesNotExist:
# 用户存在但未注册打手身份,返回基本信息
user_info = {
'yonghuid': user_main.yonghuid,
'avatar': user_main.avatar or '',
'zaixianzhuangtai': 0,
'zhanghaozhuangtai': 0,
'zhuangtai': 0,
'nicheng': '',
'chenghao': '',
'dianhua': '',
'wechat': '',
'phone': user_main.phone or '',
'create_time': user_main.create_time,
'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.yonghuid,
'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 '',
'create_time': user_main.create_time,
'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.objects.filter(
yonghu_id=uid
).order_by('-create_time')
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.objects.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
}
})
from yonghu.models import Xiugaijilu # 确保已导入
from houtai.utils import verify_kefu_permission
logger = logging.getLogger(__name__)
class KefuUpdateDashouView(APIView):
"""
客服修改打手信息接口
支持操作:
- 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 = UserMain.objects.get(yonghuid=dashou_id, user_type='dashou')
dashou_profile = dashou_user.dashou_profile
except UserMain.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.objects.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.objects.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.objects.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.objects.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.objects.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.objects.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.objects.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.objects.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.objects.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.objects.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.objects.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.objects.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id)
except Huiyuangoumai.DoesNotExist:
return Response({'code': 404, 'msg': '该打手未购买此会员'})
# 删除会员记录(或设置为过期,根据业务要求)
record.delete()
Xiugaijilu.objects.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 KefuUpdateDashouView(APIView):
"""
客服修改打手信息接口
请求:POST /yonghu/kefuxgds
参数:{
"phone": "客服手机号",
"uid": "打手ID",
// 以下字段可选,只传需要修改的字段
"yue": 100.00,
"jifen": 200,
"yajin": 50.00,
"zaixianzhuangtai": 0/1,
"zhanghaozhuangtai": 0/1,
"zhuangtai": 1/2
}
认证: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.user_type != 'kefu':
logger.warning(f"用户类型非客服: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询打手主表和扩展表
try:
user_main = UserMain.objects.select_related('dashou_profile').get(
yonghuid=uid
)
except UserMain.DoesNotExist:
return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
try:
dashou = user_main.dashou_profile
except ObjectDoesNotExist:
return Response({'code': 400, 'msg': '该用户未注册打手身份'}, status=status.HTTP_400_BAD_REQUEST)
# 4. 收集要修改的字段
update_fields = {}
editable_fields = [
'yue', 'jifen', 'yajin',
'zaixianzhuangtai', 'zhanghaozhuangtai', 'zhuangtai'
]
for field in editable_fields:
if field in request.data:
value = request.data.get(field)
# 类型转换
if field in ['yue', 'yajin']:
try:
value = Decimal(str(value))
except:
return Response({'code': 400, 'msg': f'{field} 格式错误'}, status=status.HTTP_400_BAD_REQUEST)
elif field in ['jifen']:
try:
value = int(value)
except:
return Response({'code': 400, 'msg': f'{field} 必须为整数'}, status=status.HTTP_400_BAD_REQUEST)
elif field in ['zaixianzhuangtai', 'zhanghaozhuangtai', 'zhuangtai']:
try:
value = int(value)
if field == 'zhuangtai' and value not in [1, 2]:
return Response({'code': 400, 'msg': f'{field} 值无效'}, status=status.HTTP_400_BAD_REQUEST)
if field != 'zhuangtai' and value not in [0, 1]:
return Response({'code': 400, 'msg': f'{field} 值无效'}, status=status.HTTP_400_BAD_REQUEST)
except:
return Response({'code': 400, 'msg': f'{field} 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
update_fields[field] = value
if not update_fields:
return Response({'code': 400, 'msg': '没有要修改的字段'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 使用事务更新并记录修改日志
with transaction.atomic():
# 保存旧值用于记录(可选)
old_values = {field: getattr(dashou, field) for field in update_fields}
for field, value in update_fields.items():
setattr(dashou, field, value)
dashou.save()
# 记录修改日志
try:
# 构建修改描述
desc_parts = []
for field in update_fields:
old = old_values.get(field)
new = update_fields[field]
desc_parts.append(f"{field}: {old} -> {new}")
desc = "; ".join(desc_parts)
Xiugaijilu.objects.create(
yonghuid=uid,
xiugaiid=phone, # 客服手机号
leixing=2, # 打手类型
qitashuoming=desc,
create_time=timezone.now()
)
except Exception as e:
logger.error(f"记录修改日志失败: {e}")
# 不影响主流程,但记录错误
return Response({'code': 0, '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': Chufajilu.objects.count(),
'daichuli': Chufajilu.objects.filter(sqzhuangtai__in=[0, 3]).count(),
'yichuli': Chufajilu.objects.filter(sqzhuangtai__in=[1, 2]).count(),
}
# 4. 构建查询条件
queryset = Chufajilu.objects.all()
if status_list and isinstance(status_list, list):
queryset = queryset.filter(sqzhuangtai__in=status_list)
if dingdan_id:
queryset = queryset.filter(dingdan_id=dingdan_id)
if dashouid:
queryset = queryset.filter(dashouid=dashouid)
# 5. 分页
total = queryset.count()
offset = (page - 1) * page_size
records = queryset.order_by('-create_time')[offset:offset + page_size]
# 6. 构建返回列表
list_data = []
for item in records:
list_data.append({
'dingdan_id': item.dingdan_id,
'dashouid': item.dashouid,
'qingqiuid': item.qingqiuid,
'cfliyou': item.cfliyou,
'sqzhuangtai': item.sqzhuangtai,
'create_time': item.create_time,
'update_time': item.update_time,
# 其他字段按需添加
})
return Response({
'code': 0,
'data': {
'list': list_data,
'total': total,
'stats': stats
}
})
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.user_type != 'kefu':
logger.warning(f"用户类型非客服: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 查询处罚记录(按创建时间倒序取最新一条)
try:
record = Chufajilu.objects.filter(dingdan_id=dingdan_id).order_by('-create_time').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:
# 商家证据图片:上传者为请求人 (qingqiuid)
zhengju_qs = Chufatupian.objects.filter(dingdan_id=dingdan_id, yonghuid=record.qingqiuid)
zhengju_images = [item.tupian for item in zhengju_qs if item.tupian]
# 打手申诉图片:上传者为打手 (dashouid)
shensu_qs = Chufatupian.objects.filter(dingdan_id=dingdan_id, yonghuid=record.dashouid)
shensu_images = [item.tupian for item in shensu_qs if item.tupian]
except Exception as e:
logger.error(f"查询图片失败: {e}")
# 构建返回数据
data = {
'dingdan_id': record.dingdan_id,
'dashouid': record.dashouid,
'qingqiuid': record.qingqiuid,
'chuliid': record.chuliid,
'cfliyou': record.cfliyou,
'sqzhuangtai': record.sqzhuangtai,
'bhliyou': record.bhliyou,
'jifen': record.jifen,
'ssliyou': record.ssliyou or '',
'zhengju_tupian': zhengju_images,
'shensu_tupian': shensu_images,
'create_time': record.create_time,
'update_time': record.update_time,
}
return Response({'code': 0, 'data': data})
from django.db import transaction, IntegrityError
from decimal import Decimal
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.user_type != 'kefu':
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
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 = Chufajilu.objects.filter(
dingdan_id=dingdan_id,
sqzhuangtai__in=[0, 3]
).order_by('-create_time').first()
if not record:
exists = Chufajilu.objects.filter(dingdan_id=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 = Chufajilu.objects.filter(
dingdan_id=dingdan_id,
sqzhuangtai__in=[0, 3]
).exclude(id=record.id)
for dup in duplicate_records:
dup.sqzhuangtai = 2
dup.chuliid = phone
dup.bhliyou = "系统自动驳回:存在重复处罚记录"
dup.save()
logger.info(f"自动驳回重复处罚记录 ID={dup.id}")
if action == 1: # 同意
# 可选验证打手存在
if record.dashouid:
try:
dashou = UserDashou.objects.get(user__yonghuid=record.dashouid)
except UserDashou.DoesNotExist:
return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST)
record.sqzhuangtai = 1
record.chuliid = phone
record.save()
# 更新商家订单扩展表
try:
dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
if hasattr(dingdan, 'shangjia_kuozhan'):
dingdan.shangjia_kuozhan.sqzhuangtai = 1
dingdan.shangjia_kuozhan.save()
except Exception as e:
logger.warning(f"更新商家扩展表失败: {e}")
elif action == 2: # 驳回
# 返还积分
if record.jifen > 0 and record.dashouid:
try:
dashou = UserDashou.objects.get(user__yonghuid=record.dashouid)
if dashou.jifen <= 1000: # 安全限制
dashou.jifen += record.jifen
dashou.save()
logger.info(f"驳回处罚,打手{record.dashouid}返还积分{record.jifen}")
else:
logger.warning(f"打手积分异常({dashou.jifen}),不再返还")
except UserDashou.DoesNotExist:
logger.warning(f"打手{record.dashouid}不存在,无法返还积分")
record.sqzhuangtai = 2
record.chuliid = phone
record.bhliyou = reject_reason
record.save()
try:
dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
if hasattr(dingdan, 'shangjia_kuozhan'):
dingdan.shangjia_kuozhan.sqzhuangtai = 2
dingdan.shangjia_kuozhan.bhliyou = reject_reason
dingdan.shangjia_kuozhan.save()
except Exception as e:
logger.warning(f"更新商家扩展表失败: {e}")
return Response({'code': 0, 'msg': '处理成功'})
# yonghu/views.py
import logging
import sys
from django.db import DatabaseError
from django.db.models import Q, Sum
from yonghu.models import Tixianjilu, TixianShenheJilu
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', 'create_time', 'update_time',
)
_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.objects.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.objects.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(create_time__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('create_time')
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:
from .tixian_shenhe_services import load_audit_meta_map
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 '',
'create_time': item.create_time.strftime('%Y-%m-%d %H:%M:%S') if item.create_time else '',
'update_time': item.update_time.strftime('%Y-%m-%d %H:%M:%S') if item.update_time 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)
import logging
from decimal import Decimal
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from yonghu.models import UserMain, UserKefu, Tixianjilu, UserDashou, UserGuanshi
logger = logging.getLogger(__name__)
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.user_type != 'kefu':
logger.warning(f"用户类型非客服: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 查询提现记录
try:
record = Tixianjilu.objects.get(id=tixian_id)
except Tixianjilu.DoesNotExist:
return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
# 查询用户主表
try:
user = UserMain.objects.get(yonghuid=record.yonghuid)
except UserMain.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 '',
'create_time': record.create_time,
'update_time': record.update_time,
}
# 根据提现类型添加扩展信息
if user:
if record.leixing == 1: # 打手
try:
dashou = UserDashou.objects.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.objects.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})
import logging
import json
from decimal import Decimal
from django.db import transaction, IntegrityError
from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from yonghu.models import UserMain, UserKefu, Tixianjilu, UserDashou, UserGuanshi
logger = logging.getLogger(__name__)
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+驳回原因+退还申请扣款额(shenqing_jine),不更新平台收支/每日统计
手动打款(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.user_type != 'kefu':
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
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 内调用)"""
from .models import TixianShenheJilu
from .tixian_shenhe_services import refund_balance, sync_audit_and_jilu_status
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 = UserMain.objects.select_for_update().get(yonghuid=req_yonghuid)
except UserMain.DoesNotExist:
raise ValueError(f'用户{req_yonghuid}不存在')
if action == 1:
sync_audit_and_jilu_status(audit, 6, shenhe_ren_id=kefu_user.yonghuid)
tixian.shenheid = kefu_user.yonghuid
tixian.save(update_fields=['shenheid', 'update_time'])
else:
sync_audit_and_jilu_status(
audit, 5,
bhliyou=reason,
bo_hui_ren_id=kefu_user.yonghuid,
)
# 退还申请时扣除的 shenqing_jine(全额申请扣款),手续费不单独返还
refund_balance(user, audit.leixing, audit.shenqing_jine)
tixian.shenheid = kefu_user.yonghuid
tixian.bhliyou = reason
tixian.save(update_fields=['shenheid', 'bhliyou', 'update_time'])
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 = UserMain.objects.get(yonghuid=tixian.yonghuid)
except UserMain.DoesNotExist:
raise ValueError(f'用户{tixian.yonghuid}不存在')
if action == 1:
tixian.zhuangtai = 2
try:
from dingdan.utils import update_daily_payout
from .tixian_shenhe_services import record_manual_withdraw_success_stats
update_daily_payout(tixian.jine)
record_manual_withdraw_success_stats(user, tixian)
from peizhi.models import Szjilu
szjilu, _ = Szjilu.objects.get_or_create(id=1)
szjilu.zongsy -= tixian.jine
szjilu.zongzc += tixian.jine
szjilu.jrzc += tixian.jine
szjilu.update_time = timezone.now()
szjilu.save()
except Exception as e:
logger.error(f'更新收支记录失败: {e}')
else:
from .tixian_shenhe_services import refund_manual_tixianjilu
refund_manual_tixianjilu(user, tixian)
tixian.zhuangtai = 3
tixian.bhliyou = reason
tixian.shenheid = phone
tixian.update_time = 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)}})
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.user_type != 'kefu':
logger.warning(f"用户类型非客服: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
kefu = current_user.kefu_profile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.yonghuid}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单
try:
order = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 4. 获取打手ID
dashou_id = order.jiedan_dashou_id
if not dashou_id:
return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 查询打手
try:
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
dashou = dashou_user.dashou_profile
except UserMain.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():
chufa = Chufajilu.objects.create(
dashouid=dashou_id,
qingqiuid=phone, # 客服手机号作为请求人
cfliyou=reason or '',
sqzhuangtai=0, # 待处理
jifen=0,
dingdan_id=dingdan_id,
)
# 可选:更新订单的 clkf 字段记录处理客服
order.clkf = phone
order.save()
return Response({'code': 0, 'msg': '处罚成功'})
from django.db import transaction, IntegrityError
from django.contrib.auth.hashers import make_password
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.user_type != 'admin':
return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
admin_profile = current_user.admin_profile
except:
return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 检查手机号是否已被使用
#if UserMain.objects.filter(phone=phone).exists():
#return Response({'code': 400, 'message': '该手机号已被注册'})
# 4. 生成唯一 yonghuid
def generate_yonghuid():
import random, time
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 UserMain.objects.filter(yonghuid=uid).exists():
return uid
raise Exception('无法生成唯一用户ID')
with transaction.atomic():
# 创建主表用户
user = UserMain.objects.create(
yonghuid=generate_yonghuid(),
phone=phone,
password=password,
user_type='kefu',
openid=f'kefu_{phone}' # 临时openid
)
# 创建客服扩展表
kefu = UserKefu.objects.create(
user=user,
nicheng=nicheng or f'客服{user.yonghuid}',
erjimima=erjimima, # 建议实际项目中加密存储
zhuangtai=1,
jinrichuli=0,
jinyuechuli=0,
zongchuli=0
)
return Response({'code': 0, 'message': '添加成功'})
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from django.db.models import Q
import logging
from yonghu.models import UserMain, UserKefu, AdminProfile
logger = logging.getLogger(__name__)
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.user_type != 'admin':
return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
try:
admin_profile = current_user.admin_profile
except AttributeError:
return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询所有客服(通过扩展表关联主表)
queryset = UserKefu.objects.select_related('user').all()
if keyword:
queryset = queryset.filter(
#Q(user__yonghuid__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.user_type != 'admin':
return Response({'code': 401, 'message': '身份验证失败'})
try:
admin_profile = current_user.admin_profile
except:
return Response({'code': 401, 'message': '身份验证失败'})
# 查询客服
try:
user = UserMain.objects.get(phone=uid, user_type='kefu')
kefu = user.kefu_profile
except (UserMain.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 UserMain.objects.filter(phone=new_phone).exclude(phone=uid).exists():
return Response({'code': 400, 'message': '手机号已被使用'})
user.phone = new_phone
if 'password' in request.data:
user.password = request.data['password']
user.save()
# 更新扩展表
if 'nicheng' in request.data:
kefu.nicheng = request.data['nicheng']
if 'erjimima' in request.data:
kefu.erjimima = request.data['erjimima']
if 'zhuangtai' in request.data:
kefu.zhuangtai = int(request.data['zhuangtai'])
kefu.save()
return Response({'code': 0, 'message': '修改成功'})
# views_tixian_v3.py
import decimal
import json
import random
import string
import time
from datetime import date
import requests
from django.db import transaction, IntegrityError
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from rest_framework.views import APIView
from rest_framework import permissions, status
from rest_framework.response import Response
from peizhi.models import TixianQuotaDefault
from .models import UserMain, UserDashou, UserGuanshi, Tixianjilu, TixianAutoRecord
from utils.wechat_v3 import build_authorization
# views_tixian_v3.py
import decimal
import json
import random
import string
import time
from datetime import date
import logging
import requests
from django.db import transaction, IntegrityError
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from django.core.cache import cache
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework import permissions, status
from rest_framework.response import Response
from houtai.utils import TixianRiTongji
from houtai.utils import update_tixian_daily_stat # 引入公共方法
from houtai.utils import update_tixian_daily_stat,decrease_tixian_daily_stat # 引入公共方法
logger = logging.getLogger(__name__)
# views_tixian_v3.py
import decimal
import json
import random
import string
import time
from datetime import date
import logging
import requests
from django.db import transaction, IntegrityError
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from django.core.cache import cache
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework import permissions, status
from rest_framework.response import Response
from houtai.utils import TixianRiTongji
from houtai.utils import update_tixian_daily_stat,decrease_tixian_daily_stat # 引入公共方法
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:
from .tixian_shenhe_views import process_audit_collect
return process_audit_collect(request)
return Response({
'code': 7,
'msg': '缺少审核单号,请先提交提现申请(/yonghu/zddksh)并等待审核通过后再收款',
})
'''class TixianShenqingV3View(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
user_main = request.user
yonghuid = user_main.yonghuid
from utils.redis_lock import acquire_lock, release_lock
lock_key = f"tixian_lock:{yonghuid}"
if not acquire_lock(lock_key, timeout=10):
return Response({'code': 429, 'msg': '操作频繁,请稍后重试'})
try:
# ========== 2. 参数获取与校验 ==========
leixing = request.data.get('leixing')
jine_str = request.data.get('jine')
if leixing is None or jine_str is None:
return Response({'code': 1, 'msg': '缺少参数: leixing 或 jine'})
try:
leixing = int(leixing)
jine = decimal.Decimal(jine_str)
except (ValueError, decimal.InvalidOperation):
return Response({'code': 2, 'msg': '参数格式错误'})
if leixing not in [1, 2, 3, 4]:
return Response({'code': 3, 'msg': '提现类型无效'})
if jine <= 0.1:
return Response({'code': 4, 'msg': '提现金额必须大于0.1'})
# ========== 3. 获取费率 ==========
try:
if leixing == 1:
lilu_obj = Lilubiao.objects.get(fadanpingtai='5')
elif leixing == 2:
lilu_obj = Lilubiao.objects.get(fadanpingtai='6')
elif leixing == 3:
lilu_obj = Lilubiao.objects.get(fadanpingtai='8')
elif leixing == 4: # 🆕 考核官费率(fadanpingtai='9')
lilu_obj = Lilubiao.objects.get(fadanpingtai='9')
else:
return Response({'code': 5, 'msg': '未知提现类型'})
feilv = decimal.Decimal(str(lilu_obj.lilu))
except Lilubiao.DoesNotExist:
return Response({'code': 5, 'msg': '费率未配置'})
shouxufei = (jine * feilv).quantize(decimal.Decimal('0.01'))
shijidaozhang = (jine - shouxufei).quantize(decimal.Decimal('0.01'))
if shijidaozhang <= 0:
return Response({'code': 6, 'msg': '提现金额过低,扣除手续费后无实际到账'})
# ========== 4. 幂等性检查:同一用户不能有处理中的提现(状态0) ==========
# if TixianAutoRecord.objects.filter(yonghuid=yonghuid, zhuangtai=0).exists():
# return Response({'code': 18, 'msg': '您有正在处理中的提现,请勿重复提交'}, status=status.HTTP_400_BAD_REQUEST)
# ========== 5. 平台每日总额校验 ==========
today = date.today()
try:
daily_total_obj = TixianRiTongji.objects.get(riqi=today, leixing=leixing)
daily_total = daily_total_obj.total_amount
except TixianRiTongji.DoesNotExist:
daily_total = decimal.Decimal('0')
try:
total_limit_obj = TixianQuotaDefault.objects.get(leixing=leixing + 3) # 打手->4, 管事->5, 组长->6, 考核官->7
total_limit = total_limit_obj.default_quota
except TixianQuotaDefault.DoesNotExist:
total_limit = decimal.Decimal('100')
if daily_total + shijidaozhang > total_limit:
return Response({
'code': 33,
'msg': f'今日该角色提现总额已达上限,请明日再来'
})
# ========== 6. 数据库事务:扣款 + 更新个人当日提现 + 创建记录 ==========
try:
with transaction.atomic():
if leixing == 1: # 打手
dashou = UserDashou.objects.select_for_update().get(user=user_main)
if dashou.zhanghaozhuangtai != 1:
return Response({'code': 8, 'msg': '打手账号被封禁'})
if dashou.zhuangtai != 1:
return Response({'code': 9, 'msg': '有订单进行中,请完成后提现'})
if dashou.jifen != 10:
return Response({'code': 10, 'msg': '积分不是10分'})
if dashou.yue < jine:
return Response({'code': 11, 'msg': f'余额不足,当前余额: {dashou.yue}'})
# 个人每日限额
if dashou.last_tixian_date != today:
dashou.jinritixian_jine = decimal.Decimal('0.00')
dashou.last_tixian_date = today
quota = dashou.ewai_xiane if (dashou.kaioi_ewai_xiane and dashou.ewai_xiane > 0) \
else TixianQuotaDefault.objects.get(leixing=1).default_quota
if dashou.jinritixian_jine + jine > quota:
return Response({
'code': 12,
'msg': f'今日提现已达个人限额,今日已提{dashou.jinritixian_jine}元,限额{quota}元'
})
# 扣款 & 累加个人当日提现
dashou.yue -= jine
dashou.jinritixian_jine += jine
dashou.save()
nicheng = dashou.nicheng or ''
elif leixing == 2: # 管事
guanshi = UserGuanshi.objects.select_for_update().get(user=user_main)
if guanshi.zhuangtai != 1:
return Response({'code': 14, 'msg': '管事账号被封禁'})
if guanshi.yue < jine:
return Response({'code': 15, 'msg': f'余额不足,当前余额: {guanshi.yue}'})
if guanshi.last_tixian_date != today:
guanshi.jinritixian_jine = decimal.Decimal('0.00')
guanshi.last_tixian_date = today
quota = guanshi.ewai_xiane if (guanshi.kaioi_ewai_xiane and guanshi.ewai_xiane > 0) \
else TixianQuotaDefault.objects.get(leixing=2).default_quota
if guanshi.jinritixian_jine + jine > quota:
return Response({
'code': 16,
'msg': f'今日提现已达个人限额,今日已提{guanshi.jinritixian_jine}元,限额{quota}元'
})
guanshi.yue -= jine
guanshi.jinritixian_jine += jine
guanshi.save()
nicheng = '管事用户'
elif leixing == 3: # 组长
zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main)
if zuzhang.zhuangtai != 1:
return Response({'code': 30, 'msg': '组长账号被封禁'})
if zuzhang.ketixian_jine < jine:
return Response({'code': 31, 'msg': f'组长可提现金额不足,当前余额: {zuzhang.ketixian_jine}'})
# 组长个人每日限额(字段不同)
if zuzhang.last_tixian_time and zuzhang.last_tixian_time.date() != today:
zuzhang.jinri_tixian = decimal.Decimal('0.00')
elif not zuzhang.last_tixian_time:
zuzhang.jinri_tixian = decimal.Decimal('0.00')
try:
default_quota = TixianQuotaDefault.objects.get(leixing=3).default_quota
except TixianQuotaDefault.DoesNotExist:
default_quota = decimal.Decimal('0.00')
quota = zuzhang.ewai_tixian_xiane if (zuzhang.kaioi_ewai_tixian and zuzhang.ewai_tixian_xiane > 0) else default_quota
if zuzhang.jinri_tixian + jine > quota:
return Response({
'code': 32,
'msg': f'今日提现已达个人限额,今日已提{zuzhang.jinri_tixian}元,限额{quota}元'
})
zuzhang.ketixian_jine -= jine
zuzhang.jinri_tixian += jine
zuzhang.last_tixian_time = timezone.now()
zuzhang.save()
nicheng = ''
elif leixing == 4: # 🆕 考核官
kaoheguan = UserShenheguan.objects.select_for_update().get(user=user_main)
# 校验1: 账号状态(zhuangtai=1正常)
if kaoheguan.zhuangtai != 1:
return Response({'code': 40, 'msg': '考核官账号已被封禁,无法提现'})
# 校验2: 可提现余额是否足够
if kaoheguan.yue < jine:
return Response({'code': 41, 'msg': f'考核官可提现金额不足,当前余额: {kaoheguan.yue}'})
# 考核官没有个人每日限额字段,跳过个人限额检查
# 扣款
kaoheguan.yue -= jine
kaoheguan.save()
nicheng = '考核官'
# 生成订单号,创建记录(状态0)
tixian_id = generate_tixian_id()
auto_record = TixianAutoRecord.objects.create(
tixian_id=tixian_id,
yonghuid=yonghuid,
leixing=leixing,
jine=jine,
shouxufei=shouxufei,
shijidaozhang=shijidaozhang,
feilv=feilv,
zhuangtai=0,
)
# 事务结束,余额已扣,个人当日提现已累加,记录已创建
except TixianQuotaDefault.DoesNotExist:
return Response({'code': 17, 'msg': '默认提现限额未配置'})
except Exception as e:
logger.error(f"提现申请事务异常: {str(e)}", exc_info=True)
return Response({'code': 99, 'msg': '提现申请失败,请稍后重试'})
# ========== 7. 调用微信接口(事务外) ==========
try:
url = 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills'
transfer_scene_id = settings.WECHAT_PAY_V3_CONFIG['TRANSFER_SCENE_ID']
transfer_scene_report_infos = []
if transfer_scene_id == '1005':
if leixing == 1:
role = "打手"
desc = "佣金提现"
elif leixing == 2:
role = "管事"
desc = "分红提现"
elif leixing == 3:
role = "组长"
desc = "分红提现"
elif leixing == 4: # 🆕 考核官
role = "考核官"
desc = "分佣提现"
else:
role = "用户"
desc = "提现"
transfer_scene_report_infos = [
{"info_type": "岗位类型", "info_content": role},
{"info_type": "报酬说明", "info_content": desc}
]
elif transfer_scene_id == '1009':
transfer_scene_report_infos = [
{"info_type": "采购商品名称", "info_content": "平台服务"}
]
body = {
'appid': settings.WECHAT_PAY_V3_CONFIG['APPID'],
'out_bill_no': tixian_id,
'transfer_scene_id': transfer_scene_id,
'openid': user_main.openid,
'transfer_amount': int(shijidaozhang * 100),
'transfer_remark': f'平台提现-{yonghuid}',
'transfer_scene_report_infos': transfer_scene_report_infos,
}
body_json = json.dumps(body, separators=(',', ':'))
auth = build_authorization('POST', url, body_json)
headers = {
'Authorization': auth,
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0',
}
resp = requests.post(url, data=body_json, headers=headers, timeout=10)
wx_result = resp.json()
if resp.status_code == 200 and wx_result.get('state') == 'WAIT_USER_CONFIRM':
package_info = wx_result.get('package_info')
auto_record.wechat_package = json.dumps(package_info)
auto_record.save(update_fields=['wechat_package'])
update_tixian_daily_stat(auto_record.leixing, auto_record.shijidaozhang)
return Response({
'code': 0,
'msg': '提现申请成功,请确认收款',
'data': {
'tixian_id': tixian_id,
'package_info': package_info,
'shouxufei': str(shouxufei),
'shijidaozhang': str(shijidaozhang),
'current_rate': str(feilv),
'mch_id': settings.WECHAT_PAY_V3_CONFIG['MCHID'],
'app_id': settings.WECHAT_PAY_V3_CONFIG['APPID'],
}
})
else:
err_msg = wx_result.get('message', wx_result.get('code', '微信接口调用失败'))
raise Exception(f'微信转账接口失败: {err_msg} (HTTP {resp.status_code})')
except Exception as e:
# 微信调用失败,回滚操作
logger.error(f"微信调用失败: {str(e)}", exc_info=True)
with transaction.atomic():
auto_record.zhuangtai = 2
auto_record.fail_reason = str(e)
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
if leixing == 1:
dashou = UserDashou.objects.select_for_update().get(user=user_main)
dashou.yue += jine
dashou.jinritixian_jine -= jine
dashou.save()
elif leixing == 2:
guanshi = UserGuanshi.objects.select_for_update().get(user=user_main)
guanshi.yue += jine
guanshi.jinritixian_jine -= jine
guanshi.save()
elif leixing == 3:
zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main)
zuzhang.ketixian_jine += jine
zuzhang.jinri_tixian -= jine
zuzhang.save()
elif leixing == 4: # 🆕 考核官回滚
kaoheguan = UserShenheguan.objects.select_for_update().get(user=user_main)
kaoheguan.yue += jine
kaoheguan.save()
return Response({
'code': 99,
'msg': '运营账户资金不足,需等管理员充值后才能提现'
})
finally:
release_lock(lock_key)'''
from utils.wechat_v3 import verify_wechat_sign, decrypt_callback_ciphertext
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') or callback_data.get('state') or ''
).upper()
wechat_transfer_no = callback_data.get('transfer_no') or callback_data.get('transfer_bill_no')
fail_reason = callback_data.get('fail_reason', '') or callback_data.get('close_reason', '')
if not out_bill_no:
return Response({'code': 'FAIL', 'message': 'out_bill_no缺失'}, status=status.HTTP_400_BAD_REQUEST)
from .tixian_shenhe_services import (
ensure_shenhe_collect_completed,
mark_transfer_success,
release_collect_quota_for_record,
sync_audit_and_jilu_status,
)
from .models import TixianShenheJilu
# 4. 幂等处理
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 transfer_status == 'SUCCESS':
if auto_record.zhuangtai == 0:
mark_transfer_success(
auto_record,
wechat_transfer_no=wechat_transfer_no,
with_accounting=True,
)
elif auto_record.shenhe_danhao:
ensure_shenhe_collect_completed(auto_record.shenhe_danhao)
return Response({'code': 'SUCCESS', 'message': 'ok'})
if auto_record.zhuangtai != 0:
return Response({'code': 'SUCCESS', 'message': 'already processed'})
# 微信转账失败
auto_record.zhuangtai = 2
auto_record.fail_reason = (fail_reason or '微信转账失败')[:500]
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
release_collect_quota_for_record(auto_record)
if auto_record.shenhe_danhao:
try:
audit = TixianShenheJilu.objects.select_for_update().get(
shenhe_danhao=auto_record.shenhe_danhao
)
audit.tixian_auto_id = None
audit.fail_reason = auto_record.fail_reason
sync_audit_and_jilu_status(audit, 6, fail_reason=auto_record.fail_reason)
except TixianShenheJilu.DoesNotExist:
logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}')
decrease_tixian_daily_stat(auto_record.leixing, auto_record.shijidaozhang)
else:
# 旧流程:回滚余额
yonghuid = auto_record.yonghuid
jine = auto_record.jine
leixing = auto_record.leixing
user_main = UserMain.objects.get(yonghuid=yonghuid)
if leixing == 1:
dashou = UserDashou.objects.select_for_update().get(user=user_main)
dashou.yue += jine
dashou.jinritixian_jine -= jine
dashou.save()
elif leixing == 2:
guanshi = UserGuanshi.objects.select_for_update().get(user=user_main)
guanshi.yue += jine
guanshi.jinritixian_jine -= jine
guanshi.save()
else:
zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main)
zuzhang.ketixian_jine += jine
zuzhang.jinri_tixian -= jine
zuzhang.save()
decrease_tixian_daily_stat(auto_record.leixing, auto_record.shijidaozhang)
return Response({'code': 'SUCCESS', 'message': 'ok'})
class TixianQueRenAutoView(APIView):
"""
用户确认提现结果
POST /yonghu/tixianqr
新审核流程:打款记录带 shenhe_danhao 时,确认成功同步审核表+提现记录表为 2;取消不退款(申请已扣),恢复待收款 6 可重试
"""
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
from .tixian_shenhe_services import mark_transfer_success, release_collect_quota_for_record
with transaction.atomic():
try:
auto_record = TixianAutoRecord.objects.select_for_update().get(
tixian_id=tixian_id,
yonghuid=user_main.yonghuid,
)
except TixianAutoRecord.DoesNotExist:
return Response({'code': 4, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
if auto_record.zhuangtai != 0:
if result == 1:
if auto_record.shenhe_danhao:
from .tixian_shenhe_services import ensure_shenhe_collect_completed
ensure_shenhe_collect_completed(auto_record.shenhe_danhao)
return Response({'code': 0, 'msg': '提现成功', 'data': None})
return Response({'code': 5, 'msg': '该提现已处理'})
is_audit_flow = bool(auto_record.shenhe_danhao)
if result == 1:
mark_transfer_success(auto_record, with_accounting=True)
return Response({'code': 0, 'msg': '提现成功', 'data': None})
# 用户取消或失败
auto_record.zhuangtai = 2
auto_record.fail_reason = '用户取消收款'
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
release_collect_quota_for_record(auto_record)
if is_audit_flow:
# 新审核流程:申请时已扣款,取消不退余额,审核单恢复 6 待收款,可再次点收款
from .models import TixianShenheJilu
from .tixian_shenhe_services import sync_audit_and_jilu_status
try:
audit = TixianShenheJilu.objects.select_for_update().get(
shenhe_danhao=auto_record.shenhe_danhao,
)
audit.tixian_auto_id = None
sync_audit_and_jilu_status(
audit, 6,
fail_reason='用户取消收款,可重新发起',
)
except TixianShenheJilu.DoesNotExist:
logger.warning(f'确认取消但审核记录不存在: {auto_record.shenhe_danhao}')
decrease_tixian_daily_stat(auto_record.leixing, auto_record.shijidaozhang)
else:
# 旧流程:收款时才扣款,取消需退还
jine = auto_record.jine
leixing = auto_record.leixing
if leixing == 1:
dashou = UserDashou.objects.select_for_update().get(user=user_main)
dashou.yue += jine
dashou.jinritixian_jine -= jine
dashou.save()
elif leixing == 2:
guanshi = UserGuanshi.objects.select_for_update().get(user=user_main)
guanshi.yue += jine
guanshi.jinritixian_jine -= jine
guanshi.save()
else:
zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main)
zuzhang.ketixian_jine += jine
zuzhang.jinri_tixian -= jine
zuzhang.save()
decrease_tixian_daily_stat(auto_record.leixing, auto_record.shijidaozhang)
return Response({'code': 0, 'msg': '提现已取消', 'data': None})
except Exception as e:
logger.error(f'提现确认异常: {str(e)}', exc_info=True)
return Response({'code': 99, 'msg': '系统错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
from utils.wechat_v3 import verify_wechat_sign, decrypt_callback_ciphertext
'''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':
# 微信转账成功
auto_record.zhuangtai = 1
auto_record.wechat_transfer_no = wechat_transfer_no
auto_record.save(update_fields=['zhuangtai', 'wechat_transfer_no'])
# 更新收支记录(平台资金统计)
try:
from dingdan.utils import update_daily_payout
update_daily_payout(auto_record.shijidaozhang)
szjilu = Szjilu.objects.select_for_update().get(id=1)
szjilu.zongsy -= auto_record.shijidaozhang
szjilu.zongzc += auto_record.shijidaozhang
szjilu.jrzc += auto_record.shijidaozhang
szjilu.save()
except Szjilu.DoesNotExist:
Szjilu.objects.create(
id=1,
zongsy=-auto_record.shijidaozhang,
zongzc=auto_record.shijidaozhang,
jrzc=auto_record.shijidaozhang
)
except Exception as e:
logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
raise # 抛出异常让事务回滚
else: # transfer_status == 'FAIL'
# 微信转账失败
auto_record.zhuangtai = 2
auto_record.fail_reason = fail_reason or '微信转账失败'
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
# 回滚:恢复用户余额和个人当日提现
yonghuid = auto_record.yonghuid
jine = auto_record.jine
leixing = auto_record.leixing
user_main = UserMain.objects.get(yonghuid=yonghuid)
if leixing == 1:
dashou = UserDashou.objects.select_for_update().get(user=user_main)
dashou.yue += jine
dashou.jinritixian_jine -= jine
dashou.save()
elif leixing == 2:
guanshi = UserGuanshi.objects.select_for_update().get(user=user_main)
guanshi.yue += jine
guanshi.jinritixian_jine -= jine
guanshi.save()
else: # leixing == 3
zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main)
zuzhang.ketixian_jine += jine
zuzhang.jinri_tixian -= jine
zuzhang.save()
# 回滚平台每日统计(减去申请时占用的实际到账金额)
decrease_tixian_daily_stat(auto_record.leixing, auto_record.shijidaozhang)
return Response({'code': 'SUCCESS', 'message': 'ok'})
class TixianQueRenAutoView(APIView):
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():
auto_record = TixianAutoRecord.objects.select_for_update().get(
tixian_id=tixian_id,
yonghuid=user_main.yonghuid
)
# 幂等检查:只有状态0才能处理
if auto_record.zhuangtai != 0:
return Response({'code': 5, 'msg': '该提现已处理'})
if result == 1:
# 用户确认成功 → 直接处理为成功
auto_record.zhuangtai = 1
auto_record.save(update_fields=['zhuangtai'])
# 更新收支记录(平台资金统计)
try:
from dingdan.utils import update_daily_payout
update_daily_payout(auto_record.shijidaozhang)
szjilu = Szjilu.objects.select_for_update().get(id=1)
szjilu.zongsy -= auto_record.shijidaozhang
szjilu.zongzc += auto_record.shijidaozhang
szjilu.jrzc += auto_record.shijidaozhang
szjilu.save()
except Szjilu.DoesNotExist:
Szjilu.objects.create(
id=1,
zongsy=-auto_record.shijidaozhang,
zongzc=auto_record.shijidaozhang,
jrzc=auto_record.shijidaozhang
)
except Exception as e:
logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
raise
# 注意:个人当日提现金额已经在申请接口中累加,此处不需要再操作
return Response({'code': 0, 'msg': '提现成功', 'data': None})
else:
# 用户取消:回滚余额和个人当日提现
auto_record.zhuangtai = 2
auto_record.fail_reason = '用户取消'
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
jine = auto_record.jine
leixing = auto_record.leixing
if leixing == 1:
dashou = UserDashou.objects.select_for_update().get(user=user_main)
dashou.yue += jine
dashou.jinritixian_jine -= jine
dashou.save()
elif leixing == 2:
guanshi = UserGuanshi.objects.select_for_update().get(user=user_main)
guanshi.yue += jine
guanshi.jinritixian_jine -= jine
guanshi.save()
else:
zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main)
zuzhang.ketixian_jine += jine
zuzhang.jinri_tixian -= jine
zuzhang.save()
# 回滚平台每日统计(减去申请时占用的实际到账金额)
decrease_tixian_daily_stat(auto_record.leixing, auto_record.shijidaozhang)
return Response({'code': 0, 'msg': '提现已取消', 'data': None})
except Exception as e:
logger.error(f"提现确认异常: {str(e)}", exc_info=True)
return Response({'code': 99, 'msg': '系统错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)'''
from .models import (
UserMain, UserBoss, UserDashou, UserGuanshi,
UserZuzhang
)
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 = get_or_create_wechat_user(openid, user_type='normal')
update_wechat_user_login_info(user_main, kehuduan_ip)
if created:
UserBoss.objects.create(user=user_main, nickname='微信用户')
if hasattr(user_main, 'guanshi_profile'):
return self._fanhui_xianyou_guanshi_info(user_main)
try:
zuzhang = UserZuzhang.objects.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.yonghuid
guanshi_yaoqingma = chuangjianYaoqingma(str(user_main.yonghuid))
guanshi_profile = UserGuanshi.objects.create(
user=user_main,
yaoqingma=guanshi_yaoqingma,
yaoqingren=zuzhang_yonghuid,
)
UserZuzhang.objects.filter(pk=zuzhang.pk).update(
yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
)
if not hasattr(user_main, 'dashou_profile'):
dashou_profile = UserDashou.objects.create(
user=user_main,
nicheng='大手子',
chenghao='普通大手',
yaoqingren=zuzhang_yonghuid,
)
else:
dashou_profile = user_main.dashou_profile
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 = wx_cfg.WEIXIN_APPID
secret = wx_cfg.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 UserMain.objects.filter(yonghuid=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.objects.filter(id__in=[1, 2])
result = {
'dashouqun': '',
'dashouqunid': '',
'guanshiqun': '',
'guanshiqunid': ''
}
for config in qun_configs:
if config.id == 1:
result['dashouqun'] = config.neirong or ''
result['dashouqunid'] = config.qunid or ''
elif config.id == 2:
result['guanshiqun'] = config.neirong or ''
result['guanshiqunid'] = config.qunid 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.objects.filter(yonghu_id=dashou_profile.user.yonghuid)
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.guanshi_profile
dashou_profile = getattr(user_main, 'dashou_profile', None)
group_info = self._huoqu_qun_peizhi()
dashou_status = 1 if dashou_profile else 0
shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0
guanshi_status = 1
try:
nicheng = user_main.boss_profile.nickname or '微信用户'
except:
nicheng = '微信用户'
refresh = RefreshToken.for_user(user_main)
token = str(refresh.access_token)
data = {
'token': token,
'nicheng': nicheng,
'uid': user_main.yonghuid,
'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.boss_profile.nickname or '微信用户'
except:
nicheng = '微信用户'
dashou_status = 1
shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0
guanshi_status = 1
group_info = self._huoqu_qun_peizhi()
data = {
'token': token,
'nicheng': nicheng,
'uid': user_main.yonghuid,
'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
from django.db import transaction, IntegrityError, models
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, 'guanshi_profile'):
# 用户已是管事,直接返回现有信息
return self._fanhui_xianyou_guanshi_info(current_user)
# 3. 验证邀请码对应的组长
try:
zuzhang = UserZuzhang.objects.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.yonghuid
# 4. 事务内创建管事实体和打手(如果需要)
with transaction.atomic():
# 创建管事扩展表
guanshi_yaoqingma = chuangjianYaoqingma(str(current_user.yonghuid))
guanshi_profile = UserGuanshi.objects.create(
user=current_user,
yaoqingma=guanshi_yaoqingma,
yaoqingren=zuzhang_yonghuid,
)
# 更新组长的邀请总数
UserZuzhang.objects.filter(pk=zuzhang.pk).update(
yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
)
# 检查用户是否已是打手,如果不是则创建
if not hasattr(current_user, 'dashou_profile'):
dashou_profile = UserDashou.objects.create(
user=current_user,
nicheng='大手子',
chenghao='普通大手',
yaoqingren=zuzhang_yonghuid,
)
else:
dashou_profile = current_user.dashou_profile
# 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.objects.filter(id__in=[1, 2])
result = {
'dashouqun': '',
'dashouqunid': '',
'guanshiqun': '',
'guanshiqunid': ''
}
for config in qun_configs:
if config.id == 1:
result['dashouqun'] = config.neirong or ''
result['dashouqunid'] = config.qunid or ''
elif config.id == 2:
result['guanshiqun'] = config.neirong or ''
result['guanshiqunid'] = config.qunid 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.objects.filter(yonghu_id=dashou_profile.user.yonghuid)
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.guanshi_profile
dashou_profile = getattr(user_main, 'dashou_profile', None)
group_info = self._huoqu_qun_peizhi()
dashou_status = 1 if dashou_profile else 0
shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0
guanshi_status = 1
try:
nicheng = user_main.boss_profile.nickname or '微信用户'
except:
nicheng = '微信用户'
refresh = RefreshToken.for_user(user_main)
token = str(refresh.access_token)
data = {
'token': token,
'nicheng': nicheng,
'uid': user_main.yonghuid,
'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.boss_profile.nickname or '微信用户'
except:
nicheng = '微信用户'
dashou_status = 1
shangjia_status = 1 if hasattr(user_main, 'shop_profile') else 0
guanshi_status = 1
group_info = self._huoqu_qun_peizhi()
data = {
'token': token,
'nicheng': nicheng,
'uid': user_main.yonghuid,
'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
from rest_framework_simplejwt.authentication import JWTAuthentication
from django.db import IntegrityError
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 = "jdjdhsavjsjdb64646/'3464jdjs"
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.objects.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.yonghuid)
# 创建组长扩展记录
try:
with transaction.atomic():
zuzhang = UserZuzhang.objects.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.yonghuid, retry=True)
zuzhang = UserZuzhang.objects.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位字符"""
import random, string
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.objects.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.objects.filter(yaoqingren=user.yonghuid).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.objects.get(user=user)
except UserZuzhang.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是组长'}, status=404)
guanshi_count = UserGuanshi.objects.filter(yaoqingren=user.yonghuid).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)
鉴权逻辑:已注册打手且押金 > 0 即通过
"""
class DashouJianquanView(APIView):
"""打手消息权限鉴权"""
permission_classes = [IsAuthenticated]
def post(self, request):
user = request.user
try:
dashou = user.dashou_profile
except UserDashou.DoesNotExist:
return Response({
'code': 0,
'allow': 0,
'msg': '您尚未注册打手身份,请先注册'
})
yajin = dashou.yajin or 0
if yajin > 0:
return Response({
'code': 0,
'allow': 1,
'msg': '鉴权通过(押金满足条件)'
})
return Response({
'code': 0,
'allow': 0,
'msg': '消息权限不足,请先缴纳押金'
})
# apps/yonghu/views_laoban_jianquan.py
"""
老板(普通用户) IM 消息权限鉴权接口
URL: /yonghu/lbhqltqx
方法: POST
认证: JWT Token (request.user)
鉴权逻辑:已登录用户默认允许使用消息功能
"""
class LaobanJianquanView(APIView):
"""老板消息权限鉴权"""
permission_classes = [IsAuthenticated]
def post(self, request):
return Response({
'code': 0,
'allow': 1,
'msg': '鉴权通过'
})
class HuoQuYaoQingRenView(APIView):
"""
获取当前打手的邀请人信息
POST /yonghu/hqwdyqr
Headers: Authorization: Bearer
返回邀请人的用户ID、昵称(来自老板扩展表)、头像相对URL
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
current_user = request.user # 当前登录用户
yonghuid = current_user.yonghuid # 7位用户ID
# 1. 校验打手身份及账号状态(只检查账号状态,不检查接单状态)
try:
dashou = UserDashou.objects.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 = UserMain.objects.get(yonghuid=yaoqingren_id)
touxiang = inviter_main.avatar or ''
except UserMain.DoesNotExist:
return Response({
'code': 404,
'message': '邀请人信息不存在',
'data': None
})
# 4. 查询老板扩展表(昵称),如果没有则使用空字符串
nicheng = ''
try:
boss = UserBoss.objects.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
})
# views.py (在处罚相关app的views中)
import logging
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from django.db.models import Count, Q, Case, When, IntegerField
from dingdan.models import Fadan
from dingdan.models import FadanShensuTupian
logger = logging.getLogger('xiaochengxu')
class FaKuanJiFenTongJiView(APIView):
"""
获取罚款与积分处罚统计总览
路径: POST /yonghu/cffktjhq
"""
permission_classes = [IsAuthenticated, DashouPermission]
def post(self, request):
try:
user_main = request.user
yonghuid = user_main.yonghuid
# ================= 罚款统计 =================
fadan_stats = Fadan.objects.filter(beichufa_id=yonghuid).aggregate(
total=Count('id'),
daijiaona=Count('id', filter=Q(zhuangtai=1)),
shensuzhong=Count('id', filter=Q(zhuangtai=3)),
yijiaona=Count('id', filter=Q(zhuangtai=2)),
yibohui=Count('id', filter=Q(zhuangtai=4)),
)
# ================= 积分处罚统计 =================
jifen_stats = Chufajilu.objects.filter(dashouid=yonghuid).aggregate(
total=Count('id'),
daichuli=Count('id', filter=Q(sqzhuangtai=0) | Q(sqzhuangtai=3)),
yichuli=Count('id', filter=Q(sqzhuangtai=1) | Q(sqzhuangtai=2)),
# 细分
daichuli_0=Count('id', filter=Q(sqzhuangtai=0)),
shensuzhong_3=Count('id', filter=Q(sqzhuangtai=3)),
yichufa_1=Count('id', filter=Q(sqzhuangtai=1)),
yibohui_2=Count('id', filter=Q(sqzhuangtai=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)
create_time, update_time
}],
has_more, page, page_size
}
"""
permission_classes = [IsAuthenticated, DashouPermission]
def post(self, request):
try:
user_main = request.user
yonghuid = user_main.yonghuid
# 获取分页参数
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 = Fadan.objects.filter(beichufa_id=yonghuid)
# 状态筛选
if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]:
qs = qs.filter(zhuangtai=int(zhuangtai))
# 订单号模糊搜索
if sousuo_dingdan_id:
qs = qs.filter(guanliandingdan_id__icontains=sousuo_dingdan_id)
# 按创建时间倒序
qs = qs.order_by('-create_time')
# 分页
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 = FadanShensuTupian.objects.filter(
fadan_id__in=fadan_ids
).values('fadan_id', 'tupian_url', 'yongtu')
for t in tupians:
fid = t['fadan_id']
url = t['tupian_url']
if t['yongtu'] == 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.beichufa_id,
'guanliandingdan_id': r.guanliandingdan_id or '',
'chufaliyou': r.chufaliyou or '',
'fakuanjine': str(r.fakuanjine),
'zhuangtai': r.zhuangtai,
'yingxiang_qiangdan': r.yingxiang_qiangdan,
'shensuliyou': r.shensuliyou or '',
'bohuiliyou': r.bohuiliyou or '',
'zhengju_tupian': zhengju_map.get(rid, []), # 证据图片
'shensu_tupian': shensu_map.get(rid, []), # 申诉图片
'create_time': r.create_time.strftime('%Y-%m-%d %H:%M:%S') if r.create_time else '',
'update_time': r.update_time.strftime('%Y-%m-%d %H:%M:%S') if r.update_time 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.yonghuid
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 = Fadan.objects.get(id=fadan_id, beichufa_id=yonghuid)
except Fadan.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在或无权操作'}, status=404)
# 只允许待缴纳状态(状态1)且没有驳回理由的罚单申诉
if fadan.zhuangtai != 1:
return Response({'code': 400, 'msg': '当前状态不可申诉'}, status=400)
if fadan.bohuiliyou:
return Response({'code': 400, 'msg': '该罚单申诉已被驳回,不可再次申诉'}, status=400)
# 更新罚单状态为申诉中,并记录理由
fadan.zhuangtai = 3
fadan.shensuliyou = shensu_liyou
fadan.save()
# 删除旧的申诉图片(yongtu=2),仅删除打手自己上传的申诉图
FadanShensuTupian.objects.filter(fadan=fadan, yongtu=2).delete()
# 插入新上传的申诉图片
for url in tupian_urls:
FadanShensuTupian.objects.create(
fadan=fadan,
beichufa_id=yonghuid,
tupian_url=url,
yongtu=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)
# 需要替换为你自己项目中的导入路径
from shangpin.models import Czjilu
import string # 【致命错误修正】必须导入 string
from django.views import View
logger = logging.getLogger('xiaochengxu')
# ==================== 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 = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid)
except Fadan.DoesNotExist:
return Response({'code': 404, 'message': '罚单不存在或不属于您'}, status=status.HTTP_404_NOT_FOUND)
if fadan.zhuangtai not in [1, 3]:
return Response({'code': 400, 'message': '罚单当前状态不可支付'}, status=status.HTTP_400_BAD_REQUEST)
jine = float(fadan.fakuanjine)
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.objects.create(
dingdan_id=dingdanid,
zhuangtai=9,
yonghuid=request.user.yonghuid,
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 = wx_cfg.WEIXIN_APPID
MCHID = wx_cfg.WEIXIN_MCHID
KEY = wx_cfg.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"""
{APPID}
{PAY_DESCRIPTION}
{MCHID}
{nonce_str}
{NOTIFY_URL}
{openid}
{dingdanid}
{user_ip}
{total_fee}
{trade_type}
{sign}
"""
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. 罚款支付回调接口 ====================
from django.views import View
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.objects.get(dingdan_id=out_trade_no)
logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}")
except Czjilu.DoesNotExist:
logger.error(f"订单不存在: {out_trade_no}")
return self.wechat_response('FAIL', '订单不存在')
# 5. 检查订单状态是否为未支付(9)
if order.zhuangtai != 9:
logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
# 已处理过的订单直接返回成功,避免微信重复回调
return self.wechat_response('SUCCESS', 'OK')
# 6. 验证金额(微信返回的是分,需要转换)
wechat_amount_yuan = total_fee / 100 # 转换为元
order_amount_yuan = float(order.jine) # 订单金额(元)
# 允许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.zhuangtai = 3
order.save(update_fields=['zhuangtai'])
logger.info(f"订单状态更新成功: {order.dingdan_id} -> 3")
# 8. 更新罚单状态为已缴纳(2)
try:
# 根据订单中的用户ID和金额匹配待缴纳的罚单
fadan = Fadan.objects.filter(
beichufa_id=order.yonghuid,
fakuanjine=order.jine,
zhuangtai__in=[1, 3]
).first()
if fadan:
fadan.zhuangtai = 2 # 已缴纳
fadan.save(update_fields=['zhuangtai'])
logger.info(f"罚单 {fadan.id} 已自动标记为已缴纳")
from yonghu.fadan_fenhong_utils import process_fadan_fenhong
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:
from peizhi.models import Szjilu
from decimal import Decimal
# 获取订单金额(确保是Decimal类型)
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
from dingdan.utils import update_daily_income
update_daily_income(jine_decimal)
# 查询或创建ID=1的收支记录
szjilu, created = Szjilu.objects.get_or_create(
id=1,
defaults={
'zongsy': Decimal('0.00'),
'zongls': Decimal('0.00'),
'zongzc': Decimal('0.00'),
'jrzc': Decimal('0.00'),
'jrls': Decimal('0.00')
}
)
# 更新三个字段
szjilu.zongsy += jine_decimal # 总收益
szjilu.zongls += jine_decimal # 总流水
szjilu.jrls += jine_decimal # 今日流水
szjilu.save()
logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{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={wx_cfg.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"""
"""
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.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid)
if order.zhuangtai == 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.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid)
if order.zhuangtai == 9: # 未支付
order.delete()
logger.info(f"用户{request.user.yonghuid}删除未支付订单: {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': '服务器内部错误'})
from shangpin.models import Czjilu
from dengji.models import Chenghao, ShenheJilu, KaoheCishuFeiyong, YonghuChenghao
from dengji.utils import get_tag_fee # 您需要确保此函数已定义
from dengji.utils import create_shenhe_jilu
from dengji.utils import validate_shenheguan
logger = logging.getLogger('xiaochengxu')
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.yonghuid} 发起考核支付申请,标签ID: {tag_id}, 再次申请: {reapply}")
if not tag_id or not game_nick:
logger.warning(f"用户 {user.yonghuid} 参数不完整: tag_id={tag_id}, game_nick={game_nick}")
return Response({'code': 400, 'message': '标签ID和游戏昵称不能为空'})
# 1. 检查是否有未支付的考核订单
"""unpaid_order = Czjilu.objects.filter(
yonghuid=user.yonghuid,
leixing=5,
zhuangtai=9
).exists()
if unpaid_order:
logger.warning(f"用户 {user.yonghuid} 存在未支付的考核订单")
return Response({'code': 400, 'message': '您有未支付的考核订单,请先完成支付或取消'})"""
# 2. 验证标签
try:
chenghao = Chenghao.objects.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.objects.filter(yonghu=user, chenghao=chenghao).exists():
logger.warning(f"用户 {user.yonghuid} 已拥有标签 {chenghao.mingcheng}")
return Response({'code': 400, 'message': '您已拥有该标签,无需重复考核'})
# 4. 检查是否有未完成的申请
if ShenheJilu.objects.filter(
shenqingren_id=user.yonghuid,
chenghao=chenghao,
zhuangtai__in=[0, 3] # 审核中、待审核
).exists():
logger.warning(f"用户 {user.yonghuid} 有未完成的考核申请")
return Response({'code': 400, 'message': '您有未完成的考核申请,请等待结果'})
# 5. 计算考核次数和费用
if reapply and jilu_id:
last = ShenheJilu.objects.filter(
jilu_id=jilu_id,
shenqingren_id=user.yonghuid,
zhuangtai=2
).first()
if not last:
logger.warning(f"用户 {user.yonghuid} 再次申请失败:原记录不存在或状态不是未通过,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.objects.filter(
shenqingren_id=user.yonghuid,
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.objects.create(
dingdan_id=dingdanid,
zhuangtai=9, # 未支付
yonghuid=user.yonghuid,
jine=float(fee),
leixing=5, # 5-考核支付
shuoming=settings.KAOHE_PAY_DESCRIPTION
)
logger.info(f"创建支付订单成功: {dingdanid}")
# 9. 保存考核支付临时上下文(用于回调时取业务参数)
try:
from dengji.models import KaohePayTemp
KaohePayTemp.objects.create(
dingdan_id=dingdanid,
yonghuid=user.yonghuid,
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:
from dengji.models import KaohePayTemp
KaohePayTemp.objects.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 = wx_cfg.WEIXIN_APPID
MCHID = wx_cfg.WEIXIN_MCHID
KEY = wx_cfg.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"""
{APPID}
{PAY_DESCRIPTION}
{MCHID}
{nonce_str}
{NOTIFY_URL}
{openid}
{dingdanid}
{user_ip}
{total_fee}
{trade_type}
{sign}
"""
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.objects.get(dingdan_id=out_trade_no, leixing=5)
logger.info(f"找到订单: {out_trade_no}, 当前状态: {order.zhuangtai}")
except Czjilu.DoesNotExist:
logger.error(f"考核订单不存在: {out_trade_no}")
return self.wechat_response('FAIL', '订单不存在')
if order.zhuangtai != 9:
logger.warning(f"订单状态非未支付: {order.zhuangtai}, 订单号: {out_trade_no}")
# 已支付的订单直接返回成功,避免微信重复回调
if order.zhuangtai == 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.jine)) > 0.01:
logger.error(f"金额不一致: 订单金额={order.jine}, 回调金额={wechat_yuan}, 订单号: {out_trade_no}")
return self.wechat_response('FAIL', '金额不一致')
logger.info(f"金额验证通过: {wechat_yuan}元")
# 更新订单状态为已支付
order.zhuangtai = 3
order.save()
logger.info(f"订单状态更新为已支付: {out_trade_no}")
# 从临时表获取业务参数并创建审核记录
try:
from dengji.models import KaohePayTemp
from dengji.utils import create_shenhe_jilu_from_temp
from yonghu.models import UserMain
temp = KaohePayTemp.objects.get(dingdan_id=out_trade_no, yonghuid=order.yonghuid)
logger.info(f"找到临时表记录: {out_trade_no}, 用户ID: {order.yonghuid}")
user = UserMain.objects.get(yonghuid=order.yonghuid)
logger.info(f"获取用户信息成功: {user.yonghuid}")
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.zhuangtai = 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.zhuangtai = 10
order.save()
logger.error(f"订单状态标记为异常(10): {out_trade_no}")
# 更新收支记录
try:
from peizhi.models import Szjilu
from decimal import Decimal
from dingdan.utils import update_daily_income
jine_decimal = Decimal(str(order.jine))
update_daily_income(jine_decimal)
logger.info(f"更新每日收入成功: {jine_decimal}元")
szjilu, created = Szjilu.objects.get_or_create(
id=1,
defaults={
'zongsy': Decimal('0.00'),
'zongls': Decimal('0.00'),
'zongzc': Decimal('0.00'),
'jrzc': Decimal('0.00'),
'jrls': Decimal('0.00'),
}
)
szjilu.zongsy += jine_decimal
szjilu.zongls += jine_decimal
szjilu.jrls += jine_decimal
szjilu.save()
logger.info(f"更新收支记录成功: zongsy={szjilu.zongsy}")
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={wx_cfg.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"""
"""
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.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5)
if order.zhuangtai == 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.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5)
if order.zhuangtai == 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.dashou_profile
data['dashou_yue'] = str(dashou.yue)
data['dashou_yajin'] = str(dashou.yajin)
except ObjectDoesNotExist:
pass
# ------------------- 商家资产 -------------------
try:
shangjia = user.shop_profile
data['shangjia_yue'] = str(shangjia.yue)
except ObjectDoesNotExist:
pass
# ------------------- 管事资产 -------------------
try:
guanshi = user.guanshi_profile
data['guanshi_fenyong'] = str(guanshi.yue)
except ObjectDoesNotExist:
pass
# ------------------- 组长资产 -------------------
try:
zuzhang = user.zuzhang_profile
data['zuzhang_fenyong'] = str(zuzhang.ketixian_jine)
except ObjectDoesNotExist:
pass
# ------------------- 审核官(考核官)资产 -------------------
try:
kaoheguan = user.shenheguan_profile
data['kaoheguan_fenyong'] = str(kaoheguan.yue)
except ObjectDoesNotExist:
pass
# ------------------- 费率查询(从 Lilubiao 获取) -------------------
# 打手提现费率 (类型 '5')
lilu_obj = Lilubiao.objects.filter(fadanpingtai='5').first()
data['dashou_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
# 打手保证金提现费率 (类型 '11')
lilu_obj = Lilubiao.objects.filter(fadanpingtai='11').first()
data['dashou_yajin_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
# 商家提现费率 (类型 '10')
lilu_obj = Lilubiao.objects.filter(fadanpingtai='10').first()
data['shangjia_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
# 管事提现费率 (类型 '6')
lilu_obj = Lilubiao.objects.filter(fadanpingtai='6').first()
data['guanshi_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
# 组长提现费率 (类型 '8')
lilu_obj = Lilubiao.objects.filter(fadanpingtai='8').first()
data['zuzhang_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
# 审核官(考核官)提现费率 (类型 '9')
lilu_obj = Lilubiao.objects.filter(fadanpingtai='9').first()
data['kaoheguan_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00
return Response({
'code': 200,
'msg': '获取成功',
'data': data
}, status=status.HTTP_200_OK)