9292 lines
385 KiB
Python
9292 lines
385 KiB
Python
import hmac
|
||
import threading
|
||
import traceback
|
||
import uuid
|
||
import hashlib
|
||
import xmltodict
|
||
import time
|
||
import logging
|
||
import requests
|
||
import os
|
||
import secrets
|
||
import random
|
||
import string
|
||
from calendar import monthrange
|
||
from decimal import Decimal
|
||
from datetime import date, datetime, timedelta
|
||
from django.utils import timezone
|
||
from django.conf import settings
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from django.core.paginator import Paginator
|
||
from django.db import models, transaction, IntegrityError
|
||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||
from gvsdsdk.fluent import db, func, FQ
|
||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||
from django.contrib.auth.hashers import make_password
|
||
from django.views.decorators.csrf import csrf_exempt
|
||
from django.utils.decorators import method_decorator
|
||
|
||
from rest_framework.views import APIView
|
||
from rest_framework.response import Response
|
||
from rest_framework import status
|
||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||
|
||
# 工具类导入
|
||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||
from backend.utils import (
|
||
update_dashou_daily_by_action,
|
||
update_shangjia_daily
|
||
)
|
||
from jituan.services.club_context import filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||
from jituan.services.club_penalty import (
|
||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||
)
|
||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||
from shop.utils import update_dianpu_daily_stat
|
||
from products.utils import update_shangpin_daily_stat
|
||
from orders.notice_tasks import dingdan_guangbo
|
||
from .utils import (
|
||
verify_kefu_permission,
|
||
PERMISSION_TO_SHENFEN,
|
||
SHENFEN_PROFILE_MAP,
|
||
has_fadan_view_permission,
|
||
check_fadan_permission,
|
||
pick_order_id,
|
||
pick_penalty_create_params,
|
||
write_xiugai_log,
|
||
XIUGAI_LEIXING_DASHOU,
|
||
XIUGAI_LEIXING_GUANSHI,
|
||
XIUGAI_LEIXING_SHANGJIA,
|
||
XIUGAI_LEIXING_ZUZHANG,
|
||
XIUGAI_LEIXING_LABEL,
|
||
)
|
||
|
||
# models 集中导入
|
||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||
## backend
|
||
from backend.models import WithdrawalDailyStats
|
||
|
||
## users
|
||
from users.models import (
|
||
UserGuanshi, UserBoss, UserZuzhang,
|
||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||
)
|
||
from users.business_models import User
|
||
|
||
## orders
|
||
from orders.models import (
|
||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||
OrderPlayerHistory, Order, RefundRecord,
|
||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||
)
|
||
|
||
## shop
|
||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||
|
||
## products
|
||
from products.models import (
|
||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||
)
|
||
|
||
## config
|
||
from config.models import (
|
||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||
TixianQuotaDefault,
|
||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||
)
|
||
|
||
## rank
|
||
from rank.models import (
|
||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||
)
|
||
|
||
# 序列化器
|
||
from .serializers import PopupPageSerializer
|
||
|
||
# 全局常量、日志对象
|
||
logger = logging.getLogger('houtai')
|
||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||
|
||
|
||
def _serialize_permission_row(perm_row):
|
||
"""gvsdsdk Permission → 后台 Vue 约定的小写字段"""
|
||
if isinstance(perm_row, dict):
|
||
code = perm_row.get('PermCode') or perm_row.get('perm_code') or ''
|
||
name = perm_row.get('PermName') or perm_row.get('perm_name') or code
|
||
desc = perm_row.get('PermDesc') or perm_row.get('perm_desc') or ''
|
||
else:
|
||
code = perm_row.PermCode or ''
|
||
name = perm_row.PermName or code
|
||
desc = perm_row.PermDesc or ''
|
||
return {'perm_code': code, 'perm_name': name, 'perm_desc': desc}
|
||
|
||
|
||
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.filter(PermStatus=1).values('PermCode', 'PermName', 'PermDesc')
|
||
permissions_list = [_serialize_permission_row(p) for p in all_perms]
|
||
|
||
# 4. 获取所有角色,并统计每个角色下的用户数(仅后台客服账号)
|
||
kefu_user_uuids = list(
|
||
User.objects.filter(KefuProfile__isnull=False).values_list('UserUUID', flat=True)
|
||
)
|
||
roles = Role.objects.filter(RoleStatus=1)
|
||
roles_data = []
|
||
for role in roles:
|
||
user_count = UserRole.objects.filter(
|
||
RoleUUID=role.RoleUUID,
|
||
UserUUID__in=kefu_user_uuids,
|
||
).count()
|
||
role_perm_uuids = RolePermission.objects.filter(
|
||
RoleUUID=role.RoleUUID
|
||
).values_list('PermUUID', flat=True)
|
||
perms_of_role = [
|
||
_serialize_permission_row(p)
|
||
for p in Permission.objects.filter(
|
||
PermUUID__in=list(role_perm_uuids)
|
||
).values('PermCode', 'PermName', 'PermDesc')
|
||
]
|
||
roles_data.append({
|
||
'role_code': role.RoleName,
|
||
'role_name': role.RoleName,
|
||
'description': role.RoleDesc or '',
|
||
'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(RoleName=role_code)
|
||
except Role.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '角色不存在'})
|
||
|
||
# 超级管理员角色的特殊限制
|
||
if role.RoleName == '管理员':
|
||
if action in ['delete_role']:
|
||
return Response({'code': 403, 'msg': '超级管理员角色不能删除'})
|
||
|
||
# 根据 action 处理
|
||
if action == 'delete_role':
|
||
# 删除角色:同时删除角色权限关联、用户角色关联
|
||
with transaction.atomic():
|
||
RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete()
|
||
UserRole.objects.filter(RoleUUID=role.RoleUUID).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': '缺少权限编码'})
|
||
if role.RoleName == '管理员' and perm_code == '000001':
|
||
return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'})
|
||
try:
|
||
perm = Permission.objects.get(PermCode=perm_code)
|
||
except Permission.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '权限不存在'})
|
||
# 检查是否已存在
|
||
if RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).exists():
|
||
return Response({'code': 400, 'msg': '该角色已拥有此权限'})
|
||
RolePermission.objects.create(
|
||
RolePermUUID=uuid.uuid4().bytes,
|
||
RoleUUID=role.RoleUUID,
|
||
PermUUID=perm.PermUUID,
|
||
CreateTime=timezone.now(),
|
||
)
|
||
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': '缺少权限编码'})
|
||
if role.RoleName == '管理员' and perm_code == '000001':
|
||
return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'})
|
||
try:
|
||
perm = Permission.objects.get(PermCode=perm_code)
|
||
except Permission.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '权限不存在'})
|
||
deleted, _ = RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).delete()
|
||
if deleted == 0:
|
||
return Response({'code': 400, 'msg': '该角色未拥有此权限'})
|
||
return Response({'code': 0, 'msg': '权限移除成功'})
|
||
|
||
elif action == 'update_role':
|
||
role_name = (request.data.get('role_name') or '').strip()
|
||
description = (request.data.get('description') or '').strip()
|
||
perm_codes = request.data.get('perm_codes') or []
|
||
if role.RoleName == '管理员':
|
||
role_name = role.RoleName
|
||
elif role_name and role_name != role.RoleName:
|
||
if Role.objects.filter(RoleName=role_name).exclude(RoleUUID=role.RoleUUID).exists():
|
||
return Response({'code': 400, 'msg': '角色名称已存在'})
|
||
with transaction.atomic():
|
||
if role.RoleName != '管理员':
|
||
if role_name:
|
||
role.RoleName = role_name
|
||
role.RoleDesc = description
|
||
role.save(update_fields=['RoleName', 'RoleDesc'])
|
||
RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete()
|
||
for pc in perm_codes:
|
||
if pc == '000001' and role.RoleName != '管理员':
|
||
continue
|
||
try:
|
||
perm = Permission.objects.get(PermCode=pc, PermStatus=1)
|
||
except Permission.DoesNotExist:
|
||
continue
|
||
RolePermission.objects.create(
|
||
RolePermUUID=uuid.uuid4().bytes,
|
||
RoleUUID=role.RoleUUID,
|
||
PermUUID=perm.PermUUID,
|
||
CreateTime=timezone.now(),
|
||
)
|
||
if role.RoleName == '管理员':
|
||
try:
|
||
super_perm = Permission.objects.get(PermCode='000001', PermStatus=1)
|
||
if not RolePermission.objects.filter(
|
||
RoleUUID=role.RoleUUID, PermUUID=super_perm.PermUUID,
|
||
).exists():
|
||
RolePermission.objects.create(
|
||
RolePermUUID=uuid.uuid4().bytes,
|
||
RoleUUID=role.RoleUUID,
|
||
PermUUID=super_perm.PermUUID,
|
||
CreateTime=timezone.now(),
|
||
)
|
||
except Permission.DoesNotExist:
|
||
pass
|
||
return Response({'code': 0, 'msg': '角色保存成功'})
|
||
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的 action'})
|
||
|
||
|
||
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(RoleName=role_name).exists():
|
||
return Response({'code': 400, 'msg': '角色名称已存在'})
|
||
|
||
# 生成唯一角色编码(格式:ROLE_ + 8位随机字母数字)
|
||
role_code = f"ROLE_{''.join(random.choices(string.ascii_uppercase + string.digits, k=8))}"
|
||
|
||
# 创建角色
|
||
with transaction.atomic():
|
||
role = Role.objects.create(
|
||
RoleUUID=uuid.uuid4().bytes,
|
||
TenantUUID=b'\x00' * 16,
|
||
RoleName=role_name,
|
||
RoleType=2,
|
||
RoleStatus=1,
|
||
AssignScope=0,
|
||
RoleDesc=description,
|
||
CreateTime=timezone.now(),
|
||
)
|
||
# 绑定权限(过滤掉不存在的权限编码)
|
||
for pc in perm_codes:
|
||
# 不允许添加 000001 权限(此权限仅供超级管理员)
|
||
if pc == '000001':
|
||
continue
|
||
try:
|
||
perm = Permission.objects.get(PermCode=pc)
|
||
RolePermission.objects.create(
|
||
RolePermUUID=uuid.uuid4().bytes,
|
||
RoleUUID=role.RoleUUID,
|
||
PermUUID=perm.PermUUID,
|
||
CreateTime=timezone.now(),
|
||
)
|
||
except Permission.DoesNotExist:
|
||
logger.warning(f"添加角色时忽略不存在的权限编码: {pc}")
|
||
|
||
return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}})
|
||
|
||
|
||
|
||
class GetAdminUserListView(APIView):
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
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))
|
||
|
||
# UserType 是 @property 非数据库字段,不能用 filter;
|
||
# KefuProfile__isnull=False 已确保只查到客服用户
|
||
users = User.query.filter(
|
||
KefuProfile__isnull=False
|
||
).select_related('KefuProfile')
|
||
|
||
if phone:
|
||
users = users.filter(Phone__icontains=phone)
|
||
if nicheng:
|
||
users = users.filter(KefuProfile__nicheng__icontains=nicheng)
|
||
|
||
# 关键修正:先求值为列表,再传入 phone__in
|
||
if role_code:
|
||
target_role = Role.objects.filter(RoleName=role_code).first()
|
||
if target_role:
|
||
user_uuids = list(
|
||
UserRole.objects.filter(RoleUUID=target_role.RoleUUID)
|
||
.values_list('UserUUID', flat=True)
|
||
)
|
||
users = users.filter(UserUUID__in=user_uuids)
|
||
else:
|
||
users = users.none()
|
||
|
||
users = users.order_by('-UserCreateTime')
|
||
paginator = Paginator(users, page_size)
|
||
page_obj = paginator.get_page(page)
|
||
|
||
data_list = []
|
||
for user in page_obj:
|
||
# 通过 gvsdsdk UserRole 获取用户角色
|
||
user_role_uuids = list(
|
||
UserRole.objects.filter(UserUUID=user.UserUUID)
|
||
.values_list('RoleUUID', flat=True)
|
||
)
|
||
user_roles = Role.objects.filter(RoleUUID__in=user_role_uuids)
|
||
roles_data = [
|
||
{'role_code': r.RoleName, 'role_name': r.RoleName}
|
||
for r in user_roles
|
||
]
|
||
data_list.append({
|
||
'phone': user.Phone or '',
|
||
'yonghuid': user.UserUID or '',
|
||
'nicheng': user.KefuProfile.nicheng,
|
||
'roles': roles_data,
|
||
'status': user.KefuProfile.zhuangtai,
|
||
'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
|
||
'update_time': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '',
|
||
'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
|
||
'UpdateTime': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '',
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': data_list,
|
||
'total': paginator.count,
|
||
'page': page,
|
||
'pageSize': page_size
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"GetAdminUserListView 异常: {e}\n{traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'})
|
||
|
||
|
||
|
||
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.filter(RoleStatus=1).values('RoleName')
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'roles': [{'role_code': r['RoleName'], 'role_name': r['RoleName']} for r in 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))
|
||
|
||
# 基础查询:存在客服扩展表
|
||
users = User.query.filter(KefuProfile__isnull=False).select_related('KefuProfile')
|
||
if phone:
|
||
users = users.filter(Phone__icontains=phone)
|
||
if nicheng:
|
||
users = users.filter(KefuProfile__nicheng__icontains=nicheng)
|
||
if role_code:
|
||
# 筛选拥有该角色的用户
|
||
user_ids_with_role = UserRole.query.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('-CreateTime')
|
||
|
||
# 分页
|
||
paginator = Paginator(users, page_size)
|
||
page_obj = paginator.get_page(page)
|
||
|
||
# 构建返回数据
|
||
data_list = []
|
||
for user in page_obj:
|
||
# 获取用户的所有角色
|
||
user_roles = UserRole.query.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.KefuProfile.nicheng,
|
||
'roles': roles_data,
|
||
'status': user.KefuProfile.zhuangtai,
|
||
'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S'),
|
||
'UpdateTime': user.UpdateTime.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': '缺少目标用户账号'})
|
||
|
||
# Phone 非唯一,同一手机号可能对应多个微信用户;后台操作只定位客服账号
|
||
target_user = User.query.filter(
|
||
Phone=target_phone,
|
||
KefuProfile__isnull=False,
|
||
).first()
|
||
if not target_user:
|
||
return Response({'code': 404, 'msg': '用户不存在或非后台账号'})
|
||
try:
|
||
target_kefu = target_user.KefuProfile
|
||
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.SetPassword(password)
|
||
if erjimima:
|
||
if len(erjimima) < 6:
|
||
return Response({'code': 400, 'msg': '二级密码至少6位'})
|
||
import bcrypt as _bcrypt
|
||
target_kefu.erjimima = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
|
||
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(RoleName=rc)
|
||
except Role.DoesNotExist:
|
||
continue
|
||
UserRole.objects.get_or_create(
|
||
UserRoleUUID=uuid.uuid4().bytes,
|
||
UserUUID=target_user.UserUUID,
|
||
RoleUUID=role.RoleUUID,
|
||
CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
|
||
)
|
||
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(RoleName=role_code)
|
||
except Role.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '角色不存在'})
|
||
UserRole.objects.filter(UserUUID=target_user.UserUUID, RoleUUID=role.RoleUUID).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位'})
|
||
|
||
# 校验手机号是否已被客服占用
|
||
if User.query.filter(Phone=phone, KefuProfile__isnull=False).exists():
|
||
return Response({'code': 400, 'msg': '该账号已存在'})
|
||
|
||
# 生成唯一的 yonghuid(7位数字)
|
||
while True:
|
||
yonghuid = str(random.randint(1000000, 9999999))
|
||
if not User.query.filter(UserUID=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 User.query.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 = User.query.create(
|
||
UserUID=yonghuid,
|
||
UserName=openid,
|
||
OpenID=openid,
|
||
Phone=phone,
|
||
)
|
||
user_main.SetPassword(password)
|
||
user_main.save(update_fields=['UserPassword'])
|
||
# 创建客服扩展表(后台用户扩展)
|
||
import bcrypt as _bcrypt
|
||
erjimima_hashed = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
|
||
UserKefu.query.create(
|
||
user=user_main,
|
||
nicheng=nicheng,
|
||
erjimima=erjimima_hashed,
|
||
zhuangtai=1
|
||
)
|
||
# 分配客服角色
|
||
kefu_role = Role.objects.filter(RoleName='客服').first()
|
||
if kefu_role:
|
||
UserRole.objects.create(
|
||
UserRoleUUID=uuid.uuid4().bytes,
|
||
UserUUID=user_main.UserUUID,
|
||
RoleUUID=kefu_role.RoleUUID,
|
||
CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
|
||
)
|
||
# 分配额外角色
|
||
for rc in role_codes:
|
||
try:
|
||
role = Role.objects.get(RoleName=rc)
|
||
UserRole.objects.create(
|
||
UserRoleUUID=uuid.uuid4().bytes,
|
||
UserUUID=user_main.UserUUID,
|
||
RoleUUID=role.RoleUUID,
|
||
CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
|
||
)
|
||
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 = filter_user_related_by_club(
|
||
UserDashou.query.select_related('user').all(), request,
|
||
)
|
||
|
||
# 关键词筛选(用户ID或昵称)
|
||
if keyword:
|
||
dashou_qs = dashou_qs.filter(
|
||
Q(user__UserUID__icontains=keyword) |
|
||
Q(nicheng__icontains=keyword)
|
||
)
|
||
|
||
# 5. 统计打手总数
|
||
total_dashou = dashou_qs.count()
|
||
|
||
# 6. 统计有效打手数(有会员且未过期)
|
||
dashou_user_ids = dashou_qs.values_list('user__UserUID', flat=True)
|
||
now = timezone.now()
|
||
hy_qs = filter_queryset_by_club(Huiyuangoumai.query.all(), request)
|
||
valid_dashou_count = hy_qs.filter(
|
||
yonghu_id__in=dashou_user_ids,
|
||
huiyuan_zhuangtai=1,
|
||
daoqi_time__gt=now,
|
||
).values('yonghu_id').distinct().count()
|
||
|
||
# 7. 分页
|
||
offset = (page - 1) * page_size
|
||
dashou_list = dashou_qs.order_by('-CreateTime')[offset:offset + page_size]
|
||
|
||
# 8. 构建返回数据
|
||
result = []
|
||
for dashou in dashou_list:
|
||
user = dashou.user
|
||
result.append({
|
||
'yonghuid': user.UserUID,
|
||
'avatar': user.Avatar or '',
|
||
'zaixianzhuangtai': dashou.zaixianzhuangtai,
|
||
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
|
||
'nicheng': dashou.nicheng or '',
|
||
'zhuangtai': dashou.zhuangtai,
|
||
})
|
||
|
||
has_more = (offset + len(dashou_list)) < total_dashou
|
||
|
||
meta = list_response_meta(request)
|
||
return Response({
|
||
'code': 0,
|
||
'club_id': meta['club_id'],
|
||
'scope': meta['scope'],
|
||
'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 = User.query.select_related('DashouProfile').get(
|
||
UserUID=uid
|
||
)
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手不存在'})
|
||
|
||
denied = forbid_if_user_out_of_scope(request, user_main)
|
||
if denied:
|
||
return denied
|
||
|
||
try:
|
||
dashou = user_main.DashouProfile
|
||
except ObjectDoesNotExist:
|
||
# 用户存在但未注册打手身份,返回基本信息
|
||
user_info = {
|
||
'yonghuid': user_main.UserUID,
|
||
'avatar': user_main.Avatar or '',
|
||
'zaixianzhuangtai': 0,
|
||
'zhanghaozhuangtai': 0,
|
||
'zhuangtai': 0,
|
||
'nicheng': '',
|
||
'chenghao': '',
|
||
'dianhua': '',
|
||
'wechat': '',
|
||
'phone': user_main.Phone or '',
|
||
'CreateTime': user_main.UserCreateTime,
|
||
'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.query.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.UserUID,
|
||
'avatar': user_main.Avatar or '',
|
||
'zaixianzhuangtai': dashou.zaixianzhuangtai,
|
||
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
|
||
'zhuangtai': dashou.zhuangtai,
|
||
'nicheng': dashou.nicheng or '',
|
||
'chenghao': dashou.chenghao or '',
|
||
'dianhua': dashou.dianhua or '',
|
||
'wechat': dashou.wechat or '',
|
||
'phone': user_main.Phone or '',
|
||
'CreateTime': user_main.UserCreateTime,
|
||
'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.query.filter(
|
||
yonghu_id=uid
|
||
).order_by('-CreateTime')
|
||
member_list = []
|
||
for record in member_records:
|
||
# 检查是否过期
|
||
if hasattr(record, 'jiance_shifou_daoqi'):
|
||
is_expired = record.jiance_shifou_daoqi()
|
||
else:
|
||
is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
|
||
|
||
if not is_expired:
|
||
try:
|
||
huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id)
|
||
huiyuan_name = huiyuan.jieshao
|
||
except Huiyuan.DoesNotExist:
|
||
huiyuan_name = '未知会员'
|
||
member_list.append({
|
||
'id': record.id,
|
||
'huiyuan_name': huiyuan_name,
|
||
'daoqi_time': record.daoqi_time,
|
||
'is_active': record.huiyuan_zhuangtai == 1,
|
||
'huiyuan_id': record.huiyuan_id, # 用于删除操作
|
||
})
|
||
|
||
# 7. 查询所有会员类型(用于前端添加会员)
|
||
all_members = list(Huiyuan.query.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 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 = User.query.get(UserUID=dashou_id)
|
||
dashou_profile = dashou_user.DashouProfile
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手不存在'})
|
||
except UserDashou.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手扩展信息缺失'})
|
||
|
||
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
|
||
write_xiugai_log(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_DASHOU,
|
||
xiugaitijiao=after,
|
||
xiugaitijiaoq=before['yue'],
|
||
qitashuoming=(
|
||
f'【后台】减少打手可提现余额:{before["yue"]}元 → {after}元,'
|
||
f'减少{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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
|
||
write_xiugai_log(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_DASHOU,
|
||
xiugaitijiao=after,
|
||
xiugaitijiaoq=before['yue'],
|
||
qitashuoming=(
|
||
f'【后台】增加打手可提现余额:{before["yue"]}元 → {after}元,'
|
||
f'增加{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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
|
||
write_xiugai_log(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_DASHOU,
|
||
yajin=after,
|
||
yyajin=before['yajin'],
|
||
qitashuoming=(
|
||
f'【后台】减少打手押金:{before["yajin"]}元 → {after}元,'
|
||
f'减少{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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
|
||
write_xiugai_log(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_DASHOU,
|
||
yajin=after,
|
||
yyajin=before['yajin'],
|
||
qitashuoming=(
|
||
f'【后台】增加打手押金:{before["yajin"]}元 → {after}元,'
|
||
f'增加{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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
|
||
write_xiugai_log(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_DASHOU,
|
||
jifen=after,
|
||
yjifen=before['jifen'],
|
||
qitashuoming=(
|
||
f'【后台】减少打手积分:{before["jifen"]} → {after},'
|
||
f'减少{jifen}分,打手ID={dashou_id},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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
|
||
write_xiugai_log(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_DASHOU,
|
||
jifen=after,
|
||
yjifen=before['jifen'],
|
||
qitashuoming=(
|
||
f'【后台】增加打手积分:{before["jifen"]} → {after},'
|
||
f'增加{jifen}分,打手ID={dashou_id},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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()
|
||
status_label = '正常' if new_status == 1 else '封禁'
|
||
old_label = '正常' if before['zhanghaozhuangtai'] == 1 else '封禁'
|
||
write_xiugai_log(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_DASHOU,
|
||
qitashuoming=(
|
||
f'【后台】修改打手账号状态:{old_label} → {status_label},'
|
||
f'打手ID={dashou_id},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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()
|
||
status_label = '在线' if new_status == 1 else '离线'
|
||
old_label = '在线' if before['zaixianzhuangtai'] == 1 else '离线'
|
||
write_xiugai_log(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_DASHOU,
|
||
qitashuoming=(
|
||
f'【后台】修改打手在线状态:{old_label} → {status_label},'
|
||
f'打手ID={dashou_id},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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()
|
||
status_label = '空闲' if new_status == 1 else '忙碌'
|
||
old_label = '空闲' if before['zhuangtai'] == 1 else '忙碌'
|
||
write_xiugai_log(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_DASHOU,
|
||
qitashuoming=(
|
||
f'【后台】修改打手接单状态:{old_label} → {status_label},'
|
||
f'打手ID={dashou_id},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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.query.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()
|
||
write_xiugai_log(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_DASHOU,
|
||
huiyuants=days,
|
||
huiyuan_id=huiyuan_id,
|
||
qitashuoming=(
|
||
f'【后台】给打手添加会员:会员ID={huiyuan_id}({huiyuan.jieshao}),'
|
||
f'增加{days}天,到期时间={record.daoqi_time.strftime("%Y-%m-%d %H:%M:%S")},'
|
||
f'打手ID={dashou_id},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
|
||
|
||
elif caozuo == 'jian_huiyuan':
|
||
if '001gg' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限给打手减会员'})
|
||
if not huiyuan_id:
|
||
return Response({'code': 400, 'msg': '缺少会员ID'})
|
||
try:
|
||
record = Huiyuangoumai.query.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id)
|
||
except Huiyuangoumai.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '该打手未购买此会员'})
|
||
record.delete()
|
||
write_xiugai_log(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_DASHOU,
|
||
huiyuan_id=huiyuan_id,
|
||
qitashuoming=(
|
||
f'【后台】移除打手会员:会员ID={huiyuan_id},'
|
||
f'打手ID={dashou_id},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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()
|
||
write_xiugai_log(
|
||
yonghuid=dashou_id,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_DASHOU,
|
||
qitashuoming=(
|
||
f'【后台】修改打手称号:{before["chenghao"] or "无"} → {new_chenghao},'
|
||
f'打手ID={dashou_id},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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 = filter_user_related_by_club(
|
||
UserShangjia.query.select_related('user').all(), request,
|
||
)
|
||
|
||
# 关键词筛选(用户ID或昵称)
|
||
if keyword:
|
||
shangjia_qs = shangjia_qs.filter(
|
||
Q(user__UserUID__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()
|
||
valid_merchant_count = shangjia_qs.filter(zhuangtai=1).count()
|
||
|
||
# 6. 分页
|
||
offset = (page - 1) * page_size
|
||
shangjia_list = shangjia_qs.order_by('-CreateTime')[offset:offset + page_size]
|
||
|
||
# 7. 构建返回数据
|
||
result = []
|
||
for shangjia in shangjia_list:
|
||
user = shangjia.user
|
||
result.append({
|
||
'yonghuid': user.UserUID,
|
||
'avatar': user.Avatar or '',
|
||
'nicheng': shangjia.nicheng or '',
|
||
'yue': str(shangjia.yue), # 转换为字符串,避免前端精度问题
|
||
'fabu': shangjia.fabu,
|
||
'chengjiao': shangjia.chengjiao,
|
||
'zhuangtai': shangjia.zhuangtai, # 1=正常,2=禁用
|
||
})
|
||
|
||
meta = list_response_meta(request)
|
||
return Response({
|
||
'code': 0,
|
||
'club_id': meta['club_id'],
|
||
'scope': meta['scope'],
|
||
'data': {
|
||
'list': result,
|
||
'total': total,
|
||
'valid_merchant_count': valid_merchant_count,
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# 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.query.select_related('user').get(user__UserUID=yonghuid)
|
||
except UserShangjia.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商家不存在'})
|
||
|
||
denied = forbid_if_user_out_of_scope(request, shangjia.user)
|
||
if denied:
|
||
return denied
|
||
|
||
user = shangjia.user
|
||
|
||
data = {
|
||
'yonghuid': user.UserUID,
|
||
'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),
|
||
'CreateTime': shangjia.CreateTime.isoformat() if shangjia.CreateTime 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__UserUID=yonghuid)
|
||
except UserShangjia.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商家不存在'})
|
||
|
||
# 增加余额
|
||
if action == 'add_balance':
|
||
amount_str = request.data.get('amount')
|
||
try:
|
||
amount = Decimal(str(amount_str))
|
||
if amount <= 0 or amount > 1000000:
|
||
return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'})
|
||
except:
|
||
return Response({'code': 400, 'msg': '金额格式错误'})
|
||
old_yue = shangjia.yue
|
||
new_yue = shangjia.yue + amount
|
||
if new_yue > 999999999.99:
|
||
return Response({'code': 400, 'msg': '增加后余额超出上限'})
|
||
shangjia.yue = new_yue
|
||
shangjia.save(update_fields=['yue'])
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_SHANGJIA,
|
||
shangjiayue=new_yue,
|
||
shangjiayueq=old_yue,
|
||
qitashuoming=(
|
||
f'【后台】增加商家余额:{old_yue}元 → {new_yue}元,增加{amount}元,'
|
||
f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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': '余额不足'})
|
||
old_yue = shangjia.yue
|
||
shangjia.yue = shangjia.yue - amount
|
||
shangjia.save(update_fields=['yue'])
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_SHANGJIA,
|
||
shangjiayue=shangjia.yue,
|
||
shangjiayueq=old_yue,
|
||
qitashuoming=(
|
||
f'【后台】减少商家余额:{old_yue}元 → {shangjia.yue}元,减少{amount}元,'
|
||
f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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'])
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_SHANGJIA,
|
||
qitashuoming=(
|
||
f'【后台】封禁商家账号:正常 → 禁用,'
|
||
f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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'])
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_SHANGJIA,
|
||
qitashuoming=(
|
||
f'【后台】解封商家账号:禁用 → 正常,'
|
||
f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
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.query.all().order_by('paixu')
|
||
type_list = []
|
||
for t in type_qs:
|
||
# 🔥 修改:统计该类型下的公共商品数量(店铺为空且店铺商品类型为空)
|
||
product_count = Shangpin.query.filter(
|
||
leixing_id=t.id,
|
||
dianpu__isnull=True, # 所属店铺为空
|
||
dianpu_leixing__isnull=True # 店铺商品类型为空
|
||
).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.query.all().order_by('paixu')
|
||
zone_list = []
|
||
for z in zone_qs:
|
||
# 🔥 修改:统计该专区下的公共商品数量(店铺为空且店铺商品类型为空)
|
||
product_count = Shangpin.query.filter(
|
||
zhuanqu_id=z.id,
|
||
dianpu__isnull=True, # 所属店铺为空
|
||
dianpu_leixing__isnull=True # 店铺商品类型为空
|
||
).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.query.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.query.filter(
|
||
dianpu__isnull=True, # 所属店铺为空
|
||
dianpu_leixing__isnull=True # 店铺商品类型为空
|
||
)
|
||
|
||
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('-CreateTime') # 默认按创建时间倒序
|
||
|
||
# 分页
|
||
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 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.query.all().order_by('paixu')
|
||
type_list = []
|
||
for t in type_qs:
|
||
# 统计该类型下的商品数量(忽略审核状态?通常统计所有商品,可根据需求调整)
|
||
product_count = Shangpin.query.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.query.all().order_by('paixu')
|
||
zone_list = []
|
||
for z in zone_qs:
|
||
product_count = Shangpin.query.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.query.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.query.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('-CreateTime') # 默认按创建时间倒序
|
||
|
||
# 分页
|
||
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.query.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.query.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_PlayerCommission_str = request.data.get('ewai_PlayerCommission', '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.query.get(id=leixing_id)
|
||
except ShangpinLeixing.DoesNotExist:
|
||
return Response({'code': 400, 'msg': '商品类型不存在'})
|
||
try:
|
||
zhuanqu_obj = ShangpinZhuanqu.query.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.query.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_PlayerCommission_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.query.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.query.get(id=product_id)
|
||
except Shangpin.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商品不存在'})
|
||
|
||
# 先删除OSS上的图片
|
||
tupian_url = product.tupian_url
|
||
if tupian_url:
|
||
# 调用已有的 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.query.get(id=product_id)
|
||
except Shangpin.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商品不存在'})
|
||
|
||
# 查询所有商品类型、专区、会员(用于前端下拉选择)
|
||
type_list = list(ShangpinLeixing.query.all().values(
|
||
'id', 'jieshao', 'tupian_url', 'shenhezhuangtai'
|
||
))
|
||
zone_list = list(ShangpinZhuanqu.query.all().values(
|
||
'id', 'mingzi', 'leixing_id', 'shenhezhuangtai'
|
||
))
|
||
member_list = list(Huiyuan.query.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_PlayerCommission': product.kaioi_ewai_dashou_fencheng,
|
||
'ewai_PlayerCommission': str(product.ewai_dashou_fencheng) if product.ewai_dashou_fencheng is not None else '0',
|
||
'CreateTime': product.CreateTime.isoformat() if product.CreateTime 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.query.filter(id=leixing_id).exists():
|
||
return Response({'code': 400, 'msg': '商品类型不存在'})
|
||
if not ShangpinZhuanqu.query.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.query.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_PlayerCommission') == 'true'
|
||
ewai_fencheng = Decimal('0')
|
||
if kaioi_ewai:
|
||
try:
|
||
ewai_fencheng = Decimal(str(request.data.get('ewai_PlayerCommission', 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.query.all().order_by('-CreateTime')
|
||
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,
|
||
'CreateTime': m.CreateTime.isoformat() if m.CreateTime else None,
|
||
'UpdateTime': m.UpdateTime.isoformat() if m.UpdateTime 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': '修改成功'})
|
||
|
||
|
||
|
||
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.query.filter(huiyuan_id=new_id).exists():
|
||
return new_id
|
||
|
||
with transaction.atomic():
|
||
new_id = generate_huiyuan_id()
|
||
member = Huiyuan.query.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')
|
||
|
||
# 构建查询:从 User 开始,关联 UserGuanshi 和 UserBoss(左连接获取昵称)
|
||
qs = filter_user_qs_by_club(
|
||
User.query.filter(
|
||
GuanshiProfile__isnull=False # 确保是管事
|
||
).select_related('GuanshiProfile').select_related('BossProfile'),
|
||
request,
|
||
)
|
||
|
||
# 关键词搜索(用户ID 或 昵称来自 boss_profile.nickname)
|
||
if keyword:
|
||
qs = qs.filter(
|
||
Q(UserUID__icontains=keyword) |
|
||
Q(BossProfile__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(GuanshiProfile__yue__gte=balance_val)
|
||
elif balance_op == 'lte':
|
||
qs = qs.filter(GuanshiProfile__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(GuanshiProfile__chongzhifenrun__gte=commission_val)
|
||
elif commission_op == 'lte':
|
||
qs = qs.filter(GuanshiProfile__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(GuanshiProfile__yaogingshuliang__gte=invite_val)
|
||
elif invite_count_op == 'lte':
|
||
qs = qs.filter(GuanshiProfile__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(GuanshiProfile__zhuangtai=status_val)
|
||
except:
|
||
return Response({'code': 400, 'msg': '状态值无效'})
|
||
|
||
# 计算总数
|
||
total = qs.count()
|
||
|
||
# 分页
|
||
offset = (page - 1) * page_size
|
||
users = qs.order_by('-UserCreateTime')[offset:offset + page_size]
|
||
|
||
# 构建返回数据
|
||
result = []
|
||
for user in users:
|
||
guanshi = user.GuanshiProfile
|
||
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||
result.append({
|
||
'yonghuid': user.UserUID,
|
||
'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,
|
||
**list_response_meta(request),
|
||
'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 = User.query.select_related('GuanshiProfile', 'BossProfile').get(UserUID=yonghuid)
|
||
guanshi = user.GuanshiProfile
|
||
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '管事不存在'})
|
||
except UserGuanshi.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不是管事'})
|
||
|
||
denied = forbid_if_user_out_of_scope(request, user)
|
||
if denied:
|
||
return denied
|
||
|
||
# 基础信息
|
||
is_zuzhang = UserZuzhang.query.filter(user=user).exists()
|
||
base_data = {
|
||
'yonghuid': user.UserUID,
|
||
'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.CreateTime.isoformat() if guanshi.CreateTime else None,
|
||
'CreateTime': guanshi.CreateTime.isoformat() if guanshi.CreateTime 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.query.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.query.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.query.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,
|
||
'is_zuzhang': is_zuzhang,
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
def _generate_unique_zuzhang_yaoqingma(yonghuid):
|
||
"""生成唯一组长邀请码(与小程序组长注册逻辑一致)"""
|
||
timestamp = str(int(time.time()))[-6:]
|
||
uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0')
|
||
rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2))
|
||
base = f"Z{timestamp}{uid_part}{rand_str}"
|
||
for _ in range(5):
|
||
if not UserZuzhang.query.filter(yaoqingma=base).exists():
|
||
return base
|
||
base = base + random.choice(string.digits)
|
||
return base + str(int(time.time() * 1000))[-4:]
|
||
|
||
|
||
def _create_zuzhang_profile(user):
|
||
"""为已有用户创建组长扩展记录(默认字段与小程序注册一致)"""
|
||
invite_guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count()
|
||
yaoqingma = _generate_unique_zuzhang_yaoqingma(user.UserUID)
|
||
return UserZuzhang.query.create(
|
||
user=user,
|
||
yaoqingma=yaoqingma,
|
||
fenyong_zonge=Decimal('0.00'),
|
||
ketixian_jine=Decimal('0.00'),
|
||
yaoqing_zongshu=invite_guanshi_count,
|
||
jinri_fenyong=Decimal('0.00'),
|
||
jinyue_fenyong=Decimal('0.00'),
|
||
zhuangtai=1,
|
||
jinri_tixian=Decimal('0.00'),
|
||
ewai_tixian_xiane=Decimal('0.00'),
|
||
kaioi_ewai_tixian=False,
|
||
kaioi_ewai_fenhong=False,
|
||
ewai_fenhong_jine=Decimal('0.00'),
|
||
)
|
||
|
||
|
||
class PromoteGuanshiToZuzhangView(APIView):
|
||
"""
|
||
后台将管事用户升级为组长(写入 user_zuzhang 扩展表)
|
||
POST /houtai/htgssjzz
|
||
权限:sjzz666
|
||
参数:username, yonghuid
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
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
|
||
if 'sjzz666' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限升级组长(需要 sjzz666)'})
|
||
|
||
try:
|
||
user = User.query.select_related('GuanshiProfile').get(UserUID=yonghuid)
|
||
guanshi = user.GuanshiProfile
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不存在'})
|
||
except UserGuanshi.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不是管事,无法升级组长'})
|
||
|
||
if UserZuzhang.query.filter(user=user).exists():
|
||
zuzhang = user.ZuzhangProfile
|
||
return Response({
|
||
'code': 400,
|
||
'msg': '该用户已是组长',
|
||
'data': {
|
||
'yonghuid': user.UserUID,
|
||
'yaoqingma': zuzhang.yaoqingma,
|
||
},
|
||
})
|
||
|
||
if guanshi.zhuangtai != 1:
|
||
return Response({'code': 400, 'msg': '管事账号已禁用,无法升级组长'})
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
zuzhang = _create_zuzhang_profile(user)
|
||
except IntegrityError:
|
||
return Response({'code': 400, 'msg': '升级失败,请稍后重试'})
|
||
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=kefu.user.Phone,
|
||
leixing=XIUGAI_LEIXING_GUANSHI,
|
||
qitashuoming=(
|
||
f'【后台】管事升级组长:用户ID={yonghuid},'
|
||
f'生成组长邀请码={zuzhang.yaoqingma},操作人={kefu.user.Phone}'
|
||
),
|
||
)
|
||
|
||
logger.info(f"管事升级组长成功: yonghuid={yonghuid}, yaoqingma={zuzhang.yaoqingma}")
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '已升级为组长',
|
||
'data': {
|
||
'yonghuid': user.UserUID,
|
||
'nickname': getattr(user.BossProfile, 'nickname', '') if hasattr(user, 'BossProfile') else '',
|
||
'yaoqingma': zuzhang.yaoqingma,
|
||
'yaoqing_zongshu': zuzhang.yaoqing_zongshu,
|
||
'ketixian_jine': str(zuzhang.ketixian_jine),
|
||
},
|
||
})
|
||
|
||
|
||
|
||
|
||
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 = User.query.get(UserUID=yonghuid)
|
||
guanshi = user.GuanshiProfile
|
||
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不存在'})
|
||
except UserGuanshi.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不是管事'})
|
||
|
||
# 开始事务,内部使用 select_for_update
|
||
with transaction.atomic():
|
||
# 重新获取加锁对象
|
||
user = User.objects.select_for_update().get(UserUID=yonghuid)
|
||
guanshi = user.GuanshiProfile
|
||
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||
operator = kefu.user.Phone
|
||
|
||
# ---------- 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)'})
|
||
base_changes = []
|
||
old_nickname = boss.nickname if boss else ''
|
||
old_dianhua = guanshi.dianhua
|
||
old_wechat = guanshi.wechat
|
||
old_zhuangtai = guanshi.zhuangtai
|
||
# 处理昵称(在老板扩展表中)
|
||
if nickname is not None:
|
||
if not boss:
|
||
boss = UserBoss.query.create(user=user, nickname=nickname)
|
||
else:
|
||
boss.nickname = nickname
|
||
boss.save()
|
||
if str(nickname) != str(old_nickname):
|
||
base_changes.append(f'昵称:{old_nickname or "无"}→{nickname}')
|
||
# 处理其他字段
|
||
if dianhua is not None:
|
||
guanshi.dianhua = dianhua
|
||
if str(dianhua) != str(old_dianhua):
|
||
base_changes.append(f'电话:{old_dianhua or "无"}→{dianhua}')
|
||
if wechat is not None:
|
||
guanshi.wechat = wechat
|
||
if str(wechat) != str(old_wechat):
|
||
base_changes.append(f'微信:{old_wechat or "无"}→{wechat}')
|
||
if zhuangtai is not None:
|
||
try:
|
||
zt = int(zhuangtai)
|
||
if zt in (0, 1):
|
||
guanshi.zhuangtai = zt
|
||
if zt != old_zhuangtai:
|
||
zt_map = {0: '禁用', 1: '正常'}
|
||
base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}→{zt_map.get(zt, zt)}')
|
||
except:
|
||
pass
|
||
if avatar is not None:
|
||
user.Avatar = avatar
|
||
user.save()
|
||
base_changes.append('更新了头像')
|
||
guanshi.save()
|
||
if base_changes:
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=operator,
|
||
leixing=XIUGAI_LEIXING_GUANSHI,
|
||
qitashuoming=(
|
||
f'【后台】修改管事基础信息:{";".join(base_changes)},'
|
||
f'管事ID={yonghuid},操作人={operator}'
|
||
),
|
||
)
|
||
|
||
# ---------- 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)'})
|
||
action_desc = f'增加{new_yue - old_yue}元'
|
||
elif new_yue < old_yue:
|
||
if '4400b' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限减少余额(需要4400b)'})
|
||
action_desc = f'减少{old_yue - new_yue}元'
|
||
else:
|
||
action_desc = '无变动'
|
||
if new_yue != old_yue:
|
||
guanshi.yue = new_yue
|
||
guanshi.save()
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=operator,
|
||
leixing=XIUGAI_LEIXING_GUANSHI,
|
||
guanshiyue=new_yue,
|
||
guanshiyueq=old_yue,
|
||
qitashuoming=(
|
||
f'【后台】修改管事可提现余额:{old_yue}元 → {new_yue}元({action_desc}),'
|
||
f'管事ID={yonghuid},操作人={operator}'
|
||
),
|
||
)
|
||
|
||
# ---------- 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)'})
|
||
limit_changes = []
|
||
old_kaioi = guanshi.kaioi_ewai_xiane
|
||
old_ewai = guanshi.ewai_xiane
|
||
if kaioi_ewai is not None:
|
||
guanshi.kaioi_ewai_xiane = bool(kaioi_ewai)
|
||
if bool(kaioi_ewai) != old_kaioi:
|
||
limit_changes.append(f'额外限额开关:{"开" if old_kaioi else "关"}→{"开" if kaioi_ewai else "关"}')
|
||
if ewai_val is not None:
|
||
try:
|
||
guanshi.ewai_xiane = Decimal(str(ewai_val))
|
||
if guanshi.ewai_xiane != old_ewai:
|
||
limit_changes.append(f'额外限额金额:{old_ewai}→{guanshi.ewai_xiane}')
|
||
except:
|
||
return Response({'code': 400, 'msg': '额外限额金额格式错误'})
|
||
guanshi.save()
|
||
if limit_changes:
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=operator,
|
||
leixing=XIUGAI_LEIXING_GUANSHI,
|
||
qitashuoming=(
|
||
f'【后台】修改管事提现限额:{";".join(limit_changes)},'
|
||
f'管事ID={yonghuid},操作人={operator}'
|
||
),
|
||
)
|
||
|
||
# ---------- 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):
|
||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||
# 获取当前所有首次定制记录
|
||
existing_first = DuociFenhong.query.filter(
|
||
club_id=guanshi_club, yonghuid=yonghuid, cishu=1,
|
||
)
|
||
# 前端传来的定制字典 {会员ID: 金额}
|
||
for huiyuan_id, amount in custom_first.items():
|
||
try:
|
||
amount = Decimal(str(amount))
|
||
except:
|
||
continue
|
||
if amount <= 0:
|
||
DuociFenhong.query.filter(
|
||
club_id=guanshi_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
|
||
).delete()
|
||
else:
|
||
DuociFenhong.query.update_or_create(
|
||
club_id=guanshi_club,
|
||
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):
|
||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||
# 收集需要保留的记录
|
||
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.query.update_or_create(
|
||
club_id=guanshi_club,
|
||
yonghuid=yonghuid,
|
||
huiyuan=huiyuan_id,
|
||
cishu=cishu,
|
||
defaults={
|
||
'guanshi_fenhong': amount,
|
||
'zuzhang_fenhong': Decimal('0')
|
||
}
|
||
)
|
||
# 删除不在 keep_set 中的额外次数记录
|
||
existing_extra = DuociFenhong.query.filter(
|
||
club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2,
|
||
)
|
||
for record in existing_extra:
|
||
if (record.cishu, record.huiyuan) not in keep_set:
|
||
record.delete()
|
||
|
||
dividend_parts = []
|
||
if perm_enabled is not None:
|
||
dividend_parts.append(f'永久分红开关→{"开" if perm_enabled else "关"}')
|
||
if perm_amount is not None:
|
||
dividend_parts.append(f'永久分红金额→{perm_amount}')
|
||
if custom_first is not None:
|
||
dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)')
|
||
if extra_map is not None:
|
||
dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)')
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=operator,
|
||
leixing=XIUGAI_LEIXING_GUANSHI,
|
||
qitashuoming=(
|
||
f'【后台】修改管事分红策略:{";".join(dividend_parts) or "配置已更新"},'
|
||
f'管事ID={yonghuid},操作人={operator}'
|
||
),
|
||
)
|
||
|
||
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': '无权限查看提现设置'})
|
||
|
||
from jituan.services.withdraw_config import build_withdraw_settings_payload
|
||
return Response({'code': 0, 'data': build_withdraw_settings_payload(request)})
|
||
|
||
|
||
class UpdateWithdrawSettingsView(APIView):
|
||
"""
|
||
修改提现设置(权限细分):
|
||
- 修改打手相关(利率、个人限额、总限额)需要5500a
|
||
- 修改管事相关需要5500b
|
||
- 修改组长相关需要5500c
|
||
注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算),
|
||
后端不直接修改 WithdrawalDailyStats,该表由提现流程自动累加。
|
||
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
|
||
from jituan.services.withdraw_config import apply_withdraw_settings_update
|
||
return apply_withdraw_settings_update(request, permissions)
|
||
|
||
|
||
|
||
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 = filter_user_related_by_club(
|
||
UserZuzhang.query.select_related('user', 'user__BossProfile').all(),
|
||
request,
|
||
)
|
||
|
||
# 6. 应用筛选条件(使用 ORM 安全过滤)
|
||
if keyword:
|
||
queryset = queryset.filter(
|
||
Q(user__UserUID__icontains=keyword) |
|
||
Q(user__BossProfile__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, 'BossProfile') and user.BossProfile:
|
||
nickname = user.BossProfile.nickname or ''
|
||
|
||
data_list.append({
|
||
'yonghuid': user.UserUID,
|
||
'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',
|
||
**list_response_meta(request),
|
||
'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 = User.query.select_related('ZuzhangProfile', 'BossProfile').get(UserUID=yonghuid)
|
||
zuzhang = user.ZuzhangProfile
|
||
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '组长不存在'})
|
||
except UserZuzhang.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不是组长'})
|
||
|
||
denied = forbid_if_user_out_of_scope(request, user)
|
||
if denied:
|
||
return denied
|
||
|
||
# 基础信息
|
||
base_data = {
|
||
'yonghuid': user.UserUID,
|
||
'nickname': boss.nickname if boss else '',
|
||
'avatar': user.Avatar or '',
|
||
'dianhua': '', # 组长表无电话字段?组长扩展表没有电话/微信,但前端可能需要,可从User关联?实际上组长表无电话字段,这里留空或从其他表获取
|
||
'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),
|
||
'CreateTime': zuzhang.CreateTime.isoformat() if zuzhang.CreateTime 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.query.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.query.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.query.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 = User.query.get(UserUID=yonghuid)
|
||
zuzhang = user.ZuzhangProfile
|
||
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不存在'})
|
||
except UserZuzhang.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不是组长'})
|
||
|
||
# 获取原始数据(用于对比金额变化)
|
||
old_ketixian_jine = zuzhang.ketixian_jine
|
||
|
||
# 开始事务
|
||
with transaction.atomic():
|
||
operator = kefu.user.Phone
|
||
old_nickname = boss.nickname if boss else ''
|
||
old_zhuangtai = zuzhang.zhuangtai
|
||
# ---------- 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')
|
||
base_changes = []
|
||
# 状态修改需要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
|
||
if zt != old_zhuangtai:
|
||
zt_map = {0: '禁用', 1: '正常'}
|
||
base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}→{zt_map.get(zt, zt)}')
|
||
except:
|
||
pass
|
||
if nickname:
|
||
if not boss:
|
||
boss = UserBoss.query.create(user=user, nickname=nickname)
|
||
else:
|
||
boss.nickname = nickname
|
||
boss.save()
|
||
if nickname != old_nickname:
|
||
base_changes.append(f'昵称:{old_nickname or "无"}→{nickname}')
|
||
if avatar:
|
||
user.Avatar = avatar
|
||
user.save()
|
||
base_changes.append('更新了头像')
|
||
if base_changes:
|
||
zuzhang.save()
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=operator,
|
||
leixing=XIUGAI_LEIXING_ZUZHANG,
|
||
qitashuoming=(
|
||
f'【后台】修改组长基础信息:{";".join(base_changes)},'
|
||
f'组长ID={yonghuid},操作人={operator}'
|
||
),
|
||
)
|
||
# 组长表没有电话/微信字段,如需存储可暂存到User或忽略
|
||
# 这里不处理
|
||
|
||
# ---------- 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)'})
|
||
action_desc = f'增加{new_val - old_ketixian_jine}元'
|
||
elif new_val < old_ketixian_jine:
|
||
if '6600b' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限减少可提现金额(需要6600b)'})
|
||
action_desc = f'减少{old_ketixian_jine - new_val}元'
|
||
else:
|
||
action_desc = '无变动'
|
||
if new_val != old_ketixian_jine:
|
||
zuzhang.ketixian_jine = new_val
|
||
zuzhang.save(update_fields=['ketixian_jine'])
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=operator,
|
||
leixing=XIUGAI_LEIXING_ZUZHANG,
|
||
xiugaitijiao=new_val,
|
||
xiugaitijiaoq=old_ketixian_jine,
|
||
qitashuoming=(
|
||
f'【后台】修改组长可提现金额:{old_ketixian_jine}元 → {new_val}元({action_desc}),'
|
||
f'组长ID={yonghuid},操作人={operator}'
|
||
),
|
||
)
|
||
|
||
# ---------- 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)'})
|
||
limit_changes = []
|
||
old_kaioi = zuzhang.kaioi_ewai_tixian
|
||
old_ewai = zuzhang.ewai_tixian_xiane
|
||
if kaioi_ewai is not None:
|
||
zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai)
|
||
if bool(kaioi_ewai) != old_kaioi:
|
||
limit_changes.append(f'额外限额开关:{"开" if old_kaioi else "关"}→{"开" if kaioi_ewai else "关"}')
|
||
if ewai_val is not None:
|
||
try:
|
||
zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val))
|
||
if zuzhang.ewai_tixian_xiane != old_ewai:
|
||
limit_changes.append(f'额外限额金额:{old_ewai}→{zuzhang.ewai_tixian_xiane}')
|
||
except:
|
||
return Response({'code': 400, 'msg': '额外限额金额格式错误'})
|
||
zuzhang.save()
|
||
if limit_changes:
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=operator,
|
||
leixing=XIUGAI_LEIXING_ZUZHANG,
|
||
qitashuoming=(
|
||
f'【后台】修改组长提现限额:{";".join(limit_changes)},'
|
||
f'组长ID={yonghuid},操作人={operator}'
|
||
),
|
||
)
|
||
|
||
# ---------- 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):
|
||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||
existing_first = DuociFenhong.query.filter(
|
||
club_id=zuzhang_club, yonghuid=yonghuid, cishu=1,
|
||
)
|
||
for huiyuan_id, amount in custom_first.items():
|
||
try:
|
||
amount = Decimal(str(amount))
|
||
except:
|
||
continue
|
||
if amount <= 0:
|
||
# 删除定制(恢复默认)
|
||
DuociFenhong.query.filter(
|
||
club_id=zuzhang_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
|
||
).delete()
|
||
else:
|
||
DuociFenhong.query.update_or_create(
|
||
club_id=zuzhang_club,
|
||
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):
|
||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||
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.query.update_or_create(
|
||
club_id=zuzhang_club,
|
||
yonghuid=yonghuid,
|
||
huiyuan=huiyuan_id,
|
||
cishu=cishu,
|
||
defaults={
|
||
'zuzhang_fenhong': amount,
|
||
'guanshi_fenhong': Decimal('0')
|
||
}
|
||
)
|
||
# 删除不在 keep_set 中的额外次数记录
|
||
existing_extra = DuociFenhong.query.filter(
|
||
club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2,
|
||
)
|
||
for record in existing_extra:
|
||
if (record.cishu, record.huiyuan) not in keep_set:
|
||
record.delete()
|
||
|
||
dividend_parts = []
|
||
if perm_enabled is not None:
|
||
dividend_parts.append(f'永久分红开关→{"开" if perm_enabled else "关"}')
|
||
if perm_amount is not None:
|
||
dividend_parts.append(f'永久分红金额→{perm_amount}')
|
||
if custom_first is not None:
|
||
dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)')
|
||
if extra_map is not None:
|
||
dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)')
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=operator,
|
||
leixing=XIUGAI_LEIXING_ZUZHANG,
|
||
qitashuoming=(
|
||
f'【后台】修改组长分红策略:{";".join(dividend_parts) or "配置已更新"},'
|
||
f'组长ID={yonghuid},操作人={operator}'
|
||
),
|
||
)
|
||
|
||
# 保存组长其他可能修改的字段(例如电话、微信没有,忽略)
|
||
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
|
||
|
||
class ChangeInviterView(APIView):
|
||
"""
|
||
后台更换邀请人/上级
|
||
POST /houtai/htghyqr
|
||
参数:
|
||
username: 操作人账号
|
||
yonghuid: 被修改用户ID
|
||
target_type: dashou(打手)| guanshi(管事)
|
||
new_yaoqingren: 新邀请人用户ID(须在 User 主表存在)
|
||
权限:
|
||
dashou → 001ee
|
||
guanshi → 4400a
|
||
校验:
|
||
打手新上级须为管事;管事新上级须为组长;不可设为自己
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
yonghuid = request.data.get('yonghuid', '').strip()
|
||
target_type = request.data.get('target_type', '').strip().lower()
|
||
new_yaoqingren = request.data.get('new_yaoqingren', '').strip()
|
||
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
if not yonghuid:
|
||
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||
if target_type not in ('dashou', 'guanshi'):
|
||
return Response({'code': 400, 'msg': 'target_type 须为 dashou 或 guanshi'})
|
||
if not new_yaoqingren:
|
||
return Response({'code': 400, 'msg': '缺少新邀请人ID'})
|
||
if new_yaoqingren == yonghuid:
|
||
return Response({'code': 400, 'msg': '不能将自己设为邀请人'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
if target_type == 'dashou':
|
||
if '001ee' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限更换打手邀请人(需要001ee)'})
|
||
else:
|
||
if '4400a' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限更换管事邀请人(需要4400a)'})
|
||
|
||
try:
|
||
target_user = User.query.get(UserUID=yonghuid)
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '被修改用户不存在'})
|
||
|
||
try:
|
||
inviter_user = User.query.get(UserUID=new_yaoqingren)
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '新邀请人用户ID不存在'})
|
||
|
||
operator = kefu.user.Phone
|
||
|
||
with transaction.atomic():
|
||
if target_type == 'dashou':
|
||
try:
|
||
dashou = target_user.DashouProfile
|
||
except UserDashou.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不是打手'})
|
||
try:
|
||
guanshi_profile = inviter_user.GuanshiProfile
|
||
except UserGuanshi.DoesNotExist:
|
||
return Response({'code': 400, 'msg': '新邀请人不是管事,无法作为打手上级'})
|
||
if guanshi_profile.zhuangtai != 1:
|
||
return Response({'code': 400, 'msg': '新邀请人管事账号已禁用'})
|
||
old_inviter = dashou.yaoqingren or '无'
|
||
dashou.yaoqingren = new_yaoqingren
|
||
dashou.save(update_fields=['yaoqingren'])
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=operator,
|
||
leixing=XIUGAI_LEIXING_DASHOU,
|
||
qitashuoming=(
|
||
f'【后台】更换打手邀请人:{old_inviter} → {new_yaoqingren},'
|
||
f'打手ID={yonghuid},新上级须为管事(已校验),操作人={operator}'
|
||
),
|
||
)
|
||
else:
|
||
try:
|
||
guanshi = target_user.GuanshiProfile
|
||
except UserGuanshi.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不是管事'})
|
||
try:
|
||
zuzhang_profile = inviter_user.ZuzhangProfile
|
||
except UserZuzhang.DoesNotExist:
|
||
return Response({'code': 400, 'msg': '新邀请人不是组长,无法作为管事上级'})
|
||
if zuzhang_profile.zhuangtai != 1:
|
||
return Response({'code': 400, 'msg': '新邀请人组长账号已禁用'})
|
||
old_inviter = guanshi.yaoqingren or '无'
|
||
guanshi.yaoqingren = new_yaoqingren
|
||
guanshi.save(update_fields=['yaoqingren'])
|
||
write_xiugai_log(
|
||
yonghuid=yonghuid,
|
||
xiugaiid=operator,
|
||
leixing=XIUGAI_LEIXING_GUANSHI,
|
||
qitashuoming=(
|
||
f'【后台】更换管事邀请人:{old_inviter} → {new_yaoqingren},'
|
||
f'管事ID={yonghuid},新上级须为组长(已校验),操作人={operator}'
|
||
),
|
||
)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '邀请人更换成功',
|
||
'data': {'yonghuid': yonghuid, 'new_yaoqingren': new_yaoqingren},
|
||
})
|
||
|
||
|
||
class GetOperationLogListView(APIView):
|
||
"""
|
||
后台操作日志列表(xiugaijilu)
|
||
POST /houtai/hqczrz
|
||
权限:000001 超级权限 或 czrz666
|
||
"""
|
||
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 '000001' not in permissions and 'czrz666' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限查看操作日志(需要000001或czrz666)'})
|
||
|
||
yonghuid = request.data.get('yonghuid', '').strip()
|
||
xiugaiid = request.data.get('xiugaiid', '').strip()
|
||
leixing = request.data.get('leixing')
|
||
keyword = request.data.get('keyword', '').strip()
|
||
try:
|
||
page = max(1, int(request.data.get('page', 1)))
|
||
except (ValueError, TypeError):
|
||
page = 1
|
||
try:
|
||
page_size = min(100, max(1, int(request.data.get('page_size', 20))))
|
||
except (ValueError, TypeError):
|
||
page_size = 20
|
||
|
||
qs = Xiugaijilu.query.all().order_by('-CreateTime')
|
||
from jituan.services.admin_audit import filter_xiugaijilu_by_request
|
||
qs = filter_xiugaijilu_by_request(qs, request)
|
||
if yonghuid:
|
||
qs = qs.filter(yonghuid=yonghuid)
|
||
if xiugaiid:
|
||
qs = qs.filter(xiugaiid__icontains=xiugaiid)
|
||
if leixing not in (None, ''):
|
||
try:
|
||
qs = qs.filter(leixing=int(leixing))
|
||
except (ValueError, TypeError):
|
||
pass
|
||
if keyword:
|
||
qs = qs.filter(qitashuoming__icontains=keyword)
|
||
|
||
total = qs.count()
|
||
start = (page - 1) * page_size
|
||
records = qs[start:start + page_size]
|
||
|
||
def _fmt_decimal(val):
|
||
if val is None:
|
||
return None
|
||
return str(val)
|
||
|
||
data_list = []
|
||
for r in records:
|
||
data_list.append({
|
||
'id': r.id,
|
||
'yonghuid': r.yonghuid,
|
||
'xiugaiid': r.xiugaiid,
|
||
'leixing': r.leixing,
|
||
'leixing_label': XIUGAI_LEIXING_LABEL.get(r.leixing, f'类型{r.leixing}'),
|
||
'xiugaitijiao': _fmt_decimal(r.xiugaitijiao),
|
||
'xiugaitijiaoq': _fmt_decimal(r.xiugaitijiaoq),
|
||
'jifen': r.jifen,
|
||
'yjifen': r.yjifen,
|
||
'yajin': _fmt_decimal(r.yajin),
|
||
'yyajin': _fmt_decimal(r.yyajin),
|
||
'shangjiayue': _fmt_decimal(r.shangjiayue),
|
||
'shangjiayueq': _fmt_decimal(r.shangjiayueq),
|
||
'guanshiyue': _fmt_decimal(r.guanshiyue),
|
||
'guanshiyueq': _fmt_decimal(r.guanshiyueq),
|
||
'huiyuants': r.huiyuants,
|
||
'huiyuan_id': r.huiyuan_id,
|
||
'qitashuoming': r.qitashuoming or '',
|
||
'create_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||
})
|
||
|
||
from jituan.services.club_user_access import list_response_meta
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': data_list,
|
||
'total': total,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
**list_response_meta(request),
|
||
},
|
||
})
|
||
|
||
|
||
class GetProductTypeZoneView(APIView):
|
||
"""
|
||
获取商品类型、专区及会员列表
|
||
权限:需要拥有 7007a
|
||
请求:POST /houtai/hthqsplxzq
|
||
参数:{"username": "客服手机号"}
|
||
返回:
|
||
code: 0
|
||
data: {
|
||
type_list: [{id, jieshao, tupian_url, yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, 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.query.all().order_by('-paixu') # paixu 越大越靠前
|
||
type_list = []
|
||
for t in types:
|
||
# 统计该类型下的专区数量
|
||
zone_count = ShangpinZhuanqu.query.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,
|
||
'paixu': t.paixu,
|
||
'product_count': zone_count,
|
||
})
|
||
|
||
# 查询所有专区
|
||
zones = ShangpinZhuanqu.query.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.query.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]:
|
||
raise ValueError
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
|
||
|
||
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:
|
||
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.query.create(
|
||
jieshao=jieshao,
|
||
tupian_url=oss_path,
|
||
yaoqiuleixing=yaoqiuleixing,
|
||
huiyuan_id=huiyuan_id,
|
||
yongjin=yongjin,
|
||
shenhezhuangtai=shenhezhuangtai,
|
||
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.query.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]:
|
||
raise ValueError
|
||
if new_val != leixing.shenhezhuangtai:
|
||
changes['shenhezhuangtai'] = new_val
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
|
||
|
||
# 图片处理
|
||
image_file = request.FILES.get('file')
|
||
old_image_path = leixing.tupian_url
|
||
if image_file:
|
||
try:
|
||
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.query.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.query.get(id=type_id)
|
||
except ShangpinLeixing.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '类型不存在'})
|
||
|
||
with transaction.atomic():
|
||
# 删除该类型下的所有专区
|
||
zones = ShangpinZhuanqu.query.filter(leixing_id=type_id)
|
||
if delete_products:
|
||
# 删除专区下的所有商品
|
||
for zone in zones:
|
||
Shangpin.query.filter(zhuanqu_id=zone.id).delete()
|
||
zones.delete()
|
||
# 删除直接关联该类型的商品
|
||
Shangpin.query.filter(leixing_id=type_id).delete()
|
||
else:
|
||
zones.delete()
|
||
# 仅清除商品的类型关联
|
||
Shangpin.query.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.query.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]:
|
||
raise ValueError
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
zhuanqu = ShangpinZhuanqu.query.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.query.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.query.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]:
|
||
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.query.get(id=zone_id)
|
||
except ShangpinZhuanqu.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '专区不存在'})
|
||
|
||
with transaction.atomic():
|
||
if delete_products:
|
||
Shangpin.query.filter(zhuanqu_id=zone_id).delete()
|
||
else:
|
||
Shangpin.query.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': '您无权查看费率'})
|
||
|
||
try:
|
||
platform_rate_obj = CommissionRate.query.get(Platform='1')
|
||
merchant_rate_obj = CommissionRate.query.get(Platform='3')
|
||
except CommissionRate.DoesNotExist as e:
|
||
logger.error(f"费率记录缺失: {e}")
|
||
return Response({'code': 500, 'msg': '费率配置缺失'})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'platform_rate': float(platform_rate_obj.Rate) if platform_rate_obj.Rate else 0,
|
||
'merchant_rate': float(merchant_rate_obj.Rate) if merchant_rate_obj.Rate 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':
|
||
Platform = '1'
|
||
elif rate_type == 'merchant':
|
||
Platform = '3'
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的type参数'})
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
rate_obj = CommissionRate.objects.select_for_update().get(Platform=Platform)
|
||
rate_obj.Rate = rate_value
|
||
rate_obj.save()
|
||
except CommissionRate.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查询)
|
||
from jituan.services.display_config import filter_popup_page_by_request, list_response_meta
|
||
|
||
pages = filter_popup_page_by_request(
|
||
PopupPage.query.all().prefetch_related('popups__images'),
|
||
request,
|
||
).order_by('id')
|
||
|
||
# 5. 序列化输出
|
||
serializer = PopupPageSerializer(pages, many=True)
|
||
return Response({
|
||
'code': 0,
|
||
'data': {'pages': serializer.data, **list_response_meta(request)},
|
||
})
|
||
|
||
|
||
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': '您没有权限操作弹窗管理'})
|
||
|
||
from jituan.services.display_config import forbid_display_write_in_all_scope
|
||
deny = forbid_display_write_in_all_scope(request)
|
||
if deny:
|
||
return deny
|
||
|
||
# ---------- 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:
|
||
# 捕获未预期的异常,返回服务器错误
|
||
traceback.print_exc()
|
||
return Response({'code': 500, 'msg': f'服务器错误: {str(e)}'})
|
||
|
||
# ==================== 页面相关操作 ====================
|
||
|
||
def add_page(self, request):
|
||
"""添加新页面"""
|
||
from jituan.services.club_context import resolve_club_id_from_request
|
||
from jituan.services.display_config import forbid_display_write_in_all_scope
|
||
|
||
deny = forbid_display_write_in_all_scope(request)
|
||
if deny:
|
||
return deny
|
||
|
||
data = request.data
|
||
# 必填字段校验
|
||
if not data.get('page_key') or not data.get('name'):
|
||
return Response({'code': 400, 'msg': '缺少 page_key 或 name'})
|
||
club_id = resolve_club_id_from_request(request)
|
||
# 唯一性校验(同俱乐部内)
|
||
if PopupPage.query.filter(club_id=club_id, page_key=data['page_key']).exists():
|
||
return Response({'code': 400, 'msg': '页面标识已存在'})
|
||
|
||
page = PopupPage.query.create(
|
||
club_id=club_id,
|
||
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.query.get(id=page_id)
|
||
except PopupPage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '页面不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, page)
|
||
if deny:
|
||
return deny
|
||
|
||
# 更新允许修改的字段
|
||
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.query.get(id=page_id)
|
||
except PopupPage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '页面不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope, forbid_display_write_in_all_scope
|
||
deny = forbid_display_write_in_all_scope(request) or forbid_popup_page_out_of_scope(request, page)
|
||
if deny:
|
||
return deny
|
||
|
||
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.query.get(id=page_id)
|
||
except PopupPage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '页面不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, page)
|
||
if deny:
|
||
return deny
|
||
|
||
# 同一页面下 popup_id 必须唯一
|
||
if PopupConfig.query.filter(page=page, popup_id=popup_id).exists():
|
||
return Response({'code': 400, 'msg': '该页面下已存在相同的弹窗ID'})
|
||
|
||
popup = PopupConfig.query.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.query.get(id=popup_id)
|
||
except PopupConfig.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '弹窗不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, popup.page)
|
||
if deny:
|
||
return deny
|
||
|
||
# 更新弹窗字段
|
||
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.query.get(id=popup_id)
|
||
except PopupConfig.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '弹窗不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, popup.page)
|
||
if deny:
|
||
return deny
|
||
|
||
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.query.get(id=popup_id)
|
||
except PopupConfig.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '弹窗不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, popup.page)
|
||
if deny:
|
||
return deny
|
||
|
||
# 获取上传的文件
|
||
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.query.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.query.get(id=image_id)
|
||
except PopupImage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '图片不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, image.popup_config.page)
|
||
if deny:
|
||
return deny
|
||
|
||
# 更新文字和排序
|
||
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.query.get(id=image_id)
|
||
except PopupImage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '图片不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, image.popup_config.page)
|
||
if deny:
|
||
return deny
|
||
|
||
if image.image_url:
|
||
delete_from_oss(image.image_url)
|
||
image.delete()
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
|
||
|
||
class ShopListView(APIView):
|
||
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': '身份验证失败,请检查登录状态'})
|
||
|
||
# 2. 检查是否拥有所需权限(999a~999e 任意一个)
|
||
required_perms = {'999a', '999b', '999c', '999d', '999e'}
|
||
if not required_perms.intersection(set(permissions)):
|
||
return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'})
|
||
|
||
# 3. 解析请求参数
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 10))
|
||
dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip()
|
||
zhuangtai = request.data.get('zhuangtai')
|
||
lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip()
|
||
user_id = request.data.get('user_id', '').strip()
|
||
ketixian_yue_min = request.data.get('ketixian_yue_min')
|
||
ketixian_yue_max = request.data.get('ketixian_yue_max')
|
||
zhengqu_zonge_min = request.data.get('zhengqu_zonge_min')
|
||
zhengqu_zonge_max = request.data.get('zhengqu_zonge_max')
|
||
|
||
# 4. 构建查询集(先查店铺,预加载关联凭证)
|
||
queryset = Dianpu.query.select_related('pingzheng').all()
|
||
|
||
if dianpu_mingcheng:
|
||
queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng)
|
||
if zhuangtai is not None:
|
||
queryset = queryset.filter(zhuangtai=zhuangtai)
|
||
if lianxi_dianhua:
|
||
queryset = queryset.filter(lianxi_dianhua__icontains=lianxi_dianhua)
|
||
if user_id:
|
||
queryset = queryset.filter(pingzheng__yonghu=user_id)
|
||
|
||
# 金额范围过滤
|
||
if ketixian_yue_min is not None:
|
||
queryset = queryset.filter(ketixian_yue__gte=ketixian_yue_min)
|
||
if ketixian_yue_max is not None:
|
||
queryset = queryset.filter(ketixian_yue__lte=ketixian_yue_max)
|
||
if zhengqu_zonge_min is not None:
|
||
queryset = queryset.filter(zhengqu_zonge__gte=zhengqu_zonge_min)
|
||
if zhengqu_zonge_max is not None:
|
||
queryset = queryset.filter(zhengqu_zonge__lte=zhengqu_zonge_max)
|
||
|
||
# 5. 分页
|
||
paginator = Paginator(queryset, page_size)
|
||
page_obj = paginator.get_page(page)
|
||
|
||
# 6. 构造返回数据
|
||
shop_list = []
|
||
for dianpu in page_obj:
|
||
pingzheng = dianpu.pingzheng
|
||
shop_list.append({
|
||
'id': dianpu.id,
|
||
'dianpu_mingcheng': dianpu.dianpu_mingcheng,
|
||
'dianpu_touxiang': dianpu.dianpu_touxiang or '',
|
||
'lianxi_dianhua': dianpu.lianxi_dianhua or '',
|
||
'weixinhao': dianpu.weixinhao or '',
|
||
'zhuangtai': dianpu.zhuangtai,
|
||
'bangding_yonghushu': dianpu.bangding_yonghushu,
|
||
'erweima_url': dianpu.erweima_url or '',
|
||
'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng,
|
||
'shouyi_choucheng_feilv': dianpu.shouyi_choucheng_feilv or 0,
|
||
'ketixian_yue': dianpu.ketixian_yue,
|
||
'zhengqu_zonge': dianpu.zhengqu_zonge,
|
||
'meiri_tixian_xiane': dianpu.meiri_tixian_xiane or 0,
|
||
'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane,
|
||
'pingzheng__yonghu': pingzheng.yonghu, # 用户ID
|
||
'pingzheng__zhanghao': pingzheng.zhanghao, # 登录账号
|
||
})
|
||
|
||
# 获取默认配置(ID=1,不存在则创建)
|
||
default_config, _ = DianpuMorenPeizhi.query.get_or_create(id=1, defaults={
|
||
'moren_choucheng_feilv': 0.1,
|
||
'moren_tixian_xiane': 1000.00
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'success',
|
||
'data': {
|
||
'list': shop_list,
|
||
'total': paginator.count,
|
||
'default_config': {
|
||
'moren_choucheng_feilv': default_config.moren_choucheng_feilv,
|
||
'moren_tixian_xiane': default_config.moren_tixian_xiane,
|
||
}
|
||
}
|
||
})
|
||
|
||
|
||
class ShopModifyView(APIView):
|
||
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': '身份验证失败,请检查登录状态'})
|
||
|
||
# 转换权限为集合方便判断
|
||
user_perms = set(permissions)
|
||
action = request.data.get('action', '')
|
||
|
||
# 2. 根据 action 分派任务
|
||
if action == 'update_dianpu':
|
||
return self._update_dianpu(request, user_perms)
|
||
elif action == 'add_dianpu':
|
||
return self._add_dianpu(request, user_perms)
|
||
elif action == 'update_default':
|
||
return self._update_default(request, user_perms)
|
||
else:
|
||
return Response({'code': 400, 'msg': '未知操作类型'})
|
||
|
||
def _update_dianpu(self, request, user_perms):
|
||
"""修改店铺信息,精确权限控制"""
|
||
dianpu_id = request.data.get('dianpu_id')
|
||
if not dianpu_id:
|
||
return Response({'code': 400, 'msg': '缺少店铺ID'})
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id)
|
||
|
||
# 基础信息修改 (999e)
|
||
if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']):
|
||
if '999e' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限修改店铺基本信息(999e)'})
|
||
if 'dianpu_mingcheng' in request.data:
|
||
dianpu.dianpu_mingcheng = request.data['dianpu_mingcheng']
|
||
if 'lianxi_dianhua' in request.data:
|
||
dianpu.lianxi_dianhua = request.data['lianxi_dianhua']
|
||
if 'weixinhao' in request.data:
|
||
dianpu.weixinhao = request.data['weixinhao']
|
||
|
||
# 店铺状态 (999a)
|
||
if 'zhuangtai' in request.data:
|
||
if '999a' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限修改店铺状态(999a)'})
|
||
new_status = int(request.data['zhuangtai'])
|
||
if new_status not in (0, 1):
|
||
return Response({'code': 400, 'msg': '店铺状态无效'})
|
||
dianpu.zhuangtai = new_status
|
||
|
||
# 可提现余额 (999b)
|
||
if 'ketixian_yue' in request.data:
|
||
if '999b' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限修改可提现余额(999b)'})
|
||
dianpu.ketixian_yue = request.data['ketixian_yue']
|
||
|
||
# 提现限额相关 (999c)
|
||
if 'kaiqi_meiri_xiane' in request.data or 'meiri_tixian_xiane' in request.data:
|
||
if '999c' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限修改提现限额配置(999c)'})
|
||
if 'kaiqi_meiri_xiane' in request.data:
|
||
dianpu.kaiqi_meiri_xiane = request.data['kaiqi_meiri_xiane']
|
||
if 'meiri_tixian_xiane' in request.data:
|
||
dianpu.meiri_tixian_xiane = request.data['meiri_tixian_xiane']
|
||
|
||
# 抽成相关 (999d)
|
||
if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data:
|
||
if '999d' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限修改抽成配置(999d)'})
|
||
if 'kaiqi_shouyi_choucheng' in request.data:
|
||
dianpu.kaiqi_shouyi_choucheng = request.data['kaiqi_shouyi_choucheng']
|
||
if 'shouyi_choucheng_feilv' in request.data:
|
||
dianpu.shouyi_choucheng_feilv = request.data['shouyi_choucheng_feilv']
|
||
|
||
dianpu.save() # Django 会自动判断更新哪些字段
|
||
return Response({'code': 0, 'msg': '店铺信息更新成功'})
|
||
|
||
except Dianpu.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '店铺不存在'})
|
||
except Exception as e:
|
||
logger.exception("修改店铺失败")
|
||
return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
|
||
|
||
def _add_dianpu(self, request, user_perms):
|
||
"""添加店铺,需要权限 999b"""
|
||
if '999b' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限添加店铺(999b)'})
|
||
|
||
yonghu_id = request.data.get('yonghu_id')
|
||
zhanghao = request.data.get('zhanghao')
|
||
mima = request.data.get('mima')
|
||
dianpu_mingcheng = request.data.get('dianpu_mingcheng')
|
||
|
||
if not all([yonghu_id, zhanghao, mima, dianpu_mingcheng]):
|
||
return Response({'code': 400, 'msg': '必填字段缺失(用户ID、账号、密码、店铺名称)'})
|
||
|
||
# 检查用户是否存在
|
||
if not User.query.filter(UserUID=yonghu_id).exists():
|
||
return Response({'code': 404, 'msg': '用户不存在'})
|
||
|
||
# 检查凭证是否已存在
|
||
if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists():
|
||
return Response({'code': 400, 'msg': '登录账号已存在'})
|
||
if YonghuPingzheng.query.filter(yonghu=yonghu_id).exists():
|
||
return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'})
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
pingzheng = YonghuPingzheng.query.create(
|
||
yonghu=yonghu_id,
|
||
zhanghao=zhanghao,
|
||
mima=mima,
|
||
is_active=True
|
||
)
|
||
dianpu = Dianpu.query.create(
|
||
pingzheng=pingzheng,
|
||
dianpu_mingcheng=dianpu_mingcheng,
|
||
lianxi_dianhua=request.data.get('lianxi_dianhua', ''),
|
||
weixinhao=request.data.get('weixinhao', ''),
|
||
kaiqi_shouyi_choucheng=request.data.get('kaiqi_shouyi_choucheng', False),
|
||
shouyi_choucheng_feilv=request.data.get('shouyi_choucheng_feilv', 0),
|
||
kaiqi_meiri_xiane=request.data.get('kaiqi_meiri_xiane', False),
|
||
meiri_tixian_xiane=request.data.get('meiri_tixian_xiane', 0),
|
||
)
|
||
return Response({'code': 0, 'msg': '店铺创建成功', 'dianpu_id': dianpu.id})
|
||
except IntegrityError as e:
|
||
logger.error("创建店铺违反唯一性约束: %s", e)
|
||
return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'})
|
||
except Exception as e:
|
||
logger.exception("创建店铺失败")
|
||
return Response({'code': 500, 'msg': '创建失败,请稍后重试'})
|
||
|
||
def _update_default(self, request, user_perms):
|
||
"""修改默认配置,按修改内容校验权限"""
|
||
feilv = request.data.get('moren_choucheng_feilv')
|
||
xiane = request.data.get('moren_tixian_xiane')
|
||
modify_commission = feilv is not None
|
||
modify_withdraw = xiane is not None
|
||
|
||
if modify_commission and '999d' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'})
|
||
if modify_withdraw and '999c' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'})
|
||
|
||
try:
|
||
config, _ = DianpuMorenPeizhi.query.get_or_create(id=1)
|
||
if modify_commission:
|
||
config.moren_choucheng_feilv = feilv
|
||
if modify_withdraw:
|
||
config.moren_tixian_xiane = xiane
|
||
config.save(update_fields=(['moren_choucheng_feilv'] if modify_commission else []) +
|
||
(['moren_tixian_xiane'] if modify_withdraw else []))
|
||
return Response({'code': 0, 'msg': '默认配置更新成功'})
|
||
except Exception as e:
|
||
logger.exception("更新默认配置失败")
|
||
return Response({'code': 500, 'msg': '更新失败'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# ==================== 1. 公共商品类型 + 审核模式 ====================
|
||
class ShopPublicTypeAndAuditView(APIView):
|
||
"""
|
||
返回:①正常状态的公共商品类型列表 ②全局商品审核模式开关
|
||
前端请求路径:/houtai/htdphqsplx
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# ---------- 身份与权限校验 ----------
|
||
username = request.data.get('username')
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||
|
||
# 拥有 1199ab/1199abc/1199abd 任一个即可访问
|
||
if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
|
||
return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'})
|
||
|
||
# ---------- 查询公共商品类型(仅正常审核状态的) ----------
|
||
try:
|
||
# shenhezhuangtai = 1 表示正常 (根据模型默认值)
|
||
types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values(
|
||
'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin'
|
||
)
|
||
type_list = []
|
||
for t in types_qs:
|
||
type_list.append({
|
||
'id': t['id'],
|
||
'jieshao': t['jieshao'] or '',
|
||
'tupian_url': t['tupian_url'] or '',
|
||
'yaoqiuleixing': t['yaoqiuleixing'],
|
||
'huiyuan_id': t['huiyuan_id'],
|
||
'yongjin': t['yongjin'],
|
||
})
|
||
except Exception as e:
|
||
logger.exception("查询公共商品类型失败")
|
||
return Response({'code': 500, 'msg': '服务器错误,请稍后重试'})
|
||
|
||
# ---------- 获取审核模式配置 ----------
|
||
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
|
||
kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'public_types': type_list,
|
||
'kaiqi_shenhe': kaiqi_shenhe
|
||
}
|
||
})
|
||
|
||
|
||
# ==================== 2. 店铺列表(支持搜索、筛选、分页) ====================
|
||
class ShopListForProductView(APIView):
|
||
"""
|
||
返回店铺列表,用于商品管理页面选择店铺。
|
||
支持:关键词搜索(店铺ID/名称/用户ID)、店铺状态筛选、分页。
|
||
前端请求路径:/houtai/htdphqdpys
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username')
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return Response({'code': 403, 'msg': '身份验证失败'})
|
||
if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
|
||
return Response({'code': 403, 'msg': '无权访问店铺列表'})
|
||
|
||
# ---------- 安全解析分页参数 ----------
|
||
try:
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 10))
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '分页参数格式错误'})
|
||
|
||
# 搜索关键词(字符串,ORM 自动防注入)
|
||
keyword = str(request.data.get('keyword', '')).strip()
|
||
# 店铺状态筛选(1正常,0封禁,不传则全部)
|
||
zhuangtai = request.data.get('zhuangtai')
|
||
|
||
# ---------- 构建查询集(关联凭证表获取用户ID) ----------
|
||
shops = Dianpu.query.select_related('pingzheng').all()
|
||
|
||
# 状态筛选:只接受 0 或 1
|
||
if zhuangtai is not None:
|
||
try:
|
||
zhuangtai = int(zhuangtai)
|
||
if zhuangtai in [0, 1]:
|
||
shops = shops.filter(zhuangtai=zhuangtai)
|
||
else:
|
||
return Response({'code': 400, 'msg': '状态参数只能为0或1'})
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '状态参数必须为整数'})
|
||
|
||
# 关键词搜索(店铺ID/名称/用户ID)
|
||
if keyword:
|
||
shops = shops.filter(
|
||
models.Q(id__icontains=keyword) |
|
||
models.Q(dianpu_mingcheng__icontains=keyword) |
|
||
models.Q(pingzheng__yonghu__icontains=keyword)
|
||
)
|
||
|
||
# ---------- 分页 ----------
|
||
paginator = Paginator(shops, page_size)
|
||
page_obj = paginator.get_page(page)
|
||
|
||
shop_list = []
|
||
for shop in page_obj:
|
||
shop_list.append({
|
||
'id': shop.id,
|
||
'dianpu_mingcheng': shop.dianpu_mingcheng,
|
||
'zhuangtai': shop.zhuangtai,
|
||
'yonghu_id': shop.pingzheng.yonghu if shop.pingzheng else '',
|
||
'lianxi_dianhua': shop.lianxi_dianhua or '',
|
||
'weixinhao': shop.weixinhao or '',
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': shop_list,
|
||
'total': paginator.count
|
||
}
|
||
})
|
||
|
||
|
||
# ==================== 3. 店铺商品类型映射列表(含筛选、待审核计数) ====================
|
||
class ShopProductTypeMappingView(APIView):
|
||
"""
|
||
返回店铺商品类型映射(ShangpinLeixingDianpu)列表。
|
||
支持:店铺ID、公共类型ID、上架/封禁/审核状态筛选,关键字搜索(介绍/ID/店铺名/公共类型名)。
|
||
每次请求同时返回“待审核总数”(shenhe_zhuangtai=False 的记录数)。
|
||
前端请求路径:/houtai/htdphqyssplx
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username')
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return Response({'code': 403, 'msg': '身份验证失败'})
|
||
if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
|
||
return Response({'code': 403, 'msg': '无权访问商品类型映射'})
|
||
|
||
# ---------- 安全解析参数 ----------
|
||
try:
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 10))
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '分页参数格式错误'})
|
||
|
||
keyword = str(request.data.get('keyword', '')).strip()
|
||
shop_id = request.data.get('shop_id')
|
||
public_type_id = request.data.get('public_type_id')
|
||
|
||
# 布尔型筛选字段处理:None 表示不筛选,否则转为布尔值
|
||
def parse_bool(val):
|
||
if val is None:
|
||
return None
|
||
return bool(val)
|
||
|
||
shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
|
||
fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
|
||
shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
|
||
|
||
# ---------- 查询(连表店铺和公共类型) ----------
|
||
mappings = ShangpinLeixingDianpu.query.select_related('dianpu', 'gonggong_leixing').all()
|
||
|
||
# 筛选条件:均为参数化,完全防注入
|
||
if shop_id is not None:
|
||
mappings = mappings.filter(dianpu_id=int(shop_id))
|
||
if public_type_id is not None:
|
||
mappings = mappings.filter(gonggong_leixing_id=int(public_type_id))
|
||
if shangjia is not None:
|
||
mappings = mappings.filter(shangjia_zhuangtai=shangjia)
|
||
if fengjin is not None:
|
||
mappings = mappings.filter(fengjin_zhuangtai=fengjin)
|
||
if shenhe is not None:
|
||
mappings = mappings.filter(shenhe_zhuangtai=shenhe)
|
||
|
||
# 关键字搜索(介绍、ID、店铺名、公共类型名)
|
||
if keyword:
|
||
mappings = mappings.filter(
|
||
models.Q(jieshao__icontains=keyword) |
|
||
models.Q(id__icontains=keyword) |
|
||
models.Q(dianpu__dianpu_mingcheng__icontains=keyword) |
|
||
models.Q(gonggong_leixing__jieshao__icontains=keyword)
|
||
)
|
||
|
||
# ---------- 待审核总数(不受分页影响) ----------
|
||
pending_total = mappings.filter(shenhe_zhuangtai=False).count()
|
||
|
||
# ---------- 分页 ----------
|
||
paginator = Paginator(mappings, page_size)
|
||
page_obj = paginator.get_page(page)
|
||
|
||
mapping_list = []
|
||
for m in page_obj:
|
||
mapping_list.append({
|
||
'id': m.id,
|
||
'dianpu_id': m.dianpu_id,
|
||
'dianpu_name': m.dianpu.dianpu_mingcheng if m.dianpu else '',
|
||
'gonggong_leixing_id': m.gonggong_leixing_id,
|
||
'gonggong_leixing_name': m.gonggong_leixing.jieshao if m.gonggong_leixing else '',
|
||
'jieshao': m.jieshao,
|
||
'tupian_url': m.tupian_url or '',
|
||
'paixu': m.paixu,
|
||
'shangjia_zhuangtai': m.shangjia_zhuangtai,
|
||
'fengjin_zhuangtai': m.fengjin_zhuangtai,
|
||
'shenhe_zhuangtai': m.shenhe_zhuangtai,
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': mapping_list,
|
||
'total': paginator.count,
|
||
'pending_total': pending_total
|
||
}
|
||
})
|
||
|
||
|
||
# ==================== 统一修改接口(已修正事务) ====================
|
||
class ShopProductModifyView(APIView):
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username')
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return Response({'code': 403, 'msg': '身份验证失败'})
|
||
|
||
user_perms = set(permissions)
|
||
action = request.data.get('action')
|
||
|
||
if action == 'update_audit_mode':
|
||
return self._update_audit_mode(request, user_perms)
|
||
elif action == 'update_mapping_status':
|
||
return self._update_mapping_status(request, user_perms)
|
||
else:
|
||
return Response({'code': 400, 'msg': '未知操作'})
|
||
|
||
@transaction.atomic # ✅ 事务修复点
|
||
def _update_audit_mode(self, request, user_perms):
|
||
if '1199ab' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限修改审核模式'})
|
||
|
||
kaiqi = request.data.get('kaiqi_shenhe')
|
||
if kaiqi is None:
|
||
return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'})
|
||
|
||
try:
|
||
config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1)
|
||
config.kaiqi_shenhe = bool(kaiqi)
|
||
config.save(update_fields=['kaiqi_shenhe'])
|
||
return Response({'code': 0, 'msg': '审核模式已更新'})
|
||
except Exception as e:
|
||
logger.exception("更新审核模式失败")
|
||
return Response({'code': 500, 'msg': '服务器错误'})
|
||
|
||
@transaction.atomic
|
||
def _update_mapping_status(self, request, user_perms):
|
||
mapping_id = request.data.get('mapping_id')
|
||
if not mapping_id:
|
||
return Response({'code': 400, 'msg': '缺少 mapping_id'})
|
||
|
||
try:
|
||
mapping = ShangpinLeixingDianpu.objects.select_for_update().get(id=int(mapping_id))
|
||
except (ShangpinLeixingDianpu.DoesNotExist, ValueError):
|
||
return Response({'code': 404, 'msg': '映射不存在'})
|
||
|
||
fengjin = request.data.get('fengjin_zhuangtai')
|
||
shangjia = request.data.get('shangjia_zhuangtai')
|
||
shenhe = request.data.get('shenhe_zhuangtai')
|
||
|
||
# 权限判断
|
||
if fengjin is not None:
|
||
if fengjin:
|
||
if '1199abc' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限封禁'})
|
||
else:
|
||
if '1199abd' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限解封'})
|
||
mapping.fengjin_zhuangtai = bool(fengjin)
|
||
|
||
if shangjia is not None:
|
||
if not shangjia:
|
||
if '1199abc' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限下架'})
|
||
else:
|
||
if '1199abd' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限上架'})
|
||
mapping.shangjia_zhuangtai = bool(shangjia)
|
||
|
||
if shenhe is not None:
|
||
if shenhe:
|
||
if '1199abd' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限审核通过'})
|
||
mapping.shenhe_zhuangtai = bool(shenhe)
|
||
|
||
mapping.save()
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# 商品管理相关权限组(拥有任意一个即可查看列表)
|
||
PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd', '1199abe']
|
||
|
||
|
||
class ProductListView(APIView):
|
||
"""
|
||
商品列表接口,支持多条件筛选与分页。
|
||
请求路径: /houtai/hqhtdpsplx
|
||
权限要求: 1199ab / 1199abc / 1199abd / 1199abe 任一即可
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# ----- 身份与权限验证 -----
|
||
username = request.data.get('username')
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return Response({'code': 403, 'msg': '身份验证失败'})
|
||
|
||
if not set(permissions).intersection(PRODUCT_PERMS):
|
||
return Response({'code': 403, 'msg': '无商品管理权限'})
|
||
|
||
# ----- 安全解析分页与筛选参数 -----
|
||
try:
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 15))
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '分页参数格式错误'})
|
||
|
||
keyword = str(request.data.get('keyword', '')).strip()
|
||
shop_id = request.data.get('shop_id') # 店铺ID (可选)
|
||
shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型映射ID (可选)
|
||
public_type_id = request.data.get('public_type_id') # 公共商品类型ID (可选)
|
||
|
||
# 布尔型状态筛选
|
||
def parse_bool(val):
|
||
if val is None:
|
||
return None
|
||
return bool(val)
|
||
|
||
shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
|
||
shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
|
||
fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
|
||
|
||
# ----- 构建查询(预加载店铺和店铺商品类型) -----
|
||
products = Shangpin.query.select_related('dianpu', 'dianpu_leixing').all()
|
||
|
||
# 按店铺筛选
|
||
if shop_id is not None:
|
||
try:
|
||
products = products.filter(dianpu_id=int(shop_id))
|
||
except (ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': 'shop_id 格式错误'})
|
||
|
||
# 按店铺商品类型映射筛选
|
||
if shop_product_type_id is not None:
|
||
try:
|
||
products = products.filter(dianpu_leixing_id=int(shop_product_type_id))
|
||
except (ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'})
|
||
|
||
# 按公共商品类型筛选(leixing_id 存储的是公共类型的ID)
|
||
if public_type_id is not None:
|
||
try:
|
||
products = products.filter(leixing_id=int(public_type_id))
|
||
except (ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': 'public_type_id 格式错误'})
|
||
|
||
# 平台审核状态筛选 (BooleanField)
|
||
if shenhe is not None:
|
||
products = products.filter(shenhe_zhuangtai=shenhe)
|
||
|
||
# 上架状态筛选
|
||
if shangjia is not None:
|
||
products = products.filter(shangjia_zhuangtai=shangjia)
|
||
|
||
# 封禁状态筛选
|
||
if fengjin is not None:
|
||
products = products.filter(fengjin_zhuangtai=fengjin)
|
||
|
||
# 关键词搜索:商品ID或商品标题(搜索具有模糊性,使用icontains)
|
||
if keyword:
|
||
# 尝试纯数字可能是商品ID,使用精确匹配与标题模糊搜索
|
||
products = products.filter(
|
||
models.Q(id__icontains=keyword) |
|
||
models.Q(biaoqian__icontains=keyword)
|
||
)
|
||
|
||
# ----- 获取待审核总数(平台审核未通过的数量) -----
|
||
pending_total = products.filter(shenhe_zhuangtai=False).count()
|
||
|
||
# ----- 分页 -----
|
||
paginator = Paginator(products, page_size)
|
||
page_obj = paginator.get_page(page)
|
||
|
||
# ----- 构造返回数据(批量获取公共类型名称,避免N+1) -----
|
||
# 提取所有公共商品类型ID
|
||
type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None}
|
||
public_type_map = {}
|
||
if type_ids:
|
||
public_types = ShangpinLeixing.query.filter(id__in=type_ids).values('id', 'jieshao')
|
||
public_type_map = {t['id']: t['jieshao'] for t in public_types}
|
||
|
||
product_list = []
|
||
for p in page_obj:
|
||
product_list.append({
|
||
'id': p.id,
|
||
'biaoqian': p.biaoqian or '',
|
||
'jiage': p.jiage,
|
||
'kucun': p.kucun,
|
||
'leixing_id': p.leixing_id,
|
||
'public_type_name': public_type_map.get(p.leixing_id, ''),
|
||
'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
|
||
'duiwai_xiaoliang': p.duiwai_xiaoliang,
|
||
'jieshao': p.jieshao or '',
|
||
'xiadan_xuzhi': p.xiadan_xuzhi or '',
|
||
'guize_tupian': p.guize_tupian or '',
|
||
'yaoqiuleixing': p.yaoqiuleixing,
|
||
'huiyuan_id': p.huiyuan_id or '',
|
||
'yongjin': p.yongjin,
|
||
'kaioi_ewai_PlayerCommission': p.kaioi_ewai_dashou_fencheng,
|
||
'ewai_PlayerCommission': p.ewai_dashou_fencheng,
|
||
'CreateTime': p.CreateTime.isoformat() if p.CreateTime else '',
|
||
'UpdateTime': p.UpdateTime.isoformat() if p.UpdateTime else '',
|
||
'paixu': p.paixu,
|
||
'dianpu_id': p.dianpu_id,
|
||
'dianpu_name': p.dianpu.dianpu_mingcheng if p.dianpu else '',
|
||
'dianpu_leixing_id': p.dianpu_leixing_id,
|
||
'dianpu_leixing_name': p.dianpu_leixing.jieshao if p.dianpu_leixing else '',
|
||
'shangjia_zhuangtai': p.shangjia_zhuangtai,
|
||
'fengjin_zhuangtai': p.fengjin_zhuangtai,
|
||
'shenhe_zhuangtai': p.shenhe_zhuangtai,
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': product_list,
|
||
'total': paginator.count,
|
||
'pending_total': pending_total, # 待审核商品总数
|
||
}
|
||
})
|
||
|
||
|
||
class ProductModifyView(APIView):
|
||
"""
|
||
商品修改接口,根据修改内容细粒度鉴权。
|
||
请求路径: /houtai/htxgdpspsj
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username')
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return Response({'code': 403, 'msg': '身份验证失败'})
|
||
|
||
user_perms = set(permissions)
|
||
action = request.data.get('action', '')
|
||
|
||
if action != 'update_product':
|
||
return Response({'code': 400, 'msg': '未知操作'})
|
||
|
||
return self._update_product(request, user_perms)
|
||
|
||
@transaction.atomic
|
||
def _update_product(self, request, user_perms):
|
||
"""执行商品修改,严格校验权限"""
|
||
product_id = request.data.get('product_id')
|
||
if not product_id:
|
||
return Response({'code': 400, 'msg': '缺少商品ID'})
|
||
|
||
try:
|
||
# 行级锁防止并发修改
|
||
product = Shangpin.objects.select_for_update().get(id=int(product_id))
|
||
except (Shangpin.DoesNotExist, ValueError):
|
||
return Response({'code': 404, 'msg': '商品不存在'})
|
||
|
||
# 需要修改的字段
|
||
fields_to_update = {}
|
||
# 将要修改的状态标志用于后续权限判断
|
||
status_changes = {
|
||
'shangjia': request.data.get('shangjia_zhuangtai'),
|
||
'fengjin': request.data.get('fengjin_zhuangtai'),
|
||
'shenhe': request.data.get('shenhe_zhuangtai'),
|
||
}
|
||
|
||
# ---------- 权限分类检查 ----------
|
||
# 1. 封禁/下架操作 (消极操作) -> 1199abc
|
||
if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']):
|
||
if '1199abc' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限执行下架/封禁/驳回审核操作(1199abc)'})
|
||
if status_changes.get('fengjin') is not None and status_changes['fengjin'] == True:
|
||
if '1199abc' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限执行封禁操作(1199abc)'})
|
||
|
||
# 2. 解封/上架/过审 (积极操作) -> 1199abd
|
||
if any(status_changes.get(k) is not None and status_changes[k] == True for k in ['shangjia', 'shenhe']):
|
||
if '1199abd' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限执行上架/解封/审核通过操作(1199abd)'})
|
||
if status_changes.get('fengjin') is not None and status_changes['fengjin'] == False:
|
||
if '1199abd' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'})
|
||
|
||
# 3. 修改其他数据(非状态字段)-> 1199abe
|
||
# 除了上架、封禁、审核之外的字段都属于“其他数据”
|
||
other_fields = [k for k in request.data.keys() if k not in ['action', 'product_id', 'username',
|
||
'shangjia_zhuangtai', 'fengjin_zhuangtai',
|
||
'shenhe_zhuangtai']]
|
||
if other_fields and '1199abe' not in user_perms:
|
||
return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'})
|
||
|
||
# ---------- 应用修改 ----------
|
||
# 允许修改的字段白名单(防止注入)
|
||
# 前端参数名 → 模型字段名映射
|
||
field_name_map = {
|
||
'kaioi_ewai_PlayerCommission': 'kaioi_ewai_dashou_fencheng',
|
||
'ewai_PlayerCommission': 'ewai_dashou_fencheng',
|
||
}
|
||
allowed_fields = [
|
||
'biaoqian', 'jiage', 'kucun', 'leixing_id', 'duiwai_xiaoliang',
|
||
'jieshao', 'xiadan_xuzhi', 'yaoqiuleixing', 'yongjin',
|
||
'kaioi_ewai_PlayerCommission', 'ewai_PlayerCommission',
|
||
'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai'
|
||
]
|
||
|
||
update_fields = []
|
||
for field in allowed_fields:
|
||
value = request.data.get(field)
|
||
if value is not None:
|
||
model_field = field_name_map.get(field, field)
|
||
# 简单类型校验(根据字段类型)
|
||
if field in ['jiage', 'kucun', 'duiwai_xiaoliang', 'yongjin', 'ewai_PlayerCommission', 'paixu']:
|
||
try:
|
||
value = float(value) if 'jiage' in field or 'yongjin' in field or 'ewai' in field else int(value)
|
||
except (ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': f'字段 {field} 值格式错误'})
|
||
setattr(product, model_field, value)
|
||
update_fields.append(model_field)
|
||
|
||
if update_fields:
|
||
product.save(update_fields=update_fields)
|
||
return Response({'code': 0, '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. 若有聊天权限,获取 GoEasy AppKey
|
||
goeasy_appkey = ''
|
||
if has_chat_perm:
|
||
goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '')
|
||
|
||
# 6. 返回数据
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'permissions': permissions,
|
||
'goeasy_appkey': goeasy_appkey
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
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 has_fadan_view_permission(permissions, username):
|
||
return Response({'code': 403, 'msg': '无权限查看罚款统计'})
|
||
|
||
stats = filter_penalty_qs(Penalty.query.all(), request).aggregate(
|
||
total=Count('id'),
|
||
daijiaona=Count('id', filter=Q(Status=1)),
|
||
shensuzhong=Count('id', filter=Q(Status=3)),
|
||
yijiaona=Count('id', filter=Q(Status=2)),
|
||
yibohui=Count('id', filter=Q(Status=4)),
|
||
)
|
||
data = {k: v or 0 for k, v in stats.items()}
|
||
from jituan.services.club_user_access import list_response_meta
|
||
return Response({'code': 0, 'msg': '成功', 'data': {**data, **list_response_meta(request)}})
|
||
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 has_fadan_view_permission(permissions, username):
|
||
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 = filter_penalty_qs(Penalty.query.all(), request)
|
||
|
||
# 状态筛选
|
||
zhuangtai = request.data.get('zhuangtai')
|
||
if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]:
|
||
qs = qs.filter(Status=int(zhuangtai))
|
||
|
||
# 被处罚人ID
|
||
PenalizedUserID = request.data.get('PenalizedUserID', '').strip()
|
||
if PenalizedUserID:
|
||
qs = qs.filter(PenalizedUserID=PenalizedUserID)
|
||
|
||
# 被处罚人身份
|
||
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(Identity=int(beichufa_shenfen))
|
||
|
||
# 模糊搜索(订单ID、被罚人ID、申请人ID)
|
||
sousuo = request.data.get('sousuo', '').strip()
|
||
if sousuo:
|
||
qs = qs.filter(
|
||
Q(RelatedOrderID__icontains=sousuo) |
|
||
Q(PenalizedUserID__icontains=sousuo) |
|
||
Q(ApplicantID__icontains=sousuo)
|
||
)
|
||
|
||
qs = qs.order_by('-CreateTime')
|
||
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)
|
||
penalty_ids = [r.id for r in records]
|
||
|
||
# 批量获取图片并分组
|
||
zhengju_map = {}
|
||
shensu_map = {}
|
||
if penalty_ids:
|
||
tupians = PenaltyAppealImage.query.filter(
|
||
Penalty_id__in=penalty_ids
|
||
).values('Penalty_id', 'ImageURL', 'Purpose')
|
||
for t in tupians:
|
||
fid = t['Penalty_id']
|
||
url = t['ImageURL']
|
||
if t['Purpose'] == 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.PenalizedUserID,
|
||
'shenfen': r.Identity,
|
||
'shenfen_text': dict(Penalty._meta.get_field('Identity').choices).get(r.Identity, ''),
|
||
'shenqing_chufa': r.ApplicantID or '',
|
||
'shenqingren_shenfen': r.ApplicantIdentity,
|
||
'chufaliyou': r.Reason or '',
|
||
'fakuanjine': str(r.FineAmount),
|
||
'guanliandingdan_id': r.RelatedOrderID or '',
|
||
'zhuangtai': r.Status,
|
||
'yingxiang_qiangdan': r.AffectsGrabbing,
|
||
'shensuliyou': r.AppealReason or '',
|
||
'bohuiliyou': r.RejectReason or '',
|
||
'chulizhe': r.ProcessorID or '',
|
||
'chulizhe_shenfen': r.ProcessorIdentity,
|
||
'zhengju_tupian': zhengju_map.get(r.id, []),
|
||
'shensu_tupian': shensu_map.get(r.id, []),
|
||
'creat_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||
'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '',
|
||
})
|
||
|
||
from jituan.services.club_user_access import list_response_meta
|
||
return Response({
|
||
'code': 0, 'msg': '成功',
|
||
'data': {
|
||
'list': results,
|
||
'total': paginator.count,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
**list_response_meta(request),
|
||
}
|
||
})
|
||
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()
|
||
penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id')
|
||
action = request.data.get('action', '').strip()
|
||
chuli_liyou = request.data.get('chuli_liyou', '').strip()
|
||
|
||
if not all([username, penalty_id, action]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
with transaction.atomic():
|
||
try:
|
||
penalty = Penalty.objects.select_for_update().get(id=penalty_id)
|
||
except Penalty.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '罚单不存在'})
|
||
|
||
deny = forbid_penalty_out_of_scope(request, penalty)
|
||
if deny:
|
||
return deny
|
||
|
||
if penalty.Status != 3:
|
||
return Response({'code': 400, 'msg': '当前状态不可处理'})
|
||
|
||
if not check_fadan_permission(permissions, penalty.Identity):
|
||
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_{penalty_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': # 同意申诉 → 驳回处罚
|
||
penalty.Status = 4
|
||
penalty.RejectReason = chuli_liyou
|
||
elif action == 'reject': # 拒绝申诉 → 回到待缴纳
|
||
penalty.Status = 1
|
||
penalty.RejectReason = chuli_liyou
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的操作类型'})
|
||
|
||
# 记录处理者(使用 request.user 的 phone)
|
||
penalty.ProcessorID = request.user.Phone
|
||
penalty.ProcessorIdentity = 2 # 这里假设处理者是售后身份,可根据实际业务获取
|
||
penalty.save()
|
||
|
||
# 保存图片(证据图片,用途1)
|
||
for relative_url in uploaded_urls:
|
||
PenaltyAppealImage.query.create(
|
||
Penalty_id=penalty_id,
|
||
PenalizedUserID=penalty.PenalizedUserID,
|
||
ImageURL=relative_url,
|
||
Purpose=1
|
||
)
|
||
|
||
logger.info(f"罚款处理成功: {penalty_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()
|
||
PenalizedUserID, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data)
|
||
shenfen = request.data.get('shenfen')
|
||
|
||
if not all([username, PenalizedUserID, shenfen, Reason, FineAmount is not None]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
try:
|
||
shenfen = int(shenfen)
|
||
FineAmount = float(FineAmount)
|
||
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 = User.query.get(UserUID=PenalizedUserID)
|
||
profile_attr = SHENFEN_PROFILE_MAP.get(shenfen)
|
||
if not profile_attr or not hasattr(user, profile_attr):
|
||
return Response({'code': 400, 'msg': '该用户不是所选身份'})
|
||
except User.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():
|
||
penalty = Penalty.query.create(
|
||
PenalizedUserID=PenalizedUserID,
|
||
Identity=shenfen,
|
||
ApplicantID=request.user.Phone, # 申请人(客服)手机号
|
||
ApplicantIdentity=2, # 售后身份
|
||
Reason=Reason,
|
||
FineAmount=FineAmount,
|
||
AffectsGrabbing=AffectsGrabbing,
|
||
Status=1, # 待缴纳
|
||
ClubID=resolve_penalty_club_id(
|
||
penalized_user_id=PenalizedUserID, request=request,
|
||
),
|
||
)
|
||
for relative_url in uploaded_urls:
|
||
PenaltyAppealImage.query.create(
|
||
Penalty=penalty,
|
||
PenalizedUserID=PenalizedUserID,
|
||
ImageURL=relative_url,
|
||
Purpose=1
|
||
)
|
||
|
||
logger.info(f"罚款创建成功: {penalty.id}")
|
||
return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': penalty.id}})
|
||
|
||
except Exception as e:
|
||
logger.error(f"创建罚款异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
class FineApplyView(APIView):
|
||
"""
|
||
客服罚款申请接口(基于订单对打手发起罚款)
|
||
POST /houtai/kffkdssq
|
||
权限: JWT认证 + 客服权限验证
|
||
|
||
请求参数:
|
||
username (必填) : 客服用户名(手机号)
|
||
OrderID (必填) : 订单ID
|
||
Reason (必填) : 罚款原因
|
||
FineAmount (必填) : 罚款金额(元)
|
||
AffectsGrabbing (可选) : 是否影响抢单,1=是,0=否,默认1
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username = request.data.get('username', '').strip()
|
||
OrderID = pick_order_id(request.data)
|
||
_, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data)
|
||
|
||
if not all([username, OrderID, Reason, FineAmount is not None]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
try:
|
||
FineAmount = float(FineAmount)
|
||
except (ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': '罚款金额格式错误'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 验证订单存在
|
||
try:
|
||
order = Order.query.get(OrderID=OrderID)
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
# 获取打手ID
|
||
dashou_id = order.PlayerID
|
||
if not dashou_id:
|
||
return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'})
|
||
|
||
# 重复罚款检查
|
||
if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=OrderID).exists():
|
||
return Response({'code': 400, 'msg': '该打手已被罚款过'})
|
||
|
||
# 检查权限(打手身份 shenfen=1)
|
||
if not check_fadan_permission(permissions, 1):
|
||
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_{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():
|
||
penalty = Penalty.query.create(
|
||
PenalizedUserID=dashou_id,
|
||
ApplicantID=request.user.Phone,
|
||
Identity=1, # 被处罚人身份:1 打手
|
||
Reason=Reason,
|
||
FineAmount=FineAmount,
|
||
RelatedOrderID=OrderID,
|
||
Status=1, # 待缴纳
|
||
AffectsGrabbing=AffectsGrabbing,
|
||
ApplicantIdentity=1, # 申请人身份:1 客服
|
||
ClubID=resolve_penalty_club_id(order=order, request=request),
|
||
)
|
||
for relative_url in uploaded_urls:
|
||
PenaltyAppealImage.query.create(
|
||
Penalty=penalty,
|
||
PenalizedUserID=dashou_id,
|
||
ImageURL=relative_url,
|
||
Purpose=1
|
||
)
|
||
|
||
logger.info(f"客服罚款申请成功: 订单 {OrderID}, 打手 {dashou_id}, 金额 {FineAmount}")
|
||
return Response({'code': 0, 'msg': '罚款已生成', 'data': {'id': penalty.id}})
|
||
|
||
except Exception as e:
|
||
logger.error(f"客服罚款申请异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
class PunishDashouView(APIView):
|
||
"""
|
||
客服处罚打手接口(扣除积分)
|
||
POST /houtai/kpcf
|
||
权限: JWT认证 + 客服权限验证
|
||
|
||
请求参数:
|
||
username (必填) : 客服用户名(手机号)
|
||
OrderID (必填) : 订单ID
|
||
reason (可选) : 处罚原因
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username = request.data.get('username', '').strip()
|
||
OrderID = request.data.get('OrderID', '').strip()
|
||
reason = request.data.get('reason', '').strip()
|
||
|
||
if not all([username, OrderID]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 检查权限(打手身份 shenfen=1)
|
||
if not check_fadan_permission(permissions, 1):
|
||
return Response({'code': 403, 'msg': '无权限处罚打手'})
|
||
|
||
# 查询订单
|
||
try:
|
||
order = Order.query.get(OrderID=OrderID)
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
# 获取打手ID
|
||
dashou_id = order.PlayerID
|
||
if not dashou_id:
|
||
return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'})
|
||
|
||
# 查询打手
|
||
try:
|
||
dashou_user = User.query.get(UserUID=dashou_id)
|
||
dashou = dashou_user.DashouProfile
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手用户不存在'})
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 400, 'msg': '该用户不是打手'})
|
||
|
||
# 创建处罚记录(扣除积分5分)
|
||
jifen = 5
|
||
with transaction.atomic():
|
||
penalty_record = PenaltyRecord.query.create(
|
||
PlayerID=dashou_id,
|
||
ApplicantID=username,
|
||
PenaltyReason=reason or '',
|
||
ApplyStatus=0,
|
||
DeductedPoints=jifen,
|
||
OrderID=OrderID,
|
||
ClubID=resolve_penalty_record_club_id(order_id=OrderID, player_id=dashou_id),
|
||
)
|
||
# 扣除打手积分
|
||
dashou.jifen = F('jifen') - jifen
|
||
dashou.save(update_fields=['jifen'])
|
||
|
||
logger.info(f"客服处罚打手成功: 打手 {dashou_id}, 订单 {OrderID}, 扣除积分 {jifen}")
|
||
return Response({'code': 0, 'msg': '处罚成功', 'data': {'penalty_record_id': penalty_record.id}})
|
||
|
||
except Exception as e:
|
||
logger.error(f"客服处罚打手异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
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.query.all():
|
||
bankuai_list.append({
|
||
'bankuai_id': bk.bankuai_id,
|
||
'mingcheng': bk.mingcheng,
|
||
'CreateTime': bk.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if bk.CreateTime else ''
|
||
})
|
||
|
||
# 6. 查询所有商品类型
|
||
shangpin_leixing_list = []
|
||
for sp in ShangpinLeixing.query.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.query.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.query.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.query.create(mingcheng=bankuai_name)
|
||
if shangpin_ids:
|
||
ShangpinLeixing.query.filter(
|
||
id__in=shangpin_ids,
|
||
bankuai__isnull=True
|
||
).update(bankuai=new_bk)
|
||
if huiyuan_ids:
|
||
Huiyuan.query.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.query.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.query.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.query.filter(bankuai=bk).update(bankuai=None)
|
||
Huiyuan.query.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.query.filter(
|
||
id__in=item_ids,
|
||
bankuai__isnull=True
|
||
).update(bankuai=bk)
|
||
elif item_type == 'huiyuan':
|
||
Huiyuan.query.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.query.filter(
|
||
id__in=item_ids,
|
||
bankuai=bk
|
||
).update(bankuai=None)
|
||
elif item_type == 'huiyuan':
|
||
Huiyuan.query.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)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
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.query.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.query.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 = Order.query.filter(
|
||
CreateTime__gte=today_start,
|
||
CreateTime__lt=today_end,
|
||
Status__in=[1,2,3,4,5,6,7,8]
|
||
)
|
||
today_order_count = today_orders.count()
|
||
today_order_amount = float(today_orders.aggregate(total=Sum('Amount'))['total'] or 0.00)
|
||
|
||
# 今日成交(Status=3)
|
||
today_completed = Order.query.filter(
|
||
CreateTime__gte=today_start,
|
||
CreateTime__lt=today_end,
|
||
Status=3
|
||
)
|
||
today_completed_count = today_completed.count()
|
||
today_completed_amount = float(today_completed.aggregate(total=Sum('Amount'))['total'] or 0.00)
|
||
|
||
# 今日退款(Status=5)
|
||
today_tuikuan = Order.query.filter(
|
||
CreateTime__gte=today_start,
|
||
CreateTime__lt=today_end,
|
||
Status=5
|
||
)
|
||
today_tuikuan_count = today_tuikuan.count()
|
||
today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('Amount'))['total'] or 0.00)
|
||
|
||
# 今日充值(会员充值,leixing=1, zhuangtai=3)
|
||
today_chongzhi = Czjilu.query.filter(
|
||
CreateTime__gte=today_start,
|
||
CreateTime__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.query.filter(
|
||
CreateTime__gte=today_start,
|
||
CreateTime__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.query.filter(
|
||
CreateTime__gte=today_start,
|
||
CreateTime__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.query.aggregate(total=Sum('yue'))['total'] or 0.00)
|
||
all_dashou_yue = float(UserDashou.query.aggregate(total=Sum('yue'))['total'] or 0.00)
|
||
all_zuzhang_yue = float(UserZuzhang.query.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00)
|
||
all_shangjia_yue = float(UserShangjia.query.aggregate(total=Sum('yue'))['total'] or 0.00)
|
||
|
||
# 平台总收支及利润
|
||
total_income = float(DailyIncomeStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00)
|
||
total_payout = float(DailyPayoutStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00)
|
||
platform_profit = round(total_income - total_payout, 2)
|
||
|
||
# 近30天每日收支明细
|
||
daily_stats = []
|
||
income_qs = DailyIncomeStat.query.order_by('-date')[:30]
|
||
payout_dict = {
|
||
str(p.date): float(p.total_amount)
|
||
for p in DailyPayoutStat.query.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):
|
||
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)
|
||
|
||
bankuai_list = []
|
||
for bk in Bankuai.query.all():
|
||
members = Huiyuan.query.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})
|
||
except Exception as e:
|
||
logger.exception("CwhybkhqView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
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() 避免模块名冲突
|
||
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('CreateTime')
|
||
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('CreateTime')
|
||
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('CreateTime')
|
||
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,
|
||
'CreateTime__gte': start_dt,
|
||
'CreateTime__lt': end_dt
|
||
}
|
||
|
||
# ---------- 分红过滤 ----------
|
||
fenhong_filter = {
|
||
'huiyuan_id': huiyuan_id, # 分红表会员ID筛选
|
||
'CreateTime__gte': start_dt,
|
||
'CreateTime__lt': end_dt
|
||
}
|
||
|
||
# 充值分组查询
|
||
chongzhi_qs = filter_queryset_by_club(
|
||
Czjilu.query.filter(**chongzhi_filter), request,
|
||
) \
|
||
.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 = filter_gsfenhong_qs(
|
||
Gsfenhong.query.filter(**fenhong_filter), request,
|
||
) \
|
||
.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['CreateTime__date'] = summary_q
|
||
elif granularity == 'month':
|
||
summary_chongzhi_filter['CreateTime__year'] = summary_q.year
|
||
summary_chongzhi_filter['CreateTime__month'] = summary_q.month
|
||
else:
|
||
summary_chongzhi_filter['CreateTime__year'] = summary_q.year
|
||
|
||
summary_chongzhi = Czjilu.query.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['CreateTime__date'] = summary_q
|
||
elif granularity == 'month':
|
||
summary_fenhong_filter['CreateTime__year'] = summary_q.year
|
||
summary_fenhong_filter['CreateTime__month'] = summary_q.month
|
||
else:
|
||
summary_fenhong_filter['CreateTime__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.query.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:
|
||
# 避免模块名冲突,局部导入
|
||
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 = filter_queryset_by_club(
|
||
DailyIncomeStat.query.filter(**income_filter), request,
|
||
).values(group_field) \
|
||
.annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \
|
||
.order_by(group_field)
|
||
|
||
# ---------- 支出分组查询 ----------
|
||
payout_qs = filter_queryset_by_club(
|
||
DailyPayoutStat.query.filter(**payout_filter), request,
|
||
).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 = filter_queryset_by_club(
|
||
DailyIncomeStat.query.filter(**summary_filter), request,
|
||
).aggregate(
|
||
total_income=Sum('total_amount'), total_count=Sum('total_count')
|
||
)
|
||
payout_summary = filter_queryset_by_club(
|
||
DailyPayoutStat.query.filter(**summary_filter), request,
|
||
).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.query.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('CreateTime')
|
||
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('CreateTime')
|
||
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('CreateTime')
|
||
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 = {
|
||
'CreateTime__gte': start_dt,
|
||
'CreateTime__lt': end_dt,
|
||
'Status__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态
|
||
}
|
||
if int(type_id) != 0:
|
||
filters['ProductTypeID'] = type_id
|
||
if int(order_source) in [1, 2]:
|
||
filters['Platform'] = order_source
|
||
|
||
# ---------- 时间序列分组查询 ----------
|
||
order_base = filter_queryset_by_club(
|
||
Order.query.filter(**filters), request, club_field='ClubID',
|
||
)
|
||
qs = order_base \
|
||
.annotate(period=trunc_func) \
|
||
.values('period') \
|
||
.annotate(
|
||
# 订单总量(所有符合条件的状态)
|
||
order_count=Count('id'),
|
||
# 订单总额
|
||
order_amount=Sum('Amount'),
|
||
# 成交单量 (Status=3)
|
||
success_count=Count('id', filter=Q(Status=3)),
|
||
# 成交订单总额
|
||
success_amount=Sum('Amount', filter=Q(Status=3)),
|
||
# 退款单量 (Status=5)
|
||
refund_count=Count('id', filter=Q(Status=5)),
|
||
# 打手分成总额(仅成交订单)
|
||
PlayerCommission_sum=Sum('PlayerCommission', filter=Q(Status=3)),
|
||
# 平台成交订单的店铺分红
|
||
dianpu_fenhong_sum=Sum('pingtai_kuozhan__ShopIncome',
|
||
filter=Q(Status=3, Platform=1)),
|
||
# 成交订单中:平台单金额、商家单金额、各自打手分成
|
||
platform_success_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
|
||
merchant_success_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
|
||
platform_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
|
||
merchant_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=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['PlayerCommission_sum'] or 0),
|
||
'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0),
|
||
'profit': round(total_profit, 2)
|
||
})
|
||
|
||
# ---------- 汇总数据 ----------
|
||
summary_qs = filter_queryset_by_club(
|
||
Order.query.filter(**filters), request, club_field='ClubID',
|
||
)
|
||
total_order_count = summary_qs.count()
|
||
total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0
|
||
total_success_count = summary_qs.filter(Status=3).count()
|
||
total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0
|
||
total_refund_count = summary_qs.filter(Status=5).count()
|
||
total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||
total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate(
|
||
s=Sum('pingtai_kuozhan__ShopIncome'))['s'] or 0
|
||
|
||
platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0
|
||
merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0
|
||
platform_dashou_sum = \
|
||
summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||
merchant_dashou_sum = \
|
||
summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['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_PlayerCommission': 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)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class CwqtczhqView(APIView):
|
||
"""
|
||
其他充值/罚款数据统计(非会员充值)
|
||
POST /houtai/cwqtczhq
|
||
Body: {
|
||
"phone": "管理员手机号",
|
||
"granularity": "day" | "month" | "year",
|
||
"year": 2026,
|
||
"month": 5, // day时必填
|
||
"types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
|
||
"summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
|
||
}
|
||
"""
|
||
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, 6]) # 默认全部
|
||
summary_date = request.data.get('summary_date')
|
||
|
||
if isinstance(types, list) and len(types) == 0:
|
||
types = [2, 3, 4, 5, 6]
|
||
|
||
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('CreateTime')
|
||
# 默认汇总日期
|
||
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('CreateTime')
|
||
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('CreateTime')
|
||
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())
|
||
|
||
# ---------- 构建时间序列 ----------
|
||
time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} }
|
||
|
||
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 = filter_queryset_by_club(
|
||
Czjilu.query.filter(
|
||
CreateTime__gte=start_dt,
|
||
CreateTime__lt=end_dt,
|
||
leixing__in=cz_types,
|
||
zhuangtai=3,
|
||
),
|
||
request,
|
||
).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']
|
||
# 收益:积分(3)收益为金额本身,其他为0
|
||
profit = amount if lx == 3 else 0.0
|
||
|
||
day_data = ensure_date(period_str)
|
||
day_data[str(lx)] = {
|
||
'count': count,
|
||
'amount': amount,
|
||
'profit': profit,
|
||
'fenhong_count': 0, # 充值类型无分红
|
||
'fenhong_amount': 0.0
|
||
}
|
||
|
||
# 2. 处理罚款(Penalty)—— 修改:直接从 Penalty 表获取分红总额和分红笔数
|
||
if 6 in types:
|
||
# 使用一次聚合查询,同时获取罚款笔数、罚款金额、分红总额、分红笔数
|
||
penalty_qs = filter_penalty_qs(
|
||
Penalty.query.filter(
|
||
CreateTime__gte=start_dt,
|
||
CreateTime__lt=end_dt,
|
||
Status=2,
|
||
),
|
||
request,
|
||
).annotate(
|
||
period=trunc_func
|
||
).values('period').annotate(
|
||
penalty_count=Count('id'),
|
||
penalty_amount=Sum('FineAmount'),
|
||
# 直接使用罚单表中的申请人分红金额字段
|
||
fenhong_amount=Sum('ApplicantBonusAmount'),
|
||
fenhong_count=Count('id', filter=Q(ApplicantBonusAmount__gt=0))
|
||
).order_by('period')
|
||
|
||
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_amt = float(item['penalty_amount'] or 0)
|
||
fenhong_amt = float(item['fenhong_amount'] or 0)
|
||
profit = round(penalty_amt - fenhong_amt, 2)
|
||
day_data['6'] = {
|
||
'count': item['penalty_count'],
|
||
'amount': penalty_amt,
|
||
'profit': profit,
|
||
'fenhong_count': item['fenhong_count'],
|
||
'fenhong_amount': fenhong_amt
|
||
}
|
||
|
||
# 构造有序时间序列列表
|
||
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)—— 完全保留原逻辑,仅将罚款部分改为从 Penalty 表直接取分红 ----------
|
||
summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
|
||
|
||
# 汇总充值类型(完全不变)
|
||
for lx in cz_types:
|
||
filter_kwargs = {
|
||
'leixing': lx,
|
||
'zhuangtai': 3,
|
||
}
|
||
if granularity == 'day':
|
||
filter_kwargs['CreateTime__date'] = summary_q
|
||
elif granularity == 'month':
|
||
filter_kwargs['CreateTime__year'] = summary_q.year
|
||
filter_kwargs['CreateTime__month'] = summary_q.month
|
||
else:
|
||
filter_kwargs['CreateTime__year'] = summary_q.year
|
||
agg = filter_queryset_by_club(
|
||
Czjilu.query.filter(**filter_kwargs), request,
|
||
).aggregate(count=Count('id'), amount=Sum('jine'))
|
||
count = agg['count'] or 0
|
||
amount = float(agg['amount'] or 0)
|
||
profit = amount if lx == 3 else 0.0
|
||
summary_result['by_type'][str(lx)] = {
|
||
'count': count,
|
||
'amount': round(amount, 2),
|
||
'profit': round(profit, 2),
|
||
'fenhong_count': 0,
|
||
'fenhong_amount': 0.0
|
||
}
|
||
summary_result['total_count'] += count
|
||
summary_result['total_amount'] += amount
|
||
summary_result['total_profit'] += profit
|
||
|
||
# 汇总罚款(修改:从 Penalty 表直接取分红,不再查询 PenaltyBonus)
|
||
if 6 in types:
|
||
penalty_filter = {
|
||
'Status': 2,
|
||
}
|
||
if granularity == 'day':
|
||
penalty_filter['CreateTime__date'] = summary_q
|
||
elif granularity == 'month':
|
||
penalty_filter['CreateTime__year'] = summary_q.year
|
||
penalty_filter['CreateTime__month'] = summary_q.month
|
||
else:
|
||
penalty_filter['CreateTime__year'] = summary_q.year
|
||
|
||
# 一次聚合:罚款笔数、金额、分红总额、分红笔数
|
||
agg = filter_penalty_qs(
|
||
Penalty.query.filter(**penalty_filter), request,
|
||
).aggregate(
|
||
count=Count('id'),
|
||
amount=Sum('FineAmount'),
|
||
fenhong_amount=Sum('ApplicantBonusAmount'),
|
||
fenhong_count=Count('id', filter=Q(ApplicantBonusAmount__gt=0))
|
||
)
|
||
penalty_count = agg['count'] or 0
|
||
penalty_amount = float(agg['amount'] or 0)
|
||
fh_amount = float(agg['fenhong_amount'] or 0)
|
||
fh_count = agg['fenhong_count'] or 0
|
||
profit = round(penalty_amount - fh_amount, 2)
|
||
|
||
summary_result['by_type']['6'] = {
|
||
'count': penalty_count,
|
||
'amount': round(penalty_amount, 2),
|
||
'profit': profit,
|
||
'fenhong_count': fh_count,
|
||
'fenhong_amount': round(fh_amount, 2)
|
||
}
|
||
summary_result['total_count'] += penalty_count
|
||
summary_result['total_amount'] += penalty_amount
|
||
summary_result['total_profit'] += profit
|
||
|
||
summary_result['total_amount'] = round(summary_result['total_amount'], 2)
|
||
summary_result['total_profit'] = round(summary_result['total_profit'], 2)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'time_series': time_series,
|
||
'summary': summary_result
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.exception("CwqtczhqView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
'''class CwqtczhqView(APIView):
|
||
"""
|
||
其他充值/罚款数据统计(非会员充值)
|
||
POST /houtai/cwqtczhq
|
||
Body: {
|
||
"phone": "管理员手机号",
|
||
"granularity": "day" | "month" | "year",
|
||
"year": 2026,
|
||
"month": 5, // day时必填
|
||
"types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
|
||
"summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
|
||
}
|
||
"""
|
||
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, 6]) # 默认全部
|
||
summary_date = request.data.get('summary_date')
|
||
|
||
if isinstance(types, list) and len(types) == 0:
|
||
types = [2, 3, 4, 5, 6]
|
||
|
||
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('CreateTime')
|
||
# 默认汇总日期
|
||
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('CreateTime')
|
||
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('CreateTime')
|
||
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())
|
||
|
||
# ---------- 构建时间序列 ----------
|
||
time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} }
|
||
|
||
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.query.filter(
|
||
CreateTime__gte=start_dt,
|
||
CreateTime__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']
|
||
# 收益:积分(3)收益为金额本身,其他为0
|
||
profit = amount if lx == 3 else 0.0
|
||
|
||
day_data = ensure_date(period_str)
|
||
day_data[str(lx)] = {
|
||
'count': count,
|
||
'amount': amount,
|
||
'profit': profit,
|
||
'fenhong_count': 0, # 充值类型无分红
|
||
'fenhong_amount': 0.0
|
||
}
|
||
|
||
# 2. 处理罚款(Penalty)—— 包含分红
|
||
if 6 in types:
|
||
# 罚款记录
|
||
penalty_qs = Penalty.query.filter(
|
||
CreateTime__gte=start_dt,
|
||
CreateTime__lt=end_dt,
|
||
Status=2 # 已缴纳
|
||
).annotate(
|
||
period=trunc_func
|
||
).values('period').annotate(
|
||
penalty_count=Count('id'),
|
||
penalty_amount=Sum('FineAmount')
|
||
).order_by('period')
|
||
|
||
# 分红记录
|
||
fenhong_qs = PenaltyBonus.query.filter(
|
||
CreateTime__gte=start_dt,
|
||
CreateTime__lt=end_dt
|
||
).annotate(
|
||
period=trunc_func
|
||
).values('period').annotate(
|
||
fh_count=Count('id'),
|
||
fh_amount=Sum('WithdrawableAmount')
|
||
).order_by('period')
|
||
|
||
# 合并
|
||
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)
|
||
day_data['6'] = {
|
||
'count': item['penalty_count'],
|
||
'amount': float(item['penalty_amount'] or 0),
|
||
'profit': 0.0, # 先设为0,后续计算
|
||
'fenhong_count': 0,
|
||
'fenhong_amount': 0.0
|
||
}
|
||
|
||
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)
|
||
if '6' not in day_data:
|
||
day_data['6'] = {'count': 0, 'amount': 0.0, 'profit': 0.0, 'fenhong_count': 0, 'fenhong_amount': 0.0}
|
||
day_data['6']['fenhong_count'] += item['fh_count']
|
||
day_data['6']['fenhong_amount'] += float(item['fh_amount'] or 0)
|
||
|
||
# 计算罚款收益 = 罚款金额 - 分红金额
|
||
for period_str, day_data in time_series_map.items():
|
||
if '6' in day_data:
|
||
penalty = day_data['6']
|
||
penalty['profit'] = round(penalty['amount'] - penalty['fenhong_amount'], 2)
|
||
|
||
# 构造有序时间序列列表
|
||
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_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
|
||
|
||
# 汇总充值类型
|
||
for lx in cz_types:
|
||
filter_kwargs = {
|
||
'leixing': lx,
|
||
'zhuangtai': 3,
|
||
}
|
||
if granularity == 'day':
|
||
filter_kwargs['CreateTime__date'] = summary_q
|
||
elif granularity == 'month':
|
||
filter_kwargs['CreateTime__year'] = summary_q.year
|
||
filter_kwargs['CreateTime__month'] = summary_q.month
|
||
else:
|
||
filter_kwargs['CreateTime__year'] = summary_q.year
|
||
agg = filter_queryset_by_club(
|
||
Czjilu.query.filter(**filter_kwargs), request,
|
||
).aggregate(count=Count('id'), amount=Sum('jine'))
|
||
count = agg['count'] or 0
|
||
amount = float(agg['amount'] or 0)
|
||
profit = amount if lx == 3 else 0.0
|
||
summary_result['by_type'][str(lx)] = {
|
||
'count': count,
|
||
'amount': round(amount, 2),
|
||
'profit': round(profit, 2),
|
||
'fenhong_count': 0,
|
||
'fenhong_amount': 0.0
|
||
}
|
||
summary_result['total_count'] += count
|
||
summary_result['total_amount'] += amount
|
||
summary_result['total_profit'] += profit
|
||
|
||
# 汇总罚款
|
||
if 6 in types:
|
||
penalty_filter = {
|
||
'Status': 2,
|
||
}
|
||
fenhong_filter = {}
|
||
if granularity == 'day':
|
||
penalty_filter['CreateTime__date'] = summary_q
|
||
fenhong_filter['CreateTime__date'] = summary_q
|
||
elif granularity == 'month':
|
||
penalty_filter['CreateTime__year'] = summary_q.year
|
||
penalty_filter['CreateTime__month'] = summary_q.month
|
||
fenhong_filter['CreateTime__year'] = summary_q.year
|
||
fenhong_filter['CreateTime__month'] = summary_q.month
|
||
else:
|
||
penalty_filter['CreateTime__year'] = summary_q.year
|
||
fenhong_filter['CreateTime__year'] = summary_q.year
|
||
|
||
penalty_agg = Penalty.query.filter(**penalty_filter).aggregate(
|
||
count=Count('id'), amount=Sum('FineAmount'))
|
||
penalty_count = penalty_agg['count'] or 0
|
||
penalty_amount = float(penalty_agg['amount'] or 0)
|
||
|
||
fenhong_agg = PenaltyBonus.query.filter(**fenhong_filter).aggregate(
|
||
fh_count=Count('id'), fh_amount=Sum('WithdrawableAmount'))
|
||
fh_count = fenhong_agg['fh_count'] or 0
|
||
fh_amount = float(fenhong_agg['fh_amount'] or 0)
|
||
|
||
profit = round(penalty_amount - fh_amount, 2)
|
||
summary_result['by_type']['6'] = {
|
||
'count': penalty_count,
|
||
'amount': round(penalty_amount, 2),
|
||
'profit': profit,
|
||
'fenhong_count': fh_count,
|
||
'fenhong_amount': round(fh_amount, 2)
|
||
}
|
||
summary_result['total_count'] += penalty_count
|
||
summary_result['total_amount'] += penalty_amount
|
||
summary_result['total_profit'] += profit
|
||
|
||
summary_result['total_amount'] = round(summary_result['total_amount'], 2)
|
||
summary_result['total_profit'] = round(summary_result['total_profit'], 2)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'time_series': time_series,
|
||
'summary': summary_result
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.exception("CwqtczhqView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)'''
|
||
|
||
|
||
|
||
|
||
|
||
# ==================== 后端接口 ====================
|
||
# 文件:views.py (添加以下两个视图类)
|
||
|
||
|
||
|
||
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.query.values('bankuai_id', 'mingcheng'))
|
||
|
||
chenghao_qs = Chenghao.query.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 '',
|
||
'CreateTime': ch.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if ch.CreateTime 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.query.filter(mingcheng=mingcheng).exists():
|
||
return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
|
||
|
||
with transaction.atomic():
|
||
ch = Chenghao.query.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.query.update_or_create(
|
||
chenghao=ch, cishu=cishu,
|
||
defaults={'feiyong': feiyong}
|
||
)
|
||
else:
|
||
# 默认添加第一次免费
|
||
KaoheCishuFeiyong.query.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.query.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.query.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.query.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.query.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.query.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.query.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.query.get(id=ch_id)
|
||
except Chenghao.DoesNotExist:
|
||
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
|
||
KaoheCishuFeiyong.query.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 ====================
|
||
# 请确保已有以下导入,如果没有请在文件头部添加
|
||
|
||
|
||
|
||
|
||
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 = filter_user_related_by_club(
|
||
UserShenheguan.query.select_related('user__DashouProfile'),
|
||
request,
|
||
)
|
||
|
||
if filter_yonghuid:
|
||
queryset = queryset.filter(user__UserUID=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.query.filter(
|
||
mingcheng__icontains=filter_bankuai_name
|
||
).values_list('bankuai_id', flat=True)
|
||
shenheguan_ids = KaoheguanBankuai.query.filter(
|
||
bankuai_id__in=bankuai_ids
|
||
).values_list('kaoheguan_id', flat=True)
|
||
queryset = queryset.filter(user__UserUID__in=shenheguan_ids)
|
||
|
||
total = queryset.count()
|
||
start = (page - 1) * page_size
|
||
end = start + page_size
|
||
shenheguan_list = queryset.order_by('-CreateTime')[start:end]
|
||
|
||
from jituan.services.shenhe_club import yonghu_chenghao_qs_for_request
|
||
|
||
# ---------- 预计算每个板块的打手数量 ----------
|
||
bankuai_dashou_count_map = {}
|
||
all_banquai = Bankuai.query.all()
|
||
for bk in all_banquai:
|
||
tag_ids = Chenghao.query.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True)
|
||
count = yonghu_chenghao_qs_for_request(
|
||
YonghuChenghao.query.filter(
|
||
chenghao_id__in=tag_ids,
|
||
yonghu__DashouProfile__isnull=False,
|
||
),
|
||
request,
|
||
).values('yonghu').distinct().count()
|
||
bankuai_dashou_count_map[bk.bankuai_id] = count
|
||
|
||
# ---------- 新增:板块标签统计 ----------
|
||
bankuai_tags_data = []
|
||
for bk in all_banquai:
|
||
tags = Chenghao.query.filter(bankuai=bk, leixing='dashou')
|
||
tag_list = []
|
||
for tag in tags:
|
||
user_count = yonghu_chenghao_qs_for_request(
|
||
YonghuChenghao.query.filter(chenghao=tag),
|
||
request,
|
||
).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.query.filter(kaoheguan_id=user.UserUID).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.DashouProfile.nicheng if hasattr(user, 'DashouProfile') and user.DashouProfile else ''
|
||
|
||
data_list.append({
|
||
'yonghuid': user.UserUID,
|
||
'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),
|
||
'CreateTime': sg.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.CreateTime else '',
|
||
'UpdateTime': sg.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.UpdateTime else '',
|
||
'bankuai': bankuai_info,
|
||
'nicheng': dashou_nick,
|
||
'is_renzheng': sg.is_renzheng,
|
||
})
|
||
|
||
all_bankuai = list(Bankuai.query.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.query.select_related('user').get(user__UserUID=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.query.filter(bankuai_id=bankuai_id).exists():
|
||
return Response({'code': 1, 'msg': '板块不存在'}, status=400)
|
||
if KaoheguanBankuai.query.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists():
|
||
return Response({'code': 1, 'msg': '已关联该板块'}, status=400)
|
||
KaoheguanBankuai.query.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.query.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)
|
||
|
||
|
||
|
||
|
||
|
||
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()
|
||
OrderID = pick_order_id(request.data)
|
||
if not username or not OrderID:
|
||
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 = Order.query.select_related('shangjia_kuozhan').get(OrderID=OrderID)
|
||
except Order.DoesNotExist:
|
||
logger.warning(f"订单不存在: {OrderID}")
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
# 仅允许状态为 2(进行中)或 8(结算中)更换打手
|
||
if old_order.Status not in [1,7,2, 8]:
|
||
return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'})
|
||
|
||
# 本接口仅处理商家发单(Platform=2)
|
||
if old_order.Platform != 2:
|
||
return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'})
|
||
|
||
# 获取原链接(用于模板ID等)
|
||
try:
|
||
old_lianjie = ShangjiaLianjie.query.get(OrderID=OrderID)
|
||
except ShangjiaLianjie.DoesNotExist:
|
||
logger.error(f"原链接记录不存在,订单: {OrderID}")
|
||
return Response({'code': 500, 'msg': '链接数据异常'})
|
||
|
||
# 商家信息(仅用于记录关联,不操作余额)
|
||
try:
|
||
shangjia_ext = old_order.shangjia_kuozhan
|
||
shangjia_id = shangjia_ext.MerchantID
|
||
# 获取商家昵称用于新订单扩展表
|
||
shangjia_user = User.query.get(UserUID=shangjia_id)
|
||
shangjia_nicheng = shangjia_user.ShopProfile.nicheng or f"商家{shangjia_id}"
|
||
except (ObjectDoesNotExist, UserShangjia.DoesNotExist):
|
||
logger.error(f"商家信息缺失,订单: {OrderID}")
|
||
return Response({'code': 500, 'msg': '商家数据异常'})
|
||
|
||
# ========== 4. 退款处理(仅打手侧,不碰商家) ==========
|
||
with transaction.atomic():
|
||
# 5.1 订单状态改为已退款
|
||
old_order.Status = 5
|
||
old_order.AssignedCS = current_user.Phone
|
||
old_order.save()
|
||
|
||
# 5.2 处理接单打手(若有)
|
||
dashou_id = old_order.PlayerID
|
||
if dashou_id:
|
||
try:
|
||
dashou_user = User.query.get(UserUID=dashou_id)
|
||
dashou_profile = dashou_user.DashouProfile
|
||
dashou_profile.tuikuanliang += 1 # 退款次数+1
|
||
dashou_profile.zhuangtai = 1 # 设为空闲
|
||
dashou_profile.save()
|
||
except (User.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"打手 {dashou_id} 不存在,跳过更新")
|
||
|
||
# 5.3 更新退款记录表
|
||
RefundRecord.query.update_or_create(
|
||
OrderID=OrderID,
|
||
defaults={
|
||
'PlayerID': dashou_id or '',
|
||
'ApplicantID': '',
|
||
'ProcessorID': current_user.Phone,
|
||
'Reason': '客服更换打手退款',
|
||
'ApplyStatus': 1,
|
||
'Amount': old_order.Amount or 0,
|
||
'PlayerCommission': old_order.PlayerCommission or 0,
|
||
'Description': old_order.Description or '',
|
||
'Remark': '更换打手自动退款',
|
||
'Nickname': old_order.Nickname or '',
|
||
}
|
||
)
|
||
|
||
# 5.4 打手每日统计(退款)
|
||
if dashou_id:
|
||
try:
|
||
update_dashou_daily_by_action(
|
||
yonghuid=dashou_id,
|
||
amount=old_order.PlayerCommission,
|
||
action=3 # 3=退款
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"打手每日统计更新失败: {e}")
|
||
|
||
# 5.5 客服处理统计(管理员无 KefuProfile,跳过)
|
||
if hasattr(kefu, 'jinrichuli'):
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
# 至此,原订单已退款,事务提交
|
||
|
||
# ========== 6. 生成新订单和链接(不扣商家余额) ==========
|
||
try:
|
||
with transaction.atomic():
|
||
# ---- 6.1 生成新的订单ID和Token ----
|
||
def generate_OrderID():
|
||
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.query.filter(LinkToken=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_OrderID = generate_OrderID()
|
||
secure_token = generate_secure_token(new_OrderID, shangjia_id)
|
||
|
||
# ---- 6.2 创建新订单(复制核心数据,状态直接为1) ----
|
||
new_order = Order.query.create(
|
||
OrderID=new_OrderID,
|
||
Status=1, # 下单中
|
||
Platform=2,
|
||
Amount=old_order.Amount,
|
||
PlayerCommission=old_order.PlayerCommission,
|
||
ProductTypeID=old_order.ProductTypeID,
|
||
GrabRequirement=old_order.GrabRequirement,
|
||
MembershipID=old_order.MembershipID,
|
||
CommissionReq=old_order.CommissionReq,
|
||
Description=old_order.Description,
|
||
Nickname=old_order.Nickname, # 继承游戏昵称
|
||
Remark=old_order.Remark,
|
||
User1ID=old_order.User1ID, # 商家标识
|
||
ImageURL=old_order.ImageURL,
|
||
ClubID=getattr(old_order, 'ClubID', None) or 'xq',
|
||
# 注意:不继承 AssignedID、PlayerID
|
||
)
|
||
|
||
# ---- 6.3 创建商家扩展表 ----
|
||
MerchantOrderExt.query.create(
|
||
Order=new_order,
|
||
MerchantID=shangjia_id,
|
||
MerchantNickname=shangjia_nicheng,
|
||
)
|
||
|
||
# ---- 6.4 生成新链接并关联旧链接信息 ----
|
||
link_url = generate_link_url(secure_token)
|
||
new_lianjie = ShangjiaLianjie.query.create(
|
||
UserID=shangjia_id,
|
||
ProductTypeID=old_lianjie.ProductTypeID,
|
||
TemplateID=old_lianjie.TemplateID,
|
||
LinkURL=link_url,
|
||
LinkToken=secure_token,
|
||
OrderID=new_OrderID,
|
||
is_used=True, # 直接标记已使用
|
||
ExpireTime=timezone.now() + timezone.timedelta(days=1),
|
||
PreviousLinkID=old_lianjie.id, # 记录旧链接ID
|
||
PreviousOrderID=old_order.OrderID, # 记录旧订单ID
|
||
)
|
||
|
||
# ---- 6.5 异步通知(广播新订单、跨平台同步) ----
|
||
try:
|
||
order_info = {
|
||
'OrderID': new_OrderID,
|
||
'game_type': self._get_game_type_name(new_order.ProductTypeID),
|
||
'amount': str(new_order.Amount),
|
||
'order_desc': (new_order.Description or '')[:50],
|
||
}
|
||
dingdan_guangbo.delay(order_info)
|
||
except Exception as e:
|
||
logger.error(f"广播通知失败: {e}")
|
||
|
||
# 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单
|
||
|
||
logger.info(f"更换打手成功:旧订单{OrderID}→退款,新订单{new_OrderID}已生成")
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '更换成功',
|
||
'data': {
|
||
'new_OrderID': new_OrderID,
|
||
'new_dingdan_id': new_OrderID,
|
||
'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, ProductTypeID):
|
||
"""根据类型ID获取游戏类型名称"""
|
||
try:
|
||
gt = ShangpinLeixing.query.filter(id=ProductTypeID).first()
|
||
return gt.jieshao if gt else '游戏订单'
|
||
except Exception:
|
||
return '游戏订单'
|
||
|
||
|
||
# ==================== 考核记录管理 ====================
|
||
|
||
SHENHE_ZHUANGTAI_MAP = {
|
||
0: '考核中',
|
||
1: '已通过',
|
||
2: '未通过',
|
||
3: '待审核',
|
||
}
|
||
|
||
|
||
def _batch_user_profile_map(user_uids):
|
||
"""批量获取用户头像、昵称、手机号"""
|
||
uids = list({u for u in user_uids if u})
|
||
if not uids:
|
||
return {}
|
||
users = User.query.filter(UserUID__in=uids)
|
||
dashou_nicks = {
|
||
d.user.UserUID: d.nicheng
|
||
for d in UserDashou.query.filter(user__UserUID__in=uids).select_related('user')
|
||
}
|
||
result = {}
|
||
for u in users:
|
||
result[u.UserUID] = {
|
||
'yonghuid': u.UserUID,
|
||
'avatar': u.Avatar or '',
|
||
'phone': u.Phone or '',
|
||
'nicheng': dashou_nicks.get(u.UserUID) or u.UserName or u.Phone or u.UserUID,
|
||
}
|
||
return result
|
||
|
||
|
||
def _build_shenhe_jilu_filters(request_data):
|
||
"""构造 ShenheJilu 查询条件(列表与统计共用)"""
|
||
qs = ShenheJilu.query.select_related('bankuai', 'chenghao')
|
||
|
||
jilu_id = (request_data.get('jilu_id') or '').strip()
|
||
if jilu_id:
|
||
qs = qs.filter(jilu_id=jilu_id)
|
||
|
||
zhuangtai = request_data.get('zhuangtai')
|
||
if zhuangtai is not None and zhuangtai != '':
|
||
qs = qs.filter(zhuangtai=int(zhuangtai))
|
||
|
||
assessed = request_data.get('assessed')
|
||
if assessed == 'done':
|
||
qs = qs.filter(zhuangtai__in=[1, 2])
|
||
elif assessed == 'undone':
|
||
qs = qs.filter(zhuangtai__in=[0, 3])
|
||
|
||
bankuai_id = request_data.get('bankuai_id')
|
||
if bankuai_id not in (None, ''):
|
||
qs = qs.filter(bankuai_id=int(bankuai_id))
|
||
|
||
chenghao_id = request_data.get('chenghao_id')
|
||
if chenghao_id not in (None, ''):
|
||
qs = qs.filter(chenghao_id=int(chenghao_id))
|
||
|
||
cishu = request_data.get('cishu')
|
||
if cishu not in (None, ''):
|
||
qs = qs.filter(cishu=int(cishu))
|
||
|
||
dashou_kw = (request_data.get('dashou_keyword') or '').strip()
|
||
if dashou_kw:
|
||
dashou_uids = list(
|
||
User.query.filter(
|
||
Q(UserUID=dashou_kw) |
|
||
Q(UserName__icontains=dashou_kw) |
|
||
Q(Phone__icontains=dashou_kw) |
|
||
Q(DashouProfile__nicheng__icontains=dashou_kw)
|
||
).values_list('UserUID', flat=True)
|
||
)
|
||
if dashou_uids:
|
||
qs = qs.filter(shenqingren_id__in=dashou_uids)
|
||
else:
|
||
qs = qs.none()
|
||
|
||
kg_kw = (request_data.get('kaoheguan_keyword') or '').strip()
|
||
if kg_kw:
|
||
kg_uids = list(
|
||
User.query.filter(
|
||
Q(UserUID=kg_kw) |
|
||
Q(UserName__icontains=kg_kw) |
|
||
Q(Phone__icontains=kg_kw) |
|
||
Q(DashouProfile__nicheng__icontains=kg_kw)
|
||
).values_list('UserUID', flat=True)
|
||
)
|
||
if kg_uids:
|
||
qs = qs.filter(shenheguan_id__in=kg_uids)
|
||
else:
|
||
qs = qs.none()
|
||
|
||
start_time = (request_data.get('start_time') or '').strip()
|
||
end_time = (request_data.get('end_time') or '').strip()
|
||
if start_time:
|
||
qs = qs.filter(CreateTime__gte=start_time)
|
||
if end_time:
|
||
qs = qs.filter(CreateTime__lte=end_time)
|
||
|
||
has_kaoheguan = request_data.get('has_kaoheguan')
|
||
if has_kaoheguan in (True, 'true', '1', 1):
|
||
qs = qs.exclude(shenheguan_id__isnull=True).exclude(shenheguan_id='')
|
||
elif has_kaoheguan in (False, 'false', '0', 0):
|
||
qs = qs.filter(Q(shenheguan_id__isnull=True) | Q(shenheguan_id=''))
|
||
|
||
return qs
|
||
|
||
|
||
def _serialize_shenhe_jilu_row(jl, profile_map, fee_map=None, reject_count_map=None):
|
||
dashou = profile_map.get(jl.shenqingren_id, {
|
||
'yonghuid': jl.shenqingren_id,
|
||
'avatar': '',
|
||
'phone': '',
|
||
'nicheng': jl.shenqingren_id,
|
||
})
|
||
kaoheguan = None
|
||
if jl.shenheguan_id:
|
||
kaoheguan = profile_map.get(jl.shenheguan_id, {
|
||
'yonghuid': jl.shenheguan_id,
|
||
'avatar': '',
|
||
'phone': '',
|
||
'nicheng': jl.shenheguan_id,
|
||
})
|
||
kaoheguan['game_id'] = jl.shenheguan_youxi_id or ''
|
||
|
||
current_fee = 0.0
|
||
if fee_map is not None:
|
||
current_fee = fee_map.get((jl.jilu_id, jl.cishu), 0.0)
|
||
else:
|
||
fd = KaoheJiluFeiyong.query.filter(jilu_id=jl.jilu_id, cishu=jl.cishu).first()
|
||
if fd:
|
||
current_fee = float(fd.jine)
|
||
|
||
reject_count = 0
|
||
if reject_count_map is not None:
|
||
reject_count = reject_count_map.get(jl.jilu_id, 0)
|
||
|
||
return {
|
||
'jilu_id': jl.jilu_id,
|
||
'zhuangtai': jl.zhuangtai,
|
||
'zhuangtai_text': SHENHE_ZHUANGTAI_MAP.get(jl.zhuangtai, str(jl.zhuangtai)),
|
||
'cishu': jl.cishu,
|
||
'jiaofei_jine': float(jl.jiaofei_jine or 0),
|
||
'current_fee': current_fee,
|
||
'bankuai_id': jl.bankuai.bankuai_id if jl.bankuai else None,
|
||
'bankuai_name': jl.bankuai.mingcheng if jl.bankuai else '',
|
||
'chenghao_id': jl.chenghao.id if jl.chenghao else None,
|
||
'chenghao_name': jl.chenghao.mingcheng if jl.chenghao else '',
|
||
'chenghao_icon': jl.chenghao.tubiao_url if jl.chenghao else '',
|
||
'guize_neirong': jl.guize_neirong or '',
|
||
'dashou': {
|
||
**dashou,
|
||
'game_id': jl.dashou_youxi_id or '',
|
||
'beizhu': jl.dashou_beizhu or '',
|
||
},
|
||
'kaoheguan': kaoheguan,
|
||
'reject_count': reject_count,
|
||
'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.CreateTime else '',
|
||
'UpdateTime': jl.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.UpdateTime else '',
|
||
}
|
||
|
||
|
||
class KhjlglView(APIView):
|
||
"""
|
||
考核记录列表 + 统计
|
||
POST /houtai/khjlgl
|
||
"""
|
||
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', 20))
|
||
if page < 1:
|
||
page = 1
|
||
if page_size < 1:
|
||
page_size = 20
|
||
if page_size > 100:
|
||
page_size = 100
|
||
|
||
from jituan.services.shenhe_club import filter_shenhe_jilu_by_request
|
||
|
||
filter_data = request.data
|
||
qs = filter_shenhe_jilu_by_request(
|
||
_build_shenhe_jilu_filters(filter_data),
|
||
request,
|
||
)
|
||
|
||
# FluentQuery.filter() 会原地修改同一对象;统计与列表必须各自独立查询
|
||
stats_base = filter_shenhe_jilu_by_request(
|
||
_build_shenhe_jilu_filters(filter_data),
|
||
request,
|
||
)
|
||
agg = stats_base.aggregate(total_fee=Sum('jiaofei_jine'))
|
||
tag_rows = filter_shenhe_jilu_by_request(
|
||
_build_shenhe_jilu_filters(filter_data),
|
||
request,
|
||
).values(
|
||
'chenghao_id', 'chenghao__mingcheng'
|
||
).annotate(
|
||
total=Count('jilu_id'),
|
||
passed=Count('jilu_id', filter=Q(zhuangtai=1)),
|
||
failed=Count('jilu_id', filter=Q(zhuangtai=2)),
|
||
in_progress=Count('jilu_id', filter=Q(zhuangtai=0)),
|
||
pending=Count('jilu_id', filter=Q(zhuangtai=3)),
|
||
).order_by('-total')
|
||
|
||
stats = {
|
||
'total': stats_base.count(),
|
||
'passed': stats_base.filter(zhuangtai=1).count(),
|
||
'failed': stats_base.filter(zhuangtai=2).count(),
|
||
'in_progress': stats_base.filter(zhuangtai=0).count(),
|
||
'pending': stats_base.filter(zhuangtai=3).count(),
|
||
'total_fee': float(agg['total_fee'] or 0),
|
||
'tag_stats': [
|
||
{
|
||
'chenghao_id': r['chenghao_id'],
|
||
'chenghao_name': r['chenghao__mingcheng'] or '未知标签',
|
||
'total': r['total'],
|
||
'passed': r['passed'],
|
||
'failed': r['failed'],
|
||
'in_progress': r['in_progress'],
|
||
'pending': r['pending'],
|
||
}
|
||
for r in tag_rows if r['chenghao_id']
|
||
],
|
||
}
|
||
|
||
total = qs.count()
|
||
start = (page - 1) * page_size
|
||
rows = qs.order_by('-CreateTime')[start:start + page_size].to_list()
|
||
|
||
uids = []
|
||
jilu_ids = []
|
||
for jl in rows:
|
||
uids.append(jl.shenqingren_id)
|
||
if jl.shenheguan_id:
|
||
uids.append(jl.shenheguan_id)
|
||
jilu_ids.append(jl.jilu_id)
|
||
profile_map = _batch_user_profile_map(uids)
|
||
|
||
fee_pairs = [(jl.jilu_id, jl.cishu) for jl in rows]
|
||
fee_map = {}
|
||
if fee_pairs:
|
||
jids = [p[0] for p in fee_pairs]
|
||
for fd in KaoheJiluFeiyong.query.filter(jilu_id__in=jids):
|
||
fee_map[(fd.jilu_id, fd.cishu)] = float(fd.jine)
|
||
|
||
reject_counts = {
|
||
r['jilu_id']: r['cnt']
|
||
for r in KaoheJujueJilu.query.filter(jilu_id__in=jilu_ids)
|
||
.values('jilu_id')
|
||
.annotate(cnt=Count('id'))
|
||
.all()
|
||
} if jilu_ids else {}
|
||
|
||
data_list = [
|
||
_serialize_shenhe_jilu_row(jl, profile_map, fee_map, reject_counts)
|
||
for jl in rows
|
||
]
|
||
|
||
all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
|
||
all_tags = list(
|
||
Chenghao.query.filter(leixing='dashou')
|
||
.values('id', 'mingcheng', 'bankuai_id')
|
||
.order_by('mingcheng')
|
||
)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'total': total,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
'list': data_list,
|
||
'stats': stats,
|
||
'all_bankuai': all_bankuai,
|
||
'all_tags': all_tags,
|
||
},
|
||
})
|
||
except Exception as e:
|
||
logger.exception('KhjlglView 接口错误')
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
class KhjlczView(APIView):
|
||
"""
|
||
考核记录操作 / 详情
|
||
POST /houtai/khjlcz
|
||
action:
|
||
- get_detail: 获取单条详情(含缴费明细、拒绝历史)
|
||
- to_pending: 考核中 → 待审核(取消指定考核官,与小程序 transfer 一致)
|
||
"""
|
||
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')
|
||
jilu_id = (request.data.get('jilu_id') or '').strip()
|
||
if not jilu_id:
|
||
return Response({'code': 1, 'msg': '缺少记录ID'}, status=400)
|
||
|
||
if action == 'get_detail':
|
||
try:
|
||
jl = ShenheJilu.query.select_related('bankuai', 'chenghao').get(jilu_id=jilu_id)
|
||
except ShenheJilu.DoesNotExist:
|
||
return Response({'code': 1, 'msg': '记录不存在'}, status=404)
|
||
|
||
uids = [jl.shenqingren_id]
|
||
if jl.shenheguan_id:
|
||
uids.append(jl.shenheguan_id)
|
||
profile_map = _batch_user_profile_map(uids)
|
||
|
||
fee_list = [
|
||
{
|
||
'cishu': f.cishu,
|
||
'jine': float(f.jine),
|
||
}
|
||
for f in KaoheJiluFeiyong.query.filter(jilu_id=jilu_id).order_by('cishu')
|
||
]
|
||
reject_list = []
|
||
for r in KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'):
|
||
kg_prof = _batch_user_profile_map([r.shenheguan_id]).get(r.shenheguan_id, {})
|
||
reject_list.append({
|
||
'cishu': r.cishu,
|
||
'shenheguan_id': r.shenheguan_id,
|
||
'shenheguan_nicheng': kg_prof.get('nicheng', r.shenheguan_id),
|
||
'yuanyin': r.yuanyin,
|
||
'CreateTime': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||
})
|
||
|
||
record = _serialize_shenhe_jilu_row(jl, profile_map)
|
||
record['fee_list'] = fee_list
|
||
record['reject_list'] = reject_list
|
||
return Response({'code': 0, 'msg': 'ok', 'data': record})
|
||
|
||
if action == 'to_pending':
|
||
with transaction.atomic():
|
||
jl = ShenheJilu.objects.select_for_update().filter(jilu_id=jilu_id).first()
|
||
if not jl:
|
||
return Response({'code': 1, 'msg': '记录不存在'}, status=404)
|
||
if jl.zhuangtai != 0:
|
||
return Response({'code': 1, 'msg': '仅「考核中」的记录可退回待审核'}, status=400)
|
||
|
||
old_kg_id = jl.shenheguan_id
|
||
jl.shenheguan_id = None
|
||
jl.shenheguan_youxi_id = None
|
||
jl.zhuangtai = 3
|
||
jl.save(update_fields=['shenheguan_id', 'shenheguan_youxi_id', 'zhuangtai', 'UpdateTime'])
|
||
|
||
if old_kg_id:
|
||
UserShenheguan.query.filter(user__UserUID=old_kg_id).update(shenhe_zhuangtai=0)
|
||
|
||
return Response({'code': 0, 'msg': '已退回待审核,考核官指定已取消'})
|
||
|
||
return Response({'code': 1, 'msg': '无效的操作类型'}, status=400)
|
||
|
||
except Exception as e:
|
||
logger.exception('KhjlczView 接口错误')
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|