来源：https://basicapp.docs.xihanfun.com/backend/printing

# 打印模板

打印能力由独立模块 `modules/XiHan.BasicApp.Printing` 承载（后端）与 `frontend/src/modules/printing` + `@xihan/printing` 功能包承载（前端）：可视化设计打印模板、按编码解析、浏览器预览打印与本机静默直打。不需要此能力的部署可整体卸载模块（见仓库 README「卸载可选模块」）。

## 能力与边界

| 能力 | 依赖 | 说明 |
| --- | --- | --- |
| 模板设计器 | 仅浏览器 | [XiHan 打印设计器](https://print.docs.xihanfun.com) 可视化设计，模板 JSON（`schemaVersion` 为 1 的打印模板）存 `Sys_Print_Template` |
| 浏览器打印 | 仅浏览器 | `previewPrintByCode` 在看不见的 iframe 里排好输出页再调起浏览器打印对话框，任何部署形态可用；传对象数组时各自分页后一次打印 |
| 打印机列表 / 静默直打 | **XiHan 打印客户端** | `listPrinters` / `refreshPrinters` / `directPrintByCode` 要求每台操作机安装并运行 [打印客户端](https://print.docs.xihanfun.com/guide/client)，前端经本机 WebSocket 连接；连不上、未授权或打印机出错时这三条路径按错误码明确报错，不退回浏览器打印 |

::: warning 部署前提
静默直打是**企业内网桌面场景**的能力：纯 Web 部署（用户不装桌面客户端）只有设计器与浏览器打印可用。网站第一次连接时，客户端在桌面上弹出授权窗口，允许后才能打印。生产站点要用 HTTPS；Chrome / Edge 访问本机应用要先过浏览器的本机网络访问授权，嵌在 iframe 里时每层都要允许 `loopback-network`。
:::

## 作用域模型

模板分两种作用域，解析顺序由 `PrintTemplateScope` 控制（`Application/Services/PrintTemplateResolver.cs`）：

- **Tenant**：租户私有模板，`TenantId` = 当前租户；
- **Global**：平台全局模板（`TenantId = 0`），`AllowTenantUse = true` 时对业务租户开放解析；
- **Auto**（默认）：先找租户私有，未命中回退开放的全局模板。

同租户内启用模板的 `TemplateCode` 唯一；解析结果带分布式缓存（30 分钟，写路径经事务感知失效器整体清除）。

## 权限码

前缀 `print-template:`（`Domain/Permissions/PrintingPermissionCodes.cs`）：`read` / `create` / `update` / `status` / `delete` / `use` 两侧生效、可授予租户（租户角色与套餐白名单由运营授予；演示数据里的租户管理员角色取租户能生效的全部权限）；`global-manage` 作用侧为平台（管理全局模板与租户开放状态；由权限种子声明，只在平台生效、进不了套餐白名单）。

## 数据源目录

模板可绑定「代码注册数据源」获得字段素材与样例数据：字段列在设计器物料面板的「字段」一组，拖上画布就是绑好的元素（图片、条码、二维码字段生成对应元素，明细表生成表格）。目录以**后端注册表为单一事实源**（`Domain/DataSources/IPrintDataSourceRegistry`）：

- **注册**：业务模块在自己的 `ConfigureServices` 中调 `services.RegisterPrintDataSource(definition)` 登记定义（字段契约 + 静态样例 JSON）；打印模块内置示例数据源 `system.print-demo` 随模块自注册。
- **启动期校验**：应用启动即收纳全部登记项，重复编码、非法字段契约（未知类型/重复字段/空列明细/非法控件类型）、坏样例 JSON（不可解析或根节点不是对象/数组）都会让启动直接失败。
- **查询**：`GET /PrintDataSourceQuery/List` 输出目录，持 `print-template:read` 或 `print-template:use` 任一权限即可访问（分别服务模板管理与业务打印两条路径）。
- **写路径 fail-closed**：创建/更新模板时绑定未注册的 `DataSourceCode` 直接被拒；解析读路径不校验（既有模板宽限，前端打印使用点仍会按未注册报错）。
- **前端惰性拉取**：设计器与打印路径首次用到目录时经上面的查询端点拉取并并入本地注册表（自由模板打印不依赖目录）；`registerPrintDataSource` 仍可用于纯前端数据源；单个坏目录项只跳过自身、不影响其余数据源。

由此推出的两条约束：

- 数据源随后端代码发版，删除/改名会使既有模板的绑定失效（打印时报「数据源未注册」）；
- 不绑数据源的「自由模板」不受上述影响：直打按空记录打印，样例预览的表单按模板引用的字段生成。

绑定了数据源时，模板引用了、数据源却没登记的字段在模板设置里提示数量（不阻断保存与打印，调用方要传这些字段），样例预览的表单也为它们单独列出输入项。

模板 JSON 的服务端校验只做最小结构检查（JSON 对象、`schemaVersion` 为 1、`pages` 是非空的对象数组），完整校验在前端保存前由设计器做。库里存着旧格式内容的模板，打印时明确报格式错误；在打印模板页打开时换成空白模板并提示，重新设计保存后覆盖。

## 演示模板

配置 `Saas:Seed:EnableDemoData` 为 `true` 时，打印模块多注册五个演示数据源，并写入六张平台全局模板（`TenantId = 0`、对租户开放、启用）；开关关着时数据源目录里没有它们，库里也不写这些模板。种子按模板编码先查后写，重复启动不重复插入，已有的模板不改、删掉的不补回。

| 模板编码 | 名称 | 所绑数据源 | 演示什么 |
| --- | --- | --- | --- |
| `demo.pickup-receipt` | 取件凭条 | `demo.pickup-receipt` | 最小模板：模板文字、放不下时缩小字号 |
| `demo.delivery-note` | 送货单 | `demo.delivery-note` | 页眉标题、页码、日期格式、明细表跨页、序号列与合计行 |
| `demo.formats` | 格式示例 | `demo.formats` | 数字、百分比、日期、人民币大写、中文数字、值映射、表达式、空值与占位模板 |
| `demo.inspection` | 设备巡检记录 | `demo.inspection` | 首页页眉、长文本、表头每页重复、本页计数、带数据占位的水印 |
| `demo.outage-notice` | 停电检修通知 | `demo.outage-notice` | 静态富文本与行内字段，数据里的 HTML 排成富文本（样例故意带的脚本被丢弃） |
| `demo.element-overview` | 打印元素总览 | `system.print-demo` | 全部元素种类；七种条码与四种二维码；直线、矩形、椭圆；表格里的条码列与补空行；首页、奇偶、末页的页眉页脚；两个页面模板接续页码；水印与只在预览中显示的元素 |

前五张对应[打印设计器文档站](https://print.docs.xihanfun.com)的模板示例，数据源的样例数据就是示例里的数据。模板与数据源放在 `Infrastructure/Seeders/Demo/` 下随程序集发布；前端 `packages/printing/demo-templates.test.ts` 读同一批文件，核对每张模板通过设计器的完整校验、按样例数据排版不出错误级诊断（条码与二维码取值不合法、图片地址不安全、表达式出错，在预览里都是报错占位）、引用的字段都在所绑数据源里登记。

## 已知限制

- `previewPrintByCode` 的完成信号仅表示打印对话框已调起，无法得知用户在系统对话框中点了打印还是取消；
- 直打依赖的打印机名单来自客户端所在机器，跨机器无共享；打印机偏好存浏览器 `localStorage`（按 用户×租户×模板 隔离，不上传服务端）。
