Files
Django/backend/views/guanli.py
TermiNexus f1c8633345 perf: P1 性能优化批量修复 + kefu 视图拆分
P1 性能优化(避免循环内 N+1 / N 次 DB 查询):
- shop_order_views._batch_images: 移除 ThreadPoolExecutor 掩盖的 N 次查询,改单次批量 + Python 侧分组
- product_query.DashouHuiyuanList: 5N 查询 -> 3 次批量预取 + 本地闭包判断
- roles.GetRolePermissionView / 用户列表: 循环内 RolePermission/Permission/UserRole/Role -> 批量 __in 预取
- guanli / zuzhang 杜次分红 4.2/4.3: 循环内 update_or_create + 单条 delete -> bulk_create + bulk_update + 批量 delete
- admin_config QQ 群配置: 循环内 exists/first/save/create -> 批量预取 + bulk_create + bulk_update
- jituan 服务层多处合并统计查询、批量预取 map
- rank 多处循环 N+1 改批量预取
- backend 多处循环内 count/create 改批量
- config/orders/merchant_ops 多处循环 N+1 改批量预取

其他改动:
- users/views/kefu.py 拆分为 kefu_base/kefu_dashou/kefu_orders/kefu_punishment/kefu_withdraw 5 个文件
- 删除遗留脚本 check_prod_uid.py / create_rbac_tables.sql
2026-07-05 23:17:38 +08:00

861 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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. 余额修改4400c4400b----------
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': '修改成功'})