1. 项目概述:当C++遇上Bartender,自动化标签打印的“最后一公里”
在工业制造、物流仓储、零售门店这些领域,标签打印是再日常不过的操作。从产品外箱的条码到仓库货架的标识,一张小小的标签背后,是数据流与物理世界的关键连接点。很多项目里,我们用C++写好了核心的业务逻辑,生成了需要打印的数据,但到了打印环节,往往需要操作员手动打开Bartender这类专业标签设计软件,导入数据,再点击打印。这个“最后一公里”的手动操作,不仅效率低下,容易出错,更让整个自动化流程在这里断掉了。
我最近就接手了一个这样的项目:一个用C++开发的仓库管理系统(WMS),需要根据出库单自动打印贴标。手动操作显然不可行。经过一番摸索和实测,我找到了一套稳定、免费(指开发调用层面)的方案,实现了C++程序直接调用Bartender模板并驱动打印机完成打印。这听起来像是两个毫不相干的软件在握手,但一旦打通,带来的效率提升是巨大的。本文将详细拆解如何用C++,通过Bartender提供的自动化接口,轻松实现标签打印的集成,让你也能把这条“断头路”接上。
简单来说,核心就是:你的C++程序充当“指挥”,告诉Bartender这个“印刷厂”要印什么(数据)、按照哪个版式(模板)、印多少份。整个过程无需人工干预,完全由代码驱动。下面,我们就从原理到实操,一步步拆解这个过程。
1.1 核心需求与方案选型:为什么是Bartender Automation?
在决定用Bartender之前,我们评估过几种方案:
- 直接驱动打印机绘制标签:使用ZPL、EPL等打印机指令语言,或者Windows GDI/图形库直接画。这种方式最直接,但开发成本极高。你需要精确计算每个元素(文本、条码、图形)的位置、字体、大小,处理起来异常繁琐,且换一个标签模板就得重写大量代码,维护是噩梦。
- 使用专门的标签打印SDK:有些打印机厂商或第三方库提供SDK。但通常功能单一、兼容性差,或者价格不菲。
- 通过Bartender Automation接口:Bartender作为行业标杆,其设计器(Bartender Designer)功能强大,而它提供的Automation接口(本质上是COM组件)允许外部程序完全控制它。你可以在设计器里用可视化方式随心所欲地设计好标签模板,保存为
.btw文件。然后,C++程序只需要通过COM接口打开这个模板文件,填充数据,下达打印命令即可。所有复杂的排版、渲染、驱动兼容性问题,全部交给Bartender处理。
显然,第三种方案优势巨大:
- 开发效率高:专注于业务数据生成,无需关心图形渲染细节。
- 维护成本低:标签格式变更?只需用Bartender Designer修改
.btw模板文件,C++代码通常无需改动。 - 功能强大稳定:直接利用Bartender成熟且强大的标签设计、条码生成、打印机驱动支持。
- “免费”调用:这里说的免费,是指Bartender提供了Automation接口供开发者调用。你仍然需要一台安装了Bartender软件(至少是Runtime或Automation Edition)的电脑来运行这些接口。Bartender本身是商业软件,但其自动化接口在已安装的环境下是可以被自由调用的。
因此,我们的技术路线确定为:C++通过COM技术,调用Bartender Automation对象模型,实现模板加载、数据填充与打印执行。
2. 环境准备与Bartender Automation初探
2.1 软件环境搭建
要跑通整个流程,你需要准备以下环境:
Bartender 软件安装:
- 你的电脑上必须安装Bartender。对于开发和测试,你可以使用其提供的试用版。对于生产环境,则需要购买相应的许可证(如Automation Edition或更高版本)。
- 关键点:安装时,务必确保勾选了“Automation”或“SDK/COM组件”相关的选项。通常默认安装会包含,但最好在自定义安装中确认一下。这将注册必要的COM组件到你的系统。
- 版本注意:Bartender的不同大版本(如2016, 2019, 2022)的Automation对象模型可能有细微差别。建议你的开发环境与最终部署环境的Bartender主版本保持一致,以避免兼容性问题。本文基于较新的Bartender 2022进行演示,其核心接口与旧版基本兼容。
C++开发环境:
- IDE:Visual Studio 2015或更高版本(推荐VS2019/2022)。
- 项目类型:控制台应用程序(Console Application)或MFC/Win32应用程序均可。本文以Win32控制台应用为例,最纯粹。
- 运行库:确保目标机器上安装了对应版本的Microsoft Visual C++ Redistributable。这是运行任何VS编译的C++程序的基石。如果你的程序是x64的,就需要安装x64版本的运行库。这个依赖项经常被忽略,导致程序在别的电脑上“无法启动”或“缺少dll”。
一个简单的Bartender标签模板:
- 打开Bartender Designer,创建一个新标签。随意添加几个对象,比如:
- 一个“文本”对象,将其“数据源”类型暂时设置为“嵌入的数据”,内容写
ProductName。 - 一个“条码”对象(如Code 128),数据源同样设为嵌入数据,内容写
SKU123456。 - 一个“日期/时间”对象,使用“日期和时间”数据源。
- 一个“文本”对象,将其“数据源”类型暂时设置为“嵌入的数据”,内容写
- 设计好后,将其保存为一个
.btw文件,例如MyLabel.btw。记住这个文件的路径。
- 打开Bartender Designer,创建一个新标签。随意添加几个对象,比如:
注意:Bartender的
.btw文件是二进制格式,它完整存储了标签的版面、对象属性、数据源连接等信息。你的C++程序后续就是操作这个文件。
2.2 理解Bartender的COM对象模型
Bartender Automation暴露了一系列COM接口,其核心对象层次结构可以简化为:
- Application:根对象,代表Bartender应用程序本身。通过它可以创建或打开文档。
- Document(或Format):对应一个
.btw标签模板文件。这是我们操作的主要对象。 - DataSources和DataSource:文档的数据源集合和单个数据源。标签上每一个动态内容(如文本、条码的值)都链接到一个数据源。
- SubStrings:对于“嵌入的数据”类型的文本数据源,其内容可以被分解为多个“子字符串”,每个子字符串可以单独设置值。这是我们实现变量替换的关键。
- Printers和Printer:管理打印机。
我们的C++程序需要做的就是:启动Bartender的COM服务器(获取Application对象),打开指定的Document,找到对应的数据源并设置其值,最后执行打印。
由于Bartender是外部COM服务器,我们需要在C++项目中导入其类型库(Type Library),以便编译器能识别其接口和智能指针。
3. 核心实现:C++代码驱动Bartender打印
3.1 创建项目与导入Bartender类型库
- 在Visual Studio中创建一个新的“Windows控制台应用程序”项目,命名为
BartenderPrintDemo。 - 我们需要导入Bartender的COM类型库。最简单的方法是使用
#import指令。- 找到Bartender安装目录下的
BarTend.exe文件(通常在C:\Program Files\Seagull\BarTender Suite或类似路径)。 - 在你的C++源文件(如
main.cpp)的开头,添加以下代码:
- 找到Bartender安装目录下的
// 导入Bartender Automation类型库。 // 确保路径指向你机器上的BarTend.exe。 // 编译后会自动生成.tlh和.tli头文件。 #import "C:\\Program Files\\Seagull\\BarTender Suite\\BarTend.exe" rename_namespace("BtApi") raw_interfaces_only using namespace BtApi;`rename_namespace`是为了避免命名冲突,我们将Bartender的命名空间重命名为`BtApi`。`raw_interfaces_only`选项会生成更接近原始COM接口的代码,在某些复杂场景下控制更精细。- 初始化COM库。因为我们要使用COM,所以必须在程序开始和结束时初始化和卸载COM。
#include <iostream> #include <comdef.h> // 用于_bstr_t和_com_error #pragma comment(lib, "ole32.lib") #pragma comment(lib, "oleaut32.lib") int main() { // 初始化COM,使用多线程公寓(MTA)。对于控制台应用,MTA通常是合适的。 HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); if (FAILED(hr)) { std::cerr << "COM初始化失败。" << std::endl; return -1; } // ... 后续代码 ... // 程序结束前反初始化COM CoUninitialize(); return 0; }3.2 连接Bartender并打开标签模板
接下来,我们创建Bartender的Application对象,并打开之前设计好的.btw文件。
try { // 1. 创建Bartender Application对象的智能指针 IApplicationPtr spApp; hr = spApp.CreateInstance(__uuidof(Application)); if (FAILED(hr)) { std::cerr << "无法创建Bartender Application对象。请确保Bartender已正确安装。" << std::endl; CoUninitialize(); return -1; } // 2. (可选)设置Bartender应用程序是否可见。后台打印建议设置为不可见。 spApp->Visible = VARIANT_FALSE; // VARIANT_FALSE表示不可见 // 3. 打开标签模板文件 (.btw) // 使用_bstr_t方便地处理BSTR字符串 _bstr_t bstrFormatPath = L"C:\\MyLabels\\MyLabel.btw"; // 替换为你的.btw文件实际路径 IFormatPtr spFormat = spApp->Formats->Open(bstrFormatPath, VARIANT_FALSE, // 不指定打印机,使用模板默认打印机 L""); // 空字符串表示使用默认的打印机驱动选项 if (spFormat == nullptr) { std::cerr << "打开标签模板文件失败。" << std::endl; spApp.Release(); CoUninitialize(); return -1; } std::cout << "成功打开标签模板。" << std::endl; // ... 接下来设置数据和打印 ... } catch (const _com_error& e) { std::cerr << "COM错误发生: " << e.ErrorMessage() << std::endl; CoUninitialize(); return -1; }关键点解析:
IApplicationPtr和IFormatPtr是#import指令生成的智能指针类型,它们会自动管理COM对象的引用计数(AddRef和Release),极大简化了代码并避免内存泄漏。spApp->Visible = VARIANT_FALSE;这一行非常重要。如果你希望打印过程在后台静默完成,不弹出Bartender的用户界面,就必须将其设置为VARIANT_FALSE。否则,Bartender设计器窗口会闪出来,影响用户体验。Open方法的第二个参数如果设为VARIANT_TRUE,则可以指定一个特定的打印机名称(通过第三个参数)。这里我们使用模板默认设置。
3.3 关键步骤:为标签模板设置打印数据
这是整个流程的核心。我们需要找到模板中定义的数据源,并将C++程序中的变量值“注入”进去。
Bartender数据源类型很多,对于简单的变量替换,我们通常使用“嵌入的数据”类型的文本数据源。在设计模板时,我们放置了一个文本对象,其内容为ProductName。Bartender会为这个对象创建一个同名的数据源。
// 4. 获取数据源集合 IDataSourcesPtr spDataSources = spFormat->DataSources; // 5. 查找名为“ProductName”的数据源 _bstr_t bstrDataSourceName = L"ProductName"; IDataSourcePtr spDataSource = spDataSources->Item[bstrDataSourceName]; if (spDataSource == nullptr) { std::cerr << "未找到名为 'ProductName' 的数据源。请检查模板设计。" << std::endl; } else { // 6. 获取该数据源的子字符串集合(对于“嵌入的数据”,其内容可被分解) ISubStringsPtr spSubStrings = spDataSource->SubStrings; if (spSubStrings->Count > 0) { // 通常,第一个子字符串(索引为1)就是整个嵌入的文本 ISubStringPtr spSubString = spSubStrings->Item[1]; // 7. 设置子字符串的值 _bstr_t bstrNewValue = L"高端智能手机X1"; // 这是我们要打印的实际产品名 spSubString->Value = bstrNewValue; std::cout << "已设置 ProductName 数据源值。" << std::endl; } } // 用同样的方法设置条码数据源“SKU123456”的值 _bstr_t bstrSKUDataSourceName = L"SKU123456"; // 注意:数据源名就是条码嵌入的数据内容 IDataSourcePtr spSKUDataSource = spDataSources->Item[bstrSKUDataSourceName]; if (spSKUDataSource != nullptr) { ISubStringsPtr spSKUSubStrings = spSKUDataSource->SubStrings; if (spSKUSubStrings->Count > 0) { ISubStringPtr spSKUSubString = spSKUSubStrings->Item[1]; spSKUSubString->Value = _bstr_t(L"X1-2024-08-001"); // 实际的SKU编码 std::cout << "已设置条码数据源值。" << std::endl; } }实操心得与避坑指南:
- 数据源名称是关键:在C++代码中引用的数据源名称(
L“ProductName”),必须与Bartender模板中数据源的名称完全一致,包括大小写。最可靠的方法是在Bartender Designer中,右键点击对象(如文本),选择“属性”,在“数据源”选项卡中查看其“名称”。 - “嵌入的数据”与子字符串:当数据源类型是“嵌入的数据”时,其完整内容被视为一个字符串。
SubStrings集合将这个字符串分解。即使整个字符串只是一个变量名(如ProductName),它也是一个子字符串。所以通常我们操作Item[1]。 - 其他数据源类型:对于“数据库字段”、“命名数据源”等类型,设置方式不同。例如,如果连接了数据库,你可能需要先设置数据库连接字符串和查询,然后通过记录集(Recordset)来赋值。本文聚焦于最通用的变量替换场景。
- 日期/时间对象:模板中的日期/时间对象,如果其数据源是“日期和时间”,它是自动更新的,通常不需要在代码中设置。如果你需要打印一个特定的日期,可以将其数据源类型改为“嵌入的数据”,然后像普通文本一样赋值。
3.4 执行打印与清理资源
数据设置完成后,就可以触发打印了。
// 8. 执行打印 // 第一个参数:打印份数 // 第二个参数:是否在打印后关闭文档。VARIANT_TRUE表示打印后关闭。 spFormat->PrintOut(VARIANT_TRUE, // 打印份数,这里打印1份 VARIANT_TRUE); // 打印后关闭文档 std::cout << “打印命令已发送。” << std::endl; // 9. 如果上面没有设置打印后关闭,或者需要做其他操作,可以手动关闭文档 // spFormat->Close(VARIANT_FALSE); // 参数表示是否保存更改 // 10. 释放智能指针(实际上,超出作用域后智能指针会自动释放,这里显式释放一下更清晰) spFormat.Release(); spApp.Release(); std::cout << “标签打印任务完成!” << std::endl;PrintOut方法是指令打印的核心。第二个参数设为VARIANT_TRUE非常方便,它会在打印任务发送到打印机队列后,自动关闭Bartender中的这个文档格式,避免资源占用。- 即使设置了打印后关闭,良好的编程习惯也建议在代码逻辑结束时显式释放COM对象指针(虽然智能指针会帮我们做)。
3.5 完整示例代码整合
将上述所有步骤整合到一个完整的main.cpp中:
#import “C:\\Program Files\\Seagull\\BarTender Suite\\BarTend.exe” rename_namespace(“BtApi”) raw_interfaces_only using namespace BtApi; #include <iostream> #include <comdef.h> #pragma comment(lib, “ole32.lib”) #pragma comment(lib, “oleaut32.lib”) int main() { HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); if (FAILED(hr)) { std::cerr << “COM初始化失败。” << std::endl; return -1; } try { // 1. 创建Bartender应用实例 IApplicationPtr spApp; hr = spApp.CreateInstance(__uuidof(Application)); if (FAILED(hr)) throw _com_error(hr); spApp->Visible = VARIANT_FALSE; // 后台运行 // 2. 打开标签模板 _bstr_t bstrFormatPath = L“C:\\MyLabels\\MyLabel.btw”; IFormatPtr spFormat = spApp->Formats->Open(bstrFormatPath, VARIANT_FALSE, L“”); if (spFormat == nullptr) { std::cerr << “打开模板失败。” << std::endl; return -1; } // 3. 设置数据源值 IDataSourcesPtr spDataSources = spFormat->DataSources; // 设置产品名称 _bstr_t bstrNameDS = L“ProductName”; IDataSourcePtr spNameDS = spDataSources->Item[bstrNameDS]; if (spNameDS != nullptr && spNameDS->SubStrings->Count > 0) { spNameDS->SubStrings->Item[1]->Value = _bstr_t(L“高端智能手机X1”); } // 设置条码值 (数据源名称为嵌入的初始值) _bstr_t bstrSKUDS = L“SKU123456”; IDataSourcePtr spSKUDS = spDataSources->Item[bstrSKUDS]; if (spSKUDS != nullptr && spSKUDS->SubStrings->Count > 0) { spSKUDS->SubStrings->Item[1]->Value = _bstr_t(L“X1-2024-08-001”); } std::cout << “数据填充完毕,开始打印...” << std::endl; // 4. 打印一份并关闭文档 spFormat->PrintOut(VARIANT_TRUE, VARIANT_TRUE); std::cout << “打印任务已提交。” << std::endl; // 智能指针会自动释放,这里显式释放一下 spFormat.Release(); spApp.Release(); } catch (const _com_error& e) { std::cerr << “COM错误: ” << e.ErrorMessage() << std::endl; if (e.Description().length() > 0) { std::cerr << “描述: ” << e.Description() << std::endl; } CoUninitialize(); return -1; } CoUninitialize(); std::cout << “程序执行成功。” << std::endl; return 0; }编译并运行此程序,如果一切配置正确,你将听到打印机开始工作的声音,一张带有“高端智能手机X1”文本和“X1-2024-08-001”条码的标签就会被打印出来。
4. 进阶技巧与生产环境实战
4.1 处理多个标签与批量打印
在实际项目中,我们往往不是打印一张,而是批量打印。例如,根据一个出库单列表,循环打印每个商品的标签。你可能会想在一个循环里反复调用上面的代码。但这会频繁创建和销毁Bartender的Application和Format对象,效率较低。
更高效的做法是:打开一个模板,在循环内只更新数据源,然后重复打印。
// 伪代码逻辑 打开模板(spFormat); for (const auto& item : productList) { 设置数据源(spFormat, “ProductName”, item.name); 设置数据源(spFormat, “SKU”, item.sku); // 设置其他数据源... spFormat->PrintOut(item.quantity, VARIANT_FALSE); // 打印指定份数,不关闭文档 } // 所有打印任务完成后 spFormat->Close(VARIANT_FALSE); // 关闭文档,不保存对数据的更改(因为数据是动态的)注意:PrintOut的第一个参数可以指定打印份数。如果你的一个产品需要多份相同的标签,直接设置这个参数比循环调用更高效。
4.2 错误处理与日志记录
生产环境的代码必须有健壮的错误处理。
- COM错误捕获:我们已经使用了
try-catch来捕获_com_error异常。这是最基本的。 - 检查返回值:对于COM方法调用,除了异常,也要检查返回的
HRESULT。 - 添加详细日志:在关键步骤(创建对象、打开文件、设置数据、打印)前后输出日志,便于排查问题。可以将日志写入文件或系统事件查看器。
- 打印机状态检查:在打印前,可以通过
spFormat->Printer->IsReady属性(如果可用)粗略判断打印机状态,但更可靠的方法是通过Windows API或监听打印作业状态。
try { // ... 操作 ... } catch (const _com_error& e) { logError(“Bartender操作失败”, e.ErrorMessage(), e.Description()); // 尝试优雅关闭 if (spFormat) spFormat->Close(VARIANT_FALSE); if (spApp) spApp->Quit(BtSaveOptions::btDoNotSaveChanges); // 退出Bartender应用 throw; // 或进行其他错误恢复操作 }4.3 部署注意事项与依赖项
将你的C++程序部署到生产机器时,必须确保:
- Bartender运行时环境:目标机器必须安装Bartender,并且版本与开发时使用的兼容。通常需要安装Bartender Runtime或完整版。
- VC++运行库:确保安装了对应版本的Visual C++ Redistributable。这可以通过安装包将其打包进去。
- 管理员权限:某些操作(如访问特定打印机端口、注册COM组件)可能需要管理员权限。评估你的程序是否需要以管理员身份运行。
- 模板文件路径:程序中的模板文件路径最好是可配置的(如通过配置文件、注册表或命令行参数传入),而不是硬编码。确保目标机器上该路径可访问,并且Bartender有权限读取。
- 防病毒软件:某些防病毒软件可能会拦截COM调用或Bartender的自动化操作,需要将其加入白名单。
4.4 性能优化与小技巧
- 保持Application对象常驻:对于需要频繁打印的服务型程序,可以考虑在程序启动时创建一次
IApplicationPtr spApp,并使其在整个程序生命周期内保持活动状态,而不是每次打印都创建和销毁。但要注意管理好其可见性和资源。 - 使用命名数据源:在模板设计中,可以使用“命名数据源”。这是一个独立的数据源,可以被多个标签对象引用。在代码中,你只需要更新这个命名数据源的值,所有引用它的对象都会自动更新。这对于打印复杂标签非常方便。
- 预览与打印分离:
PrintOut是直接打印。如果你需要先预览,可以使用spFormat->PrintPreview()方法。这在调试标签格式时非常有用。 - 处理中文与特殊字符:确保你的C++程序使用Unicode字符集(在VS项目属性中设置),并使用
_bstr_t或CComBSTR来传递字符串,可以很好地支持中文。
5. 常见问题排查与解决方案实录
在实际集成过程中,我踩过不少坑。这里把一些典型问题和解决方法记录下来,希望能帮你节省时间。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
编译错误:找不到IApplicationPtr等类型 | 1.#import路径错误,未找到BarTend.exe。2. Bartender未安装或安装不完整,COM组件未注册。 | 1. 检查#import语句中的文件路径是否正确。2. 重新运行Bartender安装程序,确保选择了“Automation”或“SDK”组件。可以尝试以管理员身份运行 regsvr32手动注册相关dll(通常不必要,先重装)。 |
运行时错误:CoCreateInstance失败 (HRESULT: 0x80040154) | 类未注册。Bartender的COM组件没有在系统中正确注册。 | 1. 确认Bartender已安装。 2. 在命令行(管理员)运行: regsvr32 “C:\Program Files\Seagull\BarTender Suite\BarTend.exe”。注意路径需替换为你实际的安装路径。 |
| 程序运行后无任何反应,打印机不工作 | 1. Bartender界面被隐藏(Visible = VARIANT_FALSE),但后台可能有错误对话框被阻塞。2. 数据源名称不匹配。 3. 默认打印机设置有问题或打印机离线。 | 1. 暂时将Visible设为VARIANT_TRUE,观察是否有错误弹窗。2. 仔细检查代码中的数据源名称与模板中的名称是否完全一致(大小写、空格)。 3. 在Bartender Designer中打开模板,检查其默认打印机是否可用。在代码中,可以尝试在 Open方法中指定打印机名称。 |
| 打印出的标签数据没有变化,还是模板里的初始值 | 数据源设置代码未生效。可能: 1. 没有找到正确的数据源对象。 2. 设置了值,但在打印前数据源被刷新或覆盖。 | 1. 添加日志,确认spDataSource和spSubString对象是否成功获取且不为nullptr。2. 确保在调用 PrintOut之前完成所有数据源的设置。3. 检查模板中是否有“触发”设置(如数据库连接后刷新),这可能会在打印时用旧数据覆盖你的设置。对于简单变量替换,确保数据源类型是“嵌入的数据”。 |
| 在多线程环境中调用崩溃 | COM对象不是线程安全的。在多个线程中同时操作同一个IApplicationPtr或IFormatPtr对象会导致未定义行为。 | 1.最重要的原则:每个线程使用自己独立的BartenderApplication和Format对象实例。不要跨线程共享。2. 如果必须共享,则需要使用严格的锁(如临界区)来序列化所有COM调用。但更推荐为每个打印任务创建独立实例。 |
| 部署到服务器上运行失败 | 1. 服务器未安装Bartender或VC++运行库。 2. 服务器上Bartender以服务运行时,无法弹出UI(即使 Visible=FALSE,某些操作也需要桌面会话)。3. 权限不足。 | 1. 确保服务器安装完整的环境。 2. 对于Windows服务,打印到网络打印机通常是可行的,但涉及UI交互的复杂操作可能失败。考虑使用Bartender的“Print Station”或无头打印方案。 3. 确保运行程序的账户有权限访问模板文件、打印机以及Bartender的安装目录。 |
一个典型的调试技巧:当你怀疑是数据源名称问题时,可以在代码中添加一段遍历所有数据源的逻辑,将它们的名字打印出来,与你在设计器里看到的进行比对。
IDataSourcesPtr spDataSources = spFormat->DataSources; long count = spDataSources->Count; for (long i = 1; i <= count; ++i) { // COM集合索引通常从1开始 IDataSourcePtr spDS = spDataSources->Item[i]; _bstr_t name = spDS->Name; std::wcout << L“数据源 ” << i << L“: ” << (LPWSTR)name << std::endl; }打通C++与Bartender的自动化打印,本质上是一场精准的“握手协议”。一旦你掌握了COM接口调用的基本模式和数据源操作的诀窍,剩下的就是将你的业务逻辑与这个稳定的打印管道对接。这套方案在我经历的几个生产项目中都运行得非常稳定,真正将标签打印从手动操作中解放出来,融入了全自动的业务流。