@lark-apaas/coding-steering 0.1.6-alpha.1 → 0.1.6-alpha.11
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.
- package/README.md +11 -2
- package/package.json +1 -1
- package/steering/design-stack/skills/.gitkeep +0 -0
- package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +122 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +174 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +621 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/management-page-spec.md +505 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/runtime-role-controller-spec.md +203 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +90 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +216 -0
- package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +139 -0
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +405 -0
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +628 -0
- package/steering/nestjs-react-fullstack/skills/devops-guide/SKILL.md +119 -0
- package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +270 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +214 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +163 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +309 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +190 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +160 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +256 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +103 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +198 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +128 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +207 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +164 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +90 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +164 -0
- package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +267 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +582 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +357 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +513 -0
- package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +118 -0
- package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +177 -0
- package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +452 -0
- package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +300 -0
- package/steering/nestjs-react-fullstack/skills/user-management-best-practices/SKILL.md +142 -0
- package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +253 -0
- package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +585 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: user-identity
|
|
3
|
+
description: "Use when getting current user info/profile, displaying user name/avatar/email, converting miaoda userId to lark_user_id, or reading req.userContext fields (userId/roles/tenantId): useCurrentUserProfile, AuthNPaasService, FeishuID conversion. 触发词:用户身份, 用户信息, 用户资料, 当前用户, userProfile, useCurrentUserProfile, 飞书ID, FeishuID, 飞书用户ID, lark_user_id, 用户ID转换, AuthNPaasService, 用户上下文, userContext, userContext.roles, 用户角色, 当前用户角色, 获取请求者角色, 展示用户, 显示用户, 用户面板, 我是谁, 获取用户"
|
|
4
|
+
steering: true
|
|
5
|
+
steering-topic: user_identity
|
|
6
|
+
match-template-name: nestjs-react-fullstack
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# 用户身份与上下文
|
|
10
|
+
|
|
11
|
+
本 skill 专注于**用户身份**:ID 体系、`req.userContext` 字段、`useCurrentUserProfile`、妙搭 ↔ 飞书 ID 转换。
|
|
12
|
+
|
|
13
|
+
**接口认证**(`@NeedLogin()` 装饰器、`AuthNPaasGuard` opt-in 模式、公开接口处理、401)请使用 [`authn-guide`](../authn-guide/SKILL.md) skill。
|
|
14
|
+
|
|
15
|
+
## 零、ID 体系警告(CRITICAL)
|
|
16
|
+
|
|
17
|
+
⚠️ "user_id" 这个字面在本项目里指**三个不同的东西**,必须先区分清楚再写代码:
|
|
18
|
+
|
|
19
|
+
**作为身份 ID 字段名(指代具体某个用户的 ID 值):**
|
|
20
|
+
|
|
21
|
+
| 出现位置 | 实际含义 | 体系 |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `useCurrentUserProfile().user_id`、`req.userContext.userId` | 妙搭用户 ID(纯数字字符串) | 妙搭 |
|
|
24
|
+
| `useCurrentUserProfile().lark_user_id`、`AuthNPaasService.getCurrentUserLarkUserId()` 返回值 | 飞书 user_id(== `employee_id`,企业内身份,**无固定前缀**) | 飞书 |
|
|
25
|
+
|
|
26
|
+
**作为 API 参数的取值(不是一个 ID,而是告诉接口"我传入哪种类型的 ID"):**
|
|
27
|
+
|
|
28
|
+
| 出现位置 | 实际含义 |
|
|
29
|
+
|---|---|
|
|
30
|
+
| 飞书 OpenAPI 参数 `user_id_type: 'user_id'`(也可取 `'open_id'` / `'union_id'`) | 字符串字面,与上方的飞书 user_id 字段是一致体系但用途不同 |
|
|
31
|
+
|
|
32
|
+
**严禁**:把 `useCurrentUserProfile().user_id`(妙搭 ID)直接当飞书 ID 传给飞书 API。飞书 ID 必须通过 `lark_user_id` 字段或 `AuthNPaasService` 获取。
|
|
33
|
+
|
|
34
|
+
`useCurrentUserProfile()` 返回的字段中涉及**两套完全独立的 ID 体系**,严禁混用或互相回退:
|
|
35
|
+
|
|
36
|
+
| 字段 | 体系 | 格式 | 说明 |
|
|
37
|
+
|------|------|------|------|
|
|
38
|
+
| `user_id` | 妙搭 | 纯数字字符串 | 妙搭平台用户 ID |
|
|
39
|
+
| `lark_user_id` | 飞书 | 字符串 | 飞书 user_id(== employee_id),通过额外异步请求获取 |
|
|
40
|
+
|
|
41
|
+
> **严禁**:`useCurrentUserProfile()` 返回值里**没有** `open_id`、`feishu_id`、`openId` 字段——飞书 ID 在这个 Hook 里**只通过 `lark_user_id`** 暴露。如果在本 Hook 的消费代码里看到 `userInfo.open_id` 等写法,属于历史错误,必须改成 `lark_user_id`。
|
|
42
|
+
>
|
|
43
|
+
> (范围限定:上一条只约束 `useCurrentUserProfile()` 的消费代码。**项目其他场景** —— 例如调 spark `id_convert`、调飞书通讯录 API —— 出现 `open_id` / `union_id` 是正常且必要的,不在禁止之列。)
|
|
44
|
+
>
|
|
45
|
+
> **严禁**:当 `lark_user_id` 为空时回退到 `user_id` 展示。两套 ID 含义完全不同,回退会误导用户。正确做法是条件渲染:有值则展示,无值则不展示或展示"未关联飞书账号"。
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 一、功能决策树
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
用户需求
|
|
53
|
+
│
|
|
54
|
+
├─ 接口认证(@NeedLogin / 公开接口 / 401 / 守卫流程)?
|
|
55
|
+
│ └─ 跳到 `authn-guide` skill
|
|
56
|
+
│
|
|
57
|
+
├─ 需要在服务端读取当前用户 ID / 租户 / 角色等?
|
|
58
|
+
│ └─ 是 ──→ req.userContext(第二节)
|
|
59
|
+
│
|
|
60
|
+
├─ 需要在前端展示当前用户信息(名称/头像/邮箱)?
|
|
61
|
+
│ └─ 是 ──→ useCurrentUserProfile()(第三节)
|
|
62
|
+
│
|
|
63
|
+
├─ 需要获取当前用户的飞书 user_id(即 employee_id,企业内身份)?
|
|
64
|
+
│ ├─ 后端 ──→ AuthNPaasService.getCurrentUserLarkUserId()(第三节)
|
|
65
|
+
│ └─ 前端 ──→ useCurrentUserProfile().lark_user_id(第三节)
|
|
66
|
+
│
|
|
67
|
+
├─ 需要批量把 妙搭 userId 转成 飞书 **user_id (employee_id,无前缀)** —— 不是 open_id (`ou_`)、不是 union_id (`on_`)?
|
|
68
|
+
│ └─ 是 ──→ AuthNPaasService.getBatchLarkUserIds()(第三节)
|
|
69
|
+
│
|
|
70
|
+
├─ 需要 飞书 open_id(`ou_` 开头)/ union_id(`on_` 开头)?
|
|
71
|
+
│ └─ ⚠️ **AuthNPaasService 不产出 open_id/union_id**,只能 user_id
|
|
72
|
+
│ └─ 跳到 `feishu` skill 的 `references/id-convert.md`(spark id_convert type 10/11)
|
|
73
|
+
│
|
|
74
|
+
├─ 需要把 飞书 open_id / union_id 反查成 妙搭 userId?
|
|
75
|
+
│ └─ 跳到 `feishu` skill 的 `references/id-convert.md`(spark id_convert type 20/21)
|
|
76
|
+
│
|
|
77
|
+
├─ 需要把 飞书 user_id(employee_id)反查成 妙搭 userId?
|
|
78
|
+
│ └─ 无单步方案 ──→ `feishu` skill `references/id-convert.md` "反向两步走"
|
|
79
|
+
│
|
|
80
|
+
└─ 需要自定义飞书 ID 转换接口?
|
|
81
|
+
└─ 是 ──→ 注入 AuthNPaasService 编写 Controller(第四节,仅适用于 user_id)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> **相关技能**:接口认证/守卫/401 参见 `authn-guide`;登录/登出/获取用户信息等 Dataloom SDK 操作参见 `client-builtins-user-service`;**"用户能做什么"**(角色/权限点位鉴权)参见 `authz-guide`。本 skill 只解决**"用户是谁"**。
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 二、用户上下文(req.userContext)
|
|
89
|
+
|
|
90
|
+
`UserContextMiddleware` 解析 Gateway 注入的 `x-larkgw-suda-webuser` 头,把当前用户上下文挂到 `req.userContext`,所有 Controller 均可通过 `@Req() req: Request` 读取。
|
|
91
|
+
|
|
92
|
+
> 完整的解析→守卫流程见 `authn-guide` skill 第二节"认证流程"。
|
|
93
|
+
|
|
94
|
+
### 字段表
|
|
95
|
+
|
|
96
|
+
| 字段 | 类型 | 说明 |
|
|
97
|
+
|------|------|------|
|
|
98
|
+
| `userId` | `string` | 妙搭用户 ID |
|
|
99
|
+
| `tenantId` | `number` | 租户 ID |
|
|
100
|
+
| `appId` | `string` | 应用 ID |
|
|
101
|
+
| `loginUrl` | `string` | 登录跳转 URL |
|
|
102
|
+
| `userType` | `string` | 用户类型(如 `_employee`) |
|
|
103
|
+
| `env` | `string` | 环境(如 `preview`、`online`) |
|
|
104
|
+
| `userName` | `string` | 用户名 |
|
|
105
|
+
| `userNameI18n` | `{ zh_cn, en_us, ja_jp }` | 多语言用户名 |
|
|
106
|
+
| `isSystemAccount` | `boolean` | 是否系统账号 |
|
|
107
|
+
| `roles` | `string[] \| null` | 用户角色列表。**未开启权限服务或用户无角色时为 `null`** |
|
|
108
|
+
| `baseUrl` | `string` | 网关内部地址 |
|
|
109
|
+
|
|
110
|
+
### 服务端读取角色示例
|
|
111
|
+
|
|
112
|
+
`roles` 字段记录当前用户在应用中的角色列表,可在 Controller 业务逻辑中消费(如根据角色返回不同数据):
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { Controller, Get, Req } from '@nestjs/common';
|
|
116
|
+
import { Request } from 'express'; // ⚠️ Request 类型必须来自 'express',不要 import 自 'http' / 'undici' 或漏掉 import
|
|
117
|
+
import { TaskService } from './task.service'; // 按项目实际路径 import 业务 Service
|
|
118
|
+
|
|
119
|
+
// req.userContext 的类型由 @lark-apaas/fullstack-nestjs-core 通过 declare module 'express' 自动注入到 Request,无需手动声明
|
|
120
|
+
@Controller('api/tasks')
|
|
121
|
+
export class TasksController {
|
|
122
|
+
constructor(private readonly taskService: TaskService) {}
|
|
123
|
+
|
|
124
|
+
@Get()
|
|
125
|
+
async listTasks(@Req() req: Request) {
|
|
126
|
+
const roles: string[] | null = req.userContext?.roles ?? null;
|
|
127
|
+
// roles 示例:['text_editor', 'visitor', 'admin']
|
|
128
|
+
// ⚠️ 未开启权限服务或用户无角色时为 null
|
|
129
|
+
if (roles?.includes('admin')) {
|
|
130
|
+
return this.taskService.findAll();
|
|
131
|
+
}
|
|
132
|
+
return this.taskService.findByUser(req.userContext?.userId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
> **注意**:`roles` 在未开启权限服务或用户无角色时为 `null`,使用前必须做空值处理。
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 三、飞书 ID 转换(FeishuID Converter)
|
|
142
|
+
|
|
143
|
+
### 核心概念
|
|
144
|
+
|
|
145
|
+
妙搭平台的用户 ID(`userId`)与飞书用户 ID 是两套独立体系。调用飞书 OpenAPI 时需要传入某种飞书侧 ID(`open_id` / `union_id` / `user_id` 任选其一,具体通过哪个参数指定取决于 API:消息 API 用 `receive_id_type`,多数其他 API 用 `user_id_type`,文档协作者用 `member_id_type`)。
|
|
146
|
+
|
|
147
|
+
`AuthNPaasService` 暴露的飞书 ID 是 **`user_id`**(即 `employee_id`,飞书企业内的用户标识)这一种,且**只支持 妙搭 userId → 飞书 user_id 单向转换**。
|
|
148
|
+
|
|
149
|
+
> 如果需要 `open_id` / `union_id`,或者需要"飞书 ID → 妙搭 userId"反向,请改用飞书开放平台 `spark id_convert` 接口,参见 `feishu` skill 的 `references/id-convert.md`。
|
|
150
|
+
|
|
151
|
+
### 后端 API
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { Injectable } from '@nestjs/common';
|
|
155
|
+
import { AuthNPaasService } from '@lark-apaas/fullstack-nestjs-core';
|
|
156
|
+
|
|
157
|
+
@Injectable()
|
|
158
|
+
export class MyService {
|
|
159
|
+
constructor(private readonly authnService: AuthNPaasService) {}
|
|
160
|
+
|
|
161
|
+
async example() {
|
|
162
|
+
// 获取当前登录用户的飞书 user_id
|
|
163
|
+
const larkUserId = await this.authnService.getCurrentUserLarkUserId();
|
|
164
|
+
// => '<飞书 user_id>' | null
|
|
165
|
+
|
|
166
|
+
// 批量转换(最多 100 个)
|
|
167
|
+
const larkUserIds = await this.authnService.getBatchLarkUserIds(['uid1', 'uid2']);
|
|
168
|
+
// => ['<飞书 user_id>', null] 顺序与输入对应,失败项为 null
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
| 方法 | 签名 | 说明 |
|
|
174
|
+
|------|------|------|
|
|
175
|
+
| `getCurrentUserLarkUserId` | `() → Promise<string \| null>` | 从请求上下文获取当前用户的飞书 ID |
|
|
176
|
+
| `getBatchLarkUserIds` | `(userIds: string[]) → Promise<(string \| null)[]>` | 批量转换,最多 100 个,与输入顺序一一对应 |
|
|
177
|
+
|
|
178
|
+
### 内置接口
|
|
179
|
+
|
|
180
|
+
模块自动注册了 `GET /api/authnpaas/lark-user-id`,返回当前登录用户的飞书 ID:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{ "lark_user_id": "<飞书 user_id>" }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 前端获取
|
|
187
|
+
|
|
188
|
+
`useCurrentUserProfile()` Hook 已自动调用上述内置接口,返回值中包含 `lark_user_id` 字段:
|
|
189
|
+
|
|
190
|
+
> 本节示例只涉及 ID 相关字段(`user_id` / `lark_user_id`)和顺手的 `name`。完整返回值(`name` / `avatar` / `email` / `tenantId` 等)见 `client-builtins-user-service` skill。
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { useCurrentUserProfile } from '@lark-apaas/client-toolkit/hooks/useCurrentUserProfile';
|
|
194
|
+
|
|
195
|
+
const UserInfoPanel = () => {
|
|
196
|
+
const userInfo = useCurrentUserProfile();
|
|
197
|
+
|
|
198
|
+
if (!userInfo?.user_id) return <div>加载中...</div>;
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div>
|
|
202
|
+
<p>用户名: {userInfo.name}</p>
|
|
203
|
+
<p>用户 ID: {userInfo.user_id}</p>
|
|
204
|
+
{userInfo.lark_user_id && <p>飞书用户 ID: {userInfo.lark_user_id}</p>}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**注意**:
|
|
211
|
+
- `lark_user_id` 通过额外异步请求获取,可能晚于 `user_id` 等基础字段就绪
|
|
212
|
+
- 请求失败或用户无对应飞书账号时值为 `undefined`,**必须用条件渲染**
|
|
213
|
+
- `useCurrentUserProfile()` 的返回值里**不存在** `open_id`、`feishu_id`、`openId` 字段——在这个 Hook 的消费代码里飞书 ID 唯一字段名是 `lark_user_id`(仅约束本 Hook;项目其他场景调 spark `id_convert` / 通讯录 API 出现 `open_id` 是正常的)
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 四、自定义飞书 ID 转换接口
|
|
218
|
+
|
|
219
|
+
当内置接口不满足需求(如需要批量转换),可注入 `AuthNPaasService` 编写自定义 Controller:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { Controller, Get, Post, Body, HttpCode } from '@nestjs/common';
|
|
223
|
+
import { AuthNPaasService } from '@lark-apaas/fullstack-nestjs-core';
|
|
224
|
+
|
|
225
|
+
// 生产环境建议加 class-validator 装饰器(如 @IsArray() @IsString({ each: true })
|
|
226
|
+
// @ArrayMaxSize(100))并启用全局 ValidationPipe;本示例聚焦 AuthNPaas 用法,省略校验
|
|
227
|
+
class BatchConvertDto {
|
|
228
|
+
userIds!: string[];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@Controller('api/feishu-id')
|
|
232
|
+
export class FeishuIdController {
|
|
233
|
+
constructor(private readonly authnService: AuthNPaasService) {}
|
|
234
|
+
|
|
235
|
+
@Get('current')
|
|
236
|
+
async getCurrent() {
|
|
237
|
+
const larkUserId = await this.authnService.getCurrentUserLarkUserId();
|
|
238
|
+
return { larkUserId };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@Post('batch')
|
|
242
|
+
@HttpCode(200)
|
|
243
|
+
async batchConvert(@Body() dto: BatchConvertDto) {
|
|
244
|
+
const larkUserIds = await this.authnService.getBatchLarkUserIds(dto.userIds);
|
|
245
|
+
return { larkUserIds };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
前端调用示例:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
// 获取当前用户飞书 ID
|
|
254
|
+
const { larkUserId } = await request<{ larkUserId: string | null }>({
|
|
255
|
+
url: '/api/feishu-id/current',
|
|
256
|
+
method: 'GET',
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// 批量转换
|
|
260
|
+
const { larkUserIds } = await request<{ larkUserIds: (string | null)[] }>({
|
|
261
|
+
url: '/api/feishu-id/batch',
|
|
262
|
+
method: 'POST',
|
|
263
|
+
data: { userIds: ['uid1', 'uid2', 'uid3'] },
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 五、禁止行为清单
|
|
270
|
+
|
|
271
|
+
| 禁止行为 | 正确做法 |
|
|
272
|
+
|---------|---------|
|
|
273
|
+
| 在 `useCurrentUserProfile()` 消费代码里使用 `open_id`、`feishu_id`、`openId` 等字段名 | `useCurrentUserProfile()` 中飞书 ID 的**唯一字段名**是 `lark_user_id`(限本 Hook;调 spark `id_convert` / 通讯录 API 出现 `open_id` 正常) |
|
|
274
|
+
| `lark_user_id` 为空时回退到 `user_id` 展示 | 两套 ID 体系完全不同,严禁互相回退。无值时不展示或展示"未关联飞书账号" |
|
|
275
|
+
| 前端直接展示 `lark_user_id` 而不处理空值 | `lark_user_id` 可能为 `undefined`(加载中/获取失败/无飞书账号),必须用条件渲染 |
|
|
276
|
+
| 用 `if (!userInfo)` 判断加载状态 | 初始值为空对象(truthy),必须用 `if (!userInfo?.user_id)` |
|
|
277
|
+
| 手动实例化 `AuthNPaasService` | 通过 NestJS 依赖注入获取:`constructor(private readonly authnService: AuthNPaasService)` |
|
|
278
|
+
| 单独注册 `AuthNPaasModule.forRoot()` | 已通过 `PlatformModule.forRoot()` 自动注册,无需手动导入 |
|
|
279
|
+
| 自行调用平台 `/v1/app/{appId}/account/user/convert` 接口 | 必须使用 `AuthNPaasService` 的方法,内置错误处理和可观测性 |
|
|
280
|
+
| `getBatchLarkUserIds` 传入超过 100 个 ID | 分批调用,每批最多 100 个 |
|
|
281
|
+
| 在前端自行封装飞书 ID 转换请求 | 使用 `useCurrentUserProfile()` 获取当前用户飞书 ID;批量转换通过后端接口 |
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## 六、常见问题
|
|
286
|
+
|
|
287
|
+
> 接口 401 / 登录跳转相关问题见 `authn-guide` skill 第四节"常见问题"。
|
|
288
|
+
|
|
289
|
+
### 飞书 ID 返回 null
|
|
290
|
+
|
|
291
|
+
1. 确认用户已登录(`req.userContext.userId` 存在)
|
|
292
|
+
2. 确认 `appId` 在请求上下文中存在
|
|
293
|
+
3. 检查平台转换接口是否正常(查看后端日志中 `[batchConvertUserIds]` 相关输出)
|
|
294
|
+
4. 部分用户可能无对应飞书账号,此时转换结果为 null 是预期行为
|
|
295
|
+
|
|
296
|
+
### 前端 lark_user_id 一直为 undefined
|
|
297
|
+
|
|
298
|
+
1. 确认后端 `AuthNPaasModule` 已注册(内置接口 `/api/authnpaas/lark-user-id` 可访问)
|
|
299
|
+
2. 检查浏览器开发者工具中该接口的请求和响应
|
|
300
|
+
3. `lark_user_id` 异步加载,确保组件正确处理了加载态
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: user-management-best-practices
|
|
3
|
+
description: "Use when 决定是否创建用户表、选择用户唯一标识(user_id vs email/手机号)、设计 user_profile 字段与 UNIQUE 约束、处理防重复提交/upsert 去重,或涉及问卷、抽奖、报名、投票等用户提交场景的表结构设计。触发词:用户管理, 用户表设计, user_id, email, user_profile, 去重, 防重复提交, 批量导入用户, upsert, UNIQUE约束, UserSelect, UserDisplay, user management, deduplication"
|
|
4
|
+
steering: true
|
|
5
|
+
steering-topic: user_management_best_practices
|
|
6
|
+
match-template-name: nestjs-react-fullstack
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# 用户管理最佳实践指南
|
|
10
|
+
|
|
11
|
+
## 概述
|
|
12
|
+
|
|
13
|
+
本指南适用于生成涉及用户管理、问卷、抽奖、报名、投票等用户提交场景的代码。
|
|
14
|
+
|
|
15
|
+
**核心问题:**
|
|
16
|
+
|
|
17
|
+
- 妙搭 Agent 无法获取企业的所有用户数据,需要构建应用维度的用户表
|
|
18
|
+
- 企业无法显式感知飞书的 user_id(不透明的数字字符串,如 "1847292357012580"),无法批量导入飞书用户到应用的用户表
|
|
19
|
+
- 推荐使用邮箱或者手机号作为业务层唯一标识(不是 user_id)
|
|
20
|
+
- UserSelect 返回的 IUserProfile 对象已包含 email 和 phone_number 字段,可直接使用
|
|
21
|
+
|
|
22
|
+
**本指南解决:**
|
|
23
|
+
|
|
24
|
+
- 何时使用 UserSelect 组件 vs 何时创建用户表
|
|
25
|
+
- 如何设计用户业务表(正确的唯一约束)
|
|
26
|
+
- 防止重复提交的正确实现方式
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 核心原则:禁止事项
|
|
31
|
+
|
|
32
|
+
| 禁止操作 | 说明 | 正确做法 |
|
|
33
|
+
| ------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
|
|
34
|
+
| 不推荐实现批量导入用户的功能 | Agent 无法获取企业全量用户,批量 INSERT users 表行不通 | 推荐使用 UserSelect 组件动态选择用户 |
|
|
35
|
+
| 不推荐用 user_id 作为去重逻辑 | 企业不感知飞书用户 id,无法感知是谁 | 使用 email 或者手机号做唯一约束(UNIQUE(email)),不要使用姓名,可能会重复 |
|
|
36
|
+
| 禁止编造 user_id | Mock 数据中不能随意编造 user_id 值 | 必须使用 SQL skill 规定的测试用户列表 |
|
|
37
|
+
| 禁止全量查询用户列表 | 全量拉取数据会导致接口超时或 OOM | 使用后端分页查询(skip/take) |
|
|
38
|
+
| 禁止前端分页用户列表 | 前端分页需要全量拉取数据,导致性能问题 | 使用后端分页,前端仅控制页码 |
|
|
39
|
+
| 禁止只存 user_id 不存 email or 手机号 | 业务层无法用 user_id 做防重和查询 | 存储 user_profile 字段(已包含 email/phone),并在表中单独添加 email 或 phone 列用于唯一约束 |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 决策树示例:何时创建用户表 vs 何时使用 user_profile
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
需要记录用户相关业务数据?
|
|
47
|
+
│
|
|
48
|
+
├─ 仅需要识别"谁"执行操作(发帖人、评论者)
|
|
49
|
+
│ └─ ✅ 直接使用 user_profile 类型字段
|
|
50
|
+
│ 示例:posts.author(user_profile)
|
|
51
|
+
│
|
|
52
|
+
└─ 需要存储业务特定的用户数据
|
|
53
|
+
│
|
|
54
|
+
├─ 数据与具体业务资源强绑定(问卷提交、抽奖记录)
|
|
55
|
+
│ └─ ✅ 创建业务表 + user_profile 字段 + email 唯一约束
|
|
56
|
+
│ 示例:questionnaire_responses (id, respondent, email, UNIQUE(questionnaire_id, email))
|
|
57
|
+
│
|
|
58
|
+
└─ 数据代表"用户在系统中的身份"(员工、学生、会员)
|
|
59
|
+
└─ ✅ 创建人员实体表,user_profile 作为主键
|
|
60
|
+
示例:employees (employee_profile PRIMARY KEY, department, position)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 用户列表查询与性能注意事项
|
|
66
|
+
|
|
67
|
+
### UserSelect vs 分页列表的选择
|
|
68
|
+
|
|
69
|
+
根据场景选择正确的用户选择方式:
|
|
70
|
+
|
|
71
|
+
| 场景 | 使用组件 | 数据加载方式 | 典型数量 |
|
|
72
|
+
| ------------------ | ---------------- | ------------ | -------- |
|
|
73
|
+
| 表单选择用户 | UserSelect | 搜索驱动 | 1-10 |
|
|
74
|
+
| 用户管理后台 | Table + 后端分页 | 分页加载 | 100+ |
|
|
75
|
+
| 问卷提交记录列表 | Table + 后端分页 | 分页加载 | 100+ |
|
|
76
|
+
| 抽奖参与者名单展示 | Table + 后端分页 | 分页加载 | 100+ |
|
|
77
|
+
|
|
78
|
+
- **UserSelect 组件**:搜索驱动,返回完整 IUserProfile 对象(含 email、phone_number)。详细用法参考 **client-builtins-user-service**
|
|
79
|
+
- **Table + 后端分页**:详细实现参考 **table-skill**
|
|
80
|
+
|
|
81
|
+
### 分页实现
|
|
82
|
+
|
|
83
|
+
用户列表必须使用后端分页,禁止前端全量拉取。具体实现参考 **table-skill**。
|
|
84
|
+
|
|
85
|
+
### 动态创建用户记录
|
|
86
|
+
|
|
87
|
+
**何时动态创建:**
|
|
88
|
+
|
|
89
|
+
1. **首次提交场景**:用户首次提交问卷/参与抽奖时,使用 `upsert` 方法
|
|
90
|
+
2. **首次登录场景**:用户首次登录系统时,创建用户记录
|
|
91
|
+
|
|
92
|
+
## 常见错误速查表
|
|
93
|
+
|
|
94
|
+
| 错误模式 | 问题 | 正确做法 | 影响 |
|
|
95
|
+
| ----------------------- | ---------------------- | ------------------------------------------------------------ | ----------- |
|
|
96
|
+
| 批量 INSERT users | Agent 无法获取全量用户 | 使用 UserSelect 组件 | 🔴 CRITICAL |
|
|
97
|
+
| `UNIQUE(user_id)` | 企业不认识 user_id | `UNIQUE(email)` | 🔴 CRITICAL |
|
|
98
|
+
| 编造 user_id | Mock 数据无效 | 使用测试用户列表 | 🔴 CRITICAL |
|
|
99
|
+
| 全量查询用户列表 | 接口超时或 OOM | 使用后端分页(skip/take) | 🔴 CRITICAL |
|
|
100
|
+
| 前端分页(全量拉取) | 首次加载超时 | 使用后端分页 | 🔴 CRITICAL |
|
|
101
|
+
| 只存 user_id 不存 email | 无法做业务层防重 | 存储 user_profile(已含 email),并添加 email 列用于唯一约束 | 🟠 BUG |
|
|
102
|
+
| 直接显示 user_id | 用户体验差 | 使用 UserDisplay 组件 | 🟡 UX |
|
|
103
|
+
| 创建 users 基础表 | 无法同步飞书数据 | 使用 user_profile 类型 | 🟠 DESIGN |
|
|
104
|
+
|
|
105
|
+
## 相关技能参考
|
|
106
|
+
|
|
107
|
+
- **client-builtins-user-service**: UserSelect 详细用法、IUserProfile 类型定义、UserDisplay 组件、useCurrentUserProfile hook
|
|
108
|
+
- **sql**: user_profile 类型约束、JSONB 验证规则、测试用户列表
|
|
109
|
+
- **table-skill**: Table 组件使用、后端分页实现、前端分页最佳实践
|
|
110
|
+
- **authz-guide**: 结合基于角色的访问控制(如:只允许用户查看/编辑自己的提交)
|
|
111
|
+
|
|
112
|
+
## 自查清单
|
|
113
|
+
|
|
114
|
+
### 表设计
|
|
115
|
+
|
|
116
|
+
- [ ] 判断是否需要创建用户表(参考决策树)
|
|
117
|
+
- [ ] 人员实体表使用 `user_profile PRIMARY KEY`
|
|
118
|
+
- [ ] 业务表使用 `id UUID PRIMARY KEY` + `user_profile` 字段
|
|
119
|
+
- [ ] 业务表包含 `email VARCHAR(255)` or `phone VARCHAR(255)` 字段
|
|
120
|
+
- [ ] 设置了正确的唯一约束(使用 email or 手机号,如 `UNIQUE(resource_id, email)`)
|
|
121
|
+
|
|
122
|
+
### 前端实现
|
|
123
|
+
|
|
124
|
+
- [ ] 使用 UserSelect 组件而非手动输入
|
|
125
|
+
- [ ] 使用 UserDisplay 展示用户(不直接显示 user_id)
|
|
126
|
+
- [ ] 用户列表使用后端分页(不全量拉取)
|
|
127
|
+
- [ ] Table 组件的 onChange 触发后端请求
|
|
128
|
+
|
|
129
|
+
### 后端实现
|
|
130
|
+
|
|
131
|
+
- [ ] Entity 定义了正确的唯一约束(`@Unique(['resourceId', 'email'])` or `@Unique(['resourceId', 'phone'])`)
|
|
132
|
+
- [ ] 错误处理包含唯一约束冲突(409 Conflict)
|
|
133
|
+
- [ ] 存储 user_profile 字段(已包含 email/phone)
|
|
134
|
+
- [ ] 在表中单独添加 email 或 phone 列,用于唯一约束和索引查询
|
|
135
|
+
- [ ] 列表接口使用 skip/take 分页查询
|
|
136
|
+
- [ ] 返回 pagination 对象(page, pageSize, total)
|
|
137
|
+
|
|
138
|
+
### Mock 数据
|
|
139
|
+
|
|
140
|
+
- [ ] user_id 使用测试用户列表(不编造)
|
|
141
|
+
- [ ] user_profile 中的 email/phone 与 user_id 对应正确
|
|
142
|
+
- [ ] 测试了重复提交场景(唯一约束生效)
|