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