自研安卓POS

分享一下最近刚做的POS项目。
当然是公司需要,所以自己主动提出降本好招数,成本高我们自己造!

1、背景

公司业务是类似于瑞幸咖啡,所以门店的初期开店成本很高,但是三方的Pos一体机确实挺贵的,2万一套,还每年需要3000的服务费,我们用pad自己实现,加上打印机,扫码枪等外设成本也才3000左右,也就是一个门店可以节约1.7万左右,1000家就是1700万,甚至于迭代成熟了,我们可以卖pos机给三方用,实现盈利,对接成本也低,体验也不错。

2、设计

基于解耦的思路,把打印机对接封装成了一个黑盒的module。上层业务模块依赖于约定好的接口文档,第一版文档比较简单。考虑到海外一些国家的流量问题,设计通信数据结构的原则模仿protobuffer对重复的key进行了压缩,value采用数组对应,优点是数据量越大,压缩越明显,缺点有很明显排查问题不直观。同时,对于不同的标志位,采用位操作表示,Java中Int有32个2进制位可以表示32种状态。

{
  "data": [
  { 
    //订单单号
    orderId:String,
    
    //格式参考protbuf  目的是减少http包大小,type和data,config的长度要一致
    //小票数据
    //数据类型 1单字符串
    type:[1,2,3,4,5,6......]
    //type对应的数据
    data:["字符串","分割符字符串-","","".......]
    //type对应的配置  采用位运算
    //config默认值传0表示无配置
    config:[1,1,1,1,1,1......]
    
    //杯贴数据
    //数据类型 1单字符串
    cupType:[1,2,3,4,5,6......]
    //type对应的数据
    cupData:["字符串","分割符字符串-","","".......]
    //type对应的配置  采用位运算
    //config默认值传0表示无配置
    cupConfig:[1,1,1,1,1,1......]
    } 
  ]
   "code": 0,
   "msg": "success",
   "success": true
}

至于我的协议内容怎么制定的就不展示了,不是重点。
然后是sdk的设计,首先考虑打印机连接的灵活性,需要动态配置的一些方向,采用抽象工厂对打印机对象进行封装,

1.1 抽象打印机

abstract class BasePrinter(var connector: BaseConnector,var device: Any? = null)

这里打印机的连接方式可能分为USB,蓝牙,以太网,共享热点等方式。
目前主要考虑USB,蓝牙,以太网三种支持扩展。海外网络基础建设比较复杂,不像国内原材料丰富,基建完善。所以实际场景可能是以太网为主,蓝牙辅助的场景居多。
因为以太网连接更加稳定,而蓝牙主要cover的场景是断网兜底备用。

abstract class BasePrinter(var connector: BaseConnector,var device: Any? = null)
/**USB连接*/
abstract class USBConnector : BaseConnector()
/**wifi连接 */
abstract class WIFIConnector : BaseConnector()
/**蓝牙连接*/
abstract class BLUEToothConnector : BaseConnector()

2.1 打印机生产厂商

向下继续扩展,根据厂家的不同定义不同的工厂类用于创建连接器以及打印机实例。

image.png

这里因为只对接了2家打印机,所以对2家打印厂家的不同功能进行实现。
这里以其中一家举例
打印机连接器工厂

为什么要把连接器设计的这么灵活?
因为不同的连接方式的连接过程区别很大,各家三方打印机sdk也有自己的设计风格,各家有各家的sdk连接代码也不一样,然后连接方式不一样的话实现流程区别就更大,比如蓝牙涉及到一系列的权限检查,以及蓝牙开关的检查,以及设备绑定动作,以太网则直需要只需要检查ip就可以了。

打印机工厂

到这里其实我们已经隔离了各家厂商的打印机初始化以及连接方式的差异化。做到了随意修改,插拔。

1.3 打印行为

打印机有不同的通讯一些这里主要是基于主流的小票采用 ESC协议 和杯贴采用的 TSPL协议 进行的实现。当然目前的设计后续需要扩展实现协议方式也比较简单。

打印行为抽象

上面依次是反白,TSPL特有的初始化打印区域,结束TSPL打印,打开钱箱,打印二维码,打印图片,打印空行等。
这里主要对主流的操作方式进行了抽象,虽然是第一版,但是也cover了大部分打印机场景,后续需要扩展基本是基于这里扩展了。这也是我们定义服务端打印行为的基础。
这样设计的好处是,如果后续需要调整打印机的排版,客户端不需要发版,非常灵活

实现

上面主要是我们对厂商的变化进行了抽象,方便后续扩展,上层我们我们主要的是打印机连接的实现,异步查找,连接的过程采用订阅者模式去监听查找和连接结果。目前主要实现了以太网和蓝牙2种连接场景。

连接配置这个类采用建造者模式编写,对打印机名字(INameGenerator)以及IP可进行动态配置。
也可使用默认配置方式

public class ConnectConfig {

    private INameGenerator nameGenerate;
    private List<PrinterConfig> pendingToConnects;

    public INameGenerator getNameGenerate() {
        return nameGenerate;
    }

    public List<PrinterConfig> getPendingToConnects() {
        return pendingToConnects;
    }

    public static final class ConnectConfigBuilder {
        private INameGenerator nameGenerate;
        private List<PrinterConfig> pendingToConnects;

        private ConnectConfigBuilder() {}

