33 KiB
Design Document
Overview
HoneyBox.Admin 是一个独立可复用的后台管理模块,采用 ASP.NET Core 10.0 后端(.NET 10 + ASP.NET Core + Entity Framework Core 8 (SQL Server) + Autofac + JWT Bearer + Serilog + Mapster + Scalar (OpenAPI) + StackExchange.Redis + Newtonsoft.Json ) + Vue3 + Element Plus + TypeScript 前端架构。后端提供 RESTful API,前端打包后部署到 wwwroot 目录实现一体化部署。
Design Decision: 前端使用 TypeScript 以提供更好的类型安全和开发体验,符合 Requirements 9.1 的要求。
Architecture
整体架构
┌─────────────────────────────────────────────────────────────────┐
│ HoneyBox.Admin 项目 │
├─────────────────────────────────────────────────────────────────┤
│ wwwroot/ │ Controllers/ │
│ ├── index.html │ ├── AuthController.cs │
│ ├── assets/ │ ├── MenuController.cs │
│ └── (Vue3 打包文件) │ ├── RoleController.cs │
│ │ ├── DepartmentController.cs │
│ │ ├── AdminUserController.cs │
│ │ └── OperationLogController.cs │
├─────────────────────────────────────────────────────────────────┤
│ Services/ │ Entities/ │
│ ├── AuthService.cs │ ├── AdminUser.cs │
│ ├── MenuService.cs │ ├── Role.cs │
│ ├── RoleService.cs │ ├── Menu.cs │
│ ├── AdminUserService.cs │ ├── Department.cs │
│ ├── DepartmentService.cs │ ├── Permission.cs │
│ ├── PermissionService.cs │ └── OperationLog.cs │
│ └── OperationLogService.cs │ │
├─────────────────────────────────────────────────────────────────┤
│ Data/ │ Models/ │
│ └── AdminDbContext.cs │ ├── Auth/ (LoginRequest, etc) │
│ │ ├── Menu/ (MenuDto, etc) │
│ │ └── Common/ (ApiResponse, etc) │
├─────────────────────────────────────────────────────────────────┤
│ Filters/ │ Extensions/ │
│ ├── AdminAuthFilter.cs │ └── ServiceCollectionExt.cs │
│ └── PermissionFilter.cs │ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ honey_box_admin │
│ (SQL Server 数据库) │
└───────────────────────────────┘
请求处理流程
HTTP Request
│
▼
┌─────────────┐
│ 静态文件? │──Yes──▶ wwwroot/index.html (Vue3 SPA)
└─────────────┘
│ No
▼
┌─────────────┐
│ /api/*? │──No───▶ SPA Fallback (index.html)
└─────────────┘
│ Yes
▼
┌─────────────┐
│ AdminAuth │──Fail──▶ 401 Unauthorized
│ Filter │
└─────────────┘
│ Pass
▼
┌─────────────┐
│ Permission │──Fail──▶ 403 Forbidden
│ Filter │
└─────────────┘
│ Pass
▼
┌─────────────┐
│ Controller │
│ Action │
└─────────────┘
Components and Interfaces
1. Controllers
AuthController
[ApiController]
[Route("api/admin/auth")]
public class AuthController : ControllerBase
{
// POST /api/admin/auth/login - 管理员登录
// POST /api/admin/auth/logout - 退出登录
// GET /api/admin/auth/info - 获取当前用户信息
// PUT /api/admin/auth/password - 修改密码
}
MenuController
[ApiController]
[Route("api/admin/menus")]
[AdminAuth]
public class MenuController : ControllerBase
{
// GET /api/admin/menus - 获取菜单树
// GET /api/admin/menus/{id} - 获取菜单详情
// POST /api/admin/menus - 创建菜单
// PUT /api/admin/menus/{id} - 更新菜单
// DELETE /api/admin/menus/{id} - 删除菜单
// GET /api/admin/menus/user - 获取当前用户菜单
}
RoleController
[ApiController]
[Route("api/admin/roles")]
[AdminAuth]
public class RoleController : ControllerBase
{
// GET /api/admin/roles - 获取角色列表
// GET /api/admin/roles/{id} - 获取角色详情
// POST /api/admin/roles - 创建角色
// PUT /api/admin/roles/{id} - 更新角色
// DELETE /api/admin/roles/{id} - 删除角色
// PUT /api/admin/roles/{id}/menus - 分配菜单
// PUT /api/admin/roles/{id}/permissions - 分配权限
}
AdminUserController
[ApiController]
[Route("api/admin/users")]
[AdminAuth]
public class AdminUserController : ControllerBase
{
// GET /api/admin/users - 获取管理员列表
// GET /api/admin/users/{id} - 获取管理员详情
// POST /api/admin/users - 创建管理员
// PUT /api/admin/users/{id} - 更新管理员
// DELETE /api/admin/users/{id} - 删除管理员
// PUT /api/admin/users/{id}/roles - 分配角色
// PUT /api/admin/users/{id}/menus - 分配用户专属菜单
// PUT /api/admin/users/{id}/department - 分配部门
// PUT /api/admin/users/{id}/status - 启用/禁用
// PUT /api/admin/users/{id}/reset-password - 重置密码
}
DepartmentController
[ApiController]
[Route("api/admin/departments")]
[AdminAuth]
public class DepartmentController : ControllerBase
{
// GET /api/admin/departments - 获取部门树
// GET /api/admin/departments/{id} - 获取部门详情
// POST /api/admin/departments - 创建部门
// PUT /api/admin/departments/{id} - 更新部门
// DELETE /api/admin/departments/{id} - 删除部门
// PUT /api/admin/departments/{id}/menus - 分配部门菜单
// GET /api/admin/departments/{id}/users - 获取部门下用户
}
OperationLogController
[ApiController]
[Route("api/admin/logs")]
[AdminAuth]
public class OperationLogController : ControllerBase
{
// GET /api/admin/logs - 获取操作日志列表(分页、筛选)
// GET /api/admin/logs/{id} - 获取日志详情
}
2. Service Interfaces
public interface IAuthService
{
Task<LoginResponse> LoginAsync(LoginRequest request, string ipAddress);
Task LogoutAsync(long adminUserId);
Task<AdminUserInfo> GetCurrentUserInfoAsync(long adminUserId);
Task ChangePasswordAsync(long adminUserId, ChangePasswordRequest request);
}
public interface IMenuService
{
Task<List<MenuTreeDto>> GetMenuTreeAsync();
Task<MenuDto> GetByIdAsync(long id);
Task<long> CreateAsync(CreateMenuRequest request);
Task UpdateAsync(long id, UpdateMenuRequest request);
Task DeleteAsync(long id);
Task<List<MenuTreeDto>> GetUserMenusAsync(long adminUserId);
// 用户菜单 = 部门菜单 ∪ 角色菜单 ∪ 用户专属菜单
}
public interface IRoleService
{
Task<PagedResult<RoleDto>> GetListAsync(RoleQueryRequest request);
Task<RoleDto> GetByIdAsync(long id);
Task<long> CreateAsync(CreateRoleRequest request);
Task UpdateAsync(long id, UpdateRoleRequest request);
Task DeleteAsync(long id);
Task AssignMenusAsync(long roleId, List<long> menuIds);
Task AssignPermissionsAsync(long roleId, List<long> permissionIds);
}
public interface IAdminUserService
{
Task<PagedResult<AdminUserDto>> GetListAsync(AdminUserQueryRequest request);
Task<AdminUserDto> GetByIdAsync(long id);
Task<long> CreateAsync(CreateAdminUserRequest request);
Task UpdateAsync(long id, UpdateAdminUserRequest request);
Task DeleteAsync(long id);
Task AssignRolesAsync(long userId, List<long> roleIds);
Task AssignMenusAsync(long userId, List<long> menuIds);
Task AssignDepartmentAsync(long userId, long? departmentId);
Task SetStatusAsync(long userId, bool enabled);
Task ResetPasswordAsync(long userId, string newPassword);
}
public interface IDepartmentService
{
Task<List<DepartmentTreeDto>> GetDepartmentTreeAsync();
Task<DepartmentDto> GetByIdAsync(long id);
Task<long> CreateAsync(CreateDepartmentRequest request);
Task UpdateAsync(long id, UpdateDepartmentRequest request);
Task DeleteAsync(long id);
Task AssignMenusAsync(long departmentId, List<long> menuIds);
Task<List<AdminUserDto>> GetDepartmentUsersAsync(long departmentId);
}
public interface IPermissionService
{
Task<List<string>> GetUserPermissionsAsync(long adminUserId);
Task<bool> HasPermissionAsync(long adminUserId, string permissionCode);
void InvalidateCache(long adminUserId);
}
public interface IOperationLogService
{
Task LogAsync(OperationLogRequest request);
Task<PagedResult<OperationLogDto>> GetListAsync(OperationLogQueryRequest request);
Task<OperationLogDto> GetByIdAsync(long id);
}
3. DataSeeder
public interface IDataSeeder
{
Task SeedAsync();
}
public class DataSeeder : IDataSeeder
{
// 初始化超级管理员账号 (admin/admin123)
// 初始化超级管理员角色 (super_admin)
// 初始化系统菜单 (系统管理、菜单管理、角色管理、管理员管理、操作日志)
// 初始化系统权限 (所有 API 权限)
public async Task SeedAsync()
{
await SeedPermissionsAsync();
await SeedRolesAsync();
await SeedMenusAsync();
await SeedAdminUserAsync();
}
}
Design Decision: DataSeeder 在应用启动时检查是否需要初始化数据,仅在数据库为空时执行种子数据插入,避免重复初始化。
Data Models
Entity Classes
// 管理员
public class AdminUser
{
public long Id { get; set; }
public string Username { get; set; }
public string PasswordHash { get; set; }
public string? RealName { get; set; }
public string? Avatar { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public long? DepartmentId { get; set; }
public byte Status { get; set; } = 1; // 0禁用 1启用
public DateTime? LastLoginTime { get; set; }
public string? LastLoginIp { get; set; }
public int LoginFailCount { get; set; } = 0;
public DateTime? LockoutEnd { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public long? CreatedBy { get; set; }
public string? Remark { get; set; }
public Department? Department { get; set; }
public ICollection<AdminUserRole> AdminUserRoles { get; set; }
public ICollection<AdminUserMenu> AdminUserMenus { get; set; }
}
// 部门(支持无限嵌套)
public class Department
{
public long Id { get; set; }
public long ParentId { get; set; } = 0;
public string Name { get; set; }
public string? Code { get; set; }
public string? Description { get; set; }
public int SortOrder { get; set; } = 0;
public byte Status { get; set; } = 1;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public ICollection<AdminUser> AdminUsers { get; set; }
public ICollection<DepartmentMenu> DepartmentMenus { get; set; }
}
// 角色
public class Role
{
public long Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public string? Description { get; set; }
public int SortOrder { get; set; } = 0;
public byte Status { get; set; } = 1;
public bool IsSystem { get; set; } = false;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public ICollection<AdminUserRole> AdminUserRoles { get; set; }
public ICollection<RoleMenu> RoleMenus { get; set; }
public ICollection<RolePermission> RolePermissions { get; set; }
}
// 菜单
public class Menu
{
public long Id { get; set; }
public long ParentId { get; set; } = 0;
public string Name { get; set; }
public string? Path { get; set; }
public string? Component { get; set; }
public string? Icon { get; set; }
public byte MenuType { get; set; } = 1; // 1目录 2菜单 3按钮
public string? Permission { get; set; }
public int SortOrder { get; set; } = 0;
public byte Status { get; set; } = 1; // 0隐藏 1显示
public bool IsExternal { get; set; } = false;
public bool IsCache { get; set; } = true;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public ICollection<RoleMenu> RoleMenus { get; set; }
}
// 权限
public class Permission
{
public long Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public string? Module { get; set; }
public string? Description { get; set; }
public DateTime CreatedAt { get; set; }
public ICollection<RolePermission> RolePermissions { get; set; }
}
// 操作日志
public class OperationLog
{
public long Id { get; set; }
public long? AdminUserId { get; set; }
public string? Username { get; set; }
public string? Module { get; set; }
public string? Action { get; set; }
public string? Method { get; set; }
public string? Url { get; set; }
public string? Ip { get; set; }
public string? RequestData { get; set; }
public string? ResponseData { get; set; }
public byte Status { get; set; } // 0失败 1成功
public string? ErrorMsg { get; set; }
public int Duration { get; set; }
public DateTime CreatedAt { get; set; }
}
// 关联表
public class AdminUserRole
{
public long Id { get; set; }
public long AdminUserId { get; set; }
public long RoleId { get; set; }
public AdminUser AdminUser { get; set; }
public Role Role { get; set; }
}
public class RoleMenu
{
public long Id { get; set; }
public long RoleId { get; set; }
public long MenuId { get; set; }
public Role Role { get; set; }
public Menu Menu { get; set; }
}
public class RolePermission
{
public long Id { get; set; }
public long RoleId { get; set; }
public long PermissionId { get; set; }
public Role Role { get; set; }
public Permission Permission { get; set; }
}
// 部门-菜单关联
public class DepartmentMenu
{
public long Id { get; set; }
public long DepartmentId { get; set; }
public long MenuId { get; set; }
public Department Department { get; set; }
public Menu Menu { get; set; }
}
// 用户-菜单关联(用户专属菜单)
public class AdminUserMenu
{
public long Id { get; set; }
public long AdminUserId { get; set; }
public long MenuId { get; set; }
public AdminUser AdminUser { get; set; }
public Menu Menu { get; set; }
}
DTO Models
// 通用响应
public class ApiResponse<T>
{
public int Code { get; set; }
public string Message { get; set; }
public T? Data { get; set; }
public static ApiResponse<T> Success(T data, string message = "success")
=> new() { Code = 0, Message = message, Data = data };
public static ApiResponse<T> Error(int code, string message)
=> new() { Code = code, Message = message };
}
// 分页结果
public class PagedResult<T>
{
public List<T> List { get; set; }
public int Total { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
}
// 登录请求/响应
public class LoginRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
public class LoginResponse
{
public string Token { get; set; }
public long ExpiresIn { get; set; }
public AdminUserInfo UserInfo { get; set; }
}
public class AdminUserInfo
{
public long Id { get; set; }
public string Username { get; set; }
public string? RealName { get; set; }
public string? Avatar { get; set; }
public List<string> Roles { get; set; }
public List<string> Permissions { get; set; }
}
// 菜单树
public class MenuTreeDto
{
public long Id { get; set; }
public long ParentId { get; set; }
public string Name { get; set; }
public string? Path { get; set; }
public string? Component { get; set; }
public string? Icon { get; set; }
public byte MenuType { get; set; }
public string? Permission { get; set; }
public int SortOrder { get; set; }
public List<MenuTreeDto> Children { get; set; }
}
// 操作日志
public class OperationLogDto
{
public long Id { get; set; }
public long? AdminUserId { get; set; }
public string? Username { get; set; }
public string? Module { get; set; }
public string? Action { get; set; }
public string? Method { get; set; }
public string? Url { get; set; }
public string? Ip { get; set; }
public byte Status { get; set; }
public int Duration { get; set; }
public DateTime CreatedAt { get; set; }
}
public class OperationLogQueryRequest
{
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
public long? AdminUserId { get; set; }
public string? Module { get; set; }
public string? Action { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
}
public class OperationLogRequest
{
public long? AdminUserId { get; set; }
public string? Username { get; set; }
public string? Module { get; set; }
public string? Action { get; set; }
public string? Method { get; set; }
public string? Url { get; set; }
public string? Ip { get; set; }
public string? RequestData { get; set; }
public string? ResponseData { get; set; }
public byte Status { get; set; }
public string? ErrorMsg { get; set; }
public int Duration { get; set; }
}
// 部门树
public class DepartmentTreeDto
{
public long Id { get; set; }
public long ParentId { get; set; }
public string Name { get; set; }
public string? Code { get; set; }
public int SortOrder { get; set; }
public byte Status { get; set; }
public int UserCount { get; set; }
public List<DepartmentTreeDto> Children { get; set; }
}
public class DepartmentDto
{
public long Id { get; set; }
public long ParentId { get; set; }
public string Name { get; set; }
public string? Code { get; set; }
public string? Description { get; set; }
public int SortOrder { get; set; }
public byte Status { get; set; }
public List<long> MenuIds { get; set; }
}
public class CreateDepartmentRequest
{
public long ParentId { get; set; } = 0;
public string Name { get; set; }
public string? Code { get; set; }
public string? Description { get; set; }
public int SortOrder { get; set; } = 0;
}
public class UpdateDepartmentRequest
{
public long ParentId { get; set; }
public string Name { get; set; }
public string? Code { get; set; }
public string? Description { get; set; }
public int SortOrder { get; set; }
public byte Status { get; set; }
}
Database Schema
数据库表结构参见迁移计划文档 docs/后台管理系统迁移计划.md 第三节。
连接配置
{
"ConnectionStrings": {
"DefaultConnection": "Server=192.168.195.15;uid=sa;pwd=Dbt@com@123;Database=honey_box_admin;MultipleActiveResultSets=true;pooling=true;min pool size=5;max pool size=32767;connect timeout=20;Encrypt=True;TrustServerCertificate=True;",
"Redis": "192.168.195.15:6379,abortConnect=false,connectTimeout=5000"
}
}
MCP 工具: 使用 admin-sqlserver MCP 插件可以直接查询数据库,用于反向生成实体或验证表结构。
AdminDbContext 配置
public class AdminDbContext : DbContext
{
public DbSet<AdminUser> AdminUsers { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<Menu> Menus { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Permission> Permissions { get; set; }
public DbSet<AdminUserRole> AdminUserRoles { get; set; }
public DbSet<AdminUserMenu> AdminUserMenus { get; set; }
public DbSet<RoleMenu> RoleMenus { get; set; }
public DbSet<RolePermission> RolePermissions { get; set; }
public DbSet<DepartmentMenu> DepartmentMenus { get; set; }
public DbSet<OperationLog> OperationLogs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 配置唯一索引
modelBuilder.Entity<AdminUser>()
.HasIndex(e => e.Username).IsUnique();
modelBuilder.Entity<Role>()
.HasIndex(e => e.Code).IsUnique();
modelBuilder.Entity<Permission>()
.HasIndex(e => e.Code).IsUnique();
modelBuilder.Entity<Department>()
.HasIndex(e => e.Code).IsUnique();
// 配置关联表复合唯一索引
modelBuilder.Entity<AdminUserRole>()
.HasIndex(e => new { e.AdminUserId, e.RoleId }).IsUnique();
modelBuilder.Entity<AdminUserMenu>()
.HasIndex(e => new { e.AdminUserId, e.MenuId }).IsUnique();
modelBuilder.Entity<RoleMenu>()
.HasIndex(e => new { e.RoleId, e.MenuId }).IsUnique();
modelBuilder.Entity<RolePermission>()
.HasIndex(e => new { e.RoleId, e.PermissionId }).IsUnique();
modelBuilder.Entity<DepartmentMenu>()
.HasIndex(e => new { e.DepartmentId, e.MenuId }).IsUnique();
}
}
Vue3 Frontend Structure
项目结构
admin-web/
├── public/
│ └── favicon.ico
├── src/
│ ├── api/ # API 接口
│ │ ├── auth.ts
│ │ ├── menu.ts
│ │ ├── role.ts
│ │ ├── department.ts
│ │ └── user.ts
│ ├── assets/ # 静态资源
│ │ └── styles/
│ ├── components/ # 公共组件
│ │ └── SvgIcon/
│ ├── layout/ # 布局组件
│ │ ├── index.vue # 主布局
│ │ ├── Sidebar/ # 侧边栏
│ │ ├── Header/ # 顶部栏
│ │ └── TagsView/ # 标签页
│ ├── router/ # 路由
│ │ ├── index.ts
│ │ └── routes.ts
│ ├── store/ # Pinia 状态管理
│ │ ├── modules/
│ │ │ ├── user.ts # 用户状态
│ │ │ ├── permission.ts # 权限状态
│ │ │ └── app.ts # 应用状态
│ │ └── index.ts
│ ├── utils/ # 工具函数
│ │ ├── request.ts # Axios 封装
│ │ ├── auth.ts # Token 管理
│ │ └── permission.ts # 权限工具
│ ├── views/ # 页面组件
│ │ ├── login/ # 登录页
│ │ ├── dashboard/ # 首页
│ │ └── system/ # 系统管理
│ │ ├── menu/ # 菜单管理
│ │ ├── role/ # 角色管理
│ │ ├── department/ # 部门管理
│ │ └── user/ # 管理员管理
│ ├── directives/ # 自定义指令
│ │ └── permission.ts # v-permission
│ ├── App.vue
│ └── main.ts
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts
动态路由生成
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { usePermissionStore } from '@/store/modules/permission'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/login', component: () => import('@/views/login/index.vue') },
{ path: '/', redirect: '/dashboard' }
]
})
router.beforeEach(async (to, from, next) => {
const token = getToken()
if (token) {
if (to.path === '/login') {
next('/')
} else {
const permissionStore = usePermissionStore()
if (!permissionStore.routes.length) {
// 获取用户菜单并生成动态路由
await permissionStore.generateRoutes()
next({ ...to, replace: true })
} else {
next()
}
}
} else {
if (to.path === '/login') {
next()
} else {
next('/login')
}
}
})
权限指令
// directives/permission.ts
import type { Directive } from 'vue'
import { useUserStore } from '@/store/modules/user'
export const permission: Directive = {
mounted(el, binding) {
const { value } = binding
const userStore = useUserStore()
const permissions = userStore.permissions
if (value && !permissions.includes(value)) {
el.parentNode?.removeChild(el)
}
}
}
// 使用: <el-button v-permission="'user:add'">新增</el-button>
Correctness Properties
A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
Property 1: Valid credentials return JWT token
For any admin user with valid credentials (correct username and password, enabled status, not locked), calling the login API SHALL return a valid JWT token and user info.
Validates: Requirements 3.1
Property 2: Invalid credentials are rejected
For any login attempt with invalid credentials (wrong username or wrong password), the Auth_Service SHALL return an authentication error and not issue a token.
Validates: Requirements 3.2
Property 3: Disabled accounts cannot login
For any admin user with disabled status (status = 0), login attempts SHALL be rejected regardless of correct password.
Validates: Requirements 3.3
Property 4: Account lockout after failed attempts
For any admin user, after 5 consecutive failed login attempts, the account SHALL be locked for 30 minutes and subsequent login attempts SHALL be rejected until the lockout period expires.
Validates: Requirements 3.4
Property 5: Valid JWT tokens authenticate successfully
For any valid, non-expired JWT token issued by the system, requests with this token SHALL be authenticated successfully.
Validates: Requirements 3.6
Property 6: Invalid JWT tokens return 401
For any invalid, expired, or malformed JWT token, requests SHALL return 401 Unauthorized.
Validates: Requirements 3.7
Property 7: Menu tree structure integrity
For any set of menus with parent-child relationships, the GetMenuTree operation SHALL return a valid tree where each menu appears exactly once and children are nested under their parent.
Validates: Requirements 4.1
Property 8: Menu with children cannot be deleted
For any menu that has child menus (other menus with parent_id pointing to it), deletion SHALL fail with an error.
Validates: Requirements 4.4
Property 9: User menus are multi-source merged
For any admin user, the menus returned by GetUserMenus SHALL be the union of: department menus, role menus, and user-specific menus.
Validates: Requirements 4.7, 11.2, 11.3
Property 10: Department tree structure integrity
For any set of departments with parent-child relationships, the GetDepartmentTree operation SHALL return a valid tree where each department appears exactly once and children are nested under their parent.
Validates: Requirements 10.2
Property 11: Department with children cannot be deleted
For any department that has child departments (other departments with parent_id pointing to it), deletion SHALL fail with an error.
Validates: Requirements 10.5
Property 12: Department with users cannot be deleted
For any department that has users assigned to it, deletion SHALL fail with an error.
Validates: Requirements 10.6
Property 13: Department cannot be its own ancestor
For any department update operation, setting parent_id to itself or any of its descendants SHALL fail with an error.
Validates: Requirements 10.4
Property 14: System role protection
For any role with is_system = true, deletion SHALL fail with an error.
Validates: Requirements 5.4
Property 15: Last super admin protection
For any deletion attempt on the last admin user with super admin role, the deletion SHALL fail with an error.
Validates: Requirements 6.4
Property 16: Permission enforcement
For any API endpoint with a permission attribute, if the requesting user lacks that permission, the request SHALL return 403 Forbidden.
Validates: Requirements 7.1, 7.2
Property 17: Permission cache invalidation
For any admin user whose roles, department, or direct menus are changed, the permission cache SHALL be invalidated and subsequent permission checks SHALL reflect the new assignments.
Validates: Requirements 7.5, 11.5
Property 18: Operation logging completeness
For any create, update, or delete operation performed by an admin user, an operation log entry SHALL be created with the admin user id, action, and timestamp.
Validates: Requirements 8.1, 8.2
Error Handling
Error Code Specification
| Code Range | Category | Description |
|---|---|---|
| 0 | Success | 操作成功 |
| 40001-40099 | Authentication | 认证相关错误 |
| 40101-40199 | Authorization | 权限相关错误 |
| 40201-40299 | Validation | 参数验证错误 |
| 50001-50099 | Server | 服务器内部错误 |
Specific Error Codes
public static class AdminErrorCodes
{
// Authentication
public const int InvalidCredentials = 40001;
public const int AccountDisabled = 40002;
public const int AccountLocked = 40003;
public const int TokenExpired = 40004;
public const int TokenInvalid = 40005;
// Authorization
public const int PermissionDenied = 40101;
public const int RoleNotFound = 40102;
// Validation
public const int InvalidParameter = 40201;
public const int DuplicateUsername = 40202;
public const int DuplicateRoleCode = 40203;
public const int MenuHasChildren = 40204;
public const int CannotDeleteSystemRole = 40205;
public const int CannotDeleteLastSuperAdmin = 40206;
public const int DepartmentHasChildren = 40207;
public const int DepartmentHasUsers = 40208;
public const int DepartmentCircularReference = 40209;
public const int DuplicateDepartmentCode = 40210;
// Server
public const int InternalError = 50001;
public const int DatabaseError = 50002;
}
Testing Strategy
Unit Tests
- Service 层单元测试,使用 Mock 数据库
- 测试各 Service 的核心业务逻辑
- 测试边界条件和异常情况
Integration Tests
- API 集成测试,使用内存数据库
- 测试完整的请求-响应流程
- 测试认证和权限过滤器
Property-Based Tests
使用 FsCheck 或类似库进行属性测试:
- 测试 JWT Token 的生成和验证
- 测试菜单树结构的正确性
- 测试权限检查的一致性
Test Configuration
// 使用 xUnit + Moq + FluentAssertions
// 每个属性测试至少运行 100 次迭代
// 测试标签格式: Feature: admin-system, Property N: {property_text}