# 2026-07-13 泰额版多租户 — 前端优化说明 本文档供 **泰额版 Web 管理端 / App** 对接多租户后端时使用。后端已完成独立库多租户改造,**前端尚未改动**。 > 后端架构说明见 `项目相关文档/5-19泰额版.md`;本次后端结构变更见下文「后端已完成项」。 --- ## 一、多租户对前端的核心影响 | 变更点 | 说明 | |--------|------| | 数据库 | 每个租户独立业务库;平台主库仅存 `yitenant` | | 业务 API | 必须在**租户上下文**下调用,否则连主库、列表为空或报错 | | 登录 | 须先选租户,再登录;Token 须含 `TenantId` Claim | | 旧接口 | `POST /api/app/account/login` **不能**单独用于泰额多租户 Web 登录 | | 旧 App 登录 | `us-app-auth/login` 在多租户模式下**已禁用**(返回友好错误) | ### 租户上下文传递方式(二选一) | 方式 | 说明 | |------|------| | JWT | 使用泰额登录接口返回的 Token(已含 `TenantId`) | | 请求头 | `__tenant: {租户Guid}`(与 Token 中租户一致) | --- ## 二、登录流程改造(必做) ### 2.1 Web 管理端推荐流程 ``` 1. GET /api/app/th-multi-tenancy/tenant-select → 租户下拉 2. POST /api/app/th-web-auth/login → Web 登录(含 RBAC Token) 3. 后续业务 API 带 Authorization: Bearer {token} ``` #### 租户下拉(匿名可访问) ```http GET /api/app/th-multi-tenancy/tenant-select ``` 出参示例: ```json [ { "id": "11111111-1111-1111-1111-111111111111", "name": "Default" } ] ``` #### Web 管理端登录(新接口,替代 account/login) ```http POST /api/app/th-web-auth/login Content-Type: application/json ``` ```json { "tenantId": "11111111-1111-1111-1111-111111111111", "userName": "admin@example.com", "password": "YourPassword1!", "uuid": null, "code": null } ``` 出参: | 字段 | 说明 | |------|------| | token | JWT(含 TenantId + 角色/菜单权限 Claims) | | refreshToken | 刷新令牌 | | tenantId | 当前租户 Id | | tenantName | 租户名称 | > **不要**再使用 `POST /api/app/account/login` 作为泰额 Web 主登录入口(无 `tenantId`,会查主库无用户)。 ### 2.2 泰额 App 登录(已有,保持不变) ```http POST /api/app/th-app-auth/login ``` ```json { "tenantId": "11111111-1111-1111-1111-111111111111", "email": "user@example.com", "password": "YourPassword1!" } ``` ### 2.3 默认租户(迁移期) | 项 | 值 | |----|-----| | tenantId | `11111111-1111-1111-1111-111111111111` | | name | `Default` | | 业务库 | `antis-foodlabeling-us`(现有数据) | 开发联调可先用 Default 租户。 --- ## 三、HTTP 客户端改造(必做) ### 3.1 Axios / fetch 封装 登录成功后: 1. 持久化 `token`、`refreshToken`、`tenantId`、`tenantName` 2. 所有业务请求 Header 带 `Authorization: Bearer {token}` 3. (可选)同时带 `__tenant: {tenantId}`,与 Token 保持一致 ```typescript // 伪代码示例 api.interceptors.request.use((config) => { const token = getToken(); const tenantId = getTenantId(); if (token) config.headers.Authorization = token.startsWith("Bearer ") ? token : `Bearer ${token}`; if (tenantId) config.headers["__tenant"] = tenantId; return config; }); ``` ### 3.2 须改造的现有服务文件(泰额 Web) | 文件 | 改动 | |------|------| | `src/services/accountService.ts` | 登录改为 `th-web-auth/login`,入参增加 `tenantId` | | 新建 `src/services/tenantService.ts` | 封装 `tenant-select`、`current-tenant` | | `src/lib/apiClient.ts` | 请求拦截器附加 `__tenant` | | `src/lib/authStorage.ts` | 存储 `tenantId` / `tenantName` | | 登录页组件 | 增加租户下拉(在邮箱密码之前) | ### 3.3 退出登录 - 清除 `token`、`refreshToken`、`tenantId` - 跳转登录页并重新选择租户 --- ## 四、业务 API 调用(无路径变更) 业务接口路径与美国版一致,例如: | 模块 | 示例 | |------|------| | 标签 | `GET /api/app/label?SkipCount=1&MaxResultCount=10` | | 产品 | `GET /api/app/product?SkipCount=1&MaxResultCount=10` | | 门店 | `GET /api/app/location?...` | | 菜单权限 | `GET /api/app/auth-session/my-menus` | **分页**:`SkipCount` 从 **1** 起(与美国版一致)。 未带租户上下文时,`auth-session/my-menus` 等接口返回: > 未识别租户上下文。请使用泰额登录接口(th-web-auth / th-app-auth)或请求头 __tenant 携带租户 Id。 --- ## 五、页面级优化清单 ### P0 — 阻塞联调 | 页面/功能 | 优化内容 | |-----------|----------| | 登录页 | 租户下拉 + `th-web-auth/login` | | 全局 API 客户端 | Token + `__tenant` 注入 | | 路由守卫 | 无 `tenantId` 时跳转登录 | ### P1 — 体验 | 页面/功能 | 优化内容 | |-----------|----------| | 登录页 | 记住上次选择的租户(localStorage) | | 顶栏 | 展示当前 `tenantName` | | 错误提示 | 401/租户相关错误引导重新登录 | | Swagger 联调说明 | 文档注明须带 `__tenant` | ### P2 — 平台管理(可选) | 页面/功能 | 优化内容 | |-----------|----------| | 平台管理员 | 租户开通页对接 `th-tenant-provisioning/provision` | | 租户管理 | 对接 `/api/app/tenant` CRUD | --- ## 六、禁止 / 废弃用法 | 用法 | 原因 | |------|------| | `POST /api/app/account/login` 作为泰额 Web 主登录 | 无租户,查主库 | | `POST /api/app/us-app-auth/login` | 多租户模式下后端已拒绝 | | 业务 API 仅带 Token、无 TenantId | JWT 须来自泰额登录接口 | | 给 `fl_*` 业务传 `tenantId` 字段 | 独立库模式,无行级 TenantId | --- ## 七、泰额 Web 与美国版 Web 差异对照 | 项 | 美国版 Web | 泰额版 Web(待改) | |----|-----------|-------------------| | API Base | `19001` 等 | `19002`(以 appsettings 为准) | | 登录接口 | `account/login` | `th-web-auth/login` + `tenantId` | | 租户选择 | 无 | 登录前必选 | | 业务 API 路径 | 相同 | 相同 | | Token Claims | 无 TenantId | 含 TenantId | --- ## 八、联调检查清单 - [ ] 登录页能拉取 `tenant-select` 列表 - [ ] 选择 Default 租户后能 `th-web-auth/login` 成功 - [ ] 登录后 `auth-session/my-menus` 有数据 - [ ] 标签列表 `GET /api/app/label?SkipCount=1&MaxResultCount=10` 有数据 - [ ] 刷新页面后 Token + tenantId 仍有效 - [ ] 退出后业务 API 返回未授权/无租户提示 --- ## 九、后端已完成项(供前端参考) > Swagger 分组:**泰额版-食品标签**(`FoodLabeling.Th.Application`)。 > 基础路径:`/api/app/`(ABP 动态 API,以下路径以 Swagger 为准)。 > 泰额本地联调 Base URL 一般为 `http://localhost:19002`。 ### 9.1 模块总览 | 模块 | 应用服务 | 路径/接口 | 鉴权 | |------|----------|-----------|------| | 租户解析 | `JwtClaimTenantResolveContributor` + `HeaderTenantResolveContributor` | 请求头 `__tenant` 或 JWT `TenantId` Claim | — | | Web 登录 | `ThWebAuthAppService` | `POST /api/app/th-web-auth/login` | 匿名 | | App 登录 | `ThAppAuthAppService` | `POST /api/app/th-app-auth/login` | 匿名 | | App 我的门店 | `ThAppAuthAppService` | `GET /api/app/th-app-auth/my-locations` | Bearer | | 租户列表 | `ThMultiTenancyAppService` | `GET /api/app/th-multi-tenancy/tenant-select` | 匿名 | | 当前租户 | `ThMultiTenancyAppService` | `GET /api/app/th-multi-tenancy/current-tenant` | Bearer | | 租户开通 | `ThTenantProvisioningAppService` | `POST /api/app/th-tenant-provisioning/provision` | Bearer(平台管理员) | | 补建租户库 | `ThTenantProvisioningAppService` | `POST /api/app/th-tenant-provisioning/initialize-tenant-database` | Bearer | | 业务守卫 | `TenantContextGuard` | 如 `GET /api/app/auth-session/my-menus` | Bearer + 租户上下文 | | UsAppAuth 禁用 | `UsAppAuthAppService` | `POST /api/app/us-app-auth/login` | 多租户下直接拒绝 | | JWT TenantId | `AccountManager` | Web 登录签发 Token 时写入 Claim | — | --- ### 9.2 租户上下文解析(`JwtClaimTenantResolveContributor`) **解析顺序**(`YiAbpWebModule` 配置): 1. 请求头 `__tenant: {租户Guid}` 2. JWT Claim:`TenantId`(`TokenTypeConst.TenantId`)或 `AbpClaimTypes.TenantId` **前端约定**: | 项 | 说明 | |----|------| | 登录后 | 持久化 `tenantId`,业务请求带 `Authorization: Bearer {token}` | | 可选 | 同时带 `__tenant: {tenantId}`,须与 Token 内租户一致 | | 无租户时 | 连接平台主库,业务 API 列表为空或报「未识别租户上下文」 | --- ### 9.3 `GET /api/app/th-multi-tenancy/tenant-select` **说明**:登录页租户下拉,匿名可访问;数据来自平台主库 `yitenant`。 **请求**:无 Query / Body。 **出参**:`ThTenantSelectDto[]`(JSON 数组,非分页包装) | 字段 | 类型 | 说明 | |------|------|------| | id | Guid | 租户 Id(登录时传 `tenantId`) | | name | string | 租户名称(顶栏展示) | **响应示例**: ```json [ { "id": "11111111-1111-1111-1111-111111111111", "name": "Default" } ] ``` **curl**: ```bash curl -X GET "http://localhost:19002/api/app/th-multi-tenancy/tenant-select" ``` --- ### 9.4 `GET /api/app/th-multi-tenancy/current-tenant` **说明**:调试当前请求是否已解析到租户;登录后调用。 **请求头**: | Header | 必填 | 说明 | |--------|------|------| | Authorization | 是 | `Bearer {token}` | | __tenant | 否 | 与 Token 租户一致时可附带 | **出参**:`ThCurrentTenantDto` | 字段 | 类型 | 说明 | |------|------|------| | tenantId | Guid? | 当前租户 Id;未解析时为 `null` | | tenantName | string? | 当前租户名称 | **响应示例**: ```json { "tenantId": "11111111-1111-1111-1111-111111111111", "tenantName": "Default" } ``` --- ### 9.5 `POST /api/app/th-web-auth/login`(Web 管理端登录) **说明**: 1. 在**平台主库**校验 `tenantId` 存在且已配置 `TenantConnectionString` 2. 切换到该租户业务库,按 `userName`(邮箱或用户名)+ 密码校验 3. 通过 `AccountManager.GetTokenByUserIdAsync` 签发**含 RBAC 角色/菜单权限**的 JWT(含 `TenantId` Claim) #### 入参 `ThWebLoginInputVo`(JSON Body) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | tenantId | Guid | 是 | 平台主库 `yitenant.Id`,不可为 `00000000-0000-0000-0000-000000000000` | | userName | string | 是 | 登录账号:优先匹配 `user.Email`,亦可为 `user.UserName` | | password | string | 是 | 密码 | | uuid | string | 否 | 图形验证码 UUID(`RbacOptions.EnableCaptcha=true` 时必填) | | code | string | 否 | 图形验证码 | #### 出参 `ThWebLoginOutputDto` | 字段 | 类型 | 说明 | |------|------|------| | token | string | JWT 访问令牌(含 `TenantId`、角色、权限 Claims;部分环境已带 `Bearer ` 前缀) | | refreshToken | string | 刷新令牌 | | tenantId | Guid | 当前租户 Id | | tenantName | string | 租户名称 | #### 请求示例 ```http POST /api/app/th-web-auth/login Content-Type: application/json ``` ```json { "tenantId": "11111111-1111-1111-1111-111111111111", "userName": "admin@example.com", "password": "YourPassword1!", "uuid": null, "code": null } ``` #### 出参示例 ```json { "token": "Bearer eyJhbGciOiJIUzI1NiIs...", "refreshToken": "...", "tenantId": "11111111-1111-1111-1111-111111111111", "tenantName": "Default" } ``` #### 常见错误 | 提示 | 原因 | |------|------| | 请输入租户、邮箱与密码! | `tenantId` / `userName` / `password` 缺失 | | 租户不存在或已停用 | 主库无该 `yitenant` | | 租户未配置业务库连接串 | `TenantConnectionString` 为空 | | Sign-in failed: account not found. | 租户库中无匹配用户 | | Invalid captcha. | 验证码错误或过期 | | 用户名或密码错误 | 密码校验失败 | --- ### 9.6 `POST /api/app/th-app-auth/login`(泰额 App 登录) **说明**:在租户库校验用户,签发 App 专用 JWT(Claim 含 `TenantId`、`client_kind=th_app`),并返回绑定门店。 #### 入参 `ThAppLoginInputVo`(JSON Body) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | tenantId | Guid | 是 | 平台主库 `yitenant.Id` | | email | string | 是 | 登录邮箱(`user.Email` / 邮箱形 `UserName`) | | password | string | 是 | 密码 | | uuid | string | 否 | 图形验证码 UUID(开启验证码时必填) | | code | string | 否 | 图形验证码 | #### 出参 `ThAppLoginOutputDto` | 字段 | 类型 | 说明 | |------|------|------| | token | string | JWT(含 `TenantId`;**无** RBAC 菜单 Claims) | | refreshToken | string | 刷新令牌 | | tenantId | Guid | 当前租户 Id | | tenantName | string | 租户名称 | | locations | array | 绑定门店列表,元素为 `UsAppBoundLocationDto` | **`locations[]` 子项**: | 字段 | 类型 | 说明 | |------|------|------| | id | string | 门店 Guid | | locationCode | string | 门店编码 | | locationName | string | 门店名称 | | fullAddress | string | 拼接地址 | | state | bool | 是否启用 | #### 请求示例 ```json { "tenantId": "11111111-1111-1111-1111-111111111111", "email": "user@example.com", "password": "YourPassword1!" } ``` #### JWT Claims(节选) | Claim | 说明 | |-------|------| | `TenantId` / `tenantid` | 租户 Guid | | `client_kind` | 固定 `th_app` | | `sub` / `UserId` | 用户 Id | --- ### 9.7 `GET /api/app/th-app-auth/my-locations` **说明**:在**当前 JWT 租户上下文**下查询 `userlocation` + `location`。 **请求头**: | Header | 必填 | 说明 | |--------|------|------| | Authorization | 是 | `Bearer {token}`(须为 `th-app-auth/login` 签发) | | __tenant | 否 | 与 Token 租户一致 | **出参**:`UsAppBoundLocationDto[]`(字段同 9.6 `locations`) **常见错误**: | 提示 | 原因 | |------|------| | 用户未登录 | Token 无效或未传 | | 未识别租户,请重新登录或携带 __tenant | JWT 无 `TenantId` 且未传 `__tenant` | --- ### 9.8 `POST /api/app/th-tenant-provisioning/provision`(租户开通,P2 可选) **说明**:平台管理员在**主库**登记租户并生成独立业务库连接串;可选立即 CodeFirst 建库建表。 #### 入参 `ThProvisionTenantInputVo`(JSON Body) | 字段 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | name | string | 是 | — | 租户名称(用于生成库名,如 `antis-foodlabeling-{tenant}`) | | tenantConnectionString | string | 否 | — | 自定义 MySQL 连接串;空则按 `FoodLabeling:TenantDatabase` 模板生成 | | dbType | int | 否 | `0` | SqlSugar `DbType`,`0` = MySql | | initializeDatabase | bool | 否 | `true` | 是否立即调用 `InitAsync` 建库 + 业务表 | #### 出参 `ThProvisionTenantOutputDto` | 字段 | 类型 | 说明 | |------|------|------| | tenantId | Guid | 新租户 Id | | name | string | 租户名称 | | databaseName | string | 业务库名 | | tenantConnectionString | string | 完整连接串 | | databaseInitialized | bool | 是否已执行 Init | #### 请求示例 ```http POST /api/app/th-tenant-provisioning/provision Content-Type: application/json Authorization: Bearer {token} ``` ```json { "name": "acme", "initializeDatabase": true } ``` --- ### 9.9 `POST /api/app/th-tenant-provisioning/initialize-tenant-database` **说明**:对**已有**租户补执行业务库 CodeFirst(建库 + 业务表,不含 `yitenant`)。 | 项 | 说明 | |----|------| | 入参 | `tenantId`(Guid,路由或 Query,以 Swagger 为准) | | 鉴权 | Bearer(平台管理员) | | 出参 | 无业务体(`204` / 空对象,以实际为准) | --- ### 9.10 业务守卫 `TenantContextGuard`(`auth-session/my-menus`) **说明**:泰额 Web 登录后,须带租户上下文才能拉取菜单;`AuthSessionAppService.GetMyMenusAsync` 首行调用 `TenantContextGuard.EnsureTenantResolved`。 #### `GET /api/app/auth-session/my-menus` **请求头**: | Header | 必填 | 说明 | |--------|------|------| | Authorization | 是 | `Bearer {token}`(须为 `th-web-auth/login` 签发) | | __tenant | 否 | 建议与 Token 租户一致 | **出参**:`CurrentUserMenuPermissionsOutputDto`(与美国版字段一致) | 字段 | 类型 | 说明 | |------|------|------| | user | object | 用户简要信息 | | roleCodes | string[] | 角色编码 | | permissionCodes | string[] | 权限码 | | accessPermissionCodes | string[] | 访问权限(如 `manage_people`) | | menus | array | 可见菜单树 | | lastUpdated | datetime? | 资料/全局编辑时间戳 | | role | string | 角色展示名(逗号拼接) | | fullName | string | 全名 | **无租户时错误**: > 获取菜单权限:未识别租户上下文。请使用泰额登录接口(th-web-auth / th-app-auth)或请求头 __tenant 携带租户 Id。 #### `POST /api/app/auth-session/logout` | 项 | 说明 | |----|------| | 鉴权 | Bearer | | 出参 | `bool`,是否清除服务端用户缓存 | --- ### 9.11 `UsAppAuth` 多租户禁用 当 `DbConnOptions.EnabledSaasMultiTenancy = true` 且请求**无租户上下文**时: **接口**:`POST /api/app/us-app-auth/login` **入参**(美国版 `UsAppLoginInputVo`,**无 tenantId**): | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | email | string | 是 | 邮箱 | | password | string | 是 | 密码 | | uuid | string | 否 | 验证码 UUID | | code | string | 否 | 验证码 | **返回错误(HTTP 400)**: > 多租户模式下请使用泰额 App 登录接口 /api/app/th-app-auth/login(须传 tenantId),勿使用 us-app-auth。 **前端处理**:泰额 App 统一改用 `th-app-auth/login`,入参必须包含 `tenantId`。 --- ### 9.12 JWT `TenantId` 写入(`AccountManager`) | 登录方式 | Token 签发 | TenantId Claim | |----------|------------|----------------| | Web `th-web-auth/login` | `AccountManager.GetTokenByUserIdAsync`(在 `CurrentTenant.Change` 内调用) | 写入 `AbpClaimTypes.TenantId` + `TokenTypeConst.TenantId` | | App `th-app-auth/login` | `ThAppAuthAppService.CreateAppAccessToken` | 同上,另含 `client_kind=th_app` | **条件**:仅在 `CurrentTenant.Id` 有值时写入(泰额登录流程已在租户上下文中执行校验与签发)。 --- ### 9.13 前端对接速查 | 场景 | 接口 | 关键入参 | |------|------|----------| | 登录页拉租户 | `GET tenant-select` | 无 | | Web 登录 | `POST th-web-auth/login` | `tenantId`, `userName`, `password` | | App 登录 | `POST th-app-auth/login` | `tenantId`, `email`, `password` | | 登录后拉菜单 | `GET auth-session/my-menus` | Header: `Authorization` +(可选)`__tenant` | | 业务列表 | `GET /api/app/label` 等 | 同上;`SkipCount` 从 **1** 起 | | 调试租户 | `GET current-tenant` | Header: `Authorization` | --- ## 十、相关文档 - `项目相关文档/5-19泰额版.md` — 多租户架构与泰额专用接口 - `项目相关文档/5-27代码优化.md` — th-app-auth 登录 500 修复说明 - `项目相关文档/2026-07-09代码修改.md` — 标签批量导入等业务接口 --- ## 变更记录 | 日期 | 内容 | |------|------| | 2026-07-13 | 泰额版后端多租户结构整理完成;新增 Web 登录 `th-web-auth`;整理前端优化清单 | | 2026-07-13 | 第九节补充泰额专用接口完整入参/出参、请求头、错误说明与前端速查表 |