写点什么

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

  • 2020-03-11
  • 本文字数:5315 字

    阅读完需:约 17 分钟

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

一、背景

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


打印需求端



小票业务场景



小票打印机设备类型



过去我们存在的痛点:


  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 内容块:



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



1.1 模版编译


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


自定义能力:



1.2 打印机设备适配


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


调用对应打印机的 parser 指令解析流程



1.3 兼容性问题


切纸:支持外部传入是否需要切纸,防止外部发送打印指令时加入切纸指令后重复切纸问题,默认加切纸指令。


一机多尺寸打印:存在一台打印机支持两种纸张打印( 80mm 、 58mm ),这时需要从外部传入打印尺寸,默认 80mm。比如,sunmiT1 支持 80mm 和 58mm 打印,默认是 80mm。


1.4 容错处理


由于模版解析有一定格式要求,所以一些特殊字符及转移字符存在数据中会存在解析错误。所以 JS 在传入数据时,做了一层过滤,将 “\” 、 “\n” 、 “\b” … 等字符去掉或替换,保证打印。


如果在解析过程中存在错误,将抛出异常给移动端捕获。

2、模板管理服务

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


整体处理流程图



2.1 小票基础模版库存储示例



shopId:店铺 ID


business:业务方


type:打印内容类型


content:layout 中 content 内容


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


2.2 动态设置数据存储示例



shopId:店铺 ID


business:业务方


type:打印内容类型


params:需要替换填充的内容


2.1 接口返回整合后的小票模版 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 引擎进行打印指令转换返回给打印机打印。


3.1 动态模版配置



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


3.2 打印业务流程



该业务流程,移动端完全脱离数据,只需要做一些额外能力以及传输功能,有效解决了业务数据修改依赖移动端发版的问题。 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 进制字符串。


4.1 下载图片


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


4.2 图片压缩


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


4.3 二值图处理


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


4.4 图片像素点压缩


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



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


2020-03-11 22:191004

评论

发布
暂无评论
发现更多内容

高教授:区块链软硬件相结合能提升系统性能 也能保持一定的灵活性

CECBC

分布式 行业资讯 区块链技术 落地应用

架构师训练营第九章作业

吴吴

第九周.命题作业-GC原理

刘璐

奈学:一起了解几种序列化协议

古月木易

序列化 序列化协议

架构师0期Week9作业1

Nan Jiang

架构师训练营第8周作业

Hanson

凭实力蝉联第一!Flink 又双叒叕上榜啦

Apache Flink

flink

阿里最“短命”的P10?入职不到3周,钉钉副总裁张志琦因个人原因离职

程序员生活志

阿里巴巴 互联网热点

地方政府争夺数字货币试点,互联网巨头参与测试,央行数字货币即将来临

CECBC

行业资讯 央行数字货币

零/低代码编程风口已到,请快快上车!

代码制造者

编程语言 低代码 零代码 iVX

最右JS2Flutter框架——通信机制(三)

刘剑

flutter 大前端 探索与实践

总结

Kiroro

架构师训练营第8周总结

Hanson

为什么你总觉得自己是对的?

池建强

个人成长

设计一个秒杀系统挑战和问题及方案或思路

Young

垃圾回收&秒杀设计

dony.zhang

垃圾回收

技术贴丨教你使用华为云鲲鹏服务器部署Discuz!论坛

华为云开发者联盟

鲲鹏920 服务器 华为云 Discuz! 华为云鲲鹏

JVM类加载与执行

superman

JVM 类加载

一文带你掌握OBS的两种常见的鉴权方式

华为云开发者联盟

Rest OBS 鉴权 Header携带签名 URL携带签名

开源、云、传统IT的三国杀

郭华

云计算 开源 商业 IT

week09 总结

Z冰红茶

作业一

Kiroro

week9作业1

cxuan读者的外包面试之旅

苹果看辽宁体育

Java 面试 程序人生

秒杀系统

Z冰红茶

埋点全解析,你最关心的可视化埋点在这里!(文末附开源地址)

易观大数据

不仅性能秒杀Hadoop,现在连分布式集群功能也开源了

爱倒腾的程序员

大数据 涛思数据 tdengine 物联网 时序数据库

白鹭引擎助力《梦幻西游网页版》刷新h5游戏新高度

DT极客

数据库实践丨MySQL多表join分析

华为云开发者联盟

MySQL 数据库 华为云 join 多表join

加快速度推进区块链融合发展

CECBC

区块链 行业资讯 产业落地

【得物技术】流量之战—性能优化三十六计

得物技术

Java 流量 得物技术部 商品 电商平台

有赞零售小票打印跨平台解决方案_文化 & 方法_有赞技术_InfoQ精选文章