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
836 lines
37 KiB
Python
836 lines
37 KiB
Python
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_group_finance_exec
|
||
deny = deny_group_finance_exec(request.user, request)
|
||
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'])
|
||
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)
|
||
# 批量预取当前所有首次定制记录(避免循环内 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},
|
||
})
|
||
|
||
|