# 电子面单打印组件

电子面单打印组件运行在商家本地电脑上，应用端通过 **WebSocket** 协议与之通信，完成面单的预览和打印。无论是使用商家后台打单，还是 ISV 打单软件，均需安装此组件。

## 下载与安装

打印组件支持 Windows（64 位 / 32 位）和 Mac 系统，请前往使用指南页面获取最新安装包：详见成长中心：[微信小店「打印组件」使用指南](https://store.weixin.qq.com/chengzhang/webdoc/wiki/544/6c826cffaccc0396/growth_center_manual_for_store)

安装注意事项：
- Windows 安装时提示「需要 64 位系统」，请改用 32 位安装包
- 请务必使用最新版组件。旧版未卸载直接安装新版会导致电脑上存在两个打印组件，旧版打印新面单可能出现差异，建议先卸载旧版本
- 安装完成后需保持打印组件处于运行状态

## 使用前准备

打印面单前需按以下顺序完成设置：

1. **开通电子面单**：在微信小店后台绑定快递公司网点/月结账号
2. **设置快递面单模板**：选择或自定义面单打印样式
3. **下载安装并运行打印组件**：安装后保持运行状态
4. **完成打印设置**：确认打印机连接正常

## 对接流程总览

前端通过 WebSocket 与打印组件通信，完整流程如下：

![](https://res8.wxqcloud.qq.com.cn/wxdoc/8028f308-f253-499c-bca2-2d06b8b191cf.png)

其中前 4 步（连接 → 校验版本 → 获取打印机 → 预览/打印）均通过 WebSocket 指令完成，后 2 步（打印成功通知 → 订单发货）回到服务端 API 调用。下面逐步说明每个指令的用法。

## 通信机制

打印组件安装后会在本地启动一个 **WebSocket Server**，监听 `127.0.0.1:12705`。前端页面作为 **WebSocket Client** 连接上去，双方通过 JSON 消息通信。

通信模式为**请求-响应**：前端发送一条 JSON 指令，打印组件处理后返回一条 JSON 结果，通过 `requestID` 字段匹配请求和响应。

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#0ab8a6', 'primaryTextColor': '#ffffff', 'primaryBorderColor': '#089e8d', 'lineColor': '#0ab8a6', 'actorBkg': '#0ab8a6', 'actorBorder': '#089e8d', 'actorTextColor': '#ffffff', 'actorLineColor': '#0ab8a6', 'signalColor': '#0ab8a6', 'signalTextColor': '#333333', 'labelBoxBkgColor': '#e8f8f6', 'labelBoxBorderColor': '#0ab8a6', 'labelTextColor': '#333333', 'noteBkgColor': '#e8f8f6', 'noteBorderColor': '#0ab8a6', 'noteTextColor': '#333333'}}}%%
sequenceDiagram
    participant C as 前端页面（Client）
    participant S as 打印组件（Server @ 127.0.0.1:12705）
    C->>S: ws.send({ command, requestID, ... })
    Note right of S: 收到指令，执行操作
    S->>C: onmessage({ command, requestID, ... })
    Note left of C: 通过 requestID 匹配响应
```

关键特性：
- **仅限本机通信**：地址为 `127.0.0.1`，打印组件必须安装在与前端页面同一台电脑上
- **长连接复用**：连接建立后可反复发送多条指令，无需每次重连
- **所有指令共用同一个连接**：获取打印机列表、预览、打印等操作都在同一条 WebSocket 连接上完成
- **通过 `command` 字段区分指令类型**：`getPrinterList` / `getAppInfo` / `preview` / `print`
- **通过 `requestID` 字段匹配响应**：前端为每条指令分配唯一 ID，打印组件在响应中原样返回

## Step 1：连接打印组件

打印组件监听本地固定端口，通过 WebSocket 建立连接：

```javascript
const ws = new WebSocket('ws://127.0.0.1:12705');

ws.onopen = () => {
    console.log('与打印组件建立连接成功');
};

ws.onerror = (err) => {
    console.error('连接失败，请检查打印组件是否已启动');
};
```

连接失败时检查：1）打印组件是否已在本机启动；2）本地防火墙是否放行 `12705` 端口。

## Step 2：获取打印机列表

连接成功后，查询本机已连接的打印设备：

```javascript
ws.send(JSON.stringify({
    requestID: 'req_001',
    command: 'getPrinterList'
}));

ws.onmessage = (e) => {
    const resp = JSON.parse(e.data || '{}');
    if (resp.command === 'getPrinterList') {
        // resp.printerList: [{ name: '_KM_202_NEW_', displayName: 'KM-202(NEW)', status: 3 }]
        console.log('可用打印机：', resp.printerList);
    }
};
```

返回的 `printerList` 中，`name` 用于后续打印指令的 `printer` 参数。

## Step 3：预览面单

正式打印前可先预览，返回预览图片 URL：

```javascript
ws.send(JSON.stringify({
    command: 'preview',
    requestID: 'req_003',
    taskList: [{
        taskID: 'task_001',
        printInfo: '<获取打印报文接口返回的 print_info>',
        printNum: { curNum: 1, sumNum: 1 }
    }]
}));
// 返回：{ previewUrl: 'xxx' }
```

### print_info 如何传递给打印组件

`print_info` 由**服务端 API** 返回，但打印组件在**前端本地**通过 WebSocket 工作，两者不在同一侧。传递路径如下：

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#0ab8a6', 'primaryTextColor': '#ffffff', 'primaryBorderColor': '#089e8d', 'lineColor': '#0ab8a6', 'actorBkg': '#0ab8a6', 'actorBorder': '#089e8d', 'actorTextColor': '#ffffff', 'actorLineColor': '#0ab8a6', 'signalColor': '#0ab8a6', 'signalTextColor': '#333333', 'labelBoxBkgColor': '#e8f8f6', 'labelBoxBorderColor': '#0ab8a6', 'labelTextColor': '#333333', 'noteBkgColor': '#e8f8f6', 'noteBorderColor': '#0ab8a6', 'noteTextColor': '#333333', 'activationBkgColor': '#e8f8f6', 'activationBorderColor': '#0ab8a6', 'sequenceNumberColor': '#ffffff'}}}%%
sequenceDiagram
    participant S as 商家服务端
    participant F as 前端页面
    participant P as 本地打印组件
    S->>S: 调用取号/获取打印报文接口<br/>拿到 print_info（字符串）
    S->>F: 通过业务接口返回 print_info
    F->>P: WebSocket 发送打印指令<br/>print_info 放入 taskList[].printInfo
    P->>F: 返回打印结果（success/fail）
    F->>S: 打印成功，调用打印成功通知 API
    S->>S: 调用订单发货 API 完成履约
```

`print_info` 本身是一段**不透明的字符串**（包含面单的布局、收寄件信息、条码等渲染数据），开发者无需解析其内容，只需原样透传：

1. 服务端调用 [API] [电子面单取号 / ewaybill_createorder](/doc/store/shop/API/channels-shop-delivery/ewaybill/api_ewaybill_createorder)（传了 `template_id` 时直接返回）或 [API] [获取打印报文 / get_print_content](/doc/store/shop/API/channels-shop-delivery/ewaybill/api_get_print_content_supplier)，从响应中取出 `print_info`
2. 服务端通过自己的业务接口（如 HTTP API）将 `print_info` 返回给前端页面
3. 前端拿到后，直接放入下方打印指令的 `taskList[].printInfo` 字段发送即可

## Step 4：打印面单

核心打印指令，将 `print_info` 发送给打印组件出单：

```javascript
ws.send(JSON.stringify({
    command: 'print',
    version: '2.0',
    requestID: 'req_004',
    printType: 1,            // 1: 固定高度面单；2: 自定义 HTML 内容
    size: { width: 76, height: 130 },  // 非标准尺寸或 printType=2 时必传（单位 mm）
    printer: '_KM_202_NEW_', // 从 getPrinterList 获取的打印机 name
    taskList: [{
        taskID: 'task_001',
        printInfo: '<获取打印报文接口返回的 print_info>',
        printNum: { curNum: 1, sumNum: 1 },
        splitControl: 0,      // 0: 自动分页（默认）；1: 禁止分页；2: 强制分页
        showDeliveryLogo: 1   // 0: 不展示快递logo；1: 展示（默认）
    }]
}));
```

**主结构体参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `command` | String | 是 | 固定为 `'print'` |
| `version` | String | 是 | 固定为 `'2.0'` |
| `requestID` | String | 是 | 请求唯一标识，调用方保证唯一 |
| `printType` | Number | 是 | `1`：打印固定高度面单（标准 130×76mm）；`2`：打印自定义 HTML 内容（`printInfo` 需为 base64 编码的 HTML） |
| `size` | Object | 否 | 纸张尺寸 `{ width, height }`（单位 mm）。`printType=2` 或面单尺寸非标准 130×76mm 时必传 |
| `printer` | String | 否 | 打印机标识，对应 `getPrinterList` 返回的 `name` 字段 |
| `taskList` | Array | 是 | 打印任务列表，每个元素为一个 PrintTask |

**PrintTask 参数**（taskList 数组中的对象）：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `taskID` | String | 是 | 任务唯一标识，调用方保证唯一 |
| `printInfo` | String | 是 | 打印报文。`printType=1` 时为接口返回的 `print_info`；`printType=2` 时为 base64 格式的 HTML |
| `printNum` | Object | 是 | 打印计数：`{ curNum: 当前张数, sumNum: 总张数 }` |
| `splitControl` | Number | 否 | 分页控制，默认 `0`。`0`：自动分页 / `1`：禁止分页 / `2`：强制分页（内容在第二页） |
| `showDeliveryLogo` | Number | 否 | 快递公司 logo，默认 `1`。`0`：不展示 / `1`：展示 |
| `customInfo` | Object | 否 | 自定义模板：`templateUrl`（模板 URL）+ `data`（模板数据），详见下方「自定义模板详解」 |
| `extendData` | Object | 否 | 覆盖寄件人信息：`sender.address`（provinceName / cityName / countyName / detailInfo）、`sender.userName`、`sender.telNumber` |

### 自定义模板详解

打印指令的 `customInfo` 参数支持自定义面单下方的附加内容区域（如商品清单、店铺 logo、条形码等）。模板引擎基于 **Mustache.js**。

官方参考模板：[https://mmec-shop-1258344707.cos.ap-shanghai.myqcloud.com/shop/public/2023-11-10/a80c0110-3fb8-4190-bdcc-10124b7dd0ce.html](https://mmec-shop-1258344707.cos.ap-shanghai.myqcloud.com/shop/public/2023-11-10/a80c0110-3fb8-4190-bdcc-10124b7dd0ce.html)（可直接访问查看模板结构）

### 模板语法

| 语法 | 说明 | 示例 |
|------|------|------|
| `{{name}}` | 变量渲染（HTML 转义） | `{{shopName}}` → `测试小店` |
| `{{{name}}}` | 变量渲染（不转义 HTML） | `{{{description}}}` → `商品<br>描述` |
| `{{#list}}...{{/list}}` | 循环渲染列表 | 遍历商品列表 |
| `{{^list}}...{{/list}}` | 取反（列表为空时显示） | 无商品时显示提示 |

### 条形码与二维码

模板中可使用专用标签生成条形码和二维码：

**条形码**：

```html
<bar-code content="{{productBarcode}}" width="300" height="60"
  config='{"displayValue": true, "textAlign": "left", "format": "auto"}'></bar-code>
```

config 完整配置（基于 [JsBarcode](https://github.com/lindell/JsBarcode/wiki/Options)）：

| 配置项 | 类型 | 默认值 | 说明 |
|-------|------|--------|------|
| `format` | String | `"auto"` (CODE128) | 条码类型 |
| `width` | Number | `2` | 单条条码宽度 |
| `height` | Number | `100` | 条码高度 |
| `displayValue` | Boolean | `false` | 条码下方是否显示值 |
| `text` | String | `undefined` | 覆盖条码下方显示的文本内容 |
| `fontOptions` | String | `""` | 字体样式：`"bold"` / `"italic"` / `"bold italic"` |
| `font` | String | `"monospace"` | 字体名称 |
| `textAlign` | String | `"center"` | 文字水平位置：`"left"` / `"center"` / `"right"` |
| `textPosition` | String | `"bottom"` | 文字垂直位置：`"top"` / `"bottom"` |
| `textMargin` | Number | `2` | 条码与文字间距 |
| `fontSize` | Number | `20` | 文字字号 |
| `background` | String | `"#ffffff"` | 背景颜色 |
| `lineColor` | String | `"#000000"` | 条码颜色 |
| `margin` | Number | `10` | 四周通用间距 |
| `marginTop` | Number | `undefined` | 上方间距（优先级高于 margin） |
| `marginBottom` | Number | `undefined` | 下方间距（优先级高于 margin） |
| `marginLeft` | Number | `undefined` | 左侧间距（优先级高于 margin） |
| `marginRight` | Number | `undefined` | 右侧间距（优先级高于 margin） |

**二维码**：

```html
<qr-code content="{{productQRcode}}" width="120" height="120"></qr-code>
```

### 完整模板示例

```html
<div>
  <div style="font-weight: 700;">店铺名称 {{shopName}}</div>
  {{#productInfo}}
  <div>{{name}}, 数量: {{count}}, 编码: {{code}}, {{{description}}}</div>
  {{/productInfo}}
  <img src="{{imgSrc}}" width="300" height="50"/>
  <bar-code content="{{productBarcode}}" width="300" height="60"
    config='{"displayValue": true}'></bar-code>
  <qr-code content="{{productQRcode}}" width="120" height="120"></qr-code>
</div>
```

对应 `customInfo.data`（`templateUrl` 指向上方 HTML 模板的托管地址）：

```javascript
customInfo: {
    templateUrl: 'https://mmec-shop-1258344707.cos.ap-shanghai.myqcloud.com/shop/public/2023-11-10/a80c0110-3fb8-4190-bdcc-10124b7dd0ce.html', // 官方参考模板，实际使用时替换为自己的模板地址
    data: {
        shopName: "测试小店",
        productInfo: [
            { name: "商品1", count: 1, code: "SKU001", description: "商品描述" },
            { name: "商品2", count: 2, code: "SKU002", description: "商品<br>描述" }
        ],
        imgSrc: "https://example.com/logo.png",
        productBarcode: "BARCODE001",
        productQRcode: "https://example.com/product"
    }
}
```

### 子母单打印

大件商品需拆分多个包裹时使用子母单。支持子母单的快递公司详见 [API] [电子面单子件追加 / ewaybill_addsuborder](/doc/store/shop/API/channels-shop-delivery/ewaybill/api_ewaybill_addsuborder)。

### 取号方式

两种方式获取子母单：

1. **取号时一次性获取**：调用 [API] [电子面单取号 / ewaybill_createorder](/doc/store/shop/API/channels-shop-delivery/ewaybill/api_ewaybill_createorder) 时传入 `package_quantity` 参数（2~300），一次性获取母单号 + N 个子单号
2. **取号后逐步追加**：先正常取号获取母单，再通过 [API] [电子面单子件追加 / ewaybill_addsuborder](/doc/store/shop/API/channels-shop-delivery/ewaybill/api_ewaybill_addsuborder) 逐步追加子件

### 版本要求

打印组件需 **1.52.0 及以上版本**才支持子母单打印样式。建议打印前通过 `getAppInfo` 指令校验版本号，低版本需提示用户升级：

```javascript
ws.send(JSON.stringify({
    requestID: 'req_version',
    command: 'getAppInfo'
}));
// 返回：{ appInfo: { version: '1.52.0' } }
```

### print_info 与子母单打印

以下三个接口都可以获取到子母单打印所需的 `print_info`：
- [API] [电子面单取号 / ewaybill_createorder](/doc/store/shop/API/channels-shop-delivery/ewaybill/api_ewaybill_createorder)
- [API] [电子面单子件追加 / ewaybill_addsuborder](/doc/store/shop/API/channels-shop-delivery/ewaybill/api_ewaybill_addsuborder)
- [API] [获取打印报文 / get_print_content](/doc/store/shop/API/channels-shop-delivery/ewaybill/api_get_print_content_supplier)

将 `print_info` 传给 1.52.0+ 打印组件后，**默认会把所有子母单都打印出来**（一个打印任务，输出多张面单）。

### 单独打印某个子单

如果只需打印某个特定子单，调用 [API] [获取打印报文 / get_print_content](/doc/store/shop/API/channels-shop-delivery/ewaybill/api_get_print_content_supplier) 时传入 `sub_waybill_id`（子单运单号），返回的 `print_info` 仅包含该子单，传入打印组件后只会打印该子单的面单。

## Step 5：打印结果处理与状态流转

打印指令发出后，通过 `onmessage` 监听打印组件返回的结果，根据成功或失败执行不同的后续操作。

```javascript
ws.onmessage = (e) => {
    const resp = JSON.parse(e.data || '{}');
    if (resp.command === 'print') {
        resp.results.forEach(r => {
            if (r.success) {
                console.log(`任务 ${r.taskID} 打印成功`);
                // 1. 调用服务端 API：打印成功通知（ewaybill_printorder）
                // 2. 调用服务端 API：订单发货（senddelivery）
            } else {
                console.error(`任务 ${r.taskID} 失败：${r.failureReason}`);
                // 排查打印机状态后重试
            }
        });
    }
};
```

**返回参数**：

| 参数 | 类型 | 说明 |
|------|------|------|
| `requestID` | String | 与请求中的 requestID 一致 |
| `command` | String | 固定为 `"print"` |
| `results` | Array | 打印结果列表，与请求中 taskList 一一对应 |
| `results[].taskID` | String | 与请求中的 taskID 一致 |
| `results[].success` | Boolean | `true` 打印成功 / `false` 打印失败 |
| `results[].failureReason` | String | 失败原因，成功时为空字符串 |

**打印成功后必须完成两步状态流转**：

1. **通知平台出单结果**：调用 [API] [打印成功通知 / ewaybill_printorder](/doc/store/shop/API/channels-shop-delivery/ewaybill/api_ewaybill_printorder) 扭转电子面单状态。批量场景使用 [API] [批量打印通知 / ewaybill_batchprintorder](/doc/store/shop/API/channels-shop-delivery/ewaybill/api_ewaybill_batchprintorder)
2. **完成订单发货**：调用 [API] [订单发货 / senddelivery](/doc/store/shop/API/channels-shop-delivery/delivery/api_senddelivery) 将快递单号绑定到订单，流转订单状态为已发货

如果漏掉第 1 步，平台无法感知面单已出单；如果漏掉第 2 步，订单会一直停留在待发货状态。

## WebSocket 指令速查

| 指令 command | 功能 | 关键参数 |
|-------------|------|---------|
| `getPrinterList` | 获取打印机列表 | — |
| `getAppInfo` | 获取版本号 | — |
| `preview` | 预览面单 | `taskList[].printInfo` |
| `print` | 打印面单 | `taskList[].printInfo`、`printType`、`printer` |

![](https://res8.wxqcloud.qq.com.cn/wxdoc/56617deb-b17d-4cf1-bccd-71d2c41ccc76.svg)文档变更日志（1条）
2026 年 04 月 14 日新增 电子面单打印组件 开发指南
