TCP 返回成功,为什么小票仍未打印?

代码解释图:传输、协议与物理动作需要分别确认。
摘要| 接口显示成功,小票却没有出来。问题往往藏在“成功”这个词里:应用把字节写入了连接,不等于打印机理解了指令,更不等于纸张已经经过了出纸口。本文沿着 Microi吾码AI 的 V8.Tcp 调用链,拆开这三个状态。
✦ 01|先给“成功”划出边界
设想一个收银场景:订单已经结算,服务端调用网络小票机,返回 Code=1。页面马上弹出“打印完成”,店员却没有拿到纸。此时继续点打印,既可能仍然无纸,也可能在设备恢复后连续吐出两张。
关键判断| Code=1 是某一层操作成功的标记,不是整个业务已经完成的证明。 先确认这个返回值对应哪一层,再决定是否允许重试。
- 传输层:建立连接,并完成字节写入。
- 协议层:设备返回了与本次指令匹配、内容完整的确认。
- 物理层:实际执行了出纸、切纸或其它动作。
有些设备协议只报告“已接收”或“已入队”,没有“已完成”回执。此时应用能确认的最远边界就是已接收,不能把它改名为打印完成。
✦ 02|沿着一次调用走到底
在 Microi吾码AI 中,设备动作可由接口引擎编排:先按当前用户和订单状态判断是否允许操作,再从可信配置取得设备地址,最后调用 V8.Tcp。业务判断留在接口引擎,网络连接与字节读写由底层能力负责。

源码解释图:底层完成连接、写入与可选读取,不自动解释设备协议。
// device 来自已授权设备配置,不能直接取请求地址
var sent = V8.Tcp.Send({
Host: device.Host, Port: device.Port,
Bytes: [27, 64, 65, 10],
ConnectTimeout: 5, SendTimeout: 5
});
return sent;
当前实现先验证参数,再创建短连接;写入和刷新结束后,Send 返回 Code=1,并给出 BytesSent 与 RemoteEndpoint。这里没有读取打印机状态,也没有检测纸张。这一事实解释了“服务端成功、现场没反应”为何能同时成立。
示例边界| 这段字节只是初始化、字母 A 与换行的示例。具体命令、字符集、状态查询和切纸指令,必须按实际设备的协议手册确认。
✦ 03|收到一个字节,也可能没收到完整答案
需要设备反馈时可以改用 SendAndReceive。它返回原始字节、Base64、十六进制以及接收结束原因,但这些仍然只是材料。应用需要按自己的协议判断报文长度、终止符、校验、状态位和指令对应关系。
var reply = V8.Tcp.SendAndReceive({
Host: device.Host, Port: device.Port,
Bytes: statusQueryBytes,
ReceiveTimeout: 3, MaxReceiveBytes: 4096
});
if (reply.Code !== 1) return reply;
// 再按设备协议校验完整报文与业务状态
return { Code: 1, Data: reply.Data };
当前源码对“完全没收到”与“收到部分后停止等待”作了区分:没有任何响应的接收超时会报错;已经收到数据后超时,可能仍返回 Code=1,同时将 ReceiveEndReason 标为 Timeout。达到容量上限则会标记 MaxReceiveBytes,并设置 Truncated=true。

