front/docs/面单标签模板与PDF生成方案.md
zzh c4979a1fb4 feat: 面单标签设计与仓内操作页优化,接通模板打印链路
完善标签设计器/销售产品模板绑定与打印面单 UX,统一入库出库到仓扫码页布局,并补充库存绑定与相关文档配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 17:49:18 +08:00

22 KiB
Raw Permalink Blame History

OMS 面单标签模板设计与 PDF 生成方案

版本:v0.4(开工核对版)
日期:2026-07-18
基线文档:docs/oms系统设计-050801.md(§5.1 标签设计 / §5.2.20 t_label_template / §6 操作打印 / §9~10 订单 labelUrl)

已确认决策

项 结论
出 PDF 时机 异步(Pending → Ready / Failed)
产品「客户面单」 下拉选具体模板(LabelTemplateId),去掉写死「系统面单/客户面单」枚举作为出单依据
渲染引擎 纯 .NET:HiprintJson → QuestPDF;vg-print 只做设计
默认模板 产品未选时回落 全局默认模板 Id(配置项)
模板归属(一期) 仅运营/管理员设计与维护(客户自助 → 二期)
默认纸张(一期) 100×150 mm(模板可改;未填时用此默认)

落地进度(相对本文)

模块 状态 说明
标签设计列表 + 新建弹窗 + 懒加载设计器 已做 labelDesign:list/get/save/delete;新建弹窗填名称/类型再进设计
模板 CRUD API 已做 GET list / GET {id} / POST save / POST delete
Hiprint→QuestPDF 渲染器 已做 Parser/IR/QuestPDF + 样本单测
异步出 PDF + 订单状态字段 已做 Pending/Ready/Failed;创单投递 generate-label-pdf
替换 HTML stub / 批量&仓内打 PDF 已做 print / batch / 仓内返回 PDF printUrl;无模板时回落 HTML
IsEnabled / 纸张字段 / enable API 一期可砍 用「有无模板」即可;纸张从 JSON panel 读
独立 labelTemplate 列表页 取消 已并入 labelDesign,勿再建
设计器字段字典注入 已做 field-dictionary + 设计器左侧字段
销售产品绑 LabelTemplateId 已做 下拉接模板列表

0. 一期开工范围(核对无遗漏后按此做)

0.1 必须做(闭环)

  1. 导出真实 TemplateJson 样本 + 单位校准单测(pt/mm)。
  2. ILabelPdfRenderer:Parser → IR → QuestPDF;一期元素:text / barcode / qrcode / longText / hline / vline / rect / image。
  3. LabelPrintDataBuilder:订单 → PrintData(附录 B 字段)。
  4. 订单字段:LabelUrl(复用/确认)、LabelTemplateId、LabelGenerateStatus、LabelGenerateError;SQL 迁移。
  5. 异步任务 generateLabelPdf(复用现有批量队列机制)。
  6. 创单投递任务;POST /order/print、batch/print 返回 PDF printUrl;Pending/Failed 前端提示。
  7. 销售产品:LabelTemplateId + 下拉接 label-template/list;废弃枚举出单逻辑。
  8. 全局默认模板配置(配置表或 appsettings 键均可,先简单)。
  9. 设计器字段字典:GET field-dictionary + vg-print 左侧可拖字段(至少单号/收件/重量常用组)。
  10. 仓内 print-label:返回 printUrl;模板下拉改真实列表(可选覆盖)。

0.2 一期明确不做(避免范围膨胀)

  • Playwright / 浏览器渲染
  • table / html 元素、页眉页脚高级规则
  • 模板启停 API、预览图、客户自助设计
  • PDF 合并下载、地址变更自动 stale(二期)
  • 平台店铺 ShippingLabelUrl

0.3 建议实施顺序

① JSON 样本 + Renderer 单测
② PrintDataBuilder + preview-pdf(管理端验收版式)
③ 订单字段迁移 + 异步 generateLabelPdf + 创单挂钩
④ 销售产品 LabelTemplateId 下拉
⑤ 设计器字段字典
⑥ order/print + batch/print + 仓内 printUrl

1. 背景与目标

1.1 要解决的问题

  1. 标签样式不可配置:订单创建后的「面单」目前是固定 HTML stub,无法按客户/产品定制版式。
  2. 设计器与打印链路断开:vg-print 已能设计并保存 TemplateJson,但创建订单、单票打印、批量打印均未消费该模板。
  3. 批量打印需要稳定文件:仓内/客服批量出单希望拿到 PDF(可下载、可静默打印、可归档),而不是临时拼 HTML。
  4. 产品「客户面单」未绑模板:字段名虽叫客户面单,下拉却是写死的「系统面单/客户面单」,与 t_label_template 无关。

