@lark-apaas/coding-steering 0.1.16 → 0.1.17-beta.0
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/package.json +2 -2
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +61 -375
- package/steering/nestjs-react-fullstack/{skills → skills_common}/user-identity/SKILL.md +12 -5
- package/steering/nestjs-react-fullstack/skills_local/authz-guide/SKILL.md +196 -0
- package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/dynamic-permission-guide.md +643 -0
- package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/management-page-spec.md +505 -0
- package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/runtime-role-controller-spec.md +203 -0
- package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/sdk-examples.md +92 -0
- package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/sdk-types.md +229 -0
- package/steering/nestjs-react-fullstack/skills_local/client-builtins-user-service/SKILL.md +240 -0
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/coding-steering",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17-beta.0",
|
|
4
4
|
"description": "Stack-specific steering content for miaoda-coding templates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"steering"
|
|
8
8
|
],
|
|
9
9
|
"scripts": {
|
|
10
|
-
"lint:md": "markdownlint 'steering/**/*.md' --ignore 'steering/**/skills/**'"
|
|
10
|
+
"lint:md": "markdownlint 'steering/**/*.md' --ignore 'steering/**/skills/**' --ignore 'steering/**/skills_common/**' --ignore 'steering/**/skills_local/**'"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"markdownlint-cli": "^0.47.0"
|
|
@@ -13,27 +13,20 @@ match-template-name: nestjs-react-fullstack
|
|
|
13
13
|
项目通过 `authClient`(来自 `@lark-apaas/client-toolkit/auth`)提供用户信息与鉴权服务,用于用户登录、登出、获取用户信息等身份认证相关功能。
|
|
14
14
|
|
|
15
15
|
> **边界说明**:`authClient.session` 仅用于用户登录/登出/获取用户信息等鉴权操作。**插件调用(capability)不属于账户 SDK**,须使用独立的 `capabilityClient`(参见 plugin-guide)。
|
|
16
|
+
>
|
|
17
|
+
> **运行时边界**:本 skill 所有能力(`authClient`、`useCurrentUserProfile`、UserSelect/UserDisplay 等)仅限前端代码使用,**严禁在 `server/**` 中 import**。服务端获取用户身份用 `req.userContext` / `AuthNPaasService`(见 `user-identity` skill),完整边界规则见 coding-guide。
|
|
18
|
+
|
|
19
|
+
## 怎么选(决策指引)
|
|
20
|
+
|
|
21
|
+
- **React 组件内展示当前用户**(名称/头像/邮箱/飞书 ID)→ 用 `useCurrentUserProfile()`(见下文),不要手动调 `getUserInfo`
|
|
22
|
+
- **登录/登出/跳转用户详情页,或非 React 上下文取用户信息** → `authClient.session.*`(本节)
|
|
23
|
+
- **选人/选部门/展示任意用户** → UserSelect / DepartmentSelect / UserDisplay 组件(见下文)
|
|
16
24
|
|
|
17
25
|
## 统一响应结构
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
所有 `authClient.session.*` 接口返回统一的 `DataloomServiceResponse<T>` 结构。在非浏览器环境调用会返回失败结构(`status: 400`,`error.message: 'Incompatible runtime environment'`)。
|
|
20
28
|
|
|
21
29
|
```typescript
|
|
22
|
-
/**
|
|
23
|
-
* 统一结果返回结构
|
|
24
|
-
* 在非浏览器环境调用时返回示例:
|
|
25
|
-
* {
|
|
26
|
-
* data: null,
|
|
27
|
-
* error: {
|
|
28
|
-
* code: 400,
|
|
29
|
-
* message: 'Incompatible runtime environment',
|
|
30
|
-
* hint: 'Please check if the current environment is browser.',
|
|
31
|
-
* details: 'This method can only be invoked in browser environment.',
|
|
32
|
-
* },
|
|
33
|
-
* status: 400,
|
|
34
|
-
* statusText: 'Bad Request',
|
|
35
|
-
* }
|
|
36
|
-
*/
|
|
37
30
|
interface DataloomServiceBase {
|
|
38
31
|
status: number;
|
|
39
32
|
statusText: string;
|
|
@@ -64,202 +57,32 @@ import { authClient } from "@lark-apaas/client-toolkit/auth";
|
|
|
64
57
|
// authClient 是 SDK 内置的 singleton,零参可用,不需要异步初始化
|
|
65
58
|
```
|
|
66
59
|
|
|
67
|
-
##
|
|
68
|
-
|
|
69
|
-
#### 1. 登录跳转接口 (redirectToLogin)
|
|
70
|
-
|
|
71
|
-
##### 用途
|
|
60
|
+
## 接口速查表
|
|
72
61
|
|
|
73
|
-
|
|
62
|
+
| 方法 | 入参 | 成功时 data 类型 | 说明 |
|
|
63
|
+
| ----------------------------------------- | ------------------------------ | -------------------------- | ---------------------------------------------- |
|
|
64
|
+
| `session.redirectToLogin(options?)` | `SignInRedirectionOptions` | `'success'` | 跳转至 Dataloom 登录页(身份认证/单点登录) |
|
|
65
|
+
| `session.signOut()` | 无 | `null`(异步) | 退出登录,删除 cookie 中的登录态 |
|
|
66
|
+
| `session.navigateToUserProfile(options?)` | `NavigateToUserProfileOptions` | `'success'` | 跳转至当前登录用户详情页(头像/姓名点击进入) |
|
|
67
|
+
| `session.getUserInfo()` | 无 | `UserInfoResponse`(异步) | 获取当前登录用户信息,未登录返回 `status: 401` |
|
|
74
68
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
需要用户进行身份认证的场景
|
|
78
|
-
|
|
79
|
-
##### 入参说明
|
|
69
|
+
### 入参类型定义
|
|
80
70
|
|
|
81
71
|
```typescript
|
|
82
72
|
interface SignInRedirectionOptions {
|
|
83
|
-
/**
|
|
84
|
-
* 选填,登录成功后跳转回的页面。省略会默认用调用接口时页面的url。
|
|
85
|
-
*/
|
|
73
|
+
/** 选填,登录成功后跳转回的页面。省略默认用当前页面 URL */
|
|
86
74
|
returnUrl?: string;
|
|
87
|
-
/**
|
|
88
|
-
* 选填,是否在新浏览器tab上打开登录页。默认为false
|
|
89
|
-
*/
|
|
75
|
+
/** 选填,是否在新浏览器 tab 打开登录页。默认 false */
|
|
90
76
|
newTab?: boolean;
|
|
91
77
|
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
| 属性名 | 类型 | 必填 | 默认值 | 说明 |
|
|
95
|
-
| ----------- | --------- | ---- | ----------- | ------------------------------ |
|
|
96
|
-
| `returnUrl` | `string` | ❌ | 当前页面URL | 登录成功后跳转回的页面 |
|
|
97
|
-
| `newTab` | `boolean` | ❌ | `false` | 是否在新浏览器标签页打开登录页 |
|
|
98
|
-
|
|
99
|
-
##### 出参说明
|
|
100
|
-
|
|
101
|
-
| 字段名 | 类型 | 说明 |
|
|
102
|
-
| ------------ | ----------- | ---------------------- |
|
|
103
|
-
| `data` | `'success'` | 成功标识 |
|
|
104
|
-
| `error` | `null` | 错误信息,成功时为null |
|
|
105
|
-
| `status` | `200` | HTTP状态码 |
|
|
106
|
-
| `statusText` | `'OK'` | HTTP状态文本 |
|
|
107
|
-
|
|
108
|
-
##### 使用示例
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
/**
|
|
112
|
-
* 跳转至dataloom登录页
|
|
113
|
-
* @param {string} brand - 品牌id 例如:妙搭为 1.
|
|
114
|
-
* @param {string} appId - 运行态应用的id,由dataloom authn 服务下发。
|
|
115
|
-
* @return 成功返回示例:
|
|
116
|
-
* {
|
|
117
|
-
* data: 'success',
|
|
118
|
-
* error: null,
|
|
119
|
-
* status: 200,
|
|
120
|
-
* statusText: 'OK',
|
|
121
|
-
* }
|
|
122
|
-
*/
|
|
123
|
-
const res: DataloomServiceResponse<'success'> = authClient
|
|
124
|
-
.session
|
|
125
|
-
.redirectToLogin(options: SignInRedirectionOptions);
|
|
126
|
-
|
|
127
|
-
// 基本使用
|
|
128
|
-
const loginResult = authClient
|
|
129
|
-
.session
|
|
130
|
-
.redirectToLogin();
|
|
131
|
-
|
|
132
|
-
// 带参数使用
|
|
133
|
-
const loginResult = authClient
|
|
134
|
-
.session
|
|
135
|
-
.redirectToLogin({
|
|
136
|
-
returnUrl: 'https://example.com/dashboard',
|
|
137
|
-
newTab: true
|
|
138
|
-
});
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
#### 2. 退出登录接口 (signOut)
|
|
142
|
-
|
|
143
|
-
##### 用途
|
|
144
|
-
|
|
145
|
-
退出登录,删除cookie中的登录态,适用于:用户主动登出、会话清理、安全退出
|
|
146
|
-
|
|
147
|
-
##### 适用场景
|
|
148
|
-
|
|
149
|
-
需要清除用户登录状态的场景
|
|
150
|
-
|
|
151
|
-
##### 入参说明
|
|
152
|
-
|
|
153
|
-
无需传入参数
|
|
154
|
-
|
|
155
|
-
##### 出参说明
|
|
156
|
-
|
|
157
|
-
| 字段名 | 类型 | 说明 |
|
|
158
|
-
| ------------ | ------ | ---------------------- |
|
|
159
|
-
| `data` | `null` | 数据为空 |
|
|
160
|
-
| `error` | `null` | 错误信息,成功时为null |
|
|
161
|
-
| `status` | `200` | HTTP状态码 |
|
|
162
|
-
| `statusText` | `'OK'` | HTTP状态文本 |
|
|
163
|
-
|
|
164
|
-
##### 使用示例
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
/**
|
|
168
|
-
* 退出登录,删除cookie中的登陆态
|
|
169
|
-
* @return 成功返回示例:
|
|
170
|
-
* {
|
|
171
|
-
* data: null,
|
|
172
|
-
* error: null,
|
|
173
|
-
* status: 200,
|
|
174
|
-
* statusText: 'OK',
|
|
175
|
-
* }
|
|
176
|
-
*/
|
|
177
|
-
const res: Promise<DataloomServiceResponse<null>> = await authClient.session.signOut();
|
|
178
|
-
|
|
179
|
-
// 使用示例
|
|
180
|
-
import { logger } from "@lark-apaas/client-toolkit/logger";
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
const result = await authClient.session.signOut();
|
|
184
|
-
|
|
185
|
-
if (result.error) {
|
|
186
|
-
logger.error("退出登录失败:", result.error.message);
|
|
187
|
-
} else {
|
|
188
|
-
logger.info("退出登录成功");
|
|
189
|
-
// 跳转到登录页或首页
|
|
190
|
-
authClient.session.redirectToLogin();
|
|
191
|
-
}
|
|
192
|
-
} catch (error) {
|
|
193
|
-
logger.error("退出登录异常:", error);
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
#### 3. 跳转用户详情页接口 (navigateToUserProfile)
|
|
198
|
-
|
|
199
|
-
##### 用途
|
|
200
|
-
|
|
201
|
-
跳转至当前登录用户的详情页,适用于:查看个人资料、从用户头像/姓名点击进入详情页面
|
|
202
|
-
|
|
203
|
-
##### 适用场景
|
|
204
|
-
|
|
205
|
-
需要在应用中快速打开用户详情页面的场景
|
|
206
78
|
|
|
207
|
-
##### 入参说明
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
79
|
interface NavigateToUserProfileOptions {
|
|
211
|
-
/**
|
|
212
|
-
* 选填,是否在新浏览器tab上打开详情页。默认为false
|
|
213
|
-
*/
|
|
80
|
+
/** 选填,是否在新浏览器 tab 打开详情页。默认 false */
|
|
214
81
|
newTab?: boolean;
|
|
215
82
|
}
|
|
216
83
|
```
|
|
217
84
|
|
|
218
|
-
|
|
219
|
-
| -------- | --------- | ---- | ------- | ------------------------------ |
|
|
220
|
-
| `newTab` | `boolean` | ❌ | `false` | 是否在新浏览器标签页打开详情页 |
|
|
221
|
-
|
|
222
|
-
##### 出参说明
|
|
223
|
-
|
|
224
|
-
| 字段名 | 类型 | 说明 |
|
|
225
|
-
| ------------ | ----------- | ---------------------- |
|
|
226
|
-
| `data` | `'success'` | 成功标识 |
|
|
227
|
-
| `error` | `null` | 错误信息,成功时为null |
|
|
228
|
-
| `status` | `200` | HTTP状态码 |
|
|
229
|
-
| `statusText` | `'OK'` | HTTP状态文本 |
|
|
230
|
-
|
|
231
|
-
##### 使用示例
|
|
232
|
-
|
|
233
|
-
```typescript
|
|
234
|
-
/**
|
|
235
|
-
* 跳转至用户详情页
|
|
236
|
-
* @return 成功返回示例:
|
|
237
|
-
* {
|
|
238
|
-
* data: 'success',
|
|
239
|
-
* error: null,
|
|
240
|
-
* status: 200,
|
|
241
|
-
* statusText: 'OK',
|
|
242
|
-
* }
|
|
243
|
-
*/
|
|
244
|
-
const res: DataloomServiceResponse<"success"> = authClient.session.navigateToUserProfile();
|
|
245
|
-
|
|
246
|
-
// 在新标签页打开
|
|
247
|
-
const resNewTab = authClient.session.navigateToUserProfile({ newTab: true });
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
## 用户信息服务
|
|
251
|
-
|
|
252
|
-
#### 4. 获取用户信息接口 (getUserInfo)
|
|
253
|
-
|
|
254
|
-
##### 用途
|
|
255
|
-
|
|
256
|
-
根据当前登录态获取已登录的用户信息,适用于:用户资料展示、权限判断、个性化配置
|
|
257
|
-
|
|
258
|
-
##### 适用场景
|
|
259
|
-
|
|
260
|
-
需要获取当前登录用户详细信息的场景
|
|
261
|
-
|
|
262
|
-
##### 数据类型定义
|
|
85
|
+
### getUserInfo 返回类型定义
|
|
263
86
|
|
|
264
87
|
```typescript
|
|
265
88
|
interface I18n {
|
|
@@ -291,75 +114,39 @@ interface UserInfoResponse {
|
|
|
291
114
|
}
|
|
292
115
|
```
|
|
293
116
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
无需传入参数
|
|
297
|
-
|
|
298
|
-
##### 出参说明
|
|
299
|
-
|
|
300
|
-
| 字段名 | 类型 | 说明 |
|
|
301
|
-
| ----------------------------- | -------------- | ---------------------- |
|
|
302
|
-
| `data.user_info` | `UserBaseInfo` | 用户基本信息对象 |
|
|
303
|
-
| `data.user_info.user_id` | `number` | 用户唯一标识符 |
|
|
304
|
-
| `data.user_info.name` | `I18ns` | 用户名称(支持多语言) |
|
|
305
|
-
| `data.user_info.avatar` | `Avatar` | 用户头像信息 |
|
|
306
|
-
| `data.user_info.email` | `string` | 用户邮箱地址 |
|
|
307
|
-
| `data.user_info.phone_number` | `string` | 用户手机号码 |
|
|
308
|
-
| `data.user_info.tenant_name` | `string` | 租户名称 |
|
|
309
|
-
| `error` | `null` | 错误信息,成功时为null |
|
|
310
|
-
|
|
311
|
-
##### 使用示例
|
|
117
|
+
### 综合示例
|
|
312
118
|
|
|
313
119
|
```typescript
|
|
314
|
-
|
|
315
|
-
* 根据当前登陆态获取已登录的用户信息。
|
|
316
|
-
* @param {string} brand - 品牌id 例如:妙搭为 1.
|
|
317
|
-
* @param {string} appId - 运行态应用的id,由dataloom authn 服务下发。
|
|
318
|
-
* @return
|
|
319
|
-
*/
|
|
320
|
-
const res: Promise<DataloomServiceResponse<UserInfoResponse>> = await authClient.session.getUserInfo();
|
|
321
|
-
|
|
322
|
-
// 使用示例
|
|
120
|
+
import { authClient } from "@lark-apaas/client-toolkit/auth";
|
|
323
121
|
import { logger } from "@lark-apaas/client-toolkit/logger";
|
|
324
122
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
// 跳转到登录页
|
|
333
|
-
authClient.session.redirectToLogin();
|
|
334
|
-
}
|
|
335
|
-
} else if (result.data?.user_info) {
|
|
336
|
-
const userInfo = result.data.user_info;
|
|
337
|
-
logger.info("用户信息:", userInfo);
|
|
338
|
-
|
|
339
|
-
// 显示用户名
|
|
340
|
-
const userName = userInfo.name?.[0]?.text || "未知用户";
|
|
341
|
-
document.getElementById("username").textContent = userName;
|
|
342
|
-
|
|
343
|
-
// 显示用户头像
|
|
344
|
-
const avatarUrl = userInfo.avatar?.image?.large;
|
|
345
|
-
if (avatarUrl) {
|
|
346
|
-
document.getElementById("avatar").src = avatarUrl;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// 显示用户邮箱
|
|
350
|
-
if (userInfo.email) {
|
|
351
|
-
document.getElementById("email").textContent = userInfo.email;
|
|
352
|
-
}
|
|
123
|
+
// 获取用户信息(React 组件内展示当前用户请优先用 useCurrentUserProfile,见下文)
|
|
124
|
+
const result = await authClient.session.getUserInfo();
|
|
125
|
+
if (result.error) {
|
|
126
|
+
logger.error("获取用户信息失败:", result.error.message);
|
|
127
|
+
if (result.status === 401) {
|
|
128
|
+
// 未登录:跳转登录页(可传 returnUrl / newTab,默认回到当前页)
|
|
129
|
+
authClient.session.redirectToLogin();
|
|
353
130
|
}
|
|
354
|
-
}
|
|
355
|
-
|
|
131
|
+
} else if (result.data?.user_info) {
|
|
132
|
+
const info = result.data.user_info;
|
|
133
|
+
const userName = info.name?.[0]?.text || "未知用户"; // 名称是多语言数组
|
|
134
|
+
const avatarUrl = info.avatar?.image?.large; // 头像 URL 可能不返回
|
|
135
|
+
// React 中通过 state 渲染上述字段,禁止 document.getElementById 等直接 DOM 操作
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 退出登录后回到登录页
|
|
139
|
+
const signOutResult = await authClient.session.signOut();
|
|
140
|
+
if (!signOutResult.error) {
|
|
141
|
+
authClient.session.redirectToLogin();
|
|
356
142
|
}
|
|
143
|
+
|
|
144
|
+
// 跳转当前用户详情页(新标签页打开)
|
|
145
|
+
authClient.session.navigateToUserProfile({ newTab: true });
|
|
357
146
|
```
|
|
358
147
|
|
|
359
148
|
## 错误处理
|
|
360
149
|
|
|
361
|
-
### 常见错误类型
|
|
362
|
-
|
|
363
150
|
| 错误码 | 说明 | 处理建议 |
|
|
364
151
|
| ------ | -------------- | ---------------------- |
|
|
365
152
|
| `400` | 请求参数错误 | 检查传入参数是否正确 |
|
|
@@ -368,60 +155,19 @@ try {
|
|
|
368
155
|
| `404` | 资源不存在 | 检查请求的资源是否存在 |
|
|
369
156
|
| `500` | 服务器内部错误 | 稍后重试或联系技术支持 |
|
|
370
157
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
```typescript
|
|
374
|
-
import { toast } from "sonner";
|
|
375
|
-
|
|
376
|
-
// 统一错误处理函数
|
|
377
|
-
function handleDataloomError(response: DataloomServiceResponse<any>) {
|
|
378
|
-
if (response.error) {
|
|
379
|
-
switch (response.status) {
|
|
380
|
-
case 401:
|
|
381
|
-
// 未授权,跳转登录
|
|
382
|
-
authClient.session.redirectToLogin();
|
|
383
|
-
break;
|
|
384
|
-
case 403:
|
|
385
|
-
// 权限不足
|
|
386
|
-
toast.error("权限不足,请联系管理员");
|
|
387
|
-
break;
|
|
388
|
-
case 500:
|
|
389
|
-
// 服务器错误
|
|
390
|
-
toast.error("服务器错误,请稍后重试");
|
|
391
|
-
break;
|
|
392
|
-
default:
|
|
393
|
-
toast.error(`操作失败: ${response.error.message}`);
|
|
394
|
-
}
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
return true;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// 使用示例
|
|
401
|
-
import { logger } from "@lark-apaas/client-toolkit/logger";
|
|
402
|
-
|
|
403
|
-
const userInfoResult = await authClient.session.getUserInfo();
|
|
404
|
-
if (handleDataloomError(userInfoResult)) {
|
|
405
|
-
// 处理成功逻辑
|
|
406
|
-
logger.info("用户信息:", userInfoResult.data);
|
|
407
|
-
}
|
|
408
|
-
```
|
|
158
|
+
统一处理建议:`401` 调用 `authClient.session.redirectToLogin()`(见综合示例);`403`/`500` 用 toast(如 `sonner`)提示用户;其余情况展示 `error.message`。
|
|
409
159
|
|
|
410
160
|
## 注意事项
|
|
411
161
|
|
|
412
|
-
1.
|
|
413
|
-
2.
|
|
414
|
-
3.
|
|
415
|
-
4. **安全性**:不要在客户端代码中暴露敏感的配置信息
|
|
416
|
-
5. **错误处理**:建议对所有接口调用进行统一的错误处理
|
|
162
|
+
1. **环境限制**:本 skill 涉及的 `@lark-apaas/client-toolkit/**` 能力只能在前端/浏览器环境中使用,服务端调用会返回环境不兼容错误;更重要的是,服务端代码中禁止 import 这些前端 SDK
|
|
163
|
+
2. **跨域配置**:确保应用域名已在 Dataloom 后台配置白名单
|
|
164
|
+
3. **安全性**:不要在客户端代码中暴露敏感的配置信息
|
|
417
165
|
|
|
418
166
|
# 用户系统前端相关规范
|
|
419
167
|
|
|
420
168
|
## 概述
|
|
421
169
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
同时提供 DepartmentSelect(部门选择组件),用于部门字段的单选/多选选择,交互与受控规范与 UserSelect 保持一致。
|
|
170
|
+
内置用户前端组件:UserSelect(单选/多选用户选择器,onChange 返回用户对象)、DepartmentSelect(部门选择组件,交互与受控规范与 UserSelect 一致)、UserDisplay(用户信息展示组件)。均基于统一的 userid 数据,用于用户相关的表单输入和数据展示场景。
|
|
425
171
|
|
|
426
172
|
## 类型定义
|
|
427
173
|
|
|
@@ -448,17 +194,8 @@ export type User = {
|
|
|
448
194
|
|
|
449
195
|
## 当前用户信息的获取方案
|
|
450
196
|
|
|
451
|
-
### 常用场景
|
|
452
|
-
|
|
453
|
-
- **用户信息展示**:在界面中显示当前用户名称和头像
|
|
454
|
-
- **权限验证**:基于用户ID进行权限检查和控制
|
|
455
|
-
- **数据关联**:在数据操作时关联当前用户信息
|
|
456
|
-
- **日志记录**:记录用户操作日志时获取用户标识
|
|
457
|
-
|
|
458
197
|
### Hooks 方法: `useCurrentUserProfile` - 在 React 中获取当前用户信息
|
|
459
198
|
|
|
460
|
-
### 基本信息
|
|
461
|
-
|
|
462
199
|
- **文件路径**:`@lark-apaas/client-toolkit/hooks/useCurrentUserProfile`
|
|
463
200
|
- **功能**:获取当前登录用户的个人信息(含飞书 user_id)
|
|
464
201
|
- **返回值**:`Partial<IUserProfile>`(初始为空对象 `{}`,异步获取后填充完整字段)
|
|
@@ -473,29 +210,18 @@ export type User = {
|
|
|
473
210
|
| `avatar` | `string` | 用户头像 URL |
|
|
474
211
|
| `lark_user_id` | `string` | 飞书 user_id,通过额外异步请求获取,可能晚于其他字段就绪 |
|
|
475
212
|
|
|
476
|
-
> ⚠️ **空值处理(CRITICAL)**:Hook 初始返回空对象 `{}`(truthy
|
|
477
|
-
|
|
478
|
-
### 使用方法
|
|
213
|
+
> ⚠️ **空值处理(CRITICAL)**:Hook 初始返回空对象 `{}`(truthy),**MUST** 用 `if (!userInfo?.user_id)` 判加载态(不是 `!userInfo`);`lark_user_id` 可能为 `undefined`,使用前条件渲染。完整禁止行为清单与飞书 ID 转换指南(后端 `AuthNPaasService` 等)参见 `user-identity` skill。
|
|
479
214
|
|
|
480
215
|
```typescript
|
|
481
216
|
import { useCurrentUserProfile } from "@lark-apaas/client-toolkit/hooks/useCurrentUserProfile";
|
|
482
217
|
|
|
483
218
|
const MyComponent = () => {
|
|
484
219
|
const userInfo = useCurrentUserProfile();
|
|
485
|
-
|
|
486
|
-
// 正确:安全访问 + 加载态处理
|
|
487
220
|
if (!userInfo?.user_id) return <div>加载中...</div>;
|
|
488
|
-
return
|
|
489
|
-
<div>
|
|
490
|
-
<p>{userInfo.name}</p>
|
|
491
|
-
{userInfo.lark_user_id && <p>飞书 ID: {userInfo.lark_user_id}</p>}
|
|
492
|
-
</div>
|
|
493
|
-
);
|
|
221
|
+
return <p>{userInfo.name}</p>;
|
|
494
222
|
};
|
|
495
223
|
```
|
|
496
224
|
|
|
497
|
-
> **飞书 ID 转换详细指南**(后端 `AuthNPaasService` 用法、自定义转换接口等)参见 `user-identity` skill。
|
|
498
|
-
|
|
499
225
|
## 用户展示与选择方案
|
|
500
226
|
|
|
501
227
|
{% if projectMeta['flags']['supportBusinessUser'] %}
|
|
@@ -509,7 +235,7 @@ const MyComponent = () => {
|
|
|
509
235
|
|
|
510
236
|
{% else %}
|
|
511
237
|
|
|
512
|
-
##
|
|
238
|
+
## UserSelect - 用户选择组件
|
|
513
239
|
|
|
514
240
|
### 基本信息
|
|
515
241
|
|
|
@@ -529,14 +255,9 @@ interface UserSelectProps {
|
|
|
529
255
|
}
|
|
530
256
|
```
|
|
531
257
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
- **单选模式** (`mode="single"`):值为 userid,返回为`IUserProfile` 对象
|
|
535
|
-
- **多选模式** (`mode="multiple"`):值为 userid数组,返回为`IUserProfile` 数组
|
|
258
|
+
值类型:单选模式(`mode="single"`)值为 userid、onChange 返回 `IUserProfile` 对象;多选模式(`mode="multiple"`)值为 userid 数组、onChange 返回 `IUserProfile` 数组。
|
|
536
259
|
|
|
537
|
-
###
|
|
538
|
-
|
|
539
|
-
#### 表单集成
|
|
260
|
+
### 使用示例(表单集成)
|
|
540
261
|
|
|
541
262
|
```typescript
|
|
542
263
|
import { useForm } from "react-hook-form";
|
|
@@ -545,10 +266,8 @@ import * as z from "zod";
|
|
|
545
266
|
import { Form, FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form";
|
|
546
267
|
import { UserSelect } from "@lark-apaas/client-toolkit/components/User";
|
|
547
268
|
|
|
548
|
-
// 定义表单验证schema
|
|
549
269
|
const formSchema = z.object({
|
|
550
270
|
assignee: z.string(),
|
|
551
|
-
participants: z.array(z.string()).optional(),
|
|
552
271
|
});
|
|
553
272
|
|
|
554
273
|
const form = useForm<z.infer<typeof formSchema>>({
|
|
@@ -574,24 +293,8 @@ const form = useForm<z.infer<typeof formSchema>>({
|
|
|
574
293
|
</FormItem>
|
|
575
294
|
)}
|
|
576
295
|
/>
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
control={form.control}
|
|
580
|
-
name="participants"
|
|
581
|
-
render={({ field }) => (
|
|
582
|
-
<FormItem>
|
|
583
|
-
<FormLabel>参与人(多选)</FormLabel>
|
|
584
|
-
<FormControl>
|
|
585
|
-
<UserSelect
|
|
586
|
-
mode="multiple"
|
|
587
|
-
placeholder="选择参与人员"
|
|
588
|
-
value={field.value}
|
|
589
|
-
onChange={(users) => field.onChange(users.map(user => user.user_id))}
|
|
590
|
-
/>
|
|
591
|
-
</FormControl>
|
|
592
|
-
</FormItem>
|
|
593
|
-
)}
|
|
594
|
-
/>
|
|
296
|
+
{/* 多选字段差异仅:schema 用 z.array(z.string()),mode="multiple",
|
|
297
|
+
onChange={(users) => field.onChange(users.map((u) => u.user_id))} */}
|
|
595
298
|
</form>
|
|
596
299
|
</Form>
|
|
597
300
|
```
|
|
@@ -604,41 +307,24 @@ const form = useForm<z.infer<typeof formSchema>>({
|
|
|
604
307
|
- **功能**:用于所有用户信息的展示场景
|
|
605
308
|
- **特性**:显示用户**头像**和**姓名**,支持多用户展示
|
|
606
309
|
|
|
607
|
-
IMPORTANT:当不传递showLabel时,组件会同时展示用户头像和姓名,如果只需要头像,则需要传递showLabel的值为false
|
|
608
|
-
|
|
609
310
|
### 属性定义
|
|
610
311
|
|
|
611
312
|
```typescript
|
|
612
313
|
interface UserDisplayProps {
|
|
613
|
-
users: string[]; // 用户id
|
|
314
|
+
users: string[]; // 用户id数组(必需),单个用户传 [userId]
|
|
614
315
|
size?: "small" | "medium" | "large"; // 头像尺寸
|
|
615
316
|
className?: string; // 自定义样式类名
|
|
616
|
-
showLabel?: boolean; // 默认为true
|
|
317
|
+
showLabel?: boolean; // 默认为true,同时展示头像和姓名;只需要头像时必须显式传 false
|
|
617
318
|
}
|
|
618
319
|
```
|
|
619
320
|
|
|
620
321
|
### 使用示例
|
|
621
322
|
|
|
622
|
-
#### 基础用法
|
|
623
|
-
|
|
624
323
|
```jsx
|
|
625
324
|
import { UserDisplay } from "@lark-apaas/client-toolkit/components/User";
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
// 单个用户展示
|
|
631
|
-
<UserDisplay
|
|
632
|
-
users={[employeesId]}
|
|
633
|
-
size="small"
|
|
634
|
-
/>
|
|
635
|
-
|
|
636
|
-
// 多用户展示
|
|
637
|
-
<UserDisplay
|
|
638
|
-
users={project.participants}
|
|
639
|
-
size="medium"
|
|
640
|
-
className="project-members"
|
|
641
|
-
/>
|
|
325
|
+
|
|
326
|
+
// 单个或多个用户展示:users 传用户 id 数组
|
|
327
|
+
<UserDisplay users={project.participants} size="medium" className="project-members" />
|
|
642
328
|
```
|
|
643
329
|
|
|
644
330
|
## 使用注意事项
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: user-identity
|
|
3
|
-
description: "Use when getting current user info/profile, displaying user name/avatar/email, converting miaoda userId
|
|
3
|
+
description: "Use when getting current user info/profile, displaying user name/avatar/email, converting miaoda userId ↔ lark_user_id (both directions via AuthNPaasService), or reading req.userContext fields (userId/roles/tenantId): useCurrentUserProfile, AuthNPaasService, FeishuID conversion. 触发词:用户身份, 用户信息, 用户资料, 当前用户, userProfile, useCurrentUserProfile, 飞书ID, FeishuID, 飞书用户ID, lark_user_id, 用户ID转换, AuthNPaasService, getBatchMiaodaUserIds, 飞书ID转妙搭, employee_id 转 userId, 用户上下文, userContext, userContext.roles, 用户角色, 当前用户角色, 获取请求者角色, 展示用户, 显示用户, 用户面板, 我是谁, 获取用户"
|
|
4
4
|
steering: true
|
|
5
5
|
steering-topic: user_identity
|
|
6
6
|
match-template-name: nestjs-react-fullstack
|
|
@@ -77,7 +77,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
77
77
|
│ └─ 跳到 `feishu` skill 的 `references/id-convert.md`(spark id_convert type 20/21)
|
|
78
78
|
│
|
|
79
79
|
├─ 需要把 飞书 user_id(employee_id)反查成 妙搭 userId?
|
|
80
|
-
│ └─
|
|
80
|
+
│ └─ 后端 ──→ `AuthNPaasService.getBatchMiaodaUserIds()`(第三节,SDK 一步,convertType 31);非模板项目 ──→ `feishu` skill 两步兜底
|
|
81
81
|
│
|
|
82
82
|
└─ 需要自定义飞书 ID 转换接口?
|
|
83
83
|
└─ 是 ──→ 注入 AuthNPaasService 编写 Controller(第四节,仅适用于 user_id)
|
|
@@ -102,7 +102,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
102
102
|
| `appId` | `string` | 应用 ID |
|
|
103
103
|
| `loginUrl` | `string` | 登录跳转 URL |
|
|
104
104
|
| `userType` | `string` | 用户类型(如 `_employee`) |
|
|
105
|
-
| `env` | `string` |
|
|
105
|
+
| `env` | `string` | 环境(`preview` 预览态、`runtime` 发布运行态) |
|
|
106
106
|
| `userName` | `string` | 用户名 |
|
|
107
107
|
| `userNameI18n` | `{ zh_cn, en_us, ja_jp }` | 多语言用户名 |
|
|
108
108
|
| `isSystemAccount` | `boolean` | 是否系统账号 |
|
|
@@ -146,9 +146,9 @@ export class TasksController {
|
|
|
146
146
|
|
|
147
147
|
妙搭平台的用户 ID(`userId`)与飞书用户 ID 是两套独立体系。调用飞书 OpenAPI 时需要传入某种飞书侧 ID(`open_id` / `union_id` / `user_id` 任选其一,具体通过哪个参数指定取决于 API:消息 API 用 `receive_id_type`,多数其他 API 用 `user_id_type`,文档协作者用 `member_id_type`)。
|
|
148
148
|
|
|
149
|
-
`AuthNPaasService` 暴露的飞书 ID 是 **`user_id`**(即 `employee_id
|
|
149
|
+
`AuthNPaasService` 暴露的飞书 ID 是 **`user_id`**(即 `employee_id`,飞书企业内的用户标识)这一种,支持 **妙搭 userId ↔ 飞书 user_id 双向**转换(正向 `getBatchLarkUserIds`/`getCurrentUserLarkUserId`,反向 `getBatchMiaodaUserIds`)。
|
|
150
150
|
|
|
151
|
-
> 如果需要 `open_id` / `union_id
|
|
151
|
+
> 如果需要 `open_id` / `union_id`(无论正反向),请改用飞书开放平台 `spark id_convert` 接口,参见 `feishu` skill 的 `references/id-convert.md`。`employee_id` ↔ 妙搭 userId 双向都在本 SDK 内(见下方方法表)。
|
|
152
152
|
|
|
153
153
|
### 后端 API
|
|
154
154
|
|
|
@@ -168,6 +168,10 @@ export class MyService {
|
|
|
168
168
|
// 批量转换(最多 100 个)
|
|
169
169
|
const larkUserIds = await this.authnService.getBatchLarkUserIds(['uid1', 'uid2']);
|
|
170
170
|
// => ['<飞书 user_id>', null] 顺序与输入对应,失败项为 null
|
|
171
|
+
|
|
172
|
+
// 反向:飞书 user_id(employee_id) → 妙搭 userId
|
|
173
|
+
const miaodaUserIds = await this.authnService.getBatchMiaodaUserIds(['emp1', 'emp2']);
|
|
174
|
+
// => ['<妙搭 userId>', null] 顺序与输入对应,失败项为 null
|
|
171
175
|
}
|
|
172
176
|
}
|
|
173
177
|
```
|
|
@@ -176,6 +180,7 @@ export class MyService {
|
|
|
176
180
|
|------|------|------|
|
|
177
181
|
| `getCurrentUserLarkUserId` | `() → Promise<string \| null>` | 从请求上下文获取当前用户的飞书 ID |
|
|
178
182
|
| `getBatchLarkUserIds` | `(userIds: string[]) → Promise<(string \| null)[]>` | 批量转换,最多 100 个,与输入顺序一一对应 |
|
|
183
|
+
| `getBatchMiaodaUserIds` | `(employeeIds: string[]) → Promise<(string \| null)[]>` | 反向:批量把飞书 user_id(employee_id)转为妙搭 userId,最多 100 个,顺序一一对应,失败项 `null`(底层 convertType 31) |
|
|
179
184
|
|
|
180
185
|
### 内置接口
|
|
181
186
|
|
|
@@ -249,6 +254,8 @@ export class FeishuIdController {
|
|
|
249
254
|
}
|
|
250
255
|
```
|
|
251
256
|
|
|
257
|
+
> 反向转换(employee_id → 妙搭 userId)同理,把 `getBatchLarkUserIds` 换成 `getBatchMiaodaUserIds` 即可,无需新增 Controller。
|
|
258
|
+
|
|
252
259
|
前端调用示例:
|
|
253
260
|
|
|
254
261
|
```typescript
|