状态解释图:Timeout、RemoteClosed 和 MaxReceiveBytes 都要结合设备报文规则解释。
容易遗漏的分支| Code=1 + Timeout 可能只说明读到了部分数据。 RemoteClosed 也只是连接关闭;只有协议校验通过,才能把响应用于下一步业务决策。
✦ 04|返回前失败,与写入后未知,是两类事情
参数在连接前被拒绝,例如同时传入 Bytes 和 Hex,属于尚未发送的确定性失败。修正参数后重新执行,通常不涉及重复设备动作。相反,写入之后连接中断,服务端可能不知道设备执行到了哪一步。
- 明确未发送:保留错误,修复配置或参数后再执行。
- 已收到完整、可信的拒绝响应:按协议规定处理拒绝原因。
- 已写入但没有可靠确认:保存为结果待确认,先查询设备状态或由现场核实。
数据库事务也不能把外部设备已完成的动作撤回。即使接口引擎随后因为业务错误回滚,已经发出的网络字节不会倒流。因此,不要把“事务失败就重试整个接口”直接套在打印、闸机、标签机这类动作上。
生产取舍| 有设备任务编号与查询能力时,先关联编号再查状态;没有这些能力时,保留人工核实入口比自动连发更可控。这里是业务设计建议,并非 V8.Tcp 自带的完整打印管理系统。
✦ 05|用状态记录替代一个完成布尔值
更清楚的设计是分别保存提交、发送、确认与完成状态。订单编号不是天然的打印幂等键:同一订单可能合法补打,但每次有明确意图的打印都应该有自己的动作编号。客户端重复点击则继续查看同一动作。
// 状态命名示例:需结合设备协议与业务自行实现
var printAction = {
ActionId: actionId,
OrderId: order.Id,
Status: 'AwaitingDeviceConfirmation',
BytesSent: sent.Data.BytesSent
};
// 只有可靠完成回执或现场确认后,才进入 Completed
这种拆分允许界面显示“指令已发送,等待设备确认”,也便于把“重新查询”与“再次打印”做成两个不同动作。若使用后台任务承接发送,还应明确租约、超时与重启恢复规则;异步方法本身并不等于可靠任务队列。
✦ 06|这次验证实际证明了什么

2026-09-05 本地执行结果:8 项通过,0 项失败;测试使用本机回环监听器。
本次执行了现有测试程序集中的 V8TcpTests,8 项全部通过。测试覆盖真实 JavaScript 字节数组、Hex/Base64 与 GB18030 编码、非法或过大负载,以及部分响应超时和接收容量上限。测试前后程序集内容一致。
- 一个测试让服务端只回一个字节,再保持连接,验证部分响应超时仍能返回数据并标记 Timeout。
- 另一个测试返回超过接收容量的数据,验证 Truncated 与 MaxReceiveBytes 的边界。
- 这些结果证明本地已覆盖路径的传输行为,不能证明任意型号打印机已经兼容。
证据范围| 未连接真实小票机,未进行现场出纸验收,也未验证网络中断时具体设备的恢复策略。本文的源码分析、回环测试和设备现场结论分别陈述。
✦ 07|上线前只追问最关键的四件事
设备接入的最后一公里通常不是再写一层 HTTP,而是把地址、协议、网络和业务状态对齐。端口能连通只是开始;即使设备使用常见打印指令,也要核对固件、编码和状态回包格式。
- 地址是否来自可信设备配置,并受当前用户、租户和业务权限约束?
- 部署环境是否真正能访问设备?容器里的 localhost 指向容器自己。
- 怎样判断一条响应完整,并且对应当前动作?
- 结果不确定时,是查询状态、现场确认,还是明确授权一次补打?
把这四件事写进设计,页面上的每一种“成功”才有可解释的含义。对用户显示最远已证实的状态,并为未知结果保留出口,才能让一次网络调用稳妥地落到真实设备上。
✦ 08|可继续阅读与素材说明
本文依据吾码官方中文文档的 V8.Tcp 章节、当前 TCP 能力实现及相关测试撰写。可在 Microi吾码官网的后端 V8 文档中查阅 Send、SendAndReceive、参数格式和限制,再结合实际设备厂商协议完成接入。
本文由AI辅助创作并核对源码;已标注的概念图由AI生成,测试结论仅适用于文中说明的本地范围。
阅读提醒| 概念卡片用于帮助理解分层关系;它们不是设备照片或真实运行截图。调用链和状态图由代码生成,测试数字来自本次实际执行。
更多推荐




所有评论(0)