Blame view

项目相关文档/2026-07-23泰额版平台端操作公司级账号逻辑.md 11.7 KB
acf259d5   李曜臣   2026-07-24
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
  # 2026-07-23 泰额版:平台端操作公司级账号逻辑说明
  
  本文档说明 **泰额版** 平台管理端如何操作「公司级」账号与权限:架构、接口、写库路径、与公司端登录的关系,以及**不得影响美国版**的隔离约束。
  
  相关文档:
  
  - `2026-07-21代码优化.md`:公司列表 / 改密 / SaaS 菜单与角色菜单接口细节  
  - `2026-07-23泰额版当前登录账号菜单接口.md`:平台 vs 公司「当前登录人菜单」  
  - `th-tenant-menu-seed.sql` / `th-platform-user-role-seed.sql`:平台主库菜单与 User/Role 种子  
  
  ---
  
  ## 一、角色与库分层
  
  | 端 | 登录 | 用户库 | 典型能力 |
  |----|------|--------|----------|
  | **平台端** | `POST /api/app/account/login` | 主库 `antis-foodlabeling-host`.`user` | 公司列表、改公司管理员、分配 SaaS 菜单、改公司角色菜单、开通/删除公司 |
  | **公司端** | `POST /api/app/th-web-auth/login`(须 `tenantId`) | 该公司业务库 `user` | 业务功能;`GET /api/app/auth-session/my-menus` |
  
  **公司 = 租户**:主库表 `YiTenant`(前端常把 `name` 当 `companyName`)。
  
  ```mermaid
  flowchart LR
    subgraph platform [平台端]
      PLogin[account/login]
      PHost[(antis-foodlabeling-host)]
      PLogin --> PHost
    end
    subgraph company [公司端]
      CLogin[th-web-auth/login]
      CBiz[(租户业务库)]
      CLogin --> CBiz
    end
    PHost -->|YiTenant.TenantConnectionString| CBiz
    PHost -->|改 admin / 角色菜单| CBiz
  ```
  
  ### 1.1 库职责
  
  | 库 | 配置来源 | 平台操作相关表 |
  |----|----------|----------------|
  | **主库** `antis-foodlabeling-host` | `DbConnOptions.Url` | `YiTenant`、`fl_th_tenant_admin_credential`、`fl_th_tenant_menu_permission`、平台 `user`/`role`/`menu`/`userrole`/`rolemenu` |
  | **租户业务库** `antis-foodlabeling-{key}` | `YiTenant.TenantConnectionString` | 公司 `User`(登录哈希)、`Role`/`RoleMenu`/`Menu`、业务表 |
  | **美国版库** `antis-foodlabeling-us` | 美版 `appsettings`;泰额 **Default** 租户连接串也指向此库 | **禁止**当作泰额新公司业务库去写平台种子;对 Default 改 admin/角色会动到美版数据 |
  
  ### 1.2 两层「菜单」勿混淆
  
  | 层 | 存储 | 接口 | 语义 |
  |----|------|------|------|
  | **SaaS 公司开通模块** | 主库 `fl_th_tenant_menu_permission`(`menuPermissionKeys`) | `menu-permission-tree` / `company-menus` | 该公司「能开哪些产品模块」 |
  | **公司 RBAC 菜单** | 租户库 `Menu` + `RoleMenu` | `company-roles` / `company-role-menus`;公司端 `auth-session/my-menus` | 该公司内角色能看哪些业务菜单 |
  
  平台主库种子菜单(`th-menu-*`)服务于**平台端自身路由**,不是公司业务库菜单。
  
  ---
  
  ## 二、平台端操作公司账号:能力总览
  
  实现类:`ThMultiTenancyAppService`(路由前缀 `/api/app/th-multi-tenancy/`)。  
  开通公司:`ThTenantProvisioningAppService`(`/api/app/th-tenant-provisioning/`)。
  
  | 能力 | 方法 / 路径 | 主要读写 |
  |------|-------------|----------|
  | 登录页租户下拉(匿名) | `GET .../tenant-select` | 读主库租户 + 凭据(无 Id) |
  | 公司分页列表 | `GET .../company-list` | 读主库租户 + 凭据 + SaaS keys |
  | 改公司管理员账号/密码 | `PUT .../company-admin` | 写**租户库 User** + 写主库凭据表 |
  | SaaS 权限树 | `GET .../menu-permission-tree` | 代码目录,无库 |
  | 查/设公司 SaaS 菜单 | `GET/PUT .../company-menus` | 读写主库 `fl_th_tenant_menu_permission` |
  | 查公司角色 | `GET .../company-roles` | 读租户库 `Role`/`RoleMenu` |
  | 设公司角色菜单 | `PUT .../company-role-menus` | 写租户库 `RoleMenu` |
  | 开通公司 | `POST /th-tenant-provisioning/provision` | 写主库 `YiTenant` + 建业务库/Init |
  | 删除公司 | `DELETE .../company/{tenantId}` | 软删主库租户;可选 DROP 业务库 |
  
  鉴权:除 `tenant-select` 外,平台管理接口需 **平台 Token**(`account/login`)。
  
  ---
  
  ## 三、核心数据流
  
  ### 3.1 开通公司(产生「公司级账号」)
  
  ```text
  平台管理员
    → POST /api/app/th-tenant-provisioning/provision
    → 主库写入 YiTenant(Name + TenantConnectionString)
    → EnsureDatabaseCreated(物理建库)
    → 后台 InitAsync:业务库 CodeFirst + Seed
         └─ 默认 User.UserName = admin,密码 = RbacOptions.AdminPassword(哈希)
  ```
  
  要点:
  
  - 开通**不会**自动写 `fl_th_tenant_admin_credential`
  - 列表回显密码:无凭据行时用配置默认密码现场 AES 加密;平台首次 `PUT company-admin` 后才持久化可回显密文。  
  - Init 异步未完成时,立刻改 admin / 公司登录可能失败。
  
  ### 3.2 平台改公司管理员(重点)
  
  接口:`PUT /api/app/th-multi-tenancy/company-admin`
  
  ```text
  1. CurrentTenant=null(主库 scope)
     读 YiTenant、fl_th_tenant_admin_credential(当前 LoginAccount,默认 admin)
  2. 若有 password + passwordSalt:AES 解密得明文
  3. 用 YiTenant.TenantConnectionString 建独立 SqlSugarClient(直连业务库,不靠 Change(tenantId) 切仓储)
  4. 在业务库 User 上:
     - 可选改 UserName(查重)
     - 可选 ApplyPlainPassword → 写 Password/Salt 哈希
  5. Upsert 主库 fl_th_tenant_admin_credential(LoginAccount + AES 密文 + IV)
  ```
  
  | 写入位置 | 字段语义 |
  |----------|----------|
  | 租户库 `User.Password/Salt` | **登录校验用哈希**(不可逆) |
  | 主库 `PasswordCipher/PasswordIv` | **列表回显用 AES 密文**(可逆,密钥 `FoodLabeling:TenantSelectCrypto`) |
  
  前端改密必须先按约定 AES 加密再提交,**禁止**明文密码直传。
  
  ### 3.3 平台分配公司 SaaS 菜单
  
  ```text
  GET  menu-permission-tree     → 静态 Key 树(ThSaasMenuPermissionCatalog)
  GET  company-menus?tenantId=  → 读主库已分配 keys
  PUT  company-menus            → 主库先删后插 fl_th_tenant_menu_permission
  ```
  
  不写租户库 `Menu`/`RoleMenu`。非法 key 会校验失败。
  
  ### 3.4 平台配置公司内角色菜单
  
  ```text
  GET  company-roles?tenantId=  → 直连租户库读 Role + RoleMenu.menuIds
  PUT  company-role-menus       → 直连租户库覆盖 RoleMenu(校验 Menu 存在)
  ```
  
  这与公司员工登录后看到的 `auth-session/my-menus` 同源(租户库 RBAC)。
  
  ### 3.5 删除公司
  
  ```text
  DELETE /api/app/th-multi-tenancy/company/{tenantId}?dropDatabase=true
    → 禁止删除 Default 租户
    → DROP 业务库时禁止库名 host / us(及受保护库)
    → 软删 YiTenant
    → 删凭据表、SaaS 菜单权限、租户缓存
  ```
  
  ---
  
  ## 四、跨库访问约定(实现要点)
  
  | 约定 | 说明 |
  |------|------|
  | 主库表 | 实体带 `[DefaultTenantTable]`;读写前 `CurrentTenant.Change(null)`,连接走 `DbConnOptions.Url`(host) |
  | 租户业务库 | `TenantBusinessDatabaseAccessor`:按 `YiTenant` 连接串 **独立 SqlSugarClient**,避免空连接串误连主库 |
  | 不信任 JWT 租户 | 平台改公司数据时以入参 `tenantId` + 主库登记的连接串为准,不以「当前登录租户」替代目标公司 |
  
  ---
  
  ## 五、与美国版隔离(强制)
  
  | 规则 | 说明 |
  |------|------|
  | 平台种子只写 **host** | `menu` / `user` / `role` 等平台数据禁止写入 `antis-foodlabeling-us` |
  | Default 租户 = US 业务库 | host 中 Default 的 `TenantConnectionString` 指向 `antis-foodlabeling-us`;对该租户执行 `company-admin` / `company-role-menus` **会改美版库** |
  | 新公司必须独立库 | Provision 使用 `antis-foodlabeling-{tenant}` 模板;勿把新公司指到 `us` |
  | 删除保护 | 不可删 Default;不可 DROP `host`/`us` |
  | Token 隔离 | 平台 Token 调公司 `my-menus` 会因无租户失败;公司 Token 勿当平台管理凭证 |
  
  **建议**:平台运营界面操作公司时,只选择泰额独立业务库租户(如 `中国麦当劳公司` → `antis-foodlabeling-t46d56528`),避免点选 Default。
  
  ---
  
  ## 六、接口速查(含 curl)
  
  Base:`http://127.0.0.1:19002`(以环境为准)。
  
  ```bash
  # 平台登录
  curl -X POST "http://127.0.0.1:19002/api/app/account/login" \
    -H "Content-Type: application/json" \
    -d "{\"userName\":\"admin@example.com\",\"password\":\"123456\"}"
  
  # 公司列表(含加密账号密码)
  curl -G "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-list" \
    --data-urlencode "skipCount=0" --data-urlencode "maxResultCount=20" \
    -H "Authorization: Bearer <platform-token>"
  
  # 改公司管理员(password 为前端 AES 密文)
  curl -X PUT "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-admin" \
    -H "Authorization: Bearer <platform-token>" \
    -H "Content-Type: application/json" \
    -d "{\"tenantId\":\"<guid>\",\"loginAccount\":\"admin\",\"password\":\"<cipher>\",\"passwordSalt\":\"<iv>\"}"
  
  # 公司 SaaS 菜单
  curl -G "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-menus" \
    --data-urlencode "tenantId=<guid>" \
    -H "Authorization: Bearer <platform-token>"
  
  # 公司角色
  curl -G "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-roles" \
    --data-urlencode "tenantId=<guid>" \
    -H "Authorization: Bearer <platform-token>"
  ```
  
  公司端自测登录(验证平台改密是否生效):
  
  ```bash
  curl -X POST "http://127.0.0.1:19002/api/app/th-web-auth/login" \
    -H "Content-Type: application/json" \
    -d "{\"tenantId\":\"<guid>\",\"userName\":\"admin\",\"password\":\"新明文密码\"}"
  ```
  
  ---
  
  ## 七、关键代码路径
  
  | 模块 | 路径 |
  |------|------|
  | 平台多租户服务 | `FoodLabeling.Th.Application/Services/ThMultiTenancyAppService.cs` |
  | 开通租户 | `FoodLabeling.Th.Application/Services/ThTenantProvisioningAppService.cs` |
  | 业务库直连 | `FoodLabeling.Th.Application/MultiTenancy/TenantBusinessDatabaseAccessor.cs` |
  | AES 凭据 | `FoodLabeling.Th.Application/MultiTenancy/TenantSelectCredentialCipher.cs` |
  | SaaS Key 目录 | `FoodLabeling.Th.Application/MultiTenancy/ThSaasMenuPermissionCatalog.cs` |
  | 主库凭据实体 | `FoodLabeling.Th.Domain/Entities/ThTenantAdminCredentialEntity.cs` |
  | 主库 SaaS 菜单实体 | `FoodLabeling.Th.Domain/Entities/ThTenantMenuPermissionEntity.cs` |
  | 公司 Web 登录 | `FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs` |
  | 平台登录 | `Yi.Framework.Rbac.Application/Services/AccountService.cs` |
  
  ---
  
  ## 八、风险与自检
  
  | 风险 | 应对 |
  |------|------|
  | 对 Default 改密 / 改角色 → 写 US | 运营禁操作 Default;删除已有保护,改密无同等硬拦 |
  | 凭据表可逆 AES | 保护 `TenantSelectCrypto:SecretKey`;仅平台管理员可见列表 |
  | 开通未写凭据表 | 改密走平台接口后才会与列表回显一致 |
  | Init 未完成就改 admin | 等「后台初始化完成」或调 `initialize-tenant-database` |
  | SaaS keys ≠ RBAC 菜单 | 配置完 `company-menus` 后,公司内角色仍需 `company-role-menus` / 公司端角色管理 |
  
  自检清单:
  
  - [ ] 平台用 `account/login`(host `admin`),公司用 `th-web-auth/login`  
  - [ ] `company-list` 能看到目标公司 Id 与加密凭据  
  - [ ] `company-admin` 后,公司端可用新密码登录;主库凭据表有对应行  
  - [ ] `company-menus` 只改 host;`company-role-menus` 只改目标租户库  
  - [ ] `antis-foodlabeling-us` 的 user/role 数量未因平台种子脚本变化  
  - [ ] 未对 Default / `database=us` 执行 DROP  
  
  ---
  
  ## 九、小结
  
  平台端操作公司级账号的本质是:
  
  1. **元数据与回显凭据**放在主库 host;  
  2. **真正能登录的公司账号**在各公司业务库 `User`
  3. 平台通过 `tenantId` + 直连连接串跨库改公司 admin / 角色菜单;  
  4. SaaS 模块开关与公司内 RBAC 是两层模型;  
  5. **永远不要把美国版库当成泰额平台种子库或随意操作 Default 租户。**