写点什么

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

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

评论

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

3.面向复杂度的架构设计模式

程序员小张

「架构实战营」

(一)OpenStack---M版---双节点搭建---基础环境配置

指剑

centos OpenStack 11月月更

(八)OpenStack---M版---双节点搭建---Cinder安装和配置

指剑

centos OpenStack 11月月更

企业网络“卫生”实用指南

SEAL安全

企业安全

教你用JavaScript实现计数器

小院里的霍大侠

JavaScript 编程开发 初学者 入门实战

(四)OpenStack---M版---双节点搭建---Glance安装和配置

指剑

centos OpenStack 11月月更

AI技术实践|用腾讯云慧眼微信浮层H5解决黄牛抢票问题

牵着蜗牛去散步

人工智能 腾讯云 腾讯 腾讯云AI

(五)OpenStack---M版---双节点搭建---Nova安装和配置

指剑

centos OpenStack 11月月更

(九)OpenStack---M版---双节点搭建---Swift安装和配置(单存储节点)

指剑

centos OpenStack 11月月更

Linux安装Hbase并验证

指剑

centos HBase 11月月更

看知识图谱如何解锁隐藏的营销利器

Neo4j 图无处不在

算法 neo4j 图数据库 知识图谱 图数据

“读懂人话”,阿里AI总分首次超越人类成绩

云布道师

人工智能 阿里云

利用FreeNas创建iSCSI块级存储

指剑

centos 11月月更 freenas

SAP 异常现象之同一个IDoc可以被POST两次触发2张不同的物料凭证

SAP虾客

SAP IDoc BD87

(七)OpenStack---M版---双节点搭建---Dashboard安装和配置

指剑

centos OpenStack 11月月更

中小企业如何选择远程办公网络方案?蒲公英更具优势!

科技热闻

企业号12月PK榜,等你参与!

InfoQ写作社区官方

热门活动

效能工具如何在企业规模化落地?|线上沙龙回顾

万事ONES

嵌入式系统概述及特点

timerring

嵌入式 11月月更

我们又重写了一个关键服务

Zilliz

人工智能 Milvus 向量数据库

云小课|云小课教您如何选择Redis实例类型

华为云开发者联盟

云计算 后端 华为云

(二)OpenStack---M版---双节点搭建---数据库安装和配置

指剑

centos OpenStack 11月月更

(三)OpenStack---M版---双节点搭建---Keystone安装和配置

指剑

centos OpenStack 11月月更

(六)OpenStack---M版---双节点搭建---Neutron安装和配置

指剑

centos OpenStack 11月月更

FreeNas安装、初始化和存储池设置

指剑

centos 11月月更 freenas

云安全系列4:解析云安全工具集

HummerCloud

云计算 云安全

DTSE Tech Talk 第13期:Serverless凭什么被誉为未来云计算范式?

华为云开发者联盟

云计算 后端 华为云

AWS之EC2搭建WordPress博客

指剑

AWS WordPress 11月月更

2022叉车模组发布会:打破整车生产思维,叉车迎来模组化创新

E科讯

为什么我推荐用户故事地图?

ShineScrum捷行

Scrum PO 用户故事地图

阿里云FC-Serverless-Wordpress

指剑

阿里云 Serverless 11月月更

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