@lark-apaas/coding-steering 0.1.6-alpha.8 → 0.1.6-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/steering/design-stack/skills/.gitkeep +0 -0
  3. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +174 -0
  4. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +621 -0
  5. package/steering/nestjs-react-fullstack/skills/authz-guide/references/management-page-spec.md +505 -0
  6. package/steering/nestjs-react-fullstack/skills/authz-guide/references/runtime-role-controller-spec.md +203 -0
  7. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +90 -0
  8. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +216 -0
  9. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +405 -0
  10. package/steering/nestjs-react-fullstack/skills/devops-guide/SKILL.md +119 -0
  11. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +582 -0
  12. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +357 -0
  13. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +513 -0
  14. package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +118 -0
  15. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +177 -0
  16. package/steering/nestjs-react-fullstack/skills/user-management-best-practices/SKILL.md +142 -0
  17. package/steering/design-stack/skills/client-add-aily-web-chat/SKILL.md +0 -139
  18. package/steering/design-stack/skills/client-builtins-user-service/SKILL.md +0 -628
  19. package/steering/design-stack/skills/code-fix/SKILL.md +0 -246
  20. package/steering/design-stack/skills/feishu/SKILL.md +0 -270
  21. package/steering/design-stack/skills/feishu/references/approval.md +0 -214
  22. package/steering/design-stack/skills/feishu/references/attendance.md +0 -163
  23. package/steering/design-stack/skills/feishu/references/bitable.md +0 -309
  24. package/steering/design-stack/skills/feishu/references/calendar.md +0 -190
  25. package/steering/design-stack/skills/feishu/references/contacts.md +0 -160
  26. package/steering/design-stack/skills/feishu/references/doc.md +0 -256
  27. package/steering/design-stack/skills/feishu/references/drive.md +0 -103
  28. package/steering/design-stack/skills/feishu/references/events.md +0 -198
  29. package/steering/design-stack/skills/feishu/references/id-convert.md +0 -128
  30. package/steering/design-stack/skills/feishu/references/messaging.md +0 -207
  31. package/steering/design-stack/skills/feishu/references/oauth.md +0 -164
  32. package/steering/design-stack/skills/feishu/references/perm.md +0 -90
  33. package/steering/design-stack/skills/feishu/references/wiki.md +0 -164
  34. package/steering/design-stack/skills/user-identity/SKILL.md +0 -300
