y.y
Published on

菜鸟打印组件开发文档

菜鸟打印组件开发文档

1. 概述

1.1 简介

菜鸟云打印客户端是一个独立进程,通过 WebSocket 协议与浏览器或其他客户端进行通信,支持 JavaScript、Java、C/C++、Python 等常用编程语言。

1.2 系统要求

  • 浏览器版本:Chrome 45 及以上(推荐使用最新版本)
  • 连接方式:WebSocket
  • 端口配置:
    • HTTP环境:ws://localhost:13528
    • HTTPS环境:wss://localhost:13529

1.3 更新信息

  • 更新日期:2024-08-02
  • 当前协议版本:1.0

1.4 官方文档

2. 快速开始

2.1 建立连接

// HTTP环境
const socket = new WebSocket('ws://localhost:13528');

// HTTPS环境
const socket = new WebSocket('wss://localhost:13529');

2.2 基本通信流程

  1. 建立 WebSocket 连接
  2. 发送请求命令
  3. 接收响应结果
  4. 处理通知消息

3. 协议格式

3.1 请求协议格式

{
    "cmd": "command",
    "requestID": "unique requestID",
    "version": "1.0"
}

字段说明:

字段名类型说明是否必须
cmdstring请求的命令名称
requestIDstring请求的唯一ID(如UUID)
versionstring协议版本,当前为"1.0"

3.2 响应协议格式

{
    "cmd": "command",
    "requestID": "unique requestID"
}

4. API 参考

4.1 打印/预览(print)

请求格式(明文数据)

{
    "cmd": "print",
    "requestID": "123458976",
    "version": "1.0",
    "task": {
        "taskID": "7293666",
        "preview": false,
        "printer": "",
        "previewType": "pdf",
        "firstDocumentNumber": 10,
        "totalDocumentCount": 100,
        "documents": [{
            "documentID": "0123456789",
            "contents": [{
                "data": {
                    "nick": "张三"
                },
                "templateURL": "http://cloudprint.cainiao.com/template/standard/278250/1"
            }]
        }]
    }
}

任务参数说明

字段名类型说明是否必须
taskIDstring打印任务ID
idempotentbool是否允许taskID重复(1.5.0+)
previewbooltrue为预览,false为打印
previewTypestring预览模式:"pdf"或"image"
printerstring打印机名称,空值使用默认打印机
templateURLstring模板文件URL
documentsarray文档数组,每个元素表示一页

响应格式

{
    "cmd": "print",
    "requestID": "123458976",
    "taskID": "1",
    "status": "success",
    "previewURL": "http://127.0.0.1/previewxxx.pdf",
    "previewImage": [
        "http://127.0.0.1/preview1.jpg",
        "http://127.0.0.1/preview2.jpg"
    ]
}

4.2 获取打印机列表(getPrinters)

请求格式

{
    "cmd": "getPrinters",
    "requestID": "123458976",
    "version": "1.0"
}

响应格式

{
    "cmd": "getPrinters",
    "requestID": "123458976",
    "defaultPrinter": "XX快递打印机",
    "printers": [{
        "name": "XX快递打印机"
    }, {
        "name": "YY物流打印机"
    }]
}

4.3 获取打印机配置(getPrinterConfig)

请求格式

{
    "cmd": "getPrinterConfig",
    "printer": "菜鸟打印机",
    "version": "1.0",
    "requestID": "123456789"
}

响应格式

{
    "cmd": "getPrinterConfig",
    "requestID": "123456789",
    "status": "success",
    "printer": {
        "name": "打印机名称",
        "needTopLogo": false,
        "needBottomLogo": false,
        "horizontalOffset": 1,
        "verticalOffset": 2,
        "forceNoPageMargins": true,
        "autoPageSize": false,
        "orientation": 0,
        "paperSize": {
            "width": 100,
            "height": 180
        }
    }
}

4.4 打印通知(notifyPrintResult)

打印通知是菜鸟打印组件的重要功能,用于实时反馈打印任务的执行状态。

通知机制说明

  • 推送方式:由打印组件主动推送,无需客户端请求
  • 推送时机
    • 文档渲染完成时(taskStatus: "rendered")
    • 打印机出纸完成时(taskStatus: "printed")
    • 任务失败时(status: "failed")
  • 通知频率:每个文档的每个状态只通知一次

通知消息格式

{
    "cmd": "notifyPrintResult",
    "printer": "中通打印机A",
    "taskID": "1",
    "taskStatus": "printed",
    "printStatus": [{
        "documentID": "9890000112011",
        "status": "success",
        "msg": "",
        "detail": ""
    }]
}

字段详细说明

taskStatus(任务级别状态)

  • rendered:渲染完成
    • 表示文档已经成功生成为打印格式
    • 此时还未发送到打印机
  • printed:出纸完成
    • 表示打印机已成功打印文档
    • 这是最终的成功状态
  • failed:任务失败
    • 整个任务执行失败

