# 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 脚本说明 |