1.2 目标(一期)

目标 说明
可设计 vg-print 设计,拖拽绑定订单字段,保存 TemplateJson
可保存 模板 CRUD(名称、类型;列表浏览/删除)— 已有
可绑定 销售产品「客户面单」改为 模板下拉(LabelTemplateId)
可出 PDF 纯 .NET 解析 TemplateJson + PrintData → 异步生成 PDF
可批量 按 labelUrl 批量静默打 / 下载

1.3 非目标(一期不做)

  • 平台店铺面单(Shopee 等)仍走平台 URL。
  • 不在服务端跑 Playwright/Chromium。
  • 不保证 hiprint 全部高级特性 100% 还原(见 §3.3 支持矩阵);一期覆盖面单常用元素。
  • 不做模板审批流 / 模板市场 / 客户自助设计。

2. 现状结论(Gap)

flowchart LR
  A[vg-print 设计器] -->|保存 TemplateJson| B[(t_label_template)]
  C[销售产品 CustomerWaybill] -.->|仅枚举字符串| X[未关联模板]
  D[创建订单 / 打单] -->|CreateOrderPrintFileAsync| E[极简 HTML]
  F[仓内 print-label] -->|只写记录| G[(t_label_print_record)]
  H[批量 print] --> E
  B -.->|未消费| D
能力 现状 缺口
设计器 列表+懒加载设计器 已有;新建弹窗 已有 字段字典未注入
模板存储 t_label_template CRUD 已有 纸张可读 panel;启停可砍
产品绑定 CustomerWaybill 写死两枚举 → LabelTemplateId
渲染 HTML stub HiprintJson→PDF
仓内打印 记日志 返回/打开 PDF
批量 有框架 依赖真实 labelUrl(PDF)

3. 总体方案

3.1 核心思路

把链路拆成四段,职责清晰:

阶段 做什么 产物
① 设计 vg-print 配置版式,左侧拖订单字段占位符 TemplateJson
② 绑定 销售产品「客户面单」下拉选模板 SalesProduct.LabelTemplateId
③ 灌数出 PDF 异步:.NET 解析 TemplateJson + PrintData → PDF Order.LabelUrl
④ 打印 单票 / 批量 / 仓内,统一打 labelUrl 打印机 / 下载

3.2 架构(纯 .NET 渲染 + 客户端静默打)

sequenceDiagram
  participant Designer as vg-print 设计器
  participant API as OMS API (.NET)
  participant DB as DB
  participant Worker as 异步 Worker
  participant FE as 订单/仓内前端
  participant Hiprint as 本地 hiprint 客户端

  Designer->>API: 保存 TemplateJson
  API->>DB: t_label_template
  Note over API,DB: 产品「客户面单」下拉存 LabelTemplateId

  FE->>API: 创建订单
  API->>DB: 落单 + Pending + 投递 generateLabelPdf
  Worker->>DB: 读模板 JSON + 组 PrintData
  Worker->>Worker: HiprintJsonRenderer → QuestPDF
  Worker->>DB: 写 PDF + Ready/labelUrl

  FE->>API: 打印(Ready)
  API-->>FE: printUrl (PDF)
  FE->>Hiprint: 静默打印 PDF
  • 设计:继续 vg-print(前端)。
  • 出文件:纯 .NET,不起浏览器。
  • 送打印机:前端 hiprint 客户端打已生成的 PDF(与现网批量打印一致)。

3.3 纯 .NET 如何「转化使用」vg-print 保存的 JSON(已确认)

3.3.1 思路

vg-print / hiprint 的 TemplateJson 本质是 声明式版式:纸张尺寸 + 绝对定位元素列表(文本/条码/线/图/表…)。
我们不在服务端执行 JS,而是:

  1. 反序列化 JSON → C# 模型(HiprintTemplate / HiprintPanel / HiprintElement)
  2. 灌数:按 options.field 从 PrintData 取值(无 field 则用 title 静态文案)
  3. 绘制:用 QuestPDF(或 PdfSharp)按 left/top/width/height 绝对定位画到页面
  4. 条码/二维码:options.textType = barcode | qrcode 时用 ZXing.Net 生成图片再嵌入
TemplateJson (hiprint)
        │
        ▼
 HiprintTemplateParser   ← 只认我们支持的元素子集
        │
        ▼
 Intermediate IR         ← 统一:PageSize + List<DrawOp>
        │
        ▼
 QuestPdfEmitter         ← 输出 PDF bytes

