e41dfa09
李曜臣
泰鄂版菜单;租户列表
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
|
# 2026-07-21 代码优化
本文档说明 **2026-07-21** 后端改动。重点为 **泰额版** 平台级管理员:
1. 「公司列表」返回账号密码、支持修改(前端密码 AES 加盐)
2. **菜单权限分配**、**角色菜单分配**
另含美国版/泰额版 Dashboard `Last Updated` 修复。前端本次不改。
---
## 一、变更概述
| 项 | 说明 |
|----|------|
| 范围 | 泰额版 `Yi.Abp.Net8`(主);美国版 `Last Updated` 同步修复 |
| 公司 = 租户 | 平台主库 `YiTenant`;前端常把 `name` 映射为 `companyName` |
| 账号密码 | `GET company-list`、`PUT company-admin` |
| 菜单/角色 | `menu-permission-tree`、`company-menus`、`company-roles`、`company-role-menus` |
| 凭据存储 | 主库表 `fl_th_tenant_admin_credential`(可回显 AES 密文,非登录哈希) |
| 菜单存储 | 主库表 `fl_th_tenant_menu_permission`(SaaS `menuPermissionKeys`) |
| 传输加盐 | 与 `FoodLabeling:TenantSelectCrypto` 一致(AES-CBC + `passwordSalt`=IV) |
| 落库哈希 | 租户业务库 `User`:`MD5Helper.GenerateSalt` + `SHA2Encode`(`UserPasswordHelper`) |
---
## 二、平台公司列表(返账号密码)
### 2.1 接口
| 项 | 值 |
|----|------|
| 方法 / 路径 | `GET /api/app/th-multi-tenancy/company-list` |
| 鉴权 | 需登录(Bearer) |
| 说明 | 分页查询主库租户,并返回可解密的管理员账号/密码 |
> **路由说明**:ABP 常规控制器 RootPath 为 `api/app`;自定义 `[HttpGet(...)]` / `[HttpPut(...)]` 必须带 `th-multi-tenancy/` 前缀,完整路径才是 `/api/app/th-multi-tenancy/...`。此前误写为 `[HttpGet("company-list")]` 时实际注册在 `/api/app/company-list`(404 于文档路径);已纠正。
### 2.2 入参
| 字段 | 说明 |
|------|------|
| `keyword` / `name` | 租户名称模糊匹配(`keyword` 优先) |
| `skipCount` | 跳过条数 |
| `maxResultCount` | 每页条数 |
### 2.3 出参示例
```json
{
"totalCount": 1,
"items": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "测试公司",
"creationTime": "2026-01-01T08:00:00",
"loginAccount": "admin",
"password": "Base64CipherText",
"passwordSalt": "Base64Iv",
"menuPermissionKeys": ["dashboard", "labeling:labels"]
}
]
}
```
### 2.4 密码来源优先级
1. 主库 `fl_th_tenant_admin_credential` 中已保存的 AES 密文 / IV
2. 无记录时回退 `RbacOptions.AdminPassword`,再经 AES 加密返回
默认账号:`admin`(与 `UserDataSeed` / `ThTenantSelectConsts.DefaultAdminLoginAccount` 一致)。
---
## 三、修改公司管理员账号/密码
### 3.1 接口
| 项 | 值 |
|----|------|
| 方法 / 路径 | `PUT /api/app/th-multi-tenancy/company-admin` |
| 鉴权 | 需登录(Bearer) |
### 3.2 入参
```json
{
"tenantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"loginAccount": "admin",
"password": "Base64CipherText",
"passwordSalt": "Base64Iv"
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| `tenantId` | 是 | 租户 Guid |
| `loginAccount` | 否 | 新登录账号;空则不改账号 |
| `password` | 否 | AES 密文;与 `passwordSalt` 都空则不改密码 |
| `passwordSalt` | 改密时必填 | AES IV(Base64) |
### 3.3 处理流程
1. 主库校验租户存在,读取凭据表当前账号
2. `CurrentTenant.Change(tenantId)` 定位租户库管理员用户
3. 若改账号:校验 UserName 不重复后更新
4. 若改密码:`TenantSelectCredentialCipher.DecryptPassword` → `UserPasswordHelper.ApplyPlainPassword` + `EnsurePasswordColumnsPersistedAsync`
5. Upsert 主库凭据表(可回显 AES 密文),供列表再次返回
### 3.4 curl 示例
```bash
# 1) 登录拿 Token(按环境改 host / 账号)
curl -X POST "http://127.0.0.1:19002/api/app/th-web-auth/login" \
-H "Content-Type: application/json" \
-d "{\"tenantName\":\"Default\",\"userName\":\"admin\",\"password\":\"123456\"}"
# 2) 公司列表
curl -X GET "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-list?skipCount=0&maxResultCount=20" \
-H "Authorization: Bearer <token>"
# 3) 修改管理员(password / passwordSalt 须为前端 AES 加密结果)
curl -X PUT "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-admin" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d "{\"tenantId\":\"<租户Guid>\",\"loginAccount\":\"admin\",\"password\":\"<密文>\",\"passwordSalt\":\"<IV>\"}"
```
---
## 四、AES 加盐约定(与 07-16 一致)
| 项 | 值 |
|----|------|
| 算法 | AES-256-CBC |
| 填充 | PKCS7 |
| Key | `SHA256(UTF8(SecretKey))` → 32 字节 |
| IV | 随机 16 字节,Base64 → `passwordSalt` |
| 密文 | Base64 → `password` |
| 配置 | `FoodLabeling:TenantSelectCrypto:SecretKey` |
```json
"FoodLabeling": {
"TenantSelectCrypto": {
"SecretKey": "FoodLabelingThTenantSelect2026Key!"
}
}
```
- **出参**:后端加密,前端用同一 SecretKey + `passwordSalt` 解密展示
- **入参(改密)**:前端加密后传 `password` + `passwordSalt`,后端 `DecryptPassword` 后再做登录哈希落库
解密失败统一:`UserFriendlyException("密码解密失败")`。
---
## 五、主库凭据表
表名:`fl_th_tenant_admin_credential`(实体带 `[DefaultTenantTable]`,走平台主库;CodeFirst 开启时随主库启动建表)
| 字段 | 说明 |
|------|------|
| `TenantId` | PK,对应 `YiTenant.Id` |
| `LoginAccount` | 管理员登录账号 |
| `PasswordCipher` | AES 可回显密文 |
| `PasswordIv` | AES IV |
| `LastModificationTime` | 最后修改时间 |
说明:此表存的是**可逆展示密文**,不是 `User.EncryPassword` 的不可逆哈希。
---
## 六、平台菜单权限分配
平台管理员可为每个公司(租户)勾选可用 SaaS 菜单模块;Key 与前端 `saas-menu-tree` / `tenant-menu-drawer` 的 `menuPermissionKeys` 对齐。
### 6.1 菜单权限目录树
| 项 | 值 |
|----|------|
| 方法 / 路径 | `GET /api/app/th-multi-tenancy/menu-permission-tree` |
| 鉴权 | 需登录 |
| 说明 | 返回可分配的 SaaS 菜单树(`key` / `title` / `children`) |
出参示例:
```json
[
{
"key": "dashboard",
"title": "仪表盘",
"children": [{ "key": "dashboard:analytics", "title": "数据分析" }]
},
{
"key": "labeling",
"title": "标签管理",
"children": [
{ "key": "labeling:labels", "title": "标签列表" },
{ "key": "labeling:categories", "title": "标签分类" },
{ "key": "labeling:types", "title": "标签类型" },
{ "key": "labeling:templates", "title": "标签模板" },
{ "key": "labeling:multiple-options", "title": "多选项" }
]
},
{
"key": "management",
"title": "管理",
"children": [
{ "key": "management:account", "title": "账号管理" },
{ "key": "management:menu", "title": "菜单管理" },
{ "key": "management:reports", "title": "报表" }
]
}
]
```
完整 Key 目录(与前端一致):
| 分组 | Key |
|------|-----|
| dashboard | `dashboard`、`dashboard:analytics` |
| labeling | `labeling`、`labeling:labels`、`labeling:categories`、`labeling:types`、`labeling:templates`、`labeling:multiple-options` |
| modules | `modules`、`modules:training`、`modules:alerts`、`modules:tasks`、`modules:food-waste`、`modules:e-label` |
| management | `management`、`management:account`、`management:menu`、`management:devices`、`management:reports`、`management:invoices`、`management:qr-codes`、`management:support`、`management:api` |
### 6.2 获取 / 设置公司菜单
| 操作 | 方法 / 路径 |
|------|-------------|
| 获取 | `GET /api/app/th-multi-tenancy/company-menus?tenantId={guid}` |
| 设置 | `PUT /api/app/th-multi-tenancy/company-menus`(覆盖式) |
设置入参:
```json
{
"tenantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"menuPermissionKeys": ["dashboard", "labeling:labels", "management:account"]
}
```
- 非法 Key → `400`,提示非法项
- `menuPermissionKeys` 为空数组 → 清空该公司全部菜单权限
- `company-list` 出参已增加 `menuPermissionKeys`
### 6.3 主库表 `fl_th_tenant_menu_permission`
| 字段 | 说明 |
|------|------|
| `Id` | 主键,`YitIdHelper.NextId().ToString()` |
| `TenantId` | 租户 Guid |
| `PermissionKey` | SaaS 菜单 Key |
| `CreationTime` | 创建时间 |
唯一逻辑:同一 `TenantId + PermissionKey` 不重复;写接口覆盖式先清后插。
---
## 七、平台角色菜单分配
平台管理员可跨租户业务库,查看该公司角色并为角色绑定菜单(`Role` / `RoleMenu` / `Menu`)。
### 7.1 租户角色列表
| 项 | 值 |
|----|------|
| 方法 / 路径 | `GET /api/app/th-multi-tenancy/company-roles?tenantId={guid}` |
| 鉴权 | 需登录 |
出参示例:
```json
[
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"roleName": "管理员",
"roleCode": "admin",
"state": true,
"menuIds": ["11111111-1111-1111-1111-111111111111"]
}
]
```
### 7.2 设置角色菜单(覆盖式)
| 项 | 值 |
|----|------|
| 方法 / 路径 | `PUT /api/app/th-multi-tenancy/company-role-menus` |
| 鉴权 | 需登录 |
入参:
```json
{
"tenantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"roleId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"menuIds": ["11111111-1111-1111-1111-111111111111"]
}
```
逻辑:`CurrentTenant.Change(tenantId)` 后覆盖写 `RoleMenu`(对齐 `RbacRoleMenuAppService.Set`);校验角色/菜单属于该租户库且未删除。
---
## 八、tenant-select 同步
`GET /api/app/th-multi-tenancy/tenant-select`(匿名,登录页):
| 项 | 说明 |
|----|------|
| `SkipCount` | 由错误的 `1` 修正为 `0` |
| 账号/密码 | 与公司列表相同:优先凭据表,否则 `AdminPassword` |
| `id` | 仍不返回(保持 `ThTenantSelectDto`) |
---
## 九、Dashboard Last Updated(同日附带)
| 项 | 说明 |
|----|------|
| 接口 | `GET /api/app/auth-session/my-menus` |
| 问题 | `lastUpdated` 误用用户 `LastModificationTime`,相对时间与业务编辑不符 |
| 修复 | 优先系统编辑 stamp → 业务表最近修改 → 用户资料时间 |
| 过滤器 | `SystemEditStampGlobalFilter` 注册到 MVC;写接口成功刷新 stamp |
| 范围 | 美国版 + 泰额版 `food-labeling-us` 同步 |
---
## 十、改动文件列表
### 泰额版 — 公司账号密码
| 文件 | 变更 |
|------|------|
| `FoodLabeling.Th.Domain/Entities/ThTenantAdminCredentialEntity.cs` | 新建主库凭据实体 |
| `FoodLabeling.Th.Application/MultiTenancy/TenantSelectCredentialCipher.cs` | 新增 `DecryptPassword` |
| `FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThCompanyListInput.cs` | 新建 |
| `FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThCompanyListItemDto.cs` | 新建(含 `menuPermissionKeys`) |
| `FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThUpdateCompanyAdminInputVo.cs` | 新建 |
| `FoodLabeling.Th.Application.Contracts/IServices/IThMultiTenancyAppService.cs` | 公司列表/改密/菜单/角色接口 |
| `FoodLabeling.Th.Application/Services/ThMultiTenancyAppService.cs` | 全部实现 + 修复 tenant-select |
### 泰额版 — 菜单 / 角色分配
| 文件 | 变更 |
|------|------|
| `FoodLabeling.Th.Domain/Entities/ThTenantMenuPermissionEntity.cs` | 主库菜单权限表实体 |
| `FoodLabeling.Th.Domain.Shared/Helpers/YitIdHelper.cs` | 雪花 Id(若新建) |
| `FoodLabeling.Th.Application/MultiTenancy/ThSaasMenuPermissionCatalog.cs` | SaaS 菜单 Key 目录 |
| `FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThSaasMenuPermissionTreeNodeDto.cs` | 权限树节点 |
| `FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThCompanyMenusDto.cs` | 公司菜单出参 |
| `FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThUpdateCompanyMenusInputVo.cs` | 设置公司菜单入参 |
| `FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThCompanyRoleItemDto.cs` | 租户角色列表项 |
| `FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThUpdateCompanyRoleMenusInputVo.cs` | 设置角色菜单入参 |
### 美国版 / 泰额版 — Last Updated
| 文件 | 变更 |
|------|------|
| `FoodLabeling.Application/Services/AuthSessionAppService.cs` | `lastUpdated` 取值顺序调整 |
| `FoodLabeling.Application/FoodLabelingApplicationModule.cs` | 注册 `SystemEditStampGlobalFilter` |
| (既有)`SystemEditStampGlobalFilter.cs` / `SystemEditStampCache.cs` | 写成功刷新全局 stamp |
---
## 十一、前端对接说明(本次未改前端)
- 平台公司列表请对接 **`company-list`**(含 `id`、加密凭据、`menuPermissionKeys`),勿仅依赖匿名 `tenant-select`(无 `id`)。
- 改密时密码须按第四节 AES 约定加密后再提交,**不要**直接传明文。
- `tenant-admin-modal`:对接 `PUT company-admin`。
- `tenant-menu-drawer`:树数据用 `menu-permission-tree`;回显/保存用 `company-menus`(字段 `menuPermissionKeys`)。
- 角色菜单:列表用 `company-roles`,保存用 `company-role-menus`(`menuIds` 为租户库 Menu.Id)。
---
## 十二、自检清单
- [ ] 主库已建表 `fl_th_tenant_admin_credential`、`fl_th_tenant_menu_permission`(或 CodeFirst 已开启并重启成功)
- [ ] `TenantSelectCrypto:SecretKey` 已配置且与前端一致
- [ ] 自定义 HttpGet/HttpPut 已带 `th-multi-tenancy/` 前缀(勿访问误路由 `/api/app/company-list` 等短路径)
- [ ] `GET company-list` 返回 `loginAccount` / `password` / `passwordSalt` / `menuPermissionKeys`
- [ ] `PUT company-admin` 改密后可用新密码登录该租户
- [ ] 再次拉列表能解出与改密后一致的明文
- [ ] `GET menu-permission-tree` 返回 SaaS 菜单树
- [ ] `PUT company-menus` 后 `GET company-menus` / `company-list` 一致
- [ ] `GET company-roles` 能列出租户角色及 `menuIds`
- [ ] `PUT company-role-menus` 后该角色菜单绑定已覆盖更新
- [ ] `tenant-select` 分页从第一条开始(不再 Skip 掉首条)
- [ ] (可选)改业务数据后 `my-menus.lastUpdated` 接近当前时间
---
## 十三、主库建表 DDL(缺表报错时执行)
环境:`antis-foodlabeling-host`。若接口报 `Table '...fl_th_tenant_admin_credential' doesn't exist`,在主库执行:
```sql
CREATE TABLE IF NOT EXISTS `fl_th_tenant_admin_credential` (
`TenantId` char(36) NOT NULL COMMENT '租户Id,对应 YiTenant.Id',
`LoginAccount` varchar(64) NOT NULL DEFAULT '' COMMENT '管理员登录账号',
`PasswordCipher` varchar(512) NOT NULL DEFAULT '' COMMENT 'AES可回显密文',
`PasswordIv` varchar(64) NOT NULL DEFAULT '' COMMENT 'AES IV Base64',
`LastModificationTime` datetime(6) NULL COMMENT '最后修改时间',
PRIMARY KEY (`TenantId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='平台主库-租户管理员可回显凭据';
CREATE TABLE IF NOT EXISTS `fl_th_tenant_menu_permission` (
`Id` varchar(32) NOT NULL COMMENT '雪花Id',
`TenantId` char(36) NOT NULL COMMENT '租户Id',
`PermissionKey` varchar(128) NOT NULL COMMENT 'SaaS菜单权限Key',
`CreationTime` datetime(6) NOT NULL COMMENT '创建时间',
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_fl_th_tenant_menu_permission_tenant_key` (`TenantId`, `PermissionKey`),
KEY `IX_fl_th_tenant_menu_permission_TenantId` (`TenantId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='平台主库-租户SaaS菜单权限';
```
> 2026-07-21 已在 `antis-foodlabeling-host` 执行上述 DDL(两表已存在)。
|