# 远程打印机项目总体方案

> 状态：设计稿 v0.2（已确定首版设备通信方案）
> 范围：只定义总体架构、领域模型、接口边界和运行流程，不包含实现代码。

## 1. 背景与目标

`iotproject` 是多个物联网项目的总仓库，本文件只描述其中的 `printer` 子项目。

目标是让外部系统提交打印指令，由远程服务端生成打印内容，ESP8266 定期从服务端领取任务并转发给本地打印机，同时提供一个后台 Web 页面查询任务、设备和打印结果。

首版需要解决：

1. 打印指令可以被可靠接收并保存。
2. 打印内容在远程服务端统一渲染，设备端不承担复杂排版。
3. ESP8266 断线重连、重复轮询或重启时不会静默丢单。
4. 后台能够区分“未渲染、待打印、打印中、成功、失败”等状态。
5. 旧 Java 程序中的订单字段和热敏/针式打印布局可以逐步迁移。

## 2. 历史资料结论

历史打印机代码和二进制资料提供了以下约束：

- `Netkk.java` 在本地监听 TCP `7576`，使用 GBK 按行读取文本，收完连接后直接触发打印。
- JAR 中的 `NetworkServer_2` 默认使用 TCP `9600`，同样使用 GBK 按行读取，并以包含“地址”的行作为一份单据的触发标记。现网端口已确认为 **9100**，新服务端兼容入口使用 9100；9600 只作为旧 JAR 的历史默认值记录。
- 旧数据按订单字段和商品明细组织，字段包括序号、时间、客户、清单号、商品名、数量、单位、单价、金额、合计和备注。
- `Tt.java`/`PcpPrinter_2` 将排版写死在 Java AWT 打印代码中，并选择固定打印机名称，例如 `Jolimark FP-570K`。
- 目录中还有芯烨 58/80/76 系列 Linux 驱动，说明历史环境可能包含本地 Linux 打印机驱动和非纯 ESC/POS 打印机。

这些资料说明了业务字段和打印样式来源。新服务端在 9100 上保留独立的旧协议适配器：每个 TCP 连接使用 GBK 按行读取，连接关闭或检测到包含“地址”的行时形成一份原始单据，并将原始报文和解析结果写入 `PrintJob`。内部 API、数据库和设备协议仍使用结构化 JSON、UTF-8 和任务状态记录，避免让历史协议扩散到新模块。

## 3. 总体架构

```text
外部业务系统
      |
      | HTTPS: 提交打印指令
      v
远程服务端
  ├─ 打印接入 API
  ├─ 任务编排与状态机
  ├─ 渲染 Worker
  ├─ 设备轮询 API
  ├─ 后台查询 API
  └─ 数据库 + 打印文件存储
      ^                         |
      | HTTPS: 轮询/确认/上报     | 返回已渲染打印任务
      |                         v
 ESP8266 控制板  --串口/硬件接口-->  本地打印机
      ^
      |
 后台 Web 页面（通过后台查询 API）
```

### 3.1 部署建议

逻辑上分为五个模块，首版部署为两个运行单元：

- `server-api`：打印接入、任务状态、设备轮询、后台 API，以及 Web 静态文件或 Web 反向代理。
- `render-worker`：从数据库领取待渲染任务，生成可供设备打印的文件或字节流。

数据库使用 PostgreSQL；渲染产物首版可以存放在服务器文件系统，接口保留对象存储替换点。任务量变大后再引入 Redis 或独立消息队列，不把队列作为首版的必要依赖。

### 3.2 ESP8266 任务通信方案比较与决策

这里需要区分两种“透传”：ESP8266 收到打印字节后通过 UART 转发给本地打印机，是打印代理的必要职责；让公网服务端直接访问局域网内的 ESP8266 或打印机，则属于内网穿透，不是打印任务投递的必要条件。

| 方案 | 通信路径与延迟 | 优点 | 代价与限制 |
| --- | --- | --- | --- |
| HTTPS 轮询 | ESP8266 主动向服务端请求；按 5 秒间隔时，新任务通常在 0--5 秒内被发现，平均约 2.5 秒 | 实现简单；可穿过普通 NAT、家庭路由器和 CGNAT；断线后下一次请求自然恢复；沿用现有设备鉴权、租约、任务事件和重试逻辑 | 空闲设备也会周期性请求；100 台设备按 5 秒间隔约产生 20 次轮询/秒；不适合要求亚秒级通知的场景 |
| MQTT/WebSocket 长连接 | ESP8266 主动建立 TLS 出站连接；服务端通过连接通知有新任务 | 通知延迟低；空闲时可减少 HTTP 轮询 | 需要维护连接、心跳、重连、消息确认、设备级授权和消息去重；MQTT 还需要部署 Broker；长连接不消除任务队列、租约、确认及重试需求 |
| 公网端口转发或反向 TCP 隧道 | 通过路由器端口映射，或由设备主动连到隧道服务器后反向转发流量 | 适合远程维护、访问私网内既有 TCP 服务，或必须兼容不能改造的主动连接旧客户端 | 直接端口映射依赖可入站公网地址和路由器配置，CGNAT 下通常不可用；对外暴露打印机端口会扩大攻击面。反向隧道需要独立隧道端点、访问控制、凭据轮换、连接保活和故障恢复；它本身不提供可靠任务状态 |