status(文档级别状态)

  • success:成功
    • 该文档打印成功
  • failed:失败
    • 该文档打印失败,msg字段会包含失败原因
  • canceled:取消
    • 当任务中某个文档失败时,后续文档会被标记为取消状态

错误信息处理

当打印失败时,通过以下字段获取详细信息:

  • msg:错误原因概要,如"打印机缺纸"、"打印机离线"等
  • detail:错误的详细描述,包含更多技术细节

通知处理示例

socket.onmessage = function(event) {
    const message = JSON.parse(event.data);
    
    if (message.cmd === 'notifyPrintResult') {
        const { taskID, taskStatus, printStatus } = message;
        
        console.log(`任务 ${taskID} 状态: ${taskStatus}`);
        
        printStatus.forEach(doc => {
            if (doc.status === 'success') {
                console.log(`文档 ${doc.documentID} 打印成功`);
                // 更新业务系统状态
                updateOrderStatus(doc.documentID, 'printed');
            } else if (doc.status === 'failed') {
                console.error(`文档 ${doc.documentID} 打印失败: ${doc.msg}`);
                // 记录失败信息,准备重试
                logPrintError(doc.documentID, doc.msg, doc.detail);
            }
        });
        
        // 任务完成后可以关闭连接
        if (taskStatus === 'printed' || taskStatus === 'failed') {
            // 延迟关闭,确保所有消息处理完成
            setTimeout(() => socket.close(), 1000);
        }
    }
};

注意事项

  1. 保持连接:必须保持 WebSocket 连接才能接收通知
  2. 状态更新:及时根据通知更新业务系统中的打印状态
  3. 错误处理:失败时记录详细信息,便于问题排查
  4. 重试机制:对于失败的文档,建议实现自动或手动重试

4.5 其他API

设置打印机配置(setPrinterConfig)

用于修改打印机配置参数。

获取任务状态(getTaskStatus)

查询指定任务的打印状态。

获取/设置全局配置(getGlobalConfig/setGlobalConfig)

管理全局配置选项,如错误通知、字体显示等。

获取客户端版本(getAgentInfo)

获取打印组件的版本信息。

5. 代码示例

5.1 JavaScript 示例

// 连接管理
function doConnect() {
    const socket = new WebSocket('ws://localhost:13528');
    
    socket.onopen = function(event) {
        console.log('连接已建立');
        
        // 获取打印机列表
        getPrinterList();
    };
    
    socket.onmessage = function(event) {
        const response = JSON.parse(event.data);
        handleResponse(response);
    };
    
    socket.onclose = function(event) {
        console.log('连接已关闭', event);
    };
    
    return socket;
}

// 生成UUID
function getUUID(len, radix) {
    const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
    const uuid = [];
    radix = radix || chars.length;
    
    for (let i = 0; i < len; i++) {
        uuid[i] = chars[0 | Math.random() * radix];
    }
    
    return uuid.join('');
}

// 构造请求对象
function getRequestObject(cmd) {
    return {
        requestID: getUUID(8, 16),
        version: "1.0",
        cmd: cmd
    };
}

// 获取打印机列表
function getPrinterList() {
    const request = getRequestObject("getPrinters");
    socket.send(JSON.stringify(request));
}

// 执行打印任务
function doPrint(printer, waybillArray) {
    const request = getRequestObject("print");
    
    request.task = {
        taskID: getUUID(8, 10),
        preview: false,
        printer: printer,
        documents: waybillArray.map(waybillNo => ({
            documentID: waybillNo,
            contents: [{
                data: getWaybillData(waybillNo),
                templateURL: getTemplateURL(waybillNo)
            }]
        }))
    };
    
    socket.send(JSON.stringify(request));
}

// 处理响应
function handleResponse(response) {
    switch(response.cmd) {
        case 'getPrinters':
            console.log('打印机列表:', response.printers);
            break;
        case 'print':
            if (response.status === 'success') {
                console.log('打印任务已提交');
            }
            break;
        case 'notifyPrintResult':
            console.log('打印结果通知:', response);
            break;
    }
}

5.2 Java 示例

import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;

public class CainiaoPrintClient extends WebSocketClient {
    
    public CainiaoPrintClient(URI serverUri) {
        super(serverUri);
    }
    
    @Override
    public void onOpen(ServerHandshake handshake) {
        System.out.println("连接已建立");
        
        // 获取打印机列表
        String cmd = "{\"cmd\":\"getPrinters\",\"requestID\":\"123456\",\"version\":\"1.0\"}";
        send(cmd);
    }
    
    @Override
    public void onMessage(String message) {
        System.out.println("收到消息:" + message);
        // 处理响应消息
    }
    
