作者:王前、林昊(鱼干)

一、背景

零售商家的日常经营中,小票打印的场景无处不在,顾客的每笔消费都会收到商家打印出的消费小票,这个是顾客的消费凭证,所以小票的内容对顾客和商家都尤为重要。对于有赞零售应用软件来说,小票打印功能也是必不可少的,诸多业务场景都需要提供相应的小票打印能力。

  • 打印需求端
  • 小票业务场景
  • 小票打印机设备类型

过去我们存在的痛点:

  1. 每个端各自实现一套打印流程,方案不统一。导致每次修改都会三端修改,而且 iOS 和 Android 必须依赖发版才可上线,不具有动态性,而且研发效率比较低。
  2. 打印小票的业务场景比较多,每个业务都自己实现模板封装及打印逻辑,模板及逻辑不统一,维护成本大。
  3. 多种小票设备的适配,对于每个端来说都要适配一遍。

其中最主要的痛点还是在于第一点,多端的不统一问题。由于不统一,导致开发和维护的成本成倍级增长。

针对以上痛点,小票打印技术方案需要解决的三个主要问题:

  1. iOS 、安卓和网页端的零售软件都需要提供小票样式设置和打印的能力,如何降低小票打印代码的维护和更新成本。
  2. 如何定制显示不同业务场景的小票内容:不同业务场景下的小票信息都不尽相同,比如购物小票和退款小票,商品信息的样式是一样的,但是支付信息是不一样的,购物小票应当显示顾客的支付信息,退款小票显示商家退款信息。
  3. 如何更灵活的适配多种多样的小票打印机,从连接方式上分为蓝牙连接和 WIFI 连接,从纸张样式分为 80mm 和 58mm 两种宽度。

二、整体解决方案

针对以上三个问题,我们提出了一个涉及前端、移动端和服务端的跨平台解决方案,

  • 架构图

架构设计的核心在于通过 JS 实现支持跨平台的小票解析脚本,并具有动态更新的优势;通过服务端下发可编辑的样式模板实现小票内容的灵活定制;客户端启动 JS 执行器执行 JS 小票脚本引擎(以下简称:JS 引擎)并负责打印机设备的连接管理。

1 、JS 引擎设计

JS 引擎主要能力就是处理小票模版和业务数据,将业务数据整合到模版中(处理不了的交给移动端处理,比如图片),然后将整合模版数据转换成打印指令返给移动端。

  • 整体处理流程图
  • 结构设计
* 小票格式中,打印机是一行一行的输出。那么基本输出布局单位,我们定义为 layout
* 默认一行有一个内容块,即一个 layout 里面有一个 content object
* 当一行有多列内容的时候,即一个 layout 里面包含 N 个 content object 。 各自内容块有 pagerWeight 代表每个内容的宽度占比
* 每一行的后面的是一个占位符,用数据模型的 key 做占位
复制代码

小票 layout 样式描述:

content block 内容块:

不同类型内容所支持的能力:

  • 模版编译

这里使用了 HandleBars.js 作为模板编译的库。此外,目前还额外提供了部分能力支持。

自定义能力:

  • 打印机设备适配

主要进行适配指令集解析适配,根据连接不同设备进行不同指令解析。目前已适配设备:365wifi 、 sunmi 、 sprt80 、 sprt58 、 wangpos 、 aclas 、 xprinter 。如果连接未适配的设备抛出找不到相应打印机解析器 error。

  • 调用对应打印机的 parser 指令解析流程
  • 兼容性问题

    • 切纸:支持外部传入是否需要切纸,防止外部发送打印指令时加入切纸指令后重复切纸问题,默认加切纸指令。
    • 一机多尺寸打印:存在一台打印机支持两种纸张打印( 80mm 、 58mm ),这时需要从外部传入打印尺寸,默认 80mm 。比如,sunmiT1 支持 80mm 和 58mm 打印,默认是 80mm 。
  • 容错处理

    • 由于模版解析有一定格式要求,所以一些特殊字符及转移字符存在数据中会存在解析错误。所以 JS 在传入数据时,做了一层过滤,将 "\\" 、 "\n" 、 "\b" ... 等字符去掉或替换,保证打印。
    • 如果在解析过程中存在错误,将抛出异常给移动端捕获。

2 、模板管理服务

小票模板的动态编辑和下发,模版动态配置信息存储和各业务全量模版存储,提供移动端动态配置信息接口,拉取业务小票模版接口,各业务方业务数据接口。

  • 整体处理流程图
  • 小票基础模版库存储示例

shopId:店铺 ID

business:业务方

type:打印内容类型

content:layout 中 content 内容

sortWeight:排序比重,用于输出模板 layout 顺序

  • 动态设置数据存储示例

