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 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']) # 获取多次分红配置(按次数和会员组织,按俱乐部隔离) from jituan.services.club_penalty import resolve_gsfenhong_club_id guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid) duoci_records = DuociFenhong.query.filter( yonghuid=yonghuid, club_id=guanshi_club, ).order_by('cishu') custom_first = {} # 首次分红定制(cishu=1) extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_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.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)'}) from jituan.services.group_finance_gate import deny_group_finance_exec deny = deny_group_finance_exec(request.user, request) if deny: return deny 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) # 批量预取当前所有首次定制记录(避免循环内 N+1) existing_first_list = list( DuociFenhong.query.filter( club_id=guanshi_club, yonghuid=yonghuid, cishu=1, ) ) _existing_map = {r.huiyuan: r for r in existing_first_list} _to_create = [] _to_update = [] _to_delete_ids = [] # 前端传来的定制字典 {会员ID: 金额} 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.guanshi_fenhong = amount rec.zuzhang_fenhong = Decimal('0') _to_update.append(rec) else: _to_create.append(DuociFenhong( club_id=guanshi_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1, guanshi_fenhong=amount, zuzhang_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, ['guanshi_fenhong', 'zuzhang_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 guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid) # 收集需要保留的记录 keep_set = set() # 解析前端数据,先汇总 keep_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('guanshi_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=guanshi_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.guanshi_fenhong = amount rec.zuzhang_fenhong = Decimal('0') _to_update.append(rec) else: _to_create.append(DuociFenhong( club_id=guanshi_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=cishu, guanshi_fenhong=amount, zuzhang_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, ['guanshi_fenhong', 'zuzhang_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_GUANSHI, qitashuoming=( f'【后台】修改管事分红策略:{";".join(dividend_parts) or "配置已更新"},' f'管事ID={yonghuid},操作人={operator}' ), ) return Response({'code': 0, 'msg': '修改成功'})