@@ -0,0 +1,621 @@
1
+ # 动态权限点位鉴权实施规格
2
+
3
+ > **本规格为强制约束**,实现时必须逐项对照,严禁自由发挥。
4
+
5
+ ---
6
+
7
+ ## 技术架构
8
+
9
+ ```
10
+ 角色管理页面(增强) 前端应用 后端 API
11
+ ┌──────────────────┐ ┌──────────────┐ ┌──────────────────┐
12
+ │ 角色-权限映射配置 │ │ AppContainer │ │ @Can('read','Task')│
13
+ │ (配置权限菜单项) │ │ <Can>/useCan│ │ Guard │
14
+ └──────┬───────────┘ └──────┬───────┘ └────────┬─────────┘
15
+ │ │ │
16
+ │ 映射 CRUD API │ 自动请求内置端点 │ 鉴权时查询
17
+ ▼ ▼ ▼
18
+ ┌─────────────────────────────────────────────────────────────────┐
19
+ │ PlatformPermissionController (内置端点,自动提供) │
20
+ │ authz_permissions 表 + authz_role_permissions 表 │
21
+ │ DbPermissionResolver (IPermissionResolver) │
22
+ └─────────────────────────────────────────────────────────────────┘
23
+ ```
24
+
25
+ > **前端无需手动配置权限点位获取**:auth-sdk 的 `AuthProvider` 会自动请求内置端点获取当前用户的权限点位,业务侧不需要传任何额外配置。
26
+
27
+ **关键约束**:
28
+ - 权限点位(`authz_permissions` 表)的增删改**由 Agent 通过 DDL 操作**,确保与代码中的 `@Can`/`<Can>` 保持一致
29
+ - 管理页面**只负责角色-点位映射**的勾选/取消,不提供权限点位的 CRUD
30
+ - **⛔ 启用动态权限后禁止使用 CanRole**:所有鉴权(包括管理 API)统一使用 `@Can`/`<Can>`,不允许 CanRole 与 Can 混用
31
+
32
+ ---
33
+
34
+ ## 实现步骤清单
35
+
36
+ 按以下顺序逐步实现,**禁止跳步或合并步骤**:
37
+
38
+ ```
39
+ Step 0: 权限设计方案 + 实施计划(强制,禁止跳过。本清单即为完整说明,无独立章节)
40
+ ├─ 通读本规格全文,理解完整实施流程
41
+ ├─ 判断当前是新建还是升级场景
42
+ ├─ 新建和升级场景都必须先调用 rbac_role_manager tool(action: DESIGN)产出权限设计方案
43
+ │ ├─ 升级场景:DESIGN 基于现有代码和用户升级需求产出新版方案
44
+ │ └─ 向用户展示方案,等待确认
45
+ ├─ 输出结构化实施计划,列出每个 Step 要创建/修改的文件清单
46
+ └─ ⛔ 未完成权限设计方案就开始写代码/修改数据库 = 违规
47
+
48
+ Step 1: 数据库表 + 数据初始化(⛔ 必须严格按 1.1→1.2→1.3→1.4 顺序,禁止合并)
49
+ ├─ 1.1 DDL 建表,等待确认执行完成
50
+ ├─ 1.2 单独 INSERT authz_permissions(权限点位定义)
51
+ ├─ 1.3 确认 1.2 成功后,单独 INSERT authz_role_permissions(角色-点位映射,依赖 1.2 的 id)
52
+ └─ 1.4 SELECT 验证两张表 row_count > 0,authz_role_permissions 为 0 则重新执行 1.3
53
+
54
+ Step 2: 后端权限解析器
55
+ └─ 创建 DbPermissionResolver,实现 IPermissionResolver 接口
56
+
57
+ Step 3: 后端模块注册
58
+ ├─ 创建 PermissionModule
59
+ └─ 在 app.module.ts 中通过 PlatformModule.forRoot({ authz: { permissionResolver } }) 注册
60
+
61
+ Step 4: 后端权限管理 API
62
+ ├─ 权限点位只读查询 GET /api/permissions(供管理页面展示可勾选列表)
63
+ └─ 角色-权限映射 CRUD
64
+ (用户权限查询由内置 PlatformPermissionController 自动提供,无需实现)
65
+
66
+ Step 5: 鉴权代码
67
+ ├─ 新建场景:直接使用 @Can('action', 'Subject') 和 <Can action subject>
68
+ ├─ 升级场景:将所有 @CanRole → @Can,<CanRole> → <Can>(包括管理 API)
69
+ │ ⛔ 必须全量替换,不允许保留任何 CanRole
70
+ │ 完成替换后,执行 grep 确认零残留(见 Step 9 验证)
71
+ └─ ⛔ 对照功能权限表,将每个点位逐一落实到后端 API 和前端入口,完成后逐行核对确认无遗漏
72
+
73
+ Step 6: 前端权限初始化
74
+ └─ 在 api 层添加权限管理相关的请求函数(权限点位列表、角色映射 CRUD)
75
+ (用户权限点位获取由 auth-sdk 自动完成,无需手动配置)
76
+
77
+ Step 7: 前端 UI 权限控制
78
+ └─ 统一使用 Can 组件 / useCan Hook
79
+
80
+ Step 8: 管理页面增强
81
+ ├─ 新建场景:创建角色管理页面时直接包含权限增强
82
+ ├─ 升级场景:在已有角色管理页面上增加
83
+ ├─ 角色表格新增「权限点位」列
84
+ └─ 操作列「更多操作」`...` 菜单新增「配置权限」项 + Dialog(不外露为按钮)
85
+
86
+ Step 9: 验证(禁止跳过)
87
+ ├─ 编译通过
88
+ ├─ grep 确认 CanRole 零残留:
89
+ │ grep -r "CanRole\|useCanRole\|ROLE_SUBJECT" --include="*.ts" --include="*.tsx" server/ client/ shared/
90
+ │ ⛔ 结果必须为空,否则回到 Step 5 继续替换
91
+ └─ 逐项对照「实现检查表」
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Step 1: 数据库表 + 数据初始化
97
+
98
+ > 对应 `authz-cli` 的 Action:Step 1.1 = `PERM_CREATE_TABLE`,Step 1.2 + 1.3 = `PERM_SEED`。
99
+
100
+ **⛔ 必须严格按以下三步顺序执行,禁止合并步骤或跳步。`authz_role_permissions` 有外键依赖 `authz_permissions`,合并执行会导致映射表静默为空。**
101
+
102
+ ### 1.1 建表(DDL)
103
+
104
+ 使用 `ddl_sql` 工具创建两张表(禁止手动修改 `database/schema.ts`,DDL 执行后会自动生成)。**必须等 DDL 确认执行完成后,再进入 1.2**。
105
+
106
+ ```sql
107
+ CREATE TABLE authz_permissions (
108
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
109
+ action VARCHAR(100) NOT NULL,
110
+ subject VARCHAR(100) NOT NULL,
111
+ description TEXT DEFAULT '',
112
+ _created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
113
+ _created_by user_profile,
114
+ _updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
115
+ _updated_by user_profile,
116
+ UNIQUE(action, subject)
117
+ );
118
+
119
+ CREATE TABLE authz_role_permissions (
120
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
121
+ role_key VARCHAR(100) NOT NULL,
122
+ permission_id UUID NOT NULL REFERENCES authz_permissions(id) ON DELETE CASCADE,
123
+ _created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
124
+ _created_by user_profile,
125
+ _updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
126
+ _updated_by user_profile,
127
+ UNIQUE(role_key, permission_id)
128
+ );
129
+ ```
130
+
131
+ ### 1.2 预填权限点位(DML 第一步)
132
+
133
+ DDL 确认执行完成后,**单独执行** `authz_permissions` 的 INSERT:
134
+
135
+ ```sql
136
+ -- ⚠️ 必须包含 manage:Permission 点位,用于保护权限管理 API(替代 CanRole)
137
+ INSERT INTO authz_permissions (action, subject, description) VALUES
138
+ ('manage', 'Permission', '管理权限配置'),
139
+ ('read', 'Task', '查看任务'),
140
+ ('create', 'Task', '创建任务'),
141
+ ('update', 'Task', '编辑任务'),
142
+ ('delete', 'Task', '删除任务');
143
+ ```
144
+
145
+ ### 1.3 预填角色-点位映射(DML 第二步)
146
+
147
+ **确认 1.2 执行成功后**,再执行 `authz_role_permissions` 的 INSERT(依赖 `authz_permissions` 的 id):
148
+
149
+ ```sql
150
+ -- ⚠️ admin 角色必须映射 manage:Permission,否则管理 API 无人可访问
151
+ INSERT INTO authz_role_permissions (role_key, permission_id) VALUES
152
+ ('admin', (SELECT id FROM authz_permissions WHERE action='manage' AND subject='Permission')),
153
+ ('admin', (SELECT id FROM authz_permissions WHERE action='read' AND subject='Task')),
154
+ ('admin', (SELECT id FROM authz_permissions WHERE action='create' AND subject='Task')),
155
+ ('admin', (SELECT id FROM authz_permissions WHERE action='update' AND subject='Task')),
156
+ ('admin', (SELECT id FROM authz_permissions WHERE action='delete' AND subject='Task')),
157
+ ('member', (SELECT id FROM authz_permissions WHERE action='read' AND subject='Task')),
158
+ ('member', (SELECT id FROM authz_permissions WHERE action='create' AND subject='Task'));
159
+ ```
160
+
161
+ ### 1.4 验证(禁止跳过)
162
+
163
+ 执行完 1.3 后,**必须立即查询两张表确认数据条数**:
164
+
165
+ ```sql
166
+ SELECT 'authz_permissions' AS table_name, COUNT(*) AS row_count FROM authz_permissions
167
+ UNION ALL
168
+ SELECT 'authz_role_permissions', COUNT(*) FROM authz_role_permissions;
169
+ ```
170
+
171
+ ⛔ 两张表的 `row_count` 都必须 > 0。如果 `authz_role_permissions` 为 0,说明 1.3 未成功执行,必须重新执行。
172
+
173
+ > 以上 SQL 仅为示例,实际数据根据权限设计方案的第 4、5 节生成。`manage:Permission` 点位为必选项,确保管理 API 通过动态权限而非 CanRole 保护。
174
+
175
+ ---
176
+
177
+ ## Step 2: 后端权限解析器
178
+
179
+ 创建 `server/modules/permission/db-permission-resolver.ts`:
180
+
181
+ ```typescript
182
+ import { Injectable, Inject } from '@nestjs/common';
183
+ import type { IPermissionResolver, PermissionPoint } from '@lark-apaas/fullstack-nestjs-core';
184
+ import { DRIZZLE_DATABASE, type PostgresJsDatabase } from '@lark-apaas/fullstack-nestjs-core';
185
+ import { authzRolePermissions, authzPermissions } from '../../database/schema';
186
+ import { inArray, eq } from 'drizzle-orm';
187
+
188
+ @Injectable()
189
+ export class DbPermissionResolver implements IPermissionResolver {
190
+ constructor(
191
+ @Inject(DRIZZLE_DATABASE) private readonly db: PostgresJsDatabase,
192
+ ) {}
193
+
194
+ async resolvePermissions(roleKeys: string[]): Promise<PermissionPoint[]> {
195
+ if (roleKeys.length === 0) return [];
196
+ const rows = await this.db
197
+ .select({
198
+ id: authzPermissions.id,
199
+ action: authzPermissions.action,
200
+ subject: authzPermissions.subject,
201
+ description: authzPermissions.description,
202
+ })
203
+ .from(authzRolePermissions)
204
+ .innerJoin(authzPermissions, eq(authzRolePermissions.permissionId, authzPermissions.id))
205
+ .where(inArray(authzRolePermissions.roleKey, roleKeys));
206
+ const seen = new Set<string>();
207
+ return rows.filter(r => {
208
+ const key = `${r.action}:${r.subject}`;
209
+ if (seen.has(key)) return false;
210
+ seen.add(key);
211
+ return true;
212
+ }).map(r => ({
213
+ id: r.id,
214
+ action: r.action,
215
+ subject: r.subject,
216
+ description: r.description ?? undefined,
217
+ }));
218
+ }
219
+ }
220
+ ```
221
+
222
+ ---
223
+
224
+ ## Step 3: 后端模块注册
225
+
226
+ ### 3.1 创建 PermissionModule
227
+
228
+ ```typescript
229
+ import { Module } from '@nestjs/common';
230
+ import { DbPermissionResolver } from './db-permission-resolver';
231
+ import { PermissionController } from './permission.controller';
232
+
233
+ @Module({
234
+ controllers: [PermissionController],
235
+ providers: [DbPermissionResolver],
236
+ exports: [DbPermissionResolver],
237
+ })
238
+ export class PermissionModule {}
239
+ ```
240
+
241
+ ### 3.2 在 app.module.ts 中注册
242
+
243
+ 通过 `PlatformModule.forRoot()` 的 `authz` 选项透传 `permissionResolver`,**不要单独注册 `AuthZPaasModule.forRoot()`**(`PlatformModule` 内部已注册,重复注册会导致全局模块冲突,`permissionResolver` 被空注册覆盖为 null)。
244
+
245
+ ```typescript
246
+ import { PlatformModule } from '@lark-apaas/fullstack-nestjs-core';
247
+ import { DbPermissionResolver } from './modules/permission/db-permission-resolver';
248
+ import { PermissionModule } from './modules/permission/permission.module';
249
+
250
+ @Module({
251
+ imports: [
252
+ PlatformModule.forRoot({
253
+ authz: { permissionResolver: DbPermissionResolver },
254
+ }),
255
+ PermissionModule,
256
+ // ... 其他业务模块
257
+ ViewModule,
258
+ ],
259
+ })
260
+ export class AppModule {}
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Step 4: 后端权限管理 API
266
+
267
+ 创建 `server/modules/permission/permission.controller.ts`:
268
+
269
+ ```typescript
270
+ import { Controller, Get, Post, Body, Inject } from '@nestjs/common';
271
+ import { Can } from '@lark-apaas/fullstack-nestjs-core';
272
+ import { DRIZZLE_DATABASE, type PostgresJsDatabase } from '@lark-apaas/fullstack-nestjs-core';
273
+ import { authzPermissions, authzRolePermissions } from '../../database/schema';
274
+ import { eq, and, inArray } from 'drizzle-orm';
275
+
276
+ @Controller('api/permissions')
277
+ export class PermissionController {
278
+ constructor(
279
+ @Inject(DRIZZLE_DATABASE) private readonly db: PostgresJsDatabase,
280
+ ) {}
281
+
282
+ // ---- 权限点位只读查询(供管理页面展示可勾选列表) ----
283
+
284
+ @Can('manage', 'Permission')
285
+ @Get()
286
+ async listPermissions() {
287
+ return this.db.select().from(authzPermissions);
288
+ }
289
+
290
+ // ---- 角色-权限映射 ----
291
+
292
+ @Can('manage', 'Permission')
293
+ @Get('role-mappings')
294
+ async listRoleMappings() {
295
+ return this.db.select().from(authzRolePermissions);
296
+ }
297
+
298
+ @Can('manage', 'Permission')
299
+ @Post('role-mappings/batch')
300
+ async batchUpdateRoleMappings(
301
+ @Body() dto: { roleKey: string; add: string[]; remove: string[] },
302
+ ) {
303
+ await this.db.transaction(async (tx) => {
304
+ if (dto.remove.length > 0) {
305
+ await tx.delete(authzRolePermissions).where(
306
+ and(
307
+ eq(authzRolePermissions.roleKey, dto.roleKey),
308
+ inArray(authzRolePermissions.permissionId, dto.remove),
309
+ ),
310
+ );
311
+ }
312
+ if (dto.add.length > 0) {
313
+ await tx.insert(authzRolePermissions).values(
314
+ dto.add.map((permissionId) => ({ roleKey: dto.roleKey, permissionId })),
315
+ );
316
+ }
317
+ });
318
+ }
319
+ }
320
+ ```
321
+
322
+ API 端点汇总:
323
+
324
+ | 端点 | 方法 | 说明 | 来源 |
325
+ |------|------|------|------|
326
+ | `/api/permissions` | GET | 权限点位列表(只读) | 业务 Controller |
327
+ | `/api/permissions/role-mappings` | GET | 角色-权限映射列表 | 业务 Controller |
328
+ | `/api/permissions/role-mappings/batch` | POST | 批量更新映射(Body: `{ roleKey, add: permissionId[], remove: permissionId[] }`) | 业务 Controller |
329
+ | (内置端点) | GET | 当前用户的权限点位 | **内置**(PlatformPermissionController,自动注册) |
330
+
331
+ > - 当前用户权限点位查询由 `AuthZPaasModule` 内置的 `PlatformPermissionController` 自动提供,**业务侧无需实现**
332
+ > - 权限点位的增删改由 Agent 通过 DDL 操作,不提供 POST/PUT/DELETE 接口,确保点位定义与代码中的 `@Can`/`<Can>` 保持一致
333
+
334
+ ---
335
+
336
+ ## Step 5: 鉴权代码
337
+
338
+ **⛔ 必须对照权限设计方案的功能权限表,将每个权限点位逐一落实到对应的后端 API(`@Can`)和前端入口(`<Can>`),完成后逐行核对确认无遗漏。**
339
+
340
+ ```typescript
341
+ import { Can } from '@lark-apaas/fullstack-nestjs-core';
342
+
343
+ // 后端:新建直接用 @Can;升级则将 @CanRole(['admin']) → @Can('action', 'Subject')
344
+ @Can('read', 'Task') @Get() findAll() { ... }
345
+ @Can('create', 'Task') @Post() create() { ... }
346
+ @Can('update', 'Task') @Put(':id') update() { ... }
347
+ @Can('delete', 'Task') @Delete(':id') remove() { ... }
348
+
349
+ // 前端:<CanRole roles={['admin']}> → <Can action="update" subject="Task">
350
+ <Can action="update" subject="Task">
351
+ <Button>编辑</Button>
352
+ </Can>
353
+ ```
354
+
355
+ **关键约束**:
356
+ - **⛔ 动态权限模式下禁止 `@CanRole`**,所有鉴权(含管理 API)统一用 `@Can`
357
+ - 未注入 `permissionResolver` 时使用 `@Can` 会抛出 500 错误
358
+ - 多权限叠加装饰器:`@Can('read', 'Task') @Can('read', 'User')`
359
+
360
+ ---
361
+
362
+ ## Step 6: 前端权限初始化
363
+
364
+ ### 前端 API 函数
365
+
366
+ 在 `client/src/api/index.ts` 中添加**管理页面所需**的请求函数:
367
+ - 权限点位查询 API(listPermissions)
368
+ - 角色-权限映射 CRUD API(listRoleMappings / createRoleMapping / deleteRoleMapping)
369
+
370
+ > **无需添加用户权限查询 API**:auth-sdk 的 `AuthProvider` 会自动请求内置端点获取当前用户的权限点位,业务侧不需要手动配置。`AppContainer` 也无需传入任何额外 prop。
371
+
372
+ ---
373
+
374
+ ## Step 7: 前端 UI 权限控制
375
+
376
+ 统一使用 `Can` 组件和 `useCan` Hook(**禁止**旧的 `CanPermission` / `useCanPermission`,已删除):
377
+
378
+ ```typescript
379
+ import { Can, useCan, useAuth } from '@lark-apaas/client-toolkit/auth';
380
+
381
+ // 组件方式(推荐,自动处理 isLoading)
382
+ <Can action="delete" subject="Task">
383
+ <Button variant="destructive">删除</Button>
384
+ </Can>
385
+
386
+ // Hook 方式 — 返回 { allowed, isLoading },必须手动处理 isLoading
387
+ const { allowed: canDelete, isLoading } = useCan('delete', 'Task');
388
+ ```
389
+
390
+ ### 路由守卫(ProtectedRoute)
391
+
392
+ 动态权限模式下,路由守卫必须基于权限点位判断(`requiredPermissions`),**禁止使用 `requiredRoles` + `ability.can(role, ROLE_SUBJECT)` 的静态角色模式**:
393
+
394
+ ```typescript
395
+ const ProtectedRoute: React.FC<{
396
+ children: React.ReactNode;
397
+ requiredPermissions: { action: string; subject: string }[];
398
+ }> = ({ children, requiredPermissions }) => {
399
+ const { ability, isLoading } = useAuth();
400
+ if (isLoading) return <Loading />;
401
+ const hasPermission = requiredPermissions.every(
402
+ ({ action, subject }) => ability.can(action, subject),
403
+ );
404
+ return hasPermission ? <>{children}</> : <Navigate to="/unauthorized" replace />;
405
+ };
406
+
407
+ // 使用示例
408
+ <ProtectedRoute requiredPermissions={[{ action: 'manage', subject: 'Permission' }]}>
409
+ <RoleManagementPage />
410
+ </ProtectedRoute>
411
+ ```
412
+
413
+ ---
414
+
415
+ ## Step 8: 管理页面增强
416
+
417
+ **不创建独立的权限管理页面**,在角色管理页面基础上增强。完整 UI 规格和代码模板见 [management-page-spec.md § 动态权限增强](management-page-spec.md#动态权限增强仅动态权限点位模式)。
418
+
419
+ 新增两项功能:
420
+
421
+ ### 1. 角色表格新增「权限点位」列
422
+
423
+ | 列 | key | width | 渲染规则 |
424
+ |---|-----|-------|---------|
425
+ | 权限点位 | `permissions` | 250 | 只展示 `description`;最多 3 个,超出用 `+N` Badge + HoverCard;无绑定显示 `--` |
426
+
427
+ ### 2. 操作列新增「配置权限」
428
+
429
+ 收进 `...` 更多操作下拉菜单,**不单独外露**。点击打开 Dialog:
430
+
431
+ ```
432
+ ┌──────────────────────────────────────────────┐
433
+ │ 配置权限 - 销售专员 │
434
+ │ 勾选后保存将替换当前权限。 │
435
+ ├──────────────────────────────────────────────┤
436
+ │ ▼ 客户 (1/4) │
437
+ │ │ ☑ 查看客户列表与详情 read:Customer │
438
+ │ │ ☐ 创建客户 create:Customer │
439
+ │ ▶ 权限管理 (0/1) │
440
+ ├──────────────────────────────────────────────┤
441
+ │ [取消] [保存] │
442
+ └──────────────────────────────────────────────┘
443
+ ```
444
+
445
+ **Subject 中文映射**:
446
+
447
+ ```ts
448
+ const SUBJECT_LABELS: Record<string, string> = {
449
+ Customer: '客户',
450
+ Task: '任务',
451
+ Permission: '权限管理',
452
+ // 按权限设计方案的 Subject 补充...
453
+ };
454
+ const getSubjectLabel = (subject: string) => SUBJECT_LABELS[subject] ?? subject;
455
+ ```
456
+
457
+ **ConfigPermissionsDialog 组件**:
458
+
459
+ ```tsx
460
+ import { useMemo, useState } from 'react';
461
+ import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion';
462
+ import { Checkbox } from '@/components/ui/checkbox';
463
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
464
+ import { Button } from '@/components/ui/button';
465
+
466
+ function ConfigPermissionsDialog({ role, permissions, mappings, open, onOpenChange, onRefresh }) {
467
+ const grouped = useMemo(() => {
468
+ const map = new Map<string, typeof permissions>();
469
+ permissions.forEach(p => {
470
+ const list = map.get(p.subject) || [];
471
+ list.push(p);
472
+ map.set(p.subject, list);
473
+ });
474
+ return map;
475
+ }, [permissions]);
476
+
477
+ const initialIds = useMemo(() => new Set(
478
+ mappings.filter(m => m.roleKey === role?.bizID).map(m => m.permissionId)
479
+ ), [mappings, role]);
480
+
481
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set(initialIds));
482
+
483
+ const defaultExpanded = useMemo(() =>
484
+ Array.from(grouped.keys()).filter(subject =>
485
+ grouped.get(subject)!.some(p => initialIds.has(p.id))
486
+ ),
487
+ [grouped, initialIds]);
488
+
489
+ const handleToggle = (id: string, checked: boolean) => {
490
+ setSelectedIds(prev => {
491
+ const next = new Set(prev);
492
+ checked ? next.add(id) : next.delete(id);
493
+ return next;
494
+ });
495
+ };
496
+
497
+ const handleSave = async () => {
498
+ const add = [...selectedIds].filter(id => !initialIds.has(id));
499
+ const remove = [...initialIds].filter(id => !selectedIds.has(id));
500
+ if (add.length > 0 || remove.length > 0) {
501
+ await batchUpdateRoleMappings({ roleKey: role.bizID, add, remove });
502
+ }
503
+ onRefresh();
504
+ onOpenChange(false);
505
+ };
506
+
507
+ return (
508
+ <Dialog open={open} onOpenChange={onOpenChange}>
509
+ <DialogContent>
510
+ <DialogHeader>
511
+ <DialogTitle>配置权限 - {role?.name}</DialogTitle>
512
+ <DialogDescription>勾选后保存将替换当前权限。</DialogDescription>
513
+ </DialogHeader>
514
+ <div className="max-h-[60vh] overflow-y-auto">
515
+ <Accordion type="multiple" defaultValue={defaultExpanded}>
516
+ {Array.from(grouped.entries()).map(([subject, perms]) => {
517
+ const enabledCount = perms.filter(p => selectedIds.has(p.id)).length;
518
+ const allSelected = enabledCount === perms.length;
519
+ return (
520
+ <AccordionItem key={subject} value={subject}>
521
+ <div className="flex items-center gap-2 px-3">
522
+ <Checkbox
523
+ checked={allSelected}
524
+ onCheckedChange={(checked) => {
525
+ const ids = perms.map(p => p.id);
526
+ setSelectedIds(prev => {
527
+ const without = new Set([...prev].filter(id => !ids.includes(id)));
528
+ if (checked) ids.forEach(id => without.add(id));
529
+ return without;
530
+ });
531
+ }}
532
+ onClick={(e) => e.stopPropagation()}
533
+ />
534
+ <AccordionTrigger className="py-2 text-sm flex-1">
535
+ {getSubjectLabel(subject)} ({enabledCount}/{perms.length})
536
+ </AccordionTrigger>
537
+ </div>
538
+ <AccordionContent className="px-3 bg-muted/50 rounded-b-md">
539
+ {perms.map(p => (
540
+ <label key={p.id} className="flex items-center gap-2 py-1.5 cursor-pointer">
541
+ <Checkbox checked={selectedIds.has(p.id)} onCheckedChange={v => handleToggle(p.id, !!v)} />
542
+ <span className="text-sm">{p.description}</span>
543
+ <code className="text-xs text-muted-foreground">{p.action}:{p.subject}</code>
544
+ </label>
545
+ ))}
546
+ </AccordionContent>
547
+ </AccordionItem>
548
+ );
549
+ })}
550
+ </Accordion>
551
+ </div>
552
+ <DialogFooter>
553
+ <Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
554
+ <Button onClick={handleSave}>保存</Button>
555
+ </DialogFooter>
556
+ </DialogContent>
557
+ </Dialog>
558
+ );
559
+ }
560
+ ```
561
+
562
+ **布局规则**:
563
+ - 按 `subject` 分组为可展开/收起的 Accordion 卡片,标题 `Subject中文名 (已开启/总数)`
564
+ - 有已勾选权限的 Subject 默认展开,全未勾选的默认收起
565
+ - 每行:`Checkbox + description + action:subject badge(置灰小字)`
566
+ - 保存时对比初始/当前状态算 diff,调用 `batchUpdateRoleMappings` 提交 `{ roleKey, add, remove }`
567
+ - 复用 `Accordion`、`Checkbox`、`Dialog` 组件
568
+
569
+ ### 数据加载
570
+
571
+ `loadRoles` 需额外并行加载权限点位和映射数据:
572
+
573
+ ```typescript
574
+ const [roles, perms, maps] = await Promise.all([
575
+ getRoles(), listPermissions(), listRoleMappings(),
576
+ ]);
577
+ ```
578
+
579
+ ---
580
+
581
+ ## 错误处理
582
+
583
+ SDK 内部统一将平台错误转为 `HttpException`,Controller 无需 try-catch:
584
+
585
+ | 场景 | HTTP 状态码 | 说明 |
586
+ |------|-----------|------|
587
+ | 平台 4xx | 透传 | 参数错误、权限不足等 |
588
+ | 平台 5xx | 502 | 平台内部错误 |
589
+ | HTTP 200 + `status_code !== '0'` | 502 | 平台业务错误 |
590
+
591
+ ---
592
+
593
+ ## Step 9: 验证(禁止跳过)
594
+
595
+ 1. 确认编译通过
596
+ 2. grep 确认 CanRole 零残留:
597
+ ```bash
598
+ grep -r "CanRole\|useCanRole\|ROLE_SUBJECT" --include="*.ts" --include="*.tsx" server/ client/ shared/
599
+ ```
600
+ ⛔ 结果必须为空,否则回到 Step 5 继续替换
601
+ 3. 逐项对照下方「实现检查表」
602
+
603
+ ---
604
+
605
+ ## 实现检查表
606
+
607
+ - [ ] **权限设计方案**:升级场景已结合用户需求和现有实现产出新版权限设计方案,并获得用户确认
608
+ - [ ] **场景判断**:明确判断了是新建还是升级场景
609
+ - [ ] **数据库**:通过 `ddl_sql` 创建了 `authz_permissions` 和 `authz_role_permissions` 两张表,DDL 确认执行完成
610
+ - [ ] **数据预填**:分两步 INSERT(先 `authz_permissions`,再 `authz_role_permissions`),SELECT 验证两张表 row_count > 0
611
+ - [ ] **Resolver**:`DbPermissionResolver` 实现了 `IPermissionResolver` 接口
612
+ - [ ] **模块注册**:`app.module.ts` 中通过 `PlatformModule.forRoot({ authz: { permissionResolver: DbPermissionResolver } })` 注册(不要单独注册 `AuthZPaasModule.forRoot()`)
613
+ - [ ] **权限管理 API**:权限点位只读 GET + 角色映射 CRUD(用户权限查询由内置端点自动提供)
614
+ - [ ] **鉴权代码**:新建用 `@Can`/`<Can>`;升级已将所有 `@CanRole` → `@Can`,`<CanRole>` → `<Can>`(包括管理 API),grep 确认项目中 `CanRole`/`useCanRole` 零残留
615
+ - [ ] **manage:Permission 点位**:`authz_permissions` 表中包含 `manage:Permission` 点位,admin 角色已映射该点位,PermissionController 使用 `@Can('manage', 'Permission')`
616
+ - [ ] **前端权限控制**:使用 `<Can action subject>` 组件或 `useCan(action, subject)` Hook
617
+ - [ ] **isLoading 处理**:`useCan` 使用处检查了 `isLoading`
618
+ - [ ] **管理页面增强**:角色表格有权限点位列 + 配置权限菜单项(收在 `...` 下拉内,不外露)
619
+ - [ ] **类型安全**:代码中无 `any` 类型,使用具体类型或判别联合类型
620
+ - [ ] **前后端统一**:后端 `@Can('read', 'Task')` 对应前端 `<Can action="read" subject="Task">`
621
+ - [ ] **端到端测试**:角色权限相关接口(权限点位查询、角色-权限映射 CRUD 等)开发完成后,对每个接口进行端到端测试,确认:(1) 接口按预期定义返回 (2) 接口对数据库的 CRUD 与数据库状态一致