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 orders.utils import update_daily_payout 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, ) # 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 ) # 序列化器 from .serializers import PopupPageSerializer # 全局常量、日志对象 logger = logging.getLogger('houtai') SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd'] 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 = list(all_perms) # 4. 获取所有角色,并统计每个角色下的用户数和关联的权限 roles = Role.objects.filter(RoleStatus=1) roles_data = [] for role in roles: # 统计该角色下的用户数 user_count = UserRole.objects.filter(RoleUUID=role.RoleUUID).count() # 获取该角色绑定的权限 role_perm_uuids = RolePermission.objects.filter( RoleUUID=role.RoleUUID ).values_list('PermUUID', flat=True) perms_of_role = list(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, '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': '权限移除成功'}) 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: # 通过 gvsdsdk UserRole 查询拥有该角色的用户 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) ) # 将 UserUUID 转为 phone 列表 user_phones = list( User.objects.filter(UserUUID__in=user_uuids) .values_list('Phone', flat=True) ) users = users.filter(Phone__in=user_phones) 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, 'nicheng': user.KefuProfile.nicheng, 'roles': roles_data, 'status': user.KefuProfile.zhuangtai, 'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if hasattr(user, 'UserCreateTime') else '', 'UpdateTime': user.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if hasattr(user, 'UpdateTime') 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': '缺少目标用户账号'}) try: target_user = User.query.get(Phone=target_phone) target_kefu = target_user.KefuProfile except User.DoesNotExist: return Response({'code': 404, 'msg': '用户不存在'}) except UserKefu.DoesNotExist: return Response({'code': 404, 'msg': '用户扩展信息缺失'}) with transaction.atomic(): if action == 'update_user_info': # 修改昵称、状态、密码、二级密码 nicheng = request.data.get('nicheng') status = request.data.get('status') password = request.data.get('password') erjimima = request.data.get('erjimima') if nicheng is not None: target_kefu.nicheng = nicheng if status is not None: target_kefu.zhuangtai = int(status) if password: if len(password) < 6: return Response({'code': 400, 'msg': '密码至少6位'}) target_user.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 = UserDashou.query.select_related('user').all() # 关键词筛选(用户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() valid_user_ids = Huiyuangoumai.query.filter( yonghu_id__in=dashou_user_ids, huiyuan_zhuangtai=1, daoqi_time__gt=now ).values_list('yonghu_id', flat=True).distinct() valid_dashou_count = valid_user_ids.count() # 7. 分页 offset = (page - 1) * page_size dashou_list = dashou_qs.order_by('-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 return Response({ 'code': 0, 'data': { 'list': result, 'has_more': has_more, 'total': total_dashou, 'valid_dashou_count': valid_dashou_count, } }) class KefuGetDashouDetailView(APIView): """ 客服获取打手详情接口 请求:POST /yonghu/kefuhqdsxq 参数:{ "username": "客服手机号", "uid": "打手ID (yonghuid)" } 认证:JWT 返回:code=0 + 打手信息 + 会员列表 + 所有会员类型 + 客服权限列表 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 username = request.data.get('phone', '').strip() uid = request.data.get('uid', '').strip() if not username or not uid: return Response({'code': 400, 'msg': '缺少参数'}) # 2. 公共权限校验(验证客服身份,获取权限列表) kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 3. 检查是否拥有打手管理相关权限(至少一个) required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} if not any(p in permissions for p in required_perms): return Response({'code': 403, 'msg': '您没有权限查看打手详情'}) # 4. 查询打手主表和扩展表 try: user_main = User.query.select_related('DashouProfile').get( UserUID=uid ) except User.DoesNotExist: return Response({'code': 404, 'msg': '打手不存在'}) 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 KefuGetDashouDetailView(APIView): """ 客服获取打手详情接口 请求:POST /yonghu/kefuhqdsxq 参数:{ "phone": "客服手机号", "uid": "打手ID (yonghuid)" } 认证:JWT 返回:code=0 + 打手信息 + 会员列表 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 username = request.data.get('phone', '').strip() uid = request.data.get('uid', '').strip() if not username or not uid: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 客服身份验证 current_user = request.user # 2. 公共权限校验(验证客服身份,获取权限列表) kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 直接返回错误响应 # 3. 检查是否拥有打手管理相关权限(至少一个) required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} if not any(p in permissions for p in required_perms): return Response({'code': 403, 'msg': '您没有权限查看打手列表'}) # 3. 查询打手主表和扩展表 try: # 使用 select_related 预加载 dashou_profile,减少数据库查询 user_main = User.query.select_related('DashouProfile').get( UserUID=uid ) except User.DoesNotExist: return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND) # 验证用户类型是否为打手(可选,但建议验证) # 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册) try: dashou = user_main.DashouProfile except ObjectDoesNotExist: # 用户存在但未注册打手身份,返回基本信息 user_info = { 'yonghuid': user_main.UserUID, 'avatar': user_main.Avatar or '', 'zaixianzhuangtai': 0, 'zhanghaozhuangtai': 0, 'zhuangtai': 0, 'nicheng': '', 'chenghao': '', 'dianhua': '', 'wechat': '', 'phone': user_main.Phone or '', 'CreateTime': user_main.UserCreateTime, 'ip': user_main.IP or '', 'jiedanzongliang': 0, 'chengjiaozongliang': 0, 'tuikuanliang': 0, 'jinrijiedan': 0, 'yue': 0.00, 'zonge': 0.00, 'jifen': 0, 'yajin': 0.00, 'jinrishouyi': 0.00, 'jinyueshouyi': 0.00, 'yaoqingren': '', 'jieshao': '', } member_list = [] return Response({ 'code': 0, 'data': { 'user_info': user_info, 'member_list': member_list } }) # 4. 构建打手信息 user_info = { 'yonghuid': user_main.UserUID, 'avatar': user_main.Avatar or '', 'zaixianzhuangtai': dashou.zaixianzhuangtai, 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, 'zhuangtai': dashou.zhuangtai, 'nicheng': dashou.nicheng or '', 'chenghao': dashou.chenghao or '', 'dianhua': dashou.dianhua or '', 'wechat': dashou.wechat or '', 'phone': user_main.Phone or '', 'CreateTime': user_main.UserCreateTime, 'ip': user_main.IP or '', 'jiedanzongliang': dashou.jiedanzongliang, 'chengjiaozongliang': dashou.chengjiaozongliang, 'tuikuanliang': dashou.tuikuanliang, 'jinrijiedan': dashou.jinrijiedan, 'yue': float(dashou.yue) if dashou.yue else 0.00, 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, 'jifen': dashou.jifen, 'yajin': float(dashou.yajin) if dashou.yajin else 0.00, 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00, 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00, 'yaoqingren': dashou.yaoqingren or '', 'jieshao': dashou.jieshao or '', } # 5. 查询打手会员信息(仅未过期) member_records = Huiyuangoumai.query.filter( yonghu_id=uid ).order_by('-CreateTime') member_list = [] for record in member_records: # 调用模型方法检查是否过期 if hasattr(record, 'jiance_shifou_daoqi'): is_expired = record.jiance_shifou_daoqi() else: # 如果方法不存在,简单判断 is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True if not is_expired: # 获取会员详情 try: huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id) huiyuan_name = huiyuan.jieshao except Huiyuan.DoesNotExist: huiyuan_name = '未知会员' member_list.append({ 'id': record.id, 'huiyuan_name': huiyuan_name, 'daoqi_time': record.daoqi_time, 'is_active': record.huiyuan_zhuangtai == 1, }) return Response({ 'code': 0, 'data': { 'user_info': user_info, 'member_list': member_list } })''' class KefuUpdateDashouView(APIView): """ 客服修改打手信息接口 支持操作: - 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 Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=2, xiugaitijiao=after, xiugaitijiaoq=before['yue'], ) return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after)}}) elif caozuo == 'jia_yue': if '001bb' not in permissions: return Response({'code': 403, 'msg': '无权限加打手余额'}) amount = Decimal(str(value)) dashou_profile.yue += amount dashou_profile.save() after = dashou_profile.yue Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=2, xiugaitijiao=after, xiugaitijiaoq=before['yue'], ) return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after)}}) # ---------- 押金操作 ---------- elif caozuo == 'jian_yajin': if '001aa' not in permissions: return Response({'code': 403, 'msg': '无权限减打手押金'}) amount = Decimal(str(value)) if dashou_profile.yajin < amount: return Response({'code': 400, 'msg': '押金不足'}) dashou_profile.yajin -= amount dashou_profile.save() after = dashou_profile.yajin Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=2, yajin=after, yyajin=before['yajin'], ) return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after)}}) elif caozuo == 'jia_yajin': if '001bb' not in permissions: return Response({'code': 403, 'msg': '无权限加打手押金'}) amount = Decimal(str(value)) dashou_profile.yajin += amount dashou_profile.save() after = dashou_profile.yajin Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=2, yajin=after, yyajin=before['yajin'], ) return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after)}}) # ---------- 积分操作 ---------- elif caozuo == 'jian_jifen': if '001cc' not in permissions: return Response({'code': 403, 'msg': '无权限减打手积分'}) jifen = int(value) if dashou_profile.jifen < jifen: return Response({'code': 400, 'msg': '积分不足'}) dashou_profile.jifen -= jifen dashou_profile.save() after = dashou_profile.jifen Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=2, jifen=after, yjifen=before['jifen'], ) return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after}}) elif caozuo == 'jia_jifen': if '001dd' not in permissions: return Response({'code': 403, 'msg': '无权限加打手积分'}) jifen = int(value) dashou_profile.jifen += jifen dashou_profile.save() after = dashou_profile.jifen Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=2, jifen=after, yjifen=before['jifen'], ) return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after}}) # ---------- 状态修改 ---------- elif caozuo == 'gai_zhanghaozhuangtai': if '001ee' not in permissions: return Response({'code': 403, 'msg': '无权限修改打手账号状态'}) new_status = int(value) if new_status not in [0, 1]: return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'}) dashou_profile.zhanghaozhuangtai = new_status dashou_profile.save() Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=2, qitashuoming=f'修改账号状态为{new_status}', ) return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}}) elif caozuo == 'gai_zaixianzhuangtai': if '001ee' not in permissions: return Response({'code': 403, 'msg': '无权限修改打手在线状态'}) new_status = int(value) if new_status not in [0, 1]: return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'}) dashou_profile.zaixianzhuangtai = new_status dashou_profile.save() Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=2, qitashuoming=f'修改在线状态为{new_status}', ) return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}}) elif caozuo == 'gai_zhuangtai': if '001ee' not in permissions: return Response({'code': 403, 'msg': '无权限修改打手接单状态'}) new_status = int(value) if new_status not in [0, 1]: return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'}) dashou_profile.zhuangtai = new_status dashou_profile.save() Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=2, qitashuoming=f'修改接单状态为{new_status}', ) return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}}) # ---------- 会员操作 ---------- elif caozuo == 'jia_huiyuan': if '001ff' not in permissions: return Response({'code': 403, 'msg': '无权限给打手加会员'}) if not huiyuan_id: return Response({'code': 400, 'msg': '缺少会员ID'}) 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() Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=2, huiyuants=days, huiyuan_id=huiyuan_id, qitashuoming=f'加会员 {huiyuan_id},增加{days}天', ) return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}}) elif caozuo == 'jian_huiyuan': if '001gg' not in permissions: return Response({'code': 403, 'msg': '无权限给打手减会员'}) if not huiyuan_id: return Response({'code': 400, 'msg': '缺少会员ID'}) try: record = Huiyuangoumai.query.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id) except Huiyuangoumai.DoesNotExist: return Response({'code': 404, 'msg': '该打手未购买此会员'}) record.delete() Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=2, huiyuan_id=huiyuan_id, qitashuoming=f'移除会员 {huiyuan_id}', ) return Response({'code': 0, 'msg': '减会员成功'}) # ---------- 称号修改(金牌打手) ---------- elif caozuo == 'gai_chenghao': if '001hh' not in permissions: return Response({'code': 403, 'msg': '无权限修改打手称号'}) new_chenghao = str(value).strip() if not new_chenghao: return Response({'code': 400, 'msg': '称号不能为空'}) # 可限制合法值,这里只记录日志 dashou_profile.chenghao = new_chenghao dashou_profile.save() Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=2, qitashuoming=f'修改称号:{before["chenghao"] or "无"} → {new_chenghao}', ) return Response({'code': 0, 'msg': '修改称号成功', 'data': {'new_chenghao': new_chenghao}}) else: return Response({'code': 400, 'msg': '未知操作类型'}) except Exception as e: logger.error(f"修改打手信息异常: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误'}) class KefuGetShangjiaListView(APIView): """ 客服/管理员获取商家列表接口 需要拥有以下任一权限:1100a, 1100b 请求:POST /houtai/hqsjgllb 参数: username: 客服手机号(用于权限校验,必填) keyword: 搜索关键词(可选,搜索商家用户ID或昵称) balance_op: 余额比较符,gte(>=) / lte(<=)(可选) balance_value: 余额数值(可选,需与balance_op同时存在) order_count_op: 派单量比较符,gte(>=) / lte(<=)(可选) order_count_value: 派单量数值(可选,需与order_count_op同时存在) status: 商家状态,1=正常,2=禁用(可选) page: 页码,从1开始,默认1 page_size: 每页数量,默认20 返回: code: 0 成功,其他失败 data: { list: [商家对象], total: int, // 总商家数 } """ permission_classes = [] # 不依赖全局认证,手动校验 parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 username = request.data.get('username', '').strip() keyword = request.data.get('keyword', '').strip() balance_op = request.data.get('balance_op', '').strip().lower() balance_value_str = request.data.get('balance_value') order_count_op = request.data.get('order_count_op', '').strip().lower() order_count_value_str = request.data.get('order_count_value') status_str = request.data.get('status') try: page = int(request.data.get('page', 1)) page_size = int(request.data.get('page_size', 20)) if page < 1 or page_size < 1: raise ValueError except ValueError: return Response({'code': 400, 'msg': '分页参数错误'}) if not username: return Response({'code': 401, 'msg': '认证失败,缺少username'}) # 2. 公共权限校验(复用现有函数 verify_kefu_permission) # 假设 verify_kefu_permission 定义在某个公共模块,这里引入 kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 直接返回错误响应 # 3. 检查商家管理权限(1100a 或 1100b) required_perms = {'1100a', '1100b'} if not any(p in permissions for p in required_perms): return Response({'code': 403, 'msg': '您没有权限查看商家列表'}) # 4. 构建商家查询集(商家扩展表 + 关联用户主表) shangjia_qs = UserShangjia.query.select_related('user').all() # 关键词筛选(用户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() # 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=禁用 }) return Response({ 'code': 0, 'data': { 'list': result, 'total': total, } }) # yonghu/views.py 中添加 class KefuGetShangjiaDetailView(APIView): """ 客服/管理员获取商家详情接口 需要权限:1100a 或 1100b 请求:POST /houtai/hthqsjxq 参数: username: 客服手机号(用于权限校验,必填) yonghuid: 商家用户ID(必填) 返回: code: 0 成功 data: 商家所有字段(含主表头像等) """ permission_classes = [] parser_classes = [JSONParser] def post(self, request): username = request.data.get('username', '').strip() yonghuid = request.data.get('yonghuid', '').strip() if not username: return Response({'code': 401, 'msg': '认证失败,缺少username'}) if not yonghuid: return Response({'code': 400, 'msg': '缺少商家ID'}) # 权限校验 kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions required_perms = {'1100a', '1100b'} if not any(p in permissions for p in required_perms): return Response({'code': 403, 'msg': '您没有权限查看商家详情'}) # 查询商家 try: shangjia = UserShangjia.query.select_related('user').get(user__UserUID=yonghuid) except UserShangjia.DoesNotExist: return Response({'code': 404, 'msg': '商家不存在'}) 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': '金额格式错误'}) new_yue = shangjia.yue + amount if new_yue > 999999999.99: return Response({'code': 400, 'msg': '增加后余额超出上限'}) shangjia.yue = new_yue shangjia.save(update_fields=['yue']) return Response({'code': 0, 'msg': '余额增加成功', 'data': {'new_balance': str(shangjia.yue)}}) # 减少余额 if action == 'sub_balance': amount_str = request.data.get('amount') try: amount = Decimal(str(amount_str)) if amount <= 0 or amount > 1000000: return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) except: return Response({'code': 400, 'msg': '金额格式错误'}) if shangjia.yue < amount: return Response({'code': 400, 'msg': '余额不足'}) shangjia.yue = shangjia.yue - amount shangjia.save(update_fields=['yue']) return Response({'code': 0, 'msg': '余额减少成功', 'data': {'new_balance': str(shangjia.yue)}}) # 封禁 if action == 'ban': if shangjia.zhuangtai == 2: return Response({'code': 400, 'msg': '商家已是封禁状态'}) shangjia.zhuangtai = 2 shangjia.save(update_fields=['zhuangtai']) return Response({'code': 0, 'msg': '商家已封禁', 'data': {'zhuangtai': 2}}) # 解封 if action == 'unban': if shangjia.zhuangtai == 1: return Response({'code': 400, 'msg': '商家已是正常状态'}) shangjia.zhuangtai = 1 shangjia.save(update_fields=['zhuangtai']) return Response({'code': 0, 'msg': '商家已解封', 'data': {'zhuangtai': 1}}) class GetProductBaseDataView(APIView): """ 获取商品基础数据(类型、专区、会员) 权限:2200a POST /houtai/htsphqjcsx 参数:username 返回: code:0, data: { type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...], zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...], member_list: [ {huiyuan_id, jieshao, jiage}, ...] } """ permission_classes = [] parser_classes = [JSONParser] def post(self, request): username = request.data.get('username', '').strip() if not username: return Response({'code': 401, 'msg': '缺少username'}) # 权限校验 kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions if '2200a' not in permissions: return Response({'code': 403, 'msg': '无权限管理商品'}) # 获取所有商品类型(按 paixu 升序) type_qs = ShangpinLeixing.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 = User.query.filter( GuanshiProfile__isnull=False # 确保是管事 ).select_related('GuanshiProfile').select_related('BossProfile') # 关键词搜索(用户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, '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': '用户不是管事'}) # 基础信息 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), '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, } }) 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 # ---------- 1. 基础信息修改(需要4400a)---------- nickname = request.data.get('nickname') dianhua = request.data.get('dianhua') wechat = request.data.get('wechat') zhuangtai = request.data.get('zhuangtai') avatar = request.data.get('avatar') if any(v is not None for v in [nickname, dianhua, wechat, zhuangtai, avatar]): if '4400a' not in permissions: return Response({'code': 403, 'msg': '无权限修改管事基础信息(需要4400a)'}) # 处理昵称(在老板扩展表中) if nickname is not None: if not boss: boss = UserBoss.query.create(user=user, nickname=nickname) else: boss.nickname = nickname boss.save() # 处理其他字段 if dianhua is not None: guanshi.dianhua = dianhua if wechat is not None: guanshi.wechat = wechat if zhuangtai is not None: try: zt = int(zhuangtai) if zt in (0, 1): guanshi.zhuangtai = zt except: pass if avatar is not None: user.Avatar = avatar user.save() guanshi.save() # ---------- 2. 余额修改(加:4400c;减:4400b)---------- new_yue = request.data.get('yue') if new_yue is not None: try: new_yue = Decimal(str(new_yue)) if new_yue < 0: return Response({'code': 400, 'msg': '余额不能为负数'}) except: return Response({'code': 400, 'msg': '余额格式错误'}) old_yue = guanshi.yue if new_yue > old_yue: if '4400c' not in permissions: return Response({'code': 403, 'msg': '无权限增加余额(需要4400c)'}) elif new_yue < old_yue: if '4400b' not in permissions: return Response({'code': 403, 'msg': '无权限减少余额(需要4400b)'}) guanshi.yue = new_yue guanshi.save() # ---------- 3. 提现限额修改(需要4400e)---------- kaioi_ewai = request.data.get('kaioi_ewai_xiane') ewai_val = request.data.get('ewai_xiane') if kaioi_ewai is not None or ewai_val is not None: if '4400e' not in permissions: return Response({'code': 403, 'msg': '无权限修改提现限额(需要4400e)'}) if kaioi_ewai is not None: guanshi.kaioi_ewai_xiane = bool(kaioi_ewai) if ewai_val is not None: try: guanshi.ewai_xiane = Decimal(str(ewai_val)) except: return Response({'code': 400, 'msg': '额外限额金额格式错误'}) guanshi.save() # ---------- 4. 分红相关修改(需要4400f)---------- perm_enabled = request.data.get('permanent_enabled') perm_amount = request.data.get('permanent_amount') custom_first = request.data.get('custom_first_dividend') extra_map = request.data.get('extra_dividend_map') if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]): if '4400f' not in permissions: return Response({'code': 403, 'msg': '无权限修改分红策略(需要4400f)'}) # 4.1 永久分红 if perm_enabled is not None: guanshi.fenghong_erci_enabled = bool(perm_enabled) if perm_amount is not None: try: guanshi.fenghong_erci = Decimal(str(perm_amount)) except: return Response({'code': 400, 'msg': '永久分红金额格式错误'}) guanshi.save() # 4.2 首次定制分红(cishu=1) if custom_first is not None and isinstance(custom_first, dict): # 获取当前所有首次定制记录 existing_first = DuociFenhong.query.filter(yonghuid=yonghuid, cishu=1) # 前端传来的定制字典 {会员ID: 金额} for huiyuan_id, amount in custom_first.items(): try: amount = Decimal(str(amount)) except: continue if amount <= 0: # 金额为0或负数表示删除定制 DuociFenhong.query.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() else: DuociFenhong.query.update_or_create( yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1, defaults={ 'guanshi_fenhong': amount, 'zuzhang_fenhong': Decimal('0') } ) # 删除那些在前端字典中不存在的首次定制记录(即已取消定制) for record in existing_first: if record.huiyuan not in custom_first: record.delete() # 4.3 额外次数分红(cishu >= 2) if extra_map is not None and isinstance(extra_map, dict): # 收集需要保留的记录 keep_set = set() for times_str, members in extra_map.items(): try: cishu = int(times_str) if cishu < 2: continue except: continue if not isinstance(members, dict): continue for huiyuan_id, data in members.items(): if not isinstance(data, dict): continue amount = data.get('guanshi_fenhong') if amount is None: continue try: amount = Decimal(str(amount)) except: continue keep_set.add((cishu, huiyuan_id)) # 更新或创建 DuociFenhong.query.update_or_create( yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=cishu, defaults={ 'guanshi_fenhong': amount, 'zuzhang_fenhong': Decimal('0') } ) # 删除不在 keep_set 中的额外次数记录 existing_extra = DuociFenhong.query.filter(yonghuid=yonghuid, cishu__gte=2) for record in existing_extra: if (record.cishu, record.huiyuan) not in keep_set: record.delete() return Response({'code': 0, 'msg': '修改成功'}) class GetWithdrawSettingsView(APIView): """ 获取提现设置(手续费利率、每日限额、当日已提现总额) 权限:需要 5500a 或 5500b 或 5500c 任一 POST /houtai/hthqtxpz """ permission_classes = [] parser_classes = [JSONParser] def post(self, request): username = request.data.get('username', '').strip() if not username: return Response({'code': 401, 'msg': '缺少username'}) kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 需要5500a/b/c任一 if not any(p in permissions for p in ('5500a', '5500b', '5500c')): return Response({'code': 403, 'msg': '无权限查看提现设置'}) # 1. 手续费利率(CommissionRate:5打手/6管事/8组长/9审核官/10商家/11打手押金) rate_map = {} for leixing, code in [(1, '5'), (2, '6'), (3, '8'), (4, '9'), (5, '11'), (6, '10')]: obj = CommissionRate.query.filter(Platform=code).first() rate_map[leixing] = float(obj.Rate) if obj and obj.Rate is not None else 0.0 # 1b. 订单/押金分红费率(CommissionRate:1平台订单/3商家订单/12押金管事/13商家订单管事分红) order_rate_map = {} for leixing, code in [(1, '1'), (3, '3'), (12, '12'), (13, '13')]: obj = CommissionRate.query.filter(Platform=code).first() order_rate_map[leixing] = float(obj.Rate) if obj and obj.Rate is not None else 0.0 # 2. 个人每日限额 (leixing=1,2,3) quota_map = {} for leixing in [1, 2, 3]: obj = TixianQuotaDefault.query.filter(UserType=leixing).first() quota_map[leixing] = float(obj.default_quota) if obj else 20.0 # 3. 每日总限额(提现类型1~6 → 配置表4~9) total_limit_map = {} for withdraw_leixing, quota_leixing in [(1, 4), (2, 5), (3, 6), (4, 7), (5, 8), (6, 9)]: obj = TixianQuotaDefault.query.filter(UserType=quota_leixing).first() total_limit_map[withdraw_leixing] = float(obj.default_quota) if obj else 0.0 # 4. 当日已提现总额(按提现类型 leixing 1~6) today = date.today() today_totals = {i: 0.0 for i in range(1, 7)} records = WithdrawalDailyStats.query.filter(Date=today) for rec in records: if rec.WithdrawalType in today_totals: today_totals[rec.WithdrawalType] = float(rec.total_amount) # 🆕 获取提现模式(自动1 / 手动2),默认2 config = WithdrawConfig.query.filter(id=1).first() withdraw_mode = config.mode if config else 2 return Response({ 'code': 0, 'data': { 'withdraw_mode': withdraw_mode, 'rates': rate_map, 'order_rates': order_rate_map, 'quotas': quota_map, 'total_limits': total_limit_map, 'today_totals': today_totals, } }) class UpdateWithdrawSettingsView(APIView): """ 修改提现设置(权限细分): - 修改打手相关(利率、个人限额、总限额)需要5500a - 修改管事相关需要5500b - 修改组长相关需要5500c 注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算), 后端不直接修改 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 rates = request.data.get('rates', {}) order_rates = request.data.get('order_rates', request.data.get('commission_rates', {})) quotas = request.data.get('quotas', {}) total_limits = request.data.get('total_limits', {}) # 角色类型映射 role_perms = {1: '5500a', 2: '5500b', 3: '5500c'} # 利率代码映射(1-3 有水缸限额;4审核官/5打手押金/6商家余额仅费率) rate_code_map = {1: '5', 2: '6', 3: '8', 4: '9', 5: '11', 6: '10'} # 订单/押金分红(1平台订单/3商家订单/12押金管事/13商家订单管事分红) order_rate_code_map = {1: '1', 3: '3', 12: '12', 13: '13'} # 总限额:提现类型 → TixianQuotaDefault.leixing(4~9) total_code_map = {1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9} def _rate_key_present(data, role): return (str(role) in data and data[str(role)] is not None) or \ (role in data and data[role] is not None) with transaction.atomic(): # 遍历三个角色(限额相关) for role in [1, 2, 3]: perm_needed = role_perms[role] if perm_needed not in permissions: if _rate_key_present(rates, role) or \ (str(role) in quotas and quotas[str(role)] is not None) or \ (role in quotas and quotas.get(role) is not None) or \ (str(role) in total_limits and total_limits[str(role)] is not None) or \ (role in total_limits and total_limits.get(role) is not None): return Response({'code': 403, 'msg': f'无权限修改{["","打手","管事","组长"][role]}相关设置(需要{perm_needed})'}) # 其他提现手续费(审核官/打手押金/商家余额):需 5500a/b/c 任一 extra_role_names = {4: '考核官', 5: '打手押金', 6: '商家余额'} for role in [4, 5, 6]: if _rate_key_present(rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')): return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}提现手续费率'}) if (str(role) in total_limits and total_limits[str(role)] is not None) or \ (role in total_limits and total_limits.get(role) is not None): if not any(p in permissions for p in ('5500a', '5500b', '5500c')): return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}总限额'}) # 订单/押金分红费率:需 5500a/b/c 任一 for role in [1, 3, 12, 13]: if _rate_key_present(order_rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')): order_names = { 1: '平台订单打手分红', 3: '商家订单打手分红', 12: '押金管事分红', 13: '管事订单分红', } return Response({'code': 403, 'msg': f'无权限修改{order_names[role]}费率'}) # 1. 修改手续费利率 for role in [1, 2, 3, 4, 5, 6]: val = rates.get(str(role), rates.get(role)) if val is not None: rate = Decimal(str(val)) code = rate_code_map[role] obj, created = CommissionRate.objects.select_for_update().get_or_create( Platform=code, defaults={'Rate': rate} ) if not created: obj.Rate = rate obj.save() # 1b. 修改订单/押金分红费率 for role in [1, 3, 12, 13]: val = order_rates.get(str(role), order_rates.get(role)) if val is not None: rate = Decimal(str(val)) code = order_rate_code_map[role] obj, created = CommissionRate.objects.select_for_update().get_or_create( Platform=code, defaults={'Rate': rate} ) if not created: obj.Rate = rate obj.save() # 2. 修改个人每日限额 for role in [1, 2, 3]: if str(role) in quotas: val = quotas[str(role)] if val is not None: quota = Decimal(str(val)) obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create( UserType=role, defaults={'default_quota': quota} ) if not created: obj.default_quota = quota obj.save() # 3. 修改每日总限额(1打手/2管事/3组长/4考核官/5押金/6商家) for role in [1, 2, 3, 4, 5, 6]: val = total_limits.get(str(role), total_limits.get(role)) if val is not None: limit = Decimal(str(val)) code = total_code_map[role] obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create( UserType=code, defaults={'default_quota': limit} ) if not created: obj.default_quota = limit obj.save() # 🆕 修改提现模式(有5500a/b/c任一即可) withdraw_mode = request.data.get('withdraw_mode') if withdraw_mode is not None: if not any(p in permissions for p in ('5500a', '5500b', '5500c')): return Response({'code': 403, 'msg': '无权限修改提现模式'}) # 更新或创建 id=1 的配置 WithdrawConfig.query.update_or_create( id=1, defaults={'mode': int(withdraw_mode)} ) # 注意:WithdrawalDailyStats 表由提现流程自动更新,此处不直接修改 return Response({'code': 0, 'msg': '设置修改成功'}) class GetZuzhangListView(APIView): """ 组长列表接口(分页 + 多条件筛选) 路由:POST /houtai/hthqzzlb 权限:需要登录,且拥有 6600a~6600e 任一权限 """ permission_classes = [IsAuthenticated] @transaction.atomic # 保证查询的一致性(可选,对只读查询影响不大但无坏处) def post(self, request): # 1. 获取前端传递的 username(用于防越权) frontend_username = request.data.get('username', '') # 2. 调用公共校验方法,验证身份并获取权限列表 result = verify_kefu_permission(request, username_from_frontend=frontend_username) # 重要:verify_kefu_permission 返回格式为 (user_obj, permissions_list) 或 (None, Response) # 如果第二个元素是 Response 对象,说明校验失败,直接返回该 Response if isinstance(result, tuple) and len(result) == 2: user_obj, second = result if isinstance(second, Response): return second # 校验失败,直接返回错误响应 else: permissions = second # 正常情况:permissions 是列表 else: # 防止意外返回格式,视为失败 return Response({'code': 500, 'msg': '权限校验模块异常'}) # 3. 检查是否拥有组长管理所需权限(6600a ~ 6600e 任一) required_perms = ['6600a', '6600b', '6600c', '6600d', '6600e'] if not any(perm in permissions for perm in required_perms): logger.warning(f"用户 {request.user.Phone} 尝试访问组长列表但缺少必要权限,已有权限: {permissions}") return Response({'code': 403, 'msg': '您无权访问组长管理页面'}) # 4. 提取请求参数(分页 + 筛选) try: page = int(request.data.get('page', 1)) page_size = int(request.data.get('page_size', 20)) except ValueError: page, page_size = 1, 20 keyword = request.data.get('keyword', '').strip() status = request.data.get('status') # 1=正常, 0=禁用 invite_count_op = request.data.get('invite_count_op') # gte / lte invite_count_value = request.data.get('invite_count_value') amount_op = request.data.get('amount_op') amount_value = request.data.get('amount_value') commission_op = request.data.get('commission_op') commission_value = request.data.get('commission_value') # 5. 构建基础 QuerySet(预加载关联表) queryset = UserZuzhang.query.select_related('user', 'user__BossProfile').all() # 6. 应用筛选条件(使用 ORM 安全过滤) if keyword: queryset = queryset.filter( Q(user__UserUID__icontains=keyword) | Q(user__boss_profile__nickname__icontains=keyword) ) if status is not None and status != '': try: queryset = queryset.filter(zhuangtai=int(status)) except ValueError: pass # 邀请总数筛选 if invite_count_value not in (None, ''): try: val = float(invite_count_value) op = invite_count_op if invite_count_op in ('gte', 'lte') else 'gte' queryset = queryset.filter(**{f'yaoqing_zongshu__{op}': val}) except (ValueError, TypeError): pass # 可提现金额筛选 if amount_value not in (None, ''): try: val = float(amount_value) op = amount_op if amount_op in ('gte', 'lte') else 'gte' queryset = queryset.filter(**{f'ketixian_jine__{op}': val}) except (ValueError, TypeError): pass # 分佣总额筛选 if commission_value not in (None, ''): try: val = float(commission_value) op = commission_op if commission_op in ('gte', 'lte') else 'gte' queryset = queryset.filter(**{f'fenyong_zonge__{op}': val}) except (ValueError, TypeError): pass # 7. 总数统计 total = queryset.count() # 8. 分页 offset = (page - 1) * page_size paged_queryset = queryset[offset:offset + page_size] # 9. 构造返回数据 data_list = [] for zu in paged_queryset: user = zu.user nickname = '' if hasattr(user, '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', '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': '用户不是组长'}) # 基础信息 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(): # ---------- 1. 基础信息修改(需要6600c?按需求,基础信息修改通常用6600c,但为了安全,暂不校验,如有需要可加)---------- nickname = request.data.get('nickname', '').strip() avatar = request.data.get('avatar', '').strip() dianhua = request.data.get('dianhua', '').strip() wechat = request.data.get('wechat', '').strip() zhuangtai = request.data.get('zhuangtai') # 状态修改需要6600c if zhuangtai is not None: if '6600c' not in permissions: return Response({'code': 403, 'msg': '无权限修改组长状态(需要6600c)'}) try: zt = int(zhuangtai) if zt in (0, 1): zuzhang.zhuangtai = zt except: pass if nickname: if not boss: boss = UserBoss.query.create(user=user, nickname=nickname) else: boss.nickname = nickname boss.save() if avatar: user.Avatar = avatar user.save() # 组长表没有电话/微信字段,如需存储可暂存到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)'}) elif new_val < old_ketixian_jine: if '6600b' not in permissions: return Response({'code': 403, 'msg': '无权限减少可提现金额(需要6600b)'}) zuzhang.ketixian_jine = new_val zuzhang.save(update_fields=['ketixian_jine']) # ---------- 3. 提现限额修改(需要6600d)---------- kaioi_ewai = request.data.get('kaioi_ewai_tixian') ewai_val = request.data.get('ewai_tixian_xiane') if kaioi_ewai is not None or ewai_val is not None: if '6600d' not in permissions: return Response({'code': 403, 'msg': '无权限修改提现限额(需要6600d)'}) if kaioi_ewai is not None: zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai) if ewai_val is not None: try: zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val)) except: return Response({'code': 400, 'msg': '额外限额金额格式错误'}) zuzhang.save() # ---------- 4. 分红相关修改(需要6600e)---------- perm_enabled = request.data.get('permanent_enabled') perm_amount = request.data.get('permanent_amount') custom_first = request.data.get('custom_first_dividend') extra_map = request.data.get('extra_dividend_map') if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]): if '6600e' not in permissions: return Response({'code': 403, 'msg': '无权限修改分红策略(需要6600e)'}) # 4.1 永久分红(额外分红开关和金额) if perm_enabled is not None: zuzhang.kaioi_ewai_fenhong = bool(perm_enabled) if perm_amount is not None: try: zuzhang.ewai_fenhong_jine = Decimal(str(perm_amount)) except: return Response({'code': 400, 'msg': '永久分红金额格式错误'}) zuzhang.save() # 4.2 首次定制分红(cishu=1) if custom_first is not None and isinstance(custom_first, dict): existing_first = DuociFenhong.query.filter(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(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() else: DuociFenhong.query.update_or_create( yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1, defaults={ 'zuzhang_fenhong': amount, 'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红 } ) # 删除那些在前端字典中不存在的首次定制记录(即已取消定制) for record in existing_first: if record.huiyuan not in custom_first: record.delete() # 4.3 额外次数分红(cishu >= 2) if extra_map is not None and isinstance(extra_map, dict): keep_set = set() for times_str, members in extra_map.items(): try: cishu = int(times_str) if cishu < 2: continue except: continue if not isinstance(members, dict): continue for huiyuan_id, data in members.items(): if not isinstance(data, dict): continue amount = data.get('zuzhang_fenhong') if amount is None: continue try: amount = Decimal(str(amount)) except: continue keep_set.add((cishu, huiyuan_id)) DuociFenhong.query.update_or_create( yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=cishu, defaults={ 'zuzhang_fenhong': amount, 'guanshi_fenhong': Decimal('0') } ) # 删除不在 keep_set 中的额外次数记录 existing_extra = DuociFenhong.query.filter(yonghuid=yonghuid, cishu__gte=2) for record in existing_extra: if (record.cishu, record.huiyuan) not in keep_set: record.delete() # 保存组长其他可能修改的字段(例如电话、微信没有,忽略) return Response({'code': 0, 'msg': '修改成功'}) class GetProductTypeZoneView(APIView): """ 获取商品类型、专区及会员列表 权限:需要拥有 7007a 请求:POST /houtai/hthqsplxzq 参数:{"username": "客服手机号"} 返回: code: 0 data: { type_list: [{id, jieshao, tupian_url, yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, 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查询) pages = PopupPage.query.all().prefetch_related('popups__images').order_by('id') # 5. 序列化输出 serializer = PopupPageSerializer(pages, many=True) return Response({'code': 0, 'data': {'pages': serializer.data}}) class PopupNoticeModifyAPIView(APIView): """ 统一修改接口:支持页面、弹窗、图片的增删改 URL: POST /houtai/htxgtcxx 请求中必须包含 action 字段,用于区分操作类型。 所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段体现。 """ permission_classes = [IsAuthenticated] def post(self, request): # ---------- 1. 权限验证 ---------- username_frontend = request.data.get('username') kefu, permissions = verify_kefu_permission(request, username_frontend) if kefu is None: return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) if REQUIRED_PERMISSION not in permissions: return Response({'code': 403, 'msg': '您没有权限操作弹窗管理'}) # ---------- 2. 获取 action 并分发 ---------- action = request.data.get('action') if not action: return Response({'code': 400, 'msg': '缺少action参数'}) try: if action == 'add_page': return self.add_page(request) elif action == 'update_page': return self.update_page(request) elif action == 'delete_page': return self.delete_page(request) elif action == 'add_popup': return self.add_popup(request) elif action == 'update_popup': return self.update_popup(request) elif action == 'delete_popup': return self.delete_popup(request) elif action == 'add_image': return self.add_image(request) elif action == 'update_image': return self.update_image(request) elif action == 'delete_image': return self.delete_image(request) else: return Response({'code': 400, 'msg': f'未知的action: {action}'}) except Exception as e: # 捕获未预期的异常,返回服务器错误 traceback.print_exc() return Response({'code': 500, 'msg': f'服务器错误: {str(e)}'}) # ==================== 页面相关操作 ==================== def add_page(self, request): """添加新页面""" data = request.data # 必填字段校验 if not data.get('page_key') or not data.get('name'): return Response({'code': 400, 'msg': '缺少 page_key 或 name'}) # 唯一性校验 if PopupPage.query.filter(page_key=data['page_key']).exists(): return Response({'code': 400, 'msg': '页面标识已存在'}) page = PopupPage.query.create( page_key=data['page_key'], name=data['name'], description=data.get('description', ''), is_active=data.get('is_active', True), ignore_user_mute=data.get('ignore_user_mute', False) ) return Response({'code': 0, 'msg': '添加成功', 'data': {'id': page.id}}) def update_page(self, request): """更新页面信息""" page_id = request.data.get('id') if not page_id: return Response({'code': 400, 'msg': '缺少页面id'}) try: page = PopupPage.query.get(id=page_id) except PopupPage.DoesNotExist: return Response({'code': 404, 'msg': '页面不存在'}) # 更新允许修改的字段 page.page_key = request.data.get('page_key', page.page_key) page.name = request.data.get('name', page.name) page.description = request.data.get('description', page.description) page.is_active = request.data.get('is_active', page.is_active) page.ignore_user_mute = request.data.get('ignore_user_mute', page.ignore_user_mute) page.save() return Response({'code': 0, 'msg': '更新成功'}) def delete_page(self, request): """删除页面,可级联删除其下所有弹窗和图片""" page_id = request.data.get('id') cascade = request.data.get('cascade_delete_popups', True) # 默认级联 if not page_id: return Response({'code': 400, 'msg': '缺少页面id'}) try: page = PopupPage.query.get(id=page_id) except PopupPage.DoesNotExist: return Response({'code': 404, 'msg': '页面不存在'}) if cascade: # 级联删除:使用事务保证原子性 with transaction.atomic(): popups = page.popups.all() for popup in popups: # 删除弹窗下的所有图片文件 for img in popup.images.all(): if img.image_url: delete_from_oss(img.image_url) popups.delete() page.delete() else: # 非级联删除:如果页面下还有弹窗,则拒绝删除 if page.popups.exists(): return Response({'code': 400, 'msg': '该页面下存在弹窗,请先删除弹窗或使用级联删除'}) page.delete() return Response({'code': 0, 'msg': '删除成功'}) # ==================== 弹窗相关操作 ==================== def add_popup(self, request): """为指定页面添加新弹窗""" data = request.data page_id = data.get('page_id') popup_id = data.get('popup_id') if not page_id or not popup_id: return Response({'code': 400, 'msg': '缺少 page_id 或 popup_id'}) try: page = PopupPage.query.get(id=page_id) except PopupPage.DoesNotExist: return Response({'code': 404, 'msg': '页面不存在'}) # 同一页面下 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': '弹窗不存在'}) # 更新弹窗字段 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': '弹窗不存在'}) 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': '弹窗不存在'}) # 获取上传的文件 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': '图片不存在'}) # 更新文字和排序 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': '图片不存在'}) 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 = Penalty.query.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()} return Response({'code': 0, 'msg': '成功', 'data': data}) except Exception as e: logger.error(f"罚款统计异常: {traceback.format_exc()}") return Response({'code': 500, 'msg': '系统繁忙'}) class FaKuanLieBiaoView(APIView): """ 罚款列表接口 POST /houtai/hthqfklb """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): username = request.data.get('username', '').strip() if not username: return Response({'code': 400, 'msg': '参数不完整'}) try: kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions if not 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 = Penalty.query.all() # 状态筛选 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 '', }) return Response({ 'code': 0, 'msg': '成功', 'data': { 'list': results, 'total': paginator.count, 'page': page, 'page_size': page_size, } }) except Exception as e: logger.error(f"罚款列表异常: {traceback.format_exc()}") return Response({'code': 500, 'msg': '系统繁忙'}) class FaKuanChuLiView(APIView): """ 处理罚款申诉(同意/拒绝) POST /houtai/glyclfk """ permission_classes = [IsAuthenticated] parser_classes = [MultiPartParser, FormParser, JSONParser] def post(self, request): try: username = request.data.get('username', '').strip() penalty_id = request.data.get('penalty_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': '罚单不存在'}) 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 = request.data.get('PenalizedUserID', '').strip() shenfen = request.data.get('shenfen') Reason = request.data.get('Reason', '').strip() FineAmount = request.data.get('FineAmount') AffectsGrabbing = request.data.get('AffectsGrabbing', 1) if not all([username, PenalizedUserID, shenfen, Reason, FineAmount]): 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, # 待缴纳 ) 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 = request.data.get('OrderID', '').strip() Reason = request.data.get('Reason', '').strip() FineAmount = request.data.get('FineAmount') AffectsGrabbing = request.data.get('AffectsGrabbing', 1) if not all([username, OrderID, Reason, FineAmount]): 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 客服 ) 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, ) # 扣除打手积分 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, UserUID__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 = Czjilu.query.filter(**chongzhi_filter) \ .annotate(period=trunc_func) \ .values('period') \ .annotate( chongzhi_count=Count('id'), chongzhi_amount=Sum('jine') ).order_by('period') # 分红分组查询 fenhong_qs = None if include_guanshi or include_zuzhang: fenhong_agg = {} if include_guanshi: fenhong_agg['guanshi_amount'] = Sum('fenhong') if include_zuzhang: fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong') fenhong_qs = Gsfenhong.query.filter(**fenhong_filter) \ .annotate(period=trunc_func) \ .values('period') \ .annotate(**fenhong_agg) \ .order_by('period') # 合并数据 data_map = {} for item in chongzhi_qs: period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ item['period'].strftime('%Y-%m') if granularity == 'month' else \ item['period'].strftime('%Y') data_map[period_str] = { 'chongzhi_count': item['chongzhi_count'], 'chongzhi_amount': float(item['chongzhi_amount'] or 0) } if fenhong_qs: for item in fenhong_qs: period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ item['period'].strftime('%Y-%m') if granularity == 'month' else \ item['period'].strftime('%Y') if period_str not in data_map: data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0} if include_guanshi: data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0) if include_zuzhang: data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0) time_series = [] for period_str, vals in data_map.items(): guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0 zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0 vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2) vals['date'] = period_str time_series.append(vals) time_series.sort(key=lambda x: x['date']) # ---------- 汇总数据 ---------- summary_chongzhi_filter = chongzhi_filter.copy() if granularity == 'day': summary_chongzhi_filter['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 = DailyIncomeStat.query.filter(**income_filter) \ .values(group_field) \ .annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \ .order_by(group_field) # ---------- 支出分组查询 ---------- payout_qs = DailyPayoutStat.query.filter(**payout_filter) \ .values(group_field) \ .annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) \ .order_by(group_field) # ---------- 合并数据 ---------- data_map = {} for item in income_qs: key = item[group_field] data_map[key] = { 'income': float(item['total_income'] or 0), 'income_count': item['total_count'] or 0, 'payout': 0.0, 'payout_count': 0 } for item in payout_qs: key = item[group_field] if key not in data_map: data_map[key] = {'income': 0.0, 'income_count': 0, 'payout': 0.0, 'payout_count': 0} data_map[key]['payout'] = float(item['total_payout'] or 0) data_map[key]['payout_count'] = item['total_count'] or 0 time_series = [] for key, vals in data_map.items(): # 构造日期字符串 if granularity == 'day': date_str = f"{year}-{month:02d}-{key:02d}" elif granularity == 'month': date_str = f"{year}-{key:02d}" else: date_str = str(key) profit = round(vals['income'] - vals['payout'], 2) time_series.append({ 'date': date_str, 'income': vals['income'], 'payout': vals['payout'], 'profit': profit }) time_series.sort(key=lambda x: x['date']) # ---------- 汇总数据 ---------- income_summary = DailyIncomeStat.query.filter(**summary_filter).aggregate( total_income=Sum('total_amount'), total_count=Sum('total_count') ) payout_summary = DailyPayoutStat.query.filter(**summary_filter).aggregate( total_payout=Sum('total_amount'), total_count=Sum('total_count') ) total_income = float(income_summary['total_income'] or 0) total_payout = float(payout_summary['total_payout'] or 0) total_profit = round(total_income - total_payout, 2) return Response({ 'code': 0, 'msg': 'ok', 'data': { 'time_series': time_series, 'summary': { 'total_income': total_income, 'total_income_count': income_summary['total_count'] or 0, 'total_payout': total_payout, 'total_payout_count': payout_summary['total_count'] or 0, 'total_profit': total_profit } } }) except Exception as e: logger.exception("SzxxView 接口错误") return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) class CwddhqlxView(APIView): """ 获取订单类型(商品类型)用于筛选 POST /houtai/cwddhqlx Body: { "phone": "管理员手机号" } """ permission_classes = [IsAuthenticated] def post(self, request): try: username_from_frontend = request.data.get('phone', None) kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) if kefu_obj is None: return permissions if 'dingdancaiwu' not in permissions: return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403) # 获取非审核状态(shenhezhuangtai=1)的商品类型,只返回ID和名称 types = ShangpinLeixing.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 # ---------- 时间序列分组查询 ---------- qs = Order.query.filter(**filters) \ .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 = Order.query.filter(**filters) 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 = 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)—— 修改:直接从 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'), # 直接使用罚单表中的申请人分红金额字段 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 = Czjilu.query.filter(**filter_kwargs).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 = Penalty.query.filter(**penalty_filter).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 = Czjilu.query.filter(**filter_kwargs).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 = UserShenheguan.query.select_related('user__DashouProfile') 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] # ---------- 预计算每个板块的打手数量 ---------- 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 = YonghuChenghao.query.filter( chenghao_id__in=tag_ids, yonghu__DashouProfile__isnull=False ).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 = YonghuChenghao.query.filter(chenghao=tag).values('yonghu').distinct().count() tag_list.append({ 'id': tag.id, 'name': tag.mingcheng, 'user_count': user_count, }) bankuai_tags_data.append({ 'bankuai_id': bk.bankuai_id, 'mingcheng': bk.mingcheng, 'tags': tag_list, }) # 构造返回数据(审核官列表) data_list = [] for sg in shenheguan_list: user = sg.user # 获取该审核官关联的板块 bk_qs = KaoheguanBankuai.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 = request.data.get('OrderID', '').strip() 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 客服处理统计 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, # 注意:不继承 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_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 '游戏订单'