    @Override
    public void onClose(int code, String reason, boolean remote) {
        System.out.println("连接关闭:" + reason);
    }
    
    @Override
    public void onError(Exception ex) {
        ex.printStackTrace();
    }
    
    public static void main(String[] args) throws Exception {
        CainiaoPrintClient client = new CainiaoPrintClient(new URI("ws://localhost:13528"));
        client.connect();
    }
}

5.3 C++ 示例

5.3.1 动态链接库接口说明

C++ Demo 使用动态链接库方式,提供以下接口(调用约定:stdcall):

// 初始化打印管理器
int initPrinterManager(const char *url);

// 设置接收数据的回调函数
void setRecvDataCallback(_onMessage_func func);

// 发送消息
int sendMessage(const char *message);

// 关闭打印管理器
int closePrinterManager();

接口详细说明:

  1. initPrinterManager

    • 功能:初始化打印管理器,建立 WebSocket 连接
    • 参数:url - WebSocket 地址,如 "ws://127.0.0.1:13528"
    • 返回值:
      • 0:成功
      • -1:初始化失败
      • -2:重复初始化错误
      • -3:未输入 URL
    • 注意:每个 DLL 实例只能调用一次
  2. setRecvDataCallback

    • 功能:设置消息接收回调函数
    • 参数:函数指针,类型为 typedef void(*_onMessage_func)(const char* message)
  3. sendMessage

    • 功能:发送消息到打印客户端
    • 参数:UTF-8 编码的 JSON 字符串
    • 返回值:
      • 0:成功
      • -1:WebSocket 未连接
  4. closePrinterManager

    • 功能:关闭 WebSocket 连接
    • 返回值:
      • 0:成功
      • -1:连接已关闭或未初始化

5.3.2 完整使用示例

#include <string>
#include <iostream>
#include "cJSON.h"
#include "PrinterManager.h"

// 消息处理回调函数
void handle_message(const char* message) {
    printf("收到消息: %s\n", message);
    
    // 解析响应
    cJSON* root = cJSON_Parse(message);
    if (root) {
        const char* cmd = cJSON_GetStringValue(cJSON_GetObjectItem(root, "cmd"));
        
        if (strcmp(cmd, "getPrinters") == 0) {
            // 处理打印机列表
            cJSON* printers = cJSON_GetObjectItem(root, "printers");
            // ... 处理逻辑
        } else if (strcmp(cmd, "notifyPrintResult") == 0) {
            // 处理打印结果通知
            const char* taskID = cJSON_GetStringValue(cJSON_GetObjectItem(root, "taskID"));
            const char* taskStatus = cJSON_GetStringValue(cJSON_GetObjectItem(root, "taskStatus"));
            printf("任务 %s 状态: %s\n", taskID, taskStatus);
        }
        
        cJSON_Delete(root);
    }
}

// 构造获取打印机列表请求
std::string getRequestObject() {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "cmd", "getPrinters");
    cJSON_AddStringToObject(root, "requestID", "123458976");
    cJSON_AddStringToObject(root, "version", "1.0");
    
    char* jsonStr = cJSON_Print(root);
    std::string result(jsonStr);
    
    cJSON_free(jsonStr);
    cJSON_Delete(root);
    
    return result;
}

// 构造打印请求
std::string getPrintRequestObject(const std::string& waybillNo, const std::string& templateData) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "cmd", "print");
    cJSON_AddStringToObject(root, "requestID", "123458976");
    cJSON_AddStringToObject(root, "version", "1.0");
    
    // 创建task对象
    cJSON *task = cJSON_CreateObject();
    cJSON_AddStringToObject(task, "taskID", "1");
    cJSON_AddFalseToObject(task, "preview");
    cJSON_AddStringToObject(task, "printer", "");
    
    // 创建documents数组
    cJSON *documents = cJSON_CreateArray();
    cJSON *document = cJSON_CreateObject();
    cJSON_AddStringToObject(document, "documentID", waybillNo.c_str());
    
    // 创建contents数组
    cJSON *contents = cJSON_CreateArray();
    cJSON *content = cJSON_CreateObject();
    
    // 解析模板数据
    cJSON *data = cJSON_Parse(templateData.c_str());
    cJSON_AddItemToObject(content, "data", data);
    cJSON_AddStringToObject(content, "templateURL", "http://cloudprint.cainiao.com/template/standard/278250/1");
    
    cJSON_AddItemToArray(contents, content);
    cJSON_AddItemToObject(document, "contents", contents);
    cJSON_AddItemToArray(documents, document);
    cJSON_AddItemToObject(task, "documents", documents);
    cJSON_AddItemToObject(root, "task", task);
    
    char* jsonStr = cJSON_PrintBuffered(root, 4096, 0); // 使用PrintBuffered去除换行
    std::string result(jsonStr);
    
    cJSON_free(jsonStr);
    cJSON_Delete(root);
    
    return result;
}

