1851 lines
82 KiB
Python
1851 lines
82 KiB
Python
"""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': '服务器内部错误'})
|