# 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 " # 改公司管理员(password 为前端 AES 密文) curl -X PUT "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-admin" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d "{\"tenantId\":\"\",\"loginAccount\":\"admin\",\"password\":\"\",\"passwordSalt\":\"\"}" # 公司 SaaS 菜单 curl -G "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-menus" \ --data-urlencode "tenantId=" \ -H "Authorization: Bearer " # 公司角色 curl -G "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-roles" \ --data-urlencode "tenantId=" \ -H "Authorization: Bearer " ``` 公司端自测登录(验证平台改密是否生效): ```bash curl -X POST "http://127.0.0.1:19002/api/app/th-web-auth/login" \ -H "Content-Type: application/json" \ -d "{\"tenantId\":\"\",\"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 租户。**