# 6-11 代码优化 本文档说明 **2026-06-11** 对美国版 App 标签预览/打印相关的两项**纯后端**改造(**不改 Web / App 前端**): 1. **`POST /api/app/us-app-labeling/preview`** 出参 **`labelId`** 改为门店当日序号 `yyyyMMdd-n` 2. 模板 **Company 自动生成元素**:预览/打印时按门店从 **`fl_partner`** 填充公司名及可选地址字段 测试环境:`http://flus-test.3ffoodsafety.com` --- ## 一、Preview 出参 `labelId` 格式 ### 背景 预览页「Label ID」原先返回 **`fl_label.Id`**(GUID,如 `3a2192be-7b8f-e3e8-db9c-3a5e627b9222`),与业务要求不符。 业务规则:**Label ID = 某门店当日每次打印任务的递增序号**,与 Print Log、管理端报表一致: | 示例 | 含义 | |------|------| | `20260513-1` | 该门店 2026-05-13 当日第 1 次打印 | | `20260513-2` | 同日第 2 次 | | `20260514-1` | 次日重新从 1 计数 | 格式:`{yyyyMMdd}-{n}`(`n` 从 1 递增,按 `PrintedAt ?? CreationTime` 所在自然日、同一 `locationId` 统计)。 ### 接口说明 | 项目 | 内容 | |------|------| | 方法 | `POST` | | 路径 | `/api/app/us-app-labeling/preview` | | 鉴权 | Bearer Token | #### 入参(Body:`UsAppLabelPreviewInputVo`) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `locationId` | string | 是 | 门店 Id(`location.Id`) | | `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) | | `productId` | string | 否 | 预览产品 Id | | `baseTime` | DateTime | 否 | 基准时间;影响模板内日期/时间控件,也用于确定「哪一天的序号」;未传则用服务端当前时间 | | `printInputJson` | object | 否 | 打印时输入项 | #### 出参(`UsAppLabelPreviewDto`)变更 | 字段 | 变更前 | 变更后 | |------|--------|--------| | **`labelId`** | `fl_label.Id`(GUID) | 门店当日**下一个**打印序号 `yyyyMMdd-n` | 其余字段(`locationId`、`labelCode`、`template`、`labelLastEdited` 等)不变。 #### `labelId` 计算规则 1. 取 **`baseTime ?? 当前服务器时间`** 的日期部分 `yyyyMMdd`。 2. 统计该 **`locationId`** 在当日内已有打印任务数(`fl_label_print_task`,时间取 `PrintedAt ?? CreationTime`)。 3. **`labelId = {yyyyMMdd}-{已有任务数 + 1}`**(预览不落库,表示「若此刻点击 Print 将获得的序号」)。 4. 与 **`POST /api/app/us-app-labeling/get-print-log-list`**、管理端 **`GET /api/app/reports/print-log-list`** 使用同一 Helper:`ReportsPrintLogDailyLabelIdHelper`。 > **注意**:`labelId` 不是 `fl_label.LabelCode`,也不是 `fl_label.Id`。标签主键如需内部关联,请使用打印任务创建后的 `taskId` 或 print-log 中的 `labelEntityId`。 ### 请求示例(labelId) ```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-05-13T09:00:00" }' ``` ### 响应片段(示例) ```json { "labelId": "20260513-3", "locationId": "550e8400-e29b-41d4-a716-446655440000", "labelCode": "LBL0001", "labelLastEdited": "2026-06-01T08:30:09", "template": { } } ``` 若该门店在 `2026-05-13` 已有 2 条打印记录,预览返回 `20260513-3`;当日首条预览为 `20260513-1`。 ### 涉及代码(labelId) | 文件 | 说明 | |------|------| | `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | 新增 `ResolveNextDailyLabelIdAsync`(预览取下一序号) | | `Services/UsAppLabelingAppService.cs` | `PreviewAsync` 出参 `LabelId` 改用当日序号 | | `Dtos/UsAppLabeling/UsAppLabelPreviewDto.cs` | `labelId` XML 注释 | | `IServices/IUsAppLabelingAppService.cs` | 接口注释 | ### 验证步骤(labelId) 1. 选定门店,确认当日已有 N 条 `fl_label_print_task`(可用 print-log 列表核对)。 2. 调用 **preview**,`labelId` 应为 `{今日yyyyMMdd}-{N+1}`。 3. 执行 **print** 创建新任务后,print-log 中该任务 `labelId` 与预览时一致(同一时刻连续预览+打印)。 4. 修改 `baseTime` 为历史日期,序号应按该日任务数计算,而非「今天」。 #### SQL 抽查 ```sql SELECT Id, LocationId, PrintedAt, CreationTime FROM fl_label_print_task WHERE LocationId = '{locationId}' AND IFNULL(PrintedAt, CreationTime) >= CURDATE() AND IFNULL(PrintedAt, CreationTime) < DATE_ADD(CURDATE(), INTERVAL 1 DAY) ORDER BY IFNULL(PrintedAt, CreationTime), Id; ``` 当日行数 + 1 应与 preview 返回的 `-n` 部分一致。 --- ## 二、Company 自动生成元素(预览/打印填充) ### 背景 模板编辑器中的 **Company** 控件属于「按门店自动取数」类型。用户点击打印时,后端应: - **始终**展示当前门店所属 **Company 名称**(`fl_partner.PartnerName`) - **仅当模板元素 config 中勾选** Address / City / State / Zip / Email 时,才追加对应行;未勾选或库中无值则不展示该字段 勾选状态**不落独立表**,保存在 **`fl_label_template_element.ConfigJson`**(通过 `POST/PUT /api/app/label-template` 的 `elements[].config` 写入)。公司主数据来自 **`fl_partner`**。 > 本次**不提供** Web 属性面板 UI;需通过 **label-template 保存接口** 或 SQL 维护 `config`。 ### 元素识别规则 满足以下任一条件即视为 Company 自动生成元素(`PartnerCompanyDisplayHelper.IsCompanyAutoElement`): | 条件 | 说明 | |------|------| | `typeAdd = "auto_Company"` | 标准 Company 控件 | | `valueSourceType = "AUTO_DB"` 且 `typeAdd` 以 `auto_` 开头且含 `Company` | 兼容命名 | 常见组合:`elementType = "TEXT_STATIC"`,`valueSourceType = "AUTO_DB"`,`typeAdd = "auto_Company"`。 ### 模板 config(勾选字段) 写入 `elements[].config`(JSON),支持两种等价写法(可混用): **方式 A:数组 `companyIncludeFields`** | 数组值 | 含义 | 数据来源(`fl_partner`) | |--------|------|--------------------------| | `address` | 街道地址 | `Street` | | `city` | 城市 | `City` | | `state` | 州/省 | `StateCode` | | `zip` | 邮编 | `ZipCode` | | `email` | 邮箱 | `ContactEmail` | **方式 B:布尔开关** `includeAddress` / `includeCity` / `includeState` / `includeZip` / `includeEmail`(`true` 表示勾选) **示例(保存模板时)** ```json { "elementType": "TEXT_STATIC", "valueSourceType": "AUTO_DB", "typeAdd": "auto_Company", "name": "Company", "config": { "companyIncludeFields": ["address", "city", "state", "zip", "email"] } } ``` 或: ```json "config": { "includeAddress": true, "includeCity": true, "includeState": true, "includeZip": true, "includeEmail": false } ``` ### 展示文本格式 后端将解析结果写入 **`template.elements[].config.text`**(多行,`\n` 分隔): | 行序 | 内容 | 规则 | |------|------|------| | 第 1 行 | 公司名 | **固定输出**(有 `PartnerName` 时) | | 第 2 行 | 街道 | 仅勾选 `address` 且 `Street` 非空 | | 第 3 行 | 城市, 州, 邮编 | 勾选 city/state/zip 中任一项时,按「City, StateCode, ZipCode」逗号拼接(空段跳过) | | 第 4 行 | 邮箱 | 仅勾选 `email` 且 `ContactEmail` 非空 | **渲染示例** ``` Acme Foods Inc 123 Main Street New York, NY, 10001 sales@acme.com ``` 若仅勾选公司名(无任何 include),则 `text` 仅一行公司名。 ### 门店 → Company 解析 1. 入参 **`locationId`**(`location.Id`,Guid 字符串) 2. 查 `location` 表取 **`Partner`** 字段 3. 在 **`fl_partner`** 中按 **`Id = Partner`** 或 **`PartnerName = Partner`** 匹配(未删除) 4. 匹配失败时不抛错,`config.text` 保持原样(不填充) ### 入参要求 | 场景 | `locationId` | |------|----------------| | 模板**不含** Company 自动生成元素 | 可选(App preview 仍必填门店,与原有逻辑一致) | | 模板**含** Company 自动生成元素 | **`LabelPreviewResolveInputVo.locationId` 必填**;缺失返回 **400**:`预览/打印需要 locationId 以填充 Company 信息` | **App 调用链**:`UsAppLabelPreviewInputVo.locationId` → 内部 `LabelAppService.PreviewAsync(LabelPreviewResolveInputVo)` 时须**原样传入** `locationId`。 受影响接口(均通过 `LabelAppService.PreviewAsync` 渲染模板): | 方法 | 路径 | 说明 | |------|------|------| | POST | `/api/app/us-app-labeling/preview` | App 预览 | | POST | `/api/app/us-app-labeling/print` | App 打印(落库前解析模板) | | POST | `/api/app/label/preview`(或管理端等价预览) | Web 预览;含 Company 元素时 Body 须带 `locationId` | 重打 **`reprint`** 使用历史任务 `RenderTemplateJson` 快照,**不再**重新解析 Company。 ### 出参变更 无新增顶层字段;变更在 **`template.elements[]`** 内: | 字段 | 变更 | |------|------| | `elements[].config.text` | Company 元素由后端按上文规则写入多行文本 | ### 数据库与脚本 | 项 | 说明 | |----|------| | `fl_partner.PartnerName` | 公司名(必有) | | `fl_partner.Street` / `City` / `StateCode` / `ZipCode` / `ContactEmail` | 可选展示字段 | | `location.Partner` | 门店归属 Company 键(Id 或名称) | | DDL | `scripts/fl_partner_add_address_columns.sql`(缺地址列时执行) | 模板适用范围(Company/Region/Location 三维 scope)见 **`项目相关文档/6-4代码优化.md`**,与本节「元素内 Company 自动填值」相互独立。 ### 请求示例(含 Company 的 preview) ```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" }' ``` 响应 `template.elements` 中 Company 元素片段示例: ```json { "elementType": "TEXT_STATIC", "typeAdd": "auto_Company", "valueSourceType": "AUTO_DB", "config": { "companyIncludeFields": ["address", "city", "state", "zip"], "text": "Acme Foods Inc\n123 Main Street\nNew York, NY, 10001" } } ``` ### 涉及代码(Company) | 文件 | 说明 | |------|------| | `Helpers/PartnerCompanyDisplayHelper.cs` | 识别元素、解析 config、格式化文本、门店→Partner | | `Services/LabelAppService.cs` | `PreviewAsync` 填充 Company 的 `config.text` | | `Dtos/Label/LabelPreviewResolveInputVo.cs` | 新增 `locationId` | | `Services/UsAppLabelingAppService.cs` | preview/print 调用预览时传入 `locationId` | ### 验证步骤(Company) 1. 确认门店 `location.Partner` 能关联到有效 `fl_partner` 记录(地址列已执行 DDL)。 2. 模板含 `auto_Company` 元素,`config` 勾选若干 include 字段;保存后查 `fl_label_template_element.ConfigJson`。 3. 调用 **preview**,Body 带正确 `locationId`:`template.elements` 中 Company 的 `config.text` 行数与勾选一致。 4. 去掉 `locationId` 或传空 → 返回 400(仅当模板含 Company 元素时)。 5. 执行 **print**,检查 `fl_label_print_task.RenderTemplateJson` 内 Company `text` 与预览一致。 #### SQL 抽查 ```sql -- 门店归属 Company SELECT l.Id, l.Partner, p.PartnerName, p.Street, p.City, p.StateCode, p.ZipCode, p.ContactEmail FROM location l LEFT JOIN fl_partner p ON (p.Id = l.Partner OR p.PartnerName = l.Partner) AND p.IsDeleted = 0 WHERE l.Id = '{locationId}' AND l.IsDeleted = 0; -- 模板 Company 元素 config SELECT e.Id, e.TypeAdd, e.ValueSourceType, e.ConfigJson FROM fl_label_template_element e JOIN fl_label_template t ON t.Id = e.TemplateId WHERE t.TemplateCode = '{templateCode}' AND e.IsDeleted = 0 AND e.TypeAdd = 'auto_Company'; ``` --- ## 关联文档 - 同序号规则:`项目相关文档/6-2代码优化.md` → **App `get-print-log-list` 的 Label ID** - 模板三维 scope:`项目相关文档/6-4代码优化.md` → **`/api/app/label-template`** - App 预览页读取字段:`labelId` / `LabelId`(`preview.vue`)