**首版决策：采用 HTTPS 轮询，不把 ESP8266 做成通用内网穿透节点。** 当前服务端已经提供设备轮询 API，响应建议间隔为 5 秒，领取任务时发放 120 秒租约，并通过设备事件记录 `received`、`started`、`completed` 或 `failed`。ESP8266 通过出站 HTTPS 访问服务端并使用设备独立密钥，不需要从公网主动连入设备，也不需要开放设备端口。

轮询传输仍采用至少一次投递语义，设备需按任务 ID 去重并报告结果；断线或设备重启由租约超时和重试恢复。实现时应让固件遵守服务端返回的 `poll_after_ms`，网络失败采用指数退避；若打印耗时可能超过 120 秒，应增加租约续期/心跳或调整租约时长，避免任务仍在打印时被重新投递。

只有当实测表明 5 秒通知延迟不可接受，或设备规模使空闲轮询负载成为问题时，才考虑增加 MQTT over TLS 作为任务通知通道。届时打印产物仍通过受设备鉴权的 HTTPS 下载，任务状态仍沿用服务端租约和事件接口，并保留低频轮询用于恢复通知遗漏。WebSocket 也可实现通知，但需自行承担连接管理和消息确认逻辑。

如需远程运维或访问局域网里的其他设备，应单独部署受认证、限目标端口、可审计且可关闭的维护隧道；不要把任意 TCP 转发开放到公网，也不要用隧道替代打印任务队列。

## 4. 五个逻辑模块

### 4.1 打印指令接入服务

职责：

- 接受外部系统提交的结构化打印指令。
- 校验打印模板、打印机目标、业务单号和幂等键。
- 创建 `PrintJob`，初始状态为 `accepted`。
- 提供查询单个任务和提交结果的接口。

不负责排版，也不直接连接 ESP8266。接入成功只代表任务已持久化，不代表已经打印。

推荐入口：`POST /api/v1/print-commands`。

兼容入口：TCP `0.0.0.0:9100`，编码 GBK，消息为换行分隔的文本。兼容入口不依赖 HTTP 返回值；接收完成后先持久化任务，再由队列异步渲染和打印。

### 4.2 打印文件渲染服务

职责：

- 根据模板、订单数据和打印机能力生成打印产物。
- 处理纸张宽度（例如 58/76/80 mm）、字体、中文编码、换行、分页和二维码/条码。
- 记录渲染版本、模板版本和产物校验值。
- 渲染失败时保存可读错误原因，并允许后台重试。

首版建议生成打印机可以直接执行的 `ESC/POS` 字节流；ESP8266 只需流式转发，不需要理解 PDF 或图片。若某型号打印机需要 Linux 驱动或 Java AWT/CUPS，则把它作为服务端的另一个渲染/打印适配器，不强行让 ESP8266 解析桌面打印协议。

同一任务可以同时生成两种产物：

- `device_artifact`：给 ESP8266 打印的设备格式，首选 ESC/POS。
- `preview_artifact`：给后台预览的 PDF/PNG，可选，不参与设备投递。

### 4.3 ESP8266 任务轮询服务

职责：

- 设备注册、鉴权、心跳和能力上报。
- 设备轮询时原子领取一个待打印任务。
- 为任务发放短期租约，避免设备掉线后任务永久卡住。
- 接收设备的 `received`、`started`、`completed`、`failed` 事件。
- 根据设备状态和租约结果安排重试。

推荐接口：

```text
POST /api/v1/devices/{device_id}/poll
POST /api/v1/devices/{device_id}/tasks/{task_id}/events
GET  /api/v1/devices/{device_id}/tasks/{task_id}/artifact
```

设备接口使用 `X-Device-Key` 独立鉴权。轮询返回的任务租约默认 120 秒，设备必须先上报 `received`，再上报 `started` 和最终的 `completed`/`failed`。

### 4.4 ESP8266 本地打印代理