shopId:店铺 ID

business:业务方

type:打印内容类型

params:需要替换填充的内容

  • 接口返回整合后的小票模版 json
{"business": "shopping","shopId": 111111,"id": 321,"version": 0,"layouts": [{"name": "LOGO","content": "[{\"content\":\"http://www.test.com/test.jpg\",\"contentType\":\"image\",\"textAlign\":\"center\",\"width\":45}]"},{"name": "电话","content": "[{\"content\":\"电话:{{mobile}}\",\"contentType\":\"text\",\"textAlign\":\"left\",\"fontSize\":\"default\",\"pagerWeight\":1}]"},...]
}
复制代码

其中相关动态数据后端已经做过整合替换,需要替换的业务数据保留在模板 json 中,等获取业务数据后由 JS 引擎进行替换。 上面 json 中 http://www.test.com/test.jpg 就是动态整合替换数据,{{mobile}} 是一个需要替换的业务数据。

3 、移动端

移动端除了动态模版配置之外,主要的就是打印流程。移动端只需要关心需要打印什么业务小票,然后去后端拉取业务小票模版和业务数据,将拉取到的数据传给 JS 引擎进行预处理,返回模版中处理不了的图片 url 信息,然后移动端进行下载图片,进行二值转换,输出像素的 16 进制字符串,替换原来模版中的 url ,最后将连接的打印机类型和处理后的模版传给 JS 引擎进行打印指令转换返回给打印机打印。

  • 动态模版配置

动态配置小票内容,支持 LOGO 、店铺数据、营销活动配置等。左侧为在 80mm 和 58mm 上预览样式。通过动态配置模版,实现后端接口模版更新,然后可以实时同步修改打印内容。网页零售软件上动态配置内容和移动端一样。

  • 打印业务流程

该业务流程,移动端完全脱离数据,只需要做一些额外能力以及传输功能,有效解决了业务数据修改依赖移动端发版的问题。 Android 和 iOS 流程统一。

三、移动端功能设计

1 、动态化

动态化在本解决方案里是必不可少的一环,实时更新业务数据模板依赖于后端,但是 JS 解析引擎的下发要依靠移动端来实现,为了及时修复发现的 JS 问题或者快速适配新设备等功能。更新流程图如下:

这里说明一下,因为可能会出现执行 JS 的过程中,正在执行本地 JS 文件更新,导致执行 JS 出错。所以在完成本地更新后会发送一个通知,告知业务方 JS 已更新完成,这时业务方可根据自身需求做逻辑处理,比如重新加载 JS 进行处理业务。

2 、JS 执行器

iOS 使用 JavaScriptCore 框架,Android 使用 J2V8 框架,具体框架的介绍这里就不说明了。JS 执行器设计包含加载指定 JS 文件,调用 JS 方法,获取 JS 属性,JS 异常捕获。

  /**初始化 JSExecutor@param fileName JS 文件名@return JSExecutor*/- (instancetype)initWithScriptFile:(NSString *)fileName;/**加载 JS 文件@param fileName JS 文件名*/- (void)loadSriptFile:(NSString *)fileName;/**执行 JS 方法@param functionName 方法名@param args 入参@return 方法返回值*/- (JSValue *)runJSFunction:(NSString *)functionName args:(NSArray *)args;/**获取 JS 属性@param propertyName 属性名@return 属性值*/- (JSValue *)getJSProperty:(NSString *)propertyName;/**JS 异常捕获@param handler 异常捕获回调*/- (void)catchExceptionWithHandler:(JSExceptionHandler)handler;
复制代码

