覆盖商家/管事/组长资金与封禁、冻结解冻、后台账号变更;列表接口权限 000001。 Co-authored-by: Cursor <cursoragent@cursor.com>
11200 lines
474 KiB
Python
11200 lines
474 KiB
Python
import threading
|
||
import traceback
|
||
from calendar import monthrange
|
||
|
||
from django.contrib.auth.hashers import make_password
|
||
from django.db.models import Q, Count
|
||
from django.core.paginator import Paginator
|
||
|
||
|
||
|
||
from dingdan.models import Lilubiao
|
||
|
||
import uuid
|
||
|
||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||
|
||
from utils.oss_utils import upload_to_oss, validate_image, delete_from_oss
|
||
from shangpin.models import Shangpin, ShangpinLeixing, ShangpinZhuanqu,Huiyuan
|
||
|
||
|
||
import hashlib
|
||
from django.db.models import Q, Sum, F
|
||
from rest_framework import status
|
||
|
||
from houtai.models import Role, Permission, RolePermission, UserRole, TixianRiTongji
|
||
from houtai.utils import verify_kefu_permission
|
||
import xmltodict
|
||
|
||
from django.conf import settings
|
||
from utils.xcx_sys_config import wx_cfg
|
||
|
||
from dingdan.models import Dashoutupian, Chufajilu, PreSettlement, OrderDashouHistory
|
||
# apps/houtai/views.py 继续添加
|
||
|
||
from django.views.decorators.csrf import csrf_exempt
|
||
from django.utils.decorators import method_decorator
|
||
|
||
from houtai.models import Role
|
||
|
||
from yonghu.models import UserMain, UserGuanshi, UserBoss, UserZuzhang
|
||
|
||
from shangpin.models import DuociFenhong
|
||
|
||
import time
|
||
import logging
|
||
import requests
|
||
from datetime import date, datetime, timedelta
|
||
from django.utils import timezone
|
||
|
||
from decimal import Decimal
|
||
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from rest_framework.views import APIView
|
||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||
|
||
from rest_framework.response import Response
|
||
from rest_framework.parsers import JSONParser
|
||
|
||
from dingdan.utils import update_daily_dispatch_stat
|
||
from shangpin.models import ShangpinLeixing, Huiyuangoumai, Huiyuan
|
||
from yonghu.models import UserMain, UserKefu, UserDashou, Xiugaijilu, UserShangjia
|
||
from peizhi.models import AccountPermission, ClubConfig, Club, DailyDispatchStat, OrderTypeMapping, TixianQuotaDefault
|
||
from dingdan.models import Dingdan, Tuikuanjilu, CrossPlatformOrderData
|
||
|
||
# views.py
|
||
import os
|
||
from peizhi.models import PopupPage, PopupConfig, PopupImage
|
||
from .serializers import PopupPageSerializer, PopupConfigSerializer, PopupImageSerializer
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class GetClubConfigView(APIView):
|
||
"""
|
||
获取对方俱乐部配置列表(用于跨平台选择)
|
||
权限要求:客服身份 + 权限代码1且开启
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
"""
|
||
处理 POST 请求,返回对方俱乐部配置数据(is_self=0)
|
||
"""
|
||
try:
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '003aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
|
||
|
||
# 5. 查询所有对方俱乐部(is_self=0),只取必要字段
|
||
club_configs = ClubConfig.objects.filter(
|
||
is_self=0 # 只返回对方俱乐部
|
||
).only(
|
||
'club_id',
|
||
'club_nickname',
|
||
'club_avatar',
|
||
'storage_bucket_domain' # 如果前端需要存储桶域名,一并返回
|
||
)
|
||
|
||
# 构造返回数据列表
|
||
data_list = []
|
||
for club in club_configs:
|
||
data_list.append({
|
||
'club_id': club.club_id,
|
||
'club_nickname': club.club_nickname or club.club_id, # 若昵称为空,用ID代替
|
||
'club_avatar': club.club_avatar or '', # 可能为空
|
||
'storage_bucket_domain': club.storage_bucket_domain or '' # 可选
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'success',
|
||
'data': data_list
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取俱乐部配置失败: {str(e)}")
|
||
return Response({
|
||
'code': 500,
|
||
'msg': '服务器内部错误'
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
class GetCrossOrderListView(APIView):
|
||
"""
|
||
获取跨平台订单列表(客服后台)
|
||
权限要求:客服身份 + 权限代码1且开启
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
"""
|
||
处理 POST 请求,返回分页订单数据及统计信息
|
||
"""
|
||
try:
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '003aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看俱乐部订单'})
|
||
|
||
# 5. 获取请求参数
|
||
data = request.data
|
||
|
||
|
||
|
||
|
||
# 分页参数
|
||
page = int(data.get('page', 1))
|
||
page_size = int(data.get('pageSize', 20))
|
||
|
||
# 查询参数
|
||
leixing = data.get('leixing') # 订单类型ID
|
||
zhuangtai = data.get('zhuangtai') # 状态字符串,如 "1,7"
|
||
club_id = data.get('club_id') # 对方俱乐部ID(精确)
|
||
direction = data.get('direction') # send/receive/all
|
||
dingdan_id = data.get('dingdan_id') # 订单ID模糊搜索
|
||
club_id_search = data.get('club_id_search') # 对方俱乐部ID模糊搜索
|
||
date = data.get('date') # 日期 YYYY-MM-DD
|
||
dashou_id = data.get('dashou_id') # 打手ID(接单打手ID)
|
||
clkf = data.get('clkf') # 处理客服
|
||
|
||
# 6. 构建基础查询(跨平台订单)
|
||
base_q = Q(is_cross=1)
|
||
|
||
# 添加筛选条件(非空且非全部)
|
||
if leixing and leixing != '':
|
||
base_q &= Q(leixing_id=leixing)
|
||
|
||
if zhuangtai and zhuangtai != 'all':
|
||
# 解析逗号分隔的状态码
|
||
status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip().isdigit()]
|
||
if status_list:
|
||
base_q &= Q(zhuangtai__in=status_list)
|
||
|
||
if club_id and club_id != '':
|
||
base_q &= Q(partner_club_id=club_id)
|
||
|
||
# 方向映射
|
||
if direction == 'send':
|
||
base_q &= Q(dispatch_type=1)
|
||
elif direction == 'receive':
|
||
base_q &= Q(dispatch_type=2)
|
||
# direction == 'all' 时不加方向条件
|
||
|
||
if dingdan_id:
|
||
base_q &= Q(dingdan_id__icontains=dingdan_id)
|
||
|
||
if club_id_search:
|
||
base_q &= Q(partner_club_id__icontains=club_id_search)
|
||
|
||
if date:
|
||
# 按日期精确过滤(假设 create_time 是带时区的 DateTimeField)
|
||
base_q &= Q(create_time__date=date)
|
||
|
||
if dashou_id:
|
||
base_q &= Q(jiedan_dashou_id=dashou_id)
|
||
|
||
if clkf:
|
||
base_q &= Q(clkf__icontains=clkf)
|
||
|
||
# 7. 统计各状态数量(基于相同过滤条件)
|
||
# 获取所有符合条件的订单的状态分组统计
|
||
status_counts = Dingdan.objects.filter(base_q).values('zhuangtai').annotate(count=Count('id')).order_by()
|
||
|
||
# 转换为字典 {状态码: 数量}
|
||
status_map = {item['zhuangtai']: item['count'] for item in status_counts}
|
||
|
||
# 定义状态列表(与前端一致)
|
||
status_list_map = {
|
||
'all': 0, # 稍后填充总数量
|
||
'1,7': 0,
|
||
'2': 0,
|
||
'8': 0,
|
||
'3': 0,
|
||
'4': 0,
|
||
'5': 0,
|
||
'6': 0,
|
||
}
|
||
# 填充总数
|
||
status_list_map['all'] = sum(status_map.values())
|
||
# 填充单个状态
|
||
for code, cnt in status_map.items():
|
||
if code == 1 or code == 7:
|
||
status_list_map['1,7'] += cnt
|
||
elif code in status_list_map:
|
||
status_list_map[str(code)] = cnt
|
||
# 其他状态暂不处理
|
||
|
||
# 构建 stats 统计
|
||
total_orders = status_list_map['all']
|
||
completed_orders = status_map.get(3, 0)
|
||
refund_orders = status_map.get(5, 0)
|
||
pending_orders = status_map.get(8, 0) + status_map.get(4, 0)
|
||
|
||
stats = {
|
||
'totalOrders': total_orders,
|
||
'completedOrders': completed_orders,
|
||
'refundOrders': refund_orders,
|
||
'pendingOrders': pending_orders,
|
||
}
|
||
|
||
# 8. 分页获取订单列表
|
||
# 按创建时间倒序,保证分页稳定
|
||
orders_qs = Dingdan.objects.filter(base_q).order_by('-create_time')
|
||
|
||
# 使用 Paginator 分页
|
||
paginator = Paginator(orders_qs, page_size)
|
||
current_page = paginator.get_page(page)
|
||
order_list = current_page.object_list
|
||
|
||
# 只返回前端需要的字段,减少数据传输
|
||
data_list = []
|
||
for order in order_list:
|
||
data_list.append({
|
||
'dingdan_id': order.dingdan_id,
|
||
'nicheng': order.nicheng or '',
|
||
'jieshao': order.jieshao or '',
|
||
'jiage': float(order.jine) if order.jine else 0,
|
||
'zhuangtai': order.zhuangtai,
|
||
})
|
||
|
||
# 9. 返回结果
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'success',
|
||
'data': {
|
||
'list': data_list,
|
||
'total': total_orders,
|
||
'statusCounts': status_list_map,
|
||
'stats': stats,
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取跨平台订单列表失败: {str(e)}")
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class GetCrossOrderDetailView(APIView):
|
||
"""
|
||
客服获取跨平台订单详情接口
|
||
请求:POST /houtai/hqkptddxq
|
||
参数:{"username": "客服手机号", "dingdan_id": "订单ID"}
|
||
认证:JWT,权限码1
|
||
返回:code=0 + 订单详情数据(包含俱乐部信息、打手图片、处罚记录等)
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
start_time = time.time()
|
||
|
||
# 1. 获取参数
|
||
username = request.data.get('username', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
if not username or not dingdan_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
try:
|
||
# 2. 身份验证
|
||
current_user = request.user
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '003aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看俱乐部订单'})
|
||
|
||
# 4. 查询订单
|
||
dingdan_obj = Dingdan.objects.select_related(
|
||
'pingtai_kuozhan',
|
||
'shangjia_kuozhan'
|
||
).get(dingdan_id=dingdan_id)
|
||
|
||
# 5. 构建基础响应数据
|
||
response_data = {
|
||
'dingdan_id': dingdan_obj.dingdan_id or '',
|
||
'zhuangtai': dingdan_obj.zhuangtai or 0,
|
||
'fadanpingtai': dingdan_obj.fadan_pingtai or 0,
|
||
'dispatch_type': dingdan_obj.dispatch_type or 0, # 1=我方派单, 2=对方派单
|
||
'is_cross': dingdan_obj.is_cross or 0,
|
||
'partner_order_id': dingdan_obj.partner_order_id or '',
|
||
'partner_club_id': dingdan_obj.partner_club_id or '',
|
||
'jine': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00,
|
||
'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00,
|
||
'guanshi_fencheng': float(dingdan_obj.guanshi_fencheng) if dingdan_obj.guanshi_fencheng else 0.00,
|
||
'guanshi_shangjia_fencheng': float(dingdan_obj.guanshi_shangjia_fencheng) if dingdan_obj.guanshi_shangjia_fencheng else 0.00,
|
||
'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '',
|
||
'dashou_liuyan': dingdan_obj.dashou_liuyan or '',
|
||
'zhiding_id': dingdan_obj.zhiding_id or '',
|
||
'shangpin_id': dingdan_obj.shangpin_id or '',
|
||
'leixing': dingdan_obj.leixing_id or 0,
|
||
'tupian_url': dingdan_obj.tupian or '',
|
||
'jieshao': dingdan_obj.jieshao or '',
|
||
'beizhu': dingdan_obj.beizhu or '',
|
||
'tkly': dingdan_obj.tkly or '',
|
||
'nicheng': dingdan_obj.nicheng or '',
|
||
'clkf': dingdan_obj.clkf or '',
|
||
'chuangjianshijian': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.create_time else '',
|
||
'genggaishijian': dingdan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.update_time else '',
|
||
}
|
||
|
||
# 6. 扩展表数据
|
||
if dingdan_obj.fadan_pingtai == 1:
|
||
if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan:
|
||
response_data['pingtai_kuozhan'] = {
|
||
'laoban_id': dingdan_obj.pingtai_kuozhan.laoban_id or '',
|
||
'laoban_pingjia': dingdan_obj.pingtai_kuozhan.laoban_pingjia or '',
|
||
}
|
||
else:
|
||
response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''}
|
||
elif dingdan_obj.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.shangjia_id or '',
|
||
'sjnicheng': shangjia.sjnicheng or '',
|
||
'shangjia_pingjia': shangjia.shangjia_pingjia or '',
|
||
'cfliyou': shangjia.cfliyou or '',
|
||
'sqzhuangtai': shangjia.sqzhuangtai if shangjia.sqzhuangtai is not None else 0,
|
||
'bhliyou': shangjia.bhliyou or '',
|
||
}
|
||
else:
|
||
response_data['shangjia_kuozhan'] = {
|
||
'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '',
|
||
'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': ''
|
||
}
|
||
|
||
# ========== 核心逻辑:判断派单/接单关系 ==========
|
||
dispatch_type = dingdan_obj.dispatch_type # 1=我方派单, 2=对方派单
|
||
is_cross = dingdan_obj.is_cross
|
||
partner_club_id = dingdan_obj.partner_club_id
|
||
partner_order_id = dingdan_obj.partner_order_id
|
||
|
||
# 获取我方俱乐部配置(用于本地存储桶域名)
|
||
my_club_config = ClubConfig.objects.filter(is_self=1).first()
|
||
my_storage_domain = my_club_config.storage_bucket_domain if my_club_config else ''
|
||
|
||
# 初始化字段
|
||
response_data['club_info'] = None # 对方俱乐部信息
|
||
response_data['partner_images'] = [] # 打手提交图片(对方平台打手或我方打手)
|
||
response_data['punishment_info'] = {} # 处罚信息
|
||
response_data['is_punished'] = False # 是否已处罚(用于前端禁用按钮)
|
||
response_data['receive_side'] = '' # 接单方:'self' 我方接单,'partner' 对方接单
|
||
response_data['dispatch_side'] = '' # 派单方:'self' 我方派单,'partner' 对方派单
|
||
|
||
# 设置派单方
|
||
if dispatch_type == 1:
|
||
response_data['dispatch_side'] = 'self'
|
||
elif dispatch_type == 2:
|
||
response_data['dispatch_side'] = 'partner'
|
||
else:
|
||
# 兼容旧数据:根据 partner_club_id 或 partner_order_id 推断
|
||
if partner_club_id or partner_order_id:
|
||
response_data['dispatch_side'] = 'partner'
|
||
else:
|
||
response_data['dispatch_side'] = 'self'
|
||
|
||
# 情况1:对方派单(对方平台发单,我方打手接单)
|
||
if response_data['dispatch_side'] == 'partner':
|
||
# 接单方为我方
|
||
response_data['receive_side'] = 'self'
|
||
|
||
# 获取对方俱乐部信息(用于展示)
|
||
if partner_club_id:
|
||
partner_config = ClubConfig.objects.filter(club_id=partner_club_id, is_self=0).first()
|
||
if partner_config:
|
||
response_data['club_info'] = {
|
||
'club_id': partner_config.club_id,
|
||
'club_nickname': partner_config.club_nickname or partner_config.club_id,
|
||
'club_avatar': partner_config.club_avatar or '',
|
||
}
|
||
else:
|
||
response_data['club_info'] = {'club_id': partner_club_id, 'club_nickname': '', 'club_avatar': ''}
|
||
else:
|
||
response_data['club_info'] = None
|
||
|
||
# 查询我方打手提交的图片(本地)
|
||
dashou_images = Dashoutupian.objects.filter(
|
||
dingdan_id=dingdan_id,
|
||
dashou=dingdan_obj.jiedan_dashou_id
|
||
).values_list('tupian', flat=True)
|
||
images = []
|
||
for rel_path in dashou_images:
|
||
if rel_path:
|
||
if rel_path.startswith('http'):
|
||
images.append(rel_path)
|
||
else:
|
||
images.append(my_storage_domain + rel_path)
|
||
response_data['partner_images'] = images
|
||
|
||
# 查询我方打手处罚记录
|
||
punish_records = Chufajilu.objects.filter(
|
||
dingdan_id=dingdan_id,
|
||
dashouid=dingdan_obj.jiedan_dashou_id
|
||
).order_by('-create_time')
|
||
if punish_records.exists():
|
||
latest = punish_records.first()
|
||
response_data['punishment_info'] = {
|
||
'cfliyou': latest.cfliyou or '',
|
||
'sqzhuangtai': latest.sqzhuangtai or 0,
|
||
'bhliyou': latest.bhliyou or '',
|
||
'ssliyou': latest.ssliyou or '',
|
||
'jifen': latest.jifen or 0,
|
||
}
|
||
response_data['is_punished'] = True
|
||
else:
|
||
response_data['punishment_info'] = {}
|
||
response_data['is_punished'] = False
|
||
|
||
# 情况2:我方派单(我方平台发单)
|
||
else:
|
||
response_data['dispatch_side'] = 'self'
|
||
# 根据是否有对方订单ID/俱乐部ID判断接单方
|
||
if partner_club_id or partner_order_id:
|
||
# 有对方信息,说明订单派给了对方平台,对方接单
|
||
response_data['receive_side'] = 'partner'
|
||
|
||
# 获取对方俱乐部信息
|
||
if partner_club_id:
|
||
partner_config = ClubConfig.objects.filter(club_id=partner_club_id, is_self=0).first()
|
||
if partner_config:
|
||
response_data['club_info'] = {
|
||
'club_id': partner_config.club_id,
|
||
'club_nickname': partner_config.club_nickname or partner_config.club_id,
|
||
'club_avatar': partner_config.club_avatar or '',
|
||
}
|
||
else:
|
||
response_data['club_info'] = {'club_id': partner_club_id, 'club_nickname': '', 'club_avatar': ''}
|
||
|
||
# 需要调用对方接口获取图片和处罚记录
|
||
if partner_club_id and partner_order_id:
|
||
# 获取对方服务器域名
|
||
if my_club_config:
|
||
self_club_id = my_club_config.club_id
|
||
else:
|
||
logger.error(f"无法获取本俱乐部ID,订单ID: {dingdan_id}")
|
||
return Response({'code': 500, 'msg': '系统配置错误,请联系管理员'})
|
||
|
||
try:
|
||
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
|
||
partner_domain = club_rel.partner_domain
|
||
except ObjectDoesNotExist:
|
||
logger.error(f"未找到合作关系: {self_club_id} <-> {partner_club_id}")
|
||
return Response({'code': 500, 'msg': '未找到对方平台配置'})
|
||
|
||
# 调用对方接口,最多重试3次
|
||
url = partner_domain.rstrip('/') + '/houtai/kpthqtp'
|
||
payload = {
|
||
'order_id': partner_order_id,
|
||
'club_id': self_club_id,
|
||
}
|
||
timeout = 3
|
||
retries = 3
|
||
success = False
|
||
for attempt in range(retries):
|
||
try:
|
||
resp = requests.post(url, json=payload, timeout=timeout)
|
||
if resp.status_code == 200:
|
||
resp_json = resp.json()
|
||
if resp_json.get('code') == 0:
|
||
data = resp_json.get('data', {})
|
||
response_data['partner_images'] = data.get('images', [])
|
||
response_data['punishment_info'] = data.get('punishment', {})
|
||
response_data['is_punished'] = bool(response_data['punishment_info'].get('cfliyou'))
|
||
success = True
|
||
break
|
||
else:
|
||
logger.warning(f"对方接口返回错误 (尝试{attempt+1}): {resp_json.get('msg')}")
|
||
else:
|
||
logger.warning(f"对方接口HTTP错误 (尝试{attempt+1}): {resp.status_code}")
|
||
except requests.exceptions.Timeout:
|
||
logger.warning(f"请求对方接口超时 (尝试{attempt+1}): {url}")
|
||
except Exception as e:
|
||
logger.warning(f"请求对方接口异常 (尝试{attempt+1}): {str(e)}")
|
||
if not success:
|
||
# 三次失败,返回部分数据,但提示前端对方数据获取失败
|
||
response_data['partner_images'] = []
|
||
response_data['punishment_info'] = {}
|
||
response_data['is_punished'] = False
|
||
response_data['partner_data_error'] = '获取对方平台数据失败,请稍后重试'
|
||
else:
|
||
# 对方信息不完整,无法获取数据
|
||
response_data['partner_images'] = []
|
||
response_data['punishment_info'] = {}
|
||
response_data['is_punished'] = False
|
||
response_data['partner_data_error'] = '对方订单信息不完整'
|
||
else:
|
||
# 无对方信息,说明我方派单且我方接单
|
||
response_data['receive_side'] = 'self'
|
||
# 我方打手提交图片(本地)
|
||
dashou_images = Dashoutupian.objects.filter(
|
||
dingdan_id=dingdan_id,
|
||
dashou=dingdan_obj.jiedan_dashou_id
|
||
).values_list('tupian', flat=True)
|
||
images = []
|
||
for rel_path in dashou_images:
|
||
if rel_path:
|
||
if rel_path.startswith('http'):
|
||
images.append(rel_path)
|
||
else:
|
||
images.append(my_storage_domain + rel_path)
|
||
response_data['partner_images'] = images
|
||
|
||
# 处罚记录
|
||
punish_records = Chufajilu.objects.filter(
|
||
dingdan_id=dingdan_id,
|
||
dashouid=dingdan_obj.jiedan_dashou_id
|
||
).order_by('-create_time')
|
||
if punish_records.exists():
|
||
latest = punish_records.first()
|
||
response_data['punishment_info'] = {
|
||
'cfliyou': latest.cfliyou or '',
|
||
'sqzhuangtai': latest.sqzhuangtai or 0,
|
||
'bhliyou': latest.bhliyou or '',
|
||
'ssliyou': latest.ssliyou or '',
|
||
'jifen': latest.jifen or 0,
|
||
}
|
||
response_data['is_punished'] = True
|
||
else:
|
||
response_data['punishment_info'] = {}
|
||
response_data['is_punished'] = False
|
||
|
||
# 如果是我方派单且我方接单,对方俱乐部信息为空
|
||
response_data['club_info'] = None
|
||
|
||
# 7. 查询退款理由
|
||
try:
|
||
tuikuan_record = Tuikuanjilu.objects.filter(
|
||
dingdan_id=dingdan_id
|
||
).order_by('-create_time').first()
|
||
response_data['tuikuan_liyou'] = tuikuan_record.liyou if tuikuan_record else ''
|
||
except Exception as e:
|
||
logger.warning(f"查询退款记录失败: {str(e)}")
|
||
response_data['tuikuan_liyou'] = ''
|
||
|
||
elapsed = time.time() - start_time
|
||
logger.info(f"获取跨平台订单详情接口耗时: {elapsed:.3f}s, 订单ID: {dingdan_id}")
|
||
return Response({'code': 0, 'data': response_data})
|
||
|
||
except Dingdan.DoesNotExist:
|
||
logger.warning(f"订单不存在: {dingdan_id}")
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
except Exception as e:
|
||
logger.error(f"获取跨平台订单详情接口异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
import hmac
|
||
import hashlib
|
||
import json
|
||
import logging
|
||
import time
|
||
import random
|
||
import string
|
||
import traceback
|
||
import requests
|
||
import xmltodict
|
||
from decimal import Decimal
|
||
|
||
from django.conf import settings
|
||
from utils.xcx_sys_config import wx_cfg
|
||
from django.db import transaction
|
||
from django.core.cache import cache
|
||
from rest_framework.views import APIView
|
||
from rest_framework.response import Response
|
||
from rest_framework import permissions, status
|
||
from rest_framework.parsers import JSONParser
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
|
||
from yonghu.models import UserMain
|
||
from dingdan.models import Dingdan, Tuikuanjilu, CrossPlatformOrderData
|
||
from shangpin.models import Shangpin
|
||
from peizhi.models import Szjilu, ClubConfig, Club
|
||
from dingdan.utils import (
|
||
update_daily_payout
|
||
)
|
||
from .utils import verify_kefu_permission
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class CrossPlatformRefundView(APIView):
|
||
"""
|
||
跨平台订单退款接口(同时支持本地订单和跨平台订单)
|
||
请求:POST /houtai/kptk
|
||
参数:{
|
||
"username": "客服账号ID",
|
||
"dingdan_id": "订单ID",
|
||
"tuikuan_liyou": "退款理由(可选)"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 成功,其他失败
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
username = request.data.get('username', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
|
||
|
||
if not username or not dingdan_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 身份验证
|
||
current_user = request.user
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '003aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权处理订单'})
|
||
|
||
# 4. 查询订单(带扩展表)
|
||
try:
|
||
order = Dingdan.objects.select_related(
|
||
'pingtai_kuozhan',
|
||
'shangjia_kuozhan'
|
||
).get(dingdan_id=dingdan_id)
|
||
except Dingdan.DoesNotExist:
|
||
logger.warning(f"订单不存在: {dingdan_id}")
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
# 5. 订单状态校验(只有 1,7,2,8,4 可退款)
|
||
allowed_statuses = [1, 7, 2, 8, 4]
|
||
if order.zhuangtai not in allowed_statuses:
|
||
logger.warning(f"订单状态不允许退款: {order.zhuangtai}")
|
||
return Response({'code': 400, 'msg': '订单状态不允许退款'})
|
||
|
||
# 6. 根据派单方向分流
|
||
dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单
|
||
|
||
# ---------- 情况一:对方派单(我方打手接对方的单)----------
|
||
if dispatch_type == 2:
|
||
# 对方派单,资金在对方平台,我方只处理打手状态和订单状态
|
||
with transaction.atomic():
|
||
order.zhuangtai = 5
|
||
order.clkf = current_user.phone
|
||
if tuikuan_liyou:
|
||
order.tkly = tuikuan_liyou
|
||
order.save()
|
||
|
||
dashou_id = order.jiedan_dashou_id
|
||
if dashou_id:
|
||
try:
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
dashou_profile.tuikuanliang += 1
|
||
dashou_profile.zhuangtai = 1
|
||
dashou_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"对方派单退款:打手 {dashou_id} 不存在,跳过更新")
|
||
|
||
try:
|
||
refund_record, created = Tuikuanjilu.objects.get_or_create(
|
||
dingdan_id=dingdan_id,
|
||
defaults={
|
||
'dashouid': dashou_id or '',
|
||
'qingqiuid': '',
|
||
'chuliid': current_user.phone,
|
||
'liyou': tuikuan_liyou or '客服退款(对方派单)',
|
||
'sqzhuangtai': 1,
|
||
'jine': order.jine or 0,
|
||
'dashou_fencheng': order.dashou_fencheng or 0,
|
||
'jieshao': order.jieshao or '',
|
||
'beizhu': '对方派单退款',
|
||
'nicheng': order.nicheng or '',
|
||
}
|
||
)
|
||
if not created:
|
||
refund_record.sqzhuangtai = 1
|
||
refund_record.chuliid = current_user.phone
|
||
if tuikuan_liyou:
|
||
refund_record.liyou = tuikuan_liyou
|
||
refund_record.save()
|
||
except Exception as e:
|
||
logger.error(f"更新退款记录失败: {str(e)}")
|
||
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
logger.info(f"对方派单退款成功,订单 {dingdan_id}")
|
||
return Response({'code': 0, 'msg': '退款成功'})
|
||
|
||
# ---------- 情况二:我方派单(订单由我方发出)----------
|
||
is_cross = order.is_cross
|
||
partner_club_id = order.partner_club_id
|
||
partner_order_id = order.partner_order_id
|
||
|
||
# 子情况2.1:本地订单(非跨平台)
|
||
if not is_cross or not partner_club_id:
|
||
if order.fadan_pingtai == 1:
|
||
# 平台订单退款
|
||
return self._refund_platform_order(order, current_user, tuikuan_liyou, kefu)
|
||
elif order.fadan_pingtai == 2:
|
||
# 商家订单退款
|
||
return self._refund_merchant_order(order, current_user, tuikuan_liyou, kefu)
|
||
else:
|
||
return Response({'code': 400, 'msg': '未知的发单平台类型'})
|
||
|
||
# 子情况2.2:跨平台订单(我方派单给对方)
|
||
else:
|
||
local_refund_success = False
|
||
try:
|
||
with transaction.atomic():
|
||
if order.fadan_pingtai == 1:
|
||
refund_result = self._call_wechat_refund(order)
|
||
if refund_result['code'] != 0:
|
||
raise Exception(refund_result['msg'])
|
||
order.zhuangtai = 5
|
||
order.clkf = current_user.phone
|
||
if tuikuan_liyou:
|
||
order.tkly = tuikuan_liyou
|
||
order.save()
|
||
local_refund_success = True
|
||
elif order.fadan_pingtai == 2:
|
||
self._refund_to_merchant(order, current_user.phone, tuikuan_liyou)
|
||
order.zhuangtai = 5
|
||
order.clkf = current_user.phone
|
||
if tuikuan_liyou:
|
||
order.tkly = tuikuan_liyou
|
||
order.save()
|
||
local_refund_success = True
|
||
else:
|
||
return Response({'code': 400, 'msg': '未知的发单平台类型'})
|
||
|
||
dashou_id = order.jiedan_dashou_id
|
||
if dashou_id:
|
||
try:
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
dashou_profile.tuikuanliang += 1
|
||
dashou_profile.zhuangtai = 1
|
||
dashou_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"跨平台退款:打手 {dashou_id} 不存在,跳过更新")
|
||
|
||
self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou)
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
except Exception as e:
|
||
logger.error(f"跨平台订单本地退款失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': f'本地退款失败: {str(e)}'})
|
||
|
||
# 本地退款成功后,通知对方平台
|
||
if local_refund_success:
|
||
try:
|
||
my_club_config = ClubConfig.objects.filter(is_self=1).first()
|
||
if not my_club_config:
|
||
logger.error("未配置我方俱乐部信息")
|
||
else:
|
||
self_club_id = my_club_config.club_id
|
||
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
|
||
partner_domain = club_rel.partner_domain
|
||
|
||
notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk'
|
||
payload = {
|
||
'order_id': partner_order_id,
|
||
'club_id': self_club_id,
|
||
'status': 5,
|
||
'reason': tuikuan_liyou
|
||
}
|
||
try:
|
||
resp = requests.post(notify_url, json=payload, timeout=3)
|
||
if resp.status_code == 200:
|
||
resp_json = resp.json()
|
||
if resp_json.get('code') == 0:
|
||
cross_data, _ = CrossPlatformOrderData.objects.get_or_create(
|
||
dingdan_id=dingdan_id,
|
||
defaults={
|
||
'partner_order_id': partner_order_id,
|
||
'partner_club_id': partner_club_id,
|
||
'partner_order_status': resp_json.get('data', {}).get('partner_status', 0),
|
||
'partner_dashou_id': resp_json.get('data', {}).get('partner_dashou_id', ''),
|
||
'partner_amount': order.jine or 0,
|
||
}
|
||
)
|
||
if not _:
|
||
cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0)
|
||
cross_data.save()
|
||
else:
|
||
logger.warning(f"对方平台返回错误: {resp_json.get('msg')}")
|
||
else:
|
||
logger.warning(f"对方平台接口HTTP错误: {resp.status_code}")
|
||
except requests.exceptions.Timeout:
|
||
logger.error(f"通知对方平台超时: {notify_url}")
|
||
except Exception as e:
|
||
logger.error(f"通知对方平台异常: {str(e)}")
|
||
except Exception as e:
|
||
logger.error(f"查询对方平台配置失败: {str(e)}")
|
||
|
||
return Response({'code': 0, 'msg': '退款成功'})
|
||
|
||
# ========== 辅助方法 ==========
|
||
def _refund_platform_order(self, order, current_user, tuikuan_liyou, kefu):
|
||
"""平台订单退款(调用微信支付)"""
|
||
refund_result = self._call_wechat_refund(order)
|
||
if refund_result['code'] != 0:
|
||
return Response({'code': 400, 'msg': refund_result['msg']})
|
||
|
||
with transaction.atomic():
|
||
order.zhuangtai = 5
|
||
order.clkf = current_user.phone
|
||
if tuikuan_liyou:
|
||
order.tkly = tuikuan_liyou
|
||
order.save()
|
||
|
||
dashou_id = order.jiedan_dashou_id
|
||
if dashou_id:
|
||
try:
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
dashou_profile.tuikuanliang += 1
|
||
dashou_profile.zhuangtai = 1
|
||
dashou_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"平台退款:打手 {dashou_id} 不存在,跳过更新")
|
||
|
||
self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou)
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
logger.info(f"平台订单退款成功: {order.dingdan_id}")
|
||
return Response({'code': 0, 'msg': '退款成功'})
|
||
|
||
def _refund_merchant_order(self, order, current_user, tuikuan_liyou, kefu):
|
||
"""商家订单退款(退余额给商家)"""
|
||
try:
|
||
shangjia_ext = order.shangjia_kuozhan
|
||
shangjia_id = shangjia_ext.shangjia_id
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 400, 'msg': '订单商家信息不存在'})
|
||
|
||
with transaction.atomic():
|
||
order.zhuangtai = 5
|
||
order.clkf = current_user.phone
|
||
if tuikuan_liyou:
|
||
order.tkly = tuikuan_liyou
|
||
order.save()
|
||
|
||
try:
|
||
shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
|
||
shangjia_profile = shangjia_user.shop_profile
|
||
shangjia_profile.tuikuan += 1
|
||
shangjia_profile.yue += order.jine or 0
|
||
shangjia_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.error(f"商家退款:商家 {shangjia_id} 不存在,无法退余额")
|
||
raise Exception("商家账户不存在")
|
||
|
||
dashou_id = order.jiedan_dashou_id
|
||
if dashou_id:
|
||
try:
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
dashou_profile.tuikuanliang += 1
|
||
dashou_profile.zhuangtai = 1
|
||
dashou_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"商家退款:打手 {dashou_id} 不存在,跳过更新")
|
||
|
||
self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou)
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
logger.info(f"商家订单退款成功: {order.dingdan_id}")
|
||
return Response({'code': 0, 'msg': '退款成功'})
|
||
|
||
def _call_wechat_refund(self, order):
|
||
"""
|
||
调用虚拟支付退款接口 — 完全模仿 KefuPlatformRefundView 的 call_virtual_refund 方法
|
||
使用 /xpay/refund_order 接口,字段名、请求体结构、签名方式与成功接口保持一致
|
||
"""
|
||
uri = "/xpay/refund_order"
|
||
base_url = "https://api.weixin.qq.com"
|
||
appkey = settings.VP_APPKEY_SANDBOX if settings.VP_ENV == 1 else settings.VP_APPKEY
|
||
|
||
# 金额:元 -> 分
|
||
refund_fee = int(float(order.jine or 0) * 100)
|
||
|
||
# 获取老板 openid(和成功接口完全相同的路径)
|
||
laoban_openid = None
|
||
laoban_id = order.pingtai_kuozhan.laoban_id if hasattr(order, 'pingtai_kuozhan') else None
|
||
if laoban_id:
|
||
laoban_user = UserMain.objects.filter(yonghuid=laoban_id).first()
|
||
if laoban_user:
|
||
laoban_openid = getattr(laoban_user, 'openid', None)
|
||
|
||
if not laoban_openid:
|
||
return {'code': 400, 'msg': '未找到用户openid,无法退款'}
|
||
|
||
# 退款单号:R + 时间戳 + 6位随机数(和成功接口一模一样)
|
||
refund_order_id = f"R{int(time.time())}{random.randint(100000, 999999)}"
|
||
|
||
# 请求体:字段名、顺序与成功接口完全一致
|
||
body_data = {
|
||
"openid": laoban_openid,
|
||
"env": settings.VP_ENV,
|
||
"order_id": order.dingdan_id, # 商户订单号(和成功接口一样)
|
||
"refund_order_id": refund_order_id, # 本次退款单号(和成功接口一样)
|
||
"left_fee": refund_fee, # 订单剩余可退金额(和成功接口一样)
|
||
"refund_fee": refund_fee, # 本次退款金额(和成功接口一样)
|
||
"biz_meta": "客服退款", # 业务备注(和成功接口一样)
|
||
"refund_reason": 5, # 退款原因:5 其他(和成功接口一样)
|
||
"req_from": 1 # 退款来源:1 人工客服(和成功接口一样)
|
||
}
|
||
# 如果有微信支付交易单号,优先传入(和成功接口一样)
|
||
if getattr(order, 'wechat_transaction_id', None):
|
||
body_data["wx_order_id"] = order.wechat_transaction_id
|
||
|
||
body = json.dumps(body_data, separators=(",", ":"))
|
||
|
||
# 签名:pay_sig(和成功接口一模一样)
|
||
msg = uri + "&" + body
|
||
pay_sig = hmac.new(appkey.encode("utf-8"), msg.encode("utf-8"), hashlib.sha256).hexdigest()
|
||
|
||
# 获取 access_token
|
||
access_token = self._get_access_token()
|
||
|
||
# 构建 URL:和成功接口一模一样
|
||
url = f"{base_url}{uri}?access_token={access_token}&pay_sig={pay_sig}"
|
||
|
||
headers = {"Content-Type": "application/json"}
|
||
try:
|
||
response = requests.post(url, data=body, headers=headers, timeout=10)
|
||
result = response.json()
|
||
|
||
if result.get("errcode") == 0:
|
||
return {'code': 0, 'msg': '退款成功', 'refund_id': refund_order_id}
|
||
else:
|
||
errmsg = result.get("errmsg", "未知错误")
|
||
logger.error(f"[退款] 退款失败: errcode={result.get('errcode')}, errmsg={errmsg}")
|
||
return {'code': 400, 'msg': f"退款失败: {errmsg}"}
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
logger.error(f"[退款] 网络请求异常: {e}")
|
||
return {'code': 500, 'msg': '微信退款请求网络异常'}
|
||
|
||
def _get_access_token(self):
|
||
"""获取 access_token,带缓存(与成功接口相同)"""
|
||
cache_key = "virtual_payment_access_token"
|
||
access_token = cache.get(cache_key)
|
||
if not access_token:
|
||
logger.info("[退款] 缓存无 access_token,重新获取")
|
||
resp = requests.get(
|
||
"https://api.weixin.qq.com/cgi-bin/token",
|
||
params={
|
||
"grant_type": "client_credential",
|
||
"appid": wx_cfg.WEIXIN_APPID,
|
||
"secret": wx_cfg.WEIXIN_SECRET,
|
||
},
|
||
timeout=10,
|
||
)
|
||
data = resp.json()
|
||
if "access_token" in data:
|
||
access_token = data["access_token"]
|
||
cache.set(cache_key, access_token, data.get("expires_in", 7200) - 300)
|
||
logger.info(f"[退款] access_token 已更新")
|
||
else:
|
||
raise Exception(f"获取 access_token 失败: {data}")
|
||
return access_token
|
||
|
||
def _refund_to_merchant(self, order, clkf_phone, tuikuan_liyou):
|
||
"""给商家退余额(事务内调用,不再独立事务)"""
|
||
try:
|
||
shangjia_ext = order.shangjia_kuozhan
|
||
shangjia_id = shangjia_ext.shangjia_id
|
||
except ObjectDoesNotExist:
|
||
raise Exception("订单商家信息不存在")
|
||
|
||
try:
|
||
shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
|
||
shangjia_profile = shangjia_user.shop_profile
|
||
shangjia_profile.tuikuan += 1
|
||
shangjia_profile.yue += order.jine or 0
|
||
shangjia_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.error(f"商家退款:商家 {shangjia_id} 不存在")
|
||
raise Exception("商家账户不存在")
|
||
|
||
def _update_refund_record(self, order, dashou_id, chuli_phone, tuikuan_liyou):
|
||
"""更新退款记录表"""
|
||
try:
|
||
if order.fadan_pingtai == 1:
|
||
from dingdan.utils import update_daily_payout
|
||
update_daily_payout(order.jine)
|
||
|
||
refund_record, created = Tuikuanjilu.objects.get_or_create(
|
||
dingdan_id=order.dingdan_id,
|
||
defaults={
|
||
'dashouid': dashou_id or '',
|
||
'qingqiuid': '',
|
||
'chuliid': chuli_phone,
|
||
'liyou': tuikuan_liyou or '客服退款',
|
||
'sqzhuangtai': 1,
|
||
'jine': order.jine or 0,
|
||
'dashou_fencheng': order.dashou_fencheng or 0,
|
||
'jieshao': order.jieshao or '',
|
||
'beizhu': '客服退款',
|
||
'nicheng': order.nicheng or '',
|
||
}
|
||
)
|
||
if not created:
|
||
refund_record.sqzhuangtai = 1
|
||
refund_record.chuliid = chuli_phone
|
||
if tuikuan_liyou:
|
||
refund_record.liyou = tuikuan_liyou
|
||
refund_record.save()
|
||
except Exception as e:
|
||
logger.error(f"更新退款记录异常: {str(e)}")
|
||
|
||
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 CrossPlatformRefundView(APIView):
|
||
"""
|
||
跨平台订单退款接口(同时支持本地订单和跨平台订单)
|
||
请求:POST /houtai/kptk
|
||
参数:{
|
||
"username": "客服账号ID",
|
||
"dingdan_id": "订单ID",
|
||
"tuikuan_liyou": "退款理由(可选)"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 成功,其他失败
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
username = request.data.get('username', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
|
||
|
||
if not username or not dingdan_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 身份验证
|
||
current_user = request.user
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '003aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权处理订单'})
|
||
|
||
# 4. 查询订单(带扩展表)
|
||
try:
|
||
order = Dingdan.objects.select_related(
|
||
'pingtai_kuozhan',
|
||
'shangjia_kuozhan'
|
||
).get(dingdan_id=dingdan_id)
|
||
except Dingdan.DoesNotExist:
|
||
logger.warning(f"订单不存在: {dingdan_id}")
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
# 5. 订单状态校验(只有 1,7,2,8,4 可退款)
|
||
allowed_statuses = [1, 7, 2, 8, 4]
|
||
if order.zhuangtai not in allowed_statuses:
|
||
logger.warning(f"订单状态不允许退款: {order.zhuangtai}")
|
||
return Response({'code': 400, 'msg': '订单状态不允许退款'})
|
||
|
||
# 6. 根据派单方向分流
|
||
dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单
|
||
|
||
# ---------- 情况一:对方派单(我方打手接对方的单)----------
|
||
if dispatch_type == 2:
|
||
# 对方派单,资金在对方平台,我方只处理打手状态和订单状态
|
||
with transaction.atomic():
|
||
# 更新订单状态为已退款
|
||
order.zhuangtai = 5
|
||
order.clkf = current_user.phone
|
||
if tuikuan_liyou:
|
||
order.tkly = tuikuan_liyou
|
||
order.save()
|
||
|
||
# 更新打手数据(接单打手)
|
||
dashou_id = order.jiedan_dashou_id
|
||
if dashou_id:
|
||
try:
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
dashou_profile.tuikuanliang += 1
|
||
dashou_profile.zhuangtai = 1 # 设为空闲
|
||
dashou_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"对方派单退款:打手 {dashou_id} 不存在,跳过更新")
|
||
|
||
# 更新退款记录(记录操作)
|
||
try:
|
||
refund_record, created = Tuikuanjilu.objects.get_or_create(
|
||
dingdan_id=dingdan_id,
|
||
defaults={
|
||
'dashouid': dashou_id or '',
|
||
'qingqiuid': '',
|
||
'chuliid': current_user.phone,
|
||
'liyou': tuikuan_liyou or '客服退款(对方派单)',
|
||
'sqzhuangtai': 1,
|
||
'jine': order.jine or 0,
|
||
'dashou_fencheng': order.dashou_fencheng or 0,
|
||
'jieshao': order.jieshao or '',
|
||
'beizhu': '对方派单退款',
|
||
'nicheng': order.nicheng or '',
|
||
}
|
||
)
|
||
if not created:
|
||
refund_record.sqzhuangtai = 1
|
||
refund_record.chuliid = current_user.phone
|
||
if tuikuan_liyou:
|
||
refund_record.liyou = tuikuan_liyou
|
||
refund_record.save()
|
||
except Exception as e:
|
||
logger.error(f"更新退款记录失败: {str(e)}")
|
||
|
||
# 更新客服统计
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
logger.info(f"对方派单退款成功,订单 {dingdan_id}")
|
||
return Response({'code': 0, 'msg': '退款成功'})
|
||
|
||
# ---------- 情况二:我方派单(订单由我方发出)----------
|
||
# 需要进一步判断是本地订单还是跨平台订单
|
||
is_cross = order.is_cross
|
||
partner_club_id = order.partner_club_id
|
||
partner_order_id = order.partner_order_id
|
||
|
||
# 子情况2.1:本地订单(非跨平台)
|
||
if not is_cross or not partner_club_id:
|
||
# 按原有逻辑处理:平台订单或商家订单
|
||
if order.fadan_pingtai == 1:
|
||
# 平台订单退款(调用微信支付)
|
||
return self._refund_platform_order(order, current_user, tuikuan_liyou, kefu)
|
||
elif order.fadan_pingtai == 2:
|
||
# 商家订单退款(直接退余额)
|
||
return self._refund_merchant_order(order, current_user, tuikuan_liyou, kefu)
|
||
else:
|
||
return Response({'code': 400, 'msg': '未知的发单平台类型'})
|
||
|
||
# 子情况2.2:跨平台订单(我方派单给对方)
|
||
else:
|
||
# 必须先执行本地资金退款(因为钱在我方平台)
|
||
# 根据发单平台类型执行退款
|
||
local_refund_success = False
|
||
try:
|
||
with transaction.atomic():
|
||
if order.fadan_pingtai == 1:
|
||
# 平台订单:调用微信退款
|
||
refund_result = self._call_wechat_refund(order)
|
||
if refund_result['code'] != 0:
|
||
raise Exception(refund_result['msg'])
|
||
# 微信退款成功,更新订单状态
|
||
order.zhuangtai = 5
|
||
order.clkf = current_user.phone
|
||
if tuikuan_liyou:
|
||
order.tkly = tuikuan_liyou
|
||
order.save()
|
||
local_refund_success = True
|
||
elif order.fadan_pingtai == 2:
|
||
# 商家订单:退余额给商家
|
||
self._refund_to_merchant(order, current_user.phone, tuikuan_liyou)
|
||
order.zhuangtai = 5
|
||
order.clkf = current_user.phone
|
||
if tuikuan_liyou:
|
||
order.tkly = tuikuan_liyou
|
||
order.save()
|
||
local_refund_success = True
|
||
else:
|
||
return Response({'code': 400, 'msg': '未知的发单平台类型'})
|
||
|
||
# 更新打手数据(接单打手)
|
||
dashou_id = order.jiedan_dashou_id
|
||
if dashou_id:
|
||
try:
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
dashou_profile.tuikuanliang += 1
|
||
dashou_profile.zhuangtai = 1
|
||
dashou_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"跨平台退款:打手 {dashou_id} 不存在,跳过更新")
|
||
|
||
# 更新退款记录
|
||
self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou)
|
||
|
||
# 更新客服统计
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
except Exception as e:
|
||
logger.error(f"跨平台订单本地退款失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': f'本地退款失败: {str(e)}'})
|
||
|
||
# 本地退款成功后,通知对方平台
|
||
if local_refund_success:
|
||
# 查询对方平台域名
|
||
try:
|
||
# 获取我方俱乐部ID
|
||
my_club_config = ClubConfig.objects.filter(is_self=1).first()
|
||
if not my_club_config:
|
||
logger.error("未配置我方俱乐部信息")
|
||
# 本地已成功,仅记录错误,不影响返回
|
||
else:
|
||
self_club_id = my_club_config.club_id
|
||
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
|
||
partner_domain = club_rel.partner_domain
|
||
|
||
# 调用对方退款通知接口
|
||
notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk'
|
||
payload = {
|
||
'order_id': partner_order_id, # 对方订单ID
|
||
'club_id': self_club_id, # 我方俱乐部ID
|
||
'status': 5, # 通知对方我方已退款
|
||
'reason': tuikuan_liyou
|
||
}
|
||
# 设置超时3秒,不阻塞主流程
|
||
try:
|
||
resp = requests.post(notify_url, json=payload, timeout=3)
|
||
if resp.status_code == 200:
|
||
resp_json = resp.json()
|
||
if resp_json.get('code') == 0:
|
||
# 更新跨平台订单扩展表
|
||
cross_data, _ = CrossPlatformOrderData.objects.get_or_create(
|
||
dingdan_id=dingdan_id,
|
||
defaults={
|
||
'partner_order_id': partner_order_id,
|
||
'partner_club_id': partner_club_id,
|
||
'partner_order_status': resp_json.get('data', {}).get('partner_status', 0),
|
||
'partner_dashou_id': resp_json.get('data', {}).get('partner_dashou_id', ''),
|
||
'partner_amount': order.jine or 0,
|
||
}
|
||
)
|
||
if not _:
|
||
cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0)
|
||
cross_data.save()
|
||
else:
|
||
logger.warning(f"对方平台返回错误: {resp_json.get('msg')}")
|
||
else:
|
||
logger.warning(f"对方平台接口HTTP错误: {resp.status_code}")
|
||
except requests.exceptions.Timeout:
|
||
logger.error(f"通知对方平台超时: {notify_url}")
|
||
except Exception as e:
|
||
logger.error(f"通知对方平台异常: {str(e)}")
|
||
except Exception as e:
|
||
logger.error(f"查询对方平台配置失败: {str(e)}")
|
||
|
||
return Response({'code': 0, 'msg': '退款成功'})
|
||
|
||
# ========== 辅助方法 ==========
|
||
def _refund_platform_order(self, order, current_user, tuikuan_liyou, kefu):
|
||
"""平台订单退款(调用微信支付)"""
|
||
# 调用微信退款
|
||
refund_result = self._call_wechat_refund(order)
|
||
if refund_result['code'] != 0:
|
||
return Response({'code': 400, 'msg': refund_result['msg']})
|
||
|
||
with transaction.atomic():
|
||
order.zhuangtai = 5
|
||
order.clkf = current_user.phone
|
||
if tuikuan_liyou:
|
||
order.tkly = tuikuan_liyou
|
||
order.save()
|
||
|
||
# 更新打手
|
||
dashou_id = order.jiedan_dashou_id
|
||
if dashou_id:
|
||
try:
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
dashou_profile.tuikuanliang += 1
|
||
dashou_profile.zhuangtai = 1
|
||
dashou_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"平台退款:打手 {dashou_id} 不存在,跳过更新")
|
||
|
||
# 更新退款记录
|
||
self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou)
|
||
|
||
# 客服统计
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
logger.info(f"平台订单退款成功: {order.dingdan_id}")
|
||
return Response({'code': 0, 'msg': '退款成功'})
|
||
|
||
def _refund_merchant_order(self, order, current_user, tuikuan_liyou, kefu):
|
||
"""商家订单退款(退余额给商家)"""
|
||
# 获取商家ID
|
||
try:
|
||
shangjia_ext = order.shangjia_kuozhan
|
||
shangjia_id = shangjia_ext.shangjia_id
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 400, 'msg': '订单商家信息不存在'})
|
||
|
||
with transaction.atomic():
|
||
# 订单状态改已退款
|
||
order.zhuangtai = 5
|
||
order.clkf = current_user.phone
|
||
if tuikuan_liyou:
|
||
order.tkly = tuikuan_liyou
|
||
order.save()
|
||
|
||
# 退余额给商家
|
||
try:
|
||
shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
|
||
shangjia_profile = shangjia_user.shop_profile
|
||
shangjia_profile.tuikuan += 1
|
||
shangjia_profile.yue += order.jine or 0
|
||
shangjia_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.error(f"商家退款:商家 {shangjia_id} 不存在,无法退余额")
|
||
raise Exception("商家账户不存在")
|
||
|
||
# 更新打手
|
||
dashou_id = order.jiedan_dashou_id
|
||
if dashou_id:
|
||
try:
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
dashou_profile.tuikuanliang += 1
|
||
dashou_profile.zhuangtai = 1
|
||
dashou_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"商家退款:打手 {dashou_id} 不存在,跳过更新")
|
||
|
||
# 更新退款记录
|
||
self._update_refund_record(order, dashou_id, current_user.phone, tuikuan_liyou)
|
||
|
||
# 客服统计
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
logger.info(f"商家订单退款成功: {order.dingdan_id}")
|
||
return Response({'code': 0, 'msg': '退款成功'})
|
||
|
||
def _call_wechat_refund(self, order):
|
||
"""调用微信支付退款接口,返回 {'code':0, 'msg':'success'} 或错误信息"""
|
||
# 获取微信支付配置
|
||
appid = wx_cfg.WEIXIN_APPID
|
||
mch_id = wx_cfg.WEIXIN_MCHID
|
||
key = wx_cfg.WEIXIN_SHANGHUMIYAO
|
||
cert_path = wx_cfg.WEIXIN_CERT_PATH
|
||
key_path = wx_cfg.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(order.dingdan_id)
|
||
total_fee = int(float(order.jine or 0) * 100)
|
||
refund_fee = total_fee
|
||
refund_desc = '客服退款'
|
||
|
||
params = {
|
||
'appid': appid,
|
||
'mch_id': mch_id,
|
||
'nonce_str': self._generate_nonce_str(),
|
||
'out_refund_no': out_refund_no,
|
||
'total_fee': total_fee,
|
||
'refund_fee': refund_fee,
|
||
'refund_desc': refund_desc,
|
||
}
|
||
# 优先使用微信交易号
|
||
if order.wechat_transaction_id:
|
||
params['transaction_id'] = order.wechat_transaction_id
|
||
else:
|
||
params['out_trade_no'] = order.dingdan_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'}
|
||
else:
|
||
err_msg = result.get('err_code_des', result.get('return_msg', '未知错误'))
|
||
logger.warning(f"微信退款失败: {err_msg}")
|
||
return {'code': 400, 'msg': err_msg}
|
||
|
||
def _refund_to_merchant(self, order, clkf_phone, tuikuan_liyou):
|
||
"""给商家退余额(事务内调用,不再独立事务)"""
|
||
try:
|
||
shangjia_ext = order.shangjia_kuozhan
|
||
shangjia_id = shangjia_ext.shangjia_id
|
||
except ObjectDoesNotExist:
|
||
raise Exception("订单商家信息不存在")
|
||
|
||
try:
|
||
shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
|
||
shangjia_profile = shangjia_user.shop_profile
|
||
shangjia_profile.tuikuan += 1
|
||
shangjia_profile.yue += order.jine or 0
|
||
shangjia_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.error(f"商家退款:商家 {shangjia_id} 不存在")
|
||
raise Exception("商家账户不存在")
|
||
|
||
def _update_refund_record(self, order, dashou_id, chuli_phone, tuikuan_liyou):
|
||
"""更新退款记录表"""
|
||
try:
|
||
#更新支出记录
|
||
from dingdan.utils import update_daily_payout
|
||
update_daily_payout(order.jine)
|
||
|
||
|
||
refund_record, created = Tuikuanjilu.objects.get_or_create(
|
||
dingdan_id=order.dingdan_id,
|
||
defaults={
|
||
'dashouid': dashou_id or '',
|
||
'qingqiuid': '',
|
||
'chuliid': chuli_phone,
|
||
'liyou': tuikuan_liyou or '客服退款',
|
||
'sqzhuangtai': 1,
|
||
'jine': order.jine or 0,
|
||
'dashou_fencheng': order.dashou_fencheng or 0,
|
||
'jieshao': order.jieshao or '',
|
||
'beizhu': '客服退款',
|
||
'nicheng': order.nicheng or '',
|
||
}
|
||
)
|
||
if not created:
|
||
refund_record.sqzhuangtai = 1
|
||
refund_record.chuliid = chuli_phone
|
||
if tuikuan_liyou:
|
||
refund_record.liyou = tuikuan_liyou
|
||
refund_record.save()
|
||
except Exception as e:
|
||
logger.error(f"更新退款记录异常: {str(e)}")
|
||
|
||
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 PartnerGetOrderDataView(APIView):
|
||
"""
|
||
对方平台调用,获取我方订单的打手图片和处罚记录
|
||
请求:POST /houtai/kpthqtp
|
||
参数:{
|
||
"order_id": "对方平台的订单ID",
|
||
"club_id": "对方俱乐部ID"
|
||
}
|
||
无需认证
|
||
返回:code=0 + images列表 + punishment信息
|
||
"""
|
||
permission_classes = [AllowAny]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 1. 获取参数
|
||
partner_order_id = request.data.get('order_id', '').strip()
|
||
partner_club_id = request.data.get('club_id', '').strip()
|
||
if not partner_order_id or not partner_club_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 查询我方存储桶域名(自己俱乐部的配置)
|
||
my_club_config = ClubConfig.objects.filter(is_self=1).first()
|
||
if not my_club_config:
|
||
logger.error("未配置我方俱乐部存储桶域名")
|
||
return Response({'code': 500, 'msg': '系统配置错误'})
|
||
storage_domain = my_club_config.storage_bucket_domain or ''
|
||
|
||
# 3. 查询我方订单(通过对方订单ID和对方俱乐部ID)
|
||
try:
|
||
order = Dingdan.objects.get(
|
||
dingdan_id=partner_order_id,
|
||
partner_club_id=partner_club_id
|
||
)
|
||
except Dingdan.DoesNotExist:
|
||
logger.warning(f"订单不存在: partner_order_id={partner_order_id}, partner_club_id={partner_club_id}")
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
dingdan_id = order.dingdan_id
|
||
dashou_id = order.jiedan_dashou_id
|
||
|
||
# 4. 查询打手图片(绝对URL)
|
||
images = []
|
||
if dashou_id:
|
||
dashou_images = Dashoutupian.objects.filter(
|
||
dingdan_id=dingdan_id,
|
||
dashou=dashou_id
|
||
).values_list('tupian', flat=True)
|
||
for rel_path in dashou_images:
|
||
if rel_path:
|
||
if rel_path.startswith('http'):
|
||
images.append(rel_path)
|
||
else:
|
||
images.append(storage_domain + rel_path)
|
||
|
||
# 5. 查询处罚记录(最新一条)
|
||
punishment = {
|
||
'cfliyou': '',
|
||
'sqzhuangtai': 0,
|
||
'bhliyou': '',
|
||
'ssliyou': '',
|
||
'jifen': 0
|
||
}
|
||
if dashou_id:
|
||
punish_record = Chufajilu.objects.filter(
|
||
dingdan_id=dingdan_id,
|
||
dashouid=dashou_id
|
||
).order_by('-create_time').first()
|
||
if punish_record:
|
||
punishment = {
|
||
'cfliyou': punish_record.cfliyou or '',
|
||
'sqzhuangtai': punish_record.sqzhuangtai or 0,
|
||
'bhliyou': punish_record.bhliyou or '',
|
||
'ssliyou': punish_record.ssliyou or '',
|
||
'jifen': punish_record.jifen or 0,
|
||
}
|
||
|
||
# 6. 返回数据
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'success',
|
||
'data': {
|
||
'images': images,
|
||
'punishment': punishment
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"对方获取订单数据接口异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
class PartnerRefundNotifyView(APIView):
|
||
"""
|
||
接收对方平台推送的订单状态变更通知(完成/退款),同步更新我方数据。
|
||
请求:POST /houtai/dfddtk
|
||
参数:
|
||
{
|
||
"order_id": "对方平台订单ID(实际为我方订单ID)",
|
||
"club_id": "对方俱乐部ID",
|
||
"status": 3 或 5,
|
||
"reason": "理由(可选)"
|
||
}
|
||
返回:code=0 + data包含我方订单当前状态和打手ID
|
||
|
||
核心职责:
|
||
1. 更新跨平台订单扩展表
|
||
2. 对于对方派单(我方打手接对方的单):
|
||
a. 状态3(完成)→ 更新订单为已完成,创建预结算(如无)
|
||
b. 状态5(退款)→ 更新订单为已退款,将预结算待结算改为已拒绝
|
||
c. 在此过程中,若订单原状态为“进行中(2)”,则将打手状态重置为空闲(1)
|
||
"""
|
||
|
||
permission_classes = [AllowAny]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 参数校验(事务外执行)
|
||
partner_order_id = request.data.get('order_id', '').strip()
|
||
partner_club_id = request.data.get('club_id', '').strip()
|
||
partner_status = request.data.get('status')
|
||
reason = request.data.get('reason', '').strip()
|
||
|
||
if not partner_order_id or not partner_club_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
if partner_status not in (3, 5):
|
||
return Response({'code': 400, 'msg': '不支持的状态码'})
|
||
|
||
# 2. 核心业务在事务内执行
|
||
try:
|
||
with transaction.atomic():
|
||
# 2.1 锁定订单行(必须事务内)
|
||
try:
|
||
order = Dingdan.objects.select_for_update().get(
|
||
dingdan_id=partner_order_id,
|
||
partner_club_id=partner_club_id
|
||
)
|
||
except Dingdan.DoesNotExist:
|
||
logger.warning(f"通知订单不存在: order_id={partner_order_id}, club_id={partner_club_id}")
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
my_dingdan_id = order.dingdan_id
|
||
dashou_id = order.jiedan_dashou_id or ''
|
||
|
||
# 记录原订单状态,用于后续打手状态重置
|
||
original_order_status = order.zhuangtai
|
||
|
||
# 2.2 更新跨平台扩展表(记录对方状态)
|
||
cross_data, created = CrossPlatformOrderData.objects.get_or_create(
|
||
dingdan_id=my_dingdan_id,
|
||
defaults={
|
||
'partner_order_id': partner_order_id,
|
||
'partner_club_id': partner_club_id,
|
||
'partner_order_status': partner_status,
|
||
'partner_dashou_id': '',
|
||
'partner_amount': order.jine or 0,
|
||
}
|
||
)
|
||
if not created:
|
||
cross_data.partner_order_status = partner_status
|
||
cross_data.save(update_fields=['partner_order_status', 'update_time'])
|
||
|
||
# 2.3 仅处理对方派单(dispatch_type == 2)
|
||
if order.dispatch_type == 2:
|
||
# ========== 打手状态恢复逻辑 ==========
|
||
# 如果原订单状态为“进行中(2)”,且存在接单打手,则将打手状态改为空闲(1)
|
||
if original_order_status == 2 and dashou_id:
|
||
try:
|
||
# 通过打手ID查用户主表,再反向获取打手扩展表
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
dashou_profile.zhuangtai = 1 # 状态置为空闲
|
||
dashou_profile.save(update_fields=['zhuangtai'])
|
||
logger.info(f"打手 {dashou_id} 状态已重置为1(空闲)")
|
||
except UserMain.DoesNotExist:
|
||
logger.warning(f"打手用户不存在: {dashou_id}")
|
||
except UserDashou.DoesNotExist:
|
||
logger.warning(f"打手扩展信息不存在: {dashou_id}")
|
||
|
||
# ========== 情况A:对方完成(status=3) ==========
|
||
if partner_status == 3:
|
||
order.zhuangtai = 3
|
||
|
||
order.clkf = partner_club_id # 处理人(对方俱乐部ID)
|
||
order.save(update_fields=['zhuangtai', 'update_time','clkf'])
|
||
|
||
if dashou_id:
|
||
yjs, yjs_created = PreSettlement.objects.get_or_create(
|
||
dingdan_id=my_dingdan_id,
|
||
dashou_id=dashou_id,
|
||
defaults={
|
||
'partner_order_id': partner_order_id,
|
||
'partner_club_id': partner_club_id,
|
||
'amount': order.dashou_fencheng or 0,
|
||
'order_amount': order.jine or 0,
|
||
'status': 0, # 待结算
|
||
'operator': '',
|
||
}
|
||
)
|
||
if yjs_created:
|
||
logger.info(f"预结算创建成功: {my_dingdan_id}, 打手 {dashou_id}, 金额 {order.dashou_fencheng}")
|
||
else:
|
||
logger.info(f"预结算已存在,跳过: {my_dingdan_id}, 打手 {dashou_id}")
|
||
else:
|
||
logger.warning(f"订单 {my_dingdan_id} 无接单打手,跳过预结算创建")
|
||
|
||
# ========== 情况B:对方退款(status=5) ==========
|
||
elif partner_status == 5:
|
||
order.zhuangtai = 5
|
||
order.tkly = reason if reason else '' # 退款理由
|
||
order.clkf = partner_club_id # 处理人(对方俱乐部ID)
|
||
order.save(update_fields=['zhuangtai', 'update_time','tkly','clkf'])
|
||
|
||
if dashou_id:
|
||
try:
|
||
yjs = PreSettlement.objects.get(
|
||
dingdan_id=my_dingdan_id,
|
||
dashou_id=dashou_id
|
||
)
|
||
if yjs.status == 0: # 待结算 → 已拒绝
|
||
yjs.status = 2
|
||
yjs.process_time = timezone.now()
|
||
yjs.save(update_fields=['status', 'process_time', 'update_time'])
|
||
logger.info(f"预结算状态已改为已拒绝: {my_dingdan_id}")
|
||
except PreSettlement.DoesNotExist:
|
||
logger.info(f"订单 {my_dingdan_id} 无预结算记录,无需修改")
|
||
|
||
logger.info(f"对方通知处理完成: dingdan_id={my_dingdan_id}, partner_status={partner_status}")
|
||
|
||
# 返回结果
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'success',
|
||
'data': {
|
||
'partner_status': order.zhuangtai,
|
||
'partner_dashou_id': dashou_id
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"对方通知接口异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class CrossPlatformForceCompleteView(APIView):
|
||
"""
|
||
跨平台订单强制结单接口
|
||
请求:POST /houtai/kpqzjd
|
||
参数:{"username": "客服账号ID", "dingdan_id": "订单ID"}
|
||
认证:JWT
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
username = request.data.get('username', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
if not username or not dingdan_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 身份验证
|
||
current_user = request.user
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '003aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查处理订单'})
|
||
|
||
# 4. 查询订单并加锁
|
||
try:
|
||
with transaction.atomic():
|
||
order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
|
||
|
||
# 5. 校验订单状态必须为8(结算中)
|
||
if order.zhuangtai not in (2,8):
|
||
return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'})
|
||
|
||
# 获取关键字段
|
||
dispatch_type = order.dispatch_type # 1=我方派单,2=对方派单
|
||
is_cross = order.is_cross # 0=本地,1=跨平台
|
||
partner_club_id = order.partner_club_id
|
||
partner_order_id = order.partner_order_id
|
||
dashou_id = order.jiedan_dashou_id
|
||
dashou_fencheng = order.dashou_fencheng or Decimal('0')
|
||
jine = order.jine or Decimal('0')
|
||
fadan_pingtai = order.fadan_pingtai # 1=平台,2=商家
|
||
|
||
# ========== 修复点1:完善跨平台判断 ==========
|
||
# 跨平台且被对方打手接单的条件:is_cross==1 且 (partner_club_id 或 partner_order_id 有值)
|
||
is_cross_claimed_by_partner = (is_cross == 1 and (partner_club_id or partner_order_id))
|
||
# 如果是跨平台订单但两个字段都为空,则按本地订单处理(即自家打手接单)
|
||
|
||
# ========== 修复点2:根据方向处理 ==========
|
||
# ---------- 对方派单(我方打手接对方单)----------
|
||
if dispatch_type == 2:
|
||
# 创建预结算记录(如果已存在则更新)
|
||
pre_settlement, created = PreSettlement.objects.get_or_create(
|
||
dingdan_id=dingdan_id,
|
||
defaults={
|
||
'partner_order_id': partner_order_id,
|
||
'partner_club_id': partner_club_id,
|
||
'dashou_id': dashou_id,
|
||
'amount': dashou_fencheng,
|
||
'order_amount': jine,
|
||
'status': 0, # 待结算
|
||
'operator': current_user.phone,
|
||
}
|
||
)
|
||
if not created:
|
||
pre_settlement.partner_order_id = partner_order_id
|
||
pre_settlement.partner_club_id = partner_club_id
|
||
pre_settlement.dashou_id = dashou_id
|
||
pre_settlement.amount = dashou_fencheng
|
||
pre_settlement.order_amount = jine
|
||
pre_settlement.status = 0
|
||
pre_settlement.process_time = None
|
||
pre_settlement.operator = current_user.phone
|
||
pre_settlement.save()
|
||
|
||
# 更新订单状态
|
||
order.zhuangtai = 3
|
||
order.clkf = current_user.phone
|
||
order.save()
|
||
|
||
# 更新每日统计表(对方派单方向,使用订单金额)
|
||
if partner_club_id:
|
||
# 修复点3:使用公共统计函数
|
||
update_daily_dispatch_stat(
|
||
direction=2,
|
||
partner_club_id=partner_club_id,
|
||
success_amount=jine,
|
||
success_count=1
|
||
)
|
||
else:
|
||
# 如果没有俱乐部ID,使用默认000001
|
||
update_daily_dispatch_stat(
|
||
direction=2,
|
||
partner_club_id='000001',
|
||
success_amount=jine,
|
||
success_count=1
|
||
)
|
||
|
||
# 通知对方平台订单已完成(修复点4:移到事务外)
|
||
if partner_club_id and partner_order_id:
|
||
transaction.on_commit(
|
||
lambda: self._notify_partner_order_status(
|
||
partner_club_id=partner_club_id,
|
||
partner_order_id=partner_order_id,
|
||
new_status=3,
|
||
current_user=current_user
|
||
)
|
||
)
|
||
|
||
# ---------- 我方派单 ----------
|
||
elif dispatch_type == 1:
|
||
# 判断是否为跨平台订单且被对方打手接单(即 partner_club_id 或 partner_order_id 有值)
|
||
if is_cross_claimed_by_partner:
|
||
# 跨平台订单:我方派单给外平台打手
|
||
# 修复点5:处理只有 partner_order_id 没有 partner_club_id 的情况
|
||
real_partner_club_id = partner_club_id or '000001'
|
||
|
||
# 更新订单状态
|
||
order.zhuangtai = 3
|
||
order.clkf = current_user.phone
|
||
order.save()
|
||
|
||
# 更新每日统计表(我方派单方向,使用打手分成金额)
|
||
update_daily_dispatch_stat(
|
||
direction=1,
|
||
partner_club_id=real_partner_club_id,
|
||
success_amount=dashou_fencheng,
|
||
success_count=1
|
||
)
|
||
|
||
# 通知对方平台订单已完成(异步)
|
||
if partner_order_id:
|
||
transaction.on_commit(
|
||
lambda: self._notify_partner_order_status(
|
||
partner_club_id=real_partner_club_id,
|
||
partner_order_id=partner_order_id,
|
||
new_status=3,
|
||
current_user=current_user
|
||
)
|
||
)
|
||
else:
|
||
# 本地订单(非跨平台 或 跨平台但实际是自家打手接单),按原有逻辑给自家打手结算
|
||
if not dashou_id:
|
||
return Response({'code': 400, 'msg': '订单无接单打手'})
|
||
if dashou_fencheng < 0:
|
||
return Response({'code': 400, 'msg': '打手分成金额无效'})
|
||
|
||
# 更新打手余额和统计
|
||
try:
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
dashou_profile.chengjiaozongliang += 1
|
||
dashou_profile.yue += dashou_fencheng
|
||
dashou_profile.zonge += dashou_fencheng
|
||
dashou_profile.jinrishouyi += dashou_fencheng
|
||
dashou_profile.jinyueshouyi += dashou_fencheng
|
||
dashou_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"本地订单结算:打手 {dashou_id} 不存在,跳过")
|
||
|
||
# 如果是商家订单,更新商家成交单量
|
||
if fadan_pingtai == 2:
|
||
try:
|
||
shangjia_ext = order.shangjia_kuozhan
|
||
if shangjia_ext and shangjia_ext.shangjia_id:
|
||
shangjia_user = UserMain.objects.get(yonghuid=shangjia_ext.shangjia_id)
|
||
shangjia_profile = shangjia_user.shop_profile
|
||
shangjia_profile.chengjiao += 1
|
||
shangjia_profile.save()
|
||
except Exception as e:
|
||
logger.warning(f"商家订单更新失败: {str(e)}")
|
||
|
||
# 更新订单状态
|
||
order.zhuangtai = 3
|
||
order.clkf = current_user.phone
|
||
order.save()
|
||
|
||
# 本地订单不涉及跨平台统计,无需更新 DailyDispatchStat
|
||
|
||
else:
|
||
return Response({'code': 400, 'msg': '未知的派单方向'})
|
||
|
||
# 7. 更新客服统计
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
# 8. 记录操作日志
|
||
logger.info(f"强制结单成功,订单 {dingdan_id},方向 {dispatch_type},客服 {username}")
|
||
|
||
return Response({'code': 0, 'msg': '强制结单成功'})
|
||
|
||
except Dingdan.DoesNotExist:
|
||
logger.warning(f"订单不存在: {dingdan_id}")
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
except Exception as e:
|
||
logger.error(f"强制结单接口异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
# ========== 辅助方法 ==========
|
||
def _notify_partner_order_status(self, partner_club_id, partner_order_id, new_status, current_user):
|
||
"""
|
||
通知对方平台订单状态变更(已在事务外调用)
|
||
"""
|
||
try:
|
||
# 获取我方俱乐部ID
|
||
my_club_config = ClubConfig.objects.filter(is_self=1).first()
|
||
if not my_club_config:
|
||
logger.error("未配置我方俱乐部信息,无法通知对方")
|
||
return
|
||
self_club_id = my_club_config.club_id
|
||
|
||
# 查询对方服务器域名
|
||
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
|
||
partner_domain = club_rel.partner_domain
|
||
if not partner_domain:
|
||
logger.error(f"对方俱乐部 {partner_club_id} 未配置域名")
|
||
return
|
||
|
||
url = partner_domain.rstrip('/') + '/houtai/dfddtk'
|
||
payload = {
|
||
'order_id': partner_order_id,
|
||
'club_id': self_club_id,
|
||
'status': new_status
|
||
}
|
||
resp = requests.post(url, json=payload, timeout=3)
|
||
if resp.status_code == 200:
|
||
resp_json = resp.json()
|
||
if resp_json.get('code') == 0:
|
||
# 更新跨平台订单扩展表中的对方状态
|
||
cross_data, _ = CrossPlatformOrderData.objects.get_or_create(
|
||
dingdan_id=partner_order_id,
|
||
defaults={
|
||
'partner_order_id': partner_order_id,
|
||
'partner_club_id': partner_club_id,
|
||
'partner_order_status': new_status,
|
||
'partner_dashou_id': '',
|
||
'partner_amount': Decimal('0')
|
||
}
|
||
)
|
||
if not _:
|
||
cross_data.partner_order_status = new_status
|
||
cross_data.save(update_fields=['partner_order_status'])
|
||
logger.info(f"通知对方订单状态成功: {partner_order_id} -> {new_status}")
|
||
else:
|
||
logger.warning(f"对方接口返回错误: {resp_json.get('msg')}")
|
||
else:
|
||
logger.warning(f"对方接口HTTP错误: {resp.status_code}")
|
||
except requests.exceptions.Timeout:
|
||
logger.error(f"通知对方平台超时: {partner_club_id}")
|
||
except Exception as e:
|
||
logger.error(f"通知对方平台异常: {str(e)}")
|
||
|
||
|
||
|
||
|
||
class CrossPlatformTransferHallView(APIView):
|
||
"""
|
||
跨平台订单转移大厅接口
|
||
请求:POST /houtai/kpzydt
|
||
参数:{"username": "客服账号ID", "dingdan_id": "订单ID"}
|
||
认证:JWT
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
username = request.data.get('username', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
if not username or not dingdan_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '003aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权处理俱乐部订单'})
|
||
|
||
# 4. 查询订单并加锁
|
||
try:
|
||
with transaction.atomic():
|
||
order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
|
||
|
||
# 5. 状态校验
|
||
if order.zhuangtai not in [2, 8, 4]:
|
||
return Response({'code': 400, 'msg': f'订单状态不允许转移大厅,当前状态: {order.zhuangtai}'})
|
||
|
||
# 获取关键字段
|
||
is_cross = order.is_cross
|
||
dispatch_type = order.dispatch_type
|
||
partner_club_id = order.partner_club_id
|
||
partner_order_id = order.partner_order_id
|
||
current_dashou_id = order.jiedan_dashou_id
|
||
zhiding_id = order.zhiding_id
|
||
|
||
# ========== 判断是否需要通知对方平台 ==========
|
||
# 条件:跨平台 且 我方派单(dispatch_type=1) 且 (partner_club_id 或 partner_order_id 有值)
|
||
need_notify = (is_cross == 1 and (partner_club_id or partner_order_id))
|
||
|
||
# ========== 判断是否需要修改本地打手状态 ==========
|
||
# 当接单打手是我方打手时,才需要修改状态(改为空闲1)
|
||
# 我方打手的情况:
|
||
# 1. 非跨平台订单 (is_cross != 1)
|
||
# 2. 跨平台且对方派单 (dispatch_type == 2) —— 此时打手是我方
|
||
# 3. 跨平台且我方派单但没有对方信息 (即 need_notify == False) —— 此时打手也是我方
|
||
need_update_dashou_status = (current_dashou_id and
|
||
(is_cross != 1 or dispatch_type == 2 or not need_notify))
|
||
|
||
# ========== 记录打手历史(无论打手是谁,只要有ID就记录) ==========
|
||
if current_dashou_id:
|
||
last_history = OrderDashouHistory.objects.filter(
|
||
dingdan_id=dingdan_id
|
||
).order_by('-times').first()
|
||
next_times = (last_history.times + 1) if last_history else 1
|
||
OrderDashouHistory.objects.create(
|
||
dingdan_id=dingdan_id,
|
||
dashou_id=current_dashou_id,
|
||
times=next_times,
|
||
operator=username
|
||
)
|
||
|
||
# ========== 修改本地打手状态(仅当是我方打手时) ==========
|
||
if need_update_dashou_status:
|
||
try:
|
||
dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id)
|
||
dashou_profile.zhuangtai = 1
|
||
dashou_profile.save()
|
||
logger.info(f"打手 {current_dashou_id} 状态改为空闲")
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新")
|
||
|
||
# ========== 更新订单状态(清空打手,改为1或7) ==========
|
||
if zhiding_id:
|
||
order.zhuangtai = 7 # 指定中
|
||
else:
|
||
order.zhuangtai = 1 # 下单中
|
||
order.jiedan_dashou_id = None
|
||
order.clkf = username
|
||
order.save()
|
||
|
||
# ========== 如果需要通知对方平台,异步发送通知 ==========
|
||
if need_notify:
|
||
# 确定对方俱乐部ID(如果没有则用默认000001)
|
||
real_partner_club_id = partner_club_id if partner_club_id else '000001'
|
||
real_partner_order_id = partner_order_id if partner_order_id else dingdan_id
|
||
|
||
# 获取我方俱乐部ID
|
||
my_club_config = ClubConfig.objects.filter(is_self=1).first()
|
||
if my_club_config:
|
||
self_club_id = my_club_config.club_id
|
||
# 查询对方域名
|
||
try:
|
||
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=real_partner_club_id)
|
||
partner_domain = club_rel.partner_domain
|
||
if partner_domain:
|
||
url = partner_domain.rstrip('/') + '/houtai/kpttzzydt'
|
||
payload = {
|
||
'order_id': real_partner_order_id,
|
||
'club_id': self_club_id
|
||
}
|
||
|
||
def notify():
|
||
try:
|
||
resp = requests.post(url, json=payload, timeout=3)
|
||
if resp.status_code == 200:
|
||
resp_json = resp.json()
|
||
if resp_json.get('code') == 0:
|
||
# 更新跨平台订单扩展表中的对方状态
|
||
cross_data, created = CrossPlatformOrderData.objects.get_or_create(
|
||
dingdan_id=dingdan_id,
|
||
defaults={
|
||
'partner_order_id': real_partner_order_id,
|
||
'partner_club_id': real_partner_club_id,
|
||
'partner_order_status': resp_json.get('data', {}).get('partner_status', 0),
|
||
'partner_dashou_id': '',
|
||
'partner_amount': order.jine or 0,
|
||
}
|
||
)
|
||
if not created:
|
||
cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0)
|
||
cross_data.save(update_fields=['partner_order_status'])
|
||
logger.info(f"转移大厅通知对方成功: {dingdan_id}")
|
||
|
||
# ========== 新增代码开始 ==========
|
||
# ========== 新增代码开始 ==========
|
||
# 只有我方派单时才清空对方平台信息,对方派单则保留
|
||
if dispatch_type == 1:
|
||
Dingdan.objects.filter(dingdan_id=dingdan_id).update(
|
||
partner_club_id=None,
|
||
partner_order_id=None,
|
||
jiedan_dashou_id=None,
|
||
zhuangtai=1
|
||
)
|
||
logger.info(f"我方派单,已清空订单 {dingdan_id} 的对方信息")
|
||
else:
|
||
# 对方派单,只清空打手并恢复状态,保留对方信息
|
||
Dingdan.objects.filter(dingdan_id=dingdan_id).update(
|
||
jiedan_dashou_id=None,
|
||
zhuangtai=1
|
||
)
|
||
logger.info(f"对方派单,只清空打手,保留对方信息,订单 {dingdan_id}")
|
||
# ========== 新增代码结束 ==========
|
||
|
||
else:
|
||
logger.warning(f"对方平台返回错误: {resp_json.get('msg')}")
|
||
else:
|
||
logger.warning(f"对方接口HTTP错误: {resp.status_code}")
|
||
except Exception as e:
|
||
logger.error(f"转移大厅通知对方异常: {str(e)}")
|
||
|
||
threading.Thread(target=notify).start()
|
||
except Club.DoesNotExist:
|
||
logger.error(f"未找到对方俱乐部配置: partner_club_id={real_partner_club_id}")
|
||
except Exception as e:
|
||
logger.error(f"查询对方平台配置失败: {str(e)}")
|
||
|
||
# ========== 更新客服统计 ==========
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
return Response({'code': 0, 'msg': '转移大厅成功'})
|
||
|
||
except Dingdan.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
except Exception as e:
|
||
logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
class PartnerTransferHallNotifyView(APIView):
|
||
"""
|
||
对方平台转移大厅通知接口
|
||
对方调用此接口通知我方订单已转移大厅,我方同步更新订单状态
|
||
请求:POST /houtai/kpttzzydt
|
||
参数:{
|
||
"order_id": "对方平台的订单ID",
|
||
"club_id": "对方俱乐部ID"
|
||
}
|
||
无需认证
|
||
返回:{
|
||
"code": 0,
|
||
"msg": "success",
|
||
"data": {
|
||
"partner_status": 1 # 我方订单当前状态
|
||
}
|
||
}
|
||
"""
|
||
parser_classes = [JSONParser]
|
||
permission_classes = [AllowAny]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 1. 获取参数
|
||
partner_order_id = request.data.get('order_id', '').strip()
|
||
partner_club_id = request.data.get('club_id', '').strip()
|
||
if not partner_order_id or not partner_club_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 查询我方订单
|
||
try:
|
||
order = Dingdan.objects.get(
|
||
dingdan_id=partner_order_id,
|
||
partner_club_id=partner_club_id
|
||
)
|
||
except Dingdan.DoesNotExist:
|
||
logger.warning(f"对方转移大厅通知:订单不存在 partner_order_id={partner_order_id}, partner_club_id={partner_club_id}")
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
# 3. 加锁处理
|
||
with transaction.atomic():
|
||
order = Dingdan.objects.select_for_update().get(id=order.id)
|
||
|
||
current_status = order.zhuangtai
|
||
current_dashou_id = order.jiedan_dashou_id
|
||
|
||
# 4. 根据订单状态处理
|
||
if current_status == 3:
|
||
# 已完成状态,需要检查预结算记录
|
||
try:
|
||
pre_settlement = PreSettlement.objects.select_for_update().get(
|
||
dingdan_id=order.dingdan_id,
|
||
dashou_id=current_dashou_id
|
||
)
|
||
# 如果预结算状态是0(待结算),改为2(已拒绝)
|
||
if pre_settlement.status == 0:
|
||
pre_settlement.status = 2
|
||
pre_settlement.process_time = timezone.now()
|
||
pre_settlement.operator = partner_club_id # 记录操作方
|
||
pre_settlement.save()
|
||
logger.info(f"预结算记录已拒绝: {order.dingdan_id}")
|
||
else:
|
||
logger.info(f"预结算记录已是其他状态 {pre_settlement.status},不重复修改")
|
||
except PreSettlement.DoesNotExist:
|
||
logger.warning(f"订单 {order.dingdan_id} 已完成但无预结算记录,无法转移大厅")
|
||
return Response({'code': 400, 'msg': '订单已完成且无预结算记录,无法转移大厅'})
|
||
|
||
# 5. 记录打手历史(如果有接单打手)
|
||
if current_dashou_id:
|
||
last_history = OrderDashouHistory.objects.filter(
|
||
dingdan_id=order.dingdan_id
|
||
).order_by('-times').first()
|
||
next_times = (last_history.times + 1) if last_history else 1
|
||
OrderDashouHistory.objects.create(
|
||
dingdan_id=order.dingdan_id,
|
||
dashou_id=current_dashou_id,
|
||
times=next_times,
|
||
operator=partner_club_id # 记录操作方
|
||
)
|
||
# 将打手状态改为空闲
|
||
try:
|
||
dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id)
|
||
dashou_profile.zhuangtai = 1
|
||
dashou_profile.save()
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新")
|
||
|
||
# 6. 更新订单状态为1(下单中)或7(指定中)
|
||
# 7. 根据派单方向清理对方信息
|
||
if order.dispatch_type == 1:
|
||
# 我方派单:清空对方俱乐部ID和订单ID
|
||
order.partner_club_id = None
|
||
order.partner_order_id = None
|
||
# 对方派单时保留对方信息,不清空
|
||
|
||
# 8. 更新订单状态
|
||
if order.zhiding_id:
|
||
order.zhuangtai = 7 # 指定中
|
||
else:
|
||
order.zhuangtai = 1 # 下单中
|
||
|
||
|
||
order.jiedan_dashou_id = None
|
||
order.clkf = partner_club_id # 记录操作方
|
||
order.save()
|
||
|
||
logger.info(f"对方转移大厅通知成功: 订单 {order.dingdan_id},新状态 {order.zhuangtai}")
|
||
|
||
# 7. 返回我方订单状态
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'success',
|
||
'data': {
|
||
'partner_status': order.zhuangtai
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"对方转移大厅通知接口异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ========== 1. 获取俱乐部设置数据接口 ==========
|
||
class GetSettingsView(APIView):
|
||
"""
|
||
获取跨平台设置页面数据
|
||
请求:POST /houtai/hqkptsz
|
||
参数:{"username": "客服账号ID"}
|
||
权限:权限码2且开启
|
||
返回:包含自己俱乐部信息(含客服字段)和合作俱乐部列表
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '004aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
|
||
|
||
# 4. 查询数据
|
||
try:
|
||
# 4.1 自己俱乐部配置(is_self=1)
|
||
self_config = ClubConfig.objects.filter(is_self=1).first()
|
||
if not self_config:
|
||
logger.error("未找到自己俱乐部配置")
|
||
return Response({'code': 500, 'msg': '系统配置错误'})
|
||
|
||
self_club = {
|
||
'club_id': self_config.club_id,
|
||
'club_nickname': self_config.club_nickname,
|
||
'club_avatar': self_config.club_avatar,
|
||
'backend_domain': self_config.backend_domain,
|
||
'chat_api_key': self_config.chat_api_key,
|
||
'chat_api_id': self_config.chat_api_id,
|
||
'customer_service_api_url': self_config.customer_service_api_url,
|
||
'customer_service_corp_id': self_config.customer_service_corp_id,
|
||
'storage_bucket_domain': self_config.storage_bucket_domain,
|
||
}
|
||
|
||
# 4.2 合作俱乐部列表(Club 表中本俱乐部ID = self_config.club_id)
|
||
partner_records = Club.objects.filter(club_id=self_config.club_id)
|
||
partner_clubs = []
|
||
for record in partner_records:
|
||
partner_config = ClubConfig.objects.filter(
|
||
club_id=record.partner_club_id,
|
||
is_self=0
|
||
).first()
|
||
partner_clubs.append({
|
||
'club_id': record.partner_club_id,
|
||
'club_nickname': partner_config.club_nickname if partner_config else record.partner_club_id,
|
||
'club_avatar': partner_config.club_avatar if partner_config else '',
|
||
'is_interop_enabled': record.is_interop_enabled,
|
||
'is_receive_enabled': record.is_receive_enabled,
|
||
})
|
||
|
||
return Response({'code': 0, 'data': {'self_club': self_club, 'partner_clubs': partner_clubs}})
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取设置失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
# ========== 2. 获取订单类型映射接口 ==========
|
||
class GetMappingsView(APIView):
|
||
"""
|
||
获取指定合作俱乐部的订单类型映射
|
||
请求:POST /houtai/hqqtjlbsj
|
||
参数:{"username": "客服账号ID", "partner_club_id": "对方俱乐部ID"}
|
||
权限:权限码2
|
||
返回:对方订单类型列表、我方订单类型列表、当前映射关系
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
username = request.data.get('username', '').strip()
|
||
partner_club_id = request.data.get('partner_club_id', '').strip()
|
||
if not username or not partner_club_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '004aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
|
||
|
||
try:
|
||
# 4. 获取我方所有商品类型(订单类型)
|
||
our_types = ShangpinLeixing.objects.all().values('id', 'jieshao', 'tupian_url')
|
||
our_type_list = [
|
||
{'type_id': t['id'], 'type_name': t['jieshao'], 'icon': t['tupian_url']}
|
||
for t in our_types
|
||
]
|
||
|
||
# 5. 获取对方订单类型(从映射表去重)
|
||
mappings_query = OrderTypeMapping.objects.filter(partner_club_id=partner_club_id)
|
||
partner_types_dict = {}
|
||
for m in mappings_query:
|
||
partner_types_dict[m.partner_type_id] = {
|
||
'type_id': m.partner_type_id,
|
||
'type_name': m.partner_type_name,
|
||
'icon': ''
|
||
}
|
||
partner_type_list = list(partner_types_dict.values())
|
||
|
||
# 6. 获取当前映射关系
|
||
mapping_list = [
|
||
{'partner_type_id': m.partner_type_id, 'our_type_id': m.our_type_id}
|
||
for m in mappings_query
|
||
]
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'partner_types': partner_type_list,
|
||
'our_types': our_type_list,
|
||
'mappings': mapping_list
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取映射失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
# ========== 3. 保存设置接口(自己俱乐部 + 合作俱乐部开关)==========
|
||
class UpdateSettingsView(APIView):
|
||
"""
|
||
保存俱乐部设置(自己俱乐部信息 + 合作俱乐部开关)
|
||
请求:POST /houtai/xgjlbsj
|
||
参数:{
|
||
"username": "客服账号ID",
|
||
"self_club": {
|
||
"club_nickname": "...",
|
||
"backend_domain": "...",
|
||
"chat_api_key": "...",
|
||
"chat_api_id": "...",
|
||
"customer_service_api_url": "...",
|
||
"customer_service_corp_id": "...",
|
||
"storage_bucket_domain": "..."
|
||
},
|
||
"partner_updates": [
|
||
{"club_id": "654321", "is_interop_enabled": true, "is_receive_enabled": false}
|
||
]
|
||
}
|
||
权限:权限码2
|
||
返回:{"code": 0, "msg": "success"}
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '004aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权修改俱乐部配置'})
|
||
|
||
# 4. 获取自己俱乐部当前配置
|
||
try:
|
||
self_config = ClubConfig.objects.get(is_self=1)
|
||
except ObjectDoesNotExist:
|
||
logger.error("未找到自己俱乐部配置")
|
||
return Response({'code': 500, 'msg': '系统配置错误'})
|
||
|
||
# 5. 更新自己俱乐部配置(包含客服字段)
|
||
self_updates = request.data.get('self_club', {})
|
||
if self_updates:
|
||
allowed_fields = [
|
||
'club_nickname', 'backend_domain', 'chat_api_key', 'chat_api_id',
|
||
'customer_service_api_url', 'customer_service_corp_id',
|
||
'storage_bucket_domain'
|
||
]
|
||
fields_to_update = []
|
||
for field in allowed_fields:
|
||
if field in self_updates:
|
||
setattr(self_config, field, self_updates[field])
|
||
fields_to_update.append(field)
|
||
if fields_to_update:
|
||
self_config.save(update_fields=fields_to_update)
|
||
|
||
# 6. 更新合作俱乐部开关
|
||
partner_updates = request.data.get('partner_updates', [])
|
||
for update in partner_updates:
|
||
club_id = update.get('club_id')
|
||
if not club_id:
|
||
continue
|
||
try:
|
||
partner_record = Club.objects.get(club_id=self_config.club_id, partner_club_id=club_id)
|
||
if 'is_interop_enabled' in update:
|
||
partner_record.is_interop_enabled = update['is_interop_enabled']
|
||
if 'is_receive_enabled' in update:
|
||
partner_record.is_receive_enabled = update['is_receive_enabled']
|
||
partner_record.save(update_fields=['is_interop_enabled', 'is_receive_enabled'])
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"合作俱乐部记录不存在: {club_id}")
|
||
|
||
# 7. 异步通知所有合作俱乐部(如果自己数据有修改)
|
||
if self_updates:
|
||
self._notify_all_partners(self_config, self_updates)
|
||
|
||
return Response({'code': 0, 'msg': 'success'})
|
||
|
||
def _notify_all_partners(self, self_config, updates):
|
||
"""异步通知所有合作俱乐部更新我方配置"""
|
||
partner_records = Club.objects.filter(club_id=self_config.club_id)
|
||
if not partner_records:
|
||
return
|
||
|
||
data_to_send = {
|
||
'club_id': self_config.club_id,
|
||
'updates': updates
|
||
}
|
||
|
||
def notify(partner_domain):
|
||
try:
|
||
url = partner_domain.rstrip('/') + '/houtai/xgzjsjcs'
|
||
resp = requests.post(url, json=data_to_send, timeout=3)
|
||
if resp.status_code != 200:
|
||
logger.warning(f"通知 {partner_domain} 失败,状态码 {resp.status_code}")
|
||
except Exception as e:
|
||
logger.error(f"通知 {partner_domain} 异常: {str(e)}")
|
||
|
||
for record in partner_records:
|
||
domain = record.partner_domain
|
||
if domain:
|
||
threading.Thread(target=notify, args=(domain,)).start()
|
||
|
||
|
||
|
||
|
||
|
||
class UpdateMappingsView(APIView):
|
||
"""
|
||
保存指定合作俱乐部的订单类型映射
|
||
请求:POST /houtai/xgdfddys
|
||
参数:{
|
||
"username": "客服手机号",
|
||
"partner_club_id": "对方俱乐部ID",
|
||
"mappings": [
|
||
{"partner_type_id": 1, "our_type_id": 10},
|
||
{"partner_type_id": 2, "our_type_id": 11}
|
||
]
|
||
}
|
||
权限:权限码2
|
||
返回:{"code": 0, "msg": "success"}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
username = request.data.get('username', '').strip()
|
||
partner_club_id = request.data.get('partner_club_id', '').strip()
|
||
mappings = request.data.get('mappings', [])
|
||
|
||
if not username or not partner_club_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '004aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权修改俱乐部配置'})
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
# 4. 获取当前该俱乐部所有现有映射(按 partner_type_id 索引)
|
||
existing_mappings = {
|
||
m.partner_type_id: m
|
||
for m in OrderTypeMapping.objects.filter(partner_club_id=partner_club_id)
|
||
}
|
||
|
||
# 5. 处理前端传来的映射(更新或新增)
|
||
processed_partner_ids = set()
|
||
for mapping in mappings:
|
||
partner_type_id = mapping.get('partner_type_id')
|
||
our_type_id = mapping.get('our_type_id')
|
||
if partner_type_id is None or our_type_id is None:
|
||
continue
|
||
processed_partner_ids.add(partner_type_id)
|
||
|
||
# 获取我方类型名称
|
||
our_type_name = ''
|
||
try:
|
||
our_type = ShangpinLeixing.objects.get(id=our_type_id)
|
||
our_type_name = our_type.jieshao
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"我方订单类型不存在: {our_type_id}")
|
||
|
||
# 查找是否存在该对方类型的映射
|
||
if partner_type_id in existing_mappings:
|
||
# 更新现有记录
|
||
mapping_obj = existing_mappings[partner_type_id]
|
||
mapping_obj.our_type_id = our_type_id
|
||
mapping_obj.our_type_name = our_type_name
|
||
mapping_obj.save(update_fields=['our_type_id', 'our_type_name'])
|
||
# 从字典中移除,以便后续知道哪些被删除了
|
||
del existing_mappings[partner_type_id]
|
||
else:
|
||
# 新增记录
|
||
# 对方类型名称:优先从现有映射中查找(可能被其他俱乐部共享?不,每个俱乐部独立)
|
||
# 由于该对方类型没有映射过,我们暂时无法获取名称,可留空或使用默认值。
|
||
# 但为保证数据完整性,我们尝试从已有记录中查找(如果该俱乐部之前删除过又重建,则可能丢失名称)
|
||
# 更稳妥的方式是前端传递 partner_type_name,但当前未要求,因此此处留空并记录警告。
|
||
partner_type_name = ''
|
||
logger.warning(f"新增映射时对方类型名称缺失: partner_club_id={partner_club_id}, partner_type_id={partner_type_id}")
|
||
|
||
OrderTypeMapping.objects.create(
|
||
partner_club_id=partner_club_id,
|
||
partner_type_id=partner_type_id,
|
||
partner_type_name=partner_type_name,
|
||
our_type_id=our_type_id,
|
||
our_type_name=our_type_name
|
||
)
|
||
|
||
# 6. 删除前端未传入的映射(即用户取消的映射)
|
||
for partner_type_id, mapping_obj in existing_mappings.items():
|
||
mapping_obj.delete()
|
||
logger.info(f"删除映射: partner_club_id={partner_club_id}, partner_type_id={partner_type_id}")
|
||
|
||
return Response({'code': 0, 'msg': 'success'})
|
||
|
||
except Exception as e:
|
||
logger.error(f"保存映射失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
# ========== 5. 接收对方平台修改数据接口 ==========
|
||
class PartnerUpdateSelfConfigView(APIView):
|
||
"""
|
||
接收对方平台修改自己俱乐部配置的通知
|
||
请求:POST /houtai/xgzjsjcs
|
||
参数:{
|
||
"club_id": "对方俱乐部ID",
|
||
"updates": {
|
||
"club_nickname": "新昵称",
|
||
"backend_domain": "新域名",
|
||
"chat_api_key": "新key",
|
||
"chat_api_id": "新id",
|
||
"customer_service_api_url": "新URL",
|
||
"customer_service_corp_id": "新企业ID",
|
||
"storage_bucket_domain": "新存储桶"
|
||
}
|
||
}
|
||
无需认证,对方平台可信调用
|
||
返回:{"code": 0, "msg": "success"}
|
||
"""
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
club_id = request.data.get('club_id', '').strip()
|
||
updates = request.data.get('updates', {})
|
||
|
||
if not club_id or not updates:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 查询对方俱乐部的配置记录(is_self=0)
|
||
try:
|
||
config = ClubConfig.objects.get(club_id=club_id, is_self=0)
|
||
except ClubConfig.DoesNotExist:
|
||
logger.warning(f"对方俱乐部配置不存在: {club_id}")
|
||
return Response({'code': 404, 'msg': '俱乐部不存在'})
|
||
|
||
# 3. 允许修改的字段白名单(包含客服字段)
|
||
allowed_fields = [
|
||
'club_nickname',
|
||
'backend_domain',
|
||
'chat_api_key',
|
||
'chat_api_id',
|
||
'customer_service_api_url',
|
||
'customer_service_corp_id',
|
||
'storage_bucket_domain'
|
||
]
|
||
|
||
# 4. 更新字段
|
||
with transaction.atomic():
|
||
fields_updated = []
|
||
for field in allowed_fields:
|
||
if field in updates:
|
||
new_value = updates[field]
|
||
setattr(config, field, new_value)
|
||
fields_updated.append(field)
|
||
if fields_updated:
|
||
config.save(update_fields=fields_updated)
|
||
logger.info(f"对方俱乐部 {club_id} 配置已更新: {fields_updated}")
|
||
else:
|
||
logger.info(f"对方俱乐部 {club_id} 无有效字段更新")
|
||
|
||
return Response({'code': 0, 'msg': 'success'})
|
||
|
||
|
||
|
||
|
||
# apps/houtai/views.py
|
||
|
||
|
||
|
||
|
||
|
||
# ========== 1. 获取合作俱乐部列表接口(完整) ==========
|
||
class GetPartnerClubsView(APIView):
|
||
"""
|
||
获取我方合作俱乐部列表(用于统计页面)
|
||
请求:POST /houtai/kpthqpt
|
||
参数:{"username": "客服手机号"}
|
||
权限:权限码2且开启
|
||
返回:{"code":0, "data":[{"club_id":"xxx","club_nickname":"xxx","club_avatar":"xxx"}]}
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '004bb' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
|
||
|
||
# 4. 获取我方俱乐部ID
|
||
try:
|
||
self_config = ClubConfig.objects.get(is_self=1)
|
||
except ObjectDoesNotExist:
|
||
logger.error("未找到自己俱乐部配置")
|
||
return Response({'code': 500, 'msg': '系统配置错误'})
|
||
|
||
# 5. 查询所有合作俱乐部
|
||
try:
|
||
records = Club.objects.filter(club_id=self_config.club_id)
|
||
result = []
|
||
for record in records:
|
||
partner_config = ClubConfig.objects.filter(
|
||
club_id=record.partner_club_id,
|
||
is_self=0
|
||
).first()
|
||
result.append({
|
||
'club_id': record.partner_club_id,
|
||
'club_nickname': partner_config.club_nickname if partner_config else record.partner_club_id,
|
||
'club_avatar': partner_config.club_avatar if partner_config else '',
|
||
})
|
||
return Response({'code': 0, 'data': result})
|
||
except Exception as e:
|
||
logger.error(f"查询合作俱乐部失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
# ========== 2. 获取统计数据接口(完整) ==========
|
||
|
||
|
||
|
||
|
||
|
||
class GetStatsView(APIView):
|
||
"""
|
||
获取派单统计数据(按日或按月)
|
||
请求:POST /houtai/hqpfsj
|
||
参数:{
|
||
"username": "客服手机号",
|
||
"club_id": "对方俱乐部ID",
|
||
"direction": 1, # 1=我方派单,2=对方派单
|
||
"granularity": "day", # "day" 或 "month"
|
||
# 趋势图参数(可选,不传则使用默认值):
|
||
"trend_year_month": "YYYY-MM", # 按日时表示月份,缺省当前月
|
||
"trend_year": "YYYY", # 按月时表示年份,缺省当前年
|
||
# KPI参数(可选,不传则使用默认值):
|
||
"date": "YYYY-MM-DD", # 按日时表示日期,缺省当天
|
||
"year_month": "YYYY-MM" # 按月时表示月份,缺省当前月
|
||
}
|
||
权限:权限码2
|
||
返回:{
|
||
"code": 0,
|
||
"data": {
|
||
"summary": { ... },
|
||
"chart": { ... }
|
||
}
|
||
}
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取基础参数
|
||
username = request.data.get('username', '').strip()
|
||
club_id = request.data.get('club_id', '').strip()
|
||
direction = request.data.get('direction')
|
||
granularity = request.data.get('granularity')
|
||
|
||
# 基础参数校验
|
||
if not username:
|
||
return Response({'code': 400, 'msg': 'username 不能为空'})
|
||
if not club_id:
|
||
return Response({'code': 400, 'msg': 'club_id 不能为空'})
|
||
if direction is None:
|
||
return Response({'code': 400, 'msg': 'direction 不能为空'})
|
||
if granularity not in ('day', 'month'):
|
||
return Response({'code': 400, 'msg': 'granularity 必须是 day 或 month'})
|
||
try:
|
||
direction = int(direction)
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': 'direction 必须为整数'})
|
||
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '004bb' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
|
||
|
||
# 4. 获取当前时间用于默认值
|
||
now = datetime.now()
|
||
|
||
# 5. 处理趋势图参数(默认当前月/年)
|
||
if granularity == 'day':
|
||
trend_year_month = request.data.get('trend_year_month')
|
||
if not trend_year_month:
|
||
trend_year_month = now.strftime('%Y-%m')
|
||
try:
|
||
year, month = map(int, trend_year_month.split('-'))
|
||
start_date = datetime(year, month, 1).date()
|
||
_, days_in_month = monthrange(year, month)
|
||
end_date = datetime(year, month, days_in_month).date()
|
||
except (ValueError, TypeError):
|
||
# 如果格式错误,回退到当前月
|
||
year, month = now.year, now.month
|
||
start_date = datetime(year, month, 1).date()
|
||
_, days_in_month = monthrange(year, month)
|
||
end_date = datetime(year, month, days_in_month).date()
|
||
else:
|
||
trend_year = request.data.get('trend_year')
|
||
if not trend_year:
|
||
trend_year = str(now.year)
|
||
try:
|
||
year = int(trend_year)
|
||
except (ValueError, TypeError):
|
||
year = now.year
|
||
start_date = datetime(year, 1, 1).date()
|
||
end_date = datetime(year, 12, 31).date()
|
||
|
||
# 6. 处理KPI参数(默认当天/当月)
|
||
if granularity == 'day':
|
||
date_str = request.data.get('date')
|
||
if not date_str:
|
||
date_str = now.strftime('%Y-%m-%d')
|
||
try:
|
||
target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||
except (ValueError, TypeError):
|
||
target_date = now.date()
|
||
else:
|
||
year_month_str = request.data.get('year_month')
|
||
if not year_month_str:
|
||
year_month_str = now.strftime('%Y-%m')
|
||
try:
|
||
year, month = map(int, year_month_str.split('-'))
|
||
start_date_kpi = datetime(year, month, 1).date()
|
||
_, days_in_month = monthrange(year, month)
|
||
end_date_kpi = datetime(year, month, days_in_month).date()
|
||
except (ValueError, TypeError):
|
||
year, month = now.year, now.month
|
||
start_date_kpi = datetime(year, month, 1).date()
|
||
_, days_in_month = monthrange(year, month)
|
||
end_date_kpi = datetime(year, month, days_in_month).date()
|
||
|
||
# 7. 查询趋势图数据
|
||
response_data = {
|
||
'summary': {'dispatch_count': 0, 'dispatch_amount': 0.0, 'success_count': 0, 'success_amount': 0.0},
|
||
'chart': {'labels': [], 'dispatch_counts': [], 'dispatch_amounts': [], 'success_counts': [], 'success_amounts': []}
|
||
}
|
||
|
||
try:
|
||
if granularity == 'day':
|
||
qs = DailyDispatchStat.objects.filter(
|
||
partner_club_id=club_id,
|
||
direction=direction,
|
||
date__gte=start_date,
|
||
date__lte=end_date
|
||
).order_by('date')
|
||
data_dict = {}
|
||
for item in qs:
|
||
data_dict[item.date] = {
|
||
'dispatch_count': item.dispatch_count,
|
||
'dispatch_amount': float(item.dispatch_amount),
|
||
'success_count': item.success_count,
|
||
'success_amount': float(item.success_amount),
|
||
}
|
||
labels = []
|
||
dispatch_counts = []
|
||
dispatch_amounts = []
|
||
success_counts = []
|
||
success_amounts = []
|
||
current = start_date
|
||
while current <= end_date:
|
||
labels.append(current.strftime('%d'))
|
||
day_data = data_dict.get(current, {})
|
||
dispatch_counts.append(day_data.get('dispatch_count', 0))
|
||
dispatch_amounts.append(day_data.get('dispatch_amount', 0.0))
|
||
success_counts.append(day_data.get('success_count', 0))
|
||
success_amounts.append(day_data.get('success_amount', 0.0))
|
||
current += timedelta(days=1)
|
||
response_data['chart'] = {
|
||
'labels': labels,
|
||
'dispatch_counts': dispatch_counts,
|
||
'dispatch_amounts': dispatch_amounts,
|
||
'success_counts': success_counts,
|
||
'success_amounts': success_amounts,
|
||
}
|
||
else: # month
|
||
qs = DailyDispatchStat.objects.filter(
|
||
partner_club_id=club_id,
|
||
direction=direction,
|
||
date__gte=start_date,
|
||
date__lte=end_date
|
||
).order_by('date')
|
||
month_totals = {m: {'dispatch_count': 0, 'dispatch_amount': 0.0, 'success_count': 0, 'success_amount': 0.0}
|
||
for m in range(1, 13)}
|
||
for item in qs:
|
||
m = item.date.month
|
||
month_totals[m]['dispatch_count'] += item.dispatch_count
|
||
month_totals[m]['dispatch_amount'] += float(item.dispatch_amount)
|
||
month_totals[m]['success_count'] += item.success_count
|
||
month_totals[m]['success_amount'] += float(item.success_amount)
|
||
labels = []
|
||
dispatch_counts = []
|
||
dispatch_amounts = []
|
||
success_counts = []
|
||
success_amounts = []
|
||
for m in range(1, 13):
|
||
labels.append(f"{m}月")
|
||
dispatch_counts.append(month_totals[m]['dispatch_count'])
|
||
dispatch_amounts.append(month_totals[m]['dispatch_amount'])
|
||
success_counts.append(month_totals[m]['success_count'])
|
||
success_amounts.append(month_totals[m]['success_amount'])
|
||
response_data['chart'] = {
|
||
'labels': labels,
|
||
'dispatch_counts': dispatch_counts,
|
||
'dispatch_amounts': dispatch_amounts,
|
||
'success_counts': success_counts,
|
||
'success_amounts': success_amounts,
|
||
}
|
||
|
||
# 8. 查询KPI数据
|
||
if granularity == 'day':
|
||
try:
|
||
stat = DailyDispatchStat.objects.get(
|
||
partner_club_id=club_id,
|
||
direction=direction,
|
||
date=target_date
|
||
)
|
||
response_data['summary'] = {
|
||
'dispatch_count': stat.dispatch_count,
|
||
'dispatch_amount': float(stat.dispatch_amount),
|
||
'success_count': stat.success_count,
|
||
'success_amount': float(stat.success_amount),
|
||
}
|
||
except DailyDispatchStat.DoesNotExist:
|
||
response_data['summary'] = {
|
||
'dispatch_count': 0,
|
||
'dispatch_amount': 0.0,
|
||
'success_count': 0,
|
||
'success_amount': 0.0,
|
||
}
|
||
else: # month
|
||
qs_kpi = DailyDispatchStat.objects.filter(
|
||
partner_club_id=club_id,
|
||
direction=direction,
|
||
date__gte=start_date_kpi,
|
||
date__lte=end_date_kpi
|
||
)
|
||
summary = qs_kpi.aggregate(
|
||
dispatch_count=Sum('dispatch_count'),
|
||
dispatch_amount=Sum('dispatch_amount'),
|
||
success_count=Sum('success_count'),
|
||
success_amount=Sum('success_amount')
|
||
)
|
||
response_data['summary'] = {
|
||
'dispatch_count': summary['dispatch_count'] or 0,
|
||
'dispatch_amount': float(summary['dispatch_amount'] or 0.0),
|
||
'success_count': summary['success_count'] or 0,
|
||
'success_amount': float(summary['success_amount'] or 0.0),
|
||
}
|
||
|
||
return Response({'code': 0, 'data': response_data})
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取统计数据失败: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class PunishDashouView(APIView):
|
||
"""
|
||
客服处罚打手接口
|
||
请求:POST /houtai/kpcf
|
||
参数:{
|
||
"username": "客服手机号",
|
||
"dingdan_id": "订单ID",
|
||
"reason": "处罚原因(可选)"
|
||
}
|
||
认证:JWT,权限码1
|
||
返回:code=0 + 处罚结果
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
start_time = time.time()
|
||
|
||
# 1. 获取参数
|
||
username = request.data.get('username', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
reason = request.data.get('reason', '').strip()
|
||
|
||
if not username or not dingdan_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
try:
|
||
# 2. 身份验证
|
||
current_user = request.user
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '003aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权处罚'})
|
||
|
||
# 4. 查询订单并加锁
|
||
with transaction.atomic():
|
||
order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
|
||
|
||
# 5. 订单状态校验(允许处罚的状态:1,2,3,4,5,6,7,8,即除已完成以外的状态?这里宽松处理)
|
||
if order.zhuangtai not in [1,2,3,4,5,6,7,8]:
|
||
return Response({'code': 400, 'msg': f'订单状态 {order.zhuangtai} 不允许处罚'})
|
||
|
||
# 6. 判断是否已处罚(避免重复)
|
||
existing_punish = Chufajilu.objects.filter(
|
||
dingdan_id=dingdan_id,
|
||
dashouid=order.jiedan_dashou_id
|
||
).exists()
|
||
if existing_punish:
|
||
return Response({'code': 400, 'msg': '该打手已被处罚过'})
|
||
|
||
# 7. 区分本地处罚还是通知对方平台
|
||
is_cross = order.is_cross
|
||
dispatch_type = order.dispatch_type
|
||
partner_club_id = order.partner_club_id
|
||
partner_order_id = order.partner_order_id
|
||
dashou_id = order.jiedan_dashou_id
|
||
|
||
# 如果没有接单打手ID,无法处罚
|
||
if not dashou_id:
|
||
return Response({'code': 400, 'msg': '订单无接单打手,无法处罚'})
|
||
|
||
# 本地处罚(处罚我方打手)
|
||
def local_punish():
|
||
# 扣除打手积分(假设每次扣5分)
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
dashou_profile.jifen -= 5
|
||
dashou_profile.save()
|
||
|
||
# 创建处罚记录
|
||
Chufajilu.objects.create(
|
||
dingdan_id=dingdan_id,
|
||
dashouid=dashou_id,
|
||
qingqiuid='', # 请求处罚ID,可空
|
||
chuliid=username,
|
||
cfliyou=reason or '',
|
||
sqzhuangtai=1, # 成功
|
||
bhliyou='',
|
||
ssliyou='',
|
||
jifen=5,
|
||
)
|
||
logger.info(f"本地处罚打手成功: 订单 {dingdan_id}, 打手 {dashou_id}, 扣分5")
|
||
|
||
# 情况A:对方派单(dispatch_type=2)或 本地订单
|
||
if dispatch_type == 2 or not is_cross:
|
||
# 直接本地处罚
|
||
local_punish()
|
||
return Response({'code': 0, 'msg': '处罚成功'})
|
||
|
||
# 情况B:我方派单且有对方信息(需要通知对方平台)
|
||
elif dispatch_type == 1 and partner_club_id and partner_order_id:
|
||
# 需要通知对方平台处罚(对方平台打手)
|
||
# 获取我方俱乐部ID
|
||
my_club_config = ClubConfig.objects.filter(is_self=1).first()
|
||
if not my_club_config:
|
||
logger.error("未找到我方俱乐部配置")
|
||
return Response({'code': 500, 'msg': '系统配置错误'})
|
||
self_club_id = my_club_config.club_id
|
||
|
||
# 获取对方服务器域名
|
||
try:
|
||
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
|
||
partner_domain = club_rel.partner_domain
|
||
except ObjectDoesNotExist:
|
||
logger.error(f"未找到合作关系: {self_club_id} <-> {partner_club_id}")
|
||
return Response({'code': 500, 'msg': '未找到对方平台配置'})
|
||
|
||
# 调用对方平台处罚接口,最多重试2次
|
||
url = partner_domain.rstrip('/') + '/houtai/kptcfds'
|
||
payload = {
|
||
'order_id': partner_order_id, # 对方平台的订单ID
|
||
'club_id': self_club_id,
|
||
'reason': reason,
|
||
'operator': username
|
||
}
|
||
timeout = 3
|
||
retries = 2
|
||
success = False
|
||
for attempt in range(retries):
|
||
try:
|
||
resp = requests.post(url, json=payload, timeout=timeout)
|
||
if resp.status_code == 200:
|
||
resp_json = resp.json()
|
||
if resp_json.get('code') == 0:
|
||
success = True
|
||
logger.info(f"通知对方平台处罚成功: {dingdan_id}")
|
||
# 可选:在本平台记录处罚记录(标记已通知对方,但不扣分)
|
||
Chufajilu.objects.create(
|
||
dingdan_id=dingdan_id,
|
||
dashouid=dashou_id,
|
||
qingqiuid='',
|
||
chuliid=username,
|
||
cfliyou=reason or '',
|
||
sqzhuangtai=1,
|
||
bhliyou='',
|
||
ssliyou='',
|
||
jifen=0, # 对方扣分,本方不扣
|
||
)
|
||
break
|
||
else:
|
||
logger.warning(f"对方平台返回错误 (尝试{attempt+1}): {resp_json.get('msg')}")
|
||
else:
|
||
logger.warning(f"对方平台HTTP错误 (尝试{attempt+1}): {resp.status_code}")
|
||
except requests.exceptions.Timeout:
|
||
logger.warning(f"请求对方平台超时 (尝试{attempt+1}): {url}")
|
||
except Exception as e:
|
||
logger.warning(f"请求对方平台异常 (尝试{attempt+1}): {str(e)}")
|
||
if not success:
|
||
return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'})
|
||
|
||
return Response({'code': 0, 'msg': '处罚成功'})
|
||
|
||
else:
|
||
# 其他情况(例如我方派单但缺少对方信息),视为本地订单,处罚我方打手
|
||
local_punish()
|
||
return Response({'code': 0, 'msg': '处罚成功'})
|
||
|
||
except Dingdan.DoesNotExist:
|
||
logger.warning(f"订单不存在: {dingdan_id}")
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
except Exception as e:
|
||
logger.error(f"处罚接口异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@method_decorator(csrf_exempt, name='dispatch')
|
||
class PartnerPunishNotifyView(APIView):
|
||
"""
|
||
接收对方平台处罚通知接口
|
||
"""
|
||
authentication_classes = []
|
||
permission_classes = [AllowAny]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
order_id = request.data.get('order_id', '').strip()
|
||
club_id = request.data.get('club_id', '').strip()
|
||
reason = request.data.get('reason', '').strip()
|
||
operator = request.data.get('operator', '').strip()
|
||
|
||
if not order_id or not club_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 事务开始
|
||
with transaction.atomic():
|
||
# 查询订单并加锁
|
||
try:
|
||
order = Dingdan.objects.select_for_update().get(dingdan_id=order_id)
|
||
except Dingdan.DoesNotExist:
|
||
logger.warning(f"处罚通知:订单不存在 {order_id}")
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
dashou_id = order.jiedan_dashou_id
|
||
if not dashou_id:
|
||
logger.warning(f"订单 {order_id} 无接单打手,无法处罚")
|
||
return Response({'code': 400, 'msg': '订单无接单打手'})
|
||
|
||
# 检查是否已处罚
|
||
existing = Chufajilu.objects.filter(
|
||
dingdan_id=order_id,
|
||
dashouid=dashou_id
|
||
).exists()
|
||
if existing:
|
||
logger.info(f"订单 {order_id} 打手 {dashou_id} 已被处罚过,跳过")
|
||
return Response({'code': 0, 'msg': 'already punished'})
|
||
|
||
# 扣除积分
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
dashou_profile.jifen -= 5
|
||
dashou_profile.save()
|
||
|
||
# 创建处罚记录
|
||
Chufajilu.objects.create(
|
||
dingdan_id=order_id,
|
||
dashouid=dashou_id,
|
||
qingqiuid='',
|
||
chuliid=operator,
|
||
cfliyou=reason or '',
|
||
sqzhuangtai=0,
|
||
bhliyou='',
|
||
ssliyou='',
|
||
jifen=5,
|
||
)
|
||
logger.info(f"对方平台处罚成功: 订单 {order_id}, 打手 {dashou_id}")
|
||
|
||
return Response({'code': 0, 'msg': 'success'})
|
||
|
||
except Exception as e:
|
||
logger.error(f"对方处罚通知接口异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# ==================== 接口1:获取俱乐部列表(权限码2) ====================
|
||
class GetClubConfigForPreSettlementView(APIView):
|
||
"""
|
||
获取对方俱乐部配置列表(用于预结算页面)
|
||
权限要求:客服身份 + 权限代码2且开启
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user = request.user
|
||
# 1. 校验客服身份
|
||
try:
|
||
kefu = UserKefu.objects.select_related('user').get(user=user)
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 403, 'msg': '身份错误,非客服账号'})
|
||
if kefu.zhuangtai != 1:
|
||
return Response({'code': 403, 'msg': '客服账号已被禁用'})
|
||
|
||
# 2. 获取请求参数中的 username 并校验
|
||
data = request.data
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '004aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
|
||
|
||
# 4. 查询所有对方俱乐部(is_self=0)
|
||
club_configs = ClubConfig.objects.filter(is_self=0).only(
|
||
'club_id', 'club_nickname', 'club_avatar', 'storage_bucket_domain'
|
||
)
|
||
data_list = []
|
||
for club in club_configs:
|
||
data_list.append({
|
||
'club_id': club.club_id,
|
||
'club_nickname': club.club_nickname or club.club_id,
|
||
'club_avatar': club.club_avatar or '',
|
||
'storage_bucket_domain': club.storage_bucket_domain or ''
|
||
})
|
||
return Response({'code': 0, 'msg': 'success', 'data': data_list})
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取俱乐部配置失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
# ==================== 接口2:获取预结算订单列表 ====================
|
||
class GetPreSettlementDataView(APIView):
|
||
"""获取预结算订单列表(支持筛选、分页、统计)"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user = request.user
|
||
# 1. 客服身份校验
|
||
try:
|
||
kefu = UserKefu.objects.select_related('user').get(user=user)
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 403, 'msg': '身份错误,非客服账号'})
|
||
if kefu.zhuangtai != 1:
|
||
return Response({'code': 403, 'msg': '客服账号已被禁用'})
|
||
|
||
data = request.data
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限(003aa)
|
||
if '004aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看俱乐部配置'})
|
||
|
||
# 2. 获取筛选参数
|
||
status_filter = data.get('status_filter') # 0=待处理, 1=已处理(包含已拒绝)
|
||
club_id = data.get('club_id')
|
||
dingdan_id = data.get('dingdan_id')
|
||
partner_order_id = data.get('partner_order_id')
|
||
dashou_id = data.get('dashou_id')
|
||
clkf = data.get('clkf')
|
||
date_str = data.get('date') # 创建日期 YYYY-MM-DD
|
||
page = int(data.get('page', 1))
|
||
page_size = int(data.get('pageSize', 20))
|
||
|
||
# 3. 构建查询集
|
||
queryset = PreSettlement.objects.all()
|
||
|
||
# 状态筛选:0待处理,1已处理(status=1或2)
|
||
if status_filter == 0:
|
||
queryset = queryset.filter(status=0)
|
||
elif status_filter == 1:
|
||
queryset = queryset.filter(status__in=[1, 2])
|
||
# 如果status_filter为其他值(如'all'),则不筛选
|
||
|
||
if club_id:
|
||
queryset = queryset.filter(partner_club_id=club_id)
|
||
if dingdan_id:
|
||
queryset = queryset.filter(dingdan_id__icontains=dingdan_id)
|
||
if partner_order_id:
|
||
queryset = queryset.filter(partner_order_id__icontains=partner_order_id)
|
||
if dashou_id:
|
||
queryset = queryset.filter(dashou_id__icontains=dashou_id)
|
||
if clkf:
|
||
queryset = queryset.filter(operator__icontains=clkf)
|
||
if date_str:
|
||
try:
|
||
target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||
queryset = queryset.filter(create_time__date=target_date)
|
||
except ValueError:
|
||
pass
|
||
|
||
# 4. 统计数据(待处理总数量和总金额、已处理总数量)
|
||
pending_queryset = PreSettlement.objects.filter(status=0)
|
||
# 应用相同的筛选条件(除了状态)
|
||
if club_id:
|
||
pending_queryset = pending_queryset.filter(partner_club_id=club_id)
|
||
if dingdan_id:
|
||
pending_queryset = pending_queryset.filter(dingdan_id__icontains=dingdan_id)
|
||
if partner_order_id:
|
||
pending_queryset = pending_queryset.filter(partner_order_id__icontains=partner_order_id)
|
||
if dashou_id:
|
||
pending_queryset = pending_queryset.filter(dashou_id__icontains=dashou_id)
|
||
if clkf:
|
||
pending_queryset = pending_queryset.filter(operator__icontains=clkf)
|
||
if date_str:
|
||
pending_queryset = pending_queryset.filter(create_time__date=target_date)
|
||
|
||
pending_count = pending_queryset.count()
|
||
pending_amount = pending_queryset.aggregate(total=Sum('order_amount'))['total'] or Decimal('0.00')
|
||
|
||
processed_count = PreSettlement.objects.filter(status__in=[1, 2])
|
||
if club_id:
|
||
processed_count = processed_count.filter(partner_club_id=club_id)
|
||
if dingdan_id:
|
||
processed_count = processed_count.filter(dingdan_id__icontains=dingdan_id)
|
||
if partner_order_id:
|
||
processed_count = processed_count.filter(partner_order_id__icontains=partner_order_id)
|
||
if dashou_id:
|
||
processed_count = processed_count.filter(dashou_id__icontains=dashou_id)
|
||
if clkf:
|
||
processed_count = processed_count.filter(operator__icontains=clkf)
|
||
if date_str:
|
||
processed_count = processed_count.filter(create_time__date=target_date)
|
||
processed_count = processed_count.count()
|
||
|
||
# 5. 分页
|
||
total = queryset.count()
|
||
start = (page - 1) * page_size
|
||
end = start + page_size
|
||
items = queryset.order_by('-create_time')[start:end]
|
||
|
||
# 6. 构造返回数据
|
||
list_data = []
|
||
for item in items:
|
||
list_data.append({
|
||
'dingdan_id': item.dingdan_id,
|
||
'partner_order_id': item.partner_order_id,
|
||
'partner_club_id': item.partner_club_id,
|
||
'dashou_id': item.dashou_id,
|
||
'amount': float(item.amount),
|
||
'order_amount': float(item.order_amount),
|
||
'operator': item.operator,
|
||
'status': item.status,
|
||
'status_text': dict(PreSettlement.STATUS_CHOICES).get(item.status, '未知'),
|
||
'create_time': item.create_time.strftime('%Y-%m-%d %H:%M:%S') if item.create_time else '',
|
||
'process_time': item.process_time.strftime('%Y-%m-%d %H:%M:%S') if item.process_time else '',
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'success',
|
||
'data': {
|
||
'list': list_data,
|
||
'total': total,
|
||
'stats': {
|
||
'pending_count': pending_count,
|
||
'pending_amount': float(pending_amount),
|
||
'processed_count': processed_count
|
||
}
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取预结算数据失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
# ==================== 接口3:打款/拒绝操作 ====================
|
||
class PreSettlementActionView(APIView):
|
||
"""预结算打款/拒绝操作(原子事务,保证资金安全)"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user = request.user
|
||
# 1. 客服身份校验
|
||
try:
|
||
kefu = UserKefu.objects.select_related('user').get(user=user)
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 403, 'msg': '身份错误,非客服账号'})
|
||
if kefu.zhuangtai != 1:
|
||
return Response({'code': 403, 'msg': '客服账号已被禁用'})
|
||
|
||
data = request.data
|
||
|
||
# 1. 获取前端参数
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否有跨平台管理权限()
|
||
if '004aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权处理预结算打款'})
|
||
|
||
dingdan_id = data.get('dingdan_id')
|
||
action = data.get('action') # 'pay' 或 'reject'
|
||
|
||
if not dingdan_id or action not in ['pay', 'reject']:
|
||
return Response({'code': 400, 'msg': '参数错误'})
|
||
|
||
with transaction.atomic():
|
||
# 锁定预结算记录(行锁)
|
||
pre_settlement = PreSettlement.objects.select_for_update().get(dingdan_id=dingdan_id)
|
||
|
||
# 只有状态为0(待处理)才能操作
|
||
if pre_settlement.status != 0:
|
||
return Response({'code': 400, 'msg': '该记录已处理,不可重复操作'})
|
||
|
||
if action == 'pay':
|
||
# 打款操作:增加打手余额及相关统计
|
||
dashou_id = pre_settlement.dashou_id
|
||
amount = pre_settlement.amount
|
||
|
||
# 查询打手扩展表
|
||
try:
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
return Response({'code': 404, 'msg': '打手不存在'})
|
||
|
||
# 使用 F 表达式原子更新余额和统计字段(防止并发覆盖)
|
||
dashou_profile.yue = F('yue') + amount
|
||
dashou_profile.zonge = F('zonge') + amount
|
||
dashou_profile.jinrishouyi = F('jinrishouyi') + amount
|
||
dashou_profile.jinyueshouyi = F('jinyueshouyi') + amount
|
||
dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1
|
||
dashou_profile.save(update_fields=['yue', 'zonge', 'jinrishouyi', 'jinyueshouyi', 'chengjiaozongliang'])
|
||
|
||
# 更新预结算状态为已结算
|
||
pre_settlement.status = 1
|
||
pre_settlement.process_time = timezone.now()
|
||
pre_settlement.operator = user.phone
|
||
pre_settlement.save()
|
||
|
||
logger.info(f"预结算打款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {amount}")
|
||
|
||
elif action == 'reject':
|
||
# 拒绝操作:仅修改状态,不涉及资金变动
|
||
pre_settlement.status = 2
|
||
pre_settlement.process_time = timezone.now()
|
||
pre_settlement.operator = user.phone
|
||
pre_settlement.save()
|
||
logger.info(f"预结算拒绝: 订单 {dingdan_id}")
|
||
|
||
return Response({'code': 0, 'msg': '操作成功'})
|
||
|
||
except PreSettlement.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '预结算记录不存在'})
|
||
except Exception as e:
|
||
logger.error(f"预结算操作异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class GetRolePermissionView(APIView):
|
||
"""
|
||
获取所有角色、权限以及角色关联的用户数
|
||
权限要求:用户必须拥有 000001 权限
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 1. 权限校验(验证客服身份,并获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 2. 检查是否有 000001 权限
|
||
if '000001' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||
|
||
# 3. 获取所有权限
|
||
all_perms = Permission.objects.all().values('perm_code', 'perm_name', 'description')
|
||
permissions_list = list(all_perms)
|
||
|
||
# 4. 获取所有角色,并统计每个角色下的用户数和关联的权限
|
||
roles = Role.objects.all()
|
||
roles_data = []
|
||
for role in roles:
|
||
# 统计该角色下的用户数
|
||
user_count = UserRole.objects.filter(role=role).count()
|
||
# 获取该角色绑定的权限
|
||
role_perms = RolePermission.objects.filter(role=role).select_related('permission')
|
||
perms_of_role = [
|
||
{
|
||
'perm_code': rp.permission.perm_code,
|
||
'perm_name': rp.permission.perm_name,
|
||
'description': rp.permission.description
|
||
}
|
||
for rp in role_perms
|
||
]
|
||
roles_data.append({
|
||
'role_code': role.role_code,
|
||
'role_name': role.role_name,
|
||
'description': role.description,
|
||
'user_count': user_count,
|
||
'permissions': perms_of_role
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'roles': roles_data,
|
||
'permissions': permissions_list
|
||
}
|
||
})
|
||
|
||
|
||
class ModifyRolePermissionView(APIView):
|
||
"""
|
||
通用接口:支持删除角色、为角色添加权限、移除角色权限
|
||
操作类型通过 action 字段区分
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
action = request.data.get('action') # delete_role, add_permission, remove_permission
|
||
if not username or not action:
|
||
return Response({'code': 400, 'msg': '缺少必要参数'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '000001' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||
|
||
# 获取角色(需要角色编码)
|
||
role_code = request.data.get('role_code')
|
||
if not role_code:
|
||
return Response({'code': 400, 'msg': '缺少角色编码'})
|
||
|
||
try:
|
||
role = Role.objects.get(role_code=role_code)
|
||
except Role.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '角色不存在'})
|
||
|
||
# 超级管理员角色(role_code='000001')的特殊限制
|
||
if role.role_code == '000001':
|
||
if action in ['delete_role']:
|
||
return Response({'code': 403, 'msg': '超级管理员角色不能删除'})
|
||
# 修改名称/描述(本接口不涉及名称描述修改,但若后续扩展需限制)
|
||
|
||
# 根据 action 处理
|
||
if action == 'delete_role':
|
||
# 删除角色:同时删除角色权限关联、用户角色关联
|
||
with transaction.atomic():
|
||
RolePermission.objects.filter(role=role).delete()
|
||
UserRole.objects.filter(role=role).delete()
|
||
role.delete()
|
||
return Response({'code': 0, 'msg': '角色删除成功'})
|
||
|
||
elif action == 'add_permission':
|
||
perm_code = request.data.get('perm_code')
|
||
if not perm_code:
|
||
return Response({'code': 400, 'msg': '缺少权限编码'})
|
||
# 超级管理员角色不能添加 000001 权限(但实际它已拥有,这里防御)
|
||
if role.role_code == '000001' and perm_code == '000001':
|
||
return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'})
|
||
try:
|
||
perm = Permission.objects.get(perm_code=perm_code)
|
||
except Permission.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '权限不存在'})
|
||
# 检查是否已存在
|
||
if RolePermission.objects.filter(role=role, permission=perm).exists():
|
||
return Response({'code': 400, 'msg': '该角色已拥有此权限'})
|
||
RolePermission.objects.create(role=role, permission=perm)
|
||
return Response({'code': 0, 'msg': '权限添加成功'})
|
||
|
||
elif action == 'remove_permission':
|
||
perm_code = request.data.get('perm_code')
|
||
if not perm_code:
|
||
return Response({'code': 400, 'msg': '缺少权限编码'})
|
||
# 超级管理员角色不能删除 000001 权限
|
||
if role.role_code == '000001' and perm_code == '000001':
|
||
return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'})
|
||
try:
|
||
perm = Permission.objects.get(perm_code=perm_code)
|
||
except Permission.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '权限不存在'})
|
||
deleted = RolePermission.objects.filter(role=role, permission=perm).delete()
|
||
if deleted[0] == 0:
|
||
return Response({'code': 400, 'msg': '该角色未拥有此权限'})
|
||
return Response({'code': 0, 'msg': '权限移除成功'})
|
||
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的 action'})
|
||
|
||
|
||
import random
|
||
import string
|
||
from django.db import transaction
|
||
|
||
class AddRoleView(APIView):
|
||
"""
|
||
添加角色:自动生成角色编码,前端提供角色名称、描述、选择的权限列表
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
role_name = request.data.get('role_name', '').strip()
|
||
description = request.data.get('description', '').strip()
|
||
perm_codes = request.data.get('perm_codes', []) # 列表
|
||
|
||
if not username or not role_name:
|
||
return Response({'code': 400, 'msg': '缺少必要参数'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '000001' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||
|
||
# 检查角色名称是否已存在
|
||
if Role.objects.filter(role_name=role_name).exists():
|
||
return Response({'code': 400, 'msg': '角色名称已存在'})
|
||
|
||
# 生成唯一角色编码(格式:ROLE_ + 8位随机字母数字)
|
||
while True:
|
||
random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
||
role_code = f"ROLE_{random_str}"
|
||
if not Role.objects.filter(role_code=role_code).exists():
|
||
break
|
||
|
||
# 创建角色
|
||
with transaction.atomic():
|
||
role = Role.objects.create(
|
||
role_code=role_code,
|
||
role_name=role_name,
|
||
description=description,
|
||
|
||
)
|
||
# 绑定权限(过滤掉不存在的权限编码)
|
||
for pc in perm_codes:
|
||
# 不允许添加 000001 权限(此权限仅供超级管理员)
|
||
if pc == '000001':
|
||
continue
|
||
try:
|
||
perm = Permission.objects.get(perm_code=pc)
|
||
RolePermission.objects.create(role=role, permission=perm)
|
||
except Permission.DoesNotExist:
|
||
# 忽略不存在的权限,或记录日志
|
||
logger.warning(f"添加角色时忽略不存在的权限编码: {pc}")
|
||
pass
|
||
|
||
return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class GetAdminRolesView(APIView):
|
||
"""获取所有角色(用于筛选和分配)"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '000001' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||
|
||
roles = Role.objects.all().values('role_code', 'role_name')
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'roles': list(roles)
|
||
}
|
||
})
|
||
|
||
|
||
|
||
class GetAdminUserListView(APIView):
|
||
"""获取后台用户列表(支持分页、筛选)"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '000001' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||
|
||
# 筛选条件
|
||
phone = request.data.get('phone', '').strip()
|
||
nicheng = request.data.get('nicheng', '').strip()
|
||
role_code = request.data.get('roleCode', '').strip()
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('pageSize', 20))
|
||
|
||
# 基础查询:user_type='kefu' 且存在客服扩展表
|
||
users = UserMain.objects.filter(user_type='kefu', kefu_profile__isnull=False).select_related('kefu_profile')
|
||
if phone:
|
||
users = users.filter(phone__icontains=phone)
|
||
if nicheng:
|
||
users = users.filter(kefu_profile__nicheng__icontains=nicheng)
|
||
if role_code:
|
||
# 筛选拥有该角色的用户
|
||
user_ids_with_role = UserRole.objects.filter(role__role_code=role_code).values_list('account_id', flat=True)
|
||
users = users.filter(phone__in=user_ids_with_role)
|
||
|
||
# 排序
|
||
users = users.order_by('-create_time')
|
||
|
||
# 分页
|
||
paginator = Paginator(users, page_size)
|
||
page_obj = paginator.get_page(page)
|
||
|
||
# 构建返回数据
|
||
data_list = []
|
||
phones = [user.phone for user in page_obj]
|
||
log_counts = {
|
||
row['xiugaiid']: row['c']
|
||
for row in Xiugaijilu.objects.filter(xiugaiid__in=phones)
|
||
.values('xiugaiid')
|
||
.annotate(c=Count('id'))
|
||
}
|
||
for user in page_obj:
|
||
# 获取用户的所有角色
|
||
user_roles = UserRole.objects.filter(account_id=user.phone).select_related('role')
|
||
roles_data = [{'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles]
|
||
data_list.append({
|
||
'phone': user.phone,
|
||
'nicheng': user.kefu_profile.nicheng,
|
||
'roles': roles_data,
|
||
'status': user.kefu_profile.zhuangtai,
|
||
'jilushuliang': int(log_counts.get(user.phone, 0)),
|
||
'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||
'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': data_list,
|
||
'total': paginator.count,
|
||
'page': page,
|
||
'pageSize': page_size
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
class ModifyAdminUserView(APIView):
|
||
"""修改后台用户信息(昵称、密码、二级密码、状态、角色)"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
action = request.data.get('action')
|
||
if not username or not action:
|
||
return Response({'code': 400, 'msg': '缺少参数'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '000001' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||
|
||
target_phone = request.data.get('phone', '').strip()
|
||
if not target_phone:
|
||
return Response({'code': 400, 'msg': '缺少目标用户账号'})
|
||
|
||
try:
|
||
target_user = UserMain.objects.get(phone=target_phone, user_type='kefu')
|
||
target_kefu = target_user.kefu_profile
|
||
except UserMain.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不存在'})
|
||
except UserKefu.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户扩展信息缺失'})
|
||
|
||
with transaction.atomic():
|
||
if action == 'update_user_info':
|
||
# 修改昵称、状态、密码、二级密码
|
||
nicheng = request.data.get('nicheng')
|
||
status = request.data.get('status')
|
||
password = request.data.get('password')
|
||
erjimima = request.data.get('erjimima')
|
||
old_nicheng = target_kefu.nicheng
|
||
old_status = target_kefu.zhuangtai
|
||
changed = []
|
||
if nicheng is not None and nicheng != old_nicheng:
|
||
target_kefu.nicheng = nicheng
|
||
changed.append(f'昵称 {old_nicheng}→{nicheng}')
|
||
if status is not None:
|
||
new_status = int(status)
|
||
if new_status != old_status:
|
||
target_kefu.zhuangtai = new_status
|
||
changed.append(f'状态 {old_status}→{new_status}({"正常" if new_status == 1 else "禁用"})')
|
||
if password:
|
||
if len(password) < 6:
|
||
return Response({'code': 400, 'msg': '密码至少6位'})
|
||
target_user.password = password
|
||
changed.append('修改登录密码')
|
||
if erjimima:
|
||
if len(erjimima) < 6:
|
||
return Response({'code': 400, 'msg': '二级密码至少6位'})
|
||
target_kefu.erjimima = erjimima # 假设二级密码也需要加密
|
||
changed.append('修改二级密码')
|
||
target_user.save()
|
||
target_kefu.save()
|
||
if changed:
|
||
from yonghu.xiugai_log import write_xiugai_log, LEIXING_KEFU
|
||
write_xiugai_log(
|
||
yonghuid=target_user.yonghuid,
|
||
operator_phone=kefu.user.phone,
|
||
leixing=LEIXING_KEFU,
|
||
qitashuoming=f'后台账号[{target_phone}]:' + ';'.join(changed),
|
||
)
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
|
||
elif action == 'add_user_roles':
|
||
role_codes = request.data.get('role_codes', [])
|
||
if not role_codes:
|
||
return Response({'code': 400, 'msg': '请选择角色'})
|
||
added = []
|
||
for rc in role_codes:
|
||
try:
|
||
role = Role.objects.get(role_code=rc)
|
||
except Role.DoesNotExist:
|
||
continue
|
||
_, created = UserRole.objects.get_or_create(account_id=target_phone, role=role)
|
||
if created:
|
||
added.append(f'{role.role_name}({rc})')
|
||
if added:
|
||
from yonghu.xiugai_log import write_xiugai_log, LEIXING_KEFU
|
||
write_xiugai_log(
|
||
yonghuid=target_user.yonghuid,
|
||
operator_phone=kefu.user.phone,
|
||
leixing=LEIXING_KEFU,
|
||
qitashuoming=f'后台账号[{target_phone}]添加角色:{",".join(added)}',
|
||
)
|
||
return Response({'code': 0, 'msg': '角色添加成功'})
|
||
|
||
elif action == 'remove_user_role':
|
||
role_code = request.data.get('role_code')
|
||
if not role_code:
|
||
return Response({'code': 400, 'msg': '缺少角色编码'})
|
||
try:
|
||
role = Role.objects.get(role_code=role_code)
|
||
except Role.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '角色不存在'})
|
||
deleted, _ = UserRole.objects.filter(account_id=target_phone, role=role).delete()
|
||
if deleted:
|
||
from yonghu.xiugai_log import write_xiugai_log, LEIXING_KEFU
|
||
write_xiugai_log(
|
||
yonghuid=target_user.yonghuid,
|
||
operator_phone=kefu.user.phone,
|
||
leixing=LEIXING_KEFU,
|
||
qitashuoming=f'后台账号[{target_phone}]移除角色:{role.role_name}({role_code})',
|
||
)
|
||
return Response({'code': 0, 'msg': '角色移除成功'})
|
||
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的action'})
|
||
|
||
|
||
|
||
|
||
|
||
class AddAdminUserView(APIView):
|
||
"""添加后台用户(客服账号)"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '000001' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||
|
||
phone = request.data.get('phone', '').strip()
|
||
password = request.data.get('password', '').strip()
|
||
erjimima = request.data.get('erjimima', '').strip()
|
||
nicheng = request.data.get('nicheng', '').strip()
|
||
role_codes = request.data.get('role_codes', [])
|
||
|
||
if not all([phone, password, erjimima, nicheng]):
|
||
return Response({'code': 400, 'msg': '请填写完整信息'})
|
||
# UserMain.phone / UserRole.account_id 均为 max_length=11
|
||
if len(phone) > 11:
|
||
return Response({'code': 400, 'msg': '账号最长11位,请使用手机号'})
|
||
if len(password) < 6 or len(erjimima) < 6:
|
||
return Response({'code': 400, 'msg': '密码和二级密码至少6位'})
|
||
|
||
# 校验手机号是否已被占用(且 user_type='kefu')
|
||
if UserMain.objects.filter(phone=phone, user_type='kefu').exists():
|
||
return Response({'code': 400, 'msg': '该账号已存在'})
|
||
|
||
# 生成唯一的 yonghuid(7位数字)
|
||
while True:
|
||
yonghuid = str(random.randint(1000000, 9999999))
|
||
if not UserMain.objects.filter(yonghuid=yonghuid).exists():
|
||
break
|
||
|
||
# 生成唯一的 openid(格式:admin_{时间戳}_{随机6位})
|
||
timestamp = str(int(time.time() * 1000))
|
||
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||
openid = f"admin_{timestamp}_{random_suffix}"
|
||
while UserMain.objects.filter(openid=openid).exists():
|
||
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||
openid = f"admin_{timestamp}_{random_suffix}"
|
||
|
||
with transaction.atomic():
|
||
user_main = UserMain.objects.create(
|
||
yonghuid=yonghuid,
|
||
openid=openid,
|
||
phone=phone,
|
||
password=password,
|
||
user_type='kefu'
|
||
)
|
||
# 创建客服扩展表(后台用户扩展)
|
||
UserKefu.objects.create(
|
||
user=user_main,
|
||
nicheng=nicheng,
|
||
erjimima=erjimima,
|
||
zhuangtai=1
|
||
)
|
||
# 分配角色
|
||
for rc in role_codes:
|
||
try:
|
||
role = Role.objects.get(role_code=rc)
|
||
UserRole.objects.create(account_id=phone, role=role)
|
||
except Role.DoesNotExist:
|
||
pass
|
||
|
||
from yonghu.xiugai_log import write_xiugai_log, LEIXING_KEFU
|
||
write_xiugai_log(
|
||
yonghuid=user_main.yonghuid,
|
||
operator_phone=kefu.user.phone,
|
||
leixing=LEIXING_KEFU,
|
||
qitashuoming=f'新增后台账号[{phone}]昵称={nicheng},角色={role_codes}',
|
||
)
|
||
|
||
return Response({'code': 0, 'msg': '添加成功'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KefuGetDashouListView(APIView):
|
||
"""
|
||
客服获取打手列表接口(仅打手类型)
|
||
需要拥有以下任一权限:001aa,001bb,001cc,001dd,001ee,001ff,001gg
|
||
请求:POST /yonghu/kefuhqdslb
|
||
参数:
|
||
username: 客服手机号(用于权限校验)
|
||
keyword: 搜索关键词(可选,搜索用户ID或昵称)
|
||
page: 页码(从1开始)
|
||
page_size: 每页数量
|
||
返回:
|
||
code: 0
|
||
data: {
|
||
list: [打手对象],
|
||
has_more: bool,
|
||
total: int, // 总打手数
|
||
valid_dashou_count: int // 有效打手数(有有效会员)
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数(前端字段为 username)
|
||
username = request.data.get('username', '').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': '分页参数错误'})
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '认证失败'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否拥有打手管理相关权限(至少一个)
|
||
required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'}
|
||
if not any(p in permissions for p in required_perms):
|
||
return Response({'code': 403, 'msg': '您没有权限查看打手列表'})
|
||
|
||
# 4. 构建打手查询集(仅打手类型)
|
||
dashou_qs = UserDashou.objects.select_related('user').all()
|
||
|
||
# 关键词筛选(用户ID或昵称)
|
||
if keyword:
|
||
dashou_qs = dashou_qs.filter(
|
||
Q(user__yonghuid__icontains=keyword) |
|
||
Q(nicheng__icontains=keyword)
|
||
)
|
||
|
||
# 5. 统计打手总数
|
||
total_dashou = dashou_qs.count()
|
||
|
||
# 6. 统计有效打手数(有会员且未过期)
|
||
dashou_user_ids = dashou_qs.values_list('user__yonghuid', flat=True)
|
||
now = timezone.now()
|
||
valid_user_ids = Huiyuangoumai.objects.filter(
|
||
yonghu_id__in=dashou_user_ids,
|
||
huiyuan_zhuangtai=1,
|
||
daoqi_time__gt=now
|
||
).values_list('yonghu_id', flat=True).distinct()
|
||
valid_dashou_count = valid_user_ids.count()
|
||
|
||
# 7. 分页
|
||
offset = (page - 1) * page_size
|
||
dashou_list = dashou_qs.order_by('-create_time')[offset:offset + page_size]
|
||
|
||
# 8. 构建返回数据
|
||
result = []
|
||
for dashou in dashou_list:
|
||
user = dashou.user
|
||
result.append({
|
||
'yonghuid': user.yonghuid,
|
||
'avatar': user.avatar or '',
|
||
'zaixianzhuangtai': dashou.zaixianzhuangtai,
|
||
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
|
||
'nicheng': dashou.nicheng or '',
|
||
'zhuangtai': dashou.zhuangtai,
|
||
})
|
||
|
||
has_more = (offset + len(dashou_list)) < total_dashou
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': result,
|
||
'has_more': has_more,
|
||
'total': total_dashou,
|
||
'valid_dashou_count': valid_dashou_count,
|
||
}
|
||
})
|
||
|
||
|
||
|
||
class KefuGetDashouDetailView(APIView):
|
||
"""
|
||
客服获取打手详情接口
|
||
请求:POST /yonghu/kefuhqdsxq
|
||
参数:{
|
||
"username": "客服手机号",
|
||
"uid": "打手ID (yonghuid)"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 + 打手信息 + 会员列表 + 所有会员类型 + 客服权限列表
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
username = request.data.get('phone', '').strip()
|
||
uid = request.data.get('uid', '').strip()
|
||
|
||
if not username or not uid:
|
||
return Response({'code': 400, 'msg': '缺少参数'})
|
||
|
||
# 2. 公共权限校验(验证客服身份,获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 3. 检查是否拥有打手管理相关权限(至少一个)
|
||
required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'}
|
||
if not any(p in permissions for p in required_perms):
|
||
return Response({'code': 403, 'msg': '您没有权限查看打手详情'})
|
||
|
||
# 4. 查询打手主表和扩展表
|
||
try:
|
||
user_main = UserMain.objects.select_related('dashou_profile').get(
|
||
yonghuid=uid
|
||
)
|
||
except UserMain.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手不存在'})
|
||
|
||
try:
|
||
dashou = user_main.dashou_profile
|
||
except ObjectDoesNotExist:
|
||
# 用户存在但未注册打手身份,返回基本信息
|
||
user_info = {
|
||
'yonghuid': user_main.yonghuid,
|
||
'avatar': user_main.avatar or '',
|
||
'zaixianzhuangtai': 0,
|
||
'zhanghaozhuangtai': 0,
|
||
'zhuangtai': 0,
|
||
'nicheng': '',
|
||
'chenghao': '',
|
||
'dianhua': '',
|
||
'wechat': '',
|
||
'phone': user_main.phone or '',
|
||
'create_time': user_main.create_time,
|
||
'ip': user_main.ip or '',
|
||
'jiedanzongliang': 0,
|
||
'chengjiaozongliang': 0,
|
||
'tuikuanliang': 0,
|
||
'jinrijiedan': 0,
|
||
'yue': 0.00,
|
||
'zonge': 0.00,
|
||
'jifen': 0,
|
||
'yajin': 0.00,
|
||
'jinrishouyi': 0.00,
|
||
'jinyueshouyi': 0.00,
|
||
'yaoqingren': '',
|
||
'jieshao': '',
|
||
}
|
||
member_list = []
|
||
# 仍然需要返回 all_members 和 permissions
|
||
all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao'))
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'user_info': user_info,
|
||
'member_list': member_list,
|
||
'all_members': all_members,
|
||
'permissions': permissions,
|
||
}
|
||
})
|
||
|
||
# 5. 构建打手信息
|
||
user_info = {
|
||
'yonghuid': user_main.yonghuid,
|
||
'avatar': user_main.avatar or '',
|
||
'zaixianzhuangtai': dashou.zaixianzhuangtai,
|
||
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
|
||
'zhuangtai': dashou.zhuangtai,
|
||
'nicheng': dashou.nicheng or '',
|
||
'chenghao': dashou.chenghao or '',
|
||
'dianhua': dashou.dianhua or '',
|
||
'wechat': dashou.wechat or '',
|
||
'phone': user_main.phone or '',
|
||
'create_time': user_main.create_time,
|
||
'ip': user_main.ip or '',
|
||
'jiedanzongliang': dashou.jiedanzongliang,
|
||
'chengjiaozongliang': dashou.chengjiaozongliang,
|
||
'tuikuanliang': dashou.tuikuanliang,
|
||
'jinrijiedan': dashou.jinrijiedan,
|
||
'yue': float(dashou.yue) if dashou.yue else 0.00,
|
||
'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
|
||
'jifen': dashou.jifen,
|
||
'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
|
||
'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
|
||
'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
|
||
'yaoqingren': dashou.yaoqingren or '',
|
||
'jieshao': dashou.jieshao or '',
|
||
}
|
||
|
||
# 6. 查询打手已拥有的会员(未过期)
|
||
member_records = Huiyuangoumai.objects.filter(
|
||
yonghu_id=uid
|
||
).order_by('-create_time')
|
||
member_list = []
|
||
for record in member_records:
|
||
# 检查是否过期
|
||
if hasattr(record, 'jiance_shifou_daoqi'):
|
||
is_expired = record.jiance_shifou_daoqi()
|
||
else:
|
||
is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
|
||
|
||
if not is_expired:
|
||
try:
|
||
huiyuan = Huiyuan.objects.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,
|
||
'huiyuan_id': record.huiyuan_id, # 用于删除操作
|
||
})
|
||
|
||
# 7. 查询所有会员类型(用于前端添加会员)
|
||
all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao'))
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'user_info': user_info,
|
||
'member_list': member_list,
|
||
'all_members': all_members,
|
||
'permissions': permissions, # 返回当前客服的权限列表
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
'''class KefuGetDashouDetailView(APIView):
|
||
"""
|
||
客服获取打手详情接口
|
||
请求:POST /yonghu/kefuhqdsxq
|
||
参数:{
|
||
"phone": "客服手机号",
|
||
"uid": "打手ID (yonghuid)"
|
||
}
|
||
认证:JWT
|
||
返回:code=0 + 打手信息 + 会员列表
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
username = request.data.get('phone', '').strip()
|
||
uid = request.data.get('uid', '').strip()
|
||
|
||
if not username or not uid:
|
||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 客服身份验证
|
||
current_user = request.user
|
||
|
||
# 2. 公共权限校验(验证客服身份,获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查是否拥有打手管理相关权限(至少一个)
|
||
required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'}
|
||
if not any(p in permissions for p in required_perms):
|
||
return Response({'code': 403, 'msg': '您没有权限查看打手列表'})
|
||
|
||
# 3. 查询打手主表和扩展表
|
||
try:
|
||
# 使用 select_related 预加载 dashou_profile,减少数据库查询
|
||
user_main = UserMain.objects.select_related('dashou_profile').get(
|
||
yonghuid=uid
|
||
)
|
||
except UserMain.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 验证用户类型是否为打手(可选,但建议验证)
|
||
# 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册)
|
||
try:
|
||
dashou = user_main.dashou_profile
|
||
except ObjectDoesNotExist:
|
||
# 用户存在但未注册打手身份,返回基本信息
|
||
user_info = {
|
||
'yonghuid': user_main.yonghuid,
|
||
'avatar': user_main.avatar or '',
|
||
'zaixianzhuangtai': 0,
|
||
'zhanghaozhuangtai': 0,
|
||
'zhuangtai': 0,
|
||
'nicheng': '',
|
||
'chenghao': '',
|
||
'dianhua': '',
|
||
'wechat': '',
|
||
'phone': user_main.phone or '',
|
||
'create_time': user_main.create_time,
|
||
'ip': user_main.ip or '',
|
||
'jiedanzongliang': 0,
|
||
'chengjiaozongliang': 0,
|
||
'tuikuanliang': 0,
|
||
'jinrijiedan': 0,
|
||
'yue': 0.00,
|
||
'zonge': 0.00,
|
||
'jifen': 0,
|
||
'yajin': 0.00,
|
||
'jinrishouyi': 0.00,
|
||
'jinyueshouyi': 0.00,
|
||
'yaoqingren': '',
|
||
'jieshao': '',
|
||
}
|
||
member_list = []
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'user_info': user_info,
|
||
'member_list': member_list
|
||
}
|
||
})
|
||
|
||
# 4. 构建打手信息
|
||
user_info = {
|
||
'yonghuid': user_main.yonghuid,
|
||
'avatar': user_main.avatar or '',
|
||
'zaixianzhuangtai': dashou.zaixianzhuangtai,
|
||
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
|
||
'zhuangtai': dashou.zhuangtai,
|
||
'nicheng': dashou.nicheng or '',
|
||
'chenghao': dashou.chenghao or '',
|
||
'dianhua': dashou.dianhua or '',
|
||
'wechat': dashou.wechat or '',
|
||
'phone': user_main.phone or '',
|
||
'create_time': user_main.create_time,
|
||
'ip': user_main.ip or '',
|
||
'jiedanzongliang': dashou.jiedanzongliang,
|
||
'chengjiaozongliang': dashou.chengjiaozongliang,
|
||
'tuikuanliang': dashou.tuikuanliang,
|
||
'jinrijiedan': dashou.jinrijiedan,
|
||
'yue': float(dashou.yue) if dashou.yue else 0.00,
|
||
'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
|
||
'jifen': dashou.jifen,
|
||
'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
|
||
'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
|
||
'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
|
||
'yaoqingren': dashou.yaoqingren or '',
|
||
'jieshao': dashou.jieshao or '',
|
||
}
|
||
|
||
# 5. 查询打手会员信息(仅未过期)
|
||
member_records = Huiyuangoumai.objects.filter(
|
||
yonghu_id=uid
|
||
).order_by('-create_time')
|
||
|
||
member_list = []
|
||
for record in member_records:
|
||
# 调用模型方法检查是否过期
|
||
if hasattr(record, 'jiance_shifou_daoqi'):
|
||
is_expired = record.jiance_shifou_daoqi()
|
||
else:
|
||
# 如果方法不存在,简单判断
|
||
is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
|
||
|
||
if not is_expired:
|
||
# 获取会员详情
|
||
try:
|
||
huiyuan = Huiyuan.objects.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):
|
||
"""
|
||
客服修改打手信息接口
|
||
支持操作:
|
||
- 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)
|
||
- gai_chenghao : 修改打手称号(升级/降级金牌) (权限001hh)
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
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()
|
||
|
||
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 = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
except UserMain.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手不存在'})
|
||
except UserDashou.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手扩展信息缺失'})
|
||
|
||
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,
|
||
'chenghao': dashou_profile.chenghao,
|
||
}
|
||
|
||
# ---------- 余额操作 ----------
|
||
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 = dashou_profile.yue
|
||
Xiugaijilu.objects.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.phone,
|
||
leixing=2,
|
||
xiugaitijiao=after,
|
||
xiugaitijiaoq=before['yue'],
|
||
qitashuoming=f'打手减余额 {amount},{before["yue"]} → {after}',
|
||
)
|
||
return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after)}})
|
||
|
||
elif caozuo == 'jia_yue':
|
||
if '001bb' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限加打手余额'})
|
||
amount = Decimal(str(value))
|
||
dashou_profile.yue += amount
|
||
dashou_profile.save()
|
||
after = dashou_profile.yue
|
||
Xiugaijilu.objects.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.phone,
|
||
leixing=2,
|
||
xiugaitijiao=after,
|
||
xiugaitijiaoq=before['yue'],
|
||
qitashuoming=f'打手加余额 {amount},{before["yue"]} → {after}',
|
||
)
|
||
return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after)}})
|
||
|
||
# ---------- 押金操作 ----------
|
||
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 = dashou_profile.yajin
|
||
Xiugaijilu.objects.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.phone,
|
||
leixing=2,
|
||
yajin=after,
|
||
yyajin=before['yajin'],
|
||
qitashuoming=f'打手减押金 {amount},{before["yajin"]} → {after}',
|
||
)
|
||
return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after)}})
|
||
|
||
elif caozuo == 'jia_yajin':
|
||
if '001bb' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限加打手押金'})
|
||
amount = Decimal(str(value))
|
||
dashou_profile.yajin += amount
|
||
dashou_profile.save()
|
||
after = dashou_profile.yajin
|
||
Xiugaijilu.objects.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.phone,
|
||
leixing=2,
|
||
yajin=after,
|
||
yyajin=before['yajin'],
|
||
qitashuoming=f'打手加押金 {amount},{before["yajin"]} → {after}',
|
||
)
|
||
return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after)}})
|
||
|
||
# ---------- 积分操作 ----------
|
||
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 = dashou_profile.jifen
|
||
Xiugaijilu.objects.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.phone,
|
||
leixing=2,
|
||
jifen=after,
|
||
yjifen=before['jifen'],
|
||
qitashuoming=f'打手减积分 {jifen},{before["jifen"]} → {after}',
|
||
)
|
||
return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after}})
|
||
|
||
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 = dashou_profile.jifen
|
||
Xiugaijilu.objects.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.phone,
|
||
leixing=2,
|
||
jifen=after,
|
||
yjifen=before['jifen'],
|
||
qitashuoming=f'打手加积分 {jifen},{before["jifen"]} → {after}',
|
||
)
|
||
return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after}})
|
||
|
||
# ---------- 状态修改 ----------
|
||
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.objects.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.phone,
|
||
leixing=2,
|
||
qitashuoming=f'打手账号{"解封" if new_status == 1 else "封禁"},状态→{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.objects.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.objects.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.phone,
|
||
leixing=2,
|
||
qitashuoming=f'修改接单状态为{new_status}',
|
||
)
|
||
return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}})
|
||
|
||
# ---------- 会员操作 ----------
|
||
elif caozuo == 'jia_huiyuan':
|
||
if '001ff' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限给打手加会员'})
|
||
if not huiyuan_id:
|
||
return Response({'code': 400, 'msg': '缺少会员ID'})
|
||
days = int(value) if value else 30
|
||
try:
|
||
huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id)
|
||
except Huiyuan.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '会员类型不存在'})
|
||
record, created = Huiyuangoumai.objects.select_for_update().get_or_create(
|
||
yonghu_id=dashou_id,
|
||
huiyuan_id=huiyuan_id,
|
||
defaults={
|
||
'huiyuan_zhuangtai': 1,
|
||
'jieshao': huiyuan.jieshao,
|
||
'daoqi_time': timezone.now() + timedelta(days=days),
|
||
}
|
||
)
|
||
if not created:
|
||
new_daoqi = record.daoqi_time + timedelta(days=days)
|
||
record.daoqi_time = new_daoqi
|
||
record.huiyuan_zhuangtai = 1
|
||
record.save()
|
||
Xiugaijilu.objects.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.objects.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id)
|
||
except Huiyuangoumai.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '该打手未购买此会员'})
|
||
record.delete()
|
||
Xiugaijilu.objects.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.phone,
|
||
leixing=2,
|
||
huiyuan_id=huiyuan_id,
|
||
qitashuoming=f'移除会员 {huiyuan_id}',
|
||
)
|
||
return Response({'code': 0, 'msg': '减会员成功'})
|
||
|
||
# ---------- 称号修改(金牌打手) ----------
|
||
elif caozuo == 'gai_chenghao':
|
||
if '001hh' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限修改打手称号'})
|
||
new_chenghao = str(value).strip()
|
||
if not new_chenghao:
|
||
return Response({'code': 400, 'msg': '称号不能为空'})
|
||
# 可限制合法值,这里只记录日志
|
||
dashou_profile.chenghao = new_chenghao
|
||
dashou_profile.save()
|
||
Xiugaijilu.objects.create(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.phone,
|
||
leixing=2,
|
||
qitashuoming=f'修改称号:{before["chenghao"] or "无"} → {new_chenghao}',
|
||
)
|
||
return Response({'code': 0, 'msg': '修改称号成功', 'data': {'new_chenghao': new_chenghao}})
|
||
|
||
else:
|
||
return Response({'code': 400, 'msg': '未知操作类型'})
|
||
|
||
except Exception as e:
|
||
logger.error(f"修改打手信息异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KefuGetShangjiaListView(APIView):
|
||
"""
|
||
客服/管理员获取商家列表接口
|
||
需要拥有以下任一权限:1100a, 1100b
|
||
请求:POST /houtai/hqsjgllb
|
||
参数:
|
||
username: 客服手机号(用于权限校验,必填)
|
||
keyword: 搜索关键词(可选,搜索商家用户ID或昵称)
|
||
balance_op: 余额比较符,gte(>=) / lte(<=)(可选)
|
||
balance_value: 余额数值(可选,需与balance_op同时存在)
|
||
order_count_op: 派单量比较符,gte(>=) / lte(<=)(可选)
|
||
order_count_value: 派单量数值(可选,需与order_count_op同时存在)
|
||
status: 商家状态,1=正常,2=禁用(可选)
|
||
page: 页码,从1开始,默认1
|
||
page_size: 每页数量,默认20
|
||
返回:
|
||
code: 0 成功,其他失败
|
||
data: {
|
||
list: [商家对象],
|
||
total: int, // 总商家数
|
||
}
|
||
"""
|
||
permission_classes = [] # 不依赖全局认证,手动校验
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
username = request.data.get('username', '').strip()
|
||
keyword = request.data.get('keyword', '').strip()
|
||
balance_op = request.data.get('balance_op', '').strip().lower()
|
||
balance_value_str = request.data.get('balance_value')
|
||
order_count_op = request.data.get('order_count_op', '').strip().lower()
|
||
order_count_value_str = request.data.get('order_count_value')
|
||
status_str = request.data.get('status')
|
||
|
||
try:
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 20))
|
||
if page < 1 or page_size < 1:
|
||
raise ValueError
|
||
except ValueError:
|
||
return Response({'code': 400, 'msg': '分页参数错误'})
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '认证失败,缺少username'})
|
||
|
||
# 2. 公共权限校验(复用现有函数 verify_kefu_permission)
|
||
# 假设 verify_kefu_permission 定义在某个公共模块,这里引入
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 直接返回错误响应
|
||
|
||
# 3. 检查商家管理权限(1100a 或 1100b)
|
||
required_perms = {'1100a', '1100b'}
|
||
if not any(p in permissions for p in required_perms):
|
||
return Response({'code': 403, 'msg': '您没有权限查看商家列表'})
|
||
|
||
# 4. 构建商家查询集(商家扩展表 + 关联用户主表)
|
||
shangjia_qs = UserShangjia.objects.select_related('user').all()
|
||
|
||
# 关键词筛选(用户ID或昵称)
|
||
if keyword:
|
||
shangjia_qs = shangjia_qs.filter(
|
||
Q(user__yonghuid__icontains=keyword) |
|
||
Q(nicheng__icontains=keyword)
|
||
)
|
||
|
||
# 余额筛选
|
||
if balance_op and balance_value_str is not None and balance_value_str != '':
|
||
try:
|
||
balance_value = Decimal(str(balance_value_str))
|
||
if balance_op == 'gte':
|
||
shangjia_qs = shangjia_qs.filter(yue__gte=balance_value)
|
||
elif balance_op == 'lte':
|
||
shangjia_qs = shangjia_qs.filter(yue__lte=balance_value)
|
||
else:
|
||
return Response({'code': 400, 'msg': '余额比较符只支持gte或lte'})
|
||
except Exception:
|
||
return Response({'code': 400, 'msg': '余额数值格式错误'})
|
||
|
||
# 派单总量筛选(fabu字段)
|
||
if order_count_op and order_count_value_str is not None and order_count_value_str != '':
|
||
try:
|
||
order_count_value = int(order_count_value_str)
|
||
if order_count_op == 'gte':
|
||
shangjia_qs = shangjia_qs.filter(fabu__gte=order_count_value)
|
||
elif order_count_op == 'lte':
|
||
shangjia_qs = shangjia_qs.filter(fabu__lte=order_count_value)
|
||
else:
|
||
return Response({'code': 400, 'msg': '派单量比较符只支持gte或lte'})
|
||
except Exception:
|
||
return Response({'code': 400, 'msg': '派单量数值必须为整数'})
|
||
|
||
# 商家状态筛选
|
||
if status_str is not None and status_str != '':
|
||
try:
|
||
status_val = int(status_str)
|
||
if status_val not in [1, 2]:
|
||
return Response({'code': 400, 'msg': '商家状态值无效,应为1或2'})
|
||
shangjia_qs = shangjia_qs.filter(zhuangtai=status_val)
|
||
except ValueError:
|
||
return Response({'code': 400, 'msg': '商家状态格式错误'})
|
||
|
||
# 5. 统计总商家数(应用筛选后)
|
||
total = shangjia_qs.count()
|
||
|
||
# 6. 分页
|
||
offset = (page - 1) * page_size
|
||
shangjia_list = shangjia_qs.order_by('-create_time')[offset:offset + page_size]
|
||
|
||
# 7. 构建返回数据
|
||
result = []
|
||
for shangjia in shangjia_list:
|
||
user = shangjia.user
|
||
result.append({
|
||
'yonghuid': user.yonghuid,
|
||
'avatar': user.avatar or '',
|
||
'nicheng': shangjia.nicheng or '',
|
||
'yue': str(shangjia.yue), # 转换为字符串,避免前端精度问题
|
||
'fabu': shangjia.fabu,
|
||
'chengjiao': shangjia.chengjiao,
|
||
'zhuangtai': shangjia.zhuangtai, # 1=正常,2=禁用
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': result,
|
||
'total': total,
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# yonghu/views.py 中添加
|
||
|
||
class KefuGetShangjiaDetailView(APIView):
|
||
"""
|
||
客服/管理员获取商家详情接口
|
||
需要权限:1100a 或 1100b
|
||
请求:POST /houtai/hthqsjxq
|
||
参数:
|
||
username: 客服手机号(用于权限校验,必填)
|
||
yonghuid: 商家用户ID(必填)
|
||
返回:
|
||
code: 0 成功
|
||
data: 商家所有字段(含主表头像等)
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
yonghuid = request.data.get('yonghuid', '').strip()
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '认证失败,缺少username'})
|
||
if not yonghuid:
|
||
return Response({'code': 400, 'msg': '缺少商家ID'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
required_perms = {'1100a', '1100b'}
|
||
if not any(p in permissions for p in required_perms):
|
||
return Response({'code': 403, 'msg': '您没有权限查看商家详情'})
|
||
|
||
# 查询商家
|
||
try:
|
||
shangjia = UserShangjia.objects.select_related('user').get(user__yonghuid=yonghuid)
|
||
except UserShangjia.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商家不存在'})
|
||
|
||
user = shangjia.user
|
||
|
||
data = {
|
||
'yonghuid': user.yonghuid,
|
||
'avatar': user.avatar or '',
|
||
'nicheng': shangjia.nicheng,
|
||
'zhuangtai': shangjia.zhuangtai, # 1正常,2禁用
|
||
'dianhua': shangjia.dianhua,
|
||
'wechat': shangjia.wechat,
|
||
'yue': str(shangjia.yue),
|
||
'fabu': shangjia.fabu,
|
||
'chengjiao': shangjia.chengjiao,
|
||
'tuikuan': shangjia.tuikuan,
|
||
'jinridingdan': shangjia.jinridingdan,
|
||
'jinriliushui': str(shangjia.jinriliushui),
|
||
'jinyuedingdan': shangjia.jinyuedingdan,
|
||
'jinyueliushui': str(shangjia.jinyueliushui),
|
||
'create_time': shangjia.create_time.isoformat() if shangjia.create_time else None,
|
||
}
|
||
|
||
return Response({'code': 0, 'data': data})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KefuUpdateShangjiaView(APIView):
|
||
"""
|
||
客服/管理员修改商家信息(余额、状态)
|
||
POST /houtai/xgsjxx
|
||
参数:
|
||
username, yonghuid, action
|
||
action可选:add_balance, sub_balance, ban, unban
|
||
amount(add_balance/sub_balance时必填)
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
yonghuid = request.data.get('yonghuid', '').strip()
|
||
action = request.data.get('action', '').strip().lower()
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
if not yonghuid:
|
||
return Response({'code': 400, 'msg': '缺少商家ID'})
|
||
if action not in ('add_balance', 'sub_balance', 'ban', 'unban'):
|
||
return Response({'code': 400, 'msg': 'action参数无效'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 操作权限校验
|
||
if action in ('add_balance', 'ban', 'unban') and '1100a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限执行此操作(需要1100a)'})
|
||
if action == 'sub_balance' and '1100b' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限减少商家余额(需要1100b)'})
|
||
|
||
# 使用事务,select_for_update 必须在事务内
|
||
with transaction.atomic():
|
||
try:
|
||
shangjia = UserShangjia.objects.select_for_update().get(user__yonghuid=yonghuid)
|
||
except UserShangjia.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商家不存在'})
|
||
|
||
# 增加余额
|
||
if action == 'add_balance':
|
||
amount_str = request.data.get('amount')
|
||
try:
|
||
amount = Decimal(str(amount_str))
|
||
if amount <= 0 or amount > 1000000:
|
||
return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'})
|
||
except:
|
||
return Response({'code': 400, 'msg': '金额格式错误'})
|
||
old_yue = shangjia.yue
|
||
new_yue = old_yue + amount
|
||
if new_yue > 999999999.99:
|
||
return Response({'code': 400, 'msg': '增加后余额超出上限'})
|
||
shangjia.yue = new_yue
|
||
shangjia.save(update_fields=['yue'])
|
||
from yonghu.xiugai_log import write_xiugai_log, LEIXING_SHANGJIA
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
operator_phone=kefu.user.phone,
|
||
leixing=LEIXING_SHANGJIA,
|
||
shangjia_yue_before=old_yue,
|
||
shangjia_yue_after=new_yue,
|
||
qitashuoming=f'商家加余额 {amount},{old_yue} → {new_yue}',
|
||
)
|
||
return Response({'code': 0, 'msg': '余额增加成功', 'data': {'new_balance': str(shangjia.yue)}})
|
||
|
||
# 减少余额
|
||
if action == 'sub_balance':
|
||
amount_str = request.data.get('amount')
|
||
try:
|
||
amount = Decimal(str(amount_str))
|
||
if amount <= 0 or amount > 1000000:
|
||
return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'})
|
||
except:
|
||
return Response({'code': 400, 'msg': '金额格式错误'})
|
||
old_yue = shangjia.yue
|
||
if old_yue < amount:
|
||
return Response({'code': 400, 'msg': '余额不足'})
|
||
new_yue = old_yue - amount
|
||
shangjia.yue = new_yue
|
||
shangjia.save(update_fields=['yue'])
|
||
from yonghu.xiugai_log import write_xiugai_log, LEIXING_SHANGJIA
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
operator_phone=kefu.user.phone,
|
||
leixing=LEIXING_SHANGJIA,
|
||
shangjia_yue_before=old_yue,
|
||
shangjia_yue_after=new_yue,
|
||
qitashuoming=f'商家减余额 {amount},{old_yue} → {new_yue}',
|
||
)
|
||
return Response({'code': 0, 'msg': '余额减少成功', 'data': {'new_balance': str(shangjia.yue)}})
|
||
|
||
# 封禁
|
||
if action == 'ban':
|
||
if shangjia.zhuangtai == 2:
|
||
return Response({'code': 400, 'msg': '商家已是封禁状态'})
|
||
old_zt = shangjia.zhuangtai
|
||
shangjia.zhuangtai = 2
|
||
shangjia.save(update_fields=['zhuangtai'])
|
||
from yonghu.xiugai_log import write_xiugai_log, LEIXING_SHANGJIA
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
operator_phone=kefu.user.phone,
|
||
leixing=LEIXING_SHANGJIA,
|
||
qitashuoming=f'商家封禁,状态 {old_zt} → 2',
|
||
)
|
||
return Response({'code': 0, 'msg': '商家已封禁', 'data': {'zhuangtai': 2}})
|
||
|
||
# 解封
|
||
if action == 'unban':
|
||
if shangjia.zhuangtai == 1:
|
||
return Response({'code': 400, 'msg': '商家已是正常状态'})
|
||
old_zt = shangjia.zhuangtai
|
||
shangjia.zhuangtai = 1
|
||
shangjia.save(update_fields=['zhuangtai'])
|
||
from yonghu.xiugai_log import write_xiugai_log, LEIXING_SHANGJIA
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
operator_phone=kefu.user.phone,
|
||
leixing=LEIXING_SHANGJIA,
|
||
qitashuoming=f'商家解封,状态 {old_zt} → 1',
|
||
)
|
||
return Response({'code': 0, 'msg': '商家已解封', 'data': {'zhuangtai': 1}})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class GetProductBaseDataView(APIView):
|
||
"""
|
||
获取商品基础数据(类型、专区、会员)
|
||
权限:2200a
|
||
POST /houtai/htsphqjcsx
|
||
参数:username
|
||
返回:
|
||
code:0, data: {
|
||
type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...],
|
||
zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...],
|
||
member_list: [ {huiyuan_id, jieshao, jiage}, ...]
|
||
}
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '2200a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限管理商品'})
|
||
|
||
# 获取所有商品类型(按 paixu 升序)
|
||
type_qs = ShangpinLeixing.objects.all().order_by('paixu')
|
||
type_list = []
|
||
for t in type_qs:
|
||
# 统计该类型下的商品数量(忽略审核状态?通常统计所有商品,可根据需求调整)
|
||
product_count = Shangpin.objects.filter(leixing_id=t.id).count()
|
||
type_list.append({
|
||
'id': t.id,
|
||
'jieshao': t.jieshao or '',
|
||
'tupian_url': t.tupian_url or '',
|
||
'paixu': t.paixu,
|
||
'shenhezhuangtai': t.shenhezhuangtai,
|
||
'product_count': product_count,
|
||
})
|
||
|
||
# 获取所有专区(按 paixu 升序)
|
||
zone_qs = ShangpinZhuanqu.objects.all().order_by('paixu')
|
||
zone_list = []
|
||
for z in zone_qs:
|
||
product_count = Shangpin.objects.filter(zhuanqu_id=z.id).count()
|
||
zone_list.append({
|
||
'id': z.id,
|
||
'mingzi': z.mingzi or '',
|
||
'leixing_id': z.leixing_id,
|
||
'paixu': z.paixu,
|
||
'shenhezhuangtai': z.shenhezhuangtai,
|
||
'product_count': product_count,
|
||
})
|
||
|
||
# 获取所有会员(只需 ID、介绍、价格)
|
||
member_list = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage'))
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'type_list': type_list,
|
||
'zone_list': zone_list,
|
||
'member_list': member_list,
|
||
}
|
||
})
|
||
|
||
|
||
class GetProductListView(APIView):
|
||
"""
|
||
获取商品列表(支持筛选、排序)
|
||
权限:2200a
|
||
POST /houtai/hthqsplb
|
||
参数:
|
||
username: 必填
|
||
leixing_id: 类型ID(可选)
|
||
zhuanqu_id: 专区ID(可选)
|
||
keyword: 搜索关键词(商品标题/介绍)
|
||
sales_op: 销量比较符 gte/lte
|
||
sales_value: 销量数值
|
||
stock_op: 库存比较符 gte/lte
|
||
stock_value: 库存数值
|
||
sort_field: 排序字段 zhenshi_xiaoliang / jiage
|
||
sort_order: asc/desc
|
||
page: 页码(默认1)
|
||
page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留)
|
||
返回:
|
||
code:0, data: { list: [...], total }
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '2200a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限管理商品'})
|
||
|
||
# 获取参数
|
||
leixing_id = request.data.get('leixing_id')
|
||
zhuanqu_id = request.data.get('zhuanqu_id')
|
||
keyword = request.data.get('keyword', '').strip()
|
||
sales_op = request.data.get('sales_op', '').strip().lower()
|
||
sales_value = request.data.get('sales_value')
|
||
stock_op = request.data.get('stock_op', '').strip().lower()
|
||
stock_value = request.data.get('stock_value')
|
||
sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage
|
||
sort_order = request.data.get('sort_order', 'desc').lower()
|
||
try:
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 1000))
|
||
if page < 1 or page_size < 1:
|
||
raise ValueError
|
||
except:
|
||
return Response({'code': 400, 'msg': '分页参数错误'})
|
||
|
||
# 构建查询集
|
||
qs = Shangpin.objects.all()
|
||
|
||
if leixing_id:
|
||
try:
|
||
leixing_id = int(leixing_id)
|
||
qs = qs.filter(leixing_id=leixing_id)
|
||
except:
|
||
pass
|
||
if zhuanqu_id:
|
||
try:
|
||
zhuanqu_id = int(zhuanqu_id)
|
||
qs = qs.filter(zhuanqu_id=zhuanqu_id)
|
||
except:
|
||
pass
|
||
if keyword:
|
||
qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword))
|
||
|
||
# 销量筛选
|
||
if sales_op and sales_value is not None:
|
||
try:
|
||
sales_val = int(sales_value)
|
||
if sales_op == 'gte':
|
||
qs = qs.filter(zhenshi_xiaoliang__gte=sales_val)
|
||
elif sales_op == 'lte':
|
||
qs = qs.filter(zhenshi_xiaoliang__lte=sales_val)
|
||
except:
|
||
pass
|
||
|
||
# 库存筛选
|
||
if stock_op and stock_value is not None:
|
||
try:
|
||
stock_val = int(stock_value)
|
||
if stock_op == 'gte':
|
||
qs = qs.filter(kucun__gte=stock_val)
|
||
elif stock_op == 'lte':
|
||
qs = qs.filter(kucun__lte=stock_val)
|
||
except:
|
||
pass
|
||
|
||
# 排序
|
||
if sort_field in ('zhenshi_xiaoliang', 'jiage'):
|
||
if sort_order == 'asc':
|
||
qs = qs.order_by(sort_field)
|
||
else:
|
||
qs = qs.order_by(f'-{sort_field}')
|
||
else:
|
||
qs = qs.order_by('-create_time') # 默认按创建时间倒序
|
||
|
||
# 分页
|
||
total = qs.count()
|
||
offset = (page - 1) * page_size
|
||
products = qs[offset:offset + page_size]
|
||
|
||
# 构建返回数据
|
||
product_list = []
|
||
for p in products:
|
||
product_list.append({
|
||
'id': p.id,
|
||
'biaoqian': p.biaoqian or '',
|
||
'jiage': str(p.jiage),
|
||
'kucun': p.kucun,
|
||
'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
|
||
'tupian_url': p.tupian_url or '',
|
||
'jieshao': p.jieshao or '',
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': product_list,
|
||
'total': total,
|
||
}
|
||
})
|
||
|
||
|
||
class SaveProductOrderView(APIView):
|
||
"""
|
||
保存商品类型或专区的排序
|
||
权限:2200a
|
||
POST /houtai/htsppaixu
|
||
参数:
|
||
username: 必填
|
||
type: 'leixing' 或 'zhuanqu'
|
||
leixing_id: 当 type='zhuanqu' 时必填(用于限定该类型下的专区)
|
||
items: [ {id, paixu}, ... ] # 每个项目的新排序值(从1开始)
|
||
返回:
|
||
code:0 成功
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '2200a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限管理商品'})
|
||
|
||
sort_type = request.data.get('type', '').strip().lower()
|
||
if sort_type not in ('leixing', 'zhuanqu'):
|
||
return Response({'code': 400, 'msg': 'type参数必须为leixing或zhuanqu'})
|
||
|
||
items = request.data.get('items')
|
||
if not isinstance(items, list) or len(items) == 0:
|
||
return Response({'code': 400, 'msg': 'items参数无效'})
|
||
|
||
# 校验每个 item 的格式
|
||
for item in items:
|
||
if not isinstance(item, dict) or 'id' not in item or 'paixu' not in item:
|
||
return Response({'code': 400, 'msg': 'items内每项需包含id和paixu'})
|
||
try:
|
||
item['id'] = int(item['id'])
|
||
item['paixu'] = int(item['paixu'])
|
||
if item['paixu'] < 1:
|
||
return Response({'code': 400, 'msg': 'paixu必须为正整数'})
|
||
except:
|
||
return Response({'code': 400, 'msg': 'id或paixu格式错误'})
|
||
|
||
with transaction.atomic():
|
||
if sort_type == 'leixing':
|
||
# 更新商品类型的排序
|
||
for item in items:
|
||
ShangpinLeixing.objects.filter(id=item['id']).update(paixu=item['paixu'])
|
||
else: # zhuanqu
|
||
leixing_id = request.data.get('leixing_id')
|
||
if not leixing_id:
|
||
return Response({'code': 400, 'msg': '更新专区排序时必须提供leixing_id'})
|
||
try:
|
||
leixing_id = int(leixing_id)
|
||
except:
|
||
return Response({'code': 400, 'msg': 'leixing_id格式错误'})
|
||
# 可选:检查该专区是否属于该类型,但不强制,由前端保证
|
||
for item in items:
|
||
ShangpinZhuanqu.objects.filter(id=item['id']).update(paixu=item['paixu'])
|
||
|
||
return Response({'code': 0, 'msg': '排序保存成功'})
|
||
|
||
|
||
class AddProductView(APIView):
|
||
"""
|
||
添加商品(含图片上传)
|
||
权限:2200a
|
||
POST /houtai/htfbsp
|
||
参数:multipart/form-data,包含所有商品字段及图片文件
|
||
字段说明见代码
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [MultiPartParser, FormParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '2200a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限管理商品'})
|
||
|
||
# 获取表单字段
|
||
biaoqian = request.data.get('biaoqian', '').strip()
|
||
jiage_str = request.data.get('jiage')
|
||
kucun_str = request.data.get('kucun')
|
||
duiwai_xiaoliang_str = request.data.get('duiwai_xiaoliang', '0')
|
||
leixing_id_str = request.data.get('leixing_id')
|
||
zhuanqu_id_str = request.data.get('zhuanqu_id')
|
||
yaoqiuleixing_str = request.data.get('yaoqiuleixing', '1')
|
||
huiyuan_id = request.data.get('huiyuan_id', '').strip()
|
||
yongjin_str = request.data.get('yongjin', '0')
|
||
fencheng_type = request.data.get('fencheng_type', 'default')
|
||
ewai_dashou_fencheng_str = request.data.get('ewai_dashou_fencheng', '0')
|
||
jieshao = request.data.get('jieshao', '').strip()
|
||
xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip()
|
||
# 图片文件
|
||
image_file = request.FILES.get('file') # 前端上传时字段名为 'file'
|
||
|
||
# 必填字段校验
|
||
if not biaoqian:
|
||
return Response({'code': 400, 'msg': '商品标题不能为空'})
|
||
try:
|
||
jiage = Decimal(jiage_str)
|
||
if jiage <= 0:
|
||
return Response({'code': 400, 'msg': '价格必须大于0'})
|
||
except:
|
||
return Response({'code': 400, 'msg': '价格格式错误'})
|
||
try:
|
||
kucun = int(kucun_str)
|
||
if kucun < 0:
|
||
return Response({'code': 400, 'msg': '库存不能为负数'})
|
||
except:
|
||
return Response({'code': 400, 'msg': '库存格式错误'})
|
||
try:
|
||
duiwai_xiaoliang = int(duiwai_xiaoliang_str)
|
||
except:
|
||
duiwai_xiaoliang = 0
|
||
try:
|
||
leixing_id = int(leixing_id_str)
|
||
except:
|
||
return Response({'code': 400, 'msg': '商品类型ID无效'})
|
||
try:
|
||
zhuanqu_id = int(zhuanqu_id_str)
|
||
except:
|
||
return Response({'code': 400, 'msg': '商品专区ID无效'})
|
||
try:
|
||
yaoqiuleixing = int(yaoqiuleixing_str)
|
||
if yaoqiuleixing not in (1, 2):
|
||
raise ValueError
|
||
except:
|
||
return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'})
|
||
|
||
# 校验类型和专区的存在性及关联性
|
||
try:
|
||
leixing_obj = ShangpinLeixing.objects.get(id=leixing_id)
|
||
except ShangpinLeixing.DoesNotExist:
|
||
return Response({'code': 400, 'msg': '商品类型不存在'})
|
||
try:
|
||
zhuanqu_obj = ShangpinZhuanqu.objects.get(id=zhuanqu_id)
|
||
if zhuanqu_obj.leixing_id != leixing_id:
|
||
return Response({'code': 400, 'msg': '专区不属于该商品类型'})
|
||
except ShangpinZhuanqu.DoesNotExist:
|
||
return Response({'code': 400, 'msg': '商品专区不存在'})
|
||
|
||
# 处理抢单要求相关字段
|
||
huiyuan_id_val = None
|
||
yongjin_val = None
|
||
if yaoqiuleixing == 1:
|
||
# 会员要求
|
||
if not huiyuan_id:
|
||
# 未传递时,尝试从商品类型中获取默认会员ID
|
||
huiyuan_id_val = leixing_obj.huiyuan_id
|
||
if not huiyuan_id_val:
|
||
return Response({'code': 400, 'msg': '未指定会员且商品类型无默认会员'})
|
||
else:
|
||
huiyuan_id_val = huiyuan_id
|
||
# 验证会员是否存在
|
||
if not Huiyuan.objects.filter(huiyuan_id=huiyuan_id_val).exists():
|
||
return Response({'code': 400, 'msg': '指定的会员不存在'})
|
||
else:
|
||
# 佣金要求
|
||
try:
|
||
yongjin_val = Decimal(yongjin_str)
|
||
if yongjin_val < 0:
|
||
return Response({'code': 400, 'msg': '佣金不能为负数'})
|
||
except:
|
||
# 若未传递或无效,尝试从商品类型中获取默认佣金
|
||
if leixing_obj.yongjin is not None:
|
||
yongjin_val = leixing_obj.yongjin
|
||
else:
|
||
return Response({'code': 400, 'msg': '佣金格式错误且类型无默认佣金'})
|
||
|
||
# 处理打手分成方式
|
||
kaioi_ewai = False
|
||
ewai_fencheng = Decimal('0')
|
||
if fencheng_type == 'custom':
|
||
try:
|
||
ewai_fencheng = Decimal(ewai_dashou_fencheng_str)
|
||
if ewai_fencheng < 0:
|
||
return Response({'code': 400, 'msg': '自定义分成金额不能为负数'})
|
||
kaioi_ewai = True
|
||
except:
|
||
return Response({'code': 400, 'msg': '自定义分成金额格式错误'})
|
||
|
||
# 图片上传处理
|
||
tupian_url = ''
|
||
if image_file:
|
||
# 验证图片
|
||
valid, msg = validate_image(image_file)
|
||
if not valid:
|
||
return Response({'code': 400, 'msg': f'图片无效:{msg}'})
|
||
# 生成唯一文件名
|
||
ext = image_file.name.split('.')[-1].lower()
|
||
if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']:
|
||
return Response({'code': 400, 'msg': '不支持的图片格式'})
|
||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
||
random_str = uuid.uuid4().hex[:8]
|
||
filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}"
|
||
# 上传到 OSS
|
||
uploaded_url = upload_to_oss(image_file, filename)
|
||
if not uploaded_url:
|
||
return Response({'code': 500, 'msg': '图片上传失败,请稍后重试'})
|
||
tupian_url = filename # 存储相对路径,前端拼接域名
|
||
else:
|
||
return Response({'code': 400, 'msg': '商品图片不能为空'})
|
||
|
||
# 创建商品
|
||
try:
|
||
with transaction.atomic():
|
||
product = Shangpin.objects.create(
|
||
biaoqian=biaoqian,
|
||
jiage=jiage,
|
||
kucun=kucun,
|
||
leixing_id=leixing_id,
|
||
zhuanqu_id=zhuanqu_id,
|
||
zhenshi_xiaoliang=0, # 初始真实销量为0
|
||
duiwai_xiaoliang=duiwai_xiaoliang,
|
||
jieshao=jieshao,
|
||
xiadan_xuzhi=xiadan_xuzhi,
|
||
tupian_url=tupian_url,
|
||
yaoqiuleixing=yaoqiuleixing,
|
||
huiyuan_id=huiyuan_id_val,
|
||
yongjin=yongjin_val,
|
||
kaioi_ewai_dashou_fencheng=kaioi_ewai,
|
||
ewai_dashou_fencheng=ewai_fencheng,
|
||
paixu=0, # 新商品默认排序最后
|
||
shenhezhuangtai=1, # 正常状态
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"创建商品失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '创建商品失败,请稍后重试'})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '商品发布成功',
|
||
'data': {
|
||
'id': product.id,
|
||
'tupian_url': tupian_url,
|
||
}
|
||
})
|
||
|
||
|
||
# houtai/views_product.py 中添加
|
||
|
||
class DeleteProductView(APIView):
|
||
"""
|
||
删除商品(物理删除,同时删除OSS图片)
|
||
权限:2200a
|
||
POST /houtai/htscsp
|
||
参数:
|
||
username: 客服/管理员账号
|
||
id: 商品ID
|
||
返回:
|
||
code:0 成功
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
product_id = request.data.get('id')
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
if not product_id:
|
||
return Response({'code': 400, 'msg': '缺少商品ID'})
|
||
try:
|
||
product_id = int(product_id)
|
||
except:
|
||
return Response({'code': 400, 'msg': '商品ID格式错误'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '2200a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限管理商品'})
|
||
|
||
# 查询商品是否存在
|
||
try:
|
||
product = Shangpin.objects.get(id=product_id)
|
||
except Shangpin.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商品不存在'})
|
||
|
||
# 先删除OSS上的图片
|
||
tupian_url = product.tupian_url
|
||
if tupian_url:
|
||
# 调用已有的 delete_from_oss 函数
|
||
from utils.oss_utils import delete_from_oss
|
||
delete_success = delete_from_oss(tupian_url)
|
||
if not delete_success:
|
||
logger.warning(f"删除商品图片失败: {tupian_url}, 商品ID: {product_id}")
|
||
# 注意:即使图片删除失败,也应继续删除数据库记录,避免数据不一致
|
||
# 但可以记录日志供后续人工处理
|
||
|
||
# 删除数据库记录
|
||
product.delete()
|
||
|
||
logger.info(f"商品删除成功: id={product_id}, 图片路径={tupian_url}, 操作人={username}")
|
||
return Response({'code': 0, 'msg': '商品删除成功'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class GetProductDetailView(APIView):
|
||
"""
|
||
获取商品详情(包含商品类型、专区、会员列表)
|
||
权限:2200a
|
||
POST /houtai/htsphqxq
|
||
参数:
|
||
username: 客服/管理员账号
|
||
id: 商品ID
|
||
返回:
|
||
code:0, data: { product, type_list, zone_list, member_list }
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
product_id = request.data.get('id')
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
if not product_id:
|
||
return Response({'code': 400, 'msg': '缺少商品ID'})
|
||
try:
|
||
product_id = int(product_id)
|
||
except ValueError:
|
||
return Response({'code': 400, 'msg': '商品ID格式错误'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '2200a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限管理商品'})
|
||
|
||
# 查询商品
|
||
try:
|
||
product = Shangpin.objects.get(id=product_id)
|
||
except Shangpin.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商品不存在'})
|
||
|
||
# 查询所有商品类型、专区、会员(用于前端下拉选择)
|
||
type_list = list(ShangpinLeixing.objects.all().values(
|
||
'id', 'jieshao', 'tupian_url', 'shenhezhuangtai'
|
||
))
|
||
zone_list = list(ShangpinZhuanqu.objects.all().values(
|
||
'id', 'mingzi', 'leixing_id', 'shenhezhuangtai'
|
||
))
|
||
member_list = list(Huiyuan.objects.all().values(
|
||
'huiyuan_id', 'jieshao', 'jiage'
|
||
))
|
||
|
||
# 构建商品返回数据
|
||
product_data = {
|
||
'id': product.id,
|
||
'biaoqian': product.biaoqian,
|
||
'jiage': str(product.jiage),
|
||
'kucun': product.kucun,
|
||
'leixing_id': product.leixing_id,
|
||
'zhuanqu_id': product.zhuanqu_id,
|
||
'zhenshi_xiaoliang': product.zhenshi_xiaoliang,
|
||
'duiwai_xiaoliang': product.duiwai_xiaoliang,
|
||
'jieshao': product.jieshao,
|
||
'xiadan_xuzhi': product.xiadan_xuzhi,
|
||
'guize_tupian': product.guize_tupian,
|
||
'yaoqiuleixing': product.yaoqiuleixing,
|
||
'huiyuan_id': product.huiyuan_id,
|
||
'yongjin': str(product.yongjin) if product.yongjin is not None else None,
|
||
'tupian_url': product.tupian_url,
|
||
'kaioi_ewai_dashou_fencheng': product.kaioi_ewai_dashou_fencheng,
|
||
'ewai_dashou_fencheng': str(product.ewai_dashou_fencheng) if product.ewai_dashou_fencheng is not None else '0',
|
||
'create_time': product.create_time.isoformat() if product.create_time else None,
|
||
'shenhezhuangtai': product.shenhezhuangtai,
|
||
}
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'product': product_data,
|
||
'type_list': type_list,
|
||
'zone_list': zone_list,
|
||
'member_list': member_list,
|
||
}
|
||
})
|
||
|
||
|
||
|
||
class UpdateProductView(APIView):
|
||
"""
|
||
修改商品(支持商品图片和规则图片的更新)
|
||
权限:2200a
|
||
POST /houtai/bcspxg
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [MultiPartParser, FormParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
product_id = request.data.get('id')
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
if not product_id:
|
||
return Response({'code': 400, 'msg': '缺少商品ID'})
|
||
try:
|
||
product_id = int(product_id)
|
||
except ValueError:
|
||
return Response({'code': 400, 'msg': '商品ID格式错误'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '2200a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限管理商品'})
|
||
|
||
# 使用事务,将 select_for_update 放在事务内
|
||
with transaction.atomic():
|
||
try:
|
||
product = Shangpin.objects.select_for_update().get(id=product_id)
|
||
except Shangpin.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商品不存在'})
|
||
|
||
# ---------- 1. 基础字段 ----------
|
||
biaoqian = request.data.get('biaoqian', '').strip()
|
||
if not biaoqian:
|
||
return Response({'code': 400, 'msg': '商品标题不能为空'})
|
||
|
||
try:
|
||
jiage = Decimal(str(request.data.get('jiage', 0)))
|
||
if jiage <= 0:
|
||
return Response({'code': 400, 'msg': '价格必须大于0'})
|
||
except:
|
||
return Response({'code': 400, 'msg': '价格格式错误'})
|
||
|
||
try:
|
||
kucun = int(request.data.get('kucun', 0))
|
||
if kucun < 0:
|
||
return Response({'code': 400, 'msg': '库存不能为负数'})
|
||
except:
|
||
return Response({'code': 400, 'msg': '库存格式错误'})
|
||
|
||
try:
|
||
duiwai_xiaoliang = int(request.data.get('duiwai_xiaoliang', 0))
|
||
except:
|
||
duiwai_xiaoliang = 0
|
||
|
||
try:
|
||
leixing_id = int(request.data.get('leixing_id'))
|
||
except:
|
||
return Response({'code': 400, 'msg': '商品类型ID无效'})
|
||
|
||
try:
|
||
zhuanqu_id = int(request.data.get('zhuanqu_id'))
|
||
except:
|
||
return Response({'code': 400, 'msg': '商品专区ID无效'})
|
||
|
||
try:
|
||
yaoqiuleixing = int(request.data.get('yaoqiuleixing', 1))
|
||
if yaoqiuleixing not in (1, 2):
|
||
raise ValueError
|
||
except:
|
||
return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'})
|
||
|
||
try:
|
||
shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1))
|
||
except:
|
||
shenhezhuangtai = 1
|
||
|
||
# 校验类型和专区存在性
|
||
if not ShangpinLeixing.objects.filter(id=leixing_id).exists():
|
||
return Response({'code': 400, 'msg': '商品类型不存在'})
|
||
if not ShangpinZhuanqu.objects.filter(id=zhuanqu_id, leixing_id=leixing_id).exists():
|
||
return Response({'code': 400, 'msg': '专区不存在或不属于该类型'})
|
||
|
||
# ---------- 2. 会员/佣金 ----------
|
||
huiyuan_id = None
|
||
yongjin = None
|
||
if yaoqiuleixing == 1:
|
||
huiyuan_id = request.data.get('huiyuan_id', '').strip() or None
|
||
if huiyuan_id and not Huiyuan.objects.filter(huiyuan_id=huiyuan_id).exists():
|
||
return Response({'code': 400, 'msg': '指定的会员不存在'})
|
||
else:
|
||
try:
|
||
yongjin = Decimal(str(request.data.get('yongjin', 0)))
|
||
if yongjin < 0:
|
||
return Response({'code': 400, 'msg': '佣金不能为负数'})
|
||
except:
|
||
yongjin = Decimal('0')
|
||
|
||
# ---------- 3. 打手分成 ----------
|
||
kaioi_ewai = request.data.get('kaioi_ewai_dashou_fencheng') == 'true'
|
||
ewai_fencheng = Decimal('0')
|
||
if kaioi_ewai:
|
||
try:
|
||
ewai_fencheng = Decimal(str(request.data.get('ewai_dashou_fencheng', 0)))
|
||
if ewai_fencheng < 0:
|
||
return Response({'code': 400, 'msg': '分成金额不能为负数'})
|
||
except:
|
||
return Response({'code': 400, 'msg': '分成金额格式错误'})
|
||
|
||
jieshao = request.data.get('jieshao', '').strip()
|
||
xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip()
|
||
|
||
# ---------- 4. 商品图片更新 ----------
|
||
tupian_url = product.tupian_url
|
||
tupian_file = request.FILES.get('tupian_file')
|
||
if tupian_file:
|
||
valid, msg = validate_image(tupian_file)
|
||
if not valid:
|
||
return Response({'code': 400, 'msg': f'商品图片无效:{msg}'})
|
||
ext = tupian_file.name.split('.')[-1].lower()
|
||
if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']:
|
||
return Response({'code': 400, 'msg': '不支持的图片格式'})
|
||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
||
random_str = uuid.uuid4().hex[:8]
|
||
new_filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}"
|
||
uploaded_url = upload_to_oss(tupian_file, new_filename)
|
||
if not uploaded_url:
|
||
return Response({'code': 500, 'msg': '商品图片上传失败'})
|
||
if product.tupian_url:
|
||
delete_from_oss(product.tupian_url)
|
||
tupian_url = new_filename
|
||
|
||
# ---------- 5. 规则图片更新 ----------
|
||
guize_tupian = product.guize_tupian
|
||
guize_file = request.FILES.get('guize_file')
|
||
if guize_file:
|
||
valid, msg = validate_image(guize_file)
|
||
if not valid:
|
||
return Response({'code': 400, 'msg': f'规则图片无效:{msg}'})
|
||
ext = guize_file.name.split('.')[-1].lower()
|
||
if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']:
|
||
return Response({'code': 400, 'msg': '不支持的图片格式'})
|
||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
||
random_str = uuid.uuid4().hex[:8]
|
||
new_filename = f"shangpin/guizetupian/{timestamp}_{random_str}.{ext}"
|
||
uploaded_url = upload_to_oss(guize_file, new_filename)
|
||
if not uploaded_url:
|
||
return Response({'code': 500, 'msg': '规则图片上传失败'})
|
||
if product.guize_tupian:
|
||
delete_from_oss(product.guize_tupian)
|
||
guize_tupian = new_filename
|
||
|
||
# ---------- 6. 更新数据库 ----------
|
||
product.biaoqian = biaoqian
|
||
product.jiage = jiage
|
||
product.kucun = kucun
|
||
product.duiwai_xiaoliang = duiwai_xiaoliang
|
||
product.leixing_id = leixing_id
|
||
product.zhuanqu_id = zhuanqu_id
|
||
product.yaoqiuleixing = yaoqiuleixing
|
||
product.huiyuan_id = huiyuan_id
|
||
product.yongjin = yongjin
|
||
product.kaioi_ewai_dashou_fencheng = kaioi_ewai
|
||
product.ewai_dashou_fencheng = ewai_fencheng
|
||
product.jieshao = jieshao
|
||
product.xiadan_xuzhi = xiadan_xuzhi
|
||
product.tupian_url = tupian_url
|
||
product.guize_tupian = guize_tupian
|
||
product.shenhezhuangtai = shenhezhuangtai
|
||
product.save()
|
||
|
||
return Response({'code': 0, 'msg': '商品修改成功', 'data': {'id': product.id}})
|
||
|
||
|
||
|
||
|
||
|
||
class GetMemberListView(APIView):
|
||
"""
|
||
获取会员列表
|
||
权限:3300a
|
||
POST /houtai/hthqhylb
|
||
参数:username
|
||
返回:code:0, data: { list: [...] }
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '3300a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限查看会员列表'})
|
||
|
||
members = Huiyuan.objects.all().order_by('-create_time')
|
||
member_list = []
|
||
for m in members:
|
||
member_list.append({
|
||
'huiyuan_id': m.huiyuan_id,
|
||
'jieshao': m.jieshao,
|
||
'jtjieshao': m.jtjieshao,
|
||
'jiage': str(m.jiage),
|
||
'guanshifc': str(m.guanshifc),
|
||
'zuzhangfc': str(m.zuzhangfc),
|
||
'goumai_cishu': m.goumai_cishu,
|
||
'create_time': m.create_time.isoformat() if m.create_time else None,
|
||
'update_time': m.update_time.isoformat() if m.update_time else None,
|
||
})
|
||
return Response({'code': 0, 'data': {'list': member_list}})
|
||
|
||
|
||
class UpdateMemberView(APIView):
|
||
"""
|
||
修改会员信息
|
||
权限:3300a
|
||
POST /houtai/htxghyxx
|
||
参数:
|
||
username, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
|
||
返回:code:0 成功
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
huiyuan_id = request.data.get('huiyuan_id', '').strip()
|
||
jieshao = request.data.get('jieshao', '').strip()
|
||
jiage = request.data.get('jiage')
|
||
guanshifc = request.data.get('guanshifc')
|
||
zuzhangfc = request.data.get('zuzhangfc')
|
||
jtjieshao = request.data.get('jtjieshao', '').strip()
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
if not huiyuan_id:
|
||
return Response({'code': 400, 'msg': '缺少会员ID'})
|
||
if not jieshao:
|
||
return Response({'code': 400, 'msg': '会员名称不能为空'})
|
||
|
||
try:
|
||
jiage = Decimal(str(jiage))
|
||
guanshifc = Decimal(str(guanshifc))
|
||
zuzhangfc = Decimal(str(zuzhangfc))
|
||
if jiage < 0 or guanshifc < 0 or zuzhangfc < 0:
|
||
raise ValueError
|
||
except:
|
||
return Response({'code': 400, 'msg': '金额格式错误'})
|
||
|
||
if guanshifc + zuzhangfc > jiage:
|
||
return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '3300a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限修改会员'})
|
||
|
||
with transaction.atomic():
|
||
try:
|
||
member = Huiyuan.objects.select_for_update().get(huiyuan_id=huiyuan_id)
|
||
except Huiyuan.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '会员不存在'})
|
||
|
||
member.jieshao = jieshao
|
||
member.jiage = jiage
|
||
member.guanshifc = guanshifc
|
||
member.zuzhangfc = zuzhangfc
|
||
member.jtjieshao = jtjieshao
|
||
member.save()
|
||
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
|
||
|
||
|
||
|
||
import random
|
||
import string
|
||
|
||
class AddMemberView(APIView):
|
||
"""
|
||
添加会员
|
||
权限:3300a
|
||
POST /houtai/httjhy
|
||
参数:
|
||
username, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
|
||
返回:code:0, data: { huiyuan_id }
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
jieshao = request.data.get('jieshao', '').strip()
|
||
jiage = request.data.get('jiage')
|
||
guanshifc = request.data.get('guanshifc')
|
||
zuzhangfc = request.data.get('zuzhangfc')
|
||
jtjieshao = request.data.get('jtjieshao', '').strip()
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
if not jieshao:
|
||
return Response({'code': 400, 'msg': '会员名称不能为空'})
|
||
|
||
try:
|
||
jiage = Decimal(str(jiage))
|
||
guanshifc = Decimal(str(guanshifc))
|
||
zuzhangfc = Decimal(str(zuzhangfc))
|
||
if jiage <= 0 or guanshifc < 0 or zuzhangfc < 0:
|
||
raise ValueError
|
||
except:
|
||
return Response({'code': 400, 'msg': '金额格式错误'})
|
||
|
||
if guanshifc + zuzhangfc > jiage:
|
||
return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '3300a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限添加会员'})
|
||
|
||
# 生成唯一6位会员ID(数字+字母)
|
||
def generate_huiyuan_id():
|
||
while True:
|
||
new_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
|
||
if not Huiyuan.objects.filter(huiyuan_id=new_id).exists():
|
||
return new_id
|
||
|
||
with transaction.atomic():
|
||
new_id = generate_huiyuan_id()
|
||
member = Huiyuan.objects.create(
|
||
huiyuan_id=new_id,
|
||
jieshao=jieshao,
|
||
jiage=jiage,
|
||
guanshifc=guanshifc,
|
||
zuzhangfc=zuzhangfc,
|
||
jtjieshao=jtjieshao,
|
||
goumai_cishu=0
|
||
)
|
||
|
||
return Response({'code': 0, 'msg': '添加成功', 'data': {'huiyuan_id': member.huiyuan_id}})
|
||
|
||
|
||
|
||
|
||
|
||
class GetGuanliListView(APIView):
|
||
"""
|
||
获取管事列表
|
||
权限:4400a,4400b,4400c,4400d,4400e,4400f 任一
|
||
POST /houtai/hthqgslb
|
||
参数:
|
||
username, keyword(可选), balance_op/balance_value, commission_op/commission_value,
|
||
invite_count_op/invite_count_value, status, page, page_size
|
||
返回:
|
||
code:0, data: { list: [...], total }
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
|
||
# 权限校验(需要任一权限)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
|
||
if not any(p in permissions for p in required_perms):
|
||
return Response({'code': 403, 'msg': '您没有权限查看管事列表'})
|
||
|
||
# 分页参数
|
||
try:
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 20))
|
||
if page < 1 or page_size < 1:
|
||
raise ValueError
|
||
except:
|
||
return Response({'code': 400, 'msg': '分页参数错误'})
|
||
|
||
# 筛选参数
|
||
keyword = request.data.get('keyword', '').strip()
|
||
balance_op = request.data.get('balance_op', '').strip().lower()
|
||
balance_value = request.data.get('balance_value')
|
||
commission_op = request.data.get('commission_op', '').strip().lower()
|
||
commission_value = request.data.get('commission_value')
|
||
invite_count_op = request.data.get('invite_count_op', '').strip().lower()
|
||
invite_count_value = request.data.get('invite_count_value')
|
||
status = request.data.get('status')
|
||
|
||
# 构建查询:从 UserMain 开始,关联 UserGuanshi 和 UserBoss(左连接获取昵称)
|
||
qs = UserMain.objects.filter(
|
||
guanshi_profile__isnull=False # 确保是管事
|
||
).select_related('guanshi_profile').select_related('boss_profile')
|
||
|
||
# 关键词搜索(用户ID 或 昵称来自 boss_profile.nickname)
|
||
if keyword:
|
||
qs = qs.filter(
|
||
Q(yonghuid__icontains=keyword) |
|
||
Q(boss_profile__nickname__icontains=keyword)
|
||
)
|
||
|
||
# 余额筛选(使用 guanshi_profile.yue)
|
||
if balance_op and balance_value is not None and balance_value != '':
|
||
try:
|
||
balance_val = Decimal(str(balance_value))
|
||
if balance_op == 'gte':
|
||
qs = qs.filter(guanshi_profile__yue__gte=balance_val)
|
||
elif balance_op == 'lte':
|
||
qs = qs.filter(guanshi_profile__yue__lte=balance_val)
|
||
else:
|
||
return Response({'code': 400, 'msg': '余额比较符无效'})
|
||
except:
|
||
return Response({'code': 400, 'msg': '余额数值格式错误'})
|
||
|
||
# 分佣总额筛选(guanshi_profile.chongzhifenrun)
|
||
if commission_op and commission_value is not None and commission_value != '':
|
||
try:
|
||
commission_val = Decimal(str(commission_value))
|
||
if commission_op == 'gte':
|
||
qs = qs.filter(guanshi_profile__chongzhifenrun__gte=commission_val)
|
||
elif commission_op == 'lte':
|
||
qs = qs.filter(guanshi_profile__chongzhifenrun__lte=commission_val)
|
||
else:
|
||
return Response({'code': 400, 'msg': '分佣比较符无效'})
|
||
except:
|
||
return Response({'code': 400, 'msg': '分佣数值格式错误'})
|
||
|
||
# 邀请打手总数筛选(guanshi_profile.yaogingshuliang)
|
||
if invite_count_op and invite_count_value is not None and invite_count_value != '':
|
||
try:
|
||
invite_val = int(invite_count_value)
|
||
if invite_count_op == 'gte':
|
||
qs = qs.filter(guanshi_profile__yaogingshuliang__gte=invite_val)
|
||
elif invite_count_op == 'lte':
|
||
qs = qs.filter(guanshi_profile__yaogingshuliang__lte=invite_val)
|
||
else:
|
||
return Response({'code': 400, 'msg': '邀请数量比较符无效'})
|
||
except:
|
||
return Response({'code': 400, 'msg': '邀请数量格式错误'})
|
||
|
||
# 状态筛选
|
||
if status is not None and status != '':
|
||
try:
|
||
status_val = int(status)
|
||
if status_val not in (0, 1):
|
||
raise ValueError
|
||
qs = qs.filter(guanshi_profile__zhuangtai=status_val)
|
||
except:
|
||
return Response({'code': 400, 'msg': '状态值无效'})
|
||
|
||
# 计算总数
|
||
total = qs.count()
|
||
|
||
# 分页
|
||
offset = (page - 1) * page_size
|
||
users = qs.order_by('-create_time')[offset:offset + page_size]
|
||
|
||
# 构建返回数据
|
||
result = []
|
||
for user in users:
|
||
guanshi = user.guanshi_profile
|
||
boss = user.boss_profile if hasattr(user, 'boss_profile') else None
|
||
result.append({
|
||
'yonghuid': user.yonghuid,
|
||
'avatar': user.avatar or '',
|
||
'nickname': boss.nickname if boss else '',
|
||
'zhuangtai': guanshi.zhuangtai,
|
||
'yaogingshuliang': guanshi.yaogingshuliang,
|
||
'yue': str(guanshi.yue),
|
||
'chongzhifenrun': str(guanshi.chongzhifenrun),
|
||
})
|
||
|
||
return Response({'code': 0, 'data': {'list': result, 'total': total}})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class GetGuanliDetailView(APIView):
|
||
"""
|
||
获取管事详情
|
||
权限:4400a,4400b,4400c,4400d,4400e,4400f 任一
|
||
POST /houtai/hthqgsxq
|
||
参数:username, yonghuid
|
||
返回:
|
||
code:0, data: {
|
||
base: {...},
|
||
balance: {...},
|
||
permanent_enabled, permanent_amount,
|
||
custom_first_dividend: {},
|
||
extra_dividend_map: {},
|
||
all_members: []
|
||
}
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
yonghuid = request.data.get('yonghuid', '').strip()
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
if not yonghuid:
|
||
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||
|
||
# 权限校验(需要任一权限)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
|
||
if not any(p in permissions for p in required_perms):
|
||
return Response({'code': 403, 'msg': '您没有权限查看管事详情'})
|
||
|
||
# 查询用户主表和管事扩展表
|
||
try:
|
||
user = UserMain.objects.select_related('guanshi_profile', 'boss_profile').get(yonghuid=yonghuid)
|
||
guanshi = user.guanshi_profile
|
||
boss = user.boss_profile if hasattr(user, 'boss_profile') else None
|
||
except UserMain.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '管事不存在'})
|
||
except UserGuanshi.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不是管事'})
|
||
|
||
# 基础信息
|
||
base_data = {
|
||
'yonghuid': user.yonghuid,
|
||
'nickname': boss.nickname if boss else '',
|
||
'avatar': user.avatar or '',
|
||
'dianhua': guanshi.dianhua or '',
|
||
'wechat': guanshi.wechat or '',
|
||
'zhuangtai': guanshi.zhuangtai,
|
||
'yaoqingma': guanshi.yaoqingma,
|
||
'yaoqingren': guanshi.yaoqingren,
|
||
'invite_qrcode_url': guanshi.invite_qrcode_url or '',
|
||
'yaogingshuliang': guanshi.yaogingshuliang,
|
||
'jinrichongzhi': guanshi.jinrichongzhi,
|
||
'jinyuechongzhi': guanshi.jinyuechongzhi,
|
||
'chongzhifenrun': str(guanshi.chongzhifenrun),
|
||
'create_time': guanshi.create_time.isoformat() if guanshi.create_time else None,
|
||
}
|
||
|
||
# 余额提现相关
|
||
balance_data = {
|
||
'yue': str(guanshi.yue),
|
||
'jinritixian_jine': str(guanshi.jinritixian_jine),
|
||
'last_tixian_date': guanshi.last_tixian_date.isoformat() if guanshi.last_tixian_date else None,
|
||
'kaioi_ewai_xiane': guanshi.kaioi_ewai_xiane,
|
||
'ewai_xiane': str(guanshi.ewai_xiane),
|
||
}
|
||
|
||
# 永久分红(二次分红)
|
||
permanent_enabled = guanshi.fenghong_erci_enabled
|
||
permanent_amount = str(guanshi.fenghong_erci) if guanshi.fenghong_erci else '0'
|
||
|
||
# 获取所有会员(用于前端选择)
|
||
all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc'))
|
||
for m in all_members:
|
||
m['jiage'] = str(m['jiage'])
|
||
m['guanshifc'] = str(m['guanshifc'])
|
||
|
||
# 获取多次分红配置(按次数和会员组织)
|
||
# 查询该管事的所有多次分红记录
|
||
duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu')
|
||
custom_first = {} # 首次分红定制(cishu=1)
|
||
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } }
|
||
|
||
for record in duoci_records:
|
||
# 获取会员信息
|
||
member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first()
|
||
if not member:
|
||
continue
|
||
member_info = {
|
||
'jieshao': member.jieshao,
|
||
'jiage': str(member.jiage),
|
||
}
|
||
if record.cishu == 1:
|
||
custom_first[record.huiyuan] = float(record.guanshi_fenhong)
|
||
else:
|
||
times = record.cishu
|
||
if times not in extra_map:
|
||
extra_map[times] = {}
|
||
extra_map[times][record.huiyuan] = {
|
||
'guanshi_fenhong': float(record.guanshi_fenhong),
|
||
**member_info
|
||
}
|
||
|
||
# 注意:首次分红定制只存储有定制记录的会员,没有定制的会员使用默认值(前端通过会员表展示)
|
||
# 额外次数分红按次数返回
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'base': base_data,
|
||
'balance': balance_data,
|
||
'permanent_enabled': permanent_enabled,
|
||
'permanent_amount': permanent_amount,
|
||
'custom_first_dividend': custom_first,
|
||
'extra_dividend_map': extra_map,
|
||
'all_members': all_members,
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
class UpdateGuanliView(APIView):
|
||
"""
|
||
修改管事信息(基础信息、余额、提现限额、分红策略)
|
||
权限细分:
|
||
- 修改状态(封禁/解封)及昵称/电话/微信:需要4400a
|
||
- 增加余额:需要4400c
|
||
- 减少余额:需要4400b
|
||
- 修改提现限额(kaioi_ewai_xiane/ewai_xiane):需要4400e
|
||
- 修改分红相关(永久分红、定制首次、额外次数):需要4400f
|
||
POST /houtai/htxggsxx
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
yonghuid = request.data.get('yonghuid', '').strip()
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
if not yonghuid:
|
||
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||
|
||
# 权限校验(先获取权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 查询管事对象(不在事务外加锁,事务内加锁)
|
||
try:
|
||
user = UserMain.objects.get(yonghuid=yonghuid)
|
||
guanshi = user.guanshi_profile
|
||
boss = user.boss_profile if hasattr(user, 'boss_profile') else None
|
||
except UserMain.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不存在'})
|
||
except UserGuanshi.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不是管事'})
|
||
|
||
# 开始事务,内部使用 select_for_update
|
||
with transaction.atomic():
|
||
# 重新获取加锁对象
|
||
user = UserMain.objects.select_for_update().get(yonghuid=yonghuid)
|
||
guanshi = user.guanshi_profile
|
||
boss = user.boss_profile if hasattr(user, 'boss_profile') else None
|
||
|
||
old_yue = guanshi.yue
|
||
old_zhuangtai = guanshi.zhuangtai
|
||
old_kaioi = guanshi.kaioi_ewai_xiane
|
||
old_ewai = guanshi.ewai_xiane
|
||
|
||
# ---------- 1. 基础信息修改(需要4400a)----------
|
||
nickname = request.data.get('nickname')
|
||
dianhua = request.data.get('dianhua')
|
||
wechat = request.data.get('wechat')
|
||
zhuangtai = request.data.get('zhuangtai')
|
||
avatar = request.data.get('avatar')
|
||
|
||
if any(v is not None for v in [nickname, dianhua, wechat, zhuangtai, avatar]):
|
||
if '4400a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限修改管事基础信息(需要4400a)'})
|
||
# 处理昵称(在老板扩展表中)
|
||
if nickname is not None:
|
||
if not boss:
|
||
boss = UserBoss.objects.create(user=user, nickname=nickname)
|
||
else:
|
||
boss.nickname = nickname
|
||
boss.save()
|
||
# 处理其他字段
|
||
if dianhua is not None:
|
||
guanshi.dianhua = dianhua
|
||
if wechat is not None:
|
||
guanshi.wechat = wechat
|
||
if zhuangtai is not None:
|
||
try:
|
||
zt = int(zhuangtai)
|
||
if zt in (0, 1):
|
||
guanshi.zhuangtai = zt
|
||
except:
|
||
pass
|
||
if avatar is not None:
|
||
user.avatar = avatar
|
||
user.save()
|
||
guanshi.save()
|
||
|
||
# ---------- 2. 余额修改(加:4400c;减:4400b)----------
|
||
new_yue = request.data.get('yue')
|
||
if new_yue is not None:
|
||
try:
|
||
new_yue = Decimal(str(new_yue))
|
||
if new_yue < 0:
|
||
return Response({'code': 400, 'msg': '余额不能为负数'})
|
||
except:
|
||
return Response({'code': 400, 'msg': '余额格式错误'})
|
||
|
||
if new_yue > old_yue:
|
||
if '4400c' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限增加余额(需要4400c)'})
|
||
elif new_yue < old_yue:
|
||
if '4400b' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限减少余额(需要4400b)'})
|
||
guanshi.yue = new_yue
|
||
guanshi.save()
|
||
|
||
# ---------- 3. 提现限额修改(需要4400e)----------
|
||
kaioi_ewai = request.data.get('kaioi_ewai_xiane')
|
||
ewai_val = request.data.get('ewai_xiane')
|
||
if kaioi_ewai is not None or ewai_val is not None:
|
||
if '4400e' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限修改提现限额(需要4400e)'})
|
||
if kaioi_ewai is not None:
|
||
guanshi.kaioi_ewai_xiane = bool(kaioi_ewai)
|
||
if ewai_val is not None:
|
||
try:
|
||
guanshi.ewai_xiane = Decimal(str(ewai_val))
|
||
except:
|
||
return Response({'code': 400, 'msg': '额外限额金额格式错误'})
|
||
guanshi.save()
|
||
|
||
# ---------- 4. 分红相关修改(需要4400f)----------
|
||
perm_enabled = request.data.get('permanent_enabled')
|
||
perm_amount = request.data.get('permanent_amount')
|
||
custom_first = request.data.get('custom_first_dividend')
|
||
extra_map = request.data.get('extra_dividend_map')
|
||
fenhong_changed = False
|
||
|
||
if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
|
||
if '4400f' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限修改分红策略(需要4400f)'})
|
||
fenhong_changed = True
|
||
|
||
# 4.1 永久分红
|
||
if perm_enabled is not None:
|
||
guanshi.fenghong_erci_enabled = bool(perm_enabled)
|
||
if perm_amount is not None:
|
||
try:
|
||
guanshi.fenghong_erci = Decimal(str(perm_amount))
|
||
except:
|
||
return Response({'code': 400, 'msg': '永久分红金额格式错误'})
|
||
guanshi.save()
|
||
|
||
# 4.2 首次定制分红(cishu=1)
|
||
if custom_first is not None and isinstance(custom_first, dict):
|
||
# 获取当前所有首次定制记录
|
||
existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1)
|
||
# 前端传来的定制字典 {会员ID: 金额}
|
||
for huiyuan_id, amount in custom_first.items():
|
||
try:
|
||
amount = Decimal(str(amount))
|
||
except:
|
||
continue
|
||
if amount <= 0:
|
||
# 金额为0或负数表示删除定制
|
||
DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete()
|
||
else:
|
||
DuociFenhong.objects.update_or_create(
|
||
yonghuid=yonghuid,
|
||
huiyuan=huiyuan_id,
|
||
cishu=1,
|
||
defaults={
|
||
'guanshi_fenhong': amount,
|
||
'zuzhang_fenhong': Decimal('0')
|
||
}
|
||
)
|
||
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
|
||
for record in existing_first:
|
||
if record.huiyuan not in custom_first:
|
||
record.delete()
|
||
|
||
# 4.3 额外次数分红(cishu >= 2)
|
||
if extra_map is not None and isinstance(extra_map, dict):
|
||
# 收集需要保留的记录
|
||
keep_set = set()
|
||
for times_str, members in extra_map.items():
|
||
try:
|
||
cishu = int(times_str)
|
||
if cishu < 2:
|
||
continue
|
||
except:
|
||
continue
|
||
if not isinstance(members, dict):
|
||
continue
|
||
for huiyuan_id, data in members.items():
|
||
if not isinstance(data, dict):
|
||
continue
|
||
amount = data.get('guanshi_fenhong')
|
||
if amount is None:
|
||
continue
|
||
try:
|
||
amount = Decimal(str(amount))
|
||
except:
|
||
continue
|
||
keep_set.add((cishu, huiyuan_id))
|
||
# 更新或创建
|
||
DuociFenhong.objects.update_or_create(
|
||
yonghuid=yonghuid,
|
||
huiyuan=huiyuan_id,
|
||
cishu=cishu,
|
||
defaults={
|
||
'guanshi_fenhong': amount,
|
||
'zuzhang_fenhong': Decimal('0')
|
||
}
|
||
)
|
||
# 删除不在 keep_set 中的额外次数记录
|
||
existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2)
|
||
for record in existing_extra:
|
||
if (record.cishu, record.huiyuan) not in keep_set:
|
||
record.delete()
|
||
|
||
# ---------- 敏感操作写日志 ----------
|
||
from yonghu.xiugai_log import write_xiugai_log, LEIXING_GUANSHI
|
||
op_phone = kefu.user.phone
|
||
if guanshi.yue != old_yue:
|
||
delta = guanshi.yue - old_yue
|
||
act = '加余额' if delta > 0 else '减余额'
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
operator_phone=op_phone,
|
||
leixing=LEIXING_GUANSHI,
|
||
guanshi_yue_before=old_yue,
|
||
guanshi_yue_after=guanshi.yue,
|
||
qitashuoming=f'管事{act} {abs(delta)},{old_yue} → {guanshi.yue}',
|
||
)
|
||
if guanshi.zhuangtai != old_zhuangtai:
|
||
zt_txt = '解封' if guanshi.zhuangtai == 1 else '封禁'
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
operator_phone=op_phone,
|
||
leixing=LEIXING_GUANSHI,
|
||
qitashuoming=f'管事{zt_txt},状态 {old_zhuangtai} → {guanshi.zhuangtai}',
|
||
)
|
||
if (kaioi_ewai is not None or ewai_val is not None) and (
|
||
guanshi.kaioi_ewai_xiane != old_kaioi or guanshi.ewai_xiane != old_ewai
|
||
):
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
operator_phone=op_phone,
|
||
leixing=LEIXING_GUANSHI,
|
||
qitashuoming=(
|
||
f'管事提现限额变更:开关 {old_kaioi}→{guanshi.kaioi_ewai_xiane},'
|
||
f'额外限额 {old_ewai}→{guanshi.ewai_xiane}'
|
||
),
|
||
)
|
||
if fenhong_changed:
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
operator_phone=op_phone,
|
||
leixing=LEIXING_GUANSHI,
|
||
qitashuoming='管事分红策略变更(永久/首次定制/额外次数)',
|
||
)
|
||
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class GetWithdrawSettingsView(APIView):
|
||
"""
|
||
获取提现设置(手续费利率、每日限额、当日已提现总额)
|
||
权限:需要 5500a 或 5500b 或 5500c 任一
|
||
POST /houtai/hthqtxpz
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
# 需要5500a/b/c任一
|
||
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
|
||
return Response({'code': 403, 'msg': '无权限查看提现设置'})
|
||
|
||
# 1. 手续费利率(Lilubiao:5打手/6管事/8组长/9审核官/10商家/11打手押金)
|
||
rate_map = {}
|
||
for leixing, code in [(1, '5'), (2, '6'), (3, '8'), (4, '9'), (5, '11'), (6, '10')]:
|
||
obj = Lilubiao.objects.filter(fadanpingtai=code).first()
|
||
rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0
|
||
|
||
# 1b. 订单/押金分红费率(Lilubiao:1平台订单/3商家订单/12押金管事/13管事打手接单分红/15商家派单管事分红)
|
||
order_rate_map = {}
|
||
for leixing, code in [(1, '1'), (3, '3'), (12, '12'), (13, '13'), (15, '15')]:
|
||
obj = Lilubiao.objects.filter(fadanpingtai=code).first()
|
||
order_rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0
|
||
|
||
# 2. 个人每日限额 (leixing=1,2,3)
|
||
quota_map = {}
|
||
for leixing in [1, 2, 3]:
|
||
obj = TixianQuotaDefault.objects.filter(leixing=leixing).first()
|
||
quota_map[leixing] = float(obj.default_quota) if obj else 20.0
|
||
|
||
# 3. 每日总限额(提现类型1~6 → 配置表4~9)
|
||
total_limit_map = {}
|
||
for withdraw_leixing, quota_leixing in [(1, 4), (2, 5), (3, 6), (4, 7), (5, 8), (6, 9)]:
|
||
obj = TixianQuotaDefault.objects.filter(leixing=quota_leixing).first()
|
||
total_limit_map[withdraw_leixing] = float(obj.default_quota) if obj else 0.0
|
||
|
||
# 4. 当日已提现总额(按提现类型 leixing 1~6)
|
||
today = date.today()
|
||
today_totals = {i: 0.0 for i in range(1, 7)}
|
||
records = TixianRiTongji.objects.filter(riqi=today)
|
||
for rec in records:
|
||
if rec.leixing in today_totals:
|
||
today_totals[rec.leixing] = float(rec.total_amount)
|
||
|
||
# 🆕 获取提现模式(自动1 / 手动2),默认2
|
||
from peizhi.models import WithdrawConfig # 确保导入模型
|
||
from yonghu.freeze_service import get_public_freeze_settings
|
||
config = WithdrawConfig.objects.filter(id=1).first()
|
||
withdraw_mode = config.mode if config else 2
|
||
freeze_settings = get_public_freeze_settings()
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'withdraw_mode': withdraw_mode,
|
||
'rates': rate_map,
|
||
'order_rates': order_rate_map,
|
||
'quotas': quota_map,
|
||
'total_limits': total_limit_map,
|
||
'today_totals': today_totals,
|
||
'dongjie_quanju_enabled': freeze_settings['dongjie_quanju_enabled'],
|
||
'dongjie_roles': freeze_settings['dongjie_roles'],
|
||
}
|
||
})
|
||
|
||
|
||
class UpdateWithdrawSettingsView(APIView):
|
||
"""
|
||
修改提现设置(权限细分):
|
||
- 修改打手相关(利率、个人限额、总限额)需要5500a
|
||
- 修改管事相关需要5500b
|
||
- 修改组长相关需要5500c
|
||
注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算),
|
||
后端不直接修改 TixianRiTongji,该表由提现流程自动累加。
|
||
POST /houtai/htxgtxsz
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 权限映射:要修改的角色需要对应的权限
|
||
# 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3
|
||
rates = request.data.get('rates', {})
|
||
order_rates = request.data.get('order_rates', request.data.get('commission_rates', {}))
|
||
quotas = request.data.get('quotas', {})
|
||
total_limits = request.data.get('total_limits', {})
|
||
|
||
# 角色类型映射
|
||
role_perms = {1: '5500a', 2: '5500b', 3: '5500c'}
|
||
# 利率代码映射(1-3 有水缸限额;4审核官/5打手押金/6商家余额仅费率)
|
||
rate_code_map = {1: '5', 2: '6', 3: '8', 4: '9', 5: '11', 6: '10'}
|
||
# 订单/押金分红(1平台订单/3商家订单/12押金管事/13管事打手接单分红/15商家派单管事分红)
|
||
order_rate_code_map = {1: '1', 3: '3', 12: '12', 13: '13', 15: '15'}
|
||
# 总限额:提现类型 → TixianQuotaDefault.leixing(4~9)
|
||
total_code_map = {1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9}
|
||
|
||
def _rate_key_present(data, role):
|
||
return (str(role) in data and data[str(role)] is not None) or \
|
||
(role in data and data[role] is not None)
|
||
|
||
with transaction.atomic():
|
||
# 遍历三个角色(限额相关)
|
||
for role in [1, 2, 3]:
|
||
perm_needed = role_perms[role]
|
||
if perm_needed not in permissions:
|
||
if _rate_key_present(rates, role) or \
|
||
(str(role) in quotas and quotas[str(role)] is not None) or \
|
||
(role in quotas and quotas.get(role) is not None) or \
|
||
(str(role) in total_limits and total_limits[str(role)] is not None) or \
|
||
(role in total_limits and total_limits.get(role) is not None):
|
||
return Response({'code': 403, 'msg': f'无权限修改{["","打手","管事","组长"][role]}相关设置(需要{perm_needed})'})
|
||
|
||
# 其他提现手续费(审核官/打手押金/商家余额):需 5500a/b/c 任一
|
||
extra_role_names = {4: '考核官', 5: '打手押金', 6: '商家余额'}
|
||
for role in [4, 5, 6]:
|
||
if _rate_key_present(rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')):
|
||
return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}提现手续费率'})
|
||
if (str(role) in total_limits and total_limits[str(role)] is not None) or \
|
||
(role in total_limits and total_limits.get(role) is not None):
|
||
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
|
||
return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}总限额'})
|
||
|
||
# 订单/押金分红费率:需 5500a/b/c 任一
|
||
for role in [1, 3, 12, 13, 15]:
|
||
if _rate_key_present(order_rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')):
|
||
order_names = {
|
||
1: '平台订单打手分红', 3: '商家订单打手分红',
|
||
12: '押金管事分红', 13: '管事打手接单分红',
|
||
15: '商家派单管事分红',
|
||
}
|
||
return Response({'code': 403, 'msg': f'无权限修改{order_names[role]}费率'})
|
||
|
||
# 1. 修改手续费利率
|
||
for role in [1, 2, 3, 4, 5, 6]:
|
||
val = rates.get(str(role), rates.get(role))
|
||
if val is not None:
|
||
rate = Decimal(str(val))
|
||
code = rate_code_map[role]
|
||
obj, created = Lilubiao.objects.select_for_update().get_or_create(
|
||
fadanpingtai=code,
|
||
defaults={'lilu': rate}
|
||
)
|
||
if not created:
|
||
obj.lilu = rate
|
||
obj.save()
|
||
|
||
# 1b. 修改订单/押金分红费率
|
||
for role in [1, 3, 12, 13, 15]:
|
||
val = order_rates.get(str(role), order_rates.get(role))
|
||
if val is not None:
|
||
rate = Decimal(str(val))
|
||
if rate > 1:
|
||
rate = (rate / Decimal('100')).quantize(Decimal('0.0001'))
|
||
code = order_rate_code_map[role]
|
||
obj, created = Lilubiao.objects.select_for_update().get_or_create(
|
||
fadanpingtai=code,
|
||
defaults={'lilu': rate}
|
||
)
|
||
if not created:
|
||
obj.lilu = rate
|
||
obj.save()
|
||
|
||
# 2. 修改个人每日限额
|
||
for role in [1, 2, 3]:
|
||
if str(role) in quotas:
|
||
val = quotas[str(role)]
|
||
if val is not None:
|
||
quota = Decimal(str(val))
|
||
obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create(
|
||
leixing=role,
|
||
defaults={'default_quota': quota}
|
||
)
|
||
if not created:
|
||
obj.default_quota = quota
|
||
obj.save()
|
||
|
||
# 3. 修改每日总限额(1打手/2管事/3组长/4考核官/5押金/6商家)
|
||
for role in [1, 2, 3, 4, 5, 6]:
|
||
val = total_limits.get(str(role), total_limits.get(role))
|
||
if val is not None:
|
||
limit = Decimal(str(val))
|
||
code = total_code_map[role]
|
||
obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create(
|
||
leixing=code,
|
||
defaults={'default_quota': limit}
|
||
)
|
||
if not created:
|
||
obj.default_quota = limit
|
||
obj.save()
|
||
|
||
# 🆕 修改提现模式(有5500a/b/c任一即可)
|
||
from peizhi.models import WithdrawConfig
|
||
from yonghu.freeze_service import save_public_freeze_settings
|
||
withdraw_mode = request.data.get('withdraw_mode')
|
||
if withdraw_mode is not None:
|
||
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
|
||
return Response({'code': 403, 'msg': '无权限修改提现模式'})
|
||
# 更新或创建 id=1 的配置
|
||
WithdrawConfig.objects.update_or_create(
|
||
id=1,
|
||
defaults={'mode': int(withdraw_mode)}
|
||
)
|
||
|
||
if 'dongjie_quanju_enabled' in request.data or 'dongjie_roles' in request.data:
|
||
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
|
||
return Response({'code': 403, 'msg': '无权限修改冻结配置'})
|
||
save_public_freeze_settings(
|
||
bool(request.data.get('dongjie_quanju_enabled', False)),
|
||
request.data.get('dongjie_roles', []),
|
||
)
|
||
from yonghu.xiugai_log import write_xiugai_log, LEIXING_SYSTEM
|
||
write_xiugai_log(
|
||
yonghuid='0000000',
|
||
operator_phone=kefu.user.phone,
|
||
leixing=LEIXING_SYSTEM,
|
||
qitashuoming=(
|
||
f'修改全局冻结配置:开关={request.data.get("dongjie_quanju_enabled")},'
|
||
f'角色配置={request.data.get("dongjie_roles")}'
|
||
),
|
||
)
|
||
|
||
if withdraw_mode is not None:
|
||
from yonghu.xiugai_log import write_xiugai_log, LEIXING_SYSTEM
|
||
write_xiugai_log(
|
||
yonghuid='0000000',
|
||
operator_phone=kefu.user.phone,
|
||
leixing=LEIXING_SYSTEM,
|
||
qitashuoming=f'修改提现模式为 {withdraw_mode}(1自动/2手动)',
|
||
)
|
||
|
||
# 注意:TixianRiTongji 表由提现流程自动更新,此处不直接修改
|
||
return Response({'code': 0, 'msg': '设置修改成功'})
|
||
|
||
|
||
|
||
|
||
|
||
class GetZuzhangListView(APIView):
|
||
"""
|
||
组长列表接口(分页 + 多条件筛选)
|
||
路由:POST /houtai/hthqzzlb
|
||
权限:需要登录,且拥有 6600a~6600e 任一权限
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@transaction.atomic # 保证查询的一致性(可选,对只读查询影响不大但无坏处)
|
||
def post(self, request):
|
||
# 1. 获取前端传递的 username(用于防越权)
|
||
frontend_username = request.data.get('username', '')
|
||
|
||
# 2. 调用公共校验方法,验证身份并获取权限列表
|
||
result = verify_kefu_permission(request, username_from_frontend=frontend_username)
|
||
|
||
# 重要:verify_kefu_permission 返回格式为 (user_obj, permissions_list) 或 (None, Response)
|
||
# 如果第二个元素是 Response 对象,说明校验失败,直接返回该 Response
|
||
if isinstance(result, tuple) and len(result) == 2:
|
||
user_obj, second = result
|
||
if isinstance(second, Response):
|
||
return second # 校验失败,直接返回错误响应
|
||
else:
|
||
permissions = second # 正常情况:permissions 是列表
|
||
else:
|
||
# 防止意外返回格式,视为失败
|
||
return Response({'code': 500, 'msg': '权限校验模块异常'})
|
||
|
||
# 3. 检查是否拥有组长管理所需权限(6600a ~ 6600e 任一)
|
||
required_perms = ['6600a', '6600b', '6600c', '6600d', '6600e']
|
||
if not any(perm in permissions for perm in required_perms):
|
||
logger.warning(f"用户 {request.user.phone} 尝试访问组长列表但缺少必要权限,已有权限: {permissions}")
|
||
return Response({'code': 403, 'msg': '您无权访问组长管理页面'})
|
||
|
||
# 4. 提取请求参数(分页 + 筛选)
|
||
try:
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 20))
|
||
except ValueError:
|
||
page, page_size = 1, 20
|
||
|
||
keyword = request.data.get('keyword', '').strip()
|
||
status = request.data.get('status') # 1=正常, 0=禁用
|
||
invite_count_op = request.data.get('invite_count_op') # gte / lte
|
||
invite_count_value = request.data.get('invite_count_value')
|
||
amount_op = request.data.get('amount_op')
|
||
amount_value = request.data.get('amount_value')
|
||
commission_op = request.data.get('commission_op')
|
||
commission_value = request.data.get('commission_value')
|
||
|
||
# 5. 构建基础 QuerySet(预加载关联表)
|
||
queryset = UserZuzhang.objects.select_related('user', 'user__boss_profile').all()
|
||
|
||
# 6. 应用筛选条件(使用 ORM 安全过滤)
|
||
if keyword:
|
||
queryset = queryset.filter(
|
||
Q(user__yonghuid__icontains=keyword) |
|
||
Q(user__boss_profile__nickname__icontains=keyword)
|
||
)
|
||
|
||
if status is not None and status != '':
|
||
try:
|
||
queryset = queryset.filter(zhuangtai=int(status))
|
||
except ValueError:
|
||
pass
|
||
|
||
# 邀请总数筛选
|
||
if invite_count_value not in (None, ''):
|
||
try:
|
||
val = float(invite_count_value)
|
||
op = invite_count_op if invite_count_op in ('gte', 'lte') else 'gte'
|
||
queryset = queryset.filter(**{f'yaoqing_zongshu__{op}': val})
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# 可提现金额筛选
|
||
if amount_value not in (None, ''):
|
||
try:
|
||
val = float(amount_value)
|
||
op = amount_op if amount_op in ('gte', 'lte') else 'gte'
|
||
queryset = queryset.filter(**{f'ketixian_jine__{op}': val})
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# 分佣总额筛选
|
||
if commission_value not in (None, ''):
|
||
try:
|
||
val = float(commission_value)
|
||
op = commission_op if commission_op in ('gte', 'lte') else 'gte'
|
||
queryset = queryset.filter(**{f'fenyong_zonge__{op}': val})
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# 7. 总数统计
|
||
total = queryset.count()
|
||
|
||
# 8. 分页
|
||
offset = (page - 1) * page_size
|
||
paged_queryset = queryset[offset:offset + page_size]
|
||
|
||
# 9. 构造返回数据
|
||
data_list = []
|
||
for zu in paged_queryset:
|
||
user = zu.user
|
||
nickname = ''
|
||
if hasattr(user, 'boss_profile') and user.boss_profile:
|
||
nickname = user.boss_profile.nickname or ''
|
||
|
||
data_list.append({
|
||
'yonghuid': user.yonghuid,
|
||
'avatar': user.avatar or '',
|
||
'nickname': nickname,
|
||
'fenyong_zonge': float(zu.fenyong_zonge),
|
||
'ketixian_jine': float(zu.ketixian_jine),
|
||
'yaoqing_zongshu': zu.yaoqing_zongshu,
|
||
'zhuangtai': zu.zhuangtai,
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'success',
|
||
'data': {
|
||
'list': data_list,
|
||
'total': total,
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class GetZuzhangDetailView(APIView):
|
||
"""
|
||
获取组长详情
|
||
权限:6600a,6600b,6600c,6600d,6600e 任一
|
||
POST /houtai/hthqzzxq
|
||
参数:username, yonghuid
|
||
返回:
|
||
code:0, data: {
|
||
base: {...},
|
||
balance: {...},
|
||
permanent_enabled, permanent_amount,
|
||
custom_first_dividend: {},
|
||
extra_dividend_map: {},
|
||
all_members: []
|
||
}
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
yonghuid = request.data.get('yonghuid', '').strip()
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
if not yonghuid:
|
||
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||
|
||
# 权限校验(需要任一权限)
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
required_perms = {'6600a', '6600b', '6600c', '6600d', '6600e'}
|
||
if not any(p in permissions for p in required_perms):
|
||
return Response({'code': 403, 'msg': '您没有权限查看组长详情'})
|
||
|
||
# 查询组长主表、扩展表、老板扩展表(昵称)
|
||
try:
|
||
user = UserMain.objects.select_related('zuzhang_profile', 'boss_profile').get(yonghuid=yonghuid)
|
||
zuzhang = user.zuzhang_profile
|
||
boss = user.boss_profile if hasattr(user, 'boss_profile') else None
|
||
except UserMain.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '组长不存在'})
|
||
except UserZuzhang.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不是组长'})
|
||
|
||
# 基础信息
|
||
base_data = {
|
||
'yonghuid': user.yonghuid,
|
||
'nickname': boss.nickname if boss else '',
|
||
'avatar': user.avatar or '',
|
||
'dianhua': '', # 组长表无电话字段?组长扩展表没有电话/微信,但前端可能需要,可从UserMain关联?实际上组长表无电话字段,这里留空或从其他表获取
|
||
'wechat': '',
|
||
'zhuangtai': zuzhang.zhuangtai,
|
||
'yaoqingma': zuzhang.yaoqingma,
|
||
'yaoqingren': '', # 组长表没有邀请人字段
|
||
'haibao_url': zuzhang.haibao_url or '',
|
||
'yaoqing_zongshu': zuzhang.yaoqing_zongshu,
|
||
'jinri_fenyong': str(zuzhang.jinri_fenyong),
|
||
'jinyue_fenyong': str(zuzhang.jinyue_fenyong),
|
||
'fenyong_zonge': str(zuzhang.fenyong_zonge),
|
||
'create_time': zuzhang.create_time.isoformat() if zuzhang.create_time else None,
|
||
}
|
||
|
||
# 余额提现相关
|
||
balance_data = {
|
||
'ketixian_jine': str(zuzhang.ketixian_jine),
|
||
'jinri_tixian': str(zuzhang.jinri_tixian),
|
||
'last_tixian_time': zuzhang.last_tixian_time.isoformat() if zuzhang.last_tixian_time else None,
|
||
'kaioi_ewai_tixian': zuzhang.kaioi_ewai_tixian,
|
||
'ewai_tixian_xiane': str(zuzhang.ewai_tixian_xiane),
|
||
}
|
||
|
||
# 永久分红(额外分红开关和金额)
|
||
permanent_enabled = zuzhang.kaioi_ewai_fenhong
|
||
permanent_amount = str(zuzhang.ewai_fenhong_jine) if zuzhang.ewai_fenhong_jine else '0'
|
||
|
||
# 获取所有会员(用于前端选择)
|
||
all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc'))
|
||
for m in all_members:
|
||
m['jiage'] = str(m['jiage'])
|
||
m['zuzhangfc'] = str(m['zuzhangfc'])
|
||
|
||
# 获取多次分红配置(按次数和会员组织)
|
||
duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu')
|
||
custom_first = {} # 首次分红定制(cishu=1)
|
||
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } }
|
||
|
||
for record in duoci_records:
|
||
# 获取会员信息
|
||
member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first()
|
||
if not member:
|
||
continue
|
||
member_info = {
|
||
'jieshao': member.jieshao,
|
||
'jiage': str(member.jiage),
|
||
}
|
||
if record.cishu == 1:
|
||
custom_first[record.huiyuan] = float(record.zuzhang_fenhong)
|
||
else:
|
||
times = record.cishu
|
||
if times not in extra_map:
|
||
extra_map[times] = {}
|
||
extra_map[times][record.huiyuan] = {
|
||
'zuzhang_fenhong': float(record.zuzhang_fenhong),
|
||
**member_info
|
||
}
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'base': base_data,
|
||
'balance': balance_data,
|
||
'permanent_enabled': permanent_enabled,
|
||
'permanent_amount': permanent_amount,
|
||
'custom_first_dividend': custom_first,
|
||
'extra_dividend_map': extra_map,
|
||
'all_members': all_members,
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
class UpdateZuzhangView(APIView):
|
||
"""
|
||
修改组长信息(基础信息、余额、提现限额、分红策略)
|
||
权限细分:
|
||
- 增加可提现金额:6600a
|
||
- 减少可提现金额:6600b
|
||
- 修改状态(封禁/解封):6600c
|
||
- 修改提现限额(kaioi_ewai_tixian/ewai_tixian_xiane):6600d
|
||
- 修改分红相关(永久分红、定制首次、额外次数):6600e
|
||
POST /houtai/htxgzzxx
|
||
参数:
|
||
username, yonghuid,
|
||
nickname, avatar, dianhua, wechat, zhuangtai, # 基础
|
||
ketixian_jine, kaioi_ewai_tixian, ewai_tixian_xiane, # 余额提现
|
||
permanent_enabled, permanent_amount, # 永久分红
|
||
custom_first_dividend: {}, # 首次定制分红 {会员ID: 金额}
|
||
extra_dividend_map: {} # 额外次数分红 {次数: {会员ID: {zuzhang_fenhong}}}
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
yonghuid = request.data.get('yonghuid', '').strip()
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
if not yonghuid:
|
||
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 查询组长对象
|
||
try:
|
||
user = UserMain.objects.get(yonghuid=yonghuid)
|
||
zuzhang = user.zuzhang_profile
|
||
boss = user.boss_profile if hasattr(user, 'boss_profile') else None
|
||
except UserMain.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不存在'})
|
||
except UserZuzhang.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不是组长'})
|
||
|
||
# 获取原始数据(用于对比金额变化)
|
||
old_ketixian_jine = zuzhang.ketixian_jine
|
||
old_zhuangtai = zuzhang.zhuangtai
|
||
old_kaioi = zuzhang.kaioi_ewai_tixian
|
||
old_ewai = zuzhang.ewai_tixian_xiane
|
||
|
||
# 开始事务
|
||
with transaction.atomic():
|
||
# ---------- 1. 基础信息修改(需要6600c?按需求,基础信息修改通常用6600c,但为了安全,暂不校验,如有需要可加)----------
|
||
nickname = request.data.get('nickname', '').strip()
|
||
avatar = request.data.get('avatar', '').strip()
|
||
dianhua = request.data.get('dianhua', '').strip()
|
||
wechat = request.data.get('wechat', '').strip()
|
||
zhuangtai = request.data.get('zhuangtai')
|
||
# 状态修改需要6600c
|
||
if zhuangtai is not None:
|
||
if '6600c' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限修改组长状态(需要6600c)'})
|
||
try:
|
||
zt = int(zhuangtai)
|
||
if zt in (0, 1):
|
||
zuzhang.zhuangtai = zt
|
||
except:
|
||
pass
|
||
if nickname:
|
||
if not boss:
|
||
boss = UserBoss.objects.create(user=user, nickname=nickname)
|
||
else:
|
||
boss.nickname = nickname
|
||
boss.save()
|
||
if avatar:
|
||
user.avatar = avatar
|
||
user.save()
|
||
# 组长表没有电话/微信字段,如需存储可暂存到UserMain或忽略
|
||
# 这里不处理
|
||
|
||
# ---------- 2. 可提现金额修改(加:6600a;减:6600b)----------
|
||
new_ketixian_jine = request.data.get('ketixian_jine')
|
||
if new_ketixian_jine is not None:
|
||
try:
|
||
new_val = Decimal(str(new_ketixian_jine))
|
||
if new_val < 0:
|
||
return Response({'code': 400, 'msg': '可提现金额不能为负数'})
|
||
except:
|
||
return Response({'code': 400, 'msg': '金额格式错误'})
|
||
if new_val > old_ketixian_jine:
|
||
if '6600a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限增加可提现金额(需要6600a)'})
|
||
elif new_val < old_ketixian_jine:
|
||
if '6600b' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限减少可提现金额(需要6600b)'})
|
||
zuzhang.ketixian_jine = new_val
|
||
zuzhang.save(update_fields=['ketixian_jine'])
|
||
|
||
# ---------- 3. 提现限额修改(需要6600d)----------
|
||
kaioi_ewai = request.data.get('kaioi_ewai_tixian')
|
||
ewai_val = request.data.get('ewai_tixian_xiane')
|
||
if kaioi_ewai is not None or ewai_val is not None:
|
||
if '6600d' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限修改提现限额(需要6600d)'})
|
||
if kaioi_ewai is not None:
|
||
zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai)
|
||
if ewai_val is not None:
|
||
try:
|
||
zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val))
|
||
except:
|
||
return Response({'code': 400, 'msg': '额外限额金额格式错误'})
|
||
zuzhang.save()
|
||
|
||
# ---------- 4. 分红相关修改(需要6600e)----------
|
||
perm_enabled = request.data.get('permanent_enabled')
|
||
perm_amount = request.data.get('permanent_amount')
|
||
custom_first = request.data.get('custom_first_dividend')
|
||
extra_map = request.data.get('extra_dividend_map')
|
||
fenhong_changed = False
|
||
|
||
if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
|
||
if '6600e' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限修改分红策略(需要6600e)'})
|
||
fenhong_changed = True
|
||
|
||
# 4.1 永久分红(额外分红开关和金额)
|
||
if perm_enabled is not None:
|
||
zuzhang.kaioi_ewai_fenhong = bool(perm_enabled)
|
||
if perm_amount is not None:
|
||
try:
|
||
zuzhang.ewai_fenhong_jine = Decimal(str(perm_amount))
|
||
except:
|
||
return Response({'code': 400, 'msg': '永久分红金额格式错误'})
|
||
zuzhang.save()
|
||
|
||
# 4.2 首次定制分红(cishu=1)
|
||
if custom_first is not None and isinstance(custom_first, dict):
|
||
existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1)
|
||
for huiyuan_id, amount in custom_first.items():
|
||
try:
|
||
amount = Decimal(str(amount))
|
||
except:
|
||
continue
|
||
if amount <= 0:
|
||
# 删除定制(恢复默认)
|
||
DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete()
|
||
else:
|
||
DuociFenhong.objects.update_or_create(
|
||
yonghuid=yonghuid,
|
||
huiyuan=huiyuan_id,
|
||
cishu=1,
|
||
defaults={
|
||
'zuzhang_fenhong': amount,
|
||
'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红
|
||
}
|
||
)
|
||
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
|
||
for record in existing_first:
|
||
if record.huiyuan not in custom_first:
|
||
record.delete()
|
||
|
||
# 4.3 额外次数分红(cishu >= 2)
|
||
if extra_map is not None and isinstance(extra_map, dict):
|
||
keep_set = set()
|
||
for times_str, members in extra_map.items():
|
||
try:
|
||
cishu = int(times_str)
|
||
if cishu < 2:
|
||
continue
|
||
except:
|
||
continue
|
||
if not isinstance(members, dict):
|
||
continue
|
||
for huiyuan_id, data in members.items():
|
||
if not isinstance(data, dict):
|
||
continue
|
||
amount = data.get('zuzhang_fenhong')
|
||
if amount is None:
|
||
continue
|
||
try:
|
||
amount = Decimal(str(amount))
|
||
except:
|
||
continue
|
||
keep_set.add((cishu, huiyuan_id))
|
||
DuociFenhong.objects.update_or_create(
|
||
yonghuid=yonghuid,
|
||
huiyuan=huiyuan_id,
|
||
cishu=cishu,
|
||
defaults={
|
||
'zuzhang_fenhong': amount,
|
||
'guanshi_fenhong': Decimal('0')
|
||
}
|
||
)
|
||
# 删除不在 keep_set 中的额外次数记录
|
||
existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2)
|
||
for record in existing_extra:
|
||
if (record.cishu, record.huiyuan) not in keep_set:
|
||
record.delete()
|
||
|
||
# 保存状态字段
|
||
if zhuangtai is not None:
|
||
zuzhang.save(update_fields=['zhuangtai'])
|
||
|
||
# ---------- 敏感操作写日志 ----------
|
||
from yonghu.xiugai_log import write_xiugai_log, LEIXING_ZUZHANG
|
||
op_phone = kefu.user.phone
|
||
if zuzhang.ketixian_jine != old_ketixian_jine:
|
||
delta = zuzhang.ketixian_jine - old_ketixian_jine
|
||
act = '加可提现' if delta > 0 else '减可提现'
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
operator_phone=op_phone,
|
||
leixing=LEIXING_ZUZHANG,
|
||
zuzhang_yue_before=old_ketixian_jine,
|
||
zuzhang_yue_after=zuzhang.ketixian_jine,
|
||
qitashuoming=f'组长{act} {abs(delta)},{old_ketixian_jine} → {zuzhang.ketixian_jine}',
|
||
)
|
||
if zuzhang.zhuangtai != old_zhuangtai:
|
||
zt_txt = '解封' if zuzhang.zhuangtai == 1 else '封禁'
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
operator_phone=op_phone,
|
||
leixing=LEIXING_ZUZHANG,
|
||
qitashuoming=f'组长{zt_txt},状态 {old_zhuangtai} → {zuzhang.zhuangtai}',
|
||
)
|
||
if (kaioi_ewai is not None or ewai_val is not None) and (
|
||
zuzhang.kaioi_ewai_tixian != old_kaioi or zuzhang.ewai_tixian_xiane != old_ewai
|
||
):
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
operator_phone=op_phone,
|
||
leixing=LEIXING_ZUZHANG,
|
||
qitashuoming=(
|
||
f'组长提现限额变更:开关 {old_kaioi}→{zuzhang.kaioi_ewai_tixian},'
|
||
f'额外限额 {old_ewai}→{zuzhang.ewai_tixian_xiane}'
|
||
),
|
||
)
|
||
if fenhong_changed:
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
operator_phone=op_phone,
|
||
leixing=LEIXING_ZUZHANG,
|
||
qitashuoming='组长分红策略变更(永久/首次定制/额外次数)',
|
||
)
|
||
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
|
||
|
||
|
||
class GetProductTypeZoneView(APIView):
|
||
"""
|
||
获取商品类型、专区及会员列表
|
||
权限:需要拥有 7007a
|
||
请求:POST /houtai/hthqsplxzq
|
||
参数:{"username": "客服手机号"}
|
||
返回:
|
||
code: 0
|
||
data: {
|
||
type_list: [{id, jieshao, tupian_url, yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, is_cross_enabled, paixu, product_count}, ...],
|
||
zone_list: [{id, mingzi, leixing_id, shenhezhuangtai, paixu}, ...],
|
||
member_list: [{huiyuan_id, jieshao, jiage}, ...]
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '7007a' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||
|
||
# 查询所有商品类型(含专区数量)
|
||
types = ShangpinLeixing.objects.all().order_by('-paixu') # paixu 越大越靠前
|
||
type_list = []
|
||
for t in types:
|
||
# 统计该类型下的专区数量
|
||
zone_count = ShangpinZhuanqu.objects.filter(leixing_id=t.id).count()
|
||
type_list.append({
|
||
'id': t.id,
|
||
'jieshao': t.jieshao or '',
|
||
'tupian_url': t.tupian_url or '',
|
||
'yaoqiuleixing': t.yaoqiuleixing,
|
||
'huiyuan_id': t.huiyuan_id or '',
|
||
'yongjin': float(t.yongjin) if t.yongjin is not None else None,
|
||
'shenhezhuangtai': t.shenhezhuangtai,
|
||
'is_cross_enabled': t.is_cross_enabled,
|
||
'is_special_type': t.is_special_type,
|
||
'paixu': t.paixu,
|
||
'product_count': zone_count,
|
||
})
|
||
|
||
# 查询所有专区
|
||
zones = ShangpinZhuanqu.objects.all().order_by('-paixu')
|
||
zone_list = [
|
||
{
|
||
'id': z.id,
|
||
'mingzi': z.mingzi or '',
|
||
'leixing_id': z.leixing_id,
|
||
'shenhezhuangtai': z.shenhezhuangtai,
|
||
'paixu': z.paixu,
|
||
}
|
||
for z in zones
|
||
]
|
||
|
||
# 查询所有会员(只返回 id、介绍、价格)
|
||
members = Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage')
|
||
member_list = [
|
||
{
|
||
'huiyuan_id': m['huiyuan_id'],
|
||
'jieshao': m['jieshao'],
|
||
'jiage': float(m['jiage']),
|
||
}
|
||
for m in members
|
||
]
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'type_list': type_list,
|
||
'zone_list': zone_list,
|
||
'member_list': member_list,
|
||
}
|
||
})
|
||
|
||
class ModifyProductTypeZoneView(APIView):
|
||
"""
|
||
统一修改商品类型/专区(增、删、改)
|
||
权限:需要拥有 7007a
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser, MultiPartParser, FormParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
obj_type = request.data.get('type') # 'leixing' 或 'zhuanqu'
|
||
action = request.data.get('action') # 'add', 'update', 'delete'
|
||
|
||
if not username or not obj_type or not action:
|
||
return Response({'code': 400, 'msg': '缺少必要参数'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '7007a' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||
|
||
if obj_type == 'leixing':
|
||
return self.handle_leixing(request, action)
|
||
elif obj_type == 'zhuanqu':
|
||
return self.handle_zhuanqu(request, action)
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的type参数'})
|
||
|
||
# ---------- 商品类型处理 ----------
|
||
def handle_leixing(self, request, action):
|
||
if action == 'add':
|
||
return self.add_leixing(request)
|
||
elif action == 'update':
|
||
return self.update_leixing(request)
|
||
elif action == 'delete':
|
||
return self.delete_leixing(request)
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的action'})
|
||
|
||
def add_leixing(self, request):
|
||
# 获取并校验字段
|
||
jieshao = request.data.get('jieshao', '').strip()
|
||
if not jieshao:
|
||
return Response({'code': 400, 'msg': '类型名称不能为空'})
|
||
|
||
# 强制转为整数
|
||
try:
|
||
yaoqiuleixing = int(request.data.get('yaoqiuleixing', 0))
|
||
if yaoqiuleixing not in [1, 2]:
|
||
raise ValueError
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'})
|
||
|
||
try:
|
||
shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1))
|
||
if shenhezhuangtai not in [1, 2,3]:
|
||
raise ValueError
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
|
||
|
||
is_cross_enabled = request.data.get('is_cross_enabled', False)
|
||
is_cross_enabled = is_cross_enabled in [True, 'true', '1', 'on']
|
||
is_special_type = request.data.get('is_special_type', False)
|
||
is_special_type = is_special_type in [True, 'true', '1', 'on']
|
||
|
||
huiyuan_id = request.data.get('huiyuan_id', '').strip() or None
|
||
yongjin = request.data.get('yongjin')
|
||
if yongjin is not None and yongjin != '':
|
||
try:
|
||
yongjin = Decimal(str(yongjin))
|
||
except:
|
||
return Response({'code': 400, 'msg': '佣金金额格式错误'})
|
||
else:
|
||
yongjin = None
|
||
|
||
# 图片校验
|
||
image_file = request.FILES.get('file')
|
||
if not image_file:
|
||
return Response({'code': 400, 'msg': '请上传类型图片'})
|
||
|
||
# 验证图片
|
||
try:
|
||
from utils.oss_utils import validate_image
|
||
valid, msg = validate_image(image_file)
|
||
if not valid:
|
||
return Response({'code': 400, 'msg': msg})
|
||
except ImportError:
|
||
pass
|
||
|
||
# 上传图片
|
||
ext = image_file.name.split('.')[-1].lower()
|
||
new_filename = f"{uuid.uuid4().hex}.{ext}"
|
||
oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}"
|
||
url = upload_to_oss(image_file, oss_path)
|
||
if not url:
|
||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||
|
||
# 创建类型
|
||
try:
|
||
with transaction.atomic():
|
||
leixing = ShangpinLeixing.objects.create(
|
||
jieshao=jieshao,
|
||
tupian_url=oss_path,
|
||
yaoqiuleixing=yaoqiuleixing,
|
||
huiyuan_id=huiyuan_id,
|
||
yongjin=yongjin,
|
||
shenhezhuangtai=shenhezhuangtai,
|
||
is_cross_enabled=is_cross_enabled,
|
||
is_special_type=is_special_type,
|
||
paixu=0
|
||
)
|
||
return Response({'code': 0, 'msg': '添加成功', 'data': {'id': leixing.id}})
|
||
except Exception as e:
|
||
logger.error(f"添加类型失败: {e}", exc_info=True)
|
||
delete_from_oss(oss_path)
|
||
return Response({'code': 500, 'msg': '数据库错误'})
|
||
|
||
def update_leixing(self, request):
|
||
type_id = request.data.get('id')
|
||
if not type_id:
|
||
return Response({'code': 400, 'msg': '缺少类型ID'})
|
||
try:
|
||
leixing = ShangpinLeixing.objects.get(id=type_id)
|
||
except ShangpinLeixing.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '类型不存在'})
|
||
|
||
changes = {}
|
||
# 名称
|
||
jieshao = request.data.get('jieshao')
|
||
if jieshao is not None and jieshao.strip() != leixing.jieshao:
|
||
changes['jieshao'] = jieshao.strip()
|
||
|
||
# 抢单要求类型(强制转整数)
|
||
yaoqiuleixing = request.data.get('yaoqiuleixing')
|
||
if yaoqiuleixing is not None:
|
||
try:
|
||
new_val = int(yaoqiuleixing)
|
||
if new_val not in [1, 2]:
|
||
raise ValueError
|
||
if new_val != leixing.yaoqiuleixing:
|
||
changes['yaoqiuleixing'] = new_val
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'})
|
||
|
||
# 会员ID
|
||
huiyuan_id = request.data.get('huiyuan_id', '').strip() or None
|
||
if huiyuan_id != leixing.huiyuan_id:
|
||
changes['huiyuan_id'] = huiyuan_id
|
||
|
||
# 佣金金额
|
||
yongjin = request.data.get('yongjin')
|
||
if yongjin is not None:
|
||
if yongjin == '':
|
||
new_val = None
|
||
else:
|
||
try:
|
||
new_val = Decimal(str(yongjin))
|
||
except:
|
||
return Response({'code': 400, 'msg': '佣金金额格式错误'})
|
||
if new_val != leixing.yongjin:
|
||
changes['yongjin'] = new_val
|
||
|
||
# 审核状态
|
||
shenhezhuangtai = request.data.get('shenhezhuangtai')
|
||
if shenhezhuangtai is not None:
|
||
try:
|
||
new_val = int(shenhezhuangtai)
|
||
if new_val not in [1, 2,3]:
|
||
raise ValueError
|
||
if new_val != leixing.shenhezhuangtai:
|
||
changes['shenhezhuangtai'] = new_val
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
|
||
|
||
# 跨平台互通
|
||
is_cross = request.data.get('is_cross_enabled')
|
||
if is_cross is not None:
|
||
new_cross = is_cross in [True, 'true', '1', 'on']
|
||
if new_cross != leixing.is_cross_enabled:
|
||
changes['is_cross_enabled'] = new_cross
|
||
|
||
# 吃龙虾特殊类型
|
||
is_special = request.data.get('is_special_type')
|
||
if is_special is not None:
|
||
new_special = is_special in [True, 'true', '1', 'on']
|
||
if new_special != leixing.is_special_type:
|
||
changes['is_special_type'] = new_special
|
||
|
||
# 图片处理
|
||
image_file = request.FILES.get('file')
|
||
old_image_path = leixing.tupian_url
|
||
if image_file:
|
||
try:
|
||
from utils.oss_utils import validate_image
|
||
valid, msg = validate_image(image_file)
|
||
if not valid:
|
||
return Response({'code': 400, 'msg': msg})
|
||
except ImportError:
|
||
pass
|
||
ext = image_file.name.split('.')[-1].lower()
|
||
new_filename = f"{uuid.uuid4().hex}.{ext}"
|
||
oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}"
|
||
url = upload_to_oss(image_file, oss_path)
|
||
if not url:
|
||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||
changes['tupian_url'] = oss_path
|
||
|
||
if not changes and not image_file:
|
||
return Response({'code': 400, 'msg': '未做任何修改'})
|
||
|
||
sync_to_products = request.data.get('sync_to_products') in [True, 'true', '1', 'on']
|
||
try:
|
||
with transaction.atomic():
|
||
for field, value in changes.items():
|
||
setattr(leixing, field, value)
|
||
leixing.save()
|
||
|
||
# 同步商品
|
||
if sync_to_products and ('yaoqiuleixing' in changes or 'huiyuan_id' in changes or 'yongjin' in changes):
|
||
product_updates = {}
|
||
if 'yaoqiuleixing' in changes:
|
||
product_updates['yaoqiuleixing'] = changes['yaoqiuleixing']
|
||
if 'huiyuan_id' in changes:
|
||
product_updates['huiyuan_id'] = changes['huiyuan_id']
|
||
if 'yongjin' in changes:
|
||
product_updates['yongjin'] = changes['yongjin']
|
||
if product_updates:
|
||
Shangpin.objects.filter(leixing_id=type_id).update(**product_updates)
|
||
|
||
# 删除旧图片
|
||
if image_file and old_image_path:
|
||
delete_from_oss(old_image_path)
|
||
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
except Exception as e:
|
||
logger.error(f"更新类型失败: {e}", exc_info=True)
|
||
if image_file and 'oss_path' in locals():
|
||
delete_from_oss(oss_path)
|
||
return Response({'code': 500, 'msg': '修改失败'})
|
||
|
||
def delete_leixing(self, request):
|
||
type_id = request.data.get('id')
|
||
if not type_id:
|
||
return Response({'code': 400, 'msg': '缺少类型ID'})
|
||
delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on']
|
||
|
||
try:
|
||
leixing = ShangpinLeixing.objects.get(id=type_id)
|
||
except ShangpinLeixing.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '类型不存在'})
|
||
|
||
with transaction.atomic():
|
||
# 删除该类型下的所有专区
|
||
zones = ShangpinZhuanqu.objects.filter(leixing_id=type_id)
|
||
if delete_products:
|
||
# 删除专区下的所有商品
|
||
for zone in zones:
|
||
Shangpin.objects.filter(zhuanqu_id=zone.id).delete()
|
||
zones.delete()
|
||
# 删除直接关联该类型的商品
|
||
Shangpin.objects.filter(leixing_id=type_id).delete()
|
||
else:
|
||
zones.delete()
|
||
# 仅清除商品的类型关联
|
||
Shangpin.objects.filter(leixing_id=type_id).update(leixing_id=None)
|
||
|
||
leixing.delete()
|
||
|
||
if leixing.tupian_url:
|
||
delete_from_oss(leixing.tupian_url)
|
||
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
# ---------- 商品专区处理 ----------
|
||
def handle_zhuanqu(self, request, action):
|
||
if action == 'add':
|
||
return self.add_zhuanqu(request)
|
||
elif action == 'update':
|
||
return self.update_zhuanqu(request)
|
||
elif action == 'delete':
|
||
return self.delete_zhuanqu(request)
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的action'})
|
||
|
||
def add_zhuanqu(self, request):
|
||
mingzi = request.data.get('mingzi', '').strip()
|
||
leixing_id = request.data.get('leixing_id')
|
||
if not mingzi:
|
||
return Response({'code': 400, 'msg': '专区名称不能为空'})
|
||
if not leixing_id:
|
||
return Response({'code': 400, 'msg': '请选择所属类型'})
|
||
|
||
try:
|
||
leixing_id = int(leixing_id)
|
||
if not ShangpinLeixing.objects.filter(id=leixing_id).exists():
|
||
return Response({'code': 404, 'msg': '所属类型不存在'})
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '所属类型ID无效'})
|
||
|
||
try:
|
||
shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1))
|
||
if shenhezhuangtai not in [1, 2,3]:
|
||
raise ValueError
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
zhuanqu = ShangpinZhuanqu.objects.create(
|
||
mingzi=mingzi,
|
||
leixing_id=leixing_id,
|
||
shenhezhuangtai=shenhezhuangtai,
|
||
paixu=0
|
||
)
|
||
return Response({'code': 0, 'msg': '添加成功', 'data': {'id': zhuanqu.id}})
|
||
except Exception as e:
|
||
logger.error(f"添加专区失败: {e}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '数据库错误'})
|
||
|
||
def update_zhuanqu(self, request):
|
||
zone_id = request.data.get('id')
|
||
if not zone_id:
|
||
return Response({'code': 400, 'msg': '缺少专区ID'})
|
||
try:
|
||
zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id)
|
||
except ShangpinZhuanqu.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '专区不存在'})
|
||
|
||
changes = {}
|
||
mingzi = request.data.get('mingzi')
|
||
if mingzi is not None and mingzi.strip() != zhuanqu.mingzi:
|
||
changes['mingzi'] = mingzi.strip()
|
||
|
||
leixing_id = request.data.get('leixing_id')
|
||
if leixing_id is not None:
|
||
try:
|
||
new_id = int(leixing_id)
|
||
if not ShangpinLeixing.objects.filter(id=new_id).exists():
|
||
return Response({'code': 404, 'msg': '新所属类型不存在'})
|
||
if new_id != zhuanqu.leixing_id:
|
||
changes['leixing_id'] = new_id
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '所属类型ID无效'})
|
||
|
||
shenhezhuangtai = request.data.get('shenhezhuangtai')
|
||
if shenhezhuangtai is not None:
|
||
try:
|
||
new_val = int(shenhezhuangtai)
|
||
if new_val not in [1, 2,3]:
|
||
raise ValueError
|
||
if new_val != zhuanqu.shenhezhuangtai:
|
||
changes['shenhezhuangtai'] = new_val
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
|
||
|
||
if not changes:
|
||
return Response({'code': 400, 'msg': '未做任何修改'})
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
for field, value in changes.items():
|
||
setattr(zhuanqu, field, value)
|
||
zhuanqu.save()
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
except Exception as e:
|
||
logger.error(f"更新专区失败: {e}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '修改失败'})
|
||
|
||
def delete_zhuanqu(self, request):
|
||
zone_id = request.data.get('id')
|
||
if not zone_id:
|
||
return Response({'code': 400, 'msg': '缺少专区ID'})
|
||
delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on']
|
||
|
||
try:
|
||
zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id)
|
||
except ShangpinZhuanqu.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '专区不存在'})
|
||
|
||
with transaction.atomic():
|
||
if delete_products:
|
||
Shangpin.objects.filter(zhuanqu_id=zone_id).delete()
|
||
else:
|
||
Shangpin.objects.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None)
|
||
zhuanqu.delete()
|
||
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
|
||
# 继续在 houtai/views_product.py 中添加
|
||
class GetRateView(APIView):
|
||
"""
|
||
获取打手分成费率(平台订单和商家订单)
|
||
权限:需要拥有 7007b
|
||
请求:POST /houtai/hthqfhpz
|
||
参数:{"username": "客服手机号"}
|
||
返回:{"code":0, "data": {"platform_rate": xx, "merchant_rate": xx}}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '7007b' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看费率'})
|
||
|
||
from dingdan.models import Lilubiao
|
||
try:
|
||
platform_rate_obj = Lilubiao.objects.get(fadanpingtai='1')
|
||
merchant_rate_obj = Lilubiao.objects.get(fadanpingtai='3')
|
||
except Lilubiao.DoesNotExist as e:
|
||
logger.error(f"费率记录缺失: {e}")
|
||
return Response({'code': 500, 'msg': '费率配置缺失'})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'platform_rate': float(platform_rate_obj.lilu) if platform_rate_obj.lilu else 0,
|
||
'merchant_rate': float(merchant_rate_obj.lilu) if merchant_rate_obj.lilu else 0,
|
||
}
|
||
})
|
||
|
||
|
||
class ModifyRateView(APIView):
|
||
"""
|
||
修改打手分成费率
|
||
权限:需要拥有 7007b
|
||
请求:POST /houtai/htxgfl
|
||
参数:{"username": "客服手机号", "type": "platform" / "merchant", "value": 费率(百分比数字)}
|
||
返回:{"code":0, "msg":"修改成功"}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
rate_type = request.data.get('type')
|
||
value = request.data.get('value')
|
||
|
||
if not username or not rate_type or value is None:
|
||
return Response({'code': 400, 'msg': '缺少参数'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '7007b' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权修改费率'})
|
||
|
||
try:
|
||
rate_value = Decimal(str(value))
|
||
except:
|
||
return Response({'code': 400, 'msg': '费率值格式错误'})
|
||
|
||
|
||
if rate_type == 'platform':
|
||
fadanpingtai = '1'
|
||
elif rate_type == 'merchant':
|
||
fadanpingtai = '3'
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的type参数'})
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
rate_obj = Lilubiao.objects.select_for_update().get(fadanpingtai=fadanpingtai)
|
||
rate_obj.lilu = rate_value
|
||
rate_obj.save()
|
||
except Lilubiao.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '费率记录不存在'})
|
||
except Exception as e:
|
||
logger.error(f"修改费率失败: {e}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '修改失败'})
|
||
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# 权限码(根据你要求,使用 '8080a')
|
||
REQUIRED_PERMISSION = '8080a'
|
||
class PopupNoticeListAPIView(APIView):
|
||
"""
|
||
获取所有弹窗配置(页面 + 弹窗 + 图片)
|
||
URL: POST /houtai/hthqtcxx
|
||
请求参数: { "username": "客服账号" }
|
||
响应格式: { "code": 0, "data": { "pages": [...] } }
|
||
注意:所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段区分
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 获取前端传递的 username(用于防越权)
|
||
username_frontend = request.data.get('username')
|
||
# 2. 调用公共权限验证方法,返回 (客服对象, 权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username_frontend)
|
||
if kefu is None:
|
||
# 身份验证失败,返回业务码 403,但 HTTP 状态码仍是 200
|
||
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||
|
||
# 3. 检查是否拥有所需权限码 '8080a'
|
||
if REQUIRED_PERMISSION not in permissions:
|
||
return Response({'code': 403, 'msg': '您没有权限访问弹窗管理功能'})
|
||
|
||
# 4. 查询所有页面,并预加载关联的弹窗和图片(避免N+1查询)
|
||
pages = PopupPage.objects.all().prefetch_related('popups__images').order_by('id')
|
||
|
||
# 5. 序列化输出
|
||
serializer = PopupPageSerializer(pages, many=True)
|
||
return Response({'code': 0, 'data': {'pages': serializer.data}})
|
||
|
||
|
||
class PopupNoticeModifyAPIView(APIView):
|
||
"""
|
||
统一修改接口:支持页面、弹窗、图片的增删改
|
||
URL: POST /houtai/htxgtcxx
|
||
请求中必须包含 action 字段,用于区分操作类型。
|
||
所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段体现。
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# ---------- 1. 权限验证 ----------
|
||
username_frontend = request.data.get('username')
|
||
kefu, permissions = verify_kefu_permission(request, username_frontend)
|
||
if kefu is None:
|
||
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||
if REQUIRED_PERMISSION not in permissions:
|
||
return Response({'code': 403, 'msg': '您没有权限操作弹窗管理'})
|
||
|
||
# ---------- 2. 获取 action 并分发 ----------
|
||
action = request.data.get('action')
|
||
if not action:
|
||
return Response({'code': 400, 'msg': '缺少action参数'})
|
||
|
||
try:
|
||
if action == 'add_page':
|
||
return self.add_page(request)
|
||
elif action == 'update_page':
|
||
return self.update_page(request)
|
||
elif action == 'delete_page':
|
||
return self.delete_page(request)
|
||
elif action == 'add_popup':
|
||
return self.add_popup(request)
|
||
elif action == 'update_popup':
|
||
return self.update_popup(request)
|
||
elif action == 'delete_popup':
|
||
return self.delete_popup(request)
|
||
elif action == 'add_image':
|
||
return self.add_image(request)
|
||
elif action == 'update_image':
|
||
return self.update_image(request)
|
||
elif action == 'delete_image':
|
||
return self.delete_image(request)
|
||
else:
|
||
return Response({'code': 400, 'msg': f'未知的action: {action}'})
|
||
except Exception as e:
|
||
# 捕获未预期的异常,返回服务器错误
|
||
import traceback
|
||
traceback.print_exc()
|
||
return Response({'code': 500, 'msg': f'服务器错误: {str(e)}'})
|
||
|
||
# ==================== 页面相关操作 ====================
|
||
|
||
def add_page(self, request):
|
||
"""添加新页面"""
|
||
data = request.data
|
||
# 必填字段校验
|
||
if not data.get('page_key') or not data.get('name'):
|
||
return Response({'code': 400, 'msg': '缺少 page_key 或 name'})
|
||
# 唯一性校验
|
||
if PopupPage.objects.filter(page_key=data['page_key']).exists():
|
||
return Response({'code': 400, 'msg': '页面标识已存在'})
|
||
|
||
page = PopupPage.objects.create(
|
||
page_key=data['page_key'],
|
||
name=data['name'],
|
||
description=data.get('description', ''),
|
||
is_active=data.get('is_active', True),
|
||
ignore_user_mute=data.get('ignore_user_mute', False)
|
||
)
|
||
return Response({'code': 0, 'msg': '添加成功', 'data': {'id': page.id}})
|
||
|
||
def update_page(self, request):
|
||
"""更新页面信息"""
|
||
page_id = request.data.get('id')
|
||
if not page_id:
|
||
return Response({'code': 400, 'msg': '缺少页面id'})
|
||
try:
|
||
page = PopupPage.objects.get(id=page_id)
|
||
except PopupPage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '页面不存在'})
|
||
|
||
# 更新允许修改的字段
|
||
page.page_key = request.data.get('page_key', page.page_key)
|
||
page.name = request.data.get('name', page.name)
|
||
page.description = request.data.get('description', page.description)
|
||
page.is_active = request.data.get('is_active', page.is_active)
|
||
page.ignore_user_mute = request.data.get('ignore_user_mute', page.ignore_user_mute)
|
||
page.save()
|
||
return Response({'code': 0, 'msg': '更新成功'})
|
||
|
||
def delete_page(self, request):
|
||
"""删除页面,可级联删除其下所有弹窗和图片"""
|
||
page_id = request.data.get('id')
|
||
cascade = request.data.get('cascade_delete_popups', True) # 默认级联
|
||
if not page_id:
|
||
return Response({'code': 400, 'msg': '缺少页面id'})
|
||
try:
|
||
page = PopupPage.objects.get(id=page_id)
|
||
except PopupPage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '页面不存在'})
|
||
|
||
if cascade:
|
||
# 级联删除:使用事务保证原子性
|
||
with transaction.atomic():
|
||
popups = page.popups.all()
|
||
for popup in popups:
|
||
# 删除弹窗下的所有图片文件
|
||
for img in popup.images.all():
|
||
if img.image_url:
|
||
delete_from_oss(img.image_url)
|
||
popups.delete()
|
||
page.delete()
|
||
else:
|
||
# 非级联删除:如果页面下还有弹窗,则拒绝删除
|
||
if page.popups.exists():
|
||
return Response({'code': 400, 'msg': '该页面下存在弹窗,请先删除弹窗或使用级联删除'})
|
||
page.delete()
|
||
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
# ==================== 弹窗相关操作 ====================
|
||
|
||
def add_popup(self, request):
|
||
"""为指定页面添加新弹窗"""
|
||
data = request.data
|
||
page_id = data.get('page_id')
|
||
popup_id = data.get('popup_id')
|
||
if not page_id or not popup_id:
|
||
return Response({'code': 400, 'msg': '缺少 page_id 或 popup_id'})
|
||
try:
|
||
page = PopupPage.objects.get(id=page_id)
|
||
except PopupPage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '页面不存在'})
|
||
|
||
# 同一页面下 popup_id 必须唯一
|
||
if PopupConfig.objects.filter(page=page, popup_id=popup_id).exists():
|
||
return Response({'code': 400, 'msg': '该页面下已存在相同的弹窗ID'})
|
||
|
||
popup = PopupConfig.objects.create(
|
||
page=page,
|
||
popup_id=popup_id,
|
||
title=data.get('title', ''),
|
||
description=data.get('description', ''),
|
||
strategy_type=data.get('strategy_type', 'daily'),
|
||
max_count=data.get('max_count', 1),
|
||
reset_interval=data.get('reset_interval', 'day'),
|
||
duration=data.get('duration', 0),
|
||
force_even_muted=data.get('force_even_muted', False),
|
||
sort_order=data.get('sort_order', 0),
|
||
is_active=data.get('is_active', True),
|
||
start_time=data.get('start_time') or None,
|
||
end_time=data.get('end_time') or None
|
||
)
|
||
return Response({'code': 0, 'msg': '添加成功', 'data': {'id': popup.id}})
|
||
|
||
def update_popup(self, request):
|
||
"""更新弹窗信息"""
|
||
popup_id = request.data.get('id')
|
||
if not popup_id:
|
||
return Response({'code': 400, 'msg': '缺少弹窗id'})
|
||
try:
|
||
popup = PopupConfig.objects.get(id=popup_id)
|
||
except PopupConfig.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '弹窗不存在'})
|
||
|
||
# 更新弹窗字段
|
||
popup.popup_id = request.data.get('popup_id', popup.popup_id)
|
||
popup.title = request.data.get('title', popup.title)
|
||
popup.description = request.data.get('description', popup.description)
|
||
popup.strategy_type = request.data.get('strategy_type', popup.strategy_type)
|
||
popup.max_count = request.data.get('max_count', popup.max_count)
|
||
popup.reset_interval = request.data.get('reset_interval', popup.reset_interval)
|
||
popup.duration = request.data.get('duration', popup.duration)
|
||
popup.force_even_muted = request.data.get('force_even_muted', popup.force_even_muted)
|
||
popup.sort_order = request.data.get('sort_order', popup.sort_order)
|
||
popup.is_active = request.data.get('is_active', popup.is_active)
|
||
popup.start_time = request.data.get('start_time') or None
|
||
popup.end_time = request.data.get('end_time') or None
|
||
popup.save()
|
||
return Response({'code': 0, 'msg': '更新成功'})
|
||
|
||
def delete_popup(self, request):
|
||
"""删除弹窗,同时删除其下所有图片(包括OSS文件)"""
|
||
popup_id = request.data.get('id')
|
||
if not popup_id:
|
||
return Response({'code': 400, 'msg': '缺少弹窗id'})
|
||
try:
|
||
popup = PopupConfig.objects.get(id=popup_id)
|
||
except PopupConfig.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '弹窗不存在'})
|
||
|
||
with transaction.atomic():
|
||
for img in popup.images.all():
|
||
if img.image_url:
|
||
delete_from_oss(img.image_url)
|
||
popup.images.all().delete()
|
||
popup.delete()
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
# ==================== 图片相关操作 ====================
|
||
|
||
def _upload_image_file(self, file_obj, page_key):
|
||
"""
|
||
内部方法:将图片上传到OSS指定目录
|
||
目录: a_long/tanchuang/tanchuang/
|
||
文件名: {page_key}_{时间戳}_{uuid8}.{ext}
|
||
返回: 相对路径(例如 a_long/tanchuang/tanchuang/dashouzhongxin_20250419120000_abc12345.jpg)
|
||
"""
|
||
# 验证图片格式和大小
|
||
valid, msg = validate_image(file_obj)
|
||
if not valid:
|
||
raise ValueError(msg)
|
||
|
||
# 生成唯一文件名
|
||
ext = os.path.splitext(file_obj.name)[1].lower()
|
||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
||
unique_id = str(uuid.uuid4())[:8]
|
||
filename = f"{page_key}_{timestamp}_{unique_id}{ext}"
|
||
relative_path = f"a_long/tanchuang/tanchuang/{filename}"
|
||
|
||
# 上传到OSS
|
||
url = upload_to_oss(file_obj, relative_path)
|
||
if not url:
|
||
raise ValueError("图片上传失败")
|
||
return relative_path
|
||
|
||
def add_image(self, request):
|
||
"""为指定弹窗添加图片(支持上传文件)"""
|
||
popup_id = request.data.get('popup_id')
|
||
if not popup_id:
|
||
return Response({'code': 400, 'msg': '缺少 popup_id'})
|
||
try:
|
||
popup = PopupConfig.objects.get(id=popup_id)
|
||
except PopupConfig.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '弹窗不存在'})
|
||
|
||
# 获取上传的文件
|
||
file_obj = request.FILES.get('file')
|
||
if not file_obj:
|
||
return Response({'code': 400, 'msg': '未提供图片文件'})
|
||
|
||
try:
|
||
page_key = popup.page.page_key
|
||
relative_path = self._upload_image_file(file_obj, page_key)
|
||
except ValueError as e:
|
||
return Response({'code': 400, 'msg': str(e)})
|
||
|
||
# 保存图片记录
|
||
image = PopupImage.objects.create(
|
||
popup_config=popup,
|
||
image_url=relative_path,
|
||
text=request.data.get('text', ''),
|
||
sort_order=request.data.get('sort_order', 0)
|
||
)
|
||
return Response({'code': 0, 'msg': '添加成功', 'data': {'id': image.id}})
|
||
|
||
def update_image(self, request):
|
||
"""更新图片信息(可替换图片文件、修改文字、排序)"""
|
||
image_id = request.data.get('id')
|
||
if not image_id:
|
||
return Response({'code': 400, 'msg': '缺少图片id'})
|
||
try:
|
||
image = PopupImage.objects.get(id=image_id)
|
||
except PopupImage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '图片不存在'})
|
||
|
||
# 更新文字和排序
|
||
image.text = request.data.get('text', image.text)
|
||
image.sort_order = request.data.get('sort_order', image.sort_order)
|
||
|
||
# 如果提供了新图片文件,则替换
|
||
file_obj = request.FILES.get('file')
|
||
if file_obj:
|
||
try:
|
||
page_key = image.popup_config.page.page_key
|
||
new_path = self._upload_image_file(file_obj, page_key)
|
||
except ValueError as e:
|
||
return Response({'code': 400, 'msg': str(e)})
|
||
# 删除旧图片
|
||
if image.image_url:
|
||
delete_from_oss(image.image_url)
|
||
image.image_url = new_path
|
||
|
||
image.save()
|
||
return Response({'code': 0, 'msg': '更新成功'})
|
||
|
||
def delete_image(self, request):
|
||
"""删除图片(同时删除OSS文件)"""
|
||
image_id = request.data.get('id')
|
||
if not image_id:
|
||
return Response({'code': 400, 'msg': '缺少图片id'})
|
||
try:
|
||
image = PopupImage.objects.get(id=image_id)
|
||
except PopupImage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '图片不存在'})
|
||
|
||
if image.image_url:
|
||
delete_from_oss(image.image_url)
|
||
image.delete()
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
from dingdan.models import Dingdan, Fadan, CrossPlatformOrderData # 按实际路径
|
||
class FineApplyView(APIView):
|
||
"""
|
||
客服申请罚款接口(金额罚款,非积分)
|
||
路径:POST /houtai/kffkdssq
|
||
参数:
|
||
username: 客服手机号
|
||
dingdan_id: 订单ID
|
||
chufaliyou: 罚款原因
|
||
fakuanjine: 罚款金额(元)
|
||
yingxiang_qiangdan: 1影响抢单 / 0不影响抢单
|
||
shenqingren_shenfen: 操作人身份 (1客服/2售后/3管理员等)
|
||
权限:JWT + 权限码99933abs
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 提取参数
|
||
username = request.data.get('username', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
chufaliyou = request.data.get('chufaliyou', '').strip()
|
||
fakuanjine = request.data.get('fakuanjine', 0)
|
||
yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1)
|
||
shenqingren_shenfen = request.data.get('shenqingren_shenfen', 2)
|
||
|
||
if not all([username, dingdan_id, chufaliyou, fakuanjine]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
try:
|
||
# 2. 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '99933abs' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无罚款权限'})
|
||
|
||
# 3. 获取订单(无需 select_for_update,因不修改订单)
|
||
try:
|
||
order = Dingdan.objects.get(dingdan_id=dingdan_id)
|
||
except Dingdan.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
dashou_id = order.jiedan_dashou_id
|
||
if not dashou_id:
|
||
return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'})
|
||
|
||
# 4. 重复罚单检查
|
||
if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists():
|
||
return Response({'code': 400, 'msg': '该打手已被罚款过'})
|
||
|
||
# 5. 跨平台相关字段
|
||
partner_order_id = order.partner_order_id or ''
|
||
partner_club_id = order.partner_club_id or ''
|
||
dispatch_type = order.dispatch_type or 0
|
||
|
||
# 本地创建罚单的可复用函数
|
||
def create_local_fadan():
|
||
Fadan.objects.create(
|
||
beichufa_id=dashou_id,
|
||
shenqing_chufa=username,
|
||
shenfen=1, # 1 代表打手
|
||
chufaliyou=chufaliyou,
|
||
fakuanjine=fakuanjine,
|
||
guanliandingdan_id=dingdan_id,
|
||
zhuangtai=1, # 待缴纳
|
||
yingxiang_qiangdan=yingxiang_qiangdan,
|
||
shenqingren_shenfen=shenqingren_shenfen,
|
||
partner_club_id='',
|
||
)
|
||
|
||
# 6. 判断:没有对方订单ID,或对方派单 → 本地处罚
|
||
if not partner_order_id or dispatch_type == 2:
|
||
with transaction.atomic():
|
||
create_local_fadan()
|
||
logger.info(f"本地罚款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}")
|
||
return Response({'code': 0, 'msg': '罚款已生成'})
|
||
|
||
# 7. 我方派单且有对方订单ID → 通知对方平台
|
||
# 获取我方俱乐部ID
|
||
try:
|
||
my_club = ClubConfig.objects.filter(is_self=1).first()
|
||
if not my_club:
|
||
return Response({'code': 500, 'msg': '系统配置错误'})
|
||
self_club_id = my_club.club_id
|
||
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
|
||
partner_domain = club_rel.partner_domain
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 500, 'msg': '对方平台信息未配置'})
|
||
|
||
url = partner_domain.rstrip('/') + '/houtai/kffkdssq_notify'
|
||
payload = {
|
||
'order_id': partner_order_id,
|
||
'club_id': self_club_id,
|
||
'chufaliyou': chufaliyou,
|
||
'fakuanjine': fakuanjine,
|
||
'yingxiang_qiangdan': yingxiang_qiangdan,
|
||
'shenqingren_shenfen': shenqingren_shenfen,
|
||
'operator': username,
|
||
}
|
||
timeout = 3
|
||
retries = 2
|
||
success = False
|
||
for attempt in range(retries):
|
||
try:
|
||
resp = requests.post(url, json=payload, timeout=timeout)
|
||
if resp.status_code == 200 and resp.json().get('code') == 0:
|
||
success = True
|
||
break
|
||
except Exception:
|
||
continue
|
||
|
||
if not success:
|
||
return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'})
|
||
|
||
# 通知成功后,本地也记录一笔(标记为跨平台)
|
||
with transaction.atomic():
|
||
Fadan.objects.create(
|
||
beichufa_id=dashou_id,
|
||
shenqing_chufa=username,
|
||
shenfen=1,
|
||
chufaliyou=chufaliyou,
|
||
fakuanjine=fakuanjine,
|
||
guanliandingdan_id=dingdan_id,
|
||
zhuangtai=1,
|
||
yingxiang_qiangdan=yingxiang_qiangdan,
|
||
shenqingren_shenfen=shenqingren_shenfen,
|
||
partner_club_id=partner_club_id,
|
||
)
|
||
logger.info(f"跨平台罚款通知成功: {dingdan_id}")
|
||
return Response({'code': 0, 'msg': '罚款已提交至对方平台'})
|
||
|
||
except Exception as e:
|
||
logger.error(f"罚款申请异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
from rest_framework.permissions import AllowAny
|
||
|
||
class PartnerFineNotifyView(APIView):
|
||
"""
|
||
接收对方平台罚款通知(无认证)
|
||
路径:POST /houtai/kffkdssq_notify
|
||
参数:
|
||
order_id: 对方平台的订单ID(需通过这个找到我方订单)
|
||
club_id: 对方俱乐部ID
|
||
chufaliyou: 罚款原因
|
||
fakuanjine: 罚款金额
|
||
yingxiang_qiangdan: 1影响抢单 / 0不影响
|
||
shenqingren_shenfen: 申请处罚人身份
|
||
operator: 操作人标识
|
||
"""
|
||
authentication_classes = []
|
||
permission_classes = [AllowAny]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
order_id = request.data.get('order_id', '').strip() # 对方平台订单ID
|
||
club_id = request.data.get('club_id', '').strip()
|
||
chufaliyou = request.data.get('chufaliyou', '').strip()
|
||
fakuanjine = request.data.get('fakuanjine', 0)
|
||
yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1)
|
||
shenqingren_shenfen = request.data.get('shenqingren_shenfen', 2)
|
||
operator = request.data.get('operator', '').strip()
|
||
|
||
if not all([order_id, chufaliyou]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 通过对方订单ID查找我方订单
|
||
try:
|
||
cross = CrossPlatformOrderData.objects.get(partner_order_id=order_id)
|
||
my_dingdan_id = cross.dingdan_id
|
||
order = Dingdan.objects.get(dingdan_id=my_dingdan_id)
|
||
dashou_id = order.jiedan_dashou_id
|
||
except CrossPlatformOrderData.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '未找到关联订单'})
|
||
except Dingdan.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '我方订单不存在'})
|
||
|
||
if not dashou_id:
|
||
return Response({'code': 400, 'msg': '订单无接单打手'})
|
||
|
||
# 重复检查
|
||
if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=my_dingdan_id).exists():
|
||
return Response({'code': 0, 'msg': '已存在罚单'})
|
||
|
||
# 创建罚单
|
||
Fadan.objects.create(
|
||
beichufa_id=dashou_id,
|
||
shenqing_chufa=operator,
|
||
shenfen=1,
|
||
chufaliyou=chufaliyou,
|
||
fakuanjine=fakuanjine,
|
||
guanliandingdan_id=my_dingdan_id,
|
||
zhuangtai=1,
|
||
yingxiang_qiangdan=yingxiang_qiangdan,
|
||
shenqingren_shenfen=shenqingren_shenfen,
|
||
partner_club_id=club_id,
|
||
)
|
||
logger.info(f"接收对方罚款成功: 订单 {my_dingdan_id}, 打手 {dashou_id}")
|
||
return Response({'code': 0, 'msg': 'success'})
|
||
|
||
except Exception as e:
|
||
logger.error(f"接收罚款通知异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
from dingdan.models import Fadan, FadanShensuTupian
|
||
|
||
|
||
# 权限码到身份的映射
|
||
PERMISSION_TO_SHENFEN = {
|
||
'66693a': 1, # 打手
|
||
'66693b': 3, # 商家
|
||
'66693c': 2, # 管事
|
||
'66694c': 4, # 组长
|
||
}
|
||
|
||
# 身份对应的用户扩展关系名
|
||
SHENFEN_PROFILE_MAP = {
|
||
1: 'dashou_profile',
|
||
2: 'guanshi_profile',
|
||
3: 'shop_profile',
|
||
4: 'zuzhang_profile',
|
||
}
|
||
|
||
|
||
def check_fadan_permission(permissions, shenfen):
|
||
"""检查是否有处理指定身份罚单的权限"""
|
||
perm_code = {1: '66693a', 2: '66693c', 3: '66693b', 4: '66694c'}.get(shenfen)
|
||
return perm_code in permissions
|
||
|
||
|
||
class FaKuanTongJiView(APIView):
|
||
"""
|
||
罚款统计接口
|
||
POST /houtai/htglyhqcfsltj
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
try:
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 至少有一个罚款管理权限
|
||
if not any(p in permissions for p in PERMISSION_TO_SHENFEN.keys()):
|
||
return Response({'code': 403, 'msg': '无权限查看罚款统计'})
|
||
|
||
stats = Fadan.objects.aggregate(
|
||
total=Count('id'),
|
||
daijiaona=Count('id', filter=Q(zhuangtai=1)),
|
||
shensuzhong=Count('id', filter=Q(zhuangtai=3)),
|
||
yijiaona=Count('id', filter=Q(zhuangtai=2)),
|
||
yibohui=Count('id', filter=Q(zhuangtai=4)),
|
||
)
|
||
data = {k: v or 0 for k, v in stats.items()}
|
||
return Response({'code': 0, 'msg': '成功', 'data': data})
|
||
except Exception as e:
|
||
logger.error(f"罚款统计异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
class FaKuanLieBiaoView(APIView):
|
||
"""
|
||
罚款列表接口
|
||
POST /houtai/hthqfklb
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
try:
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if not any(p in permissions for p in PERMISSION_TO_SHENFEN.keys()):
|
||
return Response({'code': 403, 'msg': '无权限查看罚款列表'})
|
||
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 20))
|
||
if page_size > 100:
|
||
page_size = 100
|
||
|
||
qs = Fadan.objects.all()
|
||
|
||
# 状态筛选
|
||
zhuangtai = request.data.get('zhuangtai')
|
||
if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]:
|
||
qs = qs.filter(zhuangtai=int(zhuangtai))
|
||
|
||
# 被处罚人ID
|
||
beichufa_id = request.data.get('beichufa_id', '').strip()
|
||
if beichufa_id:
|
||
qs = qs.filter(beichufa_id=beichufa_id)
|
||
|
||
# 被处罚人身份
|
||
beichufa_shenfen = request.data.get('beichufa_shenfen')
|
||
if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]:
|
||
qs = qs.filter(shenfen=int(beichufa_shenfen))
|
||
|
||
# 模糊搜索(订单ID、被罚人ID、申请人ID)
|
||
sousuo = request.data.get('sousuo', '').strip()
|
||
if sousuo:
|
||
qs = qs.filter(
|
||
Q(guanliandingdan_id__icontains=sousuo) |
|
||
Q(beichufa_id__icontains=sousuo) |
|
||
Q(shenqing_chufa__icontains=sousuo)
|
||
)
|
||
|
||
qs = qs.order_by('-create_time')
|
||
paginator = Paginator(qs, page_size)
|
||
try:
|
||
page_obj = paginator.page(page)
|
||
except Exception:
|
||
return Response({
|
||
'code': 0, 'msg': '成功',
|
||
'data': {'list': [], 'total': 0, 'page': page, 'page_size': page_size}
|
||
})
|
||
|
||
records = list(page_obj)
|
||
fadan_ids = [r.id for r in records]
|
||
|
||
# 批量获取图片并分组
|
||
zhengju_map = {}
|
||
shensu_map = {}
|
||
if fadan_ids:
|
||
tupians = FadanShensuTupian.objects.filter(
|
||
fadan_id__in=fadan_ids
|
||
).values('fadan_id', 'tupian_url', 'yongtu')
|
||
for t in tupians:
|
||
fid = t['fadan_id']
|
||
url = t['tupian_url']
|
||
if t['yongtu'] == 1:
|
||
zhengju_map.setdefault(fid, []).append(url)
|
||
else:
|
||
shensu_map.setdefault(fid, []).append(url)
|
||
|
||
# 组装列表数据
|
||
results = []
|
||
for r in records:
|
||
results.append({
|
||
'id': r.id,
|
||
'beichufa_id': r.beichufa_id,
|
||
'shenfen': r.shenfen,
|
||
'shenfen_text': dict(Fadan._meta.get_field('shenfen').choices).get(r.shenfen, ''),
|
||
'shenqing_chufa': r.shenqing_chufa or '',
|
||
'shenqingren_shenfen': r.shenqingren_shenfen,
|
||
'chufaliyou': r.chufaliyou or '',
|
||
'fakuanjine': str(r.fakuanjine),
|
||
'guanliandingdan_id': r.guanliandingdan_id or '',
|
||
'zhuangtai': r.zhuangtai,
|
||
'yingxiang_qiangdan': r.yingxiang_qiangdan,
|
||
'shensuliyou': r.shensuliyou or '',
|
||
'bohuiliyou': r.bohuiliyou or '',
|
||
'chulizhe': r.chulizhe or '',
|
||
'chulizhe_shenfen': r.chulizhe_shenfen,
|
||
'zhengju_tupian': zhengju_map.get(r.id, []),
|
||
'shensu_tupian': shensu_map.get(r.id, []),
|
||
'create_time': r.create_time.strftime('%Y-%m-%d %H:%M:%S') if r.create_time else '',
|
||
'update_time': r.update_time.strftime('%Y-%m-%d %H:%M:%S') if r.update_time else '',
|
||
})
|
||
|
||
return Response({
|
||
'code': 0, 'msg': '成功',
|
||
'data': {
|
||
'list': results,
|
||
'total': paginator.count,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"罚款列表异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
class FaKuanChuLiView(APIView):
|
||
"""
|
||
处理罚款申诉(同意/拒绝)
|
||
POST /houtai/glyclfk
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username = request.data.get('username', '').strip()
|
||
fadan_id = request.data.get('fadan_id')
|
||
action = request.data.get('action', '').strip()
|
||
chuli_liyou = request.data.get('chuli_liyou', '').strip()
|
||
|
||
if not all([username, fadan_id, action]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
with transaction.atomic():
|
||
try:
|
||
fadan = Fadan.objects.select_for_update().get(id=fadan_id)
|
||
except Fadan.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '罚单不存在'})
|
||
|
||
if fadan.zhuangtai != 3:
|
||
return Response({'code': 400, 'msg': '当前状态不可处理'})
|
||
|
||
if not check_fadan_permission(permissions, fadan.shenfen):
|
||
return Response({'code': 403, 'msg': '无权限处理该类罚单'})
|
||
|
||
# 处理上传的图片
|
||
uploaded_urls = []
|
||
files = request.FILES.getlist('tupian') or []
|
||
if not files:
|
||
single = request.FILES.get('tupian')
|
||
if single:
|
||
files = [single]
|
||
for file in files:
|
||
valid, msg = validate_image(file)
|
||
if not valid:
|
||
return Response({'code': 400, 'msg': msg})
|
||
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
|
||
file_name = f"chufa_{fadan_id}_{int(time.time() * 1000)}.{ext}"
|
||
file_path = f"chufatupian/kechufatupian/{file_name}"
|
||
url = upload_to_oss(file, file_path)
|
||
if not url:
|
||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||
uploaded_urls.append(file_path)
|
||
|
||
# 更新状态
|
||
if action == 'agree': # 同意申诉 → 驳回处罚
|
||
fadan.zhuangtai = 4
|
||
fadan.bohuiliyou = chuli_liyou
|
||
elif action == 'reject': # 拒绝申诉 → 回到待缴纳
|
||
fadan.zhuangtai = 1
|
||
fadan.bohuiliyou = chuli_liyou
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的操作类型'})
|
||
|
||
# 记录处理者(使用 request.user 的 phone)
|
||
fadan.chulizhe = request.user.phone
|
||
fadan.chulizhe_shenfen = 2 # 这里假设处理者是售后身份,可根据实际业务获取
|
||
fadan.save()
|
||
|
||
# 保存图片(证据图片,用途1)
|
||
for relative_url in uploaded_urls:
|
||
FadanShensuTupian.objects.create(
|
||
fadan_id=fadan_id,
|
||
beichufa_id=fadan.beichufa_id,
|
||
tupian_url=relative_url,
|
||
yongtu=1
|
||
)
|
||
|
||
logger.info(f"罚款处理成功: {fadan_id}, 操作: {action}")
|
||
return Response({'code': 0, 'msg': '处理成功'})
|
||
|
||
except Exception as e:
|
||
logger.error(f"罚款处理异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
class FaKuanChuangJianView(APIView):
|
||
"""
|
||
管理员创建罚款
|
||
POST /houtai/htfksc
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username = request.data.get('username', '').strip()
|
||
beichufa_id = request.data.get('beichufa_id', '').strip()
|
||
shenfen = request.data.get('shenfen')
|
||
chufaliyou = request.data.get('chufaliyou', '').strip()
|
||
fakuanjine = request.data.get('fakuanjine')
|
||
yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1)
|
||
|
||
if not all([username, beichufa_id, shenfen, chufaliyou, fakuanjine]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
try:
|
||
shenfen = int(shenfen)
|
||
fakuanjine = float(fakuanjine)
|
||
except (ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': '参数格式错误'})
|
||
|
||
if shenfen not in [1, 2, 3, 4]:
|
||
return Response({'code': 400, 'msg': '无效的被罚人身份'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
if not check_fadan_permission(permissions, shenfen):
|
||
return Response({'code': 403, 'msg': '无权限创建该类罚单'})
|
||
|
||
# 验证用户是否存在对应的扩展表
|
||
try:
|
||
user = UserMain.objects.get(yonghuid=beichufa_id)
|
||
profile_attr = SHENFEN_PROFILE_MAP.get(shenfen)
|
||
if not profile_attr or not hasattr(user, profile_attr):
|
||
return Response({'code': 400, 'msg': '该用户不是所选身份'})
|
||
except UserMain.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不存在'})
|
||
|
||
# 处理图片上传
|
||
uploaded_urls = []
|
||
files = request.FILES.getlist('tupian') or []
|
||
if not files:
|
||
single = request.FILES.get('tupian')
|
||
if single:
|
||
files = [single]
|
||
for file in files:
|
||
valid, msg = validate_image(file)
|
||
if not valid:
|
||
return Response({'code': 400, 'msg': msg})
|
||
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
|
||
file_name = f"chufa_{int(time.time() * 1000)}.{ext}"
|
||
file_path = f"chufatupian/kechufatupian/{file_name}"
|
||
url = upload_to_oss(file, file_path)
|
||
if not url:
|
||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||
uploaded_urls.append(file_path)
|
||
|
||
with transaction.atomic():
|
||
fadan = Fadan.objects.create(
|
||
beichufa_id=beichufa_id,
|
||
shenfen=shenfen,
|
||
shenqing_chufa=request.user.phone, # 申请人(客服)手机号
|
||
shenqingren_shenfen=2, # 售后身份
|
||
chufaliyou=chufaliyou,
|
||
fakuanjine=fakuanjine,
|
||
yingxiang_qiangdan=yingxiang_qiangdan,
|
||
zhuangtai=1, # 待缴纳
|
||
)
|
||
for relative_url in uploaded_urls:
|
||
FadanShensuTupian.objects.create(
|
||
fadan=fadan,
|
||
beichufa_id=beichufa_id,
|
||
tupian_url=relative_url,
|
||
yongtu=1
|
||
)
|
||
|
||
logger.info(f"罚款创建成功: {fadan.id}")
|
||
return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': fadan.id}})
|
||
|
||
except Exception as e:
|
||
logger.error(f"创建罚款异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
|
||
|
||
|
||
# 与前端约定的聊天权限码
|
||
CHAT_PERM_CODES = ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4']
|
||
|
||
class KefuChatPermissionsView(APIView):
|
||
"""
|
||
获取客服聊天权限及GoEasy配置
|
||
POST /houtai/kfhqltqx
|
||
Body: { "phone": "客服手机号" }
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 从前端获取传递的账号,用于防越权校验
|
||
username_from_frontend = request.data.get('phone', None)
|
||
|
||
# 2. 调用公共验证方法。
|
||
# 返回 (kefu_obj, permissions) 或 (None, Response)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
|
||
# 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值(错误Response)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
# ---------- 验证通过,permissions 已经是权限码列表 ----------
|
||
# 4. 检查是否拥有至少一个聊天权限
|
||
has_chat_perm = any(p in CHAT_PERM_CODES for p in permissions)
|
||
|
||
# 5. 若有聊天权限,查询我方俱乐部的 chat_api_key
|
||
goeasy_appkey = ''
|
||
if has_chat_perm:
|
||
config = ClubConfig.objects.filter(is_self=1).first()
|
||
if config:
|
||
goeasy_appkey = config.chat_api_key or ''
|
||
|
||
# 6. 返回数据
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'permissions': permissions,
|
||
'goeasy_appkey': goeasy_appkey
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class HqbkxxView(APIView):
|
||
"""
|
||
获取板块配置所需全部数据
|
||
POST /houtai/hqbkxx
|
||
Body: { "phone": "管理员手机号" }
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 从前端获取传递的账号,用于防越权校验
|
||
username_from_frontend = request.data.get('phone', None)
|
||
|
||
# 2. 调用公共验证方法
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
|
||
# 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
# ---------- 验证通过 ----------
|
||
# 4. 检查是否有板块管理权限
|
||
if 'bankuai' not in permissions:
|
||
return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
|
||
|
||
# 5. 查询所有板块
|
||
bankuai_list = []
|
||
for bk in Bankuai.objects.all():
|
||
bankuai_list.append({
|
||
'bankuai_id': bk.bankuai_id,
|
||
'mingcheng': bk.mingcheng,
|
||
'create_time': bk.create_time.strftime('%Y-%m-%d %H:%M:%S') if bk.create_time else ''
|
||
})
|
||
|
||
# 6. 查询所有商品类型
|
||
shangpin_leixing_list = []
|
||
for sp in ShangpinLeixing.objects.all():
|
||
shangpin_leixing_list.append({
|
||
'id': sp.id,
|
||
'jieshao': sp.jieshao,
|
||
'tupian_url': sp.tupian_url or '',
|
||
'bankuai_id': sp.bankuai_id
|
||
})
|
||
|
||
# 7. 查询所有会员
|
||
huiyuan_list = []
|
||
for hy in Huiyuan.objects.all():
|
||
huiyuan_list.append({
|
||
'huiyuan_id': hy.huiyuan_id,
|
||
'jieshao': hy.jieshao,
|
||
'bankuai_id': hy.bankuai_id
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'bankuai': bankuai_list,
|
||
'shangpin_leixing': shangpin_leixing_list,
|
||
'huiyuan': huiyuan_list
|
||
}
|
||
})
|
||
|
||
|
||
class BkxgView(APIView):
|
||
"""
|
||
板块增删改统一接口
|
||
POST /houtai/bkxg
|
||
Body: {
|
||
"phone": "管理员手机号",
|
||
"action": "create | update | delete | add_items | remove_items",
|
||
"bankuai_id": int,
|
||
"bankuai_name": str,
|
||
"item_type": "shangpin_leixing | huiyuan",
|
||
"item_ids": [1,2,3]
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 从前端获取传递的账号
|
||
username_from_frontend = request.data.get('phone', None)
|
||
|
||
# 2. 调用公共验证方法
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
|
||
# 3. 验证失败直接返回
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
# 4. 检查板块管理权限
|
||
if 'bankuai' not in permissions:
|
||
return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
|
||
|
||
# 5. 获取操作类型
|
||
action = request.data.get('action')
|
||
if not action:
|
||
return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
|
||
|
||
# ==================== 创建板块 ====================
|
||
if action == 'create':
|
||
bankuai_name = request.data.get('bankuai_name', '').strip()
|
||
if not bankuai_name:
|
||
return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
|
||
if Bankuai.objects.filter(mingcheng=bankuai_name).exists():
|
||
return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
|
||
|
||
shangpin_ids = request.data.get('shangpin_ids', [])
|
||
huiyuan_ids = request.data.get('huiyuan_ids', [])
|
||
|
||
with transaction.atomic():
|
||
new_bk = Bankuai.objects.create(mingcheng=bankuai_name)
|
||
if shangpin_ids:
|
||
ShangpinLeixing.objects.filter(
|
||
id__in=shangpin_ids,
|
||
bankuai__isnull=True
|
||
).update(bankuai=new_bk)
|
||
if huiyuan_ids:
|
||
Huiyuan.objects.filter(
|
||
huiyuan_id__in=huiyuan_ids,
|
||
bankuai__isnull=True
|
||
).update(bankuai=new_bk)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '创建成功',
|
||
'data': {'bankuai_id': new_bk.bankuai_id}
|
||
})
|
||
|
||
# ==================== 以下操作需要 bankuai_id ====================
|
||
bankuai_id = request.data.get('bankuai_id')
|
||
if not bankuai_id:
|
||
return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400)
|
||
|
||
try:
|
||
bk = Bankuai.objects.get(bankuai_id=bankuai_id)
|
||
except Bankuai.DoesNotExist:
|
||
return Response({'code': 1, 'msg': '板块不存在'}, status=404)
|
||
|
||
# ==================== 修改板块名称 ====================
|
||
if action == 'update':
|
||
new_name = request.data.get('bankuai_name', '').strip()
|
||
if not new_name:
|
||
return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
|
||
if Bankuai.objects.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists():
|
||
return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
|
||
bk.mingcheng = new_name
|
||
bk.save()
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
|
||
# ==================== 删除板块 ====================
|
||
elif action == 'delete':
|
||
with transaction.atomic():
|
||
ShangpinLeixing.objects.filter(bankuai=bk).update(bankuai=None)
|
||
Huiyuan.objects.filter(bankuai=bk).update(bankuai=None)
|
||
bk.delete()
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
# ==================== 添加绑定 ====================
|
||
elif action == 'add_items':
|
||
item_type = request.data.get('item_type')
|
||
item_ids = request.data.get('item_ids', [])
|
||
if not item_type or not item_ids:
|
||
return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
|
||
|
||
if item_type == 'shangpin_leixing':
|
||
ShangpinLeixing.objects.filter(
|
||
id__in=item_ids,
|
||
bankuai__isnull=True
|
||
).update(bankuai=bk)
|
||
elif item_type == 'huiyuan':
|
||
Huiyuan.objects.filter(
|
||
huiyuan_id__in=item_ids,
|
||
bankuai__isnull=True
|
||
).update(bankuai=bk)
|
||
else:
|
||
return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
|
||
return Response({'code': 0, 'msg': '添加成功'})
|
||
|
||
# ==================== 移除绑定 ====================
|
||
elif action == 'remove_items':
|
||
item_type = request.data.get('item_type')
|
||
item_ids = request.data.get('item_ids', [])
|
||
if not item_type or not item_ids:
|
||
return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
|
||
|
||
if item_type == 'shangpin_leixing':
|
||
ShangpinLeixing.objects.filter(
|
||
id__in=item_ids,
|
||
bankuai=bk
|
||
).update(bankuai=None)
|
||
elif item_type == 'huiyuan':
|
||
Huiyuan.objects.filter(
|
||
huiyuan_id__in=item_ids,
|
||
bankuai=bk
|
||
).update(bankuai=None)
|
||
else:
|
||
return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
|
||
return Response({'code': 0, 'msg': '移除成功'})
|
||
|
||
else:
|
||
return Response({'code': 1, 'msg': '未知操作'}, status=400)
|
||
|
||
|
||
|
||
|
||
|
||
from rest_framework.permissions import IsAuthenticated
|
||
from rest_framework.views import APIView
|
||
from rest_framework.response import Response
|
||
from django.db.models import Sum
|
||
from django.utils import timezone
|
||
from datetime import date, timedelta
|
||
from peizhi.models import (
|
||
DailyIncomeStat, DailyPayoutStat
|
||
)
|
||
from shangpin.models import (
|
||
Czjilu
|
||
)
|
||
from .utils import verify_kefu_permission
|
||
|
||
|
||
|
||
class CaiwuView(APIView):
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
if 'caiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
|
||
|
||
today = date.today()
|
||
today_start = datetime.combine(today, datetime.min.time())
|
||
today_end = today_start + timedelta(days=1)
|
||
|
||
# 今日收入
|
||
today_income = DailyIncomeStat.objects.filter(date=today).first()
|
||
today_income_amount = float(today_income.total_amount) if today_income else 0.00
|
||
today_income_count = today_income.total_count if today_income else 0
|
||
|
||
# 今日支出
|
||
today_payout = DailyPayoutStat.objects.filter(date=today).first()
|
||
today_payout_amount = float(today_payout.total_amount) if today_payout else 0.00
|
||
today_payout_count = today_payout.total_count if today_payout else 0
|
||
|
||
# 今日订单(已付款及之后的状态)
|
||
today_orders = Dingdan.objects.filter(
|
||
create_time__gte=today_start,
|
||
create_time__lt=today_end,
|
||
zhuangtai__in=[1,2,3,4,5,6,7,8]
|
||
)
|
||
today_order_count = today_orders.count()
|
||
today_order_amount = float(today_orders.aggregate(total=Sum('jine'))['total'] or 0.00)
|
||
|
||
# 今日成交(zhuangtai=3)
|
||
today_completed = Dingdan.objects.filter(
|
||
create_time__gte=today_start,
|
||
create_time__lt=today_end,
|
||
zhuangtai=3
|
||
)
|
||
today_completed_count = today_completed.count()
|
||
today_completed_amount = float(today_completed.aggregate(total=Sum('jine'))['total'] or 0.00)
|
||
|
||
# 今日退款(zhuangtai=5)
|
||
today_tuikuan = Dingdan.objects.filter(
|
||
create_time__gte=today_start,
|
||
create_time__lt=today_end,
|
||
zhuangtai=5
|
||
)
|
||
today_tuikuan_count = today_tuikuan.count()
|
||
today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('jine'))['total'] or 0.00)
|
||
|
||
# 今日充值(会员充值,leixing=1, zhuangtai=3)
|
||
today_chongzhi = Czjilu.objects.filter(
|
||
create_time__gte=today_start,
|
||
create_time__lt=today_end,
|
||
leixing=1,
|
||
zhuangtai=3
|
||
)
|
||
today_chongzhi_count = today_chongzhi.count()
|
||
today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00)
|
||
|
||
# 今日新增会员:从购买记录表统计今天创建的用户(去重)
|
||
new_huiyuan_user_ids = Huiyuangoumai.objects.filter(
|
||
create_time__gte=today_start,
|
||
create_time__lt=today_end
|
||
).values_list('yonghu_id', flat=True).distinct()
|
||
today_new_huiyuan = len(new_huiyuan_user_ids)
|
||
|
||
# 新增会员充值总额(这些用户今天的会员充值金额)
|
||
today_new_huiyuan_amount = 0.0
|
||
if new_huiyuan_user_ids:
|
||
amount_sum = Czjilu.objects.filter(
|
||
create_time__gte=today_start,
|
||
create_time__lt=today_end,
|
||
yonghuid__in=new_huiyuan_user_ids,
|
||
leixing=1,
|
||
zhuangtai=3
|
||
).aggregate(total=Sum('jine'))
|
||
today_new_huiyuan_amount = float(amount_sum['total'] or 0.0)
|
||
|
||
# 各角色可提现余额总和
|
||
all_guanshi_yue = float(UserGuanshi.objects.aggregate(total=Sum('yue'))['total'] or 0.00)
|
||
all_dashou_yue = float(UserDashou.objects.aggregate(total=Sum('yue'))['total'] or 0.00)
|
||
all_zuzhang_yue = float(UserZuzhang.objects.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00)
|
||
all_shangjia_yue = float(UserShangjia.objects.aggregate(total=Sum('yue'))['total'] or 0.00)
|
||
|
||
# 平台总收支及利润
|
||
total_income = float(DailyIncomeStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00)
|
||
total_payout = float(DailyPayoutStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00)
|
||
platform_profit = round(total_income - total_payout, 2)
|
||
|
||
# 近30天每日收支明细
|
||
daily_stats = []
|
||
income_qs = DailyIncomeStat.objects.order_by('-date')[:30]
|
||
payout_dict = {
|
||
str(p.date): float(p.total_amount)
|
||
for p in DailyPayoutStat.objects.filter(date__gte=today - timedelta(days=30))
|
||
}
|
||
for inc in income_qs:
|
||
d = str(inc.date)
|
||
daily_stats.append({
|
||
'date': d,
|
||
'income': float(inc.total_amount),
|
||
'income_count': inc.total_count,
|
||
'payout': payout_dict.get(d, 0.00),
|
||
'profit': round(float(inc.total_amount) - payout_dict.get(d, 0.00), 2)
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'today_income': round(today_income_amount, 2),
|
||
'today_income_count': today_income_count,
|
||
'today_payout': round(today_payout_amount, 2),
|
||
'today_payout_count': today_payout_count,
|
||
'today_order_count': today_order_count,
|
||
'today_order_amount': round(today_order_amount, 2),
|
||
'today_completed_count': today_completed_count,
|
||
'today_completed_amount': round(today_completed_amount, 2),
|
||
'today_tuikuan_count': today_tuikuan_count,
|
||
'today_tuikuan_amount': round(today_tuikuan_amount, 2),
|
||
'today_chongzhi_count': today_chongzhi_count,
|
||
'today_chongzhi_amount': round(today_chongzhi_amount, 2),
|
||
'today_new_huiyuan': today_new_huiyuan,
|
||
'today_new_huiyuan_amount': round(today_new_huiyuan_amount, 2),
|
||
'all_guanshi_yue': round(all_guanshi_yue, 2),
|
||
'all_dashou_yue': round(all_dashou_yue, 2),
|
||
'all_zuzhang_yue': round(all_zuzhang_yue, 2),
|
||
'all_shangjia_yue': round(all_shangjia_yue, 2),
|
||
'total_income': round(total_income, 2),
|
||
'total_payout': round(total_payout, 2),
|
||
'platform_profit': platform_profit,
|
||
'daily_stats': daily_stats
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class CwhybkhqView(APIView):
|
||
"""
|
||
获取所有板块及对应会员列表
|
||
POST /houtai/cwhybkhq
|
||
Body: { "phone": "管理员手机号" }
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
if 'huiyuancaiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
|
||
|
||
# 查询所有板块
|
||
bankuai_list = []
|
||
for bk in Bankuai.objects.all():
|
||
# 获取该板块下所有会员
|
||
members = Huiyuan.objects.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id')
|
||
bankuai_list.append({
|
||
'bankuai_id': bk.bankuai_id,
|
||
'mingcheng': bk.mingcheng,
|
||
'members': list(members)
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': bankuai_list
|
||
})
|
||
|
||
|
||
|
||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||
from shangpin.models import Czjilu, Gsfenhong
|
||
from .utils import verify_kefu_permission
|
||
|
||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||
from shangpin.models import Czjilu, Gsfenhong # 请根据实际路径调整
|
||
class HybkjtsjView(APIView):
|
||
"""
|
||
会员详细统计数据
|
||
POST /houtai/hybkjtsj
|
||
Body: {
|
||
"phone": "管理员手机号",
|
||
"huiyuan_id": "会员ID",
|
||
"granularity": "day" | "month" | "year",
|
||
"year": 2026,
|
||
"month": 5,
|
||
"include_guanshi": true,
|
||
"include_zuzhang": true,
|
||
"summary_date": "2026-05-16"
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
if 'huiyuancaiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
|
||
|
||
huiyuan_id = request.data.get('huiyuan_id')
|
||
granularity = request.data.get('granularity', 'day')
|
||
year = request.data.get('year')
|
||
month = request.data.get('month')
|
||
include_guanshi = request.data.get('include_guanshi', False)
|
||
include_zuzhang = request.data.get('include_zuzhang', False)
|
||
summary_date = request.data.get('summary_date')
|
||
|
||
if not huiyuan_id:
|
||
return Response({'code': 1, 'msg': '缺少会员ID'}, status=400)
|
||
|
||
# 🔧 直接使用 date.today() 避免模块名冲突
|
||
from datetime import date, datetime, timedelta
|
||
now = date.today()
|
||
|
||
# ---------- 构造时间范围 ----------
|
||
if granularity == 'day':
|
||
if not year or not month:
|
||
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||
start_date = date(year, month, 1)
|
||
if month == 12:
|
||
end_date = date(year + 1, 1, 1)
|
||
else:
|
||
end_date = date(year, month + 1, 1)
|
||
trunc_func = TruncDate('create_time')
|
||
if not summary_date:
|
||
summary_date = now.strftime('%Y-%m-%d')
|
||
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
||
elif granularity == 'month':
|
||
if not year:
|
||
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||
start_date = date(year, 1, 1)
|
||
end_date = date(year + 1, 1, 1)
|
||
trunc_func = TruncMonth('create_time')
|
||
if not summary_date:
|
||
summary_date = now.strftime('%Y-%m')
|
||
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
||
elif granularity == 'year':
|
||
year = year or now.year
|
||
start_date = date(year, 1, 1)
|
||
end_date = date(year + 1, 1, 1)
|
||
trunc_func = TruncYear('create_time')
|
||
if not summary_date:
|
||
summary_date = str(year)
|
||
summary_q = date(year, 1, 1)
|
||
else:
|
||
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||
|
||
# 生成 naive datetime(避免 USE_TZ=False 报错)
|
||
start_dt = datetime.combine(start_date, datetime.min.time())
|
||
end_dt = datetime.combine(end_date, datetime.min.time())
|
||
|
||
# ---------- 充值过滤 ----------
|
||
chongzhi_filter = {
|
||
'huiyuan_id': huiyuan_id, # 会员ID筛选
|
||
'leixing': 1,
|
||
'zhuangtai': 3,
|
||
'create_time__gte': start_dt,
|
||
'create_time__lt': end_dt
|
||
}
|
||
|
||
# ---------- 分红过滤 ----------
|
||
fenhong_filter = {
|
||
'huiyuan_id': huiyuan_id, # 分红表会员ID筛选
|
||
'create_time__gte': start_dt,
|
||
'create_time__lt': end_dt
|
||
}
|
||
|
||
# 充值分组查询
|
||
chongzhi_qs = Czjilu.objects.filter(**chongzhi_filter) \
|
||
.annotate(period=trunc_func) \
|
||
.values('period') \
|
||
.annotate(
|
||
chongzhi_count=Count('id'),
|
||
chongzhi_amount=Sum('jine')
|
||
).order_by('period')
|
||
|
||
# 分红分组查询
|
||
fenhong_qs = None
|
||
if include_guanshi or include_zuzhang:
|
||
fenhong_agg = {}
|
||
if include_guanshi:
|
||
fenhong_agg['guanshi_amount'] = Sum('fenhong')
|
||
if include_zuzhang:
|
||
fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong')
|
||
fenhong_qs = Gsfenhong.objects.filter(**fenhong_filter) \
|
||
.annotate(period=trunc_func) \
|
||
.values('period') \
|
||
.annotate(**fenhong_agg) \
|
||
.order_by('period')
|
||
|
||
# 合并数据
|
||
data_map = {}
|
||
for item in chongzhi_qs:
|
||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||
item['period'].strftime('%Y')
|
||
data_map[period_str] = {
|
||
'chongzhi_count': item['chongzhi_count'],
|
||
'chongzhi_amount': float(item['chongzhi_amount'] or 0)
|
||
}
|
||
|
||
if fenhong_qs:
|
||
for item in fenhong_qs:
|
||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||
item['period'].strftime('%Y')
|
||
if period_str not in data_map:
|
||
data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0}
|
||
if include_guanshi:
|
||
data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0)
|
||
if include_zuzhang:
|
||
data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0)
|
||
|
||
time_series = []
|
||
for period_str, vals in data_map.items():
|
||
guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0
|
||
zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0
|
||
vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2)
|
||
vals['date'] = period_str
|
||
time_series.append(vals)
|
||
time_series.sort(key=lambda x: x['date'])
|
||
|
||
# ---------- 汇总数据 ----------
|
||
summary_chongzhi_filter = chongzhi_filter.copy()
|
||
if granularity == 'day':
|
||
summary_chongzhi_filter['create_time__date'] = summary_q
|
||
elif granularity == 'month':
|
||
summary_chongzhi_filter['create_time__year'] = summary_q.year
|
||
summary_chongzhi_filter['create_time__month'] = summary_q.month
|
||
else:
|
||
summary_chongzhi_filter['create_time__year'] = summary_q.year
|
||
|
||
summary_chongzhi = Czjilu.objects.filter(**summary_chongzhi_filter).aggregate(
|
||
total_count=Count('id'),
|
||
total_amount=Sum('jine')
|
||
)
|
||
total_chongzhi_count = summary_chongzhi['total_count'] or 0
|
||
total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0)
|
||
|
||
total_guanshi = 0.0
|
||
total_zuzhang = 0.0
|
||
if include_guanshi or include_zuzhang:
|
||
summary_fenhong_filter = fenhong_filter.copy()
|
||
if granularity == 'day':
|
||
summary_fenhong_filter['create_time__date'] = summary_q
|
||
elif granularity == 'month':
|
||
summary_fenhong_filter['create_time__year'] = summary_q.year
|
||
summary_fenhong_filter['create_time__month'] = summary_q.month
|
||
else:
|
||
summary_fenhong_filter['create_time__year'] = summary_q.year
|
||
|
||
fenhong_agg = {}
|
||
if include_guanshi:
|
||
fenhong_agg['guanshi_sum'] = Sum('fenhong')
|
||
if include_zuzhang:
|
||
fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong')
|
||
summary_fenhong = Gsfenhong.objects.filter(**summary_fenhong_filter).aggregate(**fenhong_agg)
|
||
total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0)
|
||
total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0)
|
||
|
||
total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'time_series': time_series,
|
||
'summary': {
|
||
'total_chongzhi_count': total_chongzhi_count,
|
||
'total_chongzhi_amount': total_chongzhi_amount,
|
||
'total_guanshi_fenhong': total_guanshi,
|
||
'total_zuzhang_fenhong': total_zuzhang,
|
||
'total_shouyi': total_shouyi
|
||
}
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.exception("HybkjtsjView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
|
||
|
||
|
||
class SzxxView(APIView):
|
||
"""
|
||
每日收支详细统计数据
|
||
POST /houtai/szxx
|
||
Body: {
|
||
"phone": "管理员手机号",
|
||
"granularity": "day" | "month" | "year",
|
||
"year": 2026, // day/month 时必填
|
||
"month": 5, // day 时必填
|
||
"summary_date": "2026-05-16" // 可选,格式随颗粒度
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 避免模块名冲突,局部导入
|
||
from datetime import date, datetime
|
||
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
if 'caiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
|
||
|
||
granularity = request.data.get('granularity', 'day')
|
||
year = request.data.get('year')
|
||
month = request.data.get('month')
|
||
summary_date = request.data.get('summary_date')
|
||
|
||
now = date.today()
|
||
|
||
# ---------- 时间范围与分组字段 ----------
|
||
if granularity == 'day':
|
||
if not year or not month:
|
||
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||
# 分组字段
|
||
group_field = 'day'
|
||
# 过滤条件
|
||
income_filter = {'year': year, 'month': month}
|
||
payout_filter = {'year': year, 'month': month}
|
||
# 默认汇总日期
|
||
if not summary_date:
|
||
summary_date = now.strftime('%Y-%m-%d')
|
||
summary_parts = summary_date.split('-')
|
||
summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1]), 'day': int(summary_parts[2])}
|
||
# 用于构造返回的日期字符串
|
||
date_format = '%Y-%m-%d'
|
||
|
||
elif granularity == 'month':
|
||
if not year:
|
||
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||
group_field = 'month'
|
||
income_filter = {'year': year}
|
||
payout_filter = {'year': year}
|
||
if not summary_date:
|
||
summary_date = now.strftime('%Y-%m')
|
||
summary_parts = summary_date.split('-')
|
||
summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1])}
|
||
date_format = '%Y-%m'
|
||
|
||
elif granularity == 'year':
|
||
group_field = 'year'
|
||
income_filter = {}
|
||
payout_filter = {}
|
||
if not summary_date:
|
||
summary_date = str(now.year)
|
||
summary_filter = {'year': int(summary_date)}
|
||
date_format = '%Y'
|
||
else:
|
||
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||
|
||
# ---------- 收入分组查询 ----------
|
||
income_qs = DailyIncomeStat.objects.filter(**income_filter) \
|
||
.values(group_field) \
|
||
.annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \
|
||
.order_by(group_field)
|
||
|
||
# ---------- 支出分组查询 ----------
|
||
payout_qs = DailyPayoutStat.objects.filter(**payout_filter) \
|
||
.values(group_field) \
|
||
.annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) \
|
||
.order_by(group_field)
|
||
|
||
# ---------- 合并数据 ----------
|
||
data_map = {}
|
||
for item in income_qs:
|
||
key = item[group_field]
|
||
data_map[key] = {
|
||
'income': float(item['total_income'] or 0),
|
||
'income_count': item['total_count'] or 0,
|
||
'payout': 0.0,
|
||
'payout_count': 0
|
||
}
|
||
|
||
for item in payout_qs:
|
||
key = item[group_field]
|
||
if key not in data_map:
|
||
data_map[key] = {'income': 0.0, 'income_count': 0, 'payout': 0.0, 'payout_count': 0}
|
||
data_map[key]['payout'] = float(item['total_payout'] or 0)
|
||
data_map[key]['payout_count'] = item['total_count'] or 0
|
||
|
||
time_series = []
|
||
for key, vals in data_map.items():
|
||
# 构造日期字符串
|
||
if granularity == 'day':
|
||
date_str = f"{year}-{month:02d}-{key:02d}"
|
||
elif granularity == 'month':
|
||
date_str = f"{year}-{key:02d}"
|
||
else:
|
||
date_str = str(key)
|
||
profit = round(vals['income'] - vals['payout'], 2)
|
||
time_series.append({
|
||
'date': date_str,
|
||
'income': vals['income'],
|
||
'payout': vals['payout'],
|
||
'profit': profit
|
||
})
|
||
time_series.sort(key=lambda x: x['date'])
|
||
|
||
# ---------- 汇总数据 ----------
|
||
income_summary = DailyIncomeStat.objects.filter(**summary_filter).aggregate(
|
||
total_income=Sum('total_amount'), total_count=Sum('total_count')
|
||
)
|
||
payout_summary = DailyPayoutStat.objects.filter(**summary_filter).aggregate(
|
||
total_payout=Sum('total_amount'), total_count=Sum('total_count')
|
||
)
|
||
total_income = float(income_summary['total_income'] or 0)
|
||
total_payout = float(payout_summary['total_payout'] or 0)
|
||
total_profit = round(total_income - total_payout, 2)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'time_series': time_series,
|
||
'summary': {
|
||
'total_income': total_income,
|
||
'total_income_count': income_summary['total_count'] or 0,
|
||
'total_payout': total_payout,
|
||
'total_payout_count': payout_summary['total_count'] or 0,
|
||
'total_profit': total_profit
|
||
}
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.exception("SzxxView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class CwddhqlxView(APIView):
|
||
"""
|
||
获取订单类型(商品类型)用于筛选
|
||
POST /houtai/cwddhqlx
|
||
Body: { "phone": "管理员手机号" }
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
if 'dingdancaiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
|
||
|
||
# 获取非审核状态(shenhezhuangtai=1)的商品类型,只返回ID和名称
|
||
types = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values('id', 'jieshao')
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': list(types)
|
||
})
|
||
except Exception as e:
|
||
logger.exception("CwddhqlxView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
|
||
class CwhqjtddsjView(APIView):
|
||
"""
|
||
订单详细统计数据(修正版)
|
||
POST /houtai/cwhqjtddsj
|
||
Body: {
|
||
"phone": "管理员手机号",
|
||
"granularity": "day" | "month" | "year",
|
||
"year": 2026,
|
||
"month": 5,
|
||
"type_id": 1, # 商品类型ID,0表示全部
|
||
"order_source": 1, # 1=平台单,2=商家单,0=全部
|
||
"summary_date": "2026-05-16"
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
if 'dingdancaiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
|
||
|
||
granularity = request.data.get('granularity', 'day')
|
||
year = request.data.get('year')
|
||
month = request.data.get('month')
|
||
type_id = request.data.get('type_id', 0)
|
||
order_source = request.data.get('order_source', 0)
|
||
summary_date = request.data.get('summary_date')
|
||
|
||
now = date.today()
|
||
|
||
# ---------- 时间范围 ----------
|
||
if granularity == 'day':
|
||
if not year or not month:
|
||
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||
start_date = date(year, month, 1)
|
||
end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
|
||
trunc_func = TruncDate('create_time')
|
||
if not summary_date:
|
||
summary_date = now.strftime('%Y-%m-%d')
|
||
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
||
elif granularity == 'month':
|
||
if not year:
|
||
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||
start_date = date(year, 1, 1)
|
||
end_date = date(year + 1, 1, 1)
|
||
trunc_func = TruncMonth('create_time')
|
||
if not summary_date:
|
||
summary_date = now.strftime('%Y-%m')
|
||
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
||
elif granularity == 'year':
|
||
year = year or now.year
|
||
start_date = date(year, 1, 1)
|
||
end_date = date(year + 1, 1, 1)
|
||
trunc_func = TruncYear('create_time')
|
||
if not summary_date:
|
||
summary_date = str(year)
|
||
summary_q = date(year, 1, 1)
|
||
else:
|
||
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||
|
||
start_dt = datetime.combine(start_date, datetime.min.time())
|
||
end_dt = datetime.combine(end_date, datetime.min.time())
|
||
|
||
# ---------- 基础过滤 ----------
|
||
filters = {
|
||
'create_time__gte': start_dt,
|
||
'create_time__lt': end_dt,
|
||
'zhuangtai__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态
|
||
}
|
||
if int(type_id) != 0:
|
||
filters['leixing_id'] = type_id
|
||
if int(order_source) in [1, 2]:
|
||
filters['fadan_pingtai'] = order_source
|
||
|
||
# ---------- 时间序列分组查询 ----------
|
||
qs = Dingdan.objects.filter(**filters) \
|
||
.annotate(period=trunc_func) \
|
||
.values('period') \
|
||
.annotate(
|
||
# 订单总量(所有符合条件的状态)
|
||
order_count=Count('id'),
|
||
# 订单总额
|
||
order_amount=Sum('jine'),
|
||
# 成交单量 (zhuangtai=3)
|
||
success_count=Count('id', filter=Q(zhuangtai=3)),
|
||
# 成交订单总额
|
||
success_amount=Sum('jine', filter=Q(zhuangtai=3)),
|
||
# 退款单量 (zhuangtai=5)
|
||
refund_count=Count('id', filter=Q(zhuangtai=5)),
|
||
# 打手分成总额(仅成交订单)
|
||
dashou_fencheng_sum=Sum('dashou_fencheng', filter=Q(zhuangtai=3)),
|
||
# 平台成交订单的店铺分红
|
||
dianpu_fenhong_sum=Sum('pingtai_kuozhan__dianpu_shouyi',
|
||
filter=Q(zhuangtai=3, fadan_pingtai=1)),
|
||
# 成交订单中:平台单金额、商家单金额、各自打手分成
|
||
platform_success_amount=Sum('jine', filter=Q(zhuangtai=3, fadan_pingtai=1)),
|
||
merchant_success_amount=Sum('jine', filter=Q(zhuangtai=3, fadan_pingtai=2)),
|
||
platform_dashou=Sum('dashou_fencheng', filter=Q(zhuangtai=3, fadan_pingtai=1)),
|
||
merchant_dashou=Sum('dashou_fencheng', filter=Q(zhuangtai=3, fadan_pingtai=2)),
|
||
).order_by('period')
|
||
|
||
time_series = []
|
||
for item in qs:
|
||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||
item['period'].strftime('%Y')
|
||
|
||
# 平台收益计算
|
||
platform_profit = (item['platform_success_amount'] or 0) \
|
||
- (item['platform_dashou'] or 0) \
|
||
- (item['dianpu_fenhong_sum'] or 0)
|
||
merchant_profit = (item['merchant_success_amount'] or 0) \
|
||
- (item['merchant_dashou'] or 0)
|
||
total_profit = platform_profit + merchant_profit
|
||
|
||
time_series.append({
|
||
'date': period_str,
|
||
'order_count': item['order_count'],
|
||
'order_amount': float(item['order_amount'] or 0),
|
||
'success_count': item['success_count'],
|
||
'success_amount': float(item['success_amount'] or 0),
|
||
'refund_count': item['refund_count'],
|
||
'dashou_fencheng': float(item['dashou_fencheng_sum'] or 0),
|
||
'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0),
|
||
'profit': round(total_profit, 2)
|
||
})
|
||
|
||
# ---------- 汇总数据 ----------
|
||
summary_qs = Dingdan.objects.filter(**filters)
|
||
total_order_count = summary_qs.count()
|
||
total_order_amount = summary_qs.aggregate(s=Sum('jine'))['s'] or 0
|
||
total_success_count = summary_qs.filter(zhuangtai=3).count()
|
||
total_success_amount = summary_qs.filter(zhuangtai=3).aggregate(s=Sum('jine'))['s'] or 0
|
||
total_refund_count = summary_qs.filter(zhuangtai=5).count()
|
||
total_dashou = summary_qs.filter(zhuangtai=3).aggregate(s=Sum('dashou_fencheng'))['s'] or 0
|
||
total_dianpu_fenhong = summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(
|
||
s=Sum('pingtai_kuozhan__dianpu_shouyi'))['s'] or 0
|
||
|
||
platform_amount = summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(s=Sum('jine'))['s'] or 0
|
||
merchant_amount = summary_qs.filter(zhuangtai=3, fadan_pingtai=2).aggregate(s=Sum('jine'))['s'] or 0
|
||
platform_dashou_sum = \
|
||
summary_qs.filter(zhuangtai=3, fadan_pingtai=1).aggregate(s=Sum('dashou_fencheng'))['s'] or 0
|
||
merchant_dashou_sum = \
|
||
summary_qs.filter(zhuangtai=3, fadan_pingtai=2).aggregate(s=Sum('dashou_fencheng'))['s'] or 0
|
||
|
||
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
|
||
profit_merchant = merchant_amount - merchant_dashou_sum
|
||
total_profit = profit_platform + profit_merchant
|
||
|
||
summary = {
|
||
'total_order_count': total_order_count,
|
||
'total_order_amount': round(float(total_order_amount), 2),
|
||
'total_success_count': total_success_count,
|
||
'total_success_amount': round(float(total_success_amount), 2),
|
||
'total_refund_count': total_refund_count,
|
||
'total_dashou_fencheng': round(float(total_dashou), 2),
|
||
'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2),
|
||
'total_profit': round(total_profit, 2)
|
||
}
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'time_series': time_series,
|
||
'summary': summary
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.exception("CwhqjtddsjView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
|
||
|
||
|
||
from dingdan.models import Fadan, FadanFenhong # 罚单和罚款分红表,根据实际路径调整
|
||
|
||
|
||
class CwqtczhqView(APIView):
|
||
"""
|
||
其他充值/罚款数据统计(非会员充值)
|
||
POST /houtai/cwqtczhq
|
||
Body: {
|
||
"phone": "管理员手机号",
|
||
"granularity": "day" | "month" | "year",
|
||
"year": 2026,
|
||
"month": 5, // granularity=day 时必填
|
||
"types": [2,3,4,5] // 充值类型:2=押金,3=积分,4=商家,5=考核;包含 -1 或 [2,3,4,5] 表示全部;罚款始终单独统计(前端控制是否展示)
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
if 'qtczcaiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
|
||
|
||
granularity = request.data.get('granularity', 'day')
|
||
year = request.data.get('year')
|
||
month = request.data.get('month')
|
||
types = request.data.get('types', [2, 3, 4, 5]) # 默认全部非会员类型
|
||
|
||
if isinstance(types, list) and len(types) == 0:
|
||
types = [2, 3, 4, 5]
|
||
|
||
now = date.today()
|
||
|
||
# ---------- 时间范围 ----------
|
||
if granularity == 'day':
|
||
if not year or not month:
|
||
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||
start_date = date(year, month, 1)
|
||
end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
|
||
trunc_func = TruncDate('create_time')
|
||
elif granularity == 'month':
|
||
if not year:
|
||
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||
start_date = date(year, 1, 1)
|
||
end_date = date(year + 1, 1, 1)
|
||
trunc_func = TruncMonth('create_time')
|
||
elif granularity == 'year':
|
||
year = year or now.year
|
||
start_date = date(year, 1, 1)
|
||
end_date = date(year + 1, 1, 1)
|
||
trunc_func = TruncYear('create_time')
|
||
else:
|
||
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||
|
||
start_dt = datetime.combine(start_date, datetime.min.time())
|
||
end_dt = datetime.combine(end_date, datetime.min.time())
|
||
|
||
# ---------- 定义收益计算规则 ----------
|
||
# 商家充值(4)、押金(2)、考核(5):平台收益为 0
|
||
# 积分充值(3):收益 = 充值总额
|
||
# 罚款:收益 = 罚款总额 - 分红总额
|
||
|
||
# 最终返回的时间序列结构:
|
||
# { "2026-05-01": { "2": {count, amount, profit}, "3": {...}, ... } }
|
||
|
||
time_series_map = {} # date_str -> { type_code: {count, amount, profit} }
|
||
|
||
# 公共函数:初始化日期条目
|
||
def ensure_date(ds):
|
||
if ds not in time_series_map:
|
||
time_series_map[ds] = {}
|
||
return time_series_map[ds]
|
||
|
||
# ---------- 1. 处理充值记录表(Czjilu) ----------
|
||
cz_types = [t for t in types if t in [2, 3, 4, 5]] # 只取充值类型
|
||
if cz_types:
|
||
cz_qs = Czjilu.objects.filter(
|
||
create_time__gte=start_dt,
|
||
create_time__lt=end_dt,
|
||
leixing__in=cz_types,
|
||
zhuangtai=3 # 支付成功状态(根据你之前的设置)
|
||
).annotate(
|
||
period=trunc_func
|
||
).values('period', 'leixing').annotate(
|
||
total_count=Count('id'),
|
||
total_amount=Sum('jine')
|
||
).order_by('period', 'leixing')
|
||
|
||
for item in cz_qs:
|
||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||
item['period'].strftime('%Y')
|
||
lx = item['leixing']
|
||
amount = float(item['total_amount'] or 0)
|
||
count = item['total_count']
|
||
# 计算收益
|
||
if lx == 3: # 积分
|
||
profit = amount
|
||
else: # 商家、押金、考核
|
||
profit = 0.0
|
||
|
||
day_data = ensure_date(period_str)
|
||
day_data[str(lx)] = {
|
||
'count': count,
|
||
'amount': amount,
|
||
'profit': profit
|
||
}
|
||
|
||
# ---------- 2. 处理罚款(Fadan) ----------
|
||
# 罚款统计是否展示由前端 types 是否包含 6 决定?约定 types 数组可以包含 6 代表罚款,或不包含则默认返回罚款数据(由前端决定隐藏)
|
||
# 简单处理:只要 types 中包含 6 或不指定时默认返回罚款(因为我们把 types 默认值设为全部,而罚款不是一个 leixing,我们约定前端传递 types 中包含 6 表示需要罚款数据)
|
||
# 但为了保持一致性,我们在 types 数组中用 6 代表“罚款”。接口支持 types = [2,3,4,5,6]。
|
||
# 后端根据 types 是否含有 6 来决定是否统计罚款。
|
||
include_penalty = (6 in types) if isinstance(types, list) else True
|
||
|
||
if include_penalty:
|
||
# 已缴纳罚款记录
|
||
penalty_qs = Fadan.objects.filter(
|
||
create_time__gte=start_dt,
|
||
create_time__lt=end_dt,
|
||
zhuangtai=2 # 已缴纳
|
||
).annotate(
|
||
period=trunc_func
|
||
).values('period').annotate(
|
||
penalty_count=Count('id'),
|
||
penalty_amount=Sum('fakuanjine')
|
||
).order_by('period')
|
||
|
||
# 罚款分红(按创建时间分组)
|
||
fenhong_qs = FadanFenhong.objects.filter(
|
||
create_time__gte=start_dt,
|
||
create_time__lt=end_dt
|
||
).annotate(
|
||
period=trunc_func
|
||
).values('period').annotate(
|
||
fenhong_count=Count('id'),
|
||
fenhong_amount=Sum('ketixian_fenhong')
|
||
).order_by('period')
|
||
|
||
# 填充到 time_series_map
|
||
for item in penalty_qs:
|
||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||
item['period'].strftime('%Y')
|
||
day_data = ensure_date(period_str)
|
||
# 罚款可能之前已有其他类型数据
|
||
penalty_data = day_data.get('6', {'count': 0, 'amount': 0.0, 'profit': 0.0})
|
||
penalty_data['count'] = item['penalty_count']
|
||
penalty_data['amount'] = float(item['penalty_amount'] or 0)
|
||
day_data['6'] = penalty_data
|
||
|
||
for item in fenhong_qs:
|
||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||
item['period'].strftime('%Y')
|
||
day_data = ensure_date(period_str)
|
||
pen = day_data.get('6', {'count': 0, 'amount': 0.0, 'profit': 0.0})
|
||
# 分红字段单独存储
|
||
pen['fenhong_count'] = item['fenhong_count']
|
||
pen['fenhong_amount'] = float(item['fenhong_amount'] or 0)
|
||
# 计算收益 = 罚款金额 - 分红金额
|
||
pen['profit'] = round(pen.get('amount', 0.0) - pen.get('fenhong_amount', 0.0), 2)
|
||
day_data['6'] = pen
|
||
|
||
# 确保没有罚款但有分红的日子也有记录(上面已处理)
|
||
# 还需要对只有分红没有罚款的日子补全罚款数据
|
||
for period_str, day_data in time_series_map.items():
|
||
if '6' in day_data and 'amount' not in day_data['6']:
|
||
day_data['6']['amount'] = 0.0
|
||
day_data['6']['count'] = 0
|
||
day_data['6']['profit'] = round(0.0 - day_data['6'].get('fenhong_amount', 0.0), 2)
|
||
|
||
# ---------- 构造有序 time_series 列表 ----------
|
||
time_series = []
|
||
for period_str in sorted(time_series_map.keys()):
|
||
entry = {'date': period_str, 'types': {}}
|
||
for type_code in sorted(time_series_map[period_str].keys()):
|
||
entry['types'][type_code] = time_series_map[period_str][type_code]
|
||
time_series.append(entry)
|
||
|
||
# ---------- 汇总数据 ----------
|
||
summary = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
|
||
# 汇总各类型数据
|
||
for type_code in [str(t) for t in types]:
|
||
total_count = 0
|
||
total_amount = 0.0
|
||
total_profit = 0.0
|
||
for day in time_series:
|
||
if type_code in day['types']:
|
||
total_count += day['types'][type_code].get('count', 0)
|
||
total_amount += day['types'][type_code].get('amount', 0.0)
|
||
total_profit += day['types'][type_code].get('profit', 0.0)
|
||
summary['by_type'][type_code] = {
|
||
'count': total_count,
|
||
'amount': round(total_amount, 2),
|
||
'profit': round(total_profit, 2)
|
||
}
|
||
summary['total_count'] += total_count
|
||
summary['total_amount'] += total_amount
|
||
summary['total_profit'] += total_profit
|
||
|
||
summary['total_amount'] = round(summary['total_amount'], 2)
|
||
summary['total_profit'] = round(summary['total_profit'], 2)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'time_series': time_series,
|
||
'summary': summary
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.exception("CwqtczhqView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
|
||
|
||
# ==================== 后端接口 ====================
|
||
# 文件:views.py (添加以下两个视图类)
|
||
|
||
from dengji.models import (
|
||
Chenghao, KaoheCishuFeiyong, Bankuai,
|
||
YonghuChenghao, ShenheJilu, ShenheGuize, # 可能不需要用到但导入无妨
|
||
)
|
||
|
||
|
||
|
||
class KhpzhqView(APIView):
|
||
"""
|
||
获取所有考核配置数据(称号列表、板块、费用)
|
||
POST /houtai/khpzhq
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
if 'kaohepeizhi' not in permissions:
|
||
return Response({'code': 403, 'msg': '无考核配置权限'}, status=403)
|
||
|
||
bankuai_list = list(Bankuai.objects.values('bankuai_id', 'mingcheng'))
|
||
|
||
chenghao_qs = Chenghao.objects.prefetch_related('kaohecishufeiyong_set').all()
|
||
chenghao_list = []
|
||
for ch in chenghao_qs:
|
||
# 获取费用:按次数排序,转为 {cishu: feiyong} 字典
|
||
feiyong_dict = {
|
||
item.cishu: float(item.feiyong)
|
||
for item in ch.kaohecishufeiyong_set.order_by('cishu')
|
||
}
|
||
max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0
|
||
|
||
chenghao_list.append({
|
||
'id': ch.id,
|
||
'mingcheng': ch.mingcheng,
|
||
'leixing': ch.leixing,
|
||
'leixing_name': ch.get_leixing_display(),
|
||
'bankuai_id': ch.bankuai_id,
|
||
'tubiao_url': ch.tubiao_url or '',
|
||
'texiao_miaoshu': ch.texiao_miaoshu or '',
|
||
'create_time': ch.create_time.strftime('%Y-%m-%d %H:%M:%S') if ch.create_time else '',
|
||
'kaohe_guize': ch.kaohe_guize or '',
|
||
'kaioi_jinpai': ch.kaioi_jinpai,
|
||
'feiyong': feiyong_dict, # {1:100.0, 2:200.0}
|
||
'max_cishu': max_cishu
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'bankuai': bankuai_list,
|
||
'chenghao': chenghao_list
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.exception("KhpzhqView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
|
||
|
||
class ChzsgcView(APIView):
|
||
"""
|
||
称号(标签)增删改查统一接口
|
||
POST /houtai/chzsgc
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
if 'kaohepeizhi' not in permissions:
|
||
return Response({'code': 403, 'msg': '无考核配置权限'}, status=403)
|
||
|
||
action = request.data.get('action')
|
||
if not action:
|
||
return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
|
||
|
||
# ========== 创建称号 ==========
|
||
if action == 'create':
|
||
mingcheng = request.data.get('mingcheng', '').strip()
|
||
leixing = request.data.get('leixing', 'dashou')
|
||
bankuai_id = request.data.get('bankuai_id')
|
||
texiao_miaoshu = request.data.get('texiao_miaoshu', '')
|
||
kaohe_guize = request.data.get('kaohe_guize', '')
|
||
kaioi_jinpai = request.data.get('kaioi_jinpai', False)
|
||
feiyong_list = request.data.get('feiyong_list', [])
|
||
|
||
if not mingcheng:
|
||
return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
|
||
if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
|
||
return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
|
||
if Chenghao.objects.filter(mingcheng=mingcheng).exists():
|
||
return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
|
||
|
||
with transaction.atomic():
|
||
ch = Chenghao.objects.create(
|
||
mingcheng=mingcheng,
|
||
leixing=leixing,
|
||
bankuai_id=bankuai_id,
|
||
texiao_miaoshu=texiao_miaoshu,
|
||
kaohe_guize=kaohe_guize,
|
||
kaioi_jinpai=kaioi_jinpai
|
||
)
|
||
# 处理费用:前端传来 [{cishu:1, feiyong:100}, ...]
|
||
if feiyong_list:
|
||
for item in feiyong_list:
|
||
cishu = int(item.get('cishu', 1))
|
||
feiyong = float(item.get('feiyong', 0))
|
||
KaoheCishuFeiyong.objects.update_or_create(
|
||
chenghao=ch, cishu=cishu,
|
||
defaults={'feiyong': feiyong}
|
||
)
|
||
else:
|
||
# 默认添加第一次免费
|
||
KaoheCishuFeiyong.objects.create(chenghao=ch, cishu=1, feiyong=0)
|
||
|
||
return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}})
|
||
|
||
# ========== 修改称号 ==========
|
||
elif action == 'update':
|
||
ch_id = request.data.get('chenghao_id')
|
||
if not ch_id:
|
||
return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
|
||
try:
|
||
ch = Chenghao.objects.get(id=ch_id)
|
||
except Chenghao.DoesNotExist:
|
||
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
|
||
|
||
mingcheng = request.data.get('mingcheng', ch.mingcheng).strip()
|
||
leixing = request.data.get('leixing', ch.leixing)
|
||
bankuai_id = request.data.get('bankuai_id', ch.bankuai_id)
|
||
texiao_miaoshu = request.data.get('texiao_miaoshu', ch.texiao_miaoshu)
|
||
kaohe_guize = request.data.get('kaohe_guize', ch.kaohe_guize)
|
||
kaioi_jinpai = request.data.get('kaioi_jinpai', ch.kaioi_jinpai)
|
||
|
||
if not mingcheng:
|
||
return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
|
||
if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
|
||
return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
|
||
if Chenghao.objects.filter(mingcheng=mingcheng).exclude(id=ch_id).exists():
|
||
return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
|
||
|
||
ch.mingcheng = mingcheng
|
||
ch.leixing = leixing
|
||
ch.bankuai_id = bankuai_id
|
||
ch.texiao_miaoshu = texiao_miaoshu
|
||
ch.kaohe_guize = kaohe_guize
|
||
ch.kaioi_jinpai = kaioi_jinpai
|
||
ch.save()
|
||
|
||
# 如果前端传递了 feiyong_list,则全量替换费用
|
||
feiyong_list = request.data.get('feiyong_list')
|
||
if feiyong_list is not None:
|
||
with transaction.atomic():
|
||
ch.kaohecishufeiyong_set.all().delete()
|
||
for item in feiyong_list:
|
||
KaoheCishuFeiyong.objects.create(
|
||
chenghao=ch,
|
||
cishu=int(item.get('cishu', 1)),
|
||
feiyong=float(item.get('feiyong', 0))
|
||
)
|
||
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
|
||
# ========== 删除称号 ==========
|
||
elif action == 'delete':
|
||
ch_id = request.data.get('chenghao_id')
|
||
if not ch_id:
|
||
return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
|
||
try:
|
||
ch = Chenghao.objects.get(id=ch_id)
|
||
except Chenghao.DoesNotExist:
|
||
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
|
||
ch.delete()
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
# ========== 添加/修改某次费用 ==========
|
||
elif action == 'add_feiyong':
|
||
ch_id = request.data.get('chenghao_id')
|
||
cishu = request.data.get('cishu')
|
||
feiyong = request.data.get('feiyong', 0)
|
||
if not ch_id or not cishu:
|
||
return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
|
||
try:
|
||
ch = Chenghao.objects.get(id=ch_id)
|
||
except Chenghao.DoesNotExist:
|
||
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
|
||
if int(cishu) < 1:
|
||
return Response({'code': 1, 'msg': '次数必须>=1'}, status=400)
|
||
KaoheCishuFeiyong.objects.update_or_create(
|
||
chenghao=ch, cishu=cishu,
|
||
defaults={'feiyong': feiyong}
|
||
)
|
||
return Response({'code': 0, 'msg': '添加/修改费用成功'})
|
||
|
||
# ========== 删除某次费用 ==========
|
||
elif action == 'remove_feiyong':
|
||
ch_id = request.data.get('chenghao_id')
|
||
cishu = request.data.get('cishu')
|
||
if not ch_id or not cishu:
|
||
return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
|
||
if int(cishu) == 1:
|
||
return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400)
|
||
try:
|
||
ch = Chenghao.objects.get(id=ch_id)
|
||
except Chenghao.DoesNotExist:
|
||
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
|
||
KaoheCishuFeiyong.objects.filter(chenghao=ch, cishu=cishu).delete()
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
# ========== 修改某次费用(等价于add) ==========
|
||
elif action == 'update_feiyong':
|
||
return self.add_feiyong(request) # 复用逻辑
|
||
|
||
else:
|
||
return Response({'code': 1, 'msg': '未知操作'}, status=400)
|
||
|
||
except Exception as e:
|
||
logger.exception("ChzsgcView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
|
||
# ==================== 后端 views.py ====================
|
||
# 请确保已有以下导入,如果没有请在文件头部添加
|
||
|
||
from yonghu.models import (
|
||
UserMain, UserShenheguan
|
||
)
|
||
|
||
from dengji.models import (
|
||
KaoheguanBankuai, Bankuai
|
||
)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class KhgglView(APIView):
|
||
"""
|
||
考核官管理数据接口(列表、筛选、分页)
|
||
POST /houtai/khggl
|
||
Body: {
|
||
"phone": "管理员手机号",
|
||
"page": 1, // 可选,默认1
|
||
"page_size": 15, // 可选,默认15
|
||
"yonghuid": "1234567", // 按用户ID筛选,可选
|
||
"zhuangtai": 1, // 账号状态 0=禁用,1=正常,可选
|
||
"shenhe_zhuangtai": 0, // 审核官状态 0=空闲,1=审核中,可选
|
||
"bankuai_name": "王者" // 板块名称模糊筛选,可选
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
if 'kaohepeizhi' not in permissions:
|
||
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
|
||
|
||
# 分页参数
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 15))
|
||
if page < 1: page = 1
|
||
if page_size < 1: page_size = 15
|
||
|
||
# 筛选条件
|
||
filter_yonghuid = request.data.get('yonghuid', '').strip()
|
||
filter_zhuangtai = request.data.get('zhuangtai')
|
||
filter_shenhe_zhuangtai = request.data.get('shenhe_zhuangtai')
|
||
filter_bankuai_name = request.data.get('bankuai_name', '').strip()
|
||
|
||
# 基础查询:所有审核官,预加载用户及打手扩展表
|
||
queryset = UserShenheguan.objects.select_related('user__dashou_profile')
|
||
|
||
if filter_yonghuid:
|
||
queryset = queryset.filter(user__yonghuid=filter_yonghuid)
|
||
if filter_zhuangtai is not None:
|
||
queryset = queryset.filter(zhuangtai=filter_zhuangtai)
|
||
if filter_shenhe_zhuangtai is not None:
|
||
queryset = queryset.filter(shenhe_zhuangtai=filter_shenhe_zhuangtai)
|
||
|
||
# 如果填写了板块名称,筛选出关联该板块的审核官
|
||
if filter_bankuai_name:
|
||
bankuai_ids = Bankuai.objects.filter(
|
||
mingcheng__icontains=filter_bankuai_name
|
||
).values_list('bankuai_id', flat=True)
|
||
shenheguan_ids = KaoheguanBankuai.objects.filter(
|
||
bankuai_id__in=bankuai_ids
|
||
).values_list('kaoheguan_id', flat=True)
|
||
queryset = queryset.filter(user__yonghuid__in=shenheguan_ids)
|
||
|
||
total = queryset.count()
|
||
start = (page - 1) * page_size
|
||
end = start + page_size
|
||
shenheguan_list = queryset.order_by('-create_time')[start:end]
|
||
|
||
# ---------- 预计算每个板块的打手数量 ----------
|
||
bankuai_dashou_count_map = {}
|
||
all_banquai = Bankuai.objects.all()
|
||
for bk in all_banquai:
|
||
tag_ids = Chenghao.objects.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True)
|
||
count = YonghuChenghao.objects.filter(
|
||
chenghao_id__in=tag_ids,
|
||
yonghu__dashou_profile__isnull=False
|
||
).values('yonghu').distinct().count()
|
||
bankuai_dashou_count_map[bk.bankuai_id] = count
|
||
|
||
# ---------- 新增:板块标签统计 ----------
|
||
bankuai_tags_data = []
|
||
for bk in all_banquai:
|
||
tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou')
|
||
tag_list = []
|
||
for tag in tags:
|
||
user_count = YonghuChenghao.objects.filter(chenghao=tag).values('yonghu').distinct().count()
|
||
tag_list.append({
|
||
'id': tag.id,
|
||
'name': tag.mingcheng,
|
||
'user_count': user_count,
|
||
})
|
||
bankuai_tags_data.append({
|
||
'bankuai_id': bk.bankuai_id,
|
||
'mingcheng': bk.mingcheng,
|
||
'tags': tag_list,
|
||
})
|
||
|
||
# 构造返回数据(审核官列表)
|
||
data_list = []
|
||
for sg in shenheguan_list:
|
||
user = sg.user
|
||
# 获取该审核官关联的板块
|
||
bk_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=user.yonghuid).select_related('bankuai')
|
||
bankuai_info = []
|
||
for bk in bk_qs:
|
||
bid = bk.bankuai.bankuai_id
|
||
bankuai_info.append({
|
||
'bankuai_id': bid,
|
||
'mingcheng': bk.bankuai.mingcheng,
|
||
'dashou_count': bankuai_dashou_count_map.get(bid, 0)
|
||
})
|
||
|
||
dashou_nick = user.dashou_profile.nicheng if hasattr(user, 'dashou_profile') and user.dashou_profile else ''
|
||
|
||
data_list.append({
|
||
'yonghuid': user.yonghuid,
|
||
'avatar': user.avatar or '',
|
||
'phone': user.phone or '',
|
||
'zhuangtai': sg.zhuangtai,
|
||
'shenhe_zhuangtai': sg.shenhe_zhuangtai,
|
||
'shenhe_zongshu': sg.shenhe_zongshu,
|
||
'tongguo_zongshu': sg.tongguo_zongshu,
|
||
'yue': float(sg.yue),
|
||
'zonge': float(sg.zonge),
|
||
'create_time': sg.create_time.strftime('%Y-%m-%d %H:%M:%S') if sg.create_time else '',
|
||
'update_time': sg.update_time.strftime('%Y-%m-%d %H:%M:%S') if sg.update_time else '',
|
||
'bankuai': bankuai_info,
|
||
'nicheng': dashou_nick,
|
||
'is_renzheng': sg.is_renzheng,
|
||
})
|
||
|
||
all_bankuai = list(Bankuai.objects.values('bankuai_id', 'mingcheng'))
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'total': total,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
'list': data_list,
|
||
'all_bankuai': all_bankuai,
|
||
'bankuai_tags': bankuai_tags_data, # 新增板块标签统计
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.exception("KhgglView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
class ShgxgsjView(APIView):
|
||
"""
|
||
审核官信息修改(余额、状态、板块关联)
|
||
POST /houtai/shgxgsj
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
if 'kaohepeizhi' not in permissions:
|
||
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
|
||
|
||
yonghuid = request.data.get('yonghuid')
|
||
if not yonghuid:
|
||
return Response({'code': 1, 'msg': '缺少用户ID'}, status=400)
|
||
|
||
try:
|
||
shenheguan = UserShenheguan.objects.select_related('user').get(user__yonghuid=yonghuid)
|
||
except UserShenheguan.DoesNotExist:
|
||
return Response({'code': 1, 'msg': '审核官不存在'}, status=404)
|
||
|
||
action = request.data.get('action')
|
||
|
||
if action == 'update_info':
|
||
if 'yue' in request.data:
|
||
shenheguan.yue = float(request.data.get('yue', 0))
|
||
if 'zhuangtai' in request.data:
|
||
zt = int(request.data.get('zhuangtai'))
|
||
if zt not in [0, 1]:
|
||
return Response({'code': 1, 'msg': '无效的账号状态'}, status=400)
|
||
shenheguan.zhuangtai = zt
|
||
if 'shenhe_zhuangtai' in request.data:
|
||
szt = int(request.data.get('shenhe_zhuangtai'))
|
||
if szt not in [0, 1]:
|
||
return Response({'code': 1, 'msg': '无效的审核官状态'}, status=400)
|
||
shenheguan.shenhe_zhuangtai = szt
|
||
# 新增认证状态编辑
|
||
if 'is_renzheng' in request.data:
|
||
shenheguan.is_renzheng = bool(request.data.get('is_renzheng'))
|
||
shenheguan.save()
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
|
||
elif action == 'add_bankuai':
|
||
bankuai_id = request.data.get('bankuai_id')
|
||
if not bankuai_id:
|
||
return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
|
||
if not Bankuai.objects.filter(bankuai_id=bankuai_id).exists():
|
||
return Response({'code': 1, 'msg': '板块不存在'}, status=400)
|
||
if KaoheguanBankuai.objects.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists():
|
||
return Response({'code': 1, 'msg': '已关联该板块'}, status=400)
|
||
KaoheguanBankuai.objects.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id)
|
||
return Response({'code': 0, 'msg': '板块关联成功'})
|
||
|
||
elif action == 'remove_bankuai':
|
||
bankuai_id = request.data.get('bankuai_id')
|
||
if not bankuai_id:
|
||
return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
|
||
deleted, _ = KaoheguanBankuai.objects.filter(
|
||
kaoheguan_id=yonghuid, bankuai_id=bankuai_id
|
||
).delete()
|
||
if not deleted:
|
||
return Response({'code': 1, 'msg': '未找到该板块关联'}, status=400)
|
||
return Response({'code': 0, 'msg': '板块移除成功'})
|
||
|
||
else:
|
||
return Response({'code': 1, 'msg': '未知操作'}, status=400)
|
||
|
||
except Exception as e:
|
||
logger.exception("ShgxgsjView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
import secrets
|
||
from dingdan.models import Dingdan, DingdanShangjia
|
||
from dingdan.tongzhi_tasks import dingdan_guangbo
|
||
|
||
from peizhi.models import ShangjiaLianjie
|
||
|
||
class ZxkfghdsView(APIView):
|
||
"""
|
||
客服更换打手接口
|
||
路径:POST /houtai/zxkfghds
|
||
功能:
|
||
1. 将当前订单状态改为已退款(5),释放接单打手(若有)
|
||
2. 如需跨平台通知,则调用对方平台退款通知接口
|
||
3. 基于原订单信息生成全新订单(状态直接为 1,已替老板填写)
|
||
4. 生成新的派单链接,并标记为已使用
|
||
5. 新链接记录上一个链接ID和上一个订单ID
|
||
6. 【关键】不操作商家余额、退款次数、每日统计,因新订单是替换而非新增扣款
|
||
权限:需要'003aa'(跨平台管理)权限,与退款接口一致
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# ========== 1. 参数提取 ==========
|
||
username = request.data.get('username', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
if not username or not dingdan_id:
|
||
logger.warning("更换打手参数不完整")
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# ========== 2. 权限校验(与跨平台退款相同) ==========
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 返回错误响应
|
||
if '003aa' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权处理订单'})
|
||
|
||
current_user = request.user
|
||
|
||
# ========== 3. 查询原订单及关联信息 ==========
|
||
try:
|
||
old_order = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id)
|
||
except Dingdan.DoesNotExist:
|
||
logger.warning(f"订单不存在: {dingdan_id}")
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
# 仅允许状态为 2(进行中)或 8(结算中)更换打手
|
||
if old_order.zhuangtai not in [1,7,2, 8]:
|
||
return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'})
|
||
|
||
# 本接口仅处理商家发单(fadan_pingtai=2)
|
||
if old_order.fadan_pingtai != 2:
|
||
return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'})
|
||
|
||
# 获取原链接(用于模板ID等)
|
||
try:
|
||
old_lianjie = ShangjiaLianjie.objects.get(dingdan_id=dingdan_id)
|
||
except ShangjiaLianjie.DoesNotExist:
|
||
logger.error(f"原链接记录不存在,订单: {dingdan_id}")
|
||
return Response({'code': 500, 'msg': '链接数据异常'})
|
||
|
||
# 商家信息(仅用于记录关联,不操作余额)
|
||
try:
|
||
shangjia_ext = old_order.shangjia_kuozhan
|
||
shangjia_id = shangjia_ext.shangjia_id
|
||
# 获取商家昵称用于新订单扩展表
|
||
shangjia_user = UserMain.objects.get(yonghuid=shangjia_id)
|
||
shangjia_nicheng = shangjia_user.shop_profile.nicheng or f"商家{shangjia_id}"
|
||
except (ObjectDoesNotExist, UserShangjia.DoesNotExist):
|
||
logger.error(f"商家信息缺失,订单: {dingdan_id}")
|
||
return Response({'code': 500, 'msg': '商家数据异常'})
|
||
|
||
# ========== 4. 判断是否需要跨平台退款通知 ==========
|
||
is_cross = old_order.is_cross
|
||
partner_club_id = old_order.partner_club_id
|
||
partner_order_id = old_order.partner_order_id
|
||
need_notify_partner = is_cross and partner_club_id and partner_order_id
|
||
|
||
# ========== 5. 退款处理(仅打手侧,不碰商家) ==========
|
||
with transaction.atomic():
|
||
# 5.1 订单状态改为已退款
|
||
old_order.zhuangtai = 5
|
||
old_order.clkf = current_user.phone
|
||
old_order.save()
|
||
|
||
# 5.2 处理接单打手(若有)
|
||
dashou_id = old_order.jiedan_dashou_id
|
||
if dashou_id:
|
||
try:
|
||
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
|
||
dashou_profile = dashou_user.dashou_profile
|
||
dashou_profile.tuikuanliang += 1 # 退款次数+1
|
||
dashou_profile.zhuangtai = 1 # 设为空闲
|
||
dashou_profile.save()
|
||
except (UserMain.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"打手 {dashou_id} 不存在,跳过更新")
|
||
|
||
# 5.3 更新退款记录表
|
||
Tuikuanjilu.objects.update_or_create(
|
||
dingdan_id=dingdan_id,
|
||
defaults={
|
||
'dashouid': dashou_id or '',
|
||
'qingqiuid': '',
|
||
'chuliid': current_user.phone,
|
||
'liyou': '客服更换打手退款',
|
||
'sqzhuangtai': 1,
|
||
'jine': old_order.jine or 0,
|
||
'dashou_fencheng': old_order.dashou_fencheng or 0,
|
||
'jieshao': old_order.jieshao or '',
|
||
'beizhu': '更换打手自动退款',
|
||
'nicheng': old_order.nicheng or '',
|
||
}
|
||
)
|
||
|
||
# 5.4 打手每日统计(退款)
|
||
if dashou_id:
|
||
try:
|
||
update_dashou_daily_by_action(
|
||
yonghuid=dashou_id,
|
||
amount=old_order.dashou_fencheng,
|
||
action=3 # 3=退款
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"打手每日统计更新失败: {e}")
|
||
|
||
# 5.5 客服处理统计
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
# 5.6 跨平台退款通知(需要时)
|
||
if need_notify_partner:
|
||
try:
|
||
# 获取我方俱乐部ID
|
||
my_club_config = ClubConfig.objects.filter(is_self=1).first()
|
||
if not my_club_config:
|
||
raise Exception("我方俱乐部配置不存在")
|
||
self_club_id = my_club_config.club_id
|
||
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
|
||
partner_domain = club_rel.partner_domain
|
||
|
||
notify_url = partner_domain.rstrip('/') + '/houtai/dfddtk'
|
||
payload = {
|
||
'order_id': partner_order_id,
|
||
'club_id': self_club_id,
|
||
'status': 5,
|
||
'reason': '客服更换打手退款'
|
||
}
|
||
|
||
# 调用对方接口(必须成功,否则整个事务回滚)
|
||
resp = requests.post(notify_url, json=payload, timeout=5)
|
||
if resp.status_code == 200:
|
||
resp_json = resp.json()
|
||
if resp_json.get('code') != 0:
|
||
transaction.set_rollback(True)
|
||
logger.error(f"对方平台退款通知失败: {resp_json.get('msg')}")
|
||
return Response({'code': 500, 'msg': f'对方平台通知失败: {resp_json.get("msg")}'})
|
||
else:
|
||
transaction.set_rollback(True)
|
||
logger.error(f"对方平台接口HTTP错误: {resp.status_code}")
|
||
return Response({'code': 500, 'msg': '通知对方平台失败'})
|
||
|
||
# 更新跨平台扩展表(如果存在)
|
||
from dingdan.models import CrossPlatformOrderData
|
||
CrossPlatformOrderData.objects.update_or_create(
|
||
dingdan_id=dingdan_id,
|
||
defaults={
|
||
'partner_order_id': partner_order_id,
|
||
'partner_club_id': partner_club_id,
|
||
'partner_order_status': 5,
|
||
}
|
||
)
|
||
except Exception as e:
|
||
transaction.set_rollback(True)
|
||
logger.error(f"通知对方平台异常: {e}")
|
||
return Response({'code': 500, 'msg': f'通知对方平台失败: {str(e)}'})
|
||
|
||
# 至此,原订单已退款,事务提交
|
||
|
||
# ========== 6. 生成新订单和链接(不扣商家余额) ==========
|
||
try:
|
||
with transaction.atomic():
|
||
# ---- 6.1 生成新的订单ID和Token ----
|
||
def generate_dingdan_id():
|
||
timestamp = int(time.time() * 1000)
|
||
process_id = os.getpid() % 1000
|
||
random_num = random.randint(1000, 9999)
|
||
return f"SJ{timestamp}{process_id:03d}{random_num}"
|
||
|
||
def generate_secure_token(dd_id, uid):
|
||
timestamp = int(time.time() * 1_000_000)
|
||
salt = secrets.token_hex(8)
|
||
raw = f"{timestamp}:{dd_id}:{uid}:{salt}"
|
||
token = hashlib.sha256(raw.encode()).hexdigest()[:24]
|
||
if ShangjiaLianjie.objects.filter(lianjie_token=token).exists():
|
||
return generate_secure_token(dd_id, uid)
|
||
return token
|
||
|
||
def generate_link_url(token):
|
||
base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/')
|
||
return f"{base}/order/{token}"
|
||
|
||
new_dingdan_id = generate_dingdan_id()
|
||
secure_token = generate_secure_token(new_dingdan_id, shangjia_id)
|
||
|
||
# ---- 6.2 创建新订单(复制核心数据,状态直接为1) ----
|
||
new_order = Dingdan.objects.create(
|
||
dingdan_id=new_dingdan_id,
|
||
zhuangtai=1, # 下单中
|
||
fadan_pingtai=2,
|
||
jine=old_order.jine,
|
||
dashou_fencheng=old_order.dashou_fencheng,
|
||
leixing_id=old_order.leixing_id,
|
||
yaoqiuleixing=old_order.yaoqiuleixing,
|
||
huiyuan_id=old_order.huiyuan_id,
|
||
yongjin=old_order.yongjin,
|
||
jieshao=old_order.jieshao,
|
||
nicheng=old_order.nicheng, # 继承游戏昵称
|
||
beizhu=old_order.beizhu,
|
||
user1_id=old_order.user1_id, # 商家标识
|
||
tupian=old_order.tupian,
|
||
# 注意:不继承 zhiding_id、jiedan_dashou_id
|
||
)
|
||
|
||
# ---- 6.3 创建商家扩展表 ----
|
||
DingdanShangjia.objects.create(
|
||
dingdan=new_order,
|
||
shangjia_id=shangjia_id,
|
||
sjnicheng=shangjia_nicheng,
|
||
)
|
||
|
||
# ---- 6.4 生成新链接并关联旧链接信息 ----
|
||
link_url = generate_link_url(secure_token)
|
||
new_lianjie = ShangjiaLianjie.objects.create(
|
||
yonghu_id=shangjia_id,
|
||
leixing_id=old_lianjie.leixing_id,
|
||
moban_id=old_lianjie.moban_id,
|
||
lianjie=link_url,
|
||
lianjie_token=secure_token,
|
||
dingdan_id=new_dingdan_id,
|
||
is_used=True, # 直接标记已使用
|
||
youxiao_shijian=timezone.now() + timezone.timedelta(days=1),
|
||
previous_lianjie_id=old_lianjie.id, # 记录旧链接ID
|
||
previous_dingdan_id=old_order.dingdan_id, # 记录旧订单ID
|
||
)
|
||
|
||
# ---- 6.5 异步通知(广播新订单、跨平台同步) ----
|
||
try:
|
||
from utils.order_broadcast import submit_dingdan_guangbo
|
||
submit_dingdan_guangbo({
|
||
'dingdan_id': new_dingdan_id,
|
||
'game_type': self._get_game_type_name(new_order.leixing_id),
|
||
'amount': str(new_order.jine),
|
||
'order_desc': (new_order.jieshao or '')[:50],
|
||
'order_type': '商家派单',
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"广播通知失败: {e}")
|
||
|
||
# 跨平台同步(与客户填写接口保持一致)
|
||
try:
|
||
from dingdan.utils import sync_order_to_partners
|
||
sync_order_to_partners(new_dingdan_id)
|
||
except Exception as e:
|
||
logger.error(f"跨平台同步失败: {e}")
|
||
|
||
# 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单
|
||
|
||
logger.info(f"更换打手成功:旧订单{dingdan_id}→退款,新订单{new_dingdan_id}已生成")
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '更换成功',
|
||
'data': {
|
||
'new_dingdan_id': new_dingdan_id,
|
||
'new_lianjie': link_url,
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"生成新订单事务失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '生成新订单失败'})
|
||
|
||
def _get_game_type_name(self, leixing_id):
|
||
"""根据类型ID获取游戏类型名称"""
|
||
try:
|
||
from shangpin.models import ShangpinLeixing
|
||
gt = ShangpinLeixing.objects.filter(id=leixing_id).first()
|
||
return gt.jieshao if gt else '游戏订单'
|
||
except Exception:
|
||
return '游戏订单'
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
from django.conf import settings
|
||
from utils.xcx_sys_config import wx_cfg
|
||
from django.db.models import ObjectDoesNotExist
|
||
from rest_framework import status
|
||
from rest_framework.parsers import JSONParser
|
||
from rest_framework.response import Response
|
||
from rest_framework.views import APIView
|
||
|
||
from dingdan.models import Dingdan
|
||
|
||
logger = logging.getLogger('houtai')
|
||
|
||
|
||
class KptxwztjbView(APIView):
|
||
"""
|
||
跨平台订单状态同步接口(对方平台调用)
|
||
请求:POST /houtai/kptxwztjb
|
||
参数:
|
||
- token: 同步专用令牌(明文)
|
||
- partner_order_id: 我方订单ID(在对方眼中是partner_order_id,实际查本库dingdan_id)
|
||
- our_order_id: 对方订单ID(在本库中为partner_order_id)
|
||
- club_id: 俱乐部ID(可选验证)
|
||
返回:
|
||
code=0 成功,data.status 为订单状态
|
||
其他 code 为错误
|
||
注意:此接口仅用于查询对方订单状态,不做任何修改
|
||
"""
|
||
|
||
authentication_classes = [] # 无需JWT等认证
|
||
permission_classes = [] # 公开接口
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
token = request.data.get('token', '').strip()
|
||
partner_order_id = request.data.get('partner_order_id', '').strip()
|
||
our_order_id = request.data.get('our_order_id', '').strip()
|
||
club_id = request.data.get('club_id', '').strip()
|
||
|
||
if not all([token, partner_order_id, our_order_id]):
|
||
logger.warning("跨平台状态同步参数缺失")
|
||
return Response({'code': 400, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 2. 验证同步令牌
|
||
SYNC_TOKEN = getattr(settings, 'CROSS_PLATFORM_SYNC_TOKEN', 'xK9mP2vL8qW5rT7y')
|
||
if token != SYNC_TOKEN:
|
||
logger.warning(f"无效的同步令牌: {token[:8]}...")
|
||
return Response({'code': 403, 'msg': '无效令牌'}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 3. 查询订单
|
||
# partner_order_id 在对方视角是“我方订单ID”,在本库中应匹配 dingdan_id
|
||
try:
|
||
order = Dingdan.objects.get(dingdan_id=partner_order_id)
|
||
except Dingdan.DoesNotExist:
|
||
logger.warning(f"订单不存在, partner_order_id: {partner_order_id}")
|
||
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 4. 验证订单关联关系:本库中的 partner_order_id 应等于传入的 our_order_id
|
||
if order.partner_order_id != our_order_id:
|
||
logger.warning(
|
||
f"订单关联不匹配: partner_order_id={order.partner_order_id}, our_order_id={our_order_id}"
|
||
)
|
||
return Response({'code': 400, 'msg': '订单关联信息不匹配'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
|
||
|
||
# 6. 返回订单状态
|
||
status_code = order.zhuangtai
|
||
logger.info(
|
||
f"跨平台状态查询成功, dingdan_id={order.dingdan_id}, status={status_code}"
|
||
)
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'success',
|
||
'data': {
|
||
'dingdan_id': order.dingdan_id,
|
||
'status': status_code,
|
||
}
|
||
}) |