2026-07-21 代码优化
本文档说明 2026-07-21 后端改动。重点为 泰额版 平台级管理员:
- 「公司列表」返回账号密码、支持修改(前端密码 AES 加盐)
- 菜单权限分配、角色菜单分配
另含美国版/泰额版 Dashboard Last Updated 修复。前端本次不改。
一、变更概述
| 项 | 说明 |
|---|---|
| 范围 | 泰额版 Yi.Abp.Net8(主);美国版 Last Updated 同步修复 |
| 公司 = 租户 | 平台主库 YiTenant;前端常把 name 映射为 companyName |
| 账号密码 | GET company-list、PUT company-admin |
| 菜单/角色 | menu-permission-tree、company-menus、company-roles、company-role-menus |
| 凭据存储 | 主库表 fl_th_tenant_admin_credential(可回显 AES 密文,非登录哈希) |
| 菜单存储 | 主库表 fl_th_tenant_menu_permission(SaaS menuPermissionKeys) |
| 传输加盐 | 与 FoodLabeling:TenantSelectCrypto 一致(AES-CBC + passwordSalt=IV) |
| 落库哈希 | 租户业务库 User:MD5Helper.GenerateSalt + SHA2Encode(UserPasswordHelper) |
二、平台公司列表(返账号密码)
2.1 接口
| 项 | 值 |
|---|---|
| 方法 / 路径 | GET /api/app/th-multi-tenancy/company-list |
| 鉴权 | 需登录(Bearer) |
| 说明 | 分页查询主库租户,并返回可解密的管理员账号/密码 |
路由说明:ABP 常规控制器 RootPath 为
api/app;自定义[HttpGet(...)]/[HttpPut(...)]必须带th-multi-tenancy/前缀,完整路径才是/api/app/th-multi-tenancy/...。此前误写为[HttpGet("company-list")]时实际注册在/api/app/company-list(404 于文档路径);已纠正。
2.2 入参
| 字段 | 说明 |
|---|---|
keyword / name |
租户名称模糊匹配(keyword 优先) |
skipCount |
跳过条数 |
maxResultCount |
每页条数 |
2.3 出参示例
{
"totalCount": 1,
"items": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "测试公司",
"creationTime": "2026-01-01T08:00:00",
"loginAccount": "admin",
"password": "Base64CipherText",
"passwordSalt": "Base64Iv",
"menuPermissionKeys": ["dashboard", "labeling:labels"]
}
]
}
2.4 密码来源优先级
- 主库
fl_th_tenant_admin_credential中已保存的 AES 密文 / IV - 无记录时回退
RbacOptions.AdminPassword,再经 AES 加密返回
默认账号:admin(与 UserDataSeed / ThTenantSelectConsts.DefaultAdminLoginAccount 一致)。
三、修改公司管理员账号/密码
3.1 接口
| 项 | 值 |
|---|---|
| 方法 / 路径 | PUT /api/app/th-multi-tenancy/company-admin |
| 鉴权 | 需登录(Bearer) |
3.2 入参
{
"tenantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"loginAccount": "admin",
"password": "Base64CipherText",
"passwordSalt": "Base64Iv"
}
| 字段 | 必填 | 说明 |
|---|---|---|
tenantId |
是 | 租户 Guid |
loginAccount |
否 | 新登录账号;空则不改账号 |
password |
否 | AES 密文;与 passwordSalt 都空则不改密码 |
passwordSalt |
改密时必填 | AES IV(Base64) |
3.3 处理流程
- 主库校验租户存在,读取凭据表当前账号
CurrentTenant.Change(tenantId)定位租户库管理员用户- 若改账号:校验 UserName 不重复后更新
- 若改密码:
TenantSelectCredentialCipher.DecryptPassword→UserPasswordHelper.ApplyPlainPassword+EnsurePasswordColumnsPersistedAsync - Upsert 主库凭据表(可回显 AES 密文),供列表再次返回
3.4 curl 示例
# 1) 登录拿 Token(按环境改 host / 账号)
curl -X POST "http://127.0.0.1:19002/api/app/th-web-auth/login" \
-H "Content-Type: application/json" \
-d "{\"tenantName\":\"Default\",\"userName\":\"admin\",\"password\":\"123456\"}"
# 2) 公司列表
curl -X GET "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-list?skipCount=0&maxResultCount=20" \
-H "Authorization: Bearer <token>"
# 3) 修改管理员(password / passwordSalt 须为前端 AES 加密结果)
curl -X PUT "http://127.0.0.1:19002/api/app/th-multi-tenancy/company-admin" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d "{\"tenantId\":\"<租户Guid>\",\"loginAccount\":\"admin\",\"password\":\"<密文>\",\"passwordSalt\":\"<IV>\"}"
四、AES 加盐约定(与 07-16 一致)
| 项 | 值 |
|---|---|
| 算法 | AES-256-CBC |
| 填充 | PKCS7 |
| Key | SHA256(UTF8(SecretKey)) → 32 字节 |
| IV | 随机 16 字节,Base64 → passwordSalt |
| 密文 | Base64 → password |
| 配置 | FoodLabeling:TenantSelectCrypto:SecretKey |
"FoodLabeling": {
"TenantSelectCrypto": {
"SecretKey": "FoodLabelingThTenantSelect2026Key!"
}
}
- 出参:后端加密,前端用同一 SecretKey +
passwordSalt解密展示 - 入参(改密):前端加密后传
password+passwordSalt,后端DecryptPassword后再做登录哈希落库
解密失败统一:UserFriendlyException("密码解密失败")。
五、主库凭据表
表名:fl_th_tenant_admin_credential(实体带 [DefaultTenantTable],走平台主库;CodeFirst 开启时随主库启动建表)
| 字段 | 说明 |
|---|---|
TenantId |
PK,对应 YiTenant.Id |
LoginAccount |
管理员登录账号 |
PasswordCipher |
AES 可回显密文 |
PasswordIv |
AES IV |
LastModificationTime |
最后修改时间 |
说明:此表存的是可逆展示密文,不是 User.EncryPassword 的不可逆哈希。
六、平台菜单权限分配
平台管理员可为每个公司(租户)勾选可用 SaaS 菜单模块;Key 与前端 saas-menu-tree / tenant-menu-drawer 的 menuPermissionKeys 对齐。
6.1 菜单权限目录树
| 项 | 值 |
|---|---|
| 方法 / 路径 | GET /api/app/th-multi-tenancy/menu-permission-tree |
| 鉴权 | 需登录 |
| 说明 | 返回可分配的 SaaS 菜单树(key / title / children) |
出参示例:
[
{
"key": "dashboard",
"title": "仪表盘",
"children": [{ "key": "dashboard:analytics", "title": "数据分析" }]
},
{
"key": "labeling",
"title": "标签管理",
"children": [
{ "key": "labeling:labels", "title": "标签列表" },
{ "key": "labeling:categories", "title": "标签分类" },
{ "key": "labeling:types", "title": "标签类型" },
{ "key": "labeling:templates", "title": "标签模板" },
{ "key": "labeling:multiple-options", "title": "多选项" }
]
},
{
"key": "management",
"title": "管理",
"children": [
{ "key": "management:account", "title": "账号管理" },
{ "key": "management:menu", "title": "菜单管理" },
{ "key": "management:reports", "title": "报表" }
]
}
]
完整 Key 目录(与前端一致):
| 分组 | Key |
|---|---|
| dashboard | dashboard、dashboard:analytics |
| labeling | labeling、labeling:labels、labeling:categories、labeling:types、labeling:templates、labeling:multiple-options |
| modules | modules、modules:training、modules:alerts、modules:tasks、modules:food-waste、modules:e-label |
| management | management、management:account、management:menu、management:devices、management:reports、management:invoices、management:qr-codes、management:support、management:api |
6.2 获取 / 设置公司菜单
| 操作 | 方法 / 路径 |
|---|---|
| 获取 | GET /api/app/th-multi-tenancy/company-menus?tenantId={guid} |
| 设置 | PUT /api/app/th-multi-tenancy/company-menus(覆盖式) |
设置入参:
{
"tenantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"menuPermissionKeys": ["dashboard", "labeling:labels", "management:account"]
}
- 非法 Key →
400,提示非法项 menuPermissionKeys为空数组 → 清空该公司全部菜单权限company-list出参已增加menuPermissionKeys
6.3 主库表 fl_th_tenant_menu_permission
| 字段 | 说明 |
|---|---|
Id |
主键,YitIdHelper.NextId().ToString() |
TenantId |
租户 Guid |
PermissionKey |
SaaS 菜单 Key |
CreationTime |
创建时间 |
唯一逻辑:同一 TenantId + PermissionKey 不重复;写接口覆盖式先清后插。
七、平台角色菜单分配
平台管理员可跨租户业务库,查看该公司角色并为角色绑定菜单(Role / RoleMenu / Menu)。
7.1 租户角色列表
| 项 | 值 |
|---|---|
| 方法 / 路径 | GET /api/app/th-multi-tenancy/company-roles?tenantId={guid} |
| 鉴权 | 需登录 |
出参示例:
[
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"roleName": "管理员",
"roleCode": "admin",
"state": true,
"menuIds": ["11111111-1111-1111-1111-111111111111"]
}
]
7.2 设置角色菜单(覆盖式)
| 项 | 值 |
|---|---|
| 方法 / 路径 | PUT /api/app/th-multi-tenancy/company-role-menus |
| 鉴权 | 需登录 |
入参:
{
"tenantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"roleId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"menuIds": ["11111111-1111-1111-1111-111111111111"]
}
逻辑:CurrentTenant.Change(tenantId) 后覆盖写 RoleMenu(对齐 RbacRoleMenuAppService.Set);校验角色/菜单属于该租户库且未删除。
八、tenant-select 同步
GET /api/app/th-multi-tenancy/tenant-select(匿名,登录页):
| 项 | 说明 |
|---|---|
SkipCount |
由错误的 1 修正为 0 |
| 账号/密码 | 与公司列表相同:优先凭据表,否则 AdminPassword |
id |
仍不返回(保持 ThTenantSelectDto) |
九、Dashboard Last Updated(同日附带)
| 项 | 说明 |
|---|---|
| 接口 | GET /api/app/auth-session/my-menus |
| 问题 | lastUpdated 误用用户 LastModificationTime,相对时间与业务编辑不符 |
| 修复 | 优先系统编辑 stamp → 业务表最近修改 → 用户资料时间 |
| 过滤器 | SystemEditStampGlobalFilter 注册到 MVC;写接口成功刷新 stamp |
| 范围 | 美国版 + 泰额版 food-labeling-us 同步 |
十、改动文件列表
泰额版 — 公司账号密码
| 文件 | 变更 |
|---|---|
FoodLabeling.Th.Domain/Entities/ThTenantAdminCredentialEntity.cs |
新建主库凭据实体 |
FoodLabeling.Th.Application/MultiTenancy/TenantSelectCredentialCipher.cs |
新增 DecryptPassword |
FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThCompanyListInput.cs |
新建 |
FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThCompanyListItemDto.cs |
新建(含 menuPermissionKeys) |
FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThUpdateCompanyAdminInputVo.cs |
新建 |
FoodLabeling.Th.Application.Contracts/IServices/IThMultiTenancyAppService.cs |
公司列表/改密/菜单/角色接口 |
FoodLabeling.Th.Application/Services/ThMultiTenancyAppService.cs |
全部实现 + 修复 tenant-select |
泰额版 — 菜单 / 角色分配
| 文件 | 变更 |
|---|---|
FoodLabeling.Th.Domain/Entities/ThTenantMenuPermissionEntity.cs |
主库菜单权限表实体 |
FoodLabeling.Th.Domain.Shared/Helpers/YitIdHelper.cs |
雪花 Id(若新建) |
FoodLabeling.Th.Application/MultiTenancy/ThSaasMenuPermissionCatalog.cs |
SaaS 菜单 Key 目录 |
FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThSaasMenuPermissionTreeNodeDto.cs |
权限树节点 |
FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThCompanyMenusDto.cs |
公司菜单出参 |
FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThUpdateCompanyMenusInputVo.cs |
设置公司菜单入参 |
FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThCompanyRoleItemDto.cs |
租户角色列表项 |
FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThUpdateCompanyRoleMenusInputVo.cs |
设置角色菜单入参 |
美国版 / 泰额版 — Last Updated
| 文件 | 变更 |
|---|---|
FoodLabeling.Application/Services/AuthSessionAppService.cs |
lastUpdated 取值顺序调整 |
FoodLabeling.Application/FoodLabelingApplicationModule.cs |
注册 SystemEditStampGlobalFilter |
(既有)SystemEditStampGlobalFilter.cs / SystemEditStampCache.cs |
写成功刷新全局 stamp |
十一、前端对接说明(本次未改前端)
- 平台公司列表请对接
company-list(含id、加密凭据、menuPermissionKeys),勿仅依赖匿名tenant-select(无id)。 - 改密时密码须按第四节 AES 约定加密后再提交,不要直接传明文。
tenant-admin-modal:对接PUT company-admin。tenant-menu-drawer:树数据用menu-permission-tree;回显/保存用company-menus(字段menuPermissionKeys)。- 角色菜单:列表用
company-roles,保存用company-role-menus(menuIds为租户库 Menu.Id)。
十二、自检清单
- [ ] 主库已建表
fl_th_tenant_admin_credential、fl_th_tenant_menu_permission(或 CodeFirst 已开启并重启成功) - [ ]
TenantSelectCrypto:SecretKey已配置且与前端一致 - [ ] 自定义 HttpGet/HttpPut 已带
th-multi-tenancy/前缀(勿访问误路由/api/app/company-list等短路径) - [ ]
GET company-list返回loginAccount/password/passwordSalt/menuPermissionKeys - [ ]
PUT company-admin改密后可用新密码登录该租户 - [ ] 再次拉列表能解出与改密后一致的明文
- [ ]
GET menu-permission-tree返回 SaaS 菜单树 - [ ]
PUT company-menus后GET company-menus/company-list一致 - [ ]
GET company-roles能列出租户角色及menuIds - [ ]
PUT company-role-menus后该角色菜单绑定已覆盖更新 - [ ]
tenant-select分页从第一条开始(不再 Skip 掉首条) - [ ] (可选)改业务数据后
my-menus.lastUpdated接近当前时间
十三、主库建表 DDL(缺表报错时执行)
环境:antis-foodlabeling-host。若接口报 Table '...fl_th_tenant_admin_credential' doesn't exist,在主库执行:
CREATE TABLE IF NOT EXISTS `fl_th_tenant_admin_credential` (
`TenantId` char(36) NOT NULL COMMENT '租户Id,对应 YiTenant.Id',
`LoginAccount` varchar(64) NOT NULL DEFAULT '' COMMENT '管理员登录账号',
`PasswordCipher` varchar(512) NOT NULL DEFAULT '' COMMENT 'AES可回显密文',
`PasswordIv` varchar(64) NOT NULL DEFAULT '' COMMENT 'AES IV Base64',
`LastModificationTime` datetime(6) NULL COMMENT '最后修改时间',
PRIMARY KEY (`TenantId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='平台主库-租户管理员可回显凭据';
CREATE TABLE IF NOT EXISTS `fl_th_tenant_menu_permission` (
`Id` varchar(32) NOT NULL COMMENT '雪花Id',
`TenantId` char(36) NOT NULL COMMENT '租户Id',
`PermissionKey` varchar(128) NOT NULL COMMENT 'SaaS菜单权限Key',
`CreationTime` datetime(6) NOT NULL COMMENT '创建时间',
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_fl_th_tenant_menu_permission_tenant_key` (`TenantId`, `PermissionKey`),
KEY `IX_fl_th_tenant_menu_permission_TenantId` (`TenantId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='平台主库-租户SaaS菜单权限';
2026-07-21 已在
antis-foodlabeling-host执行上述 DDL(两表已存在)。