加载 JS 文件方法,可以加载动态下发的 JS 。逻辑是先判断本地下发的文件是否存在,如果存在就加载下发 JS ,否则加载 app 中 bundle 里面的 JS 文件。

 - (void)loadSriptFile:(NSString *)fileName{NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);if (paths.count > 0) {NSString *docDir = [paths objectAtIndex:0];NSString *docSourcePath = [docDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.js", fileName]];NSFileManager *fm = [NSFileManager defaultManager];if ([fm fileExistsAtPath:docSourcePath]) {NSString *jsString = [NSString stringWithContentsOfFile:docSourcePath encoding:NSUTF8StringEncoding error:nil];[self.content evaluateScript:jsString];return;}}NSString *sourcePath = [[YZCommonBundle bundle] pathForResource:fileName ofType:@"js"];NSAssert(sourcePath, @"can't find jscript file");NSString *jsString = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];[self.content evaluateScript:jsString];}
复制代码

这时候可能会有人疑问,为什么这里是直接强制加载本地下发 JS ,而不是对比版本优先加载。这里主要有两点原因:

  • 动态下发 JS 文件,就是为了补丁或者优化更新,所以一般新版本下发配置不会存在
  • 为了支持 JS 版本回滚

JS 异常捕获功能,将异常抛出给业务方,可以让调用者各自实现逻辑处理。

3 、缓存优化

由于模板和数据都在后端,需要拉取两次接口进行打印,所以需要提供一套缓存机制来提高打印体验。由于业务数据需要实时拉取,所以必须走接口,模板相对于业务数据来说,可以允许一定的延迟。所以,模板采用本地文件缓存,业务数据采用和业务打印页面挂钩的内存缓存,业务数据只需要第一次打印是请求接口,重新打印直接使用。

流程图:

本缓方案存会存在偶现的模板不同步问题,在即将打印时,如果网页后台修改了模板,就会出现本次打印模板不是最新的,但是在下一次打印时就会是最新的了。由于出现的几率比较低,模板也允许有一点延迟,所以不会影响整体流程。

对于离线场景,我们在 app 中存放一个最小可用模板,专门用于离线下小票打印使用。为什么是最小可用模板,因为离线下,业务数据及一些其他数据有可能不全,所以最小可用模板可以保证打印出来的数据准确性。

4 、图片处理

由于 JS 引擎是不能解析图片文件的,所以在最初模板中存在图片链接时,全部由移动端进行处理,然后进行替换。图片处理主要就是下载图片,图片压缩,二值图处理,图片像素点压缩(打印指令要求),每个字节转换成 16 进制,拼接 16 进制字符串。

  • 下载图片

采用 SDWebImage 进行下载缓存,创建并行队列进行多图片下载,每下载成功一张后回到主线程进行后续的相关处理。所有图片都处理完成或,回调给 JS 引擎进行指令解析。

  • 图片压缩

根据 JS 引擎模板要求的 width(必须是 8 的倍数,后续说明),进行等比例压缩,转换成 jpg 格式,过滤掉 alpha 通道。

  • 二值图处理

遍历每一个像素点,进行 RGB 取值,然后算出 RGB 均值与 255 的比值,根据比值进行取值 0 或 255 。这里没有使用直方图寻找阈值 T 的方式进行处理,是出于性能和时间考虑。

  • 像素点压缩

由于打印机指令要求,需要对转换成二值后的每个点进行 width 上压缩,需要将 8 个字节压缩到 1 个字节,这里也是为什么图片压缩时 width 必须是 8 的倍数的原因,否则打印出来的图片会错位。

  • 16 进制字符串

因为打印机打印图片接收的是 16 进制字符串,所以需要将处理后的每个字节转换成 16 进制字符,然后拼成一个字符串。

5 、实现多次打印

由于业务场景需要,需要自动打印多张小票,所以设计了多次打印逻辑。由于每次打印都是异步线程中,所以不可以直接循环打印,这里使用信号量 dispatch_semaphore_t ,在异步线程中创建和 wait 信号量,每次打印完成回调线程中 signal 信号量,实现多次打印,保证每次打印依次进行。如果中途打印出错,则终止后续打印。

    dispatch_async(dispatch_get_global_queue(0, 0), ^{dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);for (int i = 1; i <= printCount; i++) {if (stop) {break;}[self print:template andCompletionBlock:^(State state, NSString *errorStr) {dispatch_async(dispatch_get_main_queue(), ^{if (errorStr.length > 0 || i == printCount) {if (completion) {completion(state, errorStr);}stop = YES;}dispatch_semaphore_signal(semaphore);});}];dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 15*NSEC_PER_SEC));}});
复制代码

四、总结与展望

本方案已经实施,在零售 app 中使用来看,已经满足目前大部分业务场景及需求,后续的开发及维护成本也会大幅度降低,提高了研发效率,接入新业务小票也比较方便。客户使用上来说,使用体验和以前没有较大差别,同时在处理客户反映的问题来说,也可以做到快速修改,实时下发等。不过目前还存在一些不足点,比如说图片打印的功能,还不能完全满足所有图片都做到完美打印,毕竟图片处理考虑到性能体验方面;还有模板后续可以增加版本号,这样在模板存在异常时也可以回滚或兼容处理等;再者就是缓存优化可以后续进一步优化体验,比如加入模板推送,本地缓存优化等。

参考链接

  • HandleBars, by wycats
  • date-fns, by date-fns
  • lodash, by Lodash
  • JavaScriptCore, by Apple
  • J2V8, by eclipsesource

