diff --git a/backend/apps/accounts/bootstrap.py b/backend/apps/accounts/bootstrap.py new file mode 100644 index 0000000..4c921c0 --- /dev/null +++ b/backend/apps/accounts/bootstrap.py @@ -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() diff --git a/backend/apps/accounts/management/commands/bootstrap_governance.py b/backend/apps/accounts/management/commands/bootstrap_governance.py new file mode 100644 index 0000000..f93dc0f --- /dev/null +++ b/backend/apps/accounts/management/commands/bootstrap_governance.py @@ -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.")) diff --git a/backend/apps/accounts/management/commands/seed_permissions.py b/backend/apps/accounts/management/commands/seed_permissions.py index 87ba7b5..dd310ef 100644 --- a/backend/apps/accounts/management/commands/seed_permissions.py +++ b/backend/apps/accounts/management/commands/seed_permissions.py @@ -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 apps.accounts.permissions import DEFAULT_GROUPS, PERMISSIONS +from apps.accounts.bootstrap import seed_permissions, seed_roles class Command(BaseCommand): help = "Seed MVP permissions and default user groups." def handle(self, *args, **options): - 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 - - 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) - + seed_roles(seed_permissions()) self.stdout.write(self.style.SUCCESS("Seeded permissions and groups.")) diff --git a/backend/apps/accounts/menus.py b/backend/apps/accounts/menus.py new file mode 100644 index 0000000..a609b34 --- /dev/null +++ b/backend/apps/accounts/menus.py @@ -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), +) diff --git a/backend/apps/accounts/migrations/0001_initial.py b/backend/apps/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..d5f6ea3 --- /dev/null +++ b/backend/apps/accounts/migrations/0001_initial.py @@ -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'], + }, + ), + ] diff --git a/backend/apps/accounts/migrations/__init__.py b/backend/apps/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/apps/accounts/models.py b/backend/apps/accounts/models.py index e8d39c5..7c31648 100644 --- a/backend/apps/accounts/models.py +++ b/backend/apps/accounts/models.py @@ -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 diff --git a/backend/apps/accounts/permissions.py b/backend/apps/accounts/permissions.py index 366bf3e..4fbcc75 100644 --- a/backend/apps/accounts/permissions.py +++ b/backend/apps/accounts/permissions.py @@ -5,30 +5,39 @@ from dataclasses import dataclass class PermissionSpec: codename: str name: str + module: str PERMISSIONS: tuple[PermissionSpec, ...] = ( - PermissionSpec("ticket.view", "View tickets"), - PermissionSpec("ticket.create", "Create tickets"), - PermissionSpec("ticket.edit", "Edit tickets"), - PermissionSpec("ticket.export", "Export tickets"), - PermissionSpec("ticket.assign", "Assign tickets"), - PermissionSpec("ticket.close", "Close tickets"), - PermissionSpec("knowledge.view", "View knowledge"), - PermissionSpec("knowledge.manage", "Manage knowledge"), - PermissionSpec("solution.view", "View solutions"), - PermissionSpec("solution.manage", "Manage solutions"), - PermissionSpec("agent.use", "Use Agents"), - PermissionSpec("agent.manage", "Manage Agents"), - PermissionSpec("model.manage", "Manage model configs"), - PermissionSpec("tool.manage", "Manage MCP tools"), - PermissionSpec("audit.view", "View audit logs"), - PermissionSpec("system.manage", "Manage system settings"), + PermissionSpec("ticket.view", "查看工单", "工单"), + PermissionSpec("ticket.create", "创建工单", "工单"), + PermissionSpec("ticket.edit", "编辑工单", "工单"), + PermissionSpec("ticket.export", "导出工单", "工单"), + PermissionSpec("ticket.assign", "分派工单", "工单"), + PermissionSpec("ticket.close", "关闭工单", "工单"), + PermissionSpec("solution.view", "查看解决方案", "解决方案"), + PermissionSpec("solution.manage", "管理解决方案", "解决方案"), + PermissionSpec("knowledge.view", "查看知识库", "知识库"), + PermissionSpec("knowledge.manage", "管理知识库", "知识库"), + PermissionSpec("agent.use", "使用 Agent", "AI 平台"), + PermissionSpec("agent.manage", "管理 Agent", "AI 平台"), + PermissionSpec("model.manage", "管理模型配置", "AI 平台"), + PermissionSpec("tool.manage", "管理 MCP 工具", "AI 平台"), + PermissionSpec("system.user.view", "查看用户", "系统治理"), + 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, ...]] = { - "System Admin": tuple(permission.codename for permission in PERMISSIONS), - "Knowledge Admin": ( + "系统管理员": tuple(permission.codename for permission in PERMISSIONS), + "知识库管理员": ( "ticket.view", "knowledge.view", "knowledge.manage", @@ -36,7 +45,7 @@ DEFAULT_GROUPS: dict[str, tuple[str, ...]] = { "solution.manage", "agent.use", ), - "Support Engineer": ( + "技术支持": ( "ticket.view", "ticket.create", "ticket.edit", @@ -46,16 +55,16 @@ DEFAULT_GROUPS: dict[str, tuple[str, ...]] = { "solution.view", "agent.use", ), - "Requester": ( + "普通建单人": ( "ticket.view", "ticket.create", "knowledge.view", "solution.view", ), - "Auditor": ( + "只读审计员": ( "ticket.view", "knowledge.view", "solution.view", - "audit.view", + "system.audit.view", ), } diff --git a/backend/apps/accounts/tests/test_bootstrap_governance.py b/backend/apps/accounts/tests/test_bootstrap_governance.py new file mode 100644 index 0000000..5642b69 --- /dev/null +++ b/backend/apps/accounts/tests/test_bootstrap_governance.py @@ -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()