        public static ConnectConfigBuilder builder() {
            return new ConnectConfigBuilder();
        }

        public ConnectConfigBuilder defaultConfig(INameGenerator nameGenerate){
            withNameGenerate(nameGenerate);
            withPrinter(ConnectConfig.PrinterConfigBuilder.builder()
                    .withExtra("WiFi,10.1.2.199,9100")
                    .withConnectWay(ConnectWay.WIFI)
                    .withType(PrinterType.Tag)
                    .build());
            withPrinter(ConnectConfig.PrinterConfigBuilder.builder()
                    .withConnectWay(ConnectWay.BLUETooth)
                    .withType(PrinterType.Ticket)
                    .build());
   
            StringBuilder sb = new StringBuilder(20);
            sb.append("配置打印机连接方式:\n");
            for (PrinterConfig pendingToConnect : pendingToConnects) {
                sb.append(">>Type:").append(pendingToConnect.type).append(">>way:").append(pendingToConnect.connectWay)
                        .append(">>extra:").append(pendingToConnect.extra)
                        .append("\n");
            }
            PrintLog.getInstance().log(sb.toString());
            return this;
        }

        public ConnectConfigBuilder withNameGenerate(INameGenerator nameGenerate) {
            this.nameGenerate = nameGenerate;
            return this;
        }

        public ConnectConfigBuilder withPrinter(PrinterConfig printerConfig) {
            if(pendingToConnects == null){
                pendingToConnects = new ArrayList<>();
            }
            pendingToConnects.add(printerConfig);
            return this;
        }

        public ConnectConfig build() {
            ConnectConfig connectConfig = new ConnectConfig();
            connectConfig.pendingToConnects = this.pendingToConnects;
            connectConfig.nameGenerate = this.nameGenerate;
            return connectConfig;
        }
    }

    public static class PrinterConfig{
        private PrinterType type;
        private ConnectWay connectWay;
        private String extra;

        public PrinterType getType() {
            return type;
        }

        public ConnectWay getConnectWay() {
            return connectWay;
        }

        public String getExtra() {
            return extra;
        }
    }

    public static final class PrinterConfigBuilder {
        private PrinterType type;
        private ConnectWay connectWay;
        private String extra;

        public static PrinterConfigBuilder builder() {
            return new PrinterConfigBuilder();
        }

        public PrinterConfigBuilder withConnectWay(ConnectWay way) {
            this.connectWay = way;
            return this;
        }

        public PrinterConfigBuilder withType(PrinterType type) {
            this.type = type;
            return this;
        }

        public PrinterConfigBuilder withExtra(String extra){
            this.extra = extra;
            return this;
        }

        public PrinterConfig build() {
            PrinterConfig connectConfig = new PrinterConfig();
            connectConfig.connectWay = this.connectWay;
            connectConfig.type = this.type;
            connectConfig.extra = this.extra;
            return connectConfig;
        }
    }
}

外部调用打印机初始化这个对象就可以了。主要方式是

PrintManager

1、release()释放资源
2、startConnect()根据ConnectConfig连接打印机
3、command()和commandAsy()一个是同步调用,一个是采用异步调用的方式。

object Command{
    //打开钱箱
    //> 0 表示打开钱箱
    const val Open_drawer = "Open_drawer"
    const val origin_ticket_data = "ticket_data"
    const val origin_tag_data = "tag_data"
    const val Fetch_print_info = "fetch_print_info"

    const val test = "test"
}
这个是打印机入口管理类

其他的就是一些辅助工具了,利用Kotlin的扩展函数机制,可以优雅的实现程序入口链式调用。不过这个链式调用只适用于纯kotlin项目,Java还是需要通过对象去getInstance()。

object Print

/**保存失败日志 主要保存打印失败的数据,设置超过N天自动清理数据的逻辑等*/
val Print.record : PrintRecord
    get() = PrintRecord.getInstance()

/**保存日志 主要用来打印操作日志  设置超过N天的数据自动清理数据等*/
val Print.log : PrintLog
    get() = PrintLog.getInstance()

下层封装基本就是这样了,上层在加上一个任务队列


image.png
    //打印机全部订单并流转所有订单状态为已接单
    const val PRINT_ALL_AND_PROCESS
    //打印机全部订单
    const val PRINT_ALL
    //打印日结小票
    const val PRINT_DAILY
    //打印订单并流转订单状态为待接单
    const val PRINT_ORDER_AND_PROCESS
    //打印订单
    const val PRINT_ORDER
    //本地类型 打印机失败重试任务
    const val PRINT_FAILURE
    //添加打印任务
    fun addPrint(type:String? = null,
      orderId:String?= null,
      cashierId:String? = null,
      selectedDate:String? = null,
      byUser :Boolean = false) : Boolean

在初始化打印机之后,需要进行打印任务的通过addPrint进行任务添加,打印机队列是一个异步阻塞队列,在异步线程中等待新的任务。

添加打印任务可以通过支付成功的时机,以及收到推送,或者轮询等方式。
内部还有打印错误,接口错误等重试机制。


总结

上面基本就是自研POS SDK的设计了。
调用SDK方只需要配置好ConnnectConfig调用初始化对象PrintManager的startConnect()就可以实现打印机连接。之后通过PrintQueue异步等待实时触发打印任务。服务端通过使用我提供的数据文档,可实现对打印数据随意组装,从而做到打印机SDK与业务解耦。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容