2026-07-21代码优化.md 15.4 KB

2026-07-21 代码优化

本文档说明 2026-07-21 后端改动。重点为 泰额版 平台级管理员:

  1. 「公司列表」返回账号密码、支持修改(前端密码 AES 加盐)
  2. 菜单权限分配角色菜单分配

另含美国版/泰额版 Dashboard Last Updated 修复。前端本次不改。


一、变更概述

说明
范围 泰额版 Yi.Abp.Net8(主);美国版 Last Updated 同步修复
公司 = 租户 平台主库 YiTenant;前端常把 name 映射为 companyName
账号密码 GET company-listPUT company-admin
菜单/角色 menu-permission-treecompany-menuscompany-rolescompany-role-menus
凭据存储 主库表 fl_th_tenant_admin_credential(可回显 AES 密文,非登录哈希)
菜单存储 主库表 fl_th_tenant_menu_permission(SaaS menuPermissionKeys
传输加盐 FoodLabeling:TenantSelectCrypto 一致(AES-CBC + passwordSalt=IV)
落库哈希 租户业务库 UserMD5Helper.GenerateSalt + SHA2EncodeUserPasswordHelper

二、平台公司列表(返账号密码)

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 密码来源优先级

  1. 主库 fl_th_tenant_admin_credential 中已保存的 AES 密文 / IV
  2. 无记录时回退 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 处理流程

  1. 主库校验租户存在,读取凭据表当前账号
  2. CurrentTenant.Change(tenantId) 定位租户库管理员用户
  3. 若改账号:校验 UserName 不重复后更新
  4. 若改密码:TenantSelectCredentialCipher.DecryptPasswordUserPasswordHelper.ApplyPlainPassword + EnsurePasswordColumnsPersistedAsync
  5. 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-drawermenuPermissionKeys 对齐。

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 dashboarddashboard:analytics
labeling labelinglabeling:labelslabeling:categorieslabeling:typeslabeling:templateslabeling:multiple-options
modules modulesmodules:trainingmodules:alertsmodules:tasksmodules:food-wastemodules:e-label
management managementmanagement:accountmanagement:menumanagement:devicesmanagement:reportsmanagement:invoicesmanagement:qr-codesmanagement:supportmanagement: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-menusmenuIds 为租户库 Menu.Id)。

十二、自检清单

  • [ ] 主库已建表 fl_th_tenant_admin_credentialfl_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-menusGET 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(两表已存在)。