# 角色权限设计

## Context

当前系统有 5 个默认角色（超管/产品经理/开发/测试/设计师），但 `RoleItem` 没有 `permissions` 字段，新增角色表单也只能填名字+描述。所有页面/按钮都没做权限拦截，任何登录用户都能进所有模块、做所有操作。

本期目标：给角色加权限字段、为默认 5 角色按职能分配权限、新增角色 UI 支持勾选权限、并且把权限拦截一并接入到 7 个主模块 + 版本详情 7 个 Tab + 各按钮。

## 决策摘要

| 项 | 决策 |
|---|---|
| 权限粒度 | 细粒度（view/create/edit/delete + 加班特殊操作 export） |
| 模块清单 | 7 主模块 + 7 个版本详情 Tab；与我相关不纳入 |
| 数据结构 | `RoleItem.permissions: string[]`，`module:action` 形式（推荐方案 A） |
| 超管标记 | `permissions: ['*']` 通配符 |
| Bug Tab 粒度 | 单独拆 4 档（view/create/edit/delete），其他 Tab 二档 |
| 默认 5 角色映射 | 按职能划分（详见下方） |
| 新增角色默认值 | 全部不勾选（最小授权） |
| 新增角色 UI | 现有表单内嵌权限矩阵 + 顶部"全选/反选/仅查看"快捷 |
| 拦截接入范围 | 本期一并接入：路由级 + 按钮级 + Sidebar 菜单 + Tab 级 |
| 未登录/无角色 | 默认按只读（最严格） |
| 权限点总数 | 37 个 |
| localStorage 迁移 | fetchMembers 启动检测旧 RoleItem 无 permissions 字段则补默认值 |

## 1. 权限点字典（37 个）

**主模块（4 件套，加班加 export）**：

| 模块 | 权限点 |
|---|---|
| 产品 product | view / create / edit / delete |
| 项目 project | view / create / edit / delete |
| 版本 version | view / create / edit / delete |
| 需求池 requirement | view / create / edit / delete |
| 加班记录 overtime | view / create / delete / export |
| 成员 member | view / create / edit / delete |
| 角色 role | view / create / edit / delete |

**版本详情 Tab（其他二档，Bug 单独 4 档）**：

| Tab | 权限点 |
|---|---|
| 需求 Tab `version.req` | view / manage |
| 调研 Tab `version.research` | view / manage |
| 产品方案 Tab `version.product_plan` | view / manage |
| UI 设计 Tab `version.ui_plan` | view / manage |
| 开发任务 Tab `version.devtask` | view / manage |
| 测试用例 Tab `version.testcase` | view / manage |
| Bug Tab `version.bug` | view / create / edit / delete |

**说明**：Tab 二档因为 Tab 内部操作高度耦合（manage = 该 Tab 内全部增删改）。Bug 单独拆 4 档因为开发与测试在 Bug 流程上职能不同：测试创建/关闭，开发改状态/修复，测试验证。

## 2. 默认 5 角色权限映射

**(1) 超级管理员 `role-admin`** — `permissions: ['*']`

**(2) 产品经理 `role-pm`**：
- 全权：product / project / version / requirement / version.req / version.product_plan
- 仅查看：overtime（view）/ member（view）/ role（view）/ version.research / version.ui_plan / version.devtask / version.testcase / version.bug

**(3) 开发工程师 `role-dev`**：
- 全权：version.devtask
- Bug Tab：view + edit
- 加班：view + create
- 仅查看：product / project / version / requirement / version.req / version.research / version.product_plan / version.ui_plan / version.testcase
- 不可：member / role / overtime export+delete / 主模块的 create/edit/delete

**(4) 测试工程师 `role-test`**：
- 全权：version.testcase / version.bug（4 档全权）
- 加班：view + create
- 仅查看：product / project / version / requirement / version.req / version.research / version.product_plan / version.ui_plan / version.devtask
- 不可：同开发

**(5) 设计师 `role-design`**：
- 全权：version.ui_plan
- 加班：view + create
- 仅查看：product / project / version / requirement / version.req / version.research / version.product_plan / version.devtask / version.testcase / version.bug
- 不可：同开发

## 3. 数据结构 + Store

**`apps/web/lib/members.ts`**：`RoleItem` 加 `permissions: string[]`（必填，默认 `[]`）

**`apps/web/lib/permissions.ts`** 新增：

```ts
export type Permission = string;

export interface ActionDef {
  action: 'view' | 'create' | 'edit' | 'delete' | 'manage' | 'export';
  label: string;
  permission: string;
}

export interface PermissionGroup {
  module: string;
  moduleLabel: string;
  category: 'main' | 'version_tab';
  actions: ActionDef[];
}

export const PERMISSION_GROUPS: PermissionGroup[] = [/* 14 组 */];
export const ALL_PERMISSIONS: string[] = /* 37 个展开 */;
export const DEFAULT_ROLE_PERMISSIONS: Record<string, string[]> = {
  'role-admin':  ['*'],
  'role-pm':     [...],
  'role-dev':    [...],
  'role-test':   [...],
  'role-design': [...],
};

export function hasPermission(role: RoleItem | undefined, permission: string): boolean {
  if (!role) return false;
  if (role.permissions.includes('*')) return true;
  return role.permissions.includes(permission);
}
```

**`apps/web/stores/useMemberStore.ts`**：
- `PRESET_ROLES` 5 条加 `permissions`
- `fetchMembers` 加迁移：旧 RoleItem 无 permissions 字段时补默认值
- `createRole/updateRole` 接收 `permissions: string[]`
- 超管 `role-admin` 不可改 permissions（`updateRole` 拦截）

## 4. UI 改动

**(A) 角色表单**（`apps/web/app/admin/roles/page.tsx`）

