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 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 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": settings.WEIXIN_APPID,
"secret": settings.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 = ['']
for k, v in data.items():
xml.append(f'<{k}>{v}{k}>')
xml.append('')
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 = getattr(settings, 'WEIXIN_APPID', '')
mch_id = getattr(settings, 'WEIXIN_MCHID', '')
key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '')
cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '')
key_path = getattr(settings, 'WEIXIN_KEY_PATH', '')
if not all([appid, mch_id, key, cert_path, key_path]):
missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v]
logger.error(f"微信支付配置缺失: {missing}")
return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'}
# 生成退款单号
out_refund_no = self._generate_refund_no(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 = ['']
for k, v in data.items():
xml.append(f'<{k}>{v}{k}>')
xml.append('')
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 = []
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,
'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')
if nicheng is not None:
target_kefu.nicheng = nicheng
if status is not None:
target_kefu.zhuangtai = int(status)
if password:
if len(password) < 6:
return Response({'code': 400, 'msg': '密码至少6位'})
target_user.password = password
if erjimima:
if len(erjimima) < 6:
return Response({'code': 400, 'msg': '二级密码至少6位'})
target_kefu.erjimima = erjimima # 假设二级密码也需要加密
target_user.save()
target_kefu.save()
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': '请选择角色'})
for rc in role_codes:
try:
role = Role.objects.get(role_code=rc)
except Role.DoesNotExist:
continue
UserRole.objects.get_or_create(account_id=target_phone, role=role)
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': '角色不存在'})
UserRole.objects.filter(account_id=target_phone, role=role).delete()
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': '请填写完整信息'})
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
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'],
)
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'],
)
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'],
)
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'],
)
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'],
)
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'],
)
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'修改账号状态为{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': '金额格式错误'})
new_yue = shangjia.yue + amount
if new_yue > 999999999.99:
return Response({'code': 400, 'msg': '增加后余额超出上限'})
shangjia.yue = new_yue
shangjia.save(update_fields=['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': '金额格式错误'})
if shangjia.yue < amount:
return Response({'code': 400, 'msg': '余额不足'})
shangjia.yue = shangjia.yue - amount
shangjia.save(update_fields=['yue'])
return Response({'code': 0, 'msg': '余额减少成功', 'data': {'new_balance': str(shangjia.yue)}})
# 封禁
if action == 'ban':
if shangjia.zhuangtai == 2:
return Response({'code': 400, 'msg': '商家已是封禁状态'})
shangjia.zhuangtai = 2
shangjia.save(update_fields=['zhuangtai'])
return Response({'code': 0, 'msg': '商家已封禁', 'data': {'zhuangtai': 2}})
# 解封
if action == 'unban':
if shangjia.zhuangtai == 1:
return Response({'code': 400, 'msg': '商家已是正常状态'})
shangjia.zhuangtai = 1
shangjia.save(update_fields=['zhuangtai'])
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
# ---------- 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': '余额格式错误'})
old_yue = guanshi.yue
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')
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)'})
# 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()
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 # 确保导入模型
config = WithdrawConfig.objects.filter(id=1).first()
withdraw_mode = config.mode if config else 2
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,
}
})
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
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)}
)
# 注意: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
# 开始事务
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')
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)'})
# 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()
# 保存组长其他可能修改的字段(例如电话、微信没有,忽略)
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 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,
}
})