# 6-23 代码优化 本文档说明 **2026-06-23** 对美国版标签(Label)接口的多门店绑定改造:一个标签可绑定多个门店(`locationIds` 数组),关联表为 `fl_label_location`。 --- ## 一、背景与目标 | 项目 | 说明 | |------|------| | 原行为 | 标签仅通过 `fl_label.LocationId` 绑定单门店 | | 新行为 | **`locationIds` 为主字段**,落库 `fl_label_location`;`fl_label.LocationId` 保留为首项兼容列 | | 兼容 | 未建 `fl_label_location` 时仍读/写 `LocationId`;已建表时列表/筛选/App 打印校验均命中关联表 | --- ## 二、数据库迁移 脚本路径:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_location_scope.sql` | 步骤 | 内容 | |------|------| | 1 | 创建 `fl_label_location`(`LabelId` + `LocationId`) | | 2 | 若关联表为空,将现有 `fl_label.LocationId` 迁移写入关联表 | **执行前请备份数据库。** --- ## 三、`locationIds` 字段规范 ### 入参(POST / PUT) | 字段 | 类型 | 说明 | |------|------|------| | `locationIds` | `string[]` | **主字段**。适用门店 Id(`location.Id`),全量写入 `fl_label_location` | | `locationId` | `string` | **兼容**。与 `locationIds` 合并;合并后首项写入 `fl_label.LocationId` | | `appliedRegionType` | `string` | `ALL` / `SPECIFIED` | | `regionIds` / `groupIds` | `string[]` | Region(`fl_group.Id`),落库 `fl_label_region`;可与 `locationIds` 组合校验 | **解析规则(`ResolveScopeForSaveAsync`)** - `appliedRegionType=ALL` 且未传 region:仅校验/保存 `locationIds`(可为空表示全部门店) - `appliedRegionType=SPECIFIED`:须传 `regionIds` 和/或 `locationIds`;显式 `locationIds` 须在 Region 展开门店范围内 - `locationId` 与 `locationIds` 合并去重,`locationId` 排在数组首位 ### 出参(GET 列表 / 详情) | 字段 | 说明 | |------|------| | `locationIds` | 门店 Id 数组(来自 `fl_label_location`,无关联表时回退 `LocationId`) | | `location` / `locationName` | 门店名称逗号拼接展示 | | `locationId` | **兼容**:等于 `locationIds[0]`,无门店时为空 | --- ## 四、接口说明 ### 1. 标签列表 | 项目 | 内容 | |------|------| | 方法 | `GET` | | 路径 | `/api/app/label` | | 鉴权 | Bearer Token | **筛选(Query / data)** | 参数 | 说明 | |------|------| | `SkipCount` | 页码,从 1 起 | | `MaxResultCount` | 每页条数 | | `GroupId` | 按 Region 筛选:命中 `fl_label_region`、`fl_label_location` 或 `AppliedRegionType=ALL` | | `LocationId` | 按门店筛选:命中 `fl_label_location` 或兼容列 `LocationId` | | `PartnerId` | 按 Company 下门店集合筛选(同 Location 逻辑) | | `ProductId` / `Keyword` / `LabelCategoryId` / `LabelTypeId` / `TemplateCode` / `State` | 其它原有筛选 | **出参 `items[]` 新增/变更** | 字段 | 说明 | |------|------| | `locationIds` | 适用门店 Id 数组 | | `locationName` | 门店展示名(逗号拼接) | | `region` / `regionIds` / `appliedRegionType` | Region 范围(逻辑不变) | --- ### 2. 标签详情 | 项目 | 内容 | |------|------| | 方法 | `GET` | | 路径 | `/api/app/label/{id}` | | 路径参数 | `id` = 标签 `LabelCode` | **出参要点** | 字段 | 说明 | |------|------| | `locationIds` | 适用门店数组 | | `location` | 门店展示名 | | `locationId` | 兼容首项 | | `regionIds` / `groupIds` / `appliedRegionType` | Region 范围 | --- ### 3. 新增标签 | 项目 | 内容 | |------|------| | 方法 | `POST` | | 路径 | `/api/app/label` | **Body 示例** ```json { "labelName": "Price Label A", "templateCode": "TPL_PRICE_2X2", "labelCategoryId": "...", "labelTypeId": "...", "productIds": ["..."], "appliedRegionType": "SPECIFIED", "regionIds": ["group-id-1"], "locationIds": ["loc-id-1", "loc-id-2"], "state": true } ``` **落库** - `fl_label.LocationId` ← `locationIds[0]` - `fl_label_location` ← 全量 `locationIds` - `fl_label_region` ← `regionIds`(SPECIFIED 时) --- ### 4. 编辑标签 | 项目 | 内容 | |------|------| | 方法 | `PUT` | | 路径 | `/api/app/label/{id}` | | Body | 同 POST;`locationIds` 全量覆盖关联表 | --- ### 5. 删除标签 逻辑删除 `fl_label`,并删除 `fl_label_region`、`fl_label_location`、`fl_label_product` 关联行(表存在时)。 --- ## 五、App 打印校验 `UsAppLabelingAppService.PrintAsync` / `PreviewAsync` 使用 `LabelRegionScopeHelper.EnsureLabelAppliesToLocationAsync` / `LabelAppliesToLocationAsync`,支持: - `AppliedRegionType=ALL` 且配置了 `fl_label_location`:须在关联门店内 - `SPECIFIED`:命中 `fl_label_location`、`LocationId` 或 `fl_label_region` --- ## 六、代码改动摘要 | 文件 | 改动 | |------|------| | `LabelRegionScopeHelper.cs` | 多门店解析/保存/列表筛选/展示;`ApplyLabelRegionListFilter` 用 SQL `EXISTS` 查 `fl_label_location` | | `LabelAppService.cs` | CRUD 读写 `locationIds`;列表筛选走 Helper | | `LabelCreateInputVo` / `LabelGetOutputDto` / `LabelGetListOutputDto` | 增加 `locationIds` 等字段 | | `UsAppLabelingAppService.cs` | 打印/预览门店校验对齐多门店 | | `ILabelAppService.cs` | XML 注释补充 `locationIds` | --- ## 七、向后兼容 | 场景 | 行为 | |------|------| | 仅传 `locationId` | 合并为单元素 `locationIds` | | 未执行 SQL 迁移 | 无 `fl_label_location` 时读写 `fl_label.LocationId` | | 前端仍读 `locationId` | 详情/列表返回首项,不影响旧页面 | --- ## 八、部署注意 1. 测试/生产库执行 `fl_label_location_scope.sql` 2. `dotnet build` 编译 `FoodLabeling.Application` 后重启 Web 进程 3. 管理端表单改为多选 `locationIds`;旧单选 `locationId` 可逐步下线 --- ## 九、请求示例 ### 登录获取 Token ```bash curl -X POST "http://flus-test.3ffoodsafety.com/api/oauth/Login" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "userName=admin&password=123456" ``` ### 标签列表(第一页) ```bash curl -G "http://flus-test.3ffoodsafety.com/api/app/label" \ -H "Authorization: Bearer {token}" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" ``` ### 按门店筛选 ```bash curl -G "http://flus-test.3ffoodsafety.com/api/app/label" \ -H "Authorization: Bearer {token}" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" \ --data-urlencode "LocationId={location.Id}" ``` ### 新增标签(多门店) ```bash curl -X POST "http://flus-test.3ffoodsafety.com/api/app/label" \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "labelName": "Price Label A", "templateCode": "TPL_PRICE_2X2", "labelCategoryId": "...", "labelTypeId": "...", "productIds": ["..."], "appliedRegionType": "SPECIFIED", "regionIds": ["group-id-1"], "locationIds": ["loc-id-1", "loc-id-2"], "state": true }' ``` ### 编辑标签(全量覆盖 locationIds) ```bash curl -X PUT "http://flus-test.3ffoodsafety.com/api/app/label/{labelCode}" \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "labelName": "Price Label A", "templateCode": "TPL_PRICE_2X2", "labelCategoryId": "...", "labelTypeId": "...", "productIds": ["..."], "locationIds": ["loc-id-1", "loc-id-3"], "state": true }' ``` ### 详情 ```bash curl -G "http://flus-test.3ffoodsafety.com/api/app/label/{labelCode}" \ -H "Authorization: Bearer {token}" ``` ### 响应片段(详情) ```json { "id": "LBL0001", "labelName": "Price Label A", "locationIds": ["loc-id-1", "loc-id-2"], "locationId": "loc-id-1", "location": "Store A, Store B", "appliedRegionType": "SPECIFIED", "regionIds": ["group-id-1"], "region": "West Region" } ``` --- ## 十、标签模板列表 500 修复(`GET /api/app/label-template`) ### 现象 `GET /api/app/label-template?SkipCount=1&MaxResultCount=10` 返回 **500**,日志: ```text Unknown column 'AppliedPartnerType' in 'field list' ``` 对应 SQL 会 SELECT 库中不存在的列:`AppliedPartnerType`、`AppliedRegionType`、`BorderType`。 **MCP 查库(当前测试库)**:`fl_label_template` 仅有 `AppliedLocationType`、`PrintOrientation` 等基础列;**无** scope 列与 `BorderType`;**无** `fl_label_template_partner` / `fl_label_template_region` 表(仅有 `fl_label_template_location`)。 ### 根因 `FlLabelTemplateDbEntity` 仍映射 ORM 属性到不存在的列,SqlSugar 生成 `SELECT` 时全部拼入,未执行 `fl_label_template_scope.sql` 的库直接报错。 ### 修复说明 | 改动 | 说明 | |------|------| | `FlLabelTemplateDbEntity` | **删除** `AppliedPartnerType` / `AppliedRegionType` / `BorderType` 属性 | | `LabelTemplateQueryHelper.ProjectListColumns` | 列表/详情只读查询 **显式 Select** 真实列 | | `LabelTemplateQueryHelper.ApplyListSorting` | 默认 `IFNULL(LastModificationTime, CreationTime) DESC`;`Sorting` 白名单 | | `LabelTemplateScopeSchemaHelper` | 探测 scope / border 列;已迁移时用 raw SQL 读写 | | `LabelTemplateScopeHelper.SaveTemplateScopeAsync` | 保存关联表后调用 `SetAppliedScopeTypesAsync` | | `LabelTemplateAppService.GetListAsync` | 排序后 `ProjectListColumns` 再分页 | > Company/Region 展示与 Id 数组仍由 `fl_label_template_location`(及已迁移时的 partner/region 关联表)推断,**不依赖**主表 scope 列。 ### 接口说明 | 项目 | 内容 | |------|------| | 方法 | `GET` | | 路径 | `/api/app/label-template` | | 鉴权 | Bearer Token | **入参(Query)** | 参数 | 说明 | |------|------| | `SkipCount` | 页码,从 1 起 | | `MaxResultCount` | 每页条数 | | `Keyword` | 模板名称/编码模糊搜索 | | `PartnerId` / `GroupId` / `LocationId` | 适用范围筛选 | | `LabelType` / `State` / `Sorting` | 其它筛选与排序(`Sorting` 白名单字段) | **出参 `items[]` 要点** | 字段 | 说明 | |------|------| | `id` / `templateCode` | 模板编码 | | `company` / `region` / `location` | 适用范围展示 | | `partnerIds` / `regionIds` / `locationIds` | 对应 Id 数组 | | `printOrientation` | 打印方向 `vertical` / `horizontal` | | `sizeText` / `contentsCount` / `lastEdited` | 尺寸、控件数、最近编辑 | ### 请求示例 ```bash curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \ -H "Authorization: Bearer {token}" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" ``` ### 部署注意 1. **必须**重新编译 `FoodLabeling.Application` 并**完全重启** Web 进程;若 Yi-SQL 仍出现 `AppliedPartnerType`,说明旧 DLL 未替换。 2. 可选:执行 `scripts/fl_label_template_scope.sql` 启用 Company/Region 主表列与关联表(非列表 500 的必要条件)。 3. 可选:为 `BorderType` 增加列后,`SetBorderTypeAsync` / `GetBorderTypeForTemplateAsync` 会自动读写;未迁移时详情/预览 `border` 默认为 `none`。 --- ## 十一、我的菜单 500 修复(`GET /api/app/auth-session/my-menus`) ### 现象 `GET /api/app/auth-session/my-menus` 返回 **500**,日志: ```text Unknown column 'r.AccessPermissionCodesJson' in 'field list' ``` 对应 SQL 错误地 SELECT 了 `Role.AccessPermissionCodesJson`。 ### 根因 | 层级 | 名称 | |------|------| | C# 属性 | `RoleAggregateRoot.AccessPermissionCodesJson` | | **数据库列** | **`Role.AccessPermissionCodes`**(`VARCHAR`,JSON 数组字符串) | `UserRoleAccessPermissionHelper` 直写 SQL 时误用了 C# 属性名作为列名,导致 MySQL 报列不存在。 **MCP 查库**:`Role` 表**有** `AccessPermissionCodes` 列,**无** `AccessPermissionCodesJson` 列。 ### 修复说明 | 文件 | 改动 | |------|------| | `RoleAccessPermissionSchemaHelper.cs` | **新增**:探测 `Role.AccessPermissionCodes` 列是否存在 | | `UserRoleAccessPermissionHelper.cs` | SQL 改为读 `AccessPermissionCodes`;列不存在时返回空数组 | | DTO 注释 | 统一说明库字段名为 `AccessPermissionCodes` | 未执行列迁移的环境(无 `AccessPermissionCodes`)接口仍可返回菜单树,`accessPermissionCodes` 为空数组,不 500。 ### 接口说明 | 项目 | 内容 | |------|------| | 方法 | `GET` | | 路径 | `/api/app/auth-session/my-menus` | | 鉴权 | Bearer Token(须已登录) | **出参要点(`CurrentUserMenuPermissionsOutputDto`)** | 字段 | 说明 | |------|------| | `user` | 当前用户摘要(id、userName、nick、email 等) | | `roleCodes` | JWT 中的角色编码列表 | | `role` | 角色展示名 | | `menus` | 树形菜单(admin 返回全部未删除菜单;其它用户按 RoleMenu 关联) | | `permissionCodes` | 可见菜单的 `PermissionCode` 去重列表 | | `accessPermissionCodes` | 绑定角色 `AccessPermissionCodes` JSON 合并(如 `manage_people`、`manage_labels`) | | `lastUpdated` | 用户最后修改时间 | **`accessPermissionCodes` 与菜单的关系** - 菜单可见性仍由 **RoleMenu + Menu** 决定; - `accessPermissionCodes` 用于前端/Account Management 等「访问权限」勾选项(Region/Location 管理等能力开关); - 列未迁移时该数组为空,不影响基础菜单加载。 ### 数据库(可选迁移) 若库中尚无 `AccessPermissionCodes` 列,可执行: `scripts/repair_after_clear_keep_admin_menu_role.sql` 第 1 段,或: ```sql ALTER TABLE `Role` ADD COLUMN `AccessPermissionCodes` VARCHAR(2000) NULL COMMENT '访问权限 JSON 数组,如 ["manage_labels","manage_people"]'; ``` ### 请求示例 ```bash curl -G "http://flus-test.3ffoodsafety.com/api/app/auth-session/my-menus" \ -H "Authorization: Bearer {token}" ``` ### 响应片段 ```json { "user": { "id": "...", "userName": "admin", "nick": "Admin" }, "roleCodes": ["admin"], "permissionCodes": ["menu.labels", "menu.reports"], "accessPermissionCodes": ["manage_labels", "manage_people"], "menus": [ { "id": "...", "menuName": "Labels", "router": "/labels", "children": [] } ] } ``` ### 部署注意 1. 重新编译并重启 Web 进程; 2. 若仍报 `AccessPermissionCodesJson`,说明旧 DLL 未替换; 3. 无 `AccessPermissionCodes` 列时接口可正常返回菜单,仅 `accessPermissionCodes` 为空。