diff --git a/backend/view.py b/backend/view.py index 152373d..845968e 100644 --- a/backend/view.py +++ b/backend/view.py @@ -1,9246 +1,9291 @@ -import hmac -import threading -import traceback -import uuid -import hashlib -import xmltodict -import time -import logging -import requests -import os -import secrets -import random -import string -from calendar import monthrange -from decimal import Decimal -from datetime import date, datetime, timedelta -from django.utils import timezone -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from django.core.paginator import Paginator -from django.db import models, transaction, IntegrityError -from django.db.models import Q, Count, Sum, F, Exists, OuterRef -from gvsdsdk.fluent import db, func, FQ -from django.db.models.functions import TruncDate, TruncMonth, TruncYear -from django.contrib.auth.hashers import make_password -from django.views.decorators.csrf import csrf_exempt -from django.utils.decorators import method_decorator - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated, AllowAny -from rest_framework.parsers import JSONParser, MultiPartParser, FormParser - -# 工具类导入 -from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss -from backend.utils import ( - update_dashou_daily_by_action, - update_shangjia_daily -) -from jituan.services.club_context import filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request -from jituan.services.club_penalty import ( - filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs, - forbid_penalty_out_of_scope, resolve_penalty_record_club_id, -) -from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta -from shop.utils import update_dianpu_daily_stat -from products.utils import update_shangpin_daily_stat -from orders.notice_tasks import dingdan_guangbo -from .utils import ( - verify_kefu_permission, - PERMISSION_TO_SHENFEN, - SHENFEN_PROFILE_MAP, - has_fadan_view_permission, - check_fadan_permission, - pick_order_id, - pick_penalty_create_params, - write_xiugai_log, - XIUGAI_LEIXING_DASHOU, - XIUGAI_LEIXING_GUANSHI, - XIUGAI_LEIXING_SHANGJIA, - XIUGAI_LEIXING_ZUZHANG, - XIUGAI_LEIXING_LABEL, -) - -# models 集中导入 -## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构) -from gvsdsdk.models import Role, Permission, RolePermission, UserRole -## backend -from backend.models import WithdrawalDailyStats - -## users -from users.models import ( - UserGuanshi, UserBoss, UserZuzhang, - UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan -) -from users.business_models import User - -## orders -from orders.models import ( - CommissionRate, PlayerDeliveryImage, PenaltyRecord, - OrderPlayerHistory, Order, RefundRecord, - MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus -) - -## shop -from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi - -## products -from products.models import ( - Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan, - DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong -) - -## config -from config.models import ( - WithdrawConfig, ShangjiaLianjie, AccountPermission, - TixianQuotaDefault, - DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage -) - -## rank -from rank.models import ( - KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong, - YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu -) - -# 序列化器 -from .serializers import PopupPageSerializer - -# 全局常量、日志对象 -logger = logging.getLogger('houtai') -SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd'] - - -def _serialize_permission_row(perm_row): - """gvsdsdk Permission → 后台 Vue 约定的小写字段""" - if isinstance(perm_row, dict): - code = perm_row.get('PermCode') or perm_row.get('perm_code') or '' - name = perm_row.get('PermName') or perm_row.get('perm_name') or code - desc = perm_row.get('PermDesc') or perm_row.get('perm_desc') or '' - else: - code = perm_row.PermCode or '' - name = perm_row.PermName or code - desc = perm_row.PermDesc or '' - return {'perm_code': code, 'perm_name': name, 'perm_desc': desc} - - -class GetRolePermissionView(APIView): - """ - 获取所有角色、权限以及角色关联的用户数 - 权限要求:用户必须拥有 000001 权限 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 1. 权限校验(验证客服身份,并获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 2. 检查是否有 000001 权限 - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权访问此页面'}) - - # 3. 获取所有权限 - all_perms = Permission.objects.filter(PermStatus=1).values('PermCode', 'PermName', 'PermDesc') - permissions_list = [_serialize_permission_row(p) for p in all_perms] - - # 4. 获取所有角色,并统计每个角色下的用户数(仅后台客服账号) - kefu_user_uuids = list( - User.objects.filter(KefuProfile__isnull=False).values_list('UserUUID', flat=True) - ) - roles = Role.objects.filter(RoleStatus=1) - roles_data = [] - for role in roles: - user_count = UserRole.objects.filter( - RoleUUID=role.RoleUUID, - UserUUID__in=kefu_user_uuids, - ).count() - role_perm_uuids = RolePermission.objects.filter( - RoleUUID=role.RoleUUID - ).values_list('PermUUID', flat=True) - perms_of_role = [ - _serialize_permission_row(p) - for p in Permission.objects.filter( - PermUUID__in=list(role_perm_uuids) - ).values('PermCode', 'PermName', 'PermDesc') - ] - roles_data.append({ - 'role_code': role.RoleName, - 'role_name': role.RoleName, - 'description': role.RoleDesc or '', - 'user_count': user_count, - 'permissions': perms_of_role - }) - - return Response({ - 'code': 0, - 'data': { - 'roles': roles_data, - 'permissions': permissions_list - } - }) - - -class ModifyRolePermissionView(APIView): - """ - 通用接口:支持删除角色、为角色添加权限、移除角色权限 - 操作类型通过 action 字段区分 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - action = request.data.get('action') # delete_role, add_permission, remove_permission - if not username or not action: - return Response({'code': 400, 'msg': '缺少必要参数'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权进行此操作'}) - - # 获取角色(需要角色名称) - role_code = request.data.get('role_code') - if not role_code: - return Response({'code': 400, 'msg': '缺少角色编码'}) - - try: - role = Role.objects.get(RoleName=role_code) - except Role.DoesNotExist: - return Response({'code': 404, 'msg': '角色不存在'}) - - # 超级管理员角色的特殊限制 - if role.RoleName == '管理员': - if action in ['delete_role']: - return Response({'code': 403, 'msg': '超级管理员角色不能删除'}) - - # 根据 action 处理 - if action == 'delete_role': - # 删除角色:同时删除角色权限关联、用户角色关联 - with transaction.atomic(): - RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete() - UserRole.objects.filter(RoleUUID=role.RoleUUID).delete() - role.delete() - return Response({'code': 0, 'msg': '角色删除成功'}) - - elif action == 'add_permission': - perm_code = request.data.get('perm_code') - if not perm_code: - return Response({'code': 400, 'msg': '缺少权限编码'}) - if role.RoleName == '管理员' and perm_code == '000001': - return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'}) - try: - perm = Permission.objects.get(PermCode=perm_code) - except Permission.DoesNotExist: - return Response({'code': 404, 'msg': '权限不存在'}) - # 检查是否已存在 - if RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).exists(): - return Response({'code': 400, 'msg': '该角色已拥有此权限'}) - RolePermission.objects.create( - RolePermUUID=uuid.uuid4().bytes, - RoleUUID=role.RoleUUID, - PermUUID=perm.PermUUID, - CreateTime=timezone.now(), - ) - return Response({'code': 0, 'msg': '权限添加成功'}) - - elif action == 'remove_permission': - perm_code = request.data.get('perm_code') - if not perm_code: - return Response({'code': 400, 'msg': '缺少权限编码'}) - if role.RoleName == '管理员' and perm_code == '000001': - return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'}) - try: - perm = Permission.objects.get(PermCode=perm_code) - except Permission.DoesNotExist: - return Response({'code': 404, 'msg': '权限不存在'}) - deleted, _ = RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).delete() - if deleted == 0: - return Response({'code': 400, 'msg': '该角色未拥有此权限'}) - return Response({'code': 0, 'msg': '权限移除成功'}) - - else: - return Response({'code': 400, 'msg': '无效的 action'}) - - -class AddRoleView(APIView): - """ - 添加角色:自动生成角色编码,前端提供角色名称、描述、选择的权限列表 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - role_name = request.data.get('role_name', '').strip() - description = request.data.get('description', '').strip() - perm_codes = request.data.get('perm_codes', []) # 列表 - - if not username or not role_name: - return Response({'code': 400, 'msg': '缺少必要参数'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权进行此操作'}) - - # 检查角色名称是否已存在 - if Role.objects.filter(RoleName=role_name).exists(): - return Response({'code': 400, 'msg': '角色名称已存在'}) - - # 生成唯一角色编码(格式:ROLE_ + 8位随机字母数字) - role_code = f"ROLE_{''.join(random.choices(string.ascii_uppercase + string.digits, k=8))}" - - # 创建角色 - with transaction.atomic(): - role = Role.objects.create( - RoleUUID=uuid.uuid4().bytes, - TenantUUID=b'\x00' * 16, - RoleName=role_name, - RoleType=2, - RoleStatus=1, - AssignScope=0, - RoleDesc=description, - CreateTime=timezone.now(), - ) - # 绑定权限(过滤掉不存在的权限编码) - for pc in perm_codes: - # 不允许添加 000001 权限(此权限仅供超级管理员) - if pc == '000001': - continue - try: - perm = Permission.objects.get(PermCode=pc) - RolePermission.objects.create( - RolePermUUID=uuid.uuid4().bytes, - RoleUUID=role.RoleUUID, - PermUUID=perm.PermUUID, - CreateTime=timezone.now(), - ) - except Permission.DoesNotExist: - logger.warning(f"添加角色时忽略不存在的权限编码: {pc}") - - return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}}) - - - -class GetAdminUserListView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权访问此页面'}) - - phone = request.data.get('phone', '').strip() - nicheng = request.data.get('nicheng', '').strip() - role_code = request.data.get('roleCode', '').strip() - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('pageSize', 20)) - - # UserType 是 @property 非数据库字段,不能用 filter; - # KefuProfile__isnull=False 已确保只查到客服用户 - users = User.query.filter( - KefuProfile__isnull=False - ).select_related('KefuProfile') - - if phone: - users = users.filter(Phone__icontains=phone) - if nicheng: - users = users.filter(KefuProfile__nicheng__icontains=nicheng) - - # 关键修正:先求值为列表,再传入 phone__in - if role_code: - target_role = Role.objects.filter(RoleName=role_code).first() - if target_role: - user_uuids = list( - UserRole.objects.filter(RoleUUID=target_role.RoleUUID) - .values_list('UserUUID', flat=True) - ) - users = users.filter(UserUUID__in=user_uuids) - else: - users = users.none() - - users = users.order_by('-UserCreateTime') - paginator = Paginator(users, page_size) - page_obj = paginator.get_page(page) - - data_list = [] - for user in page_obj: - # 通过 gvsdsdk UserRole 获取用户角色 - user_role_uuids = list( - UserRole.objects.filter(UserUUID=user.UserUUID) - .values_list('RoleUUID', flat=True) - ) - user_roles = Role.objects.filter(RoleUUID__in=user_role_uuids) - roles_data = [ - {'role_code': r.RoleName, 'role_name': r.RoleName} - for r in user_roles - ] - data_list.append({ - 'phone': user.Phone or '', - 'yonghuid': user.UserUID or '', - 'nicheng': user.KefuProfile.nicheng, - 'roles': roles_data, - 'status': user.KefuProfile.zhuangtai, - 'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '', - 'update_time': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '', - 'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '', - 'UpdateTime': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '', - }) - - return Response({ - 'code': 0, - 'data': { - 'list': data_list, - 'total': paginator.count, - 'page': page, - 'pageSize': page_size - } - }) - - except Exception as e: - logger.error(f"GetAdminUserListView 异常: {e}\n{traceback.format_exc()}") - return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}) - - - -class GetAdminRolesView(APIView): - """获取所有角色(用于筛选和分配)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权访问此页面'}) - - roles = Role.objects.filter(RoleStatus=1).values('RoleName') - return Response({ - 'code': 0, - 'data': { - 'roles': [{'role_code': r['RoleName'], 'role_name': r['RoleName']} for r in roles] - } - }) - - - - - - -'''class GetAdminUserListView(APIView): - """获取后台用户列表(支持分页、筛选)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权访问此页面'}) - - # 筛选条件 - phone = request.data.get('phone', '').strip() - nicheng = request.data.get('nicheng', '').strip() - role_code = request.data.get('roleCode', '').strip() - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('pageSize', 20)) - - # 基础查询:存在客服扩展表 - users = User.query.filter(KefuProfile__isnull=False).select_related('KefuProfile') - if phone: - users = users.filter(Phone__icontains=phone) - if nicheng: - users = users.filter(KefuProfile__nicheng__icontains=nicheng) - if role_code: - # 筛选拥有该角色的用户 - user_ids_with_role = UserRole.query.filter(role__role_code=role_code).values_list('account_id', flat=True) - users = users.filter(phone__in=user_ids_with_role) - - # 排序 - users = users.order_by('-CreateTime') - - # 分页 - paginator = Paginator(users, page_size) - page_obj = paginator.get_page(page) - - # 构建返回数据 - data_list = [] - for user in page_obj: - # 获取用户的所有角色 - user_roles = UserRole.query.filter(account_id=user.Phone).select_related('role') - roles_data = [{'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles] - data_list.append({ - 'phone': user.Phone, - 'nicheng': user.KefuProfile.nicheng, - 'roles': roles_data, - 'status': user.KefuProfile.zhuangtai, - 'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S'), - 'UpdateTime': user.UpdateTime.strftime('%Y-%m-%d %H:%M:%S'), - }) - - return Response({ - 'code': 0, - 'data': { - 'list': data_list, - 'total': paginator.count, - 'page': page, - 'pageSize': page_size - } - })''' - - - - - -class ModifyAdminUserView(APIView): - """修改后台用户信息(昵称、密码、二级密码、状态、角色)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - action = request.data.get('action') - if not username or not action: - return Response({'code': 400, 'msg': '缺少参数'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权进行此操作'}) - - target_phone = request.data.get('phone', '').strip() - if not target_phone: - return Response({'code': 400, 'msg': '缺少目标用户账号'}) - - # Phone 非唯一,同一手机号可能对应多个微信用户;后台操作只定位客服账号 - target_user = User.query.filter( - Phone=target_phone, - KefuProfile__isnull=False, - ).first() - if not target_user: - return Response({'code': 404, 'msg': '用户不存在或非后台账号'}) - try: - target_kefu = target_user.KefuProfile - except UserKefu.DoesNotExist: - return Response({'code': 404, 'msg': '用户扩展信息缺失'}) - - with transaction.atomic(): - if action == 'update_user_info': - # 修改昵称、状态、密码、二级密码 - nicheng = request.data.get('nicheng') - status = request.data.get('status') - password = request.data.get('password') - erjimima = request.data.get('erjimima') - if nicheng is not None: - target_kefu.nicheng = nicheng - if status is not None: - target_kefu.zhuangtai = int(status) - if password: - if len(password) < 6: - return Response({'code': 400, 'msg': '密码至少6位'}) - target_user.SetPassword(password) - if erjimima: - if len(erjimima) < 6: - return Response({'code': 400, 'msg': '二级密码至少6位'}) - import bcrypt as _bcrypt - target_kefu.erjimima = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8') - target_user.save() - target_kefu.save() - return Response({'code': 0, 'msg': '修改成功'}) - - elif action == 'add_user_roles': - role_codes = request.data.get('role_codes', []) - if not role_codes: - return Response({'code': 400, 'msg': '请选择角色'}) - for rc in role_codes: - try: - role = Role.objects.get(RoleName=rc) - except Role.DoesNotExist: - continue - UserRole.objects.get_or_create( - UserRoleUUID=uuid.uuid4().bytes, - UserUUID=target_user.UserUUID, - RoleUUID=role.RoleUUID, - CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes, - ) - return Response({'code': 0, 'msg': '角色添加成功'}) - - elif action == 'remove_user_role': - role_code = request.data.get('role_code') - if not role_code: - return Response({'code': 400, 'msg': '缺少角色编码'}) - try: - role = Role.objects.get(RoleName=role_code) - except Role.DoesNotExist: - return Response({'code': 404, 'msg': '角色不存在'}) - UserRole.objects.filter(UserUUID=target_user.UserUUID, RoleUUID=role.RoleUUID).delete() - return Response({'code': 0, 'msg': '角色移除成功'}) - - else: - return Response({'code': 400, 'msg': '无效的action'}) - - - - - -class AddAdminUserView(APIView): - """添加后台用户(客服账号)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions: - return Response({'code': 403, 'msg': '您无权进行此操作'}) - - phone = request.data.get('phone', '').strip() - password = request.data.get('password', '').strip() - erjimima = request.data.get('erjimima', '').strip() - nicheng = request.data.get('nicheng', '').strip() - role_codes = request.data.get('role_codes', []) - - if not all([phone, password, erjimima, nicheng]): - return Response({'code': 400, 'msg': '请填写完整信息'}) - if len(password) < 6 or len(erjimima) < 6: - return Response({'code': 400, 'msg': '密码和二级密码至少6位'}) - - # 校验手机号是否已被客服占用 - if User.query.filter(Phone=phone, KefuProfile__isnull=False).exists(): - return Response({'code': 400, 'msg': '该账号已存在'}) - - # 生成唯一的 yonghuid(7位数字) - while True: - yonghuid = str(random.randint(1000000, 9999999)) - if not User.query.filter(UserUID=yonghuid).exists(): - break - - # 生成唯一的 openid(格式:admin_{时间戳}_{随机6位}) - timestamp = str(int(time.time() * 1000)) - random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) - openid = f"admin_{timestamp}_{random_suffix}" - while User.query.filter(OpenID=openid).exists(): - random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) - openid = f"admin_{timestamp}_{random_suffix}" - - with transaction.atomic(): - user_main = User.query.create( - UserUID=yonghuid, - UserName=openid, - OpenID=openid, - Phone=phone, - ) - user_main.SetPassword(password) - user_main.save(update_fields=['UserPassword']) - # 创建客服扩展表(后台用户扩展) - import bcrypt as _bcrypt - erjimima_hashed = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8') - UserKefu.query.create( - user=user_main, - nicheng=nicheng, - erjimima=erjimima_hashed, - zhuangtai=1 - ) - # 分配客服角色 - kefu_role = Role.objects.filter(RoleName='客服').first() - if kefu_role: - UserRole.objects.create( - UserRoleUUID=uuid.uuid4().bytes, - UserUUID=user_main.UserUUID, - RoleUUID=kefu_role.RoleUUID, - CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes, - ) - # 分配额外角色 - for rc in role_codes: - try: - role = Role.objects.get(RoleName=rc) - UserRole.objects.create( - UserRoleUUID=uuid.uuid4().bytes, - UserUUID=user_main.UserUUID, - RoleUUID=role.RoleUUID, - CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes, - ) - except Role.DoesNotExist: - pass - - return Response({'code': 0, 'msg': '添加成功'}) - - - - - - - - -class KefuGetDashouListView(APIView): - """ - 客服获取打手列表接口(仅打手类型) - 需要拥有以下任一权限:001aa,001bb,001cc,001dd,001ee,001ff,001gg - 请求:POST /yonghu/kefuhqdslb - 参数: - username: 客服手机号(用于权限校验) - keyword: 搜索关键词(可选,搜索用户ID或昵称) - page: 页码(从1开始) - page_size: 每页数量 - 返回: - code: 0 - data: { - list: [打手对象], - has_more: bool, - total: int, // 总打手数 - valid_dashou_count: int // 有效打手数(有有效会员) - } - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数(前端字段为 username) - username = request.data.get('username', '').strip() - keyword = request.data.get('keyword', '').strip() - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - except ValueError: - return Response({'code': 400, 'msg': '分页参数错误'}) - - if not username: - return Response({'code': 401, 'msg': '认证失败'}) - - # 2. 公共权限校验(验证客服身份,获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查是否拥有打手管理相关权限(至少一个) - required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看打手列表'}) - - # 4. 构建打手查询集(仅打手类型) - dashou_qs = filter_user_related_by_club( - UserDashou.query.select_related('user').all(), request, - ) - - # 关键词筛选(用户ID或昵称) - if keyword: - dashou_qs = dashou_qs.filter( - Q(user__UserUID__icontains=keyword) | - Q(nicheng__icontains=keyword) - ) - - # 5. 统计打手总数 - total_dashou = dashou_qs.count() - - # 6. 统计有效打手数(有会员且未过期) - dashou_user_ids = dashou_qs.values_list('user__UserUID', flat=True) - now = timezone.now() - hy_qs = filter_queryset_by_club(Huiyuangoumai.query.all(), request) - valid_dashou_count = hy_qs.filter( - yonghu_id__in=dashou_user_ids, - huiyuan_zhuangtai=1, - daoqi_time__gt=now, - ).values('yonghu_id').distinct().count() - - # 7. 分页 - offset = (page - 1) * page_size - dashou_list = dashou_qs.order_by('-CreateTime')[offset:offset + page_size] - - # 8. 构建返回数据 - result = [] - for dashou in dashou_list: - user = dashou.user - result.append({ - 'yonghuid': user.UserUID, - 'avatar': user.Avatar or '', - 'zaixianzhuangtai': dashou.zaixianzhuangtai, - 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, - 'nicheng': dashou.nicheng or '', - 'zhuangtai': dashou.zhuangtai, - }) - - has_more = (offset + len(dashou_list)) < total_dashou - - meta = list_response_meta(request) - return Response({ - 'code': 0, - 'club_id': meta['club_id'], - 'scope': meta['scope'], - 'data': { - 'list': result, - 'has_more': has_more, - 'total': total_dashou, - 'valid_dashou_count': valid_dashou_count, - } - }) - - - -class KefuGetDashouDetailView(APIView): - """ - 客服获取打手详情接口 - 请求:POST /yonghu/kefuhqdsxq - 参数:{ - "username": "客服手机号", - "uid": "打手ID (yonghuid)" - } - 认证:JWT - 返回:code=0 + 打手信息 + 会员列表 + 所有会员类型 + 客服权限列表 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - username = request.data.get('phone', '').strip() - uid = request.data.get('uid', '').strip() - - if not username or not uid: - return Response({'code': 400, 'msg': '缺少参数'}) - - # 2. 公共权限校验(验证客服身份,获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 3. 检查是否拥有打手管理相关权限(至少一个) - required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看打手详情'}) - - # 4. 查询打手主表和扩展表 - try: - user_main = User.query.select_related('DashouProfile').get( - UserUID=uid - ) - except User.DoesNotExist: - return Response({'code': 404, 'msg': '打手不存在'}) - - denied = forbid_if_user_out_of_scope(request, user_main) - if denied: - return denied - - try: - dashou = user_main.DashouProfile - except ObjectDoesNotExist: - # 用户存在但未注册打手身份,返回基本信息 - user_info = { - 'yonghuid': user_main.UserUID, - 'avatar': user_main.Avatar or '', - 'zaixianzhuangtai': 0, - 'zhanghaozhuangtai': 0, - 'zhuangtai': 0, - 'nicheng': '', - 'chenghao': '', - 'dianhua': '', - 'wechat': '', - 'phone': user_main.Phone or '', - 'CreateTime': user_main.UserCreateTime, - 'ip': user_main.IP or '', - 'jiedanzongliang': 0, - 'chengjiaozongliang': 0, - 'tuikuanliang': 0, - 'jinrijiedan': 0, - 'yue': 0.00, - 'zonge': 0.00, - 'jifen': 0, - 'yajin': 0.00, - 'jinrishouyi': 0.00, - 'jinyueshouyi': 0.00, - 'yaoqingren': '', - 'jieshao': '', - } - member_list = [] - # 仍然需要返回 all_members 和 permissions - all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao')) - return Response({ - 'code': 0, - 'data': { - 'user_info': user_info, - 'member_list': member_list, - 'all_members': all_members, - 'permissions': permissions, - } - }) - - # 5. 构建打手信息 - user_info = { - 'yonghuid': user_main.UserUID, - 'avatar': user_main.Avatar or '', - 'zaixianzhuangtai': dashou.zaixianzhuangtai, - 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, - 'zhuangtai': dashou.zhuangtai, - 'nicheng': dashou.nicheng or '', - 'chenghao': dashou.chenghao or '', - 'dianhua': dashou.dianhua or '', - 'wechat': dashou.wechat or '', - 'phone': user_main.Phone or '', - 'CreateTime': user_main.UserCreateTime, - 'ip': user_main.IP or '', - 'jiedanzongliang': dashou.jiedanzongliang, - 'chengjiaozongliang': dashou.chengjiaozongliang, - 'tuikuanliang': dashou.tuikuanliang, - 'jinrijiedan': dashou.jinrijiedan, - 'yue': float(dashou.yue) if dashou.yue else 0.00, - 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, - 'jifen': dashou.jifen, - 'yajin': float(dashou.yajin) if dashou.yajin else 0.00, - 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00, - 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00, - 'yaoqingren': dashou.yaoqingren or '', - 'jieshao': dashou.jieshao or '', - } - - # 6. 查询打手已拥有的会员(未过期) - member_records = Huiyuangoumai.query.filter( - yonghu_id=uid - ).order_by('-CreateTime') - member_list = [] - for record in member_records: - # 检查是否过期 - if hasattr(record, 'jiance_shifou_daoqi'): - is_expired = record.jiance_shifou_daoqi() - else: - is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True - - if not is_expired: - try: - huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id) - huiyuan_name = huiyuan.jieshao - except Huiyuan.DoesNotExist: - huiyuan_name = '未知会员' - member_list.append({ - 'id': record.id, - 'huiyuan_name': huiyuan_name, - 'daoqi_time': record.daoqi_time, - 'is_active': record.huiyuan_zhuangtai == 1, - 'huiyuan_id': record.huiyuan_id, # 用于删除操作 - }) - - # 7. 查询所有会员类型(用于前端添加会员) - all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao')) - - return Response({ - 'code': 0, - 'data': { - 'user_info': user_info, - 'member_list': member_list, - 'all_members': all_members, - 'permissions': permissions, # 返回当前客服的权限列表 - } - }) - - - -class KefuUpdateDashouView(APIView): - """ - 客服修改打手信息接口 - 支持操作: - - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb) - - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb) - - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd) - - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee) - - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee) - - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee) - - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg) - - gai_chenghao : 修改打手称号(升级/降级金牌) (权限001hh) - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取前端参数 - username = request.data.get('username', '').strip() - dashou_id = request.data.get('dashou_id', '').strip() - caozuo = request.data.get('caozuo', '').strip() - value = request.data.get('value') # 金额、积分、天数、称号等 - huiyuan_id = request.data.get('huiyuan_id', '').strip() - - if not username or not dashou_id or not caozuo: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 3. 查询打手 - try: - dashou_user = User.query.get(UserUID=dashou_id) - dashou_profile = dashou_user.DashouProfile - except User.DoesNotExist: - return Response({'code': 404, 'msg': '打手不存在'}) - except UserDashou.DoesNotExist: - return Response({'code': 404, 'msg': '打手扩展信息缺失'}) - - with transaction.atomic(): - try: - before = { - 'yue': dashou_profile.yue, - 'yajin': dashou_profile.yajin, - 'jifen': dashou_profile.jifen, - 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai, - 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, - 'zhuangtai': dashou_profile.zhuangtai, - 'chenghao': dashou_profile.chenghao, - } - - # ---------- 余额操作 ---------- - if caozuo == 'jian_yue': - if '001aa' not in permissions: - return Response({'code': 403, 'msg': '无权限减打手余额'}) - amount = Decimal(str(value)) - if dashou_profile.yue < amount: - return Response({'code': 400, 'msg': '余额不足'}) - dashou_profile.yue -= amount - dashou_profile.save() - after = dashou_profile.yue - write_xiugai_log( - yonghuid=dashou_id, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_DASHOU, - xiugaitijiao=after, - xiugaitijiaoq=before['yue'], - qitashuoming=( - f'【后台】减少打手可提现余额:{before["yue"]}元 → {after}元,' - f'减少{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after)}}) - - elif caozuo == 'jia_yue': - if '001bb' not in permissions: - return Response({'code': 403, 'msg': '无权限加打手余额'}) - amount = Decimal(str(value)) - dashou_profile.yue += amount - dashou_profile.save() - after = dashou_profile.yue - write_xiugai_log( - yonghuid=dashou_id, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_DASHOU, - xiugaitijiao=after, - xiugaitijiaoq=before['yue'], - qitashuoming=( - f'【后台】增加打手可提现余额:{before["yue"]}元 → {after}元,' - f'增加{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after)}}) - - # ---------- 押金操作 ---------- - elif caozuo == 'jian_yajin': - if '001aa' not in permissions: - return Response({'code': 403, 'msg': '无权限减打手押金'}) - amount = Decimal(str(value)) - if dashou_profile.yajin < amount: - return Response({'code': 400, 'msg': '押金不足'}) - dashou_profile.yajin -= amount - dashou_profile.save() - after = dashou_profile.yajin - write_xiugai_log( - yonghuid=dashou_id, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_DASHOU, - yajin=after, - yyajin=before['yajin'], - qitashuoming=( - f'【后台】减少打手押金:{before["yajin"]}元 → {after}元,' - f'减少{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after)}}) - - elif caozuo == 'jia_yajin': - if '001bb' not in permissions: - return Response({'code': 403, 'msg': '无权限加打手押金'}) - amount = Decimal(str(value)) - dashou_profile.yajin += amount - dashou_profile.save() - after = dashou_profile.yajin - write_xiugai_log( - yonghuid=dashou_id, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_DASHOU, - yajin=after, - yyajin=before['yajin'], - qitashuoming=( - f'【后台】增加打手押金:{before["yajin"]}元 → {after}元,' - f'增加{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after)}}) - - # ---------- 积分操作 ---------- - elif caozuo == 'jian_jifen': - if '001cc' not in permissions: - return Response({'code': 403, 'msg': '无权限减打手积分'}) - jifen = int(value) - if dashou_profile.jifen < jifen: - return Response({'code': 400, 'msg': '积分不足'}) - dashou_profile.jifen -= jifen - dashou_profile.save() - after = dashou_profile.jifen - write_xiugai_log( - yonghuid=dashou_id, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_DASHOU, - jifen=after, - yjifen=before['jifen'], - qitashuoming=( - f'【后台】减少打手积分:{before["jifen"]} → {after},' - f'减少{jifen}分,打手ID={dashou_id},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after}}) - - elif caozuo == 'jia_jifen': - if '001dd' not in permissions: - return Response({'code': 403, 'msg': '无权限加打手积分'}) - jifen = int(value) - dashou_profile.jifen += jifen - dashou_profile.save() - after = dashou_profile.jifen - write_xiugai_log( - yonghuid=dashou_id, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_DASHOU, - jifen=after, - yjifen=before['jifen'], - qitashuoming=( - f'【后台】增加打手积分:{before["jifen"]} → {after},' - f'增加{jifen}分,打手ID={dashou_id},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after}}) - - # ---------- 状态修改 ---------- - elif caozuo == 'gai_zhanghaozhuangtai': - if '001ee' not in permissions: - return Response({'code': 403, 'msg': '无权限修改打手账号状态'}) - new_status = int(value) - if new_status not in [0, 1]: - return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'}) - dashou_profile.zhanghaozhuangtai = new_status - dashou_profile.save() - status_label = '正常' if new_status == 1 else '封禁' - old_label = '正常' if before['zhanghaozhuangtai'] == 1 else '封禁' - write_xiugai_log( - yonghuid=dashou_id, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_DASHOU, - qitashuoming=( - f'【后台】修改打手账号状态:{old_label} → {status_label},' - f'打手ID={dashou_id},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}}) - - elif caozuo == 'gai_zaixianzhuangtai': - if '001ee' not in permissions: - return Response({'code': 403, 'msg': '无权限修改打手在线状态'}) - new_status = int(value) - if new_status not in [0, 1]: - return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'}) - dashou_profile.zaixianzhuangtai = new_status - dashou_profile.save() - status_label = '在线' if new_status == 1 else '离线' - old_label = '在线' if before['zaixianzhuangtai'] == 1 else '离线' - write_xiugai_log( - yonghuid=dashou_id, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_DASHOU, - qitashuoming=( - f'【后台】修改打手在线状态:{old_label} → {status_label},' - f'打手ID={dashou_id},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}}) - - elif caozuo == 'gai_zhuangtai': - if '001ee' not in permissions: - return Response({'code': 403, 'msg': '无权限修改打手接单状态'}) - new_status = int(value) - if new_status not in [0, 1]: - return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'}) - dashou_profile.zhuangtai = new_status - dashou_profile.save() - status_label = '空闲' if new_status == 1 else '忙碌' - old_label = '空闲' if before['zhuangtai'] == 1 else '忙碌' - write_xiugai_log( - yonghuid=dashou_id, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_DASHOU, - qitashuoming=( - f'【后台】修改打手接单状态:{old_label} → {status_label},' - f'打手ID={dashou_id},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}}) - - # ---------- 会员操作 ---------- - elif caozuo == 'jia_huiyuan': - if '001ff' not in permissions: - return Response({'code': 403, 'msg': '无权限给打手加会员'}) - if not huiyuan_id: - return Response({'code': 400, 'msg': '缺少会员ID'}) - days = int(value) if value else 30 - try: - huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) - except Huiyuan.DoesNotExist: - return Response({'code': 404, 'msg': '会员类型不存在'}) - record, created = Huiyuangoumai.objects.select_for_update().get_or_create( - yonghu_id=dashou_id, - huiyuan_id=huiyuan_id, - defaults={ - 'huiyuan_zhuangtai': 1, - 'jieshao': huiyuan.jieshao, - 'daoqi_time': timezone.now() + timedelta(days=days), - } - ) - if not created: - new_daoqi = record.daoqi_time + timedelta(days=days) - record.daoqi_time = new_daoqi - record.huiyuan_zhuangtai = 1 - record.save() - write_xiugai_log( - yonghuid=dashou_id, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_DASHOU, - huiyuants=days, - huiyuan_id=huiyuan_id, - qitashuoming=( - f'【后台】给打手添加会员:会员ID={huiyuan_id}({huiyuan.jieshao}),' - f'增加{days}天,到期时间={record.daoqi_time.strftime("%Y-%m-%d %H:%M:%S")},' - f'打手ID={dashou_id},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}}) - - elif caozuo == 'jian_huiyuan': - if '001gg' not in permissions: - return Response({'code': 403, 'msg': '无权限给打手减会员'}) - if not huiyuan_id: - return Response({'code': 400, 'msg': '缺少会员ID'}) - try: - record = Huiyuangoumai.query.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id) - except Huiyuangoumai.DoesNotExist: - return Response({'code': 404, 'msg': '该打手未购买此会员'}) - record.delete() - write_xiugai_log( - yonghuid=dashou_id, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_DASHOU, - huiyuan_id=huiyuan_id, - qitashuoming=( - f'【后台】移除打手会员:会员ID={huiyuan_id},' - f'打手ID={dashou_id},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '减会员成功'}) - - # ---------- 称号修改(金牌打手) ---------- - elif caozuo == 'gai_chenghao': - if '001hh' not in permissions: - return Response({'code': 403, 'msg': '无权限修改打手称号'}) - new_chenghao = str(value).strip() - if not new_chenghao: - return Response({'code': 400, 'msg': '称号不能为空'}) - # 可限制合法值,这里只记录日志 - dashou_profile.chenghao = new_chenghao - dashou_profile.save() - write_xiugai_log( - yonghuid=dashou_id, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_DASHOU, - qitashuoming=( - f'【后台】修改打手称号:{before["chenghao"] or "无"} → {new_chenghao},' - f'打手ID={dashou_id},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '修改称号成功', 'data': {'new_chenghao': new_chenghao}}) - - else: - return Response({'code': 400, 'msg': '未知操作类型'}) - - except Exception as e: - logger.error(f"修改打手信息异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - -class KefuGetShangjiaListView(APIView): - """ - 客服/管理员获取商家列表接口 - 需要拥有以下任一权限:1100a, 1100b - 请求:POST /houtai/hqsjgllb - 参数: - username: 客服手机号(用于权限校验,必填) - keyword: 搜索关键词(可选,搜索商家用户ID或昵称) - balance_op: 余额比较符,gte(>=) / lte(<=)(可选) - balance_value: 余额数值(可选,需与balance_op同时存在) - order_count_op: 派单量比较符,gte(>=) / lte(<=)(可选) - order_count_value: 派单量数值(可选,需与order_count_op同时存在) - status: 商家状态,1=正常,2=禁用(可选) - page: 页码,从1开始,默认1 - page_size: 每页数量,默认20 - 返回: - code: 0 成功,其他失败 - data: { - list: [商家对象], - total: int, // 总商家数 - } - """ - permission_classes = [] # 不依赖全局认证,手动校验 - parser_classes = [JSONParser] - - def post(self, request): - # 1. 获取参数 - username = request.data.get('username', '').strip() - keyword = request.data.get('keyword', '').strip() - balance_op = request.data.get('balance_op', '').strip().lower() - balance_value_str = request.data.get('balance_value') - order_count_op = request.data.get('order_count_op', '').strip().lower() - order_count_value_str = request.data.get('order_count_value') - status_str = request.data.get('status') - - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - if page < 1 or page_size < 1: - raise ValueError - except ValueError: - return Response({'code': 400, 'msg': '分页参数错误'}) - - if not username: - return Response({'code': 401, 'msg': '认证失败,缺少username'}) - - # 2. 公共权限校验(复用现有函数 verify_kefu_permission) - # 假设 verify_kefu_permission 定义在某个公共模块,这里引入 - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 直接返回错误响应 - - # 3. 检查商家管理权限(1100a 或 1100b) - required_perms = {'1100a', '1100b'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看商家列表'}) - - # 4. 构建商家查询集(商家扩展表 + 关联用户主表) - shangjia_qs = filter_user_related_by_club( - UserShangjia.query.select_related('user').all(), request, - ) - - # 关键词筛选(用户ID或昵称) - if keyword: - shangjia_qs = shangjia_qs.filter( - Q(user__UserUID__icontains=keyword) | - Q(nicheng__icontains=keyword) - ) - - # 余额筛选 - if balance_op and balance_value_str is not None and balance_value_str != '': - try: - balance_value = Decimal(str(balance_value_str)) - if balance_op == 'gte': - shangjia_qs = shangjia_qs.filter(yue__gte=balance_value) - elif balance_op == 'lte': - shangjia_qs = shangjia_qs.filter(yue__lte=balance_value) - else: - return Response({'code': 400, 'msg': '余额比较符只支持gte或lte'}) - except Exception: - return Response({'code': 400, 'msg': '余额数值格式错误'}) - - # 派单总量筛选(fabu字段) - if order_count_op and order_count_value_str is not None and order_count_value_str != '': - try: - order_count_value = int(order_count_value_str) - if order_count_op == 'gte': - shangjia_qs = shangjia_qs.filter(fabu__gte=order_count_value) - elif order_count_op == 'lte': - shangjia_qs = shangjia_qs.filter(fabu__lte=order_count_value) - else: - return Response({'code': 400, 'msg': '派单量比较符只支持gte或lte'}) - except Exception: - return Response({'code': 400, 'msg': '派单量数值必须为整数'}) - - # 商家状态筛选 - if status_str is not None and status_str != '': - try: - status_val = int(status_str) - if status_val not in [1, 2]: - return Response({'code': 400, 'msg': '商家状态值无效,应为1或2'}) - shangjia_qs = shangjia_qs.filter(zhuangtai=status_val) - except ValueError: - return Response({'code': 400, 'msg': '商家状态格式错误'}) - - # 5. 统计总商家数(应用筛选后) - total = shangjia_qs.count() - valid_merchant_count = shangjia_qs.filter(zhuangtai=1).count() - - # 6. 分页 - offset = (page - 1) * page_size - shangjia_list = shangjia_qs.order_by('-CreateTime')[offset:offset + page_size] - - # 7. 构建返回数据 - result = [] - for shangjia in shangjia_list: - user = shangjia.user - result.append({ - 'yonghuid': user.UserUID, - 'avatar': user.Avatar or '', - 'nicheng': shangjia.nicheng or '', - 'yue': str(shangjia.yue), # 转换为字符串,避免前端精度问题 - 'fabu': shangjia.fabu, - 'chengjiao': shangjia.chengjiao, - 'zhuangtai': shangjia.zhuangtai, # 1=正常,2=禁用 - }) - - meta = list_response_meta(request) - return Response({ - 'code': 0, - 'club_id': meta['club_id'], - 'scope': meta['scope'], - 'data': { - 'list': result, - 'total': total, - 'valid_merchant_count': valid_merchant_count, - } - }) - - - - - - -# yonghu/views.py 中添加 - -class KefuGetShangjiaDetailView(APIView): - """ - 客服/管理员获取商家详情接口 - 需要权限:1100a 或 1100b - 请求:POST /houtai/hthqsjxq - 参数: - username: 客服手机号(用于权限校验,必填) - yonghuid: 商家用户ID(必填) - 返回: - code: 0 成功 - data: 商家所有字段(含主表头像等) - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - - if not username: - return Response({'code': 401, 'msg': '认证失败,缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少商家ID'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - required_perms = {'1100a', '1100b'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看商家详情'}) - - # 查询商家 - try: - shangjia = UserShangjia.query.select_related('user').get(user__UserUID=yonghuid) - except UserShangjia.DoesNotExist: - return Response({'code': 404, 'msg': '商家不存在'}) - - denied = forbid_if_user_out_of_scope(request, shangjia.user) - if denied: - return denied - - user = shangjia.user - - data = { - 'yonghuid': user.UserUID, - 'avatar': user.Avatar or '', - 'nicheng': shangjia.nicheng, - 'zhuangtai': shangjia.zhuangtai, # 1正常,2禁用 - 'dianhua': shangjia.dianhua, - 'wechat': shangjia.wechat, - 'yue': str(shangjia.yue), - 'fabu': shangjia.fabu, - 'chengjiao': shangjia.chengjiao, - 'tuikuan': shangjia.tuikuan, - 'jinridingdan': shangjia.jinridingdan, - 'jinriliushui': str(shangjia.jinriliushui), - 'jinyuedingdan': shangjia.jinyuedingdan, - 'jinyueliushui': str(shangjia.jinyueliushui), - 'CreateTime': shangjia.CreateTime.isoformat() if shangjia.CreateTime else None, - } - - return Response({'code': 0, 'data': data}) - - - - - - - -class KefuUpdateShangjiaView(APIView): - """ - 客服/管理员修改商家信息(余额、状态) - POST /houtai/xgsjxx - 参数: - username, yonghuid, action - action可选:add_balance, sub_balance, ban, unban - amount(add_balance/sub_balance时必填) - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - action = request.data.get('action', '').strip().lower() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少商家ID'}) - if action not in ('add_balance', 'sub_balance', 'ban', 'unban'): - return Response({'code': 400, 'msg': 'action参数无效'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 操作权限校验 - if action in ('add_balance', 'ban', 'unban') and '1100a' not in permissions: - return Response({'code': 403, 'msg': '无权限执行此操作(需要1100a)'}) - if action == 'sub_balance' and '1100b' not in permissions: - return Response({'code': 403, 'msg': '无权限减少商家余额(需要1100b)'}) - - # 使用事务,select_for_update 必须在事务内 - with transaction.atomic(): - try: - shangjia = UserShangjia.objects.select_for_update().get(user__UserUID=yonghuid) - except UserShangjia.DoesNotExist: - return Response({'code': 404, 'msg': '商家不存在'}) - - # 增加余额 - if action == 'add_balance': - amount_str = request.data.get('amount') - try: - amount = Decimal(str(amount_str)) - if amount <= 0 or amount > 1000000: - return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) - except: - return Response({'code': 400, 'msg': '金额格式错误'}) - old_yue = shangjia.yue - new_yue = shangjia.yue + amount - if new_yue > 999999999.99: - return Response({'code': 400, 'msg': '增加后余额超出上限'}) - shangjia.yue = new_yue - shangjia.save(update_fields=['yue']) - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_SHANGJIA, - shangjiayue=new_yue, - shangjiayueq=old_yue, - qitashuoming=( - f'【后台】增加商家余额:{old_yue}元 → {new_yue}元,增加{amount}元,' - f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '余额增加成功', 'data': {'new_balance': str(shangjia.yue)}}) - - # 减少余额 - if action == 'sub_balance': - amount_str = request.data.get('amount') - try: - amount = Decimal(str(amount_str)) - if amount <= 0 or amount > 1000000: - return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) - except: - return Response({'code': 400, 'msg': '金额格式错误'}) - if shangjia.yue < amount: - return Response({'code': 400, 'msg': '余额不足'}) - old_yue = shangjia.yue - shangjia.yue = shangjia.yue - amount - shangjia.save(update_fields=['yue']) - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_SHANGJIA, - shangjiayue=shangjia.yue, - shangjiayueq=old_yue, - qitashuoming=( - f'【后台】减少商家余额:{old_yue}元 → {shangjia.yue}元,减少{amount}元,' - f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '余额减少成功', 'data': {'new_balance': str(shangjia.yue)}}) - - # 封禁 - if action == 'ban': - if shangjia.zhuangtai == 2: - return Response({'code': 400, 'msg': '商家已是封禁状态'}) - shangjia.zhuangtai = 2 - shangjia.save(update_fields=['zhuangtai']) - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_SHANGJIA, - qitashuoming=( - f'【后台】封禁商家账号:正常 → 禁用,' - f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '商家已封禁', 'data': {'zhuangtai': 2}}) - - # 解封 - if action == 'unban': - if shangjia.zhuangtai == 1: - return Response({'code': 400, 'msg': '商家已是正常状态'}) - shangjia.zhuangtai = 1 - shangjia.save(update_fields=['zhuangtai']) - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_SHANGJIA, - qitashuoming=( - f'【后台】解封商家账号:禁用 → 正常,' - f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' - ), - ) - return Response({'code': 0, 'msg': '商家已解封', 'data': {'zhuangtai': 1}}) - - - - - - - -class GetProductBaseDataView(APIView): - """ - 获取商品基础数据(类型、专区、会员) - 权限:2200a - POST /houtai/htsphqjcsx - 参数:username - 返回: - code:0, data: { - type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...], - zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...], - member_list: [ {huiyuan_id, jieshao, jiage}, ...] - } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 获取所有商品类型(按 paixu 升序) - type_qs = ShangpinLeixing.query.all().order_by('paixu') - type_list = [] - for t in type_qs: - # 🔥 修改:统计该类型下的公共商品数量(店铺为空且店铺商品类型为空) - product_count = Shangpin.query.filter( - leixing_id=t.id, - dianpu__isnull=True, # 所属店铺为空 - dianpu_leixing__isnull=True # 店铺商品类型为空 - ).count() - type_list.append({ - 'id': t.id, - 'jieshao': t.jieshao or '', - 'tupian_url': t.tupian_url or '', - 'paixu': t.paixu, - 'shenhezhuangtai': t.shenhezhuangtai, - 'product_count': product_count, - }) - - # 获取所有专区(按 paixu 升序) - zone_qs = ShangpinZhuanqu.query.all().order_by('paixu') - zone_list = [] - for z in zone_qs: - # 🔥 修改:统计该专区下的公共商品数量(店铺为空且店铺商品类型为空) - product_count = Shangpin.query.filter( - zhuanqu_id=z.id, - dianpu__isnull=True, # 所属店铺为空 - dianpu_leixing__isnull=True # 店铺商品类型为空 - ).count() - zone_list.append({ - 'id': z.id, - 'mingzi': z.mingzi or '', - 'leixing_id': z.leixing_id, - 'paixu': z.paixu, - 'shenhezhuangtai': z.shenhezhuangtai, - 'product_count': product_count, - }) - - # 获取所有会员(只需 ID、介绍、价格) - member_list = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage')) - - return Response({ - 'code': 0, - 'data': { - 'type_list': type_list, - 'zone_list': zone_list, - 'member_list': member_list, - } - }) - - - -class GetProductListView(APIView): - """ - 获取商品列表(支持筛选、排序) - 权限:2200a - POST /houtai/hthqsplb - 参数: - username: 必填 - leixing_id: 类型ID(可选) - zhuanqu_id: 专区ID(可选) - keyword: 搜索关键词(商品标题/介绍) - sales_op: 销量比较符 gte/lte - sales_value: 销量数值 - stock_op: 库存比较符 gte/lte - stock_value: 库存数值 - sort_field: 排序字段 zhenshi_xiaoliang / jiage - sort_order: asc/desc - page: 页码(默认1) - page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留) - 返回: - code:0, data: { list: [...], total } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 获取参数 - leixing_id = request.data.get('leixing_id') - zhuanqu_id = request.data.get('zhuanqu_id') - keyword = request.data.get('keyword', '').strip() - sales_op = request.data.get('sales_op', '').strip().lower() - sales_value = request.data.get('sales_value') - stock_op = request.data.get('stock_op', '').strip().lower() - stock_value = request.data.get('stock_value') - sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage - sort_order = request.data.get('sort_order', 'desc').lower() - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 1000)) - if page < 1 or page_size < 1: - raise ValueError - except: - return Response({'code': 400, 'msg': '分页参数错误'}) - - # 🔥 修改:构建查询集,增加公共商品筛选(店铺为空且店铺商品类型为空) - qs = Shangpin.query.filter( - dianpu__isnull=True, # 所属店铺为空 - dianpu_leixing__isnull=True # 店铺商品类型为空 - ) - - if leixing_id: - try: - leixing_id = int(leixing_id) - qs = qs.filter(leixing_id=leixing_id) - except: - pass - if zhuanqu_id: - try: - zhuanqu_id = int(zhuanqu_id) - qs = qs.filter(zhuanqu_id=zhuanqu_id) - except: - pass - if keyword: - qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword)) - - # 销量筛选 - if sales_op and sales_value is not None: - try: - sales_val = int(sales_value) - if sales_op == 'gte': - qs = qs.filter(zhenshi_xiaoliang__gte=sales_val) - elif sales_op == 'lte': - qs = qs.filter(zhenshi_xiaoliang__lte=sales_val) - except: - pass - - # 库存筛选 - if stock_op and stock_value is not None: - try: - stock_val = int(stock_value) - if stock_op == 'gte': - qs = qs.filter(kucun__gte=stock_val) - elif stock_op == 'lte': - qs = qs.filter(kucun__lte=stock_val) - except: - pass - - # 排序 - if sort_field in ('zhenshi_xiaoliang', 'jiage'): - if sort_order == 'asc': - qs = qs.order_by(sort_field) - else: - qs = qs.order_by(f'-{sort_field}') - else: - qs = qs.order_by('-CreateTime') # 默认按创建时间倒序 - - # 分页 - total = qs.count() - offset = (page - 1) * page_size - products = qs[offset:offset + page_size] - - # 构建返回数据 - product_list = [] - for p in products: - product_list.append({ - 'id': p.id, - 'biaoqian': p.biaoqian or '', - 'jiage': str(p.jiage), - 'kucun': p.kucun, - 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, - 'tupian_url': p.tupian_url or '', - 'jieshao': p.jieshao or '', - }) - - return Response({ - 'code': 0, - 'data': { - 'list': product_list, - 'total': total, - } - }) - - - - -'''class GetProductBaseDataView(APIView): - """ - 获取商品基础数据(类型、专区、会员) - 权限:2200a - POST /houtai/htsphqjcsx - 参数:username - 返回: - code:0, data: { - type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...], - zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...], - member_list: [ {huiyuan_id, jieshao, jiage}, ...] - } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 获取所有商品类型(按 paixu 升序) - type_qs = ShangpinLeixing.query.all().order_by('paixu') - type_list = [] - for t in type_qs: - # 统计该类型下的商品数量(忽略审核状态?通常统计所有商品,可根据需求调整) - product_count = Shangpin.query.filter(leixing_id=t.id).count() - type_list.append({ - 'id': t.id, - 'jieshao': t.jieshao or '', - 'tupian_url': t.tupian_url or '', - 'paixu': t.paixu, - 'shenhezhuangtai': t.shenhezhuangtai, - 'product_count': product_count, - }) - - # 获取所有专区(按 paixu 升序) - zone_qs = ShangpinZhuanqu.query.all().order_by('paixu') - zone_list = [] - for z in zone_qs: - product_count = Shangpin.query.filter(zhuanqu_id=z.id).count() - zone_list.append({ - 'id': z.id, - 'mingzi': z.mingzi or '', - 'leixing_id': z.leixing_id, - 'paixu': z.paixu, - 'shenhezhuangtai': z.shenhezhuangtai, - 'product_count': product_count, - }) - - # 获取所有会员(只需 ID、介绍、价格) - member_list = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage')) - - return Response({ - 'code': 0, - 'data': { - 'type_list': type_list, - 'zone_list': zone_list, - 'member_list': member_list, - } - }) - - -class GetProductListView(APIView): - """ - 获取商品列表(支持筛选、排序) - 权限:2200a - POST /houtai/hthqsplb - 参数: - username: 必填 - leixing_id: 类型ID(可选) - zhuanqu_id: 专区ID(可选) - keyword: 搜索关键词(商品标题/介绍) - sales_op: 销量比较符 gte/lte - sales_value: 销量数值 - stock_op: 库存比较符 gte/lte - stock_value: 库存数值 - sort_field: 排序字段 zhenshi_xiaoliang / jiage - sort_order: asc/desc - page: 页码(默认1) - page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留) - 返回: - code:0, data: { list: [...], total } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 获取参数 - leixing_id = request.data.get('leixing_id') - zhuanqu_id = request.data.get('zhuanqu_id') - keyword = request.data.get('keyword', '').strip() - sales_op = request.data.get('sales_op', '').strip().lower() - sales_value = request.data.get('sales_value') - stock_op = request.data.get('stock_op', '').strip().lower() - stock_value = request.data.get('stock_value') - sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage - sort_order = request.data.get('sort_order', 'desc').lower() - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 1000)) - if page < 1 or page_size < 1: - raise ValueError - except: - return Response({'code': 400, 'msg': '分页参数错误'}) - - # 构建查询集 - qs = Shangpin.query.all() - - if leixing_id: - try: - leixing_id = int(leixing_id) - qs = qs.filter(leixing_id=leixing_id) - except: - pass - if zhuanqu_id: - try: - zhuanqu_id = int(zhuanqu_id) - qs = qs.filter(zhuanqu_id=zhuanqu_id) - except: - pass - if keyword: - qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword)) - - # 销量筛选 - if sales_op and sales_value is not None: - try: - sales_val = int(sales_value) - if sales_op == 'gte': - qs = qs.filter(zhenshi_xiaoliang__gte=sales_val) - elif sales_op == 'lte': - qs = qs.filter(zhenshi_xiaoliang__lte=sales_val) - except: - pass - - # 库存筛选 - if stock_op and stock_value is not None: - try: - stock_val = int(stock_value) - if stock_op == 'gte': - qs = qs.filter(kucun__gte=stock_val) - elif stock_op == 'lte': - qs = qs.filter(kucun__lte=stock_val) - except: - pass - - # 排序 - if sort_field in ('zhenshi_xiaoliang', 'jiage'): - if sort_order == 'asc': - qs = qs.order_by(sort_field) - else: - qs = qs.order_by(f'-{sort_field}') - else: - qs = qs.order_by('-CreateTime') # 默认按创建时间倒序 - - # 分页 - total = qs.count() - offset = (page - 1) * page_size - products = qs[offset:offset + page_size] - - # 构建返回数据 - product_list = [] - for p in products: - product_list.append({ - 'id': p.id, - 'biaoqian': p.biaoqian or '', - 'jiage': str(p.jiage), - 'kucun': p.kucun, - 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, - 'tupian_url': p.tupian_url or '', - 'jieshao': p.jieshao or '', - }) - - return Response({ - 'code': 0, - 'data': { - 'list': product_list, - 'total': total, - } - })''' - - -class SaveProductOrderView(APIView): - """ - 保存商品类型或专区的排序 - 权限:2200a - POST /houtai/htsppaixu - 参数: - username: 必填 - type: 'leixing' 或 'zhuanqu' - leixing_id: 当 type='zhuanqu' 时必填(用于限定该类型下的专区) - items: [ {id, paixu}, ... ] # 每个项目的新排序值(从1开始) - 返回: - code:0 成功 - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - sort_type = request.data.get('type', '').strip().lower() - if sort_type not in ('leixing', 'zhuanqu'): - return Response({'code': 400, 'msg': 'type参数必须为leixing或zhuanqu'}) - - items = request.data.get('items') - if not isinstance(items, list) or len(items) == 0: - return Response({'code': 400, 'msg': 'items参数无效'}) - - # 校验每个 item 的格式 - for item in items: - if not isinstance(item, dict) or 'id' not in item or 'paixu' not in item: - return Response({'code': 400, 'msg': 'items内每项需包含id和paixu'}) - try: - item['id'] = int(item['id']) - item['paixu'] = int(item['paixu']) - if item['paixu'] < 1: - return Response({'code': 400, 'msg': 'paixu必须为正整数'}) - except: - return Response({'code': 400, 'msg': 'id或paixu格式错误'}) - - with transaction.atomic(): - if sort_type == 'leixing': - # 更新商品类型的排序 - for item in items: - ShangpinLeixing.query.filter(id=item['id']).update(paixu=item['paixu']) - else: # zhuanqu - leixing_id = request.data.get('leixing_id') - if not leixing_id: - return Response({'code': 400, 'msg': '更新专区排序时必须提供leixing_id'}) - try: - leixing_id = int(leixing_id) - except: - return Response({'code': 400, 'msg': 'leixing_id格式错误'}) - # 可选:检查该专区是否属于该类型,但不强制,由前端保证 - for item in items: - ShangpinZhuanqu.query.filter(id=item['id']).update(paixu=item['paixu']) - - return Response({'code': 0, 'msg': '排序保存成功'}) - - -class AddProductView(APIView): - """ - 添加商品(含图片上传) - 权限:2200a - POST /houtai/htfbsp - 参数:multipart/form-data,包含所有商品字段及图片文件 - 字段说明见代码 - """ - permission_classes = [] - parser_classes = [MultiPartParser, FormParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 获取表单字段 - biaoqian = request.data.get('biaoqian', '').strip() - jiage_str = request.data.get('jiage') - kucun_str = request.data.get('kucun') - duiwai_xiaoliang_str = request.data.get('duiwai_xiaoliang', '0') - leixing_id_str = request.data.get('leixing_id') - zhuanqu_id_str = request.data.get('zhuanqu_id') - yaoqiuleixing_str = request.data.get('yaoqiuleixing', '1') - huiyuan_id = request.data.get('huiyuan_id', '').strip() - yongjin_str = request.data.get('yongjin', '0') - fencheng_type = request.data.get('fencheng_type', 'default') - ewai_PlayerCommission_str = request.data.get('ewai_PlayerCommission', '0') - jieshao = request.data.get('jieshao', '').strip() - xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip() - # 图片文件 - image_file = request.FILES.get('file') # 前端上传时字段名为 'file' - - # 必填字段校验 - if not biaoqian: - return Response({'code': 400, 'msg': '商品标题不能为空'}) - try: - jiage = Decimal(jiage_str) - if jiage <= 0: - return Response({'code': 400, 'msg': '价格必须大于0'}) - except: - return Response({'code': 400, 'msg': '价格格式错误'}) - try: - kucun = int(kucun_str) - if kucun < 0: - return Response({'code': 400, 'msg': '库存不能为负数'}) - except: - return Response({'code': 400, 'msg': '库存格式错误'}) - try: - duiwai_xiaoliang = int(duiwai_xiaoliang_str) - except: - duiwai_xiaoliang = 0 - try: - leixing_id = int(leixing_id_str) - except: - return Response({'code': 400, 'msg': '商品类型ID无效'}) - try: - zhuanqu_id = int(zhuanqu_id_str) - except: - return Response({'code': 400, 'msg': '商品专区ID无效'}) - try: - yaoqiuleixing = int(yaoqiuleixing_str) - if yaoqiuleixing not in (1, 2): - raise ValueError - except: - return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'}) - - # 校验类型和专区的存在性及关联性 - try: - leixing_obj = ShangpinLeixing.query.get(id=leixing_id) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 400, 'msg': '商品类型不存在'}) - try: - zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id) - if zhuanqu_obj.leixing_id != leixing_id: - return Response({'code': 400, 'msg': '专区不属于该商品类型'}) - except ShangpinZhuanqu.DoesNotExist: - return Response({'code': 400, 'msg': '商品专区不存在'}) - - # 处理抢单要求相关字段 - huiyuan_id_val = None - yongjin_val = None - if yaoqiuleixing == 1: - # 会员要求 - if not huiyuan_id: - # 未传递时,尝试从商品类型中获取默认会员ID - huiyuan_id_val = leixing_obj.huiyuan_id - if not huiyuan_id_val: - return Response({'code': 400, 'msg': '未指定会员且商品类型无默认会员'}) - else: - huiyuan_id_val = huiyuan_id - # 验证会员是否存在 - if not Huiyuan.query.filter(huiyuan_id=huiyuan_id_val).exists(): - return Response({'code': 400, 'msg': '指定的会员不存在'}) - else: - # 佣金要求 - try: - yongjin_val = Decimal(yongjin_str) - if yongjin_val < 0: - return Response({'code': 400, 'msg': '佣金不能为负数'}) - except: - # 若未传递或无效,尝试从商品类型中获取默认佣金 - if leixing_obj.yongjin is not None: - yongjin_val = leixing_obj.yongjin - else: - return Response({'code': 400, 'msg': '佣金格式错误且类型无默认佣金'}) - - # 处理打手分成方式 - kaioi_ewai = False - ewai_fencheng = Decimal('0') - if fencheng_type == 'custom': - try: - ewai_fencheng = Decimal(ewai_PlayerCommission_str) - if ewai_fencheng < 0: - return Response({'code': 400, 'msg': '自定义分成金额不能为负数'}) - kaioi_ewai = True - except: - return Response({'code': 400, 'msg': '自定义分成金额格式错误'}) - - # 图片上传处理 - tupian_url = '' - if image_file: - # 验证图片 - valid, msg = validate_image(image_file) - if not valid: - return Response({'code': 400, 'msg': f'图片无效:{msg}'}) - # 生成唯一文件名 - ext = image_file.name.split('.')[-1].lower() - if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: - return Response({'code': 400, 'msg': '不支持的图片格式'}) - timestamp = datetime.now().strftime('%Y%m%d%H%M%S') - random_str = uuid.uuid4().hex[:8] - filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}" - # 上传到 OSS - uploaded_url = upload_to_oss(image_file, filename) - if not uploaded_url: - return Response({'code': 500, 'msg': '图片上传失败,请稍后重试'}) - tupian_url = filename # 存储相对路径,前端拼接域名 - else: - return Response({'code': 400, 'msg': '商品图片不能为空'}) - - # 创建商品 - try: - with transaction.atomic(): - product = Shangpin.query.create( - biaoqian=biaoqian, - jiage=jiage, - kucun=kucun, - leixing_id=leixing_id, - zhuanqu_id=zhuanqu_id, - zhenshi_xiaoliang=0, # 初始真实销量为0 - duiwai_xiaoliang=duiwai_xiaoliang, - jieshao=jieshao, - xiadan_xuzhi=xiadan_xuzhi, - tupian_url=tupian_url, - yaoqiuleixing=yaoqiuleixing, - huiyuan_id=huiyuan_id_val, - yongjin=yongjin_val, - kaioi_ewai_dashou_fencheng=kaioi_ewai, - ewai_dashou_fencheng=ewai_fencheng, - paixu=0, # 新商品默认排序最后 - shenhezhuangtai=1, # 正常状态 - ) - except Exception as e: - logger.error(f"创建商品失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '创建商品失败,请稍后重试'}) - - return Response({ - 'code': 0, - 'msg': '商品发布成功', - 'data': { - 'id': product.id, - 'tupian_url': tupian_url, - } - }) - - -# houtai/views_product.py 中添加 - -class DeleteProductView(APIView): - """ - 删除商品(物理删除,同时删除OSS图片) - 权限:2200a - POST /houtai/htscsp - 参数: - username: 客服/管理员账号 - id: 商品ID - 返回: - code:0 成功 - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - product_id = request.data.get('id') - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not product_id: - return Response({'code': 400, 'msg': '缺少商品ID'}) - try: - product_id = int(product_id) - except: - return Response({'code': 400, 'msg': '商品ID格式错误'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 查询商品是否存在 - try: - product = Shangpin.query.get(id=product_id) - except Shangpin.DoesNotExist: - return Response({'code': 404, 'msg': '商品不存在'}) - - # 先删除OSS上的图片 - tupian_url = product.tupian_url - if tupian_url: - # 调用已有的 delete_from_oss 函数 - delete_success = delete_from_oss(tupian_url) - if not delete_success: - logger.warning(f"删除商品图片失败: {tupian_url}, 商品ID: {product_id}") - # 注意:即使图片删除失败,也应继续删除数据库记录,避免数据不一致 - # 但可以记录日志供后续人工处理 - - # 删除数据库记录 - product.delete() - - logger.info(f"商品删除成功: id={product_id}, 图片路径={tupian_url}, 操作人={username}") - return Response({'code': 0, 'msg': '商品删除成功'}) - - - - - - - -class GetProductDetailView(APIView): - """ - 获取商品详情(包含商品类型、专区、会员列表) - 权限:2200a - POST /houtai/htsphqxq - 参数: - username: 客服/管理员账号 - id: 商品ID - 返回: - code:0, data: { product, type_list, zone_list, member_list } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - product_id = request.data.get('id') - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not product_id: - return Response({'code': 400, 'msg': '缺少商品ID'}) - try: - product_id = int(product_id) - except ValueError: - return Response({'code': 400, 'msg': '商品ID格式错误'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 查询商品 - try: - product = Shangpin.query.get(id=product_id) - except Shangpin.DoesNotExist: - return Response({'code': 404, 'msg': '商品不存在'}) - - # 查询所有商品类型、专区、会员(用于前端下拉选择) - type_list = list(ShangpinLeixing.query.all().values( - 'id', 'jieshao', 'tupian_url', 'shenhezhuangtai' - )) - zone_list = list(ShangpinZhuanqu.query.all().values( - 'id', 'mingzi', 'leixing_id', 'shenhezhuangtai' - )) - member_list = list(Huiyuan.query.all().values( - 'huiyuan_id', 'jieshao', 'jiage' - )) - - # 构建商品返回数据 - product_data = { - 'id': product.id, - 'biaoqian': product.biaoqian, - 'jiage': str(product.jiage), - 'kucun': product.kucun, - 'leixing_id': product.leixing_id, - 'zhuanqu_id': product.zhuanqu_id, - 'zhenshi_xiaoliang': product.zhenshi_xiaoliang, - 'duiwai_xiaoliang': product.duiwai_xiaoliang, - 'jieshao': product.jieshao, - 'xiadan_xuzhi': product.xiadan_xuzhi, - 'guize_tupian': product.guize_tupian, - 'yaoqiuleixing': product.yaoqiuleixing, - 'huiyuan_id': product.huiyuan_id, - 'yongjin': str(product.yongjin) if product.yongjin is not None else None, - 'tupian_url': product.tupian_url, - 'kaioi_ewai_PlayerCommission': product.kaioi_ewai_dashou_fencheng, - 'ewai_PlayerCommission': str(product.ewai_dashou_fencheng) if product.ewai_dashou_fencheng is not None else '0', - 'CreateTime': product.CreateTime.isoformat() if product.CreateTime else None, - 'shenhezhuangtai': product.shenhezhuangtai, - } - - return Response({ - 'code': 0, - 'data': { - 'product': product_data, - 'type_list': type_list, - 'zone_list': zone_list, - 'member_list': member_list, - } - }) - - - -class UpdateProductView(APIView): - """ - 修改商品(支持商品图片和规则图片的更新) - 权限:2200a - POST /houtai/bcspxg - """ - permission_classes = [] - parser_classes = [MultiPartParser, FormParser] - - def post(self, request): - username = request.data.get('username', '').strip() - product_id = request.data.get('id') - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not product_id: - return Response({'code': 400, 'msg': '缺少商品ID'}) - try: - product_id = int(product_id) - except ValueError: - return Response({'code': 400, 'msg': '商品ID格式错误'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '2200a' not in permissions: - return Response({'code': 403, 'msg': '无权限管理商品'}) - - # 使用事务,将 select_for_update 放在事务内 - with transaction.atomic(): - try: - product = Shangpin.objects.select_for_update().get(id=product_id) - except Shangpin.DoesNotExist: - return Response({'code': 404, 'msg': '商品不存在'}) - - # ---------- 1. 基础字段 ---------- - biaoqian = request.data.get('biaoqian', '').strip() - if not biaoqian: - return Response({'code': 400, 'msg': '商品标题不能为空'}) - - try: - jiage = Decimal(str(request.data.get('jiage', 0))) - if jiage <= 0: - return Response({'code': 400, 'msg': '价格必须大于0'}) - except: - return Response({'code': 400, 'msg': '价格格式错误'}) - - try: - kucun = int(request.data.get('kucun', 0)) - if kucun < 0: - return Response({'code': 400, 'msg': '库存不能为负数'}) - except: - return Response({'code': 400, 'msg': '库存格式错误'}) - - try: - duiwai_xiaoliang = int(request.data.get('duiwai_xiaoliang', 0)) - except: - duiwai_xiaoliang = 0 - - try: - leixing_id = int(request.data.get('leixing_id')) - except: - return Response({'code': 400, 'msg': '商品类型ID无效'}) - - try: - zhuanqu_id = int(request.data.get('zhuanqu_id')) - except: - return Response({'code': 400, 'msg': '商品专区ID无效'}) - - try: - yaoqiuleixing = int(request.data.get('yaoqiuleixing', 1)) - if yaoqiuleixing not in (1, 2): - raise ValueError - except: - return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'}) - - try: - shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) - except: - shenhezhuangtai = 1 - - # 校验类型和专区存在性 - if not ShangpinLeixing.query.filter(id=leixing_id).exists(): - return Response({'code': 400, 'msg': '商品类型不存在'}) - if not ShangpinZhuanqu.query.filter(id=zhuanqu_id, leixing_id=leixing_id).exists(): - return Response({'code': 400, 'msg': '专区不存在或不属于该类型'}) - - # ---------- 2. 会员/佣金 ---------- - huiyuan_id = None - yongjin = None - if yaoqiuleixing == 1: - huiyuan_id = request.data.get('huiyuan_id', '').strip() or None - if huiyuan_id and not Huiyuan.query.filter(huiyuan_id=huiyuan_id).exists(): - return Response({'code': 400, 'msg': '指定的会员不存在'}) - else: - try: - yongjin = Decimal(str(request.data.get('yongjin', 0))) - if yongjin < 0: - return Response({'code': 400, 'msg': '佣金不能为负数'}) - except: - yongjin = Decimal('0') - - # ---------- 3. 打手分成 ---------- - kaioi_ewai = request.data.get('kaioi_ewai_PlayerCommission') == 'true' - ewai_fencheng = Decimal('0') - if kaioi_ewai: - try: - ewai_fencheng = Decimal(str(request.data.get('ewai_PlayerCommission', 0))) - if ewai_fencheng < 0: - return Response({'code': 400, 'msg': '分成金额不能为负数'}) - except: - return Response({'code': 400, 'msg': '分成金额格式错误'}) - - jieshao = request.data.get('jieshao', '').strip() - xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip() - - # ---------- 4. 商品图片更新 ---------- - tupian_url = product.tupian_url - tupian_file = request.FILES.get('tupian_file') - if tupian_file: - valid, msg = validate_image(tupian_file) - if not valid: - return Response({'code': 400, 'msg': f'商品图片无效:{msg}'}) - ext = tupian_file.name.split('.')[-1].lower() - if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: - return Response({'code': 400, 'msg': '不支持的图片格式'}) - timestamp = datetime.now().strftime('%Y%m%d%H%M%S') - random_str = uuid.uuid4().hex[:8] - new_filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}" - uploaded_url = upload_to_oss(tupian_file, new_filename) - if not uploaded_url: - return Response({'code': 500, 'msg': '商品图片上传失败'}) - if product.tupian_url: - delete_from_oss(product.tupian_url) - tupian_url = new_filename - - # ---------- 5. 规则图片更新 ---------- - guize_tupian = product.guize_tupian - guize_file = request.FILES.get('guize_file') - if guize_file: - valid, msg = validate_image(guize_file) - if not valid: - return Response({'code': 400, 'msg': f'规则图片无效:{msg}'}) - ext = guize_file.name.split('.')[-1].lower() - if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: - return Response({'code': 400, 'msg': '不支持的图片格式'}) - timestamp = datetime.now().strftime('%Y%m%d%H%M%S') - random_str = uuid.uuid4().hex[:8] - new_filename = f"shangpin/guizetupian/{timestamp}_{random_str}.{ext}" - uploaded_url = upload_to_oss(guize_file, new_filename) - if not uploaded_url: - return Response({'code': 500, 'msg': '规则图片上传失败'}) - if product.guize_tupian: - delete_from_oss(product.guize_tupian) - guize_tupian = new_filename - - # ---------- 6. 更新数据库 ---------- - product.biaoqian = biaoqian - product.jiage = jiage - product.kucun = kucun - product.duiwai_xiaoliang = duiwai_xiaoliang - product.leixing_id = leixing_id - product.zhuanqu_id = zhuanqu_id - product.yaoqiuleixing = yaoqiuleixing - product.huiyuan_id = huiyuan_id - product.yongjin = yongjin - product.kaioi_ewai_dashou_fencheng = kaioi_ewai - product.ewai_dashou_fencheng = ewai_fencheng - product.jieshao = jieshao - product.xiadan_xuzhi = xiadan_xuzhi - product.tupian_url = tupian_url - product.guize_tupian = guize_tupian - product.shenhezhuangtai = shenhezhuangtai - product.save() - - return Response({'code': 0, 'msg': '商品修改成功', 'data': {'id': product.id}}) - - - - - -class GetMemberListView(APIView): - """ - 获取会员列表 - 权限:3300a - POST /houtai/hthqhylb - 参数:username - 返回:code:0, data: { list: [...] } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '3300a' not in permissions: - return Response({'code': 403, 'msg': '无权限查看会员列表'}) - - members = Huiyuan.query.all().order_by('-CreateTime') - member_list = [] - for m in members: - member_list.append({ - 'huiyuan_id': m.huiyuan_id, - 'jieshao': m.jieshao, - 'jtjieshao': m.jtjieshao, - 'jiage': str(m.jiage), - 'guanshifc': str(m.guanshifc), - 'zuzhangfc': str(m.zuzhangfc), - 'goumai_cishu': m.goumai_cishu, - 'CreateTime': m.CreateTime.isoformat() if m.CreateTime else None, - 'UpdateTime': m.UpdateTime.isoformat() if m.UpdateTime else None, - }) - return Response({'code': 0, 'data': {'list': member_list}}) - - -class UpdateMemberView(APIView): - """ - 修改会员信息 - 权限:3300a - POST /houtai/htxghyxx - 参数: - username, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao - 返回:code:0 成功 - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - huiyuan_id = request.data.get('huiyuan_id', '').strip() - jieshao = request.data.get('jieshao', '').strip() - jiage = request.data.get('jiage') - guanshifc = request.data.get('guanshifc') - zuzhangfc = request.data.get('zuzhangfc') - jtjieshao = request.data.get('jtjieshao', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not huiyuan_id: - return Response({'code': 400, 'msg': '缺少会员ID'}) - if not jieshao: - return Response({'code': 400, 'msg': '会员名称不能为空'}) - - try: - jiage = Decimal(str(jiage)) - guanshifc = Decimal(str(guanshifc)) - zuzhangfc = Decimal(str(zuzhangfc)) - if jiage < 0 or guanshifc < 0 or zuzhangfc < 0: - raise ValueError - except: - return Response({'code': 400, 'msg': '金额格式错误'}) - - if guanshifc + zuzhangfc > jiage: - return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '3300a' not in permissions: - return Response({'code': 403, 'msg': '无权限修改会员'}) - - with transaction.atomic(): - try: - member = Huiyuan.objects.select_for_update().get(huiyuan_id=huiyuan_id) - except Huiyuan.DoesNotExist: - return Response({'code': 404, 'msg': '会员不存在'}) - - member.jieshao = jieshao - member.jiage = jiage - member.guanshifc = guanshifc - member.zuzhangfc = zuzhangfc - member.jtjieshao = jtjieshao - member.save() - - return Response({'code': 0, 'msg': '修改成功'}) - - - -class AddMemberView(APIView): - """ - 添加会员 - 权限:3300a - POST /houtai/httjhy - 参数: - username, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao - 返回:code:0, data: { huiyuan_id } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - jieshao = request.data.get('jieshao', '').strip() - jiage = request.data.get('jiage') - guanshifc = request.data.get('guanshifc') - zuzhangfc = request.data.get('zuzhangfc') - jtjieshao = request.data.get('jtjieshao', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not jieshao: - return Response({'code': 400, 'msg': '会员名称不能为空'}) - - try: - jiage = Decimal(str(jiage)) - guanshifc = Decimal(str(guanshifc)) - zuzhangfc = Decimal(str(zuzhangfc)) - if jiage <= 0 or guanshifc < 0 or zuzhangfc < 0: - raise ValueError - except: - return Response({'code': 400, 'msg': '金额格式错误'}) - - if guanshifc + zuzhangfc > jiage: - return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '3300a' not in permissions: - return Response({'code': 403, 'msg': '无权限添加会员'}) - - # 生成唯一6位会员ID(数字+字母) - def generate_huiyuan_id(): - while True: - new_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) - if not Huiyuan.query.filter(huiyuan_id=new_id).exists(): - return new_id - - with transaction.atomic(): - new_id = generate_huiyuan_id() - member = Huiyuan.query.create( - huiyuan_id=new_id, - jieshao=jieshao, - jiage=jiage, - guanshifc=guanshifc, - zuzhangfc=zuzhangfc, - jtjieshao=jtjieshao, - goumai_cishu=0 - ) - - return Response({'code': 0, 'msg': '添加成功', 'data': {'huiyuan_id': member.huiyuan_id}}) - - - - - -class GetGuanliListView(APIView): - """ - 获取管事列表 - 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一 - POST /houtai/hthqgslb - 参数: - username, keyword(可选), balance_op/balance_value, commission_op/commission_value, - invite_count_op/invite_count_value, status, page, page_size - 返回: - code:0, data: { list: [...], total } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - # 权限校验(需要任一权限) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看管事列表'}) - - # 分页参数 - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - if page < 1 or page_size < 1: - raise ValueError - except: - return Response({'code': 400, 'msg': '分页参数错误'}) - - # 筛选参数 - keyword = request.data.get('keyword', '').strip() - balance_op = request.data.get('balance_op', '').strip().lower() - balance_value = request.data.get('balance_value') - commission_op = request.data.get('commission_op', '').strip().lower() - commission_value = request.data.get('commission_value') - invite_count_op = request.data.get('invite_count_op', '').strip().lower() - invite_count_value = request.data.get('invite_count_value') - status = request.data.get('status') - - # 构建查询:从 User 开始,关联 UserGuanshi 和 UserBoss(左连接获取昵称) - qs = filter_user_qs_by_club( - User.query.filter( - GuanshiProfile__isnull=False # 确保是管事 - ).select_related('GuanshiProfile').select_related('BossProfile'), - request, - ) - - # 关键词搜索(用户ID 或 昵称来自 boss_profile.nickname) - if keyword: - qs = qs.filter( - Q(UserUID__icontains=keyword) | - Q(BossProfile__nickname__icontains=keyword) - ) - - # 余额筛选(使用 guanshi_profile.yue) - if balance_op and balance_value is not None and balance_value != '': - try: - balance_val = Decimal(str(balance_value)) - if balance_op == 'gte': - qs = qs.filter(GuanshiProfile__yue__gte=balance_val) - elif balance_op == 'lte': - qs = qs.filter(GuanshiProfile__yue__lte=balance_val) - else: - return Response({'code': 400, 'msg': '余额比较符无效'}) - except: - return Response({'code': 400, 'msg': '余额数值格式错误'}) - - # 分佣总额筛选(guanshi_profile.chongzhifenrun) - if commission_op and commission_value is not None and commission_value != '': - try: - commission_val = Decimal(str(commission_value)) - if commission_op == 'gte': - qs = qs.filter(GuanshiProfile__chongzhifenrun__gte=commission_val) - elif commission_op == 'lte': - qs = qs.filter(GuanshiProfile__chongzhifenrun__lte=commission_val) - else: - return Response({'code': 400, 'msg': '分佣比较符无效'}) - except: - return Response({'code': 400, 'msg': '分佣数值格式错误'}) - - # 邀请打手总数筛选(guanshi_profile.yaogingshuliang) - if invite_count_op and invite_count_value is not None and invite_count_value != '': - try: - invite_val = int(invite_count_value) - if invite_count_op == 'gte': - qs = qs.filter(GuanshiProfile__yaogingshuliang__gte=invite_val) - elif invite_count_op == 'lte': - qs = qs.filter(GuanshiProfile__yaogingshuliang__lte=invite_val) - else: - return Response({'code': 400, 'msg': '邀请数量比较符无效'}) - except: - return Response({'code': 400, 'msg': '邀请数量格式错误'}) - - # 状态筛选 - if status is not None and status != '': - try: - status_val = int(status) - if status_val not in (0, 1): - raise ValueError - qs = qs.filter(GuanshiProfile__zhuangtai=status_val) - except: - return Response({'code': 400, 'msg': '状态值无效'}) - - # 计算总数 - total = qs.count() - - # 分页 - offset = (page - 1) * page_size - users = qs.order_by('-UserCreateTime')[offset:offset + page_size] - - # 构建返回数据 - result = [] - for user in users: - guanshi = user.GuanshiProfile - boss = user.BossProfile if hasattr(user, 'BossProfile') else None - result.append({ - 'yonghuid': user.UserUID, - 'avatar': user.Avatar or '', - 'nickname': boss.nickname if boss else '', - 'zhuangtai': guanshi.zhuangtai, - 'yaogingshuliang': guanshi.yaogingshuliang, - 'yue': str(guanshi.yue), - 'chongzhifenrun': str(guanshi.chongzhifenrun), - }) - - return Response({ - 'code': 0, - **list_response_meta(request), - 'data': {'list': result, 'total': total}, - }) - - - - - - - - -class GetGuanliDetailView(APIView): - """ - 获取管事详情 - 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一 - POST /houtai/hthqgsxq - 参数:username, yonghuid - 返回: - code:0, data: { - base: {...}, - balance: {...}, - permanent_enabled, permanent_amount, - custom_first_dividend: {}, - extra_dividend_map: {}, - all_members: [] - } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少用户ID'}) - - # 权限校验(需要任一权限) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看管事详情'}) - - # 查询用户主表和管事扩展表 - try: - user = User.query.select_related('GuanshiProfile', 'BossProfile').get(UserUID=yonghuid) - guanshi = user.GuanshiProfile - boss = user.BossProfile if hasattr(user, 'BossProfile') else None - except User.DoesNotExist: - return Response({'code': 404, 'msg': '管事不存在'}) - except UserGuanshi.DoesNotExist: - return Response({'code': 404, 'msg': '用户不是管事'}) - - denied = forbid_if_user_out_of_scope(request, user) - if denied: - return denied - - # 基础信息 - is_zuzhang = UserZuzhang.query.filter(user=user).exists() - base_data = { - 'yonghuid': user.UserUID, - 'nickname': boss.nickname if boss else '', - 'avatar': user.Avatar or '', - 'dianhua': guanshi.dianhua or '', - 'wechat': guanshi.wechat or '', - 'zhuangtai': guanshi.zhuangtai, - 'yaoqingma': guanshi.yaoqingma, - 'yaoqingren': guanshi.yaoqingren, - 'invite_qrcode_url': guanshi.invite_qrcode_url or '', - 'yaogingshuliang': guanshi.yaogingshuliang, - 'jinrichongzhi': guanshi.jinrichongzhi, - 'jinyuechongzhi': guanshi.jinyuechongzhi, - 'chongzhifenrun': str(guanshi.chongzhifenrun), - 'create_time': guanshi.CreateTime.isoformat() if guanshi.CreateTime else None, - 'CreateTime': guanshi.CreateTime.isoformat() if guanshi.CreateTime else None, - } - - # 余额提现相关 - balance_data = { - 'yue': str(guanshi.yue), - 'jinritixian_jine': str(guanshi.jinritixian_jine), - 'last_tixian_date': guanshi.last_tixian_date.isoformat() if guanshi.last_tixian_date else None, - 'kaioi_ewai_xiane': guanshi.kaioi_ewai_xiane, - 'ewai_xiane': str(guanshi.ewai_xiane), - } - - # 永久分红(二次分红) - permanent_enabled = guanshi.fenghong_erci_enabled - permanent_amount = str(guanshi.fenghong_erci) if guanshi.fenghong_erci else '0' - - # 获取所有会员(用于前端选择) - all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc')) - for m in all_members: - m['jiage'] = str(m['jiage']) - m['guanshifc'] = str(m['guanshifc']) - - # 获取多次分红配置(按次数和会员组织) - # 查询该管事的所有多次分红记录 - duoci_records = DuociFenhong.query.filter(yonghuid=yonghuid).order_by('cishu') - custom_first = {} # 首次分红定制(cishu=1) - extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } } - - for record in duoci_records: - # 获取会员信息 - member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first() - if not member: - continue - member_info = { - 'jieshao': member.jieshao, - 'jiage': str(member.jiage), - } - if record.cishu == 1: - custom_first[record.huiyuan] = float(record.guanshi_fenhong) - else: - times = record.cishu - if times not in extra_map: - extra_map[times] = {} - extra_map[times][record.huiyuan] = { - 'guanshi_fenhong': float(record.guanshi_fenhong), - **member_info - } - - # 注意:首次分红定制只存储有定制记录的会员,没有定制的会员使用默认值(前端通过会员表展示) - # 额外次数分红按次数返回 - - return Response({ - 'code': 0, - 'data': { - 'base': base_data, - 'balance': balance_data, - 'permanent_enabled': permanent_enabled, - 'permanent_amount': permanent_amount, - 'custom_first_dividend': custom_first, - 'extra_dividend_map': extra_map, - 'all_members': all_members, - 'is_zuzhang': is_zuzhang, - } - }) - - - - -def _generate_unique_zuzhang_yaoqingma(yonghuid): - """生成唯一组长邀请码(与小程序组长注册逻辑一致)""" - timestamp = str(int(time.time()))[-6:] - uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0') - rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2)) - base = f"Z{timestamp}{uid_part}{rand_str}" - for _ in range(5): - if not UserZuzhang.query.filter(yaoqingma=base).exists(): - return base - base = base + random.choice(string.digits) - return base + str(int(time.time() * 1000))[-4:] - - -def _create_zuzhang_profile(user): - """为已有用户创建组长扩展记录(默认字段与小程序注册一致)""" - invite_guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count() - yaoqingma = _generate_unique_zuzhang_yaoqingma(user.UserUID) - return UserZuzhang.query.create( - user=user, - yaoqingma=yaoqingma, - fenyong_zonge=Decimal('0.00'), - ketixian_jine=Decimal('0.00'), - yaoqing_zongshu=invite_guanshi_count, - jinri_fenyong=Decimal('0.00'), - jinyue_fenyong=Decimal('0.00'), - zhuangtai=1, - jinri_tixian=Decimal('0.00'), - ewai_tixian_xiane=Decimal('0.00'), - kaioi_ewai_tixian=False, - kaioi_ewai_fenhong=False, - ewai_fenhong_jine=Decimal('0.00'), - ) - - -class PromoteGuanshiToZuzhangView(APIView): - """ - 后台将管事用户升级为组长(写入 user_zuzhang 扩展表) - POST /houtai/htgssjzz - 权限:sjzz666 - 参数:username, yonghuid - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少用户ID'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if 'sjzz666' not in permissions: - return Response({'code': 403, 'msg': '无权限升级组长(需要 sjzz666)'}) - - try: - user = User.query.select_related('GuanshiProfile').get(UserUID=yonghuid) - guanshi = user.GuanshiProfile - except User.DoesNotExist: - return Response({'code': 404, 'msg': '用户不存在'}) - except UserGuanshi.DoesNotExist: - return Response({'code': 404, 'msg': '用户不是管事,无法升级组长'}) - - if UserZuzhang.query.filter(user=user).exists(): - zuzhang = user.ZuzhangProfile - return Response({ - 'code': 400, - 'msg': '该用户已是组长', - 'data': { - 'yonghuid': user.UserUID, - 'yaoqingma': zuzhang.yaoqingma, - }, - }) - - if guanshi.zhuangtai != 1: - return Response({'code': 400, 'msg': '管事账号已禁用,无法升级组长'}) - - try: - with transaction.atomic(): - zuzhang = _create_zuzhang_profile(user) - except IntegrityError: - return Response({'code': 400, 'msg': '升级失败,请稍后重试'}) - - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=kefu.user.Phone, - leixing=XIUGAI_LEIXING_GUANSHI, - qitashuoming=( - f'【后台】管事升级组长:用户ID={yonghuid},' - f'生成组长邀请码={zuzhang.yaoqingma},操作人={kefu.user.Phone}' - ), - ) - - logger.info(f"管事升级组长成功: yonghuid={yonghuid}, yaoqingma={zuzhang.yaoqingma}") - return Response({ - 'code': 0, - 'msg': '已升级为组长', - 'data': { - 'yonghuid': user.UserUID, - 'nickname': getattr(user.BossProfile, 'nickname', '') if hasattr(user, 'BossProfile') else '', - 'yaoqingma': zuzhang.yaoqingma, - 'yaoqing_zongshu': zuzhang.yaoqing_zongshu, - 'ketixian_jine': str(zuzhang.ketixian_jine), - }, - }) - - - - -class UpdateGuanliView(APIView): - """ - 修改管事信息(基础信息、余额、提现限额、分红策略) - 权限细分: - - 修改状态(封禁/解封)及昵称/电话/微信:需要4400a - - 增加余额:需要4400c - - 减少余额:需要4400b - - 修改提现限额(kaioi_ewai_xiane/ewai_xiane):需要4400e - - 修改分红相关(永久分红、定制首次、额外次数):需要4400f - POST /houtai/htxggsxx - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少用户ID'}) - - # 权限校验(先获取权限列表) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 查询管事对象(不在事务外加锁,事务内加锁) - try: - user = User.query.get(UserUID=yonghuid) - guanshi = user.GuanshiProfile - boss = user.BossProfile if hasattr(user, 'BossProfile') else None - except User.DoesNotExist: - return Response({'code': 404, 'msg': '用户不存在'}) - except UserGuanshi.DoesNotExist: - return Response({'code': 404, 'msg': '用户不是管事'}) - - # 开始事务,内部使用 select_for_update - with transaction.atomic(): - # 重新获取加锁对象 - user = User.objects.select_for_update().get(UserUID=yonghuid) - guanshi = user.GuanshiProfile - boss = user.BossProfile if hasattr(user, 'BossProfile') else None - operator = kefu.user.Phone - - # ---------- 1. 基础信息修改(需要4400a)---------- - nickname = request.data.get('nickname') - dianhua = request.data.get('dianhua') - wechat = request.data.get('wechat') - zhuangtai = request.data.get('zhuangtai') - avatar = request.data.get('avatar') - - if any(v is not None for v in [nickname, dianhua, wechat, zhuangtai, avatar]): - if '4400a' not in permissions: - return Response({'code': 403, 'msg': '无权限修改管事基础信息(需要4400a)'}) - base_changes = [] - old_nickname = boss.nickname if boss else '' - old_dianhua = guanshi.dianhua - old_wechat = guanshi.wechat - old_zhuangtai = guanshi.zhuangtai - # 处理昵称(在老板扩展表中) - if nickname is not None: - if not boss: - boss = UserBoss.query.create(user=user, nickname=nickname) - else: - boss.nickname = nickname - boss.save() - if str(nickname) != str(old_nickname): - base_changes.append(f'昵称:{old_nickname or "无"}→{nickname}') - # 处理其他字段 - if dianhua is not None: - guanshi.dianhua = dianhua - if str(dianhua) != str(old_dianhua): - base_changes.append(f'电话:{old_dianhua or "无"}→{dianhua}') - if wechat is not None: - guanshi.wechat = wechat - if str(wechat) != str(old_wechat): - base_changes.append(f'微信:{old_wechat or "无"}→{wechat}') - if zhuangtai is not None: - try: - zt = int(zhuangtai) - if zt in (0, 1): - guanshi.zhuangtai = zt - if zt != old_zhuangtai: - zt_map = {0: '禁用', 1: '正常'} - base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}→{zt_map.get(zt, zt)}') - except: - pass - if avatar is not None: - user.Avatar = avatar - user.save() - base_changes.append('更新了头像') - guanshi.save() - if base_changes: - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=operator, - leixing=XIUGAI_LEIXING_GUANSHI, - qitashuoming=( - f'【后台】修改管事基础信息:{";".join(base_changes)},' - f'管事ID={yonghuid},操作人={operator}' - ), - ) - - # ---------- 2. 余额修改(加:4400c;减:4400b)---------- - new_yue = request.data.get('yue') - if new_yue is not None: - try: - new_yue = Decimal(str(new_yue)) - if new_yue < 0: - return Response({'code': 400, 'msg': '余额不能为负数'}) - except: - return Response({'code': 400, 'msg': '余额格式错误'}) - - old_yue = guanshi.yue - if new_yue > old_yue: - if '4400c' not in permissions: - return Response({'code': 403, 'msg': '无权限增加余额(需要4400c)'}) - action_desc = f'增加{new_yue - old_yue}元' - elif new_yue < old_yue: - if '4400b' not in permissions: - return Response({'code': 403, 'msg': '无权限减少余额(需要4400b)'}) - action_desc = f'减少{old_yue - new_yue}元' - else: - action_desc = '无变动' - if new_yue != old_yue: - guanshi.yue = new_yue - guanshi.save() - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=operator, - leixing=XIUGAI_LEIXING_GUANSHI, - guanshiyue=new_yue, - guanshiyueq=old_yue, - qitashuoming=( - f'【后台】修改管事可提现余额:{old_yue}元 → {new_yue}元({action_desc}),' - f'管事ID={yonghuid},操作人={operator}' - ), - ) - - # ---------- 3. 提现限额修改(需要4400e)---------- - kaioi_ewai = request.data.get('kaioi_ewai_xiane') - ewai_val = request.data.get('ewai_xiane') - if kaioi_ewai is not None or ewai_val is not None: - if '4400e' not in permissions: - return Response({'code': 403, 'msg': '无权限修改提现限额(需要4400e)'}) - limit_changes = [] - old_kaioi = guanshi.kaioi_ewai_xiane - old_ewai = guanshi.ewai_xiane - if kaioi_ewai is not None: - guanshi.kaioi_ewai_xiane = bool(kaioi_ewai) - if bool(kaioi_ewai) != old_kaioi: - limit_changes.append(f'额外限额开关:{"开" if old_kaioi else "关"}→{"开" if kaioi_ewai else "关"}') - if ewai_val is not None: - try: - guanshi.ewai_xiane = Decimal(str(ewai_val)) - if guanshi.ewai_xiane != old_ewai: - limit_changes.append(f'额外限额金额:{old_ewai}→{guanshi.ewai_xiane}') - except: - return Response({'code': 400, 'msg': '额外限额金额格式错误'}) - guanshi.save() - if limit_changes: - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=operator, - leixing=XIUGAI_LEIXING_GUANSHI, - qitashuoming=( - f'【后台】修改管事提现限额:{";".join(limit_changes)},' - f'管事ID={yonghuid},操作人={operator}' - ), - ) - - # ---------- 4. 分红相关修改(需要4400f)---------- - perm_enabled = request.data.get('permanent_enabled') - perm_amount = request.data.get('permanent_amount') - custom_first = request.data.get('custom_first_dividend') - extra_map = request.data.get('extra_dividend_map') - - if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]): - if '4400f' not in permissions: - return Response({'code': 403, 'msg': '无权限修改分红策略(需要4400f)'}) - - # 4.1 永久分红 - if perm_enabled is not None: - guanshi.fenghong_erci_enabled = bool(perm_enabled) - if perm_amount is not None: - try: - guanshi.fenghong_erci = Decimal(str(perm_amount)) - except: - return Response({'code': 400, 'msg': '永久分红金额格式错误'}) - guanshi.save() - - # 4.2 首次定制分红(cishu=1) - if custom_first is not None and isinstance(custom_first, dict): - from jituan.services.club_penalty import resolve_gsfenhong_club_id - guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid) - # 获取当前所有首次定制记录 - existing_first = DuociFenhong.query.filter( - club_id=guanshi_club, yonghuid=yonghuid, cishu=1, - ) - # 前端传来的定制字典 {会员ID: 金额} - for huiyuan_id, amount in custom_first.items(): - try: - amount = Decimal(str(amount)) - except: - continue - if amount <= 0: - DuociFenhong.query.filter( - club_id=guanshi_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1, - ).delete() - else: - DuociFenhong.query.update_or_create( - club_id=guanshi_club, - yonghuid=yonghuid, - huiyuan=huiyuan_id, - cishu=1, - defaults={ - 'guanshi_fenhong': amount, - 'zuzhang_fenhong': Decimal('0'), - }, - ) - # 删除那些在前端字典中不存在的首次定制记录(即已取消定制) - for record in existing_first: - if record.huiyuan not in custom_first: - record.delete() - - # 4.3 额外次数分红(cishu >= 2) - if extra_map is not None and isinstance(extra_map, dict): - from jituan.services.club_penalty import resolve_gsfenhong_club_id - guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid) - # 收集需要保留的记录 - keep_set = set() - for times_str, members in extra_map.items(): - try: - cishu = int(times_str) - if cishu < 2: - continue - except: - continue - if not isinstance(members, dict): - continue - for huiyuan_id, data in members.items(): - if not isinstance(data, dict): - continue - amount = data.get('guanshi_fenhong') - if amount is None: - continue - try: - amount = Decimal(str(amount)) - except: - continue - keep_set.add((cishu, huiyuan_id)) - # 更新或创建 - DuociFenhong.query.update_or_create( - club_id=guanshi_club, - yonghuid=yonghuid, - huiyuan=huiyuan_id, - cishu=cishu, - defaults={ - 'guanshi_fenhong': amount, - 'zuzhang_fenhong': Decimal('0') - } - ) - # 删除不在 keep_set 中的额外次数记录 - existing_extra = DuociFenhong.query.filter( - club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2, - ) - for record in existing_extra: - if (record.cishu, record.huiyuan) not in keep_set: - record.delete() - - dividend_parts = [] - if perm_enabled is not None: - dividend_parts.append(f'永久分红开关→{"开" if perm_enabled else "关"}') - if perm_amount is not None: - dividend_parts.append(f'永久分红金额→{perm_amount}') - if custom_first is not None: - dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)') - if extra_map is not None: - dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)') - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=operator, - leixing=XIUGAI_LEIXING_GUANSHI, - qitashuoming=( - f'【后台】修改管事分红策略:{";".join(dividend_parts) or "配置已更新"},' - f'管事ID={yonghuid},操作人={operator}' - ), - ) - - return Response({'code': 0, 'msg': '修改成功'}) - - - - - - - - - -class GetWithdrawSettingsView(APIView): - """ - 获取提现设置(手续费利率、每日限额、当日已提现总额) - 权限:需要 5500a 或 5500b 或 5500c 任一 - POST /houtai/hthqtxpz - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - # 需要5500a/b/c任一 - if not any(p in permissions for p in ('5500a', '5500b', '5500c')): - return Response({'code': 403, 'msg': '无权限查看提现设置'}) - - from jituan.services.withdraw_config import build_withdraw_settings_payload - return Response({'code': 0, 'data': build_withdraw_settings_payload(request)}) - - -class UpdateWithdrawSettingsView(APIView): - """ - 修改提现设置(权限细分): - - 修改打手相关(利率、个人限额、总限额)需要5500a - - 修改管事相关需要5500b - - 修改组长相关需要5500c - 注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算), - 后端不直接修改 WithdrawalDailyStats,该表由提现流程自动累加。 - POST /houtai/htxgtxsz - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 权限映射:要修改的角色需要对应的权限 - # 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3 - from jituan.services.withdraw_config import apply_withdraw_settings_update - return apply_withdraw_settings_update(request, permissions) - - - -class GetZuzhangListView(APIView): - """ - 组长列表接口(分页 + 多条件筛选) - 路由:POST /houtai/hthqzzlb - 权限:需要登录,且拥有 6600a~6600e 任一权限 - """ - permission_classes = [IsAuthenticated] - - @transaction.atomic # 保证查询的一致性(可选,对只读查询影响不大但无坏处) - def post(self, request): - # 1. 获取前端传递的 username(用于防越权) - frontend_username = request.data.get('username', '') - - # 2. 调用公共校验方法,验证身份并获取权限列表 - result = verify_kefu_permission(request, username_from_frontend=frontend_username) - - # 重要:verify_kefu_permission 返回格式为 (user_obj, permissions_list) 或 (None, Response) - # 如果第二个元素是 Response 对象,说明校验失败,直接返回该 Response - if isinstance(result, tuple) and len(result) == 2: - user_obj, second = result - if isinstance(second, Response): - return second # 校验失败,直接返回错误响应 - else: - permissions = second # 正常情况:permissions 是列表 - else: - # 防止意外返回格式,视为失败 - return Response({'code': 500, 'msg': '权限校验模块异常'}) - - # 3. 检查是否拥有组长管理所需权限(6600a ~ 6600e 任一) - required_perms = ['6600a', '6600b', '6600c', '6600d', '6600e'] - if not any(perm in permissions for perm in required_perms): - logger.warning(f"用户 {request.user.Phone} 尝试访问组长列表但缺少必要权限,已有权限: {permissions}") - return Response({'code': 403, 'msg': '您无权访问组长管理页面'}) - - # 4. 提取请求参数(分页 + 筛选) - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - except ValueError: - page, page_size = 1, 20 - - keyword = request.data.get('keyword', '').strip() - status = request.data.get('status') # 1=正常, 0=禁用 - invite_count_op = request.data.get('invite_count_op') # gte / lte - invite_count_value = request.data.get('invite_count_value') - amount_op = request.data.get('amount_op') - amount_value = request.data.get('amount_value') - commission_op = request.data.get('commission_op') - commission_value = request.data.get('commission_value') - - # 5. 构建基础 QuerySet(预加载关联表) - queryset = filter_user_related_by_club( - UserZuzhang.query.select_related('user', 'user__BossProfile').all(), - request, - ) - - # 6. 应用筛选条件(使用 ORM 安全过滤) - if keyword: - queryset = queryset.filter( - Q(user__UserUID__icontains=keyword) | - Q(user__BossProfile__nickname__icontains=keyword) - ) - - if status is not None and status != '': - try: - queryset = queryset.filter(zhuangtai=int(status)) - except ValueError: - pass - - # 邀请总数筛选 - if invite_count_value not in (None, ''): - try: - val = float(invite_count_value) - op = invite_count_op if invite_count_op in ('gte', 'lte') else 'gte' - queryset = queryset.filter(**{f'yaoqing_zongshu__{op}': val}) - except (ValueError, TypeError): - pass - - # 可提现金额筛选 - if amount_value not in (None, ''): - try: - val = float(amount_value) - op = amount_op if amount_op in ('gte', 'lte') else 'gte' - queryset = queryset.filter(**{f'ketixian_jine__{op}': val}) - except (ValueError, TypeError): - pass - - # 分佣总额筛选 - if commission_value not in (None, ''): - try: - val = float(commission_value) - op = commission_op if commission_op in ('gte', 'lte') else 'gte' - queryset = queryset.filter(**{f'fenyong_zonge__{op}': val}) - except (ValueError, TypeError): - pass - - # 7. 总数统计 - total = queryset.count() - - # 8. 分页 - offset = (page - 1) * page_size - paged_queryset = queryset[offset:offset + page_size] - - # 9. 构造返回数据 - data_list = [] - for zu in paged_queryset: - user = zu.user - nickname = '' - if hasattr(user, 'BossProfile') and user.BossProfile: - nickname = user.BossProfile.nickname or '' - - data_list.append({ - 'yonghuid': user.UserUID, - 'avatar': user.Avatar or '', - 'nickname': nickname, - 'fenyong_zonge': float(zu.fenyong_zonge), - 'ketixian_jine': float(zu.ketixian_jine), - 'yaoqing_zongshu': zu.yaoqing_zongshu, - 'zhuangtai': zu.zhuangtai, - }) - - return Response({ - 'code': 0, - 'msg': 'success', - **list_response_meta(request), - 'data': { - 'list': data_list, - 'total': total, - } - }) - - - - - - - - - - -class GetZuzhangDetailView(APIView): - """ - 获取组长详情 - 权限:6600a,6600b,6600c,6600d,6600e 任一 - POST /houtai/hthqzzxq - 参数:username, yonghuid - 返回: - code:0, data: { - base: {...}, - balance: {...}, - permanent_enabled, permanent_amount, - custom_first_dividend: {}, - extra_dividend_map: {}, - all_members: [] - } - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少用户ID'}) - - # 权限校验(需要任一权限) - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - required_perms = {'6600a', '6600b', '6600c', '6600d', '6600e'} - if not any(p in permissions for p in required_perms): - return Response({'code': 403, 'msg': '您没有权限查看组长详情'}) - - # 查询组长主表、扩展表、老板扩展表(昵称) - try: - user = User.query.select_related('ZuzhangProfile', 'BossProfile').get(UserUID=yonghuid) - zuzhang = user.ZuzhangProfile - boss = user.BossProfile if hasattr(user, 'BossProfile') else None - except User.DoesNotExist: - return Response({'code': 404, 'msg': '组长不存在'}) - except UserZuzhang.DoesNotExist: - return Response({'code': 404, 'msg': '用户不是组长'}) - - denied = forbid_if_user_out_of_scope(request, user) - if denied: - return denied - - # 基础信息 - base_data = { - 'yonghuid': user.UserUID, - 'nickname': boss.nickname if boss else '', - 'avatar': user.Avatar or '', - 'dianhua': '', # 组长表无电话字段?组长扩展表没有电话/微信,但前端可能需要,可从User关联?实际上组长表无电话字段,这里留空或从其他表获取 - 'wechat': '', - 'zhuangtai': zuzhang.zhuangtai, - 'yaoqingma': zuzhang.yaoqingma, - 'yaoqingren': '', # 组长表没有邀请人字段 - 'haibao_url': zuzhang.haibao_url or '', - 'yaoqing_zongshu': zuzhang.yaoqing_zongshu, - 'jinri_fenyong': str(zuzhang.jinri_fenyong), - 'jinyue_fenyong': str(zuzhang.jinyue_fenyong), - 'fenyong_zonge': str(zuzhang.fenyong_zonge), - 'CreateTime': zuzhang.CreateTime.isoformat() if zuzhang.CreateTime else None, - } - - # 余额提现相关 - balance_data = { - 'ketixian_jine': str(zuzhang.ketixian_jine), - 'jinri_tixian': str(zuzhang.jinri_tixian), - 'last_tixian_time': zuzhang.last_tixian_time.isoformat() if zuzhang.last_tixian_time else None, - 'kaioi_ewai_tixian': zuzhang.kaioi_ewai_tixian, - 'ewai_tixian_xiane': str(zuzhang.ewai_tixian_xiane), - } - - # 永久分红(额外分红开关和金额) - permanent_enabled = zuzhang.kaioi_ewai_fenhong - permanent_amount = str(zuzhang.ewai_fenhong_jine) if zuzhang.ewai_fenhong_jine else '0' - - # 获取所有会员(用于前端选择) - all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc')) - for m in all_members: - m['jiage'] = str(m['jiage']) - m['zuzhangfc'] = str(m['zuzhangfc']) - - # 获取多次分红配置(按次数和会员组织) - duoci_records = DuociFenhong.query.filter(yonghuid=yonghuid).order_by('cishu') - custom_first = {} # 首次分红定制(cishu=1) - extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } } - - for record in duoci_records: - # 获取会员信息 - member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first() - if not member: - continue - member_info = { - 'jieshao': member.jieshao, - 'jiage': str(member.jiage), - } - if record.cishu == 1: - custom_first[record.huiyuan] = float(record.zuzhang_fenhong) - else: - times = record.cishu - if times not in extra_map: - extra_map[times] = {} - extra_map[times][record.huiyuan] = { - 'zuzhang_fenhong': float(record.zuzhang_fenhong), - **member_info - } - - return Response({ - 'code': 0, - 'data': { - 'base': base_data, - 'balance': balance_data, - 'permanent_enabled': permanent_enabled, - 'permanent_amount': permanent_amount, - 'custom_first_dividend': custom_first, - 'extra_dividend_map': extra_map, - 'all_members': all_members, - } - }) - - - - -class UpdateZuzhangView(APIView): - """ - 修改组长信息(基础信息、余额、提现限额、分红策略) - 权限细分: - - 增加可提现金额:6600a - - 减少可提现金额:6600b - - 修改状态(封禁/解封):6600c - - 修改提现限额(kaioi_ewai_tixian/ewai_tixian_xiane):6600d - - 修改分红相关(永久分红、定制首次、额外次数):6600e - POST /houtai/htxgzzxx - 参数: - username, yonghuid, - nickname, avatar, dianhua, wechat, zhuangtai, # 基础 - ketixian_jine, kaioi_ewai_tixian, ewai_tixian_xiane, # 余额提现 - permanent_enabled, permanent_amount, # 永久分红 - custom_first_dividend: {}, # 首次定制分红 {会员ID: 金额} - extra_dividend_map: {} # 额外次数分红 {次数: {会员ID: {zuzhang_fenhong}}} - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少用户ID'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 查询组长对象 - try: - user = User.query.get(UserUID=yonghuid) - zuzhang = user.ZuzhangProfile - boss = user.BossProfile if hasattr(user, 'BossProfile') else None - except User.DoesNotExist: - return Response({'code': 404, 'msg': '用户不存在'}) - except UserZuzhang.DoesNotExist: - return Response({'code': 404, 'msg': '用户不是组长'}) - - # 获取原始数据(用于对比金额变化) - old_ketixian_jine = zuzhang.ketixian_jine - - # 开始事务 - with transaction.atomic(): - operator = kefu.user.Phone - old_nickname = boss.nickname if boss else '' - old_zhuangtai = zuzhang.zhuangtai - # ---------- 1. 基础信息修改(需要6600c?按需求,基础信息修改通常用6600c,但为了安全,暂不校验,如有需要可加)---------- - nickname = request.data.get('nickname', '').strip() - avatar = request.data.get('avatar', '').strip() - dianhua = request.data.get('dianhua', '').strip() - wechat = request.data.get('wechat', '').strip() - zhuangtai = request.data.get('zhuangtai') - base_changes = [] - # 状态修改需要6600c - if zhuangtai is not None: - if '6600c' not in permissions: - return Response({'code': 403, 'msg': '无权限修改组长状态(需要6600c)'}) - try: - zt = int(zhuangtai) - if zt in (0, 1): - zuzhang.zhuangtai = zt - if zt != old_zhuangtai: - zt_map = {0: '禁用', 1: '正常'} - base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}→{zt_map.get(zt, zt)}') - except: - pass - if nickname: - if not boss: - boss = UserBoss.query.create(user=user, nickname=nickname) - else: - boss.nickname = nickname - boss.save() - if nickname != old_nickname: - base_changes.append(f'昵称:{old_nickname or "无"}→{nickname}') - if avatar: - user.Avatar = avatar - user.save() - base_changes.append('更新了头像') - if base_changes: - zuzhang.save() - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=operator, - leixing=XIUGAI_LEIXING_ZUZHANG, - qitashuoming=( - f'【后台】修改组长基础信息:{";".join(base_changes)},' - f'组长ID={yonghuid},操作人={operator}' - ), - ) - # 组长表没有电话/微信字段,如需存储可暂存到User或忽略 - # 这里不处理 - - # ---------- 2. 可提现金额修改(加:6600a;减:6600b)---------- - new_ketixian_jine = request.data.get('ketixian_jine') - if new_ketixian_jine is not None: - try: - new_val = Decimal(str(new_ketixian_jine)) - if new_val < 0: - return Response({'code': 400, 'msg': '可提现金额不能为负数'}) - except: - return Response({'code': 400, 'msg': '金额格式错误'}) - if new_val > old_ketixian_jine: - if '6600a' not in permissions: - return Response({'code': 403, 'msg': '无权限增加可提现金额(需要6600a)'}) - action_desc = f'增加{new_val - old_ketixian_jine}元' - elif new_val < old_ketixian_jine: - if '6600b' not in permissions: - return Response({'code': 403, 'msg': '无权限减少可提现金额(需要6600b)'}) - action_desc = f'减少{old_ketixian_jine - new_val}元' - else: - action_desc = '无变动' - if new_val != old_ketixian_jine: - zuzhang.ketixian_jine = new_val - zuzhang.save(update_fields=['ketixian_jine']) - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=operator, - leixing=XIUGAI_LEIXING_ZUZHANG, - xiugaitijiao=new_val, - xiugaitijiaoq=old_ketixian_jine, - qitashuoming=( - f'【后台】修改组长可提现金额:{old_ketixian_jine}元 → {new_val}元({action_desc}),' - f'组长ID={yonghuid},操作人={operator}' - ), - ) - - # ---------- 3. 提现限额修改(需要6600d)---------- - kaioi_ewai = request.data.get('kaioi_ewai_tixian') - ewai_val = request.data.get('ewai_tixian_xiane') - if kaioi_ewai is not None or ewai_val is not None: - if '6600d' not in permissions: - return Response({'code': 403, 'msg': '无权限修改提现限额(需要6600d)'}) - limit_changes = [] - old_kaioi = zuzhang.kaioi_ewai_tixian - old_ewai = zuzhang.ewai_tixian_xiane - if kaioi_ewai is not None: - zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai) - if bool(kaioi_ewai) != old_kaioi: - limit_changes.append(f'额外限额开关:{"开" if old_kaioi else "关"}→{"开" if kaioi_ewai else "关"}') - if ewai_val is not None: - try: - zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val)) - if zuzhang.ewai_tixian_xiane != old_ewai: - limit_changes.append(f'额外限额金额:{old_ewai}→{zuzhang.ewai_tixian_xiane}') - except: - return Response({'code': 400, 'msg': '额外限额金额格式错误'}) - zuzhang.save() - if limit_changes: - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=operator, - leixing=XIUGAI_LEIXING_ZUZHANG, - qitashuoming=( - f'【后台】修改组长提现限额:{";".join(limit_changes)},' - f'组长ID={yonghuid},操作人={operator}' - ), - ) - - # ---------- 4. 分红相关修改(需要6600e)---------- - perm_enabled = request.data.get('permanent_enabled') - perm_amount = request.data.get('permanent_amount') - custom_first = request.data.get('custom_first_dividend') - extra_map = request.data.get('extra_dividend_map') - - if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]): - if '6600e' not in permissions: - return Response({'code': 403, 'msg': '无权限修改分红策略(需要6600e)'}) - - # 4.1 永久分红(额外分红开关和金额) - if perm_enabled is not None: - zuzhang.kaioi_ewai_fenhong = bool(perm_enabled) - if perm_amount is not None: - try: - zuzhang.ewai_fenhong_jine = Decimal(str(perm_amount)) - except: - return Response({'code': 400, 'msg': '永久分红金额格式错误'}) - zuzhang.save() - - # 4.2 首次定制分红(cishu=1) - if custom_first is not None and isinstance(custom_first, dict): - from jituan.services.club_penalty import resolve_gsfenhong_club_id - zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid) - existing_first = DuociFenhong.query.filter( - club_id=zuzhang_club, yonghuid=yonghuid, cishu=1, - ) - for huiyuan_id, amount in custom_first.items(): - try: - amount = Decimal(str(amount)) - except: - continue - if amount <= 0: - # 删除定制(恢复默认) - DuociFenhong.query.filter( - club_id=zuzhang_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1, - ).delete() - else: - DuociFenhong.query.update_or_create( - club_id=zuzhang_club, - yonghuid=yonghuid, - huiyuan=huiyuan_id, - cishu=1, - defaults={ - 'zuzhang_fenhong': amount, - 'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红 - } - ) - # 删除那些在前端字典中不存在的首次定制记录(即已取消定制) - for record in existing_first: - if record.huiyuan not in custom_first: - record.delete() - - # 4.3 额外次数分红(cishu >= 2) - if extra_map is not None and isinstance(extra_map, dict): - from jituan.services.club_penalty import resolve_gsfenhong_club_id - zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid) - keep_set = set() - for times_str, members in extra_map.items(): - try: - cishu = int(times_str) - if cishu < 2: - continue - except: - continue - if not isinstance(members, dict): - continue - for huiyuan_id, data in members.items(): - if not isinstance(data, dict): - continue - amount = data.get('zuzhang_fenhong') - if amount is None: - continue - try: - amount = Decimal(str(amount)) - except: - continue - keep_set.add((cishu, huiyuan_id)) - DuociFenhong.query.update_or_create( - club_id=zuzhang_club, - yonghuid=yonghuid, - huiyuan=huiyuan_id, - cishu=cishu, - defaults={ - 'zuzhang_fenhong': amount, - 'guanshi_fenhong': Decimal('0') - } - ) - # 删除不在 keep_set 中的额外次数记录 - existing_extra = DuociFenhong.query.filter( - club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2, - ) - for record in existing_extra: - if (record.cishu, record.huiyuan) not in keep_set: - record.delete() - - dividend_parts = [] - if perm_enabled is not None: - dividend_parts.append(f'永久分红开关→{"开" if perm_enabled else "关"}') - if perm_amount is not None: - dividend_parts.append(f'永久分红金额→{perm_amount}') - if custom_first is not None: - dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)') - if extra_map is not None: - dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)') - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=operator, - leixing=XIUGAI_LEIXING_ZUZHANG, - qitashuoming=( - f'【后台】修改组长分红策略:{";".join(dividend_parts) or "配置已更新"},' - f'组长ID={yonghuid},操作人={operator}' - ), - ) - - # 保存组长其他可能修改的字段(例如电话、微信没有,忽略) - - return Response({'code': 0, 'msg': '修改成功'}) - - -class ChangeInviterView(APIView): - """ - 后台更换邀请人/上级 - POST /houtai/htghyqr - 参数: - username: 操作人账号 - yonghuid: 被修改用户ID - target_type: dashou(打手)| guanshi(管事) - new_yaoqingren: 新邀请人用户ID(须在 User 主表存在) - 权限: - dashou → 001ee - guanshi → 4400a - 校验: - 打手新上级须为管事;管事新上级须为组长;不可设为自己 - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - yonghuid = request.data.get('yonghuid', '').strip() - target_type = request.data.get('target_type', '').strip().lower() - new_yaoqingren = request.data.get('new_yaoqingren', '').strip() - - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - if not yonghuid: - return Response({'code': 400, 'msg': '缺少用户ID'}) - if target_type not in ('dashou', 'guanshi'): - return Response({'code': 400, 'msg': 'target_type 须为 dashou 或 guanshi'}) - if not new_yaoqingren: - return Response({'code': 400, 'msg': '缺少新邀请人ID'}) - if new_yaoqingren == yonghuid: - return Response({'code': 400, 'msg': '不能将自己设为邀请人'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - if target_type == 'dashou': - if '001ee' not in permissions: - return Response({'code': 403, 'msg': '无权限更换打手邀请人(需要001ee)'}) - else: - if '4400a' not in permissions: - return Response({'code': 403, 'msg': '无权限更换管事邀请人(需要4400a)'}) - - try: - target_user = User.query.get(UserUID=yonghuid) - except User.DoesNotExist: - return Response({'code': 404, 'msg': '被修改用户不存在'}) - - try: - inviter_user = User.query.get(UserUID=new_yaoqingren) - except User.DoesNotExist: - return Response({'code': 404, 'msg': '新邀请人用户ID不存在'}) - - operator = kefu.user.Phone - - with transaction.atomic(): - if target_type == 'dashou': - try: - dashou = target_user.DashouProfile - except UserDashou.DoesNotExist: - return Response({'code': 404, 'msg': '用户不是打手'}) - try: - guanshi_profile = inviter_user.GuanshiProfile - except UserGuanshi.DoesNotExist: - return Response({'code': 400, 'msg': '新邀请人不是管事,无法作为打手上级'}) - if guanshi_profile.zhuangtai != 1: - return Response({'code': 400, 'msg': '新邀请人管事账号已禁用'}) - old_inviter = dashou.yaoqingren or '无' - dashou.yaoqingren = new_yaoqingren - dashou.save(update_fields=['yaoqingren']) - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=operator, - leixing=XIUGAI_LEIXING_DASHOU, - qitashuoming=( - f'【后台】更换打手邀请人:{old_inviter} → {new_yaoqingren},' - f'打手ID={yonghuid},新上级须为管事(已校验),操作人={operator}' - ), - ) - else: - try: - guanshi = target_user.GuanshiProfile - except UserGuanshi.DoesNotExist: - return Response({'code': 404, 'msg': '用户不是管事'}) - try: - zuzhang_profile = inviter_user.ZuzhangProfile - except UserZuzhang.DoesNotExist: - return Response({'code': 400, 'msg': '新邀请人不是组长,无法作为管事上级'}) - if zuzhang_profile.zhuangtai != 1: - return Response({'code': 400, 'msg': '新邀请人组长账号已禁用'}) - old_inviter = guanshi.yaoqingren or '无' - guanshi.yaoqingren = new_yaoqingren - guanshi.save(update_fields=['yaoqingren']) - write_xiugai_log( - yonghuid=yonghuid, - xiugaiid=operator, - leixing=XIUGAI_LEIXING_GUANSHI, - qitashuoming=( - f'【后台】更换管事邀请人:{old_inviter} → {new_yaoqingren},' - f'管事ID={yonghuid},新上级须为组长(已校验),操作人={operator}' - ), - ) - - return Response({ - 'code': 0, - 'msg': '邀请人更换成功', - 'data': {'yonghuid': yonghuid, 'new_yaoqingren': new_yaoqingren}, - }) - - -class GetOperationLogListView(APIView): - """ - 后台操作日志列表(xiugaijilu) - POST /houtai/hqczrz - 权限:000001 超级权限 或 czrz666 - """ - permission_classes = [] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 401, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '000001' not in permissions and 'czrz666' not in permissions: - return Response({'code': 403, 'msg': '无权限查看操作日志(需要000001或czrz666)'}) - - yonghuid = request.data.get('yonghuid', '').strip() - xiugaiid = request.data.get('xiugaiid', '').strip() - leixing = request.data.get('leixing') - keyword = request.data.get('keyword', '').strip() - try: - page = max(1, int(request.data.get('page', 1))) - except (ValueError, TypeError): - page = 1 - try: - page_size = min(100, max(1, int(request.data.get('page_size', 20)))) - except (ValueError, TypeError): - page_size = 20 - - qs = Xiugaijilu.query.all().order_by('-CreateTime') - from jituan.services.admin_audit import filter_xiugaijilu_by_request - qs = filter_xiugaijilu_by_request(qs, request) - if yonghuid: - qs = qs.filter(yonghuid=yonghuid) - if xiugaiid: - qs = qs.filter(xiugaiid__icontains=xiugaiid) - if leixing not in (None, ''): - try: - qs = qs.filter(leixing=int(leixing)) - except (ValueError, TypeError): - pass - if keyword: - qs = qs.filter(qitashuoming__icontains=keyword) - - total = qs.count() - start = (page - 1) * page_size - records = qs[start:start + page_size] - - def _fmt_decimal(val): - if val is None: - return None - return str(val) - - data_list = [] - for r in records: - data_list.append({ - 'id': r.id, - 'yonghuid': r.yonghuid, - 'xiugaiid': r.xiugaiid, - 'leixing': r.leixing, - 'leixing_label': XIUGAI_LEIXING_LABEL.get(r.leixing, f'类型{r.leixing}'), - 'xiugaitijiao': _fmt_decimal(r.xiugaitijiao), - 'xiugaitijiaoq': _fmt_decimal(r.xiugaitijiaoq), - 'jifen': r.jifen, - 'yjifen': r.yjifen, - 'yajin': _fmt_decimal(r.yajin), - 'yyajin': _fmt_decimal(r.yyajin), - 'shangjiayue': _fmt_decimal(r.shangjiayue), - 'shangjiayueq': _fmt_decimal(r.shangjiayueq), - 'guanshiyue': _fmt_decimal(r.guanshiyue), - 'guanshiyueq': _fmt_decimal(r.guanshiyueq), - 'huiyuants': r.huiyuants, - 'huiyuan_id': r.huiyuan_id, - 'qitashuoming': r.qitashuoming or '', - 'create_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '', - }) - - from jituan.services.club_user_access import list_response_meta - return Response({ - 'code': 0, - 'data': { - 'list': data_list, - 'total': total, - 'page': page, - 'page_size': page_size, - **list_response_meta(request), - }, - }) - - -class GetProductTypeZoneView(APIView): - """ - 获取商品类型、专区及会员列表 - 权限:需要拥有 7007a - 请求:POST /houtai/hthqsplxzq - 参数:{"username": "客服手机号"} - 返回: - code: 0 - data: { - type_list: [{id, jieshao, tupian_url, yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, paixu, product_count}, ...], - zone_list: [{id, mingzi, leixing_id, shenhezhuangtai, paixu}, ...], - member_list: [{huiyuan_id, jieshao, jiage}, ...] - } - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '7007a' not in permissions: - return Response({'code': 403, 'msg': '您无权访问此页面'}) - - # 查询所有商品类型(含专区数量) - types = ShangpinLeixing.query.all().order_by('-paixu') # paixu 越大越靠前 - type_list = [] - for t in types: - # 统计该类型下的专区数量 - zone_count = ShangpinZhuanqu.query.filter(leixing_id=t.id).count() - type_list.append({ - 'id': t.id, - 'jieshao': t.jieshao or '', - 'tupian_url': t.tupian_url or '', - 'yaoqiuleixing': t.yaoqiuleixing, - 'huiyuan_id': t.huiyuan_id or '', - 'yongjin': float(t.yongjin) if t.yongjin is not None else None, - 'shenhezhuangtai': t.shenhezhuangtai, - 'paixu': t.paixu, - 'product_count': zone_count, - }) - - # 查询所有专区 - zones = ShangpinZhuanqu.query.all().order_by('-paixu') - zone_list = [ - { - 'id': z.id, - 'mingzi': z.mingzi or '', - 'leixing_id': z.leixing_id, - 'shenhezhuangtai': z.shenhezhuangtai, - 'paixu': z.paixu, - } - for z in zones - ] - - # 查询所有会员(只返回 id、介绍、价格) - members = Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage') - member_list = [ - { - 'huiyuan_id': m['huiyuan_id'], - 'jieshao': m['jieshao'], - 'jiage': float(m['jiage']), - } - for m in members - ] - - return Response({ - 'code': 0, - 'data': { - 'type_list': type_list, - 'zone_list': zone_list, - 'member_list': member_list, - } - }) - - - - -class ModifyProductTypeZoneView(APIView): - """ - 统一修改商品类型/专区(增、删、改) - 权限:需要拥有 7007a - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser, MultiPartParser, FormParser] - - def post(self, request): - username = request.data.get('username', '').strip() - obj_type = request.data.get('type') # 'leixing' 或 'zhuanqu' - action = request.data.get('action') # 'add', 'update', 'delete' - - if not username or not obj_type or not action: - return Response({'code': 400, 'msg': '缺少必要参数'}) - - # 权限校验 - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '7007a' not in permissions: - return Response({'code': 403, 'msg': '您无权进行此操作'}) - - if obj_type == 'leixing': - return self.handle_leixing(request, action) - elif obj_type == 'zhuanqu': - return self.handle_zhuanqu(request, action) - else: - return Response({'code': 400, 'msg': '无效的type参数'}) - - # ---------- 商品类型处理 ---------- - def handle_leixing(self, request, action): - if action == 'add': - return self.add_leixing(request) - elif action == 'update': - return self.update_leixing(request) - elif action == 'delete': - return self.delete_leixing(request) - else: - return Response({'code': 400, 'msg': '无效的action'}) - - def add_leixing(self, request): - # 获取并校验字段 - jieshao = request.data.get('jieshao', '').strip() - if not jieshao: - return Response({'code': 400, 'msg': '类型名称不能为空'}) - - # 强制转为整数 - try: - yaoqiuleixing = int(request.data.get('yaoqiuleixing', 0)) - if yaoqiuleixing not in [1, 2]: - raise ValueError - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'}) - - try: - shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) - if shenhezhuangtai not in [1, 2]: - raise ValueError - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) - - huiyuan_id = request.data.get('huiyuan_id', '').strip() or None - yongjin = request.data.get('yongjin') - if yongjin is not None and yongjin != '': - try: - yongjin = Decimal(str(yongjin)) - except: - return Response({'code': 400, 'msg': '佣金金额格式错误'}) - else: - yongjin = None - - # 图片校验 - image_file = request.FILES.get('file') - if not image_file: - return Response({'code': 400, 'msg': '请上传类型图片'}) - - # 验证图片 - try: - valid, msg = validate_image(image_file) - if not valid: - return Response({'code': 400, 'msg': msg}) - except ImportError: - pass - - # 上传图片 - ext = image_file.name.split('.')[-1].lower() - new_filename = f"{uuid.uuid4().hex}.{ext}" - oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" - url = upload_to_oss(image_file, oss_path) - if not url: - return Response({'code': 500, 'msg': '图片上传失败'}) - - # 创建类型 - try: - with transaction.atomic(): - leixing = ShangpinLeixing.query.create( - jieshao=jieshao, - tupian_url=oss_path, - yaoqiuleixing=yaoqiuleixing, - huiyuan_id=huiyuan_id, - yongjin=yongjin, - shenhezhuangtai=shenhezhuangtai, - paixu=0 - ) - return Response({'code': 0, 'msg': '添加成功', 'data': {'id': leixing.id}}) - except Exception as e: - logger.error(f"添加类型失败: {e}", exc_info=True) - delete_from_oss(oss_path) - return Response({'code': 500, 'msg': '数据库错误'}) - - def update_leixing(self, request): - type_id = request.data.get('id') - if not type_id: - return Response({'code': 400, 'msg': '缺少类型ID'}) - try: - leixing = ShangpinLeixing.query.get(id=type_id) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 404, 'msg': '类型不存在'}) - - changes = {} - # 名称 - jieshao = request.data.get('jieshao') - if jieshao is not None and jieshao.strip() != leixing.jieshao: - changes['jieshao'] = jieshao.strip() - - # 抢单要求类型(强制转整数) - yaoqiuleixing = request.data.get('yaoqiuleixing') - if yaoqiuleixing is not None: - try: - new_val = int(yaoqiuleixing) - if new_val not in [1, 2]: - raise ValueError - if new_val != leixing.yaoqiuleixing: - changes['yaoqiuleixing'] = new_val - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'}) - - # 会员ID - huiyuan_id = request.data.get('huiyuan_id', '').strip() or None - if huiyuan_id != leixing.huiyuan_id: - changes['huiyuan_id'] = huiyuan_id - - # 佣金金额 - yongjin = request.data.get('yongjin') - if yongjin is not None: - if yongjin == '': - new_val = None - else: - try: - new_val = Decimal(str(yongjin)) - except: - return Response({'code': 400, 'msg': '佣金金额格式错误'}) - if new_val != leixing.yongjin: - changes['yongjin'] = new_val - - # 审核状态 - shenhezhuangtai = request.data.get('shenhezhuangtai') - if shenhezhuangtai is not None: - try: - new_val = int(shenhezhuangtai) - if new_val not in [1, 2]: - raise ValueError - if new_val != leixing.shenhezhuangtai: - changes['shenhezhuangtai'] = new_val - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) - - # 图片处理 - image_file = request.FILES.get('file') - old_image_path = leixing.tupian_url - if image_file: - try: - valid, msg = validate_image(image_file) - if not valid: - return Response({'code': 400, 'msg': msg}) - except ImportError: - pass - ext = image_file.name.split('.')[-1].lower() - new_filename = f"{uuid.uuid4().hex}.{ext}" - oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" - url = upload_to_oss(image_file, oss_path) - if not url: - return Response({'code': 500, 'msg': '图片上传失败'}) - changes['tupian_url'] = oss_path - - if not changes and not image_file: - return Response({'code': 400, 'msg': '未做任何修改'}) - - sync_to_products = request.data.get('sync_to_products') in [True, 'true', '1', 'on'] - try: - with transaction.atomic(): - for field, value in changes.items(): - setattr(leixing, field, value) - leixing.save() - - # 同步商品 - if sync_to_products and ('yaoqiuleixing' in changes or 'huiyuan_id' in changes or 'yongjin' in changes): - product_updates = {} - if 'yaoqiuleixing' in changes: - product_updates['yaoqiuleixing'] = changes['yaoqiuleixing'] - if 'huiyuan_id' in changes: - product_updates['huiyuan_id'] = changes['huiyuan_id'] - if 'yongjin' in changes: - product_updates['yongjin'] = changes['yongjin'] - if product_updates: - Shangpin.query.filter(leixing_id=type_id).update(**product_updates) - - # 删除旧图片 - if image_file and old_image_path: - delete_from_oss(old_image_path) - - return Response({'code': 0, 'msg': '修改成功'}) - except Exception as e: - logger.error(f"更新类型失败: {e}", exc_info=True) - if image_file and 'oss_path' in locals(): - delete_from_oss(oss_path) - return Response({'code': 500, 'msg': '修改失败'}) - - def delete_leixing(self, request): - type_id = request.data.get('id') - if not type_id: - return Response({'code': 400, 'msg': '缺少类型ID'}) - delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on'] - - try: - leixing = ShangpinLeixing.query.get(id=type_id) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 404, 'msg': '类型不存在'}) - - with transaction.atomic(): - # 删除该类型下的所有专区 - zones = ShangpinZhuanqu.query.filter(leixing_id=type_id) - if delete_products: - # 删除专区下的所有商品 - for zone in zones: - Shangpin.query.filter(zhuanqu_id=zone.id).delete() - zones.delete() - # 删除直接关联该类型的商品 - Shangpin.query.filter(leixing_id=type_id).delete() - else: - zones.delete() - # 仅清除商品的类型关联 - Shangpin.query.filter(leixing_id=type_id).update(leixing_id=None) - - leixing.delete() - - if leixing.tupian_url: - delete_from_oss(leixing.tupian_url) - - return Response({'code': 0, 'msg': '删除成功'}) - - # ---------- 商品专区处理 ---------- - def handle_zhuanqu(self, request, action): - if action == 'add': - return self.add_zhuanqu(request) - elif action == 'update': - return self.update_zhuanqu(request) - elif action == 'delete': - return self.delete_zhuanqu(request) - else: - return Response({'code': 400, 'msg': '无效的action'}) - - def add_zhuanqu(self, request): - mingzi = request.data.get('mingzi', '').strip() - leixing_id = request.data.get('leixing_id') - if not mingzi: - return Response({'code': 400, 'msg': '专区名称不能为空'}) - if not leixing_id: - return Response({'code': 400, 'msg': '请选择所属类型'}) - - try: - leixing_id = int(leixing_id) - if not ShangpinLeixing.query.filter(id=leixing_id).exists(): - return Response({'code': 404, 'msg': '所属类型不存在'}) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '所属类型ID无效'}) - - try: - shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) - if shenhezhuangtai not in [1, 2]: - raise ValueError - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) - - try: - with transaction.atomic(): - zhuanqu = ShangpinZhuanqu.query.create( - mingzi=mingzi, - leixing_id=leixing_id, - shenhezhuangtai=shenhezhuangtai, - paixu=0 - ) - return Response({'code': 0, 'msg': '添加成功', 'data': {'id': zhuanqu.id}}) - except Exception as e: - logger.error(f"添加专区失败: {e}", exc_info=True) - return Response({'code': 500, 'msg': '数据库错误'}) - - def update_zhuanqu(self, request): - zone_id = request.data.get('id') - if not zone_id: - return Response({'code': 400, 'msg': '缺少专区ID'}) - try: - zhuanqu = ShangpinZhuanqu.query.get(id=zone_id) - except ShangpinZhuanqu.DoesNotExist: - return Response({'code': 404, 'msg': '专区不存在'}) - - changes = {} - mingzi = request.data.get('mingzi') - if mingzi is not None and mingzi.strip() != zhuanqu.mingzi: - changes['mingzi'] = mingzi.strip() - - leixing_id = request.data.get('leixing_id') - if leixing_id is not None: - try: - new_id = int(leixing_id) - if not ShangpinLeixing.query.filter(id=new_id).exists(): - return Response({'code': 404, 'msg': '新所属类型不存在'}) - if new_id != zhuanqu.leixing_id: - changes['leixing_id'] = new_id - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '所属类型ID无效'}) - - shenhezhuangtai = request.data.get('shenhezhuangtai') - if shenhezhuangtai is not None: - try: - new_val = int(shenhezhuangtai) - if new_val not in [1, 2]: - raise ValueError - if new_val != zhuanqu.shenhezhuangtai: - changes['shenhezhuangtai'] = new_val - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) - - if not changes: - return Response({'code': 400, 'msg': '未做任何修改'}) - - try: - with transaction.atomic(): - for field, value in changes.items(): - setattr(zhuanqu, field, value) - zhuanqu.save() - return Response({'code': 0, 'msg': '修改成功'}) - except Exception as e: - logger.error(f"更新专区失败: {e}", exc_info=True) - return Response({'code': 500, 'msg': '修改失败'}) - - def delete_zhuanqu(self, request): - zone_id = request.data.get('id') - if not zone_id: - return Response({'code': 400, 'msg': '缺少专区ID'}) - delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on'] - - try: - zhuanqu = ShangpinZhuanqu.query.get(id=zone_id) - except ShangpinZhuanqu.DoesNotExist: - return Response({'code': 404, 'msg': '专区不存在'}) - - with transaction.atomic(): - if delete_products: - Shangpin.query.filter(zhuanqu_id=zone_id).delete() - else: - Shangpin.query.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None) - zhuanqu.delete() - - return Response({'code': 0, 'msg': '删除成功'}) - - -# 继续在 houtai/views_product.py 中添加 -class GetRateView(APIView): - """ - 获取打手分成费率(平台订单和商家订单) - 权限:需要拥有 7007b - 请求:POST /houtai/hthqfhpz - 参数:{"username": "客服手机号"} - 返回:{"code":0, "data": {"platform_rate": xx, "merchant_rate": xx}} - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '缺少username'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '7007b' not in permissions: - return Response({'code': 403, 'msg': '您无权查看费率'}) - - try: - platform_rate_obj = CommissionRate.query.get(Platform='1') - merchant_rate_obj = CommissionRate.query.get(Platform='3') - except CommissionRate.DoesNotExist as e: - logger.error(f"费率记录缺失: {e}") - return Response({'code': 500, 'msg': '费率配置缺失'}) - - return Response({ - 'code': 0, - 'data': { - 'platform_rate': float(platform_rate_obj.Rate) if platform_rate_obj.Rate else 0, - 'merchant_rate': float(merchant_rate_obj.Rate) if merchant_rate_obj.Rate else 0, - } - }) - - -class ModifyRateView(APIView): - """ - 修改打手分成费率 - 权限:需要拥有 7007b - 请求:POST /houtai/htxgfl - 参数:{"username": "客服手机号", "type": "platform" / "merchant", "value": 费率(百分比数字)} - 返回:{"code":0, "msg":"修改成功"} - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - rate_type = request.data.get('type') - value = request.data.get('value') - - if not username or not rate_type or value is None: - return Response({'code': 400, 'msg': '缺少参数'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if '7007b' not in permissions: - return Response({'code': 403, 'msg': '您无权修改费率'}) - - try: - rate_value = Decimal(str(value)) - except: - return Response({'code': 400, 'msg': '费率值格式错误'}) - - - if rate_type == 'platform': - Platform = '1' - elif rate_type == 'merchant': - Platform = '3' - else: - return Response({'code': 400, 'msg': '无效的type参数'}) - - try: - with transaction.atomic(): - rate_obj = CommissionRate.objects.select_for_update().get(Platform=Platform) - rate_obj.Rate = rate_value - rate_obj.save() - except CommissionRate.DoesNotExist: - return Response({'code': 404, 'msg': '费率记录不存在'}) - except Exception as e: - logger.error(f"修改费率失败: {e}", exc_info=True) - return Response({'code': 500, 'msg': '修改失败'}) - - return Response({'code': 0, 'msg': '修改成功'}) - - - - - - -# 权限码(根据你要求,使用 '8080a') -REQUIRED_PERMISSION = '8080a' -class PopupNoticeListAPIView(APIView): - """ - 获取所有弹窗配置(页面 + 弹窗 + 图片) - URL: POST /houtai/hthqtcxx - 请求参数: { "username": "客服账号" } - 响应格式: { "code": 0, "data": { "pages": [...] } } - 注意:所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段区分 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取前端传递的 username(用于防越权) - username_frontend = request.data.get('username') - # 2. 调用公共权限验证方法,返回 (客服对象, 权限列表) - kefu, permissions = verify_kefu_permission(request, username_frontend) - if kefu is None: - # 身份验证失败,返回业务码 403,但 HTTP 状态码仍是 200 - return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) - - # 3. 检查是否拥有所需权限码 '8080a' - if REQUIRED_PERMISSION not in permissions: - return Response({'code': 403, 'msg': '您没有权限访问弹窗管理功能'}) - - # 4. 查询所有页面,并预加载关联的弹窗和图片(避免N+1查询) - from jituan.services.display_config import filter_popup_page_by_request, list_response_meta - - pages = filter_popup_page_by_request( - PopupPage.query.all().prefetch_related('popups__images'), - request, - ).order_by('id') - - # 5. 序列化输出 - serializer = PopupPageSerializer(pages, many=True) - return Response({ - 'code': 0, - 'data': {'pages': serializer.data, **list_response_meta(request)}, - }) - - -class PopupNoticeModifyAPIView(APIView): - """ - 统一修改接口:支持页面、弹窗、图片的增删改 - URL: POST /houtai/htxgtcxx - 请求中必须包含 action 字段,用于区分操作类型。 - 所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段体现。 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # ---------- 1. 权限验证 ---------- - username_frontend = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username_frontend) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) - if REQUIRED_PERMISSION not in permissions: - return Response({'code': 403, 'msg': '您没有权限操作弹窗管理'}) - - from jituan.services.display_config import forbid_display_write_in_all_scope - deny = forbid_display_write_in_all_scope(request) - if deny: - return deny - - # ---------- 2. 获取 action 并分发 ---------- - action = request.data.get('action') - if not action: - return Response({'code': 400, 'msg': '缺少action参数'}) - - try: - if action == 'add_page': - return self.add_page(request) - elif action == 'update_page': - return self.update_page(request) - elif action == 'delete_page': - return self.delete_page(request) - elif action == 'add_popup': - return self.add_popup(request) - elif action == 'update_popup': - return self.update_popup(request) - elif action == 'delete_popup': - return self.delete_popup(request) - elif action == 'add_image': - return self.add_image(request) - elif action == 'update_image': - return self.update_image(request) - elif action == 'delete_image': - return self.delete_image(request) - else: - return Response({'code': 400, 'msg': f'未知的action: {action}'}) - except Exception as e: - # 捕获未预期的异常,返回服务器错误 - traceback.print_exc() - return Response({'code': 500, 'msg': f'服务器错误: {str(e)}'}) - - # ==================== 页面相关操作 ==================== - - def add_page(self, request): - """添加新页面""" - from jituan.services.club_context import resolve_club_id_from_request - from jituan.services.display_config import forbid_display_write_in_all_scope - - deny = forbid_display_write_in_all_scope(request) - if deny: - return deny - - data = request.data - # 必填字段校验 - if not data.get('page_key') or not data.get('name'): - return Response({'code': 400, 'msg': '缺少 page_key 或 name'}) - club_id = resolve_club_id_from_request(request) - # 唯一性校验(同俱乐部内) - if PopupPage.query.filter(club_id=club_id, page_key=data['page_key']).exists(): - return Response({'code': 400, 'msg': '页面标识已存在'}) - - page = PopupPage.query.create( - club_id=club_id, - page_key=data['page_key'], - name=data['name'], - description=data.get('description', ''), - is_active=data.get('is_active', True), - ignore_user_mute=data.get('ignore_user_mute', False) - ) - return Response({'code': 0, 'msg': '添加成功', 'data': {'id': page.id}}) - - def update_page(self, request): - """更新页面信息""" - page_id = request.data.get('id') - if not page_id: - return Response({'code': 400, 'msg': '缺少页面id'}) - try: - page = PopupPage.query.get(id=page_id) - except PopupPage.DoesNotExist: - return Response({'code': 404, 'msg': '页面不存在'}) - - from jituan.services.display_config import forbid_popup_page_out_of_scope - deny = forbid_popup_page_out_of_scope(request, page) - if deny: - return deny - - # 更新允许修改的字段 - page.page_key = request.data.get('page_key', page.page_key) - page.name = request.data.get('name', page.name) - page.description = request.data.get('description', page.description) - page.is_active = request.data.get('is_active', page.is_active) - page.ignore_user_mute = request.data.get('ignore_user_mute', page.ignore_user_mute) - page.save() - return Response({'code': 0, 'msg': '更新成功'}) - - def delete_page(self, request): - """删除页面,可级联删除其下所有弹窗和图片""" - page_id = request.data.get('id') - cascade = request.data.get('cascade_delete_popups', True) # 默认级联 - if not page_id: - return Response({'code': 400, 'msg': '缺少页面id'}) - try: - page = PopupPage.query.get(id=page_id) - except PopupPage.DoesNotExist: - return Response({'code': 404, 'msg': '页面不存在'}) - - from jituan.services.display_config import forbid_popup_page_out_of_scope, forbid_display_write_in_all_scope - deny = forbid_display_write_in_all_scope(request) or forbid_popup_page_out_of_scope(request, page) - if deny: - return deny - - if cascade: - # 级联删除:使用事务保证原子性 - with transaction.atomic(): - popups = page.popups.all() - for popup in popups: - # 删除弹窗下的所有图片文件 - for img in popup.images.all(): - if img.image_url: - delete_from_oss(img.image_url) - popups.delete() - page.delete() - else: - # 非级联删除:如果页面下还有弹窗,则拒绝删除 - if page.popups.exists(): - return Response({'code': 400, 'msg': '该页面下存在弹窗,请先删除弹窗或使用级联删除'}) - page.delete() - - return Response({'code': 0, 'msg': '删除成功'}) - - # ==================== 弹窗相关操作 ==================== - - def add_popup(self, request): - """为指定页面添加新弹窗""" - data = request.data - page_id = data.get('page_id') - popup_id = data.get('popup_id') - if not page_id or not popup_id: - return Response({'code': 400, 'msg': '缺少 page_id 或 popup_id'}) - try: - page = PopupPage.query.get(id=page_id) - except PopupPage.DoesNotExist: - return Response({'code': 404, 'msg': '页面不存在'}) - - from jituan.services.display_config import forbid_popup_page_out_of_scope - deny = forbid_popup_page_out_of_scope(request, page) - if deny: - return deny - - # 同一页面下 popup_id 必须唯一 - if PopupConfig.query.filter(page=page, popup_id=popup_id).exists(): - return Response({'code': 400, 'msg': '该页面下已存在相同的弹窗ID'}) - - popup = PopupConfig.query.create( - page=page, - popup_id=popup_id, - title=data.get('title', ''), - description=data.get('description', ''), - strategy_type=data.get('strategy_type', 'daily'), - max_count=data.get('max_count', 1), - reset_interval=data.get('reset_interval', 'day'), - duration=data.get('duration', 0), - force_even_muted=data.get('force_even_muted', False), - sort_order=data.get('sort_order', 0), - is_active=data.get('is_active', True), - start_time=data.get('start_time') or None, - end_time=data.get('end_time') or None - ) - return Response({'code': 0, 'msg': '添加成功', 'data': {'id': popup.id}}) - - def update_popup(self, request): - """更新弹窗信息""" - popup_id = request.data.get('id') - if not popup_id: - return Response({'code': 400, 'msg': '缺少弹窗id'}) - try: - popup = PopupConfig.query.get(id=popup_id) - except PopupConfig.DoesNotExist: - return Response({'code': 404, 'msg': '弹窗不存在'}) - - from jituan.services.display_config import forbid_popup_page_out_of_scope - deny = forbid_popup_page_out_of_scope(request, popup.page) - if deny: - return deny - - # 更新弹窗字段 - popup.popup_id = request.data.get('popup_id', popup.popup_id) - popup.title = request.data.get('title', popup.title) - popup.description = request.data.get('description', popup.description) - popup.strategy_type = request.data.get('strategy_type', popup.strategy_type) - popup.max_count = request.data.get('max_count', popup.max_count) - popup.reset_interval = request.data.get('reset_interval', popup.reset_interval) - popup.duration = request.data.get('duration', popup.duration) - popup.force_even_muted = request.data.get('force_even_muted', popup.force_even_muted) - popup.sort_order = request.data.get('sort_order', popup.sort_order) - popup.is_active = request.data.get('is_active', popup.is_active) - popup.start_time = request.data.get('start_time') or None - popup.end_time = request.data.get('end_time') or None - popup.save() - return Response({'code': 0, 'msg': '更新成功'}) - - def delete_popup(self, request): - """删除弹窗,同时删除其下所有图片(包括OSS文件)""" - popup_id = request.data.get('id') - if not popup_id: - return Response({'code': 400, 'msg': '缺少弹窗id'}) - try: - popup = PopupConfig.query.get(id=popup_id) - except PopupConfig.DoesNotExist: - return Response({'code': 404, 'msg': '弹窗不存在'}) - - from jituan.services.display_config import forbid_popup_page_out_of_scope - deny = forbid_popup_page_out_of_scope(request, popup.page) - if deny: - return deny - - with transaction.atomic(): - for img in popup.images.all(): - if img.image_url: - delete_from_oss(img.image_url) - popup.images.all().delete() - popup.delete() - return Response({'code': 0, 'msg': '删除成功'}) - - # ==================== 图片相关操作 ==================== - - def _upload_image_file(self, file_obj, page_key): - """ - 内部方法:将图片上传到OSS指定目录 - 目录: a_long/tanchuang/tanchuang/ - 文件名: {page_key}_{时间戳}_{uuid8}.{ext} - 返回: 相对路径(例如 a_long/tanchuang/tanchuang/dashouzhongxin_20250419120000_abc12345.jpg) - """ - # 验证图片格式和大小 - valid, msg = validate_image(file_obj) - if not valid: - raise ValueError(msg) - - # 生成唯一文件名 - ext = os.path.splitext(file_obj.name)[1].lower() - timestamp = datetime.now().strftime('%Y%m%d%H%M%S') - unique_id = str(uuid.uuid4())[:8] - filename = f"{page_key}_{timestamp}_{unique_id}{ext}" - relative_path = f"a_long/tanchuang/tanchuang/{filename}" - - # 上传到OSS - url = upload_to_oss(file_obj, relative_path) - if not url: - raise ValueError("图片上传失败") - return relative_path - - def add_image(self, request): - """为指定弹窗添加图片(支持上传文件)""" - popup_id = request.data.get('popup_id') - if not popup_id: - return Response({'code': 400, 'msg': '缺少 popup_id'}) - try: - popup = PopupConfig.query.get(id=popup_id) - except PopupConfig.DoesNotExist: - return Response({'code': 404, 'msg': '弹窗不存在'}) - - from jituan.services.display_config import forbid_popup_page_out_of_scope - deny = forbid_popup_page_out_of_scope(request, popup.page) - if deny: - return deny - - # 获取上传的文件 - file_obj = request.FILES.get('file') - if not file_obj: - return Response({'code': 400, 'msg': '未提供图片文件'}) - - try: - page_key = popup.page.page_key - relative_path = self._upload_image_file(file_obj, page_key) - except ValueError as e: - return Response({'code': 400, 'msg': str(e)}) - - # 保存图片记录 - image = PopupImage.query.create( - popup_config=popup, - image_url=relative_path, - text=request.data.get('text', ''), - sort_order=request.data.get('sort_order', 0) - ) - return Response({'code': 0, 'msg': '添加成功', 'data': {'id': image.id}}) - - def update_image(self, request): - """更新图片信息(可替换图片文件、修改文字、排序)""" - image_id = request.data.get('id') - if not image_id: - return Response({'code': 400, 'msg': '缺少图片id'}) - try: - image = PopupImage.query.get(id=image_id) - except PopupImage.DoesNotExist: - return Response({'code': 404, 'msg': '图片不存在'}) - - from jituan.services.display_config import forbid_popup_page_out_of_scope - deny = forbid_popup_page_out_of_scope(request, image.popup_config.page) - if deny: - return deny - - # 更新文字和排序 - image.text = request.data.get('text', image.text) - image.sort_order = request.data.get('sort_order', image.sort_order) - - # 如果提供了新图片文件,则替换 - file_obj = request.FILES.get('file') - if file_obj: - try: - page_key = image.popup_config.page.page_key - new_path = self._upload_image_file(file_obj, page_key) - except ValueError as e: - return Response({'code': 400, 'msg': str(e)}) - # 删除旧图片 - if image.image_url: - delete_from_oss(image.image_url) - image.image_url = new_path - - image.save() - return Response({'code': 0, 'msg': '更新成功'}) - - def delete_image(self, request): - """删除图片(同时删除OSS文件)""" - image_id = request.data.get('id') - if not image_id: - return Response({'code': 400, 'msg': '缺少图片id'}) - try: - image = PopupImage.query.get(id=image_id) - except PopupImage.DoesNotExist: - return Response({'code': 404, 'msg': '图片不存在'}) - - from jituan.services.display_config import forbid_popup_page_out_of_scope - deny = forbid_popup_page_out_of_scope(request, image.popup_config.page) - if deny: - return deny - - if image.image_url: - delete_from_oss(image.image_url) - image.delete() - return Response({'code': 0, 'msg': '删除成功'}) - - - -class ShopListView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 调用公共权限验证(与您给的示例完全一致) - username_frontend = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username_frontend) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) - - # 2. 检查是否拥有所需权限(999a~999e 任意一个) - required_perms = {'999a', '999b', '999c', '999d', '999e'} - if not required_perms.intersection(set(permissions)): - return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'}) - - # 3. 解析请求参数 - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 10)) - dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip() - zhuangtai = request.data.get('zhuangtai') - lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip() - user_id = request.data.get('user_id', '').strip() - ketixian_yue_min = request.data.get('ketixian_yue_min') - ketixian_yue_max = request.data.get('ketixian_yue_max') - zhengqu_zonge_min = request.data.get('zhengqu_zonge_min') - zhengqu_zonge_max = request.data.get('zhengqu_zonge_max') - - # 4. 构建查询集(先查店铺,预加载关联凭证) - queryset = Dianpu.query.select_related('pingzheng').all() - - if dianpu_mingcheng: - queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng) - if zhuangtai is not None: - queryset = queryset.filter(zhuangtai=zhuangtai) - if lianxi_dianhua: - queryset = queryset.filter(lianxi_dianhua__icontains=lianxi_dianhua) - if user_id: - queryset = queryset.filter(pingzheng__yonghu=user_id) - - # 金额范围过滤 - if ketixian_yue_min is not None: - queryset = queryset.filter(ketixian_yue__gte=ketixian_yue_min) - if ketixian_yue_max is not None: - queryset = queryset.filter(ketixian_yue__lte=ketixian_yue_max) - if zhengqu_zonge_min is not None: - queryset = queryset.filter(zhengqu_zonge__gte=zhengqu_zonge_min) - if zhengqu_zonge_max is not None: - queryset = queryset.filter(zhengqu_zonge__lte=zhengqu_zonge_max) - - # 5. 分页 - paginator = Paginator(queryset, page_size) - page_obj = paginator.get_page(page) - - # 6. 构造返回数据 - shop_list = [] - for dianpu in page_obj: - pingzheng = dianpu.pingzheng - shop_list.append({ - 'id': dianpu.id, - 'dianpu_mingcheng': dianpu.dianpu_mingcheng, - 'dianpu_touxiang': dianpu.dianpu_touxiang or '', - 'lianxi_dianhua': dianpu.lianxi_dianhua or '', - 'weixinhao': dianpu.weixinhao or '', - 'zhuangtai': dianpu.zhuangtai, - 'bangding_yonghushu': dianpu.bangding_yonghushu, - 'erweima_url': dianpu.erweima_url or '', - 'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng, - 'shouyi_choucheng_feilv': dianpu.shouyi_choucheng_feilv or 0, - 'ketixian_yue': dianpu.ketixian_yue, - 'zhengqu_zonge': dianpu.zhengqu_zonge, - 'meiri_tixian_xiane': dianpu.meiri_tixian_xiane or 0, - 'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane, - 'pingzheng__yonghu': pingzheng.yonghu, # 用户ID - 'pingzheng__zhanghao': pingzheng.zhanghao, # 登录账号 - }) - - # 获取默认配置(ID=1,不存在则创建) - default_config, _ = DianpuMorenPeizhi.query.get_or_create(id=1, defaults={ - 'moren_choucheng_feilv': 0.1, - 'moren_tixian_xiane': 1000.00 - }) - - return Response({ - 'code': 0, - 'msg': 'success', - 'data': { - 'list': shop_list, - 'total': paginator.count, - 'default_config': { - 'moren_choucheng_feilv': default_config.moren_choucheng_feilv, - 'moren_tixian_xiane': default_config.moren_tixian_xiane, - } - } - }) - - -class ShopModifyView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 身份验证 - username_frontend = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username_frontend) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) - - # 转换权限为集合方便判断 - user_perms = set(permissions) - action = request.data.get('action', '') - - # 2. 根据 action 分派任务 - if action == 'update_dianpu': - return self._update_dianpu(request, user_perms) - elif action == 'add_dianpu': - return self._add_dianpu(request, user_perms) - elif action == 'update_default': - return self._update_default(request, user_perms) - else: - return Response({'code': 400, 'msg': '未知操作类型'}) - - def _update_dianpu(self, request, user_perms): - """修改店铺信息,精确权限控制""" - dianpu_id = request.data.get('dianpu_id') - if not dianpu_id: - return Response({'code': 400, 'msg': '缺少店铺ID'}) - - try: - with transaction.atomic(): - dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id) - - # 基础信息修改 (999e) - if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']): - if '999e' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改店铺基本信息(999e)'}) - if 'dianpu_mingcheng' in request.data: - dianpu.dianpu_mingcheng = request.data['dianpu_mingcheng'] - if 'lianxi_dianhua' in request.data: - dianpu.lianxi_dianhua = request.data['lianxi_dianhua'] - if 'weixinhao' in request.data: - dianpu.weixinhao = request.data['weixinhao'] - - # 店铺状态 (999a) - if 'zhuangtai' in request.data: - if '999a' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改店铺状态(999a)'}) - new_status = int(request.data['zhuangtai']) - if new_status not in (0, 1): - return Response({'code': 400, 'msg': '店铺状态无效'}) - dianpu.zhuangtai = new_status - - # 可提现余额 (999b) - if 'ketixian_yue' in request.data: - if '999b' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改可提现余额(999b)'}) - dianpu.ketixian_yue = request.data['ketixian_yue'] - - # 提现限额相关 (999c) - if 'kaiqi_meiri_xiane' in request.data or 'meiri_tixian_xiane' in request.data: - if '999c' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改提现限额配置(999c)'}) - if 'kaiqi_meiri_xiane' in request.data: - dianpu.kaiqi_meiri_xiane = request.data['kaiqi_meiri_xiane'] - if 'meiri_tixian_xiane' in request.data: - dianpu.meiri_tixian_xiane = request.data['meiri_tixian_xiane'] - - # 抽成相关 (999d) - if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data: - if '999d' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改抽成配置(999d)'}) - if 'kaiqi_shouyi_choucheng' in request.data: - dianpu.kaiqi_shouyi_choucheng = request.data['kaiqi_shouyi_choucheng'] - if 'shouyi_choucheng_feilv' in request.data: - dianpu.shouyi_choucheng_feilv = request.data['shouyi_choucheng_feilv'] - - dianpu.save() # Django 会自动判断更新哪些字段 - return Response({'code': 0, 'msg': '店铺信息更新成功'}) - - except Dianpu.DoesNotExist: - return Response({'code': 404, 'msg': '店铺不存在'}) - except Exception as e: - logger.exception("修改店铺失败") - return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) - - def _add_dianpu(self, request, user_perms): - """添加店铺,需要权限 999b""" - if '999b' not in user_perms: - return Response({'code': 403, 'msg': '无权限添加店铺(999b)'}) - - yonghu_id = request.data.get('yonghu_id') - zhanghao = request.data.get('zhanghao') - mima = request.data.get('mima') - dianpu_mingcheng = request.data.get('dianpu_mingcheng') - - if not all([yonghu_id, zhanghao, mima, dianpu_mingcheng]): - return Response({'code': 400, 'msg': '必填字段缺失(用户ID、账号、密码、店铺名称)'}) - - # 检查用户是否存在 - if not User.query.filter(UserUID=yonghu_id).exists(): - return Response({'code': 404, 'msg': '用户不存在'}) - - # 检查凭证是否已存在 - if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists(): - return Response({'code': 400, 'msg': '登录账号已存在'}) - if YonghuPingzheng.query.filter(yonghu=yonghu_id).exists(): - return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'}) - - try: - with transaction.atomic(): - pingzheng = YonghuPingzheng.query.create( - yonghu=yonghu_id, - zhanghao=zhanghao, - mima=mima, - is_active=True - ) - dianpu = Dianpu.query.create( - pingzheng=pingzheng, - dianpu_mingcheng=dianpu_mingcheng, - lianxi_dianhua=request.data.get('lianxi_dianhua', ''), - weixinhao=request.data.get('weixinhao', ''), - kaiqi_shouyi_choucheng=request.data.get('kaiqi_shouyi_choucheng', False), - shouyi_choucheng_feilv=request.data.get('shouyi_choucheng_feilv', 0), - kaiqi_meiri_xiane=request.data.get('kaiqi_meiri_xiane', False), - meiri_tixian_xiane=request.data.get('meiri_tixian_xiane', 0), - ) - return Response({'code': 0, 'msg': '店铺创建成功', 'dianpu_id': dianpu.id}) - except IntegrityError as e: - logger.error("创建店铺违反唯一性约束: %s", e) - return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'}) - except Exception as e: - logger.exception("创建店铺失败") - return Response({'code': 500, 'msg': '创建失败,请稍后重试'}) - - def _update_default(self, request, user_perms): - """修改默认配置,按修改内容校验权限""" - feilv = request.data.get('moren_choucheng_feilv') - xiane = request.data.get('moren_tixian_xiane') - modify_commission = feilv is not None - modify_withdraw = xiane is not None - - if modify_commission and '999d' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'}) - if modify_withdraw and '999c' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'}) - - try: - config, _ = DianpuMorenPeizhi.query.get_or_create(id=1) - if modify_commission: - config.moren_choucheng_feilv = feilv - if modify_withdraw: - config.moren_tixian_xiane = xiane - config.save(update_fields=(['moren_choucheng_feilv'] if modify_commission else []) + - (['moren_tixian_xiane'] if modify_withdraw else [])) - return Response({'code': 0, 'msg': '默认配置更新成功'}) - except Exception as e: - logger.exception("更新默认配置失败") - return Response({'code': 500, 'msg': '更新失败'}) - - - - - - - - -# ==================== 1. 公共商品类型 + 审核模式 ==================== -class ShopPublicTypeAndAuditView(APIView): - """ - 返回:①正常状态的公共商品类型列表 ②全局商品审核模式开关 - 前端请求路径:/houtai/htdphqsplx - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # ---------- 身份与权限校验 ---------- - username = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) - - # 拥有 1199ab/1199abc/1199abd 任一个即可访问 - if not set(permissions).intersection(SHOP_PRODUCT_PERMS): - return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'}) - - # ---------- 查询公共商品类型(仅正常审核状态的) ---------- - try: - # shenhezhuangtai = 1 表示正常 (根据模型默认值) - types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values( - 'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin' - ) - type_list = [] - for t in types_qs: - type_list.append({ - 'id': t['id'], - 'jieshao': t['jieshao'] or '', - 'tupian_url': t['tupian_url'] or '', - 'yaoqiuleixing': t['yaoqiuleixing'], - 'huiyuan_id': t['huiyuan_id'], - 'yongjin': t['yongjin'], - }) - except Exception as e: - logger.exception("查询公共商品类型失败") - return Response({'code': 500, 'msg': '服务器错误,请稍后重试'}) - - # ---------- 获取审核模式配置 ---------- - audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() - kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False - - return Response({ - 'code': 0, - 'data': { - 'public_types': type_list, - 'kaiqi_shenhe': kaiqi_shenhe - } - }) - - -# ==================== 2. 店铺列表(支持搜索、筛选、分页) ==================== -class ShopListForProductView(APIView): - """ - 返回店铺列表,用于商品管理页面选择店铺。 - 支持:关键词搜索(店铺ID/名称/用户ID)、店铺状态筛选、分页。 - 前端请求路径:/houtai/htdphqdpys - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败'}) - if not set(permissions).intersection(SHOP_PRODUCT_PERMS): - return Response({'code': 403, 'msg': '无权访问店铺列表'}) - - # ---------- 安全解析分页参数 ---------- - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 10)) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '分页参数格式错误'}) - - # 搜索关键词(字符串,ORM 自动防注入) - keyword = str(request.data.get('keyword', '')).strip() - # 店铺状态筛选(1正常,0封禁,不传则全部) - zhuangtai = request.data.get('zhuangtai') - - # ---------- 构建查询集(关联凭证表获取用户ID) ---------- - shops = Dianpu.query.select_related('pingzheng').all() - - # 状态筛选:只接受 0 或 1 - if zhuangtai is not None: - try: - zhuangtai = int(zhuangtai) - if zhuangtai in [0, 1]: - shops = shops.filter(zhuangtai=zhuangtai) - else: - return Response({'code': 400, 'msg': '状态参数只能为0或1'}) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '状态参数必须为整数'}) - - # 关键词搜索(店铺ID/名称/用户ID) - if keyword: - shops = shops.filter( - models.Q(id__icontains=keyword) | - models.Q(dianpu_mingcheng__icontains=keyword) | - models.Q(pingzheng__yonghu__icontains=keyword) - ) - - # ---------- 分页 ---------- - paginator = Paginator(shops, page_size) - page_obj = paginator.get_page(page) - - shop_list = [] - for shop in page_obj: - shop_list.append({ - 'id': shop.id, - 'dianpu_mingcheng': shop.dianpu_mingcheng, - 'zhuangtai': shop.zhuangtai, - 'yonghu_id': shop.pingzheng.yonghu if shop.pingzheng else '', - 'lianxi_dianhua': shop.lianxi_dianhua or '', - 'weixinhao': shop.weixinhao or '', - }) - - return Response({ - 'code': 0, - 'data': { - 'list': shop_list, - 'total': paginator.count - } - }) - - -# ==================== 3. 店铺商品类型映射列表(含筛选、待审核计数) ==================== -class ShopProductTypeMappingView(APIView): - """ - 返回店铺商品类型映射(ShangpinLeixingDianpu)列表。 - 支持:店铺ID、公共类型ID、上架/封禁/审核状态筛选,关键字搜索(介绍/ID/店铺名/公共类型名)。 - 每次请求同时返回“待审核总数”(shenhe_zhuangtai=False 的记录数)。 - 前端请求路径:/houtai/htdphqyssplx - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败'}) - if not set(permissions).intersection(SHOP_PRODUCT_PERMS): - return Response({'code': 403, 'msg': '无权访问商品类型映射'}) - - # ---------- 安全解析参数 ---------- - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 10)) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '分页参数格式错误'}) - - keyword = str(request.data.get('keyword', '')).strip() - shop_id = request.data.get('shop_id') - public_type_id = request.data.get('public_type_id') - - # 布尔型筛选字段处理:None 表示不筛选,否则转为布尔值 - def parse_bool(val): - if val is None: - return None - return bool(val) - - shangjia = parse_bool(request.data.get('shangjia_zhuangtai')) - fengjin = parse_bool(request.data.get('fengjin_zhuangtai')) - shenhe = parse_bool(request.data.get('shenhe_zhuangtai')) - - # ---------- 查询(连表店铺和公共类型) ---------- - mappings = ShangpinLeixingDianpu.query.select_related('dianpu', 'gonggong_leixing').all() - - # 筛选条件:均为参数化,完全防注入 - if shop_id is not None: - mappings = mappings.filter(dianpu_id=int(shop_id)) - if public_type_id is not None: - mappings = mappings.filter(gonggong_leixing_id=int(public_type_id)) - if shangjia is not None: - mappings = mappings.filter(shangjia_zhuangtai=shangjia) - if fengjin is not None: - mappings = mappings.filter(fengjin_zhuangtai=fengjin) - if shenhe is not None: - mappings = mappings.filter(shenhe_zhuangtai=shenhe) - - # 关键字搜索(介绍、ID、店铺名、公共类型名) - if keyword: - mappings = mappings.filter( - models.Q(jieshao__icontains=keyword) | - models.Q(id__icontains=keyword) | - models.Q(dianpu__dianpu_mingcheng__icontains=keyword) | - models.Q(gonggong_leixing__jieshao__icontains=keyword) - ) - - # ---------- 待审核总数(不受分页影响) ---------- - pending_total = mappings.filter(shenhe_zhuangtai=False).count() - - # ---------- 分页 ---------- - paginator = Paginator(mappings, page_size) - page_obj = paginator.get_page(page) - - mapping_list = [] - for m in page_obj: - mapping_list.append({ - 'id': m.id, - 'dianpu_id': m.dianpu_id, - 'dianpu_name': m.dianpu.dianpu_mingcheng if m.dianpu else '', - 'gonggong_leixing_id': m.gonggong_leixing_id, - 'gonggong_leixing_name': m.gonggong_leixing.jieshao if m.gonggong_leixing else '', - 'jieshao': m.jieshao, - 'tupian_url': m.tupian_url or '', - 'paixu': m.paixu, - 'shangjia_zhuangtai': m.shangjia_zhuangtai, - 'fengjin_zhuangtai': m.fengjin_zhuangtai, - 'shenhe_zhuangtai': m.shenhe_zhuangtai, - }) - - return Response({ - 'code': 0, - 'data': { - 'list': mapping_list, - 'total': paginator.count, - 'pending_total': pending_total - } - }) - - -# ==================== 统一修改接口(已修正事务) ==================== -class ShopProductModifyView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败'}) - - user_perms = set(permissions) - action = request.data.get('action') - - if action == 'update_audit_mode': - return self._update_audit_mode(request, user_perms) - elif action == 'update_mapping_status': - return self._update_mapping_status(request, user_perms) - else: - return Response({'code': 400, 'msg': '未知操作'}) - - @transaction.atomic # ✅ 事务修复点 - def _update_audit_mode(self, request, user_perms): - if '1199ab' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改审核模式'}) - - kaiqi = request.data.get('kaiqi_shenhe') - if kaiqi is None: - return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'}) - - try: - config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1) - config.kaiqi_shenhe = bool(kaiqi) - config.save(update_fields=['kaiqi_shenhe']) - return Response({'code': 0, 'msg': '审核模式已更新'}) - except Exception as e: - logger.exception("更新审核模式失败") - return Response({'code': 500, 'msg': '服务器错误'}) - - @transaction.atomic - def _update_mapping_status(self, request, user_perms): - mapping_id = request.data.get('mapping_id') - if not mapping_id: - return Response({'code': 400, 'msg': '缺少 mapping_id'}) - - try: - mapping = ShangpinLeixingDianpu.objects.select_for_update().get(id=int(mapping_id)) - except (ShangpinLeixingDianpu.DoesNotExist, ValueError): - return Response({'code': 404, 'msg': '映射不存在'}) - - fengjin = request.data.get('fengjin_zhuangtai') - shangjia = request.data.get('shangjia_zhuangtai') - shenhe = request.data.get('shenhe_zhuangtai') - - # 权限判断 - if fengjin is not None: - if fengjin: - if '1199abc' not in user_perms: - return Response({'code': 403, 'msg': '无权限封禁'}) - else: - if '1199abd' not in user_perms: - return Response({'code': 403, 'msg': '无权限解封'}) - mapping.fengjin_zhuangtai = bool(fengjin) - - if shangjia is not None: - if not shangjia: - if '1199abc' not in user_perms: - return Response({'code': 403, 'msg': '无权限下架'}) - else: - if '1199abd' not in user_perms: - return Response({'code': 403, 'msg': '无权限上架'}) - mapping.shangjia_zhuangtai = bool(shangjia) - - if shenhe is not None: - if shenhe: - if '1199abd' not in user_perms: - return Response({'code': 403, 'msg': '无权限审核通过'}) - mapping.shenhe_zhuangtai = bool(shenhe) - - mapping.save() - return Response({'code': 0, 'msg': '修改成功'}) - - - - - - -# 商品管理相关权限组(拥有任意一个即可查看列表) -PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd', '1199abe'] - - -class ProductListView(APIView): - """ - 商品列表接口,支持多条件筛选与分页。 - 请求路径: /houtai/hqhtdpsplx - 权限要求: 1199ab / 1199abc / 1199abd / 1199abe 任一即可 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # ----- 身份与权限验证 ----- - username = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败'}) - - if not set(permissions).intersection(PRODUCT_PERMS): - return Response({'code': 403, 'msg': '无商品管理权限'}) - - # ----- 安全解析分页与筛选参数 ----- - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 15)) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '分页参数格式错误'}) - - keyword = str(request.data.get('keyword', '')).strip() - shop_id = request.data.get('shop_id') # 店铺ID (可选) - shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型映射ID (可选) - public_type_id = request.data.get('public_type_id') # 公共商品类型ID (可选) - - # 布尔型状态筛选 - def parse_bool(val): - if val is None: - return None - return bool(val) - - shenhe = parse_bool(request.data.get('shenhe_zhuangtai')) - shangjia = parse_bool(request.data.get('shangjia_zhuangtai')) - fengjin = parse_bool(request.data.get('fengjin_zhuangtai')) - - # ----- 构建查询(预加载店铺和店铺商品类型) ----- - products = Shangpin.query.select_related('dianpu', 'dianpu_leixing').all() - - # 按店铺筛选 - if shop_id is not None: - try: - products = products.filter(dianpu_id=int(shop_id)) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': 'shop_id 格式错误'}) - - # 按店铺商品类型映射筛选 - if shop_product_type_id is not None: - try: - products = products.filter(dianpu_leixing_id=int(shop_product_type_id)) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'}) - - # 按公共商品类型筛选(leixing_id 存储的是公共类型的ID) - if public_type_id is not None: - try: - products = products.filter(leixing_id=int(public_type_id)) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': 'public_type_id 格式错误'}) - - # 平台审核状态筛选 (BooleanField) - if shenhe is not None: - products = products.filter(shenhe_zhuangtai=shenhe) - - # 上架状态筛选 - if shangjia is not None: - products = products.filter(shangjia_zhuangtai=shangjia) - - # 封禁状态筛选 - if fengjin is not None: - products = products.filter(fengjin_zhuangtai=fengjin) - - # 关键词搜索:商品ID或商品标题(搜索具有模糊性,使用icontains) - if keyword: - # 尝试纯数字可能是商品ID,使用精确匹配与标题模糊搜索 - products = products.filter( - models.Q(id__icontains=keyword) | - models.Q(biaoqian__icontains=keyword) - ) - - # ----- 获取待审核总数(平台审核未通过的数量) ----- - pending_total = products.filter(shenhe_zhuangtai=False).count() - - # ----- 分页 ----- - paginator = Paginator(products, page_size) - page_obj = paginator.get_page(page) - - # ----- 构造返回数据(批量获取公共类型名称,避免N+1) ----- - # 提取所有公共商品类型ID - type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None} - public_type_map = {} - if type_ids: - public_types = ShangpinLeixing.query.filter(id__in=type_ids).values('id', 'jieshao') - public_type_map = {t['id']: t['jieshao'] for t in public_types} - - product_list = [] - for p in page_obj: - product_list.append({ - 'id': p.id, - 'biaoqian': p.biaoqian or '', - 'jiage': p.jiage, - 'kucun': p.kucun, - 'leixing_id': p.leixing_id, - 'public_type_name': public_type_map.get(p.leixing_id, ''), - 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, - 'duiwai_xiaoliang': p.duiwai_xiaoliang, - 'jieshao': p.jieshao or '', - 'xiadan_xuzhi': p.xiadan_xuzhi or '', - 'guize_tupian': p.guize_tupian or '', - 'yaoqiuleixing': p.yaoqiuleixing, - 'huiyuan_id': p.huiyuan_id or '', - 'yongjin': p.yongjin, - 'kaioi_ewai_PlayerCommission': p.kaioi_ewai_dashou_fencheng, - 'ewai_PlayerCommission': p.ewai_dashou_fencheng, - 'CreateTime': p.CreateTime.isoformat() if p.CreateTime else '', - 'UpdateTime': p.UpdateTime.isoformat() if p.UpdateTime else '', - 'paixu': p.paixu, - 'dianpu_id': p.dianpu_id, - 'dianpu_name': p.dianpu.dianpu_mingcheng if p.dianpu else '', - 'dianpu_leixing_id': p.dianpu_leixing_id, - 'dianpu_leixing_name': p.dianpu_leixing.jieshao if p.dianpu_leixing else '', - 'shangjia_zhuangtai': p.shangjia_zhuangtai, - 'fengjin_zhuangtai': p.fengjin_zhuangtai, - 'shenhe_zhuangtai': p.shenhe_zhuangtai, - }) - - return Response({ - 'code': 0, - 'data': { - 'list': product_list, - 'total': paginator.count, - 'pending_total': pending_total, # 待审核商品总数 - } - }) - - -class ProductModifyView(APIView): - """ - 商品修改接口,根据修改内容细粒度鉴权。 - 请求路径: /houtai/htxgdpspsj - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - username = request.data.get('username') - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return Response({'code': 403, 'msg': '身份验证失败'}) - - user_perms = set(permissions) - action = request.data.get('action', '') - - if action != 'update_product': - return Response({'code': 400, 'msg': '未知操作'}) - - return self._update_product(request, user_perms) - - @transaction.atomic - def _update_product(self, request, user_perms): - """执行商品修改,严格校验权限""" - product_id = request.data.get('product_id') - if not product_id: - return Response({'code': 400, 'msg': '缺少商品ID'}) - - try: - # 行级锁防止并发修改 - product = Shangpin.objects.select_for_update().get(id=int(product_id)) - except (Shangpin.DoesNotExist, ValueError): - return Response({'code': 404, 'msg': '商品不存在'}) - - # 需要修改的字段 - fields_to_update = {} - # 将要修改的状态标志用于后续权限判断 - status_changes = { - 'shangjia': request.data.get('shangjia_zhuangtai'), - 'fengjin': request.data.get('fengjin_zhuangtai'), - 'shenhe': request.data.get('shenhe_zhuangtai'), - } - - # ---------- 权限分类检查 ---------- - # 1. 封禁/下架操作 (消极操作) -> 1199abc - if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']): - if '1199abc' not in user_perms: - return Response({'code': 403, 'msg': '无权限执行下架/封禁/驳回审核操作(1199abc)'}) - if status_changes.get('fengjin') is not None and status_changes['fengjin'] == True: - if '1199abc' not in user_perms: - return Response({'code': 403, 'msg': '无权限执行封禁操作(1199abc)'}) - - # 2. 解封/上架/过审 (积极操作) -> 1199abd - if any(status_changes.get(k) is not None and status_changes[k] == True for k in ['shangjia', 'shenhe']): - if '1199abd' not in user_perms: - return Response({'code': 403, 'msg': '无权限执行上架/解封/审核通过操作(1199abd)'}) - if status_changes.get('fengjin') is not None and status_changes['fengjin'] == False: - if '1199abd' not in user_perms: - return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'}) - - # 3. 修改其他数据(非状态字段)-> 1199abe - # 除了上架、封禁、审核之外的字段都属于“其他数据” - other_fields = [k for k in request.data.keys() if k not in ['action', 'product_id', 'username', - 'shangjia_zhuangtai', 'fengjin_zhuangtai', - 'shenhe_zhuangtai']] - if other_fields and '1199abe' not in user_perms: - return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'}) - - # ---------- 应用修改 ---------- - # 允许修改的字段白名单(防止注入) - # 前端参数名 → 模型字段名映射 - field_name_map = { - 'kaioi_ewai_PlayerCommission': 'kaioi_ewai_dashou_fencheng', - 'ewai_PlayerCommission': 'ewai_dashou_fencheng', - } - allowed_fields = [ - 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'duiwai_xiaoliang', - 'jieshao', 'xiadan_xuzhi', 'yaoqiuleixing', 'yongjin', - 'kaioi_ewai_PlayerCommission', 'ewai_PlayerCommission', - 'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai' - ] - - update_fields = [] - for field in allowed_fields: - value = request.data.get(field) - if value is not None: - model_field = field_name_map.get(field, field) - # 简单类型校验(根据字段类型) - if field in ['jiage', 'kucun', 'duiwai_xiaoliang', 'yongjin', 'ewai_PlayerCommission', 'paixu']: - try: - value = float(value) if 'jiage' in field or 'yongjin' in field or 'ewai' in field else int(value) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': f'字段 {field} 值格式错误'}) - setattr(product, model_field, value) - update_fields.append(model_field) - - if update_fields: - product.save(update_fields=update_fields) - return Response({'code': 0, 'msg': '商品修改成功'}) - - - - -# 与前端约定的聊天权限码 -CHAT_PERM_CODES = ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4'] - -class KefuChatPermissionsView(APIView): - """ - 获取客服聊天权限及GoEasy配置 - POST /houtai/kfhqltqx - Body: { "phone": "客服手机号" } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 从前端获取传递的账号,用于防越权校验 - username_from_frontend = request.data.get('phone', None) - - # 2. 调用公共验证方法。 - # 返回 (kefu_obj, permissions) 或 (None, Response) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - - # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值(错误Response) - if kefu_obj is None: - return permissions - - # ---------- 验证通过,permissions 已经是权限码列表 ---------- - # 4. 检查是否拥有至少一个聊天权限 - has_chat_perm = any(p in CHAT_PERM_CODES for p in permissions) - - # 5. 若有聊天权限,获取 GoEasy AppKey - goeasy_appkey = '' - if has_chat_perm: - goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '') - - # 6. 返回数据 - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'permissions': permissions, - 'goeasy_appkey': goeasy_appkey - } - }) - - - - - -class FaKuanTongJiView(APIView): - """ - 罚款统计接口 - POST /houtai/htglyhqcfsltj - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '参数不完整'}) - - try: - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - if not has_fadan_view_permission(permissions, username): - return Response({'code': 403, 'msg': '无权限查看罚款统计'}) - - stats = filter_penalty_qs(Penalty.query.all(), request).aggregate( - total=Count('id'), - daijiaona=Count('id', filter=Q(Status=1)), - shensuzhong=Count('id', filter=Q(Status=3)), - yijiaona=Count('id', filter=Q(Status=2)), - yibohui=Count('id', filter=Q(Status=4)), - ) - data = {k: v or 0 for k, v in stats.items()} - from jituan.services.club_user_access import list_response_meta - return Response({'code': 0, 'msg': '成功', 'data': {**data, **list_response_meta(request)}}) - except Exception as e: - logger.error(f"罚款统计异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '系统繁忙'}) - - -class FaKuanLieBiaoView(APIView): - """ - 罚款列表接口 - POST /houtai/hthqfklb - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - username = request.data.get('username', '').strip() - if not username: - return Response({'code': 400, 'msg': '参数不完整'}) - - try: - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - if not has_fadan_view_permission(permissions, username): - return Response({'code': 403, 'msg': '无权限查看罚款列表'}) - - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - if page_size > 100: - page_size = 100 - - qs = filter_penalty_qs(Penalty.query.all(), request) - - # 状态筛选 - zhuangtai = request.data.get('zhuangtai') - if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]: - qs = qs.filter(Status=int(zhuangtai)) - - # 被处罚人ID - PenalizedUserID = request.data.get('PenalizedUserID', '').strip() - if PenalizedUserID: - qs = qs.filter(PenalizedUserID=PenalizedUserID) - - # 被处罚人身份 - beichufa_shenfen = request.data.get('beichufa_shenfen') - if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]: - qs = qs.filter(Identity=int(beichufa_shenfen)) - - # 模糊搜索(订单ID、被罚人ID、申请人ID) - sousuo = request.data.get('sousuo', '').strip() - if sousuo: - qs = qs.filter( - Q(RelatedOrderID__icontains=sousuo) | - Q(PenalizedUserID__icontains=sousuo) | - Q(ApplicantID__icontains=sousuo) - ) - - qs = qs.order_by('-CreateTime') - paginator = Paginator(qs, page_size) - try: - page_obj = paginator.page(page) - except Exception: - return Response({ - 'code': 0, 'msg': '成功', - 'data': {'list': [], 'total': 0, 'page': page, 'page_size': page_size} - }) - - records = list(page_obj) - penalty_ids = [r.id for r in records] - - # 批量获取图片并分组 - zhengju_map = {} - shensu_map = {} - if penalty_ids: - tupians = PenaltyAppealImage.query.filter( - Penalty_id__in=penalty_ids - ).values('Penalty_id', 'ImageURL', 'Purpose') - for t in tupians: - fid = t['Penalty_id'] - url = t['ImageURL'] - if t['Purpose'] == 1: - zhengju_map.setdefault(fid, []).append(url) - else: - shensu_map.setdefault(fid, []).append(url) - - # 组装列表数据 - results = [] - for r in records: - results.append({ - 'id': r.id, - 'beichufa_id': r.PenalizedUserID, - 'shenfen': r.Identity, - 'shenfen_text': dict(Penalty._meta.get_field('Identity').choices).get(r.Identity, ''), - 'shenqing_chufa': r.ApplicantID or '', - 'shenqingren_shenfen': r.ApplicantIdentity, - 'chufaliyou': r.Reason or '', - 'fakuanjine': str(r.FineAmount), - 'guanliandingdan_id': r.RelatedOrderID or '', - 'zhuangtai': r.Status, - 'yingxiang_qiangdan': r.AffectsGrabbing, - 'shensuliyou': r.AppealReason or '', - 'bohuiliyou': r.RejectReason or '', - 'chulizhe': r.ProcessorID or '', - 'chulizhe_shenfen': r.ProcessorIdentity, - 'zhengju_tupian': zhengju_map.get(r.id, []), - 'shensu_tupian': shensu_map.get(r.id, []), - 'creat_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '', - 'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '', - }) - - from jituan.services.club_user_access import list_response_meta - return Response({ - 'code': 0, 'msg': '成功', - 'data': { - 'list': results, - 'total': paginator.count, - 'page': page, - 'page_size': page_size, - **list_response_meta(request), - } - }) - except Exception as e: - logger.error(f"罚款列表异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '系统繁忙'}) - - -class FaKuanChuLiView(APIView): - """ - 处理罚款申诉(同意/拒绝) - POST /houtai/glyclfk - """ - permission_classes = [IsAuthenticated] - parser_classes = [MultiPartParser, FormParser, JSONParser] - - def post(self, request): - try: - username = request.data.get('username', '').strip() - penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id') - action = request.data.get('action', '').strip() - chuli_liyou = request.data.get('chuli_liyou', '').strip() - - if not all([username, penalty_id, action]): - return Response({'code': 400, 'msg': '参数不完整'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - with transaction.atomic(): - try: - penalty = Penalty.objects.select_for_update().get(id=penalty_id) - except Penalty.DoesNotExist: - return Response({'code': 404, 'msg': '罚单不存在'}) - - deny = forbid_penalty_out_of_scope(request, penalty) - if deny: - return deny - - if penalty.Status != 3: - return Response({'code': 400, 'msg': '当前状态不可处理'}) - - if not check_fadan_permission(permissions, penalty.Identity): - return Response({'code': 403, 'msg': '无权限处理该类罚单'}) - - # 处理上传的图片 - uploaded_urls = [] - files = request.FILES.getlist('tupian') or [] - if not files: - single = request.FILES.get('tupian') - if single: - files = [single] - for file in files: - valid, msg = validate_image(file) - if not valid: - return Response({'code': 400, 'msg': msg}) - ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' - file_name = f"chufa_{penalty_id}_{int(time.time() * 1000)}.{ext}" - file_path = f"chufatupian/kechufatupian/{file_name}" - url = upload_to_oss(file, file_path) - if not url: - return Response({'code': 500, 'msg': '图片上传失败'}) - uploaded_urls.append(file_path) - - # 更新状态 - if action == 'agree': # 同意申诉 → 驳回处罚 - penalty.Status = 4 - penalty.RejectReason = chuli_liyou - elif action == 'reject': # 拒绝申诉 → 回到待缴纳 - penalty.Status = 1 - penalty.RejectReason = chuli_liyou - else: - return Response({'code': 400, 'msg': '无效的操作类型'}) - - # 记录处理者(使用 request.user 的 phone) - penalty.ProcessorID = request.user.Phone - penalty.ProcessorIdentity = 2 # 这里假设处理者是售后身份,可根据实际业务获取 - penalty.save() - - # 保存图片(证据图片,用途1) - for relative_url in uploaded_urls: - PenaltyAppealImage.query.create( - Penalty_id=penalty_id, - PenalizedUserID=penalty.PenalizedUserID, - ImageURL=relative_url, - Purpose=1 - ) - - logger.info(f"罚款处理成功: {penalty_id}, 操作: {action}") - return Response({'code': 0, 'msg': '处理成功'}) - - except Exception as e: - logger.error(f"罚款处理异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '系统繁忙'}) - - -class FaKuanChuangJianView(APIView): - """ - 管理员创建罚款 - POST /houtai/htfksc - """ - permission_classes = [IsAuthenticated] - parser_classes = [MultiPartParser, FormParser, JSONParser] - - def post(self, request): - try: - username = request.data.get('username', '').strip() - PenalizedUserID, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data) - shenfen = request.data.get('shenfen') - - if not all([username, PenalizedUserID, shenfen, Reason, FineAmount is not None]): - return Response({'code': 400, 'msg': '参数不完整'}) - - try: - shenfen = int(shenfen) - FineAmount = float(FineAmount) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': '参数格式错误'}) - - if shenfen not in [1, 2, 3, 4]: - return Response({'code': 400, 'msg': '无效的被罚人身份'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - if not check_fadan_permission(permissions, shenfen): - return Response({'code': 403, 'msg': '无权限创建该类罚单'}) - - # 验证用户是否存在对应的扩展表 - try: - user = User.query.get(UserUID=PenalizedUserID) - profile_attr = SHENFEN_PROFILE_MAP.get(shenfen) - if not profile_attr or not hasattr(user, profile_attr): - return Response({'code': 400, 'msg': '该用户不是所选身份'}) - except User.DoesNotExist: - return Response({'code': 404, 'msg': '用户不存在'}) - - # 处理图片上传 - uploaded_urls = [] - files = request.FILES.getlist('tupian') or [] - if not files: - single = request.FILES.get('tupian') - if single: - files = [single] - for file in files: - valid, msg = validate_image(file) - if not valid: - return Response({'code': 400, 'msg': msg}) - ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' - file_name = f"chufa_{int(time.time() * 1000)}.{ext}" - file_path = f"chufatupian/kechufatupian/{file_name}" - url = upload_to_oss(file, file_path) - if not url: - return Response({'code': 500, 'msg': '图片上传失败'}) - uploaded_urls.append(file_path) - - with transaction.atomic(): - penalty = Penalty.query.create( - PenalizedUserID=PenalizedUserID, - Identity=shenfen, - ApplicantID=request.user.Phone, # 申请人(客服)手机号 - ApplicantIdentity=2, # 售后身份 - Reason=Reason, - FineAmount=FineAmount, - AffectsGrabbing=AffectsGrabbing, - Status=1, # 待缴纳 - ClubID=resolve_penalty_club_id( - penalized_user_id=PenalizedUserID, request=request, - ), - ) - for relative_url in uploaded_urls: - PenaltyAppealImage.query.create( - Penalty=penalty, - PenalizedUserID=PenalizedUserID, - ImageURL=relative_url, - Purpose=1 - ) - - logger.info(f"罚款创建成功: {penalty.id}") - return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': penalty.id}}) - - except Exception as e: - logger.error(f"创建罚款异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '系统繁忙'}) - - -class FineApplyView(APIView): - """ - 客服罚款申请接口(基于订单对打手发起罚款) - POST /houtai/kffkdssq - 权限: JWT认证 + 客服权限验证 - - 请求参数: - username (必填) : 客服用户名(手机号) - OrderID (必填) : 订单ID - Reason (必填) : 罚款原因 - FineAmount (必填) : 罚款金额(元) - AffectsGrabbing (可选) : 是否影响抢单,1=是,0=否,默认1 - """ - permission_classes = [IsAuthenticated] - parser_classes = [MultiPartParser, FormParser, JSONParser] - - def post(self, request): - try: - username = request.data.get('username', '').strip() - OrderID = pick_order_id(request.data) - _, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data) - - if not all([username, OrderID, Reason, FineAmount is not None]): - return Response({'code': 400, 'msg': '参数不完整'}) - - try: - FineAmount = float(FineAmount) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': '罚款金额格式错误'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 验证订单存在 - try: - order = Order.query.get(OrderID=OrderID) - except Order.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}) - - # 获取打手ID - dashou_id = order.PlayerID - if not dashou_id: - return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'}) - - # 重复罚款检查 - if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=OrderID).exists(): - return Response({'code': 400, 'msg': '该打手已被罚款过'}) - - # 检查权限(打手身份 shenfen=1) - if not check_fadan_permission(permissions, 1): - return Response({'code': 403, 'msg': '无权限处罚打手'}) - - # 处理图片上传 - uploaded_urls = [] - files = request.FILES.getlist('tupian') or [] - if not files: - single = request.FILES.get('tupian') - if single: - files = [single] - for file in files: - valid, msg = validate_image(file) - if not valid: - return Response({'code': 400, 'msg': msg}) - ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' - file_name = f"chufa_{int(time.time() * 1000)}.{ext}" - file_path = f"chufatupian/kechufatupian/{file_name}" - url = upload_to_oss(file, file_path) - if not url: - return Response({'code': 500, 'msg': '图片上传失败'}) - uploaded_urls.append(file_path) - - # 创建罚单 - with transaction.atomic(): - penalty = Penalty.query.create( - PenalizedUserID=dashou_id, - ApplicantID=request.user.Phone, - Identity=1, # 被处罚人身份:1 打手 - Reason=Reason, - FineAmount=FineAmount, - RelatedOrderID=OrderID, - Status=1, # 待缴纳 - AffectsGrabbing=AffectsGrabbing, - ApplicantIdentity=1, # 申请人身份:1 客服 - ClubID=resolve_penalty_club_id(order=order, request=request), - ) - for relative_url in uploaded_urls: - PenaltyAppealImage.query.create( - Penalty=penalty, - PenalizedUserID=dashou_id, - ImageURL=relative_url, - Purpose=1 - ) - - logger.info(f"客服罚款申请成功: 订单 {OrderID}, 打手 {dashou_id}, 金额 {FineAmount}") - return Response({'code': 0, 'msg': '罚款已生成', 'data': {'id': penalty.id}}) - - except Exception as e: - logger.error(f"客服罚款申请异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '系统繁忙'}) - - -class PunishDashouView(APIView): - """ - 客服处罚打手接口(扣除积分) - POST /houtai/kpcf - 权限: JWT认证 + 客服权限验证 - - 请求参数: - username (必填) : 客服用户名(手机号) - OrderID (必填) : 订单ID - reason (可选) : 处罚原因 - """ - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - try: - username = request.data.get('username', '').strip() - OrderID = request.data.get('OrderID', '').strip() - reason = request.data.get('reason', '').strip() - - if not all([username, OrderID]): - return Response({'code': 400, 'msg': '参数不完整'}) - - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions - - # 检查权限(打手身份 shenfen=1) - if not check_fadan_permission(permissions, 1): - return Response({'code': 403, 'msg': '无权限处罚打手'}) - - # 查询订单 - try: - order = Order.query.get(OrderID=OrderID) - except Order.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}) - - # 获取打手ID - dashou_id = order.PlayerID - if not dashou_id: - return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}) - - # 查询打手 - try: - dashou_user = User.query.get(UserUID=dashou_id) - dashou = dashou_user.DashouProfile - except User.DoesNotExist: - return Response({'code': 404, 'msg': '打手用户不存在'}) - except ObjectDoesNotExist: - return Response({'code': 400, 'msg': '该用户不是打手'}) - - # 创建处罚记录(扣除积分5分) - jifen = 5 - with transaction.atomic(): - penalty_record = PenaltyRecord.query.create( - PlayerID=dashou_id, - ApplicantID=username, - PenaltyReason=reason or '', - ApplyStatus=0, - DeductedPoints=jifen, - OrderID=OrderID, - ClubID=resolve_penalty_record_club_id(order_id=OrderID, player_id=dashou_id), - ) - # 扣除打手积分 - dashou.jifen = F('jifen') - jifen - dashou.save(update_fields=['jifen']) - - logger.info(f"客服处罚打手成功: 打手 {dashou_id}, 订单 {OrderID}, 扣除积分 {jifen}") - return Response({'code': 0, 'msg': '处罚成功', 'data': {'penalty_record_id': penalty_record.id}}) - - except Exception as e: - logger.error(f"客服处罚打手异常: {traceback.format_exc()}") - return Response({'code': 500, 'msg': '系统繁忙'}) - - - - - - - -class HqbkxxView(APIView): - """ - 获取板块配置所需全部数据 - POST /houtai/hqbkxx - Body: { "phone": "管理员手机号" } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 从前端获取传递的账号,用于防越权校验 - username_from_frontend = request.data.get('phone', None) - - # 2. 调用公共验证方法 - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - - # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值 - if kefu_obj is None: - return permissions - - # ---------- 验证通过 ---------- - # 4. 检查是否有板块管理权限 - if 'bankuai' not in permissions: - return Response({'code': 403, 'msg': '无板块管理权限'}, status=403) - - # 5. 查询所有板块 - bankuai_list = [] - for bk in Bankuai.query.all(): - bankuai_list.append({ - 'bankuai_id': bk.bankuai_id, - 'mingcheng': bk.mingcheng, - 'CreateTime': bk.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if bk.CreateTime else '' - }) - - # 6. 查询所有商品类型 - shangpin_leixing_list = [] - for sp in ShangpinLeixing.query.all(): - shangpin_leixing_list.append({ - 'id': sp.id, - 'jieshao': sp.jieshao, - 'tupian_url': sp.tupian_url or '', - 'bankuai_id': sp.bankuai_id - }) - - # 7. 查询所有会员 - huiyuan_list = [] - for hy in Huiyuan.query.all(): - huiyuan_list.append({ - 'huiyuan_id': hy.huiyuan_id, - 'jieshao': hy.jieshao, - 'bankuai_id': hy.bankuai_id - }) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'bankuai': bankuai_list, - 'shangpin_leixing': shangpin_leixing_list, - 'huiyuan': huiyuan_list - } - }) - - -class BkxgView(APIView): - """ - 板块增删改统一接口 - POST /houtai/bkxg - Body: { - "phone": "管理员手机号", - "action": "create | update | delete | add_items | remove_items", - "bankuai_id": int, - "bankuai_name": str, - "item_type": "shangpin_leixing | huiyuan", - "item_ids": [1,2,3] - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 从前端获取传递的账号 - username_from_frontend = request.data.get('phone', None) - - # 2. 调用公共验证方法 - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - - # 3. 验证失败直接返回 - if kefu_obj is None: - return permissions - - # 4. 检查板块管理权限 - if 'bankuai' not in permissions: - return Response({'code': 403, 'msg': '无板块管理权限'}, status=403) - - # 5. 获取操作类型 - action = request.data.get('action') - if not action: - return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400) - - # ==================== 创建板块 ==================== - if action == 'create': - bankuai_name = request.data.get('bankuai_name', '').strip() - if not bankuai_name: - return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400) - if Bankuai.query.filter(mingcheng=bankuai_name).exists(): - return Response({'code': 1, 'msg': '板块名称已存在'}, status=400) - - shangpin_ids = request.data.get('shangpin_ids', []) - huiyuan_ids = request.data.get('huiyuan_ids', []) - - with transaction.atomic(): - new_bk = Bankuai.query.create(mingcheng=bankuai_name) - if shangpin_ids: - ShangpinLeixing.query.filter( - id__in=shangpin_ids, - bankuai__isnull=True - ).update(bankuai=new_bk) - if huiyuan_ids: - Huiyuan.query.filter( - huiyuan_id__in=huiyuan_ids, - bankuai__isnull=True - ).update(bankuai=new_bk) - - return Response({ - 'code': 0, - 'msg': '创建成功', - 'data': {'bankuai_id': new_bk.bankuai_id} - }) - - # ==================== 以下操作需要 bankuai_id ==================== - bankuai_id = request.data.get('bankuai_id') - if not bankuai_id: - return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400) - - try: - bk = Bankuai.query.get(bankuai_id=bankuai_id) - except Bankuai.DoesNotExist: - return Response({'code': 1, 'msg': '板块不存在'}, status=404) - - # ==================== 修改板块名称 ==================== - if action == 'update': - new_name = request.data.get('bankuai_name', '').strip() - if not new_name: - return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400) - if Bankuai.query.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists(): - return Response({'code': 1, 'msg': '板块名称已存在'}, status=400) - bk.mingcheng = new_name - bk.save() - return Response({'code': 0, 'msg': '修改成功'}) - - # ==================== 删除板块 ==================== - elif action == 'delete': - with transaction.atomic(): - ShangpinLeixing.query.filter(bankuai=bk).update(bankuai=None) - Huiyuan.query.filter(bankuai=bk).update(bankuai=None) - bk.delete() - return Response({'code': 0, 'msg': '删除成功'}) - - # ==================== 添加绑定 ==================== - elif action == 'add_items': - item_type = request.data.get('item_type') - item_ids = request.data.get('item_ids', []) - if not item_type or not item_ids: - return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400) - - if item_type == 'shangpin_leixing': - ShangpinLeixing.query.filter( - id__in=item_ids, - bankuai__isnull=True - ).update(bankuai=bk) - elif item_type == 'huiyuan': - Huiyuan.query.filter( - huiyuan_id__in=item_ids, - bankuai__isnull=True - ).update(bankuai=bk) - else: - return Response({'code': 1, 'msg': '无效的 item_type'}, status=400) - return Response({'code': 0, 'msg': '添加成功'}) - - # ==================== 移除绑定 ==================== - elif action == 'remove_items': - item_type = request.data.get('item_type') - item_ids = request.data.get('item_ids', []) - if not item_type or not item_ids: - return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400) - - if item_type == 'shangpin_leixing': - ShangpinLeixing.query.filter( - id__in=item_ids, - bankuai=bk - ).update(bankuai=None) - elif item_type == 'huiyuan': - Huiyuan.query.filter( - huiyuan_id__in=item_ids, - bankuai=bk - ).update(bankuai=None) - else: - return Response({'code': 1, 'msg': '无效的 item_type'}, status=400) - return Response({'code': 0, 'msg': '移除成功'}) - - else: - return Response({'code': 1, 'msg': '未知操作'}, status=400) - - - - - - - - - -class CaiwuView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request): - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - - if kefu_obj is None: - return permissions - - if 'caiwu' not in permissions: - return Response({'code': 403, 'msg': '无财务查看权限'}, status=403) - - today = date.today() - today_start = datetime.combine(today, datetime.min.time()) - today_end = today_start + timedelta(days=1) - - # 今日收入 - today_income = DailyIncomeStat.query.filter(date=today).first() - today_income_amount = float(today_income.total_amount) if today_income else 0.00 - today_income_count = today_income.total_count if today_income else 0 - - # 今日支出 - today_payout = DailyPayoutStat.query.filter(date=today).first() - today_payout_amount = float(today_payout.total_amount) if today_payout else 0.00 - today_payout_count = today_payout.total_count if today_payout else 0 - - # 今日订单(已付款及之后的状态) - today_orders = Order.query.filter( - CreateTime__gte=today_start, - CreateTime__lt=today_end, - Status__in=[1,2,3,4,5,6,7,8] - ) - today_order_count = today_orders.count() - today_order_amount = float(today_orders.aggregate(total=Sum('Amount'))['total'] or 0.00) - - # 今日成交(Status=3) - today_completed = Order.query.filter( - CreateTime__gte=today_start, - CreateTime__lt=today_end, - Status=3 - ) - today_completed_count = today_completed.count() - today_completed_amount = float(today_completed.aggregate(total=Sum('Amount'))['total'] or 0.00) - - # 今日退款(Status=5) - today_tuikuan = Order.query.filter( - CreateTime__gte=today_start, - CreateTime__lt=today_end, - Status=5 - ) - today_tuikuan_count = today_tuikuan.count() - today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('Amount'))['total'] or 0.00) - - # 今日充值(会员充值,leixing=1, zhuangtai=3) - today_chongzhi = Czjilu.query.filter( - CreateTime__gte=today_start, - CreateTime__lt=today_end, - leixing=1, - zhuangtai=3 - ) - today_chongzhi_count = today_chongzhi.count() - today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00) - - # 今日新增会员:从购买记录表统计今天创建的用户(去重) - new_huiyuan_user_ids = Huiyuangoumai.query.filter( - CreateTime__gte=today_start, - CreateTime__lt=today_end - ).values_list('yonghu_id', flat=True).distinct() - today_new_huiyuan = len(new_huiyuan_user_ids) - - # 新增会员充值总额(这些用户今天的会员充值金额) - today_new_huiyuan_amount = 0.0 - if new_huiyuan_user_ids: - amount_sum = Czjilu.query.filter( - CreateTime__gte=today_start, - CreateTime__lt=today_end, - yonghuid__in=new_huiyuan_user_ids, - leixing=1, - zhuangtai=3 - ).aggregate(total=Sum('jine')) - today_new_huiyuan_amount = float(amount_sum['total'] or 0.0) - - # 各角色可提现余额总和 - all_guanshi_yue = float(UserGuanshi.query.aggregate(total=Sum('yue'))['total'] or 0.00) - all_dashou_yue = float(UserDashou.query.aggregate(total=Sum('yue'))['total'] or 0.00) - all_zuzhang_yue = float(UserZuzhang.query.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00) - all_shangjia_yue = float(UserShangjia.query.aggregate(total=Sum('yue'))['total'] or 0.00) - - # 平台总收支及利润 - total_income = float(DailyIncomeStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00) - total_payout = float(DailyPayoutStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00) - platform_profit = round(total_income - total_payout, 2) - - # 近30天每日收支明细 - daily_stats = [] - income_qs = DailyIncomeStat.query.order_by('-date')[:30] - payout_dict = { - str(p.date): float(p.total_amount) - for p in DailyPayoutStat.query.filter(date__gte=today - timedelta(days=30)) - } - for inc in income_qs: - d = str(inc.date) - daily_stats.append({ - 'date': d, - 'income': float(inc.total_amount), - 'income_count': inc.total_count, - 'payout': payout_dict.get(d, 0.00), - 'profit': round(float(inc.total_amount) - payout_dict.get(d, 0.00), 2) - }) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'today_income': round(today_income_amount, 2), - 'today_income_count': today_income_count, - 'today_payout': round(today_payout_amount, 2), - 'today_payout_count': today_payout_count, - 'today_order_count': today_order_count, - 'today_order_amount': round(today_order_amount, 2), - 'today_completed_count': today_completed_count, - 'today_completed_amount': round(today_completed_amount, 2), - 'today_tuikuan_count': today_tuikuan_count, - 'today_tuikuan_amount': round(today_tuikuan_amount, 2), - 'today_chongzhi_count': today_chongzhi_count, - 'today_chongzhi_amount': round(today_chongzhi_amount, 2), - 'today_new_huiyuan': today_new_huiyuan, - 'today_new_huiyuan_amount': round(today_new_huiyuan_amount, 2), - 'all_guanshi_yue': round(all_guanshi_yue, 2), - 'all_dashou_yue': round(all_dashou_yue, 2), - 'all_zuzhang_yue': round(all_zuzhang_yue, 2), - 'all_shangjia_yue': round(all_shangjia_yue, 2), - 'total_income': round(total_income, 2), - 'total_payout': round(total_payout, 2), - 'platform_profit': platform_profit, - 'daily_stats': daily_stats - } - }) - - - - - -class CwhybkhqView(APIView): - """ - 获取所有板块及对应会员列表 - POST /houtai/cwhybkhq - Body: { "phone": "管理员手机号" } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - - if 'huiyuancaiwu' not in permissions: - return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403) - - bankuai_list = [] - for bk in Bankuai.query.all(): - members = Huiyuan.query.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id') - bankuai_list.append({ - 'bankuai_id': bk.bankuai_id, - 'mingcheng': bk.mingcheng, - 'members': list(members) - }) - - return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list}) - except Exception as e: - logger.exception("CwhybkhqView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - -class HybkjtsjView(APIView): - """ - 会员详细统计数据 - POST /houtai/hybkjtsj - Body: { - "phone": "管理员手机号", - "huiyuan_id": "会员ID", - "granularity": "day" | "month" | "year", - "year": 2026, - "month": 5, - "include_guanshi": true, - "include_zuzhang": true, - "summary_date": "2026-05-16" - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - - if 'huiyuancaiwu' not in permissions: - return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403) - - huiyuan_id = request.data.get('huiyuan_id') - granularity = request.data.get('granularity', 'day') - year = request.data.get('year') - month = request.data.get('month') - include_guanshi = request.data.get('include_guanshi', False) - include_zuzhang = request.data.get('include_zuzhang', False) - summary_date = request.data.get('summary_date') - - if not huiyuan_id: - return Response({'code': 1, 'msg': '缺少会员ID'}, status=400) - - # 🔧 直接使用 date.today() 避免模块名冲突 - now = date.today() - - # ---------- 构造时间范围 ---------- - if granularity == 'day': - if not year or not month: - return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) - start_date = date(year, month, 1) - if month == 12: - end_date = date(year + 1, 1, 1) - else: - end_date = date(year, month + 1, 1) - trunc_func = TruncDate('CreateTime') - if not summary_date: - summary_date = now.strftime('%Y-%m-%d') - summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() - elif granularity == 'month': - if not year: - return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncMonth('CreateTime') - if not summary_date: - summary_date = now.strftime('%Y-%m') - summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() - elif granularity == 'year': - year = year or now.year - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncYear('CreateTime') - if not summary_date: - summary_date = str(year) - summary_q = date(year, 1, 1) - else: - return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) - - # 生成 naive datetime(避免 USE_TZ=False 报错) - start_dt = datetime.combine(start_date, datetime.min.time()) - end_dt = datetime.combine(end_date, datetime.min.time()) - - # ---------- 充值过滤 ---------- - chongzhi_filter = { - 'huiyuan_id': huiyuan_id, # 会员ID筛选 - 'leixing': 1, - 'zhuangtai': 3, - 'CreateTime__gte': start_dt, - 'CreateTime__lt': end_dt - } - - # ---------- 分红过滤 ---------- - fenhong_filter = { - 'huiyuan_id': huiyuan_id, # 分红表会员ID筛选 - 'CreateTime__gte': start_dt, - 'CreateTime__lt': end_dt - } - - # 充值分组查询 - chongzhi_qs = filter_queryset_by_club( - Czjilu.query.filter(**chongzhi_filter), request, - ) \ - .annotate(period=trunc_func) \ - .values('period') \ - .annotate( - chongzhi_count=Count('id'), - chongzhi_amount=Sum('jine') - ).order_by('period') - - # 分红分组查询 - fenhong_qs = None - if include_guanshi or include_zuzhang: - fenhong_agg = {} - if include_guanshi: - fenhong_agg['guanshi_amount'] = Sum('fenhong') - if include_zuzhang: - fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong') - fenhong_qs = filter_gsfenhong_qs( - Gsfenhong.query.filter(**fenhong_filter), request, - ) \ - .annotate(period=trunc_func) \ - .values('period') \ - .annotate(**fenhong_agg) \ - .order_by('period') - - # 合并数据 - data_map = {} - for item in chongzhi_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - data_map[period_str] = { - 'chongzhi_count': item['chongzhi_count'], - 'chongzhi_amount': float(item['chongzhi_amount'] or 0) - } - - if fenhong_qs: - for item in fenhong_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - if period_str not in data_map: - data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0} - if include_guanshi: - data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0) - if include_zuzhang: - data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0) - - time_series = [] - for period_str, vals in data_map.items(): - guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0 - zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0 - vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2) - vals['date'] = period_str - time_series.append(vals) - time_series.sort(key=lambda x: x['date']) - - # ---------- 汇总数据 ---------- - summary_chongzhi_filter = chongzhi_filter.copy() - if granularity == 'day': - summary_chongzhi_filter['CreateTime__date'] = summary_q - elif granularity == 'month': - summary_chongzhi_filter['CreateTime__year'] = summary_q.year - summary_chongzhi_filter['CreateTime__month'] = summary_q.month - else: - summary_chongzhi_filter['CreateTime__year'] = summary_q.year - - summary_chongzhi = Czjilu.query.filter(**summary_chongzhi_filter).aggregate( - total_count=Count('id'), - total_amount=Sum('jine') - ) - total_chongzhi_count = summary_chongzhi['total_count'] or 0 - total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0) - - total_guanshi = 0.0 - total_zuzhang = 0.0 - if include_guanshi or include_zuzhang: - summary_fenhong_filter = fenhong_filter.copy() - if granularity == 'day': - summary_fenhong_filter['CreateTime__date'] = summary_q - elif granularity == 'month': - summary_fenhong_filter['CreateTime__year'] = summary_q.year - summary_fenhong_filter['CreateTime__month'] = summary_q.month - else: - summary_fenhong_filter['CreateTime__year'] = summary_q.year - - fenhong_agg = {} - if include_guanshi: - fenhong_agg['guanshi_sum'] = Sum('fenhong') - if include_zuzhang: - fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong') - summary_fenhong = Gsfenhong.query.filter(**summary_fenhong_filter).aggregate(**fenhong_agg) - total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0) - total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0) - - total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'time_series': time_series, - 'summary': { - 'total_chongzhi_count': total_chongzhi_count, - 'total_chongzhi_amount': total_chongzhi_amount, - 'total_guanshi_fenhong': total_guanshi, - 'total_zuzhang_fenhong': total_zuzhang, - 'total_shouyi': total_shouyi - } - } - }) - except Exception as e: - logger.exception("HybkjtsjView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - - -class SzxxView(APIView): - """ - 每日收支详细统计数据 - POST /houtai/szxx - Body: { - "phone": "管理员手机号", - "granularity": "day" | "month" | "year", - "year": 2026, // day/month 时必填 - "month": 5, // day 时必填 - "summary_date": "2026-05-16" // 可选,格式随颗粒度 - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 避免模块名冲突,局部导入 - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - - if 'caiwu' not in permissions: - return Response({'code': 403, 'msg': '无财务查看权限'}, status=403) - - granularity = request.data.get('granularity', 'day') - year = request.data.get('year') - month = request.data.get('month') - summary_date = request.data.get('summary_date') - - now = date.today() - - # ---------- 时间范围与分组字段 ---------- - if granularity == 'day': - if not year or not month: - return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) - # 分组字段 - group_field = 'day' - # 过滤条件 - income_filter = {'year': year, 'month': month} - payout_filter = {'year': year, 'month': month} - # 默认汇总日期 - if not summary_date: - summary_date = now.strftime('%Y-%m-%d') - summary_parts = summary_date.split('-') - summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1]), 'day': int(summary_parts[2])} - # 用于构造返回的日期字符串 - date_format = '%Y-%m-%d' - - elif granularity == 'month': - if not year: - return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) - group_field = 'month' - income_filter = {'year': year} - payout_filter = {'year': year} - if not summary_date: - summary_date = now.strftime('%Y-%m') - summary_parts = summary_date.split('-') - summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1])} - date_format = '%Y-%m' - - elif granularity == 'year': - group_field = 'year' - income_filter = {} - payout_filter = {} - if not summary_date: - summary_date = str(now.year) - summary_filter = {'year': int(summary_date)} - date_format = '%Y' - else: - return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) - - # ---------- 收入分组查询 ---------- - income_qs = filter_queryset_by_club( - DailyIncomeStat.query.filter(**income_filter), request, - ).values(group_field) \ - .annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \ - .order_by(group_field) - - # ---------- 支出分组查询 ---------- - payout_qs = filter_queryset_by_club( - DailyPayoutStat.query.filter(**payout_filter), request, - ).values(group_field) \ - .annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) \ - .order_by(group_field) - - # ---------- 合并数据 ---------- - data_map = {} - for item in income_qs: - key = item[group_field] - data_map[key] = { - 'income': float(item['total_income'] or 0), - 'income_count': item['total_count'] or 0, - 'payout': 0.0, - 'payout_count': 0 - } - - for item in payout_qs: - key = item[group_field] - if key not in data_map: - data_map[key] = {'income': 0.0, 'income_count': 0, 'payout': 0.0, 'payout_count': 0} - data_map[key]['payout'] = float(item['total_payout'] or 0) - data_map[key]['payout_count'] = item['total_count'] or 0 - - time_series = [] - for key, vals in data_map.items(): - # 构造日期字符串 - if granularity == 'day': - date_str = f"{year}-{month:02d}-{key:02d}" - elif granularity == 'month': - date_str = f"{year}-{key:02d}" - else: - date_str = str(key) - profit = round(vals['income'] - vals['payout'], 2) - time_series.append({ - 'date': date_str, - 'income': vals['income'], - 'payout': vals['payout'], - 'profit': profit - }) - time_series.sort(key=lambda x: x['date']) - - # ---------- 汇总数据 ---------- - income_summary = filter_queryset_by_club( - DailyIncomeStat.query.filter(**summary_filter), request, - ).aggregate( - total_income=Sum('total_amount'), total_count=Sum('total_count') - ) - payout_summary = filter_queryset_by_club( - DailyPayoutStat.query.filter(**summary_filter), request, - ).aggregate( - total_payout=Sum('total_amount'), total_count=Sum('total_count') - ) - total_income = float(income_summary['total_income'] or 0) - total_payout = float(payout_summary['total_payout'] or 0) - total_profit = round(total_income - total_payout, 2) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'time_series': time_series, - 'summary': { - 'total_income': total_income, - 'total_income_count': income_summary['total_count'] or 0, - 'total_payout': total_payout, - 'total_payout_count': payout_summary['total_count'] or 0, - 'total_profit': total_profit - } - } - }) - - except Exception as e: - logger.exception("SzxxView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - - - - - -class CwddhqlxView(APIView): - """ - 获取订单类型(商品类型)用于筛选 - POST /houtai/cwddhqlx - Body: { "phone": "管理员手机号" } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - - if 'dingdancaiwu' not in permissions: - return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403) - - # 获取非审核状态(shenhezhuangtai=1)的商品类型,只返回ID和名称 - types = ShangpinLeixing.query.filter(shenhezhuangtai=1).values('id', 'jieshao') - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': list(types) - }) - except Exception as e: - logger.exception("CwddhqlxView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - -class CwhqjtddsjView(APIView): - """ - 订单详细统计数据(修正版) - POST /houtai/cwhqjtddsj - Body: { - "phone": "管理员手机号", - "granularity": "day" | "month" | "year", - "year": 2026, - "month": 5, - "type_id": 1, # 商品类型ID,0表示全部 - "order_source": 1, # 1=平台单,2=商家单,0=全部 - "summary_date": "2026-05-16" - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'dingdancaiwu' not in permissions: - return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403) - - granularity = request.data.get('granularity', 'day') - year = request.data.get('year') - month = request.data.get('month') - type_id = request.data.get('type_id', 0) - order_source = request.data.get('order_source', 0) - summary_date = request.data.get('summary_date') - - now = date.today() - - # ---------- 时间范围 ---------- - if granularity == 'day': - if not year or not month: - return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) - start_date = date(year, month, 1) - end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) - trunc_func = TruncDate('CreateTime') - if not summary_date: - summary_date = now.strftime('%Y-%m-%d') - summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() - elif granularity == 'month': - if not year: - return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncMonth('CreateTime') - if not summary_date: - summary_date = now.strftime('%Y-%m') - summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() - elif granularity == 'year': - year = year or now.year - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncYear('CreateTime') - if not summary_date: - summary_date = str(year) - summary_q = date(year, 1, 1) - else: - return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) - - start_dt = datetime.combine(start_date, datetime.min.time()) - end_dt = datetime.combine(end_date, datetime.min.time()) - - # ---------- 基础过滤 ---------- - filters = { - 'CreateTime__gte': start_dt, - 'CreateTime__lt': end_dt, - 'Status__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态 - } - if int(type_id) != 0: - filters['ProductTypeID'] = type_id - if int(order_source) in [1, 2]: - filters['Platform'] = order_source - - # ---------- 时间序列分组查询 ---------- - order_base = filter_queryset_by_club( - Order.query.filter(**filters), request, club_field='ClubID', - ) - qs = order_base \ - .annotate(period=trunc_func) \ - .values('period') \ - .annotate( - # 订单总量(所有符合条件的状态) - order_count=Count('id'), - # 订单总额 - order_amount=Sum('Amount'), - # 成交单量 (Status=3) - success_count=Count('id', filter=Q(Status=3)), - # 成交订单总额 - success_amount=Sum('Amount', filter=Q(Status=3)), - # 退款单量 (Status=5) - refund_count=Count('id', filter=Q(Status=5)), - # 打手分成总额(仅成交订单) - PlayerCommission_sum=Sum('PlayerCommission', filter=Q(Status=3)), - # 平台成交订单的店铺分红 - dianpu_fenhong_sum=Sum('pingtai_kuozhan__ShopIncome', - filter=Q(Status=3, Platform=1)), - # 成交订单中:平台单金额、商家单金额、各自打手分成 - platform_success_amount=Sum('Amount', filter=Q(Status=3, Platform=1)), - merchant_success_amount=Sum('Amount', filter=Q(Status=3, Platform=2)), - platform_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)), - merchant_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)), - ).order_by('period') - - time_series = [] - for item in qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - - # 平台收益计算 - platform_profit = (item['platform_success_amount'] or 0) \ - - (item['platform_dashou'] or 0) \ - - (item['dianpu_fenhong_sum'] or 0) - merchant_profit = (item['merchant_success_amount'] or 0) \ - - (item['merchant_dashou'] or 0) - total_profit = platform_profit + merchant_profit - - time_series.append({ - 'date': period_str, - 'order_count': item['order_count'], - 'order_amount': float(item['order_amount'] or 0), - 'success_count': item['success_count'], - 'success_amount': float(item['success_amount'] or 0), - 'refund_count': item['refund_count'], - 'dashou_fencheng': float(item['PlayerCommission_sum'] or 0), - 'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0), - 'profit': round(total_profit, 2) - }) - - # ---------- 汇总数据 ---------- - summary_qs = filter_queryset_by_club( - Order.query.filter(**filters), request, club_field='ClubID', - ) - total_order_count = summary_qs.count() - total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0 - total_success_count = summary_qs.filter(Status=3).count() - total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0 - total_refund_count = summary_qs.filter(Status=5).count() - total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0 - total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate( - s=Sum('pingtai_kuozhan__ShopIncome'))['s'] or 0 - - platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0 - merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0 - platform_dashou_sum = \ - summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0 - merchant_dashou_sum = \ - summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0 - - profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong - profit_merchant = merchant_amount - merchant_dashou_sum - total_profit = profit_platform + profit_merchant - - summary = { - 'total_order_count': total_order_count, - 'total_order_amount': round(float(total_order_amount), 2), - 'total_success_count': total_success_count, - 'total_success_amount': round(float(total_success_amount), 2), - 'total_refund_count': total_refund_count, - 'total_PlayerCommission': round(float(total_dashou), 2), - 'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2), - 'total_profit': round(total_profit, 2) - } - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'time_series': time_series, - 'summary': summary - } - }) - except Exception as e: - logger.exception("CwhqjtddsjView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - - - - - - - -class CwqtczhqView(APIView): - """ - 其他充值/罚款数据统计(非会员充值) - POST /houtai/cwqtczhq - Body: { - "phone": "管理员手机号", - "granularity": "day" | "month" | "year", - "year": 2026, - "month": 5, // day时必填 - "types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款 - "summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY) - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'qtczcaiwu' not in permissions: - return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403) - - granularity = request.data.get('granularity', 'day') - year = request.data.get('year') - month = request.data.get('month') - types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部 - summary_date = request.data.get('summary_date') - - if isinstance(types, list) and len(types) == 0: - types = [2, 3, 4, 5, 6] - - now = date.today() - - # ---------- 时间范围(用于曲线图) ---------- - if granularity == 'day': - if not year or not month: - return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) - start_date = date(year, month, 1) - end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) - trunc_func = TruncDate('CreateTime') - # 默认汇总日期 - if not summary_date: - summary_date = now.strftime('%Y-%m-%d') - summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() - elif granularity == 'month': - if not year: - return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncMonth('CreateTime') - if not summary_date: - summary_date = now.strftime('%Y-%m') - summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() - elif granularity == 'year': - year = year or now.year - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncYear('CreateTime') - if not summary_date: - summary_date = str(year) - summary_q = date(year, 1, 1) - else: - return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) - - start_dt = datetime.combine(start_date, datetime.min.time()) - end_dt = datetime.combine(end_date, datetime.min.time()) - - # ---------- 构建时间序列 ---------- - time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} } - - def ensure_date(ds): - if ds not in time_series_map: - time_series_map[ds] = {} - return time_series_map[ds] - - # 1. 处理充值记录(Czjilu)—— 完全不变 - cz_types = [t for t in types if t in [2, 3, 4, 5]] - if cz_types: - cz_qs = filter_queryset_by_club( - Czjilu.query.filter( - CreateTime__gte=start_dt, - CreateTime__lt=end_dt, - leixing__in=cz_types, - zhuangtai=3, - ), - request, - ).annotate( - period=trunc_func - ).values('period', 'leixing').annotate( - total_count=Count('id'), - total_amount=Sum('jine') - ).order_by('period', 'leixing') - - for item in cz_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - lx = item['leixing'] - amount = float(item['total_amount'] or 0) - count = item['total_count'] - # 收益:积分(3)收益为金额本身,其他为0 - profit = amount if lx == 3 else 0.0 - - day_data = ensure_date(period_str) - day_data[str(lx)] = { - 'count': count, - 'amount': amount, - 'profit': profit, - 'fenhong_count': 0, # 充值类型无分红 - 'fenhong_amount': 0.0 - } - - # 2. 处理罚款(Penalty)—— 修改:直接从 Penalty 表获取分红总额和分红笔数 - if 6 in types: - # 使用一次聚合查询,同时获取罚款笔数、罚款金额、分红总额、分红笔数 - penalty_qs = filter_penalty_qs( - Penalty.query.filter( - CreateTime__gte=start_dt, - CreateTime__lt=end_dt, - Status=2, - ), - request, - ).annotate( - period=trunc_func - ).values('period').annotate( - penalty_count=Count('id'), - penalty_amount=Sum('FineAmount'), - # 直接使用罚单表中的申请人分红金额字段 - fenhong_amount=Sum('ApplicantBonusAmount'), - fenhong_count=Count('id', filter=Q(ApplicantBonusAmount__gt=0)) - ).order_by('period') - - for item in penalty_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - day_data = ensure_date(period_str) - penalty_amt = float(item['penalty_amount'] or 0) - fenhong_amt = float(item['fenhong_amount'] or 0) - profit = round(penalty_amt - fenhong_amt, 2) - day_data['6'] = { - 'count': item['penalty_count'], - 'amount': penalty_amt, - 'profit': profit, - 'fenhong_count': item['fenhong_count'], - 'fenhong_amount': fenhong_amt - } - - # 构造有序时间序列列表 - time_series = [] - for period_str in sorted(time_series_map.keys()): - entry = {'date': period_str, 'types': {}} - for type_code in sorted(time_series_map[period_str].keys()): - entry['types'][type_code] = time_series_map[period_str][type_code] - time_series.append(entry) - - # ---------- 汇总指定日期(summary)—— 完全保留原逻辑,仅将罚款部分改为从 Penalty 表直接取分红 ---------- - summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0} - - # 汇总充值类型(完全不变) - for lx in cz_types: - filter_kwargs = { - 'leixing': lx, - 'zhuangtai': 3, - } - if granularity == 'day': - filter_kwargs['CreateTime__date'] = summary_q - elif granularity == 'month': - filter_kwargs['CreateTime__year'] = summary_q.year - filter_kwargs['CreateTime__month'] = summary_q.month - else: - filter_kwargs['CreateTime__year'] = summary_q.year - agg = filter_queryset_by_club( - Czjilu.query.filter(**filter_kwargs), request, - ).aggregate(count=Count('id'), amount=Sum('jine')) - count = agg['count'] or 0 - amount = float(agg['amount'] or 0) - profit = amount if lx == 3 else 0.0 - summary_result['by_type'][str(lx)] = { - 'count': count, - 'amount': round(amount, 2), - 'profit': round(profit, 2), - 'fenhong_count': 0, - 'fenhong_amount': 0.0 - } - summary_result['total_count'] += count - summary_result['total_amount'] += amount - summary_result['total_profit'] += profit - - # 汇总罚款(修改:从 Penalty 表直接取分红,不再查询 PenaltyBonus) - if 6 in types: - penalty_filter = { - 'Status': 2, - } - if granularity == 'day': - penalty_filter['CreateTime__date'] = summary_q - elif granularity == 'month': - penalty_filter['CreateTime__year'] = summary_q.year - penalty_filter['CreateTime__month'] = summary_q.month - else: - penalty_filter['CreateTime__year'] = summary_q.year - - # 一次聚合:罚款笔数、金额、分红总额、分红笔数 - agg = filter_penalty_qs( - Penalty.query.filter(**penalty_filter), request, - ).aggregate( - count=Count('id'), - amount=Sum('FineAmount'), - fenhong_amount=Sum('ApplicantBonusAmount'), - fenhong_count=Count('id', filter=Q(ApplicantBonusAmount__gt=0)) - ) - penalty_count = agg['count'] or 0 - penalty_amount = float(agg['amount'] or 0) - fh_amount = float(agg['fenhong_amount'] or 0) - fh_count = agg['fenhong_count'] or 0 - profit = round(penalty_amount - fh_amount, 2) - - summary_result['by_type']['6'] = { - 'count': penalty_count, - 'amount': round(penalty_amount, 2), - 'profit': profit, - 'fenhong_count': fh_count, - 'fenhong_amount': round(fh_amount, 2) - } - summary_result['total_count'] += penalty_count - summary_result['total_amount'] += penalty_amount - summary_result['total_profit'] += profit - - summary_result['total_amount'] = round(summary_result['total_amount'], 2) - summary_result['total_profit'] = round(summary_result['total_profit'], 2) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'time_series': time_series, - 'summary': summary_result - } - }) - except Exception as e: - logger.exception("CwqtczhqView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - -'''class CwqtczhqView(APIView): - """ - 其他充值/罚款数据统计(非会员充值) - POST /houtai/cwqtczhq - Body: { - "phone": "管理员手机号", - "granularity": "day" | "month" | "year", - "year": 2026, - "month": 5, // day时必填 - "types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款 - "summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY) - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'qtczcaiwu' not in permissions: - return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403) - - granularity = request.data.get('granularity', 'day') - year = request.data.get('year') - month = request.data.get('month') - types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部 - summary_date = request.data.get('summary_date') - - if isinstance(types, list) and len(types) == 0: - types = [2, 3, 4, 5, 6] - - now = date.today() - - # ---------- 时间范围(用于曲线图) ---------- - if granularity == 'day': - if not year or not month: - return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) - start_date = date(year, month, 1) - end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) - trunc_func = TruncDate('CreateTime') - # 默认汇总日期 - if not summary_date: - summary_date = now.strftime('%Y-%m-%d') - summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() - elif granularity == 'month': - if not year: - return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncMonth('CreateTime') - if not summary_date: - summary_date = now.strftime('%Y-%m') - summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() - elif granularity == 'year': - year = year or now.year - start_date = date(year, 1, 1) - end_date = date(year + 1, 1, 1) - trunc_func = TruncYear('CreateTime') - if not summary_date: - summary_date = str(year) - summary_q = date(year, 1, 1) - else: - return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) - - start_dt = datetime.combine(start_date, datetime.min.time()) - end_dt = datetime.combine(end_date, datetime.min.time()) - - # ---------- 构建时间序列 ---------- - time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} } - - def ensure_date(ds): - if ds not in time_series_map: - time_series_map[ds] = {} - return time_series_map[ds] - - # 1. 处理充值记录(Czjilu) - cz_types = [t for t in types if t in [2, 3, 4, 5]] - if cz_types: - cz_qs = Czjilu.query.filter( - CreateTime__gte=start_dt, - CreateTime__lt=end_dt, - leixing__in=cz_types, - zhuangtai=3 # 只统计支付成功的 - ).annotate( - period=trunc_func - ).values('period', 'leixing').annotate( - total_count=Count('id'), - total_amount=Sum('jine') - ).order_by('period', 'leixing') - - for item in cz_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - lx = item['leixing'] - amount = float(item['total_amount'] or 0) - count = item['total_count'] - # 收益:积分(3)收益为金额本身,其他为0 - profit = amount if lx == 3 else 0.0 - - day_data = ensure_date(period_str) - day_data[str(lx)] = { - 'count': count, - 'amount': amount, - 'profit': profit, - 'fenhong_count': 0, # 充值类型无分红 - 'fenhong_amount': 0.0 - } - - # 2. 处理罚款(Penalty)—— 包含分红 - if 6 in types: - # 罚款记录 - penalty_qs = Penalty.query.filter( - CreateTime__gte=start_dt, - CreateTime__lt=end_dt, - Status=2 # 已缴纳 - ).annotate( - period=trunc_func - ).values('period').annotate( - penalty_count=Count('id'), - penalty_amount=Sum('FineAmount') - ).order_by('period') - - # 分红记录 - fenhong_qs = PenaltyBonus.query.filter( - CreateTime__gte=start_dt, - CreateTime__lt=end_dt - ).annotate( - period=trunc_func - ).values('period').annotate( - fh_count=Count('id'), - fh_amount=Sum('WithdrawableAmount') - ).order_by('period') - - # 合并 - for item in penalty_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - day_data = ensure_date(period_str) - day_data['6'] = { - 'count': item['penalty_count'], - 'amount': float(item['penalty_amount'] or 0), - 'profit': 0.0, # 先设为0,后续计算 - 'fenhong_count': 0, - 'fenhong_amount': 0.0 - } - - for item in fenhong_qs: - period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ - item['period'].strftime('%Y-%m') if granularity == 'month' else \ - item['period'].strftime('%Y') - day_data = ensure_date(period_str) - if '6' not in day_data: - day_data['6'] = {'count': 0, 'amount': 0.0, 'profit': 0.0, 'fenhong_count': 0, 'fenhong_amount': 0.0} - day_data['6']['fenhong_count'] += item['fh_count'] - day_data['6']['fenhong_amount'] += float(item['fh_amount'] or 0) - - # 计算罚款收益 = 罚款金额 - 分红金额 - for period_str, day_data in time_series_map.items(): - if '6' in day_data: - penalty = day_data['6'] - penalty['profit'] = round(penalty['amount'] - penalty['fenhong_amount'], 2) - - # 构造有序时间序列列表 - time_series = [] - for period_str in sorted(time_series_map.keys()): - entry = {'date': period_str, 'types': {}} - for type_code in sorted(time_series_map[period_str].keys()): - entry['types'][type_code] = time_series_map[period_str][type_code] - time_series.append(entry) - - # ---------- 汇总指定日期 ---------- - summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0} - - # 汇总充值类型 - for lx in cz_types: - filter_kwargs = { - 'leixing': lx, - 'zhuangtai': 3, - } - if granularity == 'day': - filter_kwargs['CreateTime__date'] = summary_q - elif granularity == 'month': - filter_kwargs['CreateTime__year'] = summary_q.year - filter_kwargs['CreateTime__month'] = summary_q.month - else: - filter_kwargs['CreateTime__year'] = summary_q.year - agg = filter_queryset_by_club( - Czjilu.query.filter(**filter_kwargs), request, - ).aggregate(count=Count('id'), amount=Sum('jine')) - count = agg['count'] or 0 - amount = float(agg['amount'] or 0) - profit = amount if lx == 3 else 0.0 - summary_result['by_type'][str(lx)] = { - 'count': count, - 'amount': round(amount, 2), - 'profit': round(profit, 2), - 'fenhong_count': 0, - 'fenhong_amount': 0.0 - } - summary_result['total_count'] += count - summary_result['total_amount'] += amount - summary_result['total_profit'] += profit - - # 汇总罚款 - if 6 in types: - penalty_filter = { - 'Status': 2, - } - fenhong_filter = {} - if granularity == 'day': - penalty_filter['CreateTime__date'] = summary_q - fenhong_filter['CreateTime__date'] = summary_q - elif granularity == 'month': - penalty_filter['CreateTime__year'] = summary_q.year - penalty_filter['CreateTime__month'] = summary_q.month - fenhong_filter['CreateTime__year'] = summary_q.year - fenhong_filter['CreateTime__month'] = summary_q.month - else: - penalty_filter['CreateTime__year'] = summary_q.year - fenhong_filter['CreateTime__year'] = summary_q.year - - penalty_agg = Penalty.query.filter(**penalty_filter).aggregate( - count=Count('id'), amount=Sum('FineAmount')) - penalty_count = penalty_agg['count'] or 0 - penalty_amount = float(penalty_agg['amount'] or 0) - - fenhong_agg = PenaltyBonus.query.filter(**fenhong_filter).aggregate( - fh_count=Count('id'), fh_amount=Sum('WithdrawableAmount')) - fh_count = fenhong_agg['fh_count'] or 0 - fh_amount = float(fenhong_agg['fh_amount'] or 0) - - profit = round(penalty_amount - fh_amount, 2) - summary_result['by_type']['6'] = { - 'count': penalty_count, - 'amount': round(penalty_amount, 2), - 'profit': profit, - 'fenhong_count': fh_count, - 'fenhong_amount': round(fh_amount, 2) - } - summary_result['total_count'] += penalty_count - summary_result['total_amount'] += penalty_amount - summary_result['total_profit'] += profit - - summary_result['total_amount'] = round(summary_result['total_amount'], 2) - summary_result['total_profit'] = round(summary_result['total_profit'], 2) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'time_series': time_series, - 'summary': summary_result - } - }) - except Exception as e: - logger.exception("CwqtczhqView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)''' - - - - - -# ==================== 后端接口 ==================== -# 文件:views.py (添加以下两个视图类) - - - -class KhpzhqView(APIView): - """ - 获取所有考核配置数据(称号列表、板块、费用) - POST /houtai/khpzhq - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'kaohepeizhi' not in permissions: - return Response({'code': 403, 'msg': '无考核配置权限'}, status=403) - - bankuai_list = list(Bankuai.query.values('bankuai_id', 'mingcheng')) - - chenghao_qs = Chenghao.query.prefetch_related('kaohecishufeiyong_set').all() - chenghao_list = [] - for ch in chenghao_qs: - # 获取费用:按次数排序,转为 {cishu: feiyong} 字典 - feiyong_dict = { - item.cishu: float(item.feiyong) - for item in ch.kaohecishufeiyong_set.order_by('cishu') - } - max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0 - - chenghao_list.append({ - 'id': ch.id, - 'mingcheng': ch.mingcheng, - 'leixing': ch.leixing, - 'leixing_name': ch.get_leixing_display(), - 'bankuai_id': ch.bankuai_id, - 'tubiao_url': ch.tubiao_url or '', - 'texiao_miaoshu': ch.texiao_miaoshu or '', - 'CreateTime': ch.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if ch.CreateTime else '', - 'kaohe_guize': ch.kaohe_guize or '', - 'kaioi_jinpai': ch.kaioi_jinpai, - 'feiyong': feiyong_dict, # {1:100.0, 2:200.0} - 'max_cishu': max_cishu - }) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'bankuai': bankuai_list, - 'chenghao': chenghao_list - } - }) - except Exception as e: - logger.exception("KhpzhqView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - -class ChzsgcView(APIView): - """ - 称号(标签)增删改查统一接口 - POST /houtai/chzsgc - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'kaohepeizhi' not in permissions: - return Response({'code': 403, 'msg': '无考核配置权限'}, status=403) - - action = request.data.get('action') - if not action: - return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400) - - # ========== 创建称号 ========== - if action == 'create': - mingcheng = request.data.get('mingcheng', '').strip() - leixing = request.data.get('leixing', 'dashou') - bankuai_id = request.data.get('bankuai_id') - texiao_miaoshu = request.data.get('texiao_miaoshu', '') - kaohe_guize = request.data.get('kaohe_guize', '') - kaioi_jinpai = request.data.get('kaioi_jinpai', False) - feiyong_list = request.data.get('feiyong_list', []) - - if not mingcheng: - return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) - if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']: - return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) - if Chenghao.query.filter(mingcheng=mingcheng).exists(): - return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) - - with transaction.atomic(): - ch = Chenghao.query.create( - mingcheng=mingcheng, - leixing=leixing, - bankuai_id=bankuai_id, - texiao_miaoshu=texiao_miaoshu, - kaohe_guize=kaohe_guize, - kaioi_jinpai=kaioi_jinpai - ) - # 处理费用:前端传来 [{cishu:1, feiyong:100}, ...] - if feiyong_list: - for item in feiyong_list: - cishu = int(item.get('cishu', 1)) - feiyong = float(item.get('feiyong', 0)) - KaoheCishuFeiyong.query.update_or_create( - chenghao=ch, cishu=cishu, - defaults={'feiyong': feiyong} - ) - else: - # 默认添加第一次免费 - KaoheCishuFeiyong.query.create(chenghao=ch, cishu=1, feiyong=0) - - return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}}) - - # ========== 修改称号 ========== - elif action == 'update': - ch_id = request.data.get('chenghao_id') - if not ch_id: - return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) - try: - ch = Chenghao.query.get(id=ch_id) - except Chenghao.DoesNotExist: - return Response({'code': 1, 'msg': '称号不存在'}, status=404) - - mingcheng = request.data.get('mingcheng', ch.mingcheng).strip() - leixing = request.data.get('leixing', ch.leixing) - bankuai_id = request.data.get('bankuai_id', ch.bankuai_id) - texiao_miaoshu = request.data.get('texiao_miaoshu', ch.texiao_miaoshu) - kaohe_guize = request.data.get('kaohe_guize', ch.kaohe_guize) - kaioi_jinpai = request.data.get('kaioi_jinpai', ch.kaioi_jinpai) - - if not mingcheng: - return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) - if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']: - return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) - if Chenghao.query.filter(mingcheng=mingcheng).exclude(id=ch_id).exists(): - return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) - - ch.mingcheng = mingcheng - ch.leixing = leixing - ch.bankuai_id = bankuai_id - ch.texiao_miaoshu = texiao_miaoshu - ch.kaohe_guize = kaohe_guize - ch.kaioi_jinpai = kaioi_jinpai - ch.save() - - # 如果前端传递了 feiyong_list,则全量替换费用 - feiyong_list = request.data.get('feiyong_list') - if feiyong_list is not None: - with transaction.atomic(): - ch.kaohecishufeiyong_set.all().delete() - for item in feiyong_list: - KaoheCishuFeiyong.query.create( - chenghao=ch, - cishu=int(item.get('cishu', 1)), - feiyong=float(item.get('feiyong', 0)) - ) - - return Response({'code': 0, 'msg': '修改成功'}) - - # ========== 删除称号 ========== - elif action == 'delete': - ch_id = request.data.get('chenghao_id') - if not ch_id: - return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) - try: - ch = Chenghao.query.get(id=ch_id) - except Chenghao.DoesNotExist: - return Response({'code': 1, 'msg': '称号不存在'}, status=404) - ch.delete() - return Response({'code': 0, 'msg': '删除成功'}) - - # ========== 添加/修改某次费用 ========== - elif action == 'add_feiyong': - ch_id = request.data.get('chenghao_id') - cishu = request.data.get('cishu') - feiyong = request.data.get('feiyong', 0) - if not ch_id or not cishu: - return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400) - try: - ch = Chenghao.query.get(id=ch_id) - except Chenghao.DoesNotExist: - return Response({'code': 1, 'msg': '称号不存在'}, status=404) - if int(cishu) < 1: - return Response({'code': 1, 'msg': '次数必须>=1'}, status=400) - KaoheCishuFeiyong.query.update_or_create( - chenghao=ch, cishu=cishu, - defaults={'feiyong': feiyong} - ) - return Response({'code': 0, 'msg': '添加/修改费用成功'}) - - # ========== 删除某次费用 ========== - elif action == 'remove_feiyong': - ch_id = request.data.get('chenghao_id') - cishu = request.data.get('cishu') - if not ch_id or not cishu: - return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400) - if int(cishu) == 1: - return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400) - try: - ch = Chenghao.query.get(id=ch_id) - except Chenghao.DoesNotExist: - return Response({'code': 1, 'msg': '称号不存在'}, status=404) - KaoheCishuFeiyong.query.filter(chenghao=ch, cishu=cishu).delete() - return Response({'code': 0, 'msg': '删除成功'}) - - # ========== 修改某次费用(等价于add) ========== - elif action == 'update_feiyong': - return self.add_feiyong(request) # 复用逻辑 - - else: - return Response({'code': 1, 'msg': '未知操作'}, status=400) - - except Exception as e: - logger.exception("ChzsgcView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - -# ==================== 后端 views.py ==================== -# 请确保已有以下导入,如果没有请在文件头部添加 - - - - -class KhgglView(APIView): - """ - 考核官管理数据接口(列表、筛选、分页) - POST /houtai/khggl - Body: { - "phone": "管理员手机号", - "page": 1, // 可选,默认1 - "page_size": 15, // 可选,默认15 - "yonghuid": "1234567", // 按用户ID筛选,可选 - "zhuangtai": 1, // 账号状态 0=禁用,1=正常,可选 - "shenhe_zhuangtai": 0, // 审核官状态 0=空闲,1=审核中,可选 - "bankuai_name": "王者" // 板块名称模糊筛选,可选 - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - - if 'kaohepeizhi' not in permissions: - return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) - - # 分页参数 - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 15)) - if page < 1: page = 1 - if page_size < 1: page_size = 15 - - # 筛选条件 - filter_yonghuid = request.data.get('yonghuid', '').strip() - filter_zhuangtai = request.data.get('zhuangtai') - filter_shenhe_zhuangtai = request.data.get('shenhe_zhuangtai') - filter_bankuai_name = request.data.get('bankuai_name', '').strip() - - # 基础查询:所有审核官,预加载用户及打手扩展表 - queryset = filter_user_related_by_club( - UserShenheguan.query.select_related('user__DashouProfile'), - request, - ) - - if filter_yonghuid: - queryset = queryset.filter(user__UserUID=filter_yonghuid) - if filter_zhuangtai is not None: - queryset = queryset.filter(zhuangtai=filter_zhuangtai) - if filter_shenhe_zhuangtai is not None: - queryset = queryset.filter(shenhe_zhuangtai=filter_shenhe_zhuangtai) - - # 如果填写了板块名称,筛选出关联该板块的审核官 - if filter_bankuai_name: - bankuai_ids = Bankuai.query.filter( - mingcheng__icontains=filter_bankuai_name - ).values_list('bankuai_id', flat=True) - shenheguan_ids = KaoheguanBankuai.query.filter( - bankuai_id__in=bankuai_ids - ).values_list('kaoheguan_id', flat=True) - queryset = queryset.filter(user__UserUID__in=shenheguan_ids) - - total = queryset.count() - start = (page - 1) * page_size - end = start + page_size - shenheguan_list = queryset.order_by('-CreateTime')[start:end] - - from jituan.services.shenhe_club import yonghu_chenghao_qs_for_request - - # ---------- 预计算每个板块的打手数量 ---------- - bankuai_dashou_count_map = {} - all_banquai = Bankuai.query.all() - for bk in all_banquai: - tag_ids = Chenghao.query.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True) - count = yonghu_chenghao_qs_for_request( - YonghuChenghao.query.filter( - chenghao_id__in=tag_ids, - yonghu__DashouProfile__isnull=False, - ), - request, - ).values('yonghu').distinct().count() - bankuai_dashou_count_map[bk.bankuai_id] = count - - # ---------- 新增:板块标签统计 ---------- - bankuai_tags_data = [] - for bk in all_banquai: - tags = Chenghao.query.filter(bankuai=bk, leixing='dashou') - tag_list = [] - for tag in tags: - user_count = yonghu_chenghao_qs_for_request( - YonghuChenghao.query.filter(chenghao=tag), - request, - ).values('yonghu').distinct().count() - tag_list.append({ - 'id': tag.id, - 'name': tag.mingcheng, - 'user_count': user_count, - }) - bankuai_tags_data.append({ - 'bankuai_id': bk.bankuai_id, - 'mingcheng': bk.mingcheng, - 'tags': tag_list, - }) - - # 构造返回数据(审核官列表) - data_list = [] - for sg in shenheguan_list: - user = sg.user - # 获取该审核官关联的板块 - bk_qs = KaoheguanBankuai.query.filter(kaoheguan_id=user.UserUID).select_related('bankuai') - bankuai_info = [] - for bk in bk_qs: - bid = bk.bankuai.bankuai_id - bankuai_info.append({ - 'bankuai_id': bid, - 'mingcheng': bk.bankuai.mingcheng, - 'dashou_count': bankuai_dashou_count_map.get(bid, 0) - }) - - dashou_nick = user.DashouProfile.nicheng if hasattr(user, 'DashouProfile') and user.DashouProfile else '' - - data_list.append({ - 'yonghuid': user.UserUID, - 'avatar': user.Avatar or '', - 'phone': user.Phone or '', - 'zhuangtai': sg.zhuangtai, - 'shenhe_zhuangtai': sg.shenhe_zhuangtai, - 'shenhe_zongshu': sg.shenhe_zongshu, - 'tongguo_zongshu': sg.tongguo_zongshu, - 'yue': float(sg.yue), - 'zonge': float(sg.zonge), - 'CreateTime': sg.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.CreateTime else '', - 'UpdateTime': sg.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.UpdateTime else '', - 'bankuai': bankuai_info, - 'nicheng': dashou_nick, - 'is_renzheng': sg.is_renzheng, - }) - - all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng')) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'total': total, - 'page': page, - 'page_size': page_size, - 'list': data_list, - 'all_bankuai': all_bankuai, - 'bankuai_tags': bankuai_tags_data, # 新增板块标签统计 - } - }) - - except Exception as e: - logger.exception("KhgglView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - -class ShgxgsjView(APIView): - """ - 审核官信息修改(余额、状态、板块关联) - POST /houtai/shgxgsj - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'kaohepeizhi' not in permissions: - return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) - - yonghuid = request.data.get('yonghuid') - if not yonghuid: - return Response({'code': 1, 'msg': '缺少用户ID'}, status=400) - - try: - shenheguan = UserShenheguan.query.select_related('user').get(user__UserUID=yonghuid) - except UserShenheguan.DoesNotExist: - return Response({'code': 1, 'msg': '审核官不存在'}, status=404) - - action = request.data.get('action') - - if action == 'update_info': - if 'yue' in request.data: - shenheguan.yue = float(request.data.get('yue', 0)) - if 'zhuangtai' in request.data: - zt = int(request.data.get('zhuangtai')) - if zt not in [0, 1]: - return Response({'code': 1, 'msg': '无效的账号状态'}, status=400) - shenheguan.zhuangtai = zt - if 'shenhe_zhuangtai' in request.data: - szt = int(request.data.get('shenhe_zhuangtai')) - if szt not in [0, 1]: - return Response({'code': 1, 'msg': '无效的审核官状态'}, status=400) - shenheguan.shenhe_zhuangtai = szt - # 新增认证状态编辑 - if 'is_renzheng' in request.data: - shenheguan.is_renzheng = bool(request.data.get('is_renzheng')) - shenheguan.save() - return Response({'code': 0, 'msg': '修改成功'}) - - elif action == 'add_bankuai': - bankuai_id = request.data.get('bankuai_id') - if not bankuai_id: - return Response({'code': 1, 'msg': '缺少板块ID'}, status=400) - if not Bankuai.query.filter(bankuai_id=bankuai_id).exists(): - return Response({'code': 1, 'msg': '板块不存在'}, status=400) - if KaoheguanBankuai.query.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists(): - return Response({'code': 1, 'msg': '已关联该板块'}, status=400) - KaoheguanBankuai.query.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id) - return Response({'code': 0, 'msg': '板块关联成功'}) - - elif action == 'remove_bankuai': - bankuai_id = request.data.get('bankuai_id') - if not bankuai_id: - return Response({'code': 1, 'msg': '缺少板块ID'}, status=400) - deleted, _ = KaoheguanBankuai.query.filter( - kaoheguan_id=yonghuid, bankuai_id=bankuai_id - ).delete() - if not deleted: - return Response({'code': 1, 'msg': '未找到该板块关联'}, status=400) - return Response({'code': 0, 'msg': '板块移除成功'}) - - else: - return Response({'code': 1, 'msg': '未知操作'}, status=400) - - except Exception as e: - logger.exception("ShgxgsjView 接口错误") - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - - -class ZxkfghdsView(APIView): - """ - 客服更换打手接口 - 路径:POST /houtai/zxkfghds - 功能: - 1. 将当前订单状态改为已退款(5),释放接单打手(若有) - 2. 如需跨平台通知,则调用对方平台退款通知接口 - 3. 基于原订单信息生成全新订单(状态直接为 1,已替老板填写) - 4. 生成新的派单链接,并标记为已使用 - 5. 新链接记录上一个链接ID和上一个订单ID - 6. 【关键】不操作商家余额、退款次数、每日统计,因新订单是替换而非新增扣款 - 权限:需要'003aa'(跨平台管理)权限,与退款接口一致 - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - # ========== 1. 参数提取 ========== - username = request.data.get('username', '').strip() - OrderID = pick_order_id(request.data) - if not username or not OrderID: - logger.warning("更换打手参数不完整") - return Response({'code': 400, 'msg': '参数不完整'}) - - # ========== 2. 权限校验(与跨平台退款相同) ========== - kefu, permissions = verify_kefu_permission(request, username) - if kefu is None: - return permissions # 返回错误响应 - if '003aa' not in permissions: - return Response({'code': 403, 'msg': '您无权处理订单'}) - - current_user = request.user - - # ========== 3. 查询原订单及关联信息 ========== - try: - old_order = Order.query.select_related('shangjia_kuozhan').get(OrderID=OrderID) - except Order.DoesNotExist: - logger.warning(f"订单不存在: {OrderID}") - return Response({'code': 404, 'msg': '订单不存在'}) - - # 仅允许状态为 2(进行中)或 8(结算中)更换打手 - if old_order.Status not in [1,7,2, 8]: - return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'}) - - # 本接口仅处理商家发单(Platform=2) - if old_order.Platform != 2: - return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'}) - - # 获取原链接(用于模板ID等) - try: - old_lianjie = ShangjiaLianjie.query.get(OrderID=OrderID) - except ShangjiaLianjie.DoesNotExist: - logger.error(f"原链接记录不存在,订单: {OrderID}") - return Response({'code': 500, 'msg': '链接数据异常'}) - - # 商家信息(仅用于记录关联,不操作余额) - try: - shangjia_ext = old_order.shangjia_kuozhan - shangjia_id = shangjia_ext.MerchantID - # 获取商家昵称用于新订单扩展表 - shangjia_user = User.query.get(UserUID=shangjia_id) - shangjia_nicheng = shangjia_user.ShopProfile.nicheng or f"商家{shangjia_id}" - except (ObjectDoesNotExist, UserShangjia.DoesNotExist): - logger.error(f"商家信息缺失,订单: {OrderID}") - return Response({'code': 500, 'msg': '商家数据异常'}) - - # ========== 4. 退款处理(仅打手侧,不碰商家) ========== - with transaction.atomic(): - # 5.1 订单状态改为已退款 - old_order.Status = 5 - old_order.AssignedCS = current_user.Phone - old_order.save() - - # 5.2 处理接单打手(若有) - dashou_id = old_order.PlayerID - if dashou_id: - try: - dashou_user = User.query.get(UserUID=dashou_id) - dashou_profile = dashou_user.DashouProfile - dashou_profile.tuikuanliang += 1 # 退款次数+1 - dashou_profile.zhuangtai = 1 # 设为空闲 - dashou_profile.save() - except (User.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"打手 {dashou_id} 不存在,跳过更新") - - # 5.3 更新退款记录表 - RefundRecord.query.update_or_create( - OrderID=OrderID, - defaults={ - 'PlayerID': dashou_id or '', - 'ApplicantID': '', - 'ProcessorID': current_user.Phone, - 'Reason': '客服更换打手退款', - 'ApplyStatus': 1, - 'Amount': old_order.Amount or 0, - 'PlayerCommission': old_order.PlayerCommission or 0, - 'Description': old_order.Description or '', - 'Remark': '更换打手自动退款', - 'Nickname': old_order.Nickname or '', - } - ) - - # 5.4 打手每日统计(退款) - if dashou_id: - try: - update_dashou_daily_by_action( - yonghuid=dashou_id, - amount=old_order.PlayerCommission, - action=3 # 3=退款 - ) - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - - # 5.5 客服处理统计(管理员无 KefuProfile,跳过) - if hasattr(kefu, 'jinrichuli'): - kefu.jinrichuli += 1 - kefu.jinyuechuli += 1 - kefu.zongchuli += 1 - kefu.save() - - # 至此,原订单已退款,事务提交 - - # ========== 6. 生成新订单和链接(不扣商家余额) ========== - try: - with transaction.atomic(): - # ---- 6.1 生成新的订单ID和Token ---- - def generate_OrderID(): - timestamp = int(time.time() * 1000) - process_id = os.getpid() % 1000 - random_num = random.randint(1000, 9999) - return f"SJ{timestamp}{process_id:03d}{random_num}" - - def generate_secure_token(dd_id, uid): - timestamp = int(time.time() * 1_000_000) - salt = secrets.token_hex(8) - raw = f"{timestamp}:{dd_id}:{uid}:{salt}" - token = hashlib.sha256(raw.encode()).hexdigest()[:24] - if ShangjiaLianjie.query.filter(LinkToken=token).exists(): - return generate_secure_token(dd_id, uid) - return token - - def generate_link_url(token): - base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/') - return f"{base}/order/{token}" - - new_OrderID = generate_OrderID() - secure_token = generate_secure_token(new_OrderID, shangjia_id) - - # ---- 6.2 创建新订单(复制核心数据,状态直接为1) ---- - new_order = Order.query.create( - OrderID=new_OrderID, - Status=1, # 下单中 - Platform=2, - Amount=old_order.Amount, - PlayerCommission=old_order.PlayerCommission, - ProductTypeID=old_order.ProductTypeID, - GrabRequirement=old_order.GrabRequirement, - MembershipID=old_order.MembershipID, - CommissionReq=old_order.CommissionReq, - Description=old_order.Description, - Nickname=old_order.Nickname, # 继承游戏昵称 - Remark=old_order.Remark, - User1ID=old_order.User1ID, # 商家标识 - ImageURL=old_order.ImageURL, - ClubID=getattr(old_order, 'ClubID', None) or 'xq', - # 注意:不继承 AssignedID、PlayerID - ) - - # ---- 6.3 创建商家扩展表 ---- - MerchantOrderExt.query.create( - Order=new_order, - MerchantID=shangjia_id, - MerchantNickname=shangjia_nicheng, - ) - - # ---- 6.4 生成新链接并关联旧链接信息 ---- - link_url = generate_link_url(secure_token) - new_lianjie = ShangjiaLianjie.query.create( - UserID=shangjia_id, - ProductTypeID=old_lianjie.ProductTypeID, - TemplateID=old_lianjie.TemplateID, - LinkURL=link_url, - LinkToken=secure_token, - OrderID=new_OrderID, - is_used=True, # 直接标记已使用 - ExpireTime=timezone.now() + timezone.timedelta(days=1), - PreviousLinkID=old_lianjie.id, # 记录旧链接ID - PreviousOrderID=old_order.OrderID, # 记录旧订单ID - ) - - # ---- 6.5 异步通知(广播新订单、跨平台同步) ---- - try: - order_info = { - 'OrderID': new_OrderID, - 'game_type': self._get_game_type_name(new_order.ProductTypeID), - 'amount': str(new_order.Amount), - 'order_desc': (new_order.Description or '')[:50], - } - dingdan_guangbo.delay(order_info) - except Exception as e: - logger.error(f"广播通知失败: {e}") - - # 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单 - - logger.info(f"更换打手成功:旧订单{OrderID}→退款,新订单{new_OrderID}已生成") - - return Response({ - 'code': 0, - 'msg': '更换成功', - 'data': { - 'new_OrderID': new_OrderID, - 'new_dingdan_id': new_OrderID, - 'new_lianjie': link_url, - } - }) - - except Exception as e: - logger.error(f"生成新订单事务失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '生成新订单失败'}) - - def _get_game_type_name(self, ProductTypeID): - """根据类型ID获取游戏类型名称""" - try: - gt = ShangpinLeixing.query.filter(id=ProductTypeID).first() - return gt.jieshao if gt else '游戏订单' - except Exception: - return '游戏订单' - - -# ==================== 考核记录管理 ==================== - -SHENHE_ZHUANGTAI_MAP = { - 0: '考核中', - 1: '已通过', - 2: '未通过', - 3: '待审核', -} - - -def _batch_user_profile_map(user_uids): - """批量获取用户头像、昵称、手机号""" - uids = list({u for u in user_uids if u}) - if not uids: - return {} - users = User.query.filter(UserUID__in=uids) - dashou_nicks = { - d.user.UserUID: d.nicheng - for d in UserDashou.query.filter(user__UserUID__in=uids).select_related('user') - } - result = {} - for u in users: - result[u.UserUID] = { - 'yonghuid': u.UserUID, - 'avatar': u.Avatar or '', - 'phone': u.Phone or '', - 'nicheng': dashou_nicks.get(u.UserUID) or u.UserName or u.Phone or u.UserUID, - } - return result - - -def _build_shenhe_jilu_filters(request_data): - """构造 ShenheJilu 查询条件(列表与统计共用)""" - qs = ShenheJilu.query.select_related('bankuai', 'chenghao') - - jilu_id = (request_data.get('jilu_id') or '').strip() - if jilu_id: - qs = qs.filter(jilu_id=jilu_id) - - zhuangtai = request_data.get('zhuangtai') - if zhuangtai is not None and zhuangtai != '': - qs = qs.filter(zhuangtai=int(zhuangtai)) - - assessed = request_data.get('assessed') - if assessed == 'done': - qs = qs.filter(zhuangtai__in=[1, 2]) - elif assessed == 'undone': - qs = qs.filter(zhuangtai__in=[0, 3]) - - bankuai_id = request_data.get('bankuai_id') - if bankuai_id not in (None, ''): - qs = qs.filter(bankuai_id=int(bankuai_id)) - - chenghao_id = request_data.get('chenghao_id') - if chenghao_id not in (None, ''): - qs = qs.filter(chenghao_id=int(chenghao_id)) - - cishu = request_data.get('cishu') - if cishu not in (None, ''): - qs = qs.filter(cishu=int(cishu)) - - dashou_kw = (request_data.get('dashou_keyword') or '').strip() - if dashou_kw: - dashou_uids = list( - User.query.filter( - Q(UserUID=dashou_kw) | - Q(UserName__icontains=dashou_kw) | - Q(Phone__icontains=dashou_kw) | - Q(DashouProfile__nicheng__icontains=dashou_kw) - ).values_list('UserUID', flat=True) - ) - if dashou_uids: - qs = qs.filter(shenqingren_id__in=dashou_uids) - else: - qs = qs.none() - - kg_kw = (request_data.get('kaoheguan_keyword') or '').strip() - if kg_kw: - kg_uids = list( - User.query.filter( - Q(UserUID=kg_kw) | - Q(UserName__icontains=kg_kw) | - Q(Phone__icontains=kg_kw) | - Q(DashouProfile__nicheng__icontains=kg_kw) - ).values_list('UserUID', flat=True) - ) - if kg_uids: - qs = qs.filter(shenheguan_id__in=kg_uids) - else: - qs = qs.none() - - start_time = (request_data.get('start_time') or '').strip() - end_time = (request_data.get('end_time') or '').strip() - if start_time: - qs = qs.filter(CreateTime__gte=start_time) - if end_time: - qs = qs.filter(CreateTime__lte=end_time) - - has_kaoheguan = request_data.get('has_kaoheguan') - if has_kaoheguan in (True, 'true', '1', 1): - qs = qs.exclude(shenheguan_id__isnull=True).exclude(shenheguan_id='') - elif has_kaoheguan in (False, 'false', '0', 0): - qs = qs.filter(Q(shenheguan_id__isnull=True) | Q(shenheguan_id='')) - - return qs - - -def _serialize_shenhe_jilu_row(jl, profile_map, fee_map=None, reject_count_map=None): - dashou = profile_map.get(jl.shenqingren_id, { - 'yonghuid': jl.shenqingren_id, - 'avatar': '', - 'phone': '', - 'nicheng': jl.shenqingren_id, - }) - kaoheguan = None - if jl.shenheguan_id: - kaoheguan = profile_map.get(jl.shenheguan_id, { - 'yonghuid': jl.shenheguan_id, - 'avatar': '', - 'phone': '', - 'nicheng': jl.shenheguan_id, - }) - kaoheguan['game_id'] = jl.shenheguan_youxi_id or '' - - current_fee = 0.0 - if fee_map is not None: - current_fee = fee_map.get((jl.jilu_id, jl.cishu), 0.0) - else: - fd = KaoheJiluFeiyong.query.filter(jilu_id=jl.jilu_id, cishu=jl.cishu).first() - if fd: - current_fee = float(fd.jine) - - reject_count = 0 - if reject_count_map is not None: - reject_count = reject_count_map.get(jl.jilu_id, 0) - - return { - 'jilu_id': jl.jilu_id, - 'zhuangtai': jl.zhuangtai, - 'zhuangtai_text': SHENHE_ZHUANGTAI_MAP.get(jl.zhuangtai, str(jl.zhuangtai)), - 'cishu': jl.cishu, - 'jiaofei_jine': float(jl.jiaofei_jine or 0), - 'current_fee': current_fee, - 'bankuai_id': jl.bankuai.bankuai_id if jl.bankuai else None, - 'bankuai_name': jl.bankuai.mingcheng if jl.bankuai else '', - 'chenghao_id': jl.chenghao.id if jl.chenghao else None, - 'chenghao_name': jl.chenghao.mingcheng if jl.chenghao else '', - 'chenghao_icon': jl.chenghao.tubiao_url if jl.chenghao else '', - 'guize_neirong': jl.guize_neirong or '', - 'dashou': { - **dashou, - 'game_id': jl.dashou_youxi_id or '', - 'beizhu': jl.dashou_beizhu or '', - }, - 'kaoheguan': kaoheguan, - 'reject_count': reject_count, - 'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.CreateTime else '', - 'UpdateTime': jl.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.UpdateTime else '', - } - - -class KhjlglView(APIView): - """ - 考核记录列表 + 统计 - POST /houtai/khjlgl - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'kaohepeizhi' not in permissions: - return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) - - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - if page < 1: - page = 1 - if page_size < 1: - page_size = 20 - if page_size > 100: - page_size = 100 - - from jituan.services.shenhe_club import filter_shenhe_jilu_by_request - - filter_data = request.data - qs = filter_shenhe_jilu_by_request( - _build_shenhe_jilu_filters(filter_data), - request, - ) - - # FluentQuery.filter() 会原地修改同一对象;统计与列表必须各自独立查询 - stats_base = filter_shenhe_jilu_by_request( - _build_shenhe_jilu_filters(filter_data), - request, - ) - agg = stats_base.aggregate(total_fee=Sum('jiaofei_jine')) - tag_rows = filter_shenhe_jilu_by_request( - _build_shenhe_jilu_filters(filter_data), - request, - ).values( - 'chenghao_id', 'chenghao__mingcheng' - ).annotate( - total=Count('jilu_id'), - passed=Count('jilu_id', filter=Q(zhuangtai=1)), - failed=Count('jilu_id', filter=Q(zhuangtai=2)), - in_progress=Count('jilu_id', filter=Q(zhuangtai=0)), - pending=Count('jilu_id', filter=Q(zhuangtai=3)), - ).order_by('-total') - - stats = { - 'total': stats_base.count(), - 'passed': stats_base.filter(zhuangtai=1).count(), - 'failed': stats_base.filter(zhuangtai=2).count(), - 'in_progress': stats_base.filter(zhuangtai=0).count(), - 'pending': stats_base.filter(zhuangtai=3).count(), - 'total_fee': float(agg['total_fee'] or 0), - 'tag_stats': [ - { - 'chenghao_id': r['chenghao_id'], - 'chenghao_name': r['chenghao__mingcheng'] or '未知标签', - 'total': r['total'], - 'passed': r['passed'], - 'failed': r['failed'], - 'in_progress': r['in_progress'], - 'pending': r['pending'], - } - for r in tag_rows if r['chenghao_id'] - ], - } - - total = qs.count() - start = (page - 1) * page_size - rows = qs.order_by('-CreateTime')[start:start + page_size].to_list() - - uids = [] - jilu_ids = [] - for jl in rows: - uids.append(jl.shenqingren_id) - if jl.shenheguan_id: - uids.append(jl.shenheguan_id) - jilu_ids.append(jl.jilu_id) - profile_map = _batch_user_profile_map(uids) - - fee_pairs = [(jl.jilu_id, jl.cishu) for jl in rows] - fee_map = {} - if fee_pairs: - jids = [p[0] for p in fee_pairs] - for fd in KaoheJiluFeiyong.query.filter(jilu_id__in=jids): - fee_map[(fd.jilu_id, fd.cishu)] = float(fd.jine) - - reject_counts = { - r['jilu_id']: r['cnt'] - for r in KaoheJujueJilu.query.filter(jilu_id__in=jilu_ids) - .values('jilu_id') - .annotate(cnt=Count('id')) - .all() - } if jilu_ids else {} - - data_list = [ - _serialize_shenhe_jilu_row(jl, profile_map, fee_map, reject_counts) - for jl in rows - ] - - all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng')) - all_tags = list( - Chenghao.query.filter(leixing='dashou') - .values('id', 'mingcheng', 'bankuai_id') - .order_by('mingcheng') - ) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'total': total, - 'page': page, - 'page_size': page_size, - 'list': data_list, - 'stats': stats, - 'all_bankuai': all_bankuai, - 'all_tags': all_tags, - }, - }) - except Exception as e: - logger.exception('KhjlglView 接口错误') - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - -class KhjlczView(APIView): - """ - 考核记录操作 / 详情 - POST /houtai/khjlcz - action: - - get_detail: 获取单条详情(含缴费明细、拒绝历史) - - to_pending: 考核中 → 待审核(取消指定考核官,与小程序 transfer 一致) - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - username_from_frontend = request.data.get('phone', None) - kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) - if kefu_obj is None: - return permissions - if 'kaohepeizhi' not in permissions: - return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) - - action = request.data.get('action') - jilu_id = (request.data.get('jilu_id') or '').strip() - if not jilu_id: - return Response({'code': 1, 'msg': '缺少记录ID'}, status=400) - - if action == 'get_detail': - try: - jl = ShenheJilu.query.select_related('bankuai', 'chenghao').get(jilu_id=jilu_id) - except ShenheJilu.DoesNotExist: - return Response({'code': 1, 'msg': '记录不存在'}, status=404) - - uids = [jl.shenqingren_id] - if jl.shenheguan_id: - uids.append(jl.shenheguan_id) - profile_map = _batch_user_profile_map(uids) - - fee_list = [ - { - 'cishu': f.cishu, - 'jine': float(f.jine), - } - for f in KaoheJiluFeiyong.query.filter(jilu_id=jilu_id).order_by('cishu') - ] - reject_list = [] - for r in KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'): - kg_prof = _batch_user_profile_map([r.shenheguan_id]).get(r.shenheguan_id, {}) - reject_list.append({ - 'cishu': r.cishu, - 'shenheguan_id': r.shenheguan_id, - 'shenheguan_nicheng': kg_prof.get('nicheng', r.shenheguan_id), - 'yuanyin': r.yuanyin, - 'CreateTime': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '', - }) - - record = _serialize_shenhe_jilu_row(jl, profile_map) - record['fee_list'] = fee_list - record['reject_list'] = reject_list - return Response({'code': 0, 'msg': 'ok', 'data': record}) - - if action == 'to_pending': - with transaction.atomic(): - jl = ShenheJilu.objects.select_for_update().filter(jilu_id=jilu_id).first() - if not jl: - return Response({'code': 1, 'msg': '记录不存在'}, status=404) - if jl.zhuangtai != 0: - return Response({'code': 1, 'msg': '仅「考核中」的记录可退回待审核'}, status=400) - - old_kg_id = jl.shenheguan_id - jl.shenheguan_id = None - jl.shenheguan_youxi_id = None - jl.zhuangtai = 3 - jl.save(update_fields=['shenheguan_id', 'shenheguan_youxi_id', 'zhuangtai', 'UpdateTime']) - - if old_kg_id: - UserShenheguan.query.filter(user__UserUID=old_kg_id).update(shenhe_zhuangtai=0) - - return Response({'code': 0, 'msg': '已退回待审核,考核官指定已取消'}) - - return Response({'code': 1, 'msg': '无效的操作类型'}, status=400) - - except Exception as e: - logger.exception('KhjlczView 接口错误') - return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) - - - - - - - - - - - +import hmac +import threading +import traceback +import uuid +import hashlib +import xmltodict +import time +import logging +import requests +import os +import secrets +import random +import string +from calendar import monthrange +from decimal import Decimal +from datetime import date, datetime, timedelta +from django.utils import timezone +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.core.paginator import Paginator +from django.db import models, transaction, IntegrityError +from django.db.models import Q, Count, Sum, F, Exists, OuterRef +from gvsdsdk.fluent import db, func, FQ +from django.db.models.functions import TruncDate, TruncMonth, TruncYear +from django.contrib.auth.hashers import make_password +from django.views.decorators.csrf import csrf_exempt +from django.utils.decorators import method_decorator + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.parsers import JSONParser, MultiPartParser, FormParser + +# 工具类导入 +from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss +from backend.utils import ( + update_dashou_daily_by_action, + update_shangjia_daily +) +from jituan.services.club_context import filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request +from jituan.services.club_penalty import ( + filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs, + forbid_penalty_out_of_scope, resolve_penalty_record_club_id, +) +from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta +from shop.utils import update_dianpu_daily_stat +from products.utils import update_shangpin_daily_stat +from orders.notice_tasks import dingdan_guangbo +from .utils import ( + verify_kefu_permission, + PERMISSION_TO_SHENFEN, + SHENFEN_PROFILE_MAP, + has_fadan_view_permission, + check_fadan_permission, + pick_order_id, + pick_penalty_create_params, + write_xiugai_log, + XIUGAI_LEIXING_DASHOU, + XIUGAI_LEIXING_GUANSHI, + XIUGAI_LEIXING_SHANGJIA, + XIUGAI_LEIXING_ZUZHANG, + XIUGAI_LEIXING_LABEL, +) + +# models 集中导入 +## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构) +from gvsdsdk.models import Role, Permission, RolePermission, UserRole +## backend +from backend.models import WithdrawalDailyStats + +## users +from users.models import ( + UserGuanshi, UserBoss, UserZuzhang, + UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan +) +from users.business_models import User + +## orders +from orders.models import ( + CommissionRate, PlayerDeliveryImage, PenaltyRecord, + OrderPlayerHistory, Order, RefundRecord, + MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus +) + +## shop +from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi + +## products +from products.models import ( + Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan, + DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong +) + +## config +from config.models import ( + WithdrawConfig, ShangjiaLianjie, AccountPermission, + TixianQuotaDefault, + DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage +) + +## rank +from rank.models import ( + KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong, + YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu +) + +# 序列化器 +from .serializers import PopupPageSerializer + +# 全局常量、日志对象 +logger = logging.getLogger('houtai') +SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd'] + + +def _serialize_permission_row(perm_row): + """gvsdsdk Permission → 后台 Vue 约定的小写字段""" + if isinstance(perm_row, dict): + code = perm_row.get('PermCode') or perm_row.get('perm_code') or '' + name = perm_row.get('PermName') or perm_row.get('perm_name') or code + desc = perm_row.get('PermDesc') or perm_row.get('perm_desc') or '' + else: + code = perm_row.PermCode or '' + name = perm_row.PermName or code + desc = perm_row.PermDesc or '' + return {'perm_code': code, 'perm_name': name, 'perm_desc': desc} + + +class GetRolePermissionView(APIView): + """ + 获取所有角色、权限以及角色关联的用户数 + 权限要求:用户必须拥有 000001 权限 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 1. 权限校验(验证客服身份,并获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 2. 检查是否有 000001 权限 + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + # 3. 获取所有权限 + all_perms = Permission.objects.filter(PermStatus=1).values('PermCode', 'PermName', 'PermDesc') + permissions_list = [_serialize_permission_row(p) for p in all_perms] + + # 4. 获取所有角色,并统计每个角色下的用户数(仅后台客服账号) + kefu_user_uuids = list( + User.objects.filter(KefuProfile__isnull=False).values_list('UserUUID', flat=True) + ) + roles = Role.objects.filter(RoleStatus=1) + roles_data = [] + for role in roles: + user_count = UserRole.objects.filter( + RoleUUID=role.RoleUUID, + UserUUID__in=kefu_user_uuids, + ).count() + role_perm_uuids = RolePermission.objects.filter( + RoleUUID=role.RoleUUID + ).values_list('PermUUID', flat=True) + perms_of_role = [ + _serialize_permission_row(p) + for p in Permission.objects.filter( + PermUUID__in=list(role_perm_uuids) + ).values('PermCode', 'PermName', 'PermDesc') + ] + roles_data.append({ + 'role_code': role.RoleName, + 'role_name': role.RoleName, + 'description': role.RoleDesc or '', + 'user_count': user_count, + 'permissions': perms_of_role + }) + + return Response({ + 'code': 0, + 'data': { + 'roles': roles_data, + 'permissions': permissions_list + } + }) + + +class ModifyRolePermissionView(APIView): + """ + 通用接口:支持删除角色、为角色添加权限、移除角色权限 + 操作类型通过 action 字段区分 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + action = request.data.get('action') # delete_role, add_permission, remove_permission + if not username or not action: + return Response({'code': 400, 'msg': '缺少必要参数'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + # 获取角色(需要角色名称) + role_code = request.data.get('role_code') + if not role_code: + return Response({'code': 400, 'msg': '缺少角色编码'}) + + try: + role = Role.objects.get(RoleName=role_code) + except Role.DoesNotExist: + return Response({'code': 404, 'msg': '角色不存在'}) + + # 超级管理员角色的特殊限制 + if role.RoleName == '管理员': + if action in ['delete_role']: + return Response({'code': 403, 'msg': '超级管理员角色不能删除'}) + + # 根据 action 处理 + if action == 'delete_role': + # 删除角色:同时删除角色权限关联、用户角色关联 + with transaction.atomic(): + RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete() + UserRole.objects.filter(RoleUUID=role.RoleUUID).delete() + role.delete() + return Response({'code': 0, 'msg': '角色删除成功'}) + + elif action == 'add_permission': + perm_code = request.data.get('perm_code') + if not perm_code: + return Response({'code': 400, 'msg': '缺少权限编码'}) + if role.RoleName == '管理员' and perm_code == '000001': + return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'}) + try: + perm = Permission.objects.get(PermCode=perm_code) + except Permission.DoesNotExist: + return Response({'code': 404, 'msg': '权限不存在'}) + # 检查是否已存在 + if RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).exists(): + return Response({'code': 400, 'msg': '该角色已拥有此权限'}) + RolePermission.objects.create( + RolePermUUID=uuid.uuid4().bytes, + RoleUUID=role.RoleUUID, + PermUUID=perm.PermUUID, + CreateTime=timezone.now(), + ) + return Response({'code': 0, 'msg': '权限添加成功'}) + + elif action == 'remove_permission': + perm_code = request.data.get('perm_code') + if not perm_code: + return Response({'code': 400, 'msg': '缺少权限编码'}) + if role.RoleName == '管理员' and perm_code == '000001': + return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'}) + try: + perm = Permission.objects.get(PermCode=perm_code) + except Permission.DoesNotExist: + return Response({'code': 404, 'msg': '权限不存在'}) + deleted, _ = RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).delete() + if deleted == 0: + return Response({'code': 400, 'msg': '该角色未拥有此权限'}) + return Response({'code': 0, 'msg': '权限移除成功'}) + + elif action == 'update_role': + role_name = (request.data.get('role_name') or '').strip() + description = (request.data.get('description') or '').strip() + perm_codes = request.data.get('perm_codes') or [] + if role.RoleName == '管理员': + role_name = role.RoleName + elif role_name and role_name != role.RoleName: + if Role.objects.filter(RoleName=role_name).exclude(RoleUUID=role.RoleUUID).exists(): + return Response({'code': 400, 'msg': '角色名称已存在'}) + with transaction.atomic(): + if role.RoleName != '管理员': + if role_name: + role.RoleName = role_name + role.RoleDesc = description + role.save(update_fields=['RoleName', 'RoleDesc']) + RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete() + for pc in perm_codes: + if pc == '000001' and role.RoleName != '管理员': + continue + try: + perm = Permission.objects.get(PermCode=pc, PermStatus=1) + except Permission.DoesNotExist: + continue + RolePermission.objects.create( + RolePermUUID=uuid.uuid4().bytes, + RoleUUID=role.RoleUUID, + PermUUID=perm.PermUUID, + CreateTime=timezone.now(), + ) + if role.RoleName == '管理员': + try: + super_perm = Permission.objects.get(PermCode='000001', PermStatus=1) + if not RolePermission.objects.filter( + RoleUUID=role.RoleUUID, PermUUID=super_perm.PermUUID, + ).exists(): + RolePermission.objects.create( + RolePermUUID=uuid.uuid4().bytes, + RoleUUID=role.RoleUUID, + PermUUID=super_perm.PermUUID, + CreateTime=timezone.now(), + ) + except Permission.DoesNotExist: + pass + return Response({'code': 0, 'msg': '角色保存成功'}) + + else: + return Response({'code': 400, 'msg': '无效的 action'}) + + +class AddRoleView(APIView): + """ + 添加角色:自动生成角色编码,前端提供角色名称、描述、选择的权限列表 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + role_name = request.data.get('role_name', '').strip() + description = request.data.get('description', '').strip() + perm_codes = request.data.get('perm_codes', []) # 列表 + + if not username or not role_name: + return Response({'code': 400, 'msg': '缺少必要参数'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + # 检查角色名称是否已存在 + if Role.objects.filter(RoleName=role_name).exists(): + return Response({'code': 400, 'msg': '角色名称已存在'}) + + # 生成唯一角色编码(格式:ROLE_ + 8位随机字母数字) + role_code = f"ROLE_{''.join(random.choices(string.ascii_uppercase + string.digits, k=8))}" + + # 创建角色 + with transaction.atomic(): + role = Role.objects.create( + RoleUUID=uuid.uuid4().bytes, + TenantUUID=b'\x00' * 16, + RoleName=role_name, + RoleType=2, + RoleStatus=1, + AssignScope=0, + RoleDesc=description, + CreateTime=timezone.now(), + ) + # 绑定权限(过滤掉不存在的权限编码) + for pc in perm_codes: + # 不允许添加 000001 权限(此权限仅供超级管理员) + if pc == '000001': + continue + try: + perm = Permission.objects.get(PermCode=pc) + RolePermission.objects.create( + RolePermUUID=uuid.uuid4().bytes, + RoleUUID=role.RoleUUID, + PermUUID=perm.PermUUID, + CreateTime=timezone.now(), + ) + except Permission.DoesNotExist: + logger.warning(f"添加角色时忽略不存在的权限编码: {pc}") + + return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}}) + + + +class GetAdminUserListView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + phone = request.data.get('phone', '').strip() + nicheng = request.data.get('nicheng', '').strip() + role_code = request.data.get('roleCode', '').strip() + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('pageSize', 20)) + + # UserType 是 @property 非数据库字段,不能用 filter; + # KefuProfile__isnull=False 已确保只查到客服用户 + users = User.query.filter( + KefuProfile__isnull=False + ).select_related('KefuProfile') + + if phone: + users = users.filter(Phone__icontains=phone) + if nicheng: + users = users.filter(KefuProfile__nicheng__icontains=nicheng) + + # 关键修正:先求值为列表,再传入 phone__in + if role_code: + target_role = Role.objects.filter(RoleName=role_code).first() + if target_role: + user_uuids = list( + UserRole.objects.filter(RoleUUID=target_role.RoleUUID) + .values_list('UserUUID', flat=True) + ) + users = users.filter(UserUUID__in=user_uuids) + else: + users = users.none() + + users = users.order_by('-UserCreateTime') + paginator = Paginator(users, page_size) + page_obj = paginator.get_page(page) + + data_list = [] + for user in page_obj: + # 通过 gvsdsdk UserRole 获取用户角色 + user_role_uuids = list( + UserRole.objects.filter(UserUUID=user.UserUUID) + .values_list('RoleUUID', flat=True) + ) + user_roles = Role.objects.filter(RoleUUID__in=user_role_uuids) + roles_data = [ + {'role_code': r.RoleName, 'role_name': r.RoleName} + for r in user_roles + ] + data_list.append({ + 'phone': user.Phone or '', + 'yonghuid': user.UserUID or '', + 'nicheng': user.KefuProfile.nicheng, + 'roles': roles_data, + 'status': user.KefuProfile.zhuangtai, + 'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '', + 'update_time': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '', + 'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '', + 'UpdateTime': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': data_list, + 'total': paginator.count, + 'page': page, + 'pageSize': page_size + } + }) + + except Exception as e: + logger.error(f"GetAdminUserListView 异常: {e}\n{traceback.format_exc()}") + return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}) + + + +class GetAdminRolesView(APIView): + """获取所有角色(用于筛选和分配)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + roles = Role.objects.filter(RoleStatus=1).values('RoleName') + return Response({ + 'code': 0, + 'data': { + 'roles': [{'role_code': r['RoleName'], 'role_name': r['RoleName']} for r in roles] + } + }) + + + + + + +'''class GetAdminUserListView(APIView): + """获取后台用户列表(支持分页、筛选)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + # 筛选条件 + phone = request.data.get('phone', '').strip() + nicheng = request.data.get('nicheng', '').strip() + role_code = request.data.get('roleCode', '').strip() + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('pageSize', 20)) + + # 基础查询:存在客服扩展表 + users = User.query.filter(KefuProfile__isnull=False).select_related('KefuProfile') + if phone: + users = users.filter(Phone__icontains=phone) + if nicheng: + users = users.filter(KefuProfile__nicheng__icontains=nicheng) + if role_code: + # 筛选拥有该角色的用户 + user_ids_with_role = UserRole.query.filter(role__role_code=role_code).values_list('account_id', flat=True) + users = users.filter(phone__in=user_ids_with_role) + + # 排序 + users = users.order_by('-CreateTime') + + # 分页 + paginator = Paginator(users, page_size) + page_obj = paginator.get_page(page) + + # 构建返回数据 + data_list = [] + for user in page_obj: + # 获取用户的所有角色 + user_roles = UserRole.query.filter(account_id=user.Phone).select_related('role') + roles_data = [{'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles] + data_list.append({ + 'phone': user.Phone, + 'nicheng': user.KefuProfile.nicheng, + 'roles': roles_data, + 'status': user.KefuProfile.zhuangtai, + 'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S'), + 'UpdateTime': user.UpdateTime.strftime('%Y-%m-%d %H:%M:%S'), + }) + + return Response({ + 'code': 0, + 'data': { + 'list': data_list, + 'total': paginator.count, + 'page': page, + 'pageSize': page_size + } + })''' + + + + + +class ModifyAdminUserView(APIView): + """修改后台用户信息(昵称、密码、二级密码、状态、角色)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + action = request.data.get('action') + if not username or not action: + return Response({'code': 400, 'msg': '缺少参数'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + target_phone = request.data.get('phone', '').strip() + if not target_phone: + return Response({'code': 400, 'msg': '缺少目标用户账号'}) + + # Phone 非唯一,同一手机号可能对应多个微信用户;后台操作只定位客服账号 + target_user = User.query.filter( + Phone=target_phone, + KefuProfile__isnull=False, + ).first() + if not target_user: + return Response({'code': 404, 'msg': '用户不存在或非后台账号'}) + try: + target_kefu = target_user.KefuProfile + except UserKefu.DoesNotExist: + return Response({'code': 404, 'msg': '用户扩展信息缺失'}) + + with transaction.atomic(): + if action == 'update_user_info': + # 修改昵称、状态、密码、二级密码 + nicheng = request.data.get('nicheng') + status = request.data.get('status') + password = request.data.get('password') + erjimima = request.data.get('erjimima') + if nicheng is not None: + target_kefu.nicheng = nicheng + if status is not None: + target_kefu.zhuangtai = int(status) + if password: + if len(password) < 6: + return Response({'code': 400, 'msg': '密码至少6位'}) + target_user.SetPassword(password) + if erjimima: + if len(erjimima) < 6: + return Response({'code': 400, 'msg': '二级密码至少6位'}) + import bcrypt as _bcrypt + target_kefu.erjimima = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8') + target_user.save() + target_kefu.save() + return Response({'code': 0, 'msg': '修改成功'}) + + elif action == 'add_user_roles': + role_codes = request.data.get('role_codes', []) + if not role_codes: + return Response({'code': 400, 'msg': '请选择角色'}) + for rc in role_codes: + try: + role = Role.objects.get(RoleName=rc) + except Role.DoesNotExist: + continue + UserRole.objects.get_or_create( + UserRoleUUID=uuid.uuid4().bytes, + UserUUID=target_user.UserUUID, + RoleUUID=role.RoleUUID, + CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes, + ) + return Response({'code': 0, 'msg': '角色添加成功'}) + + elif action == 'remove_user_role': + role_code = request.data.get('role_code') + if not role_code: + return Response({'code': 400, 'msg': '缺少角色编码'}) + try: + role = Role.objects.get(RoleName=role_code) + except Role.DoesNotExist: + return Response({'code': 404, 'msg': '角色不存在'}) + UserRole.objects.filter(UserUUID=target_user.UserUUID, RoleUUID=role.RoleUUID).delete() + return Response({'code': 0, 'msg': '角色移除成功'}) + + else: + return Response({'code': 400, 'msg': '无效的action'}) + + + + + +class AddAdminUserView(APIView): + """添加后台用户(客服账号)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + phone = request.data.get('phone', '').strip() + password = request.data.get('password', '').strip() + erjimima = request.data.get('erjimima', '').strip() + nicheng = request.data.get('nicheng', '').strip() + role_codes = request.data.get('role_codes', []) + + if not all([phone, password, erjimima, nicheng]): + return Response({'code': 400, 'msg': '请填写完整信息'}) + if len(password) < 6 or len(erjimima) < 6: + return Response({'code': 400, 'msg': '密码和二级密码至少6位'}) + + # 校验手机号是否已被客服占用 + if User.query.filter(Phone=phone, KefuProfile__isnull=False).exists(): + return Response({'code': 400, 'msg': '该账号已存在'}) + + # 生成唯一的 yonghuid(7位数字) + while True: + yonghuid = str(random.randint(1000000, 9999999)) + if not User.query.filter(UserUID=yonghuid).exists(): + break + + # 生成唯一的 openid(格式:admin_{时间戳}_{随机6位}) + timestamp = str(int(time.time() * 1000)) + random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) + openid = f"admin_{timestamp}_{random_suffix}" + while User.query.filter(OpenID=openid).exists(): + random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) + openid = f"admin_{timestamp}_{random_suffix}" + + with transaction.atomic(): + user_main = User.query.create( + UserUID=yonghuid, + UserName=openid, + OpenID=openid, + Phone=phone, + ) + user_main.SetPassword(password) + user_main.save(update_fields=['UserPassword']) + # 创建客服扩展表(后台用户扩展) + import bcrypt as _bcrypt + erjimima_hashed = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8') + UserKefu.query.create( + user=user_main, + nicheng=nicheng, + erjimima=erjimima_hashed, + zhuangtai=1 + ) + # 分配客服角色 + kefu_role = Role.objects.filter(RoleName='客服').first() + if kefu_role: + UserRole.objects.create( + UserRoleUUID=uuid.uuid4().bytes, + UserUUID=user_main.UserUUID, + RoleUUID=kefu_role.RoleUUID, + CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes, + ) + # 分配额外角色 + for rc in role_codes: + try: + role = Role.objects.get(RoleName=rc) + UserRole.objects.create( + UserRoleUUID=uuid.uuid4().bytes, + UserUUID=user_main.UserUUID, + RoleUUID=role.RoleUUID, + CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes, + ) + except Role.DoesNotExist: + pass + + return Response({'code': 0, 'msg': '添加成功'}) + + + + + + + + +class KefuGetDashouListView(APIView): + """ + 客服获取打手列表接口(仅打手类型) + 需要拥有以下任一权限:001aa,001bb,001cc,001dd,001ee,001ff,001gg + 请求:POST /yonghu/kefuhqdslb + 参数: + username: 客服手机号(用于权限校验) + keyword: 搜索关键词(可选,搜索用户ID或昵称) + page: 页码(从1开始) + page_size: 每页数量 + 返回: + code: 0 + data: { + list: [打手对象], + has_more: bool, + total: int, // 总打手数 + valid_dashou_count: int // 有效打手数(有有效会员) + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数(前端字段为 username) + username = request.data.get('username', '').strip() + keyword = request.data.get('keyword', '').strip() + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + except ValueError: + return Response({'code': 400, 'msg': '分页参数错误'}) + + if not username: + return Response({'code': 401, 'msg': '认证失败'}) + + # 2. 公共权限校验(验证客服身份,获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查是否拥有打手管理相关权限(至少一个) + required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看打手列表'}) + + # 4. 构建打手查询集(仅打手类型) + dashou_qs = filter_user_related_by_club( + UserDashou.query.select_related('user').all(), request, + ) + + # 关键词筛选(用户ID或昵称) + if keyword: + dashou_qs = dashou_qs.filter( + Q(user__UserUID__icontains=keyword) | + Q(nicheng__icontains=keyword) + ) + + # 5. 统计打手总数 + total_dashou = dashou_qs.count() + + # 6. 统计有效打手数(有会员且未过期) + dashou_user_ids = dashou_qs.values_list('user__UserUID', flat=True) + now = timezone.now() + hy_qs = filter_queryset_by_club(Huiyuangoumai.query.all(), request) + valid_dashou_count = hy_qs.filter( + yonghu_id__in=dashou_user_ids, + huiyuan_zhuangtai=1, + daoqi_time__gt=now, + ).values('yonghu_id').distinct().count() + + # 7. 分页 + offset = (page - 1) * page_size + dashou_list = dashou_qs.order_by('-CreateTime')[offset:offset + page_size] + + # 8. 构建返回数据 + result = [] + for dashou in dashou_list: + user = dashou.user + result.append({ + 'yonghuid': user.UserUID, + 'avatar': user.Avatar or '', + 'zaixianzhuangtai': dashou.zaixianzhuangtai, + 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, + 'nicheng': dashou.nicheng or '', + 'zhuangtai': dashou.zhuangtai, + }) + + has_more = (offset + len(dashou_list)) < total_dashou + + meta = list_response_meta(request) + return Response({ + 'code': 0, + 'club_id': meta['club_id'], + 'scope': meta['scope'], + 'data': { + 'list': result, + 'has_more': has_more, + 'total': total_dashou, + 'valid_dashou_count': valid_dashou_count, + } + }) + + + +class KefuGetDashouDetailView(APIView): + """ + 客服获取打手详情接口 + 请求:POST /yonghu/kefuhqdsxq + 参数:{ + "username": "客服手机号", + "uid": "打手ID (yonghuid)" + } + 认证:JWT + 返回:code=0 + 打手信息 + 会员列表 + 所有会员类型 + 客服权限列表 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('phone', '').strip() + uid = request.data.get('uid', '').strip() + + if not username or not uid: + return Response({'code': 400, 'msg': '缺少参数'}) + + # 2. 公共权限校验(验证客服身份,获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 3. 检查是否拥有打手管理相关权限(至少一个) + required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看打手详情'}) + + # 4. 查询打手主表和扩展表 + try: + user_main = User.query.select_related('DashouProfile').get( + UserUID=uid + ) + except User.DoesNotExist: + return Response({'code': 404, 'msg': '打手不存在'}) + + denied = forbid_if_user_out_of_scope(request, user_main) + if denied: + return denied + + try: + dashou = user_main.DashouProfile + except ObjectDoesNotExist: + # 用户存在但未注册打手身份,返回基本信息 + user_info = { + 'yonghuid': user_main.UserUID, + 'avatar': user_main.Avatar or '', + 'zaixianzhuangtai': 0, + 'zhanghaozhuangtai': 0, + 'zhuangtai': 0, + 'nicheng': '', + 'chenghao': '', + 'dianhua': '', + 'wechat': '', + 'phone': user_main.Phone or '', + 'CreateTime': user_main.UserCreateTime, + 'ip': user_main.IP or '', + 'jiedanzongliang': 0, + 'chengjiaozongliang': 0, + 'tuikuanliang': 0, + 'jinrijiedan': 0, + 'yue': 0.00, + 'zonge': 0.00, + 'jifen': 0, + 'yajin': 0.00, + 'jinrishouyi': 0.00, + 'jinyueshouyi': 0.00, + 'yaoqingren': '', + 'jieshao': '', + } + member_list = [] + # 仍然需要返回 all_members 和 permissions + all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao')) + return Response({ + 'code': 0, + 'data': { + 'user_info': user_info, + 'member_list': member_list, + 'all_members': all_members, + 'permissions': permissions, + } + }) + + # 5. 构建打手信息 + user_info = { + 'yonghuid': user_main.UserUID, + 'avatar': user_main.Avatar or '', + 'zaixianzhuangtai': dashou.zaixianzhuangtai, + 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, + 'zhuangtai': dashou.zhuangtai, + 'nicheng': dashou.nicheng or '', + 'chenghao': dashou.chenghao or '', + 'dianhua': dashou.dianhua or '', + 'wechat': dashou.wechat or '', + 'phone': user_main.Phone or '', + 'CreateTime': user_main.UserCreateTime, + 'ip': user_main.IP or '', + 'jiedanzongliang': dashou.jiedanzongliang, + 'chengjiaozongliang': dashou.chengjiaozongliang, + 'tuikuanliang': dashou.tuikuanliang, + 'jinrijiedan': dashou.jinrijiedan, + 'yue': float(dashou.yue) if dashou.yue else 0.00, + 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, + 'jifen': dashou.jifen, + 'yajin': float(dashou.yajin) if dashou.yajin else 0.00, + 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00, + 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00, + 'yaoqingren': dashou.yaoqingren or '', + 'jieshao': dashou.jieshao or '', + } + + # 6. 查询打手已拥有的会员(未过期) + member_records = Huiyuangoumai.query.filter( + yonghu_id=uid + ).order_by('-CreateTime') + member_list = [] + for record in member_records: + # 检查是否过期 + if hasattr(record, 'jiance_shifou_daoqi'): + is_expired = record.jiance_shifou_daoqi() + else: + is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True + + if not is_expired: + try: + huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id) + huiyuan_name = huiyuan.jieshao + except Huiyuan.DoesNotExist: + huiyuan_name = '未知会员' + member_list.append({ + 'id': record.id, + 'huiyuan_name': huiyuan_name, + 'daoqi_time': record.daoqi_time, + 'is_active': record.huiyuan_zhuangtai == 1, + 'huiyuan_id': record.huiyuan_id, # 用于删除操作 + }) + + # 7. 查询所有会员类型(用于前端添加会员) + all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao')) + + return Response({ + 'code': 0, + 'data': { + 'user_info': user_info, + 'member_list': member_list, + 'all_members': all_members, + 'permissions': permissions, # 返回当前客服的权限列表 + } + }) + + + +class KefuUpdateDashouView(APIView): + """ + 客服修改打手信息接口 + 支持操作: + - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb) + - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb) + - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd) + - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee) + - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee) + - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee) + - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg) + - gai_chenghao : 修改打手称号(升级/降级金牌) (权限001hh) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取前端参数 + username = request.data.get('username', '').strip() + dashou_id = request.data.get('dashou_id', '').strip() + caozuo = request.data.get('caozuo', '').strip() + value = request.data.get('value') # 金额、积分、天数、称号等 + huiyuan_id = request.data.get('huiyuan_id', '').strip() + + if not username or not dashou_id or not caozuo: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 3. 查询打手 + try: + dashou_user = User.query.get(UserUID=dashou_id) + dashou_profile = dashou_user.DashouProfile + except User.DoesNotExist: + return Response({'code': 404, 'msg': '打手不存在'}) + except UserDashou.DoesNotExist: + return Response({'code': 404, 'msg': '打手扩展信息缺失'}) + + with transaction.atomic(): + try: + before = { + 'yue': dashou_profile.yue, + 'yajin': dashou_profile.yajin, + 'jifen': dashou_profile.jifen, + 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai, + 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, + 'zhuangtai': dashou_profile.zhuangtai, + 'chenghao': dashou_profile.chenghao, + } + + # ---------- 余额操作 ---------- + if caozuo == 'jian_yue': + if '001aa' not in permissions: + return Response({'code': 403, 'msg': '无权限减打手余额'}) + amount = Decimal(str(value)) + if dashou_profile.yue < amount: + return Response({'code': 400, 'msg': '余额不足'}) + dashou_profile.yue -= amount + dashou_profile.save() + after = dashou_profile.yue + write_xiugai_log( + yonghuid=dashou_id, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_DASHOU, + xiugaitijiao=after, + xiugaitijiaoq=before['yue'], + qitashuoming=( + f'【后台】减少打手可提现余额:{before["yue"]}元 → {after}元,' + f'减少{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after)}}) + + elif caozuo == 'jia_yue': + if '001bb' not in permissions: + return Response({'code': 403, 'msg': '无权限加打手余额'}) + amount = Decimal(str(value)) + dashou_profile.yue += amount + dashou_profile.save() + after = dashou_profile.yue + write_xiugai_log( + yonghuid=dashou_id, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_DASHOU, + xiugaitijiao=after, + xiugaitijiaoq=before['yue'], + qitashuoming=( + f'【后台】增加打手可提现余额:{before["yue"]}元 → {after}元,' + f'增加{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after)}}) + + # ---------- 押金操作 ---------- + elif caozuo == 'jian_yajin': + if '001aa' not in permissions: + return Response({'code': 403, 'msg': '无权限减打手押金'}) + amount = Decimal(str(value)) + if dashou_profile.yajin < amount: + return Response({'code': 400, 'msg': '押金不足'}) + dashou_profile.yajin -= amount + dashou_profile.save() + after = dashou_profile.yajin + write_xiugai_log( + yonghuid=dashou_id, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_DASHOU, + yajin=after, + yyajin=before['yajin'], + qitashuoming=( + f'【后台】减少打手押金:{before["yajin"]}元 → {after}元,' + f'减少{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after)}}) + + elif caozuo == 'jia_yajin': + if '001bb' not in permissions: + return Response({'code': 403, 'msg': '无权限加打手押金'}) + amount = Decimal(str(value)) + dashou_profile.yajin += amount + dashou_profile.save() + after = dashou_profile.yajin + write_xiugai_log( + yonghuid=dashou_id, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_DASHOU, + yajin=after, + yyajin=before['yajin'], + qitashuoming=( + f'【后台】增加打手押金:{before["yajin"]}元 → {after}元,' + f'增加{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after)}}) + + # ---------- 积分操作 ---------- + elif caozuo == 'jian_jifen': + if '001cc' not in permissions: + return Response({'code': 403, 'msg': '无权限减打手积分'}) + jifen = int(value) + if dashou_profile.jifen < jifen: + return Response({'code': 400, 'msg': '积分不足'}) + dashou_profile.jifen -= jifen + dashou_profile.save() + after = dashou_profile.jifen + write_xiugai_log( + yonghuid=dashou_id, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_DASHOU, + jifen=after, + yjifen=before['jifen'], + qitashuoming=( + f'【后台】减少打手积分:{before["jifen"]} → {after},' + f'减少{jifen}分,打手ID={dashou_id},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after}}) + + elif caozuo == 'jia_jifen': + if '001dd' not in permissions: + return Response({'code': 403, 'msg': '无权限加打手积分'}) + jifen = int(value) + dashou_profile.jifen += jifen + dashou_profile.save() + after = dashou_profile.jifen + write_xiugai_log( + yonghuid=dashou_id, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_DASHOU, + jifen=after, + yjifen=before['jifen'], + qitashuoming=( + f'【后台】增加打手积分:{before["jifen"]} → {after},' + f'增加{jifen}分,打手ID={dashou_id},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after}}) + + # ---------- 状态修改 ---------- + elif caozuo == 'gai_zhanghaozhuangtai': + if '001ee' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手账号状态'}) + new_status = int(value) + if new_status not in [0, 1]: + return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'}) + dashou_profile.zhanghaozhuangtai = new_status + dashou_profile.save() + status_label = '正常' if new_status == 1 else '封禁' + old_label = '正常' if before['zhanghaozhuangtai'] == 1 else '封禁' + write_xiugai_log( + yonghuid=dashou_id, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_DASHOU, + qitashuoming=( + f'【后台】修改打手账号状态:{old_label} → {status_label},' + f'打手ID={dashou_id},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}}) + + elif caozuo == 'gai_zaixianzhuangtai': + if '001ee' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手在线状态'}) + new_status = int(value) + if new_status not in [0, 1]: + return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'}) + dashou_profile.zaixianzhuangtai = new_status + dashou_profile.save() + status_label = '在线' if new_status == 1 else '离线' + old_label = '在线' if before['zaixianzhuangtai'] == 1 else '离线' + write_xiugai_log( + yonghuid=dashou_id, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_DASHOU, + qitashuoming=( + f'【后台】修改打手在线状态:{old_label} → {status_label},' + f'打手ID={dashou_id},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}}) + + elif caozuo == 'gai_zhuangtai': + if '001ee' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手接单状态'}) + new_status = int(value) + if new_status not in [0, 1]: + return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'}) + dashou_profile.zhuangtai = new_status + dashou_profile.save() + status_label = '空闲' if new_status == 1 else '忙碌' + old_label = '空闲' if before['zhuangtai'] == 1 else '忙碌' + write_xiugai_log( + yonghuid=dashou_id, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_DASHOU, + qitashuoming=( + f'【后台】修改打手接单状态:{old_label} → {status_label},' + f'打手ID={dashou_id},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}}) + + # ---------- 会员操作 ---------- + elif caozuo == 'jia_huiyuan': + if '001ff' not in permissions: + return Response({'code': 403, 'msg': '无权限给打手加会员'}) + if not huiyuan_id: + return Response({'code': 400, 'msg': '缺少会员ID'}) + days = int(value) if value else 30 + try: + huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response({'code': 404, 'msg': '会员类型不存在'}) + record, created = Huiyuangoumai.objects.select_for_update().get_or_create( + yonghu_id=dashou_id, + huiyuan_id=huiyuan_id, + defaults={ + 'huiyuan_zhuangtai': 1, + 'jieshao': huiyuan.jieshao, + 'daoqi_time': timezone.now() + timedelta(days=days), + } + ) + if not created: + new_daoqi = record.daoqi_time + timedelta(days=days) + record.daoqi_time = new_daoqi + record.huiyuan_zhuangtai = 1 + record.save() + write_xiugai_log( + yonghuid=dashou_id, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_DASHOU, + huiyuants=days, + huiyuan_id=huiyuan_id, + qitashuoming=( + f'【后台】给打手添加会员:会员ID={huiyuan_id}({huiyuan.jieshao}),' + f'增加{days}天,到期时间={record.daoqi_time.strftime("%Y-%m-%d %H:%M:%S")},' + f'打手ID={dashou_id},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}}) + + elif caozuo == 'jian_huiyuan': + if '001gg' not in permissions: + return Response({'code': 403, 'msg': '无权限给打手减会员'}) + if not huiyuan_id: + return Response({'code': 400, 'msg': '缺少会员ID'}) + try: + record = Huiyuangoumai.query.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id) + except Huiyuangoumai.DoesNotExist: + return Response({'code': 404, 'msg': '该打手未购买此会员'}) + record.delete() + write_xiugai_log( + yonghuid=dashou_id, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_DASHOU, + huiyuan_id=huiyuan_id, + qitashuoming=( + f'【后台】移除打手会员:会员ID={huiyuan_id},' + f'打手ID={dashou_id},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '减会员成功'}) + + # ---------- 称号修改(金牌打手) ---------- + elif caozuo == 'gai_chenghao': + if '001hh' not in permissions: + return Response({'code': 403, 'msg': '无权限修改打手称号'}) + new_chenghao = str(value).strip() + if not new_chenghao: + return Response({'code': 400, 'msg': '称号不能为空'}) + # 可限制合法值,这里只记录日志 + dashou_profile.chenghao = new_chenghao + dashou_profile.save() + write_xiugai_log( + yonghuid=dashou_id, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_DASHOU, + qitashuoming=( + f'【后台】修改打手称号:{before["chenghao"] or "无"} → {new_chenghao},' + f'打手ID={dashou_id},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '修改称号成功', 'data': {'new_chenghao': new_chenghao}}) + + else: + return Response({'code': 400, 'msg': '未知操作类型'}) + + except Exception as e: + logger.error(f"修改打手信息异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + +class KefuGetShangjiaListView(APIView): + """ + 客服/管理员获取商家列表接口 + 需要拥有以下任一权限:1100a, 1100b + 请求:POST /houtai/hqsjgllb + 参数: + username: 客服手机号(用于权限校验,必填) + keyword: 搜索关键词(可选,搜索商家用户ID或昵称) + balance_op: 余额比较符,gte(>=) / lte(<=)(可选) + balance_value: 余额数值(可选,需与balance_op同时存在) + order_count_op: 派单量比较符,gte(>=) / lte(<=)(可选) + order_count_value: 派单量数值(可选,需与order_count_op同时存在) + status: 商家状态,1=正常,2=禁用(可选) + page: 页码,从1开始,默认1 + page_size: 每页数量,默认20 + 返回: + code: 0 成功,其他失败 + data: { + list: [商家对象], + total: int, // 总商家数 + } + """ + permission_classes = [] # 不依赖全局认证,手动校验 + parser_classes = [JSONParser] + + def post(self, request): + # 1. 获取参数 + username = request.data.get('username', '').strip() + keyword = request.data.get('keyword', '').strip() + balance_op = request.data.get('balance_op', '').strip().lower() + balance_value_str = request.data.get('balance_value') + order_count_op = request.data.get('order_count_op', '').strip().lower() + order_count_value_str = request.data.get('order_count_value') + status_str = request.data.get('status') + + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + if page < 1 or page_size < 1: + raise ValueError + except ValueError: + return Response({'code': 400, 'msg': '分页参数错误'}) + + if not username: + return Response({'code': 401, 'msg': '认证失败,缺少username'}) + + # 2. 公共权限校验(复用现有函数 verify_kefu_permission) + # 假设 verify_kefu_permission 定义在某个公共模块,这里引入 + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 直接返回错误响应 + + # 3. 检查商家管理权限(1100a 或 1100b) + required_perms = {'1100a', '1100b'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看商家列表'}) + + # 4. 构建商家查询集(商家扩展表 + 关联用户主表) + shangjia_qs = filter_user_related_by_club( + UserShangjia.query.select_related('user').all(), request, + ) + + # 关键词筛选(用户ID或昵称) + if keyword: + shangjia_qs = shangjia_qs.filter( + Q(user__UserUID__icontains=keyword) | + Q(nicheng__icontains=keyword) + ) + + # 余额筛选 + if balance_op and balance_value_str is not None and balance_value_str != '': + try: + balance_value = Decimal(str(balance_value_str)) + if balance_op == 'gte': + shangjia_qs = shangjia_qs.filter(yue__gte=balance_value) + elif balance_op == 'lte': + shangjia_qs = shangjia_qs.filter(yue__lte=balance_value) + else: + return Response({'code': 400, 'msg': '余额比较符只支持gte或lte'}) + except Exception: + return Response({'code': 400, 'msg': '余额数值格式错误'}) + + # 派单总量筛选(fabu字段) + if order_count_op and order_count_value_str is not None and order_count_value_str != '': + try: + order_count_value = int(order_count_value_str) + if order_count_op == 'gte': + shangjia_qs = shangjia_qs.filter(fabu__gte=order_count_value) + elif order_count_op == 'lte': + shangjia_qs = shangjia_qs.filter(fabu__lte=order_count_value) + else: + return Response({'code': 400, 'msg': '派单量比较符只支持gte或lte'}) + except Exception: + return Response({'code': 400, 'msg': '派单量数值必须为整数'}) + + # 商家状态筛选 + if status_str is not None and status_str != '': + try: + status_val = int(status_str) + if status_val not in [1, 2]: + return Response({'code': 400, 'msg': '商家状态值无效,应为1或2'}) + shangjia_qs = shangjia_qs.filter(zhuangtai=status_val) + except ValueError: + return Response({'code': 400, 'msg': '商家状态格式错误'}) + + # 5. 统计总商家数(应用筛选后) + total = shangjia_qs.count() + valid_merchant_count = shangjia_qs.filter(zhuangtai=1).count() + + # 6. 分页 + offset = (page - 1) * page_size + shangjia_list = shangjia_qs.order_by('-CreateTime')[offset:offset + page_size] + + # 7. 构建返回数据 + result = [] + for shangjia in shangjia_list: + user = shangjia.user + result.append({ + 'yonghuid': user.UserUID, + 'avatar': user.Avatar or '', + 'nicheng': shangjia.nicheng or '', + 'yue': str(shangjia.yue), # 转换为字符串,避免前端精度问题 + 'fabu': shangjia.fabu, + 'chengjiao': shangjia.chengjiao, + 'zhuangtai': shangjia.zhuangtai, # 1=正常,2=禁用 + }) + + meta = list_response_meta(request) + return Response({ + 'code': 0, + 'club_id': meta['club_id'], + 'scope': meta['scope'], + 'data': { + 'list': result, + 'total': total, + 'valid_merchant_count': valid_merchant_count, + } + }) + + + + + + +# yonghu/views.py 中添加 + +class KefuGetShangjiaDetailView(APIView): + """ + 客服/管理员获取商家详情接口 + 需要权限:1100a 或 1100b + 请求:POST /houtai/hthqsjxq + 参数: + username: 客服手机号(用于权限校验,必填) + yonghuid: 商家用户ID(必填) + 返回: + code: 0 成功 + data: 商家所有字段(含主表头像等) + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '认证失败,缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少商家ID'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + required_perms = {'1100a', '1100b'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看商家详情'}) + + # 查询商家 + try: + shangjia = UserShangjia.query.select_related('user').get(user__UserUID=yonghuid) + except UserShangjia.DoesNotExist: + return Response({'code': 404, 'msg': '商家不存在'}) + + denied = forbid_if_user_out_of_scope(request, shangjia.user) + if denied: + return denied + + user = shangjia.user + + data = { + 'yonghuid': user.UserUID, + 'avatar': user.Avatar or '', + 'nicheng': shangjia.nicheng, + 'zhuangtai': shangjia.zhuangtai, # 1正常,2禁用 + 'dianhua': shangjia.dianhua, + 'wechat': shangjia.wechat, + 'yue': str(shangjia.yue), + 'fabu': shangjia.fabu, + 'chengjiao': shangjia.chengjiao, + 'tuikuan': shangjia.tuikuan, + 'jinridingdan': shangjia.jinridingdan, + 'jinriliushui': str(shangjia.jinriliushui), + 'jinyuedingdan': shangjia.jinyuedingdan, + 'jinyueliushui': str(shangjia.jinyueliushui), + 'CreateTime': shangjia.CreateTime.isoformat() if shangjia.CreateTime else None, + } + + return Response({'code': 0, 'data': data}) + + + + + + + +class KefuUpdateShangjiaView(APIView): + """ + 客服/管理员修改商家信息(余额、状态) + POST /houtai/xgsjxx + 参数: + username, yonghuid, action + action可选:add_balance, sub_balance, ban, unban + amount(add_balance/sub_balance时必填) + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + action = request.data.get('action', '').strip().lower() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少商家ID'}) + if action not in ('add_balance', 'sub_balance', 'ban', 'unban'): + return Response({'code': 400, 'msg': 'action参数无效'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 操作权限校验 + if action in ('add_balance', 'ban', 'unban') and '1100a' not in permissions: + return Response({'code': 403, 'msg': '无权限执行此操作(需要1100a)'}) + if action == 'sub_balance' and '1100b' not in permissions: + return Response({'code': 403, 'msg': '无权限减少商家余额(需要1100b)'}) + + # 使用事务,select_for_update 必须在事务内 + with transaction.atomic(): + try: + shangjia = UserShangjia.objects.select_for_update().get(user__UserUID=yonghuid) + except UserShangjia.DoesNotExist: + return Response({'code': 404, 'msg': '商家不存在'}) + + # 增加余额 + if action == 'add_balance': + amount_str = request.data.get('amount') + try: + amount = Decimal(str(amount_str)) + if amount <= 0 or amount > 1000000: + return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + old_yue = shangjia.yue + new_yue = shangjia.yue + amount + if new_yue > 999999999.99: + return Response({'code': 400, 'msg': '增加后余额超出上限'}) + shangjia.yue = new_yue + shangjia.save(update_fields=['yue']) + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_SHANGJIA, + shangjiayue=new_yue, + shangjiayueq=old_yue, + qitashuoming=( + f'【后台】增加商家余额:{old_yue}元 → {new_yue}元,增加{amount}元,' + f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '余额增加成功', 'data': {'new_balance': str(shangjia.yue)}}) + + # 减少余额 + if action == 'sub_balance': + amount_str = request.data.get('amount') + try: + amount = Decimal(str(amount_str)) + if amount <= 0 or amount > 1000000: + return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + if shangjia.yue < amount: + return Response({'code': 400, 'msg': '余额不足'}) + old_yue = shangjia.yue + shangjia.yue = shangjia.yue - amount + shangjia.save(update_fields=['yue']) + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_SHANGJIA, + shangjiayue=shangjia.yue, + shangjiayueq=old_yue, + qitashuoming=( + f'【后台】减少商家余额:{old_yue}元 → {shangjia.yue}元,减少{amount}元,' + f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '余额减少成功', 'data': {'new_balance': str(shangjia.yue)}}) + + # 封禁 + if action == 'ban': + if shangjia.zhuangtai == 2: + return Response({'code': 400, 'msg': '商家已是封禁状态'}) + shangjia.zhuangtai = 2 + shangjia.save(update_fields=['zhuangtai']) + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_SHANGJIA, + qitashuoming=( + f'【后台】封禁商家账号:正常 → 禁用,' + f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '商家已封禁', 'data': {'zhuangtai': 2}}) + + # 解封 + if action == 'unban': + if shangjia.zhuangtai == 1: + return Response({'code': 400, 'msg': '商家已是正常状态'}) + shangjia.zhuangtai = 1 + shangjia.save(update_fields=['zhuangtai']) + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_SHANGJIA, + qitashuoming=( + f'【后台】解封商家账号:禁用 → 正常,' + f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' + ), + ) + return Response({'code': 0, 'msg': '商家已解封', 'data': {'zhuangtai': 1}}) + + + + + + + +class GetProductBaseDataView(APIView): + """ + 获取商品基础数据(类型、专区、会员) + 权限:2200a + POST /houtai/htsphqjcsx + 参数:username + 返回: + code:0, data: { + type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...], + zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...], + member_list: [ {huiyuan_id, jieshao, jiage}, ...] + } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取所有商品类型(按 paixu 升序) + type_qs = ShangpinLeixing.query.all().order_by('paixu') + type_list = [] + for t in type_qs: + # 🔥 修改:统计该类型下的公共商品数量(店铺为空且店铺商品类型为空) + product_count = Shangpin.query.filter( + leixing_id=t.id, + dianpu__isnull=True, # 所属店铺为空 + dianpu_leixing__isnull=True # 店铺商品类型为空 + ).count() + type_list.append({ + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'paixu': t.paixu, + 'shenhezhuangtai': t.shenhezhuangtai, + 'product_count': product_count, + }) + + # 获取所有专区(按 paixu 升序) + zone_qs = ShangpinZhuanqu.query.all().order_by('paixu') + zone_list = [] + for z in zone_qs: + # 🔥 修改:统计该专区下的公共商品数量(店铺为空且店铺商品类型为空) + product_count = Shangpin.query.filter( + zhuanqu_id=z.id, + dianpu__isnull=True, # 所属店铺为空 + dianpu_leixing__isnull=True # 店铺商品类型为空 + ).count() + zone_list.append({ + 'id': z.id, + 'mingzi': z.mingzi or '', + 'leixing_id': z.leixing_id, + 'paixu': z.paixu, + 'shenhezhuangtai': z.shenhezhuangtai, + 'product_count': product_count, + }) + + # 获取所有会员(只需 ID、介绍、价格) + member_list = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage')) + + return Response({ + 'code': 0, + 'data': { + 'type_list': type_list, + 'zone_list': zone_list, + 'member_list': member_list, + } + }) + + + +class GetProductListView(APIView): + """ + 获取商品列表(支持筛选、排序) + 权限:2200a + POST /houtai/hthqsplb + 参数: + username: 必填 + leixing_id: 类型ID(可选) + zhuanqu_id: 专区ID(可选) + keyword: 搜索关键词(商品标题/介绍) + sales_op: 销量比较符 gte/lte + sales_value: 销量数值 + stock_op: 库存比较符 gte/lte + stock_value: 库存数值 + sort_field: 排序字段 zhenshi_xiaoliang / jiage + sort_order: asc/desc + page: 页码(默认1) + page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留) + 返回: + code:0, data: { list: [...], total } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取参数 + leixing_id = request.data.get('leixing_id') + zhuanqu_id = request.data.get('zhuanqu_id') + keyword = request.data.get('keyword', '').strip() + sales_op = request.data.get('sales_op', '').strip().lower() + sales_value = request.data.get('sales_value') + stock_op = request.data.get('stock_op', '').strip().lower() + stock_value = request.data.get('stock_value') + sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage + sort_order = request.data.get('sort_order', 'desc').lower() + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 1000)) + if page < 1 or page_size < 1: + raise ValueError + except: + return Response({'code': 400, 'msg': '分页参数错误'}) + + # 🔥 修改:构建查询集,增加公共商品筛选(店铺为空且店铺商品类型为空) + qs = Shangpin.query.filter( + dianpu__isnull=True, # 所属店铺为空 + dianpu_leixing__isnull=True # 店铺商品类型为空 + ) + + if leixing_id: + try: + leixing_id = int(leixing_id) + qs = qs.filter(leixing_id=leixing_id) + except: + pass + if zhuanqu_id: + try: + zhuanqu_id = int(zhuanqu_id) + qs = qs.filter(zhuanqu_id=zhuanqu_id) + except: + pass + if keyword: + qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword)) + + # 销量筛选 + if sales_op and sales_value is not None: + try: + sales_val = int(sales_value) + if sales_op == 'gte': + qs = qs.filter(zhenshi_xiaoliang__gte=sales_val) + elif sales_op == 'lte': + qs = qs.filter(zhenshi_xiaoliang__lte=sales_val) + except: + pass + + # 库存筛选 + if stock_op and stock_value is not None: + try: + stock_val = int(stock_value) + if stock_op == 'gte': + qs = qs.filter(kucun__gte=stock_val) + elif stock_op == 'lte': + qs = qs.filter(kucun__lte=stock_val) + except: + pass + + # 排序 + if sort_field in ('zhenshi_xiaoliang', 'jiage'): + if sort_order == 'asc': + qs = qs.order_by(sort_field) + else: + qs = qs.order_by(f'-{sort_field}') + else: + qs = qs.order_by('-CreateTime') # 默认按创建时间倒序 + + # 分页 + total = qs.count() + offset = (page - 1) * page_size + products = qs[offset:offset + page_size] + + # 构建返回数据 + product_list = [] + for p in products: + product_list.append({ + 'id': p.id, + 'biaoqian': p.biaoqian or '', + 'jiage': str(p.jiage), + 'kucun': p.kucun, + 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, + 'tupian_url': p.tupian_url or '', + 'jieshao': p.jieshao or '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': product_list, + 'total': total, + } + }) + + + + +'''class GetProductBaseDataView(APIView): + """ + 获取商品基础数据(类型、专区、会员) + 权限:2200a + POST /houtai/htsphqjcsx + 参数:username + 返回: + code:0, data: { + type_list: [ {id, jieshao, tupian_url, paixu, shenhezhuangtai, product_count}, ...], + zone_list: [ {id, mingzi, leixing_id, paixu, shenhezhuangtai, product_count}, ...], + member_list: [ {huiyuan_id, jieshao, jiage}, ...] + } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取所有商品类型(按 paixu 升序) + type_qs = ShangpinLeixing.query.all().order_by('paixu') + type_list = [] + for t in type_qs: + # 统计该类型下的商品数量(忽略审核状态?通常统计所有商品,可根据需求调整) + product_count = Shangpin.query.filter(leixing_id=t.id).count() + type_list.append({ + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'paixu': t.paixu, + 'shenhezhuangtai': t.shenhezhuangtai, + 'product_count': product_count, + }) + + # 获取所有专区(按 paixu 升序) + zone_qs = ShangpinZhuanqu.query.all().order_by('paixu') + zone_list = [] + for z in zone_qs: + product_count = Shangpin.query.filter(zhuanqu_id=z.id).count() + zone_list.append({ + 'id': z.id, + 'mingzi': z.mingzi or '', + 'leixing_id': z.leixing_id, + 'paixu': z.paixu, + 'shenhezhuangtai': z.shenhezhuangtai, + 'product_count': product_count, + }) + + # 获取所有会员(只需 ID、介绍、价格) + member_list = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage')) + + return Response({ + 'code': 0, + 'data': { + 'type_list': type_list, + 'zone_list': zone_list, + 'member_list': member_list, + } + }) + + +class GetProductListView(APIView): + """ + 获取商品列表(支持筛选、排序) + 权限:2200a + POST /houtai/hthqsplb + 参数: + username: 必填 + leixing_id: 类型ID(可选) + zhuanqu_id: 专区ID(可选) + keyword: 搜索关键词(商品标题/介绍) + sales_op: 销量比较符 gte/lte + sales_value: 销量数值 + stock_op: 库存比较符 gte/lte + stock_value: 库存数值 + sort_field: 排序字段 zhenshi_xiaoliang / jiage + sort_order: asc/desc + page: 页码(默认1) + page_size: 每页数量(默认1000,前端说不需要分页,但为性能仍保留) + 返回: + code:0, data: { list: [...], total } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取参数 + leixing_id = request.data.get('leixing_id') + zhuanqu_id = request.data.get('zhuanqu_id') + keyword = request.data.get('keyword', '').strip() + sales_op = request.data.get('sales_op', '').strip().lower() + sales_value = request.data.get('sales_value') + stock_op = request.data.get('stock_op', '').strip().lower() + stock_value = request.data.get('stock_value') + sort_field = request.data.get('sort_field', '') # zhenshi_xiaoliang / jiage + sort_order = request.data.get('sort_order', 'desc').lower() + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 1000)) + if page < 1 or page_size < 1: + raise ValueError + except: + return Response({'code': 400, 'msg': '分页参数错误'}) + + # 构建查询集 + qs = Shangpin.query.all() + + if leixing_id: + try: + leixing_id = int(leixing_id) + qs = qs.filter(leixing_id=leixing_id) + except: + pass + if zhuanqu_id: + try: + zhuanqu_id = int(zhuanqu_id) + qs = qs.filter(zhuanqu_id=zhuanqu_id) + except: + pass + if keyword: + qs = qs.filter(Q(biaoqian__icontains=keyword) | Q(jieshao__icontains=keyword)) + + # 销量筛选 + if sales_op and sales_value is not None: + try: + sales_val = int(sales_value) + if sales_op == 'gte': + qs = qs.filter(zhenshi_xiaoliang__gte=sales_val) + elif sales_op == 'lte': + qs = qs.filter(zhenshi_xiaoliang__lte=sales_val) + except: + pass + + # 库存筛选 + if stock_op and stock_value is not None: + try: + stock_val = int(stock_value) + if stock_op == 'gte': + qs = qs.filter(kucun__gte=stock_val) + elif stock_op == 'lte': + qs = qs.filter(kucun__lte=stock_val) + except: + pass + + # 排序 + if sort_field in ('zhenshi_xiaoliang', 'jiage'): + if sort_order == 'asc': + qs = qs.order_by(sort_field) + else: + qs = qs.order_by(f'-{sort_field}') + else: + qs = qs.order_by('-CreateTime') # 默认按创建时间倒序 + + # 分页 + total = qs.count() + offset = (page - 1) * page_size + products = qs[offset:offset + page_size] + + # 构建返回数据 + product_list = [] + for p in products: + product_list.append({ + 'id': p.id, + 'biaoqian': p.biaoqian or '', + 'jiage': str(p.jiage), + 'kucun': p.kucun, + 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, + 'tupian_url': p.tupian_url or '', + 'jieshao': p.jieshao or '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': product_list, + 'total': total, + } + })''' + + +class SaveProductOrderView(APIView): + """ + 保存商品类型或专区的排序 + 权限:2200a + POST /houtai/htsppaixu + 参数: + username: 必填 + type: 'leixing' 或 'zhuanqu' + leixing_id: 当 type='zhuanqu' 时必填(用于限定该类型下的专区) + items: [ {id, paixu}, ... ] # 每个项目的新排序值(从1开始) + 返回: + code:0 成功 + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + sort_type = request.data.get('type', '').strip().lower() + if sort_type not in ('leixing', 'zhuanqu'): + return Response({'code': 400, 'msg': 'type参数必须为leixing或zhuanqu'}) + + items = request.data.get('items') + if not isinstance(items, list) or len(items) == 0: + return Response({'code': 400, 'msg': 'items参数无效'}) + + # 校验每个 item 的格式 + for item in items: + if not isinstance(item, dict) or 'id' not in item or 'paixu' not in item: + return Response({'code': 400, 'msg': 'items内每项需包含id和paixu'}) + try: + item['id'] = int(item['id']) + item['paixu'] = int(item['paixu']) + if item['paixu'] < 1: + return Response({'code': 400, 'msg': 'paixu必须为正整数'}) + except: + return Response({'code': 400, 'msg': 'id或paixu格式错误'}) + + with transaction.atomic(): + if sort_type == 'leixing': + # 更新商品类型的排序 + for item in items: + ShangpinLeixing.query.filter(id=item['id']).update(paixu=item['paixu']) + else: # zhuanqu + leixing_id = request.data.get('leixing_id') + if not leixing_id: + return Response({'code': 400, 'msg': '更新专区排序时必须提供leixing_id'}) + try: + leixing_id = int(leixing_id) + except: + return Response({'code': 400, 'msg': 'leixing_id格式错误'}) + # 可选:检查该专区是否属于该类型,但不强制,由前端保证 + for item in items: + ShangpinZhuanqu.query.filter(id=item['id']).update(paixu=item['paixu']) + + return Response({'code': 0, 'msg': '排序保存成功'}) + + +class AddProductView(APIView): + """ + 添加商品(含图片上传) + 权限:2200a + POST /houtai/htfbsp + 参数:multipart/form-data,包含所有商品字段及图片文件 + 字段说明见代码 + """ + permission_classes = [] + parser_classes = [MultiPartParser, FormParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 获取表单字段 + biaoqian = request.data.get('biaoqian', '').strip() + jiage_str = request.data.get('jiage') + kucun_str = request.data.get('kucun') + duiwai_xiaoliang_str = request.data.get('duiwai_xiaoliang', '0') + leixing_id_str = request.data.get('leixing_id') + zhuanqu_id_str = request.data.get('zhuanqu_id') + yaoqiuleixing_str = request.data.get('yaoqiuleixing', '1') + huiyuan_id = request.data.get('huiyuan_id', '').strip() + yongjin_str = request.data.get('yongjin', '0') + fencheng_type = request.data.get('fencheng_type', 'default') + ewai_PlayerCommission_str = request.data.get('ewai_PlayerCommission', '0') + jieshao = request.data.get('jieshao', '').strip() + xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip() + # 图片文件 + image_file = request.FILES.get('file') # 前端上传时字段名为 'file' + + # 必填字段校验 + if not biaoqian: + return Response({'code': 400, 'msg': '商品标题不能为空'}) + try: + jiage = Decimal(jiage_str) + if jiage <= 0: + return Response({'code': 400, 'msg': '价格必须大于0'}) + except: + return Response({'code': 400, 'msg': '价格格式错误'}) + try: + kucun = int(kucun_str) + if kucun < 0: + return Response({'code': 400, 'msg': '库存不能为负数'}) + except: + return Response({'code': 400, 'msg': '库存格式错误'}) + try: + duiwai_xiaoliang = int(duiwai_xiaoliang_str) + except: + duiwai_xiaoliang = 0 + try: + leixing_id = int(leixing_id_str) + except: + return Response({'code': 400, 'msg': '商品类型ID无效'}) + try: + zhuanqu_id = int(zhuanqu_id_str) + except: + return Response({'code': 400, 'msg': '商品专区ID无效'}) + try: + yaoqiuleixing = int(yaoqiuleixing_str) + if yaoqiuleixing not in (1, 2): + raise ValueError + except: + return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'}) + + # 校验类型和专区的存在性及关联性 + try: + leixing_obj = ShangpinLeixing.query.get(id=leixing_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 400, 'msg': '商品类型不存在'}) + try: + zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id) + if zhuanqu_obj.leixing_id != leixing_id: + return Response({'code': 400, 'msg': '专区不属于该商品类型'}) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 400, 'msg': '商品专区不存在'}) + + # 处理抢单要求相关字段 + huiyuan_id_val = None + yongjin_val = None + if yaoqiuleixing == 1: + # 会员要求 + if not huiyuan_id: + # 未传递时,尝试从商品类型中获取默认会员ID + huiyuan_id_val = leixing_obj.huiyuan_id + if not huiyuan_id_val: + return Response({'code': 400, 'msg': '未指定会员且商品类型无默认会员'}) + else: + huiyuan_id_val = huiyuan_id + # 验证会员是否存在 + if not Huiyuan.query.filter(huiyuan_id=huiyuan_id_val).exists(): + return Response({'code': 400, 'msg': '指定的会员不存在'}) + else: + # 佣金要求 + try: + yongjin_val = Decimal(yongjin_str) + if yongjin_val < 0: + return Response({'code': 400, 'msg': '佣金不能为负数'}) + except: + # 若未传递或无效,尝试从商品类型中获取默认佣金 + if leixing_obj.yongjin is not None: + yongjin_val = leixing_obj.yongjin + else: + return Response({'code': 400, 'msg': '佣金格式错误且类型无默认佣金'}) + + # 处理打手分成方式 + kaioi_ewai = False + ewai_fencheng = Decimal('0') + if fencheng_type == 'custom': + try: + ewai_fencheng = Decimal(ewai_PlayerCommission_str) + if ewai_fencheng < 0: + return Response({'code': 400, 'msg': '自定义分成金额不能为负数'}) + kaioi_ewai = True + except: + return Response({'code': 400, 'msg': '自定义分成金额格式错误'}) + + # 图片上传处理 + tupian_url = '' + if image_file: + # 验证图片 + valid, msg = validate_image(image_file) + if not valid: + return Response({'code': 400, 'msg': f'图片无效:{msg}'}) + # 生成唯一文件名 + ext = image_file.name.split('.')[-1].lower() + if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: + return Response({'code': 400, 'msg': '不支持的图片格式'}) + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + random_str = uuid.uuid4().hex[:8] + filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}" + # 上传到 OSS + uploaded_url = upload_to_oss(image_file, filename) + if not uploaded_url: + return Response({'code': 500, 'msg': '图片上传失败,请稍后重试'}) + tupian_url = filename # 存储相对路径,前端拼接域名 + else: + return Response({'code': 400, 'msg': '商品图片不能为空'}) + + # 创建商品 + try: + with transaction.atomic(): + product = Shangpin.query.create( + biaoqian=biaoqian, + jiage=jiage, + kucun=kucun, + leixing_id=leixing_id, + zhuanqu_id=zhuanqu_id, + zhenshi_xiaoliang=0, # 初始真实销量为0 + duiwai_xiaoliang=duiwai_xiaoliang, + jieshao=jieshao, + xiadan_xuzhi=xiadan_xuzhi, + tupian_url=tupian_url, + yaoqiuleixing=yaoqiuleixing, + huiyuan_id=huiyuan_id_val, + yongjin=yongjin_val, + kaioi_ewai_dashou_fencheng=kaioi_ewai, + ewai_dashou_fencheng=ewai_fencheng, + paixu=0, # 新商品默认排序最后 + shenhezhuangtai=1, # 正常状态 + ) + except Exception as e: + logger.error(f"创建商品失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '创建商品失败,请稍后重试'}) + + return Response({ + 'code': 0, + 'msg': '商品发布成功', + 'data': { + 'id': product.id, + 'tupian_url': tupian_url, + } + }) + + +# houtai/views_product.py 中添加 + +class DeleteProductView(APIView): + """ + 删除商品(物理删除,同时删除OSS图片) + 权限:2200a + POST /houtai/htscsp + 参数: + username: 客服/管理员账号 + id: 商品ID + 返回: + code:0 成功 + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + product_id = request.data.get('id') + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not product_id: + return Response({'code': 400, 'msg': '缺少商品ID'}) + try: + product_id = int(product_id) + except: + return Response({'code': 400, 'msg': '商品ID格式错误'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 查询商品是否存在 + try: + product = Shangpin.query.get(id=product_id) + except Shangpin.DoesNotExist: + return Response({'code': 404, 'msg': '商品不存在'}) + + # 先删除OSS上的图片 + tupian_url = product.tupian_url + if tupian_url: + # 调用已有的 delete_from_oss 函数 + delete_success = delete_from_oss(tupian_url) + if not delete_success: + logger.warning(f"删除商品图片失败: {tupian_url}, 商品ID: {product_id}") + # 注意:即使图片删除失败,也应继续删除数据库记录,避免数据不一致 + # 但可以记录日志供后续人工处理 + + # 删除数据库记录 + product.delete() + + logger.info(f"商品删除成功: id={product_id}, 图片路径={tupian_url}, 操作人={username}") + return Response({'code': 0, 'msg': '商品删除成功'}) + + + + + + + +class GetProductDetailView(APIView): + """ + 获取商品详情(包含商品类型、专区、会员列表) + 权限:2200a + POST /houtai/htsphqxq + 参数: + username: 客服/管理员账号 + id: 商品ID + 返回: + code:0, data: { product, type_list, zone_list, member_list } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + product_id = request.data.get('id') + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not product_id: + return Response({'code': 400, 'msg': '缺少商品ID'}) + try: + product_id = int(product_id) + except ValueError: + return Response({'code': 400, 'msg': '商品ID格式错误'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 查询商品 + try: + product = Shangpin.query.get(id=product_id) + except Shangpin.DoesNotExist: + return Response({'code': 404, 'msg': '商品不存在'}) + + # 查询所有商品类型、专区、会员(用于前端下拉选择) + type_list = list(ShangpinLeixing.query.all().values( + 'id', 'jieshao', 'tupian_url', 'shenhezhuangtai' + )) + zone_list = list(ShangpinZhuanqu.query.all().values( + 'id', 'mingzi', 'leixing_id', 'shenhezhuangtai' + )) + member_list = list(Huiyuan.query.all().values( + 'huiyuan_id', 'jieshao', 'jiage' + )) + + # 构建商品返回数据 + product_data = { + 'id': product.id, + 'biaoqian': product.biaoqian, + 'jiage': str(product.jiage), + 'kucun': product.kucun, + 'leixing_id': product.leixing_id, + 'zhuanqu_id': product.zhuanqu_id, + 'zhenshi_xiaoliang': product.zhenshi_xiaoliang, + 'duiwai_xiaoliang': product.duiwai_xiaoliang, + 'jieshao': product.jieshao, + 'xiadan_xuzhi': product.xiadan_xuzhi, + 'guize_tupian': product.guize_tupian, + 'yaoqiuleixing': product.yaoqiuleixing, + 'huiyuan_id': product.huiyuan_id, + 'yongjin': str(product.yongjin) if product.yongjin is not None else None, + 'tupian_url': product.tupian_url, + 'kaioi_ewai_PlayerCommission': product.kaioi_ewai_dashou_fencheng, + 'ewai_PlayerCommission': str(product.ewai_dashou_fencheng) if product.ewai_dashou_fencheng is not None else '0', + 'CreateTime': product.CreateTime.isoformat() if product.CreateTime else None, + 'shenhezhuangtai': product.shenhezhuangtai, + } + + return Response({ + 'code': 0, + 'data': { + 'product': product_data, + 'type_list': type_list, + 'zone_list': zone_list, + 'member_list': member_list, + } + }) + + + +class UpdateProductView(APIView): + """ + 修改商品(支持商品图片和规则图片的更新) + 权限:2200a + POST /houtai/bcspxg + """ + permission_classes = [] + parser_classes = [MultiPartParser, FormParser] + + def post(self, request): + username = request.data.get('username', '').strip() + product_id = request.data.get('id') + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not product_id: + return Response({'code': 400, 'msg': '缺少商品ID'}) + try: + product_id = int(product_id) + except ValueError: + return Response({'code': 400, 'msg': '商品ID格式错误'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '2200a' not in permissions: + return Response({'code': 403, 'msg': '无权限管理商品'}) + + # 使用事务,将 select_for_update 放在事务内 + with transaction.atomic(): + try: + product = Shangpin.objects.select_for_update().get(id=product_id) + except Shangpin.DoesNotExist: + return Response({'code': 404, 'msg': '商品不存在'}) + + # ---------- 1. 基础字段 ---------- + biaoqian = request.data.get('biaoqian', '').strip() + if not biaoqian: + return Response({'code': 400, 'msg': '商品标题不能为空'}) + + try: + jiage = Decimal(str(request.data.get('jiage', 0))) + if jiage <= 0: + return Response({'code': 400, 'msg': '价格必须大于0'}) + except: + return Response({'code': 400, 'msg': '价格格式错误'}) + + try: + kucun = int(request.data.get('kucun', 0)) + if kucun < 0: + return Response({'code': 400, 'msg': '库存不能为负数'}) + except: + return Response({'code': 400, 'msg': '库存格式错误'}) + + try: + duiwai_xiaoliang = int(request.data.get('duiwai_xiaoliang', 0)) + except: + duiwai_xiaoliang = 0 + + try: + leixing_id = int(request.data.get('leixing_id')) + except: + return Response({'code': 400, 'msg': '商品类型ID无效'}) + + try: + zhuanqu_id = int(request.data.get('zhuanqu_id')) + except: + return Response({'code': 400, 'msg': '商品专区ID无效'}) + + try: + yaoqiuleixing = int(request.data.get('yaoqiuleixing', 1)) + if yaoqiuleixing not in (1, 2): + raise ValueError + except: + return Response({'code': 400, 'msg': '抢单要求类型必须为1或2'}) + + try: + shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) + except: + shenhezhuangtai = 1 + + # 校验类型和专区存在性 + if not ShangpinLeixing.query.filter(id=leixing_id).exists(): + return Response({'code': 400, 'msg': '商品类型不存在'}) + if not ShangpinZhuanqu.query.filter(id=zhuanqu_id, leixing_id=leixing_id).exists(): + return Response({'code': 400, 'msg': '专区不存在或不属于该类型'}) + + # ---------- 2. 会员/佣金 ---------- + huiyuan_id = None + yongjin = None + if yaoqiuleixing == 1: + huiyuan_id = request.data.get('huiyuan_id', '').strip() or None + if huiyuan_id and not Huiyuan.query.filter(huiyuan_id=huiyuan_id).exists(): + return Response({'code': 400, 'msg': '指定的会员不存在'}) + else: + try: + yongjin = Decimal(str(request.data.get('yongjin', 0))) + if yongjin < 0: + return Response({'code': 400, 'msg': '佣金不能为负数'}) + except: + yongjin = Decimal('0') + + # ---------- 3. 打手分成 ---------- + kaioi_ewai = request.data.get('kaioi_ewai_PlayerCommission') == 'true' + ewai_fencheng = Decimal('0') + if kaioi_ewai: + try: + ewai_fencheng = Decimal(str(request.data.get('ewai_PlayerCommission', 0))) + if ewai_fencheng < 0: + return Response({'code': 400, 'msg': '分成金额不能为负数'}) + except: + return Response({'code': 400, 'msg': '分成金额格式错误'}) + + jieshao = request.data.get('jieshao', '').strip() + xiadan_xuzhi = request.data.get('xiadan_xuzhi', '').strip() + + # ---------- 4. 商品图片更新 ---------- + tupian_url = product.tupian_url + tupian_file = request.FILES.get('tupian_file') + if tupian_file: + valid, msg = validate_image(tupian_file) + if not valid: + return Response({'code': 400, 'msg': f'商品图片无效:{msg}'}) + ext = tupian_file.name.split('.')[-1].lower() + if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: + return Response({'code': 400, 'msg': '不支持的图片格式'}) + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + random_str = uuid.uuid4().hex[:8] + new_filename = f"shangpin/shangpintupian/{timestamp}_{random_str}.{ext}" + uploaded_url = upload_to_oss(tupian_file, new_filename) + if not uploaded_url: + return Response({'code': 500, 'msg': '商品图片上传失败'}) + if product.tupian_url: + delete_from_oss(product.tupian_url) + tupian_url = new_filename + + # ---------- 5. 规则图片更新 ---------- + guize_tupian = product.guize_tupian + guize_file = request.FILES.get('guize_file') + if guize_file: + valid, msg = validate_image(guize_file) + if not valid: + return Response({'code': 400, 'msg': f'规则图片无效:{msg}'}) + ext = guize_file.name.split('.')[-1].lower() + if ext not in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']: + return Response({'code': 400, 'msg': '不支持的图片格式'}) + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + random_str = uuid.uuid4().hex[:8] + new_filename = f"shangpin/guizetupian/{timestamp}_{random_str}.{ext}" + uploaded_url = upload_to_oss(guize_file, new_filename) + if not uploaded_url: + return Response({'code': 500, 'msg': '规则图片上传失败'}) + if product.guize_tupian: + delete_from_oss(product.guize_tupian) + guize_tupian = new_filename + + # ---------- 6. 更新数据库 ---------- + product.biaoqian = biaoqian + product.jiage = jiage + product.kucun = kucun + product.duiwai_xiaoliang = duiwai_xiaoliang + product.leixing_id = leixing_id + product.zhuanqu_id = zhuanqu_id + product.yaoqiuleixing = yaoqiuleixing + product.huiyuan_id = huiyuan_id + product.yongjin = yongjin + product.kaioi_ewai_dashou_fencheng = kaioi_ewai + product.ewai_dashou_fencheng = ewai_fencheng + product.jieshao = jieshao + product.xiadan_xuzhi = xiadan_xuzhi + product.tupian_url = tupian_url + product.guize_tupian = guize_tupian + product.shenhezhuangtai = shenhezhuangtai + product.save() + + return Response({'code': 0, 'msg': '商品修改成功', 'data': {'id': product.id}}) + + + + + +class GetMemberListView(APIView): + """ + 获取会员列表 + 权限:3300a + POST /houtai/hthqhylb + 参数:username + 返回:code:0, data: { list: [...] } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '3300a' not in permissions: + return Response({'code': 403, 'msg': '无权限查看会员列表'}) + + members = Huiyuan.query.all().order_by('-CreateTime') + member_list = [] + for m in members: + member_list.append({ + 'huiyuan_id': m.huiyuan_id, + 'jieshao': m.jieshao, + 'jtjieshao': m.jtjieshao, + 'jiage': str(m.jiage), + 'guanshifc': str(m.guanshifc), + 'zuzhangfc': str(m.zuzhangfc), + 'goumai_cishu': m.goumai_cishu, + 'CreateTime': m.CreateTime.isoformat() if m.CreateTime else None, + 'UpdateTime': m.UpdateTime.isoformat() if m.UpdateTime else None, + }) + return Response({'code': 0, 'data': {'list': member_list}}) + + +class UpdateMemberView(APIView): + """ + 修改会员信息 + 权限:3300a + POST /houtai/htxghyxx + 参数: + username, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao + 返回:code:0 成功 + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + huiyuan_id = request.data.get('huiyuan_id', '').strip() + jieshao = request.data.get('jieshao', '').strip() + jiage = request.data.get('jiage') + guanshifc = request.data.get('guanshifc') + zuzhangfc = request.data.get('zuzhangfc') + jtjieshao = request.data.get('jtjieshao', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not huiyuan_id: + return Response({'code': 400, 'msg': '缺少会员ID'}) + if not jieshao: + return Response({'code': 400, 'msg': '会员名称不能为空'}) + + try: + jiage = Decimal(str(jiage)) + guanshifc = Decimal(str(guanshifc)) + zuzhangfc = Decimal(str(zuzhangfc)) + if jiage < 0 or guanshifc < 0 or zuzhangfc < 0: + raise ValueError + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + + if guanshifc + zuzhangfc > jiage: + return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '3300a' not in permissions: + return Response({'code': 403, 'msg': '无权限修改会员'}) + + with transaction.atomic(): + try: + member = Huiyuan.objects.select_for_update().get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response({'code': 404, 'msg': '会员不存在'}) + + member.jieshao = jieshao + member.jiage = jiage + member.guanshifc = guanshifc + member.zuzhangfc = zuzhangfc + member.jtjieshao = jtjieshao + member.save() + + return Response({'code': 0, 'msg': '修改成功'}) + + + +class AddMemberView(APIView): + """ + 添加会员 + 权限:3300a + POST /houtai/httjhy + 参数: + username, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao + 返回:code:0, data: { huiyuan_id } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + jieshao = request.data.get('jieshao', '').strip() + jiage = request.data.get('jiage') + guanshifc = request.data.get('guanshifc') + zuzhangfc = request.data.get('zuzhangfc') + jtjieshao = request.data.get('jtjieshao', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not jieshao: + return Response({'code': 400, 'msg': '会员名称不能为空'}) + + try: + jiage = Decimal(str(jiage)) + guanshifc = Decimal(str(guanshifc)) + zuzhangfc = Decimal(str(zuzhangfc)) + if jiage <= 0 or guanshifc < 0 or zuzhangfc < 0: + raise ValueError + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + + if guanshifc + zuzhangfc > jiage: + return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '3300a' not in permissions: + return Response({'code': 403, 'msg': '无权限添加会员'}) + + # 生成唯一6位会员ID(数字+字母) + def generate_huiyuan_id(): + while True: + new_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) + if not Huiyuan.query.filter(huiyuan_id=new_id).exists(): + return new_id + + with transaction.atomic(): + new_id = generate_huiyuan_id() + member = Huiyuan.query.create( + huiyuan_id=new_id, + jieshao=jieshao, + jiage=jiage, + guanshifc=guanshifc, + zuzhangfc=zuzhangfc, + jtjieshao=jtjieshao, + goumai_cishu=0 + ) + + return Response({'code': 0, 'msg': '添加成功', 'data': {'huiyuan_id': member.huiyuan_id}}) + + + + + +class GetGuanliListView(APIView): + """ + 获取管事列表 + 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一 + POST /houtai/hthqgslb + 参数: + username, keyword(可选), balance_op/balance_value, commission_op/commission_value, + invite_count_op/invite_count_value, status, page, page_size + 返回: + code:0, data: { list: [...], total } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + # 权限校验(需要任一权限) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看管事列表'}) + + # 分页参数 + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + if page < 1 or page_size < 1: + raise ValueError + except: + return Response({'code': 400, 'msg': '分页参数错误'}) + + # 筛选参数 + keyword = request.data.get('keyword', '').strip() + balance_op = request.data.get('balance_op', '').strip().lower() + balance_value = request.data.get('balance_value') + commission_op = request.data.get('commission_op', '').strip().lower() + commission_value = request.data.get('commission_value') + invite_count_op = request.data.get('invite_count_op', '').strip().lower() + invite_count_value = request.data.get('invite_count_value') + status = request.data.get('status') + + # 构建查询:从 User 开始,关联 UserGuanshi 和 UserBoss(左连接获取昵称) + qs = filter_user_qs_by_club( + User.query.filter( + GuanshiProfile__isnull=False # 确保是管事 + ).select_related('GuanshiProfile').select_related('BossProfile'), + request, + ) + + # 关键词搜索(用户ID 或 昵称来自 boss_profile.nickname) + if keyword: + qs = qs.filter( + Q(UserUID__icontains=keyword) | + Q(BossProfile__nickname__icontains=keyword) + ) + + # 余额筛选(使用 guanshi_profile.yue) + if balance_op and balance_value is not None and balance_value != '': + try: + balance_val = Decimal(str(balance_value)) + if balance_op == 'gte': + qs = qs.filter(GuanshiProfile__yue__gte=balance_val) + elif balance_op == 'lte': + qs = qs.filter(GuanshiProfile__yue__lte=balance_val) + else: + return Response({'code': 400, 'msg': '余额比较符无效'}) + except: + return Response({'code': 400, 'msg': '余额数值格式错误'}) + + # 分佣总额筛选(guanshi_profile.chongzhifenrun) + if commission_op and commission_value is not None and commission_value != '': + try: + commission_val = Decimal(str(commission_value)) + if commission_op == 'gte': + qs = qs.filter(GuanshiProfile__chongzhifenrun__gte=commission_val) + elif commission_op == 'lte': + qs = qs.filter(GuanshiProfile__chongzhifenrun__lte=commission_val) + else: + return Response({'code': 400, 'msg': '分佣比较符无效'}) + except: + return Response({'code': 400, 'msg': '分佣数值格式错误'}) + + # 邀请打手总数筛选(guanshi_profile.yaogingshuliang) + if invite_count_op and invite_count_value is not None and invite_count_value != '': + try: + invite_val = int(invite_count_value) + if invite_count_op == 'gte': + qs = qs.filter(GuanshiProfile__yaogingshuliang__gte=invite_val) + elif invite_count_op == 'lte': + qs = qs.filter(GuanshiProfile__yaogingshuliang__lte=invite_val) + else: + return Response({'code': 400, 'msg': '邀请数量比较符无效'}) + except: + return Response({'code': 400, 'msg': '邀请数量格式错误'}) + + # 状态筛选 + if status is not None and status != '': + try: + status_val = int(status) + if status_val not in (0, 1): + raise ValueError + qs = qs.filter(GuanshiProfile__zhuangtai=status_val) + except: + return Response({'code': 400, 'msg': '状态值无效'}) + + # 计算总数 + total = qs.count() + + # 分页 + offset = (page - 1) * page_size + users = qs.order_by('-UserCreateTime')[offset:offset + page_size] + + # 构建返回数据 + result = [] + for user in users: + guanshi = user.GuanshiProfile + boss = user.BossProfile if hasattr(user, 'BossProfile') else None + result.append({ + 'yonghuid': user.UserUID, + 'avatar': user.Avatar or '', + 'nickname': boss.nickname if boss else '', + 'zhuangtai': guanshi.zhuangtai, + 'yaogingshuliang': guanshi.yaogingshuliang, + 'yue': str(guanshi.yue), + 'chongzhifenrun': str(guanshi.chongzhifenrun), + }) + + return Response({ + 'code': 0, + **list_response_meta(request), + 'data': {'list': result, 'total': total}, + }) + + + + + + + + +class GetGuanliDetailView(APIView): + """ + 获取管事详情 + 权限:4400a,4400b,4400c,4400d,4400e,4400f 任一 + POST /houtai/hthqgsxq + 参数:username, yonghuid + 返回: + code:0, data: { + base: {...}, + balance: {...}, + permanent_enabled, permanent_amount, + custom_first_dividend: {}, + extra_dividend_map: {}, + all_members: [] + } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + + # 权限校验(需要任一权限) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看管事详情'}) + + # 查询用户主表和管事扩展表 + try: + user = User.query.select_related('GuanshiProfile', 'BossProfile').get(UserUID=yonghuid) + guanshi = user.GuanshiProfile + boss = user.BossProfile if hasattr(user, 'BossProfile') else None + except User.DoesNotExist: + return Response({'code': 404, 'msg': '管事不存在'}) + except UserGuanshi.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是管事'}) + + denied = forbid_if_user_out_of_scope(request, user) + if denied: + return denied + + # 基础信息 + is_zuzhang = UserZuzhang.query.filter(user=user).exists() + base_data = { + 'yonghuid': user.UserUID, + 'nickname': boss.nickname if boss else '', + 'avatar': user.Avatar or '', + 'dianhua': guanshi.dianhua or '', + 'wechat': guanshi.wechat or '', + 'zhuangtai': guanshi.zhuangtai, + 'yaoqingma': guanshi.yaoqingma, + 'yaoqingren': guanshi.yaoqingren, + 'invite_qrcode_url': guanshi.invite_qrcode_url or '', + 'yaogingshuliang': guanshi.yaogingshuliang, + 'jinrichongzhi': guanshi.jinrichongzhi, + 'jinyuechongzhi': guanshi.jinyuechongzhi, + 'chongzhifenrun': str(guanshi.chongzhifenrun), + 'create_time': guanshi.CreateTime.isoformat() if guanshi.CreateTime else None, + 'CreateTime': guanshi.CreateTime.isoformat() if guanshi.CreateTime else None, + } + + # 余额提现相关 + balance_data = { + 'yue': str(guanshi.yue), + 'jinritixian_jine': str(guanshi.jinritixian_jine), + 'last_tixian_date': guanshi.last_tixian_date.isoformat() if guanshi.last_tixian_date else None, + 'kaioi_ewai_xiane': guanshi.kaioi_ewai_xiane, + 'ewai_xiane': str(guanshi.ewai_xiane), + } + + # 永久分红(二次分红) + permanent_enabled = guanshi.fenghong_erci_enabled + permanent_amount = str(guanshi.fenghong_erci) if guanshi.fenghong_erci else '0' + + # 获取所有会员(用于前端选择) + all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc')) + for m in all_members: + m['jiage'] = str(m['jiage']) + m['guanshifc'] = str(m['guanshifc']) + + # 获取多次分红配置(按次数和会员组织) + # 查询该管事的所有多次分红记录 + duoci_records = DuociFenhong.query.filter(yonghuid=yonghuid).order_by('cishu') + custom_first = {} # 首次分红定制(cishu=1) + extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } } + + for record in duoci_records: + # 获取会员信息 + member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first() + if not member: + continue + member_info = { + 'jieshao': member.jieshao, + 'jiage': str(member.jiage), + } + if record.cishu == 1: + custom_first[record.huiyuan] = float(record.guanshi_fenhong) + else: + times = record.cishu + if times not in extra_map: + extra_map[times] = {} + extra_map[times][record.huiyuan] = { + 'guanshi_fenhong': float(record.guanshi_fenhong), + **member_info + } + + # 注意:首次分红定制只存储有定制记录的会员,没有定制的会员使用默认值(前端通过会员表展示) + # 额外次数分红按次数返回 + + return Response({ + 'code': 0, + 'data': { + 'base': base_data, + 'balance': balance_data, + 'permanent_enabled': permanent_enabled, + 'permanent_amount': permanent_amount, + 'custom_first_dividend': custom_first, + 'extra_dividend_map': extra_map, + 'all_members': all_members, + 'is_zuzhang': is_zuzhang, + } + }) + + + + +def _generate_unique_zuzhang_yaoqingma(yonghuid): + """生成唯一组长邀请码(与小程序组长注册逻辑一致)""" + timestamp = str(int(time.time()))[-6:] + uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0') + rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2)) + base = f"Z{timestamp}{uid_part}{rand_str}" + for _ in range(5): + if not UserZuzhang.query.filter(yaoqingma=base).exists(): + return base + base = base + random.choice(string.digits) + return base + str(int(time.time() * 1000))[-4:] + + +def _create_zuzhang_profile(user): + """为已有用户创建组长扩展记录(默认字段与小程序注册一致)""" + invite_guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count() + yaoqingma = _generate_unique_zuzhang_yaoqingma(user.UserUID) + return UserZuzhang.query.create( + user=user, + yaoqingma=yaoqingma, + fenyong_zonge=Decimal('0.00'), + ketixian_jine=Decimal('0.00'), + yaoqing_zongshu=invite_guanshi_count, + jinri_fenyong=Decimal('0.00'), + jinyue_fenyong=Decimal('0.00'), + zhuangtai=1, + jinri_tixian=Decimal('0.00'), + ewai_tixian_xiane=Decimal('0.00'), + kaioi_ewai_tixian=False, + kaioi_ewai_fenhong=False, + ewai_fenhong_jine=Decimal('0.00'), + ) + + +class PromoteGuanshiToZuzhangView(APIView): + """ + 后台将管事用户升级为组长(写入 user_zuzhang 扩展表) + POST /houtai/htgssjzz + 权限:sjzz666 + 参数:username, yonghuid + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if 'sjzz666' not in permissions: + return Response({'code': 403, 'msg': '无权限升级组长(需要 sjzz666)'}) + + try: + user = User.query.select_related('GuanshiProfile').get(UserUID=yonghuid) + guanshi = user.GuanshiProfile + except User.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + except UserGuanshi.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是管事,无法升级组长'}) + + if UserZuzhang.query.filter(user=user).exists(): + zuzhang = user.ZuzhangProfile + return Response({ + 'code': 400, + 'msg': '该用户已是组长', + 'data': { + 'yonghuid': user.UserUID, + 'yaoqingma': zuzhang.yaoqingma, + }, + }) + + if guanshi.zhuangtai != 1: + return Response({'code': 400, 'msg': '管事账号已禁用,无法升级组长'}) + + try: + with transaction.atomic(): + zuzhang = _create_zuzhang_profile(user) + except IntegrityError: + return Response({'code': 400, 'msg': '升级失败,请稍后重试'}) + + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_GUANSHI, + qitashuoming=( + f'【后台】管事升级组长:用户ID={yonghuid},' + f'生成组长邀请码={zuzhang.yaoqingma},操作人={kefu.user.Phone}' + ), + ) + + logger.info(f"管事升级组长成功: yonghuid={yonghuid}, yaoqingma={zuzhang.yaoqingma}") + return Response({ + 'code': 0, + 'msg': '已升级为组长', + 'data': { + 'yonghuid': user.UserUID, + 'nickname': getattr(user.BossProfile, 'nickname', '') if hasattr(user, 'BossProfile') else '', + 'yaoqingma': zuzhang.yaoqingma, + 'yaoqing_zongshu': zuzhang.yaoqing_zongshu, + 'ketixian_jine': str(zuzhang.ketixian_jine), + }, + }) + + + + +class UpdateGuanliView(APIView): + """ + 修改管事信息(基础信息、余额、提现限额、分红策略) + 权限细分: + - 修改状态(封禁/解封)及昵称/电话/微信:需要4400a + - 增加余额:需要4400c + - 减少余额:需要4400b + - 修改提现限额(kaioi_ewai_xiane/ewai_xiane):需要4400e + - 修改分红相关(永久分红、定制首次、额外次数):需要4400f + POST /houtai/htxggsxx + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + + # 权限校验(先获取权限列表) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 查询管事对象(不在事务外加锁,事务内加锁) + try: + user = User.query.get(UserUID=yonghuid) + guanshi = user.GuanshiProfile + boss = user.BossProfile if hasattr(user, 'BossProfile') else None + except User.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + except UserGuanshi.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是管事'}) + + # 开始事务,内部使用 select_for_update + with transaction.atomic(): + # 重新获取加锁对象 + user = User.objects.select_for_update().get(UserUID=yonghuid) + guanshi = user.GuanshiProfile + boss = user.BossProfile if hasattr(user, 'BossProfile') else None + operator = kefu.user.Phone + + # ---------- 1. 基础信息修改(需要4400a)---------- + nickname = request.data.get('nickname') + dianhua = request.data.get('dianhua') + wechat = request.data.get('wechat') + zhuangtai = request.data.get('zhuangtai') + avatar = request.data.get('avatar') + + if any(v is not None for v in [nickname, dianhua, wechat, zhuangtai, avatar]): + if '4400a' not in permissions: + return Response({'code': 403, 'msg': '无权限修改管事基础信息(需要4400a)'}) + base_changes = [] + old_nickname = boss.nickname if boss else '' + old_dianhua = guanshi.dianhua + old_wechat = guanshi.wechat + old_zhuangtai = guanshi.zhuangtai + # 处理昵称(在老板扩展表中) + if nickname is not None: + if not boss: + boss = UserBoss.query.create(user=user, nickname=nickname) + else: + boss.nickname = nickname + boss.save() + if str(nickname) != str(old_nickname): + base_changes.append(f'昵称:{old_nickname or "无"}→{nickname}') + # 处理其他字段 + if dianhua is not None: + guanshi.dianhua = dianhua + if str(dianhua) != str(old_dianhua): + base_changes.append(f'电话:{old_dianhua or "无"}→{dianhua}') + if wechat is not None: + guanshi.wechat = wechat + if str(wechat) != str(old_wechat): + base_changes.append(f'微信:{old_wechat or "无"}→{wechat}') + if zhuangtai is not None: + try: + zt = int(zhuangtai) + if zt in (0, 1): + guanshi.zhuangtai = zt + if zt != old_zhuangtai: + zt_map = {0: '禁用', 1: '正常'} + base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}→{zt_map.get(zt, zt)}') + except: + pass + if avatar is not None: + user.Avatar = avatar + user.save() + base_changes.append('更新了头像') + guanshi.save() + if base_changes: + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=operator, + leixing=XIUGAI_LEIXING_GUANSHI, + qitashuoming=( + f'【后台】修改管事基础信息:{";".join(base_changes)},' + f'管事ID={yonghuid},操作人={operator}' + ), + ) + + # ---------- 2. 余额修改(加:4400c;减:4400b)---------- + new_yue = request.data.get('yue') + if new_yue is not None: + try: + new_yue = Decimal(str(new_yue)) + if new_yue < 0: + return Response({'code': 400, 'msg': '余额不能为负数'}) + except: + return Response({'code': 400, 'msg': '余额格式错误'}) + + old_yue = guanshi.yue + if new_yue > old_yue: + if '4400c' not in permissions: + return Response({'code': 403, 'msg': '无权限增加余额(需要4400c)'}) + action_desc = f'增加{new_yue - old_yue}元' + elif new_yue < old_yue: + if '4400b' not in permissions: + return Response({'code': 403, 'msg': '无权限减少余额(需要4400b)'}) + action_desc = f'减少{old_yue - new_yue}元' + else: + action_desc = '无变动' + if new_yue != old_yue: + guanshi.yue = new_yue + guanshi.save() + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=operator, + leixing=XIUGAI_LEIXING_GUANSHI, + guanshiyue=new_yue, + guanshiyueq=old_yue, + qitashuoming=( + f'【后台】修改管事可提现余额:{old_yue}元 → {new_yue}元({action_desc}),' + f'管事ID={yonghuid},操作人={operator}' + ), + ) + + # ---------- 3. 提现限额修改(需要4400e)---------- + kaioi_ewai = request.data.get('kaioi_ewai_xiane') + ewai_val = request.data.get('ewai_xiane') + if kaioi_ewai is not None or ewai_val is not None: + if '4400e' not in permissions: + return Response({'code': 403, 'msg': '无权限修改提现限额(需要4400e)'}) + limit_changes = [] + old_kaioi = guanshi.kaioi_ewai_xiane + old_ewai = guanshi.ewai_xiane + if kaioi_ewai is not None: + guanshi.kaioi_ewai_xiane = bool(kaioi_ewai) + if bool(kaioi_ewai) != old_kaioi: + limit_changes.append(f'额外限额开关:{"开" if old_kaioi else "关"}→{"开" if kaioi_ewai else "关"}') + if ewai_val is not None: + try: + guanshi.ewai_xiane = Decimal(str(ewai_val)) + if guanshi.ewai_xiane != old_ewai: + limit_changes.append(f'额外限额金额:{old_ewai}→{guanshi.ewai_xiane}') + except: + return Response({'code': 400, 'msg': '额外限额金额格式错误'}) + guanshi.save() + if limit_changes: + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=operator, + leixing=XIUGAI_LEIXING_GUANSHI, + qitashuoming=( + f'【后台】修改管事提现限额:{";".join(limit_changes)},' + f'管事ID={yonghuid},操作人={operator}' + ), + ) + + # ---------- 4. 分红相关修改(需要4400f)---------- + perm_enabled = request.data.get('permanent_enabled') + perm_amount = request.data.get('permanent_amount') + custom_first = request.data.get('custom_first_dividend') + extra_map = request.data.get('extra_dividend_map') + + if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]): + if '4400f' not in permissions: + return Response({'code': 403, 'msg': '无权限修改分红策略(需要4400f)'}) + + # 4.1 永久分红 + if perm_enabled is not None: + guanshi.fenghong_erci_enabled = bool(perm_enabled) + if perm_amount is not None: + try: + guanshi.fenghong_erci = Decimal(str(perm_amount)) + except: + return Response({'code': 400, 'msg': '永久分红金额格式错误'}) + guanshi.save() + + # 4.2 首次定制分红(cishu=1) + if custom_first is not None and isinstance(custom_first, dict): + from jituan.services.club_penalty import resolve_gsfenhong_club_id + guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid) + # 获取当前所有首次定制记录 + existing_first = DuociFenhong.query.filter( + club_id=guanshi_club, yonghuid=yonghuid, cishu=1, + ) + # 前端传来的定制字典 {会员ID: 金额} + for huiyuan_id, amount in custom_first.items(): + try: + amount = Decimal(str(amount)) + except: + continue + if amount <= 0: + DuociFenhong.query.filter( + club_id=guanshi_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1, + ).delete() + else: + DuociFenhong.query.update_or_create( + club_id=guanshi_club, + yonghuid=yonghuid, + huiyuan=huiyuan_id, + cishu=1, + defaults={ + 'guanshi_fenhong': amount, + 'zuzhang_fenhong': Decimal('0'), + }, + ) + # 删除那些在前端字典中不存在的首次定制记录(即已取消定制) + for record in existing_first: + if record.huiyuan not in custom_first: + record.delete() + + # 4.3 额外次数分红(cishu >= 2) + if extra_map is not None and isinstance(extra_map, dict): + from jituan.services.club_penalty import resolve_gsfenhong_club_id + guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid) + # 收集需要保留的记录 + keep_set = set() + for times_str, members in extra_map.items(): + try: + cishu = int(times_str) + if cishu < 2: + continue + except: + continue + if not isinstance(members, dict): + continue + for huiyuan_id, data in members.items(): + if not isinstance(data, dict): + continue + amount = data.get('guanshi_fenhong') + if amount is None: + continue + try: + amount = Decimal(str(amount)) + except: + continue + keep_set.add((cishu, huiyuan_id)) + # 更新或创建 + DuociFenhong.query.update_or_create( + club_id=guanshi_club, + yonghuid=yonghuid, + huiyuan=huiyuan_id, + cishu=cishu, + defaults={ + 'guanshi_fenhong': amount, + 'zuzhang_fenhong': Decimal('0') + } + ) + # 删除不在 keep_set 中的额外次数记录 + existing_extra = DuociFenhong.query.filter( + club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2, + ) + for record in existing_extra: + if (record.cishu, record.huiyuan) not in keep_set: + record.delete() + + dividend_parts = [] + if perm_enabled is not None: + dividend_parts.append(f'永久分红开关→{"开" if perm_enabled else "关"}') + if perm_amount is not None: + dividend_parts.append(f'永久分红金额→{perm_amount}') + if custom_first is not None: + dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)') + if extra_map is not None: + dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)') + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=operator, + leixing=XIUGAI_LEIXING_GUANSHI, + qitashuoming=( + f'【后台】修改管事分红策略:{";".join(dividend_parts) or "配置已更新"},' + f'管事ID={yonghuid},操作人={operator}' + ), + ) + + return Response({'code': 0, 'msg': '修改成功'}) + + + + + + + + + +class GetWithdrawSettingsView(APIView): + """ + 获取提现设置(手续费利率、每日限额、当日已提现总额) + 权限:需要 5500a 或 5500b 或 5500c 任一 + POST /houtai/hthqtxpz + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + # 需要5500a/b/c任一 + if not any(p in permissions for p in ('5500a', '5500b', '5500c')): + return Response({'code': 403, 'msg': '无权限查看提现设置'}) + + from jituan.services.withdraw_config import build_withdraw_settings_payload + return Response({'code': 0, 'data': build_withdraw_settings_payload(request)}) + + +class UpdateWithdrawSettingsView(APIView): + """ + 修改提现设置(权限细分): + - 修改打手相关(利率、个人限额、总限额)需要5500a + - 修改管事相关需要5500b + - 修改组长相关需要5500c + 注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算), + 后端不直接修改 WithdrawalDailyStats,该表由提现流程自动累加。 + POST /houtai/htxgtxsz + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 权限映射:要修改的角色需要对应的权限 + # 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3 + from jituan.services.withdraw_config import apply_withdraw_settings_update + return apply_withdraw_settings_update(request, permissions) + + + +class GetZuzhangListView(APIView): + """ + 组长列表接口(分页 + 多条件筛选) + 路由:POST /houtai/hthqzzlb + 权限:需要登录,且拥有 6600a~6600e 任一权限 + """ + permission_classes = [IsAuthenticated] + + @transaction.atomic # 保证查询的一致性(可选,对只读查询影响不大但无坏处) + def post(self, request): + # 1. 获取前端传递的 username(用于防越权) + frontend_username = request.data.get('username', '') + + # 2. 调用公共校验方法,验证身份并获取权限列表 + result = verify_kefu_permission(request, username_from_frontend=frontend_username) + + # 重要:verify_kefu_permission 返回格式为 (user_obj, permissions_list) 或 (None, Response) + # 如果第二个元素是 Response 对象,说明校验失败,直接返回该 Response + if isinstance(result, tuple) and len(result) == 2: + user_obj, second = result + if isinstance(second, Response): + return second # 校验失败,直接返回错误响应 + else: + permissions = second # 正常情况:permissions 是列表 + else: + # 防止意外返回格式,视为失败 + return Response({'code': 500, 'msg': '权限校验模块异常'}) + + # 3. 检查是否拥有组长管理所需权限(6600a ~ 6600e 任一) + required_perms = ['6600a', '6600b', '6600c', '6600d', '6600e'] + if not any(perm in permissions for perm in required_perms): + logger.warning(f"用户 {request.user.Phone} 尝试访问组长列表但缺少必要权限,已有权限: {permissions}") + return Response({'code': 403, 'msg': '您无权访问组长管理页面'}) + + # 4. 提取请求参数(分页 + 筛选) + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + except ValueError: + page, page_size = 1, 20 + + keyword = request.data.get('keyword', '').strip() + status = request.data.get('status') # 1=正常, 0=禁用 + invite_count_op = request.data.get('invite_count_op') # gte / lte + invite_count_value = request.data.get('invite_count_value') + amount_op = request.data.get('amount_op') + amount_value = request.data.get('amount_value') + commission_op = request.data.get('commission_op') + commission_value = request.data.get('commission_value') + + # 5. 构建基础 QuerySet(预加载关联表) + queryset = filter_user_related_by_club( + UserZuzhang.query.select_related('user', 'user__BossProfile').all(), + request, + ) + + # 6. 应用筛选条件(使用 ORM 安全过滤) + if keyword: + queryset = queryset.filter( + Q(user__UserUID__icontains=keyword) | + Q(user__BossProfile__nickname__icontains=keyword) + ) + + if status is not None and status != '': + try: + queryset = queryset.filter(zhuangtai=int(status)) + except ValueError: + pass + + # 邀请总数筛选 + if invite_count_value not in (None, ''): + try: + val = float(invite_count_value) + op = invite_count_op if invite_count_op in ('gte', 'lte') else 'gte' + queryset = queryset.filter(**{f'yaoqing_zongshu__{op}': val}) + except (ValueError, TypeError): + pass + + # 可提现金额筛选 + if amount_value not in (None, ''): + try: + val = float(amount_value) + op = amount_op if amount_op in ('gte', 'lte') else 'gte' + queryset = queryset.filter(**{f'ketixian_jine__{op}': val}) + except (ValueError, TypeError): + pass + + # 分佣总额筛选 + if commission_value not in (None, ''): + try: + val = float(commission_value) + op = commission_op if commission_op in ('gte', 'lte') else 'gte' + queryset = queryset.filter(**{f'fenyong_zonge__{op}': val}) + except (ValueError, TypeError): + pass + + # 7. 总数统计 + total = queryset.count() + + # 8. 分页 + offset = (page - 1) * page_size + paged_queryset = queryset[offset:offset + page_size] + + # 9. 构造返回数据 + data_list = [] + for zu in paged_queryset: + user = zu.user + nickname = '' + if hasattr(user, 'BossProfile') and user.BossProfile: + nickname = user.BossProfile.nickname or '' + + data_list.append({ + 'yonghuid': user.UserUID, + 'avatar': user.Avatar or '', + 'nickname': nickname, + 'fenyong_zonge': float(zu.fenyong_zonge), + 'ketixian_jine': float(zu.ketixian_jine), + 'yaoqing_zongshu': zu.yaoqing_zongshu, + 'zhuangtai': zu.zhuangtai, + }) + + return Response({ + 'code': 0, + 'msg': 'success', + **list_response_meta(request), + 'data': { + 'list': data_list, + 'total': total, + } + }) + + + + + + + + + + +class GetZuzhangDetailView(APIView): + """ + 获取组长详情 + 权限:6600a,6600b,6600c,6600d,6600e 任一 + POST /houtai/hthqzzxq + 参数:username, yonghuid + 返回: + code:0, data: { + base: {...}, + balance: {...}, + permanent_enabled, permanent_amount, + custom_first_dividend: {}, + extra_dividend_map: {}, + all_members: [] + } + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + + # 权限校验(需要任一权限) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + required_perms = {'6600a', '6600b', '6600c', '6600d', '6600e'} + if not any(p in permissions for p in required_perms): + return Response({'code': 403, 'msg': '您没有权限查看组长详情'}) + + # 查询组长主表、扩展表、老板扩展表(昵称) + try: + user = User.query.select_related('ZuzhangProfile', 'BossProfile').get(UserUID=yonghuid) + zuzhang = user.ZuzhangProfile + boss = user.BossProfile if hasattr(user, 'BossProfile') else None + except User.DoesNotExist: + return Response({'code': 404, 'msg': '组长不存在'}) + except UserZuzhang.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是组长'}) + + denied = forbid_if_user_out_of_scope(request, user) + if denied: + return denied + + # 基础信息 + base_data = { + 'yonghuid': user.UserUID, + 'nickname': boss.nickname if boss else '', + 'avatar': user.Avatar or '', + 'dianhua': '', # 组长表无电话字段?组长扩展表没有电话/微信,但前端可能需要,可从User关联?实际上组长表无电话字段,这里留空或从其他表获取 + 'wechat': '', + 'zhuangtai': zuzhang.zhuangtai, + 'yaoqingma': zuzhang.yaoqingma, + 'yaoqingren': '', # 组长表没有邀请人字段 + 'haibao_url': zuzhang.haibao_url or '', + 'yaoqing_zongshu': zuzhang.yaoqing_zongshu, + 'jinri_fenyong': str(zuzhang.jinri_fenyong), + 'jinyue_fenyong': str(zuzhang.jinyue_fenyong), + 'fenyong_zonge': str(zuzhang.fenyong_zonge), + 'CreateTime': zuzhang.CreateTime.isoformat() if zuzhang.CreateTime else None, + } + + # 余额提现相关 + balance_data = { + 'ketixian_jine': str(zuzhang.ketixian_jine), + 'jinri_tixian': str(zuzhang.jinri_tixian), + 'last_tixian_time': zuzhang.last_tixian_time.isoformat() if zuzhang.last_tixian_time else None, + 'kaioi_ewai_tixian': zuzhang.kaioi_ewai_tixian, + 'ewai_tixian_xiane': str(zuzhang.ewai_tixian_xiane), + } + + # 永久分红(额外分红开关和金额) + permanent_enabled = zuzhang.kaioi_ewai_fenhong + permanent_amount = str(zuzhang.ewai_fenhong_jine) if zuzhang.ewai_fenhong_jine else '0' + + # 获取所有会员(用于前端选择) + all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc')) + for m in all_members: + m['jiage'] = str(m['jiage']) + m['zuzhangfc'] = str(m['zuzhangfc']) + + # 获取多次分红配置(按次数和会员组织) + duoci_records = DuociFenhong.query.filter(yonghuid=yonghuid).order_by('cishu') + custom_first = {} # 首次分红定制(cishu=1) + extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } } + + for record in duoci_records: + # 获取会员信息 + member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first() + if not member: + continue + member_info = { + 'jieshao': member.jieshao, + 'jiage': str(member.jiage), + } + if record.cishu == 1: + custom_first[record.huiyuan] = float(record.zuzhang_fenhong) + else: + times = record.cishu + if times not in extra_map: + extra_map[times] = {} + extra_map[times][record.huiyuan] = { + 'zuzhang_fenhong': float(record.zuzhang_fenhong), + **member_info + } + + return Response({ + 'code': 0, + 'data': { + 'base': base_data, + 'balance': balance_data, + 'permanent_enabled': permanent_enabled, + 'permanent_amount': permanent_amount, + 'custom_first_dividend': custom_first, + 'extra_dividend_map': extra_map, + 'all_members': all_members, + } + }) + + + + +class UpdateZuzhangView(APIView): + """ + 修改组长信息(基础信息、余额、提现限额、分红策略) + 权限细分: + - 增加可提现金额:6600a + - 减少可提现金额:6600b + - 修改状态(封禁/解封):6600c + - 修改提现限额(kaioi_ewai_tixian/ewai_tixian_xiane):6600d + - 修改分红相关(永久分红、定制首次、额外次数):6600e + POST /houtai/htxgzzxx + 参数: + username, yonghuid, + nickname, avatar, dianhua, wechat, zhuangtai, # 基础 + ketixian_jine, kaioi_ewai_tixian, ewai_tixian_xiane, # 余额提现 + permanent_enabled, permanent_amount, # 永久分红 + custom_first_dividend: {}, # 首次定制分红 {会员ID: 金额} + extra_dividend_map: {} # 额外次数分红 {次数: {会员ID: {zuzhang_fenhong}}} + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 查询组长对象 + try: + user = User.query.get(UserUID=yonghuid) + zuzhang = user.ZuzhangProfile + boss = user.BossProfile if hasattr(user, 'BossProfile') else None + except User.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + except UserZuzhang.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是组长'}) + + # 获取原始数据(用于对比金额变化) + old_ketixian_jine = zuzhang.ketixian_jine + + # 开始事务 + with transaction.atomic(): + operator = kefu.user.Phone + old_nickname = boss.nickname if boss else '' + old_zhuangtai = zuzhang.zhuangtai + # ---------- 1. 基础信息修改(需要6600c?按需求,基础信息修改通常用6600c,但为了安全,暂不校验,如有需要可加)---------- + nickname = request.data.get('nickname', '').strip() + avatar = request.data.get('avatar', '').strip() + dianhua = request.data.get('dianhua', '').strip() + wechat = request.data.get('wechat', '').strip() + zhuangtai = request.data.get('zhuangtai') + base_changes = [] + # 状态修改需要6600c + if zhuangtai is not None: + if '6600c' not in permissions: + return Response({'code': 403, 'msg': '无权限修改组长状态(需要6600c)'}) + try: + zt = int(zhuangtai) + if zt in (0, 1): + zuzhang.zhuangtai = zt + if zt != old_zhuangtai: + zt_map = {0: '禁用', 1: '正常'} + base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}→{zt_map.get(zt, zt)}') + except: + pass + if nickname: + if not boss: + boss = UserBoss.query.create(user=user, nickname=nickname) + else: + boss.nickname = nickname + boss.save() + if nickname != old_nickname: + base_changes.append(f'昵称:{old_nickname or "无"}→{nickname}') + if avatar: + user.Avatar = avatar + user.save() + base_changes.append('更新了头像') + if base_changes: + zuzhang.save() + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=operator, + leixing=XIUGAI_LEIXING_ZUZHANG, + qitashuoming=( + f'【后台】修改组长基础信息:{";".join(base_changes)},' + f'组长ID={yonghuid},操作人={operator}' + ), + ) + # 组长表没有电话/微信字段,如需存储可暂存到User或忽略 + # 这里不处理 + + # ---------- 2. 可提现金额修改(加:6600a;减:6600b)---------- + new_ketixian_jine = request.data.get('ketixian_jine') + if new_ketixian_jine is not None: + try: + new_val = Decimal(str(new_ketixian_jine)) + if new_val < 0: + return Response({'code': 400, 'msg': '可提现金额不能为负数'}) + except: + return Response({'code': 400, 'msg': '金额格式错误'}) + if new_val > old_ketixian_jine: + if '6600a' not in permissions: + return Response({'code': 403, 'msg': '无权限增加可提现金额(需要6600a)'}) + action_desc = f'增加{new_val - old_ketixian_jine}元' + elif new_val < old_ketixian_jine: + if '6600b' not in permissions: + return Response({'code': 403, 'msg': '无权限减少可提现金额(需要6600b)'}) + action_desc = f'减少{old_ketixian_jine - new_val}元' + else: + action_desc = '无变动' + if new_val != old_ketixian_jine: + zuzhang.ketixian_jine = new_val + zuzhang.save(update_fields=['ketixian_jine']) + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=operator, + leixing=XIUGAI_LEIXING_ZUZHANG, + xiugaitijiao=new_val, + xiugaitijiaoq=old_ketixian_jine, + qitashuoming=( + f'【后台】修改组长可提现金额:{old_ketixian_jine}元 → {new_val}元({action_desc}),' + f'组长ID={yonghuid},操作人={operator}' + ), + ) + + # ---------- 3. 提现限额修改(需要6600d)---------- + kaioi_ewai = request.data.get('kaioi_ewai_tixian') + ewai_val = request.data.get('ewai_tixian_xiane') + if kaioi_ewai is not None or ewai_val is not None: + if '6600d' not in permissions: + return Response({'code': 403, 'msg': '无权限修改提现限额(需要6600d)'}) + limit_changes = [] + old_kaioi = zuzhang.kaioi_ewai_tixian + old_ewai = zuzhang.ewai_tixian_xiane + if kaioi_ewai is not None: + zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai) + if bool(kaioi_ewai) != old_kaioi: + limit_changes.append(f'额外限额开关:{"开" if old_kaioi else "关"}→{"开" if kaioi_ewai else "关"}') + if ewai_val is not None: + try: + zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val)) + if zuzhang.ewai_tixian_xiane != old_ewai: + limit_changes.append(f'额外限额金额:{old_ewai}→{zuzhang.ewai_tixian_xiane}') + except: + return Response({'code': 400, 'msg': '额外限额金额格式错误'}) + zuzhang.save() + if limit_changes: + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=operator, + leixing=XIUGAI_LEIXING_ZUZHANG, + qitashuoming=( + f'【后台】修改组长提现限额:{";".join(limit_changes)},' + f'组长ID={yonghuid},操作人={operator}' + ), + ) + + # ---------- 4. 分红相关修改(需要6600e)---------- + perm_enabled = request.data.get('permanent_enabled') + perm_amount = request.data.get('permanent_amount') + custom_first = request.data.get('custom_first_dividend') + extra_map = request.data.get('extra_dividend_map') + + if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]): + if '6600e' not in permissions: + return Response({'code': 403, 'msg': '无权限修改分红策略(需要6600e)'}) + + # 4.1 永久分红(额外分红开关和金额) + if perm_enabled is not None: + zuzhang.kaioi_ewai_fenhong = bool(perm_enabled) + if perm_amount is not None: + try: + zuzhang.ewai_fenhong_jine = Decimal(str(perm_amount)) + except: + return Response({'code': 400, 'msg': '永久分红金额格式错误'}) + zuzhang.save() + + # 4.2 首次定制分红(cishu=1) + if custom_first is not None and isinstance(custom_first, dict): + from jituan.services.club_penalty import resolve_gsfenhong_club_id + zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid) + existing_first = DuociFenhong.query.filter( + club_id=zuzhang_club, yonghuid=yonghuid, cishu=1, + ) + for huiyuan_id, amount in custom_first.items(): + try: + amount = Decimal(str(amount)) + except: + continue + if amount <= 0: + # 删除定制(恢复默认) + DuociFenhong.query.filter( + club_id=zuzhang_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1, + ).delete() + else: + DuociFenhong.query.update_or_create( + club_id=zuzhang_club, + yonghuid=yonghuid, + huiyuan=huiyuan_id, + cishu=1, + defaults={ + 'zuzhang_fenhong': amount, + 'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红 + } + ) + # 删除那些在前端字典中不存在的首次定制记录(即已取消定制) + for record in existing_first: + if record.huiyuan not in custom_first: + record.delete() + + # 4.3 额外次数分红(cishu >= 2) + if extra_map is not None and isinstance(extra_map, dict): + from jituan.services.club_penalty import resolve_gsfenhong_club_id + zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid) + keep_set = set() + for times_str, members in extra_map.items(): + try: + cishu = int(times_str) + if cishu < 2: + continue + except: + continue + if not isinstance(members, dict): + continue + for huiyuan_id, data in members.items(): + if not isinstance(data, dict): + continue + amount = data.get('zuzhang_fenhong') + if amount is None: + continue + try: + amount = Decimal(str(amount)) + except: + continue + keep_set.add((cishu, huiyuan_id)) + DuociFenhong.query.update_or_create( + club_id=zuzhang_club, + yonghuid=yonghuid, + huiyuan=huiyuan_id, + cishu=cishu, + defaults={ + 'zuzhang_fenhong': amount, + 'guanshi_fenhong': Decimal('0') + } + ) + # 删除不在 keep_set 中的额外次数记录 + existing_extra = DuociFenhong.query.filter( + club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2, + ) + for record in existing_extra: + if (record.cishu, record.huiyuan) not in keep_set: + record.delete() + + dividend_parts = [] + if perm_enabled is not None: + dividend_parts.append(f'永久分红开关→{"开" if perm_enabled else "关"}') + if perm_amount is not None: + dividend_parts.append(f'永久分红金额→{perm_amount}') + if custom_first is not None: + dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)') + if extra_map is not None: + dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)') + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=operator, + leixing=XIUGAI_LEIXING_ZUZHANG, + qitashuoming=( + f'【后台】修改组长分红策略:{";".join(dividend_parts) or "配置已更新"},' + f'组长ID={yonghuid},操作人={operator}' + ), + ) + + # 保存组长其他可能修改的字段(例如电话、微信没有,忽略) + + return Response({'code': 0, 'msg': '修改成功'}) + + +class ChangeInviterView(APIView): + """ + 后台更换邀请人/上级 + POST /houtai/htghyqr + 参数: + username: 操作人账号 + yonghuid: 被修改用户ID + target_type: dashou(打手)| guanshi(管事) + new_yaoqingren: 新邀请人用户ID(须在 User 主表存在) + 权限: + dashou → 001ee + guanshi → 4400a + 校验: + 打手新上级须为管事;管事新上级须为组长;不可设为自己 + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = request.data.get('yonghuid', '').strip() + target_type = request.data.get('target_type', '').strip().lower() + new_yaoqingren = request.data.get('new_yaoqingren', '').strip() + + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '缺少用户ID'}) + if target_type not in ('dashou', 'guanshi'): + return Response({'code': 400, 'msg': 'target_type 须为 dashou 或 guanshi'}) + if not new_yaoqingren: + return Response({'code': 400, 'msg': '缺少新邀请人ID'}) + if new_yaoqingren == yonghuid: + return Response({'code': 400, 'msg': '不能将自己设为邀请人'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + if target_type == 'dashou': + if '001ee' not in permissions: + return Response({'code': 403, 'msg': '无权限更换打手邀请人(需要001ee)'}) + else: + if '4400a' not in permissions: + return Response({'code': 403, 'msg': '无权限更换管事邀请人(需要4400a)'}) + + try: + target_user = User.query.get(UserUID=yonghuid) + except User.DoesNotExist: + return Response({'code': 404, 'msg': '被修改用户不存在'}) + + try: + inviter_user = User.query.get(UserUID=new_yaoqingren) + except User.DoesNotExist: + return Response({'code': 404, 'msg': '新邀请人用户ID不存在'}) + + operator = kefu.user.Phone + + with transaction.atomic(): + if target_type == 'dashou': + try: + dashou = target_user.DashouProfile + except UserDashou.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是打手'}) + try: + guanshi_profile = inviter_user.GuanshiProfile + except UserGuanshi.DoesNotExist: + return Response({'code': 400, 'msg': '新邀请人不是管事,无法作为打手上级'}) + if guanshi_profile.zhuangtai != 1: + return Response({'code': 400, 'msg': '新邀请人管事账号已禁用'}) + old_inviter = dashou.yaoqingren or '无' + dashou.yaoqingren = new_yaoqingren + dashou.save(update_fields=['yaoqingren']) + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=operator, + leixing=XIUGAI_LEIXING_DASHOU, + qitashuoming=( + f'【后台】更换打手邀请人:{old_inviter} → {new_yaoqingren},' + f'打手ID={yonghuid},新上级须为管事(已校验),操作人={operator}' + ), + ) + else: + try: + guanshi = target_user.GuanshiProfile + except UserGuanshi.DoesNotExist: + return Response({'code': 404, 'msg': '用户不是管事'}) + try: + zuzhang_profile = inviter_user.ZuzhangProfile + except UserZuzhang.DoesNotExist: + return Response({'code': 400, 'msg': '新邀请人不是组长,无法作为管事上级'}) + if zuzhang_profile.zhuangtai != 1: + return Response({'code': 400, 'msg': '新邀请人组长账号已禁用'}) + old_inviter = guanshi.yaoqingren or '无' + guanshi.yaoqingren = new_yaoqingren + guanshi.save(update_fields=['yaoqingren']) + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=operator, + leixing=XIUGAI_LEIXING_GUANSHI, + qitashuoming=( + f'【后台】更换管事邀请人:{old_inviter} → {new_yaoqingren},' + f'管事ID={yonghuid},新上级须为组长(已校验),操作人={operator}' + ), + ) + + return Response({ + 'code': 0, + 'msg': '邀请人更换成功', + 'data': {'yonghuid': yonghuid, 'new_yaoqingren': new_yaoqingren}, + }) + + +class GetOperationLogListView(APIView): + """ + 后台操作日志列表(xiugaijilu) + POST /houtai/hqczrz + 权限:000001 超级权限 或 czrz666 + """ + permission_classes = [] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions and 'czrz666' not in permissions: + return Response({'code': 403, 'msg': '无权限查看操作日志(需要000001或czrz666)'}) + + yonghuid = request.data.get('yonghuid', '').strip() + xiugaiid = request.data.get('xiugaiid', '').strip() + leixing = request.data.get('leixing') + keyword = request.data.get('keyword', '').strip() + try: + page = max(1, int(request.data.get('page', 1))) + except (ValueError, TypeError): + page = 1 + try: + page_size = min(100, max(1, int(request.data.get('page_size', 20)))) + except (ValueError, TypeError): + page_size = 20 + + qs = Xiugaijilu.query.all().order_by('-CreateTime') + from jituan.services.admin_audit import filter_xiugaijilu_by_request + qs = filter_xiugaijilu_by_request(qs, request) + if yonghuid: + qs = qs.filter(yonghuid=yonghuid) + if xiugaiid: + qs = qs.filter(xiugaiid__icontains=xiugaiid) + if leixing not in (None, ''): + try: + qs = qs.filter(leixing=int(leixing)) + except (ValueError, TypeError): + pass + if keyword: + qs = qs.filter(qitashuoming__icontains=keyword) + + total = qs.count() + start = (page - 1) * page_size + records = qs[start:start + page_size] + + def _fmt_decimal(val): + if val is None: + return None + return str(val) + + data_list = [] + for r in records: + data_list.append({ + 'id': r.id, + 'yonghuid': r.yonghuid, + 'xiugaiid': r.xiugaiid, + 'leixing': r.leixing, + 'leixing_label': XIUGAI_LEIXING_LABEL.get(r.leixing, f'类型{r.leixing}'), + 'xiugaitijiao': _fmt_decimal(r.xiugaitijiao), + 'xiugaitijiaoq': _fmt_decimal(r.xiugaitijiaoq), + 'jifen': r.jifen, + 'yjifen': r.yjifen, + 'yajin': _fmt_decimal(r.yajin), + 'yyajin': _fmt_decimal(r.yyajin), + 'shangjiayue': _fmt_decimal(r.shangjiayue), + 'shangjiayueq': _fmt_decimal(r.shangjiayueq), + 'guanshiyue': _fmt_decimal(r.guanshiyue), + 'guanshiyueq': _fmt_decimal(r.guanshiyueq), + 'huiyuants': r.huiyuants, + 'huiyuan_id': r.huiyuan_id, + 'qitashuoming': r.qitashuoming or '', + 'create_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '', + }) + + from jituan.services.club_user_access import list_response_meta + return Response({ + 'code': 0, + 'data': { + 'list': data_list, + 'total': total, + 'page': page, + 'page_size': page_size, + **list_response_meta(request), + }, + }) + + +class GetProductTypeZoneView(APIView): + """ + 获取商品类型、专区及会员列表 + 权限:需要拥有 7007a + 请求:POST /houtai/hthqsplxzq + 参数:{"username": "客服手机号"} + 返回: + code: 0 + data: { + type_list: [{id, jieshao, tupian_url, yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, paixu, product_count}, ...], + zone_list: [{id, mingzi, leixing_id, shenhezhuangtai, paixu}, ...], + member_list: [{huiyuan_id, jieshao, jiage}, ...] + } + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '7007a' not in permissions: + return Response({'code': 403, 'msg': '您无权访问此页面'}) + + # 查询所有商品类型(含专区数量) + types = ShangpinLeixing.query.all().order_by('-paixu') # paixu 越大越靠前 + type_list = [] + for t in types: + # 统计该类型下的专区数量 + zone_count = ShangpinZhuanqu.query.filter(leixing_id=t.id).count() + type_list.append({ + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'yaoqiuleixing': t.yaoqiuleixing, + 'huiyuan_id': t.huiyuan_id or '', + 'yongjin': float(t.yongjin) if t.yongjin is not None else None, + 'shenhezhuangtai': t.shenhezhuangtai, + 'paixu': t.paixu, + 'product_count': zone_count, + }) + + # 查询所有专区 + zones = ShangpinZhuanqu.query.all().order_by('-paixu') + zone_list = [ + { + 'id': z.id, + 'mingzi': z.mingzi or '', + 'leixing_id': z.leixing_id, + 'shenhezhuangtai': z.shenhezhuangtai, + 'paixu': z.paixu, + } + for z in zones + ] + + # 查询所有会员(只返回 id、介绍、价格) + members = Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage') + member_list = [ + { + 'huiyuan_id': m['huiyuan_id'], + 'jieshao': m['jieshao'], + 'jiage': float(m['jiage']), + } + for m in members + ] + + return Response({ + 'code': 0, + 'data': { + 'type_list': type_list, + 'zone_list': zone_list, + 'member_list': member_list, + } + }) + + + + +class ModifyProductTypeZoneView(APIView): + """ + 统一修改商品类型/专区(增、删、改) + 权限:需要拥有 7007a + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser, MultiPartParser, FormParser] + + def post(self, request): + username = request.data.get('username', '').strip() + obj_type = request.data.get('type') # 'leixing' 或 'zhuanqu' + action = request.data.get('action') # 'add', 'update', 'delete' + + if not username or not obj_type or not action: + return Response({'code': 400, 'msg': '缺少必要参数'}) + + # 权限校验 + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '7007a' not in permissions: + return Response({'code': 403, 'msg': '您无权进行此操作'}) + + if obj_type == 'leixing': + return self.handle_leixing(request, action) + elif obj_type == 'zhuanqu': + return self.handle_zhuanqu(request, action) + else: + return Response({'code': 400, 'msg': '无效的type参数'}) + + # ---------- 商品类型处理 ---------- + def handle_leixing(self, request, action): + if action == 'add': + return self.add_leixing(request) + elif action == 'update': + return self.update_leixing(request) + elif action == 'delete': + return self.delete_leixing(request) + else: + return Response({'code': 400, 'msg': '无效的action'}) + + def add_leixing(self, request): + # 获取并校验字段 + jieshao = request.data.get('jieshao', '').strip() + if not jieshao: + return Response({'code': 400, 'msg': '类型名称不能为空'}) + + # 强制转为整数 + try: + yaoqiuleixing = int(request.data.get('yaoqiuleixing', 0)) + if yaoqiuleixing not in [1, 2]: + raise ValueError + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'}) + + try: + shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) + if shenhezhuangtai not in [1, 2]: + raise ValueError + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) + + huiyuan_id = request.data.get('huiyuan_id', '').strip() or None + yongjin = request.data.get('yongjin') + if yongjin is not None and yongjin != '': + try: + yongjin = Decimal(str(yongjin)) + except: + return Response({'code': 400, 'msg': '佣金金额格式错误'}) + else: + yongjin = None + + # 图片校验 + image_file = request.FILES.get('file') + if not image_file: + return Response({'code': 400, 'msg': '请上传类型图片'}) + + # 验证图片 + try: + valid, msg = validate_image(image_file) + if not valid: + return Response({'code': 400, 'msg': msg}) + except ImportError: + pass + + # 上传图片 + ext = image_file.name.split('.')[-1].lower() + new_filename = f"{uuid.uuid4().hex}.{ext}" + oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" + url = upload_to_oss(image_file, oss_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + + # 创建类型 + try: + with transaction.atomic(): + leixing = ShangpinLeixing.query.create( + jieshao=jieshao, + tupian_url=oss_path, + yaoqiuleixing=yaoqiuleixing, + huiyuan_id=huiyuan_id, + yongjin=yongjin, + shenhezhuangtai=shenhezhuangtai, + paixu=0 + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': leixing.id}}) + except Exception as e: + logger.error(f"添加类型失败: {e}", exc_info=True) + delete_from_oss(oss_path) + return Response({'code': 500, 'msg': '数据库错误'}) + + def update_leixing(self, request): + type_id = request.data.get('id') + if not type_id: + return Response({'code': 400, 'msg': '缺少类型ID'}) + try: + leixing = ShangpinLeixing.query.get(id=type_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 404, 'msg': '类型不存在'}) + + changes = {} + # 名称 + jieshao = request.data.get('jieshao') + if jieshao is not None and jieshao.strip() != leixing.jieshao: + changes['jieshao'] = jieshao.strip() + + # 抢单要求类型(强制转整数) + yaoqiuleixing = request.data.get('yaoqiuleixing') + if yaoqiuleixing is not None: + try: + new_val = int(yaoqiuleixing) + if new_val not in [1, 2]: + raise ValueError + if new_val != leixing.yaoqiuleixing: + changes['yaoqiuleixing'] = new_val + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'}) + + # 会员ID + huiyuan_id = request.data.get('huiyuan_id', '').strip() or None + if huiyuan_id != leixing.huiyuan_id: + changes['huiyuan_id'] = huiyuan_id + + # 佣金金额 + yongjin = request.data.get('yongjin') + if yongjin is not None: + if yongjin == '': + new_val = None + else: + try: + new_val = Decimal(str(yongjin)) + except: + return Response({'code': 400, 'msg': '佣金金额格式错误'}) + if new_val != leixing.yongjin: + changes['yongjin'] = new_val + + # 审核状态 + shenhezhuangtai = request.data.get('shenhezhuangtai') + if shenhezhuangtai is not None: + try: + new_val = int(shenhezhuangtai) + if new_val not in [1, 2]: + raise ValueError + if new_val != leixing.shenhezhuangtai: + changes['shenhezhuangtai'] = new_val + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) + + # 图片处理 + image_file = request.FILES.get('file') + old_image_path = leixing.tupian_url + if image_file: + try: + valid, msg = validate_image(image_file) + if not valid: + return Response({'code': 400, 'msg': msg}) + except ImportError: + pass + ext = image_file.name.split('.')[-1].lower() + new_filename = f"{uuid.uuid4().hex}.{ext}" + oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}" + url = upload_to_oss(image_file, oss_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + changes['tupian_url'] = oss_path + + if not changes and not image_file: + return Response({'code': 400, 'msg': '未做任何修改'}) + + sync_to_products = request.data.get('sync_to_products') in [True, 'true', '1', 'on'] + try: + with transaction.atomic(): + for field, value in changes.items(): + setattr(leixing, field, value) + leixing.save() + + # 同步商品 + if sync_to_products and ('yaoqiuleixing' in changes or 'huiyuan_id' in changes or 'yongjin' in changes): + product_updates = {} + if 'yaoqiuleixing' in changes: + product_updates['yaoqiuleixing'] = changes['yaoqiuleixing'] + if 'huiyuan_id' in changes: + product_updates['huiyuan_id'] = changes['huiyuan_id'] + if 'yongjin' in changes: + product_updates['yongjin'] = changes['yongjin'] + if product_updates: + Shangpin.query.filter(leixing_id=type_id).update(**product_updates) + + # 删除旧图片 + if image_file and old_image_path: + delete_from_oss(old_image_path) + + return Response({'code': 0, 'msg': '修改成功'}) + except Exception as e: + logger.error(f"更新类型失败: {e}", exc_info=True) + if image_file and 'oss_path' in locals(): + delete_from_oss(oss_path) + return Response({'code': 500, 'msg': '修改失败'}) + + def delete_leixing(self, request): + type_id = request.data.get('id') + if not type_id: + return Response({'code': 400, 'msg': '缺少类型ID'}) + delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on'] + + try: + leixing = ShangpinLeixing.query.get(id=type_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 404, 'msg': '类型不存在'}) + + with transaction.atomic(): + # 删除该类型下的所有专区 + zones = ShangpinZhuanqu.query.filter(leixing_id=type_id) + if delete_products: + # 删除专区下的所有商品 + for zone in zones: + Shangpin.query.filter(zhuanqu_id=zone.id).delete() + zones.delete() + # 删除直接关联该类型的商品 + Shangpin.query.filter(leixing_id=type_id).delete() + else: + zones.delete() + # 仅清除商品的类型关联 + Shangpin.query.filter(leixing_id=type_id).update(leixing_id=None) + + leixing.delete() + + if leixing.tupian_url: + delete_from_oss(leixing.tupian_url) + + return Response({'code': 0, 'msg': '删除成功'}) + + # ---------- 商品专区处理 ---------- + def handle_zhuanqu(self, request, action): + if action == 'add': + return self.add_zhuanqu(request) + elif action == 'update': + return self.update_zhuanqu(request) + elif action == 'delete': + return self.delete_zhuanqu(request) + else: + return Response({'code': 400, 'msg': '无效的action'}) + + def add_zhuanqu(self, request): + mingzi = request.data.get('mingzi', '').strip() + leixing_id = request.data.get('leixing_id') + if not mingzi: + return Response({'code': 400, 'msg': '专区名称不能为空'}) + if not leixing_id: + return Response({'code': 400, 'msg': '请选择所属类型'}) + + try: + leixing_id = int(leixing_id) + if not ShangpinLeixing.query.filter(id=leixing_id).exists(): + return Response({'code': 404, 'msg': '所属类型不存在'}) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '所属类型ID无效'}) + + try: + shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1)) + if shenhezhuangtai not in [1, 2]: + raise ValueError + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) + + try: + with transaction.atomic(): + zhuanqu = ShangpinZhuanqu.query.create( + mingzi=mingzi, + leixing_id=leixing_id, + shenhezhuangtai=shenhezhuangtai, + paixu=0 + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': zhuanqu.id}}) + except Exception as e: + logger.error(f"添加专区失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '数据库错误'}) + + def update_zhuanqu(self, request): + zone_id = request.data.get('id') + if not zone_id: + return Response({'code': 400, 'msg': '缺少专区ID'}) + try: + zhuanqu = ShangpinZhuanqu.query.get(id=zone_id) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 404, 'msg': '专区不存在'}) + + changes = {} + mingzi = request.data.get('mingzi') + if mingzi is not None and mingzi.strip() != zhuanqu.mingzi: + changes['mingzi'] = mingzi.strip() + + leixing_id = request.data.get('leixing_id') + if leixing_id is not None: + try: + new_id = int(leixing_id) + if not ShangpinLeixing.query.filter(id=new_id).exists(): + return Response({'code': 404, 'msg': '新所属类型不存在'}) + if new_id != zhuanqu.leixing_id: + changes['leixing_id'] = new_id + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '所属类型ID无效'}) + + shenhezhuangtai = request.data.get('shenhezhuangtai') + if shenhezhuangtai is not None: + try: + new_val = int(shenhezhuangtai) + if new_val not in [1, 2]: + raise ValueError + if new_val != zhuanqu.shenhezhuangtai: + changes['shenhezhuangtai'] = new_val + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'}) + + if not changes: + return Response({'code': 400, 'msg': '未做任何修改'}) + + try: + with transaction.atomic(): + for field, value in changes.items(): + setattr(zhuanqu, field, value) + zhuanqu.save() + return Response({'code': 0, 'msg': '修改成功'}) + except Exception as e: + logger.error(f"更新专区失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '修改失败'}) + + def delete_zhuanqu(self, request): + zone_id = request.data.get('id') + if not zone_id: + return Response({'code': 400, 'msg': '缺少专区ID'}) + delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on'] + + try: + zhuanqu = ShangpinZhuanqu.query.get(id=zone_id) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 404, 'msg': '专区不存在'}) + + with transaction.atomic(): + if delete_products: + Shangpin.query.filter(zhuanqu_id=zone_id).delete() + else: + Shangpin.query.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None) + zhuanqu.delete() + + return Response({'code': 0, 'msg': '删除成功'}) + + +# 继续在 houtai/views_product.py 中添加 +class GetRateView(APIView): + """ + 获取打手分成费率(平台订单和商家订单) + 权限:需要拥有 7007b + 请求:POST /houtai/hthqfhpz + 参数:{"username": "客服手机号"} + 返回:{"code":0, "data": {"platform_rate": xx, "merchant_rate": xx}} + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '7007b' not in permissions: + return Response({'code': 403, 'msg': '您无权查看费率'}) + + try: + platform_rate_obj = CommissionRate.query.get(Platform='1') + merchant_rate_obj = CommissionRate.query.get(Platform='3') + except CommissionRate.DoesNotExist as e: + logger.error(f"费率记录缺失: {e}") + return Response({'code': 500, 'msg': '费率配置缺失'}) + + return Response({ + 'code': 0, + 'data': { + 'platform_rate': float(platform_rate_obj.Rate) if platform_rate_obj.Rate else 0, + 'merchant_rate': float(merchant_rate_obj.Rate) if merchant_rate_obj.Rate else 0, + } + }) + + +class ModifyRateView(APIView): + """ + 修改打手分成费率 + 权限:需要拥有 7007b + 请求:POST /houtai/htxgfl + 参数:{"username": "客服手机号", "type": "platform" / "merchant", "value": 费率(百分比数字)} + 返回:{"code":0, "msg":"修改成功"} + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + rate_type = request.data.get('type') + value = request.data.get('value') + + if not username or not rate_type or value is None: + return Response({'code': 400, 'msg': '缺少参数'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '7007b' not in permissions: + return Response({'code': 403, 'msg': '您无权修改费率'}) + + try: + rate_value = Decimal(str(value)) + except: + return Response({'code': 400, 'msg': '费率值格式错误'}) + + + if rate_type == 'platform': + Platform = '1' + elif rate_type == 'merchant': + Platform = '3' + else: + return Response({'code': 400, 'msg': '无效的type参数'}) + + try: + with transaction.atomic(): + rate_obj = CommissionRate.objects.select_for_update().get(Platform=Platform) + rate_obj.Rate = rate_value + rate_obj.save() + except CommissionRate.DoesNotExist: + return Response({'code': 404, 'msg': '费率记录不存在'}) + except Exception as e: + logger.error(f"修改费率失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '修改失败'}) + + return Response({'code': 0, 'msg': '修改成功'}) + + + + + + +# 权限码(根据你要求,使用 '8080a') +REQUIRED_PERMISSION = '8080a' +class PopupNoticeListAPIView(APIView): + """ + 获取所有弹窗配置(页面 + 弹窗 + 图片) + URL: POST /houtai/hthqtcxx + 请求参数: { "username": "客服账号" } + 响应格式: { "code": 0, "data": { "pages": [...] } } + 注意:所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段区分 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取前端传递的 username(用于防越权) + username_frontend = request.data.get('username') + # 2. 调用公共权限验证方法,返回 (客服对象, 权限列表) + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + # 身份验证失败,返回业务码 403,但 HTTP 状态码仍是 200 + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + + # 3. 检查是否拥有所需权限码 '8080a' + if REQUIRED_PERMISSION not in permissions: + return Response({'code': 403, 'msg': '您没有权限访问弹窗管理功能'}) + + # 4. 查询所有页面,并预加载关联的弹窗和图片(避免N+1查询) + from jituan.services.display_config import filter_popup_page_by_request, list_response_meta + + pages = filter_popup_page_by_request( + PopupPage.query.all().prefetch_related('popups__images'), + request, + ).order_by('id') + + # 5. 序列化输出 + serializer = PopupPageSerializer(pages, many=True) + return Response({ + 'code': 0, + 'data': {'pages': serializer.data, **list_response_meta(request)}, + }) + + +class PopupNoticeModifyAPIView(APIView): + """ + 统一修改接口:支持页面、弹窗、图片的增删改 + URL: POST /houtai/htxgtcxx + 请求中必须包含 action 字段,用于区分操作类型。 + 所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段体现。 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ---------- 1. 权限验证 ---------- + username_frontend = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + if REQUIRED_PERMISSION not in permissions: + return Response({'code': 403, 'msg': '您没有权限操作弹窗管理'}) + + from jituan.services.display_config import forbid_display_write_in_all_scope + deny = forbid_display_write_in_all_scope(request) + if deny: + return deny + + # ---------- 2. 获取 action 并分发 ---------- + action = request.data.get('action') + if not action: + return Response({'code': 400, 'msg': '缺少action参数'}) + + try: + if action == 'add_page': + return self.add_page(request) + elif action == 'update_page': + return self.update_page(request) + elif action == 'delete_page': + return self.delete_page(request) + elif action == 'add_popup': + return self.add_popup(request) + elif action == 'update_popup': + return self.update_popup(request) + elif action == 'delete_popup': + return self.delete_popup(request) + elif action == 'add_image': + return self.add_image(request) + elif action == 'update_image': + return self.update_image(request) + elif action == 'delete_image': + return self.delete_image(request) + else: + return Response({'code': 400, 'msg': f'未知的action: {action}'}) + except Exception as e: + # 捕获未预期的异常,返回服务器错误 + traceback.print_exc() + return Response({'code': 500, 'msg': f'服务器错误: {str(e)}'}) + + # ==================== 页面相关操作 ==================== + + def add_page(self, request): + """添加新页面""" + from jituan.services.club_context import resolve_club_id_from_request + from jituan.services.display_config import forbid_display_write_in_all_scope + + deny = forbid_display_write_in_all_scope(request) + if deny: + return deny + + data = request.data + # 必填字段校验 + if not data.get('page_key') or not data.get('name'): + return Response({'code': 400, 'msg': '缺少 page_key 或 name'}) + club_id = resolve_club_id_from_request(request) + # 唯一性校验(同俱乐部内) + if PopupPage.query.filter(club_id=club_id, page_key=data['page_key']).exists(): + return Response({'code': 400, 'msg': '页面标识已存在'}) + + page = PopupPage.query.create( + club_id=club_id, + page_key=data['page_key'], + name=data['name'], + description=data.get('description', ''), + is_active=data.get('is_active', True), + ignore_user_mute=data.get('ignore_user_mute', False) + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': page.id}}) + + def update_page(self, request): + """更新页面信息""" + page_id = request.data.get('id') + if not page_id: + return Response({'code': 400, 'msg': '缺少页面id'}) + try: + page = PopupPage.query.get(id=page_id) + except PopupPage.DoesNotExist: + return Response({'code': 404, 'msg': '页面不存在'}) + + from jituan.services.display_config import forbid_popup_page_out_of_scope + deny = forbid_popup_page_out_of_scope(request, page) + if deny: + return deny + + # 更新允许修改的字段 + page.page_key = request.data.get('page_key', page.page_key) + page.name = request.data.get('name', page.name) + page.description = request.data.get('description', page.description) + page.is_active = request.data.get('is_active', page.is_active) + page.ignore_user_mute = request.data.get('ignore_user_mute', page.ignore_user_mute) + page.save() + return Response({'code': 0, 'msg': '更新成功'}) + + def delete_page(self, request): + """删除页面,可级联删除其下所有弹窗和图片""" + page_id = request.data.get('id') + cascade = request.data.get('cascade_delete_popups', True) # 默认级联 + if not page_id: + return Response({'code': 400, 'msg': '缺少页面id'}) + try: + page = PopupPage.query.get(id=page_id) + except PopupPage.DoesNotExist: + return Response({'code': 404, 'msg': '页面不存在'}) + + from jituan.services.display_config import forbid_popup_page_out_of_scope, forbid_display_write_in_all_scope + deny = forbid_display_write_in_all_scope(request) or forbid_popup_page_out_of_scope(request, page) + if deny: + return deny + + if cascade: + # 级联删除:使用事务保证原子性 + with transaction.atomic(): + popups = page.popups.all() + for popup in popups: + # 删除弹窗下的所有图片文件 + for img in popup.images.all(): + if img.image_url: + delete_from_oss(img.image_url) + popups.delete() + page.delete() + else: + # 非级联删除:如果页面下还有弹窗,则拒绝删除 + if page.popups.exists(): + return Response({'code': 400, 'msg': '该页面下存在弹窗,请先删除弹窗或使用级联删除'}) + page.delete() + + return Response({'code': 0, 'msg': '删除成功'}) + + # ==================== 弹窗相关操作 ==================== + + def add_popup(self, request): + """为指定页面添加新弹窗""" + data = request.data + page_id = data.get('page_id') + popup_id = data.get('popup_id') + if not page_id or not popup_id: + return Response({'code': 400, 'msg': '缺少 page_id 或 popup_id'}) + try: + page = PopupPage.query.get(id=page_id) + except PopupPage.DoesNotExist: + return Response({'code': 404, 'msg': '页面不存在'}) + + from jituan.services.display_config import forbid_popup_page_out_of_scope + deny = forbid_popup_page_out_of_scope(request, page) + if deny: + return deny + + # 同一页面下 popup_id 必须唯一 + if PopupConfig.query.filter(page=page, popup_id=popup_id).exists(): + return Response({'code': 400, 'msg': '该页面下已存在相同的弹窗ID'}) + + popup = PopupConfig.query.create( + page=page, + popup_id=popup_id, + title=data.get('title', ''), + description=data.get('description', ''), + strategy_type=data.get('strategy_type', 'daily'), + max_count=data.get('max_count', 1), + reset_interval=data.get('reset_interval', 'day'), + duration=data.get('duration', 0), + force_even_muted=data.get('force_even_muted', False), + sort_order=data.get('sort_order', 0), + is_active=data.get('is_active', True), + start_time=data.get('start_time') or None, + end_time=data.get('end_time') or None + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': popup.id}}) + + def update_popup(self, request): + """更新弹窗信息""" + popup_id = request.data.get('id') + if not popup_id: + return Response({'code': 400, 'msg': '缺少弹窗id'}) + try: + popup = PopupConfig.query.get(id=popup_id) + except PopupConfig.DoesNotExist: + return Response({'code': 404, 'msg': '弹窗不存在'}) + + from jituan.services.display_config import forbid_popup_page_out_of_scope + deny = forbid_popup_page_out_of_scope(request, popup.page) + if deny: + return deny + + # 更新弹窗字段 + popup.popup_id = request.data.get('popup_id', popup.popup_id) + popup.title = request.data.get('title', popup.title) + popup.description = request.data.get('description', popup.description) + popup.strategy_type = request.data.get('strategy_type', popup.strategy_type) + popup.max_count = request.data.get('max_count', popup.max_count) + popup.reset_interval = request.data.get('reset_interval', popup.reset_interval) + popup.duration = request.data.get('duration', popup.duration) + popup.force_even_muted = request.data.get('force_even_muted', popup.force_even_muted) + popup.sort_order = request.data.get('sort_order', popup.sort_order) + popup.is_active = request.data.get('is_active', popup.is_active) + popup.start_time = request.data.get('start_time') or None + popup.end_time = request.data.get('end_time') or None + popup.save() + return Response({'code': 0, 'msg': '更新成功'}) + + def delete_popup(self, request): + """删除弹窗,同时删除其下所有图片(包括OSS文件)""" + popup_id = request.data.get('id') + if not popup_id: + return Response({'code': 400, 'msg': '缺少弹窗id'}) + try: + popup = PopupConfig.query.get(id=popup_id) + except PopupConfig.DoesNotExist: + return Response({'code': 404, 'msg': '弹窗不存在'}) + + from jituan.services.display_config import forbid_popup_page_out_of_scope + deny = forbid_popup_page_out_of_scope(request, popup.page) + if deny: + return deny + + with transaction.atomic(): + for img in popup.images.all(): + if img.image_url: + delete_from_oss(img.image_url) + popup.images.all().delete() + popup.delete() + return Response({'code': 0, 'msg': '删除成功'}) + + # ==================== 图片相关操作 ==================== + + def _upload_image_file(self, file_obj, page_key): + """ + 内部方法:将图片上传到OSS指定目录 + 目录: a_long/tanchuang/tanchuang/ + 文件名: {page_key}_{时间戳}_{uuid8}.{ext} + 返回: 相对路径(例如 a_long/tanchuang/tanchuang/dashouzhongxin_20250419120000_abc12345.jpg) + """ + # 验证图片格式和大小 + valid, msg = validate_image(file_obj) + if not valid: + raise ValueError(msg) + + # 生成唯一文件名 + ext = os.path.splitext(file_obj.name)[1].lower() + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + unique_id = str(uuid.uuid4())[:8] + filename = f"{page_key}_{timestamp}_{unique_id}{ext}" + relative_path = f"a_long/tanchuang/tanchuang/{filename}" + + # 上传到OSS + url = upload_to_oss(file_obj, relative_path) + if not url: + raise ValueError("图片上传失败") + return relative_path + + def add_image(self, request): + """为指定弹窗添加图片(支持上传文件)""" + popup_id = request.data.get('popup_id') + if not popup_id: + return Response({'code': 400, 'msg': '缺少 popup_id'}) + try: + popup = PopupConfig.query.get(id=popup_id) + except PopupConfig.DoesNotExist: + return Response({'code': 404, 'msg': '弹窗不存在'}) + + from jituan.services.display_config import forbid_popup_page_out_of_scope + deny = forbid_popup_page_out_of_scope(request, popup.page) + if deny: + return deny + + # 获取上传的文件 + file_obj = request.FILES.get('file') + if not file_obj: + return Response({'code': 400, 'msg': '未提供图片文件'}) + + try: + page_key = popup.page.page_key + relative_path = self._upload_image_file(file_obj, page_key) + except ValueError as e: + return Response({'code': 400, 'msg': str(e)}) + + # 保存图片记录 + image = PopupImage.query.create( + popup_config=popup, + image_url=relative_path, + text=request.data.get('text', ''), + sort_order=request.data.get('sort_order', 0) + ) + return Response({'code': 0, 'msg': '添加成功', 'data': {'id': image.id}}) + + def update_image(self, request): + """更新图片信息(可替换图片文件、修改文字、排序)""" + image_id = request.data.get('id') + if not image_id: + return Response({'code': 400, 'msg': '缺少图片id'}) + try: + image = PopupImage.query.get(id=image_id) + except PopupImage.DoesNotExist: + return Response({'code': 404, 'msg': '图片不存在'}) + + from jituan.services.display_config import forbid_popup_page_out_of_scope + deny = forbid_popup_page_out_of_scope(request, image.popup_config.page) + if deny: + return deny + + # 更新文字和排序 + image.text = request.data.get('text', image.text) + image.sort_order = request.data.get('sort_order', image.sort_order) + + # 如果提供了新图片文件,则替换 + file_obj = request.FILES.get('file') + if file_obj: + try: + page_key = image.popup_config.page.page_key + new_path = self._upload_image_file(file_obj, page_key) + except ValueError as e: + return Response({'code': 400, 'msg': str(e)}) + # 删除旧图片 + if image.image_url: + delete_from_oss(image.image_url) + image.image_url = new_path + + image.save() + return Response({'code': 0, 'msg': '更新成功'}) + + def delete_image(self, request): + """删除图片(同时删除OSS文件)""" + image_id = request.data.get('id') + if not image_id: + return Response({'code': 400, 'msg': '缺少图片id'}) + try: + image = PopupImage.query.get(id=image_id) + except PopupImage.DoesNotExist: + return Response({'code': 404, 'msg': '图片不存在'}) + + from jituan.services.display_config import forbid_popup_page_out_of_scope + deny = forbid_popup_page_out_of_scope(request, image.popup_config.page) + if deny: + return deny + + if image.image_url: + delete_from_oss(image.image_url) + image.delete() + return Response({'code': 0, 'msg': '删除成功'}) + + + +class ShopListView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 调用公共权限验证(与您给的示例完全一致) + username_frontend = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + + # 2. 检查是否拥有所需权限(999a~999e 任意一个) + required_perms = {'999a', '999b', '999c', '999d', '999e'} + if not required_perms.intersection(set(permissions)): + return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'}) + + # 3. 解析请求参数 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip() + zhuangtai = request.data.get('zhuangtai') + lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip() + user_id = request.data.get('user_id', '').strip() + ketixian_yue_min = request.data.get('ketixian_yue_min') + ketixian_yue_max = request.data.get('ketixian_yue_max') + zhengqu_zonge_min = request.data.get('zhengqu_zonge_min') + zhengqu_zonge_max = request.data.get('zhengqu_zonge_max') + + # 4. 构建查询集(先查店铺,预加载关联凭证) + queryset = Dianpu.query.select_related('pingzheng').all() + + if dianpu_mingcheng: + queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng) + if zhuangtai is not None: + queryset = queryset.filter(zhuangtai=zhuangtai) + if lianxi_dianhua: + queryset = queryset.filter(lianxi_dianhua__icontains=lianxi_dianhua) + if user_id: + queryset = queryset.filter(pingzheng__yonghu=user_id) + + # 金额范围过滤 + if ketixian_yue_min is not None: + queryset = queryset.filter(ketixian_yue__gte=ketixian_yue_min) + if ketixian_yue_max is not None: + queryset = queryset.filter(ketixian_yue__lte=ketixian_yue_max) + if zhengqu_zonge_min is not None: + queryset = queryset.filter(zhengqu_zonge__gte=zhengqu_zonge_min) + if zhengqu_zonge_max is not None: + queryset = queryset.filter(zhengqu_zonge__lte=zhengqu_zonge_max) + + # 5. 分页 + paginator = Paginator(queryset, page_size) + page_obj = paginator.get_page(page) + + # 6. 构造返回数据 + shop_list = [] + for dianpu in page_obj: + pingzheng = dianpu.pingzheng + shop_list.append({ + 'id': dianpu.id, + 'dianpu_mingcheng': dianpu.dianpu_mingcheng, + 'dianpu_touxiang': dianpu.dianpu_touxiang or '', + 'lianxi_dianhua': dianpu.lianxi_dianhua or '', + 'weixinhao': dianpu.weixinhao or '', + 'zhuangtai': dianpu.zhuangtai, + 'bangding_yonghushu': dianpu.bangding_yonghushu, + 'erweima_url': dianpu.erweima_url or '', + 'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng, + 'shouyi_choucheng_feilv': dianpu.shouyi_choucheng_feilv or 0, + 'ketixian_yue': dianpu.ketixian_yue, + 'zhengqu_zonge': dianpu.zhengqu_zonge, + 'meiri_tixian_xiane': dianpu.meiri_tixian_xiane or 0, + 'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane, + 'pingzheng__yonghu': pingzheng.yonghu, # 用户ID + 'pingzheng__zhanghao': pingzheng.zhanghao, # 登录账号 + }) + + # 获取默认配置(ID=1,不存在则创建) + default_config, _ = DianpuMorenPeizhi.query.get_or_create(id=1, defaults={ + 'moren_choucheng_feilv': 0.1, + 'moren_tixian_xiane': 1000.00 + }) + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'list': shop_list, + 'total': paginator.count, + 'default_config': { + 'moren_choucheng_feilv': default_config.moren_choucheng_feilv, + 'moren_tixian_xiane': default_config.moren_tixian_xiane, + } + } + }) + + +class ShopModifyView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 身份验证 + username_frontend = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + + # 转换权限为集合方便判断 + user_perms = set(permissions) + action = request.data.get('action', '') + + # 2. 根据 action 分派任务 + if action == 'update_dianpu': + return self._update_dianpu(request, user_perms) + elif action == 'add_dianpu': + return self._add_dianpu(request, user_perms) + elif action == 'update_default': + return self._update_default(request, user_perms) + else: + return Response({'code': 400, 'msg': '未知操作类型'}) + + def _update_dianpu(self, request, user_perms): + """修改店铺信息,精确权限控制""" + dianpu_id = request.data.get('dianpu_id') + if not dianpu_id: + return Response({'code': 400, 'msg': '缺少店铺ID'}) + + try: + with transaction.atomic(): + dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id) + + # 基础信息修改 (999e) + if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']): + if '999e' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改店铺基本信息(999e)'}) + if 'dianpu_mingcheng' in request.data: + dianpu.dianpu_mingcheng = request.data['dianpu_mingcheng'] + if 'lianxi_dianhua' in request.data: + dianpu.lianxi_dianhua = request.data['lianxi_dianhua'] + if 'weixinhao' in request.data: + dianpu.weixinhao = request.data['weixinhao'] + + # 店铺状态 (999a) + if 'zhuangtai' in request.data: + if '999a' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改店铺状态(999a)'}) + new_status = int(request.data['zhuangtai']) + if new_status not in (0, 1): + return Response({'code': 400, 'msg': '店铺状态无效'}) + dianpu.zhuangtai = new_status + + # 可提现余额 (999b) + if 'ketixian_yue' in request.data: + if '999b' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改可提现余额(999b)'}) + dianpu.ketixian_yue = request.data['ketixian_yue'] + + # 提现限额相关 (999c) + if 'kaiqi_meiri_xiane' in request.data or 'meiri_tixian_xiane' in request.data: + if '999c' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改提现限额配置(999c)'}) + if 'kaiqi_meiri_xiane' in request.data: + dianpu.kaiqi_meiri_xiane = request.data['kaiqi_meiri_xiane'] + if 'meiri_tixian_xiane' in request.data: + dianpu.meiri_tixian_xiane = request.data['meiri_tixian_xiane'] + + # 抽成相关 (999d) + if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data: + if '999d' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改抽成配置(999d)'}) + if 'kaiqi_shouyi_choucheng' in request.data: + dianpu.kaiqi_shouyi_choucheng = request.data['kaiqi_shouyi_choucheng'] + if 'shouyi_choucheng_feilv' in request.data: + dianpu.shouyi_choucheng_feilv = request.data['shouyi_choucheng_feilv'] + + dianpu.save() # Django 会自动判断更新哪些字段 + return Response({'code': 0, 'msg': '店铺信息更新成功'}) + + except Dianpu.DoesNotExist: + return Response({'code': 404, 'msg': '店铺不存在'}) + except Exception as e: + logger.exception("修改店铺失败") + return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) + + def _add_dianpu(self, request, user_perms): + """添加店铺,需要权限 999b""" + if '999b' not in user_perms: + return Response({'code': 403, 'msg': '无权限添加店铺(999b)'}) + + yonghu_id = request.data.get('yonghu_id') + zhanghao = request.data.get('zhanghao') + mima = request.data.get('mima') + dianpu_mingcheng = request.data.get('dianpu_mingcheng') + + if not all([yonghu_id, zhanghao, mima, dianpu_mingcheng]): + return Response({'code': 400, 'msg': '必填字段缺失(用户ID、账号、密码、店铺名称)'}) + + # 检查用户是否存在 + if not User.query.filter(UserUID=yonghu_id).exists(): + return Response({'code': 404, 'msg': '用户不存在'}) + + # 检查凭证是否已存在 + if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists(): + return Response({'code': 400, 'msg': '登录账号已存在'}) + if YonghuPingzheng.query.filter(yonghu=yonghu_id).exists(): + return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'}) + + try: + with transaction.atomic(): + pingzheng = YonghuPingzheng.query.create( + yonghu=yonghu_id, + zhanghao=zhanghao, + mima=mima, + is_active=True + ) + dianpu = Dianpu.query.create( + pingzheng=pingzheng, + dianpu_mingcheng=dianpu_mingcheng, + lianxi_dianhua=request.data.get('lianxi_dianhua', ''), + weixinhao=request.data.get('weixinhao', ''), + kaiqi_shouyi_choucheng=request.data.get('kaiqi_shouyi_choucheng', False), + shouyi_choucheng_feilv=request.data.get('shouyi_choucheng_feilv', 0), + kaiqi_meiri_xiane=request.data.get('kaiqi_meiri_xiane', False), + meiri_tixian_xiane=request.data.get('meiri_tixian_xiane', 0), + ) + return Response({'code': 0, 'msg': '店铺创建成功', 'dianpu_id': dianpu.id}) + except IntegrityError as e: + logger.error("创建店铺违反唯一性约束: %s", e) + return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'}) + except Exception as e: + logger.exception("创建店铺失败") + return Response({'code': 500, 'msg': '创建失败,请稍后重试'}) + + def _update_default(self, request, user_perms): + """修改默认配置,按修改内容校验权限""" + feilv = request.data.get('moren_choucheng_feilv') + xiane = request.data.get('moren_tixian_xiane') + modify_commission = feilv is not None + modify_withdraw = xiane is not None + + if modify_commission and '999d' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'}) + if modify_withdraw and '999c' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'}) + + try: + config, _ = DianpuMorenPeizhi.query.get_or_create(id=1) + if modify_commission: + config.moren_choucheng_feilv = feilv + if modify_withdraw: + config.moren_tixian_xiane = xiane + config.save(update_fields=(['moren_choucheng_feilv'] if modify_commission else []) + + (['moren_tixian_xiane'] if modify_withdraw else [])) + return Response({'code': 0, 'msg': '默认配置更新成功'}) + except Exception as e: + logger.exception("更新默认配置失败") + return Response({'code': 500, 'msg': '更新失败'}) + + + + + + + + +# ==================== 1. 公共商品类型 + 审核模式 ==================== +class ShopPublicTypeAndAuditView(APIView): + """ + 返回:①正常状态的公共商品类型列表 ②全局商品审核模式开关 + 前端请求路径:/houtai/htdphqsplx + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ---------- 身份与权限校验 ---------- + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + + # 拥有 1199ab/1199abc/1199abd 任一个即可访问 + if not set(permissions).intersection(SHOP_PRODUCT_PERMS): + return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'}) + + # ---------- 查询公共商品类型(仅正常审核状态的) ---------- + try: + # shenhezhuangtai = 1 表示正常 (根据模型默认值) + types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values( + 'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin' + ) + type_list = [] + for t in types_qs: + type_list.append({ + 'id': t['id'], + 'jieshao': t['jieshao'] or '', + 'tupian_url': t['tupian_url'] or '', + 'yaoqiuleixing': t['yaoqiuleixing'], + 'huiyuan_id': t['huiyuan_id'], + 'yongjin': t['yongjin'], + }) + except Exception as e: + logger.exception("查询公共商品类型失败") + return Response({'code': 500, 'msg': '服务器错误,请稍后重试'}) + + # ---------- 获取审核模式配置 ---------- + audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() + kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False + + return Response({ + 'code': 0, + 'data': { + 'public_types': type_list, + 'kaiqi_shenhe': kaiqi_shenhe + } + }) + + +# ==================== 2. 店铺列表(支持搜索、筛选、分页) ==================== +class ShopListForProductView(APIView): + """ + 返回店铺列表,用于商品管理页面选择店铺。 + 支持:关键词搜索(店铺ID/名称/用户ID)、店铺状态筛选、分页。 + 前端请求路径:/houtai/htdphqdpys + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + if not set(permissions).intersection(SHOP_PRODUCT_PERMS): + return Response({'code': 403, 'msg': '无权访问店铺列表'}) + + # ---------- 安全解析分页参数 ---------- + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '分页参数格式错误'}) + + # 搜索关键词(字符串,ORM 自动防注入) + keyword = str(request.data.get('keyword', '')).strip() + # 店铺状态筛选(1正常,0封禁,不传则全部) + zhuangtai = request.data.get('zhuangtai') + + # ---------- 构建查询集(关联凭证表获取用户ID) ---------- + shops = Dianpu.query.select_related('pingzheng').all() + + # 状态筛选:只接受 0 或 1 + if zhuangtai is not None: + try: + zhuangtai = int(zhuangtai) + if zhuangtai in [0, 1]: + shops = shops.filter(zhuangtai=zhuangtai) + else: + return Response({'code': 400, 'msg': '状态参数只能为0或1'}) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '状态参数必须为整数'}) + + # 关键词搜索(店铺ID/名称/用户ID) + if keyword: + shops = shops.filter( + models.Q(id__icontains=keyword) | + models.Q(dianpu_mingcheng__icontains=keyword) | + models.Q(pingzheng__yonghu__icontains=keyword) + ) + + # ---------- 分页 ---------- + paginator = Paginator(shops, page_size) + page_obj = paginator.get_page(page) + + shop_list = [] + for shop in page_obj: + shop_list.append({ + 'id': shop.id, + 'dianpu_mingcheng': shop.dianpu_mingcheng, + 'zhuangtai': shop.zhuangtai, + 'yonghu_id': shop.pingzheng.yonghu if shop.pingzheng else '', + 'lianxi_dianhua': shop.lianxi_dianhua or '', + 'weixinhao': shop.weixinhao or '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': shop_list, + 'total': paginator.count + } + }) + + +# ==================== 3. 店铺商品类型映射列表(含筛选、待审核计数) ==================== +class ShopProductTypeMappingView(APIView): + """ + 返回店铺商品类型映射(ShangpinLeixingDianpu)列表。 + 支持:店铺ID、公共类型ID、上架/封禁/审核状态筛选,关键字搜索(介绍/ID/店铺名/公共类型名)。 + 每次请求同时返回“待审核总数”(shenhe_zhuangtai=False 的记录数)。 + 前端请求路径:/houtai/htdphqyssplx + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + if not set(permissions).intersection(SHOP_PRODUCT_PERMS): + return Response({'code': 403, 'msg': '无权访问商品类型映射'}) + + # ---------- 安全解析参数 ---------- + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '分页参数格式错误'}) + + keyword = str(request.data.get('keyword', '')).strip() + shop_id = request.data.get('shop_id') + public_type_id = request.data.get('public_type_id') + + # 布尔型筛选字段处理:None 表示不筛选,否则转为布尔值 + def parse_bool(val): + if val is None: + return None + return bool(val) + + shangjia = parse_bool(request.data.get('shangjia_zhuangtai')) + fengjin = parse_bool(request.data.get('fengjin_zhuangtai')) + shenhe = parse_bool(request.data.get('shenhe_zhuangtai')) + + # ---------- 查询(连表店铺和公共类型) ---------- + mappings = ShangpinLeixingDianpu.query.select_related('dianpu', 'gonggong_leixing').all() + + # 筛选条件:均为参数化,完全防注入 + if shop_id is not None: + mappings = mappings.filter(dianpu_id=int(shop_id)) + if public_type_id is not None: + mappings = mappings.filter(gonggong_leixing_id=int(public_type_id)) + if shangjia is not None: + mappings = mappings.filter(shangjia_zhuangtai=shangjia) + if fengjin is not None: + mappings = mappings.filter(fengjin_zhuangtai=fengjin) + if shenhe is not None: + mappings = mappings.filter(shenhe_zhuangtai=shenhe) + + # 关键字搜索(介绍、ID、店铺名、公共类型名) + if keyword: + mappings = mappings.filter( + models.Q(jieshao__icontains=keyword) | + models.Q(id__icontains=keyword) | + models.Q(dianpu__dianpu_mingcheng__icontains=keyword) | + models.Q(gonggong_leixing__jieshao__icontains=keyword) + ) + + # ---------- 待审核总数(不受分页影响) ---------- + pending_total = mappings.filter(shenhe_zhuangtai=False).count() + + # ---------- 分页 ---------- + paginator = Paginator(mappings, page_size) + page_obj = paginator.get_page(page) + + mapping_list = [] + for m in page_obj: + mapping_list.append({ + 'id': m.id, + 'dianpu_id': m.dianpu_id, + 'dianpu_name': m.dianpu.dianpu_mingcheng if m.dianpu else '', + 'gonggong_leixing_id': m.gonggong_leixing_id, + 'gonggong_leixing_name': m.gonggong_leixing.jieshao if m.gonggong_leixing else '', + 'jieshao': m.jieshao, + 'tupian_url': m.tupian_url or '', + 'paixu': m.paixu, + 'shangjia_zhuangtai': m.shangjia_zhuangtai, + 'fengjin_zhuangtai': m.fengjin_zhuangtai, + 'shenhe_zhuangtai': m.shenhe_zhuangtai, + }) + + return Response({ + 'code': 0, + 'data': { + 'list': mapping_list, + 'total': paginator.count, + 'pending_total': pending_total + } + }) + + +# ==================== 统一修改接口(已修正事务) ==================== +class ShopProductModifyView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + + user_perms = set(permissions) + action = request.data.get('action') + + if action == 'update_audit_mode': + return self._update_audit_mode(request, user_perms) + elif action == 'update_mapping_status': + return self._update_mapping_status(request, user_perms) + else: + return Response({'code': 400, 'msg': '未知操作'}) + + @transaction.atomic # ✅ 事务修复点 + def _update_audit_mode(self, request, user_perms): + if '1199ab' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改审核模式'}) + + kaiqi = request.data.get('kaiqi_shenhe') + if kaiqi is None: + return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'}) + + try: + config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1) + config.kaiqi_shenhe = bool(kaiqi) + config.save(update_fields=['kaiqi_shenhe']) + return Response({'code': 0, 'msg': '审核模式已更新'}) + except Exception as e: + logger.exception("更新审核模式失败") + return Response({'code': 500, 'msg': '服务器错误'}) + + @transaction.atomic + def _update_mapping_status(self, request, user_perms): + mapping_id = request.data.get('mapping_id') + if not mapping_id: + return Response({'code': 400, 'msg': '缺少 mapping_id'}) + + try: + mapping = ShangpinLeixingDianpu.objects.select_for_update().get(id=int(mapping_id)) + except (ShangpinLeixingDianpu.DoesNotExist, ValueError): + return Response({'code': 404, 'msg': '映射不存在'}) + + fengjin = request.data.get('fengjin_zhuangtai') + shangjia = request.data.get('shangjia_zhuangtai') + shenhe = request.data.get('shenhe_zhuangtai') + + # 权限判断 + if fengjin is not None: + if fengjin: + if '1199abc' not in user_perms: + return Response({'code': 403, 'msg': '无权限封禁'}) + else: + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限解封'}) + mapping.fengjin_zhuangtai = bool(fengjin) + + if shangjia is not None: + if not shangjia: + if '1199abc' not in user_perms: + return Response({'code': 403, 'msg': '无权限下架'}) + else: + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限上架'}) + mapping.shangjia_zhuangtai = bool(shangjia) + + if shenhe is not None: + if shenhe: + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限审核通过'}) + mapping.shenhe_zhuangtai = bool(shenhe) + + mapping.save() + return Response({'code': 0, 'msg': '修改成功'}) + + + + + + +# 商品管理相关权限组(拥有任意一个即可查看列表) +PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd', '1199abe'] + + +class ProductListView(APIView): + """ + 商品列表接口,支持多条件筛选与分页。 + 请求路径: /houtai/hqhtdpsplx + 权限要求: 1199ab / 1199abc / 1199abd / 1199abe 任一即可 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ----- 身份与权限验证 ----- + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + + if not set(permissions).intersection(PRODUCT_PERMS): + return Response({'code': 403, 'msg': '无商品管理权限'}) + + # ----- 安全解析分页与筛选参数 ----- + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 15)) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '分页参数格式错误'}) + + keyword = str(request.data.get('keyword', '')).strip() + shop_id = request.data.get('shop_id') # 店铺ID (可选) + shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型映射ID (可选) + public_type_id = request.data.get('public_type_id') # 公共商品类型ID (可选) + + # 布尔型状态筛选 + def parse_bool(val): + if val is None: + return None + return bool(val) + + shenhe = parse_bool(request.data.get('shenhe_zhuangtai')) + shangjia = parse_bool(request.data.get('shangjia_zhuangtai')) + fengjin = parse_bool(request.data.get('fengjin_zhuangtai')) + + # ----- 构建查询(预加载店铺和店铺商品类型) ----- + products = Shangpin.query.select_related('dianpu', 'dianpu_leixing').all() + + # 按店铺筛选 + if shop_id is not None: + try: + products = products.filter(dianpu_id=int(shop_id)) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': 'shop_id 格式错误'}) + + # 按店铺商品类型映射筛选 + if shop_product_type_id is not None: + try: + products = products.filter(dianpu_leixing_id=int(shop_product_type_id)) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'}) + + # 按公共商品类型筛选(leixing_id 存储的是公共类型的ID) + if public_type_id is not None: + try: + products = products.filter(leixing_id=int(public_type_id)) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': 'public_type_id 格式错误'}) + + # 平台审核状态筛选 (BooleanField) + if shenhe is not None: + products = products.filter(shenhe_zhuangtai=shenhe) + + # 上架状态筛选 + if shangjia is not None: + products = products.filter(shangjia_zhuangtai=shangjia) + + # 封禁状态筛选 + if fengjin is not None: + products = products.filter(fengjin_zhuangtai=fengjin) + + # 关键词搜索:商品ID或商品标题(搜索具有模糊性,使用icontains) + if keyword: + # 尝试纯数字可能是商品ID,使用精确匹配与标题模糊搜索 + products = products.filter( + models.Q(id__icontains=keyword) | + models.Q(biaoqian__icontains=keyword) + ) + + # ----- 获取待审核总数(平台审核未通过的数量) ----- + pending_total = products.filter(shenhe_zhuangtai=False).count() + + # ----- 分页 ----- + paginator = Paginator(products, page_size) + page_obj = paginator.get_page(page) + + # ----- 构造返回数据(批量获取公共类型名称,避免N+1) ----- + # 提取所有公共商品类型ID + type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None} + public_type_map = {} + if type_ids: + public_types = ShangpinLeixing.query.filter(id__in=type_ids).values('id', 'jieshao') + public_type_map = {t['id']: t['jieshao'] for t in public_types} + + product_list = [] + for p in page_obj: + product_list.append({ + 'id': p.id, + 'biaoqian': p.biaoqian or '', + 'jiage': p.jiage, + 'kucun': p.kucun, + 'leixing_id': p.leixing_id, + 'public_type_name': public_type_map.get(p.leixing_id, ''), + 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, + 'duiwai_xiaoliang': p.duiwai_xiaoliang, + 'jieshao': p.jieshao or '', + 'xiadan_xuzhi': p.xiadan_xuzhi or '', + 'guize_tupian': p.guize_tupian or '', + 'yaoqiuleixing': p.yaoqiuleixing, + 'huiyuan_id': p.huiyuan_id or '', + 'yongjin': p.yongjin, + 'kaioi_ewai_PlayerCommission': p.kaioi_ewai_dashou_fencheng, + 'ewai_PlayerCommission': p.ewai_dashou_fencheng, + 'CreateTime': p.CreateTime.isoformat() if p.CreateTime else '', + 'UpdateTime': p.UpdateTime.isoformat() if p.UpdateTime else '', + 'paixu': p.paixu, + 'dianpu_id': p.dianpu_id, + 'dianpu_name': p.dianpu.dianpu_mingcheng if p.dianpu else '', + 'dianpu_leixing_id': p.dianpu_leixing_id, + 'dianpu_leixing_name': p.dianpu_leixing.jieshao if p.dianpu_leixing else '', + 'shangjia_zhuangtai': p.shangjia_zhuangtai, + 'fengjin_zhuangtai': p.fengjin_zhuangtai, + 'shenhe_zhuangtai': p.shenhe_zhuangtai, + }) + + return Response({ + 'code': 0, + 'data': { + 'list': product_list, + 'total': paginator.count, + 'pending_total': pending_total, # 待审核商品总数 + } + }) + + +class ProductModifyView(APIView): + """ + 商品修改接口,根据修改内容细粒度鉴权。 + 请求路径: /houtai/htxgdpspsj + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + + user_perms = set(permissions) + action = request.data.get('action', '') + + if action != 'update_product': + return Response({'code': 400, 'msg': '未知操作'}) + + return self._update_product(request, user_perms) + + @transaction.atomic + def _update_product(self, request, user_perms): + """执行商品修改,严格校验权限""" + product_id = request.data.get('product_id') + if not product_id: + return Response({'code': 400, 'msg': '缺少商品ID'}) + + try: + # 行级锁防止并发修改 + product = Shangpin.objects.select_for_update().get(id=int(product_id)) + except (Shangpin.DoesNotExist, ValueError): + return Response({'code': 404, 'msg': '商品不存在'}) + + # 需要修改的字段 + fields_to_update = {} + # 将要修改的状态标志用于后续权限判断 + status_changes = { + 'shangjia': request.data.get('shangjia_zhuangtai'), + 'fengjin': request.data.get('fengjin_zhuangtai'), + 'shenhe': request.data.get('shenhe_zhuangtai'), + } + + # ---------- 权限分类检查 ---------- + # 1. 封禁/下架操作 (消极操作) -> 1199abc + if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']): + if '1199abc' not in user_perms: + return Response({'code': 403, 'msg': '无权限执行下架/封禁/驳回审核操作(1199abc)'}) + if status_changes.get('fengjin') is not None and status_changes['fengjin'] == True: + if '1199abc' not in user_perms: + return Response({'code': 403, 'msg': '无权限执行封禁操作(1199abc)'}) + + # 2. 解封/上架/过审 (积极操作) -> 1199abd + if any(status_changes.get(k) is not None and status_changes[k] == True for k in ['shangjia', 'shenhe']): + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限执行上架/解封/审核通过操作(1199abd)'}) + if status_changes.get('fengjin') is not None and status_changes['fengjin'] == False: + if '1199abd' not in user_perms: + return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'}) + + # 3. 修改其他数据(非状态字段)-> 1199abe + # 除了上架、封禁、审核之外的字段都属于“其他数据” + other_fields = [k for k in request.data.keys() if k not in ['action', 'product_id', 'username', + 'shangjia_zhuangtai', 'fengjin_zhuangtai', + 'shenhe_zhuangtai']] + if other_fields and '1199abe' not in user_perms: + return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'}) + + # ---------- 应用修改 ---------- + # 允许修改的字段白名单(防止注入) + # 前端参数名 → 模型字段名映射 + field_name_map = { + 'kaioi_ewai_PlayerCommission': 'kaioi_ewai_dashou_fencheng', + 'ewai_PlayerCommission': 'ewai_dashou_fencheng', + } + allowed_fields = [ + 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'duiwai_xiaoliang', + 'jieshao', 'xiadan_xuzhi', 'yaoqiuleixing', 'yongjin', + 'kaioi_ewai_PlayerCommission', 'ewai_PlayerCommission', + 'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai' + ] + + update_fields = [] + for field in allowed_fields: + value = request.data.get(field) + if value is not None: + model_field = field_name_map.get(field, field) + # 简单类型校验(根据字段类型) + if field in ['jiage', 'kucun', 'duiwai_xiaoliang', 'yongjin', 'ewai_PlayerCommission', 'paixu']: + try: + value = float(value) if 'jiage' in field or 'yongjin' in field or 'ewai' in field else int(value) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': f'字段 {field} 值格式错误'}) + setattr(product, model_field, value) + update_fields.append(model_field) + + if update_fields: + product.save(update_fields=update_fields) + return Response({'code': 0, 'msg': '商品修改成功'}) + + + + +# 与前端约定的聊天权限码 +CHAT_PERM_CODES = ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4'] + +class KefuChatPermissionsView(APIView): + """ + 获取客服聊天权限及GoEasy配置 + POST /houtai/kfhqltqx + Body: { "phone": "客服手机号" } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 从前端获取传递的账号,用于防越权校验 + username_from_frontend = request.data.get('phone', None) + + # 2. 调用公共验证方法。 + # 返回 (kefu_obj, permissions) 或 (None, Response) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + + # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值(错误Response) + if kefu_obj is None: + return permissions + + # ---------- 验证通过,permissions 已经是权限码列表 ---------- + # 4. 检查是否拥有至少一个聊天权限 + has_chat_perm = any(p in CHAT_PERM_CODES for p in permissions) + + # 5. 若有聊天权限,获取 GoEasy AppKey + goeasy_appkey = '' + if has_chat_perm: + goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '') + + # 6. 返回数据 + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'permissions': permissions, + 'goeasy_appkey': goeasy_appkey + } + }) + + + + + +class FaKuanTongJiView(APIView): + """ + 罚款统计接口 + POST /houtai/htglyhqcfsltj + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + if not has_fadan_view_permission(permissions, username): + return Response({'code': 403, 'msg': '无权限查看罚款统计'}) + + stats = filter_penalty_qs(Penalty.query.all(), request).aggregate( + total=Count('id'), + daijiaona=Count('id', filter=Q(Status=1)), + shensuzhong=Count('id', filter=Q(Status=3)), + yijiaona=Count('id', filter=Q(Status=2)), + yibohui=Count('id', filter=Q(Status=4)), + ) + data = {k: v or 0 for k, v in stats.items()} + from jituan.services.club_user_access import list_response_meta + return Response({'code': 0, 'msg': '成功', 'data': {**data, **list_response_meta(request)}}) + except Exception as e: + logger.error(f"罚款统计异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + +class FaKuanLieBiaoView(APIView): + """ + 罚款列表接口 + POST /houtai/hthqfklb + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = request.data.get('username', '').strip() + if not username: + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if not has_fadan_view_permission(permissions, username): + return Response({'code': 403, 'msg': '无权限查看罚款列表'}) + + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + if page_size > 100: + page_size = 100 + + qs = filter_penalty_qs(Penalty.query.all(), request) + + # 状态筛选 + zhuangtai = request.data.get('zhuangtai') + if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]: + qs = qs.filter(Status=int(zhuangtai)) + + # 被处罚人ID + PenalizedUserID = request.data.get('PenalizedUserID', '').strip() + if PenalizedUserID: + qs = qs.filter(PenalizedUserID=PenalizedUserID) + + # 被处罚人身份 + beichufa_shenfen = request.data.get('beichufa_shenfen') + if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]: + qs = qs.filter(Identity=int(beichufa_shenfen)) + + # 模糊搜索(订单ID、被罚人ID、申请人ID) + sousuo = request.data.get('sousuo', '').strip() + if sousuo: + qs = qs.filter( + Q(RelatedOrderID__icontains=sousuo) | + Q(PenalizedUserID__icontains=sousuo) | + Q(ApplicantID__icontains=sousuo) + ) + + qs = qs.order_by('-CreateTime') + paginator = Paginator(qs, page_size) + try: + page_obj = paginator.page(page) + except Exception: + return Response({ + 'code': 0, 'msg': '成功', + 'data': {'list': [], 'total': 0, 'page': page, 'page_size': page_size} + }) + + records = list(page_obj) + penalty_ids = [r.id for r in records] + + # 批量获取图片并分组 + zhengju_map = {} + shensu_map = {} + if penalty_ids: + tupians = PenaltyAppealImage.query.filter( + Penalty_id__in=penalty_ids + ).values('Penalty_id', 'ImageURL', 'Purpose') + for t in tupians: + fid = t['Penalty_id'] + url = t['ImageURL'] + if t['Purpose'] == 1: + zhengju_map.setdefault(fid, []).append(url) + else: + shensu_map.setdefault(fid, []).append(url) + + # 组装列表数据 + results = [] + for r in records: + results.append({ + 'id': r.id, + 'beichufa_id': r.PenalizedUserID, + 'shenfen': r.Identity, + 'shenfen_text': dict(Penalty._meta.get_field('Identity').choices).get(r.Identity, ''), + 'shenqing_chufa': r.ApplicantID or '', + 'shenqingren_shenfen': r.ApplicantIdentity, + 'chufaliyou': r.Reason or '', + 'fakuanjine': str(r.FineAmount), + 'guanliandingdan_id': r.RelatedOrderID or '', + 'zhuangtai': r.Status, + 'yingxiang_qiangdan': r.AffectsGrabbing, + 'shensuliyou': r.AppealReason or '', + 'bohuiliyou': r.RejectReason or '', + 'chulizhe': r.ProcessorID or '', + 'chulizhe_shenfen': r.ProcessorIdentity, + 'zhengju_tupian': zhengju_map.get(r.id, []), + 'shensu_tupian': shensu_map.get(r.id, []), + 'creat_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '', + 'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '', + }) + + from jituan.services.club_user_access import list_response_meta + return Response({ + 'code': 0, 'msg': '成功', + 'data': { + 'list': results, + 'total': paginator.count, + 'page': page, + 'page_size': page_size, + **list_response_meta(request), + } + }) + except Exception as e: + logger.error(f"罚款列表异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + +class FaKuanChuLiView(APIView): + """ + 处理罚款申诉(同意/拒绝) + POST /houtai/glyclfk + """ + permission_classes = [IsAuthenticated] + parser_classes = [MultiPartParser, FormParser, JSONParser] + + def post(self, request): + try: + username = request.data.get('username', '').strip() + penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id') + action = request.data.get('action', '').strip() + chuli_liyou = request.data.get('chuli_liyou', '').strip() + + if not all([username, penalty_id, action]): + return Response({'code': 400, 'msg': '参数不完整'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + with transaction.atomic(): + try: + penalty = Penalty.objects.select_for_update().get(id=penalty_id) + except Penalty.DoesNotExist: + return Response({'code': 404, 'msg': '罚单不存在'}) + + deny = forbid_penalty_out_of_scope(request, penalty) + if deny: + return deny + + if penalty.Status != 3: + return Response({'code': 400, 'msg': '当前状态不可处理'}) + + if not check_fadan_permission(permissions, penalty.Identity): + return Response({'code': 403, 'msg': '无权限处理该类罚单'}) + + # 处理上传的图片 + uploaded_urls = [] + files = request.FILES.getlist('tupian') or [] + if not files: + single = request.FILES.get('tupian') + if single: + files = [single] + for file in files: + valid, msg = validate_image(file) + if not valid: + return Response({'code': 400, 'msg': msg}) + ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' + file_name = f"chufa_{penalty_id}_{int(time.time() * 1000)}.{ext}" + file_path = f"chufatupian/kechufatupian/{file_name}" + url = upload_to_oss(file, file_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + uploaded_urls.append(file_path) + + # 更新状态 + if action == 'agree': # 同意申诉 → 驳回处罚 + penalty.Status = 4 + penalty.RejectReason = chuli_liyou + elif action == 'reject': # 拒绝申诉 → 回到待缴纳 + penalty.Status = 1 + penalty.RejectReason = chuli_liyou + else: + return Response({'code': 400, 'msg': '无效的操作类型'}) + + # 记录处理者(使用 request.user 的 phone) + penalty.ProcessorID = request.user.Phone + penalty.ProcessorIdentity = 2 # 这里假设处理者是售后身份,可根据实际业务获取 + penalty.save() + + # 保存图片(证据图片,用途1) + for relative_url in uploaded_urls: + PenaltyAppealImage.query.create( + Penalty_id=penalty_id, + PenalizedUserID=penalty.PenalizedUserID, + ImageURL=relative_url, + Purpose=1 + ) + + logger.info(f"罚款处理成功: {penalty_id}, 操作: {action}") + return Response({'code': 0, 'msg': '处理成功'}) + + except Exception as e: + logger.error(f"罚款处理异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + +class FaKuanChuangJianView(APIView): + """ + 管理员创建罚款 + POST /houtai/htfksc + """ + permission_classes = [IsAuthenticated] + parser_classes = [MultiPartParser, FormParser, JSONParser] + + def post(self, request): + try: + username = request.data.get('username', '').strip() + PenalizedUserID, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data) + shenfen = request.data.get('shenfen') + + if not all([username, PenalizedUserID, shenfen, Reason, FineAmount is not None]): + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + shenfen = int(shenfen) + FineAmount = float(FineAmount) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': '参数格式错误'}) + + if shenfen not in [1, 2, 3, 4]: + return Response({'code': 400, 'msg': '无效的被罚人身份'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + if not check_fadan_permission(permissions, shenfen): + return Response({'code': 403, 'msg': '无权限创建该类罚单'}) + + # 验证用户是否存在对应的扩展表 + try: + user = User.query.get(UserUID=PenalizedUserID) + profile_attr = SHENFEN_PROFILE_MAP.get(shenfen) + if not profile_attr or not hasattr(user, profile_attr): + return Response({'code': 400, 'msg': '该用户不是所选身份'}) + except User.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + + # 处理图片上传 + uploaded_urls = [] + files = request.FILES.getlist('tupian') or [] + if not files: + single = request.FILES.get('tupian') + if single: + files = [single] + for file in files: + valid, msg = validate_image(file) + if not valid: + return Response({'code': 400, 'msg': msg}) + ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' + file_name = f"chufa_{int(time.time() * 1000)}.{ext}" + file_path = f"chufatupian/kechufatupian/{file_name}" + url = upload_to_oss(file, file_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + uploaded_urls.append(file_path) + + with transaction.atomic(): + penalty = Penalty.query.create( + PenalizedUserID=PenalizedUserID, + Identity=shenfen, + ApplicantID=request.user.Phone, # 申请人(客服)手机号 + ApplicantIdentity=2, # 售后身份 + Reason=Reason, + FineAmount=FineAmount, + AffectsGrabbing=AffectsGrabbing, + Status=1, # 待缴纳 + ClubID=resolve_penalty_club_id( + penalized_user_id=PenalizedUserID, request=request, + ), + ) + for relative_url in uploaded_urls: + PenaltyAppealImage.query.create( + Penalty=penalty, + PenalizedUserID=PenalizedUserID, + ImageURL=relative_url, + Purpose=1 + ) + + logger.info(f"罚款创建成功: {penalty.id}") + return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': penalty.id}}) + + except Exception as e: + logger.error(f"创建罚款异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + +class FineApplyView(APIView): + """ + 客服罚款申请接口(基于订单对打手发起罚款) + POST /houtai/kffkdssq + 权限: JWT认证 + 客服权限验证 + + 请求参数: + username (必填) : 客服用户名(手机号) + OrderID (必填) : 订单ID + Reason (必填) : 罚款原因 + FineAmount (必填) : 罚款金额(元) + AffectsGrabbing (可选) : 是否影响抢单,1=是,0=否,默认1 + """ + permission_classes = [IsAuthenticated] + parser_classes = [MultiPartParser, FormParser, JSONParser] + + def post(self, request): + try: + username = request.data.get('username', '').strip() + OrderID = pick_order_id(request.data) + _, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data) + + if not all([username, OrderID, Reason, FineAmount is not None]): + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + FineAmount = float(FineAmount) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': '罚款金额格式错误'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 验证订单存在 + try: + order = Order.query.get(OrderID=OrderID) + except Order.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}) + + # 获取打手ID + dashou_id = order.PlayerID + if not dashou_id: + return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'}) + + # 重复罚款检查 + if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=OrderID).exists(): + return Response({'code': 400, 'msg': '该打手已被罚款过'}) + + # 检查权限(打手身份 shenfen=1) + if not check_fadan_permission(permissions, 1): + return Response({'code': 403, 'msg': '无权限处罚打手'}) + + # 处理图片上传 + uploaded_urls = [] + files = request.FILES.getlist('tupian') or [] + if not files: + single = request.FILES.get('tupian') + if single: + files = [single] + for file in files: + valid, msg = validate_image(file) + if not valid: + return Response({'code': 400, 'msg': msg}) + ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' + file_name = f"chufa_{int(time.time() * 1000)}.{ext}" + file_path = f"chufatupian/kechufatupian/{file_name}" + url = upload_to_oss(file, file_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + uploaded_urls.append(file_path) + + # 创建罚单 + with transaction.atomic(): + penalty = Penalty.query.create( + PenalizedUserID=dashou_id, + ApplicantID=request.user.Phone, + Identity=1, # 被处罚人身份:1 打手 + Reason=Reason, + FineAmount=FineAmount, + RelatedOrderID=OrderID, + Status=1, # 待缴纳 + AffectsGrabbing=AffectsGrabbing, + ApplicantIdentity=1, # 申请人身份:1 客服 + ClubID=resolve_penalty_club_id(order=order, request=request), + ) + for relative_url in uploaded_urls: + PenaltyAppealImage.query.create( + Penalty=penalty, + PenalizedUserID=dashou_id, + ImageURL=relative_url, + Purpose=1 + ) + + logger.info(f"客服罚款申请成功: 订单 {OrderID}, 打手 {dashou_id}, 金额 {FineAmount}") + return Response({'code': 0, 'msg': '罚款已生成', 'data': {'id': penalty.id}}) + + except Exception as e: + logger.error(f"客服罚款申请异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + +class PunishDashouView(APIView): + """ + 客服处罚打手接口(扣除积分) + POST /houtai/kpcf + 权限: JWT认证 + 客服权限验证 + + 请求参数: + username (必填) : 客服用户名(手机号) + OrderID (必填) : 订单ID + reason (可选) : 处罚原因 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + username = request.data.get('username', '').strip() + OrderID = request.data.get('OrderID', '').strip() + reason = request.data.get('reason', '').strip() + + if not all([username, OrderID]): + return Response({'code': 400, 'msg': '参数不完整'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 检查权限(打手身份 shenfen=1) + if not check_fadan_permission(permissions, 1): + return Response({'code': 403, 'msg': '无权限处罚打手'}) + + # 查询订单 + try: + order = Order.query.get(OrderID=OrderID) + except Order.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}) + + # 获取打手ID + dashou_id = order.PlayerID + if not dashou_id: + return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}) + + # 查询打手 + try: + dashou_user = User.query.get(UserUID=dashou_id) + dashou = dashou_user.DashouProfile + except User.DoesNotExist: + return Response({'code': 404, 'msg': '打手用户不存在'}) + except ObjectDoesNotExist: + return Response({'code': 400, 'msg': '该用户不是打手'}) + + # 创建处罚记录(扣除积分5分) + jifen = 5 + with transaction.atomic(): + penalty_record = PenaltyRecord.query.create( + PlayerID=dashou_id, + ApplicantID=username, + PenaltyReason=reason or '', + ApplyStatus=0, + DeductedPoints=jifen, + OrderID=OrderID, + ClubID=resolve_penalty_record_club_id(order_id=OrderID, player_id=dashou_id), + ) + # 扣除打手积分 + dashou.jifen = F('jifen') - jifen + dashou.save(update_fields=['jifen']) + + logger.info(f"客服处罚打手成功: 打手 {dashou_id}, 订单 {OrderID}, 扣除积分 {jifen}") + return Response({'code': 0, 'msg': '处罚成功', 'data': {'penalty_record_id': penalty_record.id}}) + + except Exception as e: + logger.error(f"客服处罚打手异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + + + + + + +class HqbkxxView(APIView): + """ + 获取板块配置所需全部数据 + POST /houtai/hqbkxx + Body: { "phone": "管理员手机号" } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 从前端获取传递的账号,用于防越权校验 + username_from_frontend = request.data.get('phone', None) + + # 2. 调用公共验证方法 + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + + # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值 + if kefu_obj is None: + return permissions + + # ---------- 验证通过 ---------- + # 4. 检查是否有板块管理权限 + if 'bankuai' not in permissions: + return Response({'code': 403, 'msg': '无板块管理权限'}, status=403) + + # 5. 查询所有板块 + bankuai_list = [] + for bk in Bankuai.query.all(): + bankuai_list.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'CreateTime': bk.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if bk.CreateTime else '' + }) + + # 6. 查询所有商品类型 + shangpin_leixing_list = [] + for sp in ShangpinLeixing.query.all(): + shangpin_leixing_list.append({ + 'id': sp.id, + 'jieshao': sp.jieshao, + 'tupian_url': sp.tupian_url or '', + 'bankuai_id': sp.bankuai_id + }) + + # 7. 查询所有会员 + huiyuan_list = [] + for hy in Huiyuan.query.all(): + huiyuan_list.append({ + 'huiyuan_id': hy.huiyuan_id, + 'jieshao': hy.jieshao, + 'bankuai_id': hy.bankuai_id + }) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'bankuai': bankuai_list, + 'shangpin_leixing': shangpin_leixing_list, + 'huiyuan': huiyuan_list + } + }) + + +class BkxgView(APIView): + """ + 板块增删改统一接口 + POST /houtai/bkxg + Body: { + "phone": "管理员手机号", + "action": "create | update | delete | add_items | remove_items", + "bankuai_id": int, + "bankuai_name": str, + "item_type": "shangpin_leixing | huiyuan", + "item_ids": [1,2,3] + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 从前端获取传递的账号 + username_from_frontend = request.data.get('phone', None) + + # 2. 调用公共验证方法 + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + + # 3. 验证失败直接返回 + if kefu_obj is None: + return permissions + + # 4. 检查板块管理权限 + if 'bankuai' not in permissions: + return Response({'code': 403, 'msg': '无板块管理权限'}, status=403) + + # 5. 获取操作类型 + action = request.data.get('action') + if not action: + return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400) + + # ==================== 创建板块 ==================== + if action == 'create': + bankuai_name = request.data.get('bankuai_name', '').strip() + if not bankuai_name: + return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400) + if Bankuai.query.filter(mingcheng=bankuai_name).exists(): + return Response({'code': 1, 'msg': '板块名称已存在'}, status=400) + + shangpin_ids = request.data.get('shangpin_ids', []) + huiyuan_ids = request.data.get('huiyuan_ids', []) + + with transaction.atomic(): + new_bk = Bankuai.query.create(mingcheng=bankuai_name) + if shangpin_ids: + ShangpinLeixing.query.filter( + id__in=shangpin_ids, + bankuai__isnull=True + ).update(bankuai=new_bk) + if huiyuan_ids: + Huiyuan.query.filter( + huiyuan_id__in=huiyuan_ids, + bankuai__isnull=True + ).update(bankuai=new_bk) + + return Response({ + 'code': 0, + 'msg': '创建成功', + 'data': {'bankuai_id': new_bk.bankuai_id} + }) + + # ==================== 以下操作需要 bankuai_id ==================== + bankuai_id = request.data.get('bankuai_id') + if not bankuai_id: + return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400) + + try: + bk = Bankuai.query.get(bankuai_id=bankuai_id) + except Bankuai.DoesNotExist: + return Response({'code': 1, 'msg': '板块不存在'}, status=404) + + # ==================== 修改板块名称 ==================== + if action == 'update': + new_name = request.data.get('bankuai_name', '').strip() + if not new_name: + return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400) + if Bankuai.query.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists(): + return Response({'code': 1, 'msg': '板块名称已存在'}, status=400) + bk.mingcheng = new_name + bk.save() + return Response({'code': 0, 'msg': '修改成功'}) + + # ==================== 删除板块 ==================== + elif action == 'delete': + with transaction.atomic(): + ShangpinLeixing.query.filter(bankuai=bk).update(bankuai=None) + Huiyuan.query.filter(bankuai=bk).update(bankuai=None) + bk.delete() + return Response({'code': 0, 'msg': '删除成功'}) + + # ==================== 添加绑定 ==================== + elif action == 'add_items': + item_type = request.data.get('item_type') + item_ids = request.data.get('item_ids', []) + if not item_type or not item_ids: + return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400) + + if item_type == 'shangpin_leixing': + ShangpinLeixing.query.filter( + id__in=item_ids, + bankuai__isnull=True + ).update(bankuai=bk) + elif item_type == 'huiyuan': + Huiyuan.query.filter( + huiyuan_id__in=item_ids, + bankuai__isnull=True + ).update(bankuai=bk) + else: + return Response({'code': 1, 'msg': '无效的 item_type'}, status=400) + return Response({'code': 0, 'msg': '添加成功'}) + + # ==================== 移除绑定 ==================== + elif action == 'remove_items': + item_type = request.data.get('item_type') + item_ids = request.data.get('item_ids', []) + if not item_type or not item_ids: + return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400) + + if item_type == 'shangpin_leixing': + ShangpinLeixing.query.filter( + id__in=item_ids, + bankuai=bk + ).update(bankuai=None) + elif item_type == 'huiyuan': + Huiyuan.query.filter( + huiyuan_id__in=item_ids, + bankuai=bk + ).update(bankuai=None) + else: + return Response({'code': 1, 'msg': '无效的 item_type'}, status=400) + return Response({'code': 0, 'msg': '移除成功'}) + + else: + return Response({'code': 1, 'msg': '未知操作'}, status=400) + + + + + + + + + +class CaiwuView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + + if kefu_obj is None: + return permissions + + if 'caiwu' not in permissions: + return Response({'code': 403, 'msg': '无财务查看权限'}, status=403) + + today = date.today() + today_start = datetime.combine(today, datetime.min.time()) + today_end = today_start + timedelta(days=1) + + # 今日收入 + today_income = DailyIncomeStat.query.filter(date=today).first() + today_income_amount = float(today_income.total_amount) if today_income else 0.00 + today_income_count = today_income.total_count if today_income else 0 + + # 今日支出 + today_payout = DailyPayoutStat.query.filter(date=today).first() + today_payout_amount = float(today_payout.total_amount) if today_payout else 0.00 + today_payout_count = today_payout.total_count if today_payout else 0 + + # 今日订单(已付款及之后的状态) + today_orders = Order.query.filter( + CreateTime__gte=today_start, + CreateTime__lt=today_end, + Status__in=[1,2,3,4,5,6,7,8] + ) + today_order_count = today_orders.count() + today_order_amount = float(today_orders.aggregate(total=Sum('Amount'))['total'] or 0.00) + + # 今日成交(Status=3) + today_completed = Order.query.filter( + CreateTime__gte=today_start, + CreateTime__lt=today_end, + Status=3 + ) + today_completed_count = today_completed.count() + today_completed_amount = float(today_completed.aggregate(total=Sum('Amount'))['total'] or 0.00) + + # 今日退款(Status=5) + today_tuikuan = Order.query.filter( + CreateTime__gte=today_start, + CreateTime__lt=today_end, + Status=5 + ) + today_tuikuan_count = today_tuikuan.count() + today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('Amount'))['total'] or 0.00) + + # 今日充值(会员充值,leixing=1, zhuangtai=3) + today_chongzhi = Czjilu.query.filter( + CreateTime__gte=today_start, + CreateTime__lt=today_end, + leixing=1, + zhuangtai=3 + ) + today_chongzhi_count = today_chongzhi.count() + today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00) + + # 今日新增会员:从购买记录表统计今天创建的用户(去重) + new_huiyuan_user_ids = Huiyuangoumai.query.filter( + CreateTime__gte=today_start, + CreateTime__lt=today_end + ).values_list('yonghu_id', flat=True).distinct() + today_new_huiyuan = len(new_huiyuan_user_ids) + + # 新增会员充值总额(这些用户今天的会员充值金额) + today_new_huiyuan_amount = 0.0 + if new_huiyuan_user_ids: + amount_sum = Czjilu.query.filter( + CreateTime__gte=today_start, + CreateTime__lt=today_end, + yonghuid__in=new_huiyuan_user_ids, + leixing=1, + zhuangtai=3 + ).aggregate(total=Sum('jine')) + today_new_huiyuan_amount = float(amount_sum['total'] or 0.0) + + # 各角色可提现余额总和 + all_guanshi_yue = float(UserGuanshi.query.aggregate(total=Sum('yue'))['total'] or 0.00) + all_dashou_yue = float(UserDashou.query.aggregate(total=Sum('yue'))['total'] or 0.00) + all_zuzhang_yue = float(UserZuzhang.query.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00) + all_shangjia_yue = float(UserShangjia.query.aggregate(total=Sum('yue'))['total'] or 0.00) + + # 平台总收支及利润 + total_income = float(DailyIncomeStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00) + total_payout = float(DailyPayoutStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00) + platform_profit = round(total_income - total_payout, 2) + + # 近30天每日收支明细 + daily_stats = [] + income_qs = DailyIncomeStat.query.order_by('-date')[:30] + payout_dict = { + str(p.date): float(p.total_amount) + for p in DailyPayoutStat.query.filter(date__gte=today - timedelta(days=30)) + } + for inc in income_qs: + d = str(inc.date) + daily_stats.append({ + 'date': d, + 'income': float(inc.total_amount), + 'income_count': inc.total_count, + 'payout': payout_dict.get(d, 0.00), + 'profit': round(float(inc.total_amount) - payout_dict.get(d, 0.00), 2) + }) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'today_income': round(today_income_amount, 2), + 'today_income_count': today_income_count, + 'today_payout': round(today_payout_amount, 2), + 'today_payout_count': today_payout_count, + 'today_order_count': today_order_count, + 'today_order_amount': round(today_order_amount, 2), + 'today_completed_count': today_completed_count, + 'today_completed_amount': round(today_completed_amount, 2), + 'today_tuikuan_count': today_tuikuan_count, + 'today_tuikuan_amount': round(today_tuikuan_amount, 2), + 'today_chongzhi_count': today_chongzhi_count, + 'today_chongzhi_amount': round(today_chongzhi_amount, 2), + 'today_new_huiyuan': today_new_huiyuan, + 'today_new_huiyuan_amount': round(today_new_huiyuan_amount, 2), + 'all_guanshi_yue': round(all_guanshi_yue, 2), + 'all_dashou_yue': round(all_dashou_yue, 2), + 'all_zuzhang_yue': round(all_zuzhang_yue, 2), + 'all_shangjia_yue': round(all_shangjia_yue, 2), + 'total_income': round(total_income, 2), + 'total_payout': round(total_payout, 2), + 'platform_profit': platform_profit, + 'daily_stats': daily_stats + } + }) + + + + + +class CwhybkhqView(APIView): + """ + 获取所有板块及对应会员列表 + POST /houtai/cwhybkhq + Body: { "phone": "管理员手机号" } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'huiyuancaiwu' not in permissions: + return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403) + + bankuai_list = [] + for bk in Bankuai.query.all(): + members = Huiyuan.query.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id') + bankuai_list.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'members': list(members) + }) + + return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list}) + except Exception as e: + logger.exception("CwhybkhqView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + +class HybkjtsjView(APIView): + """ + 会员详细统计数据 + POST /houtai/hybkjtsj + Body: { + "phone": "管理员手机号", + "huiyuan_id": "会员ID", + "granularity": "day" | "month" | "year", + "year": 2026, + "month": 5, + "include_guanshi": true, + "include_zuzhang": true, + "summary_date": "2026-05-16" + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'huiyuancaiwu' not in permissions: + return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403) + + huiyuan_id = request.data.get('huiyuan_id') + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + include_guanshi = request.data.get('include_guanshi', False) + include_zuzhang = request.data.get('include_zuzhang', False) + summary_date = request.data.get('summary_date') + + if not huiyuan_id: + return Response({'code': 1, 'msg': '缺少会员ID'}, status=400) + + # 🔧 直接使用 date.today() 避免模块名冲突 + now = date.today() + + # ---------- 构造时间范围 ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + start_date = date(year, month, 1) + if month == 12: + end_date = date(year + 1, 1, 1) + else: + end_date = date(year, month + 1, 1) + trunc_func = TruncDate('CreateTime') + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncMonth('CreateTime') + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() + elif granularity == 'year': + year = year or now.year + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncYear('CreateTime') + if not summary_date: + summary_date = str(year) + summary_q = date(year, 1, 1) + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + # 生成 naive datetime(避免 USE_TZ=False 报错) + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date, datetime.min.time()) + + # ---------- 充值过滤 ---------- + chongzhi_filter = { + 'huiyuan_id': huiyuan_id, # 会员ID筛选 + 'leixing': 1, + 'zhuangtai': 3, + 'CreateTime__gte': start_dt, + 'CreateTime__lt': end_dt + } + + # ---------- 分红过滤 ---------- + fenhong_filter = { + 'huiyuan_id': huiyuan_id, # 分红表会员ID筛选 + 'CreateTime__gte': start_dt, + 'CreateTime__lt': end_dt + } + + # 充值分组查询 + chongzhi_qs = filter_queryset_by_club( + Czjilu.query.filter(**chongzhi_filter), request, + ) \ + .annotate(period=trunc_func) \ + .values('period') \ + .annotate( + chongzhi_count=Count('id'), + chongzhi_amount=Sum('jine') + ).order_by('period') + + # 分红分组查询 + fenhong_qs = None + if include_guanshi or include_zuzhang: + fenhong_agg = {} + if include_guanshi: + fenhong_agg['guanshi_amount'] = Sum('fenhong') + if include_zuzhang: + fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong') + fenhong_qs = filter_gsfenhong_qs( + Gsfenhong.query.filter(**fenhong_filter), request, + ) \ + .annotate(period=trunc_func) \ + .values('period') \ + .annotate(**fenhong_agg) \ + .order_by('period') + + # 合并数据 + data_map = {} + for item in chongzhi_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + data_map[period_str] = { + 'chongzhi_count': item['chongzhi_count'], + 'chongzhi_amount': float(item['chongzhi_amount'] or 0) + } + + if fenhong_qs: + for item in fenhong_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + if period_str not in data_map: + data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0} + if include_guanshi: + data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0) + if include_zuzhang: + data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0) + + time_series = [] + for period_str, vals in data_map.items(): + guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0 + zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0 + vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2) + vals['date'] = period_str + time_series.append(vals) + time_series.sort(key=lambda x: x['date']) + + # ---------- 汇总数据 ---------- + summary_chongzhi_filter = chongzhi_filter.copy() + if granularity == 'day': + summary_chongzhi_filter['CreateTime__date'] = summary_q + elif granularity == 'month': + summary_chongzhi_filter['CreateTime__year'] = summary_q.year + summary_chongzhi_filter['CreateTime__month'] = summary_q.month + else: + summary_chongzhi_filter['CreateTime__year'] = summary_q.year + + summary_chongzhi = Czjilu.query.filter(**summary_chongzhi_filter).aggregate( + total_count=Count('id'), + total_amount=Sum('jine') + ) + total_chongzhi_count = summary_chongzhi['total_count'] or 0 + total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0) + + total_guanshi = 0.0 + total_zuzhang = 0.0 + if include_guanshi or include_zuzhang: + summary_fenhong_filter = fenhong_filter.copy() + if granularity == 'day': + summary_fenhong_filter['CreateTime__date'] = summary_q + elif granularity == 'month': + summary_fenhong_filter['CreateTime__year'] = summary_q.year + summary_fenhong_filter['CreateTime__month'] = summary_q.month + else: + summary_fenhong_filter['CreateTime__year'] = summary_q.year + + fenhong_agg = {} + if include_guanshi: + fenhong_agg['guanshi_sum'] = Sum('fenhong') + if include_zuzhang: + fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong') + summary_fenhong = Gsfenhong.query.filter(**summary_fenhong_filter).aggregate(**fenhong_agg) + total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0) + total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0) + + total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': { + 'total_chongzhi_count': total_chongzhi_count, + 'total_chongzhi_amount': total_chongzhi_amount, + 'total_guanshi_fenhong': total_guanshi, + 'total_zuzhang_fenhong': total_zuzhang, + 'total_shouyi': total_shouyi + } + } + }) + except Exception as e: + logger.exception("HybkjtsjView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + + +class SzxxView(APIView): + """ + 每日收支详细统计数据 + POST /houtai/szxx + Body: { + "phone": "管理员手机号", + "granularity": "day" | "month" | "year", + "year": 2026, // day/month 时必填 + "month": 5, // day 时必填 + "summary_date": "2026-05-16" // 可选,格式随颗粒度 + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 避免模块名冲突,局部导入 + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'caiwu' not in permissions: + return Response({'code': 403, 'msg': '无财务查看权限'}, status=403) + + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + summary_date = request.data.get('summary_date') + + now = date.today() + + # ---------- 时间范围与分组字段 ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + # 分组字段 + group_field = 'day' + # 过滤条件 + income_filter = {'year': year, 'month': month} + payout_filter = {'year': year, 'month': month} + # 默认汇总日期 + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_parts = summary_date.split('-') + summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1]), 'day': int(summary_parts[2])} + # 用于构造返回的日期字符串 + date_format = '%Y-%m-%d' + + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + group_field = 'month' + income_filter = {'year': year} + payout_filter = {'year': year} + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_parts = summary_date.split('-') + summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1])} + date_format = '%Y-%m' + + elif granularity == 'year': + group_field = 'year' + income_filter = {} + payout_filter = {} + if not summary_date: + summary_date = str(now.year) + summary_filter = {'year': int(summary_date)} + date_format = '%Y' + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + # ---------- 收入分组查询 ---------- + income_qs = filter_queryset_by_club( + DailyIncomeStat.query.filter(**income_filter), request, + ).values(group_field) \ + .annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \ + .order_by(group_field) + + # ---------- 支出分组查询 ---------- + payout_qs = filter_queryset_by_club( + DailyPayoutStat.query.filter(**payout_filter), request, + ).values(group_field) \ + .annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) \ + .order_by(group_field) + + # ---------- 合并数据 ---------- + data_map = {} + for item in income_qs: + key = item[group_field] + data_map[key] = { + 'income': float(item['total_income'] or 0), + 'income_count': item['total_count'] or 0, + 'payout': 0.0, + 'payout_count': 0 + } + + for item in payout_qs: + key = item[group_field] + if key not in data_map: + data_map[key] = {'income': 0.0, 'income_count': 0, 'payout': 0.0, 'payout_count': 0} + data_map[key]['payout'] = float(item['total_payout'] or 0) + data_map[key]['payout_count'] = item['total_count'] or 0 + + time_series = [] + for key, vals in data_map.items(): + # 构造日期字符串 + if granularity == 'day': + date_str = f"{year}-{month:02d}-{key:02d}" + elif granularity == 'month': + date_str = f"{year}-{key:02d}" + else: + date_str = str(key) + profit = round(vals['income'] - vals['payout'], 2) + time_series.append({ + 'date': date_str, + 'income': vals['income'], + 'payout': vals['payout'], + 'profit': profit + }) + time_series.sort(key=lambda x: x['date']) + + # ---------- 汇总数据 ---------- + income_summary = filter_queryset_by_club( + DailyIncomeStat.query.filter(**summary_filter), request, + ).aggregate( + total_income=Sum('total_amount'), total_count=Sum('total_count') + ) + payout_summary = filter_queryset_by_club( + DailyPayoutStat.query.filter(**summary_filter), request, + ).aggregate( + total_payout=Sum('total_amount'), total_count=Sum('total_count') + ) + total_income = float(income_summary['total_income'] or 0) + total_payout = float(payout_summary['total_payout'] or 0) + total_profit = round(total_income - total_payout, 2) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': { + 'total_income': total_income, + 'total_income_count': income_summary['total_count'] or 0, + 'total_payout': total_payout, + 'total_payout_count': payout_summary['total_count'] or 0, + 'total_profit': total_profit + } + } + }) + + except Exception as e: + logger.exception("SzxxView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + + + + + +class CwddhqlxView(APIView): + """ + 获取订单类型(商品类型)用于筛选 + POST /houtai/cwddhqlx + Body: { "phone": "管理员手机号" } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'dingdancaiwu' not in permissions: + return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403) + + # 获取非审核状态(shenhezhuangtai=1)的商品类型,只返回ID和名称 + types = ShangpinLeixing.query.filter(shenhezhuangtai=1).values('id', 'jieshao') + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': list(types) + }) + except Exception as e: + logger.exception("CwddhqlxView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + +class CwhqjtddsjView(APIView): + """ + 订单详细统计数据(修正版) + POST /houtai/cwhqjtddsj + Body: { + "phone": "管理员手机号", + "granularity": "day" | "month" | "year", + "year": 2026, + "month": 5, + "type_id": 1, # 商品类型ID,0表示全部 + "order_source": 1, # 1=平台单,2=商家单,0=全部 + "summary_date": "2026-05-16" + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'dingdancaiwu' not in permissions: + return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403) + + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + type_id = request.data.get('type_id', 0) + order_source = request.data.get('order_source', 0) + summary_date = request.data.get('summary_date') + + now = date.today() + + # ---------- 时间范围 ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + start_date = date(year, month, 1) + end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) + trunc_func = TruncDate('CreateTime') + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncMonth('CreateTime') + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() + elif granularity == 'year': + year = year or now.year + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncYear('CreateTime') + if not summary_date: + summary_date = str(year) + summary_q = date(year, 1, 1) + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date, datetime.min.time()) + + # ---------- 基础过滤 ---------- + filters = { + 'CreateTime__gte': start_dt, + 'CreateTime__lt': end_dt, + 'Status__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态 + } + if int(type_id) != 0: + filters['ProductTypeID'] = type_id + if int(order_source) in [1, 2]: + filters['Platform'] = order_source + + # ---------- 时间序列分组查询 ---------- + order_base = filter_queryset_by_club( + Order.query.filter(**filters), request, club_field='ClubID', + ) + qs = order_base \ + .annotate(period=trunc_func) \ + .values('period') \ + .annotate( + # 订单总量(所有符合条件的状态) + order_count=Count('id'), + # 订单总额 + order_amount=Sum('Amount'), + # 成交单量 (Status=3) + success_count=Count('id', filter=Q(Status=3)), + # 成交订单总额 + success_amount=Sum('Amount', filter=Q(Status=3)), + # 退款单量 (Status=5) + refund_count=Count('id', filter=Q(Status=5)), + # 打手分成总额(仅成交订单) + PlayerCommission_sum=Sum('PlayerCommission', filter=Q(Status=3)), + # 平台成交订单的店铺分红 + dianpu_fenhong_sum=Sum('pingtai_kuozhan__ShopIncome', + filter=Q(Status=3, Platform=1)), + # 成交订单中:平台单金额、商家单金额、各自打手分成 + platform_success_amount=Sum('Amount', filter=Q(Status=3, Platform=1)), + merchant_success_amount=Sum('Amount', filter=Q(Status=3, Platform=2)), + platform_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)), + merchant_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)), + ).order_by('period') + + time_series = [] + for item in qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + + # 平台收益计算 + platform_profit = (item['platform_success_amount'] or 0) \ + - (item['platform_dashou'] or 0) \ + - (item['dianpu_fenhong_sum'] or 0) + merchant_profit = (item['merchant_success_amount'] or 0) \ + - (item['merchant_dashou'] or 0) + total_profit = platform_profit + merchant_profit + + time_series.append({ + 'date': period_str, + 'order_count': item['order_count'], + 'order_amount': float(item['order_amount'] or 0), + 'success_count': item['success_count'], + 'success_amount': float(item['success_amount'] or 0), + 'refund_count': item['refund_count'], + 'dashou_fencheng': float(item['PlayerCommission_sum'] or 0), + 'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0), + 'profit': round(total_profit, 2) + }) + + # ---------- 汇总数据 ---------- + summary_qs = filter_queryset_by_club( + Order.query.filter(**filters), request, club_field='ClubID', + ) + total_order_count = summary_qs.count() + total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0 + total_success_count = summary_qs.filter(Status=3).count() + total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0 + total_refund_count = summary_qs.filter(Status=5).count() + total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0 + total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate( + s=Sum('pingtai_kuozhan__ShopIncome'))['s'] or 0 + + platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0 + merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0 + platform_dashou_sum = \ + summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0 + merchant_dashou_sum = \ + summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0 + + profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong + profit_merchant = merchant_amount - merchant_dashou_sum + total_profit = profit_platform + profit_merchant + + summary = { + 'total_order_count': total_order_count, + 'total_order_amount': round(float(total_order_amount), 2), + 'total_success_count': total_success_count, + 'total_success_amount': round(float(total_success_amount), 2), + 'total_refund_count': total_refund_count, + 'total_PlayerCommission': round(float(total_dashou), 2), + 'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2), + 'total_profit': round(total_profit, 2) + } + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': summary + } + }) + except Exception as e: + logger.exception("CwhqjtddsjView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + + + + + + + +class CwqtczhqView(APIView): + """ + 其他充值/罚款数据统计(非会员充值) + POST /houtai/cwqtczhq + Body: { + "phone": "管理员手机号", + "granularity": "day" | "month" | "year", + "year": 2026, + "month": 5, // day时必填 + "types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款 + "summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY) + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'qtczcaiwu' not in permissions: + return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403) + + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部 + summary_date = request.data.get('summary_date') + + if isinstance(types, list) and len(types) == 0: + types = [2, 3, 4, 5, 6] + + now = date.today() + + # ---------- 时间范围(用于曲线图) ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + start_date = date(year, month, 1) + end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) + trunc_func = TruncDate('CreateTime') + # 默认汇总日期 + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncMonth('CreateTime') + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() + elif granularity == 'year': + year = year or now.year + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncYear('CreateTime') + if not summary_date: + summary_date = str(year) + summary_q = date(year, 1, 1) + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date, datetime.min.time()) + + # ---------- 构建时间序列 ---------- + time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} } + + def ensure_date(ds): + if ds not in time_series_map: + time_series_map[ds] = {} + return time_series_map[ds] + + # 1. 处理充值记录(Czjilu)—— 完全不变 + cz_types = [t for t in types if t in [2, 3, 4, 5]] + if cz_types: + cz_qs = filter_queryset_by_club( + Czjilu.query.filter( + CreateTime__gte=start_dt, + CreateTime__lt=end_dt, + leixing__in=cz_types, + zhuangtai=3, + ), + request, + ).annotate( + period=trunc_func + ).values('period', 'leixing').annotate( + total_count=Count('id'), + total_amount=Sum('jine') + ).order_by('period', 'leixing') + + for item in cz_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + lx = item['leixing'] + amount = float(item['total_amount'] or 0) + count = item['total_count'] + # 收益:积分(3)收益为金额本身,其他为0 + profit = amount if lx == 3 else 0.0 + + day_data = ensure_date(period_str) + day_data[str(lx)] = { + 'count': count, + 'amount': amount, + 'profit': profit, + 'fenhong_count': 0, # 充值类型无分红 + 'fenhong_amount': 0.0 + } + + # 2. 处理罚款(Penalty)—— 修改:直接从 Penalty 表获取分红总额和分红笔数 + if 6 in types: + # 使用一次聚合查询,同时获取罚款笔数、罚款金额、分红总额、分红笔数 + penalty_qs = filter_penalty_qs( + Penalty.query.filter( + CreateTime__gte=start_dt, + CreateTime__lt=end_dt, + Status=2, + ), + request, + ).annotate( + period=trunc_func + ).values('period').annotate( + penalty_count=Count('id'), + penalty_amount=Sum('FineAmount'), + # 直接使用罚单表中的申请人分红金额字段 + fenhong_amount=Sum('ApplicantBonusAmount'), + fenhong_count=Count('id', filter=Q(ApplicantBonusAmount__gt=0)) + ).order_by('period') + + for item in penalty_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + day_data = ensure_date(period_str) + penalty_amt = float(item['penalty_amount'] or 0) + fenhong_amt = float(item['fenhong_amount'] or 0) + profit = round(penalty_amt - fenhong_amt, 2) + day_data['6'] = { + 'count': item['penalty_count'], + 'amount': penalty_amt, + 'profit': profit, + 'fenhong_count': item['fenhong_count'], + 'fenhong_amount': fenhong_amt + } + + # 构造有序时间序列列表 + time_series = [] + for period_str in sorted(time_series_map.keys()): + entry = {'date': period_str, 'types': {}} + for type_code in sorted(time_series_map[period_str].keys()): + entry['types'][type_code] = time_series_map[period_str][type_code] + time_series.append(entry) + + # ---------- 汇总指定日期(summary)—— 完全保留原逻辑,仅将罚款部分改为从 Penalty 表直接取分红 ---------- + summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0} + + # 汇总充值类型(完全不变) + for lx in cz_types: + filter_kwargs = { + 'leixing': lx, + 'zhuangtai': 3, + } + if granularity == 'day': + filter_kwargs['CreateTime__date'] = summary_q + elif granularity == 'month': + filter_kwargs['CreateTime__year'] = summary_q.year + filter_kwargs['CreateTime__month'] = summary_q.month + else: + filter_kwargs['CreateTime__year'] = summary_q.year + agg = filter_queryset_by_club( + Czjilu.query.filter(**filter_kwargs), request, + ).aggregate(count=Count('id'), amount=Sum('jine')) + count = agg['count'] or 0 + amount = float(agg['amount'] or 0) + profit = amount if lx == 3 else 0.0 + summary_result['by_type'][str(lx)] = { + 'count': count, + 'amount': round(amount, 2), + 'profit': round(profit, 2), + 'fenhong_count': 0, + 'fenhong_amount': 0.0 + } + summary_result['total_count'] += count + summary_result['total_amount'] += amount + summary_result['total_profit'] += profit + + # 汇总罚款(修改:从 Penalty 表直接取分红,不再查询 PenaltyBonus) + if 6 in types: + penalty_filter = { + 'Status': 2, + } + if granularity == 'day': + penalty_filter['CreateTime__date'] = summary_q + elif granularity == 'month': + penalty_filter['CreateTime__year'] = summary_q.year + penalty_filter['CreateTime__month'] = summary_q.month + else: + penalty_filter['CreateTime__year'] = summary_q.year + + # 一次聚合:罚款笔数、金额、分红总额、分红笔数 + agg = filter_penalty_qs( + Penalty.query.filter(**penalty_filter), request, + ).aggregate( + count=Count('id'), + amount=Sum('FineAmount'), + fenhong_amount=Sum('ApplicantBonusAmount'), + fenhong_count=Count('id', filter=Q(ApplicantBonusAmount__gt=0)) + ) + penalty_count = agg['count'] or 0 + penalty_amount = float(agg['amount'] or 0) + fh_amount = float(agg['fenhong_amount'] or 0) + fh_count = agg['fenhong_count'] or 0 + profit = round(penalty_amount - fh_amount, 2) + + summary_result['by_type']['6'] = { + 'count': penalty_count, + 'amount': round(penalty_amount, 2), + 'profit': profit, + 'fenhong_count': fh_count, + 'fenhong_amount': round(fh_amount, 2) + } + summary_result['total_count'] += penalty_count + summary_result['total_amount'] += penalty_amount + summary_result['total_profit'] += profit + + summary_result['total_amount'] = round(summary_result['total_amount'], 2) + summary_result['total_profit'] = round(summary_result['total_profit'], 2) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': summary_result + } + }) + except Exception as e: + logger.exception("CwqtczhqView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + +'''class CwqtczhqView(APIView): + """ + 其他充值/罚款数据统计(非会员充值) + POST /houtai/cwqtczhq + Body: { + "phone": "管理员手机号", + "granularity": "day" | "month" | "year", + "year": 2026, + "month": 5, // day时必填 + "types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款 + "summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY) + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'qtczcaiwu' not in permissions: + return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403) + + granularity = request.data.get('granularity', 'day') + year = request.data.get('year') + month = request.data.get('month') + types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部 + summary_date = request.data.get('summary_date') + + if isinstance(types, list) and len(types) == 0: + types = [2, 3, 4, 5, 6] + + now = date.today() + + # ---------- 时间范围(用于曲线图) ---------- + if granularity == 'day': + if not year or not month: + return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400) + start_date = date(year, month, 1) + end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) + trunc_func = TruncDate('CreateTime') + # 默认汇总日期 + if not summary_date: + summary_date = now.strftime('%Y-%m-%d') + summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() + elif granularity == 'month': + if not year: + return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400) + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncMonth('CreateTime') + if not summary_date: + summary_date = now.strftime('%Y-%m') + summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() + elif granularity == 'year': + year = year or now.year + start_date = date(year, 1, 1) + end_date = date(year + 1, 1, 1) + trunc_func = TruncYear('CreateTime') + if not summary_date: + summary_date = str(year) + summary_q = date(year, 1, 1) + else: + return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) + + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date, datetime.min.time()) + + # ---------- 构建时间序列 ---------- + time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} } + + def ensure_date(ds): + if ds not in time_series_map: + time_series_map[ds] = {} + return time_series_map[ds] + + # 1. 处理充值记录(Czjilu) + cz_types = [t for t in types if t in [2, 3, 4, 5]] + if cz_types: + cz_qs = Czjilu.query.filter( + CreateTime__gte=start_dt, + CreateTime__lt=end_dt, + leixing__in=cz_types, + zhuangtai=3 # 只统计支付成功的 + ).annotate( + period=trunc_func + ).values('period', 'leixing').annotate( + total_count=Count('id'), + total_amount=Sum('jine') + ).order_by('period', 'leixing') + + for item in cz_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + lx = item['leixing'] + amount = float(item['total_amount'] or 0) + count = item['total_count'] + # 收益:积分(3)收益为金额本身,其他为0 + profit = amount if lx == 3 else 0.0 + + day_data = ensure_date(period_str) + day_data[str(lx)] = { + 'count': count, + 'amount': amount, + 'profit': profit, + 'fenhong_count': 0, # 充值类型无分红 + 'fenhong_amount': 0.0 + } + + # 2. 处理罚款(Penalty)—— 包含分红 + if 6 in types: + # 罚款记录 + penalty_qs = Penalty.query.filter( + CreateTime__gte=start_dt, + CreateTime__lt=end_dt, + Status=2 # 已缴纳 + ).annotate( + period=trunc_func + ).values('period').annotate( + penalty_count=Count('id'), + penalty_amount=Sum('FineAmount') + ).order_by('period') + + # 分红记录 + fenhong_qs = PenaltyBonus.query.filter( + CreateTime__gte=start_dt, + CreateTime__lt=end_dt + ).annotate( + period=trunc_func + ).values('period').annotate( + fh_count=Count('id'), + fh_amount=Sum('WithdrawableAmount') + ).order_by('period') + + # 合并 + for item in penalty_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + day_data = ensure_date(period_str) + day_data['6'] = { + 'count': item['penalty_count'], + 'amount': float(item['penalty_amount'] or 0), + 'profit': 0.0, # 先设为0,后续计算 + 'fenhong_count': 0, + 'fenhong_amount': 0.0 + } + + for item in fenhong_qs: + period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \ + item['period'].strftime('%Y-%m') if granularity == 'month' else \ + item['period'].strftime('%Y') + day_data = ensure_date(period_str) + if '6' not in day_data: + day_data['6'] = {'count': 0, 'amount': 0.0, 'profit': 0.0, 'fenhong_count': 0, 'fenhong_amount': 0.0} + day_data['6']['fenhong_count'] += item['fh_count'] + day_data['6']['fenhong_amount'] += float(item['fh_amount'] or 0) + + # 计算罚款收益 = 罚款金额 - 分红金额 + for period_str, day_data in time_series_map.items(): + if '6' in day_data: + penalty = day_data['6'] + penalty['profit'] = round(penalty['amount'] - penalty['fenhong_amount'], 2) + + # 构造有序时间序列列表 + time_series = [] + for period_str in sorted(time_series_map.keys()): + entry = {'date': period_str, 'types': {}} + for type_code in sorted(time_series_map[period_str].keys()): + entry['types'][type_code] = time_series_map[period_str][type_code] + time_series.append(entry) + + # ---------- 汇总指定日期 ---------- + summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0} + + # 汇总充值类型 + for lx in cz_types: + filter_kwargs = { + 'leixing': lx, + 'zhuangtai': 3, + } + if granularity == 'day': + filter_kwargs['CreateTime__date'] = summary_q + elif granularity == 'month': + filter_kwargs['CreateTime__year'] = summary_q.year + filter_kwargs['CreateTime__month'] = summary_q.month + else: + filter_kwargs['CreateTime__year'] = summary_q.year + agg = filter_queryset_by_club( + Czjilu.query.filter(**filter_kwargs), request, + ).aggregate(count=Count('id'), amount=Sum('jine')) + count = agg['count'] or 0 + amount = float(agg['amount'] or 0) + profit = amount if lx == 3 else 0.0 + summary_result['by_type'][str(lx)] = { + 'count': count, + 'amount': round(amount, 2), + 'profit': round(profit, 2), + 'fenhong_count': 0, + 'fenhong_amount': 0.0 + } + summary_result['total_count'] += count + summary_result['total_amount'] += amount + summary_result['total_profit'] += profit + + # 汇总罚款 + if 6 in types: + penalty_filter = { + 'Status': 2, + } + fenhong_filter = {} + if granularity == 'day': + penalty_filter['CreateTime__date'] = summary_q + fenhong_filter['CreateTime__date'] = summary_q + elif granularity == 'month': + penalty_filter['CreateTime__year'] = summary_q.year + penalty_filter['CreateTime__month'] = summary_q.month + fenhong_filter['CreateTime__year'] = summary_q.year + fenhong_filter['CreateTime__month'] = summary_q.month + else: + penalty_filter['CreateTime__year'] = summary_q.year + fenhong_filter['CreateTime__year'] = summary_q.year + + penalty_agg = Penalty.query.filter(**penalty_filter).aggregate( + count=Count('id'), amount=Sum('FineAmount')) + penalty_count = penalty_agg['count'] or 0 + penalty_amount = float(penalty_agg['amount'] or 0) + + fenhong_agg = PenaltyBonus.query.filter(**fenhong_filter).aggregate( + fh_count=Count('id'), fh_amount=Sum('WithdrawableAmount')) + fh_count = fenhong_agg['fh_count'] or 0 + fh_amount = float(fenhong_agg['fh_amount'] or 0) + + profit = round(penalty_amount - fh_amount, 2) + summary_result['by_type']['6'] = { + 'count': penalty_count, + 'amount': round(penalty_amount, 2), + 'profit': profit, + 'fenhong_count': fh_count, + 'fenhong_amount': round(fh_amount, 2) + } + summary_result['total_count'] += penalty_count + summary_result['total_amount'] += penalty_amount + summary_result['total_profit'] += profit + + summary_result['total_amount'] = round(summary_result['total_amount'], 2) + summary_result['total_profit'] = round(summary_result['total_profit'], 2) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'time_series': time_series, + 'summary': summary_result + } + }) + except Exception as e: + logger.exception("CwqtczhqView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)''' + + + + + +# ==================== 后端接口 ==================== +# 文件:views.py (添加以下两个视图类) + + + +class KhpzhqView(APIView): + """ + 获取所有考核配置数据(称号列表、板块、费用) + POST /houtai/khpzhq + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核配置权限'}, status=403) + + bankuai_list = list(Bankuai.query.values('bankuai_id', 'mingcheng')) + + chenghao_qs = Chenghao.query.prefetch_related('kaohecishufeiyong_set').all() + chenghao_list = [] + for ch in chenghao_qs: + # 获取费用:按次数排序,转为 {cishu: feiyong} 字典 + feiyong_dict = { + item.cishu: float(item.feiyong) + for item in ch.kaohecishufeiyong_set.order_by('cishu') + } + max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0 + + chenghao_list.append({ + 'id': ch.id, + 'mingcheng': ch.mingcheng, + 'leixing': ch.leixing, + 'leixing_name': ch.get_leixing_display(), + 'bankuai_id': ch.bankuai_id, + 'tubiao_url': ch.tubiao_url or '', + 'texiao_miaoshu': ch.texiao_miaoshu or '', + 'CreateTime': ch.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if ch.CreateTime else '', + 'kaohe_guize': ch.kaohe_guize or '', + 'kaioi_jinpai': ch.kaioi_jinpai, + 'feiyong': feiyong_dict, # {1:100.0, 2:200.0} + 'max_cishu': max_cishu + }) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'bankuai': bankuai_list, + 'chenghao': chenghao_list + } + }) + except Exception as e: + logger.exception("KhpzhqView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + +class ChzsgcView(APIView): + """ + 称号(标签)增删改查统一接口 + POST /houtai/chzsgc + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核配置权限'}, status=403) + + action = request.data.get('action') + if not action: + return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400) + + # ========== 创建称号 ========== + if action == 'create': + mingcheng = request.data.get('mingcheng', '').strip() + leixing = request.data.get('leixing', 'dashou') + bankuai_id = request.data.get('bankuai_id') + texiao_miaoshu = request.data.get('texiao_miaoshu', '') + kaohe_guize = request.data.get('kaohe_guize', '') + kaioi_jinpai = request.data.get('kaioi_jinpai', False) + feiyong_list = request.data.get('feiyong_list', []) + + if not mingcheng: + return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) + if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']: + return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) + if Chenghao.query.filter(mingcheng=mingcheng).exists(): + return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) + + with transaction.atomic(): + ch = Chenghao.query.create( + mingcheng=mingcheng, + leixing=leixing, + bankuai_id=bankuai_id, + texiao_miaoshu=texiao_miaoshu, + kaohe_guize=kaohe_guize, + kaioi_jinpai=kaioi_jinpai + ) + # 处理费用:前端传来 [{cishu:1, feiyong:100}, ...] + if feiyong_list: + for item in feiyong_list: + cishu = int(item.get('cishu', 1)) + feiyong = float(item.get('feiyong', 0)) + KaoheCishuFeiyong.query.update_or_create( + chenghao=ch, cishu=cishu, + defaults={'feiyong': feiyong} + ) + else: + # 默认添加第一次免费 + KaoheCishuFeiyong.query.create(chenghao=ch, cishu=1, feiyong=0) + + return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}}) + + # ========== 修改称号 ========== + elif action == 'update': + ch_id = request.data.get('chenghao_id') + if not ch_id: + return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) + try: + ch = Chenghao.query.get(id=ch_id) + except Chenghao.DoesNotExist: + return Response({'code': 1, 'msg': '称号不存在'}, status=404) + + mingcheng = request.data.get('mingcheng', ch.mingcheng).strip() + leixing = request.data.get('leixing', ch.leixing) + bankuai_id = request.data.get('bankuai_id', ch.bankuai_id) + texiao_miaoshu = request.data.get('texiao_miaoshu', ch.texiao_miaoshu) + kaohe_guize = request.data.get('kaohe_guize', ch.kaohe_guize) + kaioi_jinpai = request.data.get('kaioi_jinpai', ch.kaioi_jinpai) + + if not mingcheng: + return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) + if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']: + return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) + if Chenghao.query.filter(mingcheng=mingcheng).exclude(id=ch_id).exists(): + return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) + + ch.mingcheng = mingcheng + ch.leixing = leixing + ch.bankuai_id = bankuai_id + ch.texiao_miaoshu = texiao_miaoshu + ch.kaohe_guize = kaohe_guize + ch.kaioi_jinpai = kaioi_jinpai + ch.save() + + # 如果前端传递了 feiyong_list,则全量替换费用 + feiyong_list = request.data.get('feiyong_list') + if feiyong_list is not None: + with transaction.atomic(): + ch.kaohecishufeiyong_set.all().delete() + for item in feiyong_list: + KaoheCishuFeiyong.query.create( + chenghao=ch, + cishu=int(item.get('cishu', 1)), + feiyong=float(item.get('feiyong', 0)) + ) + + return Response({'code': 0, 'msg': '修改成功'}) + + # ========== 删除称号 ========== + elif action == 'delete': + ch_id = request.data.get('chenghao_id') + if not ch_id: + return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) + try: + ch = Chenghao.query.get(id=ch_id) + except Chenghao.DoesNotExist: + return Response({'code': 1, 'msg': '称号不存在'}, status=404) + ch.delete() + return Response({'code': 0, 'msg': '删除成功'}) + + # ========== 添加/修改某次费用 ========== + elif action == 'add_feiyong': + ch_id = request.data.get('chenghao_id') + cishu = request.data.get('cishu') + feiyong = request.data.get('feiyong', 0) + if not ch_id or not cishu: + return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400) + try: + ch = Chenghao.query.get(id=ch_id) + except Chenghao.DoesNotExist: + return Response({'code': 1, 'msg': '称号不存在'}, status=404) + if int(cishu) < 1: + return Response({'code': 1, 'msg': '次数必须>=1'}, status=400) + KaoheCishuFeiyong.query.update_or_create( + chenghao=ch, cishu=cishu, + defaults={'feiyong': feiyong} + ) + return Response({'code': 0, 'msg': '添加/修改费用成功'}) + + # ========== 删除某次费用 ========== + elif action == 'remove_feiyong': + ch_id = request.data.get('chenghao_id') + cishu = request.data.get('cishu') + if not ch_id or not cishu: + return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400) + if int(cishu) == 1: + return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400) + try: + ch = Chenghao.query.get(id=ch_id) + except Chenghao.DoesNotExist: + return Response({'code': 1, 'msg': '称号不存在'}, status=404) + KaoheCishuFeiyong.query.filter(chenghao=ch, cishu=cishu).delete() + return Response({'code': 0, 'msg': '删除成功'}) + + # ========== 修改某次费用(等价于add) ========== + elif action == 'update_feiyong': + return self.add_feiyong(request) # 复用逻辑 + + else: + return Response({'code': 1, 'msg': '未知操作'}, status=400) + + except Exception as e: + logger.exception("ChzsgcView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + +# ==================== 后端 views.py ==================== +# 请确保已有以下导入,如果没有请在文件头部添加 + + + + +class KhgglView(APIView): + """ + 考核官管理数据接口(列表、筛选、分页) + POST /houtai/khggl + Body: { + "phone": "管理员手机号", + "page": 1, // 可选,默认1 + "page_size": 15, // 可选,默认15 + "yonghuid": "1234567", // 按用户ID筛选,可选 + "zhuangtai": 1, // 账号状态 0=禁用,1=正常,可选 + "shenhe_zhuangtai": 0, // 审核官状态 0=空闲,1=审核中,可选 + "bankuai_name": "王者" // 板块名称模糊筛选,可选 + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) + + # 分页参数 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 15)) + if page < 1: page = 1 + if page_size < 1: page_size = 15 + + # 筛选条件 + filter_yonghuid = request.data.get('yonghuid', '').strip() + filter_zhuangtai = request.data.get('zhuangtai') + filter_shenhe_zhuangtai = request.data.get('shenhe_zhuangtai') + filter_bankuai_name = request.data.get('bankuai_name', '').strip() + + # 基础查询:所有审核官,预加载用户及打手扩展表 + queryset = filter_user_related_by_club( + UserShenheguan.query.select_related('user__DashouProfile'), + request, + ) + + if filter_yonghuid: + queryset = queryset.filter(user__UserUID=filter_yonghuid) + if filter_zhuangtai is not None: + queryset = queryset.filter(zhuangtai=filter_zhuangtai) + if filter_shenhe_zhuangtai is not None: + queryset = queryset.filter(shenhe_zhuangtai=filter_shenhe_zhuangtai) + + # 如果填写了板块名称,筛选出关联该板块的审核官 + if filter_bankuai_name: + bankuai_ids = Bankuai.query.filter( + mingcheng__icontains=filter_bankuai_name + ).values_list('bankuai_id', flat=True) + shenheguan_ids = KaoheguanBankuai.query.filter( + bankuai_id__in=bankuai_ids + ).values_list('kaoheguan_id', flat=True) + queryset = queryset.filter(user__UserUID__in=shenheguan_ids) + + total = queryset.count() + start = (page - 1) * page_size + end = start + page_size + shenheguan_list = queryset.order_by('-CreateTime')[start:end] + + from jituan.services.shenhe_club import yonghu_chenghao_qs_for_request + + # ---------- 预计算每个板块的打手数量 ---------- + bankuai_dashou_count_map = {} + all_banquai = Bankuai.query.all() + for bk in all_banquai: + tag_ids = Chenghao.query.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True) + count = yonghu_chenghao_qs_for_request( + YonghuChenghao.query.filter( + chenghao_id__in=tag_ids, + yonghu__DashouProfile__isnull=False, + ), + request, + ).values('yonghu').distinct().count() + bankuai_dashou_count_map[bk.bankuai_id] = count + + # ---------- 新增:板块标签统计 ---------- + bankuai_tags_data = [] + for bk in all_banquai: + tags = Chenghao.query.filter(bankuai=bk, leixing='dashou') + tag_list = [] + for tag in tags: + user_count = yonghu_chenghao_qs_for_request( + YonghuChenghao.query.filter(chenghao=tag), + request, + ).values('yonghu').distinct().count() + tag_list.append({ + 'id': tag.id, + 'name': tag.mingcheng, + 'user_count': user_count, + }) + bankuai_tags_data.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'tags': tag_list, + }) + + # 构造返回数据(审核官列表) + data_list = [] + for sg in shenheguan_list: + user = sg.user + # 获取该审核官关联的板块 + bk_qs = KaoheguanBankuai.query.filter(kaoheguan_id=user.UserUID).select_related('bankuai') + bankuai_info = [] + for bk in bk_qs: + bid = bk.bankuai.bankuai_id + bankuai_info.append({ + 'bankuai_id': bid, + 'mingcheng': bk.bankuai.mingcheng, + 'dashou_count': bankuai_dashou_count_map.get(bid, 0) + }) + + dashou_nick = user.DashouProfile.nicheng if hasattr(user, 'DashouProfile') and user.DashouProfile else '' + + data_list.append({ + 'yonghuid': user.UserUID, + 'avatar': user.Avatar or '', + 'phone': user.Phone or '', + 'zhuangtai': sg.zhuangtai, + 'shenhe_zhuangtai': sg.shenhe_zhuangtai, + 'shenhe_zongshu': sg.shenhe_zongshu, + 'tongguo_zongshu': sg.tongguo_zongshu, + 'yue': float(sg.yue), + 'zonge': float(sg.zonge), + 'CreateTime': sg.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.CreateTime else '', + 'UpdateTime': sg.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.UpdateTime else '', + 'bankuai': bankuai_info, + 'nicheng': dashou_nick, + 'is_renzheng': sg.is_renzheng, + }) + + all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng')) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'total': total, + 'page': page, + 'page_size': page_size, + 'list': data_list, + 'all_bankuai': all_bankuai, + 'bankuai_tags': bankuai_tags_data, # 新增板块标签统计 + } + }) + + except Exception as e: + logger.exception("KhgglView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + +class ShgxgsjView(APIView): + """ + 审核官信息修改(余额、状态、板块关联) + POST /houtai/shgxgsj + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) + + yonghuid = request.data.get('yonghuid') + if not yonghuid: + return Response({'code': 1, 'msg': '缺少用户ID'}, status=400) + + try: + shenheguan = UserShenheguan.query.select_related('user').get(user__UserUID=yonghuid) + except UserShenheguan.DoesNotExist: + return Response({'code': 1, 'msg': '审核官不存在'}, status=404) + + action = request.data.get('action') + + if action == 'update_info': + if 'yue' in request.data: + shenheguan.yue = float(request.data.get('yue', 0)) + if 'zhuangtai' in request.data: + zt = int(request.data.get('zhuangtai')) + if zt not in [0, 1]: + return Response({'code': 1, 'msg': '无效的账号状态'}, status=400) + shenheguan.zhuangtai = zt + if 'shenhe_zhuangtai' in request.data: + szt = int(request.data.get('shenhe_zhuangtai')) + if szt not in [0, 1]: + return Response({'code': 1, 'msg': '无效的审核官状态'}, status=400) + shenheguan.shenhe_zhuangtai = szt + # 新增认证状态编辑 + if 'is_renzheng' in request.data: + shenheguan.is_renzheng = bool(request.data.get('is_renzheng')) + shenheguan.save() + return Response({'code': 0, 'msg': '修改成功'}) + + elif action == 'add_bankuai': + bankuai_id = request.data.get('bankuai_id') + if not bankuai_id: + return Response({'code': 1, 'msg': '缺少板块ID'}, status=400) + if not Bankuai.query.filter(bankuai_id=bankuai_id).exists(): + return Response({'code': 1, 'msg': '板块不存在'}, status=400) + if KaoheguanBankuai.query.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists(): + return Response({'code': 1, 'msg': '已关联该板块'}, status=400) + KaoheguanBankuai.query.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id) + return Response({'code': 0, 'msg': '板块关联成功'}) + + elif action == 'remove_bankuai': + bankuai_id = request.data.get('bankuai_id') + if not bankuai_id: + return Response({'code': 1, 'msg': '缺少板块ID'}, status=400) + deleted, _ = KaoheguanBankuai.query.filter( + kaoheguan_id=yonghuid, bankuai_id=bankuai_id + ).delete() + if not deleted: + return Response({'code': 1, 'msg': '未找到该板块关联'}, status=400) + return Response({'code': 0, 'msg': '板块移除成功'}) + + else: + return Response({'code': 1, 'msg': '未知操作'}, status=400) + + except Exception as e: + logger.exception("ShgxgsjView 接口错误") + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + + +class ZxkfghdsView(APIView): + """ + 客服更换打手接口 + 路径:POST /houtai/zxkfghds + 功能: + 1. 将当前订单状态改为已退款(5),释放接单打手(若有) + 2. 如需跨平台通知,则调用对方平台退款通知接口 + 3. 基于原订单信息生成全新订单(状态直接为 1,已替老板填写) + 4. 生成新的派单链接,并标记为已使用 + 5. 新链接记录上一个链接ID和上一个订单ID + 6. 【关键】不操作商家余额、退款次数、每日统计,因新订单是替换而非新增扣款 + 权限:需要'003aa'(跨平台管理)权限,与退款接口一致 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + # ========== 1. 参数提取 ========== + username = request.data.get('username', '').strip() + OrderID = pick_order_id(request.data) + if not username or not OrderID: + logger.warning("更换打手参数不完整") + return Response({'code': 400, 'msg': '参数不完整'}) + + # ========== 2. 权限校验(与跨平台退款相同) ========== + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions # 返回错误响应 + if '003aa' not in permissions: + return Response({'code': 403, 'msg': '您无权处理订单'}) + + current_user = request.user + + # ========== 3. 查询原订单及关联信息 ========== + try: + old_order = Order.query.select_related('shangjia_kuozhan').get(OrderID=OrderID) + except Order.DoesNotExist: + logger.warning(f"订单不存在: {OrderID}") + return Response({'code': 404, 'msg': '订单不存在'}) + + # 仅允许状态为 2(进行中)或 8(结算中)更换打手 + if old_order.Status not in [1,7,2, 8]: + return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'}) + + # 本接口仅处理商家发单(Platform=2) + if old_order.Platform != 2: + return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'}) + + # 获取原链接(用于模板ID等) + try: + old_lianjie = ShangjiaLianjie.query.get(OrderID=OrderID) + except ShangjiaLianjie.DoesNotExist: + logger.error(f"原链接记录不存在,订单: {OrderID}") + return Response({'code': 500, 'msg': '链接数据异常'}) + + # 商家信息(仅用于记录关联,不操作余额) + try: + shangjia_ext = old_order.shangjia_kuozhan + shangjia_id = shangjia_ext.MerchantID + # 获取商家昵称用于新订单扩展表 + shangjia_user = User.query.get(UserUID=shangjia_id) + shangjia_nicheng = shangjia_user.ShopProfile.nicheng or f"商家{shangjia_id}" + except (ObjectDoesNotExist, UserShangjia.DoesNotExist): + logger.error(f"商家信息缺失,订单: {OrderID}") + return Response({'code': 500, 'msg': '商家数据异常'}) + + # ========== 4. 退款处理(仅打手侧,不碰商家) ========== + with transaction.atomic(): + # 5.1 订单状态改为已退款 + old_order.Status = 5 + old_order.AssignedCS = current_user.Phone + old_order.save() + + # 5.2 处理接单打手(若有) + dashou_id = old_order.PlayerID + if dashou_id: + try: + dashou_user = User.query.get(UserUID=dashou_id) + dashou_profile = dashou_user.DashouProfile + dashou_profile.tuikuanliang += 1 # 退款次数+1 + dashou_profile.zhuangtai = 1 # 设为空闲 + dashou_profile.save() + except (User.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"打手 {dashou_id} 不存在,跳过更新") + + # 5.3 更新退款记录表 + RefundRecord.query.update_or_create( + OrderID=OrderID, + defaults={ + 'PlayerID': dashou_id or '', + 'ApplicantID': '', + 'ProcessorID': current_user.Phone, + 'Reason': '客服更换打手退款', + 'ApplyStatus': 1, + 'Amount': old_order.Amount or 0, + 'PlayerCommission': old_order.PlayerCommission or 0, + 'Description': old_order.Description or '', + 'Remark': '更换打手自动退款', + 'Nickname': old_order.Nickname or '', + } + ) + + # 5.4 打手每日统计(退款) + if dashou_id: + try: + update_dashou_daily_by_action( + yonghuid=dashou_id, + amount=old_order.PlayerCommission, + action=3 # 3=退款 + ) + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + + # 5.5 客服处理统计(管理员无 KefuProfile,跳过) + if hasattr(kefu, 'jinrichuli'): + kefu.jinrichuli += 1 + kefu.jinyuechuli += 1 + kefu.zongchuli += 1 + kefu.save() + + # 至此,原订单已退款,事务提交 + + # ========== 6. 生成新订单和链接(不扣商家余额) ========== + try: + with transaction.atomic(): + # ---- 6.1 生成新的订单ID和Token ---- + def generate_OrderID(): + timestamp = int(time.time() * 1000) + process_id = os.getpid() % 1000 + random_num = random.randint(1000, 9999) + return f"SJ{timestamp}{process_id:03d}{random_num}" + + def generate_secure_token(dd_id, uid): + timestamp = int(time.time() * 1_000_000) + salt = secrets.token_hex(8) + raw = f"{timestamp}:{dd_id}:{uid}:{salt}" + token = hashlib.sha256(raw.encode()).hexdigest()[:24] + if ShangjiaLianjie.query.filter(LinkToken=token).exists(): + return generate_secure_token(dd_id, uid) + return token + + def generate_link_url(token): + base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/') + return f"{base}/order/{token}" + + new_OrderID = generate_OrderID() + secure_token = generate_secure_token(new_OrderID, shangjia_id) + + # ---- 6.2 创建新订单(复制核心数据,状态直接为1) ---- + new_order = Order.query.create( + OrderID=new_OrderID, + Status=1, # 下单中 + Platform=2, + Amount=old_order.Amount, + PlayerCommission=old_order.PlayerCommission, + ProductTypeID=old_order.ProductTypeID, + GrabRequirement=old_order.GrabRequirement, + MembershipID=old_order.MembershipID, + CommissionReq=old_order.CommissionReq, + Description=old_order.Description, + Nickname=old_order.Nickname, # 继承游戏昵称 + Remark=old_order.Remark, + User1ID=old_order.User1ID, # 商家标识 + ImageURL=old_order.ImageURL, + ClubID=getattr(old_order, 'ClubID', None) or 'xq', + # 注意:不继承 AssignedID、PlayerID + ) + + # ---- 6.3 创建商家扩展表 ---- + MerchantOrderExt.query.create( + Order=new_order, + MerchantID=shangjia_id, + MerchantNickname=shangjia_nicheng, + ) + + # ---- 6.4 生成新链接并关联旧链接信息 ---- + link_url = generate_link_url(secure_token) + new_lianjie = ShangjiaLianjie.query.create( + UserID=shangjia_id, + ProductTypeID=old_lianjie.ProductTypeID, + TemplateID=old_lianjie.TemplateID, + LinkURL=link_url, + LinkToken=secure_token, + OrderID=new_OrderID, + is_used=True, # 直接标记已使用 + ExpireTime=timezone.now() + timezone.timedelta(days=1), + PreviousLinkID=old_lianjie.id, # 记录旧链接ID + PreviousOrderID=old_order.OrderID, # 记录旧订单ID + ) + + # ---- 6.5 异步通知(广播新订单、跨平台同步) ---- + try: + order_info = { + 'OrderID': new_OrderID, + 'game_type': self._get_game_type_name(new_order.ProductTypeID), + 'amount': str(new_order.Amount), + 'order_desc': (new_order.Description or '')[:50], + } + dingdan_guangbo.delay(order_info) + except Exception as e: + logger.error(f"广播通知失败: {e}") + + # 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单 + + logger.info(f"更换打手成功:旧订单{OrderID}→退款,新订单{new_OrderID}已生成") + + return Response({ + 'code': 0, + 'msg': '更换成功', + 'data': { + 'new_OrderID': new_OrderID, + 'new_dingdan_id': new_OrderID, + 'new_lianjie': link_url, + } + }) + + except Exception as e: + logger.error(f"生成新订单事务失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '生成新订单失败'}) + + def _get_game_type_name(self, ProductTypeID): + """根据类型ID获取游戏类型名称""" + try: + gt = ShangpinLeixing.query.filter(id=ProductTypeID).first() + return gt.jieshao if gt else '游戏订单' + except Exception: + return '游戏订单' + + +# ==================== 考核记录管理 ==================== + +SHENHE_ZHUANGTAI_MAP = { + 0: '考核中', + 1: '已通过', + 2: '未通过', + 3: '待审核', +} + + +def _batch_user_profile_map(user_uids): + """批量获取用户头像、昵称、手机号""" + uids = list({u for u in user_uids if u}) + if not uids: + return {} + users = User.query.filter(UserUID__in=uids) + dashou_nicks = { + d.user.UserUID: d.nicheng + for d in UserDashou.query.filter(user__UserUID__in=uids).select_related('user') + } + result = {} + for u in users: + result[u.UserUID] = { + 'yonghuid': u.UserUID, + 'avatar': u.Avatar or '', + 'phone': u.Phone or '', + 'nicheng': dashou_nicks.get(u.UserUID) or u.UserName or u.Phone or u.UserUID, + } + return result + + +def _build_shenhe_jilu_filters(request_data): + """构造 ShenheJilu 查询条件(列表与统计共用)""" + qs = ShenheJilu.query.select_related('bankuai', 'chenghao') + + jilu_id = (request_data.get('jilu_id') or '').strip() + if jilu_id: + qs = qs.filter(jilu_id=jilu_id) + + zhuangtai = request_data.get('zhuangtai') + if zhuangtai is not None and zhuangtai != '': + qs = qs.filter(zhuangtai=int(zhuangtai)) + + assessed = request_data.get('assessed') + if assessed == 'done': + qs = qs.filter(zhuangtai__in=[1, 2]) + elif assessed == 'undone': + qs = qs.filter(zhuangtai__in=[0, 3]) + + bankuai_id = request_data.get('bankuai_id') + if bankuai_id not in (None, ''): + qs = qs.filter(bankuai_id=int(bankuai_id)) + + chenghao_id = request_data.get('chenghao_id') + if chenghao_id not in (None, ''): + qs = qs.filter(chenghao_id=int(chenghao_id)) + + cishu = request_data.get('cishu') + if cishu not in (None, ''): + qs = qs.filter(cishu=int(cishu)) + + dashou_kw = (request_data.get('dashou_keyword') or '').strip() + if dashou_kw: + dashou_uids = list( + User.query.filter( + Q(UserUID=dashou_kw) | + Q(UserName__icontains=dashou_kw) | + Q(Phone__icontains=dashou_kw) | + Q(DashouProfile__nicheng__icontains=dashou_kw) + ).values_list('UserUID', flat=True) + ) + if dashou_uids: + qs = qs.filter(shenqingren_id__in=dashou_uids) + else: + qs = qs.none() + + kg_kw = (request_data.get('kaoheguan_keyword') or '').strip() + if kg_kw: + kg_uids = list( + User.query.filter( + Q(UserUID=kg_kw) | + Q(UserName__icontains=kg_kw) | + Q(Phone__icontains=kg_kw) | + Q(DashouProfile__nicheng__icontains=kg_kw) + ).values_list('UserUID', flat=True) + ) + if kg_uids: + qs = qs.filter(shenheguan_id__in=kg_uids) + else: + qs = qs.none() + + start_time = (request_data.get('start_time') or '').strip() + end_time = (request_data.get('end_time') or '').strip() + if start_time: + qs = qs.filter(CreateTime__gte=start_time) + if end_time: + qs = qs.filter(CreateTime__lte=end_time) + + has_kaoheguan = request_data.get('has_kaoheguan') + if has_kaoheguan in (True, 'true', '1', 1): + qs = qs.exclude(shenheguan_id__isnull=True).exclude(shenheguan_id='') + elif has_kaoheguan in (False, 'false', '0', 0): + qs = qs.filter(Q(shenheguan_id__isnull=True) | Q(shenheguan_id='')) + + return qs + + +def _serialize_shenhe_jilu_row(jl, profile_map, fee_map=None, reject_count_map=None): + dashou = profile_map.get(jl.shenqingren_id, { + 'yonghuid': jl.shenqingren_id, + 'avatar': '', + 'phone': '', + 'nicheng': jl.shenqingren_id, + }) + kaoheguan = None + if jl.shenheguan_id: + kaoheguan = profile_map.get(jl.shenheguan_id, { + 'yonghuid': jl.shenheguan_id, + 'avatar': '', + 'phone': '', + 'nicheng': jl.shenheguan_id, + }) + kaoheguan['game_id'] = jl.shenheguan_youxi_id or '' + + current_fee = 0.0 + if fee_map is not None: + current_fee = fee_map.get((jl.jilu_id, jl.cishu), 0.0) + else: + fd = KaoheJiluFeiyong.query.filter(jilu_id=jl.jilu_id, cishu=jl.cishu).first() + if fd: + current_fee = float(fd.jine) + + reject_count = 0 + if reject_count_map is not None: + reject_count = reject_count_map.get(jl.jilu_id, 0) + + return { + 'jilu_id': jl.jilu_id, + 'zhuangtai': jl.zhuangtai, + 'zhuangtai_text': SHENHE_ZHUANGTAI_MAP.get(jl.zhuangtai, str(jl.zhuangtai)), + 'cishu': jl.cishu, + 'jiaofei_jine': float(jl.jiaofei_jine or 0), + 'current_fee': current_fee, + 'bankuai_id': jl.bankuai.bankuai_id if jl.bankuai else None, + 'bankuai_name': jl.bankuai.mingcheng if jl.bankuai else '', + 'chenghao_id': jl.chenghao.id if jl.chenghao else None, + 'chenghao_name': jl.chenghao.mingcheng if jl.chenghao else '', + 'chenghao_icon': jl.chenghao.tubiao_url if jl.chenghao else '', + 'guize_neirong': jl.guize_neirong or '', + 'dashou': { + **dashou, + 'game_id': jl.dashou_youxi_id or '', + 'beizhu': jl.dashou_beizhu or '', + }, + 'kaoheguan': kaoheguan, + 'reject_count': reject_count, + 'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.CreateTime else '', + 'UpdateTime': jl.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.UpdateTime else '', + } + + +class KhjlglView(APIView): + """ + 考核记录列表 + 统计 + POST /houtai/khjlgl + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) + + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + if page < 1: + page = 1 + if page_size < 1: + page_size = 20 + if page_size > 100: + page_size = 100 + + from jituan.services.shenhe_club import filter_shenhe_jilu_by_request + + filter_data = request.data + qs = filter_shenhe_jilu_by_request( + _build_shenhe_jilu_filters(filter_data), + request, + ) + + # FluentQuery.filter() 会原地修改同一对象;统计与列表必须各自独立查询 + stats_base = filter_shenhe_jilu_by_request( + _build_shenhe_jilu_filters(filter_data), + request, + ) + agg = stats_base.aggregate(total_fee=Sum('jiaofei_jine')) + tag_rows = filter_shenhe_jilu_by_request( + _build_shenhe_jilu_filters(filter_data), + request, + ).values( + 'chenghao_id', 'chenghao__mingcheng' + ).annotate( + total=Count('jilu_id'), + passed=Count('jilu_id', filter=Q(zhuangtai=1)), + failed=Count('jilu_id', filter=Q(zhuangtai=2)), + in_progress=Count('jilu_id', filter=Q(zhuangtai=0)), + pending=Count('jilu_id', filter=Q(zhuangtai=3)), + ).order_by('-total') + + stats = { + 'total': stats_base.count(), + 'passed': stats_base.filter(zhuangtai=1).count(), + 'failed': stats_base.filter(zhuangtai=2).count(), + 'in_progress': stats_base.filter(zhuangtai=0).count(), + 'pending': stats_base.filter(zhuangtai=3).count(), + 'total_fee': float(agg['total_fee'] or 0), + 'tag_stats': [ + { + 'chenghao_id': r['chenghao_id'], + 'chenghao_name': r['chenghao__mingcheng'] or '未知标签', + 'total': r['total'], + 'passed': r['passed'], + 'failed': r['failed'], + 'in_progress': r['in_progress'], + 'pending': r['pending'], + } + for r in tag_rows if r['chenghao_id'] + ], + } + + total = qs.count() + start = (page - 1) * page_size + rows = qs.order_by('-CreateTime')[start:start + page_size].to_list() + + uids = [] + jilu_ids = [] + for jl in rows: + uids.append(jl.shenqingren_id) + if jl.shenheguan_id: + uids.append(jl.shenheguan_id) + jilu_ids.append(jl.jilu_id) + profile_map = _batch_user_profile_map(uids) + + fee_pairs = [(jl.jilu_id, jl.cishu) for jl in rows] + fee_map = {} + if fee_pairs: + jids = [p[0] for p in fee_pairs] + for fd in KaoheJiluFeiyong.query.filter(jilu_id__in=jids): + fee_map[(fd.jilu_id, fd.cishu)] = float(fd.jine) + + reject_counts = { + r['jilu_id']: r['cnt'] + for r in KaoheJujueJilu.query.filter(jilu_id__in=jilu_ids) + .values('jilu_id') + .annotate(cnt=Count('id')) + .all() + } if jilu_ids else {} + + data_list = [ + _serialize_shenhe_jilu_row(jl, profile_map, fee_map, reject_counts) + for jl in rows + ] + + all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng')) + all_tags = list( + Chenghao.query.filter(leixing='dashou') + .values('id', 'mingcheng', 'bankuai_id') + .order_by('mingcheng') + ) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'total': total, + 'page': page, + 'page_size': page_size, + 'list': data_list, + 'stats': stats, + 'all_bankuai': all_bankuai, + 'all_tags': all_tags, + }, + }) + except Exception as e: + logger.exception('KhjlglView 接口错误') + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + +class KhjlczView(APIView): + """ + 考核记录操作 / 详情 + POST /houtai/khjlcz + action: + - get_detail: 获取单条详情(含缴费明细、拒绝历史) + - to_pending: 考核中 → 待审核(取消指定考核官,与小程序 transfer 一致) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + username_from_frontend = request.data.get('phone', None) + kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) + if kefu_obj is None: + return permissions + if 'kaohepeizhi' not in permissions: + return Response({'code': 403, 'msg': '无考核管理权限'}, status=403) + + action = request.data.get('action') + jilu_id = (request.data.get('jilu_id') or '').strip() + if not jilu_id: + return Response({'code': 1, 'msg': '缺少记录ID'}, status=400) + + if action == 'get_detail': + try: + jl = ShenheJilu.query.select_related('bankuai', 'chenghao').get(jilu_id=jilu_id) + except ShenheJilu.DoesNotExist: + return Response({'code': 1, 'msg': '记录不存在'}, status=404) + + uids = [jl.shenqingren_id] + if jl.shenheguan_id: + uids.append(jl.shenheguan_id) + profile_map = _batch_user_profile_map(uids) + + fee_list = [ + { + 'cishu': f.cishu, + 'jine': float(f.jine), + } + for f in KaoheJiluFeiyong.query.filter(jilu_id=jilu_id).order_by('cishu') + ] + reject_list = [] + for r in KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'): + kg_prof = _batch_user_profile_map([r.shenheguan_id]).get(r.shenheguan_id, {}) + reject_list.append({ + 'cishu': r.cishu, + 'shenheguan_id': r.shenheguan_id, + 'shenheguan_nicheng': kg_prof.get('nicheng', r.shenheguan_id), + 'yuanyin': r.yuanyin, + 'CreateTime': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '', + }) + + record = _serialize_shenhe_jilu_row(jl, profile_map) + record['fee_list'] = fee_list + record['reject_list'] = reject_list + return Response({'code': 0, 'msg': 'ok', 'data': record}) + + if action == 'to_pending': + with transaction.atomic(): + jl = ShenheJilu.objects.select_for_update().filter(jilu_id=jilu_id).first() + if not jl: + return Response({'code': 1, 'msg': '记录不存在'}, status=404) + if jl.zhuangtai != 0: + return Response({'code': 1, 'msg': '仅「考核中」的记录可退回待审核'}, status=400) + + old_kg_id = jl.shenheguan_id + jl.shenheguan_id = None + jl.shenheguan_youxi_id = None + jl.zhuangtai = 3 + jl.save(update_fields=['shenheguan_id', 'shenheguan_youxi_id', 'zhuangtai', 'UpdateTime']) + + if old_kg_id: + UserShenheguan.query.filter(user__UserUID=old_kg_id).update(shenhe_zhuangtai=0) + + return Response({'code': 0, 'msg': '已退回待审核,考核官指定已取消'}) + + return Response({'code': 1, 'msg': '无效的操作类型'}, status=400) + + except Exception as e: + logger.exception('KhjlczView 接口错误') + return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) + + + + + + + + + + + diff --git a/jituan/management/commands/seed_super_admin.py b/jituan/management/commands/seed_super_admin.py index a09b6be..918e726 100644 --- a/jituan/management/commands/seed_super_admin.py +++ b/jituan/management/commands/seed_super_admin.py @@ -1,15 +1,33 @@ """将指定手机号设为超级管理员(功能权限 + 集团视图)。""" +import uuid + from django.core.management.base import BaseCommand from django.db import transaction +from django.utils import timezone +from gvsdsdk.models import Role, Permission, RolePermission, UserRole from jituan.constants import SUPER_ADMIN_PHONES from users.business_models import User class Command(BaseCommand): - help = '将 SUPER_ADMIN_PHONES 中的账号设为 IsSuperuser + UserType=admin' + help = '将 SUPER_ADMIN_PHONES 设为 IsSuperuser,并绑定「管理员」角色(含 000001)' def handle(self, *args, **options): + admin_role = Role.objects.filter(RoleName='管理员', RoleStatus=1).first() + super_perm = Permission.objects.filter(PermCode='000001', PermStatus=1).first() + if admin_role and super_perm: + if not RolePermission.objects.filter( + RoleUUID=admin_role.RoleUUID, PermUUID=super_perm.PermUUID, + ).exists(): + RolePermission.objects.create( + RolePermUUID=uuid.uuid4().bytes, + RoleUUID=admin_role.RoleUUID, + PermUUID=super_perm.PermUUID, + CreateTime=timezone.now(), + ) + self.stdout.write(self.style.SUCCESS('已为「管理员」角色绑定 000001 权限')) + updated = 0 for phone in SUPER_ADMIN_PHONES: users = list(User.query.filter(Phone=phone)) @@ -20,10 +38,16 @@ class Command(BaseCommand): with transaction.atomic(): user.IsSuperuser = True user.IsStaff = True - if hasattr(user, 'UserType'): - # UserType 为 property 从角色推断,尽量写 gvsdsdk 角色 - pass user.save(update_fields=['IsSuperuser', 'IsStaff']) + if admin_role and not UserRole.objects.filter( + UserUUID=user.UserUUID, RoleUUID=admin_role.RoleUUID, + ).exists(): + UserRole.objects.create( + UserRoleUUID=uuid.uuid4().bytes, + UserUUID=user.UserUUID, + RoleUUID=admin_role.RoleUUID, + CreateTime=timezone.now(), + ) updated += 1 self.stdout.write(self.style.SUCCESS( f'已设置超管: {phone} (UserUID={user.UserUID})' diff --git a/jituan/services/club_bootstrap.py b/jituan/services/club_bootstrap.py new file mode 100644 index 0000000..5f4974d --- /dev/null +++ b/jituan/services/club_bootstrap.py @@ -0,0 +1,104 @@ +"""创建新俱乐部并从模板复制配置。""" +from decimal import Decimal + +from django.db import transaction + +from jituan.constants import CLUB_ID_DEFAULT +from jituan.models import Club, ClubHuiyuanPrice, ClubLilubiao, ClubTixianQuota, ClubWithdrawConfig +from jituan.services.display_config import copy_display_config +from jituan.services.szjilu_accounting import normalize_szjilu_club_id +from config.models import Szjilu + + +def bootstrap_club_from_template( + new_id: str, + name: str, + wx_appid: str = '', + template_club_id: str = CLUB_ID_DEFAULT, + extra_fields: dict = None, +): + """ + 从模板俱乐部复制支付/会员价/利率等配置,创建新俱乐部。 + 返回新建的 Club 实例。 + """ + new_id = (new_id or '').strip() + name = (name or '').strip() + template_id = normalize_szjilu_club_id(template_club_id) + extra_fields = extra_fields or {} + + if not new_id or len(new_id) > 16: + raise ValueError('club_id 无效') + if not name: + raise ValueError('俱乐部名称不能为空') + if new_id == template_id: + raise ValueError('新 club_id 不能与模板相同') + if Club.query.filter(club_id=new_id).exists(): + raise ValueError(f'俱乐部 {new_id} 已存在') + + template = Club.query.get(club_id=template_id) + + with transaction.atomic(): + club = Club.query.create( + club_id=new_id, + name=name, + status=int(extra_fields.get('status', 1)), + wx_appid=wx_appid or extra_fields.get('wx_appid') or template.wx_appid, + wx_secret=extra_fields.get('wx_secret', template.wx_secret), + mch_id=extra_fields.get('mch_id', template.mch_id), + pay_app_id=extra_fields.get('pay_app_id') or wx_appid or template.pay_app_id or template.wx_appid, + api_v3_key=extra_fields.get('api_v3_key', template.api_v3_key), + cert_serial_no=extra_fields.get('cert_serial_no', template.cert_serial_no), + private_key_path=extra_fields.get('private_key_path', template.private_key_path), + platform_cert_dir=extra_fields.get('platform_cert_dir', template.platform_cert_dir), + official_appid=extra_fields.get('official_appid', template.official_appid), + official_secret=extra_fields.get('official_secret', template.official_secret), + official_token=extra_fields.get('official_token', template.official_token), + encoding_aes_key=extra_fields.get('encoding_aes_key', template.encoding_aes_key), + template_id=extra_fields.get('template_id', template.template_id), + template_max_per_minute=extra_fields.get( + 'template_max_per_minute', template.template_max_per_minute, + ), + h5_domain=extra_fields.get('h5_domain', template.h5_domain), + oss_prefix=extra_fields.get('oss_prefix') or new_id, + goeasy_appkey=extra_fields.get('goeasy_appkey', template.goeasy_appkey), + goeasy_secret=extra_fields.get('goeasy_secret', template.goeasy_secret), + config_json=dict(template.config_json or {}), + sort_order=int(extra_fields.get('sort_order', template.sort_order + 1)), + ) + + for row in ClubHuiyuanPrice.query.filter(club_id=template_id): + ClubHuiyuanPrice.query.create( + club_id=new_id, + huiyuan_id=row.huiyuan_id, + jiage=row.jiage or Decimal('0'), + guanshifc=row.guanshifc or Decimal('0'), + zuzhangfc=row.zuzhangfc or Decimal('0'), + is_enabled=row.is_enabled, + bankuai_id=row.bankuai_id, + ) + + for row in ClubLilubiao.query.filter(club_id=template_id): + ClubLilubiao.query.create( + club_id=new_id, + config_type=row.config_type, + lilv=row.lilv or Decimal('0'), + remark=row.remark or '', + ) + + try: + tpl_wd = ClubWithdrawConfig.query.get(club_id=template_id) + ClubWithdrawConfig.query.create(club_id=new_id, mode=tpl_wd.mode) + except ClubWithdrawConfig.DoesNotExist: + ClubWithdrawConfig.query.create(club_id=new_id, mode=1) + + for row in ClubTixianQuota.query.filter(club_id=template_id): + ClubTixianQuota.query.create( + club_id=new_id, + leixing=row.leixing, + daily_limit=row.daily_limit or Decimal('0'), + ) + + Szjilu.objects.get_or_create(club_id=new_id) + copy_display_config(template_id, new_id) + + return club diff --git a/jituan/views.py b/jituan/views.py index 8acecf0..8f8d8b1 100644 --- a/jituan/views.py +++ b/jituan/views.py @@ -10,7 +10,7 @@ from rest_framework.views import APIView from rest_framework.throttling import AnonRateThrottle from rest_framework_simplejwt.tokens import RefreshToken -from jituan.constants import SUPER_ADMIN_PHONES, ADMIN_ROLE_LABELS, CLUB_ID_DEFAULT +from jituan.constants import SUPER_ADMIN_PHONES, ADMIN_ROLE_LABELS, CLUB_ID_DEFAULT, DATA_SCOPE_ALL, DATA_SCOPE_SINGLE from jituan.models import Club, AdminAssignment from jituan.services.admin_context import build_admin_club_context from jituan.services.wechat_login import login_or_register_by_club @@ -196,10 +196,43 @@ class ClubMeContextView(APIView): class ClubAdminAssignmentListView(APIView): - """POST /jituan/houtai/admin-assignments — 查看数据范围任职(非 gvsdsdk 功能权限)。""" + """POST /jituan/houtai/admin-assignments — 数据范围任职(非 gvsdsdk 功能权限)。""" + permission_classes = [IsAuthenticated] parser_classes = [JSONParser] + def _is_super(self, user): + return ( + bool(user.IsSuperuser) + or user.UserType == 'admin' + or getattr(user, 'Phone', '') in SUPER_ADMIN_PHONES + ) + + def _list_rows(self, assignments): + club_name_map = {c.club_id: c.name for c in Club.query.filter(status=1)} + uids = list({a.yonghuid for a in assignments}) + user_map = {} + if uids: + for u in User.query.filter(UserUID__in=uids).only('UserUID', 'Phone', 'UserName'): + user_map[u.UserUID] = u.Phone or u.UserName or u.UserUID + rows = [] + for a in assignments: + club_label = '集团(全部子公司)' + if a.club_id: + club_label = club_name_map.get(a.club_id, a.club_id) + rows.append({ + 'id': a.id, + 'yonghuid': a.yonghuid, + 'phone': user_map.get(a.yonghuid, ''), + 'club_id': a.club_id, + 'club_label': club_label, + 'role_code': a.role_code, + 'role_name': ADMIN_ROLE_LABELS.get(a.role_code, a.role_code), + 'data_scope': a.data_scope, + 'is_primary': a.is_primary, + }) + return rows + def post(self, request): try: username = (request.data.get('phone') or request.data.get('username') or '').strip() @@ -210,57 +243,103 @@ class ClubAdminAssignmentListView(APIView): return permissions user = request.user - is_super = ( - bool(user.IsSuperuser) - or user.UserType == 'admin' - or getattr(user, 'Phone', '') in SUPER_ADMIN_PHONES - ) + is_super = self._is_super(user) + action = (request.data.get('action') or 'list').strip() + + if action in ('create', 'update', 'delete'): + if not is_super or '000001' not in permissions: + return Response({'code': 403, 'msg': '仅超级管理员可维护任职数据'}, status=403) + + if action == 'create': + yonghuid = (request.data.get('yonghuid') or '').strip() + phone = (request.data.get('target_phone') or '').strip() + if not yonghuid and phone: + target = User.query.filter(Phone=phone).first() + if not target: + return Response({'code': 404, 'msg': f'未找到手机号 {phone}'}) + yonghuid = target.UserUID + if not yonghuid: + return Response({'code': 400, 'msg': '需要 yonghuid 或 target_phone'}) + club_id = request.data.get('club_id') + if club_id is not None and str(club_id).strip() == '': + club_id = None + elif club_id is not None: + club_id = str(club_id).strip() + role_code = (request.data.get('role_code') or 'CLUB_ADMIN').strip() + data_scope = (request.data.get('data_scope') or DATA_SCOPE_SINGLE).strip() + if club_id is None: + data_scope = DATA_SCOPE_ALL + is_primary = bool(request.data.get('is_primary', False)) + if AdminAssignment.query.filter( + yonghuid=yonghuid, club_id=club_id, role_code=role_code, + ).exists(): + return Response({'code': 400, 'msg': '该任职记录已存在'}) + AdminAssignment.query.create( + yonghuid=yonghuid, + club_id=club_id, + role_code=role_code, + data_scope=data_scope, + is_primary=is_primary, + granted_by=user.UserUID, + status=1, + ) + return Response({'code': 0, 'msg': '任职已添加'}) + + if action == 'delete': + assignment_id = request.data.get('id') + if not assignment_id: + return Response({'code': 400, 'msg': '缺少任职 id'}) + row = AdminAssignment.query.filter(id=assignment_id).first() + if not row: + return Response({'code': 404, 'msg': '任职记录不存在'}) + row.status = 0 + row.save(update_fields=['status', 'UpdateTime']) + return Response({'code': 0, 'msg': '任职已停用'}) + + if action == 'update': + assignment_id = request.data.get('id') + if not assignment_id: + return Response({'code': 400, 'msg': '缺少任职 id'}) + row = AdminAssignment.query.filter(id=assignment_id, status=1).first() + if not row: + return Response({'code': 404, 'msg': '任职记录不存在'}) + if 'role_code' in request.data: + row.role_code = (request.data.get('role_code') or '').strip() or row.role_code + if 'data_scope' in request.data: + row.data_scope = request.data.get('data_scope') or row.data_scope + if 'is_primary' in request.data: + row.is_primary = bool(request.data.get('is_primary')) + if 'club_id' in request.data: + cid = request.data.get('club_id') + row.club_id = None if cid is None or str(cid).strip() == '' else str(cid).strip() + if row.club_id is None: + row.data_scope = DATA_SCOPE_ALL + row.granted_by = user.UserUID + row.save() + return Response({'code': 0, 'msg': '任职已更新'}) + if is_super: qs = AdminAssignment.query.filter(status=1).order_by('yonghuid', '-is_primary', 'club_id') else: qs = AdminAssignment.query.filter(yonghuid=user.UserUID, status=1).order_by('-is_primary') - assignments = qs.to_list() - club_name_map = { - c.club_id: c.name for c in Club.query.filter(status=1) - } - uids = list({a.yonghuid for a in assignments}) - user_map = {} - if uids: - for u in User.query.filter(UserUID__in=uids).only('UserUID', 'Phone', 'UserName'): - user_map[u.UserUID] = u.Phone or u.UserName or u.UserUID - - rows = [] - for a in assignments: - club_label = '集团(全部子公司)' - if a.club_id: - club_label = club_name_map.get(a.club_id, a.club_id) - rows.append({ - 'yonghuid': a.yonghuid, - 'phone': user_map.get(a.yonghuid, ''), - 'club_id': a.club_id, - 'club_label': club_label, - 'role_code': a.role_code, - 'role_name': ADMIN_ROLE_LABELS.get(a.role_code, a.role_code), - 'data_scope': a.data_scope, - 'is_primary': a.is_primary, - }) - return Response({ 'code': 0, 'msg': 'ok', 'data': { - 'list': rows, + 'list': self._list_rows(qs.to_list()), + 'can_manage': is_super and '000001' in permissions, 'perm_note': ( - '功能菜单权限(能进哪些页面)在「角色管理」配置;' - '本表 admin_assignment 仅控制集团/子公司数据范围。' + '【功能权限】在「角色管理」里给账号绑 gvsdsdk 角色(perm_code 如 caiwu、002ab);' + '【数据范围】在本页维护 admin_assignment(能看哪个俱乐部/集团汇总)。' + '两套独立,超管需有 000001 权限码。' ), 'current_context': build_admin_club_context(user), }, }) except Exception: logger.exception('ClubAdminAssignmentListView 异常') - return Response({'code': 99, 'msg': '加载任职数据失败,请确认已执行 migrate jituan'}, status=500) + return Response({'code': 99, 'msg': '任职数据操作失败,请确认已执行 migrate jituan'}, status=500) class ClubManageView(APIView): @@ -350,7 +429,28 @@ class ClubManageView(APIView): 'data': self._serialize(club, full=True), }) - return Response({'code': 400, 'msg': '无效 action,支持 list/get/update'}) + if action == 'create': + from jituan.services.club_bootstrap import bootstrap_club_from_template + new_club_id = (request.data.get('new_club_id') or request.data.get('club_id') or '').strip() + name = (request.data.get('name') or '').strip() + if not new_club_id or not name: + return Response({'code': 400, 'msg': '新建俱乐部需要 new_club_id 与 name'}) + try: + club = bootstrap_club_from_template( + new_club_id, + name, + wx_appid=(request.data.get('wx_appid') or '').strip(), + template_club_id=(request.data.get('from_club') or CLUB_ID_DEFAULT).strip(), + ) + except ValueError as exc: + return Response({'code': 400, 'msg': str(exc)}) + return Response({ + 'code': 0, + 'msg': '俱乐部创建成功', + 'data': self._serialize(club, full=True), + }) + + return Response({'code': 400, 'msg': '无效 action,支持 list/get/update/create'}) except Exception: logger.exception('ClubManageView 异常') return Response({'code': 99, 'msg': '俱乐部配置操作失败'}, status=500)