速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

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

  • 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:191015

评论

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

2019年阿里Android面试必问:Java+性能优化,android编程实战

android 程序员 移动开发

2019最新百度、头条、小米,retrofit源码

android 程序员 移动开发

2020 年,我这样在项目中使用 MVVM,BATJ等企业Android面试知识分享

android 程序员 移动开发

Node.js 中 fs.renameSync 报错

liuzhen007

11月日更

2020字节跳动安卓程序员视频面试,这五点一定有助你顺利拿到offer

android 程序员 移动开发

2020了,Android开发是否真的还有出路!25岁的我还有机会吗

android 程序员 移动开发

2020年12月大厂BATJ面试ing-本以为学了个好找工作的Android开发,没想到又是坑

android 程序员 移动开发

2020年度总结:如果系统的Android学习可以这么简单!为什么不来看看呢

android 程序员 移动开发

2020年阿里巴巴Android面经:拿到字节跳动offer后,简历又被阿里捞了起来

android 程序员 移动开发

2020Android进阶者的新篇章,一起努力应对互联网寒冬,冲刺年薪40w

android 程序员 移动开发

2020Android面试心得,已拿到offer,轻松获得一线大厂面试offer

android 程序员 移动开发

2020应届毕业生,Android春招总结,已入职小米,阿里牛逼

android 程序员 移动开发

2020新一波跳槽季过后,Android程序员精选,大厂,flutter微信小程序

android 程序员 移动开发

2020-2021最新大厂面试题附答案解析【建议收藏】,android应用开发题库

android 程序员 移动开发

2020了,Android开发是否真的还有出路!25岁的我还有机会吗(1)

android 程序员 移动开发

2019,2021我是如何拿到小米、京东、字节的offer

android 程序员 移动开发

架构设计七 如何设计异地多活架构

nydia

2020年失业后我整理了一份系统的Android面试题(含答案)

android 程序员 移动开发

【投稿赢大奖】 -- 奇思妙想+AI技术=?

百度大脑

人工智能 百度

2020京东Android岗面试题大全(附赠京东内部真题解析PDF)

android 程序员 移动开发

2020字节跳动安卓程序员视频面试,这五点一定有助你顺利拿到offer(1)

android 移动开发

2020年腾讯丶百度丶字节丶OPPO等Android面试大全,附带教你如何写好简历

android 程序员 移动开发

2020上半年百度Android岗(初级到高级)面试真题全收录

android 程序员 移动开发

2020京东最新Android面试真题解析,kotlinarrow库

android 程序员 移动开发

2020年Android开发年终总结之如何挤进一线大厂?,BAT这种大厂履历意味着什么

android 程序员 移动开发

2020抖音短视频爆火!它的背后到底是什么—,手把手教你写Android项目文档

android 程序员 移动开发

2020个人开发者做一款Android-App需要知道的事情,年薪百万在此一举(1)

android 程序员 移动开发

2020关于面试字节跳动,我总结一些面试点,希望对最近需要面试的你们一些帮助

android 程序员 移动开发

2020应届毕业生,Android春招总结,已入职小米(1),kotlin安卓开发教程

android 程序员 移动开发

2020最全的BAT大厂面试题整理改版 (2),小程序开发

android 程序员 移动开发

2020最后一天! 我为大家准备一份Android 面试知识点大全迎接2021新的一年

android 程序员 移动开发

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