feat: add governance bootstrap models

This commit is contained in:
hailin
2026-07-01 14:04:22 +08:00
parent ee9aad6a02
commit 7c4b66b832
9 changed files with 304 additions and 47 deletions

View File

@@ -0,0 +1,105 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from apps.accounts.menus import DEFAULT_MENUS
from apps.accounts.models import MenuItem, UserGroup
from apps.accounts.permissions import DEFAULT_GROUPS, PERMISSIONS
def seed_permissions() -> dict[str, Permission]:
content_type, _ = ContentType.objects.get_or_create(
app_label="system",
model="platformpermission",
)
permissions_by_codename: dict[str, Permission] = {}
for spec in PERMISSIONS:
permission, _ = Permission.objects.get_or_create(
content_type=content_type,
codename=spec.codename,
defaults={"name": spec.name},
)
if permission.name != spec.name:
permission.name = spec.name
permission.save(update_fields=["name"])
permissions_by_codename[spec.codename] = permission
return permissions_by_codename
def seed_roles(permissions_by_codename: dict[str, Permission]) -> None:
for group_name, codenames in DEFAULT_GROUPS.items():
group, _ = Group.objects.get_or_create(name=group_name)
group.permissions.set(permissions_by_codename[codename] for codename in codenames)
def seed_user_groups() -> None:
UserGroup.objects.update_or_create(
code="default",
defaults={
"name": "默认用户组",
"description": "系统初始化创建的默认业务用户组。",
"is_active": True,
},
)
def seed_menus() -> None:
menus_by_code: dict[str, MenuItem] = {}
for spec in DEFAULT_MENUS:
parent = menus_by_code.get(spec.parent_code) if spec.parent_code else None
menu, _ = MenuItem.objects.update_or_create(
code=spec.code,
defaults={
"title": spec.title,
"path": spec.path,
"icon": spec.icon,
"parent": parent,
"sort_order": spec.sort_order,
"permission_code": spec.permission_code,
"is_visible": spec.is_visible,
"is_enabled": spec.is_enabled,
},
)
menus_by_code[spec.code] = menu
def seed_admin_user() -> None:
username = getattr(settings, "DJANGO_SUPERUSER_USERNAME", None) or "admin"
password = getattr(settings, "DJANGO_SUPERUSER_PASSWORD", None) or "admin12345"
email = getattr(settings, "DJANGO_SUPERUSER_EMAIL", None) or "admin@example.com"
user_model = get_user_model()
user, created = user_model.objects.get_or_create(
username=username,
defaults={
"email": email,
"is_staff": True,
"is_superuser": True,
},
)
changed_fields: list[str] = []
if user.email != email:
user.email = email
changed_fields.append("email")
if not user.is_staff:
user.is_staff = True
changed_fields.append("is_staff")
if not user.is_superuser:
user.is_superuser = True
changed_fields.append("is_superuser")
if created or not user.check_password(password):
user.set_password(password)
changed_fields.append("password")
if changed_fields:
user.save(update_fields=changed_fields)
admin_role = Group.objects.get(name="系统管理员")
user.groups.add(admin_role)
def bootstrap_governance() -> None:
permissions_by_codename = seed_permissions()
seed_roles(permissions_by_codename)
seed_user_groups()
seed_menus()
seed_admin_user()

View File

@@ -0,0 +1,11 @@
from django.core.management.base import BaseCommand
from apps.accounts.bootstrap import bootstrap_governance
class Command(BaseCommand):
help = "Bootstrap enterprise governance defaults."
def handle(self, *args, **options):
bootstrap_governance()
self.stdout.write(self.style.SUCCESS("Bootstrapped enterprise governance."))

View File

@@ -1,33 +1,11 @@
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from apps.accounts.permissions import DEFAULT_GROUPS, PERMISSIONS from apps.accounts.bootstrap import seed_permissions, seed_roles
class Command(BaseCommand): class Command(BaseCommand):
help = "Seed MVP permissions and default user groups." help = "Seed MVP permissions and default user groups."
def handle(self, *args, **options): def handle(self, *args, **options):
content_type, _ = ContentType.objects.get_or_create( seed_roles(seed_permissions())
app_label="system",
model="platformpermission",
)
permissions_by_codename: dict[str, Permission] = {}
for spec in PERMISSIONS:
permission, _ = Permission.objects.get_or_create(
content_type=content_type,
codename=spec.codename,
defaults={"name": spec.name},
)
if permission.name != spec.name:
permission.name = spec.name
permission.save(update_fields=["name"])
permissions_by_codename[spec.codename] = permission
for group_name, codenames in DEFAULT_GROUPS.items():
group, _ = Group.objects.get_or_create(name=group_name)
group.permissions.set(permissions_by_codename[codename] for codename in codenames)
self.stdout.write(self.style.SUCCESS("Seeded permissions and groups.")) self.stdout.write(self.style.SUCCESS("Seeded permissions and groups."))

