- Published on
菜鸟打印组件开发文档
菜鸟打印组件开发文档
1. 概述
1.1 简介
菜鸟云打印客户端是一个独立进程,通过 WebSocket 协议与浏览器或其他客户端进行通信,支持 JavaScript、Java、C/C++、Python 等常用编程语言。
1.2 系统要求
- 浏览器版本:Chrome 45 及以上(推荐使用最新版本)
- 连接方式:WebSocket
- 端口配置:
- HTTP环境:
ws://localhost:13528
- HTTPS环境:
wss://localhost:13529
- HTTP环境:
1.3 更新信息
- 更新日期:2024-08-02
- 当前协议版本:1.0
1.4 官方文档
- 官方文档地址:https://support-cnkuaidi.taobao.com/doc.htm#?docId=107014&docType=1
- 本文档基于官方文档整理,如有疑问请以官方文档为准
2. 快速开始
2.1 建立连接
// HTTP环境
const socket = new WebSocket('ws://localhost:13528');
// HTTPS环境
const socket = new WebSocket('wss://localhost:13529');
2.2 基本通信流程
- 建立 WebSocket 连接
- 发送请求命令
- 接收响应结果
- 处理通知消息
3. 协议格式
3.1 请求协议格式
{
"cmd": "command",
"requestID": "unique requestID",
"version": "1.0"
}
字段说明:
字段名 | 类型 | 说明 | 是否必须 |
---|---|---|---|
cmd | string | 请求的命令名称 | 是 |
requestID | string | 请求的唯一ID(如UUID) | 是 |
version | string | 协议版本,当前为"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"
}]
}]
}
}
任务参数说明
字段名 | 类型 | 说明 | 是否必须 |
---|---|---|---|
taskID | string | 打印任务ID | 是 |
idempotent | bool | 是否允许taskID重复(1.5.0+) | 否 |
preview | bool | true为预览,false为打印 | 是 |
previewType | string | 预览模式:"pdf"或"image" | 否 |
printer | string | 打印机名称,空值使用默认打印机 | 否 |
templateURL | string | 模板文件URL | 是 |
documents | array | 文档数组,每个元素表示一页 | 是 |
响应格式
{
"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);
}
}
};
注意事项
- 保持连接:必须保持 WebSocket 连接才能接收通知
- 状态更新:及时根据通知更新业务系统中的打印状态
- 错误处理:失败时记录详细信息,便于问题排查
- 重试机制:对于失败的文档,建议实现自动或手动重试
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();
接口详细说明:
initPrinterManager
- 功能:初始化打印管理器,建立 WebSocket 连接
- 参数:
url
- WebSocket 地址,如 "ws://127.0.0.1:13528" - 返回值:
- 0:成功
- -1:初始化失败
- -2:重复初始化错误
- -3:未输入 URL
- 注意:每个 DLL 实例只能调用一次
setRecvDataCallback
- 功能:设置消息接收回调函数
- 参数:函数指针,类型为
typedef void(*_onMessage_func)(const char* message)
sendMessage
- 功能:发送消息到打印客户端
- 参数:UTF-8 编码的 JSON 字符串
- 返回值:
- 0:成功
- -1:WebSocket 未连接
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 标准打印流程
- 发送指令前检查 WebSocket 连接可用性,若不可用则重连
- 发送打印指令后等待【任务已提交】响应,此时可以告知用户任务已提交打印
- 保持连接存活(不主动关闭),持续监听 notifyPrintResult 消息
- 接收到成功通知后,修改业务系统中打印任务状态并提示用户
- 接收到失败通知后,记录失败信息并提示用户任务失败
- 合理配置 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 安全注意事项
- 生产环境使用 WSS:
wss://localhost:13529
- 敏感信息处理:日志中避免记录完整的用户信息
- 访问控制:确保只有授权的应用可以访问打印服务
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 相关资源
- 菜鸟打印组件官方文档:https://support-cnkuaidi.taobao.com/doc.htm#?docId=107014&docType=1
- JSON 规范:http://www.json.org/json-zh.html
- WebSocket 库推荐:
- JavaScript:原生 WebSocket API
- Java:java-websocket 1.3.0+
- C++:配套动态链接库
更新日期:2024-08-02
版本:1.0