Files
Django/users/views/kefu_orders.py

1851 lines
82 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_orders - auto-generated by split script."""
"""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, has_perm_code, has_merchant_order_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 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, None)
if kefu is None:
return permissions
if not has_perm_code(permissions, '002ab'):
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. 权限校验(与 menu-access 同一算法、同一用户)
kefu, permissions = verify_kefu_permission(request, None)
if kefu is None:
return permissions
if not has_merchant_order_permission(permissions):
from backend.utils import _AllPermissions
if isinstance(permissions, _AllPermissions):
codes = ['*']
else:
codes = list(permissions or [])
return Response({
'code': 403,
'msg': (
f'您无权查看商家订单列表(需要 002ac/002ab/003aa'
f'当前俱乐部权限码:{",".join(codes) or ""}'
),
})
# 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.select_related('DashouProfile').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. 更新客服统计数据(管理员无客服扩展表,跳过)
if kefu is not None:
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': '错误信息'}
"""
from jituan.services.pay_refund_router import try_fubei_refund
from jituan.services.wechat_pay import club_id_from_order
fb = try_fubei_refund(
out_trade_no=dingdan_id,
amount_yuan=jine,
club_id=club_id_from_order(dingdan_id),
)
if fb is not None:
return fb
# 获取配置
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
from jituan.services.fund_freeze import credit_dashou_order_settle
credit_dashou_order_settle(
order=order,
dashou_profile=dashou_profile,
user_id=jiedan_dashou_id,
amount=dashou_fencheng,
)
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. 更新客服统计数据(管理员无客服扩展表,跳过)
if kefu is not None:
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}")
# 不影响主流程
if kefu is not None:
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 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 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
from jituan.services.fund_freeze import credit_dashou_order_settle
credit_dashou_order_settle(
order=order,
dashou_profile=dashou_profile,
user_id=dashou_id,
amount=dashou_fencheng,
)
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': '服务器内部错误'})