# 6-2 代码优化 本文档说明 **2026-06-02** 对美国版 **`/api/app/label`** 列表接口报错修复,以及 **适用 Region** 字段的接口约定。 --- ## 标签列表接口报错修复 ### 现象 `GET http://flus-test.3ffoodsafety.com/api/app/label?SkipCount=1&MaxResultCount=10` 返回 **500**,典型错误为 MySQL: - `Unknown column 'AppliedRegionType' in 'field list'` - 或 `Table 'xxx.fl_label_region' doesn't exist` ### 根因 后端已上线「适用 Region」逻辑,但测试库 **尚未执行 DDL**: - `fl_label` 缺少列 `AppliedRegionType` - 缺少表 `fl_label_region` ORM 在列表/筛选 SQL 中引用上述结构即失败。 ### 修复方式(代码 + 库) | 层级 | 处理 | |------|------| | **代码** | 新增 `LabelRegionSchemaHelper`:启动后首次访问探测 `information_schema`,无列/无表时走 **兼容路径**(仅按 `LocationId`、门店反推 Region 展示),列表不再因缺列 500 | | **实体** | `FlLabelDbEntity.AppliedRegionType` 标记 `IsIgnore`,读写经 Helper 的原始 SQL(列存在时) | | **数据库** | 须在环境执行 DDL,才能持久化 ALL/多选 Region(见下节脚本) | **DDL 脚本(可重复执行)**: `美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_region_scope.sql` ### 验证步骤 1. **未执行 DDL**:部署含本修复的后端 → `GET /api/app/label?SkipCount=1&MaxResultCount=10` 应 **200**,`region` 可由门店反推或显示「无」。 2. **执行 DDL 后**:重启应用(或等待 schema 缓存失效;生产建议重启)→ 新增/编辑可落库 `regionIds`、`AppliedRegionType=ALL`。 3. 库内确认: ```sql SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fl_label' AND COLUMN_NAME = 'AppliedRegionType'; SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fl_label_region'; ``` ### 涉及文件(本次修复) | 文件 | 说明 | |------|------| | `Helpers/LabelRegionSchemaHelper.cs` | 探测 schema、批量读 `AppliedRegionType`、按列更新 | | `Helpers/LabelRegionScopeHelper.cs` | 列表筛选/App 校验按 schema 分支 | | `Services/LabelAppService.cs` | 列表不再 SELECT 缺列;创建/更新后 `SetAppliedRegionTypeAsync` | | `Services/UsAppLabelingAppService.cs` | `labeling-tree` 按 schema 拼接条件 | | `Services/DbModels/FlLabelDbEntity.cs` | `AppliedRegionType` ORM 忽略 | | `scripts/fl_label_region_scope.sql` | 幂等 DDL | --- ## 标签 `/api/app/label` 适用 Region(接口说明) ### 背景 标签除 `locationId` 外,支持显式配置适用 **Region**(`fl_group.Id`),模式与 label-type / label-multiple-option 一致: - **ALL**:全选 Region - **SPECIFIED**:`regionIds` / `groupIds` 单选或多选 ### 存储 | 位置 | 字段 | 说明 | |------|------|------| | `fl_label` | `AppliedRegionType` | `ALL` / `SPECIFIED`(需 DDL) | | `fl_label_region` | `LabelId` + `GroupId` | Region 多选明细 | | `fl_label` | `LocationId` | 兼容 App;多 Region 未指定门店时取合并门店**第一家** | ### 接口一览 | 方法 | 路径 | 说明 | |------|------|------| | `GET` | `/api/app/label` | 分页列表;`GroupId` 按 Region 筛选 | | `GET` | `/api/app/label/{id}` | 详情(`id` = `LabelCode`) | | `POST` | `/api/app/label` | 新增 | | `PUT` | `/api/app/label/{id}` | 编辑 | | `DELETE` | `/api/app/label/{id}` | 逻辑删除 | **测试环境列表示例**: `http://flus-test.3ffoodsafety.com/api/app/label?SkipCount=1&MaxResultCount=10` **鉴权**:`Authorization: {登录返回的 data.token}`(已含 `Bearer ` 前缀) ### 列表请求参数 | 参数 | 类型 | 说明 | |------|------|------| | `SkipCount` | int | 跳过条数(分页) | | `MaxResultCount` | int | 每页条数 | | `Keyword` | string | 模糊:标签名、类别、类型、模板、产品名 | | `GroupId` | string | Region(`fl_group.Id`)筛选 | | `LocationId` | string | 门店筛选 | | `ProductId` | string | 产品筛选 | | `LabelCategoryId` | string | 标签类别 | | `LabelTypeId` | string | 标签类型 | | `TemplateCode` | string | 模板编码 | | `State` | bool | 启用状态 | | `Sorting` | string | 排序(可选) | **请求示例**: ```http GET /api/app/label?SkipCount=1&MaxResultCount=10 HTTP/1.1 Host: flus-test.3ffoodsafety.com Authorization: Bearer {access_token} ``` ### 列表响应(主要字段) | 字段 | 说明 | |------|------| | `totalCount` | 总条数 | | `items` | 行数组 | | `items[].id` | 标签 Code(`LabelCode`) | | `items[].labelName` | 名称 | | `items[].region` | Region 展示名;ALL 为 `All Regions` | | `items[].regionIds` / `groupIds` | Region Id 数组 | | `items[].appliedRegionType` | `ALL` / `SPECIFIED` | | `items[].locationName` | 门店名 | | `items[].products` | 关联产品名(逗号拼接) | | `items[].lastEdited` | 最后编辑时间 | ### 新增 / 编辑 Body(JSON) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `appliedRegionType` | string | 否 | `ALL` / `SPECIFIED`;有 `regionIds` 时按 SPECIFIED | | `regionIds` | string[] | 条件 | `fl_group.Id`,与 `groupIds` 合并去重 | | `groupIds` | string[] | 否 | 同 `regionIds` | | `locationId` | string | 条件 | 门店;须在 Region 内(SPECIFIED) | | `labelName` | string | 是 | 标签名称 | | `templateCode` | string | 是 | 模板编码 | | `labelCategoryId` | string | 是 | 标签类别 Id | | `labelTypeId` | string | 是 | 标签类型 Id | | `productIds` | string[] | 是 | 至少 1 个 | | `labelInfoJson` | object | 否 | 模板数据 | | `state` | bool | 否 | 默认 true | **全选 Region**: ```json { "labelName": "Demo", "templateCode": "TPL001", "appliedRegionType": "ALL", "regionIds": [], "labelCategoryId": "...", "labelTypeId": "...", "productIds": ["..."] } ``` **单选 Region**: ```json { "appliedRegionType": "SPECIFIED", "regionIds": ["{fl_group.Id}"], "locationId": "{location.Id}", "labelName": "...", "templateCode": "...", "labelCategoryId": "...", "labelTypeId": "...", "productIds": ["..."] } ``` ### 联调注意 | 现象 | 处理 | |------|------| | 列表仍 500 | 确认已部署含 `LabelRegionSchemaHelper` 的版本 | | `regionIds` 保存不上 | 执行 `fl_label_region_scope.sql` 并重启 | | ALL 不生效 | 需 `AppliedRegionType` 列;执行 DDL | | 未执行 DDL 时新增带 `regionIds` | 关联表不存在则仅更新 `locationId`,Region 关联不落库 | --- ## 与 6-1 文档关系 - `6-1代码优化.md`:首版 Region 功能说明(含 my-menus、team-member 等其它优化)。 - **本节(6-2)**:列表 500 **根因与兼容修复** + 接口速查;以 **6-2** 为准处理 label 列表报错与 DDL 要求。 --- ## Reports Print Log 日期格式统一(`print-log-list`) ### 背景 `GET /api/app/reports/print-log-list` 返回的 **`printedAt`** 原为 `DateTime` JSON(ISO / 客户端 `en-US` 再格式化),与 **`expiryDateText`**(来自 `PrintInputJson` 原始模板文案,如 `MM/dd/yyyy`)展示顺序不一致。要求两列统一为 **年月日在前**,且 **Expiration(`expiryDateText`)精确到分钟**。 ### 目标格式 | 字段 | 格式 | 示例 | |------|------|------| | `printedAt` | `yyyy-MM-dd HH:mm` | `2026-05-19 14:30` | | `expiryDateText` | `yyyy-MM-dd HH:mm`(可解析时) | `2026-06-02 18:00` | | 无数据 | 固定文案 | `无` | > 仅日期、无时分的数据解析后时间为 `00:00`。 ### 接口 | 项 | 说明 | |----|------| | 方法 | `GET` | | 路径 | `/api/app/reports/print-log-list` | | 鉴权 | Bearer Token | | 测试示例 | `http://flus-test.3ffoodsafety.com/api/app/reports/print-log-list?Sorting=PrintedAt+desc&StartDate=2026-05-04&EndDate=2026-06-02&SkipCount=1&MaxResultCount=10` | ### 请求参数(常用) | 参数 | 说明 | |------|------| | `SkipCount` / `MaxResultCount` | 分页 | | `Sorting` | 如 `PrintedAt desc` / `PrintedAt asc` | | `StartDate` / `EndDate` | 区间(`yyyy-MM-dd`,含起止日) | | `PartnerId` / `GroupId` / `LocationId` | 组织筛选 | | `Keyword` | 产品名等模糊 | ### 响应 `items[]`(与本次相关) | 字段 | 类型 | 说明 | |------|------|------| | `printedAt` | string | 打印时间,`yyyy-MM-dd HH:mm` | | `expiryDateText` | string | 保质期(Expiration),同上;无法解析时保留原文或 `无` | | `taskId` | string | 任务 Id(重打) | | `labelCode` | string | 门店当日序号 Label ID | | `productName` | string | 产品名 | | `templateText` | string | 模板展示 | | `printedByName` | string | 打印人 | | `locationText` | string | 门店 | **响应片段示例**: ```json { "totalCount": 42, "items": [ { "taskId": "…", "labelCode": "20260519-1", "printedAt": "2026-05-19 14:30", "expiryDateText": "2026-06-02 18:00", "productName": "Chicken Breast", "printedByName": "John", "locationText": "Store A (001)" } ] } ``` ### 实现说明 | 组件 | 说明 | |------|------| | `ReportsDateTimeDisplayHelper` | `FormatPrintedAt` / `FormatExpiryDisplay`,统一 `yyyy-MM-dd HH:mm` | | `ReportsPrintLogExpiryHelper.ExtractFormattedExpiryText` | 从 `PrintInputJson` 解析并格式化 Expiration | | `ReportsPrintLogListItemDto.PrintedAt` | 由 `DateTime` 改为 **string** 出参 | | PDF / Excel 导出 | 与列表相同格式 | | Web `ReportsView.tsx` | 直接展示接口字符串,不再 `en-US` 二次格式化 | --- ## App `get-print-log-list` 的 Label ID(`labelId`) ### 背景 `POST /api/app/us-app-labeling/get-print-log-list` 原先 `labelId` 返回 **`fl_label.Id`**(或界面误用 `labelCode` / `batchId`),与业务要求的 **门店当日打印序号** 不一致。管理端 Print Log 已使用 `ReportsPrintLogDailyLabelIdHelper`(`yyyyMMdd-n`),App 需对齐。 ### 规则 | 项 | 说明 | |----|------| | 范围 | 同一 **门店** + 同一 **自然日**(按 `PrintedAt`,无则 `CreationTime`) | | 排序 | 当日任务按打印时间升序,再按任务 `Id` 升序 | | 格式 | `{yyyyMMdd}-{n}`,如 `20260513-1`、`20260513-2` | | 非 | 不是 `fl_label.LabelCode`,也不是标签主键本身 | ### 接口 | 项 | 说明 | |----|------| | 方法 | `POST` | | 路径 | `/api/app/us-app-labeling/get-print-log-list` | | 鉴权 | App 登录 Bearer Token | **请求 Body 示例**: ```json { "locationId": "{门店Guid}", "skipCount": 1, "maxResultCount": 10 } ``` ### 响应 `items[]`(与 Label ID 相关) | 字段 | 说明 | |------|------| | `labelId` | **门店当日打印序号**(`yyyyMMdd-n`),列表 Ref / Label ID 展示用 | | `labelEntityId` | `fl_label.Id`(内部、重打关联) | | `labelCode` | `fl_label.LabelCode`(重打/预览入参,勿与 `labelId` 混用) | | `taskId` | `fl_label_print_task.Id`(重打主键) | **响应片段**: ```json { "totalCount": 3, "items": [ { "taskId": "…", "labelId": "20260513-1", "labelEntityId": "…", "labelCode": "LBL_xxx", "productName": "Chicken", "printedAt": "2026-05-13T10:30:00" }, { "taskId": "…", "labelId": "20260513-2", "labelEntityId": "…", "labelCode": "LBL_yyy" } ] } ``` ### 验证步骤 1. 同一门店当日连续打印多条 → `labelId` 为 `yyyyMMdd-1`、`yyyyMMdd-2` … 递增。 2. 跨日:次日首条从 `yyyyMMdd-1` 重新计数(日期段为当日 `yyyyMMdd`)。 3. 跨门店:各门店各自从 `-1` 计数(请求已限定 `locationId`)。 4. 重打仍用 `taskId` + `labelCode`,不依赖 `labelId` 序号。 ### 涉及文件 | 文件 | 说明 | |------|------| | `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | 序号计算(与 Web Print Log 共用) | | `Services/UsAppLabelingAppService.cs` | `GetPrintLogListAsync` | | `Contracts/Dtos/UsAppLabeling/PrintLogItemDto.cs` | `labelId` / `labelEntityId` / `labelCode` 注释 | | App `print-log.vue` | Ref 列展示 `labelId` | | App `types/usAppLabeling.ts` | 类型补充 | ### 验证步骤 1. 调用上述 `print-log-list`,检查 `printedAt`、`expiryDateText` 均为 `yyyy-MM-dd HH:mm` 或 `无`。 2. 对比改前:不应再出现 `05/19/2026` 与 `2026-05-19T…` 混用。 3. 导出 Excel/PDF 的 Printed at、Expiration 列与列表一致。 ### 涉及文件 | 文件 | 说明 | |------|------| | `Helpers/ReportsDateTimeDisplayHelper.cs` | 统一格式化 | | `Helpers/ReportsPrintLogExpiryHelper.cs` | `ExtractFormattedExpiryText` | | `Services/ReportsAppService.cs` | 列表 / 导出映射 | | `Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs` | `printedAt` 类型与注释 | | Web `ReportsView.tsx` | 列表展示 | --- ## 标签模板列表 Items 列(控件名称) ### 说明 标签模板列表需展示 **Items** 列:该模板画布上每个控件的名称,逗号拼接(如图:Barcode, Print Time, Print Date, …)。 > **接口路径**:`GET /api/app/label-template`(标签模板分页列表)。 > 若写成 `/api/app/location` 则为**门店列表**,不含模板 Items;请以本节路径为准。 ### 接口 | 项 | 说明 | |----|------| | 方法 | `GET` | | 路径 | `/api/app/label-template` | | 鉴权 | Web 登录 Bearer Token | | 示例 | `http://flus-test.3ffoodsafety.com/api/app/label-template?SkipCount=1&MaxResultCount=500` | ### 请求参数(常用) | 参数 | 说明 | |------|------| | `SkipCount` / `MaxResultCount` | 分页 | | `Keyword` | 模板名/编码模糊 | | `GroupId` / `LocationId` | Region / 门店范围筛选 | | `LabelType` | 标签类型 | | `State` | 启用状态 | | `Sorting` | 排序(可选) | ### 响应 `items[]`(本次新增) | 字段 | 类型 | 说明 | |------|------|------| | `items` | string | 控件名称逗号拼接(按 `OrderNum` 排序),无控件时为 `无` | | `itemNames` | string[] | 与 `items` 相同内容的数组 | | `contentsCount` | int | 控件数量(Contents 列) | | `templateName` / `templateCode` | string | 模板名 / 编码 | | `locationText` / `region` | string | 适用范围展示 | **展示名解析顺序**: 1. `fl_label_template_element.ElementName`(非空优先) 2. `TypeAdd` 中下划线后缀(如 `print_Print Time` → `Print Time`) 3. `ElementType` 映射(如 `BARCODE` → `Barcode`) **响应片段**: ```json { "totalCount": 12, "items": [ { "id": "TPL001", "templateName": "G'n'G 2x6", "contentsCount": 8, "items": "Barcode, Print Time, Print Date, Text Field, Duration Date, Label Name, Employee", "itemNames": ["Barcode", "Print Time", "Print Date", "Text Field", "Duration Date", "Label Name", "Employee"], "sizeText": "2x6inch" } ] } ``` ### 验证步骤 1. 打开 Web **Label Templates** 列表,Items 列与接口 `items` 一致。 2. 编辑模板增删控件后刷新列表,`items` / `contentsCount` 同步更新。 3. 无元素的模板:`items` 为 `无`,`contentsCount` 为 0。 ### 涉及文件 | 文件 | 说明 | |------|------| | `Helpers/LabelTemplateListItemsHelper.cs` | 批量汇总控件名 | | `Services/LabelTemplateAppService.cs` | `GetListAsync` 出参 | | `Contracts/Dtos/LabelTemplate/LabelTemplateGetListOutputDto.cs` | `items` / `itemNames` | | Web `labelTemplateService.ts` / `LabelTemplatesView.tsx` | 列表 Items 列展示 |