中间 IR 的好处:以后若换 PdfSharp,只换 Emitter;Parser 不动。

3.3.2 JSON 结构(需解析的关键部分)

典型形态(示意):

{
  "panels": [
    {
      "index": 0,
      "width": 100,
      "height": 150,
      "printElements": [
        {
          "printElementType": { "type": "text" },
          "options": {
            "left": 20, "top": 30, "width": 120, "height": 20,
            "field": "serviceNo",
            "title": "运单号",
            "fontSize": 12,
            "fontWeight": "bold",
            "textAlign": "left",
            "textType": "text"
          }
        },
        {
          "printElementType": { "type": "text" },
          "options": {
            "left": 20, "top": 60, "width": 160, "height": 40,
            "field": "serviceNo",
            "textType": "barcode"
          }
        }
      ]
    }
  ]
}

坐标约定(实现时用真实样本校准一次):

项 hiprint 常见单位 .NET 处理
纸张 width/height mm QuestPDF PageSizes / 自定义 mm
元素 left/top/width/height pt(1 inch = 72 pt) 换算:pt → mm = pt * 25.4 / 72
fontSize pt 直接用

落地第一步:从设计器导出 1~2 份真实 JSON 做单元测试,校准单位(不同 vg-print 版本偶有差异)。

3.3.3 元素支持矩阵(一期 / 二期)

元素 type / textType 一期 实现要点
text(普通文本) ✅ 灌 field;字体/对齐/颜色/粗体
text + barcode ✅ ZXing Code128(可配置)
text + qrcode ✅ ZXing QR
longText ✅ 多行换行、超出裁剪或缩小
hline / vline ✅ 线宽、颜色
rect ✅ 边框/填充(简化)
image ✅ 静态图 URL/相对路径;或 field 指向图片地址
table 二期 按 columns + items[] 画表;一期可用「多行 text」凑合
html ❌ 一期跳过 设计规范禁止用自定义 HTML 做面单关键信息
页眉页脚/页码高级规则 二期 面单多为单页,一期可忽略

设计规范(避免踩坑):面单模板只用支持矩阵内的控件;保存模板时可做服务端校验,遇到不支持类型给出警告。

3.3.4 关键代码落点(建议)

组件 位置(建议)
HiprintTemplateDocument 等模型 Oms.Application/Labeling/Hiprint/
HiprintTemplateParser 同上
ILabelPdfRenderer / QuestPdfLabelRenderer 同上
LabelPrintDataBuilder 订单 → PrintData 字典
GenerateLabelPdfJobHandler 异步队列 Worker
NuGet QuestPDF、ZXing.Net(及 ImageSharp 如需)

接口形态:

public interface ILabelPdfRenderer
{
    Task<byte[]> RenderAsync(string templateJson, IReadOnlyDictionary<string, object?> printData, CancellationToken ct = default);
}

3.3.5 与「设计预览」的对齐策略

手段 说明
设计器预览 仍用 vg-print 前端预览(设计时)
服务端预览 API POST .../preview-pdf 走同一套 ILabelPdfRenderer,给产品配置/验收用
回归用例 固定 TemplateJson + PrintData → 对比 PDF 哈希或关键区域截图

预期:常用面单元素视觉接近;像素级与浏览器 hiprint 不完全一致(字体 hinting 等),以「可扫码、信息齐全、布局不错位」为验收标准。

3.3.6 为何仍划算(相对 Playwright)

  • 无 Chromium 运维,部署与现有 API 一致。
  • 异步高峰更可控(线程池而非浏览器池)。
  • 设计器继续复用,不是推倒重做;成本在「解析器 + 支持矩阵」,可控且可测。

3.4 异步出 PDF(已确认)

创单:落单 → 解析产品 LabelTemplateId(空则全局默认)→ LabelGenerateStatus=Pending → 投递 generateLabelPdf → 立即返回。

Worker:PrintData → ILabelPdfRenderer → 落盘 → Ready / Failed。

打印:Ready 用 labelUrl;Pending 提示生成中或短等补生成;Failed 可「重新生成」。

3.5 产品「客户面单」字段改造(已确认)

现状:customerWaybillOptions 写死:

{ label: '系统面单', value: '系统面单' },
{ label: '客户面单', value: '客户面单' },

与模板库无关,不能表达「用哪张版式」。

改造:

