feat: 超管任职分配、俱乐部创建与bootstrap、角色update_role

This commit is contained in:
XingQue
2026-06-24 16:56:10 +08:00
parent 0ac56c5b92
commit 04c54d0733
4 changed files with 9561 additions and 9288 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,33 @@
"""将指定手机号设为超级管理员(功能权限 + 集团视图)。""" """将指定手机号设为超级管理员(功能权限 + 集团视图)。"""
import uuid
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from django.utils import timezone
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
from jituan.constants import SUPER_ADMIN_PHONES from jituan.constants import SUPER_ADMIN_PHONES
from users.business_models import User from users.business_models import User
class Command(BaseCommand): class Command(BaseCommand):
help = '将 SUPER_ADMIN_PHONES 中的账号设为 IsSuperuser + UserType=admin' help = '将 SUPER_ADMIN_PHONES 设为 IsSuperuser,并绑定「管理员」角色(含 000001'
def handle(self, *args, **options): def handle(self, *args, **options):
admin_role = Role.objects.filter(RoleName='管理员', RoleStatus=1).first()
super_perm = Permission.objects.filter(PermCode='000001', PermStatus=1).first()
if admin_role and super_perm:
if not RolePermission.objects.filter(
RoleUUID=admin_role.RoleUUID, PermUUID=super_perm.PermUUID,
).exists():
RolePermission.objects.create(
RolePermUUID=uuid.uuid4().bytes,
RoleUUID=admin_role.RoleUUID,
PermUUID=super_perm.PermUUID,
CreateTime=timezone.now(),
)
self.stdout.write(self.style.SUCCESS('已为「管理员」角色绑定 000001 权限'))
updated = 0 updated = 0
for phone in SUPER_ADMIN_PHONES: for phone in SUPER_ADMIN_PHONES:
users = list(User.query.filter(Phone=phone)) users = list(User.query.filter(Phone=phone))
@@ -20,10 +38,16 @@ class Command(BaseCommand):
with transaction.atomic(): with transaction.atomic():
user.IsSuperuser = True user.IsSuperuser = True
user.IsStaff = True user.IsStaff = True
if hasattr(user, 'UserType'):
# UserType 为 property 从角色推断,尽量写 gvsdsdk 角色
pass
user.save(update_fields=['IsSuperuser', 'IsStaff']) user.save(update_fields=['IsSuperuser', 'IsStaff'])
if admin_role and not UserRole.objects.filter(
UserUUID=user.UserUUID, RoleUUID=admin_role.RoleUUID,
).exists():
UserRole.objects.create(
UserRoleUUID=uuid.uuid4().bytes,
UserUUID=user.UserUUID,
RoleUUID=admin_role.RoleUUID,
CreateTime=timezone.now(),
)
updated += 1 updated += 1
self.stdout.write(self.style.SUCCESS( self.stdout.write(self.style.SUCCESS(
f'已设置超管: {phone} (UserUID={user.UserUID})' f'已设置超管: {phone} (UserUID={user.UserUID})'

View File

@@ -0,0 +1,104 @@
"""创建新俱乐部并从模板复制配置。"""
from decimal import Decimal
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import Club, ClubHuiyuanPrice, ClubLilubiao, ClubTixianQuota, ClubWithdrawConfig
from jituan.services.display_config import copy_display_config
from jituan.services.szjilu_accounting import normalize_szjilu_club_id
from config.models import Szjilu
def bootstrap_club_from_template(
new_id: str,
name: str,
wx_appid: str = '',
template_club_id: str = CLUB_ID_DEFAULT,
extra_fields: dict = None,
):
"""
从模板俱乐部复制支付/会员价/利率等配置,创建新俱乐部。
返回新建的 Club 实例。
"""
new_id = (new_id or '').strip()
name = (name or '').strip()
template_id = normalize_szjilu_club_id(template_club_id)
extra_fields = extra_fields or {}
if not new_id or len(new_id) > 16:
raise ValueError('club_id 无效')
if not name:
raise ValueError('俱乐部名称不能为空')
if new_id == template_id:
raise ValueError('新 club_id 不能与模板相同')
if Club.query.filter(club_id=new_id).exists():
raise ValueError(f'俱乐部 {new_id} 已存在')
template = Club.query.get(club_id=template_id)
with transaction.atomic():
club = Club.query.create(
club_id=new_id,
name=name,
status=int(extra_fields.get('status', 1)),
wx_appid=wx_appid or extra_fields.get('wx_appid') or template.wx_appid,
wx_secret=extra_fields.get('wx_secret', template.wx_secret),
mch_id=extra_fields.get('mch_id', template.mch_id),
pay_app_id=extra_fields.get('pay_app_id') or wx_appid or template.pay_app_id or template.wx_appid,
api_v3_key=extra_fields.get('api_v3_key', template.api_v3_key),
cert_serial_no=extra_fields.get('cert_serial_no', template.cert_serial_no),
private_key_path=extra_fields.get('private_key_path', template.private_key_path),
platform_cert_dir=extra_fields.get('platform_cert_dir', template.platform_cert_dir),
official_appid=extra_fields.get('official_appid', template.official_appid),
official_secret=extra_fields.get('official_secret', template.official_secret),
official_token=extra_fields.get('official_token', template.official_token),
encoding_aes_key=extra_fields.get('encoding_aes_key', template.encoding_aes_key),
template_id=extra_fields.get('template_id', template.template_id),
template_max_per_minute=extra_fields.get(
'template_max_per_minute', template.template_max_per_minute,
),
h5_domain=extra_fields.get('h5_domain', template.h5_domain),
oss_prefix=extra_fields.get('oss_prefix') or new_id,
goeasy_appkey=extra_fields.get('goeasy_appkey', template.goeasy_appkey),
goeasy_secret=extra_fields.get('goeasy_secret', template.goeasy_secret),
config_json=dict(template.config_json or {}),
sort_order=int(extra_fields.get('sort_order', template.sort_order + 1)),
)
for row in ClubHuiyuanPrice.query.filter(club_id=template_id):
ClubHuiyuanPrice.query.create(
club_id=new_id,
huiyuan_id=row.huiyuan_id,
jiage=row.jiage or Decimal('0'),
guanshifc=row.guanshifc or Decimal('0'),
zuzhangfc=row.zuzhangfc or Decimal('0'),
is_enabled=row.is_enabled,
bankuai_id=row.bankuai_id,
)
for row in ClubLilubiao.query.filter(club_id=template_id):
ClubLilubiao.query.create(
club_id=new_id,
config_type=row.config_type,
lilv=row.lilv or Decimal('0'),
remark=row.remark or '',
)
try:
tpl_wd = ClubWithdrawConfig.query.get(club_id=template_id)
ClubWithdrawConfig.query.create(club_id=new_id, mode=tpl_wd.mode)
except ClubWithdrawConfig.DoesNotExist:
ClubWithdrawConfig.query.create(club_id=new_id, mode=1)
for row in ClubTixianQuota.query.filter(club_id=template_id):
ClubTixianQuota.query.create(
club_id=new_id,
leixing=row.leixing,
daily_limit=row.daily_limit or Decimal('0'),
)
Szjilu.objects.get_or_create(club_id=new_id)
copy_display_config(template_id, new_id)
return club

View File

@@ -10,7 +10,7 @@ from rest_framework.views import APIView
from rest_framework.throttling import AnonRateThrottle from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from jituan.constants import SUPER_ADMIN_PHONES, ADMIN_ROLE_LABELS, CLUB_ID_DEFAULT from jituan.constants import SUPER_ADMIN_PHONES, ADMIN_ROLE_LABELS, CLUB_ID_DEFAULT, DATA_SCOPE_ALL, DATA_SCOPE_SINGLE
from jituan.models import Club, AdminAssignment from jituan.models import Club, AdminAssignment
from jituan.services.admin_context import build_admin_club_context from jituan.services.admin_context import build_admin_club_context
from jituan.services.wechat_login import login_or_register_by_club from jituan.services.wechat_login import login_or_register_by_club
@@ -196,10 +196,43 @@ class ClubMeContextView(APIView):
class ClubAdminAssignmentListView(APIView): class ClubAdminAssignmentListView(APIView):
"""POST /jituan/houtai/admin-assignments — 查看数据范围任职(非 gvsdsdk 功能权限)。""" """POST /jituan/houtai/admin-assignments — 数据范围任职(非 gvsdsdk 功能权限)。"""
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
parser_classes = [JSONParser] parser_classes = [JSONParser]
def _is_super(self, user):
return (
bool(user.IsSuperuser)
or user.UserType == 'admin'
or getattr(user, 'Phone', '') in SUPER_ADMIN_PHONES
)
def _list_rows(self, assignments):
club_name_map = {c.club_id: c.name for c in Club.query.filter(status=1)}
uids = list({a.yonghuid for a in assignments})
user_map = {}
if uids:
for u in User.query.filter(UserUID__in=uids).only('UserUID', 'Phone', 'UserName'):
user_map[u.UserUID] = u.Phone or u.UserName or u.UserUID
rows = []
for a in assignments:
club_label = '集团(全部子公司)'
if a.club_id:
club_label = club_name_map.get(a.club_id, a.club_id)
rows.append({
'id': a.id,
'yonghuid': a.yonghuid,
'phone': user_map.get(a.yonghuid, ''),
'club_id': a.club_id,
'club_label': club_label,
'role_code': a.role_code,
'role_name': ADMIN_ROLE_LABELS.get(a.role_code, a.role_code),
'data_scope': a.data_scope,
'is_primary': a.is_primary,
})
return rows
def post(self, request): def post(self, request):
try: try:
username = (request.data.get('phone') or request.data.get('username') or '').strip() username = (request.data.get('phone') or request.data.get('username') or '').strip()
@@ -210,57 +243,103 @@ class ClubAdminAssignmentListView(APIView):
return permissions return permissions
user = request.user user = request.user
is_super = ( is_super = self._is_super(user)
bool(user.IsSuperuser) action = (request.data.get('action') or 'list').strip()
or user.UserType == 'admin'
or getattr(user, 'Phone', '') in SUPER_ADMIN_PHONES if action in ('create', 'update', 'delete'):
) if not is_super or '000001' not in permissions:
return Response({'code': 403, 'msg': '仅超级管理员可维护任职数据'}, status=403)
if action == 'create':
yonghuid = (request.data.get('yonghuid') or '').strip()
phone = (request.data.get('target_phone') or '').strip()
if not yonghuid and phone:
target = User.query.filter(Phone=phone).first()
if not target:
return Response({'code': 404, 'msg': f'未找到手机号 {phone}'})
yonghuid = target.UserUID
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:
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:
data_scope = DATA_SCOPE_ALL
is_primary = bool(request.data.get('is_primary', False))
if AdminAssignment.query.filter(
yonghuid=yonghuid, club_id=club_id, role_code=role_code,
).exists():
return Response({'code': 400, 'msg': '该任职记录已存在'})
AdminAssignment.query.create(
yonghuid=yonghuid,
club_id=club_id,
role_code=role_code,
data_scope=data_scope,
is_primary=is_primary,
granted_by=user.UserUID,
status=1,
)
return Response({'code': 0, 'msg': '任职已添加'})
if action == 'delete':
assignment_id = request.data.get('id')
if not assignment_id:
return Response({'code': 400, 'msg': '缺少任职 id'})
row = AdminAssignment.query.filter(id=assignment_id).first()
if not row:
return Response({'code': 404, 'msg': '任职记录不存在'})
row.status = 0
row.save(update_fields=['status', 'UpdateTime'])
return Response({'code': 0, 'msg': '任职已停用'})
if action == 'update':
assignment_id = request.data.get('id')
if not assignment_id:
return Response({'code': 400, 'msg': '缺少任职 id'})
row = AdminAssignment.query.filter(id=assignment_id, status=1).first()
if not row:
return Response({'code': 404, 'msg': '任职记录不存在'})
if 'role_code' in request.data:
row.role_code = (request.data.get('role_code') or '').strip() or row.role_code
if 'data_scope' in request.data:
row.data_scope = request.data.get('data_scope') or row.data_scope
if 'is_primary' in request.data:
row.is_primary = bool(request.data.get('is_primary'))
if 'club_id' in request.data:
cid = request.data.get('club_id')
row.club_id = None if cid is None or str(cid).strip() == '' else str(cid).strip()
if row.club_id is None:
row.data_scope = DATA_SCOPE_ALL
row.granted_by = user.UserUID
row.save()
return Response({'code': 0, 'msg': '任职已更新'})
if is_super: if is_super:
qs = AdminAssignment.query.filter(status=1).order_by('yonghuid', '-is_primary', 'club_id') qs = AdminAssignment.query.filter(status=1).order_by('yonghuid', '-is_primary', 'club_id')
else: else:
qs = AdminAssignment.query.filter(yonghuid=user.UserUID, status=1).order_by('-is_primary') qs = AdminAssignment.query.filter(yonghuid=user.UserUID, status=1).order_by('-is_primary')
assignments = qs.to_list()
club_name_map = {
c.club_id: c.name for c in Club.query.filter(status=1)
}
uids = list({a.yonghuid for a in assignments})
user_map = {}
if uids:
for u in User.query.filter(UserUID__in=uids).only('UserUID', 'Phone', 'UserName'):
user_map[u.UserUID] = u.Phone or u.UserName or u.UserUID
rows = []
for a in assignments:
club_label = '集团(全部子公司)'
if a.club_id:
club_label = club_name_map.get(a.club_id, a.club_id)
rows.append({
'yonghuid': a.yonghuid,
'phone': user_map.get(a.yonghuid, ''),
'club_id': a.club_id,
'club_label': club_label,
'role_code': a.role_code,
'role_name': ADMIN_ROLE_LABELS.get(a.role_code, a.role_code),
'data_scope': a.data_scope,
'is_primary': a.is_primary,
})
return Response({ return Response({
'code': 0, 'code': 0,
'msg': 'ok', 'msg': 'ok',
'data': { 'data': {
'list': rows, 'list': self._list_rows(qs.to_list()),
'can_manage': is_super and '000001' in permissions,
'perm_note': ( 'perm_note': (
'功能菜单权限(能进哪些页面)在「角色管理」配置' '功能权限】在「角色管理」里给账号绑 gvsdsdk 角色perm_code 如 caiwu、002ab'
'本表 admin_assignment 仅控制集团/子公司数据范围' '【数据范围】在本页维护 admin_assignment(能看哪个俱乐部/集团汇总)'
'两套独立,超管需有 000001 权限码。'
), ),
'current_context': build_admin_club_context(user), 'current_context': build_admin_club_context(user),
}, },
}) })
except Exception: except Exception:
logger.exception('ClubAdminAssignmentListView 异常') logger.exception('ClubAdminAssignmentListView 异常')
return Response({'code': 99, 'msg': '加载任职数据失败,请确认已执行 migrate jituan'}, status=500) return Response({'code': 99, 'msg': '任职数据操作失败,请确认已执行 migrate jituan'}, status=500)
class ClubManageView(APIView): class ClubManageView(APIView):
@@ -350,7 +429,28 @@ class ClubManageView(APIView):
'data': self._serialize(club, full=True), 'data': self._serialize(club, full=True),
}) })
return Response({'code': 400, 'msg': '无效 action支持 list/get/update'}) if action == 'create':
from jituan.services.club_bootstrap import bootstrap_club_from_template
new_club_id = (request.data.get('new_club_id') or request.data.get('club_id') or '').strip()
name = (request.data.get('name') or '').strip()
if not new_club_id or not name:
return Response({'code': 400, 'msg': '新建俱乐部需要 new_club_id 与 name'})
try:
club = bootstrap_club_from_template(
new_club_id,
name,
wx_appid=(request.data.get('wx_appid') or '').strip(),
template_club_id=(request.data.get('from_club') or CLUB_ID_DEFAULT).strip(),
)
except ValueError as exc:
return Response({'code': 400, 'msg': str(exc)})
return Response({
'code': 0,
'msg': '俱乐部创建成功',
'data': self._serialize(club, full=True),
})
return Response({'code': 400, 'msg': '无效 action支持 list/get/update/create'})
except Exception: except Exception:
logger.exception('ClubManageView 异常') logger.exception('ClubManageView 异常')
return Response({'code': 99, 'msg': '俱乐部配置操作失败'}, status=500) return Response({'code': 99, 'msg': '俱乐部配置操作失败'}, status=500)