// 主程序
int main() {
    // 1. 初始化打印管理器
    int ret = initPrinterManager("ws://127.0.0.1:13528");
    if (ret != 0) {
        printf("初始化失败: %d\n", ret);
        return -1;
    }
    
    // 2. 设置回调函数
    setRecvDataCallback(handle_message);
    
    // 3. 获取打印机列表
    std::string getPrintersCmd = getRequestObject();
    ret = sendMessage(getPrintersCmd.c_str());
    if (ret != 0) {
        printf("发送消息失败\n");
    }
    
    // 4. 发送打印任务
    std::string printData = "{\"nick\":\"张三\",\"address\":\"浙江省杭州市\"}";
    std::string printCmd = getPrintRequestObject("9890106027", printData);
    ret = sendMessage(printCmd.c_str());
    
    // 5. 等待处理(实际应用中应该使用事件循环)
    Sleep(10000);
    
    // 6. 关闭连接
    closePrinterManager();
    
    return 0;
}

6. 最佳实践

6.1 官方推荐实践

根据官方文档,由于网络协议本身的不可靠性,建议按照以下规范进行接入,否则可能出现漏打、重复打等情况:

6.1.1 标准打印流程

  1. 发送指令前检查 WebSocket 连接可用性,若不可用则重连
  2. 发送打印指令后等待【任务已提交】响应,此时可以告知用户任务已提交打印
  3. 保持连接存活(不主动关闭),持续监听 notifyPrintResult 消息
  4. 接收到成功通知后,修改业务系统中打印任务状态并提示用户
  5. 接收到失败通知后,记录失败信息并提示用户任务失败
  6. 合理配置 taskID 和 idempotent 参数以符合业务预期

6.1.2 核心建议

  • 一个 task 使用一个 document:可以有效避免重打问题
  • 使用长连接:不要每次发送请求都创建新的 WebSocket 对象
  • 特殊字符转义:JSON 报文中的特殊字符(回车、引号等)需要正确转义

6.2 连接管理最佳实践

6.2.1 智能连接策略(支持 WS 和 WSS)

class SmartPrinterConnection {
    constructor() {
        this.socket = null;
        this.isConnected = false;
        this.pendingTasks = new Map();
        this.connectionUrls = [
            'ws://localhost:13528',   // HTTP 环境
            'wss://localhost:13529'    // HTTPS 环境
        ];
        this.currentUrlIndex = 0;
    }
    
    // 智能连接:尝试所有可用端口
    async connect() {
        const errors = [];
        
        // 尝试所有可用的连接地址
        for (let i = 0; i < this.connectionUrls.length; i++) {
            const url = this.connectionUrls[(this.currentUrlIndex + i) % this.connectionUrls.length];
            
            try {
                console.log(`尝试连接: ${url}`);
                await this.connectToUrl(url);
                
                // 连接成功,记录当前使用的URL索引
                this.currentUrlIndex = (this.currentUrlIndex + i) % this.connectionUrls.length;
                console.log(`成功连接到: ${url}`);
                return;
                
            } catch (error) {
                console.error(`连接 ${url} 失败:`, error.message);
                errors.push({ url, error: error.message });
            }
        }
        
        // 所有连接都失败
        throw new Error(`无法连接到打印服务。尝试的地址: ${JSON.stringify(errors)}`);
    }
    
    // 连接到指定URL
    connectToUrl(url) {
        return new Promise((resolve, reject) => {
            let timeout;
            
            try {
                const socket = new WebSocket(url);
                
                // 设置连接超时
                timeout = setTimeout(() => {
                    socket.close();
                    reject(new Error('连接超时'));
                }, 5000);
                
                socket.onopen = () => {
                    clearTimeout(timeout);
                    this.socket = socket;
                    this.isConnected = true;
                    this.setupEventHandlers();
                    resolve();
                };
                
                socket.onerror = (error) => {
                    clearTimeout(timeout);
                    reject(new Error('连接错误'));
                };
                
                socket.onclose = () => {
                    clearTimeout(timeout);
                    if (!this.isConnected) {
                        reject(new Error('连接被拒绝'));
                    }
                };
                
            } catch (error) {
                clearTimeout(timeout);
                reject(error);
            }
        });
    }
    
    // 设置事件处理器
    setupEventHandlers() {
        this.socket.onclose = () => {
            this.isConnected = false;
            console.log('打印服务连接断开');
            
            // 清理未完成的任务
            this.pendingTasks.forEach((task, requestID) => {
                task.reject(new Error('连接断开'));
            });
            this.pendingTasks.clear();
        };
        
        this.socket.onerror = (error) => {
            console.error('WebSocket 错误:', error);
        };
        
        this.socket.onmessage = (event) => {
            try {
                const message = JSON.parse(event.data);
                this.handleMessage(message);
            } catch (error) {
                console.error('消息解析错误:', error);
            }
        };
    }
    