项 做法
UI 「客户面单」下拉改为拉 GET /channel/label-template/list(启用中的模板),展示 Name
存储 新增/使用 LabelTemplateId(long,可空)
旧字段 CustomerWaybill 迁移期可废弃写入,或仅作展示兼容;出 PDF 只认 LabelTemplateId
未选模板 使用系统配置的 全局默认模板 Id;若默认也没有 → 异步任务 Failed,并提示去配置

仓内打印页的模板下拉同理:不要写死 lastMile/customer/... 类型枚举当「模板」,应拉真实模板列表(类型可作筛选)。

平台店铺面单 URL 仍独立,不进该下拉。

4. 领域模型

4.1 模板 t_label_template

字段 一期 说明
Id / Name / TemplateType / TemplateJson ✅ 已有 CRUD 已通
PaperWidthMm / PaperHeightMm 可选 优先从 TemplateJson.panels[0] 读;缺省 100×150
IsEnabled / PreviewImageUrl / Remark 二期 一期不做启停

4.2 销售产品绑定

字段 说明
LabelTemplateId 主字段:客户面单下拉选中的模板 Id
CustomerWaybill 旧枚举;迁移后停止作为出单依据(可留列不清)

解析:产品.LabelTemplateId → 否则 → 全局默认模板Id → 否则 Failed。

4.3 订单侧产物

字段 说明
LabelUrl PDF 路径(替换 HTML stub)
LabelTemplateId 实际模板 Id
LabelGenerateStatus Pending / Ready / Failed
LabelGenerateError 失败原因
PrintData 快照 建议一期就落 json 列或扩展表,便于重打

4.4 仓内打印记录

  • print-label/print 返回 printUrl;可选写 PrintFileUrl 到记录表。

设计页交互(已落地)

  • 默认列表;新建弹窗(名称+类型)→ 懒加载设计器;返回卸载。
  • API:list / get / save / delete。
  • 待补:字段字典注入。

5. 字段字典(设计器左侧可拖字段)

设计器不应让业务人员手写字段名。一期提供 标准 PrintData Schema,前端注入 vg-print 的元素/字段面板。

5.1 命名约定

  • 全部 camelCase,与前端/hiprint print(data) 一致。
  • 地址类同时提供分行字段与 recipientAddressFull。

5.2 字段分组(初稿)

A. 单号

字段 说明
serviceNo 运单号 / 服务商单号
clientNo 客户单号
trackNo 跟踪号
otherNo 其他单号(若有)
requestId 请求号

B. 产品 / 渠道 / 仓库

字段 说明
productCode 销售产品编码
salesProductName 销售产品名称
channelCode 渠道编码
channelName 渠道名称
warehouseName 仓库名称
customerCode / customerName 客户(管理端场景)

C. 收件人

字段 说明
recipientName 姓名
recipientCompany 公司
recipientPhone 电话
recipientEmail 邮箱
recipientZip 邮编
recipientCountry / recipientCountryName 国家
recipientProvince / City / Street1/2/3 省市区街道
recipientAddressFull 拼接地址
recipientTaxNo / vatNo / eori 税号类

D. 发件人

对称字段:senderName、senderCompany、senderPhone、senderAddressFull …

E. 重量尺寸

字段 说明
actualWeight 实重
volumetricWeight 体积重
chargeableWeight 计费重
length / width / height 长宽高
weightUnit 默认 KG
dimUnit 默认 CM

F. 货品明细(表格 — 二期)

"items": [
  { "skuCode": "", "skuName": "", "qty": 1, "weight": 0.1, "declareNameEn": "", "declareValue": 0 }
]

一期可用多个 text 字段代替。

G. 系统

字段 说明
printTime 出 PDF 时间
operatorName 操作人(仓内打印时)

5.3 设计器待补

  1. GET /channel/label-template/field-dictionary
  2. 注入 vg-print provider / 可拖字段分组
  3. 预览可用 Mock PrintData

(列表/新建弹窗/保存/删除已完成,勿重复建设独立列表页。)


6. 关键业务流程

6.1 设计与发布

  1. 运营「标签设计」→ 新建弹窗 / 设计 → 拖字段 → 预览 → 保存。
  2. 销售产品「客户面单」下拉选模板(LabelTemplateId)。
  3. 配置全局默认模板(产品未选时回落)。

6.2 创建订单 → 生成 PDF

触发 建议
订单创建成功 主路径:异步 generateLabelPdf
预报/生成包裹 地址重量补全后可 重生
地址变更 二期:labelStale
resolveTemplate(product) → buildPrintData(order) → renderPdf
→ /uploads/prints/orders/xxx.pdf → labelUrl + Ready