职责：

- 以设备凭据访问服务端。
- 按固定间隔轮询；网络失败时指数退避并持续重连。
- 领取任务后校验任务 ID、字节数和 SHA-256 校验值。
- 将打印字节流通过 UART、USB 转接或其他硬件接口发送给打印机。
- 在本地记录最近完成的任务 ID，防止重启后重复打印。
- 上报打印机缺纸、离线、串口错误、超时等可观测状态。

ESP8266 不负责模板解析、PDF 解析或中文排版。若实际打印机只有 USB/CUPS 接口而没有 ESP8266 可用的串口/网络接口，需要增加本地 Linux 打印网关；这个硬件前提必须在实施前确认。

### 4.5 后台 Web 查询页面

首版以查询和运维为主，页面至少包括：

- 任务列表：按时间、业务单号、设备、状态筛选。
- 任务详情：原始指令、渲染信息、投递尝试、错误信息和时间线。
- 设备列表：在线状态、最后轮询时间、固件版本、打印机状态。
- 任务详情中的预览图或 PDF（如果生成了 `preview_artifact`）。
- 重试渲染、重试投递、取消未执行任务。

后台操作必须经过权限控制，并记录操作人和操作时间。页面不直接访问设备，也不绕过任务状态机写数据库。

## 5. 核心领域模型

### 5.1 实体

| 实体 | 说明 | 关键字段 |
| --- | --- | --- |
| `PrintJob` 打印任务 | 一次业务打印请求 | `id`、`idempotency_key`、`source`、`template_id`、`payload`、`target_device_id`、`status`、时间戳 |
| `PrintTemplate` 打印模板 | 定义数据到打印内容的映射 | `id`、`version`、`format`、`paper_width`、启用状态 |
| `RenderedArtifact` 渲染产物 | 某任务的设备文件或预览文件 | `job_id`、`kind`、`media_type`、`size`、`sha256`、存储位置 |
| `Device` 设备 | 一个 ESP8266 控制板 | `id`、设备凭据、固件版本、能力、最后在线时间 |
| `Printer` 打印机 | 连接在设备上的打印机 | 型号、连接类型、纸宽、协议、状态 |
| `DeliveryAttempt` 投递尝试 | 一次设备领取和执行记录 | `job_id`、`device_id`、租约、开始/结束时间、结果、错误 |
| `AuditEvent` 审计事件 | 重要状态和后台操作记录 | 操作者、动作、对象、时间、结果 |

### 5.2 任务状态

```text
accepted -> rendering -> queued -> leased -> printing -> succeeded
               |           |          |          |
               v           v          v          v
          render_failed  cancelled  expired  failed
```

建议将渲染状态和投递状态分别存储，业务展示再汇总为任务状态。这样渲染成功但设备暂时离线时，任务仍然明确处于 `queued`，不会被误判为失败。

状态约束：

- `accepted`：指令已保存，尚未开始渲染。
- `rendering`：有一个渲染 Worker 持有渲染租约。
- `queued`：已生成设备产物，等待目标设备轮询。
- `leased`：设备已领取，服务端等待设备确认开始或完成。
- `printing`：设备已开始向打印机输出。
- `succeeded`：设备确认打印机完成。
- `render_failed`：渲染失败，可修复后重试。
- `failed`：设备或打印机执行失败，按策略重试或人工处理。
- `expired`：设备租约超时，任务回到 `queued` 或进入失败重试。
- `cancelled`：尚未实际打印的任务被取消。

## 6. 轮询与可靠投递

### 6.1 一次正常流程

1. 外部系统提交指令，服务端以幂等键创建 `PrintJob`。
2. Worker 领取 `accepted` 任务，渲染并保存产物校验值。
3. 任务变为 `queued`。
4. ESP8266 调用 `poll`，携带设备状态、固件版本、打印机状态和最近完成任务 ID。
5. 服务端在事务中选择一个目标匹配、状态为 `queued` 的任务，设置 `lease_id` 和过期时间，返回任务元数据。
6. ESP8266 先回复 `received`，再按分块接口下载产物并校验。
7. ESP8266 向打印机发送数据，发送前上报 `started`，结束后上报 `completed` 或 `failed`。
8. 服务端记录投递尝试，任务进入终态或按重试策略重新排队。

### 6.2 断网、重启和重复请求

