NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

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

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

评论

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

TDengine 在酷哞哞的应用

TDengine

数据库 tdengine 开源 物联网

Docker学习记录

ZuccRoger

5月月更

ShardingSphere 在东南亚|与科技保险公司 Fuse 的技术融合

SphereEx

Apache 开源 ShardingSphere SphereEx 数据库·

【刷题第12天】58. 最后一个单词的长度

白日梦

5月月更

加入MOVE,一起体验Move2Earn的运动乐趣

BlockChain先知

开启分布式应用性能观测(APM)

观测云

可观测性 可观测

B站S11破亿直播在线稳定性保障秘籍——演讲实录

TakinTalks稳定性社区

混沌工程 系统稳定性 全链路压测 安全生产

携手 TDengine,释普科技升级实验室仪器、监控智能方案

TDengine

数据库 tdengine 开源 物联网

LinkedList 源码分析-初始化&节点查询

zarmnosaj

5月月更

敏捷已死

方云AI研发绩效

netty系列之:在netty中实现线程和CPU绑定

程序那些事

Java Netty 程序那些事 5月月更

要做研发高手,就是必须能看英文、写英文

TDengine

数据库 tdengine 开源

争夺存量用户关键战,助力企业构建完美标签体系丨01期直播回顾

袋鼠云数栈

大数据 数据中台

携手数字人、数字空间、XR平台,阿里云与伙伴共同建设“新视界”

阿里云弹性计算

XR 数字人 视觉计算 瑶台

druid源码学习八

Nick

Apache Druid 自旋锁

时间序列化数据库选型?时序数据库的选择?

TDengine

数据库 tdengine

「国货」设计SaaS崛起,黑马inCreate自图冲出公装赛道

ToB行业头条

为什么企业要告别自托管并迁移到 Atlassian 云版?

龙智—DevSecOps解决方案

Atlassian Atlassian 云版 Atlassian迁移

客户成功是一种思维模式 | ONES 人物

万事ONES

[Day42]-[回溯]-组合

方勇(gopher)

LeetCode 数据结构和算法 回溯算法

直播预约|数据指标体系如何搭建才最有效,从0到1带你快速入门

袋鼠云数栈

大数据 数据中台

时序数据库的集群方案?

TDengine

数据库 tdengine 开源

[Day41]-[回溯]-全排列

方勇(gopher)

LeetCode 回溯算法 数据结构算法

百度程序员Android开发小技巧

百度Geek说

移动端

第三方 IP:管理半导体外部 IP

龙智—DevSecOps解决方案

perforce Methodics IPLM 管理 IP

TDengine在弘源泰平量化投资中的实践

TDengine

数据库 tdengine 开源 时序数据库

为什么说 MongoDB 和 HBase 不适用于汽车行业的时序数据处理?

TDengine

数据库 tdengine 开源 时序数据库

ApacheCon Asia 2022 强势来袭!16 大专题等你投稿!

阿里巴巴云原生

开源 云原生 活动

如何使用阿里云 CDN 对部署在函数计算上的静态网站进行缓存

阿里巴巴云原生

阿里云 Serverless 云原生 CDN 函数计算

场景实践 | 如何使用融云超级群构建游戏社区

融云 RongCloud

火爆的健身应用软件是如何一步一步打造出来的?

龙智—DevSecOps解决方案

DevOps perforce Helix Core

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