# 6-18 代码优化 本文档说明 **2026-06-18** 对 **`GET /api/app/label-template`** 列表接口的第二轮修复。 6-17 已完成 scope 库结构兼容(未迁移 `fl_label_template_partner` / `fl_label_template_region` 时仍可查询),但测试环境 Web **Label Templates** 页仍返回 **500**,Network 中: `GET /api/app/label-template?SkipCount=1&MaxResultCount=10` 测试环境:`http://flus-test.3ffoodsafety.com` --- ## 一、现象 - 页面提示:**Failed to load label templates. Request failed.** - 接口 HTTP **500**,响应体 `errors` 为空,无具体异常文案。 - 同页 `partner` / `group` / `location` 下拉接口可正常加载。 > `SkipCount=1` 表示**第 1 页**(页码从 1 起),不是报错原因。详见 `Helpers/PagedQueryConvention.cs`。 --- ## 二、根因 服务端日志已明确报错: ```text Unknown column 'AppliedPartnerType' in 'field list' ``` 对应 SQL: ```sql SELECT ... `AppliedLocationType`,`AppliedPartnerType`,`AppliedRegionType`, ... FROM `fl_label_template` WHERE NOT ( `IsDeleted`=1 ) ORDER BY IFNULL(`LastModificationTime`,`CreationTime`) DESC LIMIT 0,10 ``` ### 2.1 主因:ORM 实体仍映射不存在列 6-17 曾在 `FlLabelTemplateDbEntity` 上对 `AppliedPartnerType` / `AppliedRegionType` 标记 `[SugarColumn(IsIgnore = true)]`,但当前 SqlSugar 运行时**仍会**把这两列拼进 `SELECT`(与 `fl_label.AppliedRegionType` 的 `IsIgnore` 行为不一致),导致未执行 `fl_label_template_scope.sql` 的库直接 500。 **MCP 查库(测试库)**: - `fl_label_template` **无** `AppliedPartnerType` / `AppliedRegionType` 列 - **无** `fl_label_template_partner` / `fl_label_template_region` 表 - **有** `AppliedLocationType` 与 `fl_label_template_location` ### 2.2 次因:列表 SQL 其它隐患(一并修复) | # | 问题 | 后果 | |---|------|------| | 1 | 默认排序曾用 `LastModificationTime ?? CreationTime` | SqlSugar 翻译失败 → 500 | | 2 | `Sorting` 直接 `OrderBy(input.Sorting)` 拼 SQL | 非法字段 / 注入风险 | | 3 | 空 `templateIds` 仍 `Contains` 查询 | 可能生成 `IN ()` | | 4 | 无效 partner/group 筛选未安全降级 | 未捕获异常 | --- ## 三、修复说明 ### 1. 从 ORM 实体移除不存在列 + 强制列投影(核心) | 改动 | 说明 | |------|------| | `FlLabelTemplateDbEntity` | **删除** `AppliedPartnerType` / `AppliedRegionType` 属性 | | **`LabelTemplateQueryHelper.ProjectListColumns`** | 列表/详情/重复校验等只读查询 **显式 Select** 真实列,SQL 不再出现 scope 列 | | `LabelTemplateScopeSchemaHelper` | 探测列/表;已迁移时用 raw SQL 写入 scope 列 | | `LabelTemplateAppService` | `GetListAsync` 在排序后调用 `ProjectListColumns` | > **重要**:若 Yi-SQL 日志仍出现 `AppliedPartnerType`,说明进程加载的是**旧 DLL**(`FoodLabeling.Application` 未重新编译或未重启)。请先 `dotnet build` 通过后再**完全停止并重启** `Yi.Abp.Web`。 ### 2. 安全排序 新增 `ApplyLabelTemplateListSorting`: - 默认:`ORDER BY IFNULL(LastModificationTime, CreationTime) DESC, TemplateCode ASC` - `Sorting` 白名单字段 + asc/desc ### 3. 列表查询健壮性 | 改动 | 说明 | |------|------| | `input ??= new()`、`pageSize` 默认 10 | 入参/分页兜底 | | `templateIds.Count > 0` 再查 elements/items | 避免空 `IN ()` | | `ResolveFilteredLocationIdsForListAsync` | 无效 partner/group 返回空列表 | | `DeleteAsync` | 仅当 scope 关联表存在时才删除 partner/region 行 | ### 4. 与 6-17 的关系 6-17 用 `IsIgnore` 试图跳过列映射,**实测无效**;6-18 改为**实体不含列 + raw SQL 按需写入**,与 `fl_label.AppliedRegionType` 处理方式一致。 --- ## 四、接口说明 | 项目 | 内容 | |------|------| | 方法 | `GET` | | 路径 | `/api/app/label-template` | | 鉴权 | Bearer Token(`Authorization: {data.token}`,`data.token` 已含 `Bearer ` 前缀) | ### 入参(Query) | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `SkipCount` | int | 否 | **页码,从 1 起**;第一页传 `1` | | `MaxResultCount` | int | 否 | 每页条数;`<=0` 时后端按 **10** 处理 | | `Keyword` | string | 否 | 模板名称 / 编码模糊搜索 | | `PartnerId` | string | 否 | 按 Company(`fl_partner.Id`)筛选 | | `GroupId` | string | 否 | 按 Region(`fl_group.Id`)筛选 | | `LocationId` | string | 否 | 按门店(`location.Id`)筛选;**优先于** Partner/Region | | `LabelType` | string | 否 | 如 `PRICE` / `NUTRITION` | | `State` | bool | 否 | 启用状态 | | `Sorting` | string | 否 | 白名单:`TemplateName asc/desc`、`TemplateCode asc/desc`、`CreationTime asc/desc`、`LastModificationTime asc/desc`;其它值忽略并走默认排序 | 筛选解析顺序:**LocationId → GroupId → PartnerId**;均未传则不按门店范围收窄(仍返回全部未删除模板,除非前端传了无效 Id 则返回空列表)。 ### 出参(`PagedResultWithPageDto`) | 字段 | 说明 | |------|------| | `pageIndex` | 当前页码 | | `pageSize` | 每页条数 | | `totalCount` | 总条数 | | `totalPages` | 总页数 | | `items[]` | 模板列表 | **`items[]` 主要字段** | 字段 | 说明 | |------|------| | `id` / `templateCode` | 模板编码(前端主键) | | `templateName` | 模板名称 | | `company` / `region` / `location` | 适用范围展示;未迁移 scope 库时 Company/Region 为 `All Companies` / `All Regions` | | `partnerIds` / `regionIds` / `locationIds` | 对应 Id 数组 | | `items` / `itemNames` | 模板内控件名称 | | `contentsCount` | 控件数量 | | `sizeText` | 如 `2x2inch` | | `lastEdited` | 最近编辑时间(`LastModificationTime ?? CreationTime`) | --- ## 五、请求示例 ### 登录获取 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-template" \ -H "Authorization: Bearer {token}" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" ``` ### 带 Company 筛选 ```bash curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \ -H "Authorization: Bearer {token}" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" \ --data-urlencode "PartnerId={fl_partner.Id}" ``` ### 指定排序 ```bash curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \ -H "Authorization: Bearer {token}" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" \ --data-urlencode "Sorting=TemplateName asc" ``` ### 响应片段(示例) ```json { "pageIndex": 1, "pageSize": 10, "totalCount": 3, "totalPages": 1, "items": [ { "id": "tpl_n0b5h9_mpyssitm", "templateCode": "tpl_n0b5h9_mpyssitm", "templateName": "Retail Label w/Price Copy", "company": "All Companies", "region": "All Regions", "location": "Ordos Airport, Store B", "items": "Label Name, Price, Barcode", "contentsCount": 5, "sizeText": "2x2inch", "lastEdited": "2026-06-04T08:30:00" } ] } ``` --- ## 六、验证步骤 0. **重新编译并重启**(必做): ```bash cd "美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web" dotnet build ``` 停止正在运行的 Web 进程后重新启动;确认 Yi-SQL 中 **不再出现** `AppliedPartnerType`。 1. **部署**包含 6-17 + 6-18 的后端并重启。 2. 调用 `GET /api/app/label-template?SkipCount=1&MaxResultCount=10` → 应 **200**,`totalCount >= 0`。 3. Web **Label Templates** 列表可加载,不再出现红色 **Failed to load label templates**。 4. 传无效 `PartnerId` / `GroupId` → **200** 且 `items=[]`(非 500)。 5. 不传 `Sorting` → 按最近编辑时间降序;传 `Sorting=TemplateName asc` → 按名称升序。 6. 分页:第 2 页 `SkipCount=2`,`totalCount` 与 UI 一致。 ### SQL 抽查(修复后 ORM 应生成的列) ```sql SELECT Id, TemplateCode, TemplateName, AppliedLocationType, IFNULL(LastModificationTime, CreationTime) AS LastEdited FROM fl_label_template WHERE IsDeleted = 0 ORDER BY IFNULL(LastModificationTime, CreationTime) DESC, TemplateCode ASC LIMIT 10; ``` **不应再出现** `AppliedPartnerType` / `AppliedRegionType`。 ### 检查 scope 是否已迁移 ```sql SELECT COUNT(*) AS scope_table_cnt FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('fl_label_template_partner', 'fl_label_template_region'); ``` `scope_table_cnt = 0` 时行为与 6-17 一致:仅 Location 维度落库与展示。 --- ## 七、涉及代码 | 文件 | 说明 | |------|------| | `Helpers/LabelTemplateQueryHelper.cs` | **新增** `ProjectListColumns` 强制 SQL 列白名单 | | `Services/DbModels/FlLabelTemplateDbEntity.cs` | **移除** Partner/Region 列属性 | | `Helpers/LabelTemplateScopeSchemaHelper.cs` | 列/表探测 + `SetAppliedScopeTypesAsync` raw SQL | | `Services/LabelTemplateAppService.cs` | 安全排序、Create/Update 写 scope 列、列表健壮性 | | `Helpers/LabelTemplateScopeHelper.cs` | 未迁移库 scope 过滤/展示(6-17) | | `Helpers/LocationScopeBindingHelper.cs` | `ResolveFilteredLocationIdsForListAsync` | | `Helpers/LabelTemplateListItemsHelper.cs` | Items 列 | | `Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs` | `PartnerId` 筛选 | --- ## 八、数据库迁移(可选) 需完整 Company / Region 三维 scope 时,执行: `美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_template_scope.sql` 未执行前:**列表可正常返回**(6-17 + 6-18),但无法持久化 Company/Region 多选明细。 --- ## 九、App 标签预览 `POST /api/app/us-app-labeling/preview` ### 现象 App 标签预览页调用 preview 失败(500 或 400),常与 **label-template 列表** 同源:加载模板头时 ORM 仍 SELECT 不存在的 `AppliedPartnerType` / `AppliedRegionType`。 另有两项逻辑缺陷(与 `6-11` 文档不一致): | # | 问题 | 后果 | |---|------|------| | 1 | `LabelAppService.PreviewAsync` 全表查询 `FlLabelTemplateDbEntity` | 未迁移 scope 列时 **Unknown column** → 500 | | 2 | `UsAppLabelingAppService.PreviewAsync` 未向 `_labelAppService.PreviewAsync` 传 **`locationId`** | 模板含 Company 自动生成元素时报 **「预览/打印需要 locationId 以填充 Company 信息」** | | 3 | 出参 **`labelId`** 仍返回 `fl_label.Id`(GUID) | 与 Print Log 当日序号 `yyyyMMdd-n` 不一致 | ### 修复说明 | 改动 | 说明 | |------|------| | `LabelAppService.PreviewAsync` | 模板头查询改用 `LabelTemplateQueryHelper.ProjectListColumns` | | `UsAppLabelingAppService.PreviewAsync` | 传入 `LocationId`;`labelId` 改用 `ReportsPrintLogDailyLabelIdHelper.ResolveNextDailyLabelIdAsync` | | `UsAppLabelingAppService.PrintAsync` | 解析模板时同步传入 `LocationId`(打印与预览 Company 填充一致) | | `DashboardAppService` | 模板统计 Count 同样走 `ProjectListColumns` | ### 接口说明 | 项目 | 内容 | |------|------| | 方法 | `POST` | | 路径 | `/api/app/us-app-labeling/preview` | | 鉴权 | App Bearer Token | #### 入参(Body:`UsAppLabelPreviewInputVo`) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `locationId` | string | 是 | 当前门店 Id(`location.Id`) | | `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) | | `productId` | string | 否 | 预览产品 Id;不传则取标签绑定第一个产品 | | `baseTime` | DateTime | 否 | 日期/时间控件基准;也用于计算当日 `labelId` 序号;未传为服务器当前时间 | | `printInputJson` | object | 否 | `PRINT_INPUT` 元素用户输入 | #### 出参(`UsAppLabelPreviewDto`) | 字段 | 说明 | |------|------| | **`labelId`** | 门店当日**下一个**打印序号 `yyyyMMdd-n`(预览不落库) | | `locationId` / `labelCode` | 回传入参 | | `template` | 已解析 AUTO_DB / PRINT_INPUT 的模板结构(含 Company 自动填充) | | `labelLastEdited` | 标签最近编辑时间 | | 其它 | `typeName`、`productName`、`templateProductDefaultValues` 等 | #### `labelId` 规则 与 `6-11`、`get-print-log-list` 一致:`{baseTime 日期 yyyyMMdd}-{当日已有打印任务数 + 1}`。 #### Company 自动元素 模板含 Company 自动生成控件时,后端根据 **`locationId`** 查 `fl_partner` 填充 `config.text`(详见 `6-11` 第二节)。 ### 请求示例 ```bash curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/preview" \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "locationId": "550e8400-e29b-41d4-a716-446655440000", "labelCode": "LBL0001", "productId": "PROD001", "baseTime": "2026-06-18T09:00:00" }' ``` ### 响应片段(示例) ```json { "labelId": "20260618-3", "locationId": "550e8400-e29b-41d4-a716-446655440000", "labelCode": "LBL0001", "labelLastEdited": "2026-06-01T08:30:09", "template": { "id": "tpl_xxx", "width": 2, "height": 2, "unit": "inch", "elements": [] } } ``` ### 验证步骤 1. 停服 → `dotnet build` → 重启(同 label-template 一节)。 2. App 进入标签预览页,Network 中 preview 应 **200**。 3. 响应 `labelId` 为 `yyyyMMdd-n`,非 GUID。 4. 模板含 Company 元素时,`template.elements` 中对应 `config.text` 为门店所属公司名。 5. 当日已有 N 条打印任务时,preview 返回 `-{N+1}`。 ### 涉及代码(preview) | 文件 | 说明 | |------|------| | `Services/UsAppLabelingAppService.cs` | `PreviewAsync` / `PrintAsync` 传 `LocationId`、当日 `labelId` | | `Services/LabelAppService.cs` | `PreviewAsync` 模板头 `ProjectListColumns` | | `Helpers/LabelTemplateQueryHelper.cs` | 列投影 | | `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | `ResolveNextDailyLabelIdAsync` | | `Helpers/PartnerCompanyDisplayHelper.cs` | Company 自动填充 | --- ## 十、App 打印日志 `POST /api/app/us-app-labeling/get-print-log-list` ### 现象 Postman 传 `printDate: "2026-06-16"` 返回 `totalCount: 0`,误以为接口异常。 ### 根因(查库核对) 门店 `3a218397-8dda-a378-e024-ef89bcef8d24` 在测试库中: | 自然日(`DATE(IFNULL(PrintedAt, CreationTime))`) | 记录数 | |---------------------------------------------------|--------| | `2026-06-01` | **7** | | `2026-05-31` | **3** | | `2026-06-16` | **0** | **接口按入参日期筛选时,该日无数据则返回空列表,行为正确。** 文档/示例误用 `2026-06-16`,应改用 **`2026-06-01`** 验证。 另:`PrintedAt` 入库多为 `null`,筛选实际走 **`CreationTime`**。 ### 本轮修复 | 改动 | 说明 | |------|------| | 日期条件 | 改用 MySQL `DATE(IFNULL(t.PrintedAt, t.CreationTime)) = 'yyyy-MM-dd'`,避免 DateTime 区间比较时区偏差 | | `printDateDay` | 新增字符串入参(`yyyy-MM-dd`),优先于 `printDate`,避免 JSON 仅日期 UTC 歧义 | | 未传日期 | **`printDate` 与 `printDateDay` 均未传时不按日过滤**(返回该门店全部打印记录,兼容 App 未传参) | | `labelId` | 列表出参改为当日序号 `yyyyMMdd-n`;`labelEntityId` 为 `fl_label.Id` | ### 请求示例(有数据的日期) ```bash curl -X POST "http://192.168.1.4:19001/api/app/us-app-labeling/get-print-log-list" \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "locationId": "3a218397-8dda-a378-e024-ef89bcef8d24", "skipCount": 1, "maxResultCount": 20, "printDateDay": "2026-06-01" }' ``` 或使用 `"printDate": "2026-06-01"`(等价)。 ### 权限说明 非 admin / 非 Partner 角色时,仅返回 **`CreatedBy = 当前用户`** 的记录;若 Token 用户不是打印人,即使日期正确也会空列表。 ### 部署 修改在 `FoodLabeling.Application`,需 **停服 → dotnet build → 重启** 后 Postman 才生效。 ### 涉及代码 | 文件 | 说明 | |------|------| | `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | `ResolvePrintLogFilterCalendarDay`、`ApplyPrintTaskCalendarDayFilter` | | `Dtos/UsAppLabeling/PrintLogGetListInputVo.cs` | `PrintDateDay` | | `Services/UsAppLabelingAppService.cs` | `GetPrintLogListAsync` | --- ## 关联文档 - App Preview labelId / Company:`项目相关文档/6-11代码优化.md` - 第一轮 scope 兼容:`项目相关文档/6-17代码优化.md`(第一节、第二节 get-print-log-list) - 三维 scope 业务规则:`项目相关文档/6-4代码优化.md` - 分页约定:`Helpers/PagedQueryConvention.cs`