    // 确保连接可用
    async ensureConnection() {
        if (!this.isConnected || 
            !this.socket || 
            this.socket.readyState !== WebSocket.OPEN) {
            await this.connect();
        }
    }
    
    // 发送打印任务(带重连机制)
    async sendPrintTask(task, retryCount = 0) {
        try {
            // 1. 确保连接可用
            await this.ensureConnection();
            
            // 2. 发送任务
            this.socket.send(JSON.stringify(task));
            
            // 3. 等待任务提交响应
            return await this.waitForResponse(task.requestID);
            
        } catch (error) {
            // 连接失败时的重试逻辑
            if (error.message.includes('连接') && retryCount < 2) {
                console.log(`连接失败,尝试其他端口 (${retryCount + 1}/2)`);
                this.currentUrlIndex = (this.currentUrlIndex + 1) % this.connectionUrls.length;
                return this.sendPrintTask(task, retryCount + 1);
            }
            throw error;
        }
    }
    
    // 等待响应
    waitForResponse(requestID) {
        return new Promise((resolve, reject) => {
            this.pendingTasks.set(requestID, { resolve, reject });
            
            // 设置响应超时
            setTimeout(() => {
                if (this.pendingTasks.has(requestID)) {
                    this.pendingTasks.delete(requestID);
                    reject(new Error('任务提交超时'));
                }
            }, 10000);
        });
    }
    
    handleMessage(message) {
        // 处理任务提交响应
        if (message.cmd === 'print' && this.pendingTasks.has(message.requestID)) {
            const { resolve, reject } = this.pendingTasks.get(message.requestID);
            this.pendingTasks.delete(message.requestID);
            
            if (message.status === 'success') {
                console.log('任务已提交到打印队列');
                resolve(message);
            } else {
                reject(new Error(message.msg || '任务提交失败'));
            }
        }
        
        // 处理打印结果通知 - 保持连接不关闭
        if (message.cmd === 'notifyPrintResult') {
            this.handlePrintNotification(message);
        }
    }
    
    handlePrintNotification(notification) {
        const { taskID, taskStatus, printStatus } = notification;
        
        printStatus.forEach(doc => {
            if (doc.status === 'success') {
                this.updateBusinessStatus(doc.documentID, 'printed');
                this.notifyUser(`单号 ${doc.documentID} 打印成功`);
            } else if (doc.status === 'failed') {
                this.updateBusinessStatus(doc.documentID, 'print_failed', doc.msg);
                this.notifyUser(`单号 ${doc.documentID} 打印失败: ${doc.msg}`);
            }
        });
    }
    
    // 业务状态更新(需要实现)
    updateBusinessStatus(documentID, status, message) {
        // 更新业务系统中的状态
        console.log(`更新状态: ${documentID} -> ${status}`, message || '');
    }
    
    // 用户通知(需要实现)
    notifyUser(message) {
        console.log(`[通知] ${message}`);
    }
}

// 使用示例
async function initPrinter() {
    const connection = new SmartPrinterConnection();
    
    try {
        // 自动尝试 ws 和 wss 连接
        await connection.connect();
        return connection;
    } catch (error) {
        console.error('初始化打印服务失败:', error);
        // 提示用户检查打印组件是否启动
        alert('无法连接到菜鸟打印组件,请确保:\n1. 打印组件已启动\n2. 端口 13528 或 13529 未被占用');
        throw error;
    }
}

6.2.2 高级连接管理(根据页面协议自动选择)

class AdaptivePrinterConnection extends SmartPrinterConnection {
    constructor() {
        super();
        // 根据当前页面协议智能排序连接地址
        this.setupConnectionUrls();
    }
    
    setupConnectionUrls() {
        const isHttps = window.location.protocol === 'https:';
        
        if (isHttps) {
            // HTTPS 页面优先尝试 WSS
            this.connectionUrls = [
                'wss://localhost:13529',
                'ws://localhost:13528'   // 某些浏览器允许混合内容
            ];
        } else {
            // HTTP 页面优先尝试 WS
            this.connectionUrls = [
                'ws://localhost:13528',
                'wss://localhost:13529'
            ];
        }
        
        console.log(`当前页面协议: ${window.location.protocol}`);
        console.log(`连接优先级: ${this.connectionUrls.join(' -> ')}`);
    }
    
    // 检测端口占用情况(可选功能)
    async detectAvailablePorts() {
        const results = [];
        
        for (const url of this.connectionUrls) {
            const startTime = Date.now();
            
            try {
                await this.testConnection(url);
                results.push({
                    url,
                    available: true,
                    responseTime: Date.now() - startTime
                });
            } catch (error) {
                results.push({
                    url,
                    available: false,
                    error: error.message
                });
            }
        }
        
        return results;
    }
    
