5-19 泰额版 — 多租户(独立库)与接口说明
本文档说明 泰额版后端(泰额版/Food Labeling Management Code/Yi.Abp.Net8)在 2026-05-19 起的多租户改造:每个租户独立 MySQL 业务库,平台主库仅存 yitenant;以及泰额专用登录、租户开通相关接口。
与美国版业务接口(
food-labeling-us)共用同一宿主时,调用业务 API 须携带租户上下文(见 租户上下文)。
目录
架构概览
| 库 | 用途 | 连接来源 |
|---|---|---|
平台主库 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 中的TenantIdClaim。 - 无租户上下文时:连接平台主库(用于租户 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 方案已废弃):
-- 勿执行
ALTER TABLE fl_product ADD COLUMN TenantId ...
UPDATE fl_product SET TenantId = ...
自检 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):
HeaderTenantResolveContributor(__tenant)JwtClaimTenantResolveContributor(JWT)
独立库模式下不会自动回落到默认租户;未传租户时走主库连接,业务表可能查不到数据。
泰额专用接口
Swagger 分组:泰额版-食品标签(FoodLabeling.Th.Application)
基础路径前缀:/api/app/(ABP 动态 API 约定,以 Swagger 为准)
POST /api/app/th-app-auth/login
应用服务:ThAppAuthAppService
鉴权:匿名
说明:
- 在平台主库校验
tenantId是否存在且已配置TenantConnectionString。 - 切换到该租户业务库,按邮箱 + 密码校验
user(盐值哈希与美国版一致)。 - 签发 JWT(含
TenantId)、RefreshToken,并返回绑定门店列表。
入参 ThAppLoginInputVo
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| tenantId | Guid | 是 | 平台主库 yitenant.Id |
| string | 是 | 登录邮箱(user.Email / 邮箱形 UserName) |
|
| password | string | 是 | 密码 |
| uuid | string | 否 | 图形验证码 UUID(系统开启验证码时必填) |
| code | string | 否 | 图形验证码 |
请求示例
POST /api/app/th-app-auth/login
Content-Type: application/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) |
出参示例
{
"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 绑定门店。
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
鉴权:需登录(平台管理员)
说明:
- 在平台主库写入
yitenant(含TenantConnectionString)。 - 若未传连接串,按
FoodLabeling:TenantDatabase生成,如antis-foodlabeling-{tenant}。 initializeDatabase=true时调用InitAsync:建库 + CodeFirst 业务表(不含yitenant)。
入参 ThProvisionTenantInputVo
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | string | 是 | 租户名称(用于生成库名) |
| tenantConnectionString | string | 否 | 自定义连接串;空则按模板生成 |
| dbType | int | 否 | SqlSugar.DbType,默认 0 = MySql |
| initializeDatabase | bool | 否 | 默认 true,是否立即建库建表 |
请求示例
POST /api/app/th-tenant-provisioning/provision
Content-Type: application/json
Authorization: Bearer {token}
{
"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)时:
GET /api/app/product?SkipCount=1&MaxResultCount=10
Authorization: Bearer {泰额登录返回的 token}
或:
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 脚本说明 |