View File

@@ -0,0 +1,35 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class MenuSpec:
code: str
title: str
path: str = ""
icon: str = ""
parent_code: str = ""
sort_order: int = 0
permission_code: str = ""
is_visible: bool = True
is_enabled: bool = True
DEFAULT_MENUS: tuple[MenuSpec, ...] = (
MenuSpec("workbench", "工作台", icon="home", sort_order=10),
MenuSpec("workbench.overview", "运营总览", "#/", "home", "workbench", 10),
MenuSpec("business", "业务处理", icon="ticket", sort_order=20),
MenuSpec("business.tickets", "工单中心", "#/tickets", "ticket", "business", 10, "ticket.view"),
MenuSpec("business.ticket_create", "创建工单", "#/tickets/new", "briefcase", "business", 20, "ticket.create"),
MenuSpec("business.solutions", "解决方案库", "#/solutions", "library", "business", 30, "solution.view"),
MenuSpec("platform", "平台能力", icon="bot", sort_order=30),
MenuSpec("platform.knowledge", "知识库", "#/knowledge", "database", "platform", 10, "knowledge.view", is_enabled=False),
MenuSpec("platform.agents", "Agent 配置", "#/agents", "bot", "platform", 20, "agent.manage", is_enabled=False),
MenuSpec("platform.models", "模型与工具", "#/models", "settings", "platform", 30, "model.manage", is_enabled=False),
MenuSpec("system", "系统治理", icon="shield", sort_order=40),
MenuSpec("system.users", "用户管理", "#/system/users", "users", "system", 10, "system.user.view"),
MenuSpec("system.user_groups", "用户组管理", "#/system/user-groups", "user_groups", "system", 20, "system.user_group.view"),
MenuSpec("system.roles", "角色权限", "#/system/roles", "roles", "system", 30, "system.role.view"),
MenuSpec("system.menus", "菜单权限", "#/system/menus", "menu", "system", 40, "system.menu.view"),
MenuSpec("system.audit", "操作审计", "#/audit", "audit", "system", 50, "system.audit.view"),
MenuSpec("system.design", "设计系统", "#/design-system", "settings", "system", 60),
)

View File

@@ -0,0 +1,53 @@
# Generated by Django 5.2.15 on 2026-07-01 06:04
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='MenuItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=120, unique=True)),
('title', models.CharField(max_length=120)),
('path', models.CharField(blank=True, max_length=200)),
('icon', models.CharField(blank=True, max_length=80)),
('sort_order', models.PositiveIntegerField(default=0)),
('permission_code', models.CharField(blank=True, max_length=120)),
('is_visible', models.BooleanField(default=True)),
('is_enabled', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='accounts.menuitem')),
],
options={
'ordering': ['sort_order', 'id'],
},
),
migrations.CreateModel(
name='UserGroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=120, unique=True)),
('code', models.CharField(max_length=80, unique=True)),
('description', models.TextField(blank=True)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('users', models.ManyToManyField(blank=True, related_name='business_groups', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['name'],
},
),
]

View File

@@ -1 +1,44 @@
# Use Django's built-in User, Group, and Permission models for MVP RBAC. from django.conf import settings
from django.db import models
class UserGroup(models.Model):
name = models.CharField(max_length=120, unique=True)
code = models.CharField(max_length=80, unique=True)
description = models.TextField(blank=True)
users = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name="business_groups")
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["name"]
def __str__(self) -> str:
return self.name
class MenuItem(models.Model):
code = models.CharField(max_length=120, unique=True)
title = models.CharField(max_length=120)
path = models.CharField(max_length=200, blank=True)
icon = models.CharField(max_length=80, blank=True)
parent = models.ForeignKey(
"self",
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="children",
)
sort_order = models.PositiveIntegerField(default=0)
permission_code = models.CharField(max_length=120, blank=True)
is_visible = models.BooleanField(default=True)
is_enabled = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["sort_order", "id"]
def __str__(self) -> str:
return self.title

View File

