登录权限返回修改
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-30
|
||||
@@ -0,0 +1,590 @@
|
||||
## Context
|
||||
|
||||
当前登录系统通过 `auth.Service.Login()` 方法查询用户的角色权限,并返回扁平的权限码列表 `permissions: []string`。前端收到这个列表后,需要:
|
||||
|
||||
1. **菜单渲染问题**:权限码是扁平的(如 `["user:menu", "user:list:menu", "order:menu"]`),但前端侧边栏需要树形结构。前端要么额外请求菜单接口 `GET /api/admin/permissions/tree`,要么在本地维护菜单配置与权限码的映射关系。
|
||||
2. **按钮控制问题**:权限码中混合了菜单权限和按钮权限,前端需要自行区分哪些是用于显示/隐藏按钮的权限(如 `user:create`, `user:delete`)。
|
||||
|
||||
现有的 `tb_permission` 表已经包含了构建菜单树所需的所有字段:
|
||||
- `perm_type`: 权限类型(1=菜单权限,2=按钮权限)
|
||||
- `parent_id`: 父级权限 ID(用于树形结构)
|
||||
- `platform`: 平台标识(web/h5/all)
|
||||
- `sort`: 排序字段
|
||||
|
||||
现有的权限查询逻辑(`auth.Service.getUserPermissions()`)已经能够获取用户的所有权限 ID,并查询权限详情。我们只需要在此基础上增加分类和树形结构构建逻辑。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
1. 登录时返回结构化的权限数据,前端无需二次处理或额外请求
|
||||
2. 菜单数据为树形结构,可直接用于渲染侧边栏(基于 `parent_id` 构建)
|
||||
3. 按钮权限为扁平列表,可直接用于 `hasPermission()` 判断
|
||||
4. 根据 `device` 参数自动过滤平台(避免泄露其他端的菜单)
|
||||
5. 保持向后兼容性(保留原有 `permissions` 字段)
|
||||
6. 性能可控(登录响应时间增加 < 50ms)
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
1. **不修改 GetMe 接口**:避免频繁查询和构建菜单树,前端应将菜单数据缓存到 localStorage
|
||||
2. **不添加额外字段**:MenuNode 不包含 `icon`, `badge`, `hidden` 等扩展字段(保持简洁)
|
||||
3. **不修改数据库 schema**:复用现有 `tb_permission` 表的字段
|
||||
4. **不实现动态菜单刷新**:权限变更后需要重新登录(短期方案)
|
||||
5. **不处理权限变更通知**:不引入 WebSocket 推送(长期优化项)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 决策 1: 数据结构设计
|
||||
|
||||
**选择**:新增 `MenuNode` DTO,包含 `id`, `perm_code`, `name`, `url`, `sort`, `children` 字段
|
||||
|
||||
**理由**:
|
||||
- `id`: 用于调试和日志追踪(虽然前端主要使用 `perm_code`)
|
||||
- `perm_code`: 前端可能用于路由匹配或权限验证
|
||||
- `name`: 显示在侧边栏的文本
|
||||
- `url`: 路由路径(前端用于 `<router-link>`)
|
||||
- `sort`: 保持菜单顺序(前端直接渲染,不需要再排序)
|
||||
- `children`: 递归结构(支持无限层级)
|
||||
|
||||
**替代方案**:
|
||||
- **方案 A(被拒绝)**:只返回菜单 ID 列表,前端根据 ID 查询菜单详情
|
||||
- 缺点:增加前端复杂度,需要维护 ID 到菜单对象的映射
|
||||
- **方案 B(被拒绝)**:返回完整的 `Permission` 对象(包含所有字段)
|
||||
- 缺点:响应体过大,包含前端不需要的字段(如 `creator`, `updater`, `created_at`)
|
||||
|
||||
### 决策 2: 权限分类逻辑
|
||||
|
||||
**选择**:基于 `perm_type` 字段分类(1=菜单,2=按钮),在 Service 层实现
|
||||
|
||||
**数据流**:
|
||||
```
|
||||
查询用户权限 → 获取 Permission 对象列表
|
||||
↓
|
||||
遍历权限,根据 perm_type 分类:
|
||||
├── perm_type = 1 → 菜单权限列表(用于构建树)
|
||||
├── perm_type = 2 → 按钮权限码列表(直接提取 perm_code)
|
||||
└── 所有权限码 → permissions 字段(向后兼容)
|
||||
↓
|
||||
菜单权限 → buildMenuTree() → 树形结构
|
||||
按钮权限 → 扁平列表
|
||||
```
|
||||
|
||||
**实现位置**:`auth.Service.getUserPermissionsAndMenus()`(新增方法)
|
||||
|
||||
**理由**:
|
||||
- 复用现有的权限查询逻辑(`getUserPermissions()` 已经能获取所有权限)
|
||||
- 单一职责:权限分类逻辑放在 Service 层(不在 Handler 层处理)
|
||||
- 可测试性:分类和树构建逻辑可以独立单元测试
|
||||
|
||||
**替代方案**:
|
||||
- **方案 A(被拒绝)**:在数据库查询时分别查询菜单权限和按钮权限
|
||||
- 缺点:需要两次查询,增加数据库负载
|
||||
- **方案 B(被拒绝)**:在前端分类
|
||||
- 缺点:增加前端复杂度,需要理解 `perm_type` 字段含义
|
||||
|
||||
### 决策 3: 菜单树构建算法
|
||||
|
||||
**选择**:使用 HashMap + 单次遍历构建树(O(n) 时间复杂度)
|
||||
|
||||
**算法**:
|
||||
```go
|
||||
func buildMenuTree(permissions []*model.Permission) []dto.MenuNode {
|
||||
// 第一步:创建节点映射(ID → MenuNode)
|
||||
nodeMap := make(map[uint]*dto.MenuNode)
|
||||
for _, p := range permissions {
|
||||
nodeMap[p.ID] = &dto.MenuNode{
|
||||
ID: p.ID,
|
||||
PermCode: p.PermCode,
|
||||
Name: p.PermName,
|
||||
URL: p.URL,
|
||||
Sort: p.Sort,
|
||||
Children: make([]dto.MenuNode, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步:组织父子关系
|
||||
var roots []dto.MenuNode
|
||||
for _, p := range permissions {
|
||||
node := nodeMap[p.ID]
|
||||
if p.ParentID == nil || *p.ParentID == 0 {
|
||||
// 根节点
|
||||
roots = append(roots, *node)
|
||||
} else if parent, ok := nodeMap[*p.ParentID]; ok {
|
||||
// 有父节点 → 追加到父节点的 children
|
||||
parent.Children = append(parent.Children, *node)
|
||||
} else {
|
||||
// 孤儿节点(父节点不在权限列表中)→ 提升为根节点
|
||||
roots = append(roots, *node)
|
||||
}
|
||||
}
|
||||
|
||||
// 第三步:递归排序
|
||||
sortMenuNodes(roots)
|
||||
return roots
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 时间复杂度 O(n),空间复杂度 O(n)(n 为权限数量,通常 < 100)
|
||||
- 单次遍历,无需递归查询数据库
|
||||
- 自然处理孤儿节点(无父权限的子菜单提升为根节点)
|
||||
|
||||
**孤儿节点处理**:
|
||||
- 场景:用户有 `user:list:menu`(parent_id=1)但没有 `user:menu`(id=1)
|
||||
- 行为:将 `user:list:menu` 提升为根节点
|
||||
- 理由:避免菜单丢失,同时暴露权限配置问题(应该在角色分配时避免)
|
||||
|
||||
**替代方案**:
|
||||
- **方案 A(被拒绝)**:递归查询数据库构建树
|
||||
- 缺点:N+1 查询问题,性能差
|
||||
- **方案 B(被拒绝)**:孤儿节点直接丢弃
|
||||
- 缺点:用户有权限但看不到菜单,体验差
|
||||
|
||||
### 决策 4: 平台过滤策略
|
||||
|
||||
**选择**:在 Service 层根据 `device` 参数过滤 `platform` 字段
|
||||
|
||||
**过滤规则**:
|
||||
```go
|
||||
// 在分类权限时应用过滤
|
||||
for _, perm := range permissions {
|
||||
// 平台过滤
|
||||
if perm.Platform != constants.PlatformAll && perm.Platform != device {
|
||||
continue // 跳过不匹配的权限
|
||||
}
|
||||
|
||||
// 分类
|
||||
if perm.PermType == constants.PermTypeMenu {
|
||||
menuPerms = append(menuPerms, perm)
|
||||
} else if perm.PermType == constants.PermTypeButton {
|
||||
buttonCodes = append(buttonCodes, perm.PermCode)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 安全性:避免泄露其他端的菜单结构(如 Web 后台登录不返回 H5 菜单)
|
||||
- 减少响应体大小:只返回当前端口需要的菜单
|
||||
- 简化前端逻辑:前端无需二次过滤
|
||||
|
||||
**默认值**:`device` 未指定时默认为 `"web"`
|
||||
|
||||
**替代方案**:
|
||||
- **方案 A(被拒绝)**:返回所有平台的菜单,前端自行过滤
|
||||
- 缺点:安全风险(泄露其他端的菜单结构),响应体增大
|
||||
- **方案 B(被拒绝)**:在数据库查询时过滤
|
||||
- 缺点:需要修改现有的权限查询逻辑,增加复杂度
|
||||
|
||||
### 决策 5: 超级管理员特殊处理
|
||||
|
||||
**选择**:超级管理员直接查询所有启用的权限(`status=1`),不查询角色关联表
|
||||
|
||||
**实现**:
|
||||
```go
|
||||
func (s *Service) getUserPermissionsAndMenus(ctx, userID, userType, device) {
|
||||
if userType == constants.UserTypeSuperAdmin {
|
||||
// 超级管理员:查询所有启用的权限
|
||||
allPerms, err := s.permissionStore.GetAll(ctx, nil)
|
||||
// 应用平台过滤 + 分类
|
||||
return s.classifyPermissions(allPerms, device)
|
||||
}
|
||||
|
||||
// 普通用户:查询角色权限
|
||||
// ... (现有逻辑)
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 超级管理员需要看到所有功能模块(管理员职责)
|
||||
- 复用现有的 `GetAll()` 方法(已有实现)
|
||||
- 仍然应用平台过滤(超管在 Web 后台登录不应该看到 H5 菜单)
|
||||
|
||||
**性能考虑**:
|
||||
- 数据库查询:`SELECT * FROM tb_permission WHERE status = 1`(单次查询,无 JOIN)
|
||||
- 预计权限数量 < 200,查询时间 < 10ms
|
||||
|
||||
**替代方案**:
|
||||
- **方案 A(被拒绝)**:超级管理员也查询角色权限
|
||||
- 缺点:需要为超管分配角色,管理复杂度增加
|
||||
- **方案 B(被拒绝)**:超级管理员返回硬编码的菜单列表
|
||||
- 缺点:不灵活,新增菜单需要修改代码
|
||||
|
||||
### 决策 6: 排序实现
|
||||
|
||||
**选择**:递归排序(根据 `sort` 字段升序)
|
||||
|
||||
**实现**:
|
||||
```go
|
||||
func sortMenuNodes(nodes []dto.MenuNode) {
|
||||
// 排序当前层级
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].Sort < nodes[j].Sort
|
||||
})
|
||||
|
||||
// 递归排序子节点
|
||||
for i := range nodes {
|
||||
if len(nodes[i].Children) > 0 {
|
||||
sortMenuNodes(nodes[i].Children)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 前端直接渲染,无需再次排序
|
||||
- 支持多层级排序(递归应用到所有子节点)
|
||||
- 稳定排序(相同 `sort` 值时保持原有顺序)
|
||||
|
||||
**替代方案**:
|
||||
- **方案 A(被拒绝)**:在数据库查询时排序
|
||||
- 缺点:只能排序扁平列表,无法处理树形结构的层级排序
|
||||
- **方案 B(被拒绝)**:前端排序
|
||||
- 缺点:增加前端复杂度
|
||||
|
||||
### 决策 7: 向后兼容性
|
||||
|
||||
**选择**:保留原有 `permissions` 字段,同时新增 `menus` 和 `buttons` 字段
|
||||
|
||||
**LoginResponse 结构**:
|
||||
```go
|
||||
type LoginResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
User UserInfo `json:"user"`
|
||||
|
||||
// 向后兼容(现有字段)
|
||||
Permissions []string `json:"permissions"`
|
||||
|
||||
// 新增字段
|
||||
Menus []MenuNode `json:"menus"`
|
||||
Buttons []string `json:"buttons"`
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 旧版前端仍可使用 `permissions` 字段正常工作
|
||||
- 新版前端可以选择使用 `menus` 和 `buttons` 字段
|
||||
- 平滑迁移(无需强制升级前端)
|
||||
|
||||
**后续优化**(可选):
|
||||
- 6 个月后评估是否废弃 `permissions` 字段
|
||||
- 通过 API 版本控制(`/api/v2/login`)彻底移除旧字段
|
||||
|
||||
**替代方案**:
|
||||
- **方案 A(被拒绝)**:直接替换 `permissions` 字段为 `menus` 和 `buttons`
|
||||
- 缺点:破坏性变更,旧版前端无法工作
|
||||
- **方案 B(被拒绝)**:通过 `Accept` 头或 query 参数控制返回格式
|
||||
- 缺点:增加复杂度,难以维护
|
||||
|
||||
### 决策 8: GetMe 接口不返回菜单
|
||||
|
||||
**选择**:`GET /api/admin/me` 和 `GET /api/h5/me` 保持不变(只返回 `user` 和 `permissions`)
|
||||
|
||||
**理由**:
|
||||
- GetMe 是高频接口(前端可能每次路由切换都调用)
|
||||
- 菜单树构建有计算成本(虽然很小,但不必要)
|
||||
- 前端应该将菜单数据缓存到 localStorage(登录后只构建一次)
|
||||
|
||||
**前端使用模式**:
|
||||
```javascript
|
||||
// 登录成功
|
||||
const response = await api.login({username, password});
|
||||
localStorage.setItem('menus', JSON.stringify(response.menus));
|
||||
localStorage.setItem('buttons', JSON.stringify(response.buttons));
|
||||
|
||||
// 页面刷新
|
||||
const menus = JSON.parse(localStorage.getItem('menus'));
|
||||
renderSidebar(menus);
|
||||
|
||||
// 权限变更后(可选)
|
||||
// 调用 api.login() 重新获取菜单,或者提供"刷新权限"按钮
|
||||
```
|
||||
|
||||
**替代方案**:
|
||||
- **方案 A(被拒绝)**:GetMe 也返回菜单
|
||||
- 缺点:高频接口性能下降,不必要的计算
|
||||
- **方案 B(被拒绝)**:提供单独的 `GET /api/admin/menus` 端点
|
||||
- 缺点:增加端点数量,前端需要额外请求
|
||||
|
||||
## Architecture
|
||||
|
||||
### 模块依赖关系
|
||||
|
||||
```
|
||||
internal/handler/admin/auth.go (AuthHandler.Login)
|
||||
↓ 调用
|
||||
internal/service/auth/service.go (Service.Login)
|
||||
↓ 调用(新增)
|
||||
internal/service/auth/service.go (Service.getUserPermissionsAndMenus)
|
||||
↓ 调用
|
||||
internal/store/postgres/permission_store.go (PermissionStore.GetByIDs / GetAll)
|
||||
↓ 返回
|
||||
[]*model.Permission
|
||||
↓ 分类和构建
|
||||
internal/service/auth/service.go (classifyPermissions, buildMenuTree)
|
||||
↓ 返回
|
||||
([]dto.MenuNode, []string)
|
||||
```
|
||||
|
||||
### 文件修改清单
|
||||
|
||||
**新增文件**:无
|
||||
|
||||
**修改文件**:
|
||||
1. `internal/model/dto/auth_dto.go`
|
||||
- 新增 `MenuNode` 结构体
|
||||
- 修改 `LoginResponse` 结构体(新增 `Menus` 和 `Buttons` 字段)
|
||||
|
||||
2. `internal/service/auth/service.go`
|
||||
- 修改 `Login()` 方法:调用 `getUserPermissionsAndMenus()` 替代 `getUserPermissions()`
|
||||
- 新增 `getUserPermissionsAndMenus()` 方法:查询权限并分类
|
||||
- 新增 `classifyPermissions()` 方法:分类菜单和按钮权限,应用平台过滤
|
||||
- 新增 `buildMenuTree()` 方法:构建菜单树
|
||||
- 新增 `sortMenuNodes()` 方法:递归排序菜单节点
|
||||
- 新增 `getAllPermissionsForSuperAdmin()` 方法:超级管理员获取所有权限
|
||||
|
||||
### 数据流图
|
||||
|
||||
```
|
||||
用户登录 (device="web")
|
||||
↓
|
||||
AuthHandler.Login()
|
||||
↓
|
||||
Service.Login()
|
||||
↓ 验证用户名/密码
|
||||
↓ 生成 Token
|
||||
↓
|
||||
Service.getUserPermissionsAndMenus(userID, userType, device)
|
||||
↓
|
||||
├─ 超级管理员?
|
||||
│ └─> PermissionStore.GetAll(ctx, nil)
|
||||
│ → 所有启用的权限
|
||||
│
|
||||
└─ 普通用户?
|
||||
└─> AccountRoleStore.GetByAccountID(userID)
|
||||
→ RolePermissionStore.GetPermIDsByRoleIDs(roleIDs)
|
||||
→ PermissionStore.GetByIDs(permIDs)
|
||||
→ 用户的权限列表
|
||||
↓
|
||||
Service.classifyPermissions(permissions, device)
|
||||
↓
|
||||
├─ 遍历权限,平台过滤
|
||||
│ ├─ platform == "all" 或 platform == device?✅
|
||||
│ └─ 否则跳过
|
||||
│
|
||||
├─ perm_type == 1 (菜单) → 收集到 menuPerms
|
||||
├─ perm_type == 2 (按钮) → 收集到 buttonCodes
|
||||
└─ 所有权限码 → allCodes
|
||||
↓
|
||||
buildMenuTree(menuPerms)
|
||||
↓
|
||||
├─ 第一步:创建节点映射 (HashMap)
|
||||
├─ 第二步:组织父子关系
|
||||
│ ├─ parent_id == NULL → 根节点
|
||||
│ ├─ parent_id 存在 → 追加到 parent.Children
|
||||
│ └─ parent_id 不存在 → 孤儿节点提升为根节点
|
||||
└─ 第三步:递归排序
|
||||
↓
|
||||
返回 LoginResponse {
|
||||
menus: []MenuNode (树形结构),
|
||||
buttons: []string (扁平),
|
||||
permissions: []string (所有权限码)
|
||||
}
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### 风险 1: 登录响应体增大
|
||||
|
||||
**风险**:菜单树包含完整的节点信息(id, perm_code, name, url, sort, children),响应体可能增加 5-10KB。
|
||||
|
||||
**影响**:
|
||||
- 普通用户(20-30 个权限):响应体增加约 3-5KB
|
||||
- 超级管理员(100+ 个权限):响应体增加约 8-12KB
|
||||
|
||||
**缓解措施**:
|
||||
- 使用 Fiber 的自动 Gzip 压缩(压缩率约 60-70%)
|
||||
- 前端使用 localStorage 缓存,登录后只需传输一次
|
||||
- MenuNode 结构保持最小化(不包含非必要字段)
|
||||
|
||||
**监控**:登录接口响应体大小(目标 < 20KB)
|
||||
|
||||
---
|
||||
|
||||
### 风险 2: 菜单树构建性能
|
||||
|
||||
**风险**:菜单树构建算法(HashMap + 遍历 + 递归排序)可能在权限数量大时影响性能。
|
||||
|
||||
**影响分析**:
|
||||
- 时间复杂度:O(n) + O(n log n)(遍历 + 排序)
|
||||
- 100 个权限的场景:预计耗时 < 10ms
|
||||
- 500 个权限的场景(极端情况):预计耗时 < 50ms
|
||||
|
||||
**缓解措施**:
|
||||
- 算法复杂度已优化(避免递归查询数据库)
|
||||
- 超级管理员的权限数量可控(< 200)
|
||||
- 登录频率低(用户一天登录 1-2 次)
|
||||
|
||||
**备选方案**(长期优化):
|
||||
- 引入 Redis 缓存:缓存已构建的菜单树(以 `user_id + device` 为 key)
|
||||
- 缓存失效策略:权限变更时清除相关用户的缓存
|
||||
|
||||
---
|
||||
|
||||
### 风险 3: 孤儿节点提升可能导致 UI 混乱
|
||||
|
||||
**风险**:如果权限配置不当(用户有子菜单权限但无父菜单权限),孤儿节点会被提升为根节点,可能破坏前端菜单层级设计。
|
||||
|
||||
**示例**:
|
||||
- 预期:用户管理 > 用户列表(二级菜单)
|
||||
- 实际:用户列表(一级菜单)← 孤儿节点提升
|
||||
|
||||
**缓解措施**:
|
||||
- **开发阶段**:在角色分配时进行校验,确保子菜单权限必须与父菜单权限一起分配
|
||||
- **运行时**:记录警告日志(检测到孤儿节点时)
|
||||
```go
|
||||
if parentID != nil && !nodeMap[*parentID] {
|
||||
logger.Warn("检测到孤儿节点", zap.Uint("child_id", p.ID), zap.Uint("parent_id", *parentID))
|
||||
}
|
||||
```
|
||||
- **管理界面**:角色权限分配时自动勾选父菜单(前端实现)
|
||||
|
||||
**替代方案**:
|
||||
- 孤儿节点直接丢弃(不返回)
|
||||
- 缺点:用户有权限但看不到菜单,体验更差
|
||||
|
||||
---
|
||||
|
||||
### 风险 4: 前端缓存过期问题
|
||||
|
||||
**风险**:前端将菜单数据缓存到 localStorage 后,如果后端权限变更,用户需要重新登录才能看到最新菜单。
|
||||
|
||||
**场景**:
|
||||
1. 管理员为某角色新增菜单权限
|
||||
2. 用户的 Token 仍有效(24 小时内)
|
||||
3. 用户继续使用旧的菜单数据(localStorage 中的缓存)
|
||||
|
||||
**缓解措施(短期)**:
|
||||
- 前端提供"刷新权限"按钮(调用 `POST /api/admin/login` 重新登录)
|
||||
- 文档说明:权限变更后需要重新登录生效
|
||||
|
||||
**长期优化方案**:
|
||||
- 引入 WebSocket 推送:权限变更时通知在线用户刷新
|
||||
- 提供独立的 `GET /api/admin/menus` 端点(按需刷新菜单)
|
||||
- Token 过期时间缩短(如 12 小时)
|
||||
|
||||
---
|
||||
|
||||
### 权衡: GetMe 不返回菜单
|
||||
|
||||
**权衡**:GetMe 接口不返回菜单,前端依赖 localStorage 缓存。
|
||||
|
||||
**优点**:
|
||||
- GetMe 性能保持高效(高频接口)
|
||||
- 菜单只在登录时构建一次
|
||||
|
||||
**缺点**:
|
||||
- 前端需要管理 localStorage 缓存(增加复杂度)
|
||||
- 页面刷新时如果缓存丢失,需要重新登录
|
||||
|
||||
**缓解措施**:
|
||||
- 前端 SDK 封装缓存逻辑(提供统一的 `getMenus()` 方法)
|
||||
- 文档提供最佳实践示例
|
||||
|
||||
---
|
||||
|
||||
### 权衡: 向后兼容性 vs 响应体大小
|
||||
|
||||
**权衡**:保留 `permissions` 字段导致响应体包含冗余数据。
|
||||
|
||||
**数据冗余**:
|
||||
- `permissions`: 所有权限码(菜单 + 按钮)
|
||||
- `menus`: 菜单权限码(包含在 `permissions` 中)
|
||||
- `buttons`: 按钮权限码(包含在 `permissions` 中)
|
||||
|
||||
**优点**:平滑迁移,旧版前端仍可工作
|
||||
|
||||
**缺点**:响应体增加约 1-2KB(权限码重复传输)
|
||||
|
||||
**长期方案**:
|
||||
- 6 个月后评估前端升级情况
|
||||
- 通过 API 版本控制(`/api/v2/login`)移除 `permissions` 字段
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### 部署步骤
|
||||
|
||||
**第一阶段:后端部署**(向后兼容)
|
||||
1. 部署新版本代码(包含 `menus` 和 `buttons` 字段)
|
||||
2. 验证登录接口返回新字段
|
||||
3. 确认旧版前端仍可正常使用 `permissions` 字段
|
||||
|
||||
**第二阶段:前端升级**(可选)
|
||||
1. 前端适配新字段(使用 `menus` 渲染侧边栏)
|
||||
2. 前端适配新字段(使用 `buttons` 控制按钮显示)
|
||||
3. 灰度发布(10% → 50% → 100%)
|
||||
|
||||
**第三阶段:废弃旧字段**(6 个月后)
|
||||
1. 监控 `permissions` 字段的使用情况
|
||||
2. 确认所有前端已升级
|
||||
3. 通过 API 版本控制移除 `permissions` 字段
|
||||
|
||||
### 回滚策略
|
||||
|
||||
**触发条件**:
|
||||
- 登录接口响应时间增加 > 100ms
|
||||
- 登录失败率增加 > 5%
|
||||
- 前端报告菜单渲染异常
|
||||
|
||||
**回滚步骤**:
|
||||
1. 回滚到上一个稳定版本
|
||||
2. 保留原有 `getUserPermissions()` 逻辑
|
||||
3. 移除 `getUserPermissionsAndMenus()` 调用
|
||||
|
||||
**数据库回滚**:无需回滚(未修改数据库 schema)
|
||||
|
||||
### 测试计划
|
||||
|
||||
**单元测试**:
|
||||
- `buildMenuTree()` 方法(树构建逻辑)
|
||||
- 测试场景:根节点、多级嵌套、孤儿节点、排序
|
||||
- `classifyPermissions()` 方法(权限分类)
|
||||
- 测试场景:平台过滤、菜单/按钮分类、超级管理员
|
||||
- `sortMenuNodes()` 方法(递归排序)
|
||||
- 测试场景:同级排序、子节点排序、稳定排序
|
||||
|
||||
**集成测试**:
|
||||
- 登录接口测试
|
||||
- 场景:普通用户登录、超级管理员登录、无权限用户登录
|
||||
- 验证:响应包含 `menus`, `buttons`, `permissions` 三个字段
|
||||
- 验证:菜单树结构正确,排序正确,平台过滤正确
|
||||
- GetMe 接口测试
|
||||
- 场景:已登录用户调用 GetMe
|
||||
- 验证:响应不包含 `menus` 和 `buttons` 字段
|
||||
|
||||
**性能测试**:
|
||||
- 登录接口性能基准测试
|
||||
- 场景:50 个权限、100 个权限、200 个权限
|
||||
- 目标:响应时间增加 < 50ms
|
||||
|
||||
**兼容性测试**:
|
||||
- 旧版前端仍可使用 `permissions` 字段
|
||||
- 新版前端可以使用 `menus` 和 `buttons` 字段
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **是否需要为菜单树引入缓存?**
|
||||
- 当前设计:每次登录都重新构建菜单树
|
||||
- 优化方案:将菜单树缓存到 Redis(以 `user_id + device` 为 key)
|
||||
- 决策点:登录频率低(一天 1-2 次),暂不引入缓存;后续根据性能监控决定
|
||||
|
||||
2. **是否需要支持前端动态刷新菜单?**
|
||||
- 当前设计:权限变更后需要重新登录
|
||||
- 优化方案:提供 `GET /api/admin/menus` 端点或 WebSocket 推送
|
||||
- 决策点:短期方案(重新登录),长期优化(按需刷新)
|
||||
|
||||
3. **是否需要为 MenuNode 添加扩展字段(icon, badge, hidden)?**
|
||||
- 当前设计:保持最小化(6 个字段)
|
||||
- 扩展方案:根据前端需求逐步添加
|
||||
- 决策点:先实现基础功能,根据反馈迭代
|
||||
@@ -0,0 +1,51 @@
|
||||
## Why
|
||||
|
||||
当前登录接口只返回扁平的权限码列表 `permissions: []`,前端需要额外处理才能渲染侧边栏菜单(需要树形结构)和控制按钮显示(需要按钮权限列表)。这导致前端需要额外请求菜单接口或在本地维护菜单配置,增加了复杂度和请求次数。通过在登录时直接返回分类好的菜单树和按钮权限,前端可以一次性获取所有必要数据并存储到 localStorage,无需二次请求,简化前端实现并提升用户体验。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 在 `LoginResponse` DTO 中新增两个字段:
|
||||
- `menus: []MenuNode` - 菜单树(树形结构)
|
||||
- `buttons: []string` - 按钮权限码列表(扁平)
|
||||
- 新增 `MenuNode` DTO 结构体,包含 `id`, `perm_code`, `name`, `url`, `sort`, `children` 字段
|
||||
- 修改 `auth.Service.Login()` 方法,新增权限分类逻辑:
|
||||
- 基于 `perm_type` 字段分类(1=菜单权限,2=按钮权限)
|
||||
- 根据 `device` 参数过滤平台(`platform=web/h5/all`)
|
||||
- 构建菜单树(基于 `parent_id` 字段的递归结构)
|
||||
- 超级管理员返回所有菜单和按钮
|
||||
- 保留原有 `permissions` 字段(向后兼容,包含所有权限码)
|
||||
- `GetMe` 接口保持不变(不返回菜单,避免频繁查询)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `login-menu-button-response`: 登录接口返回菜单树和按钮权限,支持前端直接使用无需二次处理
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
(无现有能力被修改,这是新增功能)
|
||||
|
||||
## Impact
|
||||
|
||||
**修改的文件**:
|
||||
- `internal/model/dto/auth_dto.go` - 新增 MenuNode 结构体,修改 LoginResponse 结构体
|
||||
- `internal/service/auth/service.go` - 修改 Login() 方法,新增权限分类和菜单树构建逻辑
|
||||
|
||||
**API 变更**(向后兼容):
|
||||
- `POST /api/admin/login` - 响应体增加 `menus` 和 `buttons` 字段
|
||||
- `POST /api/h5/login` - 响应体增加 `menus` 和 `buttons` 字段
|
||||
- `GET /api/admin/me` - 保持不变(不返回菜单)
|
||||
- `GET /api/h5/me` - 保持不变(不返回菜单)
|
||||
|
||||
**数据库影响**:
|
||||
- 无需修改数据库 schema(复用现有 `tb_permission` 表的 `perm_type`, `parent_id`, `platform` 字段)
|
||||
|
||||
**前端影响**:
|
||||
- 可选升级:前端可以选择使用新的 `menus` 和 `buttons` 字段,也可以继续使用 `permissions` 字段(向后兼容)
|
||||
- 推荐使用方式:登录后将 `menus` 和 `buttons` 存储到 localStorage,页面刷新时从本地读取
|
||||
|
||||
**性能影响**:
|
||||
- 登录响应体增大(预计增加 5-10KB,取决于权限数量)
|
||||
- 菜单树构建计算量增加(O(n) 复杂度,n 为权限数量,通常 < 100)
|
||||
- 不影响 GetMe 接口性能(未修改)
|
||||
@@ -0,0 +1,198 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 登录响应包含菜单树和按钮权限
|
||||
|
||||
登录接口 SHALL 在响应中返回三个权限相关字段:
|
||||
- `menus`: 菜单树(树形结构,用于渲染侧边栏)
|
||||
- `buttons`: 按钮权限码列表(扁平数组,用于控制按钮显示)
|
||||
- `permissions`: 所有权限码列表(扁平数组,保留向后兼容性)
|
||||
|
||||
适用端点:
|
||||
- `POST /api/admin/login`(后台登录)
|
||||
- `POST /api/h5/login`(H5 端登录)
|
||||
|
||||
#### Scenario: 普通用户登录成功
|
||||
|
||||
- **WHEN** 普通用户(非超级管理员)登录成功
|
||||
- **THEN** 响应包含 `menus` 数组(包含用户有权限的菜单树)
|
||||
- **THEN** 响应包含 `buttons` 数组(包含用户有权限的按钮权限码)
|
||||
- **THEN** 响应包含 `permissions` 数组(包含所有权限码)
|
||||
- **THEN** `menus` 数组为树形结构,每个节点包含 `id`, `perm_code`, `name`, `url`, `sort`, `children` 字段
|
||||
|
||||
#### Scenario: 用户无任何权限
|
||||
|
||||
- **WHEN** 用户登录成功但未分配任何角色或权限
|
||||
- **THEN** 响应包含空的 `menus` 数组 `[]`
|
||||
- **THEN** 响应包含空的 `buttons` 数组 `[]`
|
||||
- **THEN** 响应包含空的 `permissions` 数组 `[]`
|
||||
|
||||
### Requirement: 菜单权限构建树形结构
|
||||
|
||||
系统 SHALL 基于权限表的 `perm_type` 和 `parent_id` 字段构建菜单树:
|
||||
- 只包含 `perm_type = 1`(菜单权限)的权限记录
|
||||
- 根据 `parent_id` 字段构建父子关系
|
||||
- 根节点为 `parent_id = NULL` 或 `parent_id = 0` 的权限
|
||||
- 子节点追加到父节点的 `children` 数组中
|
||||
|
||||
#### Scenario: 构建两级菜单树
|
||||
|
||||
- **WHEN** 用户有以下权限:
|
||||
- ID=1, perm_code="user:menu", perm_type=1, parent_id=NULL(用户管理)
|
||||
- ID=2, perm_code="user:list:menu", perm_type=1, parent_id=1(用户列表)
|
||||
- **THEN** `menus` 数组包含 1 个根节点(用户管理)
|
||||
- **THEN** 根节点的 `children` 数组包含 1 个子节点(用户列表)
|
||||
|
||||
#### Scenario: 孤儿节点提升为根节点
|
||||
|
||||
- **WHEN** 用户有子菜单权限(perm_code="user:list:menu", parent_id=1)
|
||||
- **WHEN** 用户没有父菜单权限(ID=1 不在权限列表中)
|
||||
- **THEN** 子菜单提升为根节点,出现在 `menus` 数组的顶层
|
||||
- **THEN** 子菜单的 `children` 数组为空
|
||||
|
||||
### Requirement: 按钮权限提取扁平列表
|
||||
|
||||
系统 SHALL 提取所有 `perm_type = 2`(按钮权限)的权限码作为 `buttons` 数组:
|
||||
- 只包含 `perm_code` 字段值
|
||||
- 不构建树形结构
|
||||
- 按原始顺序返回
|
||||
|
||||
#### Scenario: 提取按钮权限码
|
||||
|
||||
- **WHEN** 用户有以下权限:
|
||||
- perm_code="user:create", perm_type=2
|
||||
- perm_code="user:update", perm_type=2
|
||||
- perm_code="user:delete", perm_type=2
|
||||
- **THEN** `buttons` 数组包含 `["user:create", "user:update", "user:delete"]`
|
||||
|
||||
### Requirement: 平台过滤
|
||||
|
||||
系统 SHALL 根据登录请求的 `device` 参数过滤权限的 `platform` 字段:
|
||||
- `platform = "all"` 的权限对所有端口可见
|
||||
- `platform = "web"` 的权限只在 `device = "web"` 时可见
|
||||
- `platform = "h5"` 的权限只在 `device = "h5"` 时可见
|
||||
- 未指定 `device` 参数时默认为 `"web"`
|
||||
|
||||
#### Scenario: Web 后台登录过滤 H5 菜单
|
||||
|
||||
- **WHEN** 用户登录时 `device = "web"`
|
||||
- **WHEN** 用户有以下权限:
|
||||
- perm_code="dashboard:menu", perm_type=1, platform="all"
|
||||
- perm_code="user:menu", perm_type=1, platform="web"
|
||||
- perm_code="mobile:menu", perm_type=1, platform="h5"
|
||||
- **THEN** `menus` 数组包含 "dashboard:menu" 和 "user:menu"
|
||||
- **THEN** `menus` 数组不包含 "mobile:menu"(H5 专属菜单被过滤)
|
||||
|
||||
#### Scenario: H5 端登录过滤 Web 菜单
|
||||
|
||||
- **WHEN** 用户登录时 `device = "h5"`
|
||||
- **WHEN** 用户有以下权限:
|
||||
- perm_code="mobile:menu", perm_type=1, platform="h5"
|
||||
- perm_code="user:menu", perm_type=1, platform="web"
|
||||
- perm_code="common:menu", perm_type=1, platform="all"
|
||||
- **THEN** `menus` 数组包含 "mobile:menu" 和 "common:menu"
|
||||
- **THEN** `menus` 数组不包含 "user:menu"(Web 专属菜单被过滤)
|
||||
|
||||
### Requirement: 超级管理员获取所有权限
|
||||
|
||||
系统 SHALL 为超级管理员(`user_type = 1`)返回所有菜单和按钮权限:
|
||||
- 查询数据库中所有 `status = 1`(启用)的权限
|
||||
- 仍然应用平台过滤(根据 `device` 参数)
|
||||
- 不查询角色权限关联表
|
||||
|
||||
#### Scenario: 超级管理员登录
|
||||
|
||||
- **WHEN** 超级管理员(user_type=1)登录
|
||||
- **WHEN** 数据库包含 100 个启用的权限(50 个菜单 + 50 个按钮)
|
||||
- **WHEN** 登录时 `device = "web"`
|
||||
- **THEN** `menus` 数组包含所有 `platform="all"` 或 `platform="web"` 的菜单权限
|
||||
- **THEN** `buttons` 数组包含所有 `platform="all"` 或 `platform="web"` 的按钮权限
|
||||
- **THEN** 不包含 `platform="h5"` 的权限
|
||||
|
||||
### Requirement: 菜单排序
|
||||
|
||||
菜单树 SHALL 根据权限表的 `sort` 字段排序:
|
||||
- 同级菜单按 `sort` 字段升序排列
|
||||
- 子菜单在其父节点的 `children` 数组中按 `sort` 排序
|
||||
- 递归应用到所有层级
|
||||
|
||||
#### Scenario: 菜单按 sort 字段排序
|
||||
|
||||
- **WHEN** 用户有以下权限:
|
||||
- perm_code="order:menu", sort=3
|
||||
- perm_code="user:menu", sort=1
|
||||
- perm_code="dashboard:menu", sort=2
|
||||
- **THEN** `menus` 数组的顺序为 `["user:menu", "dashboard:menu", "order:menu"]`
|
||||
|
||||
#### Scenario: 子菜单按 sort 字段排序
|
||||
|
||||
- **WHEN** 父菜单 "user:menu" 有三个子菜单:
|
||||
- "user:list:menu", sort=10
|
||||
- "user:role:menu", sort=5
|
||||
- "user:dept:menu", sort=8
|
||||
- **THEN** 父菜单的 `children` 数组顺序为 `["user:role:menu", "user:dept:menu", "user:list:menu"]`
|
||||
|
||||
### Requirement: GetMe 接口不返回菜单
|
||||
|
||||
`GET /api/admin/me` 和 `GET /api/h5/me` 接口 SHALL NOT 返回 `menus` 和 `buttons` 字段:
|
||||
- 只返回 `user` 和 `permissions` 字段(现有行为保持不变)
|
||||
- 避免频繁查询和构建菜单树
|
||||
|
||||
#### Scenario: 调用 GetMe 接口
|
||||
|
||||
- **WHEN** 已登录用户调用 `GET /api/admin/me`
|
||||
- **THEN** 响应包含 `user` 对象
|
||||
- **THEN** 响应包含 `permissions` 数组(权限码列表)
|
||||
- **THEN** 响应不包含 `menus` 字段
|
||||
- **THEN** 响应不包含 `buttons` 字段
|
||||
|
||||
### Requirement: MenuNode 数据结构
|
||||
|
||||
系统 SHALL 定义 `MenuNode` DTO 结构体,包含以下字段:
|
||||
- `id` (uint): 权限 ID
|
||||
- `perm_code` (string): 权限码(如 "user:menu")
|
||||
- `name` (string): 菜单名称(如 "用户管理")
|
||||
- `url` (string): 路由路径(如 "/users")
|
||||
- `sort` (int): 排序值
|
||||
- `children` ([]MenuNode): 子菜单数组(递归结构)
|
||||
|
||||
所有字段 MUST 包含 JSON 标签。
|
||||
|
||||
#### Scenario: MenuNode 结构定义
|
||||
|
||||
- **WHEN** 定义 MenuNode 结构体
|
||||
- **THEN** 包含 `id` 字段,类型为 `uint`,JSON 标签为 `"id"`
|
||||
- **THEN** 包含 `perm_code` 字段,类型为 `string`,JSON 标签为 `"perm_code"`
|
||||
- **THEN** 包含 `name` 字段,类型为 `string`,JSON 标签为 `"name"`
|
||||
- **THEN** 包含 `url` 字段,类型为 `string`,JSON 标签为 `"url"`
|
||||
- **THEN** 包含 `sort` 字段,类型为 `int`,JSON 标签为 `"sort"`
|
||||
- **THEN** 包含 `children` 字段,类型为 `[]MenuNode`,JSON 标签为 `"children"`
|
||||
|
||||
### Requirement: 响应格式向后兼容
|
||||
|
||||
系统 SHALL 保留原有 `permissions` 字段,确保向后兼容:
|
||||
- 登录响应同时包含 `permissions`, `menus`, `buttons` 三个字段
|
||||
- 前端可以选择使用新字段或继续使用旧字段
|
||||
- `permissions` 包含所有权限码(菜单 + 按钮)
|
||||
|
||||
#### Scenario: 向后兼容性验证
|
||||
|
||||
- **WHEN** 用户登录成功
|
||||
- **WHEN** 用户有 3 个菜单权限和 2 个按钮权限
|
||||
- **THEN** 响应包含 `permissions` 数组,长度为 5
|
||||
- **THEN** 响应包含 `menus` 数组(树形结构)
|
||||
- **THEN** 响应包含 `buttons` 数组,长度为 2
|
||||
- **THEN** 旧版前端仍可使用 `permissions` 字段正常工作
|
||||
|
||||
### Requirement: 性能要求
|
||||
|
||||
菜单树构建逻辑 MUST 满足以下性能要求:
|
||||
- 时间复杂度为 O(n),n 为权限数量
|
||||
- 登录响应时间增加 < 50ms(在权限数量 < 100 的场景下)
|
||||
- 不影响 GetMe 接口性能(未修改)
|
||||
|
||||
#### Scenario: 性能基准测试
|
||||
|
||||
- **WHEN** 用户有 50 个权限(30 个菜单 + 20 个按钮)
|
||||
- **WHEN** 菜单最大层级为 3 级
|
||||
- **THEN** 登录接口响应时间增加 < 50ms
|
||||
- **THEN** 菜单树构建时间 < 10ms
|
||||
@@ -0,0 +1,114 @@
|
||||
## 1. DTO 结构定义
|
||||
|
||||
- [x] 1.1 在 `internal/model/dto/auth_dto.go` 中新增 `MenuNode` 结构体,包含 `ID`, `PermCode`, `Name`, `URL`, `Sort`, `Children` 字段,所有字段添加 JSON 标签
|
||||
- [x] 1.2 修改 `LoginResponse` 结构体,新增 `Menus []MenuNode` 和 `Buttons []string` 字段,添加 JSON 标签和 description 注释
|
||||
- [x] 1.3 运行 `lsp_diagnostics` 验证 DTO 文件无错误
|
||||
|
||||
## 2. Service 层核心方法实现
|
||||
|
||||
- [x] 2.1 在 `internal/service/auth/service.go` 中新增 `getUserPermissionsAndMenus()` 方法,接收参数 `ctx, userID, userType, device`,返回 `([]string, []MenuNode, []string, error)`
|
||||
- [x] 2.2 在 `getUserPermissionsAndMenus()` 中实现超级管理员逻辑:调用 `permissionStore.GetAll(ctx, nil)` 查询所有启用的权限
|
||||
- [x] 2.3 在 `getUserPermissionsAndMenus()` 中实现普通用户逻辑:复用现有的角色权限查询(`accountRoleStore.GetByAccountID()` → `rolePermStore.GetPermIDsByRoleIDs()` → `permissionStore.GetByIDs()`)
|
||||
- [x] 2.4 新增 `classifyPermissions()` 方法,接收参数 `permissions []*model.Permission, device string`,实现权限分类和平台过滤逻辑
|
||||
- [x] 2.5 在 `classifyPermissions()` 中实现平台过滤:`platform == "all"` 或 `platform == device` 时保留,否则跳过
|
||||
- [x] 2.6 在 `classifyPermissions()` 中实现权限分类:`perm_type == 1` 的收集到 `menuPerms`,`perm_type == 2` 的提取 `perm_code` 到 `buttonCodes`
|
||||
- [x] 2.7 在 `classifyPermissions()` 中收集所有权限码到 `allCodes` 数组(用于 `permissions` 字段)
|
||||
|
||||
## 3. 菜单树构建逻辑
|
||||
|
||||
- [x] 3.1 新增 `buildMenuTree()` 方法,接收参数 `permissions []*model.Permission`,返回 `[]MenuNode`
|
||||
- [x] 3.2 在 `buildMenuTree()` 中实现第一步:创建节点映射 `nodeMap := make(map[uint]*dto.MenuNode)`,遍历权限列表构建 MenuNode 对象
|
||||
- [x] 3.3 在 `buildMenuTree()` 中实现第二步:组织父子关系,根据 `parent_id` 将节点追加到父节点的 `Children` 数组或 `roots` 数组
|
||||
- [x] 3.4 在 `buildMenuTree()` 中实现孤儿节点处理:如果 `parent_id` 不在 `nodeMap` 中,将节点提升为根节点,并记录警告日志
|
||||
- [x] 3.5 新增 `sortMenuNodes()` 方法,接收参数 `nodes []MenuNode`,实现递归排序(根据 `Sort` 字段升序)
|
||||
- [x] 3.6 在 `buildMenuTree()` 中调用 `sortMenuNodes(roots)` 完成排序后返回
|
||||
|
||||
## 4. 超级管理员专用逻辑
|
||||
|
||||
- [x] 4.1 新增 `getAllPermissionsForSuperAdmin()` 方法,接收参数 `ctx, device`,返回 `([]string, []MenuNode, []string, error)`
|
||||
- [x] 4.2 在 `getAllPermissionsForSuperAdmin()` 中调用 `permissionStore.GetAll(ctx, nil)` 查询所有启用的权限
|
||||
- [x] 4.3 在 `getAllPermissionsForSuperAdmin()` 中调用 `classifyPermissions(allPerms, device)` 完成分类和过滤
|
||||
|
||||
## 5. 修改 Login 方法
|
||||
|
||||
- [x] 5.1 在 `auth.Service.Login()` 方法中,将 `getUserPermissions(ctx, account.ID)` 替换为 `getUserPermissionsAndMenus(ctx, account.ID, account.UserType, device)`
|
||||
- [x] 5.2 在 `Login()` 方法中,将返回的 `(permissions, menus, buttons, err)` 赋值到 `LoginResponse` 的对应字段
|
||||
- [x] 5.3 处理错误:如果 `getUserPermissionsAndMenus()` 失败,记录错误日志,返回空的 `menus: []` 和 `buttons: []`(不阻塞登录)
|
||||
- [x] 5.4 运行 `lsp_diagnostics` 验证 Service 文件无错误
|
||||
|
||||
## 6. 单元测试 - buildMenuTree
|
||||
|
||||
- [x] 6.1 创建 `internal/service/auth/menu_tree_test.go` 文件
|
||||
- [x] 6.2 编写测试 `TestBuildMenuTree_RootNodes`:测试只有根节点的场景(`parent_id = NULL`)
|
||||
- [x] 6.3 编写测试 `TestBuildMenuTree_MultiLevel`:测试两级或三级嵌套菜单场景
|
||||
- [x] 6.4 编写测试 `TestBuildMenuTree_OrphanNodes`:测试孤儿节点提升为根节点的场景(`parent_id` 不存在)
|
||||
- [x] 6.5 编写测试 `TestBuildMenuTree_Sorting`:测试菜单排序(根据 `sort` 字段升序)
|
||||
- [x] 6.6 编写测试 `TestBuildMenuTree_EmptyInput`:测试空权限列表输入,验证返回空数组
|
||||
- [x] 6.7 运行单元测试:`source .env.local && go test -v ./internal/service/auth/...`,确保所有测试通过
|
||||
|
||||
## 7. 单元测试 - classifyPermissions
|
||||
|
||||
- [x] 7.1 创建 `internal/service/auth/classify_test.go` 文件
|
||||
- [x] 7.2 编写测试 `TestClassifyPermissions_PlatformFilter`:测试平台过滤(`device="web"` 时过滤 `platform="h5"` 的权限)
|
||||
- [x] 7.3 编写测试 `TestClassifyPermissions_MenuAndButton`:测试菜单和按钮权限分类(`perm_type=1` vs `perm_type=2`)
|
||||
- [x] 7.4 编写测试 `TestClassifyPermissions_AllPermissions`:测试所有权限码收集(包含菜单和按钮)
|
||||
- [x] 7.5 编写测试 `TestClassifyPermissions_PlatformAll`:测试 `platform="all"` 的权限对所有端口可见
|
||||
- [x] 7.6 运行单元测试:`source .env.local && go test -v ./internal/service/auth/...`,确保所有测试通过
|
||||
|
||||
## 8. 单元测试 - getUserPermissionsAndMenus
|
||||
|
||||
- [x] 8.1 编写测试 `TestGetUserPermissionsAndMenus_SuperAdmin`:测试超级管理员返回所有权限
|
||||
- [x] 8.2 编写测试 `TestGetUserPermissionsAndMenus_NormalUser`:测试普通用户返回角色权限
|
||||
- [x] 8.3 编写测试 `TestGetUserPermissionsAndMenus_NoPermissions`:测试用户无权限时返回空数组
|
||||
- [x] 8.4 编写测试 `TestGetUserPermissionsAndMenus_DeviceFilter`:测试 `device` 参数过滤平台
|
||||
- [x] 8.5 运行单元测试:`source .env.local && go test -v ./internal/service/auth/...`,确保所有测试通过
|
||||
|
||||
## 9. 集成测试 - Login API
|
||||
|
||||
- [x] 9.1 创建或修改 `tests/integration/admin_auth_test.go` 文件
|
||||
- [x] 9.2 编写测试 `TestAdminLogin_MenusAndButtons`:验证登录响应包含 `menus`, `buttons`, `permissions` 三个字段
|
||||
- [x] 9.3 编写测试 `TestAdminLogin_MenuTreeStructure`:验证 `menus` 为树形结构,包含 `id`, `perm_code`, `name`, `url`, `sort`, `children` 字段
|
||||
- [x] 9.4 编写测试 `TestAdminLogin_ButtonsArray`:验证 `buttons` 为扁平数组,只包含 `perm_type=2` 的权限码
|
||||
- [x] 9.5 编写测试 `TestAdminLogin_SuperAdmin`:验证超级管理员登录时返回所有菜单和按钮
|
||||
- [x] 9.6 编写测试 `TestAdminLogin_PlatformFilter`:验证 `device="web"` 时不返回 `platform="h5"` 的菜单
|
||||
- [x] 9.7 编写测试 `TestAdminLogin_NoPermissions`:验证无权限用户登录时返回空的 `menus` 和 `buttons`
|
||||
- [x] 9.8 运行集成测试:`source .env.local && go test -v ./tests/integration/...`,确保所有测试通过
|
||||
|
||||
## 10. 集成测试 - GetMe API
|
||||
|
||||
- [x] 10.1 编写测试 `TestAdminGetMe_NoMenus`:验证 GetMe 接口不返回 `menus` 和 `buttons` 字段
|
||||
- [x] 10.2 编写测试 `TestAdminGetMe_OnlyUserAndPermissions`:验证 GetMe 接口只返回 `user` 和 `permissions` 字段
|
||||
- [x] 10.3 运行集成测试:`source .env.local && go test -v ./tests/integration/...`,确保所有测试通过
|
||||
|
||||
## 11. 性能测试
|
||||
|
||||
- [x] 11.1 编写性能基准测试 `BenchmarkBuildMenuTree`:测试不同权限数量(50、100、200)的菜单树构建性能
|
||||
- [x] 11.2 编写性能基准测试 `BenchmarkClassifyPermissions`:测试权限分类性能
|
||||
- [x] 11.3 运行基准测试:`source .env.local && go test -bench=. -benchmem ./internal/service/auth/...`
|
||||
- [x] 11.4 验证登录接口响应时间增加 < 50ms(在 50 个权限的场景下)
|
||||
|
||||
## 12. 代码审查和优化
|
||||
|
||||
- [x] 12.1 运行 `lsp_diagnostics` 检查所有修改的文件,确保无类型错误和警告
|
||||
- [x] 12.2 运行 `gofmt -w ./internal/service/auth/service.go` 格式化代码
|
||||
- [x] 12.3 运行 `gofmt -w ./internal/model/dto/auth_dto.go` 格式化代码
|
||||
- [x] 12.4 检查所有注释使用中文,变量名和函数名使用英文
|
||||
- [x] 12.5 检查错误处理:使用 `errors.New()` 或 `errors.Wrap()`,不使用 `fmt.Errorf()`
|
||||
- [x] 12.6 检查日志记录:孤儿节点检测时记录警告日志(使用 Zap)
|
||||
|
||||
## 13. 文档更新
|
||||
|
||||
- [x] 13.1 更新 `README.md`:在"核心功能"部分添加"登录接口返回菜单树和按钮权限"说明
|
||||
- [x] 13.2 创建 `docs/login-menu-button-response/使用指南.md`:说明前端如何使用 `menus` 和 `buttons` 字段
|
||||
- [x] 13.3 在使用指南中添加 localStorage 缓存示例代码
|
||||
- [x] 13.4 在使用指南中添加 MenuNode 数据结构说明和示例响应
|
||||
- [x] 13.5 在使用指南中添加性能影响说明和最佳实践建议
|
||||
|
||||
## 14. 最终验证
|
||||
|
||||
- [x] 14.1 运行完整测试套件:`source .env.local && go test ./...`,确保所有测试通过
|
||||
- [x] 14.2 启动 API 服务:`go run cmd/api/main.go`,验证服务正常启动
|
||||
- [x] 14.3 使用 Postman/curl 测试登录接口:验证响应包含 `menus`, `buttons`, `permissions` 三个字段
|
||||
- [x] 14.4 验证响应体大小 < 20KB(普通用户场景)
|
||||
- [x] 14.5 验证 GetMe 接口不返回 `menus` 和 `buttons` 字段
|
||||
- [x] 14.6 验证向后兼容性:旧版前端仍可使用 `permissions` 字段
|
||||
Reference in New Issue
Block a user