`RoleModal` 加权限区块（在 description 下方）。Modal 宽度 `max-w-xs` → `max-w-2xl`。

布局：

```
[全选] [反选] [仅查看]   ← 顶部快捷按钮

主模块
─────────────────────────
产品          ☐查看 ☐创建 ☐编辑 ☐删除
项目          ☐查看 ☐创建 ☐编辑 ☐删除
版本          ☐查看 ☐创建 ☐编辑 ☐删除
需求池        ☐查看 ☐创建 ☐编辑 ☐删除
加班记录      ☐查看 ☐创建 ☐删除 ☐导出
成员          ☐查看 ☐创建 ☐编辑 ☐删除
角色          ☐查看 ☐创建 ☐编辑 ☐删除

版本详情 Tab
─────────────────────────
需求 Tab      ☐查看 ☐管理
调研 Tab      ☐查看 ☐管理
产品方案 Tab  ☐查看 ☐管理
UI 设计 Tab   ☐查看 ☐管理
开发任务 Tab  ☐查看 ☐管理
测试用例 Tab  ☐查看 ☐管理
Bug Tab       ☐查看 ☐创建 ☐编辑 ☐删除
```

- 系统角色（`isSystem`）表单顶部黄色提示"超管权限不可修改"，所有 checkbox 设 `disabled`
- 编辑态：`initial.permissions` 默认勾选

**(B) 全局拦截层**（`apps/web/components/auth/Guard.tsx` 新增）

```tsx
// 路由级
<RouteGuard permission="product:view">
  <ProductsPage />
</RouteGuard>

// 按钮级
<PermissionGuard permission="product:create" fallback={null}>
  <button>新建产品</button>
</PermissionGuard>

// hook
const canEdit = useHasPermission('product:edit');
```

`useHasPermission` 实现：从 `useAuthStore` 取 user.roleId，从 `useMemberStore` 查 role.permissions，调 `hasPermission`。

**(C) 接入清单**

**路由级**：
- `/products` `product:view`
- `/projects` `project:view`
- `/versions` `version:view`
- `/requirements` `requirement:view`
- `/overtime` `overtime:view`
- `/admin/members` `member:view`
- `/admin/roles` `role:view`
- 无权限：`<AccessDenied />` 占位组件（不跳转，避免循环）
- `/workspace` 不拦截

**按钮级**：
- 各页面"新建/编辑/删除/导出"按钮包 `PermissionGuard`
- `OvertimeModal` 内的"删除原因"按钮要 `overtime:delete`

**Sidebar 菜单过滤**：
- 7 个主菜单按 `<module>:view` 过滤——无权限的菜单不显示（不灰显）
- 工作区组永远显示（与我相关）

**版本详情 Tab 过滤**（`apps/web/app/versions/[id]/page.tsx`）：
- Tab 列表渲染时按 `version.<tab>:view` 过滤——无权限的 Tab 不渲染
- Tab 内"新建/管理"按钮按对应 manage / 4 档权限拦截

**(D) 默认状态处理**

- mock 用户已登录但 role 找不到 / 用户未登录：当前组件 `useHasPermission` 返回 `false`，所有页面/菜单/按钮都显示无权限——最严格只读
- 现有 mock 数据 `m-8 陈十` 是 `role-admin`，登录该用户即超管全权
- 切换其他 mock 用户（PM/开发/测试/设计）测试不同视图

## 数据流

```
useAuthStore.user
   │
   └─ user.roleId
         │
         └─ useMemberStore.roles.find(r => r.id === user.roleId)
                │
                └─ role.permissions: string[]
                       │
                       ├─ useHasPermission(perm)  ─→ 按钮/Guard
                       ├─ Sidebar 菜单过滤        ─→ 顶级菜单
                       └─ Tab 过滤               ─→ 版本详情
```

## 实施清单

新增：
- `apps/web/lib/permissions.ts` — 权限字典 + DEFAULT_ROLE_PERMISSIONS + hasPermission
- `apps/web/components/auth/Guard.tsx` — RouteGuard + PermissionGuard + useHasPermission + AccessDenied

修改：
- `apps/web/lib/members.ts` — RoleItem 加 permissions
- `apps/web/stores/useMemberStore.ts` — PRESET_ROLES 补 permissions + 迁移逻辑 + 超管保护
- `apps/web/app/admin/roles/page.tsx` — RoleModal 加权限矩阵 + 顶部快捷
- `apps/web/components/layout/Sidebar.tsx` — 菜单按 view 过滤
- `apps/web/app/products/page.tsx` — RouteGuard + 按钮拦截
- `apps/web/app/projects/page.tsx` — 同上
- `apps/web/app/versions/page.tsx` — 同上
- `apps/web/app/requirements/page.tsx` — 同上
- `apps/web/app/overtime/page.tsx` — 同上 + export 按钮拦截
- `apps/web/app/admin/members/page.tsx` — 同上
- `apps/web/app/versions/[id]/page.tsx` — Tab 过滤 + Tab 内按钮拦截

## 验证

1. `pnpm type-check` 0 错误
2. `pnpm build` 14 个页面成功
3. 角色页：超管显示"系统不可修改"+ checkbox disabled；其他 4 个角色显示已勾选权限
4. 新建角色：勾选"产品-查看 + 加班-查看"保存，应能在新角色下看到对应菜单
5. 切换 mock 用户为开发工程师（如李四 m-2）：Sidebar 看不到成员/角色菜单；进版本详情看不到测试用例/Bug 的 manage 按钮（但能看 view）
6. 切换设计师（孙七 m-5）：UI 设计 Tab 全权，开发任务 Tab 只读
7. localStorage 旧 RoleItem 无 permissions 字段：启动后自动补默认值，UI 正常渲染