    // 测试连接(快速探测)
    testConnection(url) {
        return new Promise((resolve, reject) => {
            const socket = new WebSocket(url);
            const timeout = setTimeout(() => {
                socket.close();
                reject(new Error('连接超时'));
            }, 2000);
            
            socket.onopen = () => {
                clearTimeout(timeout);
                socket.close();
                resolve();
            };
            
            socket.onerror = () => {
                clearTimeout(timeout);
                reject(new Error('连接失败'));
            };
        });
    }
}

// 带诊断功能的连接示例
async function connectWithDiagnostics() {
    const connection = new AdaptivePrinterConnection();
    
    // 先进行端口检测(可选)
    console.log('检测可用端口...');
    const portStatus = await connection.detectAvailablePorts();
    console.table(portStatus);
    
    // 连接到可用端口
    try {
        await connection.connect();
        console.log('打印服务连接成功');
        return connection;
    } catch (error) {
        console.error('连接失败,端口状态:', portStatus);
        throw error;
    }
}

6.2.3 连接池管理(适用于高并发场景)

class PrinterConnectionPool {
    constructor(maxConnections = 3) {
        this.connections = [];
        this.maxConnections = maxConnections;
        this.currentIndex = 0;
    }
    
    async initialize() {
        // 创建多个连接
        for (let i = 0; i < this.maxConnections; i++) {
            try {
                const connection = new SmartPrinterConnection();
                await connection.connect();
                this.connections.push(connection);
            } catch (error) {
                console.warn(`连接 ${i + 1} 创建失败:`, error);
            }
        }
        
        if (this.connections.length === 0) {
            throw new Error('无法创建任何打印连接');
        }
        
        console.log(`连接池初始化完成,可用连接数: ${this.connections.length}`);
    }
    
    // 获取可用连接(轮询)
    getConnection() {
        if (this.connections.length === 0) {
            throw new Error('没有可用的打印连接');
        }
        
        // 轮询选择连接
        const connection = this.connections[this.currentIndex];
        this.currentIndex = (this.currentIndex + 1) % this.connections.length;
        
        return connection;
    }
    
    // 发送打印任务
    async sendPrintTask(task) {
        let lastError;
        
        // 尝试使用不同的连接
        for (let i = 0; i < this.connections.length; i++) {
            try {
                const connection = this.getConnection();
                return await connection.sendPrintTask(task);
            } catch (error) {
                lastError = error;
                console.warn(`连接 ${i + 1} 发送失败:`, error.message);
            }
        }
        
        throw lastError || new Error('所有连接都失败');
    }
}

// 使用连接池示例
const printerPool = new PrinterConnectionPool();
await printerPool.initialize();

// 发送打印任务
const task = createPrintTask(documentData);
const result = await printerPool.sendPrintTask(task);

6.3 任务管理最佳实践

6.3.1 遵循一个 task 一个 document 原则

// 推荐做法:每个任务只包含一个文档
async function printSingleDocument(documentData) {
    const task = {
        cmd: "print",
        requestID: generateUUID(),
        version: "1.0",
        task: {
            taskID: generateTaskID(),
            preview: false,
            printer: "默认打印机",
            documents: [{
                documentID: documentData.waybillNo,
                contents: [{
                    data: documentData.printData,
                    templateURL: documentData.templateURL
                }]
            }]
        }
    };
    
    return await printerConnection.sendPrintTask(task);
}

// 批量打印时,分别创建多个任务
async function batchPrint(documentList) {
    const results = [];
    
    for (const doc of documentList) {
        try {
            const result = await printSingleDocument(doc);
            results.push({ success: true, documentID: doc.waybillNo });
        } catch (error) {
            results.push({ success: false, documentID: doc.waybillNo, error });
        }
    }
    
    return results;
}

6.3.2 taskID 和 idempotent 配置

// 根据业务需求配置幂等性
function createPrintTask(documentData, options = {}) {
    const task = {
        cmd: "print",
        requestID: generateUUID(),
        version: "1.0",
        task: {
            taskID: options.taskID || generateTaskID(),
            // 1.5.0+ 版本支持
            idempotent: options.allowDuplicate === false, // true: 不允许重复
            preview: false,
            documents: [{
                documentID: documentData.waybillNo,
                contents: [...]
            }]
        }
    };
    
    return task;
}

// 业务场景示例
// 场景1:订单打印 - 不允许重复
const orderPrintTask = createPrintTask(orderData, {
    taskID: `order-${orderData.orderId}`,
    allowDuplicate: false
});