有赞零售小票打印跨平台解决方案 1相关推荐

  1. 有赞零售小票打印跨平台解决方案

    作者:王前.林昊(鱼干) 一.背景 零售商家的日常经营中,小票打印的场景无处不在,顾客的每笔消费都会收到商家打印出的消费小票,这个是顾客的消费凭证,所以小票的内容对顾客和商家都尤为重要.对于有赞零售应 ...

  2. 单据小票打印模板自定义设计,手机收银软件APP搭配蓝牙便携打印机,移动便携打印零售单单据小票

    单据小票打印模板自定义设计,手机收银软件APP搭配蓝牙便携打印机,移动便携打印零售单单据小票,轻松实现仓库条码管理,扫码入库出库盘点_哔哩哔哩_bilibili单据小票打印模板自定义设计,手机收银软件 ...

  3. 零售连锁专卖信息化解决方案简介之二

    连锁零售它提供了对商业连锁的整体管理,从商品采购开始到面向最终消费者各阶段都可以找到连锁零售的解决方案.连锁零售针对批发.连锁.零售业供应链中不同的业态提出了不同的解决方案.在信息管理系统的层次中隶属 ...

  4. 有赞零售财务中台架构设计与实践

    一.背景 传统模式下,企业的经营活动会产生大量的业务数据.财务人员需要根据业务数据,进行会计核算,并输出财务数据.通过这些财务数据,企业可以进行财务管理.财务分析.业务决策.但会计核算的工作量非常庞大 ...

  5. 百分点零售行业大数据解决方案

    类型: 定制服务 软件包: bigdata business intelligence retailing solution collateral 联系服务商 产品详情 零售行业的挑战与机遇 随着互联 ...

  6. 基于Qt的收银点餐系统之小票打印(一)

    待解决问题: 顾客在点餐完毕后给打印一份小票.如图所示: 解决方案:最开始拿到了一个基于JAVA实现的小票打印demo,使用的是ECS/POS指令集.但是并没有成功地用Qt也实现出来. 本文基于QPa ...

  7. Delphi 10 Seattle小票打印控件TQ_Printer

    TQ_Printrer控件,是一个为方便需要控制打印命令而设计的跨平台专用控件,已包含标准ESC/POS打印控制的基本指令在内(这些基本指令已能很好的满足多数项目使用). TQ_Printrer控件让 ...

  8. 商业分析 —— 有赞零售

    欢迎讨论或指出不足 : ) 前言 本文通过分析一个成功服务商,有赞集团旗下的产品之一 -- 有赞零售,来学习如何构建平台服务,并增强自身的商业分析能力.若有侵权或其它不当,会立即删除.欢迎讨论与指导- ...

  9. 有赞零售移动端收银商品实践

    文 | Alex.Siam 面对线下收银场景,针对商品收银业务,如何提升商家收银的效率?如何保证即使在弱网或无网条件下商家正常的收银?如何设计大量商品时搜索方案?如何对业务模块进行解耦和各种复杂的业务 ...

最新文章

  1. SqlParameter参数方式操作数据库(存储过程)
  2. 一种解决hadoop搭建出现的各种问题的简单粗暴的办法
  3. Binary Tree Level Order Traversal
  4. python教程:getattr函数和hasattr函数的用法
  5. SAP的程序用客户端连接正常,用C#连接死活连不上问题的解决
  6. 【Linux】一步一步学Linux——visudo命令(104)
  7. (*长期更新)软考网络工程师学习笔记——Section 12 Linux系统与文件管理命令
  8. caffeine 读操作源码走读 为什么读这么快
  9. Android Studio(14)--点9图片怎么玩
  10. 关于jquery跨域请求方法
  11. 内大计算机学院研究生奖学金,通知 | 【研究生评奖评优】关于做好浙江大学2017-2018学年计算机学院研究生学年小结及评奖评优工作的通知...
  12. 解决安卓中XML文件声明高度 宽度无效的问题
  13. redis面试题简义
  14. python典型安装_python安装某些第三方包报错解决办法
  15. sap服务器安全证书,SAP安全登录单
  16. MYSQL LEFT JOIN 的怪异行为
  17. 用户画像之ID-Mapping
  18. java-net-php-python-23jspm在线学习设计计算机毕业设计程序
  19. Git版本管理工具使用知识汇总
  20. Meta:多人联机VR游戏这样拉新

热门文章

  1. SpringCloud 和 SpringCloudAlibaba 合集
  2. MOSS 2010:Visual Studio 2010开发体验(22)——利用BCS和WCF进行应用程序集成
  3. 深度学习的Attention模型
  4. Java Web基本编程
  5. 使用jquery实现中英文切换
  6. python实现最长公共子序列(LCS)
  7. 【爬虫】QQ空间照片下载
  8. 黑客专访:天才黑客Gabriel Bergel的黑客人生
  9. backbone php,backbone-phpRestful并行
  10. 数学建模之线性规划学习笔记