91821909
杨鑫
最新
|
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
|
# 5-19 泰额版 — 多租户(独立库)与接口说明
本文档说明 **泰额版后端**(`泰额版/Food Labeling Management Code/Yi.Abp.Net8`)在 **2026-05-19** 起的多租户改造:**每个租户独立 MySQL 业务库**,平台主库仅存 `yitenant`;以及泰额专用登录、租户开通相关接口。
> 与美国版业务接口(`food-labeling-us`)共用同一宿主时,调用业务 API 须携带租户上下文(见 [租户上下文](#租户上下文))。
---
## 目录
- [架构概览](#架构概览)
- [数据库与 SQL 脚本](#数据库与-sql-脚本)
- [应用配置](#应用配置)
- [租户上下文](#租户上下文)
- [泰额专用接口](#泰额专用接口)
- [登录](#post-apiappth-app-authlogin)
- [我的门店](#get-apiappth-app-authmy-locations)
- [租户下拉 / 当前租户](#thmulti-tenancy)
- [开通租户独立库](#post-apiappth-tenant-provisioningprovision)
- [框架租户管理(补充)](#框架租户管理补充)
- [业务接口联调](#业务接口联调)
- [代码与脚本路径](#代码与脚本路径)
- [常见问题](#常见问题)
---
## 架构概览
| 库 | 用途 | 连接来源 |
|----|------|----------|
| **平台主库** `antis-foodlabeling-host` | 仅 `yitenant`(租户元数据) | `appsettings` → `DbConnOptions.Url` |
| **租户业务库** 如 `antis-foodlabeling-us` | `fl_*`、`location`、`user` 等 | `yitenant.TenantConnectionString` |
```
antis-foodlabeling-host (主库)
└── yitenant
├── Default → antis-foodlabeling-us(迁移期默认租户 / 现有数据)
└── 新租户 → antis-foodlabeling-{tenant}(Provision 自动建库)
```
- **不做** 业务表 `TenantId` 行级隔离(勿执行给 `fl_*` 加 `TenantId` 的 ALTER)。
- 切换租户:请求头 `__tenant` 和/或 JWT 中的 `TenantId` Claim。
- 无租户上下文时:连接**平台主库**(用于租户 CRUD、开通租户等)。
**默认租户(迁移期)**
| 项 | 值 |
|----|-----|
| Id | `11111111-1111-1111-1111-111111111111` |
| Name | `Default` |
| 业务库 | `antis-foodlabeling-us`(连接串写在 `yitenant.TenantConnectionString`) |
---
## 数据库与 SQL 脚本
脚本目录:`泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling/scripts/`
### 执行顺序
| 顺序 | 文件 | 说明 |
|------|------|------|
| 1 | `create_platform_host_database.sql` | 创建主库 `antis-foodlabeling-host` 及 `yitenant` 表 |
| 2 | `migrate_yitenant_to_host.sql` | **可选**:若曾在业务库 `antis-foodlabeling-us` 中写过 `yitenant`,迁移到主库 |
| 3 | `separate_database_bootstrap.sql` | 在主库登记默认租户,`TenantConnectionString` 指向 `antis-foodlabeling-us` |
### 勿执行
以下 **不要** 在业务库执行(共享库 + 行级 `TenantId` 方案已废弃):
```sql
-- 勿执行
ALTER TABLE fl_product ADD COLUMN TenantId ...
UPDATE fl_product SET TenantId = ...
```
### 自检 SQL
```sql
USE antis-foodlabeling-host;
SELECT Id, Name, LEFT(TenantConnectionString, 80) AS conn
FROM yitenant
WHERE Id = '11111111-1111-1111-1111-111111111111';
```
应有一条 `Default`,且 `conn` 中含 `database=antis-foodlabeling-us`。
---
## 应用配置
文件:`泰额版/.../src/Yi.Abp.Web/appsettings.json`
| 配置项 | 说明 |
|--------|------|
| `DbConnOptions.Url` | 平台主库,如 `database=antis-foodlabeling-host` |
| `DbConnOptions.EnabledSaasMultiTenancy` | `true` |
| `FoodLabeling:MultiTenancy:Mode` | `SeparateDatabase` |
| `FoodLabeling:TenantDatabase` | 新租户库名模板、RDS 账号等 |
| `FoodLabeling:LegacyTenant` | 默认租户 Id / Name |
新租户库名模板示例:`antis-foodlabeling-{tenant}`(`{tenant}` 为规范化后的租户名)。
---
## 租户上下文
业务请求须让后端解析到 **租户 Id**,任选其一(推荐登录后仅用 Bearer Token):
| 方式 | 说明 |
|------|------|
| 请求头 | `__tenant: {租户Guid}` |
| JWT | Claim:`TenantId`(`TokenTypeConst.TenantId`)及 `AbpClaimTypes.TenantId` |
泰额登录签发的 Token 已写入上述 Claim;`JwtClaimTenantResolveContributor` 会自动解析。
租户解析顺序(`YiAbpWebModule`):
1. `HeaderTenantResolveContributor`(`__tenant`)
2. `JwtClaimTenantResolveContributor`(JWT)
> 独立库模式下**不会**自动回落到默认租户;未传租户时走主库连接,业务表可能查不到数据。
---
## 泰额专用接口
Swagger 分组:**泰额版-食品标签**(`FoodLabeling.Th.Application`)
基础路径前缀:`/api/app/`(ABP 动态 API 约定,以 Swagger 为准)
### `POST /api/app/th-app-auth/login`
**应用服务**:`ThAppAuthAppService`
**鉴权**:匿名
**说明**:
1. 在**平台主库**校验 `tenantId` 是否存在且已配置 `TenantConnectionString`。
2. 切换到该租户业务库,按邮箱 + 密码校验 `user`(盐值哈希与美国版一致)。
3. 签发 JWT(含 `TenantId`)、RefreshToken,并返回绑定门店列表。
#### 入参 `ThAppLoginInputVo`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| tenantId | Guid | 是 | 平台主库 `yitenant.Id` |
| email | string | 是 | 登录邮箱(`user.Email` / 邮箱形 `UserName`) |
| password | string | 是 | 密码 |
| uuid | string | 否 | 图形验证码 UUID(系统开启验证码时必填) |
| code | string | 否 | 图形验证码 |
#### 请求示例
```http
POST /api/app/th-app-auth/login
Content-Type: application/json
```
```json
{
"tenantId": "11111111-1111-1111-1111-111111111111",
"email": "admin@example.com",
"password": "YourPassword1!"
}
```
#### 出参 `ThAppLoginOutputDto`
| 字段 | 类型 | 说明 |
|------|------|------|
| token | string | 访问令牌(含 TenantId Claim) |
| refreshToken | string | 刷新令牌 |
| tenantId | Guid | 当前租户 Id |
| tenantName | string | 租户名称 |
| locations | array | 绑定门店(结构同美国版 `UsAppBoundLocationDto`) |
#### 出参示例
```json
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "...",
"tenantId": "11111111-1111-1111-1111-111111111111",
"tenantName": "Default",
"locations": [
{
"id": "...",
"locationCode": "LOC001",
"locationName": "Store A",
"fullAddress": "...",
"state": true
}
]
}
```
#### JWT Claims(节选)
| Claim | 说明 |
|-------|------|
| `TenantId` / `tenantid` | 租户 Guid 字符串 |
| `client_kind` | `th_app` |
| `sub` / `UserId` | 用户 Id |
#### 错误说明
| 提示 | 原因 |
|------|------|
| 请输入租户、邮箱与密码 | 参数缺失 |
| 租户不存在或已停用 | 主库无该 `yitenant` 记录 |
| 租户未配置业务库连接串 | `TenantConnectionString` 为空 |
| 登录失败!邮箱不存在 | 租户库中无该用户 |
| 用户名或密码错误 | 密码校验失败 |
---
### `GET /api/app/th-app-auth/my-locations`
**应用服务**:`ThAppAuthAppService`
**鉴权**:Bearer Token
**说明**:在**当前 JWT 租户上下文**下,查询 `userlocation` + `location` 绑定门店。
```http
GET /api/app/th-app-auth/my-locations
Authorization: Bearer {token}
```
可选同时带:`__tenant: 11111111-1111-1111-1111-111111111111`(与 Token 中租户一致即可)。
---
### ThMulti-tenancy
**应用服务**:`ThMultiTenancyAppService`
#### `GET /api/app/th-multi-tenancy/tenant-select`(方法名以 Swagger 为准,一般为 `get-tenant-select`)
租户下拉列表(供登录页选择租户)。
**出参**:`ThTenantSelectDto[]`
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Guid | 租户 Id |
| name | string | 租户名称 |
#### `GET /api/app/th-multi-tenancy/current-tenant`(`get-current-tenant`)
当前请求解析到的租户(调试用)。
**出参**:`ThCurrentTenantDto`
| 字段 | 类型 | 说明 |
|------|------|------|
| tenantId | Guid? | 当前租户 Id |
| tenantName | string? | 当前租户名称 |
---
### `POST /api/app/th-tenant-provisioning/provision`
**应用服务**:`ThTenantProvisioningAppService`
**鉴权**:需登录(平台管理员)
**说明**:
1. 在**平台主库**写入 `yitenant`(含 `TenantConnectionString`)。
2. 若未传连接串,按 `FoodLabeling:TenantDatabase` 生成,如 `antis-foodlabeling-{tenant}`。
3. `initializeDatabase=true` 时调用 `InitAsync`:建库 + CodeFirst 业务表(**不含** `yitenant`)。
#### 入参 `ThProvisionTenantInputVo`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | string | 是 | 租户名称(用于生成库名) |
| tenantConnectionString | string | 否 | 自定义连接串;空则按模板生成 |
| dbType | int | 否 | SqlSugar.DbType,默认 `0` = MySql |
| initializeDatabase | bool | 否 | 默认 `true`,是否立即建库建表 |
#### 请求示例
```http
POST /api/app/th-tenant-provisioning/provision
Content-Type: application/json
Authorization: Bearer {token}
```
```json
{
"name": "acme",
"initializeDatabase": true
}
```
#### 出参 `ThProvisionTenantOutputDto`
| 字段 | 类型 | 说明 |
|------|------|------|
| tenantId | Guid | 新租户 Id |
| name | string | 租户名称 |
| databaseName | string | 业务库名 |
| tenantConnectionString | string | 完整连接串 |
| databaseInitialized | bool | 是否已执行 Init |
#### `POST /api/app/th-tenant-provisioning/initialize-tenant-database`
对已有租户补执行建库建表(入参:租户 `tenantId`,以 Swagger 为准)。
---
### 框架租户管理(补充)
分组:**租户管理接口**(`Yi.Framework.TenantManagement.Application`)
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/app/tenant` | 创建租户(需 `tenantConnectionString`) |
| PUT | `/api/app/tenant/init/{id}` | 租户业务库 CodeFirst 初始化 |
泰额推荐使用 `th-tenant-provisioning/provision` 一步完成登记 + 建库。
---
## 业务接口联调
宿主同时加载 `FoodLabeling.Application`(美国版业务)与 `FoodLabeling.Th.Application`。
调用任意业务 API(如 `/api/app/product`、`/api/app/location`)时:
```http
GET /api/app/product?SkipCount=1&MaxResultCount=10
Authorization: Bearer {泰额登录返回的 token}
```
或:
```http
GET /api/app/product?SkipCount=1&MaxResultCount=10
Authorization: Bearer {token}
__tenant: 11111111-1111-1111-1111-111111111111
```
**分页**:`SkipCount` 为 **1-based 页码**(与美国版一致,见 `5-18接口优化.md`)。
---
## 代码与脚本路径
| 类型 | 路径 |
|------|------|
| 泰额应用层 | `module/food-labeling/FoodLabeling.Th.Application/` |
| 泰额契约 | `module/food-labeling/FoodLabeling.Th.Application.Contracts/` |
| 美国版业务(共用) | `module/food-labeling-us/FoodLabeling.Application/` |
| SQL 脚本 | `module/food-labeling/scripts/` |
| 多租户常量 | `module/food-labeling-us/FoodLabeling.Domain.Shared/MultiTenancy/FoodLabelingMultiTenancyConsts.cs` |
| JWT 租户解析 | `module/food-labeling-us/FoodLabeling.Application/MultiTenancy/JwtClaimTenantResolveContributor.cs` |
| Web 配置 | `src/Yi.Abp.Web/appsettings.json`、`YiAbpWebModule.cs` |
---
## 常见问题
| 现象 | 处理 |
|------|------|
| 启动报连库失败 | 确认已执行 `create_platform_host_database.sql`,且 `DbConnOptions.Url` 指向 `antis-foodlabeling-host` |
| 登录报租户不存在 | 在主库执行 `separate_database_bootstrap.sql` |
| 登录成功但业务列表为空 | 检查 Token 是否带 `TenantId`;或请求头补 `__tenant` |
| 业务接口查到主库无数据 | 未解析租户,误连主库;须登录泰额接口或传 `__tenant` |
| 新租户无表 | 调用 `provision` 且 `initializeDatabase=true`,或 `PUT tenant/init/{id}` |
| 误加了 TenantId 列 | 独立库模式不需要;可保留列但不使用,建议勿加 |
---
## 变更记录
| 日期 | 内容 |
|------|------|
| 2026-05-19 | 泰额版多租户独立库方案;平台主库分离;`ThAppAuth` 登录写 JWT TenantId;租户开通与 SQL 脚本说明 |
|