fix: 集团任职硬删除,杜绝 NULL 重复冒出多条
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"""紧急封杀后台账号:摘超管、停全部任职、禁用客服、改密、清角色。
|
||||
"""紧急封杀后台账号:硬删任职、摘超管、禁用客服、改密、清角色。
|
||||
|
||||
用法(服务器):
|
||||
用法:
|
||||
python manage.py nuke_kefu_admin 13837714110
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
@@ -8,14 +8,14 @@ from django.utils import timezone
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '紧急封杀指定手机号的后台权限(非交互)'
|
||||
help = '紧急封杀指定手机号的后台权限(硬删除任职)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('phone', type=str, help='要封杀的手机号')
|
||||
parser.add_argument(
|
||||
'--also-whitelist',
|
||||
action='store_true',
|
||||
help='连代码白名单手机号也允许封(默认拒绝封白名单)',
|
||||
help='连代码白名单手机号也允许封',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
@@ -27,40 +27,33 @@ class Command(BaseCommand):
|
||||
from jituan.constants import SUPER_ADMIN_PHONES
|
||||
if phone in SUPER_ADMIN_PHONES and not options['also_whitelist']:
|
||||
self.stderr.write(
|
||||
f'{phone} 在 SUPER_ADMIN_PHONES 白名单,拒绝封杀。'
|
||||
f'确要封再加 --also-whitelist'
|
||||
f'{phone} 在白名单,拒绝。确要封加 --also-whitelist'
|
||||
)
|
||||
return
|
||||
|
||||
from users.business_models import User
|
||||
from jituan.models import AdminAssignment
|
||||
from gvsdsdk.models import UserRole as SdkUserRole
|
||||
import bcrypt
|
||||
|
||||
users = list(User.objects.filter(Phone=phone))
|
||||
if not users:
|
||||
self.stderr.write(f'未找到手机号 {phone}')
|
||||
self.stderr.write(f'未找到 {phone}')
|
||||
return
|
||||
|
||||
import bcrypt
|
||||
from gvsdsdk.models import UserRole as SdkUserRole
|
||||
|
||||
new_pwd = f'LOCKED_{phone}_REVOKED'
|
||||
for u in users:
|
||||
self.stdout.write(
|
||||
f'处理 UserUID={u.UserUID} IsSuperuser={u.IsSuperuser} IsStaff={u.IsStaff}'
|
||||
)
|
||||
self.stdout.write(f'HIT uid={u.UserUID} super={u.IsSuperuser}')
|
||||
u.IsSuperuser = False
|
||||
u.IsStaff = False
|
||||
u.SetPassword(new_pwd)
|
||||
u.save()
|
||||
|
||||
n_asg = AdminAssignment.objects.filter(yonghuid=u.UserUID).update(
|
||||
status=0,
|
||||
UpdateTime=timezone.now(),
|
||||
)
|
||||
self.stdout.write(f' 任职停用行数={n_asg}')
|
||||
n, _ = AdminAssignment.objects.filter(yonghuid=u.UserUID).delete()
|
||||
self.stdout.write(f' 硬删任职={n}')
|
||||
|
||||
n_role = SdkUserRole.objects.filter(UserUUID=u.UserUUID).delete()
|
||||
self.stdout.write(f' 功能角色删除={n_role}')
|
||||
self.stdout.write(f' 删功能角色={n_role}')
|
||||
|
||||
try:
|
||||
kefu = u.KefuProfile
|
||||
@@ -69,11 +62,15 @@ class Command(BaseCommand):
|
||||
b'LOCKED_REVOKED', bcrypt.gensalt(rounds=12)
|
||||
).decode('utf-8')
|
||||
kefu.save(update_fields=['zhuangtai', 'erjimima', 'UpdateTime'])
|
||||
self.stdout.write(' 客服状态已禁用 + 二级密码已废')
|
||||
self.stdout.write(' 客服已禁用')
|
||||
except Exception as e:
|
||||
self.stdout.write(f' 无客服扩展或跳过: {e}')
|
||||
self.stdout.write(f' 无客服扩展: {e}')
|
||||
|
||||
# 顺手清全库 NULL 俱乐部垃圾重复
|
||||
from jituan.services.admin_assignments import dedupe_active_assignments
|
||||
d = dedupe_active_assignments()
|
||||
self.stdout.write(f'全库去重硬删重复={d}')
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'已封杀 {phone}(共 {len(users)} 个账号)。对方旧 token 需重新登录才会彻底失效;'
|
||||
f'主密码已改为 LOCKED_{phone}_REVOKED,勿外传,事后请再改。'
|
||||
f'已封杀 {phone}。密码={new_pwd}。请 restart gunicorn。'
|
||||
))
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""admin_assignment 任职 CRUD(修复停用不生效、重复插入、IsSuperuser 绕过)。"""
|
||||
"""admin_assignment 任职 CRUD。
|
||||
|
||||
集团任职用 club_id=''(禁止 NULL):MySQL 对 NULL 唯一约束失效,会无限复制,
|
||||
导致「停一条冒出五六条」。停用改为硬删除。
|
||||
"""
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -14,10 +18,19 @@ GROUP_ROLE_CODES = frozenset({
|
||||
})
|
||||
|
||||
|
||||
def normalize_club_id(club_id):
|
||||
"""集团级统一成空串,禁止 NULL(避免唯一索引失效)。"""
|
||||
if club_id is None:
|
||||
return ''
|
||||
s = str(club_id).strip()
|
||||
return s
|
||||
|
||||
|
||||
def _club_filter(qs, club_id):
|
||||
if club_id is None or club_id == '':
|
||||
cid = normalize_club_id(club_id)
|
||||
if not cid:
|
||||
return qs.filter(Q(club_id__isnull=True) | Q(club_id=''))
|
||||
return qs.filter(club_id=club_id)
|
||||
return qs.filter(club_id=cid)
|
||||
|
||||
|
||||
def find_assignment(yonghuid, club_id, role_code):
|
||||
@@ -46,56 +59,7 @@ def _clear_other_primary(yonghuid, except_id=None):
|
||||
clear_other_primary_assignments(yonghuid, except_id=except_id)
|
||||
|
||||
|
||||
def create_or_reactivate_assignment(
|
||||
yonghuid,
|
||||
club_id,
|
||||
role_code,
|
||||
data_scope,
|
||||
is_primary,
|
||||
granted_by,
|
||||
):
|
||||
active = find_active_assignment(yonghuid, club_id, role_code)
|
||||
if active:
|
||||
return None, '该任职记录已存在且有效'
|
||||
|
||||
if is_primary:
|
||||
_clear_other_primary(yonghuid)
|
||||
|
||||
inactive_qs = _club_filter(
|
||||
AdminAssignment.objects.filter(yonghuid=yonghuid, role_code=role_code).exclude(status=1),
|
||||
club_id,
|
||||
).order_by('-id')
|
||||
inactive = inactive_qs.first()
|
||||
if inactive:
|
||||
if is_primary:
|
||||
_clear_other_primary(yonghuid)
|
||||
inactive_qs.exclude(pk=inactive.pk).update(status=0, UpdateTime=timezone.now())
|
||||
AdminAssignment.objects.filter(pk=inactive.id).update(
|
||||
status=1,
|
||||
data_scope=data_scope,
|
||||
is_primary=is_primary,
|
||||
granted_by=granted_by,
|
||||
UpdateTime=timezone.now(),
|
||||
)
|
||||
return inactive.id, None
|
||||
|
||||
row = AdminAssignment.query.create(
|
||||
yonghuid=yonghuid,
|
||||
club_id=club_id,
|
||||
role_code=role_code,
|
||||
data_scope=data_scope,
|
||||
is_primary=is_primary,
|
||||
granted_by=granted_by,
|
||||
status=1,
|
||||
)
|
||||
return row.id, None
|
||||
|
||||
|
||||
def _strip_system_super_if_needed(yonghuid):
|
||||
"""
|
||||
停用集团权限后:非白名单账号必须摘掉 IsSuperuser。
|
||||
否则 build_admin_club_context 仍把 is_group_admin=True,停用等于没停。
|
||||
"""
|
||||
yonghuid = (yonghuid or '').strip()
|
||||
if not yonghuid:
|
||||
return
|
||||
@@ -108,31 +72,62 @@ def _strip_system_super_if_needed(yonghuid):
|
||||
return
|
||||
if (user.Phone or '') in SUPER_ADMIN_PHONES:
|
||||
return
|
||||
if user.IsSuperuser:
|
||||
if user.IsSuperuser or user.IsStaff:
|
||||
user.IsSuperuser = False
|
||||
user.save(update_fields=['IsSuperuser'])
|
||||
user.IsStaff = False
|
||||
user.save(update_fields=['IsSuperuser', 'IsStaff'])
|
||||
|
||||
|
||||
def deactivate_all_group_assignments_for_user(yonghuid):
|
||||
"""停用该用户全部集团级任职(空俱乐部 / ALL_CLUBS / GROUP_* 角色)。"""
|
||||
yonghuid = (yonghuid or '').strip()
|
||||
if not yonghuid:
|
||||
return 0
|
||||
qs = AdminAssignment.objects.filter(yonghuid=yonghuid, status=1).filter(
|
||||
Q(club_id__isnull=True)
|
||||
| Q(club_id='')
|
||||
| Q(data_scope=DATA_SCOPE_ALL)
|
||||
| Q(role_code__in=list(GROUP_ROLE_CODES))
|
||||
def create_or_reactivate_assignment(
|
||||
yonghuid,
|
||||
club_id,
|
||||
role_code,
|
||||
data_scope,
|
||||
is_primary,
|
||||
granted_by,
|
||||
):
|
||||
club_id = normalize_club_id(club_id)
|
||||
if not club_id:
|
||||
data_scope = DATA_SCOPE_ALL
|
||||
|
||||
# 创建前先清同键垃圾(含 NULL/空串/status=0 重复)
|
||||
junk = _club_filter(
|
||||
AdminAssignment.objects.filter(yonghuid=yonghuid, role_code=role_code),
|
||||
club_id,
|
||||
)
|
||||
active = junk.filter(status=1).order_by('-id')
|
||||
if active.exists():
|
||||
keep = active.first()
|
||||
junk.exclude(pk=keep.pk).delete()
|
||||
return None, '该任职记录已存在且有效'
|
||||
|
||||
junk.delete()
|
||||
|
||||
if is_primary:
|
||||
_clear_other_primary(yonghuid)
|
||||
|
||||
row = AdminAssignment.objects.create(
|
||||
yonghuid=yonghuid,
|
||||
club_id=club_id,
|
||||
role_code=role_code,
|
||||
data_scope=data_scope,
|
||||
is_primary=is_primary,
|
||||
granted_by=granted_by or '',
|
||||
status=1,
|
||||
)
|
||||
return row.id, None
|
||||
|
||||
|
||||
def _is_group_level(row):
|
||||
return (
|
||||
not normalize_club_id(row.club_id)
|
||||
or (row.data_scope or '') == DATA_SCOPE_ALL
|
||||
or (row.role_code or '') in GROUP_ROLE_CODES
|
||||
)
|
||||
return qs.update(status=0, UpdateTime=timezone.now())
|
||||
|
||||
|
||||
def deactivate_assignment(assignment_id):
|
||||
"""
|
||||
停用任职。
|
||||
- 同人同角色同俱乐部(含 NULL 重复行)全部停
|
||||
- 若是集团级任职:再停掉该人全部集团任职,并摘掉非白名单 IsSuperuser
|
||||
"""
|
||||
"""停用=硬删除。集团级则删光该人全部集团任职并摘 IsSuperuser。"""
|
||||
try:
|
||||
pk = int(assignment_id)
|
||||
except (TypeError, ValueError):
|
||||
@@ -140,45 +135,49 @@ def deactivate_assignment(assignment_id):
|
||||
|
||||
row = AdminAssignment.objects.filter(pk=pk).first()
|
||||
if not row:
|
||||
return False, '任职记录不存在'
|
||||
# 可能已被删,幂等成功
|
||||
return True, None
|
||||
|
||||
qs = AdminAssignment.objects.filter(
|
||||
yonghuid=row.yonghuid,
|
||||
role_code=row.role_code,
|
||||
status=1,
|
||||
)
|
||||
qs = _club_filter(qs, row.club_id)
|
||||
qs.update(status=0, UpdateTime=timezone.now())
|
||||
yonghuid = row.yonghuid
|
||||
role_code = row.role_code
|
||||
club_id = row.club_id
|
||||
group_level = _is_group_level(row)
|
||||
|
||||
is_group_level = (
|
||||
not (row.club_id or '').strip()
|
||||
or (row.data_scope or '') == DATA_SCOPE_ALL
|
||||
or (row.role_code or '') in GROUP_ROLE_CODES
|
||||
)
|
||||
if is_group_level:
|
||||
deactivate_all_group_assignments_for_user(row.yonghuid)
|
||||
_strip_system_super_if_needed(row.yonghuid)
|
||||
# 同人同角色同俱乐部(含 NULL/'')全部硬删
|
||||
_club_filter(
|
||||
AdminAssignment.objects.filter(yonghuid=yonghuid, role_code=role_code),
|
||||
club_id,
|
||||
).delete()
|
||||
|
||||
if group_level:
|
||||
AdminAssignment.objects.filter(yonghuid=yonghuid).filter(
|
||||
Q(club_id__isnull=True)
|
||||
| Q(club_id='')
|
||||
| Q(data_scope=DATA_SCOPE_ALL)
|
||||
| Q(role_code__in=list(GROUP_ROLE_CODES))
|
||||
).delete()
|
||||
_strip_system_super_if_needed(yonghuid)
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
def deactivate_all_assignments_for_user(yonghuid):
|
||||
"""账号禁用时:停用该用户全部有效任职,并摘掉非白名单 IsSuperuser。"""
|
||||
"""账号禁用:硬删全部任职 + 摘超管。"""
|
||||
yonghuid = (yonghuid or '').strip()
|
||||
if not yonghuid:
|
||||
return 0
|
||||
n = AdminAssignment.objects.filter(yonghuid=yonghuid, status=1).update(
|
||||
status=0,
|
||||
UpdateTime=timezone.now(),
|
||||
)
|
||||
deleted, _ = AdminAssignment.objects.filter(yonghuid=yonghuid).delete()
|
||||
_strip_system_super_if_needed(yonghuid)
|
||||
return n
|
||||
return deleted
|
||||
|
||||
|
||||
def dedupe_active_assignments():
|
||||
"""停用重复的有效任职,每组 (yonghuid, club_id, role_code) 只保留一条。"""
|
||||
"""硬删重复任职;并把 club_id=NULL 归一成 ''。"""
|
||||
# 归一 NULL -> ''
|
||||
AdminAssignment.objects.filter(club_id__isnull=True).update(club_id='')
|
||||
|
||||
rows = list(
|
||||
AdminAssignment.query.filter(status=1).order_by('yonghuid', 'club_id', 'role_code', 'id')
|
||||
AdminAssignment.objects.filter(status=1).order_by('yonghuid', 'club_id', 'role_code', 'id')
|
||||
)
|
||||
seen = {}
|
||||
dup_ids = []
|
||||
@@ -189,8 +188,5 @@ def dedupe_active_assignments():
|
||||
else:
|
||||
seen[key] = row.id
|
||||
if dup_ids:
|
||||
AdminAssignment.objects.filter(pk__in=dup_ids).update(
|
||||
status=0,
|
||||
UpdateTime=timezone.now(),
|
||||
)
|
||||
AdminAssignment.objects.filter(pk__in=dup_ids).delete()
|
||||
return len(dup_ids)
|
||||
|
||||
@@ -382,13 +382,13 @@ class ClubAdminAssignmentListView(APIView):
|
||||
if not yonghuid:
|
||||
return Response({'code': 400, 'msg': '需要 yonghuid 或 target_phone'})
|
||||
club_id = request.data.get('club_id')
|
||||
if club_id is not None and str(club_id).strip() == '':
|
||||
club_id = None
|
||||
elif club_id is not None:
|
||||
if club_id is None or str(club_id).strip() == '':
|
||||
club_id = ''
|
||||
else:
|
||||
club_id = str(club_id).strip()
|
||||
role_code = (request.data.get('role_code') or 'CLUB_ADMIN').strip()
|
||||
data_scope = (request.data.get('data_scope') or DATA_SCOPE_SINGLE).strip()
|
||||
if club_id is None:
|
||||
if not club_id:
|
||||
data_scope = DATA_SCOPE_ALL
|
||||
is_primary = bool(request.data.get('is_primary', False))
|
||||
_, err = create_or_reactivate_assignment(
|
||||
@@ -401,6 +401,8 @@ class ClubAdminAssignmentListView(APIView):
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 400, 'msg': err})
|
||||
from jituan.services.admin_assignments import dedupe_active_assignments
|
||||
dedupe_active_assignments()
|
||||
return Response({'code': 0, 'msg': '任职已添加'})
|
||||
|
||||
if action == 'delete':
|
||||
@@ -410,8 +412,9 @@ class ClubAdminAssignmentListView(APIView):
|
||||
ok, err = deactivate_assignment(assignment_id)
|
||||
if not ok:
|
||||
return Response({'code': 404, 'msg': err or '停用失败'})
|
||||
from jituan.services.admin_assignments import dedupe_active_assignments
|
||||
dedupe_active_assignments()
|
||||
return Response({'code': 0, 'msg': '任职已停用'})
|
||||
return Response({'code': 0, 'msg': '任职已删除'})
|
||||
|
||||
if action == 'update':
|
||||
assignment_id = request.data.get('id')
|
||||
|
||||
Reference in New Issue
Block a user