import os import sys import json import re import time import uuid import random import string import requests import hashlib import traceback import decimal import jwt import ipaddress import xmltodict from datetime import datetime, timedelta from decimal import Decimal import defusedxml.ElementTree as ET from django.conf import settings from django.db import models, transaction, IntegrityError, DatabaseError, connection from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch from django.core.cache import cache from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.exceptions import ObjectDoesNotExist from django.shortcuts import get_object_or_404 from django.utils import timezone from django.http import HttpResponse from django.views import View from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.hashers import make_password from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import permissions, status from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.parsers import MultiPartParser, FormParser, JSONParser from rest_framework.throttling import AnonRateThrottle from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.authentication import JWTAuthentication from gvsdsdk.fluent import db, func, FQ from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss from utils.fadan_utils import check_fadan_qiangdan_eligible from utils.wechat_v3 import verify_wechat_sign, decrypt_callback_ciphertext from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode from users.fadan_fenhong_utils import process_fadan_fenhong from orders.utils import ( update_daily_payout, update_daily_income, settle_shangjia_order_guanshi_fenhong ) from backend.utils import ( update_dashou_daily_by_action, update_guanshi_daily_by_action, update_shangjia_daily, update_zuzhang_daily_by_action, verify_kefu_permission ) from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan from .models import ( UserBoss, UserDashou, UserShangjia, UserGuanshi, UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser, TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu, RankingRecord ) from gvsdsdk.models import User from .tixian_shenhe_services import ( load_audit_meta_map, refund_balance, sync_audit_and_jilu_status, mark_transfer_success, release_collect_quota_for_record, close_audit_wechat_failed, query_wechat_transfer_bill, check_shijidaozhang_within_limit, WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING, ) from .tixian_shenhe_views import process_audit_collect from orders.models import ( Order, PlatformOrderExt, MerchantOrderExt, CommissionRate, PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage, Penalty, PenaltyAppealImage ) from products.models import ( ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu ) from config.models import Qunpeizhi, Szjilu from backend.models import MerchantDailyStats from rank.models import ( KaohePayTemp, Chenghao, ShenheJilu, KaoheCishuFeiyong, YonghuChenghao ) from users.models import UserShenheguan from products.utils import update_shangpin_daily_stat from shop.utils import update_dianpu_daily_stat from rank.utils import create_shenhe_jilu_from_temp import logging logger = logging.getLogger(__name__) # 获取用户代理信息(用于更详细的追踪) def huoquYonghuDaili(request): """ 获取用户浏览器/设备信息 """ user_agent = request.META.get('HTTP_USER_AGENT', '') # 这里可以添加更复杂的用户代理解析 # 为简化,我们只返回原始字符串的前100个字符 return user_agent[:100] if user_agent else '' def huoquKehuduanIP(request): """ 强化版:安全、兼容地获取客户端真实IP地址 """ ip = None # 1. 从标准头部获取IP链 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: # 按逗号分割,取第一个(即最原始的客户端IP) ip = x_forwarded_for.split(',')[0].strip() # 2. 如果上面没有,则用Nginx传递的X-Real-IP if not ip: ip = request.META.get('HTTP_X_REAL_IP') # 3. 最后降级到连接地址 if not ip: ip = request.META.get('REMOTE_ADDR') # ===== 核心:清洗和验证IP格式 ===== if ip: # 情况A: 处理IPv4映射的IPv6地址 (::ffff:192.168.1.1) if ip.startswith('::ffff:'): ip = ip[7:] # 情况B: 处理带端口的IPv4地址 (223.91.110.123:62417) # 这是导致您记录“三段”的最可能原因! # 如果IP字符串中有冒号,且最后一个冒号前的部分是IPv4,则分离端口。 if ':' in ip and '.' in ip: # 尝试按冒号分割,取第一部分 possible_ip = ip.rsplit(':', 1)[0] # 从右边分割一次 # 验证它是否是有效的IPv4地址 try: ipaddress.IPv4Address(possible_ip) # 如果通过验证,说明格式正确 ip = possible_ip except (ipaddress.AddressValueError, ValueError): # 如果验证失败,说明可能不是IPv4带端口,保持原样或进行其他处理 pass # 情况C: 验证最终结果是否为有效IPv4(可选,但推荐) try: # 此步骤会抛出异常,如果ip变量此时仍是“223.91.110.”这种格式 ipaddress.IPv4Address(ip) except (ipaddress.AddressValueError, ValueError): # 记录一个警告日志,并将IP标记为无效或存为原始字符串 print(f"警告:获取到无效或非标准IP地址格式: {ip}") # 您可以选择在此处 return None 或存储原始字符串以供排查 return ip class WechatMiniProgramLoginView(APIView): """ 微信小程序登录接口 """ throttle_classes = [AnonRateThrottle] permission_classes = [AllowAny] def post(self, request): try: code = request.data.get('code', '').strip() if not code: return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # 获取微信openid和unionid wechat_data = self.get_wechat_openid(code) if not wechat_data or 'openid' not in wechat_data: error_msg = wechat_data.get('errmsg', '微信授权失败') return Response({'code': 2, 'msg': f'微信登录失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST) openid = wechat_data['openid'] session_key = wechat_data.get('session_key', '') unionid = wechat_data.get('unionid', '') # 🆕【新增】获取unionid # 获取客户端真实IP kehuduan_ip = self.huoquKehuduanIP(request) with transaction.atomic(): # 创建或获取用户 user_main, created = User.objects.select_for_update().get_or_create( OpenID=openid, defaults={'UserUID': self.shengchengYonghuID(), 'UserName': openid} ) # 保存unionid if unionid and unionid != user_main.UnionID: user_main.UnionID = unionid user_main.IP = kehuduan_ip user_main.UserLastLoginDate = timezone.now() user_main.save() if created: UserBoss.query.create(user=user_main, nickname='板板大人') # 新用户默认分配消费者角色 from gvsdsdk.models import UserRole, Role normal_role = Role.objects.filter(RoleName='消费者').first() if normal_role: UserRole.objects.create( UserRoleUUID=uuid.uuid4().bytes, UserUUID=user_main.UserUUID, RoleUUID=normal_role.RoleUUID, CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes, ) # 检查扩展表 dashou_status = 0 shangjia_status = 0 guanshi_status = 0 boss_nickname = '微信用户' try: boss_profile = UserBoss.query.get(user=user_main) boss_nickname = boss_profile.nickname if boss_profile.nickname else '微信用户' except UserBoss.DoesNotExist: pass try: UserDashou.query.get(user=user_main) dashou_status = 1 except UserDashou.DoesNotExist: dashou_status = None try: UserShangjia.query.get(user=user_main) shangjia_status = 1 except UserShangjia.DoesNotExist: shangjia_status = None try: UserGuanshi.query.get(user=user_main) guanshi_status = 1 except UserGuanshi.DoesNotExist: guanshi_status = None try: UserZuzhang.query.get(user=user_main) zuzhang_status = 1 except UserZuzhang.DoesNotExist: zuzhang_status = None # 在检查 zuzhang 扩展表之后,插入以下代码 try: kg = UserShenheguan.query.get(user=user_main) kaoheguan_status = 1 if kg.zhuangtai == 1 else 0 except UserShenheguan.DoesNotExist: kaoheguan_status = 0 # 生成JWT token refresh = RefreshToken.for_user(user_main) token = str(refresh.access_token) # 计算订单数量 order_counts = self.jisuanOrderShuliang(user_main.UserUID) # 获取群配置 group_info = self.huoquQunPeizhi() # 返回数据 response_data = { 'token': token, 'nicheng': boss_nickname, 'uid': user_main.UserUID, 'touxiang': user_main.Avatar or '', 'shangjiastatus': shangjia_status, 'dashoustatus': dashou_status, 'guanshistatus': guanshi_status, 'zuzhangstatus': zuzhang_status, 'kaoheguanstatus': kaoheguan_status, 'dingdantiaoshu': order_counts, 'dashouqun': group_info.get('dashouqun', ''), 'dashouqunid': group_info.get('dashouqunid', ''), 'guanshiqun': group_info.get('guanshiqun', ''), 'guanshiqunid': group_info.get('guanshiqunid', '') } return Response({'code': 0, 'msg': '登录成功', 'data': response_data}) except Exception as e: error_openid = locals().get('openid', 'N/A') logger.error(f"微信登录异常 - openid: {error_openid}, 错误类型: {type(e).__name__}, 错误详情: {str(e)}", exc_info=True) return Response({'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def get_wechat_openid(self, code): """ 调用微信接口获取openid和unionid """ try: appid = getattr(settings, 'WEIXIN_APPID', '') secret = getattr(settings, 'WEIXIN_SECRET', '') if not appid or not secret: raise ValueError('微信配置未设置') url = 'https://api.weixin.qq.com/sns/jscode2session' params = {'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code'} response = requests.get(url, params=params, timeout=10) result = response.json() if 'openid' in result: return {'openid': result['openid'], 'session_key': result.get('session_key', ''), 'unionid': result.get('unionid', '')} else: errcode = result.get('errcode', 'unknown') errmsg = result.get('errmsg', '未知错误') return {'errmsg': f'[{errcode}]{errmsg}'} except requests.exceptions.Timeout: return {'errmsg': '请求微信接口超时'} except Exception as e: return {'errmsg': f'请求微信接口异常: {str(e)}'} def shengchengYonghuID(self): """ 生成7位数字用户ID """ for _ in range(10): timestamp_part = str(int(time.time()))[-5:].zfill(5) random_part = str(random.randint(0, 99)).zfill(2) user_id = timestamp_part + random_part if len(user_id) == 7 and user_id.isdigit(): if not User.query.filter(UserUID=user_id).exists(): return user_id raise Exception('生成用户ID失败') def huoquKehuduanIP(self, request): """ 获取客户端真实IP地址 """ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: ip = x_forwarded_for.split(',')[0].strip() if ip: return ip ip = request.META.get('REMOTE_ADDR', '') return ip if ip else '0.0.0.0' def jisuanOrderShuliang(self, yonghuid): """ 统计订单数量 """ try: if not PlatformOrderExt.query.filter(BossID=yonghuid).exists(): return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} dingdan_ids = PlatformOrderExt.query.filter(BossID=yonghuid).values_list('Order__id', flat=True) results = Order.query.filter(id__in=dingdan_ids).aggregate( daifuwu=Count(Case(When(Q(Status=1) | Q(Status=7), then=1), output_field=IntegerField())), fuwuzhong=Count(Case(When(Status=2, then=1), output_field=IntegerField())), yiwancheng=Count(Case(When(Status=3, then=1), output_field=IntegerField())), yituikuan=Count(Case(When(Status=5, then=1), output_field=IntegerField())) ) return { 'daifuwu': results['daifuwu'] or 0, 'fuwuzhong': results['fuwuzhong'] or 0, 'yiwancheng': results['yiwancheng'] or 0, 'yituikuan': results['yituikuan'] or 0 } except Exception: return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} def huoquQunPeizhi(self): """ 获取群配置信息 """ try: qun_configs = Qunpeizhi.query.filter(id__in=[1, 2]) result = { 'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': '' } for config in qun_configs: if config.id == 1: result['dashouqun'] = config.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 - 完整文件 class WeixinOfficialCallbackView(APIView): """ 微信服务号回调接口 处理用户关注、取消关注等事件 核心:有关注事件就存,不管有没有UnionID """ throttle_classes = [] permission_classes = [AllowAny] authentication_classes = [] def get(self, request): """验证服务器配置(微信官方要求)""" try: signature = request.GET.get('signature', '') timestamp = request.GET.get('timestamp', '') nonce = request.GET.get('nonce', '') echostr = request.GET.get('echostr', '') token = getattr(settings, 'WEIXIN_OFFICIAL_TOKEN', '') if not token: logger.error("服务号Token未配置") return HttpResponse('Token not configured', status=500) # 验证签名 tmp_list = sorted([token, timestamp, nonce]) tmp_str = ''.join(tmp_list) hash_str = hashlib.sha1(tmp_str.encode()).hexdigest() if hash_str == signature: return HttpResponse(echostr) else: logger.error(f"签名验证失败") return HttpResponse('Signature verification failed', status=403) except Exception as e: logger.error(f"验证服务器异常: {str(e)}") return HttpResponse('Internal Server Error', status=500) def post(self, request): """处理事件推送(关注、取关)""" try: xml_str = request.body.decode('utf-8') logger.info(f"收到服务号事件,原始数据(前200字符): {xml_str[:200]}") root = ET.fromstring(xml_str) msg_type = root.find('MsgType').text from_user = root.find('FromUserName').text # 服务号OpenID if msg_type == 'event': event = root.find('Event').text if event == 'subscribe': return self.handle_subscribe_event(root, from_user) elif event == 'unsubscribe': return self.handle_unsubscribe_event(root, from_user) else: # 其他事件(如菜单点击)直接返回success return HttpResponse('success') # 非事件消息也直接返回success return HttpResponse('success') except Exception as e: logger.error(f"处理服务号事件异常: {str(e)}", exc_info=True) # 即使异常也返回success,避免微信服务器重试 return HttpResponse('success') def handle_subscribe_event(self, root, from_user): """处理用户关注事件 - 简化版:有就存,没有拉倒""" try: # 尝试获取UnionID(有就有,没有也没关系) unionid_elem = root.find('UnionId') unionid = unionid_elem.text if unionid_elem is not None else None logger.info(f"用户关注,OpenID: {from_user}, UnionID: {unionid}") # 核心逻辑:更新或创建记录 obj, created = OfficialAccountUser.query.update_or_create( official_openid=from_user, defaults={ 'unionid': unionid, 'is_subscribed': True, 'subscribe_time': timezone.now() } ) action = "新建" if created else "更新" logger.info(f"{action}用户记录成功: OpenID={from_user}") # 回复欢迎消息(可选) return self.generate_text_response(root, "感谢关注星阙网络服务!平台有新订单时您会收到通知。") except Exception as e: logger.error(f"处理关注事件异常: {str(e)}", exc_info=True) # 依然返回success,不让微信重试 return HttpResponse('success') def handle_unsubscribe_event(self, root, from_user): """处理用户取消关注事件 - 简化版:标记为未订阅""" try: updated_count = OfficialAccountUser.query.filter( official_openid=from_user ).update(is_subscribed=False) if updated_count > 0: logger.info(f"用户取消关注,已标记为未订阅: {from_user}") else: logger.info(f"用户取消关注,但记录不存在(无需处理): {from_user}") return HttpResponse('success') except Exception as e: logger.error(f"处理取消关注事件异常: {str(e)}", exc_info=True) return HttpResponse('success') def generate_text_response(self, root, content): """生成文本消息响应""" try: to_user = root.find('ToUserName').text from_user = root.find('FromUserName').text response_xml = f""" {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 class UserInfoUpdateView(APIView): permission_classes = [permissions.IsAuthenticated] # 进程内缓存,防止同一code被重复调用微信(单进程安全,多进程小概率重复,但用户极少可忽略) _phone_cache = {} def post(self, request): try: user = request.user boss = self._get_or_create_boss(user) result = {} # 头像 av = request.FILES.get('avatar') if av: ok, msg = validate_image(av) if not ok: return Response({'code': 1, 'msg': msg, 'data': None}, status=400) url = self._upload_avatar(user, av) if url: result['touxiang'] = url user.avatar = url else: return Response({'code': 2, 'msg': '头像上传失败', 'data': None}, status=500) # 昵称 nick = request.data.get('nicheng') if nick: nick = str(nick).strip() if len(nick) > 20: return Response({'code': 3, 'msg': '昵称过长', 'data': None}, status=400) boss.nickname = nick result['nicheng'] = nick # 手机号 —— 极简可靠版 code = request.data.get('shoujihao_code') if code: phone = self._get_phone_safe(code) if phone is None: return Response({'code': 6, 'msg': '手机号获取失败,请重新授权', 'data': None}, status=400) user.phone = phone user.shoujihao_renzheng = True result['phone'] = phone if not result: return Response({'code': 5, 'msg': '无更新', 'data': None}, status=400) with transaction.atomic(): user.save() boss.save() return Response({'code': 0, 'msg': '更新成功', 'data': result}) except Exception as e: logger.exception("系统异常") return Response({'code': 99, 'msg': str(e), 'data': None}, status=500) # ---------- 安全手机号(内存去重) ---------- def _get_phone_safe(self, code): # 如果已经处理过这个code,直接返回结果 if code in self._phone_cache: cached = self._phone_cache[code] return cached if cached != '__FAIL__' else None # 标记处理中(简单防重入) self._phone_cache[code] = '__PROCESSING__' try: phone = self._call_wechat(code) self._phone_cache[code] = phone return phone except Exception as e: logger.error(f"微信调用失败: {str(e)}") self._phone_cache[code] = '__FAIL__' return None # ---------- 微信接口调用(每次全新token) ---------- def _call_wechat(self, code): token = self._get_new_token() url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}' resp = requests.post(url, json={'code': code}, timeout=10) data = resp.json() errcode = data.get('errcode') if errcode == 0: return data['phone_info']['purePhoneNumber'] # 即使刚拿的token也可能意外失效,重试一次 if errcode == 40001: token = self._get_new_token() url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}' resp = requests.post(url, json={'code': code}, timeout=10) data = resp.json() if data.get('errcode') == 0: return data['phone_info']['purePhoneNumber'] raise Exception(f"微信错误(errcode={errcode}): {data.get('errmsg')}") # ---------- 获取全新 access_token(不缓存,每次调微信) ---------- def _get_new_token(self): appid = getattr(settings, 'WEIXIN_APPID', '') secret = getattr(settings, 'WEIXIN_SECRET', '') if not appid or not secret: raise Exception('缺少微信配置') resp = requests.get('https://api.weixin.qq.com/cgi-bin/token', params={ 'grant_type': 'client_credential', 'appid': appid, 'secret': secret }) data = resp.json() token = data.get('access_token') if token: return token raise Exception(f"获取access_token失败: {data}") # ---------- 工具 ---------- def _get_or_create_boss(self, user): try: return UserBoss.query.get(user=user) except UserBoss.DoesNotExist: return UserBoss.query.create(user=user, nickname='微信用户') def _upload_avatar(self, user, file): try: uid = user.yonghuid if not uid: return None old = user.avatar if old: delete_from_oss(old) ext = os.path.splitext(file.name)[1] or '.jpg' name = f"{uuid.uuid4().hex}{ext}" path = f"avatar/{uid}/{name}" return path if upload_to_oss(file, path) else None except: return None # yonghu/views.py 或 yonghu/api_views.py class DashouXinxiAPIView(APIView): """ 获取打手信息接口 请求方式:POST 认证方式:JWT Token 返回:打手基本信息、资产信息、会员信息,金牌打手额外返回专属字段 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取当前登录用户 current_user = request.user yonghuid = current_user.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', 'jiedanZongliang':dashou_profile.jiedanzongliang if dashou_profile.jiedanzongliang is not None else '0', 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', 'chengjiaoliang': dashou_profile.chengjiaozongliang, 'jifen': dashou_profile.jifen, 'chenghao': dashou_profile.chenghao or '', } # 4. 查询会员信息(原有逻辑) huiyuan_list = [] try: huiyuan_records = Huiyuangoumai.query.filter( yonghu_id=yonghuid ).select_related() for record in huiyuan_records: # 检查会员是否过期 is_expired = record.jiance_shifou_daoqi() # 只返回未过期的会员(huiyuan_zhuangtai = 1) if record.huiyuan_zhuangtai == 1: huiyuan_list.append({ 'huiyuanid': record.huiyuan_id, 'huiyuanming': record.jieshao or f"会员{record.huiyuan_id}", 'daoqi': record.daoqi_time }) except Exception as e: # 会员信息查询失败不影响主要功能,返回空列表 huiyuan_list = [] data['clumber'] = huiyuan_list # 5. 如果是金牌打手,添加专属字段 if dashou_profile.chenghao == "金牌打手": data.update({ 'jiedanZongliang': dashou_profile.jiedanzongliang, # 接单总量 'jinriJiedan': str(dashou_profile.jinrishouyi) if dashou_profile.jinrishouyi is not None else '0.00', # 今日接单金额 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', # 最近提现金额(今日已提现) }) # 6. 返回成功响应 return Response({ 'code': 200, 'message': '获取成功', 'data': data }, status=status.HTTP_200_OK) except Exception as e: # 生产环境应使用 logging 记录错误,而不是 print # logger.error(f"获取打手信息失败: {e}", exc_info=True) return Response({ 'code': 500, 'message': f'服务器内部错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class HuoQuYaoQingRenView(APIView): """ 获取当前打手的邀请人信息 POST /yonghu/hqwdyqr Headers: Authorization: Bearer 返回邀请人的用户ID、昵称(来自老板扩展表)、头像相对URL """ permission_classes = [IsAuthenticated] def post(self, request): try: current_user = request.user # 当前登录用户 yonghuid = current_user.yonghuid # 7位用户ID # 1. 校验打手身份及账号状态(只检查账号状态,不检查接单状态) try: dashou = UserDashou.query.get(user=current_user) if dashou.zhanghaozhuangtai != 1: return Response({ 'code': 403, 'message': '您的账号已被封禁,无法查看邀请人', 'data': None }) except UserDashou.DoesNotExist: return Response({ 'code': 404, 'message': '您还不是打手,无法查看邀请人', 'data': None }) # 2. 获取邀请人ID yaoqingren_id = dashou.yaoqingren if not yaoqingren_id: return Response({ 'code': 404, 'message': '您没有邀请人', 'data': None }) # 3. 查询邀请人主表(头像) try: inviter_main = User.query.get(UserUID=yaoqingren_id) touxiang = inviter_main.avatar or '' except User.DoesNotExist: return Response({ 'code': 404, 'message': '邀请人信息不存在', 'data': None }) # 4. 查询老板扩展表(昵称),如果没有则使用空字符串 nicheng = '' try: boss = UserBoss.query.get(user=inviter_main) nicheng = boss.nickname or '' except UserBoss.DoesNotExist: pass # 邀请人可能不是老板,昵称留空 response_data = { 'uid': yaoqingren_id, 'nicheng': nicheng, 'touxiang': touxiang # 相对路径,前端自行拼接 } return Response({ 'code': 200, 'message': '获取成功', 'data': response_data }) except Exception as e: logger.error(f'获取邀请人失败: {str(e)}', exc_info=True) return Response({ 'code': 500, 'message': '系统繁忙,请稍后重试', 'data': None }) # yonghu/views.py 或 yonghu/api_views.py class ZaixianZhuangtaiAPIView(APIView): """ 更改打手在线状态接口 请求方式:POST 认证方式:JWT Token 请求参数:status(0=离线,1=在线) 返回:更新后的状态信息 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取请求参数 new_status = request.data.get('status') if new_status is None: return Response({ 'code': 400, 'message': '缺少状态参数', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 转换为整数,确保是0或1 try: new_status = int(new_status) if new_status not in [0, 1]: return Response({ 'code': 400, 'message': '状态参数必须是0或1', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except ValueError: return Response({ 'code': 400, 'message': '状态参数必须是整数', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 2. 获取当前登录用户 current_user = request.user yonghuid = current_user.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) class GuanshiXinxiView(APIView): """ 获取管事信息接口 路径:/yonghu/guanshixinxi 方法:POST 权限:JWT认证 功能:获取当前管事的详细信息,包括基本信息、分红信息以及已充值打手数量 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 获取当前用户(通过JWT认证) current_user = request.user # 查询管事扩展表 try: guanshi_profile = UserGuanshi.query.get(user=current_user) # 🔥【新增】查询已充值打手数量(从分红表中统计) # Gsfenhong 表中 guanshi 字段存储管事ID(yonghuid) yichongzhi_count = Gsfenhong.query.filter(guanshi=current_user.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: # 捕获所有异常,返回错误信息并记录日志 logger.error(f"获取管事信息异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': f'获取信息失败: {str(e)}', 'data': None }, status=500) # yonghu/views.py class ShangJiaXinXiView(APIView): """ 获取商家信息接口 (POST /yonghu/shangjiaxinxi) 权限:需JWT认证 返回:商家基本信息 + 称号列表 + 今日/本月统计 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 获取商家扩展资料 shangjia = request.user.shop_profile except ObjectDoesNotExist: return Response({ 'code': 4001, 'msg': '您还未注册成为商家或商家资料不存在', 'data': None }, status=status.HTTP_200_OK) user = request.user yonghuid = user.yonghuid today = timezone.now().date() this_month_start = today.replace(day=1) # ------------------- 1. 从 UserShangjia 获取基础字段 ------------------- response_data = { 'sjzhzhuangtai': shangjia.zhuangtai, # 商家账号状态 'fadanzong': shangjia.fabu, # 发单总量(累计) 'tuikuanzong': shangjia.tuikuan, # 退款总量(累计) 'sjyue': float(shangjia.yue), # 商家余额 # 'riliushui' 和 'yueliushui' 将从统计表计算,不再使用原字段 } # ------------------- 2. 获取称号列表(仅限商家称号) ------------------- chenghao_list = [] # 查询当前用户的所有称号关联 yonghu_chenghao_qs = YonghuChenghao.query.filter(yonghu=user).select_related('chenghao') for yc in yonghu_chenghao_qs: chenghao_obj = yc.chenghao # 只保留类型为“商家”的称号 if chenghao_obj.leixing == 'shangjia': chenghao_list.append({ 'mingcheng': chenghao_obj.mingcheng, 'texiao_json': chenghao_obj.texiao_miaoshu, # 前端需要的特效描述 # 如果还需要其他字段(如图标URL)可在此添加,打手端有 texiao_json }) response_data['chenghao_list'] = chenghao_list # ------------------- 3. 从 MerchantDailyStats 获取今日统计 ------------------- try: today_stat = MerchantDailyStats.query.get(MerchantID=yonghuid, Date=today) jinri_paifa_dingdan = today_stat.AssignedOrderCount jinri_paifa_jine = float(today_stat.AssignedAmount) jinri_tuikuan_dingdan = today_stat.RefundOrderCount except MerchantDailyStats.DoesNotExist: jinri_paifa_dingdan = 0 jinri_paifa_jine = 0.00 jinri_tuikuan_dingdan = 0 response_data['jinripaidan'] = jinri_paifa_dingdan # 今日派单数(新增) response_data['jinrituikuan'] = jinri_tuikuan_dingdan # 今日退款数(新增) response_data['riliushui'] = jinri_paifa_jine # 今日流水(派发总金额) # ------------------- 4. 从 MerchantDailyStats 获取本月统计 ------------------- month_stats = MerchantDailyStats.query.filter( MerchantID=yonghuid, Date__gte=this_month_start, Date__lte=today ).aggregate( total_paifa_jine=Sum('AssignedAmount'), total_paifa_dingdan=Sum('AssignedOrderCount') ) yue_paifa_jine = float(month_stats.get('total_paifa_jine') or 0.00) # yue_paifa_dingdan = month_stats.get('total_paifa_dingdan') or 0 # 如果前端不需要可不加 response_data['yueliushui'] = yue_paifa_jine # 今月流水(派发总金额) # (可选)如果前端还需要今月派单总数,取消下面注释 # response_data['jinyuepaidan'] = yue_paifa_dingdan return Response({ 'code': 200, 'msg': '获取商家信息成功', 'data': response_data }, status=status.HTTP_200_OK) class BossBiaoshiView(APIView): """ 获取用户的GoEasy标识 - 极简实现 POST /yonghu/bossbiaoshi 需要JWT Token认证 """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 直接从request.user获取用户 user = request.user # 2. 获取yonghuid(确保用户已登录) if not user or not hasattr(user, '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接口 """ 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 = CreateInvitationCode(yonghuid_str) # 5. 验证生成的邀请码格式 if not VerifyInvitationCode(new_yaoqingma): return Response({ 'code': 500, 'message': '邀请码生成失败,格式验证错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 6. 保存到数据库 (使用事务确保数据一致性) try: with transaction.atomic(): guanshi_profile.yaoqingma = new_yaoqingma guanshi_profile.save() except Exception as e: # 这里可以记录日志 return Response({ 'code': 500, 'message': f'保存邀请码到数据库失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 7. 返回新生成的邀请码 return Response({ 'code': 200, 'message': '成功生成新的邀请码', 'data': { 'yaoqingma': new_yaoqingma, 'is_new': True # 标记是新生成的 } }) """ 阿龙电竞陪玩平台 - 打手注册及相关API接口 """ class DashouZhuceView(APIView): """ 打手注册接口 (通过管事邀请码) 访问路径: /api/yonghu/dashouzhuce/ 请求方法: POST 权限要求: JWT Token认证通过的用户 请求体: { "inviteCode": "AbC123..." } """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 获取并验证邀请码参数 yaoqingma = request.data.get('inviteCode') if not yaoqingma: return Response({ 'code': 400, 'message': '邀请码不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) yaoqingma = yaoqingma.strip() if len(yaoqingma) > 100: return Response({ 'code': 400, 'message': '邀请码长度超过限制', 'data': None }, status=status.HTTP_400_BAD_REQUEST) current_user = request.user # 注意:这里request.user是User实例 # 2. 检查用户是否已是打手 (使用反向查询,性能最优) try: dashou_profile = current_user.dashou_profile # 用户已是打手,直接返回现有信息 return self.fanhuiXianyouDashouXinxi(dashou_profile, current_user) except UserDashou.DoesNotExist: # 用户不是打手,继续注册流程 pass # 3. 根据邀请码查找对应的管事 try: # 使用select_related一次性获取管事及其关联的主表用户信息,性能最高 guanshi_profile = UserGuanshi.query.select_related('user').get(yaoqingma=yaoqingma) except UserGuanshi.DoesNotExist: return Response({ 'code': 404, 'message': '邀请码无效或不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 4. 验证管事状态 # 5. 核心:在数据库事务中创建用户的所有身份 try: with transaction.atomic(): # 获取管事的用户ID (yonghuid) guanshi_yonghuid = guanshi_profile.user.yonghuid # 5.1 创建打手扩展表 dashou_profile = UserDashou.query.create( user=current_user, nicheng='大手子', # 默认昵称 chenghao='普通大手', # 默认称号 yaoqingren=guanshi_yonghuid, # 设置邀请人ID # 其他字段全部使用模型定义的默认值 ) # 5.2 创建商家扩展表 try: boss_profile = current_user.boss_profile if boss_profile.nickname: boss_nickname = boss_profile.nickname except: pass # 5.3 创建管事扩展表 # 5.4 更新主表的用户类型 (如果需要,可以设为'dashou',或保持原逻辑) # current_user.user_type = 'dashou' # 根据你的业务逻辑决定 # current_user.save() # 5.5 更新原管事的邀请数量 (使用行锁防止并发问题) # 重新查询并锁定该行记录 locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk) locked_guanshi.yaogingshuliang += 1 locked_guanshi.save() # ========== 新增:管事每日统计(邀请打手) ========== try: update_guanshi_daily_by_action( yonghuid=guanshi_yonghuid, action=1, # 1 = 邀请打手 amount=Decimal('0.00') ) logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}") except Exception as e: logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}") except Exception as e: # 事务会自动回滚 # 这里可以记录更详细的日志 return Response({ 'code': 500, 'message': f'注册过程中发生系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 6. 注册成功,返回信息 (新注册用户没有会员记录,clumber返回空列表) fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=True) return Response({ 'code': 200, 'message': '注册成功!已开通打手、管事身份。', 'data': fanhui_data }) def fanhuiXianyouDashouXinxi(self, dashou_profile, current_user): """处理已是打手的用户,返回其现有信息""" fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=False) return Response({ 'code': 200, 'message': '您已是打手,返回当前信息。', 'data': fanhui_data }) def zhuangbeiFanhuiShuju(self, dashou_profile, current_user, is_new=False): """准备返回给前端的打手数据,字段名严格匹配前端""" # 构建基础信息 data = { 'dashounicheng': dashou_profile.nicheng, 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, 'yongjin': str(dashou_profile.yue), # Decimal转字符串 'zonge': str(dashou_profile.zonge), 'yajin': str(dashou_profile.yajin), 'chenghao': dashou_profile.chenghao, 'jinfen': dashou_profile.jifen, 'chengjiaoliang': dashou_profile.chengjiaozongliang, 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, 'dashouzhuangtai': dashou_profile.zhuangtai, } # 查询并处理会员列表 (clumber) # 使用当前用户的yonghuid进行查询 huiyuan_goumai_list = [] if not is_new: # 如果是新用户,直接返回空列表,无需查询 goumai_records = Huiyuangoumai.query.filter(yonghu_id=current_user.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 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.query.select_related('user').filter( yaoqingren=guanshi_yonghuid ) # 4. 全局统计(不受筛选影响,直接基于 base_qs) total_all = base_qs.count() zaixian_all = base_qs.filter(zaixianzhuangtai=1).count() zhengchang_all = base_qs.filter(zhanghaozhuangtai=1).count() fengjin_all = total_all - zhengchang_all # 5. 应用筛选条件 if keyword: base_qs = base_qs.filter( Q(nicheng__icontains=keyword) | Q(user__UserUID__icontains=keyword) ) if zhanghaozhuangtai is not None: try: val = int(zhanghaozhuangtai) if val == 1: base_qs = base_qs.filter(zhanghaozhuangtai=1) else: base_qs = base_qs.exclude(zhanghaozhuangtai=1) except (ValueError, TypeError): pass if zaixianzhuangtai is not None: try: val = int(zaixianzhuangtai) if val == 1: base_qs = base_qs.filter(zaixianzhuangtai=1) else: base_qs = base_qs.exclude(zaixianzhuangtai=1) except (ValueError, TypeError): pass # 6. 排序和分页 base_qs = base_qs.order_by('-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) class TixianXinxiHuoquView(APIView): """ 获取用户提现收款信息接口 返回用户已设置的手机号、收款码、收款账号 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): """ 处理获取收款信息的POST请求 直接返回用户当前的收款设置 """ try: # 从request.user获取当前用户 user_main = request.user # 🔥【修改开始 - 使用Decimal,字段名是Rate】 # 查询打手提现费率 (Platform='5') dashou_rate_obj = CommissionRate.query.filter(Platform='5').first() dashou_rate = decimal.Decimal(str(dashou_rate_obj.Rate)) if dashou_rate_obj else decimal.Decimal('0.00') # 查询管事提现费率 (Platform='6') guanshi_rate_obj = CommissionRate.query.filter(Platform='6').first() guanshi_rate = decimal.Decimal(str(guanshi_rate_obj.Rate)) if guanshi_rate_obj else decimal.Decimal('0.00') # 🔥【修改结束】 # 构建返回数据 response_data = { 'txdianhua': user_main.phone if user_main.phone else '', 'txtupian': user_main.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: 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 # ========== 订单状态常量(请根据您的实际业务修改)========== # 假设状态值:已完成=9,已退款=10(示例,实际以您的项目为准) ORDER_STATUS_COMPLETED = 3 # 订单已完成 ORDER_STATUS_REFUNDED = 5 # 订单已退款 ORDER_STATUS_FAIL = 6 # 进行中状态(不可提现)例如:待接单、已接单、进行中等,您需要定义排除列表 FORBIDDEN_STATUS_FOR_WITHDRAW = [1, 2, 3, 4, 5, 6, 7, 8] # 除已完成和已退款外的所有状态 class TixianShenqingView(APIView): """ 用户提现申请接口(完整版) 支持类型: 1-打手佣金提现 2-管事分红提现 3-组长分红提现 4-审核官分佣提现 5-打手押金提现(退出平台) 6-商家余额提现(退出平台) """ permission_classes = [IsAuthenticated] def post(self, request): try: user_main = request.user yonghuid = user_main.yonghuid # 1. 获取并验证参数 leixing = request.data.get('leixing') jine = request.data.get('jine') fangshi = request.data.get('fangshi') txskfs = request.data.get('txskfs', 0) # 罚单检查(共用) fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2) if not fadan_ok: return Response({'code': 400, 'msg': fadan_msg}) # 参数完整性 missing = [] if leixing is None: missing.append('leixing') if jine is None: missing.append('jine') if fangshi is None: missing.append('fangshi') if missing: return Response({'code': 1, 'msg': f'缺少参数: {", ".join(missing)}'}, status=status.HTTP_400_BAD_REQUEST) try: leixing = int(leixing) jine = decimal.Decimal(str(jine)) fangshi = int(fangshi) txskfs = int(txskfs) except (ValueError, TypeError, decimal.InvalidOperation) as e: return Response({'code': 2, 'msg': f'参数格式错误: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST) if jine <= decimal.Decimal('0'): return Response({'code': 3, 'msg': '提现金额必须大于0'}, status=status.HTTP_400_BAD_REQUEST) # 支持的提现类型(1-6) if leixing not in [1, 2, 3, 4, 5, 6]: return Response({'code': 4, 'msg': '无效的提现类型'}, status=status.HTTP_400_BAD_REQUEST) if fangshi not in [1, 2]: return Response({'code': 5, 'msg': '无效的收款方式,只能是1(微信)或2(支付宝)'}, status=status.HTTP_400_BAD_REQUEST) # 2. 获取对应费率及手续费 current_rate = decimal.Decimal('0.00') rate_key = { 1: '5', # 打手佣金 2: '6', # 管事分红 3: '8', # 组长分红 4: '9', # 审核官分佣 5: '11', # 打手押金提现 6: '10', # 商家余额提现 }.get(leixing) if rate_key: rate_obj = CommissionRate.query.filter(Platform=rate_key).first() if rate_obj: current_rate = decimal.Decimal(str(rate_obj.Rate)) else: return Response({'code': 41, 'msg': f'提现费率未配置(类型{leixing})'}, status=status.HTTP_400_BAD_REQUEST) shouxufei = jine * current_rate shijidaozhang = jine - shouxufei if shijidaozhang <= decimal.Decimal('0'): return Response({'code': 6, 'msg': '提现金额过低,扣除手续费后无实际到账'}, status=status.HTTP_400_BAD_REQUEST) # 3. 根据提现类型执行校验和扣款 nicheng = '' if leixing == 1: result = self._check_dashou_commission_withdraw(user_main, jine) if result['code'] != 0: return Response(result, status=status.HTTP_400_BAD_REQUEST) nicheng = result['data']['nicheng'] # 扣款(佣金) with transaction.atomic(): profile = UserDashou.objects.select_for_update().get(user=user_main) if profile.yue < jine: return Response({'code': 13, 'msg': '余额不足'}) profile.yue = F('yue') - jine profile.save() elif leixing == 2: result = self._check_guanshi_dividend_withdraw(user_main, jine) if result['code'] != 0: return Response(result, status=status.HTTP_400_BAD_REQUEST) with transaction.atomic(): profile = UserGuanshi.objects.select_for_update().get(user=user_main) if profile.yue < jine: return Response({'code': 22, 'msg': '余额不足'}) profile.yue = F('yue') - jine profile.save() nicheng = "管事用户" elif leixing == 3: result = self._check_zuzhang_withdraw(user_main, jine) if result['code'] != 0: return Response(result, status=status.HTTP_400_BAD_REQUEST) with transaction.atomic(): profile = UserZuzhang.objects.select_for_update().get(user=user_main) if profile.ketixian_jine < jine: return Response({'code': 32, 'msg': '可提现金额不足'}) profile.ketixian_jine = F('ketixian_jine') - jine profile.save() nicheng = user_main.nicheng if hasattr(user_main, 'nicheng') else '' elif leixing == 4: result = self._check_shenheguan_withdraw(user_main, jine) if result['code'] != 0: return Response(result, status=status.HTTP_400_BAD_REQUEST) with transaction.atomic(): profile = UserShenheguan.objects.select_for_update().get(user=user_main) if profile.yue < jine: return Response({'code': 42, 'msg': '可提现金额不足'}) profile.yue = F('yue') - jine profile.save() nicheng = "审核官" elif leixing == 5: # 打手押金提现(退出) result = self._check_dashou_yajin_withdraw(user_main, jine) if result['code'] != 0: return Response(result, status=status.HTTP_400_BAD_REQUEST) with transaction.atomic(): profile = UserDashou.objects.select_for_update().get(user=user_main) if profile.yajin < jine: return Response({'code': 53, 'msg': '押金余额不足'}) profile.yajin = F('yajin') - jine # 押金提现后封禁打手账号 profile.zhanghaozhuangtai = 0 # 0表示禁用 profile.save() nicheng = profile.nicheng or '' elif leixing == 6: # 商家余额提现(退出) result = self._check_shangjia_withdraw(user_main, jine) if result['code'] != 0: return Response(result, status=status.HTTP_400_BAD_REQUEST) with transaction.atomic(): profile = UserShangjia.objects.select_for_update().get(user=user_main) if profile.yue < jine: return Response({'code': 63, 'msg': '商家余额不足'}) profile.yue = F('yue') - jine # 注意:是否封禁商家账号?根据需求,用户未明确要求封禁,暂时只扣款,可后续扩展。 profile.save() nicheng = profile.nicheng or '' # 4. 创建提现记录(先扣款后创建,确保资金安全) try: avatar = user_main.avatar or '' phone = user_main.phone or '' zhifu = user_main.zhifu or '' skzhanghao = user_main.skzhanghao or '' tixian_record = Tixianjilu.query.create( yonghuid=yonghuid, avatar=avatar, phone=phone, nicheng=nicheng, leixing=leixing, zhifu=zhifu, skzhanghao=skzhanghao, jine=shijidaozhang, # 记录实际到账金额 zhuangtai=1, # 审核中 fangshi=fangshi, shenheid=None, bhliyou='', ) except Exception as e: return Response({'code': 99, 'msg': f'创建提现记录失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 5. 返回成功响应 return Response({ 'code': 0, 'msg': '提现申请提交成功', 'data': { 'tixian_id': tixian_record.id, 'leixing': leixing, 'jine': str(jine.quantize(decimal.Decimal('0.01'))), 'fangshi': fangshi, 'zhuangtai': 1, '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.query.get(user=user_main) except ObjectDoesNotExist: return {'code': 10, 'msg': '您不是打手身份,无法提现佣金'} if dashou.zhanghaozhuangtai != 1: return {'code': 11, 'msg': '打手账号已被封禁,无法提现'} if dashou.zhuangtai != 1: return {'code': 12, 'msg': '您有订单进行中,请完成后提现'} if dashou.jifen != 10: return {'code': 15, 'msg': '积分不足10分,请补充积分后提现'} if dashou.yue < jine: return {'code': 13, 'msg': f'余额不足,当前余额: {dashou.yue}'} return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}} def _check_guanshi_dividend_withdraw(self, user_main, jine): """管事分红提现校验""" try: guanshi = UserGuanshi.query.get(user=user_main) except ObjectDoesNotExist: return {'code': 20, 'msg': '您不是管事身份,无法提现分红'} if guanshi.zhuangtai != 1: return {'code': 21, 'msg': '管事账号已被封禁,无法提现'} if guanshi.yue < jine: return {'code': 22, 'msg': f'余额不足,当前余额: {guanshi.yue}'} return {'code': 0, 'msg': 'ok', 'data': {}} def _check_zuzhang_withdraw(self, user_main, jine): """组长分红提现校验""" try: zuzhang = UserZuzhang.query.get(user=user_main) except ObjectDoesNotExist: return {'code': 30, 'msg': '您不是组长身份,无法提现组长分红'} if zuzhang.zhuangtai != 1: return {'code': 31, 'msg': '组长账号已被封禁,无法提现'} if zuzhang.ketixian_jine < jine: return {'code': 32, 'msg': f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}'} return {'code': 0, 'msg': 'ok', 'data': {}} def _check_shenheguan_withdraw(self, user_main, jine): """审核官分佣提现校验""" try: shenheguan = UserShenheguan.query.get(user=user_main) except ObjectDoesNotExist: return {'code': 40, 'msg': '您不是审核官身份,无法提现分佣'} if shenheguan.zhuangtai != 1: return {'code': 41, 'msg': '审核官账号已被封禁,无法提现'} if shenheguan.yue < jine: return {'code': 42, 'msg': f'可提现金额不足,当前可提: {shenheguan.yue}'} return {'code': 0, 'msg': 'ok', 'data': {}} def _check_shangjia_withdraw(self, user_main, jine): """ 商家余额提现校验(退出平台) 包含:订单、自身罚单、申请的罚单检查 """ try: shangjia = UserShangjia.query.get(user=user_main) except ObjectDoesNotExist: return {'code': 60, 'msg': '您不是商家身份,无法提现商家余额'} if shangjia.zhuangtai != 1: return {'code': 61, 'msg': '商家账号已被禁用,无法提现'} if shangjia.yue < jine: return {'code': 62, 'msg': f'商家余额不足,当前余额: {shangjia.yue}'} # 订单检查(使用 Q 取反) unfinished_orders = MerchantOrderExt.query.filter( MerchantID=user_main.yonghuid, ).exclude( Order__Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED,ORDER_STATUS_FAIL] ).exists() if unfinished_orders: return {'code': 63, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提现余额'} # 自身被处罚的罚单检查(状态不是已缴纳(2)和已驳回(4)) self_fadan = Penalty.query.filter( PenalizedUserID=user_main.yonghuid, ).exclude( Status__in=[2, 4] ).exists() if self_fadan: return {'code': 64, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'} # 申请的对他人处罚罚单检查(状态不是已缴纳(2)和已驳回(4)) applied_fadan = Penalty.query.filter( ApplicantID=user_main.yonghuid, ).exclude( Status__in=[2, 4] ).exists() if applied_fadan: return {'code': 65, 'msg': '您发起的对其他用户的处罚尚未处理完毕(待缴纳/申诉中),请等待完结'} return {'code': 0, 'msg': 'ok', 'data': {'nicheng': shangjia.nicheng or ''}} def _check_dashou_yajin_withdraw(self, user_main, jine): """ 打手押金提现校验(退出平台) 要求:无进行中的订单,无未结算订单,自身罚单需已处理 """ try: dashou = UserDashou.query.get(user=user_main) except ObjectDoesNotExist: return {'code': 50, 'msg': '您不是打手身份,无法提取押金'} if dashou.zhanghaozhuangtai != 1: return {'code': 51, 'msg': '打手账号已禁用,无法提取押金'} if dashou.zhuangtai != 1: return {'code': 52, 'msg': '您有订单进行中,请完成后提取押金'} if dashou.yajin < jine: return {'code': 53, 'msg': f'押金余额不足,当前押金: {dashou.yajin}'} # 订单检查(使用 exclude + __in) unfinished_orders = Order.query.filter( PlayerID=user_main.yonghuid, ).exclude( Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED] ).exists() if unfinished_orders: return {'code': 54, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提取押金'} # 自身罚单检查 self_fadan = Penalty.query.filter( PenalizedUserID=user_main.yonghuid, ).exclude( Status__in=[2, 4] ).exists() if self_fadan: return {'code': 55, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'} return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}} class TixianJiluHuoquViewV2(APIView): """ 提现记录获取接口 mode=1:手动提现记录(默认,兼容旧版) mode=2:自动提现审核记录(含 shenhe_jilu_id / shenhe_danhao,支持 zhuangtai 筛选) """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: user_main = request.user yonghuid = user_main.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.query.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.query.filter(id__in=audit_ids).values( 'id', 'fail_reason', 'shenqing_jine', 'shenhe_danhao' ): audit_map[a['id']] = a # 批量取自动打款失败原因 danhao_list = [r['shenhe_danhao'] for r in records_to_return if r.get('shenhe_danhao')] auto_fail_map = {} if danhao_list: for ar in TixianAutoRecord.query.filter( shenhe_danhao__in=danhao_list, zhuangtai=2 ).order_by('-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 class AdminLoginView(APIView): """ 管理员登录接口 路径:/yonghu/adlog/ 方法:POST 字段:zhanghao(账号), mima(密码), ip(二次验证码) """ # 限制匿名用户访问频率,防止暴力破解 throttle_classes = [AnonRateThrottle] # 允许任何人访问(登录接口不需要认证) permission_classes = [AllowAny] def post(self, request): """ 处理管理员登录请求 安全策略:任何错误都返回401,不透露具体信息 """ try: # 1. 获取客户端真实IP和用户代理信息 kehuduan_ip = huoquKehuduanIP(request) yonghu_daili = huoquYonghuDaili(request) # 2. 获取前端传递的数据 data = request.data # 检查必需字段是否存在 bixu_ziduan = ['zhanghao', 'mima', 'ip'] for ziduan in bixu_ziduan: if ziduan not in data: # 字段缺失,返回401 return Response( {'code': 401, 'msg': '登录失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 提取并清理数据 zhanghao = data.get('zhanghao', '').strip() mima = data.get('mima', '').strip() erci_yanzhengma = data.get('ip', '').strip() # 二次验证码(前端叫ip) # 简单验证字段非空 if not zhanghao or not mima or not erci_yanzhengma: return Response( {'code': 401, 'msg': '登录失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 3. 使用事务确保数据一致性 with transaction.atomic(): # 【优化点1】使用select_related一次性获取管理员扩展表 # 只查询管理员类型的用户(密码用 CheckPassword 验证,不能在 ORM 中直接匹配哈希) user = User.query.filter( Phone=zhanghao, # 手机号作为账号 admin_profile__isnull=False # 必须是管理员类型 ).select_related('admin_profile').first() # 如果用户不存在或不是管理员,返回401 if not user: return Response( {'code': 401, 'msg': '登录失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 验证密码(bcrypt 哈希验证) if not user.CheckPassword(mima): return Response( {'code': 401, 'msg': '登录失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 【优化点2】检查管理员扩展表是否存在 if not hasattr(user, '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个字符(保持兼容性) cunchu_ip = kehuduan_ip #print(cunchu_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,不暴露具体错误信息 # 记录日志用于排查问题,但前端只收到通用错误信息 # 这里可以添加日志记录,比如: logging.error(f'管理员登录异常: {str(e)}', exc_info=True) return Response( {'code': 401, 'msg': '登录失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # yonghu/views.py class AdminFinancialDataView(APIView): """ 管理员财务数据接口 路径:/yonghu/adhq/ 方法:POST 认证:JWT Token 权限:仅管理员 返回:收支记录数据 """ # 使用JWT认证 permission_classes = [IsAuthenticated] def post(self, request): """ 处理管理员财务数据请求 安全策略:任何错误都返回401,不透露具体信息 """ try: # 1. 获取前端传递的数据 data = request.data # 检查必需字段 if 'zhanghao' not in data: return Response( {'code': 401, 'msg': '请求参数错误', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) qianzhanghao = data.get('zhanghao', '').strip() # 简单验证字段非空 if not qianzhanghao: return Response( {'code': 401, 'msg': '请求参数错误', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 2. 获取当前登录用户(JWT认证后,request.user就是User实例) dangqianyonghu = request.user # 3. 验证用户类型是否为admin if not hasattr(dangqianyonghu, '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 = User.query.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 User.DoesNotExist: return Response( {'code': 401, 'msg': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 6. 查询收支记录表(Szjilu) # 使用高效的ORM查询,获取最新的一条记录 # 注意:根据你的业务需求,这里获取最新的一条记录 # 如果需要汇总多条记录,可以使用aggregate进行聚合查询 # 方法1:获取最新的一条记录(假设表中只有一条汇总记录) shouzhijilu = Szjilu.query.order_by('-create_time').first() # 方法2:如果表中可能有多条记录,需要汇总,可以使用以下方式: # from django.db.models Sum # zonghe = Szjilu.query.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,不暴露具体错误信息 # 这里可以记录日志,但不返回给前端 # logging.error(f'管理员财务数据接口异常: {str(e)}', exc_info=True) return Response( {'code': 401, 'msg': '获取失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # shangpin/views.py class AdGetOrderTypes(APIView): """ 后台获取订单类型列表接口 前端调用地址: /shangpin/getOrderTypes 前端需要字段: id, biaoti, tupian_url """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): try: # 1. 获取参数 qianzhanghao = request.data.get('zhanghao', '').strip() if not qianzhanghao: return Response( {'code': 400, 'message': '参数不完整', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2. 身份验证(按你说的方式) dangqianyonghu = request.user # 验证手机号 if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: return Response( {'code': 400, 'message': '参数不完整', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证管理员身份 if dangqianyonghu.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: # 请替换为你的实际模型名 # 只查询前端需要的三个字段 shangpin_types = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url') # 构建响应数据 type_list = [ { 'id': item.id, 'biaoti': item.jieshao or '', 'tupian_url': item.tupian_url or '' } for item in shangpin_types ] return Response({ 'code': 0, 'message': '获取成功', 'data': type_list }) except Exception as e: logger.error(f"查询商品类型表失败: {str(e)}") # 如果表不存在,返回空数组 return Response({ 'code': 0, 'message': '获取成功', 'data': [] }) except Exception as e: logger.error(f"获取订单类型异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # yonghu/views.py 或 dingdan/views.py class AdGetOrderList(APIView): """ 后台订单列表获取接口 前端调用地址: /yonghu/adddhq 前端需要字段: dingdan_id, jieshao, zhuangtai, leixing, fadanpingtai, jiage, tupian_url, fadanshijian 统计字段: stats.platform.total, stats.platform.completed, stats.merchant.total, stats.merchant.completed """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): try: # 1. 获取参数(严格按前端传的字段名) qianzhanghao = request.data.get('zhanghao', '').strip() page = int(request.data.get('page', 1)) page_size = int(request.data.get('pageSize', 30)) # 搜索相关参数 dingdan_id = request.data.get('dingdan_id', '').strip() # 筛选相关参数(严格按前端字段名) fadanpingtai = request.data.get('fadanpingtai') # 前端叫 fadanpingtai leixing = request.data.get('leixing') # 前端叫 leixing zhuangtai = request.data.get('zhuangtai', '') # 前端叫 zhuangtai if not qianzhanghao: return Response( {'code': 400, 'message': '参数不完整', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2. 身份验证(与你说的完全一样) dangqianyonghu = request.user # 验证手机号 if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: return Response( {'code': 400, 'message': '参数不完整', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证管理员身份 if dangqianyonghu.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 ) # 5. 构建查询条件(严格按前端字段名映射到数据库字段) query_conditions = Q() # 判断是否为搜索模式 is_search_mode = bool(dingdan_id) if is_search_mode: # 搜索模式:只按订单ID查询 query_conditions &= Q(OrderID=dingdan_id) else: # 筛选模式 # 发单平台筛选(前端: fadanpingtai -> 数据库: Platform) if fadanpingtai is not None: try: fadanpingtai_int = int(fadanpingtai) if fadanpingtai_int in [1, 2]: query_conditions &= Q(Platform=fadanpingtai_int) except (ValueError, TypeError): pass # 订单类型筛选(前端: leixing -> 数据库: ProductTypeID) if leixing is not None: try: leixing_int = int(leixing) query_conditions &= Q(ProductTypeID=leixing_int) except (ValueError, TypeError): pass # 订单状态筛选(前端: zhuangtai -> 数据库: Status) if zhuangtai and zhuangtai != 'all': try: # 处理多个状态,如 "1,7" status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()] if status_list: query_conditions &= Q(Status__in=status_list) except (ValueError, TypeError): pass # 6. 获取统计数据(平台订单和商家订单) # 注意:这里统计所有订单,不考虑筛选条件 try: # 平台订单统计 (Platform=1) platform_total = Order.query.filter(Platform=1).count() platform_completed = Order.query.filter(Platform=1, Status=3).count() # 商家订单统计 (Platform=2) merchant_total = Order.query.filter(Platform=2).count() merchant_completed = Order.query.filter(Platform=2, Status=3).count() platform_stats = { 'total': platform_total, 'completed': platform_completed } merchant_stats = { 'total': merchant_total, 'completed': merchant_completed } except Exception as e: logger.warning(f"统计数据查询失败: {str(e)}") platform_stats = {'total': 0, 'completed': 0} merchant_stats = {'total': 0, 'completed': 0} # 7. 执行分页查询(只查询主表,不查扩展表) start_time = time.time() # 计算分页 offset = (page - 1) * page_size # 查询符合条件的订单总数 total_count = Order.query.filter(query_conditions).count() # 查询当前页的数据(只查询主表,只查询前端需要的字段) dingdan_list = Order.query.filter(query_conditions).only( 'OrderID', 'Description', 'Status', 'ProductTypeID', 'Platform', 'Amount', 'ImageURL', 'CreateTime' ).order_by('-CreateTime')[offset:offset + page_size] query_time = time.time() - start_time logger.info(f"订单查询完成,耗时: {query_time:.3f}s,查询条数: {len(dingdan_list)}") # 8. 构建响应数据(严格按前端需要的字段) result_list = [] for dingdan in dingdan_list: # 注意:这里字段名必须与前端完全一致 order_data = { 'dingdan_id': dingdan.OrderID or '', 'jieshao': dingdan.Description or '', 'zhuangtai': dingdan.Status or 1, 'leixing': dingdan.ProductTypeID or 0, # 前端需要 leixing 'fadanpingtai': dingdan.Platform or 1, # 前端需要 fadanpingtai 'jiage': float(dingdan.Amount) if dingdan.Amount else 0.00, # 价格对应 Amount 字段 'tupian_url': dingdan.ImageURL or '', # 图片对应 ImageURL 字段 'fadanshijian': dingdan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan.CreateTime else '' } result_list.append(order_data) # 9. 判断是否有更多数据 current_loaded_count = offset + len(result_list) has_more = len(result_list) >= page_size and current_loaded_count < total_count # 10. 构建完整响应(严格按前端需要的结构) response_data = { 'stats': { 'platform': platform_stats, 'merchant': merchant_stats }, 'list': result_list, 'hasMore': has_more, 'total': total_count, 'currentPage': page, 'pageSize': page_size } return Response({ 'code': 0, 'message': '获取成功', 'data': response_data }) except Exception as e: logger.error(f"获取订单列表异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class AdYaoQingDaShou(APIView): """ 后台获取邀请码接口(用于邀请打手) 权限: JWT Token认证 + 管理员身份验证 前端调用: POST /yonghu/adyqds 参数: { "zhanghao": "管理员账号" } 返回: { "code": 0, "message": "成功", "data": { "yaoqingma": "邀请码" } } """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): try: # 1. 获取前端参数 qianzhanghao = request.data.get('zhanghao', '').strip() if not qianzhanghao: return Response( {'code': 400, 'message': '参数不完整', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2. JWT Token身份验证 dangqianyonghu = request.user # 3. 验证手机号是否匹配 if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 4. 验证用户类型是否为管理员 if dangqianyonghu.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: # 创建默认的管事扩展表 guanshi_profile = UserGuanshi.query.create( user=dangqianyonghu, yaoqingma='', # 暂时为空,后面生成 dianhua='', # 默认空 wechat='', # 默认空 yaogingshuliang=0, # 邀请数量为0 zhuangtai=1, # 正常状态 jinrichongzhi=0, # 今日充值0 jinyuechongzhi=0, # 今月充值0 chongzhifenrun=0, # 充值分佣0 yue=0 # 余额0 ) #logger.info(f"为管理员{dangqianyonghu.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: # 生成邀请码 new_yaoqingma = CreateInvitationCode(str(yonghuid_str)) # 验证邀请码格式 if not VerifyInvitationCode(new_yaoqingma): raise Exception('邀请码格式验证失败') except ImportError: # 如果导入失败,使用简单方法生成邀请码 timestamp = str(int(time.time())) random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) new_yaoqingma = f"GL{timestamp[-6:]}{random_str}" logger.warning(f"使用简单方法生成邀请码: {new_yaoqingma}") except Exception as e: logger.error(f"生成邀请码失败: {str(e)}") return Response( {'code': 500, 'message': '生成邀请码失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # 10. 保存邀请码到数据库 try: with transaction.atomic(): guanshi_profile.yaoqingma = new_yaoqingma guanshi_profile.save() logger.info(f"管理员{dangqianyonghu.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 ) 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: stats = { '1': UserBoss.query.count(), '2': UserDashou.query.count(), '3': UserGuanshi.query.count(), '4': UserShangjia.query.count() } return stats except Exception as e: logger.error(f"获取用户统计失败: {str(e)}") return {'1': 0, '2': 0, '3': 0, '4': 0} def _query_users_by_type(self, user_type_int, keyword, page, page_size): """ 根据用户类型查询用户列表 返回: (用户列表, 是否还有更多数据) """ try: # 计算偏移量 offset = (page - 1) * page_size if user_type_int == 1: return self._query_boss_users(keyword, offset, page_size) elif user_type_int == 2: return self._query_dashou_users(keyword, offset, page_size) elif user_type_int == 3: return self._query_guanshi_users(keyword, offset, page_size) elif user_type_int == 4: return self._query_shangjia_users(keyword, offset, page_size) else: return [], False except Exception as e: logger.error(f"查询用户列表失败: {str(e)}") return [], False def _query_boss_users(self, keyword, offset, limit): """查询普通用户(老板)""" try: queryset = UserBoss.query.select_related('user') if keyword: queryset = queryset.filter( Q(user__UserUID__icontains=keyword) | Q(nickname__icontains=keyword) ) # 获取总数 total_count = queryset.count() # 获取当前页的数据 users = list(queryset.order_by('-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: queryset = UserDashou.query.select_related('user') if keyword: queryset = queryset.filter( Q(user__UserUID__icontains=keyword) | Q(nicheng__icontains=keyword) | Q(chenghao__icontains=keyword) ) # 获取总数 total_count = queryset.count() # 获取当前页的数据 dashou_list = list(queryset.order_by('-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: queryset = UserGuanshi.query.select_related('user') if keyword: queryset = queryset.filter( Q(user__UserUID__icontains=keyword) ) # 获取总数 total_count = queryset.count() # 获取当前页的数据 guanshi_list = list(queryset.order_by('-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: queryset = UserShangjia.query.select_related('user') if keyword: queryset = queryset.filter( Q(user__UserUID__icontains=keyword) | Q(nicheng__icontains=keyword) ) # 获取总数 total_count = queryset.count() # 获取当前页的数据 shangjia_list = list(queryset.order_by('-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 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 ) # 7. 获取统计信息(总是需要统计) zongshu = PenaltyRecord.query.count() daichuli = PenaltyRecord.query.filter(ApplyStatus__in=[0, 3]).count() yichuli = PenaltyRecord.query.filter(ApplyStatus__in=[1, 2]).count() # 8. 如果只请求统计信息,直接返回 if qingqiu_tongji: return Response({ 'code': 0, 'message': '成功', 'data': { 'zongshu': zongshu, 'daichuli': daichuli, 'yichuli': yichuli } }) # 9. 构建查询集 queryset = PenaltyRecord.query.all() # 10. 根据状态筛选 if sqzhuangtai is not None: if isinstance(sqzhuangtai, list): # 如果是数组,表示多个状态 queryset = queryset.filter(ApplyStatus__in=sqzhuangtai) else: # 单个状态 queryset = queryset.filter(ApplyStatus=sqzhuangtai) # 11. 获取总数(用于分页计算) total_count = queryset.count() # 12. 计算分页偏移量 offset = (page - 1) * page_size # 13. 分页查询 chufa_list = list(queryset.order_by('-CreateTime')[offset:offset + page_size]) # 🔴【新增】高效获取图片信息 if chufa_list: # 收集所有需要查询的组合 dingdan_ids = [] yonghu_combinations = [] # 保存 (dingdan_id, yonghuid) 组合 for chufa in chufa_list: if chufa.OrderID: dingdan_ids.append(chufa.OrderID) # 商家图片组合 if chufa.ApplicantID: yonghu_combinations.append((chufa.OrderID, chufa.ApplicantID)) # 打手图片组合 if chufa.PlayerID: yonghu_combinations.append((chufa.OrderID, chufa.PlayerID)) # 去重 dingdan_ids = list(set(dingdan_ids)) yonghu_combinations = list(set(yonghu_combinations)) # 🔴【新增】批量查询图片表(高效查询,避免循环) tupian_mapping = {} if yonghu_combinations: # 构建查询条件 query_conditions = Q() for dingdan_id, yonghuid in yonghu_combinations: query_conditions |= Q(OrderID=dingdan_id, UserID=yonghuid) # 批量查询 tupian_queryset = PenaltyEvidenceImage.query.filter(query_conditions).values( 'OrderID', 'UserID', 'ImageURL' ) # 构建映射:key为"OrderID_UserID",value为图片URL列表 for item in tupian_queryset: key = f"{item['OrderID']}_{item['UserID']}" if key not in tupian_mapping: tupian_mapping[key] = [] if item['ImageURL']: tupian_mapping[key].append(item['ImageURL']) else: tupian_mapping = {} # 🔴【修改】14. 构建返回数据 - 添加图片和申诉理由字段 chufa_data = [] for chufa in chufa_list: # 🔴【新增】获取商家证据图片 zhengju_tupian = [] if chufa.OrderID and chufa.ApplicantID: key = f"{chufa.OrderID}_{chufa.ApplicantID}" zhengju_tupian = tupian_mapping.get(key, []) # 🔴【新增】获取打手申诉图片 shensu_tupian = [] if chufa.OrderID and chufa.PlayerID: key = f"{chufa.OrderID}_{chufa.PlayerID}" shensu_tupian = tupian_mapping.get(key, []) chufa_data.append({ 'dashouid': chufa.PlayerID, 'qingqiuid': chufa.ApplicantID, 'chuliid': chufa.ProcessorID, 'cfliyou': chufa.PenaltyReason, 'sqzhuangtai': chufa.ApplyStatus, 'bhliyou': chufa.RejectReason, 'jifen': chufa.DeductedPoints, 'dingdan_id': chufa.OrderID, 'create_time': chufa.create_time, 'update_time': chufa.update_time, # 🔴【新增】申诉理由字段 'ssliyou': chufa.AppealReason or '', # 🔴【新增】商家证据图片 'zhengju_tupian': zhengju_tupian, # 🔴【新增】打手申诉图片 'shensu_tupian': shensu_tupian, }) # 15. 计算是否还有更多数据 current_count = len(chufa_list) haiyougengduo = (offset + current_count) < total_count return Response({ 'code': 0, 'message': '成功', 'data': { 'list': chufa_data, 'zongshu': zongshu, 'daichuli': daichuli, 'yichuli': yichuli, 'haiyougengduo': haiyougengduo } }) except ValueError: return Response( {'code': 400, 'message': '参数类型错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: logger.error(f"处罚管理列表接口异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) logger = logging.getLogger(__name__) class AdTongYiChuFa(APIView): """ 后台同意/驳回处罚接口(已修复重复记录问题) 权限: JWT Token认证 + 管理员身份验证 前端调用: POST /yonghu/adtycf 参数: { "zhanghao": "管理员账号", "dingdan_id": "订单ID", "caozuo": 1或2, # 1=同意处罚,2=拒绝处罚 "bohui_liyou": "拒绝理由" # 拒绝处罚时必填 } 返回: { "code": 0, "message": "成功", "data": null } """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): try: # 1. 获取前端参数 qianzhanghao = request.data.get('zhanghao', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() caozuo = request.data.get('caozuo') # 1=同意,2=拒绝 bohui_liyou = request.data.get('bohui_liyou', '').strip() # 验证参数 if not qianzhanghao or not dingdan_id or caozuo is None: return Response( {'code': 400, 'message': '参数不完整', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证操作类型 if caozuo not in [1, 2]: return Response( {'code': 400, 'message': '操作类型错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 如果是拒绝处罚,需要拒绝理由 if caozuo == 2 and not bohui_liyou: return Response( {'code': 400, 'message': '拒绝处罚需要填写理由', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2. JWT Token身份验证 dangqianyonghu = request.user # 3. 验证手机号是否匹配 if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 4. 验证用户类型是否为管理员 if dangqianyonghu.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(): # 8. 🔴 修复:查询处罚记录(处理重复记录问题) # 使用 filter().first() 而不是 get(),以防出现多条记录 chufajilu = PenaltyRecord.query.filter( OrderID=dingdan_id, ApplyStatus__in=[0, 3] # 🔴【修改】包括待处理和申诉中状态 ).order_by('-CreateTime').first() # 取最新的一条 if not chufajilu: # 如果没有待处理的处罚记录,检查是否有已处理的 exists_processed = PenaltyRecord.query.filter(OrderID=dingdan_id).exists() if exists_processed: return Response( {'code': 400, 'message': '该处罚记录已处理', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) else: return Response( {'code': 404, 'message': '处罚记录不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND ) # 9. 验证处罚记录状态(必须是待处理或申诉中状态才能处理) if chufajilu.ApplyStatus not in [0, 3]: # 🔴【修改】包括待处理(0)和申诉中(3) return Response( {'code': 400, 'message': '该处罚记录已处理', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 10. 🔴 修复:检查是否有重复的待处理/申诉中记录,并记录警告 duplicate_count = PenaltyRecord.query.filter( OrderID=dingdan_id, ApplyStatus__in=[0, 3] # 🔴【修改】包括待处理和申诉中 ).count() if duplicate_count > 1: # 记录警告日志 logger.warning(f"订单ID {dingdan_id} 存在 {duplicate_count} 条待处理/申诉中处罚记录,处理最新的一条") # 🔴 可选:将其他重复的待处理/申诉中记录标记为已拒绝 # 防止用户重复提交相同订单的处罚申请 duplicate_records = PenaltyRecord.query.filter( OrderID=dingdan_id, ApplyStatus__in=[0, 3] # 🔴【修改】包括待处理和申诉中 ).exclude(id=chufajilu.id) for dup in duplicate_records: dup.ApplyStatus = 2 # 已拒绝 dup.ProcessorID = qianzhanghao dup.RejectReason = "系统自动拒绝:存在重复处罚记录" dup.save() # 11. 根据操作类型处理 if caozuo == 1: # 同意处罚 - 不扣分,只改状态 # 11.1 检查打手是否存在 if not chufajilu.PlayerID: return Response( {'code': 400, 'message': '被处罚打手ID不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 11.2 查询打手扩展表(只是验证存在性) try: dashou = UserDashou.query.get(user__UserUID=chufajilu.PlayerID) except UserDashou.DoesNotExist: return Response( {'code': 404, 'message': '被处罚打手不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND ) # 🔴【修改】11.3 同意处罚时不扣分(因为申请时已经扣了),只更新状态 chufajilu.ApplyStatus = 1 # 已处罚 chufajilu.ProcessorID = qianzhanghao # 处理人ID为管理员账号 chufajilu.save() # 11.4 更新商家订单扩展表 try: dingdan = Order.query.get(OrderID=dingdan_id) if hasattr(dingdan, 'shangjia_kuozhan'): shangjia_kuozhan = dingdan.shangjia_kuozhan shangjia_kuozhan.PenaltyApplyStatus = 1 # 已处罚 shangjia_kuozhan.save() except (Order.DoesNotExist, AttributeError): # 如果订单或扩展表不存在,继续执行,不中断 pass logger.info(f"同意处罚成功:订单ID={dingdan_id},打手ID={chufajilu.PlayerID}") elif caozuo == 2: # 拒绝处罚 - 需要返还积分(加分) # 🔴【新增】11.5 返还打手积分(加5分) if chufajilu.DeductedPoints > 0: try: dashou = UserDashou.query.get(user__UserUID=chufajilu.PlayerID) # 🔴【新增】安全验证:必须积分小于等于5分才允许加分 if dashou.jifen <= 5: # 判断积分是否小于等于5分 dashou.jifen += chufajilu.DeductedPoints # 加5分 dashou.save() logger.info( f"拒绝处罚,给打手{dashou.user.yonghuid}返还积分{chufajilu.DeductedPoints}分,当前积分{dashou.jifen}") else: # 积分已超过5分,可能是异常情况,记录警告但不中断处理 logger.warning( f"拒绝处罚时打手积分异常:打手ID={chufajilu.PlayerID},当前积分{dashou.jifen}已超过5分,不再加分") except UserDashou.DoesNotExist: # 打手不存在,记录错误,但继续处理状态 logger.error(f"拒绝处罚时打手不存在:打手ID={chufajilu.PlayerID}") # 11.6 更新处罚记录 chufajilu.ApplyStatus = 2 # 已拒绝 chufajilu.ProcessorID = qianzhanghao # 处理人ID为管理员账号 chufajilu.RejectReason = bohui_liyou # 拒绝理由 chufajilu.save() # 11.7 更新商家订单扩展表 try: dingdan = Order.query.get(OrderID=dingdan_id) if hasattr(dingdan, 'shangjia_kuozhan'): shangjia_kuozhan = dingdan.shangjia_kuozhan shangjia_kuozhan.PenaltyApplyStatus = 2 # 已拒绝 shangjia_kuozhan.RejectReason = bohui_liyou # 拒绝理由 shangjia_kuozhan.save() except (Order.DoesNotExist, AttributeError): # 如果订单或扩展表不存在,继续执行,不中断 pass logger.info(f"拒绝处罚成功:订单ID={dingdan_id},拒绝理由={bohui_liyou}") # 12. 返回成功响应 return Response({ 'code': 0, 'message': '处理成功', 'data': None }) except ValueError: return Response( {'code': 400, 'message': '参数类型错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: logger.error(f"处理处罚接口异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # yonghu/views.py class AdckyhxqView(APIView): """ 获取用户详情接口 前端需要传递: zhanghao, uid, user_type 返回用户信息和会员列表 """ permission_classes = [IsAuthenticated] def post(self, request): try: # ==================== 管理员身份验证 ==================== # 1. 验证JWT Token if not request.user.is_authenticated: return Response({ 'code': 401, 'message': '未认证', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 2. 验证用户类型为管理员 if request.user.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 = User.query.filter(UserUID=uid).first() if not user_main: return Response({ 'code': 404, 'message': '用户不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 构建响应数据 response_data = { 'user_info': {}, 'member_list': [], 'all_member_list': [] } # 通用字段(所有用户都有) user_info = { 'yonghuid': user_main.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 = User.query.filter(UserUID=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 = User.query.filter(UserUID=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.query.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.query.filter( huiyuan_id=record.huiyuan_id ).first() if huiyuan: member_list.append({ 'huiyuan_id': record.huiyuan_id, 'huiyuan_name': huiyuan.jieshao, 'daoqi_time': record.daoqi_time, 'is_active': record.huiyuan_zhuangtai == 1, 'create_time': record.create_time }) response_data['member_list'] = member_list elif user_type == 3: # 管事 # 使用select_related预取guanshi_profile user_main = User.query.filter(UserUID=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 = User.query.filter(UserUID=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.query.all().order_by('jiage') all_member_list = [ { 'id': huiyuan.huiyuan_id, 'nicheng': huiyuan.jieshao, 'jiage': huiyuan.jiage, 'jieshao': huiyuan.jtjieshao } for huiyuan in all_huiyuan ] response_data['user_info'] = user_info response_data['all_member_list'] = all_member_list return Response({ 'code': 0, 'message': '获取成功', 'data': response_data }, status=status.HTTP_200_OK) except Exception as e: return Response({ 'code': 500, 'message': f'获取用户详情失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdcjxgView(APIView): """ 修改用户信息接口 前端需要传递: zhanghao, uid, user_type, 以及其他修改的字段 支持打手会员添加功能 敏感字段修改会记录到修改记录表 """ permission_classes = [IsAuthenticated] def post(self, request): try: # ==================== 管理员身份验证 ==================== if not request.user.is_authenticated: return Response({ 'code': 401, 'message': '未认证', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) if request.user.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 = User.query.filter(UserUID=uid).first() if not user_main: return Response({ 'code': 404, 'message': '用户不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 使用事务确保数据一致性 with transaction.atomic(): # 存储修改记录的数据 modify_record_data = { 'yonghuid': uid, 'xiugaiid': zhanghao, 'leixing': user_type, 'qitashuoming': '', '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.query.get_or_create( user=user_main, defaults={'nicheng': user_main.phone or f'打手{uid}'} ) # 记录敏感字段的旧值 old_yue = dashou_profile.yue # 打手可提现余额 old_zonge = dashou_profile.zonge # 打手提现金额 old_jifen = dashou_profile.jifen # 打手积分 old_yajin = dashou_profile.yajin # 打手押金 # 需要更新的字段列表 update_fields = [ 'chenghao', 'dianhua', 'yue', 'zonge', 'jifen', 'yajin', 'yaoqingren', 'jieshao', 'zhuangtai', 'zaixianzhuangtai', 'zhanghaozhuangtai' ] # 记录被修改的字段 modified_fields = [] for field in update_fields: if field in request.data: new_value = request.data.get(field) old_value = getattr(dashou_profile, field) # 检查值是否改变 if str(new_value) != str(old_value): # 处理特殊字段类型 if field in ['yue', 'zonge', 'yajin']: new_value = Decimal(str(new_value)) setattr(dashou_profile, field, new_value) elif field in ['jifen']: new_value = int(new_value) setattr(dashou_profile, field, new_value) elif field in ['zhuangtai', 'zaixianzhuangtai', 'zhanghaozhuangtai']: new_value = int(new_value) setattr(dashou_profile, field, new_value) else: setattr(dashou_profile, field, new_value) modified_fields.append(field) # 记录敏感字段修改 if field == 'yue': has_sensitive_change = True modify_record_data['xiugaitijiao'] = new_value # 使用xiugaitijiao字段记录打手余额修改 modify_record_data['xiugaitijiaoq'] = old_yue elif field == 'zonge': has_sensitive_change = True # zonge字段在修改记录表中可能没有直接对应,可以记录在备注中 modify_record_data['qitashuoming'] += f' 打手总额从{old_zonge}修改为{new_value}' elif field == 'jifen': has_sensitive_change = True modify_record_data['jifen'] = new_value modify_record_data['yjifen'] = old_jifen elif field == 'yajin': has_sensitive_change = True modify_record_data['yajin'] = new_value modify_record_data['yyajin'] = old_yajin # 保存打手信息 dashou_profile.save() # 记录被修改的非敏感字段 non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'zonge', 'jifen', 'yajin']] if non_sensitive_fields: modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}' elif user_type == 3: # 管事 # 获取或创建管事扩展记录 guanshi_profile, created = UserGuanshi.query.get_or_create( user=user_main, defaults={'yaoqingma': f'GS{uid[-4:]}'} ) # 记录敏感字段的旧值 old_yue = guanshi_profile.yue # 管事可提现余额 update_fields = [ 'yaogingshuliang', 'chongzhifenrun', 'yue', 'zhuangtai', 'dianhua', 'wechat' ] modified_fields = [] for field in update_fields: if field in request.data: new_value = request.data.get(field) old_value = getattr(guanshi_profile, field) if str(new_value) != str(old_value): if field in ['chongzhifenrun', 'yue']: new_value = Decimal(str(new_value)) setattr(guanshi_profile, field, new_value) elif field in ['yaogingshuliang']: new_value = int(new_value) setattr(guanshi_profile, field, new_value) elif field in ['zhuangtai']: new_value = int(new_value) setattr(guanshi_profile, field, new_value) else: setattr(guanshi_profile, field, new_value) modified_fields.append(field) # 记录敏感字段修改 if field == 'yue': has_sensitive_change = True modify_record_data['guanshiyue'] = new_value modify_record_data['guanshiyueq'] = old_yue elif field == 'chongzhifenrun': has_sensitive_change = True modify_record_data['qitashuoming'] += f' 管事分佣从{old_value}修改为{new_value}' guanshi_profile.save() # 记录非敏感字段 non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'chongzhifenrun']] if non_sensitive_fields: modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}' elif user_type == 4: # 商家 # 获取或创建商家扩展记录 shop_profile, created = UserShangjia.query.get_or_create( user=user_main, defaults={'nicheng': user_main.phone or f'商家{uid}'} ) # 记录敏感字段的旧值 old_yue = shop_profile.yue # 商家余额 update_fields = ['yue', 'zhuangtai', 'dianhua', 'wechat'] modified_fields = [] for field in update_fields: if field in request.data: new_value = request.data.get(field) old_value = getattr(shop_profile, field) if str(new_value) != str(old_value): if field == 'yue': new_value = Decimal(str(new_value)) setattr(shop_profile, field, new_value) elif field == 'zhuangtai': new_value = int(new_value) setattr(shop_profile, field, new_value) else: setattr(shop_profile, field, new_value) modified_fields.append(field) # 记录敏感字段修改 if field == 'yue': has_sensitive_change = True modify_record_data['shangjiayue'] = new_value modify_record_data['shangjiayueq'] = old_yue shop_profile.save() # 记录非敏感字段 non_sensitive_fields = [f for f in modified_fields if f != 'yue'] if non_sensitive_fields: modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}' # 处理打手会员添加(只有打手可以添加会员) # 🔥🔥🔥 后端修改:支持正负数会员天数 # 修改会员处理逻辑部分 if user_type == 2 and 'huiyuan_id' in request.data and 'days' in request.data: huiyuan_id = request.data.get('huiyuan_id') days = int(request.data.get('days', 0)) # 🔥 修改:允许正负数,但不能为0 if huiyuan_id and days != 0: # 🔥 修改:检查绝对值范围 if abs(days) < 1 or abs(days) > 10000: return Response({ 'code': 400, 'message': '会员天数必须在1-10000之间(正数增加,负数减少)', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 检查会员是否存在 huiyuan = Huiyuan.query.filter(huiyuan_id=huiyuan_id).first() if not huiyuan: return Response({ 'code': 404, 'message': '会员不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 检查是否已购买该会员 existing_record = Huiyuangoumai.query.filter( yonghu_id=uid, huiyuan_id=huiyuan_id ).first() if existing_record: # 🔥🔥🔥 重要修改:支持减少天数 # 计算新的到期时间 new_daoqi_time = existing_record.daoqi_time + timedelta(days=days) # 🔥 安全验证:减少天数后不能小于当前时间前90天(防止过度减少) # 这个限制可以根据业务调整 min_allowed_time = timezone.now() - timedelta(days=90) if new_daoqi_time < min_allowed_time: return Response({ 'code': 400, 'message': f'减少天数过多,调整后到期时间不能早于 {(min_allowed_time + timedelta(days=90)).strftime("%Y-%m-%d")}', 'data': None }, status=status.HTTP_400_BAD_REQUEST) existing_record.daoqi_time = new_daoqi_time existing_record.save() # 记录会员操作 has_sensitive_change = True modify_record_data['huiyuants'] = days modify_record_data['huiyuan_id'] = huiyuan_id if days > 0: modify_record_data['qitashuoming'] += f' 续费会员 {huiyuan.jieshao} {days}天' # 只有增加天数时才增加购买次数 huiyuan.goumai_cishu += 1 huiyuan.save() else: modify_record_data['qitashuoming'] += f' 减少会员 {huiyuan.jieshao} {abs(days)}天' # 减少天数时不增加购买次数 else: # 🔥 修改:新购买时只能增加天数 if days > 0: daoqi_time = timezone.now() + timedelta(days=days) new_record = Huiyuangoumai.query.create( huiyuan_id=huiyuan_id, yonghu_id=uid, jieshao=huiyuan.jieshao, daoqi_time=daoqi_time ) # 记录会员购买 has_sensitive_change = True modify_record_data['huiyuants'] = days modify_record_data['huiyuan_id'] = huiyuan_id modify_record_data['qitashuoming'] += f' 购买会员 {huiyuan.jieshao} {days}天' # 更新会员购买次数 huiyuan.goumai_cishu += 1 huiyuan.save() else: # 🔥 不能为不存在的会员减少天数 return Response({ 'code': 400, 'message': '用户未购买该会员,无法减少天数', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 如果有敏感字段被修改或会员操作,创建修改记录 if has_sensitive_change or 'huiyuan_id' in request.data: # 创建修改记录 Xiugaijilu.query.create(**modify_record_data) elif modify_record_data.get('qitashuoming', '').strip(): # 只有非敏感字段修改,也记录但qitashuoming不同 Xiugaijilu.query.create(**modify_record_data) return Response({ 'code': 0, 'message': '修改成功', 'data': None }, status=status.HTTP_200_OK) except Exception as e: return Response({ 'code': 500, 'message': f'修改失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 管理员认证装饰器 def admin_required(view_func): """管理员权限验证装饰器""" def wrapped_view(self, request, *args, **kwargs): try: # 1. 验证JWT Token if not request.user.is_authenticated: return Response({ 'code': 401, 'message': '未认证', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 2. 验证用户类型为管理员 if request.user.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 class AddtxshView(APIView): """ 获取提现审核列表接口 与用户管理页面保持一致的分页逻辑 支持按用户ID搜索,排序改为按申请时间升序(最早申请排最前) """ permission_classes = [IsAuthenticated] @admin_required def post(self, request): try: # 获取参数 zhanghao = request.data.get('zhanghao') page = int(request.data.get('page', 1)) page_size = int(request.data.get('page_size', 5)) zhuangtai = int(request.data.get('zhuangtai', 1)) leixing = int(request.data.get('leixing', 1)) fangshi = request.data.get('fangshi') # 🔴【新增】获取搜索参数(用户ID) search_uid = request.data.get('search_uid', '').strip() if page < 1: page = 1 # 计算偏移量(与用户管理页面完全一致) offset = (page - 1) * page_size # 构建查询条件 query = Q() # 状态筛选 if zhuangtai == 1: query &= Q(zhuangtai=1) elif zhuangtai == 2: query &= Q(zhuangtai__in=[2, 3]) # 类型筛选 query &= Q(leixing=leixing) # 收款方式筛选(可选) if fangshi: query &= Q(fangshi=int(fangshi)) # 🔴【新增】如果搜索了用户ID,添加条件 if search_uid: query &= Q(yonghuid=search_uid) # 🔴【修改】排序改为按申请时间升序(最早申请排最前) queryset = Tixianjilu.query.filter(query).order_by('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.query.filter( zhuangtai=1, leixing=leixing ).count(), 'processed': Tixianjilu.query.filter( zhuangtai__in=[2, 3], leixing=leixing ).count() } # 构建响应数据 withdrawal_list = [] for record in records: withdrawal_list.append({ 'id': record.id, 'yonghuid': record.yonghuid, 'avatar': record.avatar, 'phone': record.phone, 'nicheng': record.nicheng, 'leixing': record.leixing, 'zhifu': record.zhifu, 'skzhanghao': record.skzhanghao, 'jine': str(record.jine) if record.jine else '0.00', 'zhuangtai': record.zhuangtai, 'fangshi': record.fangshi, 'shenheid': record.shenheid, 'bhliyou': record.bhliyou, '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 AdtixianqkView(APIView): """ 处理提现申请接口(已更新:添加收支记录表更新) """ permission_classes = [IsAuthenticated] @admin_required # 假设你有这个装饰器 def post(self, request): try: zhanghao = request.data.get('zhanghao') tixian_id = request.data.get('tixian_id') result = int(request.data.get('result', 0)) reason = request.data.get('reason', '') if not tixian_id: return Response({ 'code': 400, 'message': '提现记录ID不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if result not in [2, 3]: return Response({ 'code': 400, 'message': '操作结果无效', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 如果是拒绝,需要理由 if result == 3 and not reason: return Response({ 'code': 400, 'message': '拒绝提现需要提供理由', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 获取提现记录 try: withdrawal = Tixianjilu.query.get(id=tixian_id) except Tixianjilu.DoesNotExist: return Response({ 'code': 404, 'message': '提现记录不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 检查状态,只有待审核的可以处理 if withdrawal.zhuangtai != 1: return Response({ 'code': 400, 'message': '该提现申请已被处理', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 使用事务确保数据一致性 with transaction.atomic(): # 更新提现记录 withdrawal.zhuangtai = result withdrawal.shenheid = request.user.yonghuid if result == 3: # 拒绝 withdrawal.bhliyou = reason withdrawal.update_time = timezone.now() withdrawal.save() # 🟢 新增:如果同意提现(result=2),更新收支记录表 if result == 2: try: # 查询收支记录表,id=1的记录 szjilu_record = Szjilu.query.get(id=1) # 获取提现金额 tixian_jine = withdrawal.jine # 假设提现记录模型中有jine字段 update_daily_payout(tixian_jine) # 更新收支记录 szjilu_record.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.query.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) # views.py (yonghu应用) class DashouZiliaoHuoquView(APIView): """ 打手资料获取接口 URL: /yonghu/dszjxxhq 方法: POST 认证: JWT Token 返回: 打手扩展表信息 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 使用select_related高效查询,避免N+1问题 dashou = UserDashou.query.select_related('user').get(user=request.user) # 构建返回数据 data = { 'nicheng': dashou.nicheng if dashou.nicheng else '', 'jieshao': dashou.jieshao if dashou.jieshao else '', 'dianhua': dashou.dianhua if dashou.dianhua else '', 'wechat': dashou.wechat if dashou.wechat else '', 'uid': request.user.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应用) class DashouGengxinView(APIView): """ 打手信息更新接口 URL: /yonghu/dsgxxx 方法: POST 认证: JWT Token 参数: nicheng(昵称), dianhua(电话), wechat(微信), jieshao(介绍) """ permission_classes = [IsAuthenticated] def post(self, request): try: # 获取请求数据 nicheng = request.data.get('nicheng', '').strip() dianhua = request.data.get('dianhua', '').strip() wechat = request.data.get('wechat', '').strip() jieshao = request.data.get('jieshao', '').strip() # 数据验证 validation_errors = self.validate_data(nicheng, dianhua, wechat, jieshao) if validation_errors: return Response({ 'code': 400, 'msg': validation_errors }, status=status.HTTP_400_BAD_REQUEST) # 获取打手扩展表 try: dashou = UserDashou.query.get(user=request.user) except UserDashou.DoesNotExist: return Response({ 'code': 404, 'msg': '打手信息不存在' }, status=status.HTTP_404_NOT_FOUND) # 更新数据 update_fields = [] if nicheng is not None: dashou.nicheng = nicheng if nicheng != '' else None update_fields.append('nicheng') if dianhua is not None: dashou.dianhua = dianhua if dianhua != '' else None update_fields.append('dianhua') if wechat is not None: dashou.wechat = wechat if wechat != '' else None update_fields.append('wechat') if jieshao is not None: dashou.jieshao = jieshao if jieshao != '' else None update_fields.append('jieshao') # 保存更新(只更新修改的字段) if update_fields: dashou.save(update_fields=update_fields) return Response({ 'code': 200, 'msg': '更新成功', 'data': { 'nicheng': dashou.nicheng or '', 'dianhua': dashou.dianhua or '', 'wechat': dashou.wechat or '', 'jieshao': dashou.jieshao or '' } }, status=status.HTTP_200_OK) except Exception as e: logger.error(f'更新打手信息异常: {str(e)}', exc_info=True) return Response({ 'code': 500, 'msg': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def validate_data(self, nicheng, dianhua, wechat, jieshao): """数据验证方法""" errors = [] # 1. 昵称验证(必填) if not nicheng: errors.append('昵称不能为空') elif len(nicheng) > 20: errors.append('昵称不能超过20个字符') # 2. 手机号验证(非必填,但填写时必须正确) if dianhua: # 手机号正则:1开头,11位数字 phone_pattern = r'^1[3-9]\d{9}$' if not re.match(phone_pattern, dianhua): errors.append('手机号格式不正确') # 3. 微信号验证(非必填) if wechat and len(wechat) > 30: errors.append('微信号不能超过30个字符') # 4. 个人介绍验证 if jieshao and len(jieshao) > 200: errors.append('个人介绍不能超过200个字符') return '; '.join(errors) if errors else None class ShangjiaZhuceView(APIView): """ 商家注册接口 (通过管理员邀请码) 访问路径: /api/yonghu/shangjiahuce/ 请求方法: POST 权限要求: JWT Token认证通过的用户 请求体: { "inviteCode": "管理员邀请码" } 返回字段说明: - code: 状态码(200成功,其他失败) - message: 提示信息 - data: 商家信息数据,包含以下字段(与前端商家页面完全一致): - nicheng: 商家昵称 - sjyue: 商家余额(字符串,保留两位小数) - fabu: 发布订单总数 - chengjiao: 成交订单总数 - tuikuan: 退款订单总数 - jinridingdan: 今日订单 - jinriliushui: 今日流水(字符串) - jinyuedingdan: 今月订单 - jinyueliushui: 今月流水(字符串) - zhuangtai: 商家状态(1正常) - dianhua: 商家电话 - wechat: 商家微信 """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 获取并验证邀请码参数 yaoqingma = request.data.get('inviteCode') if not yaoqingma: return Response({ 'code': 400, 'message': '邀请码不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) yaoqingma = yaoqingma.strip() if len(yaoqingma) > 100: return Response({ 'code': 400, 'message': '邀请码长度超过限制', 'data': None }, status=status.HTTP_400_BAD_REQUEST) current_user = request.user # 2. 检查用户是否已是商家 try: shangjia_profile = current_user.shop_profile # 用户已是商家,直接返回现有信息 return self.fanhuiXianyouShangjiaXinxi(shangjia_profile, current_user) except UserShangjia.DoesNotExist: # 用户不是商家,继续注册流程 pass # 3. 根据邀请码查找对应的管理员 try: # 从管理员表查找邀请码 admin_profile = AdminProfile.query.select_related('user').get(yaoqingma=yaoqingma) except AdminProfile.DoesNotExist: return Response({ 'code': 404, 'message': '管理员邀请码无效或不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 4. 核心:在数据库事务中创建商家身份 try: with transaction.atomic(): # 4.1 查询老板扩展表获取老板昵称 boss_nickname = '普通商家' try: boss_profile = current_user.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.query.filter(nicheng=shangjia_nicheng).exists(): # 如果存在,添加随机数确保唯一 shangjia_nicheng = f"{boss_nickname}{random.randint(1000, 9999)}" # 4.3 创建商家记录 shop_profile = UserShangjia.query.create( user=current_user, nicheng=shangjia_nicheng, zhuangtai=1, # 正常状态 yue=0.00, # 商家余额 fabu=0, # 发布订单总数 tuikuan=0, # 退款订单总数 chengjiao=0, # 成交订单总数 jinridingdan=0, # 今日订单 jinriliushui=0.00, # 今日流水 jinyuedingdan=0, # 今月订单 jinyueliushui=0.00, # 今月流水 dianhua='', # 电话 wechat='', # 微信 ) except Exception as e: # 事务会自动回滚 error_detail = traceback.format_exc() #print(f"商家注册错误详情: {error_detail}") return Response({ 'code': 500, 'message': f'商家注册过程中发生系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 5. 注册成功,返回商家信息 fanhui_data = self.zhuangbeiFanhuiShuju(shop_profile, current_user) return Response({ 'code': 200, 'message': '商家注册成功!', 'data': fanhui_data }) def fanhuiXianyouShangjiaXinxi(self, shangjia_profile, current_user): """处理已是商家的用户,返回其现有信息""" fanhui_data = self.zhuangbeiFanhuiShuju(shangjia_profile, current_user) return Response({ 'code': 200, 'message': '您已是商家,返回当前信息。', 'data': fanhui_data }) def zhuangbeiFanhuiShuju(self, shangjia_profile, current_user): """ 准备返回给前端的商家数据 字段名必须与前端商家页面完全一致 """ data = { # 🟢 商家基本信息 'nicheng': shangjia_profile.nicheng, # 商家昵称 'sjyue': format(shangjia_profile.yue, '.2f'), # 商家余额,保留两位小数 'fabu': shangjia_profile.fabu, # 发布订单总数 'chengjiao': shangjia_profile.chengjiao, # 成交订单总数 'tuikuan': shangjia_profile.tuikuan, # 退款订单总数 'jinridingdan': shangjia_profile.jinridingdan, # 今日订单 'jinriliushui': format(shangjia_profile.jinriliushui, '.2f'), # 今日流水 'jinyuedingdan': shangjia_profile.jinyuedingdan, # 今月订单 'jinyueliushui': format(shangjia_profile.jinyueliushui, '.2f'), # 今月流水 'zhuangtai': shangjia_profile.zhuangtai, # 商家状态 'dianhua': shangjia_profile.dianhua or '', # 电话 'wechat': shangjia_profile.wechat or '', # 微信 # 🟢 可能需要的主表信息 'uid': current_user.yonghuid, # 用户ID } return data # yonghu/ranking_views.py """ 排行榜查询接口 - 管理员专用 安全认证:JWT Token + 管理员权限验证 """ # yonghu/ranking_views.py - 修改PaihangbangGuanliQueryView的post方法参数接收 class PaihangbangGuanliQueryView(APIView): """ 排行榜查询接口 - 管理员专用 安全要求: 1. JWT Token认证 2. 管理员权限验证 3. 参数严格验证 4. 返回统一格式 """ # JWT Token认证 permission_classes = [IsAuthenticated] @admin_required def post(self, request): """ POST请求查询排行榜数据 参数: - zhanghao: 管理员账号 - shenfen: 用户身份(必须,2=打手,3=管事,4=商家) - zhouqi: 周期类型(必须,1=日榜,2=月榜) - date: 查询日期(必须,格式:YYYY-MM-DD) 返回格式: { "code": 0, # 0=成功,非0=失败 "message": "成功", "data": { "list": [...], # 排行榜数据列表 "query_info": {...} # 查询信息 } } """ try: # 1. 获取请求数据 data = request.data zhanghao = data.get('zhanghao') shenfen = data.get('shenfen') zhouqi = data.get('zhouqi') date_str = data.get('date') # 2. 参数验证 if not zhanghao: return Response({ 'code': 400, 'message': '管理员账号不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not shenfen: return Response({ 'code': 400, 'message': '用户身份不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not zhouqi: return Response({ 'code': 400, 'message': '周期类型不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not date_str: return Response({ 'code': 400, 'message': '查询日期不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 3. 参数类型和范围验证 try: zhouqi_int = int(zhouqi) shenfen_int = int(shenfen) date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() except ValueError as e: return Response({ 'code': 400, 'message': f'参数格式错误: {str(e)}', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证周期类型范围 if zhouqi_int not in [1, 2]: return Response({ 'code': 400, 'message': '周期类型必须为1(日榜)或2(月榜)', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证用户身份范围 if shenfen_int not in [2, 3, 4]: return Response({ 'code': 400, 'message': '用户身份必须为2(打手)、3(管事)或4(商家)', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 4. 构建查询条件 query = Q( zhouqi=zhouqi_int, tongji_date=date_obj, shenfen=shenfen_int ) # 5. 执行查询(按排名升序) records = RankingRecord.query.filter(query).order_by('paiming') # 6. 格式化返回数据 data_list = [] for record in records: # 根据用户身份确定指标值单位 zhizhi_danwei = self._get_zhizhi_danwei(shenfen_int) data_list.append({ 'paiming': record.paiming, 'yonghuid': record.yonghuid, 'nicheng': record.nicheng, 'avatar': record.avatar or '', 'zhizhi': float(record.zhizhi), 'zhizhi_danwei': zhizhi_danwei, }) # 7. 返回成功响应 response_data = { 'code': 0, 'message': '查询成功', 'data': { 'list': data_list, 'total_count': len(data_list), 'query_info': { 'zhouqi': zhouqi_int, 'zhouqi_text': '日榜' if zhouqi_int == 1 else '月榜', 'shenfen': shenfen_int, 'shenfen_text': self._get_shenfen_text(shenfen_int), 'date': date_str, } } } return Response(response_data, status=status.HTTP_200_OK) except Exception as e: logger.error(f"管理员查询排行榜失败: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': f'查询排行榜失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def _get_shenfen_text(self, shenfen): """获取身份文本""" shenfen_map = {2: '打手', 3: '管事', 4: '商家'} return shenfen_map.get(shenfen, '未知') def _get_zhizhi_danwei(self, shenfen): """获取指标值单位""" danwei_map = {2: '元', 3: '人', 4: '元'} return danwei_map.get(shenfen, '') # yonghu/ranking_views.py - 添加以下代码 class PaihangbangRiqiliebiaoView(APIView): """ 获取排行榜可查询日期列表 - 管理员专用 返回当前排行榜表中存在的日期(日榜和月榜分开) """ # JWT Token认证 permission_classes = [IsAuthenticated] @admin_required def post(self, request): """ POST请求获取可查询日期列表 参数: - zhanghao: 管理员账号(用于验证) 返回格式: { "code": 0, "message": "成功", "data": { "ribang_dates": ["2024-01-15", "2024-01-14", ...], # 日榜日期列表 "yuebang_dates": ["2024-01-01", "2023-12-01", ...], # 月榜日期列表 } } """ try: # 1. 获取请求数据 data = request.data zhanghao = data.get('zhanghao') if not zhanghao: return Response({ 'code': 400, 'message': '管理员账号不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 2. 查询日榜日期(最近3个月内的) three_months_ago = timezone.now().date() - timedelta(days=90) ribang_dates = RankingRecord.query.filter( zhouqi=1, # 日榜 tongji_date__gte=three_months_ago # 只查询最近3个月 ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date') # 3. 查询月榜日期(最近12个月内的) twelve_months_ago = timezone.now().date() - timedelta(days=365) yuebang_dates = RankingRecord.query.filter( zhouqi=2, # 月榜 tongji_date__gte=twelve_months_ago # 只查询最近12个月 ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date') # 4. 格式化日期 ribang_date_list = [date.strftime('%Y-%m-%d') for date in ribang_dates] yuebang_date_list = [date.strftime('%Y-%m-%d') for date in yuebang_dates] # 5. 返回成功响应 response_data = { 'code': 0, 'message': '获取成功', 'data': { 'ribang_dates': ribang_date_list, 'yuebang_dates': yuebang_date_list, 'ribang_count': len(ribang_date_list), 'yuebang_count': len(yuebang_date_list), } } return Response(response_data, status=status.HTTP_200_OK) except Exception as e: logger.error(f"获取排行榜日期列表失败: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': f'获取日期列表失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class WechatLoginAndDashouRegisterView(APIView): """ 微信登录并注册打手一体化接口 访问路径: /api/yonghu/wdlyhdl 请求方法: POST 权限要求: 无需登录 请求体: { "code": "微信登录code", "inviteCode": "邀请码" } 功能: 用户扫码进入但未登录时,一次性完成微信登录和打手注册 """ throttle_classes = [AnonRateThrottle] permission_classes = [AllowAny] def post(self, request): """ 处理微信登录并注册打手请求 """ try: # 1. 获取并验证参数 code = request.data.get('code', '').strip() yaoqingma = request.data.get('inviteCode', '').strip() if not code: return Response({ 'code': 1, 'msg': '微信授权码不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not yaoqingma: return Response({ 'code': 2, 'msg': '邀请码不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if len(yaoqingma) > 100: return Response({ 'code': 3, 'msg': '邀请码长度超过限制', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 2. 获取微信openid wechat_data = self.get_wechat_openid(code) if not wechat_data or 'openid' not in wechat_data: error_msg = wechat_data.get('errmsg', '微信授权失败') return Response({ 'code': 4, 'msg': f'微信登录失败: {error_msg}', 'data': None }, status=status.HTTP_400_BAD_REQUEST) openid = wechat_data['openid'] # 3. 获取客户端真实IP kehuduan_ip = self.huoquKehuduanIP(request) # 4. 开始数据库事务(确保登录和注册的原子性) with transaction.atomic(): # 4.1 创建或获取用户(登录逻辑) user_main, created = User.objects.select_for_update().get_or_create( OpenID=openid, defaults={ 'UserUID': self.shengchengYonghuID(), 'UserName': openid, } ) # 更新用户IP和最后登录时间 cunchu_ip = kehuduan_ip user_main.ip = cunchu_ip user_main.last_login_time = timezone.now() user_main.save() if created: # 新用户:创建老板扩展表 UserBoss.query.create(user=user_main, nickname='微信用户') # 4.2 验证邀请码对应的管事(注册逻辑) try: # 使用select_related获取管事及其关联的主表用户信息 guanshi_profile = UserGuanshi.query.select_related('user').get(yaoqingma=yaoqingma) except UserGuanshi.DoesNotExist: # 邀请码无效,但用户已登录,可以返回登录成功但注册失败 # 这里我们继续执行,但只返回登录信息 return self.fanhuiZhihouDengluXinxi(user_main, yaoqingma_wuxiao=True) # 验证管事状态 # 4.3 检查用户是否已是打手 dashou_exists = UserDashou.query.filter(user=user_main).exists() if dashou_exists: # 用户已是打手,直接返回信息 dashou_profile = UserDashou.query.get(user=user_main) return self.fanhuiDengluZhuceshuju(user_main, dashou_profile) # 4.4 核心:为用户创建所有身份(打手、商家、管事) guanshi_yonghuid = guanshi_profile.user.yonghuid # 创建打手扩展表 dashou_profile = UserDashou.query.create( user=user_main, nicheng='大神', chenghao='普通大神', yaoqingren=guanshi_yonghuid, # 其他字段使用模型默认值 ) # 创建商家扩展表 # 创建管事扩展表 # 更新原管事的邀请数量 locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk) locked_guanshi.yaogingshuliang += 1 locked_guanshi.save() # ========== 新增:管事每日统计(邀请打手) ========== try: update_guanshi_daily_by_action( yonghuid=guanshi_yonghuid, action=1, # 1 = 邀请打手 amount=Decimal('0.00') ) logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}") except Exception as e: logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}") # 5. 生成JWT token refresh = RefreshToken.for_user(user_main) token = str(refresh.access_token) # 6. 准备返回数据 response_data = self.zhuangbeiFanhuiShuju(user_main, dashou_profile, token) return Response({ 'code': 0, 'msg': '登录并注册成功!已开通打手、管事等身份。', 'data': response_data }) except Exception as e: # 记录错误日志 error_openid = locals().get('openid', 'N/A') logger.error( f"微信登录注册一体化接口异常 - openid: {error_openid}, " f"错误类型: {type(e).__name__}, 错误详情: {str(e)}", exc_info=True ) # 返回友好错误信息 return Response({ 'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def get_wechat_openid(self, code): """ 调用微信接口获取openid """ try: appid = getattr(settings, 'WEIXIN_APPID', '') secret = getattr(settings, 'WEIXIN_SECRET', '') if not appid or not secret: raise ValueError('微信配置未设置') url = 'https://api.weixin.qq.com/sns/jscode2session' params = { 'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code' } response = requests.get(url, params=params, timeout=10) result = response.json() if 'openid' in result: return result else: errcode = result.get('errcode', 'unknown') errmsg = result.get('errmsg', '未知错误') return {'errmsg': f'[{errcode}]{errmsg}'} except requests.exceptions.Timeout: return {'errmsg': '请求微信接口超时'} except Exception as e: return {'errmsg': f'请求微信接口异常: {str(e)}'} def shengchengYonghuID(self): """ 生成7位数字用户ID """ for _ in range(10): timestamp_part = str(int(time.time()))[-5:].zfill(5) random_part = str(random.randint(0, 99)).zfill(2) user_id = timestamp_part + random_part if len(user_id) == 7 and user_id.isdigit(): if not User.query.filter(UserUID=user_id).exists(): return user_id raise Exception('生成用户ID失败') def huoquKehuduanIP(self, request): """ 获取客户端真实IP地址 """ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: ip = x_forwarded_for.split(',')[0].strip() if ip: return ip ip = request.META.get('REMOTE_ADDR', '') return ip if ip else '0.0.0.0' def huoquQunPeizhi(self): """ 获取群配置信息 """ try: qun_configs = Qunpeizhi.query.filter(id__in=[1, 2]) result = { 'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': '' } for config in qun_configs: if config.id == 1: result['dashouqun'] = config.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.query.get(user=user_main) if boss_profile.nickname: boss_nickname = boss_profile.nickname except UserBoss.DoesNotExist: pass # 检查各身份状态 dashou_status = 1 if UserDashou.query.filter(user=user_main).exists() else 0 shangjia_status = 1 if UserShangjia.query.filter(user=user_main).exists() else 0 guanshi_status = 1 if UserGuanshi.query.filter(user=user_main).exists() else 0 # 生成token refresh = RefreshToken.for_user(user_main) token = str(refresh.access_token) # 获取群配置 group_info = self.huoquQunPeizhi() # 构建返回数据 response_data = { 'token': token, 'nicheng': boss_nickname, 'uid': user_main.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': 0, 'msg': msg, 'data': response_data }) def fanhuiDengluZhuceshuju(self, user_main, dashou_profile): """ 返回登录并注册的数据(用户已是打手的情况) """ # 获取老板昵称 boss_nickname = '微信用户' try: boss_profile = UserBoss.query.get(user=user_main) if boss_profile.nickname: boss_nickname = boss_profile.nickname except UserBoss.DoesNotExist: pass # 检查各身份状态(这里用户已是打手,所以都是1) dashou_status = 1 shangjia_status = 1 if UserShangjia.query.filter(user=user_main).exists() else 1 # 确保是1 guanshi_status = 1 if UserGuanshi.query.filter(user=user_main).exists() else 1 # 确保是1 # 生成token refresh = RefreshToken.for_user(user_main) token = str(refresh.access_token) # 获取群配置 group_info = self.huoquQunPeizhi() # 准备打手信息 dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile) # 合并所有数据 response_data = { 'token': token, 'nicheng': boss_nickname, 'uid': user_main.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.query.get(user=user_main) if boss_profile.nickname: boss_nickname = boss_profile.nickname except UserBoss.DoesNotExist: pass # 身份状态(刚注册成功,应该都是1) dashou_status = 1 shangjia_status = 1 guanshi_status = 1 # 获取群配置 group_info = self.huoquQunPeizhi() # 准备打手信息 dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile) # 合并所有数据 response_data = { 'token': token, 'nicheng': boss_nickname, 'uid': user_main.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.query.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 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: # 记录详细错误日志 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 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 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 = PenaltyRecord.query.filter(PlayerID=yonghuid).aggregate( # 总记录数 zongshu=Count('id'), # 待处理数:状态为0(待处罚)和3(申诉中) daichuli=Count( Case( When(Q(ApplyStatus=0) | Q(ApplyStatus=3), then=1), output_field=IntegerField() ) ), # 已处理数:状态为1(已处罚)和2(已驳回) yichuli=Count( Case( When(Q(ApplyStatus=1) | Q(ApplyStatus=2), then=1), output_field=IntegerField() ) ) ) # 如果查询结果为None,则设置为0 tongji_data = { 'zongshu': stats.get('zongshu', 0) or 0, 'daichuli': stats.get('daichuli', 0) or 0, 'yichuli': stats.get('yichuli', 0) or 0 } return Response({ 'code': 0, 'msg': '获取统计信息成功', 'data': tongji_data }) except Exception as e: logger.error(f"获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}") return Response({ 'code': 1, 'msg': '获取统计信息失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def get_chufa_list(self, yonghuid, request_data): """ 获取处罚记录列表(高性能批量查询) 🔴 只修改图片查询逻辑,字段保持原样 """ try: # 获取分页参数 page = int(request_data.get('page', 1)) page_size = int(request_data.get('page_size', 10)) # 限制最大每页数量,防止恶意请求 if page_size > 50: page_size = 50 if page_size < 1: page_size = 10 # 获取状态筛选参数 zhuangtai = request_data.get('zhuangtai', 0) # 🔴 构建查询条件 - 打手端根据PlayerID查询 queryset = PenaltyRecord.query.filter(PlayerID=yonghuid) if zhuangtai == 0: # 待处理:状态为0(待处罚)和3(申诉中) queryset = queryset.filter(Q(ApplyStatus=0) | Q(ApplyStatus=3)) elif zhuangtai == 1: # 已处理:状态为1(已处罚)和2(已驳回) queryset = queryset.filter(Q(ApplyStatus=1) | Q(ApplyStatus=2)) else: # 默认返回所有状态 pass # 按创建时间倒序排列(最新在前) queryset = queryset.order_by('-CreateTime') # 使用游标分页,避免传统分页的性能问题 paginator = Paginator(queryset, page_size) try: page_obj = paginator.page(page) except: # 页码超出范围,返回空列表 return Response({ 'code': 0, 'msg': '获取成功', 'data': { 'list': [], 'has_more': False, 'page': page, 'page_size': page_size, 'current_count': 0 } }) # 获取当前页的记录 chufa_records = list(page_obj) if not chufa_records: # 没有数据直接返回 return Response({ 'code': 0, 'msg': '获取成功', 'data': { 'list': [], 'has_more': False, 'page': page, 'page_size': page_size, 'current_count': 0 } }) # 🔴【核心修复】收集所有需要查询图片的组合 dingdan_ids = [] shangjia_ids = [] for record in chufa_records: if record.OrderID: dingdan_ids.append(record.OrderID) if record.ApplicantID: # 商家ID shangjia_ids.append(record.ApplicantID) # 去重 dingdan_ids = list(set(dingdan_ids)) shangjia_ids = list(set(shangjia_ids)) # 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误) # 1. 查询证据图片(商家上传的) zhengju_mapping = {} if dingdan_ids and shangjia_ids: # 构建Q对象:OrderID在列表中 AND UserID在商家ID列表中 zhengju_q = Q(OrderID__in=dingdan_ids, UserID__in=shangjia_ids) zhengju_queryset = PenaltyEvidenceImage.query.filter(zhengju_q).values('OrderID', 'UserID', 'ImageURL') for item in zhengju_queryset: key = f"{item['OrderID']}_{item['UserID']}" if key not in zhengju_mapping: zhengju_mapping[key] = [] if item['ImageURL']: zhengju_mapping[key].append(item['ImageURL']) # 2. 查询申诉图片(打手上传的) shensu_mapping = {} if dingdan_ids: # 构建Q对象:OrderID在列表中 AND UserID=当前打手ID shensu_q = Q(OrderID__in=dingdan_ids, UserID=yonghuid) shensu_queryset = PenaltyEvidenceImage.query.filter(shensu_q).values('OrderID', 'UserID', 'ImageURL') for item in shensu_queryset: key = f"{item['OrderID']}_{item['UserID']}" if key not in shensu_mapping: shensu_mapping[key] = [] if item['ImageURL']: shensu_mapping[key].append(item['ImageURL']) # 🔴【核心修复】组装返回数据 - 字段保持原样! chufa_list = [] for record in chufa_records: dingdan_id = record.OrderID shangjia_id = record.ApplicantID # 获取证据图片(商家上传的) zhengju_tupian = [] if dingdan_id and shangjia_id: key = f"{dingdan_id}_{shangjia_id}" zhengju_tupian = zhengju_mapping.get(key, []) # 获取申诉图片(打手上传的) shensu_tupian = [] if dingdan_id: key = f"{dingdan_id}_{yonghuid}" shensu_tupian = shensu_mapping.get(key, []) # 🔴【字段保持原样!】与前端完全对应! chufa_data = { 'id': record.id, 'dingdan_id': record.OrderID or '', 'qingqiuid': record.ApplicantID or '', # 商家ID 'cfliyou': record.PenaltyReason or '', 'sqzhuangtai': record.ApplyStatus or 0, 'ssliyou': record.AppealReason or '', 'jifen': record.DeductedPoints or 0, '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) # 🔴🆕【关键】使用事务确保数据一致性 with transaction.atomic(): # 1. 查询处罚记录 try: chufa_record = PenaltyRecord.objects.select_for_update().get( id=chufa_id, PlayerID=yonghuid # 确保只能申诉自己的处罚记录 ) except PenaltyRecord.DoesNotExist: return Response({ 'code': 10, 'msg': '处罚记录不存在或无权申诉', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 2. 验证处罚记录状态(必须是待处理状态0) if chufa_record.ApplyStatus != 0: return Response({ 'code': 11, 'msg': '该处罚记录已处理,无法申诉', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 3. 验证订单是否存在(可选,根据需求决定) # 这里可以根据OrderID去查询订单表,但根据需求描述,我们已经有了处罚记录 # 处罚记录中包含了OrderID,如果OrderID不存在,说明订单可能被删除 # 但根据业务逻辑,有处罚记录就应该有订单,所以这里可以跳过这个检查 # 4. 检查是否已有申诉记录(防止重复申诉) if chufa_record.AppealReason or chufa_record.ApplyStatus == 3: # 检查是否已经有申诉图片 existing_shensu_tupian = PenaltyEvidenceImage.query.filter( OrderID=chufa_record.OrderID, UserID=yonghuid ).exists() if existing_shensu_tupian: return Response({ 'code': 12, 'msg': '您已经提交过申诉,请等待处理结果', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 5. 更新处罚记录 chufa_record.AppealReason = shensu_liyou chufa_record.ApplyStatus = 3 # 状态改为申诉中 chufa_record.save() # 6. 保存申诉图片(如果有) saved_tupian_urls = [] if shensu_tupian_urls: for tupian_url in shensu_tupian_urls: if tupian_url: # 确保URL不为空 chufa_tupian = PenaltyEvidenceImage.query.create( OrderID=chufa_record.OrderID, UserID=yonghuid, ImageURL=tupian_url ) saved_tupian_urls.append(tupian_url) # 7. 构建返回数据 response_data = { 'chufa_id': chufa_record.id, 'dingdan_id': chufa_record.OrderID or '', 'shensu_liyou': shensu_liyou, 'shensu_tupian_urls': saved_tupian_urls, 'sqzhuangtai': chufa_record.ApplyStatus, '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.OrderID}") return Response({ 'code': 0, 'msg': '申诉提交成功', 'data': response_data }) except Exception as e: logger.error(f"打手申诉异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True) return Response({ 'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ShangjiaChufaJiluHuoquView(APIView): """ 商家获取处罚记录接口 路径: /yonghu/sjcfjlhq 方法: POST 权限: JWT Token + 商家权限验证 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 获取当前用户信息 user_main = request.user yonghuid = user_main.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: # 🔴 查询条件:ApplicantID = 当前商家ID stats = PenaltyRecord.query.filter(ApplicantID=yonghuid).aggregate( zongshu=Count('id'), daichuli=Count( Case( When(Q(ApplyStatus=0) | Q(ApplyStatus=3), then=1), output_field=IntegerField() ) ), yichuli=Count( Case( When(Q(ApplyStatus=1) | Q(ApplyStatus=2), then=1), output_field=IntegerField() ) ) ) tongji_data = { 'zongshu': stats.get('zongshu', 0) or 0, 'daichuli': stats.get('daichuli', 0) or 0, 'yichuli': stats.get('yichuli', 0) or 0 } return Response({ 'code': 0, 'msg': '获取统计信息成功', 'data': tongji_data }) except Exception as e: logger.error(f"商家获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}") return Response({ 'code': 1, 'msg': '获取统计信息失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def get_chufa_list(self, yonghuid, request_data): """ 获取处罚记录列表(商家端) 🔴 只修改图片查询逻辑,字段保持原样 """ try: # 获取分页参数 page = int(request_data.get('page', 1)) page_size = int(request_data.get('page_size', 10)) if page_size > 50: page_size = 50 if page_size < 1: page_size = 10 # 获取状态筛选参数 zhuangtai = request_data.get('zhuangtai', 0) # 🔴 查询条件:ApplicantID = 当前商家ID queryset = PenaltyRecord.query.filter(ApplicantID=yonghuid) if zhuangtai == 0: queryset = queryset.filter(Q(ApplyStatus=0) | Q(ApplyStatus=3)) elif zhuangtai == 1: queryset = queryset.filter(Q(ApplyStatus=1) | Q(ApplyStatus=2)) else: pass # 按创建时间倒序排列 queryset = queryset.order_by('-CreateTime') # 分页逻辑 paginator = Paginator(queryset, page_size) try: page_obj = paginator.page(page) except: return Response({ 'code': 0, 'msg': '获取成功', 'data': { 'list': [], 'has_more': False, 'page': page, 'page_size': page_size, 'current_count': 0 } }) chufa_records = list(page_obj) if not chufa_records: return Response({ 'code': 0, 'msg': '获取成功', 'data': { 'list': [], 'has_more': False, 'page': page, 'page_size': page_size, 'current_count': 0 } }) # 🔴【核心修复】收集所有需要查询图片的组合 dingdan_ids = [] dashou_ids = [] for record in chufa_records: if record.OrderID: dingdan_ids.append(record.OrderID) if record.PlayerID: # 打手ID dashou_ids.append(record.PlayerID) # 去重 dingdan_ids = list(set(dingdan_ids)) dashou_ids = list(set(dashou_ids)) # 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误) # 1. 查询证据图片(商家自己上传的) zhengju_mapping = {} if dingdan_ids: # 构建Q对象:OrderID在列表中 AND UserID=当前商家ID zhengju_q = Q(OrderID__in=dingdan_ids, UserID=yonghuid) zhengju_queryset = PenaltyEvidenceImage.query.filter(zhengju_q).values('OrderID', 'UserID', 'ImageURL') for item in zhengju_queryset: key = f"{item['OrderID']}_{item['UserID']}" if key not in zhengju_mapping: zhengju_mapping[key] = [] if item['ImageURL']: zhengju_mapping[key].append(item['ImageURL']) # 2. 查询申诉图片(打手上传的) shensu_mapping = {} if dingdan_ids and dashou_ids: # 构建Q对象:OrderID在列表中 AND UserID在打手ID列表中 shensu_q = Q(OrderID__in=dingdan_ids, UserID__in=dashou_ids) shensu_queryset = PenaltyEvidenceImage.query.filter(shensu_q).values('OrderID', 'UserID', 'ImageURL') for item in shensu_queryset: key = f"{item['OrderID']}_{item['UserID']}" if key not in shensu_mapping: shensu_mapping[key] = [] if item['ImageURL']: shensu_mapping[key].append(item['ImageURL']) # 🔴【核心修复】组装返回数据 - 字段保持原样! chufa_list = [] for record in chufa_records: dingdan_id = record.OrderID dashou_id = record.PlayerID # 获取证据图片(商家自己上传的) zhengju_tupian = [] if dingdan_id: key = f"{dingdan_id}_{yonghuid}" zhengju_tupian = zhengju_mapping.get(key, []) # 获取申诉图片(打手上传的) shensu_tupian = [] if dingdan_id and dashou_id: key = f"{dingdan_id}_{dashou_id}" shensu_tupian = shensu_mapping.get(key, []) # 🔴【字段保持原样!】与前端完全对应! chufa_data = { 'id': record.id, 'dingdan_id': record.OrderID or '', 'qingqiuid': record.PlayerID or '', # 打手ID 'cfliyou': record.PenaltyReason or '', 'sqzhuangtai': record.ApplyStatus or 0, 'ssliyou': record.AppealReason or '', 'jifen': record.DeductedPoints or 0, '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 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. 查询该手机号下所有客服账号(User 字段为 Phone,与 backend 创建客服校验写法一致) user_mains = User.query.filter( Phone=phone, kefu_profile__isnull=False, ).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: kefu = user.kefu_profile # 验证主密码(bcrypt 哈希验证) if not user.CheckPassword(password): continue # 验证二级密码(兼容 bcrypt 哈希和明文) stored_erji = kefu.erjimima or '' if stored_erji.startswith(('$2b$', '$2a$')) and len(stored_erji) == 60: import bcrypt as _bcrypt if not _bcrypt.checkpw(erjimima.encode('utf-8'), stored_erji.encode('utf-8')): continue elif stored_erji != erjimima: continue # 如果账号被禁用,记录但继续查找(可能还有其他正常账号?) if kefu.zhuangtai != 1: # 如果找到被禁用的,记录下来,但继续查找是否有正常账号 if target_user is None: # 暂时记录这个禁用账号,但继续遍历 target_user = user target_kefu = kefu continue # 找到完全匹配且状态正常的账号,立即使用 target_user = user target_kefu = kefu break else: # 循环结束未找到正常账号,但可能找到了禁用账号 if target_user and target_kefu: # 只有被禁用的账号匹配,返回封禁提示 logger.warning(f"登录失败,客服账号已禁用: {phone}, IP: {client_ip}") return Response({ 'code': 4, 'msg': '账号已被封禁', 'data': None }, status=status.HTTP_403_FORBIDDEN) else: # 没有任何账号匹配(密码/二级密码错误) logger.warning(f"登录失败,密码或二级密码错误: {phone}, IP: {client_ip}") return Response({ 'code': 1, 'msg': '登录失败', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 5. 更新IP和最后登录时间 target_user.IP = client_ip target_user.UserLastLoginDate = timezone.now() target_user.save(update_fields=['IP', 'UserLastLoginDate']) # 6. 生成JWT token refresh = RefreshToken.for_user(target_user) token = str(refresh.access_token) # 7. 返回成功数据 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) 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.query.all().only('id', 'jieshao', 'tupian_url') type_list = [ { 'id': t.id, 'biaoti': t.jieshao or '', # 假设商品类型的标题字段为 jieshao 'tupian_url': t.tupian_url or '' } for t in types_qs ] return Response({'code': 0, 'data': type_list}) except Exception as e: logger.error(f"查询商品类型表失败: {str(e)}") # 表不存在或查询失败,返回空列表 return Response({'code': 0, 'data': []}) class KefuGetOrderListView(APIView): """ 客服获取平台订单列表接口(发单平台=1,非跨平台) 需要权限:002ab(平台订单管理) 请求参数: username: 客服账号(用于权限校验) page: 页码 pageSize: 每页条数 dingdan_id: 订单ID(精确搜索) laoban_id: 老板ID(搜索平台扩展表中的老板) dashou_id: 打手ID(搜索主表中的接单打手) leixing: 商品类型ID zhuangtai: 状态(多个用逗号分隔,如 "1,7") date: 日期(YYYY-MM-DD),筛选订单创建时间 nicheng: 游戏昵称(订单主表的 nicheng 字段) jieshao: 订单介绍(订单主表的 jieshao 字段) clkf: 处理客服(订单主表的 clkf 字段) """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): start_total = time.time() # 1. 获取前端参数 username = request.data.get('phone', '').strip() if not username: return Response({'code': 400, 'msg': '缺少username'}) # 2. 公共权限校验 kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 3. 检查平台订单管理权限(002ab) if '002ab' not in permissions: return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) # 4. 获取分页和筛选参数 page = int(request.data.get('page', 1)) page_size = int(request.data.get('pageSize', 20)) dingdan_id = request.data.get('dingdan_id', '').strip() laoban_id = request.data.get('laoban_id', '').strip() dashou_id = request.data.get('dashou_id', '').strip() leixing = request.data.get('leixing') zhuangtai = request.data.get('zhuangtai', '') date = request.data.get('date', '').strip() # 新增筛选条件 nicheng = request.data.get('nicheng', '').strip() jieshao = request.data.get('jieshao', '').strip() clkf = request.data.get('clkf', '').strip() # ---------- 查询条件 ---------- # 基础条件:平台发单 + 非跨平台 base_q = Q(Platform=1) if dingdan_id: q_conditions = base_q & Q(OrderID=dingdan_id) else: q_conditions = base_q if leixing is not None: try: leixing_int = int(leixing) q_conditions &= Q(ProductTypeID=leixing_int) except (ValueError, TypeError): pass if zhuangtai and zhuangtai != 'all': try: status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()] if status_list: q_conditions &= Q(Status__in=status_list) except (ValueError, TypeError): pass if dashou_id: q_conditions &= Q(PlayerID=dashou_id) if laoban_id: q_conditions &= Q(pingtai_kuozhan__BossID=laoban_id) if date: q_conditions &= Q(CreateTime__date=date) # 新增筛选条件 if nicheng: q_conditions &= Q(Nickname__icontains=nicheng) if jieshao: q_conditions &= Q(Description__icontains=jieshao) if clkf: q_conditions &= Q(AssignedCS__icontains=clkf) # ---------- 统计(仅按发单平台,非跨平台) ---------- stats = Order.query.filter(Platform=1).aggregate( total_orders=Count('id'), completed_orders=Count('id', filter=Q(Status=3)), refund_orders=Count('id', filter=Q(Status=5)), pending_orders=Count('id', filter=Q(Status__in=[4,8])) ) # ---------- 状态计数(所有状态都返回,非跨平台) ---------- status_counts = { 'all': Order.query.filter(Platform=1).count(), '1,7': Order.query.filter(Platform=1, Status__in=[1,7]).count(), '2': Order.query.filter(Platform=1, Status=2).count(), '8': Order.query.filter(Platform=1, Status=8).count(), '3': Order.query.filter(Platform=1, Status=3).count(), '4': Order.query.filter(Platform=1, Status=4).count(), '5': Order.query.filter(Platform=1, Status=5).count(), '6': Order.query.filter(Platform=1, Status=6).count(), } # ---------- 分页查询 ---------- total_count = Order.query.filter(q_conditions).count() orders_qs = Order.query.filter(q_conditions).select_related( 'pingtai_kuozhan' ).only( 'OrderID', 'Description', 'Status', 'ProductTypeID', 'Platform', 'Amount', 'ImageURL', 'CreateTime', 'Nickname', 'PlayerID', 'pingtai_kuozhan__BossID' ).order_by('-CreateTime')[(page-1)*page_size : page*page_size] # ---------- 获取老板昵称 ---------- laoban_ids = [o.pingtai_kuozhan.BossID for o in orders_qs if hasattr(o, 'pingtai_kuozhan') and o.pingtai_kuozhan.BossID] nickname_map = {} if laoban_ids: users = User.query.filter(UserUID__in=laoban_ids).select_related('boss_profile').only( 'UserUID', '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.BossID if ext else '' youxi_nicheng = nickname_map.get(laoban_id, order.Nickname or '') result_list.append({ 'dingdan_id': order.OrderID or '', 'jieshao': order.Description or '', 'zhuangtai': order.Status, 'leixing': order.ProductTypeID, 'fadanpingtai': order.Platform, 'jiage': float(order.Amount) if order.Amount else 0.00, 'tupian_url': order.ImageURL or '', 'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '', 'youxi_nicheng': youxi_nicheng }) response_data = { 'stats': { 'totalOrders': stats['total_orders'] or 0, 'completedOrders': stats['completed_orders'] or 0, 'refundOrders': stats['refund_orders'] or 0, 'pendingOrders': stats['pending_orders'] or 0 }, 'statusCounts': status_counts, 'list': result_list, 'total': total_count, 'page': page, 'pageSize': page_size } logger.info(f"平台订单接口总耗时: {time.time()-start_total:.3f}s") return Response({'code': 0, 'data': response_data}) class KefuGetShangjiaOrderListView(APIView): """ 客服获取商家订单列表接口(发单平台=2,非跨平台) 需要权限:002ac(商家订单管理)—— 如不区分可使用 002ab 请求参数: username: 客服账号(用于权限校验) page: 页码 pageSize: 每页条数 dingdan_id: 订单ID(精确搜索) shangjia_id: 商家ID(搜索商家扩展表中的商家ID) dashou_id: 打手ID(搜索主表中的接单打手) leixing: 商品类型ID zhuangtai: 状态(多个用逗号分隔,如 "1,7") date: 日期(YYYY-MM-DD),筛选订单创建时间 nicheng: 游戏昵称(订单主表的 nicheng 字段) jieshao: 订单介绍(订单主表的 jieshao 字段) clkf: 处理客服(订单主表的 clkf 字段) shangjia_nicheng: 商家昵称(商家扩展表中的昵称) """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): start_total = time.time() # 1. 获取前端参数 username = request.data.get('username', '').strip() if not username: return Response({'code': 400, 'msg': '缺少username'}) # 2. 公共权限校验 kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 3. 检查商家订单管理权限(使用 002ac,如果没有可改用 002ab) if '002ac' not in permissions: return Response({'code': 403, 'msg': '您无权查看商家订单列表'}) # 4. 获取分页和筛选参数 page = int(request.data.get('page', 1)) page_size = int(request.data.get('pageSize', 20)) dingdan_id = request.data.get('dingdan_id', '').strip() shangjia_id = request.data.get('shangjia_id', '').strip() dashou_id = request.data.get('dashou_id', '').strip() leixing = request.data.get('leixing') zhuangtai = request.data.get('zhuangtai', '') date = request.data.get('date', '').strip() # 新增筛选条件 nicheng = request.data.get('nicheng', '').strip() jieshao = request.data.get('jieshao', '').strip() clkf = request.data.get('clkf', '').strip() shangjia_nicheng = request.data.get('shangjia_nicheng', '').strip() # ---------- 查询条件 ---------- # 基础条件:商家发单 + 非跨平台 base_q = Q(Platform=2) if dingdan_id: q_conditions = base_q & Q(OrderID=dingdan_id) else: q_conditions = base_q if leixing is not None: try: leixing_int = int(leixing) q_conditions &= Q(ProductTypeID=leixing_int) except (ValueError, TypeError): pass if zhuangtai and zhuangtai != 'all': try: status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()] if status_list: q_conditions &= Q(Status__in=status_list) except (ValueError, TypeError): pass if dashou_id: q_conditions &= Q(PlayerID=dashou_id) if shangjia_id: q_conditions &= Q(shangjia_kuozhan__MerchantID=shangjia_id) if date: q_conditions &= Q(CreateTime__date=date) if nicheng: q_conditions &= Q(Nickname__icontains=nicheng) if jieshao: q_conditions &= Q(Description__icontains=jieshao) if clkf: q_conditions &= Q(AssignedCS__icontains=clkf) if shangjia_nicheng: # 通过商家扩展表关联 UserShangjia 的昵称 q_conditions &= Q(shangjia_kuozhan__MerchantID__in=User.query.filter( shop_profile__nicheng__icontains=shangjia_nicheng ).values_list('UserUID', flat=True)) # ---------- 统计(仅按发单平台,非跨平台) ---------- stats = Order.query.filter(Platform=2).aggregate( total_orders=Count('id'), completed_orders=Count('id', filter=Q(Status=3)), refund_orders=Count('id', filter=Q(Status=5)), pending_orders=Count('id', filter=Q(Status__in=[4,8])) ) # ---------- 状态计数(所有状态都返回,非跨平台) ---------- status_counts = { 'all': Order.query.filter(Platform=2).count(), '1,7': Order.query.filter(Platform=2, Status__in=[1,7]).count(), '2': Order.query.filter(Platform=2, Status=2).count(), '8': Order.query.filter(Platform=2, Status=8).count(), '3': Order.query.filter(Platform=2, Status=3).count(), '4': Order.query.filter(Platform=2, Status=4).count(), '5': Order.query.filter(Platform=2, Status=5).count(), '6': Order.query.filter(Platform=2, Status=6).count(), } # ---------- 分页查询 ---------- total_count = Order.query.filter(q_conditions).count() orders_qs = Order.query.filter(q_conditions).select_related( 'shangjia_kuozhan' ).only( 'OrderID', 'Description', 'Status', 'ProductTypeID', 'Platform', 'Amount', 'ImageURL', 'CreateTime', 'Nickname', 'PlayerID', 'shangjia_kuozhan__MerchantID' ).order_by('-CreateTime')[(page-1)*page_size : page*page_size] # ---------- 获取商家昵称 ---------- shangjia_ids = [o.shangjia_kuozhan.MerchantID for o in orders_qs if hasattr(o, 'shangjia_kuozhan') and o.shangjia_kuozhan.MerchantID] nickname_map = {} if shangjia_ids: users = User.query.filter(UserUID__in=shangjia_ids).select_related('shop_profile').only( 'UserUID', '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.MerchantID if ext else '' youxi_nicheng = order.Nickname or '' # 商家订单的玩家昵称 result_list.append({ 'dingdan_id': order.OrderID or '', 'jieshao': order.Description or '', 'zhuangtai': order.Status, 'leixing': order.ProductTypeID, 'fadanpingtai': order.Platform, 'jiage': float(order.Amount) if order.Amount else 0.00, 'tupian_url': order.ImageURL or '', 'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '', 'youxi_nicheng': youxi_nicheng }) response_data = { 'stats': { 'totalOrders': stats['total_orders'] or 0, 'completedOrders': stats['completed_orders'] or 0, 'refundOrders': stats['refund_orders'] or 0, 'pendingOrders': stats['pending_orders'] or 0 }, 'statusCounts': status_counts, 'list': result_list, 'total': total_count, 'page': page, 'pageSize': page_size } logger.info(f"商家订单接口总耗时: {time.time()-start_total:.3f}s") return Response({'code': 0, 'data': response_data}) class KefuChangeDashouView(APIView): """ 客服更换打手接口 请求:POST /yonghu/kefughds 参数:{ "phone": "客服手机号", "dingdan_id": "订单ID", "new_dashou_id": "新打手ID" } 认证:JWT 返回:code=0 成功 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() new_dashou_id = request.data.get('new_dashou_id', '').strip() if not phone or not dingdan_id or not new_dashou_id: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 客服身份验证 current_user = request.user if getattr(current_user, 'phone', '') != phone: logger.warning(f"手机号不匹配: {phone}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.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 = Order.query.get(OrderID=dingdan_id) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) # 4. 验证订单状态是否允许更换打手(进行中2 或 结算中8) if order.Status not in [2, 8]: return Response({'code': 400, 'msg': '订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST) # 5. 查询新打手 try: new_dashou_user = User.query.get(UserUID=new_dashou_id) except User.DoesNotExist: return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND) # 6. 验证打手扩展表 try: new_dashou_profile = new_dashou_user.dashou_profile except ObjectDoesNotExist: return Response({'code': 404, 'msg': '打手信息不完整'}, status=status.HTTP_404_NOT_FOUND) # 7. 验证打手状态(打手状态和账号状态必须为1) if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1: return Response({'code': 400, 'msg': '打手状态不允许接单'}, status=status.HTTP_400_BAD_REQUEST) # 8. 验证打手积分是否足够(至少5分) if new_dashou_profile.jifen < 5: return Response({'code': 400, 'msg': '打手积分不足,无法接单'}, status=status.HTTP_400_BAD_REQUEST) # 8. 根据订单的抢单要求类型进行额外验证 yaoqiuleixing = order.GrabRequirement if yaoqiuleixing == 1: # 会员抢单 huiyuan_id = order.MembershipID if not huiyuan_id: return Response({'code': 400, 'msg': '订单会员ID为空'}, status=status.HTTP_400_BAD_REQUEST) try: # 查询打手的会员购买记录 huiyuan_goumai = Huiyuangoumai.query.get( huiyuan_id=huiyuan_id, yonghu_id=new_dashou_id # 假设 yonghu_id 字段存储用户ID ) except Huiyuangoumai.DoesNotExist: return Response({'code': 400, 'msg': '打手未购买该订单要求的会员'}, status=status.HTTP_400_BAD_REQUEST) # 检查会员是否过期(调用模型方法) is_expired = huiyuan_goumai.jiance_shifou_daoqi() if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1: return Response({'code': 400, 'msg': '打手的会员已过期'}, status=status.HTTP_400_BAD_REQUEST) elif yaoqiuleixing == 2: # 押金抢单 order_yajin = order.CommissionReq # 假设订单的 yongjin 字段存储押金要求 if order_yajin is None: return Response({'code': 400, 'msg': '订单押金为空'}, status=status.HTTP_400_BAD_REQUEST) if new_dashou_profile.yajin < order_yajin: return Response({'code': 400, 'msg': '打手押金不足'}, status=status.HTTP_400_BAD_REQUEST) else: return Response({'code': 400, 'msg': '订单抢单要求类型错误'}, status=status.HTTP_400_BAD_REQUEST) # 9. 所有验证通过,开始更换打手(使用事务) with transaction.atomic(): # 9.1 更新新打手扩展表 new_dashou_profile.zhuangtai = 0 # 设为接单中 new_dashou_profile.jiedanzongliang += 1 new_dashou_profile.jinrijiedan += 1 new_dashou_profile.jinyuejiedan += 1 new_dashou_profile.save() # 9.2 处理原打手(如果存在) old_dashou_id = order.PlayerID if old_dashou_id: try: old_dashou_user = User.query.get(UserUID=old_dashou_id) old_dashou_profile = old_dashou_user.dashou_profile old_dashou_profile.zhuangtai = 1 # 恢复空闲 old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1(业务逻辑) old_dashou_profile.save() logger.info(f"原打手 {old_dashou_id} 状态已更新") except (User.DoesNotExist, ObjectDoesNotExist): logger.warning(f"原打手 {old_dashou_id} 不存在或信息不完整,跳过更新") except Exception as e: logger.error(f"更新原打手时异常: {str(e)}", exc_info=True) # 9.3 更新订单 order.PlayerID = new_dashou_id order.Status = 2 # 进行中 order.AssignedCS = current_user.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 = Order.query.get(OrderID=dingdan_id) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) # 4. 验证订单状态是否为9(未付款) if order.Status != 9: return Response({'code': 400, 'msg': '订单状态不允许恢复'}, status=status.HTTP_400_BAD_REQUEST) # 5. 根据是否有指定ID决定新状态 if order.AssignedID: new_status = 7 # 指定中 else: new_status = 1 # 下单中 # 6. 更新订单 order.Status = new_status order.AssignedCS = current_user.yonghuid order.save() return Response({'code': 0, 'msg': '恢复成功', 'data': {'new_status': new_status}}) # dingdan/views.py class KefuPlatformRefundView(APIView): """ 客服平台订单退款接口(调用微信支付) 请求:POST /yonghu/kefupttk 参数:{ "phone": "客服手机号", "dingdan_id": "订单ID", "tuikuan_liyou": "退款理由(可选)" } 认证:JWT 返回:code=0 成功,code=其他 失败 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() if not phone or not dingdan_id: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 客服身份验证 current_user = request.user if getattr(current_user, 'phone', '') != phone: logger.warning(f"手机号不匹配: {phone}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.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 = Order.query.select_related('pingtai_kuozhan').select_for_update().get(OrderID=dingdan_id) # 4. 验证平台订单 if order.Platform != 1: return Response({'code': 400, 'msg': '该订单不是平台订单'}, status=status.HTTP_400_BAD_REQUEST) # 5. 验证状态允许退款(根据模型,状态值:1下单中,2进行中,3已完成,4退款审核,5已退款,6退款失败,7指定中,8结算中) allowed_statuses = [1, 2, 4, 7, 8] # 允许退款的已支付或处理中状态 if order.Status not in allowed_statuses: return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST) # 6. 获取微信交易号(如果存在) wechat_transaction_id = getattr(order, 'WechatTransactionID', None) # 7. 调用微信退款接口 refund_result = self.call_wechat_refund( dingdan_id=order.OrderID, jine=order.Amount, transaction_id=wechat_transaction_id ) if refund_result['code'] != 0: # 微信退款失败,直接返回错误,数据库不更新 logger.error(f"微信退款失败,订单 {dingdan_id},原因:{refund_result['msg']}") return Response({'code': 400, 'msg': refund_result['msg']}, status=status.HTTP_400_BAD_REQUEST) # 8. 更新订单状态为5(已退款) order.Status = 5 order.AssignedCS = current_user.phone # 记录处理客服手机号 if tuikuan_liyou: order.RefundReason = tuikuan_liyou order.save() # 9. 更新打手数据 jiedan_dashou_id = order.PlayerID if jiedan_dashou_id: try: dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.tuikuanliang += 1 dashou_profile.zhuangtai = 1 # 打手状态设为空闲 dashou_profile.save() logger.info(f"平台退款:打手 {jiedan_dashou_id} 退款订单+1") except (User.DoesNotExist, ObjectDoesNotExist): logger.warning(f"平台退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新") # ========== 新增:商品和店铺每日统计 ========== try: # 商品每日统计(只要有商品ID就执行) if order.ProductID: # 从扩展表获取已计算好的收益值(下单时已存储) pingtai_shouyi = Decimal('0.00') dianpu_shouyi = Decimal('0.00') if hasattr(order, 'pingtai_kuozhan'): pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00') dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_shangpin_daily_stat( shangpin_id=order.ProductID, caozuo_leixing=3, # 3 = 退款 dingdan_jiage=order.Amount, pingtai_shouyi=pingtai_shouyi, dianpu_zongshouyi=dianpu_shouyi ) logger.info(f"商品统计更新完成,商品ID: {order.ProductID}") # 店铺每日统计(仅当存在店铺ID时执行) dianpu_id = None if hasattr(order, 'pingtai_kuozhan'): dianpu_id = order.pingtai_kuozhan.ShopID if dianpu_id: dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_dianpu_daily_stat( dianpu_id=dianpu_id, caozuo_leixing=3, # 3 = 退款 dingdan_jiage=order.Amount, dianpu_shouyi=dianpu_shouyi ) logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") except Exception as e: logger.error(f"商品/店铺统计更新失败: {e}") logger.error(traceback.format_exc()) # 统计失败不影响主流程 # ============================================= # 10. 更新退款记录表(假设存在 RefundRecord 模型,字段需根据实际调整) #更新支出记录 update_daily_payout(order.Amount) try: refund_record, created = RefundRecord.query.get_or_create( OrderID=dingdan_id, defaults={ 'PlayerID': jiedan_dashou_id or '', 'ApplicantID': '', 'ProcessorID': phone, 'Reason': tuikuan_liyou or '客服退款', 'ApplyStatus': 1, # 成功 'Amount': order.Amount, 'PlayerCommission': order.PlayerCommission or 0, 'Description': order.Description or '', 'Remark': '客服平台退款', 'Nickname': order.Nickname or '', } ) if not created: refund_record.ApplyStatus = 1 refund_record.ProcessorID = phone if tuikuan_liyou: refund_record.Reason = tuikuan_liyou refund_record.save() except Exception as e: logger.error(f"平台退款更新退款记录异常: {str(e)}", exc_info=True) # 11. 更新客服统计数据 kefu.jinrichuli += 1 kefu.jinyuechuli += 1 kefu.zongchuli += 1 kefu.save() # 🔥 新增:调用打手每日统计(成交) try: update_dashou_daily_by_action( yonghuid=order.PlayerID, amount=order.PlayerCommission, action=3 # 3 表示退款(退款) ) logger.info(f"打手 {order.PlayerID} 每日统计更新成功") except Exception as e: logger.error(f"打手每日统计更新失败: {e}") # 不影响主流程 logger.info(f"平台退款成功,订单 {dingdan_id},退款单号 {refund_result.get('refund_id')}") return Response({'code': 0, 'msg': '退款成功'}) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: logger.error(f"平台退款接口异常: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def call_wechat_refund(self, dingdan_id, jine, transaction_id=None): """ 调用微信支付退款接口 :param dingdan_id: 商户订单号(我们的订单ID) :param jine: 退款金额 :param transaction_id: 微信支付订单号(优先使用) :return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'} """ # 获取配置 appid = getattr(settings, 'WEIXIN_APPID', '') mch_id = getattr(settings, 'WEIXIN_MCHID', '') key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '') key_path = getattr(settings, 'WEIXIN_KEY_PATH', '') if not all([appid, mch_id, key, cert_path, key_path]): missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v] logger.error(f"微信支付配置缺失: {missing}") return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'} # 生成退款单号 out_refund_no = self.generate_refund_no(dingdan_id) total_fee = int(float(jine) * 100) refund_fee = total_fee refund_desc = '客服退款' # 构造请求参数 params = { 'appid': appid, 'mch_id': mch_id, 'nonce_str': self.generate_nonce_str(), 'out_refund_no': out_refund_no, 'total_fee': total_fee, 'refund_fee': refund_fee, 'refund_desc': refund_desc, } if transaction_id: params['transaction_id'] = transaction_id else: params['out_trade_no'] = dingdan_id # 使用订单ID作为商户订单号 # 生成签名 sorted_keys = sorted(params.keys()) stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys]) stringSignTemp = f"{stringA}&key={key}" sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() params['sign'] = sign xml_data = self.dict_to_xml(params) url = 'https://api.mch.weixin.qq.com/secapi/pay/refund' try: response = requests.post( url, data=xml_data, cert=(cert_path, key_path), timeout=10 ) result = xmltodict.parse(response.content)['xml'] logger.info(f"微信退款响应: {result}") except Exception as e: logger.error(f"微信退款请求异常: {str(e)}", exc_info=True) return {'code': 500, 'msg': '微信退款请求异常'} if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': return {'code': 0, 'msg': 'success', 'refund_id': out_refund_no} else: err_msg = result.get('err_code_des', result.get('return_msg', '未知错误')) logger.warning(f"微信退款失败: {err_msg}") return {'code': 400, 'msg': err_msg} def generate_nonce_str(self): return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) def generate_refund_no(self, dingdan_id): timestamp = str(int(time.time())) rand = ''.join(random.choices('0123456789', k=4)) return f"{dingdan_id}REF{timestamp}{rand}" def dict_to_xml(self, data): xml = [''] for k, v in data.items(): xml.append(f'<{k}>{v}') xml.append('') return ''.join(xml) # dingdan/views.py class KefuRejectRefundView(APIView): """ 客服拒绝退款接口 请求:POST /yonghu/kefujjtk 参数:{ "phone": "客服手机号", "dingdan_id": "订单ID", "jujue_liyou": "拒绝理由(可选)" } 认证:JWT 返回:code=0 成功 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() jujue_liyou = request.data.get('jujue_liyou', '').strip() if not phone or not dingdan_id: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 客服身份验证 current_user = request.user if getattr(current_user, 'phone', '') != phone: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.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 = Order.objects.select_for_update().get(OrderID=dingdan_id) # 4. 验证订单状态为退款审核中(4) if order.Status != 4: return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST) # 获取必要字段 jiedan_dashou_id = order.PlayerID dashou_fencheng = order.PlayerCommission fadan_pingtai = order.Platform # 5. 验证打手分成金额是否存在 if dashou_fencheng is None: return Response({'code': 400, 'msg': '订单缺少打手分成金额'}, status=status.HTTP_400_BAD_REQUEST) # 6. 更新订单状态为退款失败(6) order.Status = 6 order.AssignedCS = current_user.phone # 记录处理客服手机号 if jujue_liyou: order.RefundReason = jujue_liyou order.save() # 7. 给打手结算(返还分成) if jiedan_dashou_id: try: dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_profile = dashou_user.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 (User.DoesNotExist, AttributeError): logger.warning(f"拒绝退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算") # 8. 如果是商家发单,更新商家成交订单数量 if fadan_pingtai == 2: try: # 获取商家订单扩展表 shangjia_kuozhan = order.shangjia_kuozhan shangjia_id = shangjia_kuozhan.MerchantID if shangjia_id: shangjia_user = User.query.get(UserUID=shangjia_id) shangjia_profile = shangjia_user.shop_profile shangjia_profile.chengjiao += 1 shangjia_profile.save() logger.info(f"拒绝退款:商家 {shangjia_id} 成交订单+1") # ✅ 新方法(行为驱动,更安全) update_shangjia_daily( yonghuid=shangjia_id, amount=Decimal(str(order.Amount)), action=2 # 2 = 结算 ) except (ObjectDoesNotExist, User.DoesNotExist, AttributeError): logger.warning(f"拒绝退款:商家信息不存在,跳过商家更新") # 9. 更新退款记录表 try: refund_record = RefundRecord.query.get(OrderID=dingdan_id) refund_record.ApplyStatus = 2 # 2表示拒绝 refund_record.ProcessorID = phone refund_record.RejectReason = jujue_liyou # 记录驳回理由(假设模型有此字段) refund_record.save() logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}") except RefundRecord.DoesNotExist: # 如果退款记录不存在,可以创建一条记录(可选) RefundRecord.query.create( OrderID=dingdan_id, PlayerID=jiedan_dashou_id or '', ApplicantID='', ProcessorID=phone, Reason=jujue_liyou or '客服拒绝退款', ApplyStatus=2, Amount=order.Amount, PlayerCommission=dashou_fencheng or 0, Description=order.Description or '', Remark='客服拒绝退款', Nickname=order.Nickname or '', RejectReason=jujue_liyou or '', ) except Exception as e: logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True) # 🔥 新增:调用打手每日统计(成交) try: update_dashou_daily_by_action( yonghuid=order.PlayerID, amount=dashou_fencheng, action=2 # 2 表示成交(结算) ) logger.info(f"打手 {order.PlayerID} 每日统计更新成功") except Exception as e: logger.error(f"打手每日统计更新失败: {e}") # 不影响主流程 # 10. 更新客服统计数据 kefu.jinrichuli += 1 kefu.jinyuechuli += 1 kefu.zongchuli += 1 kefu.save() return Response({'code': 0, 'msg': '拒绝退款成功'}) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class KefuMerchantRefundView(APIView): """ 客服商家订单退款接口(发单平台=2) 请求:POST /yonghu/kefusjtk 参数:{ "phone": "客服手机号", "dingdan_id": "订单ID", "tuikuan_liyou": "退款理由(可选)" } 认证:JWT 返回:code=0 成功 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip() if not phone or not dingdan_id: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 客服身份验证(同前,省略以节省篇幅,但实际必须完整) current_user = request.user # ... 验证 if getattr(current_user, 'phone', '') != phone: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.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 = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) # 验证商家订单 if order.Platform != 2: return Response({'code': 400, 'msg': '该订单不是商家订单'}, status=status.HTTP_400_BAD_REQUEST) # 验证状态允许退款 allowed_statuses = [1, 7, 2, 8, 4] if order.Status not in allowed_statuses: return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST) # 获取必要数据 jiedan_dashou_id = order.PlayerID jine = order.Amount if jine is None: return Response({'code': 400, 'msg': '订单金额缺失'}, status=status.HTTP_400_BAD_REQUEST) try: shangjia_kuozhan = order.shangjia_kuozhan shangjia_id = shangjia_kuozhan.MerchantID except ObjectDoesNotExist: return Response({'code': 400, 'msg': '订单商家信息不存在'}, status=status.HTTP_400_BAD_REQUEST) with transaction.atomic(): # 更新订单 order.Status = 5 order.AssignedCS = current_user.phone if tuikuan_liyou: order.RefundReason = tuikuan_liyou order.save() # 更新打手 if jiedan_dashou_id: try: dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.tuikuanliang += 1 dashou_profile.zhuangtai = 1 dashou_profile.save() except (User.DoesNotExist, ObjectDoesNotExist): logger.warning(f"商家退款:打手 {jiedan_dashou_id} 不存在,跳过更新") # 更新商家 try: shangjia_user = User.query.get(UserUID=shangjia_id) shangjia_profile = shangjia_user.shop_profile shangjia_profile.tuikuan += 1 shangjia_profile.yue += jine shangjia_profile.save() except (User.DoesNotExist, ObjectDoesNotExist): logger.warning(f"商家退款:商家 {shangjia_id} 不存在,跳过更新") # 更新退款记录表 try: refund_record = RefundRecord.query.get(OrderID=dingdan_id) refund_record.ApplyStatus = 1 refund_record.ProcessorID = phone refund_record.save() except RefundRecord.DoesNotExist: pass # 更新客服统计 update_shangjia_daily( yonghuid=shangjia_id, amount=Decimal(str(order.Amount)), action=3 # 3 = 退款 ) # 调用打手每日统计(成交) try: update_dashou_daily_by_action( yonghuid=order.PlayerID, amount=order.PlayerCommission, action=3 # 3 表示退款(退款) ) logger.info(f"打手 {order.PlayerID} 每日统计更新成功") except Exception as e: logger.error(f"打手每日统计更新失败: {e}") # 不影响主流程 kefu = current_user.kefu_profile kefu.jinrichuli += 1 kefu.jinyuechuli += 1 kefu.zongchuli += 1 kefu.save() return Response({'code': 0, 'msg': '退款成功'}) class KefuRejectSettlementView(APIView): """ 客服拒绝结算接口(仅平台订单,状态2/8) 请求:POST /yonghu/kefujjjs 参数:{ "phone": "客服手机号", "dingdan_id": "订单ID", "tkly": "拒绝结算理由(必填)" } 认证:JWT 返回:code=0 成功,其他失败 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() tkly = request.data.get('tkly', '').strip() if not phone or not dingdan_id or not tkly: return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 客服身份验证 current_user = request.user if getattr(current_user, 'phone', '') != phone: logger.warning(f"手机号不匹配: {phone}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.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 = Order.query.select_related('pingtai_kuozhan').get(OrderID=dingdan_id) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) # 4. 校验订单条件:平台发单(1)且状态为2或8 if order.Platform != 1 or order.Status not in [2, 8]: return Response({'code': 400, 'msg': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算'}, status=status.HTTP_400_BAD_REQUEST) jiedan_dashou_id = order.PlayerID if not jiedan_dashou_id: return Response({'code': 400, 'msg': '订单暂无接单打手'}, status=status.HTTP_400_BAD_REQUEST) # 5. 使用事务更新 with transaction.atomic(): # 5.1 更新订单主表 order.Status = 5 # 已退款 order.RefundReason = tkly # 拒绝理由 order.AssignedCS = phone # 记录客服手机号 order.save() # 5.2 更新平台订单扩展表:标记为拒绝结算(假设字段 jjjs_biaoshi) # 注意:需要确保 PlatformOrderExt 模型中已添加 jjjs_biaoshi 字段,IntegerField,默认0 if hasattr(order, 'pingtai_kuozhan') and order.pingtai_kuozhan: pingtai_ext = order.pingtai_kuozhan pingtai_ext.RejectSettlementFlag = 1 # 1表示拒绝结算 pingtai_ext.save() else: # 理论上应该存在,但若不存在则记录日志 logger.warning(f"订单 {dingdan_id} 缺少平台扩展表,无法标记拒绝结算") # 5.3 更新打手扩展表 try: dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.zhuangtai = 1 # 打手状态设为空闲 dashou_profile.tuikuanliang += 1 # 退款订单总量+1 dashou_profile.save() logger.info(f"拒绝结算:打手 {jiedan_dashou_id} 退款订单+1,状态空闲") except (User.DoesNotExist, ObjectDoesNotExist): logger.warning(f"拒绝结算:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新") except Exception as e: logger.error(f"拒绝结算更新打手异常: {str(e)}", exc_info=True) # 5.4 更新客服扩展表统计数据 # 🔥 新增:调用打手每日统计(成交) try: update_dashou_daily_by_action( yonghuid=order.PlayerID, amount=order.PlayerCommission, action=3 # 3 表示退款(退款) ) logger.info(f"打手 {order.PlayerID} 每日统计更新成功") except Exception as e: logger.error(f"打手每日统计更新失败: {e}") # 不影响主流程 kefu.jinrichuli += 1 kefu.jinyuechuli += 1 kefu.zongchuli += 1 kefu.save() return Response({'code': 0, 'msg': '拒绝结算成功'}) class KefuTransferHallView(APIView): """ 客服恢复大厅(转移大厅)接口 请求:POST /yonghu/kefuzydt 参数:{ "phone": "客服手机号", "dingdan_id": "订单ID" } 认证:JWT 返回:code=0 成功 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() if not phone or not dingdan_id: return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 客服身份验证(同前,略,但必须完整实现) current_user = request.user if getattr(current_user, 'phone', '') != phone: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.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 = Order.query.get(OrderID=dingdan_id) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) # 4. 校验订单状态(允许状态:2,8,4) if order.Status not in [2, 8, 4]: return Response({'code': 400, 'msg': '订单状态不允许转移大厅'}, status=status.HTTP_400_BAD_REQUEST) if order.PlayerID: UserDashou.query.filter(user__UserUID=order.PlayerID).update(zhuangtai=1) else: pass # 5. 根据是否有指定ID决定新状态 if order.AssignedID: new_status = 7 # 指定中 else: new_status = 1 # 下单中 # 6. 使用事务更新 with transaction.atomic(): # 清空接单打手ID,更新状态,记录客服 order.PlayerID = None order.Status = new_status order.AssignedCS = phone order.save() return Response({'code': 0, 'msg': '转移大厅成功'}) class KefuCancelDesignationView(APIView): """ 客服取消指定接口(仅状态7) 请求:POST /yonghu/kefuqxzd 参数:{ "phone": "客服手机号", "dingdan_id": "订单ID" } 认证:JWT 返回:code=0 成功 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() if not phone or not dingdan_id: return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 客服身份验证(同前,略,但必须完整实现) current_user = request.user if getattr(current_user, 'phone', '') != phone: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.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 = Order.query.get(OrderID=dingdan_id) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) # 4. 校验订单状态为7(指定中) if order.Status != 7: print(999) return Response({'code': 400, 'msg': '仅状态为指定中(7)的订单可取消指定'}, status=status.HTTP_400_BAD_REQUEST) # 5. 使用事务更新 with transaction.atomic(): # 状态改为1(下单中),清空指定ID,记录客服 order.Status = 1 order.AssignedID = None order.AssignedCS = phone order.save() return Response({'code': 0, 'msg': '取消指定成功'}) class KefuGetDashouListView(APIView): """ 客服获取打手列表接口(仅打手类型) 请求:POST /yonghu/kefuhqdslb 参数:{ "phone": "客服手机号", "keyword": "搜索关键词(可选,搜索用户ID或昵称)", "page": "页码(从1开始)", "page_size": "每页数量" } 认证:JWT 返回:{ "code": 0, "data": { "list": [打手对象], "has_more": true/false } } """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 phone = request.data.get('zhanghao', '').strip() keyword = request.data.get('keyword', '').strip() try: page = int(request.data.get('page', 1)) page_size = int(request.data.get('page_size', 20)) except ValueError: return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST) if not phone: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 客服身份验证 current_user = request.user if getattr(current_user, 'phone', '') != phone: logger.warning(f"手机号不匹配: {phone}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.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 扩展表关联 User queryset = UserDashou.query.select_related('user').all() if keyword: # 搜索用户ID或打手昵称 queryset = queryset.filter( Q(user__UserUID__icontains=keyword) | Q(nicheng__icontains=keyword) ) # 4. 计算总数并分页 total = queryset.count() offset = (page - 1) * page_size dashou_list = queryset.order_by('-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 = User.query.select_related('dashou_profile').get( yonghuid=uid ) except User.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.query.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.query.get(huiyuan_id=record.huiyuan_id) huiyuan_name = huiyuan.jieshao except Huiyuan.DoesNotExist: huiyuan_name = '未知会员' member_list.append({ 'id': record.id, 'huiyuan_name': huiyuan_name, 'daoqi_time': record.daoqi_time, 'is_active': record.huiyuan_zhuangtai == 1, }) return Response({ 'code': 0, 'data': { 'user_info': user_info, 'member_list': member_list } }) class KefuUpdateDashouView(APIView): permission_classes = [IsAuthenticated] """ 客服修改打手信息接口 支持操作: - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb) - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb) - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd) - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee) - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee) - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee) - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg) """ def post(self, request): # 1. 获取前端参数 username = request.data.get('username', '').strip() dashou_id = request.data.get('dashou_id', '').strip() caozuo = request.data.get('caozuo', '').strip() value = request.data.get('value') # 金额、积分、天数等 huiyuan_id = request.data.get('huiyuan_id', '').strip() # 会员ID(加/减会员时使用) if not username or not dashou_id or not caozuo: return Response({'code': 400, 'msg': '参数不完整'}) # 2. 权限校验(统一方法) kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 直接返回错误响应 # 3. 查询打手是否存在 try: dashou_user = User.query.get(UserUID=dashou_id) dashou_profile = dashou_user.dashou_profile except User.DoesNotExist: return Response({'code': 404, 'msg': '打手不存在'}) except UserDashou.DoesNotExist: return Response({'code': 404, 'msg': '打手扩展信息缺失'}) # 4. 根据操作类型执行(事务内) with transaction.atomic(): try: # 记录修改前的值(用于日志) before = { 'yue': dashou_profile.yue, 'yajin': dashou_profile.yajin, 'jifen': dashou_profile.jifen, 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai, 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, 'zhuangtai': dashou_profile.zhuangtai, } # ---------- 余额操作 ---------- if caozuo == 'jian_yue': if '001aa' not in permissions: return Response({'code': 403, 'msg': '无权限减打手余额'}) amount = Decimal(str(value)) if dashou_profile.yue < amount: return Response({'code': 400, 'msg': '余额不足'}) dashou_profile.yue -= amount dashou_profile.save() after_yue = dashou_profile.yue # 记录日志 Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, # 2=打手 xiugaitijiao=after_yue, xiugaitijiaoq=before['yue'], ) return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after_yue)}}) elif caozuo == 'jia_yue': if '001bb' not in permissions: return Response({'code': 403, 'msg': '无权限加打手余额'}) amount = Decimal(str(value)) dashou_profile.yue += amount dashou_profile.save() after_yue = dashou_profile.yue Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, xiugaitijiao=after_yue, xiugaitijiaoq=before['yue'], ) return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after_yue)}}) # ---------- 押金操作 ---------- elif caozuo == 'jian_yajin': if '001aa' not in permissions: return Response({'code': 403, 'msg': '无权限减打手押金'}) amount = Decimal(str(value)) if dashou_profile.yajin < amount: return Response({'code': 400, 'msg': '押金不足'}) dashou_profile.yajin -= amount dashou_profile.save() after_yajin = dashou_profile.yajin Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, yajin=after_yajin, yyajin=before['yajin'], ) return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after_yajin)}}) elif caozuo == 'jia_yajin': if '001bb' not in permissions: return Response({'code': 403, 'msg': '无权限加打手押金'}) amount = Decimal(str(value)) dashou_profile.yajin += amount dashou_profile.save() after_yajin = dashou_profile.yajin Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, yajin=after_yajin, yyajin=before['yajin'], ) return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after_yajin)}}) # ---------- 积分操作 ---------- elif caozuo == 'jian_jifen': if '001cc' not in permissions: return Response({'code': 403, 'msg': '无权限减打手积分'}) jifen = int(value) if dashou_profile.jifen < jifen: return Response({'code': 400, 'msg': '积分不足'}) dashou_profile.jifen -= jifen dashou_profile.save() after_jifen = dashou_profile.jifen Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, jifen=after_jifen, yjifen=before['jifen'], ) return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after_jifen}}) elif caozuo == 'jia_jifen': if '001dd' not in permissions: return Response({'code': 403, 'msg': '无权限加打手积分'}) jifen = int(value) dashou_profile.jifen += jifen dashou_profile.save() after_jifen = dashou_profile.jifen Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, jifen=after_jifen, yjifen=before['jifen'], ) return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after_jifen}}) # ---------- 状态修改 ---------- elif caozuo == 'gai_zhanghaozhuangtai': if '001ee' not in permissions: return Response({'code': 403, 'msg': '无权限修改打手账号状态'}) new_status = int(value) if new_status not in [0, 1]: return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'}) dashou_profile.zhanghaozhuangtai = new_status dashou_profile.save() Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, qitashuoming=f'修改账号状态为{new_status}', ) return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}}) elif caozuo == 'gai_zaixianzhuangtai': if '001ee' not in permissions: return Response({'code': 403, 'msg': '无权限修改打手在线状态'}) new_status = int(value) if new_status not in [0, 1]: return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'}) dashou_profile.zaixianzhuangtai = new_status dashou_profile.save() Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, qitashuoming=f'修改在线状态为{new_status}', ) return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}}) elif caozuo == 'gai_zhuangtai': if '001ee' not in permissions: return Response({'code': 403, 'msg': '无权限修改打手接单状态'}) new_status = int(value) if new_status not in [0, 1]: return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'}) dashou_profile.zhuangtai = new_status dashou_profile.save() Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, qitashuoming=f'修改接单状态为{new_status}', ) return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}}) # ---------- 会员操作 ---------- elif caozuo == 'jia_huiyuan': if '001ff' not in permissions: return Response({'code': 403, 'msg': '无权限给打手加会员'}) if not huiyuan_id: return Response({'code': 400, 'msg': '缺少会员ID'}) days = int(value) if value else 30 # 默认加30天 try: huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response({'code': 404, 'msg': '会员类型不存在'}) # 获取或创建购买记录 record, created = Huiyuangoumai.objects.select_for_update().get_or_create( yonghu_id=dashou_id, huiyuan_id=huiyuan_id, defaults={ 'huiyuan_zhuangtai': 1, 'jieshao': huiyuan.jieshao, 'daoqi_time': timezone.now() + timedelta(days=days), } ) if not created: # 延长有效期 new_daoqi = record.daoqi_time + timedelta(days=days) record.daoqi_time = new_daoqi record.huiyuan_zhuangtai = 1 record.save() # 记录修改日志 Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, huiyuants=days, huiyuan_id=huiyuan_id, qitashuoming=f'加会员 {huiyuan_id},增加{days}天', ) return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}}) elif caozuo == 'jian_huiyuan': if '001gg' not in permissions: return Response({'code': 403, 'msg': '无权限给打手减会员'}) if not huiyuan_id: return Response({'code': 400, 'msg': '缺少会员ID'}) try: record = Huiyuangoumai.query.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id) except Huiyuangoumai.DoesNotExist: return Response({'code': 404, 'msg': '该打手未购买此会员'}) # 删除会员记录(或设置为过期,根据业务要求) record.delete() Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, huiyuan_id=huiyuan_id, qitashuoming=f'移除会员 {huiyuan_id}', ) return Response({'code': 0, 'msg': '减会员成功'}) else: return Response({'code': 400, 'msg': '未知操作类型'}) except Exception as e: logger.error(f"修改打手信息异常: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误'}) class KefuPunishmentListView(APIView): """ 客服获取处罚记录列表接口 请求:POST /yonghu/kefu_cfgl 参数:{ "phone": "客服手机号", "status": [0,3] 或 [1,2], # 状态数组 "dingdan_id": "订单ID(可选)", "dashouid": "打手ID(可选)", "page": 1, "page_size": 20 } 认证:JWT 返回:{ "code": 0, "data": { "list": [处罚记录], "total": 总数, "stats": { "zongshu": 总记录数, "daichuli": 待处理数, "yichuli": 已处理数 } } } """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 phone = request.data.get('phone', '').strip() status_list = request.data.get('status') dingdan_id = request.data.get('dingdan_id', '').strip() dashouid = request.data.get('dashouid', '').strip() try: page = int(request.data.get('page', 1)) page_size = int(request.data.get('page_size', 20)) except ValueError: return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST) if not phone: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 客服身份验证 current_user = request.user # 1. 获取前端参数 username = request.data.get('phone', '').strip() if not username: return Response({'code': 400, 'msg': '缺少username'}) # 2. 公共权限校验 kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 3. 检查平台订单管理权限(002ab) if '005aa' not in permissions: return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) # 3. 获取统计信息(不分页) stats = { 'zongshu': PenaltyRecord.query.count(), 'daichuli': PenaltyRecord.query.filter(ApplyStatus__in=[0, 3]).count(), 'yichuli': PenaltyRecord.query.filter(ApplyStatus__in=[1, 2]).count(), } # 4. 构建查询条件 queryset = PenaltyRecord.query.all() if status_list and isinstance(status_list, list): queryset = queryset.filter(ApplyStatus__in=status_list) if dingdan_id: queryset = queryset.filter(OrderID=dingdan_id) if dashouid: queryset = queryset.filter(PlayerID=dashouid) # 5. 分页 total = queryset.count() offset = (page - 1) * page_size records = queryset.order_by('-CreateTime')[offset:offset + page_size] # 6. 构建返回列表 list_data = [] for item in records: list_data.append({ 'dingdan_id': item.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 KefuGetOrderDetailView(APIView): permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() if not phone or not dingdan_id: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 获取当前用户 current_user = request.user # 验证手机号 if getattr(current_user, 'phone', '') != phone: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 验证用户类型 if current_user.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 Exception as e: logger.exception(f"[kefuhqddxq] 获取客服扩展表异常 user={getattr(current_user, 'yonghuid', '?')}") return Response({'code': 500, 'msg': '服务器错误'}, status=500) # 3. 查询订单 try: dingdan_obj = Order.query.select_related( 'pingtai_kuozhan', 'shangjia_kuozhan' ).get(OrderID=dingdan_id) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=404) except Exception as e: logger.exception(f"[kefuhqddxq] 订单查询异常 dingdan_id={dingdan_id}") return Response({'code': 500, 'msg': '服务器错误'}, status=500) # 4. 构建基础数据 try: response_data = { 'dingdan_id': dingdan_obj.OrderID or '', 'zhuangtai': dingdan_obj.Status or 0, 'fadanpingtai': dingdan_obj.Platform or 0, 'jiage': float(dingdan_obj.Amount) if dingdan_obj.Amount else 0.00, 'dashou_fencheng': float(dingdan_obj.PlayerCommission) if dingdan_obj.PlayerCommission else 0.00, 'jiedan_dashou_id': dingdan_obj.PlayerID or '', 'dashou_liuyan': dingdan_obj.PlayerRemark or '', 'zhiding_id': dingdan_obj.AssignedID or '', 'shangpin_id': dingdan_obj.ProductID or '', 'leixing': dingdan_obj.ProductTypeID or 0, 'tupian_url': dingdan_obj.ImageURL or '', 'jieshao': dingdan_obj.Description or '', 'beizhu': dingdan_obj.Remark or '', 'tkly': dingdan_obj.RefundReason or '', 'nicheng': dingdan_obj.Nickname or '', 'clkf': dingdan_obj.AssignedCS or '', 'chuangjianshijian': dingdan_obj.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.CreateTime else '', 'genggaishijian': dingdan_obj.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.UpdateTime else '', } except Exception as e: logger.exception(f"[kefuhqddxq] 构建基础数据异常 dingdan_id={dingdan_id}") return Response({'code': 500, 'msg': '服务器错误'}, status=500) # 5. 扩展表数据 try: fadan_pingtai = dingdan_obj.Platform if fadan_pingtai == 1: if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan: pingtai = dingdan_obj.pingtai_kuozhan response_data['pingtai_kuozhan'] = { 'laoban_id': pingtai.BossID or '', 'laoban_pingjia': pingtai.BossReview or '', } else: response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''} elif fadan_pingtai == 2: if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan: shangjia = dingdan_obj.shangjia_kuozhan response_data['shangjia_kuozhan'] = { 'shangjia_id': shangjia.MerchantID or '', 'sjnicheng': shangjia.MerchantNickname or '', 'shangjia_pingjia': shangjia.MerchantReview or '', 'cfliyou': shangjia.PenaltyReason or '', 'sqzhuangtai': shangjia.PenaltyApplyStatus if shangjia.PenaltyApplyStatus is not None else 0, 'bhliyou': shangjia.RejectReason or '', } else: response_data['shangjia_kuozhan'] = { 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '', 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': '' } except Exception as e: logger.exception(f"[kefuhqddxq] 扩展数据异常 dingdan_id={dingdan_id}") return Response({'code': 500, 'msg': '服务器错误'}, status=500) # 6. 打手图片 try: dashou_images = PlayerDeliveryImage.query.filter(OrderID=dingdan_id).values_list('ImageURL', flat=True) response_data['dashou_images'] = [img for img in dashou_images if img] except Exception as e: logger.exception(f"[kefuhqddxq] 打手图片查询异常 dingdan_id={dingdan_id}") response_data['dashou_images'] = [] # 7. 退款理由 try: tuikuan_record = RefundRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first() response_data['tuikuan_liyou'] = tuikuan_record.Reason if tuikuan_record and tuikuan_record.Reason else '' except Exception as e: logger.exception(f"[kefuhqddxq] 退款理由查询异常 dingdan_id={dingdan_id}") response_data['tuikuan_liyou'] = '' return Response({'code': 0, 'data': response_data}) class KefuPunishmentDetailView(APIView): """ 客服获取处罚详情接口 请求:POST /yonghu/kefu_cf_detail 参数:{ "phone": "客服手机号", "dingdan_id": "订单ID" } 认证:JWT 返回:code=0 + 处罚记录详情(含图片) """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() if not phone or not dingdan_id: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 客服身份验证 current_user = request.user if getattr(current_user, 'phone', '') != phone: logger.warning(f"手机号不匹配: {phone}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.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 = PenaltyRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first() if not record: return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: logger.error(f"查询处罚记录失败: {e}") return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 查询商家证据图片 zhengju_images = [] shensu_images = [] try: # 商家证据图片:上传者为请求人 (ApplicantID) zhengju_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.ApplicantID) zhengju_images = [item.ImageURL for item in zhengju_qs if item.ImageURL] # 打手申诉图片:上传者为打手 (PlayerID) shensu_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.PlayerID) shensu_images = [item.ImageURL for item in shensu_qs if item.ImageURL] except Exception as e: logger.error(f"查询图片失败: {e}") # 构建返回数据 data = { 'dingdan_id': record.OrderID, 'dashouid': record.PlayerID, 'qingqiuid': record.ApplicantID, 'chuliid': record.ProcessorID, 'cfliyou': record.PenaltyReason, 'sqzhuangtai': record.ApplyStatus, 'bhliyou': record.RejectReason, 'jifen': record.DeductedPoints, 'ssliyou': record.AppealReason or '', 'zhengju_tupian': zhengju_images, 'shensu_tupian': shensu_images, 'create_time': record.create_time, 'update_time': record.update_time, } return Response({'code': 0, 'data': data}) class KefuPunishmentActionView(APIView): """ 客服同意/驳回处罚接口 请求:POST /yonghu/kefu_cf_action 参数:{ "phone": "客服手机号", "dingdan_id": "订单ID", "action": 1(同意) / 2(驳回), "reject_reason": "驳回理由(驳回时必填)" } 认证:JWT 返回:code=0 成功 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() action = request.data.get('action') reject_reason = request.data.get('reject_reason', '').strip() if not phone or not dingdan_id or action not in [1, 2]: return Response({'code': 401, 'msg': '参数错误'}, status=status.HTTP_400_BAD_REQUEST) if action == 2 and not reject_reason: return Response({'code': 400, 'msg': '驳回理由不能为空'}, status=status.HTTP_400_BAD_REQUEST) # 客服身份验证(完整验证,此处略) current_user = request.user if getattr(current_user, 'phone', '') != phone: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.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 = PenaltyRecord.query.filter( OrderID=dingdan_id, ApplyStatus__in=[0, 3] ).order_by('-CreateTime').first() if not record: exists = PenaltyRecord.query.filter(OrderID=dingdan_id).exists() if exists: return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST) else: return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: logger.error(f"查询处罚记录失败: {e}") return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) with transaction.atomic(): # 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回 duplicate_records = PenaltyRecord.query.filter( OrderID=dingdan_id, ApplyStatus__in=[0, 3] ).exclude(id=record.id) for dup in duplicate_records: dup.ApplyStatus = 2 dup.ProcessorID = phone dup.RejectReason = "系统自动驳回:存在重复处罚记录" dup.save() logger.info(f"自动驳回重复处罚记录 ID={dup.id}") if action == 1: # 同意 # 可选验证打手存在 if record.PlayerID: try: dashou = UserDashou.query.get(user__UserUID=record.PlayerID) except UserDashou.DoesNotExist: return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST) record.ApplyStatus = 1 record.ProcessorID = phone record.save() # 更新商家订单扩展表 try: dingdan = Order.query.get(OrderID=dingdan_id) if hasattr(dingdan, 'shangjia_kuozhan'): dingdan.shangjia_kuozhan.PenaltyApplyStatus = 1 dingdan.shangjia_kuozhan.save() except Exception as e: logger.warning(f"更新商家扩展表失败: {e}") elif action == 2: # 驳回 # 返还积分 if record.DeductedPoints > 0 and record.PlayerID: try: dashou = UserDashou.query.get(user__UserUID=record.PlayerID) if dashou.jifen <= 1000: # 安全限制 dashou.jifen += record.DeductedPoints dashou.save() logger.info(f"驳回处罚,打手{record.PlayerID}返还积分{record.DeductedPoints}") else: logger.warning(f"打手积分异常({dashou.jifen}),不再返还") except UserDashou.DoesNotExist: logger.warning(f"打手{record.PlayerID}不存在,无法返还积分") record.ApplyStatus = 2 record.ProcessorID = phone record.RejectReason = reject_reason record.save() try: dingdan = Order.query.get(OrderID=dingdan_id) if hasattr(dingdan, 'shangjia_kuozhan'): dingdan.shangjia_kuozhan.PenaltyApplyStatus = 2 dingdan.shangjia_kuozhan.RejectReason = reject_reason dingdan.shangjia_kuozhan.save() except Exception as e: logger.warning(f"更新商家扩展表失败: {e}") return Response({'code': 0, 'msg': '处理成功'}) class KefuForceCompleteView(APIView): """ 客服强制结单接口(仅本地订单) 请求:POST /yonghu/kefuqiangzhijiedan 参数:{"phone": "客服手机号", "dingdan_id": "订单ID"} 认证:JWT,仅限状态正常的客服 逻辑: 1. 验证客服身份及状态 2. 查询订单并加锁(事务内) 3. 校验订单状态是否为8(结算中) 4. 校验订单必须有接单打手且打手分成金额有效 5. 给打手增加余额、成交统计、今日/本月收益 6. 如果是商家发单,更新商家成交订单数量 7. 更新订单状态为3(已完成) 8. 更新客服处理统计 9. 返回成功 """ permission_classes = [permissions.IsAuthenticated] parser_classes = [JSONParser] def post(self, request): phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() if not phone or not dingdan_id: return Response({'code': 401, 'msg': '参数缺失'}) # 1. 客服身份验证 current_user = request.user if getattr(current_user, 'phone', '') != phone: return Response({'code': 401, 'msg': '认证失败'}) if current_user.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. 订单处理(事务内) try: with transaction.atomic(): order = Order.objects.select_for_update().get(OrderID=dingdan_id) # 3. 校验订单状态 if order.Status != 8: return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.Status}'}) # 4. 校验必要字段 dashou_id = order.PlayerID dashou_fencheng = order.PlayerCommission if not dashou_id: return Response({'code': 400, 'msg': '订单无接单打手'}) if not dashou_fencheng or dashou_fencheng <= 0: return Response({'code': 400, 'msg': '打手分成金额无效'}) # 5. 给打手结算 try: dashou_user = User.query.get(UserUID=dashou_id) dashou_profile = dashou_user.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 (User.DoesNotExist, AttributeError): return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'}) # 6. 如果是商家发单,更新商家成交订单数量 if order.Platform == 2: try: # 通过 MerchantOrderExt 获取商家ID shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan' if shangjia_ext and shangjia_ext.shangjia_id: shangjia_user = User.query.get(UserUID=shangjia_ext.shangjia_id) shangjia_profile = shangjia_user.shop_profile shangjia_profile.chengjiao = F('chengjiao') + 1 shangjia_profile.save(update_fields=['chengjiao']) # (行为驱动,更安全) update_shangjia_daily( yonghuid=shangjia_ext.shangjia_id, amount=Decimal(str(order.Amount)), action=2 # 2 = 结算 ) except ObjectDoesNotExist: # 没有商家扩展记录则忽略 pass # 7. 更新订单状态 order.Status = 3 order.AssignedCS = phone order.save(update_fields=['zhuangtai', 'clkf']) if order.Platform == 1: # ========== 新增:商品和店铺每日统计 ========== try: # 商品每日统计(只要有商品ID就执行) if order.ProductID: # 从扩展表获取已计算好的收益值(下单时已存储) pingtai_shouyi = Decimal('0.00') dianpu_shouyi = Decimal('0.00') if hasattr(order, 'pingtai_kuozhan'): pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00') dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_shangpin_daily_stat( shangpin_id=order.ProductID, caozuo_leixing=2, # 2 = 结单 dingdan_jiage=order.Amount, pingtai_shouyi=pingtai_shouyi, dianpu_zongshouyi=dianpu_shouyi ) logger.info(f"商品统计更新完成,商品ID: {order.ProductID}") # 店铺每日统计(仅当存在店铺ID时执行) dianpu_id = None if hasattr(order, 'pingtai_kuozhan'): dianpu_id = order.pingtai_kuozhan.ShopID if dianpu_id: dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_dianpu_daily_stat( dianpu_id=dianpu_id, caozuo_leixing=2, # 2 = 结单 dingdan_jiage=order.Amount, dianpu_shouyi=dianpu_shouyi ) logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") except Exception as e: logger.error(f"商品/店铺统计更新失败: {e}") logger.error(traceback.format_exc()) # 统计失败不影响主流程 # ============================================= # 🔥 新增:调用打手每日统计(成交) try: update_dashou_daily_by_action( yonghuid=order.PlayerID, amount=dashou_fencheng, action=2 # 2 表示成交(结算) ) logger.info(f"打手 {order.PlayerID} 每日统计更新成功") except Exception as e: logger.error(f"打手每日统计更新失败: {e}") # 不影响主流程 # 商家订单管事分红 if order.Platform == 2: settle_shangjia_order_guanshi_fenhong(order, dashou_id) # 8. 更新客服统计 kefu.jinrichuli = F('jinrichuli') + 1 kefu.jinyuechuli = F('jinyuechuli') + 1 kefu.zongchuli = F('zongchuli') + 1 kefu.save(update_fields=['jinrichuli', 'jinyuechuli', 'zongchuli']) return Response({'code': 0, 'msg': '强制结单成功'}) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}) except Exception as e: logger.error(f"强制结单异常: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误'}) kefu_txsh_list_logger = logging.getLogger('yonghu.kefu_txsh_list') if not kefu_txsh_list_logger.handlers: _console_handler = logging.StreamHandler(sys.stderr) _console_handler.setFormatter(logging.Formatter( '[%(asctime)s] %(levelname)s %(name)s: %(message)s' )) kefu_txsh_list_logger.addHandler(_console_handler) kefu_txsh_list_logger.setLevel(logging.DEBUG) kefu_txsh_list_logger.propagate = False _TIXIAN_LIST_BASE_FIELDS = ( 'id', 'yonghuid', 'avatar', 'phone', 'nicheng', 'leixing', 'zhifu', 'skzhanghao', 'jine', 'zhuangtai', 'fangshi', 'shenheid', 'bhliyou', '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.query.values_list('shenhe_jilu_id', flat=True).first() return True except DatabaseError as e: kefu_txsh_list_logger.error( 'kefu_txsh_list: shenhe_jilu_id 字段不可用,将按旧表结构查询: %s', e, exc_info=True, ) return False def _list_queryset(self, q, shenhe_field_ok): """字段未迁移时 only 旧字段,避免 SELECT 不存在的列导致 500""" fields = _TIXIAN_LIST_BASE_FIELDS + ( _TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else () ) return Tixianjilu.query.filter(q).only(*fields) def _resolve_row_dakuan_mode(self, item, audit_mode_map, shenhe_field_ok): """无关联审核或审核表未标记自动 → 默认手动(1)""" if not shenhe_field_ok: return 1 sid = getattr(item, 'shenhe_jilu_id', None) if not sid: return 1 if audit_mode_map.get(sid) == 2: return 2 return 1 def post(self, request): dakuan_mode_filter = request.data.get('dakuan_mode') req_params = self._safe_request_params(request) kefu_txsh_list_logger.info('kefu_txsh_list 请求开始 params=%s', req_params) try: try: page = int(request.data.get('page', 1)) page_size = int(request.data.get('page_size', 20)) except (TypeError, ValueError): return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST) if page < 1: page = 1 if page_size < 1 or page_size > 100: page_size = min(max(page_size, 1), 100) status_filter = request.data.get('status') type_filter = request.data.get('type') payment_filter = request.data.get('payment') search_uid = (request.data.get('search_uid') or '').strip() date_filter = (request.data.get('date') or '').strip() min_amount = request.data.get('min_amount') max_amount = request.data.get('max_amount') phone = (request.data.get('phone') or '').strip() if not phone: return Response({'code': 401, 'msg': '缺少客服手机号'}, status=status.HTTP_401_UNAUTHORIZED) kefu, permissions = verify_kefu_permission(request, phone) if kefu is None: return permissions if '005bb' not in permissions: return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) q = Q() try: status_val = int(status_filter) if status_filter is not None and str(status_filter).strip() != '' else None except (TypeError, ValueError): status_val = None try: dakuan_mode_val = int(dakuan_mode_filter) if dakuan_mode_filter is not None and str(dakuan_mode_filter).strip() != '' else None except (TypeError, ValueError): dakuan_mode_val = None # 未传打款方式参数 → 默认手动打款(1) effective_dakuan = dakuan_mode_val if dakuan_mode_val in (1, 2) else 1 if status_val == 1: q &= Q(zhuangtai=1) elif status_val == 2: if effective_dakuan == 2: q &= Q(zhuangtai__in=[2, 3, 4, 5, 6]) else: q &= Q(zhuangtai__in=[2, 3]) if type_filter is not None and str(type_filter).strip() != '': try: type_val = int(type_filter) if type_val in [1, 2, 3, 4, 5, 6]: q &= Q(leixing=type_val) except (TypeError, ValueError): pass if payment_filter is not None and str(payment_filter).strip() != '': try: payment_val = int(payment_filter) if payment_val in [1, 2]: q &= Q(fangshi=payment_val) except (TypeError, ValueError): pass if search_uid: q &= Q(yonghuid=search_uid) if date_filter: q &= Q(update_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: 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) 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.query.get(id=tixian_id) except Tixianjilu.DoesNotExist: return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND) # 查询用户主表 try: user = User.query.get(UserUID=record.yonghuid) except User.DoesNotExist: user = None # 构建基础返回数据(字段必须与前端一致) data = { 'id': record.id, 'yonghuid': record.yonghuid, 'avatar': record.avatar or '', 'phone': record.phone or '', 'nicheng': record.nicheng or '', 'leixing': record.leixing, # 1打手 2管事 'zhifu': record.zhifu or '', 'skzhanghao': record.skzhanghao or '', 'jine': str(record.jine) if record.jine else '0.00', 'zhuangtai': record.zhuangtai, 'fangshi': record.fangshi, 'shenheid': record.shenheid or '', 'bhliyou': record.bhliyou or '', 'create_time': record.create_time, 'update_time': record.update_time, } # 根据提现类型添加扩展信息 if user: if record.leixing == 1: # 打手 try: dashou = UserDashou.query.get(user=user) # 计算成交率和退款率 total = dashou.jiedanzongliang or 0 completed = dashou.chengjiaozongliang or 0 refund = dashou.tuikuanliang or 0 completion_rate = round((completed / total * 100) if total > 0 else 0) refund_rate = round((refund / completed * 100) if completed > 0 else 0) data['dashouInfo'] = { 'zhuangtai': dashou.zhuangtai, # 工作状态 'zaixianzhuangtai': dashou.zaixianzhuangtai, # 在线状态 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, # 账号状态 'yue': float(dashou.yue) if dashou.yue else 0.00, # 可提现余额 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, # 接单总额 'jifen': dashou.jifen, # 积分 'jiedanzongliang': dashou.jiedanzongliang, # 接单总量 'chengjiaozongliang': dashou.chengjiaozongliang, # 成交总量 'tuikuanliang': dashou.tuikuanliang, # 退款总量 'completionRate': completion_rate, 'refundRate': refund_rate, } except ObjectDoesNotExist: data['dashouInfo'] = None elif record.leixing == 2: # 管事 try: guanshi = UserGuanshi.query.get(user=user) data['guanshiInfo'] = { 'zhuangtai': guanshi.zhuangtai, # 账号状态 'yue': float(guanshi.yue) if guanshi.yue else 0.00, 'yaogingshuliang': guanshi.yaogingshuliang, # 邀请打手总数 'jinrichongzhi': guanshi.jinrichongzhi, # 今日充值 'jinyuechongzhi': guanshi.jinyuechongzhi, # 今月充值 'chongzhifenrun': float(guanshi.chongzhifenrun) if guanshi.chongzhifenrun else 0.00, } except ObjectDoesNotExist: data['guanshiInfo'] = None return Response({'code': 0, 'data': data}) class KefuWithdrawActionView(APIView): """ 客服处理提现接口(同意/拒绝)—— 仅此接口处理后台审核打款 POST /yonghu/kefu_txsh_action 单条:{ phone, action, tixian_id, yonghuid, leixing, reason? } 批量:{ phone, action, batch_list: [{tixian_id, yonghuid, leixing}, ...], reason? } 自动打款(dakuan_mode=2):同意→6待收款;拒绝→5+驳回原因+退还可到账金额(shijidaozhang),手续费不退,不更新平台收支/每日统计 手动打款(dakuan_mode=1):原逻辑不变 状态要求:Tixianjilu.zhuangtai=1 且 TixianShenheJilu.zhuangtai=1(审核中),其他状态一律拒绝 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def _verify_kefu(self, request, phone): if not phone: return None, Response({'code': 401, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST) current_user = request.user if getattr(current_user, 'phone', '') != phone: return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.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 内调用)""" tixian_id = item['tixian_id'] req_yonghuid = item['yonghuid'] req_leixing = item['leixing'] if req_leixing not in [1, 2, 3, 4, 5, 6]: raise ValueError(f'提现记录{tixian_id}:提现类型无效') try: tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id) except Tixianjilu.DoesNotExist: raise ValueError(f'提现记录{tixian_id}不存在') if tixian.yonghuid != req_yonghuid: raise ValueError(f'提现记录{tixian_id}:用户ID不匹配') if tixian.leixing != req_leixing: raise ValueError(f'提现记录{tixian_id}:提现类型不匹配') if tixian.zhuangtai != 1: raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理') if not tixian.shenhe_jilu_id: raise ValueError(f'提现记录{tixian_id}:非自动打款审核记录') try: audit = TixianShenheJilu.objects.select_for_update().get(id=tixian.shenhe_jilu_id) except TixianShenheJilu.DoesNotExist: raise ValueError(f'提现记录{tixian_id}:审核记录不存在') if audit.yonghuid != req_yonghuid: raise ValueError(f'提现记录{tixian_id}:审核记录用户ID不匹配') if audit.leixing != req_leixing: raise ValueError(f'提现记录{tixian_id}:审核记录提现类型不匹配') if audit.zhuangtai != 1: raise ValueError(f'提现记录{tixian_id}:审核记录非审核中状态') try: user = User.objects.select_for_update().get(UserUID=req_yonghuid) except User.DoesNotExist: raise ValueError(f'用户{req_yonghuid}不存在') if action == 1: limit_ok, limit_msg = check_shijidaozhang_within_limit(audit.shijidaozhang) if not limit_ok: sync_audit_and_jilu_status( audit, 5, bhliyou=limit_msg, bo_hui_ren_id=kefu_user.yonghuid, ) refund_balance(user, audit.leixing, audit.shijidaozhang) tixian.shenheid = kefu_user.yonghuid tixian.bhliyou = limit_msg tixian.save(update_fields=['shenheid', 'bhliyou', 'update_time']) raise ValueError(f'提现记录{tixian_id}:{limit_msg},已自动驳回并退款') 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 中的手续费(shouxufei)不退 refund_balance(user, audit.leixing, audit.shijidaozhang) 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 = User.query.get(UserUID=tixian.yonghuid) except User.DoesNotExist: raise ValueError(f'用户{tixian.yonghuid}不存在') if action == 1: tixian.zhuangtai = 2 try: update_daily_payout(tixian.jine) szjilu, _ = Szjilu.query.get_or_create(id=1) szjilu.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: try: refund_balance(user, tixian.leixing, tixian.jine) except ValueError as e: raise ValueError(str(e)) tixian.zhuangtai = 3 tixian.bhliyou = reason tixian.shenheid = phone tixian.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)}}) # dingdan/views.py class KefuPunishView(APIView): """ 客服处罚打手接口 请求:POST /yonghu/kefuchufa 参数:{ "phone": "客服手机号", "dingdan_id": "订单ID", "reason": "处罚原因(可选)" } 认证:JWT 返回:code=0 成功 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() reason = request.data.get('reason', '').strip() if not phone or not dingdan_id: return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) # 2. 客服身份验证 current_user = request.user if getattr(current_user, 'phone', '') != phone: logger.warning(f"手机号不匹配: {phone}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.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 = Order.query.get(OrderID=dingdan_id) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) # 4. 获取打手ID dashou_id = order.PlayerID if not dashou_id: return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST) # 5. 查询打手 try: dashou_user = User.query.get(UserUID=dashou_id) dashou = dashou_user.dashou_profile except User.DoesNotExist: return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND) except ObjectDoesNotExist: return Response({'code': 400, 'msg': '该用户不是打手'}, status=status.HTTP_400_BAD_REQUEST) # 6. 使用事务 with transaction.atomic(): # 创建处罚记录(积分扣除5分) jifen = 5 chufa = PenaltyRecord.query.create( PlayerID=dashou_id, ApplicantID=phone, # 客服手机号作为请求人 PenaltyReason=reason or '', ApplyStatus=0, # 待处理 DeductedPoints=jifen, OrderID=dingdan_id, ) # 扣除打手积分 if dashou.jifen >= jifen: dashou.jifen -= jifen else: dashou.jifen = 0 dashou.save() # 可选:更新订单的 clkf 字段记录处理客服 order.AssignedCS = phone order.save() return Response({'code': 0, 'msg': '处罚成功'}) class AdKftjView(APIView): """ 添加客服接口 权限:管理员JWT 请求:POST /yonghu/adkftj 参数:{ "zhanghao": "管理员账号", "phone": "客服手机号(必填)", "password": "密码(必填)", "erjimima": "二级密码(必填)", "nicheng": "昵称(可选)" } 返回:code=0 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 参数 zhanghao = request.data.get('zhanghao', '').strip() phone = request.data.get('phone', '').strip() password = request.data.get('password', '').strip() erjimima = request.data.get('erjimima', '').strip() nicheng = request.data.get('nicheng', '').strip() if not all([zhanghao, phone, password, erjimima]): return Response({'code': 400, 'message': '手机号、密码、二级密码不能为空'}) # 2. 管理员验证(同前) current_user = request.user if current_user.phone != zhanghao or current_user.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 User.query.filter(Phone=phone).exists(): return Response({'code': 400, 'message': '该手机号已被注册'}) # 4. 生成唯一 yonghuid def generate_yonghuid(): for _ in range(10): timestamp = str(int(time.time()))[-5:] rand = str(random.randint(0, 99)).zfill(2) uid = timestamp + rand if len(uid) == 7 and not User.query.filter(UserUID=uid).exists(): return uid raise Exception('无法生成唯一用户ID') with transaction.atomic(): # 创建主表用户 user = User.query.create( UserUID=generate_yonghuid(), UserName=f'kefu_{phone}', Phone=phone, OpenID=f'kefu_{phone}' # 临时openid ) user.SetPassword(password) user.save(update_fields=['UserPassword']) # 创建客服扩展表 import bcrypt as _bcrypt erjimima_hashed = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8') kefu = UserKefu.query.create( user=user, nicheng=nicheng or f'客服{user.yonghuid}', erjimima=erjimima_hashed, zhuangtai=1, jinrichuli=0, jinyuechuli=0, zongchuli=0 ) return Response({'code': 0, 'message': '添加成功'}) class AdKfglView(APIView): """ 客服管理列表接口 权限:管理员JWT 请求:POST /yonghu/adkfgl 参数:{ "zhanghao": "管理员账号", "keyword": "搜索关键词(可选,匹配客服ID或昵称)" } 返回:{ "code": 0, "data": { "list": [客服对象...] } } """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 参数获取 zhanghao = request.data.get('zhanghao', '').strip() keyword = request.data.get('keyword', '').strip() if not zhanghao: return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) # 2. 管理员身份验证 current_user = request.user if not hasattr(current_user, 'phone') or str(current_user.phone) != zhanghao: return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.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.query.select_related('user').all() if keyword: queryset = queryset.filter( #Q(user__UserUID__icontains=keyword) | Q(nicheng__icontains=keyword) | Q(user__Phone__icontains=keyword) ) # 4. 构建返回数据 kefu_list = [] for kefu in queryset: user = kefu.user kefu_list.append({ 'yonghuid': user.phone, 'phone': user.phone or '', 'nicheng': kefu.nicheng or '', 'zhuangtai': kefu.zhuangtai, 'jinrichuli': kefu.jinrichuli, 'jinyuechuli': kefu.jinyuechuli, 'zongchuli': kefu.zongchuli, }) return Response({ 'code': 0, 'data': {'list': kefu_list} }) class AdKfxgView(APIView): """ 修改客服信息接口 权限:管理员JWT 请求:POST /yonghu/adkfxg 参数:{ "zhanghao": "管理员账号", "uid": "客服ID (yonghuid)", "nicheng": "新昵称(可选)", "phone": "新手机号(可选)", "password": "新密码(可选)", "erjimima": "新二级密码(可选)", "zhuangtai": 0/1(可选) } 返回:code=0 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): zhanghao = request.data.get('zhanghao', '').strip() uid = request.data.get('uid', '').strip() if not zhanghao or not uid: return Response({'code': 400, 'message': '参数不完整'}) # 管理员验证 current_user = request.user if current_user.phone != zhanghao or current_user.user_type != 'admin': return Response({'code': 401, 'message': '身份验证失败'}) try: admin_profile = current_user.admin_profile except: return Response({'code': 401, 'message': '身份验证失败'}) # 查询客服 try: user = User.query.get(Phone=uid) kefu = user.kefu_profile except (User.DoesNotExist, UserKefu.DoesNotExist): return Response({'code': 404, 'message': '客服不存在'}) # 更新数据 with transaction.atomic(): # 更新主表字段 if 'phone' in request.data: new_phone = request.data.get('phone').strip() if new_phone and User.query.filter(Phone=new_phone).exclude(UserUID=uid).exists(): return Response({'code': 400, 'message': '手机号已被使用'}) user.Phone = new_phone if 'password' in request.data: user.UserPassword = request.data['password'] user.save() # 更新扩展表 if 'nicheng' in request.data: kefu.nicheng = request.data['nicheng'] if 'erjimima' in request.data: import bcrypt as _bcrypt new_erji = request.data['erjimima'] kefu.erjimima = _bcrypt.hashpw(new_erji.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8') if 'zhuangtai' in request.data: kefu.zhuangtai = int(request.data['zhuangtai']) kefu.save() return Response({'code': 0, 'message': '修改成功'}) class WechatLoginAndGuanshiRegisterView(APIView): """ 未登录用户微信登录+管事注册一体化接口 路径: /api/yonghu/zuzhangyqmzc 方法: POST 权限: 允许所有 请求体: { "code": "微信登录code", "inviteCode": "组长邀请码" } 成功返回 code: 200,数据同 GuanshiRegisterView """ permission_classes = [AllowAny] def post(self, request): code = request.data.get('code', '').strip() yaoqingma = request.data.get('inviteCode', '').strip() if not code: return Response({'code': 400, 'msg': '微信授权码不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) if not yaoqingma: return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) if len(yaoqingma) > 100: return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None}, status=status.HTTP_400_BAD_REQUEST) wechat_data = self._get_wechat_openid(code) if not wechat_data or 'openid' not in wechat_data: error_msg = wechat_data.get('errmsg', '微信授权失败') return Response({'code': 400, 'msg': f'微信登录失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST) openid = wechat_data['openid'] kehuduan_ip = self._huoqu_kehuduan_ip(request) with transaction.atomic(): user_main, created = User.objects.select_for_update().get_or_create( OpenID=openid, defaults={ 'UserUID': self._shengcheng_yonghu_id(), 'UserName': openid, } ) user_main.IP = kehuduan_ip user_main.UserLastLoginDate = timezone.now() user_main.save() if created: UserBoss.query.create(user=user_main, nickname='微信用户') if hasattr(user_main, 'guanshi_profile'): return self._fanhui_xianyou_guanshi_info(user_main) try: zuzhang = UserZuzhang.query.select_related('user').get(yaoqingma=yaoqingma) except UserZuzhang.DoesNotExist: return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND) if zuzhang.zhuangtai != 1: return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None}, status=status.HTTP_403_FORBIDDEN) zuzhang_yonghuid = zuzhang.user.yonghuid guanshi_yaoqingma = CreateInvitationCode(str(user_main.yonghuid)) guanshi_profile = UserGuanshi.query.create( user=user_main, yaoqingma=guanshi_yaoqingma, yaoqingren=zuzhang_yonghuid, ) UserZuzhang.query.filter(pk=zuzhang.pk).update( yaoqing_zongshu=models.F('yaoqing_zongshu') + 1 ) # ========== 新增:组长每日统计(邀请管事 action=1) ========== try: update_zuzhang_daily_by_action( yonghuid=zuzhang_yonghuid, action=1 # 1 = 邀请管事 ) logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}") except Exception as e: logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}") if not hasattr(user_main, 'dashou_profile'): dashou_profile = UserDashou.query.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 = getattr(settings, 'WEIXIN_APPID', '') secret = getattr(settings, 'WEIXIN_SECRET', '') if not appid or not secret: raise ValueError('微信配置未设置') url = 'https://api.weixin.qq.com/sns/jscode2session' params = { 'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code' } response = requests.get(url, params=params, timeout=10) result = response.json() if 'openid' in result: return result else: errcode = result.get('errcode', 'unknown') errmsg = result.get('errmsg', '未知错误') return {'errmsg': f'[{errcode}]{errmsg}'} except requests.exceptions.Timeout: logger.error('请求微信接口超时') return {'errmsg': '请求微信接口超时'} except Exception as e: logger.error(f'请求微信接口异常: {e}') return {'errmsg': f'请求微信接口异常: {str(e)}'} def _shengcheng_yonghu_id(self): for _ in range(10): timestamp_part = str(int(time.time()))[-5:].zfill(5) random_part = str(random.randint(0, 99)).zfill(2) user_id = timestamp_part + random_part if len(user_id) == 7 and user_id.isdigit(): if not User.query.filter(UserUID=user_id).exists(): return user_id raise Exception('生成用户ID失败') def _huoqu_kehuduan_ip(self, request): x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: ip = x_forwarded_for.split(',')[0].strip() if ip: return ip ip = request.META.get('REMOTE_ADDR', '') return ip if ip else '0.0.0.0' def _huoqu_qun_peizhi(self): try: qun_configs = Qunpeizhi.query.filter(id__in=[1, 2]) result = { 'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': '' } for config in qun_configs: if config.id == 1: result['dashouqun'] = config.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.query.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 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.query.select_related('user').get(yaoqingma=yaoqingma) except UserZuzhang.DoesNotExist: return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND) if zuzhang.zhuangtai != 1: return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None}, status=status.HTTP_403_FORBIDDEN) zuzhang_yonghuid = zuzhang.user.yonghuid # 4. 事务内创建管事实体和打手(如果需要) with transaction.atomic(): # 创建管事扩展表 guanshi_yaoqingma = CreateInvitationCode(str(current_user.yonghuid)) guanshi_profile = UserGuanshi.query.create( user=current_user, yaoqingma=guanshi_yaoqingma, yaoqingren=zuzhang_yonghuid, ) # 更新组长的邀请总数 UserZuzhang.query.filter(pk=zuzhang.pk).update( yaoqing_zongshu=models.F('yaoqing_zongshu') + 1 ) # ========== 新增:组长每日统计(邀请管事 action=1) ========== try: update_zuzhang_daily_by_action( yonghuid=zuzhang_yonghuid, action=1 # 1 = 邀请管事 ) logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}") except Exception as e: logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}") # 检查用户是否已是打手,如果不是则创建 if not hasattr(current_user, 'dashou_profile'): dashou_profile = UserDashou.query.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.query.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.query.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 class ZuzhangZhuceView(APIView): """ 组长注册/激活接口 POST /yonghu/zuzhangzhuce 请求体: {"inviteCode": "xxx"} 返回: { "code": 200, "msg": "success", "data": { "ketixian": "0.00", # 可提现金额 "guanshiCount": 0, # 邀请的管事数量 "fenhongZonge": "0.00" # 分红总额 } } 或错误码 """ permission_classes = [IsAuthenticated] # 硬编码激活码(注意转义特殊字符) ACTIVATION_CODE = "237huwehdw77e777eyw224fr" def post(self, request): user = request.user invite_code = request.data.get('inviteCode', '').strip() if not invite_code: return Response({'code': 400, 'msg': '邀请码不能为空'}, status=400) # 检查是否已是组长 try: zuzhang = UserZuzhang.query.select_related('user').get(user=user) # 已注册,直接返回数据 data = self._build_response_data(zuzhang, user) return Response({'code':200, 'msg': '已注册', 'data': data}) except UserZuzhang.DoesNotExist: pass # 比对激活码 if invite_code != self.ACTIVATION_CODE: return Response({'code': 400, 'msg': '邀请码无效'}, status=400) # 生成唯一组长邀请码:时间戳 + 用户ID后6位 + 随机字符 yaoqingma = self._generate_unique_yaoqingma(user.yonghuid) # 创建组长扩展记录 try: with transaction.atomic(): zuzhang = UserZuzhang.query.create( user=user, yaoqingma=yaoqingma, fenyong_zonge=0.00, ketixian_jine=0.00, yaoqing_zongshu=0, jinri_fenyong=0.00, jinyue_fenyong=0.00, zhuangtai=1, jinri_tixian=0.00, ewai_tixian_xiane=0.00, kaioi_ewai_tixian=False, kaioi_ewai_fenhong=False, ewai_fenhong_jine=0.00 ) except IntegrityError: # 极低概率重复,重试一次 yaoqingma = self._generate_unique_yaoqingma(user.yonghuid, retry=True) zuzhang = UserZuzhang.query.create( user=user, yaoqingma=yaoqingma, # 其他字段默认同上 fenyong_zonge=0.00, ketixian_jine=0.00, yaoqing_zongshu=0, jinri_fenyong=0.00, jinyue_fenyong=0.00, zhuangtai=1, jinri_tixian=0.00, ewai_tixian_xiane=0.00, kaioi_ewai_tixian=False, kaioi_ewai_fenhong=False, ewai_fenhong_jine=0.00 ) data = self._build_response_data(zuzhang, user) return Response({'code':200, 'msg': '注册成功', 'data': data}) def _generate_unique_yaoqingma(self, yonghuid, retry=False): """生成唯一邀请码:时间戳+用户ID后6位+随机2位字符""" timestamp = str(int(time.time()))[-6:] # 取后6位 uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0') rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2)) base = f"Z{timestamp}{uid_part}{rand_str}" # 确保唯一性(若已存在则追加随机数,最多尝试3次) for _ in range(3): if not UserZuzhang.query.filter(yaoqingma=base).exists(): return base base = base + random.choice(string.digits) # 极端情况,再附加时间戳毫秒 millis = str(time.time()).replace('.', '')[-4:] return base + millis def _build_response_data(self, zuzhang, user): """组装前端所需数据""" # 计算邀请的管事数量(通过UserGuanshi表的yaoqingren字段关联到user的yonghuid) guanshi_count = UserGuanshi.query.filter(yaoqingren=user.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.query.get(user=user) except UserZuzhang.DoesNotExist: return Response({'code': 404, 'msg': '用户不是组长'}, status=404) guanshi_count = UserGuanshi.query.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) 鉴权逻辑(满足任意一条即通过): 1. 押金 >= 5 元 → 通过 2. 拥有未过期会员 → 通过 3. 存在订单状态为 2 或 8 的订单(作为接单打手) → 通过 以上均不满足 → 拒绝 """ class DashouJianquanView(APIView): """打手消息权限鉴权""" permission_classes = [IsAuthenticated] def post(self, request): # ========== 1. 通过 JWT 获取当前登录用户 ========== user = request.user # User 实例 # ========== 2. 检查是否为已注册打手 ========== try: dashou = user.dashou_profile # OneToOneField 反向查询 except UserDashou.DoesNotExist: return Response({ 'code': 0, 'allow': 0, 'msg': '您尚未注册打手身份,请先注册' }) # ========== 3. 条件一:押金 >= 5 元 ========== if dashou.yajin and dashou.yajin >= 5: return Response({ 'code': 0, 'allow': 1, 'msg': '鉴权通过(押金满足条件)' }) # ========== 4. 条件二:存在未过期会员 ========== # 高效查询:利用 yonghu_id + huiyuan_zhuangtai 复合索引 huiyuan_list = Huiyuangoumai.query.filter( yonghu_id=user.yonghuid, huiyuan_zhuangtai=1 # 只查状态为"可用"的,减少内存遍历 ).only('id', 'daoqi_time', 'huiyuan_zhuangtai') for hy in huiyuan_list: if not hy.jiance_shifou_daoqi(): # 未过期返回 False return Response({ 'code': 0, 'allow': 1, 'msg': '鉴权通过(会员未过期)' }) # ========== 5. 条件三:存在订单状态为 2 或 8 的订单 ========== # 利用 jiedan_dashou_id + zhuangtai 复合索引,只查 1 条 has_valid_order = Order.query.filter( PlayerID=user.yonghuid, Status__in=[2, 8] ).only('id').exists() # exists() 高性能判断,只发 SELECT 1 LIMIT 1 if has_valid_order: return Response({ 'code': 0, 'allow': 1, 'msg': '鉴权通过(存在有效订单)' }) # ========== 6. 以上均不满足 → 拒绝 ========== return Response({ 'code': 0, 'allow': 0, 'msg': '消息权限不足,请充值会员或联系管理员' }) # apps/yonghu/views_laoban_jianquan.py """ 老板(普通用户) IM 消息权限鉴权接口 URL: /yonghu/lbhqltqx 方法: POST 认证: JWT Token (request.user) 鉴权逻辑: 查询平台订单扩展表(PlatformOrderExt), 通过关联的订单主表(Order)查找该老板的订单, 只要存在任意一条订单状态在 1~8 范围内 → 通过 """ class LaobanJianquanView(APIView): """老板消息权限鉴权""" permission_classes = [IsAuthenticated] def post(self, request): # ========== 1. 通过 JWT 获取当前登录用户 ========== user = request.user # User 实例 # ========== 2. 高效查询平台订单扩展表 ========== # 利用 PlatformOrderExt.laoban_id 索引 + Order.zhuangtai 索引 # 只查 1 条,用 exists() 实现最高性能 has_valid_order = PlatformOrderExt.query.filter( laoban_id=user.yonghuid ).filter( dingdan__zhuangtai__in=[1, 2, 3, 4, 5, 6, 7, 8] ).only('id').exists() if has_valid_order: return Response({ 'code': 0, 'allow': 1, 'msg': '鉴权通过' }) return Response({ 'code': 0, 'allow': 0, 'msg': '您暂无订单记录,消息功能暂不可用' }) # views.py (在处罚相关app的views中) class FaKuanJiFenTongJiView(APIView): """ 获取罚款与积分处罚统计总览 路径: POST /yonghu/cffktjhq """ permission_classes = [IsAuthenticated, DashouPermission] def post(self, request): try: user_main = request.user yonghuid = user_main.yonghuid # ================= 罚款统计 ================= fadan_stats = Penalty.query.filter(PenalizedUserID=yonghuid).aggregate( total=Count('id'), daijiaona=Count('id', filter=Q(Status=1)), shensuzhong=Count('id', filter=Q(Status=3)), yijiaona=Count('id', filter=Q(Status=2)), yibohui=Count('id', filter=Q(Status=4)), ) # ================= 积分处罚统计 ================= jifen_stats = PenaltyRecord.query.filter(PlayerID=yonghuid).aggregate( total=Count('id'), daichuli=Count('id', filter=Q(ApplyStatus=0) | Q(ApplyStatus=3)), yichuli=Count('id', filter=Q(ApplyStatus=1) | Q(ApplyStatus=2)), # 细分 daichuli_0=Count('id', filter=Q(ApplyStatus=0)), shensuzhong_3=Count('id', filter=Q(ApplyStatus=3)), yichufa_1=Count('id', filter=Q(ApplyStatus=1)), yibohui_2=Count('id', filter=Q(ApplyStatus=2)), ) data = { "fakuan": { "total": fadan_stats['total'], "daijiaona": fadan_stats['daijiaona'], "shensuzhong": fadan_stats['shensuzhong'], "yijiaona": fadan_stats['yijiaona'], "yibohui": fadan_stats['yibohui'], }, "jifen": { "total": jifen_stats['total'], "daichuli": jifen_stats['daichuli'], "yichuli": jifen_stats['yichuli'], "daichuli_0": jifen_stats['daichuli_0'], "shensuzhong_3": jifen_stats['shensuzhong_3'], "yichufa_1": jifen_stats['yichufa_1'], "yibohui_2": jifen_stats['yibohui_2'], } } return Response({'code': 0, 'msg': '获取统计成功', 'data': data}) except Exception as e: logger.error(f"获取统计异常 {yonghuid}: {e}", exc_info=True) return Response({'code': 99, 'msg': '系统繁忙'}, status=500) class DaShouFaKuanLieBiaoView(APIView): """ 打手罚款列表接口 路径:POST /yonghu/dsfklbhq 参数: page: 页码(默认1) page_size: 每页条数(默认10,最大50) zhuangtai: 状态筛选(1待缴纳/2已缴纳/3申诉中/4申诉成功,0或不传表示全部) sousuo_dingdan_id: 订单号模糊搜索 返回:{ list: [{ id, beichufa_id, guanliandingdan_id, chufaliyou, fakuanjine, zhuangtai, yingxiang_qiangdan, shensuliyou, bohuiliyou, zhengju_tupian: [...], // 证据图片(yongtu=1) shensu_tupian: [...], // 申诉图片(yongtu=2) 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 = Penalty.query.filter(PenalizedUserID=yonghuid) # 状态筛选 if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]: qs = qs.filter(Status=int(zhuangtai)) # 订单号模糊搜索 if sousuo_dingdan_id: qs = qs.filter(RelatedOrderID__icontains=sousuo_dingdan_id) # 按创建时间倒序 qs = qs.order_by('-CreateTime') # 分页 paginator = Paginator(qs, page_size) try: page_obj = paginator.page(page) except Exception: return Response({ 'code': 0, 'msg': '获取成功', 'data': {'list': [], 'has_more': False, 'page': page, 'page_size': page_size} }) records = list(page_obj) fadan_ids = [r.id for r in records] # 批量获取图片,按用途分组 zhengju_map = {} # key: fadan_id, value: [url, ...] shensu_map = {} if fadan_ids: tupians = PenaltyAppealImage.query.filter( Penalty_id__in=fadan_ids ).values('Penalty_id', 'ImageURL', 'Purpose') for t in tupians: fid = t['Penalty_id'] url = t['ImageURL'] if t['Purpose'] == 1: zhengju_map.setdefault(fid, []).append(url) else: shensu_map.setdefault(fid, []).append(url) # 组装返回数据 chufa_list = [] for r in records: rid = r.id item = { 'id': rid, 'beichufa_id': r.PenalizedUserID, 'guanliandingdan_id': r.RelatedOrderID or '', 'chufaliyou': r.Reason or '', 'fakuanjine': str(r.FineAmount), 'zhuangtai': r.Status, 'yingxiang_qiangdan': r.AffectsGrabbing, 'shensuliyou': r.AppealReason or '', 'bohuiliyou': r.RejectReason or '', 'zhengju_tupian': zhengju_map.get(rid, []), # 证据图片 'shensu_tupian': shensu_map.get(rid, []), # 申诉图片 '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 = Penalty.query.get(id=fadan_id, PenalizedUserID=yonghuid) except Penalty.DoesNotExist: return Response({'code': 404, 'msg': '罚单不存在或无权操作'}, status=404) # 只允许待缴纳状态(状态1)且没有驳回理由的罚单申诉 if fadan.Status != 1: return Response({'code': 400, 'msg': '罚单状态不允许申诉'}, status=400) if fadan.RejectReason: return Response({'code': 400, 'msg': '该罚单申诉已被驳回,不可再次申诉'}, status=400) # 更新罚单状态为申诉中,并记录理由 fadan.Status = 3 fadan.AppealReason = shensu_liyou fadan.save() # 删除旧的申诉图片(yongtu=2),仅删除打手自己上传的申诉图 PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=2).delete() # 插入新上传的申诉图片 for url in tupian_urls: PenaltyAppealImage.query.create( Penalty=fadan, PenalizedUserID=yonghuid, ImageURL=url, Purpose=2 ) return Response({'code': 0, 'msg': '申诉提交成功'}) except Exception as e: logger.error(f"罚款申诉异常 {yonghuid if 'yonghuid' in locals() else ''}: {e}", exc_info=True) return Response({'code': 99, 'msg': '系统繁忙'}, status=500) # 需要替换为你自己项目中的导入路径 # XIAOCHENGXU LOGGER? # ==================== 1. 罚款支付下单接口 ==================== class FaKuanPayView(APIView): """ 罚款微信支付下单接口(完全照抄 YajinGoumai) 路径:POST /yonghu/fkjn """ permission_classes = [IsAuthenticated] def post(self, request): try: fadan_id = request.data.get('fadan_id') if not fadan_id: return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) try: fadan = Penalty.query.get(id=fadan_id, PenalizedUserID=request.user.yonghuid) except Penalty.DoesNotExist: return Response({'code': 404, 'message': '罚单不存在或不属于您'}, status=status.HTTP_404_NOT_FOUND) if fadan.Status not in [1, 3]: return Response({'code': 400, 'message': '罚单当前状态不可支付'}, status=status.HTTP_400_BAD_REQUEST) jine = float(fadan.FineAmount) timestamp_ms = int(time.time() * 1000) timestamp_ns = int(time.time_ns() // 1000) % 1000000 random_num = random.randint(0, 99) dingdanid = f"FK{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" chongzhi_jilu = Czjilu.query.create( dingdan_id=dingdanid, zhuangtai=9, yonghuid=request.user.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 = settings.WEIXIN_APPID MCHID = settings.WEIXIN_MCHID KEY = settings.WEIXIN_SHANGHUMIYAO if pay_type == 'fakuan': PAY_DESCRIPTION = settings.FAKUAN_PAY_DESCRIPTION NOTIFY_URL = settings.FAKUAN_PAY_NOTIFY_URL # 回调地址在这里 else: raise Exception('未知的支付类型') nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) total_fee = int(jine * 100) trade_type = 'JSAPI' user_ip = self.get_client_ip() # 第一次签名 sign_data = { 'appid': APPID, 'mch_id': MCHID, 'nonce_str': nonce_str, 'body': PAY_DESCRIPTION, 'out_trade_no': dingdanid, 'total_fee': total_fee, 'spbill_create_ip': user_ip, 'notify_url': NOTIFY_URL, # 关键:把回调地址传入 'trade_type': trade_type, 'openid': openid } sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) sign_string += f'&key={KEY}' sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() # 构建 XML,这里必须包含 notify_url xml_data = f""" {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. 罚款支付回调接口 ==================== class FaKuanHuitiaoView(View): """ 罚款支付回调接口(严格借鉴 YajinHuitiao 接口,参数一个不少) 路径:POST /yonghu/fk_huitiao """ def post(self, request): try: # 1. 解析微信回调XML数据 request_body = request.body logger.info(f"收到微信支付回调,原始数据: {request_body.decode('utf-8')}") root = ET.fromstring(request_body) # 获取关键字段 return_code = root.find('return_code').text result_code = root.find('result_code').text out_trade_no = root.find('out_trade_no').text # 订单ID transaction_id = root.find('transaction_id').text # 微信交易号 total_fee = int(root.find('total_fee').text) # 支付金额(分) logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") # 2. 验证基本返回状态 if return_code != 'SUCCESS' or result_code != 'SUCCESS': logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}") return self.wechat_response('FAIL', '支付失败') # 3. 验证签名(确保是微信发来的) if not self.verify_signature(request_body): logger.error(f"签名验证失败: {out_trade_no}") return self.wechat_response('FAIL', '签名验证失败') # 4. 查询订单 try: order = Czjilu.query.get(dingdan_id=out_trade_no) logger.info(f"找到订单: {order.OrderID}, 状态: {order.Status}, 金额: {order.Amount}") except Czjilu.DoesNotExist: logger.error(f"订单不存在: {out_trade_no}") return self.wechat_response('FAIL', '订单不存在') # 5. 检查订单状态是否为未支付(9) if order.Status != 9: logger.warning(f"订单状态不是未支付: {order.OrderID}, 当前状态: {order.Status}") # 已处理过的订单直接返回成功,避免微信重复回调 return self.wechat_response('SUCCESS', 'OK') # 6. 验证金额(微信返回的是分,需要转换) wechat_amount_yuan = total_fee / 100 # 转换为元 order_amount_yuan = float(order.Amount) # 订单金额(元) # 允许1分钱的误差(浮点数比较) if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") return self.wechat_response('FAIL', '金额不一致') # 7. 更新订单状态为已支付(3) order.Status = 3 order.save(update_fields=['zhuangtai']) logger.info(f"订单状态更新成功: {order.OrderID} -> 3") # 8. 更新罚单状态为已缴纳(2) try: # 根据订单中的用户ID和金额匹配待缴纳的罚单 fadan = Penalty.query.filter( PenalizedUserID=order.yonghuid, FineAmount=order.Amount, Status__in=[1, 3] ).first() if fadan: fadan.Status = 2 # 已缴纳 fadan.save(update_fields=['Status']) logger.info(f"罚单 {fadan.id} 已自动标记为已缴纳") success, msg = process_fadan_fenhong(fadan.id) if not success: logger.error(f"罚单 {fadan.id} 分红处理失败: {msg}") else: logger.info(f"罚单 {fadan.id} 分红处理完成: {msg}") except Exception as e: logger.error(f"更新罚单状态失败: {str(e)}", exc_info=True) # 9. 更新收支记录表 try: # 获取订单金额(确保是Decimal类型) jine_decimal = Decimal(str(order.Amount)) if not isinstance(order.Amount, Decimal) else order.Amount update_daily_income(jine_decimal) # 查询或创建ID=1的收支记录 szjilu, created = Szjilu.query.get_or_create( id=1, defaults={ '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.OrderID}, 金额{jine_decimal}元") except Exception as e: # 收支记录更新失败不影响主流程,只记录日志 logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) # 10. 返回成功响应给微信 logger.info(f"罚款支付回调处理完成: {out_trade_no}") return self.wechat_response('SUCCESS', 'OK') except ET.ParseError as e: logger.error(f"XML解析失败: {str(e)}") return self.wechat_response('FAIL', '数据格式错误') except Exception as e: logger.error(f"罚款支付回调处理异常: {str(e)}", exc_info=True) # 返回FAIL让微信重试 return self.wechat_response('FAIL', '处理异常') def verify_signature(self, xml_data): """验证微信支付签名(完全照搬 YajinHuitiao 的 verify_signature)""" try: root = ET.fromstring(xml_data) sign = root.find('sign').text if not sign: logger.error("签名字段不存在") return False # 获取所有参数并排除sign params = {} for child in root: if child.tag != 'sign' and child.text is not None: params[child.tag] = child.text # 按照参数名ASCII码从小到大排序 sorted_params = sorted(params.items(), key=lambda x: x[0]) # 拼接字符串 sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' # 计算签名 calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() is_valid = sign == calculated_sign if not is_valid: logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") return is_valid except Exception as e: logger.error(f"验证签名异常: {str(e)}") return False def wechat_response(self, return_code, return_msg): """返回微信支付回调响应(完全照搬 YajinHuitiao 的 wechat_response)""" xml_response = f""" """ logger.info(f"返回微信响应: {return_code} - {return_msg}") return HttpResponse(xml_response, content_type='application/xml') # ==================== 3. 支付轮询接口 ==================== class FaKuanPayPollView(APIView): """罚款支付轮询接口(借鉴 YajinLunxun)""" permission_classes = [IsAuthenticated] def post(self, request): try: dingdanid = request.data.get('dingdanid') if not dingdanid: return Response({'code': 400, 'message': '订单ID不能为空'}) order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid) if order.Status == 3: return Response({'code': 200, 'message': '支付成功'}) else: return Response({'code': 400, 'message': '订单尚未支付'}) except Czjilu.DoesNotExist: return Response({'code': 400, 'message': '订单不存在'}) except Exception as e: logger.error(f"轮询异常: {str(e)}") return Response({'code': 500, 'message': '服务器内部错误'}) # ==================== 4. 支付失败处理接口 ==================== class FaKuanPayFailView(APIView): """罚款支付失败处理接口(借鉴 YajinShibai)""" permission_classes = [IsAuthenticated] def post(self, request): try: dingdanid = request.data.get('dingdanid') if not dingdanid: return Response({'code': 400, 'message': '订单ID不能为空'}) order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid) if order.Status == 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': '服务器内部错误'}) 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.query.filter( UserUID=user.yonghuid, leixing=5, zhuangtai=9 ).exists() if unpaid_order: logger.warning(f"用户 {user.yonghuid} 存在未支付的考核订单") return Response({'code': 400, 'message': '您有未支付的考核订单,请先完成支付或取消'})""" # 2. 验证标签 try: chenghao = Chenghao.query.get(id=tag_id) logger.info(f"标签验证通过: {chenghao.mingcheng}") except Chenghao.DoesNotExist: logger.error(f"标签不存在: tag_id={tag_id}") return Response({'code': 404, 'message': '标签不存在'}) # 3. 检查用户是否已拥有该标签 if YonghuChenghao.query.filter(yonghu=user, chenghao=chenghao).exists(): logger.warning(f"用户 {user.yonghuid} 已拥有标签 {chenghao.mingcheng}") return Response({'code': 400, 'message': '您已拥有该标签,无需重复考核'}) # 4. 检查是否有未完成的申请 if ShenheJilu.query.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.query.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.query.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.query.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: KaohePayTemp.query.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: KaohePayTemp.query.filter(dingdan_id=dingdanid).delete() except Exception as del_e: logger.error(f"删除临时表记录失败: {str(del_e)}") logger.error(f"生成微信支付参数失败: {str(e)}", exc_info=True) return Response({'code': 500, 'message': f'支付参数生成失败: {str(e)}'}) return Response({ 'code': 200, 'message': '支付参数生成成功', 'payParams': pay_params, 'dingdanid': dingdanid }) except Exception as e: logger.error(f"考核支付下单异常: {str(e)}", exc_info=True) return Response({'code': 500, 'message': '服务器内部错误'}) def generate_wechat_pay_params(self, dingdanid, jine, openid, attach, pay_type): """生成微信JSAPI支付参数""" APPID = settings.WEIXIN_APPID MCHID = settings.WEIXIN_MCHID KEY = settings.WEIXIN_SHANGHUMIYAO if pay_type == 'kaohe': PAY_DESCRIPTION = settings.KAOHE_PAY_DESCRIPTION NOTIFY_URL = settings.KAOHE_PAY_NOTIFY_URL else: raise Exception('未知支付类型') nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) total_fee = int(jine * 100) trade_type = 'JSAPI' user_ip = self.get_client_ip() logger.info(f"微信支付下单参数: out_trade_no={dingdanid}, total_fee={total_fee}, openid={openid}, ip={user_ip}") # 第一次签名(统一下单) sign_data = { 'appid': APPID, 'mch_id': MCHID, 'nonce_str': nonce_str, 'body': PAY_DESCRIPTION, 'out_trade_no': dingdanid, 'total_fee': total_fee, 'spbill_create_ip': user_ip, 'notify_url': NOTIFY_URL, 'trade_type': trade_type, 'openid': openid, 'attach': attach, } sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) sign_string += f'&key={KEY}' sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() # 构建XML请求体 xml_data = f""" {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.query.get(dingdan_id=out_trade_no, leixing=5) logger.info(f"找到订单: {out_trade_no}, 当前状态: {order.Status}") except Czjilu.DoesNotExist: logger.error(f"考核订单不存在: {out_trade_no}") return self.wechat_response('FAIL', '订单不存在') if order.Status != 9: logger.warning(f"订单状态非未支付: {order.Status}, 订单号: {out_trade_no}") # 已支付的订单直接返回成功,避免微信重复回调 if order.Status == 3: logger.info(f"订单已支付,直接返回成功: {out_trade_no}") return self.wechat_response('SUCCESS', 'OK') return self.wechat_response('FAIL', '订单状态异常') # 验证金额 wechat_yuan = total_fee / 100 if abs(wechat_yuan - float(order.Amount)) > 0.01: logger.error(f"金额不一致: 订单金额={order.Amount}, 回调金额={wechat_yuan}, 订单号: {out_trade_no}") return self.wechat_response('FAIL', '金额不一致') logger.info(f"金额验证通过: {wechat_yuan}元") # 更新订单状态为已支付 order.Status = 3 order.save() logger.info(f"订单状态更新为已支付: {out_trade_no}") # 从临时表获取业务参数并创建审核记录 try: temp = KaohePayTemp.query.get(dingdan_id=out_trade_no, yonghuid=order.yonghuid) logger.info(f"找到临时表记录: {out_trade_no}, 用户ID: {order.yonghuid}") user = User.query.get(UserUID=order.yonghuid) logger.info(f"获取用户信息成功: {user.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.Status = 10 # 10表示支付成功但业务处理失败 order.save() logger.error(f"订单状态标记为异常(10): {out_trade_no}") except KaohePayTemp.DoesNotExist: logger.error(f"临时表无记录,订单号: {out_trade_no}, 用户ID: {order.yonghuid}") # 尝试从attach解析(兼容旧数据) attach_element = root.find('attach') if attach_element is not None and attach_element.text: try: attach_data = json.loads(attach_element.text) logger.info(f"从attach解析数据成功: {attach_data}") # 这里可以保留旧逻辑,但建议只记录日志 except Exception as e: logger.error(f"解析attach失败: {str(e)}") except Exception as e: logger.error(f"处理临时表或创建审核记录异常: {str(e)}", exc_info=True) # 标记订单异常 order.Status = 10 order.save() logger.error(f"订单状态标记为异常(10): {out_trade_no}") # 更新收支记录 try: jine_decimal = Decimal(str(order.Amount)) update_daily_income(jine_decimal) logger.info(f"更新每日收入成功: {jine_decimal}元") szjilu, created = Szjilu.query.get_or_create( id=1, defaults={ '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={settings.WEIXIN_SHANGHUMIYAO}' calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() result = sign == calculated_sign if not result: logger.error(f"签名验证失败: 原签名={sign}, 计算签名={calculated_sign}") return result except Exception as e: logger.error(f"签名验证异常: {str(e)}", exc_info=True) return False def wechat_response(self, return_code, return_msg): """构造微信回调响应XML""" xml = f""" """ logger.info(f"返回微信响应: {xml}") return HttpResponse(xml, content_type='application/xml') class KaohePayPollView(APIView): """考核支付轮询""" permission_classes = [IsAuthenticated] def post(self, request): dingdanid = request.data.get('dingdanid') if not dingdanid: return Response({'code': 400, 'message': '订单ID不能为空'}) try: order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5) if order.Status == 3: return Response({'code': 200, 'message': '支付成功'}) return Response({'code': 400, 'message': '订单尚未支付'}) except Czjilu.DoesNotExist: return Response({'code': 400, 'message': '订单不存在'}) class KaohePayFailView(APIView): """考核支付失败处理""" permission_classes = [IsAuthenticated] def post(self, request): dingdanid = request.data.get('dingdanid') if not dingdanid: return Response({'code': 400, 'message': '订单ID不能为空'}) try: order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5) if order.Status == 9: order.delete() return Response({'code': 200, 'message': '订单已删除'}) return Response({'code': 400, 'message': '订单状态无法删除'}) except Czjilu.DoesNotExist: return Response({'code': 400, 'message': '订单不存在'}) class TixianAssetView(APIView): """ 获取用户所有可提现资产及费率 请求方式:POST 认证:JWT Token 返回: { "code": 200, "msg": "success", "data": { "dashou_yue": "123.45", "dashou_yajin": "500.00", "shangjia_yue": "0.00", "guanshi_fenyong": "67.89", "zuzhang_fenyong": "0.00", "kaoheguan_fenyong": "0.00", "dashou_rate": 0.08, "dashou_yajin_rate": 0.05, "shangjia_rate": 0.06, "guanshi_rate": 0.08, "zuzhang_rate": 0.05, "kaoheguan_rate": 0.07 } } """ permission_classes = [IsAuthenticated] def post(self, request): user = request.user # 初始化返回数据,默认全部为0 data = { 'dashou_yue': '0.00', 'dashou_yajin': '0.00', 'shangjia_yue': '0.00', 'guanshi_fenyong': '0.00', 'zuzhang_fenyong': '0.00', 'kaoheguan_fenyong': '0.00', 'dashou_rate': 0.00, 'dashou_yajin_rate': 0.00, 'shangjia_rate': 0.00, 'guanshi_rate': 0.00, 'zuzhang_rate': 0.00, 'kaoheguan_rate': 0.00, } # ------------------- 打手资产 ------------------- try: dashou = user.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 # ------------------- 费率查询(从 CommissionRate 获取) ------------------- # 打手提现费率 (类型 '5') lilu_obj = CommissionRate.query.filter(Platform='5').first() data['dashou_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00 # 打手保证金提现费率 (类型 '11') lilu_obj = CommissionRate.query.filter(Platform='11').first() data['dashou_yajin_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00 # 商家提现费率 (类型 '10') lilu_obj = CommissionRate.query.filter(Platform='10').first() data['shangjia_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00 # 管事提现费率 (类型 '6') lilu_obj = CommissionRate.query.filter(Platform='6').first() data['guanshi_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00 # 组长提现费率 (类型 '8') lilu_obj = CommissionRate.query.filter(Platform='8').first() data['zuzhang_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00 # 审核官(考核官)提现费率 (类型 '9') lilu_obj = CommissionRate.query.filter(Platform='9').first() data['kaoheguan_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00 return Response({ 'code': 200, 'msg': '获取成功', 'data': data }, status=status.HTTP_200_OK) class GuanshiContactView(APIView): """ 管事获取自己的邀请人(组长)信息 路径: /api/yonghu/guanshi_contact 方法: POST 权限: JWT认证 请求体: 无(仅需携带合法token) 成功返回: code: 200, data: { uid: "1234567", // 组长用户ID nicheng: "张三", // 组长昵称 touxiang: "avatar/xxx.jpg" // 组长头像相对路径(前端拼接完整URL) } """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 获取当前登录用户(必须已通过JWT认证) current_user = request.user # 类型: User # 2. 校验是否为管事身份(存在管事扩展表记录) try: guanshi_profile = current_user.guanshi_profile except ObjectDoesNotExist: return Response({ 'code': 403, 'msg': '您还不是管事,无法获取组长信息', 'data': None }) # 3. 获取邀请人ID(管事扩展表中的 yaoqingren 字段存储的是邀请人的 yonghuid) inviter_yonghuid = guanshi_profile.yaoqingren if not inviter_yonghuid: return Response({ 'code': 404, 'msg': '未找到邀请人信息', 'data': None }) # 4. 根据邀请人用户ID查询用户主表 try: inviter_user = User.query.get(UserUID=inviter_yonghuid) except User.DoesNotExist: logger.error(f"邀请人用户不存在: yonghuid={inviter_yonghuid}") return Response({ 'code': 404, 'msg': '邀请人账号异常', 'data': None }) # 5. 获取邀请人昵称(从老板扩展表取,若不存在则给默认值) try: inviter_nicheng = inviter_user.boss_profile.nickname except ObjectDoesNotExist: inviter_nicheng = '组长' # 兜底昵称 # 6. 构造返回数据 response_data = { 'uid': inviter_user.yonghuid, 'nicheng': inviter_nicheng, 'touxiang': inviter_user.avatar or '' # 头像相对路径 } return Response({ 'code': 200, 'msg': '获取成功', 'data': response_data }) logger = logging.getLogger(__name__) # 恢复区间? def generate_tixian_id(): """生成唯一提现订单号:TX + 时间戳(13位) + 4位随机数""" timestamp = str(int(time.time() * 1000)) rand = ''.join(random.choices(string.digits, k=4)) return f'TX{timestamp}{rand}' class TixianShenqingV3View(APIView): """ 自动提现收款接口(新流程第二步) POST /yonghu/tixiansq 必须传 shenhe_danhao;审核状态须为 6=待收款;申请时已扣款,本接口不再扣款 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): shenhe_danhao = (request.data.get('shenhe_danhao') or '').strip() if shenhe_danhao: return process_audit_collect(request) return Response({ 'code': 7, 'msg': '缺少审核单号,请先提交提现申请(/yonghu/zddksh)并等待审核通过后再收款', }) class TixianCallbackV3View(APIView): """ 微信转账结果回调接口(最终状态通知) 处理微信异步发送的转账成功/失败结果 """ permission_classes = [] # 无需认证,签名验证保证安全 def post(self, request): headers = request.headers body = request.body.decode('utf-8') # 1. 验证签名 if not verify_wechat_sign(headers, body): return Response({'code': 'FAIL', 'message': '签名验证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 解密数据 try: data = json.loads(body) resource = data.get('resource') if not resource: return Response({'code': 'FAIL', 'message': 'resource不存在'}, status=status.HTTP_400_BAD_REQUEST) associated_data = resource.get('associated_data', '') nonce = resource.get('nonce') ciphertext = resource.get('ciphertext') if not nonce or not ciphertext: return Response({'code': 'FAIL', 'message': '解密字段缺失'}, status=status.HTTP_400_BAD_REQUEST) plaintext = decrypt_callback_ciphertext(associated_data, nonce, ciphertext) callback_data = json.loads(plaintext) except Exception as e: logger.error(f"回调解密失败: {str(e)}", exc_info=True) return Response({'code': 'FAIL', 'message': f'解密失败: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST) # 3. 获取关键信息 out_bill_no = callback_data.get('out_bill_no') # 商户提现单号(即 tixian_id) transfer_status = callback_data.get('transfer_status') # SUCCESS / FAIL wechat_transfer_no = callback_data.get('transfer_no') # 微信转账单号 fail_reason = callback_data.get('fail_reason', '') if not out_bill_no: return Response({'code': 'FAIL', 'message': 'out_bill_no缺失'}, status=status.HTTP_400_BAD_REQUEST) # 4. 幂等处理:只更新状态为0(处理中)的记录,已最终状态不再处理 with transaction.atomic(): try: auto_record = TixianAutoRecord.objects.select_for_update().get(tixian_id=out_bill_no) except TixianAutoRecord.DoesNotExist: # 记录不存在,视为已处理,返回成功 return Response({'code': 'SUCCESS', 'message': 'ok'}) # 如果已经是最新状态,直接返回成功(幂等) if auto_record.zhuangtai != 0: return Response({'code': 'SUCCESS', 'message': 'already processed'}) if transfer_status == 'SUCCESS': mark_transfer_success( auto_record, wechat_transfer_no=wechat_transfer_no, with_accounting=True, ) else: # transfer_status == 'FAIL' auto_record.zhuangtai = 2 auto_record.fail_reason = fail_reason or '微信转账失败' auto_record.save(update_fields=['zhuangtai', 'fail_reason']) release_collect_quota_for_record(auto_record) if not auto_record.shenhe_danhao: logger.error('回调失败但打款记录无审核单号 bill=%s', out_bill_no) else: try: audit = TixianShenheJilu.objects.select_for_update().get( shenhe_danhao=auto_record.shenhe_danhao ) close_audit_wechat_failed( audit, None, auto_record.fail_reason, ) except TixianShenheJilu.DoesNotExist: logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}') return Response({'code': 'SUCCESS', 'message': 'ok'}) class TixianQueRenAutoView(APIView): """ 用户确认提现结果 POST /yonghu/tixianqr 以微信官方查单状态为准:SUCCESS 落库成功;FAIL/CANCELLED 关闭审核单+退款;进行中则提示等待 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: tixian_id = request.data.get('tixian_id') result = request.data.get('result') # 1成功 0失败/取消 if not tixian_id or result is None: return Response({'code': 1, 'msg': '缺少参数'}, status=status.HTTP_400_BAD_REQUEST) try: result = int(result) except ValueError: return Response({'code': 2, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST) if result not in [0, 1]: return Response({'code': 3, 'msg': 'result值无效'}, status=status.HTTP_400_BAD_REQUEST) user_main = request.user with transaction.atomic(): try: auto_record = TixianAutoRecord.objects.select_for_update().get( tixian_id=tixian_id, yonghuid=user_main.yonghuid, ) except TixianAutoRecord.DoesNotExist: return Response({'code': 4, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND) if result == 1: if auto_record.zhuangtai == 1: return Response({'code': 0, 'msg': '提现成功', 'data': None}) wx_data = query_wechat_transfer_bill(tixian_id) if wx_data is None: return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'}) if wx_data.get('_not_found'): return Response({'code': 12, 'msg': '收款处理中,请稍后再试'}) state = (wx_data.get('state') or '').upper() if state in WX_STATE_SUCCESS: mark_transfer_success( auto_record, wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'), with_accounting=True, ) return Response({'code': 0, 'msg': '提现成功', 'data': None}) if state in WX_STATE_FAIL: fail_reason = wx_data.get('fail_reason') or wx_data.get('close_reason') or state auto_record.zhuangtai = 2 auto_record.fail_reason = str(fail_reason)[:500] auto_record.save(update_fields=['zhuangtai', 'fail_reason']) release_collect_quota_for_record(auto_record) if auto_record.shenhe_danhao: audit = TixianShenheJilu.objects.select_for_update().get( shenhe_danhao=auto_record.shenhe_danhao, ) close_audit_wechat_failed(audit, None, auto_record.fail_reason) return Response({'code': 400, 'msg': '微信已确认转账失败,该笔提现已关闭,请联系客服处理'}) if state in WX_STATE_WAIT or state in WX_STATE_CANCELING: return Response({'code': 12, 'msg': '收款处理中,请在微信中完成或稍后再试'}) return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'}) # 用户报告取消/失败:以微信查单为准,不本地盲目标记 if auto_record.zhuangtai == 2: return Response({'code': 0, 'msg': '提现已取消', 'data': None}) if auto_record.zhuangtai == 1: return Response({'code': 0, 'msg': '提现成功', 'data': None}) wx_data = query_wechat_transfer_bill(tixian_id) if wx_data is None: return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试,切勿重复操作'}) if wx_data.get('_not_found'): return Response({'code': 12, 'msg': '收款处理中,请稍后再试'}) state = (wx_data.get('state') or '').upper() if state in WX_STATE_SUCCESS: mark_transfer_success( auto_record, wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'), with_accounting=True, ) return Response({'code': 0, 'msg': '提现成功', 'data': None}) if state in WX_STATE_FAIL: fail_reason = wx_data.get('fail_reason') or wx_data.get('close_reason') or '用户取消收款' auto_record.zhuangtai = 2 auto_record.fail_reason = str(fail_reason)[:500] auto_record.save(update_fields=['zhuangtai', 'fail_reason']) release_collect_quota_for_record(auto_record) if auto_record.shenhe_danhao: audit = TixianShenheJilu.objects.select_for_update().get( shenhe_danhao=auto_record.shenhe_danhao, ) close_audit_wechat_failed(audit, None, auto_record.fail_reason) return Response({'code': 0, 'msg': '提现已关闭,请联系客服处理'}) if state in WX_STATE_WAIT or state in WX_STATE_CANCELING: return Response({'code': 12, 'msg': '收款仍在进行中,请在微信中操作或稍后再试'}) return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'}) except Exception as e: logger.error(f'提现确认异常: {str(e)}', exc_info=True) return Response({'code': 99, 'msg': '系统错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)