- `poll`、事件上报和创建打印任务都要支持幂等键。
- 租约必须有过期时间，例如 2 分钟；过期任务不能永久占用队列。
- 服务端定期回收过期租约；任务在 `max_attempts` 内回到 `queued`，超过次数进入 `failed` 并保留错误原因。
- 设备在本地保存最近若干个已完成任务 ID。服务端重复返回同一个任务时，设备只确认已完成，不再次打印。
- 服务端采用“至少一次投递”语义，不能承诺网络故障下的严格一次打印。通过幂等任务 ID、设备去重和可见的投递记录降低重复打印风险。
- 对无法确认打印机是否已经出纸的超时，只能标记为 `uncertain`/`failed` 并交给后台人工确认，不能盲目自动重打。

### 6.3 设备响应大小

ESP8266 内存有限，不能假设一次响应可以装下完整 PDF 或大图片。设备产物接口应支持分块读取（建议每块 1--4 KB），服务端返回总长度和 SHA-256；设备可以使用 LittleFS 暂存，也可以边下载边发送，具体取决于打印机协议和断点续传需求。

## 7. 接口约定草案

### 7.1 提交打印指令

```json
POST /api/v1/print-commands
{
  "idempotency_key": "order-20261024-0001",
  "template_id": "receipt-v1",
  "target_device_id": "printer-room-1",
  "data": {
    "order_no": "A0001",
    "customer_name": "客户",
    "items": [
      {"name": "商品", "quantity": 2, "unit": "件", "unit_price": "10.00"}
    ],
    "total": "20.00",
    "note": "备注"
  }
}
```

响应只确认任务已经持久化：

```json
{"job_id": "job_01...", "status": "accepted"}
```

### 7.2 设备轮询响应

```json
{
  "server_time": "2026-10-24T12:00:00Z",
  "poll_after_ms": 5000,
  "task": {
    "task_id": "job_01...",
    "lease_id": "lease_01...",
    "expires_at": "2026-10-24T12:02:00Z",
    "artifact_url": "/api/v1/devices/printer-room-1/tasks/job_01.../artifact",
    "size": 1832,
    "sha256": "...",
    "format": "escpos"
  }
}
```

无任务时 `task` 为 `null`。设备不要使用固定高频率请求；服务端可以通过 `poll_after_ms` 调整轮询间隔。

## 8. 安全与运维

- 外部 API 和设备 API 全部使用 HTTPS。
- 每台 ESP8266 使用独立设备 ID 和密钥；服务端不能用一个共享密钥代表所有设备。
- 设备接口只允许访问自己的任务和产物，后台接口使用独立账号/角色。
- 打印数据按 JSON Schema 校验，限制字段长度、明细行数和产物大小，避免异常请求拖垮渲染 Worker。
- 记录结构化日志：任务 ID、设备 ID、租约 ID、状态变化和错误码，不把完整敏感打印内容写入普通日志。
- 监控待渲染数量、队列等待时间、渲染失败率、设备在线率、任务重试次数和打印成功率。
- 数据库定期备份；打印产物设置保留周期，避免磁盘无限增长。

## 9. 分阶段实施建议

### 阶段一：最小闭环

- 一个模板、一个设备、一个打印机型号。
- 接入 API、PostgreSQL 任务表、ESC/POS 渲染、设备轮询和完成/失败上报。
- 后台只提供任务和设备查询。
- 使用设备独立密钥和 HTTPS。

### 阶段二：可运维化

- 模板版本和预览产物。
- 任务重试、取消、手动重打和审计记录。
- 设备能力上报、打印机状态和更完整的后台筛选。
- 旧 TCP/GBK 协议适配器，作为迁移期兼容层。

### 阶段三：多设备与多型号

- 多打印机配置和渲染适配器。
- 分组、优先级、按设备路由和并发控制。
- 对大文件增加断点续传和对象存储。
- 必要时拆分消息队列和渲染集群。

## 10. 需要在实现前确认的问题

1. ESP8266 与打印机的实际连接方式是 UART、USB 转接、网络，还是通过本地 Linux 主机？这决定设备代理能否直接发送 ESC/POS。
2. 首台打印机的准确型号、纸宽和控制协议是什么？芯烨 58/80/76 系列与 `Jolimark FP-570K` 不能默认使用同一渲染器。
3. 打印内容是否只包含订单小票，还是还需要图片、二维码、条码、PDF 等格式？
4. 外部提交方是否已有固定的 TCP/GBK 协议，是否需要首版兼容端口 `7576` 或 `9600`？
5. 一个任务是否允许指定设备，还是由服务端按设备组/打印机类型自动路由？
6. “打印完成”的定义是 ESP8266 发完字节，还是需要打印机提供真实的出纸/故障反馈？
7. 是否需要用户登录、多个角色和远程重打权限？

在这些问题确认前，本文中的接口路径和字段名属于方案草案，不作为最终兼容承诺。
