Files
Django/users/views/kefu.py

3795 lines
167 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""users.views.kefu - auto-generated by split script."""
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.money import yuan_to_fen
from utils.fadan_utils import check_fadan_qiangdan_eligible
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from users.fadan_fenhong_utils import process_fadan_fenhong
from orders.utils import (
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from backend.utils import (
update_dashou_daily_by_action, update_guanshi_daily_by_action,
update_shangjia_daily, update_zuzhang_daily_by_action,
verify_kefu_permission
)
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from ..models import (
UserBoss, UserDashou, UserShangjia, UserGuanshi,
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
RankingRecord
)
from users.business_models import User
from ..tixian_shenhe_services import (
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
mark_transfer_success, release_collect_quota_for_record,
close_audit_wechat_failed, query_wechat_transfer_bill,
check_shijidaozhang_within_limit,
check_dashou_trial_blocks_commission_withdraw,
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
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__)
class KefuLoginView(APIView):
"""
客服/管理员登录接口(支持手机号重复)
请求POST /yonghu/kefujinru
请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"}
返回:
code=0 成功data包含token,nicheng,yonghuid
code=1 失败msg区分用户不存在/密码错误/二级密码错误)
code=4 账号被封禁
管理员IsSuperuser 或 UserType='admin')无需 erjimima 和 KefuProfile。
"""
throttle_classes = [AnonRateThrottle] # 限制匿名请求频率
permission_classes = [AllowAny] # 任何人都可访问
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
password = request.data.get('password', '')
erjimima = request.data.get('erjimima', '')
if not phone or not password:
return Response({
'code': 1,
'msg': '请填写手机号和密码',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 获取客户端真实IP
client_ip = self.get_client_ip(request)
# 3. 查询该手机号下所有账号(客服 + 管理员)
# 先查客服(有 KefuProfile再查管理员IsSuperuser 或 UserType='admin'
user_mains = list(
User.query.filter(
Phone=phone,
KefuProfile__isnull=False,
).select_related('KefuProfile')
)
# 如果没有客服账号,尝试查找管理员账号
if not user_mains:
admin_candidates = User.query.filter(Phone=phone)
for admin_user in admin_candidates:
if admin_user.IsSuperuser or admin_user.UserType == 'admin':
user_mains = [admin_user]
break
if not user_mains:
logger.warning(f"登录失败,用户不存在: {phone}, IP: {client_ip}")
return Response({
'code': 1,
'msg': '用户不存在',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 遍历所有匹配的账号,验证密码和二级密码
target_user = None
target_kefu = None
password_matched = False
for user in user_mains:
# 验证主密码bcrypt 哈希验证)
if not user.CheckPassword(password):
continue
password_matched = True
# 管理员:跳过二级密码和客服扩展表检查
is_admin = user.IsSuperuser or user.UserType == 'admin'
if is_admin:
target_user = user
target_kefu = None
break
# 客服:验证二级密码
kefu = user.KefuProfile
stored_erji = kefu.erjimima or ''
if stored_erji.startswith(('$2b$', '$2a$')) and len(stored_erji) == 60:
import bcrypt as _bcrypt
if not _bcrypt.checkpw(erjimima.encode('utf-8'), stored_erji.encode('utf-8')):
continue
elif stored_erji != erjimima:
continue
# 如果账号被禁用,记录但继续查找
if kefu.zhuangtai != 1:
if target_user is None:
target_user = user
target_kefu = kefu
continue
# 找到完全匹配且状态正常的账号,立即使用
target_user = user
target_kefu = kefu
break
else:
# 循环结束未找到正常账号
if target_user and target_kefu:
# 只有被禁用的账号匹配,返回封禁提示
logger.warning(f"登录失败,客服账号已禁用: {phone}, IP: {client_ip}")
return Response({
'code': 4,
'msg': '账号已被封禁',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
elif password_matched:
# 密码正确但二级密码错误
logger.warning(f"登录失败,二级密码错误: {phone}, IP: {client_ip}")
return Response({
'code': 1,
'msg': '二级密码错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
else:
# 密码错误
logger.warning(f"登录失败,密码错误: {phone}, IP: {client_ip}")
return Response({
'code': 1,
'msg': '密码错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 5. 更新IP和最后登录时间
target_user.IP = client_ip
target_user.UserLastLoginDate = timezone.now()
target_user.save(update_fields=['IP', 'UserLastLoginDate'])
# 6. 生成JWT token
refresh = RefreshToken.for_user(target_user)
token = str(refresh.access_token)
# 7. 返回成功数据(管理员无 KefuProfilenicheng 用 UserName 或 Phone 兜底)
nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone)
return Response({
'code': 0,
'msg': '登录成功',
'data': {
'token': token,
'nicheng': nicheng,
'yonghuid': target_user.UserUID,
}
})
def get_client_ip(self, request):
"""
获取客户端真实IP
"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
if ip:
return ip
return request.META.get('REMOTE_ADDR', '0.0.0.0')
class KefuStatsView(APIView):
"""
客服统计数据接口
请求POST /yonghu/kfjrhqzl
请求体:{"phone": "13800138000"}
认证:必须携带有效的 JWT token放在 Authorization 头)
返回:
code=0 成功data 包含 jinrichuli, jinyuechuli, zongchuli
验证失败统一返回 401不透露具体原因
"""
permission_classes = [IsAuthenticated] # 必须登录
def post(self, request):
# 1. 获取请求中的 phone
phone = request.data.get('phone', '').strip()
if not phone:
# 缺少参数,但按约定返回 401可以改为 400但为了统一用401
logger.warning(f"统计接口缺少 phone 参数,用户: {request.user.UserUID}")
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 获取当前登录用户(从 JWT 中解析)
current_user = request.user
# 3. 验证前端传来的 phone 是否与当前用户的 phone 一致
if current_user.Phone != phone:
# 不一致,可能 token 被冒用或参数错误
logger.warning(f"统计接口 phone 不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 4. 验证是否为后台客服账号
from jituan.services.admin_context import is_kefu_backend_account, is_system_super_admin
if not is_kefu_backend_account(current_user):
logger.warning(f"统计接口用户类型非客服: {current_user.UserUID}")
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 5. 尝试获取客服扩展表数据(管理员跳过)
is_admin = is_system_super_admin(current_user)
if is_admin:
return Response({
'code': 0,
'data': {
'jinrichuli': 0,
'jinyuechuli': 0,
'zongchuli': 0,
}
}, status=status.HTTP_200_OK)
try:
kefu_profile = current_user.KefuProfile # OneToOneField 反向关系
except UserKefu.DoesNotExist:
logger.warning(f"统计接口客服扩展表不存在: {current_user.UserUID}")
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 6. 返回统计数据(字段名与前端约定一致)
return Response({
'code': 0,
'data': {
'jinrichuli': kefu_profile.jinrichuli,
'jinyuechuli': kefu_profile.jinyuechuli,
'zongchuli': kefu_profile.zongchuli,
}
}, status=status.HTTP_200_OK)
class KefuGetOrderTypesView(APIView):
"""
客服获取订单类型列表接口(平台订单用)
请求POST /yonghu/kfhqptddlx
参数:{"phone": "13800138000"}
认证JWT
返回code=0 + 类型列表
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
if not phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
current_user = request.user
# 2. 验证手机号一致性
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 验证用户类型及客服状态
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 管理员跳过客服扩展表检查
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
if not is_admin:
try:
kefu_profile = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu_profile.zhuangtai != 1:
logger.warning(f"客服账号已被禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 4. 查询订单类型(商品类型)
try:
# 只查询需要的字段,减少数据传输
types_qs = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url')
type_list = [
{
'id': t.id,
'biaoti': t.jieshao or '', # 假设商品类型的标题字段为 jieshao
'tupian_url': t.tupian_url or ''
}
for t in types_qs
]
return Response({'code': 0, 'data': type_list})
except Exception as e:
logger.error(f"查询商品类型表失败: {str(e)}")
# 表不存在或查询失败,返回空列表
return Response({'code': 0, 'data': []})
class KefuGetOrderListView(APIView):
"""
客服获取平台订单列表接口(发单平台=1非跨平台
需要权限002ab平台订单管理
请求参数:
username: 客服账号(用于权限校验)
page: 页码
pageSize: 每页条数
dingdan_id: 订单ID精确搜索
laoban_id: 老板ID搜索平台扩展表中的老板
dashou_id: 打手ID搜索主表中的接单打手
leixing: 商品类型ID
zhuangtai: 状态(多个用逗号分隔,如 "1,7"
date: 日期YYYY-MM-DD筛选订单创建时间
nicheng: 游戏昵称(订单主表的 nicheng 字段)
jieshao: 订单介绍(订单主表的 jieshao 字段)
clkf: 处理客服(订单主表的 clkf 字段)
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
start_total = time.time()
# 1. 获取前端参数
username = request.data.get('phone', '').strip()
if not username:
return Response({'code': 400, 'msg': '缺少username'})
# 2. 公共权限校验
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 3. 检查平台订单管理权限002ab
if '002ab' not in permissions:
return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
# 4. 获取分页和筛选参数
page = int(request.data.get('page', 1))
page_size = int(request.data.get('pageSize', 20))
dingdan_id = request.data.get('dingdan_id', '').strip()
laoban_id = request.data.get('laoban_id', '').strip()
dashou_id = request.data.get('dashou_id', '').strip()
leixing = request.data.get('leixing')
zhuangtai = request.data.get('zhuangtai', '')
date = request.data.get('date', '').strip()
# 新增筛选条件
nicheng = request.data.get('nicheng', '').strip()
jieshao = request.data.get('jieshao', '').strip()
clkf = request.data.get('clkf', '').strip()
# ---------- 查询条件 ----------
# 基础条件:平台发单 + 兼容历史 Platform 为空
from jituan.services.club_context import platform_order_base_q
base_q = platform_order_base_q()
if dingdan_id:
q_conditions = base_q & Q(OrderID=dingdan_id)
else:
q_conditions = base_q
if leixing is not None and str(leixing).strip() != '':
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)
from jituan.services.club_context import (
count_order_status_buckets,
order_scope_qs,
paginate_fluent_query,
)
from orders.models import PlatformOrderExt
# 按俱乐部过滤订单列表
stats_qs = order_scope_qs(request).filter(q_conditions)
stats = stats_qs.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 = count_order_status_buckets(q_conditions, request)
list_qs = order_scope_qs(request).filter(q_conditions).order_by('-CreateTime')
total_count, orders_page = paginate_fluent_query(list_qs, page, page_size)
if total_count > 0 and not orders_page:
start = (max(1, page) - 1) * page_size
orders_page = list(list_qs._qs[start:start + page_size])
order_ids = [o.OrderID for o in orders_page if o.OrderID]
ext_map = {}
if order_ids:
for ext in PlatformOrderExt.query.filter(Order__OrderID__in=order_ids).select_related('Order'):
if ext.Order:
ext_map[ext.Order.OrderID] = ext
laoban_ids = [
ext.BossID for ext in ext_map.values() if ext.BossID
]
nickname_map = {}
if laoban_ids:
users = User.query.filter(UserUID__in=laoban_ids).select_related('BossProfile').only(
'UserUID', 'BossProfile__nickname'
)
for user in users:
nickname_map[user.UserUID] = (user.BossProfile.nickname or '') if getattr(user, 'BossProfile', None) else ''
result_list = []
for order in orders_page:
ext = ext_map.get(order.OrderID)
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
})
from jituan.services.club_user_access import list_response_meta
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,
**list_response_meta(request),
}
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. 获取前端参数(兼容 phone / username
username = request.data.get('username', '').strip() or 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. 检查商家订单管理权限(使用 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()
# ---------- 查询条件 ----------
from jituan.services.club_context import merchant_order_base_q
base_q = merchant_order_base_q()
if dingdan_id:
q_conditions = base_q & Q(OrderID=dingdan_id)
else:
q_conditions = base_q
if leixing is not None and str(leixing).strip() != '':
try:
leixing_int = int(leixing)
q_conditions &= Q(ProductTypeID=leixing_int)
except (ValueError, TypeError):
pass
if zhuangtai and zhuangtai != 'all':
try:
status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
if status_list:
q_conditions &= Q(Status__in=status_list)
except (ValueError, TypeError):
pass
if dashou_id:
q_conditions &= Q(PlayerID=dashou_id)
if shangjia_id:
q_conditions &= Q(shangjia_kuozhan__MerchantID=shangjia_id)
if date:
q_conditions &= Q(CreateTime__date=date)
if nicheng:
q_conditions &= Q(Nickname__icontains=nicheng)
if jieshao:
q_conditions &= Q(Description__icontains=jieshao)
if clkf:
q_conditions &= Q(AssignedCS__icontains=clkf)
if shangjia_nicheng:
# 通过商家扩展表关联 UserShangjia 的昵称
q_conditions &= Q(shangjia_kuozhan__MerchantID__in=User.query.filter(
ShopProfile__nicheng__icontains=shangjia_nicheng
).values_list('UserUID', flat=True))
from jituan.services.club_context import (
count_order_status_buckets,
order_scope_qs,
paginate_fluent_query,
)
from orders.models import MerchantOrderExt
stats_qs = order_scope_qs(request).filter(q_conditions)
stats = stats_qs.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 = count_order_status_buckets(q_conditions, request)
list_qs = order_scope_qs(request).filter(q_conditions).order_by('-CreateTime')
total_count, orders_page = paginate_fluent_query(list_qs, page, page_size)
if total_count > 0 and not orders_page:
start = (max(1, page) - 1) * page_size
orders_page = list(list_qs._qs[start:start + page_size])
order_ids = [o.OrderID for o in orders_page if o.OrderID]
ext_map = {}
if order_ids:
for ext in MerchantOrderExt.query.filter(Order__OrderID__in=order_ids).select_related('Order'):
if ext.Order:
ext_map[ext.Order.OrderID] = ext
shangjia_ids = [
ext.MerchantID for ext in ext_map.values() if ext.MerchantID
]
nickname_map = {}
if shangjia_ids:
users = User.query.filter(UserUID__in=shangjia_ids).select_related('ShopProfile').only(
'UserUID', 'ShopProfile__nicheng'
)
for user in users:
nickname_map[user.UserUID] = user.ShopProfile.nicheng or '' if hasattr(user, 'ShopProfile') else ''
result_list = []
for order in orders_page:
ext = ext_map.get(order.OrderID)
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
})
from jituan.services.club_user_access import list_response_meta
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,
**list_response_meta(request),
}
logger.info(f"商家订单接口总耗时: {time.time()-start_total:.3f}s")
return Response({'code': 0, 'data': response_data})
class KefuChangeDashouView(APIView):
"""
客服更换打手接口
请求POST /yonghu/kefughds
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID",
"new_dashou_id": "新打手ID"
}
认证JWT
返回code=0 成功
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
new_dashou_id = request.data.get('new_dashou_id', '').strip()
if not phone or not dingdan_id or not new_dashou_id:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单
try:
order = Order.query.get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 4. 验证订单状态是否允许更换打手进行中2 或 结算中8
if order.Status not in [2, 8]:
return Response({'code': 400, 'msg': '订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 查询新打手
try:
new_dashou_user = User.query.get(UserUID=new_dashou_id)
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
# 6. 验证打手扩展表
try:
new_dashou_profile = new_dashou_user.DashouProfile
except ObjectDoesNotExist:
return Response({'code': 404, 'msg': '打手信息不完整'}, status=status.HTTP_404_NOT_FOUND)
# 7. 验证打手状态打手状态和账号状态必须为1
if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1:
return Response({'code': 400, 'msg': '打手状态不允许接单'}, status=status.HTTP_400_BAD_REQUEST)
# 8. 验证打手积分是否足够至少5分
if new_dashou_profile.jifen < 5:
return Response({'code': 400, 'msg': '打手积分不足,无法接单'}, status=status.HTTP_400_BAD_REQUEST)
# 8. 根据订单的抢单要求类型进行额外验证
yaoqiuleixing = order.GrabRequirement
if yaoqiuleixing == 1: # 会员抢单
huiyuan_id = order.MembershipID
if not huiyuan_id:
return Response({'code': 400, 'msg': '订单会员ID为空'}, status=status.HTTP_400_BAD_REQUEST)
try:
# 查询打手的会员购买记录
huiyuan_goumai = Huiyuangoumai.query.get(
huiyuan_id=huiyuan_id,
yonghu_id=new_dashou_id # 假设 yonghu_id 字段存储用户ID
)
except Huiyuangoumai.DoesNotExist:
return Response({'code': 400, 'msg': '打手未购买该订单要求的会员'}, status=status.HTTP_400_BAD_REQUEST)
# 检查会员是否过期(调用模型方法)
is_expired = huiyuan_goumai.jiance_shifou_daoqi()
if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1:
return Response({'code': 400, 'msg': '打手的会员已过期'}, status=status.HTTP_400_BAD_REQUEST)
elif yaoqiuleixing == 2: # 押金抢单
order_yajin = order.CommissionReq # 假设订单的 yongjin 字段存储押金要求
if order_yajin is None:
return Response({'code': 400, 'msg': '订单押金为空'}, status=status.HTTP_400_BAD_REQUEST)
if new_dashou_profile.yajin < order_yajin:
return Response({'code': 400, 'msg': '打手押金不足'}, status=status.HTTP_400_BAD_REQUEST)
else:
return Response({'code': 400, 'msg': '订单抢单要求类型错误'}, status=status.HTTP_400_BAD_REQUEST)
# 9. 所有验证通过,开始更换打手(使用事务)
with transaction.atomic():
# 9.1 更新新打手扩展表
new_dashou_profile.zhuangtai = 0 # 设为接单中
new_dashou_profile.jiedanzongliang += 1
new_dashou_profile.jinrijiedan += 1
new_dashou_profile.jinyuejiedan += 1
new_dashou_profile.save()
# 9.2 处理原打手(如果存在)
old_dashou_id = order.PlayerID
if old_dashou_id:
try:
old_dashou_user = User.query.get(UserUID=old_dashou_id)
old_dashou_profile = old_dashou_user.DashouProfile
old_dashou_profile.zhuangtai = 1 # 恢复空闲
old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1业务逻辑
old_dashou_profile.save()
logger.info(f"原打手 {old_dashou_id} 状态已更新")
except (User.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"原打手 {old_dashou_id} 不存在或信息不完整,跳过更新")
except Exception as e:
logger.error(f"更新原打手时异常: {str(e)}", exc_info=True)
# 9.3 更新订单
order.PlayerID = new_dashou_id
order.Status = 2 # 进行中
order.AssignedCS = current_user.UserUID # 记录处理客服ID
order.save()
return Response({'code': 0, 'msg': '更换打手成功'})
class KefuRecoverOrderView(APIView):
"""
客服恢复订单接口仅状态9可恢复
请求POST /yonghu/kefuhf
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID"
}
认证JWT
返回code=0 成功
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单
try:
order = Order.query.get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 4. 验证订单状态是否为9未付款
if order.Status != 9:
return Response({'code': 400, 'msg': '订单状态不允许恢复'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 根据是否有指定ID决定新状态
if order.AssignedID:
new_status = 7 # 指定中
else:
new_status = 1 # 下单中
# 6. 更新订单
order.Status = new_status
order.AssignedCS = current_user.UserUID
order.save()
return Response({'code': 0, 'msg': '恢复成功', 'data': {'new_status': new_status}})
class KefuPlatformRefundView(APIView):
"""
客服平台订单退款接口(调用微信支付)
请求POST /yonghu/kefupttk
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID",
"tuikuan_liyou": "退款理由(可选)"
}
认证JWT
返回code=0 成功code=其他 失败
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单并加锁
try:
with transaction.atomic():
order = Order.query.select_related('pingtai_kuozhan').select_for_update().get(OrderID=dingdan_id)
# 4. 验证平台订单
if order.Platform != 1:
return Response({'code': 400, 'msg': '该订单不是平台订单'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 验证状态允许退款根据模型状态值1下单中2进行中3已完成4退款审核5已退款6退款失败7指定中8结算中
allowed_statuses = [1, 2, 4, 7, 8] # 允许退款的已支付或处理中状态
if order.Status not in allowed_statuses:
return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
# 6. 获取微信交易号(如果存在)
wechat_transaction_id = getattr(order, 'WechatTransactionID', None)
# 7. 调用微信退款接口
refund_result = self.call_wechat_refund(
dingdan_id=order.OrderID,
jine=order.Amount,
transaction_id=wechat_transaction_id
)
if refund_result['code'] != 0:
# 微信退款失败,直接返回错误,数据库不更新
logger.error(f"微信退款失败,订单 {dingdan_id},原因:{refund_result['msg']}")
return Response({'code': 400, 'msg': refund_result['msg']}, status=status.HTTP_400_BAD_REQUEST)
# 8. 更新订单状态为5已退款
order.Status = 5
order.AssignedCS = current_user.Phone # 记录处理客服手机号
if tuikuan_liyou:
order.RefundReason = tuikuan_liyou
order.save()
# 9. 更新打手数据
jiedan_dashou_id = order.PlayerID
if jiedan_dashou_id:
try:
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
dashou_profile = dashou_user.DashouProfile
dashou_profile.tuikuanliang += 1
dashou_profile.zhuangtai = 1 # 打手状态设为空闲
dashou_profile.save()
logger.info(f"平台退款:打手 {jiedan_dashou_id} 退款订单+1")
except (User.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"平台退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
# ========== 新增:商品和店铺每日统计 ==========
try:
# 商品每日统计只要有商品ID就执行
if order.ProductID:
# 从扩展表获取已计算好的收益值(下单时已存储)
pingtai_shouyi = Decimal('0.00')
dianpu_shouyi = Decimal('0.00')
if hasattr(order, 'pingtai_kuozhan'):
pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_shangpin_daily_stat(
shangpin_id=order.ProductID,
caozuo_leixing=3, # 3 = 退款
dingdan_jiage=order.Amount,
pingtai_shouyi=pingtai_shouyi,
dianpu_zongshouyi=dianpu_shouyi
)
logger.info(f"商品统计更新完成商品ID: {order.ProductID}")
# 店铺每日统计仅当存在店铺ID时执行
dianpu_id = None
if hasattr(order, 'pingtai_kuozhan'):
dianpu_id = order.pingtai_kuozhan.ShopID
if dianpu_id:
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_dianpu_daily_stat(
dianpu_id=dianpu_id,
caozuo_leixing=3, # 3 = 退款
dingdan_jiage=order.Amount,
dianpu_shouyi=dianpu_shouyi
)
logger.info(f"店铺统计更新完成店铺ID: {dianpu_id}")
except Exception as e:
logger.error(f"商品/店铺统计更新失败: {e}")
logger.error(traceback.format_exc())
# 统计失败不影响主流程
# =============================================
# 10. 更新退款记录表(假设存在 RefundRecord 模型,字段需根据实际调整)
#更新支出记录
update_daily_payout(order.Amount, getattr(order, 'ClubID', None))
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 = yuan_to_fen(jine)
refund_fee = total_fee
refund_desc = '客服退款'
# 构造请求参数
params = {
'appid': appid,
'mch_id': mch_id,
'nonce_str': self.generate_nonce_str(),
'out_refund_no': out_refund_no,
'total_fee': total_fee,
'refund_fee': refund_fee,
'refund_desc': refund_desc,
}
if transaction_id:
params['transaction_id'] = transaction_id
else:
params['out_trade_no'] = dingdan_id # 使用订单ID作为商户订单号
# 生成签名
sorted_keys = sorted(params.keys())
stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys])
stringSignTemp = f"{stringA}&key={key}"
sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
params['sign'] = sign
xml_data = self.dict_to_xml(params)
url = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
try:
response = requests.post(
url,
data=xml_data,
cert=(cert_path, key_path),
timeout=10
)
result = xmltodict.parse(response.content)['xml']
logger.info(f"微信退款响应: {result}")
except Exception as e:
logger.error(f"微信退款请求异常: {str(e)}", exc_info=True)
return {'code': 500, 'msg': '微信退款请求异常'}
if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
return {'code': 0, 'msg': 'success', 'refund_id': out_refund_no}
else:
err_msg = result.get('err_code_des', result.get('return_msg', '未知错误'))
logger.warning(f"微信退款失败: {err_msg}")
return {'code': 400, 'msg': err_msg}
def generate_nonce_str(self):
return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
def generate_refund_no(self, dingdan_id):
timestamp = str(int(time.time()))
rand = ''.join(random.choices('0123456789', k=4))
return f"{dingdan_id}REF{timestamp}{rand}"
def dict_to_xml(self, data):
xml = ['<xml>']
for k, v in data.items():
xml.append(f'<{k}>{v}</{k}>')
xml.append('</xml>')
return ''.join(xml)
class KefuRejectRefundView(APIView):
"""
客服拒绝退款接口
请求POST /yonghu/kefujjtk
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID",
"jujue_liyou": "拒绝理由(可选)"
}
认证JWT
返回code=0 成功
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
jujue_liyou = request.data.get('jujue_liyou', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单并加锁
try:
with transaction.atomic():
order = Order.objects.select_for_update().get(OrderID=dingdan_id)
# 4. 验证订单状态为退款审核中4
if order.Status != 4:
return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST)
# 获取必要字段
jiedan_dashou_id = order.PlayerID
dashou_fencheng = order.PlayerCommission
fadan_pingtai = order.Platform
# 5. 验证打手分成金额是否存在
if dashou_fencheng is None:
return Response({'code': 400, 'msg': '订单缺少打手分成金额'}, status=status.HTTP_400_BAD_REQUEST)
# 6. 更新订单状态为退款失败6
order.Status = 6
order.AssignedCS = current_user.Phone # 记录处理客服手机号
if jujue_liyou:
order.RefundReason = jujue_liyou
order.save()
# 7. 给打手结算(返还分成)
if jiedan_dashou_id:
try:
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
dashou_profile = dashou_user.DashouProfile
# 增加打手相关统计数据
dashou_profile.chengjiaozongliang += 1
dashou_profile.yue += dashou_fencheng
dashou_profile.zonge += dashou_fencheng
dashou_profile.jinrishouyi += dashou_fencheng
dashou_profile.jinyueshouyi += dashou_fencheng
dashou_profile.save()
logger.info(f"拒绝退款:打手 {jiedan_dashou_id} 获得分成 {dashou_fencheng}")
except (User.DoesNotExist, AttributeError):
logger.warning(f"拒绝退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算")
# 8. 如果是商家发单,更新商家成交订单数量
if fadan_pingtai == 2:
try:
# 获取商家订单扩展表
shangjia_kuozhan = order.shangjia_kuozhan
shangjia_id = shangjia_kuozhan.MerchantID
if shangjia_id:
shangjia_user = User.query.get(UserUID=shangjia_id)
shangjia_profile = shangjia_user.ShopProfile
shangjia_profile.chengjiao += 1
shangjia_profile.save()
logger.info(f"拒绝退款:商家 {shangjia_id} 成交订单+1")
# ✅ 新方法(行为驱动,更安全)
update_shangjia_daily(
yonghuid=shangjia_id,
amount=Decimal(str(order.Amount)),
action=2, # 2 = 结算
order_id=dingdan_id,
)
except (ObjectDoesNotExist, User.DoesNotExist, AttributeError):
logger.warning(f"拒绝退款:商家信息不存在,跳过商家更新")
# 9. 更新退款记录表
try:
refund_record = RefundRecord.query.get(OrderID=dingdan_id)
refund_record.ApplyStatus = 2 # 2表示拒绝
refund_record.ProcessorID = phone
refund_record.RejectReason = jujue_liyou # 记录驳回理由(假设模型有此字段)
refund_record.save()
logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}")
except RefundRecord.DoesNotExist:
# 如果退款记录不存在,可以创建一条记录(可选)
RefundRecord.query.create(
OrderID=dingdan_id,
PlayerID=jiedan_dashou_id or '',
ApplicantID='',
ProcessorID=phone,
Reason=jujue_liyou or '客服拒绝退款',
ApplyStatus=2,
Amount=order.Amount,
PlayerCommission=dashou_fencheng or 0,
Description=order.Description or '',
Remark='客服拒绝退款',
Nickname=order.Nickname or '',
RejectReason=jujue_liyou or '',
)
except Exception as e:
logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True)
# 🔥 新增:调用打手每日统计(成交)
try:
update_dashou_daily_by_action(
yonghuid=order.PlayerID,
amount=dashou_fencheng,
action=2 # 2 表示成交(结算)
)
logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# 不影响主流程
# 10. 更新客服统计数据
kefu.jinrichuli += 1
kefu.jinyuechuli += 1
kefu.zongchuli += 1
kefu.save()
return Response({'code': 0, 'msg': '拒绝退款成功'})
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class KefuMerchantRefundView(APIView):
"""
客服商家订单退款接口(发单平台=2
请求POST /yonghu/kefusjtk
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID",
"tuikuan_liyou": "退款理由(可选)"
}
认证JWT
返回code=0 成功
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 客服身份验证(同前,省略以节省篇幅,但实际必须完整)
current_user = request.user
# ... 验证
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 查询订单,预加载商家扩展表
try:
order = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 验证商家订单
if order.Platform != 2:
return Response({'code': 400, 'msg': '该订单不是商家订单'}, status=status.HTTP_400_BAD_REQUEST)
# 验证状态允许退款
allowed_statuses = [1, 7, 2, 8, 4]
if order.Status not in allowed_statuses:
return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
# 获取必要数据
jiedan_dashou_id = order.PlayerID
jine = order.Amount
if jine is None:
return Response({'code': 400, 'msg': '订单金额缺失'}, status=status.HTTP_400_BAD_REQUEST)
try:
shangjia_kuozhan = order.shangjia_kuozhan
shangjia_id = shangjia_kuozhan.MerchantID
except ObjectDoesNotExist:
return Response({'code': 400, 'msg': '订单商家信息不存在'}, status=status.HTTP_400_BAD_REQUEST)
with transaction.atomic():
# 更新订单
order.Status = 5
order.AssignedCS = current_user.Phone
if tuikuan_liyou:
order.RefundReason = tuikuan_liyou
order.save()
# 更新打手
if jiedan_dashou_id:
try:
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
dashou_profile = dashou_user.DashouProfile
dashou_profile.tuikuanliang += 1
dashou_profile.zhuangtai = 1
dashou_profile.save()
except (User.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"商家退款:打手 {jiedan_dashou_id} 不存在,跳过更新")
# 更新商家(客服额度出资的订单不退商家余额,仅释放子客服额度)
try:
from merchant_ops.services.wallet import is_staff_quota_order
shangjia_user = User.query.get(UserUID=shangjia_id)
shangjia_profile = shangjia_user.ShopProfile
shangjia_profile.tuikuan += 1
if not is_staff_quota_order(dingdan_id):
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 = 退款
order_id=dingdan_id,
)
# 调用打手每日统计(成交)
try:
update_dashou_daily_by_action(
yonghuid=order.PlayerID,
amount=order.PlayerCommission,
action=3 # 3 表示退款(退款)
)
logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# 不影响主流程
# 管理员无客服扩展表,跳过计数器更新
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
if not is_admin:
kefu = current_user.KefuProfile
kefu.jinrichuli += 1
kefu.jinyuechuli += 1
kefu.zongchuli += 1
kefu.save()
return Response({'code': 0, 'msg': '退款成功'})
class KefuRejectSettlementView(APIView):
"""
客服拒绝结算接口仅平台订单状态2/8
请求POST /yonghu/kefujjjs
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID",
"tkly": "拒绝结算理由(必填)"
}
认证JWT
返回code=0 成功,其他失败
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
tkly = request.data.get('tkly', '').strip()
if not phone or not dingdan_id or not tkly:
return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单,预加载平台扩展表
try:
order = Order.query.select_related('pingtai_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 4. 校验订单条件平台发单1且状态为2或8
if order.Platform != 1 or order.Status not in [2, 8]:
return Response({'code': 400, 'msg': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算'}, status=status.HTTP_400_BAD_REQUEST)
jiedan_dashou_id = order.PlayerID
if not jiedan_dashou_id:
return Response({'code': 400, 'msg': '订单暂无接单打手'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 使用事务更新
with transaction.atomic():
# 5.1 更新订单主表
order.Status = 5 # 已退款
order.RefundReason = tkly # 拒绝理由
order.AssignedCS = phone # 记录客服手机号
order.save()
# 5.2 更新平台订单扩展表:标记为拒绝结算(假设字段 jjjs_biaoshi
# 注意:需要确保 PlatformOrderExt 模型中已添加 jjjs_biaoshi 字段IntegerField默认0
if hasattr(order, 'pingtai_kuozhan') and order.pingtai_kuozhan:
pingtai_ext = order.pingtai_kuozhan
pingtai_ext.RejectSettlementFlag = 1 # 1表示拒绝结算
pingtai_ext.save()
else:
# 理论上应该存在,但若不存在则记录日志
logger.warning(f"订单 {dingdan_id} 缺少平台扩展表,无法标记拒绝结算")
# 5.3 更新打手扩展表
try:
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
dashou_profile = dashou_user.DashouProfile
dashou_profile.zhuangtai = 1 # 打手状态设为空闲
dashou_profile.tuikuanliang += 1 # 退款订单总量+1
dashou_profile.save()
logger.info(f"拒绝结算:打手 {jiedan_dashou_id} 退款订单+1状态空闲")
except (User.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"拒绝结算:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
except Exception as e:
logger.error(f"拒绝结算更新打手异常: {str(e)}", exc_info=True)
# 5.4 更新客服扩展表统计数据
# 🔥 新增:调用打手每日统计(成交)
try:
update_dashou_daily_by_action(
yonghuid=order.PlayerID,
amount=order.PlayerCommission,
action=3 # 3 表示退款(退款)
)
logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# 不影响主流程
kefu.jinrichuli += 1
kefu.jinyuechuli += 1
kefu.zongchuli += 1
kefu.save()
return Response({'code': 0, 'msg': '拒绝结算成功'})
class KefuTransferHallView(APIView):
"""
客服恢复大厅(转移大厅)接口
请求POST /yonghu/kefuzydt
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID"
}
认证JWT
返回code=0 成功
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 客服身份验证(同前,略,但必须完整实现)
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
if kefu.zhuangtai != 1:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
except ObjectDoesNotExist:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单
try:
order = Order.query.get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 4. 校验订单状态允许状态2,8,4
if order.Status not in [2, 8, 4]:
return Response({'code': 400, 'msg': '订单状态不允许转移大厅'}, status=status.HTTP_400_BAD_REQUEST)
if order.PlayerID:
UserDashou.query.filter(user__UserUID=order.PlayerID).update(zhuangtai=1)
else:
pass
# 5. 根据是否有指定ID决定新状态
if order.AssignedID:
new_status = 7 # 指定中
else:
new_status = 1 # 下单中
# 6. 使用事务更新
with transaction.atomic():
# 清空接单打手ID更新状态记录客服
order.PlayerID = None
order.Status = new_status
order.AssignedCS = phone
order.save()
return Response({'code': 0, 'msg': '转移大厅成功'})
class KefuCancelDesignationView(APIView):
"""
客服取消指定接口仅状态7
请求POST /yonghu/kefuqxzd
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID"
}
认证JWT
返回code=0 成功
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 客服身份验证(同前,略,但必须完整实现)
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
if kefu.zhuangtai != 1:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
except ObjectDoesNotExist:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单
try:
order = Order.query.get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 4. 校验订单状态为7指定中
if order.Status != 7:
print(999)
return Response({'code': 400, 'msg': '仅状态为指定中(7)的订单可取消指定'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 使用事务更新
with transaction.atomic():
# 状态改为1下单中清空指定ID记录客服
order.Status = 1
order.AssignedID = None
order.AssignedCS = phone
order.save()
return Response({'code': 0, 'msg': '取消指定成功'})
class KefuGetDashouListView(APIView):
"""
客服获取打手列表接口(仅打手类型)
请求POST /yonghu/kefuhqdslb
参数:{
"phone": "客服手机号",
"keyword": "搜索关键词可选搜索用户ID或昵称",
"page": "页码从1开始",
"page_size": "每页数量"
}
认证JWT
返回:{
"code": 0,
"data": {
"list": [打手对象],
"has_more": true/false
}
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('zhanghao', '').strip()
keyword = request.data.get('keyword', '').strip()
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
except ValueError:
return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
if not phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 构建查询条件(只查询打手)
# 通过 UserDashou 扩展表关联 User
queryset = UserDashou.query.select_related('user').all()
if keyword:
# 搜索用户ID或打手昵称
queryset = queryset.filter(
Q(user__UserUID__icontains=keyword) |
Q(nicheng__icontains=keyword)
)
# 4. 计算总数并分页
total = queryset.count()
offset = (page - 1) * page_size
dashou_list = queryset.order_by('-CreateTime')[offset:offset + page_size]
# 5. 构建返回数据(只取需要的字段)
result = []
for dashou in dashou_list:
user = dashou.user
result.append({
'yonghuid': user.UserUID,
'avatar': user.Avatar or '',
'zaixianzhuangtai': dashou.zaixianzhuangtai,
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
'nicheng': dashou.nicheng or '',
'zhuangtai': dashou.zhuangtai,
})
# 6. 判断是否有更多数据
has_more = (offset + len(dashou_list)) < total
return Response({
'code': 0,
'data': {
'list': result,
'has_more': has_more
}
})
class KefuGetDashouDetailView(APIView):
"""
客服获取打手详情接口
请求POST /yonghu/kefuhqdsxq
参数:{
"phone": "客服手机号",
"uid": "打手ID (yonghuid)"
}
认证JWT
返回code=0 + 打手信息 + 会员列表
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
uid = request.data.get('uid', '').strip()
if not phone or not uid:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询打手主表和扩展表
try:
# 使用 select_related 预加载 dashou_profile减少数据库查询
user_main = User.query.select_related('DashouProfile').get(
UserUID=uid
)
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
# 验证用户类型是否为打手(可选,但建议验证)
# 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册)
try:
dashou = user_main.DashouProfile
except ObjectDoesNotExist:
# 用户存在但未注册打手身份,返回基本信息
user_info = {
'yonghuid': user_main.UserUID,
'avatar': user_main.Avatar or '',
'zaixianzhuangtai': 0,
'zhanghaozhuangtai': 0,
'zhuangtai': 0,
'nicheng': '',
'chenghao': '',
'dianhua': '',
'wechat': '',
'phone': user_main.Phone or '',
'CreateTime': user_main.UserCreateTime,
'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '',
'ip': user_main.IP or '',
'jiedanzongliang': 0,
'chengjiaozongliang': 0,
'tuikuanliang': 0,
'jinrijiedan': 0,
'yue': 0.00,
'zonge': 0.00,
'jifen': 0,
'yajin': 0.00,
'jinrishouyi': 0.00,
'jinyueshouyi': 0.00,
'yaoqingren': '',
'jieshao': '',
}
member_list = []
return Response({
'code': 0,
'data': {
'user_info': user_info,
'member_list': member_list
}
})
# 4. 构建打手信息
user_info = {
'yonghuid': user_main.UserUID,
'avatar': user_main.Avatar or '',
'zaixianzhuangtai': dashou.zaixianzhuangtai,
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
'zhuangtai': dashou.zhuangtai,
'nicheng': dashou.nicheng or '',
'chenghao': dashou.chenghao or '',
'dianhua': dashou.dianhua or '',
'wechat': dashou.wechat or '',
'phone': user_main.Phone or '',
'CreateTime': user_main.UserCreateTime,
'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '',
'ip': user_main.IP or '',
'jiedanzongliang': dashou.jiedanzongliang,
'chengjiaozongliang': dashou.chengjiaozongliang,
'tuikuanliang': dashou.tuikuanliang,
'jinrijiedan': dashou.jinrijiedan,
'yue': float(dashou.yue) if dashou.yue else 0.00,
'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
'jifen': dashou.jifen,
'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
'yaoqingren': dashou.yaoqingren or '',
'jieshao': dashou.jieshao or '',
}
# 5. 查询打手会员信息(仅未过期)
member_records = Huiyuangoumai.query.filter(
yonghu_id=uid
).order_by('-CreateTime')
member_list = []
for record in member_records:
# 调用模型方法检查是否过期
if hasattr(record, 'jiance_shifou_daoqi'):
is_expired = record.jiance_shifou_daoqi()
else:
# 如果方法不存在,简单判断
is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
if not is_expired:
# 获取会员详情
try:
huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id)
huiyuan_name = huiyuan.jieshao
except Huiyuan.DoesNotExist:
huiyuan_name = '未知会员'
member_list.append({
'id': record.id,
'huiyuan_name': huiyuan_name,
'daoqi_time': record.daoqi_time,
'is_active': record.huiyuan_zhuangtai == 1,
})
return Response({
'code': 0,
'data': {
'user_info': user_info,
'member_list': member_list
}
})
class KefuUpdateDashouView(APIView):
permission_classes = [IsAuthenticated]
"""
客服修改打手信息接口
支持操作:
- jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb)
- jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb)
- jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd)
- gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee)
- gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee)
- gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee)
- jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg)
"""
def post(self, request):
# 1. 获取前端参数
username = request.data.get('username', '').strip()
dashou_id = request.data.get('dashou_id', '').strip()
caozuo = request.data.get('caozuo', '').strip()
value = request.data.get('value') # 金额、积分、天数等
huiyuan_id = request.data.get('huiyuan_id', '').strip() # 会员ID加/减会员时使用)
if not username or not dashou_id or not caozuo:
return Response({'code': 400, 'msg': '参数不完整'})
# 2. 权限校验(统一方法)
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions # 直接返回错误响应
# 3. 查询打手是否存在
try:
dashou_user = User.query.get(UserUID=dashou_id)
dashou_profile = dashou_user.DashouProfile
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手不存在'})
except UserDashou.DoesNotExist:
return Response({'code': 404, 'msg': '打手扩展信息缺失'})
# 4. 根据操作类型执行(事务内)
with transaction.atomic():
try:
# 记录修改前的值(用于日志)
before = {
'yue': dashou_profile.yue,
'yajin': dashou_profile.yajin,
'jifen': dashou_profile.jifen,
'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
'zhuangtai': dashou_profile.zhuangtai,
}
# ---------- 余额操作 ----------
if caozuo == 'jian_yue':
if '001aa' not in permissions:
return Response({'code': 403, 'msg': '无权限减打手余额'})
amount = Decimal(str(value))
if dashou_profile.yue < amount:
return Response({'code': 400, 'msg': '余额不足'})
dashou_profile.yue -= amount
dashou_profile.save()
after_yue = dashou_profile.yue
# 记录日志
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2, # 2=打手
xiugaitijiao=after_yue,
xiugaitijiaoq=before['yue'],
)
return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after_yue)}})
elif caozuo == 'jia_yue':
if '001bb' not in permissions:
return Response({'code': 403, 'msg': '无权限加打手余额'})
from jituan.services.group_finance_gate import deny_group_finance_exec
deny = deny_group_finance_exec(request.user, request)
if deny:
return deny
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': '无权限加打手押金'})
from jituan.services.group_finance_gate import deny_group_finance_exec
deny = deny_group_finance_exec(request.user, request)
if deny:
return deny
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'})
try:
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
except Huiyuan.DoesNotExist:
return Response({'code': 404, 'msg': '会员类型不存在'})
from jituan.services.club_user import get_user_club_id
from jituan.services.member_recharge import extend_member_entitlement_admin, get_club_huiyuan_row
player_club = get_user_club_id(dashou_user)
row = get_club_huiyuan_row(player_club, huiyuan_id, require_enabled=False)
days = int(value) if value else int(row.formal_days or 30) if row else 30
record, err = extend_member_entitlement_admin(
dashou_id, huiyuan_id, days, player_club,
)
if err:
return Response({'code': 400, 'msg': err})
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]
@staticmethod
def _parse_status_list(raw):
"""兼容 status=[0,3]、\"0,3\"、单数字;空/all 表示不过滤。"""
if raw is None:
return None
if isinstance(raw, str):
s = raw.strip().lower()
if not s or s in ('all', 'null', 'none'):
return None
if s.startswith('['):
import json
try:
raw = json.loads(s)
except (TypeError, ValueError):
return None
else:
raw = [x.strip() for x in s.split(',') if x.strip()]
if isinstance(raw, (int, float)):
return [int(raw)]
if isinstance(raw, list):
out = []
for x in raw:
try:
out.append(int(x))
except (TypeError, ValueError):
continue
return out or None
return None
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
status_list = self._parse_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. 获取统计信息(不分页)
from jituan.services.club_penalty import filter_penalty_record_qs
from jituan.services.club_user_access import list_response_meta
base_qs = filter_penalty_record_qs(PenaltyRecord.query.all(), request)
stats = {
'zongshu': base_qs.count(),
'daichuli': base_qs.filter(ApplyStatus__in=[0, 3]).count(),
'yichuli': base_qs.filter(ApplyStatus__in=[1, 2]).count(),
}
queryset = base_qs
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. 分页
page_obj = queryset.order_by('-CreateTime').paginate(page=page, per_page=page_size)
total = page_obj.total
records = page_obj.items
# 6. 构建返回列表
list_data = []
for item in records:
ct = item.CreateTime
ut = item.UpdateTime
list_data.append({
'dingdan_id': item.OrderID,
'dashouid': item.PlayerID,
'qingqiuid': item.ApplicantID,
'cfliyou': item.PenaltyReason,
'sqzhuangtai': item.ApplyStatus,
'jifen': item.DeductedPoints,
'CreateTime': ct,
'UpdateTime': ut,
'create_time': ct.isoformat() if ct else '',
'update_time': ut.isoformat() if ut else '',
})
return Response({
'code': 0,
'data': {
'list': list_data,
'total': total,
'stats': stats,
**list_response_meta(request),
}
})
class KefuGetOrderDetailView(APIView):
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 获取当前用户
current_user = request.user
# 验证手机号
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 验证用户类型
if current_user.UserType not in ('kefu', 'admin'):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 客服扩展表
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
if kefu.zhuangtai != 1:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
except Exception as e:
logger.exception(f"[kefuhqddxq] 获取客服扩展表异常 user={getattr(current_user, 'yonghuid', '?')}")
return Response({'code': 500, 'msg': '服务器错误'}, status=500)
# 3. 查询订单
try:
dingdan_obj = Order.query.select_related(
'pingtai_kuozhan', 'shangjia_kuozhan'
).get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=404)
except Exception as e:
logger.exception(f"[kefuhqddxq] 订单查询异常 dingdan_id={dingdan_id}")
return Response({'code': 500, 'msg': '服务器错误'}, status=500)
from jituan.services.club_user_access import forbid_if_order_out_of_scope
deny = forbid_if_order_out_of_scope(request, dingdan_obj)
if deny:
return deny
# 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'] = ''
# 8. 指定打手响应
ZHIDING_HF_TEXT = {1: '想接', 2: '不想接', 3: '等会接'}
hf = getattr(dingdan_obj, 'AssignedResponse', None)
response_data['zhiding_hf'] = hf
response_data['zhiding_hf_text'] = ZHIDING_HF_TEXT.get(hf, '') if hf else ''
response_data['zhiding_hf_sj'] = (
dingdan_obj.AssignedResponseAt.strftime('%Y-%m-%d %H:%M:%S')
if getattr(dingdan_obj, 'AssignedResponseAt', None) else ''
)
response_data['zhiding_dhj_sm'] = getattr(dingdan_obj, 'AssignedLaterNote', '') or ''
# 9. 关联罚款(打手)
from utils.penalty_status import PENALTY_STATUS_LABELS as PENALTY_STATUS
try:
from orders.models import Penalty
dashou_id = dingdan_obj.PlayerID or dingdan_obj.AssignedID or ''
fakuan_qs = Penalty.query.filter(RelatedOrderID=dingdan_id)
if dashou_id:
fakuan_qs = fakuan_qs.filter(PenalizedUserID=dashou_id)
fakuan_list = []
for p in fakuan_qs.order_by('-CreateTime')[:5]:
fakuan_list.append({
'amount': str(p.FineAmount or '0.00'),
'status': p.Status,
'status_text': PENALTY_STATUS.get(p.Status, ''),
'reason': p.Reason or '',
'create_time': p.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if p.CreateTime else '',
})
response_data['fakuan_list'] = fakuan_list
latest = fakuan_list[0] if fakuan_list else None
response_data['fakuan_info'] = latest
response_data['is_fadaned'] = Penalty.query.filter(
RelatedOrderID=dingdan_id, ApplicantID=getattr(current_user, 'UserUID', '')
).exists()
except Exception as e:
logger.exception(f"[kefuhqddxq] 罚款查询异常 dingdan_id={dingdan_id}")
response_data['fakuan_list'] = []
response_data['fakuan_info'] = None
response_data['is_fadaned'] = False
return Response({'code': 0, 'data': response_data})
class KefuPunishmentDetailView(APIView):
"""
客服获取处罚详情接口
请求POST /yonghu/kefu_cf_detail
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID"
}
认证JWT
返回code=0 + 处罚记录详情(含图片)
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 查询处罚记录(按创建时间倒序取最新一条)
try:
record = PenaltyRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first()
if not record:
return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"查询处罚记录失败: {e}")
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 查询商家证据图片
zhengju_images = []
shensu_images = []
try:
# 商家证据图片:上传者为请求人 (ApplicantID)
zhengju_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.ApplicantID)
zhengju_images = [item.ImageURL for item in zhengju_qs if item.ImageURL]
# 打手申诉图片:上传者为打手 (PlayerID)
shensu_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.PlayerID)
shensu_images = [item.ImageURL for item in shensu_qs if item.ImageURL]
except Exception as e:
logger.error(f"查询图片失败: {e}")
# 构建返回数据
data = {
'dingdan_id': record.OrderID,
'dashouid': record.PlayerID,
'qingqiuid': record.ApplicantID,
'chuliid': record.ProcessorID,
'cfliyou': record.PenaltyReason,
'sqzhuangtai': record.ApplyStatus,
'bhliyou': record.RejectReason,
'jifen': record.DeductedPoints,
'ssliyou': record.AppealReason or '',
'zhengju_tupian': zhengju_images,
'shensu_tupian': shensu_images,
'CreateTime': record.CreateTime,
'UpdateTime': record.UpdateTime,
}
return Response({'code': 0, 'data': data})
class KefuPunishmentActionView(APIView):
"""
客服同意/驳回处罚接口
请求POST /yonghu/kefu_cf_action
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID",
"action": 1(同意) / 2(驳回),
"reject_reason": "驳回理由(驳回时必填)"
}
认证JWT
返回code=0 成功
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
action = request.data.get('action')
reject_reason = request.data.get('reject_reason', '').strip()
if not phone or not dingdan_id or action not in [1, 2]:
return Response({'code': 401, 'msg': '参数错误'}, status=status.HTTP_400_BAD_REQUEST)
if action == 2 and not reject_reason:
return Response({'code': 400, 'msg': '驳回理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
# 客服身份验证(完整验证,此处略)
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
if kefu.zhuangtai != 1:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
except ObjectDoesNotExist:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 查询待处理的处罚记录状态0或3
try:
record = PenaltyRecord.query.filter(
OrderID=dingdan_id,
ApplyStatus__in=[0, 3]
).order_by('-CreateTime').first()
if not record:
exists = PenaltyRecord.query.filter(OrderID=dingdan_id).exists()
if exists:
return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST)
else:
return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"查询处罚记录失败: {e}")
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
with transaction.atomic():
# 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回
duplicate_records = PenaltyRecord.query.filter(
OrderID=dingdan_id,
ApplyStatus__in=[0, 3]
).exclude(id=record.id)
for dup in duplicate_records:
dup.ApplyStatus = 2
dup.ProcessorID = phone
dup.RejectReason = "系统自动驳回:存在重复处罚记录"
dup.save()
logger.info(f"自动驳回重复处罚记录 ID={dup.id}")
if action == 1: # 同意
# 可选验证打手存在
if record.PlayerID:
try:
dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
except UserDashou.DoesNotExist:
return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST)
record.ApplyStatus = 1
record.ProcessorID = phone
record.save()
# 更新商家订单扩展表
try:
dingdan = Order.query.get(OrderID=dingdan_id)
if hasattr(dingdan, 'shangjia_kuozhan'):
dingdan.shangjia_kuozhan.PenaltyApplyStatus = 1
dingdan.shangjia_kuozhan.save()
except Exception as e:
logger.warning(f"更新商家扩展表失败: {e}")
elif action == 2: # 驳回
# 返还积分
if record.DeductedPoints > 0 and record.PlayerID:
try:
dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
if dashou.jifen <= 1000: # 安全限制
dashou.jifen += record.DeductedPoints
dashou.save()
logger.info(f"驳回处罚,打手{record.PlayerID}返还积分{record.DeductedPoints}")
else:
logger.warning(f"打手积分异常({dashou.jifen}),不再返还")
except UserDashou.DoesNotExist:
logger.warning(f"打手{record.PlayerID}不存在,无法返还积分")
record.ApplyStatus = 2
record.ProcessorID = phone
record.RejectReason = reject_reason
record.save()
try:
dingdan = Order.query.get(OrderID=dingdan_id)
if hasattr(dingdan, 'shangjia_kuozhan'):
dingdan.shangjia_kuozhan.PenaltyApplyStatus = 2
dingdan.shangjia_kuozhan.RejectReason = reject_reason
dingdan.shangjia_kuozhan.save()
except Exception as e:
logger.warning(f"更新商家扩展表失败: {e}")
return Response({'code': 0, 'msg': '处理成功'})
class KefuForceCompleteView(APIView):
"""
客服强制结单接口(仅本地订单)
请求POST /yonghu/kefuqiangzhijiedan
参数:{"phone": "客服手机号", "dingdan_id": "订单ID"}
认证JWT仅限状态正常的客服
逻辑:
1. 验证客服身份及状态
2. 查询订单并加锁(事务内)
3. 校验订单状态是否为8结算中
4. 校验订单必须有接单打手且打手分成金额有效
5. 给打手增加余额、成交统计、今日/本月收益
6. 如果是商家发单,更新商家成交订单数量
7. 更新订单状态为3已完成
8. 更新客服处理统计
9. 返回成功
"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '参数缺失'})
# 1. 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'})
if current_user.UserType not in ('kefu', 'admin'):
return Response({'code': 401, 'msg': '认证失败'})
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
return Response({'code': 401, 'msg': '认证失败'})
if kefu.zhuangtai != 1:
return Response({'code': 401, 'msg': '客服状态异常'})
# 2. 订单处理(事务内)
try:
with transaction.atomic():
order = Order.objects.select_for_update().get(OrderID=dingdan_id)
# 3. 校验订单状态
if order.Status != 8:
return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.Status}'})
# 4. 校验必要字段
dashou_id = order.PlayerID
dashou_fencheng = order.PlayerCommission
if not dashou_id:
return Response({'code': 400, 'msg': '订单无接单打手'})
if not dashou_fencheng or dashou_fencheng <= 0:
return Response({'code': 400, 'msg': '打手分成金额无效'})
# 5. 给打手结算
try:
dashou_user = User.query.get(UserUID=dashou_id)
dashou_profile = dashou_user.DashouProfile
# 原子更新(避免并发重复加)
dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1
dashou_profile.yue = F('yue') + dashou_fencheng
dashou_profile.zonge = F('zonge') + dashou_fencheng
dashou_profile.jinrishouyi = F('jinrishouyi') + dashou_fencheng
dashou_profile.jinyueshouyi = F('jinyueshouyi') + dashou_fencheng
dashou_profile.save(update_fields=[
'chengjiaozongliang', 'yue', 'zonge',
'jinrishouyi', 'jinyueshouyi'
])
except (User.DoesNotExist, AttributeError):
return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'})
# 6. 如果是商家发单,更新商家成交订单数量
if order.Platform == 2:
try:
# 通过 MerchantOrderExt 获取商家ID
shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan'
if shangjia_ext and shangjia_ext.shangjia_id:
shangjia_user = User.query.get(UserUID=shangjia_ext.shangjia_id)
shangjia_profile = shangjia_user.ShopProfile
shangjia_profile.chengjiao = F('chengjiao') + 1
shangjia_profile.save(update_fields=['chengjiao'])
# (行为驱动,更安全)
update_shangjia_daily(
yonghuid=shangjia_ext.shangjia_id,
amount=Decimal(str(order.Amount)),
action=2, # 2 = 结算
order_id=dingdan_id,
)
except ObjectDoesNotExist:
# 没有商家扩展记录则忽略
pass
# 7. 更新订单状态
order.Status = 3
order.AssignedCS = phone
order.save(update_fields=['Status', 'AssignedCS'])
if order.Platform == 1:
# ========== 新增:商品和店铺每日统计 ==========
try:
# 商品每日统计只要有商品ID就执行
if order.ProductID:
# 从扩展表获取已计算好的收益值(下单时已存储)
pingtai_shouyi = Decimal('0.00')
dianpu_shouyi = Decimal('0.00')
if hasattr(order, 'pingtai_kuozhan'):
pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_shangpin_daily_stat(
shangpin_id=order.ProductID,
caozuo_leixing=2, # 2 = 结单
dingdan_jiage=order.Amount,
pingtai_shouyi=pingtai_shouyi,
dianpu_zongshouyi=dianpu_shouyi
)
logger.info(f"商品统计更新完成商品ID: {order.ProductID}")
# 店铺每日统计仅当存在店铺ID时执行
dianpu_id = None
if hasattr(order, 'pingtai_kuozhan'):
dianpu_id = order.pingtai_kuozhan.ShopID
if dianpu_id:
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_dianpu_daily_stat(
dianpu_id=dianpu_id,
caozuo_leixing=2, # 2 = 结单
dingdan_jiage=order.Amount,
dianpu_shouyi=dianpu_shouyi
)
logger.info(f"店铺统计更新完成店铺ID: {dianpu_id}")
except Exception as e:
logger.error(f"商品/店铺统计更新失败: {e}")
logger.error(traceback.format_exc())
# 统计失败不影响主流程
# =============================================
# 🔥 新增:调用打手每日统计(成交)
try:
update_dashou_daily_by_action(
yonghuid=order.PlayerID,
amount=dashou_fencheng,
action=2 # 2 表示成交(结算)
)
logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# 不影响主流程
# 商家订单管事分红
if order.Platform == 2:
settle_shangjia_order_guanshi_fenhong(order, dashou_id)
# 8. 更新客服统计
kefu.jinrichuli = F('jinrichuli') + 1
kefu.jinyuechuli = F('jinyuechuli') + 1
kefu.zongchuli = F('zongchuli') + 1
kefu.save(update_fields=['jinrichuli', 'jinyuechuli', 'zongchuli'])
return Response({'code': 0, 'msg': '强制结单成功'})
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
except Exception as e:
logger.error(f"强制结单异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'})
kefu_txsh_list_logger = logging.getLogger('yonghu.kefu_txsh_list')
if not kefu_txsh_list_logger.handlers:
_console_handler = logging.StreamHandler(sys.stderr)
_console_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s %(name)s: %(message)s'
))
kefu_txsh_list_logger.addHandler(_console_handler)
kefu_txsh_list_logger.setLevel(logging.DEBUG)
kefu_txsh_list_logger.propagate = False
_TIXIAN_LIST_BASE_FIELDS = (
'id', 'yonghuid', 'avatar', 'phone', 'nicheng', 'leixing',
'zhifu', 'skzhanghao', 'jine', 'zhuangtai', 'fangshi',
'shenheid', 'bhliyou', 'CreateTime', 'UpdateTime',
)
_TIXIAN_LIST_AUDIT_FIELDS = ('shenhe_jilu_id', 'shenhe_danhao')
class KefuWithdrawListView(APIView):
"""
客服获取提现审核列表接口
请求POST /yonghu/kefu_txsh_list
参数:{
"phone": "客服手机号",
"page": 1,
"page_size": 20,
"status": 1(待审核)/2(已处理),
"type": 1(打手佣金)/2(管事分红)/3(组长分红)/4(审核官分佣)/5(打手押金)/6(商家余额), 可选
"payment": 1(微信)/2(支付宝) 可选,
"search_uid": "用户ID搜索" 可选,
"date": "YYYY-MM-DD" 可选,
"min_amount": 0, # 最小提现金额(元)可选
"max_amount": 999999, # 最大提现金额(元)可选
"dakuan_mode": 1 # 打款方式1手动打款 2自动打款可选
}
认证JWT
返回:{
"code": 0,
"data": {
"list": [提现记录],
"total": 总数,
"stats": {
"total": 总记录数,
"awaiting": 待审核数,
"processed": 已处理数,
"total_amount": "总金额(元)"
}
}
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def _safe_request_params(self, request):
try:
return dict(request.data)
except Exception:
return {'raw': str(request.data)}
def _shenhe_jilu_field_available(self):
"""探测 shenhe_jilu_id 字段是否已在数据库中就绪"""
try:
Tixianjilu.query.values_list('shenhe_jilu_id', flat=True).first()
return True
except DatabaseError as e:
kefu_txsh_list_logger.error(
'kefu_txsh_list: shenhe_jilu_id 字段不可用,将按旧表结构查询: %s', e,
exc_info=True,
)
return False
def _list_queryset(self, q, shenhe_field_ok, request):
"""字段未迁移时 only 旧字段,避免 SELECT 不存在的列导致 500"""
fields = _TIXIAN_LIST_BASE_FIELDS + (
_TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else ()
)
from jituan.services.tixian_club import filter_tixianjilu_by_request
qs = Tixianjilu.query.filter(q).only(*fields)
return filter_tixianjilu_by_request(qs, request)
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):
from jituan.services.group_finance_gate import require_withdraw_audit_access
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, err_resp = require_withdraw_audit_access(request, phone)
if err_resp:
return err_resp
q = Q()
try:
status_val = int(status_filter) if status_filter is not None and str(status_filter).strip() != '' else None
except (TypeError, ValueError):
status_val = None
try:
dakuan_mode_val = int(dakuan_mode_filter) if dakuan_mode_filter is not None and str(dakuan_mode_filter).strip() != '' else None
except (TypeError, ValueError):
dakuan_mode_val = None
# 未传打款方式参数 → 默认手动打款(1)
effective_dakuan = dakuan_mode_val if dakuan_mode_val in (1, 2) else 1
if status_val == 1:
q &= Q(zhuangtai=1)
elif status_val == 2:
if effective_dakuan == 2:
q &= Q(zhuangtai__in=[2, 3, 4, 5, 6])
else:
q &= Q(zhuangtai__in=[2, 3])
if type_filter is not None and str(type_filter).strip() != '':
try:
type_val = int(type_filter)
if type_val in [1, 2, 3, 4, 5, 6]:
q &= Q(leixing=type_val)
except (TypeError, ValueError):
pass
if payment_filter is not None and str(payment_filter).strip() != '':
try:
payment_val = int(payment_filter)
if payment_val in [1, 2]:
q &= Q(fangshi=payment_val)
except (TypeError, ValueError):
pass
if search_uid:
q &= Q(yonghuid=search_uid)
if date_filter:
q &= Q(UpdateTime__date=date_filter)
if min_amount is not None and str(min_amount).strip() != '':
try:
q &= Q(jine__gte=float(min_amount))
except (TypeError, ValueError):
pass
if max_amount is not None and str(max_amount).strip() != '':
try:
q &= Q(jine__lte=float(max_amount))
except (TypeError, ValueError):
pass
shenhe_field_ok = self._shenhe_jilu_field_available()
if shenhe_field_ok:
if effective_dakuan == 2:
q &= Q(shenhe_jilu_id__isnull=False)
else:
q &= Q(shenhe_jilu_id__isnull=True)
elif effective_dakuan == 2:
kefu_txsh_list_logger.warning('kefu_txsh_list: 自动打款筛选不可用,返回空列表')
return Response({
'code': 0,
'data': {
'list': [],
'total': 0,
'stats': {'total': 0, 'awaiting': 0, 'processed': 0, 'total_amount': '0.00'},
},
})
processed_statuses = [2, 3, 4, 5, 6] if effective_dakuan == 2 else [2, 3]
base_qs = self._list_queryset(q, shenhe_field_ok, request)
total_count = base_qs.count()
awaiting_count = self._list_queryset(q & Q(zhuangtai=1), shenhe_field_ok, request).count()
processed_count = self._list_queryset(
q & Q(zhuangtai__in=processed_statuses), shenhe_field_ok, request,
).count()
agg = base_qs.aggregate(total=Sum('jine'))
total_amount = agg.get('total') or 0
stats = {
'total': total_count,
'awaiting': awaiting_count,
'processed': processed_count,
'total_amount': str(round(float(total_amount), 2)),
}
queryset = base_qs.order_by('CreateTime')
total = queryset.count()
records = list(queryset[(page - 1) * page_size: page * page_size])
audit_ids = [
getattr(item, 'shenhe_jilu_id', None)
for item in records if getattr(item, 'shenhe_jilu_id', None)
]
audit_meta_map = {}
if audit_ids:
try:
audit_meta_map = load_audit_meta_map(audit_ids)
except Exception as e:
kefu_txsh_list_logger.error(
'kefu_txsh_list: 查询审核表信息失败: %s', e,
exc_info=True,
)
audit_mode_map = {
aid: meta.get('dakuan_mode', 2) for aid, meta in audit_meta_map.items()
}
list_data = []
for item in records:
shenhe_jilu_id = getattr(item, 'shenhe_jilu_id', None) if shenhe_field_ok else None
shenhe_danhao = (getattr(item, 'shenhe_danhao', None) or '') if shenhe_field_ok else ''
if shenhe_jilu_id and not shenhe_danhao:
shenhe_danhao = audit_meta_map.get(shenhe_jilu_id, {}).get('shenhe_danhao', '')
list_data.append({
'id': item.id,
'shenhe_jilu_id': shenhe_jilu_id,
'shenhe_danhao': shenhe_danhao,
'yonghuid': item.yonghuid,
'avatar': item.avatar or '',
'phone': item.phone or '',
'nicheng': item.nicheng or '',
'leixing': item.leixing,
'zhifu': item.zhifu or '',
'skzhanghao': item.skzhanghao or '',
'jine': str(item.jine) if item.jine is not None else '0.00',
'zhuangtai': item.zhuangtai,
'fangshi': item.fangshi,
'dakuan_mode': self._resolve_row_dakuan_mode(item, audit_mode_map, shenhe_field_ok),
'shenheid': item.shenheid or '',
'bhliyou': item.bhliyou or '',
'CreateTime': item.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if item.CreateTime else '',
'UpdateTime': item.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if item.UpdateTime else '',
})
from jituan.services.club_user_access import list_response_meta
return Response({
'code': 0,
'data': {
'list': list_data,
'total': total,
'stats': stats,
**list_response_meta(request),
},
})
except DatabaseError as e:
kefu_txsh_list_logger.error(
'kefu_txsh_list 数据库错误 dakuan_mode=%s params=%s err=%s',
dakuan_mode_filter, req_params, e,
exc_info=True,
)
return Response({
'code': 500,
'msg': '数据库查询失败,请确认已执行 python manage.py migrate yonghu',
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
kefu_txsh_list_logger.error(
'kefu_txsh_list 接口异常 dakuan_mode=%s params=%s err=%s',
dakuan_mode_filter, req_params, e,
exc_info=True,
)
return Response({
'code': 500,
'msg': '获取提现列表失败',
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class KefuWithdrawDetailView(APIView):
"""
客服获取提现详情接口
请求POST /yonghu/kefu_txsh_detail
参数:{
"phone": "客服手机号",
"tixian_id": "提现记录ID"
}
认证JWT
返回code=0 + 提现记录详情 + 用户扩展信息
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = request.data.get('phone', '').strip()
tixian_id = request.data.get('tixian_id')
if not phone or not tixian_id:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
from jituan.services.group_finance_gate import require_withdraw_audit_access
_, err_resp = require_withdraw_audit_access(request, phone)
if err_resp:
return err_resp
# 查询提现记录
try:
record = Tixianjilu.query.get(id=tixian_id)
except Tixianjilu.DoesNotExist:
return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, record)
if deny:
return deny
# 查询用户主表
try:
user = User.query.get(UserUID=record.yonghuid)
except User.DoesNotExist:
user = None
# 构建基础返回数据(字段必须与前端一致)
data = {
'id': record.id,
'yonghuid': record.yonghuid,
'avatar': record.avatar or '',
'phone': record.phone or '',
'nicheng': record.nicheng or '',
'leixing': record.leixing, # 1打手 2管事
'zhifu': record.zhifu or '',
'skzhanghao': record.skzhanghao or '',
'jine': str(record.jine) if record.jine else '0.00',
'zhuangtai': record.zhuangtai,
'fangshi': record.fangshi,
'shenheid': record.shenheid or '',
'bhliyou': record.bhliyou or '',
'CreateTime': record.CreateTime,
'UpdateTime': record.UpdateTime,
}
# 根据提现类型添加扩展信息
if user:
if record.leixing == 1: # 打手
try:
dashou = UserDashou.query.get(user=user)
# 计算成交率和退款率
total = dashou.jiedanzongliang or 0
completed = dashou.chengjiaozongliang or 0
refund = dashou.tuikuanliang or 0
completion_rate = round((completed / total * 100) if total > 0 else 0)
refund_rate = round((refund / completed * 100) if completed > 0 else 0)
data['dashouInfo'] = {
'zhuangtai': dashou.zhuangtai, # 工作状态
'zaixianzhuangtai': dashou.zaixianzhuangtai, # 在线状态
'zhanghaozhuangtai': dashou.zhanghaozhuangtai, # 账号状态
'yue': float(dashou.yue) if dashou.yue else 0.00, # 可提现余额
'zonge': float(dashou.zonge) if dashou.zonge else 0.00, # 接单总额
'jifen': dashou.jifen, # 积分
'jiedanzongliang': dashou.jiedanzongliang, # 接单总量
'chengjiaozongliang': dashou.chengjiaozongliang, # 成交总量
'tuikuanliang': dashou.tuikuanliang, # 退款总量
'completionRate': completion_rate,
'refundRate': refund_rate,
}
except ObjectDoesNotExist:
data['dashouInfo'] = None
elif record.leixing == 2: # 管事
try:
guanshi = UserGuanshi.query.get(user=user)
data['guanshiInfo'] = {
'zhuangtai': guanshi.zhuangtai, # 账号状态
'yue': float(guanshi.yue) if guanshi.yue else 0.00,
'yaogingshuliang': guanshi.yaogingshuliang, # 邀请打手总数
'jinrichongzhi': guanshi.jinrichongzhi, # 今日充值
'jinyuechongzhi': guanshi.jinyuechongzhi, # 今月充值
'chongzhifenrun': float(guanshi.chongzhifenrun) if guanshi.chongzhifenrun else 0.00,
}
except ObjectDoesNotExist:
data['guanshiInfo'] = None
return Response({'code': 0, 'data': data})
class KefuWithdrawActionView(APIView):
"""
客服处理提现接口(同意/拒绝)—— 仅此接口处理后台审核打款
POST /yonghu/kefu_txsh_action
单条:{ phone, action, tixian_id, yonghuid, leixing, reason? }
批量:{ phone, action, batch_list: [{tixian_id, yonghuid, leixing}, ...], reason? }
自动打款(dakuan_mode=2)同意→6待收款拒绝→5+驳回原因+退还可到账金额(shijidaozhang),手续费不退,不更新平台收支/每日统计
手动打款(dakuan_mode=1):原逻辑不变
状态要求Tixianjilu.zhuangtai=1 且 TixianShenheJilu.zhuangtai=1审核中其他状态一律拒绝
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def _verify_kefu(self, request, phone):
if not phone:
return None, Response({'code': 401, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST)
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 管理员跳过客服扩展表检查
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
return current_user, None
def _parse_items(self, request):
"""解析单条或 batch_list返回 [{tixian_id, yonghuid, leixing}]"""
batch_list = request.data.get('batch_list')
if batch_list is not None:
if not isinstance(batch_list, list) or len(batch_list) == 0:
return None, Response({'code': 400, 'msg': 'batch_list 不能为空'}, status=status.HTTP_400_BAD_REQUEST)
items = []
for idx, row in enumerate(batch_list):
if not isinstance(row, dict):
return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 格式错误'}, status=status.HTTP_400_BAD_REQUEST)
tid = row.get('tixian_id')
yid = (row.get('yonghuid') or '').strip()
lx = row.get('leixing')
if not tid or not yid or lx is None:
return None, Response(
{'code': 400, 'msg': f'batch_list[{idx}] 缺少 tixian_id / yonghuid / leixing'},
status=status.HTTP_400_BAD_REQUEST,
)
try:
items.append({
'tixian_id': int(tid),
'yonghuid': yid,
'leixing': int(lx),
})
except (TypeError, ValueError):
return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
return items, None
tixian_id = request.data.get('tixian_id')
if not tixian_id:
return None, Response({'code': 400, 'msg': '提现记录ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
yonghuid = (request.data.get('yonghuid') or '').strip()
leixing = request.data.get('leixing')
try:
item = {'tixian_id': int(tixian_id), 'yonghuid': yonghuid, 'leixing': int(leixing) if leixing is not None else None}
except (TypeError, ValueError):
return None, Response({'code': 400, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
return [item], None
def _process_auto_item(self, request, 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}不存在')
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, tixian)
if deny:
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
if tixian.yonghuid != req_yonghuid:
raise ValueError(f'提现记录{tixian_id}用户ID不匹配')
if tixian.leixing != req_leixing:
raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
if tixian.zhuangtai != 1:
raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
if not tixian.shenhe_jilu_id:
raise ValueError(f'提现记录{tixian_id}:非自动打款审核记录')
try:
audit = TixianShenheJilu.objects.select_for_update().get(id=tixian.shenhe_jilu_id)
except TixianShenheJilu.DoesNotExist:
raise ValueError(f'提现记录{tixian_id}:审核记录不存在')
if audit.yonghuid != req_yonghuid:
raise ValueError(f'提现记录{tixian_id}审核记录用户ID不匹配')
if audit.leixing != req_leixing:
raise ValueError(f'提现记录{tixian_id}:审核记录提现类型不匹配')
if audit.zhuangtai != 1:
raise ValueError(f'提现记录{tixian_id}:审核记录非审核中状态')
try:
user = User.objects.select_for_update().get(UserUID=req_yonghuid)
except User.DoesNotExist:
raise ValueError(f'用户{req_yonghuid}不存在')
if action == 1:
limit_ok, limit_msg = check_shijidaozhang_within_limit(audit.shijidaozhang)
if not limit_ok:
sync_audit_and_jilu_status(
audit, 5,
bhliyou=limit_msg,
bo_hui_ren_id=kefu_user.UserUID,
)
refund_balance(user, audit.leixing, audit.shijidaozhang)
tixian.shenheid = kefu_user.UserUID
tixian.bhliyou = limit_msg
tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
raise ValueError(f'提现记录{tixian_id}{limit_msg},已自动驳回并退款')
sync_audit_and_jilu_status(audit, 6, shenhe_ren_id=kefu_user.UserUID)
tixian.shenheid = kefu_user.UserUID
tixian.save(update_fields=['shenheid', 'UpdateTime'])
else:
sync_audit_and_jilu_status(
audit, 5,
bhliyou=reason,
bo_hui_ren_id=kefu_user.UserUID,
)
# 退还可到账金额,申请时扣的 shenqing_jine 中的手续费(shouxufei)不退
refund_balance(user, audit.leixing, audit.shijidaozhang)
tixian.shenheid = kefu_user.UserUID
tixian.bhliyou = reason
tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
def _process_manual_item(self, request, 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}不存在')
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, tixian)
if deny:
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
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}不存在')
from jituan.services.club_user import get_user_club_id
if action == 1:
tixian.zhuangtai = 2
try:
from jituan.services.szjilu_accounting import apply_szjilu_expense
update_daily_payout(tixian.jine, get_user_club_id(user))
apply_szjilu_expense(tixian.jine, get_user_club_id(user))
except Exception as e:
logger.error(f'更新收支记录失败: {e}')
else:
try:
refund_balance(user, tixian.leixing, tixian.jine)
except ValueError as e:
raise ValueError(str(e))
tixian.zhuangtai = 3
tixian.bhliyou = reason
tixian.shenheid = phone
tixian.UpdateTime = timezone.now()
tixian.save()
def post(self, request):
logger.info(f'接收到提现处理请求,数据: {request.data}')
phone = request.data.get('phone', '').strip()
action_raw = request.data.get('action')
reason = request.data.get('reason', '').strip()
try:
action = int(action_raw)
except (TypeError, ValueError):
return Response({'code': 400, 'msg': 'action 参数必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
if action not in [1, 2]:
return Response({'code': 400, 'msg': 'action 必须为1(同意)或2(拒绝)'}, status=status.HTTP_400_BAD_REQUEST)
if action == 2 and not reason:
return Response({'code': 400, 'msg': '拒绝提现时理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
kefu_user, err_resp = self._verify_kefu(request, phone)
if err_resp:
return err_resp
from jituan.services.group_finance_gate import require_withdraw_audit_access
_, audit_err = require_withdraw_audit_access(request, phone)
if audit_err:
return audit_err
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}不存在')
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, tixian_probe)
if deny:
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
# 有关联审核记录(shenhe_jilu_id)即为 zddksh 自动打款,不依赖 dakuan_mode 字段
is_auto = bool(getattr(tixian_probe, 'shenhe_jilu_id', None))
if is_auto:
self._process_auto_item(request, item, action, reason, kefu_user)
else:
if is_batch:
raise ValueError(f'提现记录{tixian_id}:批量操作仅支持自动打款记录')
self._process_manual_item(request, item, action, reason, phone)
except ValueError as e:
return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f'处理提现事务失败: {e}', exc_info=True)
return Response({'code': 500, 'msg': f'处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
msg = '批量处理成功' if is_batch and len(items) > 1 else '处理成功'
return Response({'code': 0, 'msg': msg, 'data': {'count': len(items)}})
class KefuPunishView(APIView):
"""
客服处罚打手接口
请求POST /yonghu/kefuchufa
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID",
"reason": "处罚原因(可选)"
}
认证JWT
返回code=0 成功
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
reason = request.data.get('reason', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
# 2. 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单
try:
order = Order.query.get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 4. 获取打手ID
dashou_id = order.PlayerID
if not dashou_id:
return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 查询打手
try:
dashou_user = User.query.get(UserUID=dashou_id)
dashou = dashou_user.DashouProfile
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND)
except ObjectDoesNotExist:
return Response({'code': 400, 'msg': '该用户不是打手'}, status=status.HTTP_400_BAD_REQUEST)
# 6. 使用事务
from jituan.services.club_penalty import resolve_penalty_record_club_id
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,
ClubID=resolve_penalty_record_club_id(
order_id=dingdan_id,
player_id=dashou_id,
request=request,
user=request.user,
),
)
# 扣除打手积分
if dashou.jifen >= jifen:
dashou.jifen -= jifen
else:
dashou.jifen = 0
dashou.save()
# 可选:更新订单的 clkf 字段记录处理客服
order.AssignedCS = phone
order.save()
return Response({'code': 0, 'msg': '处罚成功'})