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 必须做(闭环)
- 导出真实 TemplateJson 样本 + 单位校准单测(pt/mm)。
ILabelPdfRenderer:Parser → IR → QuestPDF;一期元素:text / barcode / qrcode / longText / hline / vline / rect / image。
LabelPrintDataBuilder:订单 → PrintData(附录 B 字段)。
- 订单字段:
LabelUrl(复用/确认)、LabelTemplateId、LabelGenerateStatus、LabelGenerateError;SQL 迁移。
- 异步任务
generateLabelPdf(复用现有批量队列机制)。
- 创单投递任务;
POST /order/print、batch/print 返回 PDF printUrl;Pending/Failed 前端提示。
- 销售产品:
LabelTemplateId + 下拉接 label-template/list;废弃枚举出单逻辑。
- 全局默认模板配置(配置表或 appsettings 键均可,先简单)。
- 设计器字段字典:
GET field-dictionary + vg-print 左侧可拖字段(至少单号/收件/重量常用组)。
- 仓内 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 要解决的问题
- 标签样式不可配置:订单创建后的「面单」目前是固定 HTML stub,无法按客户/产品定制版式。
- 设计器与打印链路断开:
vg-print 已能设计并保存 TemplateJson,但创建订单、单票打印、批量打印均未消费该模板。
- 批量打印需要稳定文件:仓内/客服批量出单希望拿到 PDF(可下载、可静默打印、可归档),而不是临时拼 HTML。
- 产品「客户面单」未绑模板:字段名虽叫客户面单,下拉却是写死的「系统面单/客户面单」,与
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,而是:
- 反序列化 JSON → C# 模型(
HiprintTemplate / HiprintPanel / HiprintElement)
- 灌数:按
options.field 从 PrintData 取值(无 field 则用 title 静态文案)
- 绘制:用 QuestPDF(或 PdfSharp)按 left/top/width/height 绝对定位画到页面
- 条码/二维码:
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 设计器待补
GET /channel/label-template/field-dictionary
- 注入 vg-print provider / 可拖字段分组
- 预览可用 Mock PrintData
(列表/新建弹窗/保存/删除已完成,勿重复建设独立列表页。)
6. 关键业务流程
6.1 设计与发布
- 运营「标签设计」→ 新建弹窗 / 设计 → 拖字段 → 预览 → 保存。
- 销售产品「客户面单」下拉选模板(
LabelTemplateId)。
- 配置全局默认模板(产品未选时回落)。
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 仓内「打印标签」
- lookup(已有)→ 确认打印。
- 优先订单
labelUrl;无则现场生成。
- 返回
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。
二期
- table 元素、启停、预览图。
- 地址变更 stale / 重生;PDF 合并。
- 客户自助设计;保存时不支持元素强校验。
三期(可选)
- 仅当 QuestPDF 还原成本过高时再评估 Playwright 兜底。
10. 风险与对策
| 风险 |
对策 |
| JSON 与 PDF 视觉偏差 |
支持矩阵;preview-pdf;pt/mm 单测 |
| 中文字体 |
服务端嵌入 Noto Sans SC / 雅黑 |
| 创单后立刻打印 |
Pending 短等或提示稍后 |
| 旧枚举数据 |
出单只认 LabelTemplateId |
| 批量高峰 |
异步队列限流 |
11. 验收标准(一期)
- 设计器模板(文本+条码+线框)经 .NET 出可扫码 PDF。
- 销售产品下拉为真实模板,存
LabelTemplateId。
- 创单异步后 Ready +
labelUrl 可打开。
- 单票/批量/仓内能打该 PDF;Pending 有提示。
- 无产品模板且无全局默认 → 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"
}