## 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 个字段）
   - 扩展方案：根据前端需求逐步添加
   - 决策点：先实现基础功能，根据反馈迭代