// 场景2:补打场景 - 允许重复
const reprintTask = createPrintTask(orderData, {
    taskID: `reprint-${Date.now()}`,
    allowDuplicate: true
});

6.4 数据处理最佳实践

6.4.1 特殊字符转义

// 确保数据正确转义
function preparePrintData(rawData) {
    // 使用 JSON.stringify 自动处理转义
    const safeData = JSON.parse(JSON.stringify(rawData));
    
    // 特殊处理某些字段
    if (safeData.address) {
        // 移除可能的控制字符
        safeData.address = safeData.address.replace(/[\x00-\x1F\x7F]/g, '');
    }
    
    return safeData;
}

// 构建打印内容时的安全处理
function buildPrintContent(data, templateURL) {
    return {
        data: preparePrintData(data),
        templateURL: templateURL
    };
}

6.4.2 编码处理

// 确保所有文本使用 UTF-8 编码
function ensureUTF8(text) {
    try {
        // 尝试解码和重新编码以确保UTF-8
        return decodeURIComponent(encodeURIComponent(text));
    } catch (e) {
        console.error('编码转换失败:', e);
        return text;
    }
}

6.5 错误处理与重试机制

6.5.1 完整的错误处理流程

class PrintTaskManager {
    constructor() {
        this.connection = new ReliablePrinterConnection();
        this.retryConfig = {
            maxRetries: 3,
            retryDelay: 5000
        };
    }
    
    async printWithRetry(documentData, retryCount = 0) {
        try {
            // 发送打印任务
            const response = await this.connection.sendPrintTask(
                this.createPrintTask(documentData)
            );
            
            // 任务提交成功,等待打印结果
            console.log(`任务 ${response.taskID} 已提交,等待打印结果...`);
            
            // 这里不关闭连接,继续监听 notifyPrintResult
            return response;
            
        } catch (error) {
            console.error(`打印失败 (尝试 ${retryCount + 1}):`, error);
            
            // 判断是否需要重试
            if (retryCount < this.retryConfig.maxRetries) {
                await this.delay(this.retryConfig.retryDelay);
                return this.printWithRetry(documentData, retryCount + 1);
            } else {
                throw new Error(`打印失败,已重试 ${this.retryConfig.maxRetries}`);
            }
        }
    }
    
    delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

6.6 性能优化建议

6.6.1 连接复用

// 使用单例模式管理打印连接
class PrinterService {
    static instance = null;
    
    static getInstance() {
        if (!PrinterService.instance) {
            PrinterService.instance = new PrinterService();
        }
        return PrinterService.instance;
    }
    
    constructor() {
        if (PrinterService.instance) {
            return PrinterService.instance;
        }
        this.connection = new ReliablePrinterConnection();
    }
}

// 在应用中使用
const printerService = PrinterService.getInstance();

6.6.2 打印配置缓存

class PrinterConfigCache {
    constructor() {
        this.cache = new Map();
        this.cacheExpiry = 30 * 60 * 1000; // 30分钟
    }
    
    async getPrinterConfig(printerName) {
        const cached = this.cache.get(printerName);
        
        if (cached && Date.now() - cached.timestamp < this.cacheExpiry) {
            return cached.config;
        }
        
        // 获取新配置
        const config = await this.fetchPrinterConfig(printerName);
        this.cache.set(printerName, {
            config,
            timestamp: Date.now()
        });
        
        return config;
    }
}

6.7 监控与日志

6.7.1 打印状态跟踪

class PrintStatusTracker {
    constructor() {
        this.printJobs = new Map();
    }
    
    trackPrintJob(taskID, documentID) {
        this.printJobs.set(taskID, {
            documentID,
            submitTime: new Date(),
            status: 'submitted',
            attempts: 1
        });
    }
    
    updateJobStatus(taskID, status, message = '') {
        const job = this.printJobs.get(taskID);
        if (job) {
            job.status = status;
            job.lastUpdate = new Date();
            job.message = message;
            
            // 记录到日志
            this.logPrintStatus(job);
        }
    }
    
    logPrintStatus(job) {
        const logEntry = {
            timestamp: new Date().toISOString(),
            documentID: job.documentID,
            status: job.status,
            duration: job.lastUpdate - job.submitTime,
            message: job.message
        };
        
        console.log('[打印日志]', JSON.stringify(logEntry));
        
        // 发送到监控系统
        this.sendToMonitoring(logEntry);
    }
}

6.8 安全注意事项

