# 2026-07-22 代码优化 本文档说明 **2026-07-22** 后端改动。重点为 **泰额版** 租户业务库菜单表(`menu`)的增删改查接口。 > 与 **2026-07-21** 文档中的「平台 SaaS 菜单权限分配」(主库 `fl_th_tenant_menu_permission` / `menuPermissionKeys`)不同: > 本文接口操作的是**当前租户业务库**的 `menu` 表(RBAC 菜单资源),不是平台静态权限目录。 --- ## 一、变更概述 | 项 | 说明 | |----|------| | 范围 | 泰额版 `Yi.Abp.Net8` / `FoodLabeling.Th.*` | | 实现 | `ThRbacMenuAppService`(Swagger 分组:食品标签-泰额版接口) | | 数据表 | 租户业务库 `menu`(如 Default 租户对应 `antis-foodlabeling-us`) | | 路由前缀 | `/api/app/th-rbac-menu/*` | | Id 生成 | 新建菜单 Id 使用 `YitIdHelper.NextId().ToString()`(雪花字符串) | | 组树方式 | `MenuDbEntity` + 字符串 `ParentId` 自行组树;**禁止** `MenuAggregateRoot(Guid)` + `TreeHelper` | | 与旧接口关系 | 美国版模块仍保留 `/api/app/rbac-menu/*`;泰额前端建议改调 `th-rbac-menu` | --- ## 二、接口一览 鉴权:全部需登录(`Authorization: Bearer {token}`)。 Token 建议通过 `POST /api/app/th-web-auth/login` 获取(JWT 含 `TenantId`,操作当前租户业务库)。 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/app/th-rbac-menu/list` | 分页列表 | | GET | `/api/app/th-rbac-menu/{id}` | 单条详情 | | POST | `/api/app/th-rbac-menu` | 新增 | | PUT | `/api/app/th-rbac-menu/{id}` | 修改 | | DELETE | `/api/app/th-rbac-menu` | 批量逻辑删除 | | GET | `/api/app/th-rbac-menu/tree` | 全量菜单树(不分页) | > **路由说明**:ABP RootPath 为 `api/app`;自定义路由必须带 `th-rbac-menu/` 前缀,完整路径才是 `/api/app/th-rbac-menu/...`。 --- ## 三、分页列表 ### 3.1 接口 | 项 | 值 | |----|------| | 方法 / 路径 | `GET /api/app/th-rbac-menu/list` | | 说明 | 查询当前租户业务库未删除菜单,支持筛选与分页 | ### 3.2 入参(Query) | 字段 | 说明 | |------|------| | `skipCount` | 跳过条数 | | `maxResultCount` | 每页条数 | | `menuName` | 菜单名称模糊匹配(可选) | | `state` | 启用状态(可选) | | `menuSource` | 菜单来源枚举(可选) | | `menuType` | 菜单类型枚举(可选) | ### 3.3 出参示例 ```json { "totalCount": 2, "items": [ { "id": "1234567890123456789", "parentId": "0", "menuName": "Settings", "routerName": "settings", "router": "/settings", "permissionCode": "system:settings", "menuType": 1, "menuSource": 2, "orderNum": 10, "state": true } ] } ``` 排序:`orderNum` **降序**。 --- ## 四、单条详情 ### 4.1 接口 | 项 | 值 | |----|------| | 方法 / 路径 | `GET /api/app/th-rbac-menu/{id}` | | 说明 | 按主键查询未删除菜单;不存在返回业务友好错误 | 出参字段与列表 `items` 项一致(`ThRbacMenuGetListOutputDto`)。 --- ## 五、新增菜单 ### 5.1 接口 | 项 | 值 | |----|------| | 方法 / 路径 | `POST /api/app/th-rbac-menu` | | Content-Type | `application/json` | ### 5.2 入参 ```json { "menuName": "Settings", "parentId": "0", "menuType": 1, "menuSource": 2, "permissionCode": "system:settings", "router": "/settings", "routerName": "settings", "component": "views/settings/index", "menuIcon": "Setting", "orderNum": 10, "state": true, "isShow": true } ``` | 字段 | 必填 | 说明 | |------|------|------| | `menuName` | 是 | 菜单名称 | | `parentId` | 否 | 父级 Id;空或根用 `"0"`(兼容全 0 Guid) | | `menuType` | 是 | `MenuTypeEnum`:0=Catalogue,1=Menu,2=Component | | `menuSource` | 是 | `MenuSourceEnum`:0=Ruoyi,1=Pure,2=Vben5 | | `permissionCode` | 否 | 权限码 | | `router` / `routerName` / `component` / `menuIcon` | 否 | 路由与展示 | | `orderNum` | 否 | 排序号 | | `state` | 否 | 启用状态,默认 `true` | | `isShow` | 否 | 是否显示,默认 `true` | ### 5.3 处理规则 1. 校验 `menuName` 非空 2. 规范化 `parentId`;非根时校验父菜单存在且未删除 3. Id = `YitIdHelper.NextId().ToString()` 4. 插入当前租户业务库 `menu`,返回新建详情 --- ## 六、修改菜单 ### 6.1 接口 | 项 | 值 | |----|------| | 方法 / 路径 | `PUT /api/app/th-rbac-menu/{id}` | | Content-Type | `application/json` | 入参字段与新增相同(`ThRbacMenuUpdateInputVo` 继承 `ThRbacMenuCreateInputVo`)。 ### 6.2 处理规则 1. 菜单必须存在且未删除 2. `menuName` 必填 3. 禁止 `parentId` 等于自身 4. 禁止将菜单移动到其子节点下(一级校验) 5. 更新后返回最新详情 --- ## 七、批量删除 ### 7.1 接口 | 项 | 值 | |----|------| | 方法 / 路径 | `DELETE /api/app/th-rbac-menu` | | Content-Type | `application/json` | | 说明 | **逻辑删除**(`IsDeleted = true`),非物理删 | ### 7.2 入参 ```json ["1234567890123456789", "9876543210987654321"] ``` 空列表或全空白 Id 时直接返回(无操作)。 --- ## 八、菜单树 ### 8.1 接口 | 项 | 值 | |----|------| | 方法 / 路径 | `GET /api/app/th-rbac-menu/tree` | | 说明 | 返回当前租户全部未删除菜单,按字符串 `ParentId` 组树 | ### 8.2 出参要点 - 根节点:`parentId` 为 `"0"`(或全 0 Guid) - 子节点:`children` 数组嵌套 - 同级排序:`orderNum` **降序** - 树节点字段比列表更全(含 `menuIcon`、`isShow`、`component`、`remark`、审计字段等) ### 8.3 出参示例(节选) ```json [ { "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "parentId": "0", "menuName": "System", "menuType": 0, "menuSource": 2, "orderNum": 100, "state": true, "isShow": true, "children": [ { "id": "1234567890123456789", "parentId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "menuName": "Settings", "menuType": 1, "orderNum": 10, "children": [] } ] } ] ``` --- ## 九、数据表与枚举 ### 9.1 表:`menu`(租户业务库) | 字段 | 类型要点 | 说明 | |------|----------|------| | `Id` | varchar | 主键;历史多为 UUID 字符串,新建为雪花字符串 | | `ParentId` | varchar | 父级;根为 `"0"` | | `MenuName` | varchar | 菜单名称 | | `MenuType` | int | 见 `MenuTypeEnum` | | `MenuSource` | int | 见 `MenuSourceEnum` | | `IsDeleted` | tinyint/bool | 逻辑删除 | | 其他 | — | `Router`、`Component`、`PermissionCode`、`OrderNum`、`State`、`IsShow` 等 | 平台主库 `antis-foodlabeling-host` **无**此业务 `menu` 表。 ### 9.2 枚举 | 枚举 | 值 | |------|-----| | `MenuTypeEnum` | 0=Catalogue(目录),1=Menu(菜单),2=Component(组件/按钮) | | `MenuSourceEnum` | 0=Ruoyi,1=Pure,2=Vben5 | --- ## 十、代码文件 | 类型 | 路径 | |------|------| | 服务 | `FoodLabeling.Th.Application/Services/ThRbacMenuAppService.cs` | | 接口 | `FoodLabeling.Th.Application.Contracts/IServices/IThRbacMenuAppService.cs` | | DTO | `FoodLabeling.Th.Application.Contracts/Dtos/RbacMenu/ThRbacMenu*.cs` | | 实体映射 | 复用 `FoodLabeling.Application.Services.DbModels.MenuDbEntity`(字符串 Id) | --- ## 十一、curl 示例 ```bash # 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-rbac-menu/tree" \ -H "Authorization: Bearer " # 3) 分页列表 curl -X GET "http://127.0.0.1:19002/api/app/th-rbac-menu/list?skipCount=0&maxResultCount=20" \ -H "Authorization: Bearer " # 4) 新增 curl -X POST "http://127.0.0.1:19002/api/app/th-rbac-menu" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d "{\"menuName\":\"Settings\",\"parentId\":\"0\",\"menuType\":1,\"menuSource\":2,\"router\":\"/settings\",\"orderNum\":10,\"state\":true,\"isShow\":true}" # 5) 修改 curl -X PUT "http://127.0.0.1:19002/api/app/th-rbac-menu/" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d "{\"menuName\":\"Settings\",\"parentId\":\"0\",\"menuType\":1,\"menuSource\":2,\"orderNum\":20,\"state\":true,\"isShow\":true}" # 6) 批量删除 curl -X DELETE "http://127.0.0.1:19002/api/app/th-rbac-menu" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d "[\"\",\"\"]" ``` 新增后可用查库核对: ```sql SELECT Id, ParentId, MenuName, MenuType, MenuSource, IsDeleted, OrderNum FROM menu WHERE Id = '<新Id>'; ``` --- ## 十二、前端对接注意 1. 路由前缀使用 `/api/app/th-rbac-menu/*`,不要与旧 `/rbac-menu` 混淆。 2. 字段 camelCase:`menuName`、`parentId`、`menuType`、`menuSource`、`orderNum` 等。 3. 树管理页用 `/tree`;表格/筛选页用 `/list` 分页。 4. 根节点 `parentId` 传 `"0"`。 5. 必须在正确租户上下文下调用(登录 JWT 含 `TenantId`);写入的是该租户业务库,不是 host。 6. 部署后需**重启**后端进程,新路由才会注册生效。 --- ## 十三、与 2026-07-21「菜单权限」的区分 | 维度 | 2026-07-21 平台菜单权限 | 2026-07-22 业务菜单 CRUD(本文) | |------|-------------------------|----------------------------------| | 库 | 平台主库 `antis-foodlabeling-host` | 租户业务库(如 `antis-foodlabeling-us`) | | 表 | `fl_th_tenant_menu_permission` | `menu` | | 语义 | SaaS 公司可开通的功能 Key | RBAC 菜单资源(路由/组件/权限码) | | 典型接口 | `th-multi-tenancy/company-menus` 等 | `th-rbac-menu/*` |