6.3 单票 / 批量打印

  • 单票 / 批量:返回 PDF printUrl;Pending 提示生成中;Failed 可重生。
  • 合并下载 → 二期。

6.4 仓内「打印标签」

  1. lookup(已有)→ 确认打印。
  2. 优先订单 labelUrl;无则现场生成。
  3. 返回 printUrl;模板下拉接真实列表。

7. API 设计(增量)

7.1 模板

方法 路径 一期
POST save /channel/label-template/save ✅
GET list /channel/label-template/list ✅
GET detail /channel/label-template/{id} ✅
POST delete /channel/label-template/delete ✅
GET field-dictionary /channel/label-template/field-dictionary 待做
POST preview-pdf /channel/label-template/preview-pdf 待做
POST enable /channel/label-template/enable 砍掉(二期)

7.2 销售产品

  • Save/Detail 增加 labelTemplateId;下拉接 list。

7.3 订单 / 打印

方法 说明
创单内部 投递 generateLabelPdf(非同步堵接口)
POST /order/print PDF printUrl
POST /order/batch/print 每单 PDF url
POST /order/label/regenerate 强制重生

7.4 仓内

方法 说明
POST /operation/print-label/print 返回 printUrl

7.5 渲染(进程内)

Task<byte[]> RenderAsync(string templateJson, IReadOnlyDictionary<string, object?> printData, CancellationToken ct = default);

8. 前端改造清单

页面/模块 状态
channel/labelDesign ✅ 列表+弹窗+懒加载;⏳ 字段字典
channel/labelTemplate 独立页 ❌ 取消
channel/salesProducts ⏳ 模板下拉
OrderPrintModal / 我的订单批量 ⏳ PDF + Pending 提示
operation/printLabel ⏳ printUrl + 真实模板下拉
权限文案 随菜单既有资源即可

9. 分阶段落地

一期(按 §0.3 顺序)

见文首 §0。

二期

  1. table 元素、启停、预览图。
  2. 地址变更 stale / 重生;PDF 合并。
  3. 客户自助设计;保存时不支持元素强校验。

三期(可选)

  • 仅当 QuestPDF 还原成本过高时再评估 Playwright 兜底。

10. 风险与对策

风险 对策
JSON 与 PDF 视觉偏差 支持矩阵;preview-pdf;pt/mm 单测
中文字体 服务端嵌入 Noto Sans SC / 雅黑
创单后立刻打印 Pending 短等或提示稍后
旧枚举数据 出单只认 LabelTemplateId
批量高峰 异步队列限流

11. 验收标准(一期)

  1. 设计器模板(文本+条码+线框)经 .NET 出可扫码 PDF。
  2. 销售产品下拉为真实模板,存 LabelTemplateId。
  3. 创单异步后 Ready + labelUrl 可打开。
  4. 单票/批量/仓内能打该 PDF;Pending 有提示。
  5. 无产品模板且无全局默认 → Failed + 明确提示。

12. 决策状态

# 项 状态
1 渲染:纯 .NET + Hiprint JSON 已确认
2 出 PDF:异步 已确认
3 产品:模板下拉 已确认
4 模板归属 一期:仅运营(已拍板默认)
5 默认纸张 一期:100×150 mm(已拍板默认)

结论:方案可开工。 无阻塞性遗漏;下一刀从 §0.3 ①「真实 JSON 样本 + Renderer」开始。


附录 A. 与现有文档章节映射

本方案 设计总册
设计器 / TemplateJson §5.1、§5.2.20、§5.3.12
仓内打印记录 §6.1、§6.2.3、§6.3.3
订单 labelUrl / batch print §4.4.1.8、§9.6.3、§10
批量异步 订单批量处理与异步队列方案0707.md

附录 B. PrintData 示例(节选)

{
  "serviceNo": "S202607180001",
  "clientNo": "C-10086",
  "trackNo": "YT1234567890",
  "salesProductName": "专线-普货",
  "warehouseName": "SZ-01",
  "recipientName": "John Lee",
  "recipientPhone": "+1-xxx",
  "recipientCountryName": "United States",
  "recipientAddressFull": "xxx Street, City, CA 90001",
  "actualWeight": 1.25,
  "length": 30,
  "width": 20,
  "height": 10,
  "items": [
    { "skuCode": "SKU001", "skuName": "T-Shirt", "qty": 2 }
  ],
  "printTime": "2026-07-18 14:00:00"
}