  1. 生产环境使用 WSSwss://localhost:13529
  2. 敏感信息处理:日志中避免记录完整的用户信息
  3. 访问控制:确保只有授权的应用可以访问打印服务

7. 常见问题

7.1 连接问题

Q: 无法连接到打印组件? A: 检查打印组件是否已启动,端口是否正确(HTTP:13528, HTTPS:13529)。

7.2 打印问题

Q: 打印任务提交成功但没有打印? A: 检查打印机状态,确认是否收到 notifyPrintResult 通知。

7.3 字符编码

Q: 打印内容出现乱码? A: 确保所有数据使用 UTF-8 编码,特殊字符需要进行 JSON 转义。

8. 高级功能

8.1 PDF 直接打印

当需要直接打印 PDF 文件时,可以使用 printType: "dirctPrint" 模式:

// PDF 直打示例
function printPDF(pdfUrl, printer) {
    const task = {
        cmd: "print",
        requestID: generateUUID(),
        version: "1.0",
        task: {
            taskID: generateTaskID(),
            preview: false,
            printer: printer || "",
            printType: "dirctPrint",  // 设置为 PDF 直打模式
            documents: [{
                documentID: "pdf_" + Date.now(),
                contents: [{
                    data: {},  // PDF 模式下 data 可以为空
                    templateURL: pdfUrl  // PDF 文件的 URL
                }]
            }]
        }
    };
    
    return printerConnection.sendPrintTask(task);
}

// 使用示例
await printPDF("http://example.com/invoice.pdf", "默认打印机");

8.2 混合打印模式

在同一个打印任务中混合使用模板打印和自定义区域:

// 标准面单 + 自定义区域打印
function printWaybillWithCustomArea(waybillData, customData) {
    const task = {
        cmd: "print",
        requestID: generateUUID(),
        version: "1.0",
        task: {
            taskID: generateTaskID(),
            preview: false,
            documents: [{
                documentID: waybillData.waybillNo,
                contents: [
                    {
                        // 标准面单内容
                        data: waybillData.printData,
                        templateURL: waybillData.templateURL
                    },
                    {
                        // 自定义区域内容
                        data: {
                            value: customData.customText
                        },
                        templateURL: customData.customTemplateURL
                    }
                ]
            }]
        }
    };
    
    return printerConnection.sendPrintTask(task);
}

8.3 密文数据处理(菜鸟电子面单)

处理加密的电子面单数据:

// 处理密文面单数据
function printEncryptedWaybill(encryptedData) {
    const task = {
        cmd: "print",
        requestID: generateUUID(),
        version: "1.0",
        task: {
            taskID: generateTaskID(),
            preview: false,
            documents: [{
                documentID: encryptedData.waybillNo,
                contents: [{
                    encryptedData: encryptedData.content,
                    signature: encryptedData.signature,
                    templateURL: encryptedData.templateURL,
                    ver: "waybill_print_secret_version_1"
                }]
            }]
        }
    };
    
    return printerConnection.sendPrintTask(task);
}

9. 注意事项

9.1 特殊字符处理

在与打印组件交互过程中,JSON 报文中的特殊字符必须正确转义:

// 特殊字符转义示例
function escapeJsonString(str) {
    // JSON.stringify 会自动处理转义
    return JSON.stringify(str).slice(1, -1);
}

// 需要转义的特殊字符
const specialChars = {
    '"': '\\"',     // 双引号
    '\\': '\\\\',   // 反斜杠
    '\b': '\\b',    // 退格
    '\f': '\\f',    // 换页
    '\n': '\\n',    // 换行
    '\r': '\\r',    // 回车
    '\t': '\\t'     // 制表符
};

// 手动转义示例(通常不需要,JSON.stringify 会处理)
function manualEscape(text) {
    return text.replace(/["\\\/\b\f\n\r\t]/g, function(char) {
        return specialChars[char] || char;
    });
}

详细的 JSON 规范请参考:http://www.json.org/json-zh.html

9.2 任务去重配置

根据不同版本和业务需求配置任务去重:

// 0.x 版本:默认不允许 taskID 重复
const taskFor0x = {
    taskID: "unique_task_id",  // 必须唯一
    // ... 其他配置
};

// 1.5.0+ 版本:通过 idempotent 控制
const taskFor15x = {
    taskID: "order_12345",
    idempotent: true,  // true: 不允许重复,false: 允许重复(默认)
    // ... 其他配置
};

9.3 版本兼容性

不同版本的行为差异:

功能0.x 版本1.x 版本
taskID 重复默认不允许默认允许,通过 idempotent 控制
预览响应等待文件生成后返回立即返回,文件生成后再次通知
notifyType支持,控制通知类型已废弃,始终发送所有通知
urls 字段不支持预览时返回 urls 数组

9.4 编码要求

  • 所有文本数据必须使用 UTF-8 编码
  • C++ 开发时特别注意字符串编码转换
  • 跨平台开发时注意编码一致性

9. 附录

9.1 状态码说明

  • success:操作成功
  • failed:操作失败
  • pending:任务等待中
  • printed:打印完成
  • rendered:渲染完成

9.2 相关资源


更新日期:2024-08-02
版本:1.0