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_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request from jituan.services.catalog_scope import block_non_group_catalog_scope 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, has_merchant_order_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'] 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, 'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '', } # 余额提现相关 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']) # 获取多次分红配置(按次数和会员组织,按俱乐部隔离) from jituan.services.club_penalty import resolve_gsfenhong_club_id zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid) duoci_records = DuociFenhong.query.filter( yonghuid=yonghuid, club_id=zuzhang_club, ).order_by('cishu') custom_first = {} # 首次分红定制(cishu=1) extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } } # 批量预取会员信息 _huiyuan_ids = [r.huiyuan for r in duoci_records] _huiyuan_map = { h.huiyuan_id: h for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao', 'jiage') } for record in duoci_records: # 获取会员信息 member = _huiyuan_map.get(record.huiyuan) 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)'}) from jituan.services.group_finance_gate import deny_balance_add_exec deny = deny_balance_add_exec( request, permissions=permissions, target_user=user, ) if deny: return deny 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']) from jituan.services.club_context import resolve_club_id_from_request club_id = resolve_club_id_from_request(request) or '' 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},俱乐部={club_id},操作人={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) # 批量预取当前所有首次定制记录(避免循环内 N+1) existing_first_list = list( DuociFenhong.query.filter( club_id=zuzhang_club, yonghuid=yonghuid, cishu=1, ) ) _existing_map = {r.huiyuan: r for r in existing_first_list} _to_create = [] _to_update = [] _to_delete_ids = [] for huiyuan_id, amount in custom_first.items(): try: amount = Decimal(str(amount)) except: continue if amount <= 0: # 删除定制(恢复默认) rec = _existing_map.get(huiyuan_id) if rec is not None: _to_delete_ids.append(rec.id) else: rec = _existing_map.get(huiyuan_id) if rec is not None: rec.zuzhang_fenhong = amount rec.guanshi_fenhong = Decimal('0') # 组长表只关心组长分红 _to_update.append(rec) else: _to_create.append(DuociFenhong( club_id=zuzhang_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1, zuzhang_fenhong=amount, guanshi_fenhong=Decimal('0'), )) # 删除那些在前端字典中不存在的首次定制记录(即已取消定制) for rec in existing_first_list: if rec.huiyuan not in custom_first and rec.id not in _to_delete_ids: _to_delete_ids.append(rec.id) if _to_create: DuociFenhong.objects.bulk_create(_to_create) if _to_update: DuociFenhong.objects.bulk_update(_to_update, ['zuzhang_fenhong', 'guanshi_fenhong']) if _to_delete_ids: DuociFenhong.objects.filter(id__in=_to_delete_ids).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() _extra_targets = [] # [(cishu, huiyuan_id, amount)] 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)) _extra_targets.append((cishu, huiyuan_id, amount)) # 批量预取已有额外次数记录 existing_extra_list = list( DuociFenhong.query.filter( club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2, ) ) _extra_map = {(r.cishu, r.huiyuan): r for r in existing_extra_list} _to_create = [] _to_update = [] _to_delete_ids = [] for cishu, huiyuan_id, amount in _extra_targets: rec = _extra_map.get((cishu, huiyuan_id)) if rec is not None: rec.zuzhang_fenhong = amount rec.guanshi_fenhong = Decimal('0') _to_update.append(rec) else: _to_create.append(DuociFenhong( club_id=zuzhang_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=cishu, zuzhang_fenhong=amount, guanshi_fenhong=Decimal('0'), )) # 删除不在 keep_set 中的额外次数记录 for rec in existing_extra_list: if (rec.cishu, rec.huiyuan) not in keep_set: _to_delete_ids.append(rec.id) if _to_create: DuociFenhong.objects.bulk_create(_to_create) if _to_update: DuociFenhong.objects.bulk_update(_to_update, ['zuzhang_fenhong', 'guanshi_fenhong']) if _to_delete_ids: DuociFenhong.objects.filter(id__in=_to_delete_ids).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}, })