@@ -5,30 +5,39 @@ from dataclasses import dataclass
class PermissionSpec: class PermissionSpec:
codename: str codename: str
name: str name: str
module: str
PERMISSIONS: tuple[PermissionSpec, ...] = ( PERMISSIONS: tuple[PermissionSpec, ...] = (
PermissionSpec("ticket.view", "View tickets"), PermissionSpec("ticket.view", "查看工单", "工单"),
PermissionSpec("ticket.create", "Create tickets"), PermissionSpec("ticket.create", "创建工单", "工单"),
PermissionSpec("ticket.edit", "Edit tickets"), PermissionSpec("ticket.edit", "编辑工单", "工单"),
PermissionSpec("ticket.export", "Export tickets"), PermissionSpec("ticket.export", "导出工单", "工单"),
PermissionSpec("ticket.assign", "Assign tickets"), PermissionSpec("ticket.assign", "分派工单", "工单"),
PermissionSpec("ticket.close", "Close tickets"), PermissionSpec("ticket.close", "关闭工单", "工单"),
PermissionSpec("knowledge.view", "View knowledge"), PermissionSpec("solution.view", "查看解决方案", "解决方案"),
PermissionSpec("knowledge.manage", "Manage knowledge"), PermissionSpec("solution.manage", "管理解决方案", "解决方案"),
PermissionSpec("solution.view", "View solutions"), PermissionSpec("knowledge.view", "查看知识库", "知识库"),
PermissionSpec("solution.manage", "Manage solutions"), PermissionSpec("knowledge.manage", "管理知识库", "知识库"),
PermissionSpec("agent.use", "Use Agents"), PermissionSpec("agent.use", "使用 Agent", "AI 平台"),
PermissionSpec("agent.manage", "Manage Agents"), PermissionSpec("agent.manage", "管理 Agent", "AI 平台"),
PermissionSpec("model.manage", "Manage model configs"), PermissionSpec("model.manage", "管理模型配置", "AI 平台"),
PermissionSpec("tool.manage", "Manage MCP tools"), PermissionSpec("tool.manage", "管理 MCP 工具", "AI 平台"),
PermissionSpec("audit.view", "View audit logs"), PermissionSpec("system.user.view", "查看用户", "系统治理"),
PermissionSpec("system.manage", "Manage system settings"), PermissionSpec("system.user.manage", "管理用户", "系统治理"),
PermissionSpec("system.user_group.view", "查看用户组", "系统治理"),
PermissionSpec("system.user_group.manage", "管理用户组", "系统治理"),
PermissionSpec("system.role.view", "查看角色权限", "系统治理"),
PermissionSpec("system.role.manage", "管理角色权限", "系统治理"),
PermissionSpec("system.menu.view", "查看菜单权限", "系统治理"),
PermissionSpec("system.menu.manage", "管理菜单权限", "系统治理"),
PermissionSpec("system.audit.view", "查看操作审计", "系统治理"),
PermissionSpec("system.setting.manage", "管理系统设置", "系统治理"),
) )
DEFAULT_GROUPS: dict[str, tuple[str, ...]] = { DEFAULT_GROUPS: dict[str, tuple[str, ...]] = {
"System Admin": tuple(permission.codename for permission in PERMISSIONS), "系统管理员": tuple(permission.codename for permission in PERMISSIONS),
"Knowledge Admin": ( "知识库管理员": (
"ticket.view", "ticket.view",
"knowledge.view", "knowledge.view",
"knowledge.manage", "knowledge.manage",
@@ -36,7 +45,7 @@ DEFAULT_GROUPS: dict[str, tuple[str, ...]] = {
"solution.manage", "solution.manage",
"agent.use", "agent.use",
), ),
"Support Engineer": ( "技术支持": (
"ticket.view", "ticket.view",
"ticket.create", "ticket.create",
"ticket.edit", "ticket.edit",
@@ -46,16 +55,16 @@ DEFAULT_GROUPS: dict[str, tuple[str, ...]] = {
"solution.view", "solution.view",
"agent.use", "agent.use",
), ),
"Requester": ( "普通建单人": (
"ticket.view", "ticket.view",
"ticket.create", "ticket.create",
"knowledge.view", "knowledge.view",
"solution.view", "solution.view",
), ),
"Auditor": ( "只读审计员": (
"ticket.view", "ticket.view",
"knowledge.view", "knowledge.view",
"solution.view", "solution.view",
"audit.view", "system.audit.view",
), ),
} }

View File

@@ -0,0 +1,23 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.core.management import call_command
from apps.accounts.models import MenuItem, UserGroup
def test_bootstrap_governance_seeds_admin_roles_groups_permissions_and_menus(db, settings):
settings.DJANGO_SUPERUSER_USERNAME = "admin"
settings.DJANGO_SUPERUSER_PASSWORD = "admin12345"
settings.DJANGO_SUPERUSER_EMAIL = "admin@example.com"
call_command("bootstrap_governance")
call_command("bootstrap_governance")
user = get_user_model().objects.get(username="admin")
assert user.is_superuser is True
assert user.check_password("admin12345")
assert Group.objects.filter(name="系统管理员").exists()
assert Permission.objects.filter(codename="system.user.manage").exists()
assert UserGroup.objects.filter(code="default").exists()
assert MenuItem.objects.filter(code="system.users", title="用户管理").exists()
assert MenuItem.objects.filter(code="system.roles", permission_code="system.role.view").exists()