自研安卓POS

分享一下最近剛做的POS項(xiàng)目。
當(dāng)然是公司需要,所以自己主動提出降本好招數(shù),成本高我們自己造!

1、背景

公司業(yè)務(wù)是類似于瑞幸咖啡,所以門店的初期開店成本很高,但是三方的Pos一體機(jī)確實(shí)挺貴的,2萬一套,還每年需要3000的服務(wù)費(fèi),我們用pad自己實(shí)現(xiàn),加上打印機(jī),掃碼槍等外設(shè)成本也才3000左右,也就是一個門店可以節(jié)約1.7萬左右,1000家就是1700萬,甚至于迭代成熟了,我們可以賣pos機(jī)給三方用,實(shí)現(xiàn)盈利,對接成本也低,體驗(yàn)也不錯。

2、設(shè)計(jì)

基于解耦的思路,把打印機(jī)對接封裝成了一個黑盒的module。上層業(yè)務(wù)模塊依賴于約定好的接口文檔,第一版文檔比較簡單??紤]到海外一些國家的流量問題,設(shè)計(jì)通信數(shù)據(jù)結(jié)構(gòu)的原則模仿protobuffer對重復(fù)的key進(jìn)行了壓縮,value采用數(shù)組對應(yīng),優(yōu)點(diǎn)是數(shù)據(jù)量越大,壓縮越明顯,缺點(diǎn)有很明顯排查問題不直觀。同時(shí),對于不同的標(biāo)志位,采用位操作表示,Java中Int有32個2進(jìn)制位可以表示32種狀態(tài)。

{
  "data": [
  { 
    //訂單單號
    orderId:String,
    
    //格式參考protbuf  目的是減少http包大小,type和data,config的長度要一致
    //小票數(shù)據(jù)
    //數(shù)據(jù)類型 1單字符串
    type:[1,2,3,4,5,6......]
    //type對應(yīng)的數(shù)據(jù)
    data:["字符串","分割符字符串-","","".......]
    //type對應(yīng)的配置  采用位運(yùn)算
    //config默認(rèn)值傳0表示無配置
    config:[1,1,1,1,1,1......]
    
    //杯貼數(shù)據(jù)
    //數(shù)據(jù)類型 1單字符串
    cupType:[1,2,3,4,5,6......]
    //type對應(yīng)的數(shù)據(jù)
    cupData:["字符串","分割符字符串-","","".......]
    //type對應(yīng)的配置  采用位運(yùn)算
    //config默認(rèn)值傳0表示無配置
    cupConfig:[1,1,1,1,1,1......]
    } 
  ]
   "code": 0,
   "msg": "success",
   "success": true
}

至于我的協(xié)議內(nèi)容怎么制定的就不展示了,不是重點(diǎn)。
然后是sdk的設(shè)計(jì),首先考慮打印機(jī)連接的靈活性,需要動態(tài)配置的一些方向,采用抽象工廠對打印機(jī)對象進(jìn)行封裝,

1.1 抽象打印機(jī)

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

這里打印機(jī)的連接方式可能分為USB,藍(lán)牙,以太網(wǎng),共享熱點(diǎn)等方式。
目前主要考慮USB,藍(lán)牙,以太網(wǎng)三種支持?jǐn)U展。海外網(wǎng)絡(luò)基礎(chǔ)建設(shè)比較復(fù)雜,不像國內(nèi)原材料豐富,基建完善。所以實(shí)際場景可能是以太網(wǎng)為主,藍(lán)牙輔助的場景居多。
因?yàn)橐蕴W(wǎng)連接更加穩(wěn)定,而藍(lán)牙主要cover的場景是斷網(wǎng)兜底備用。

abstract class BasePrinter(var connector: BaseConnector,var device: Any? = null)
/**USB連接*/
abstract class USBConnector : BaseConnector()
/**wifi連接 */
abstract class WIFIConnector : BaseConnector()
/**藍(lán)牙連接*/
abstract class BLUEToothConnector : BaseConnector()

2.1 打印機(jī)生產(chǎn)廠商

向下繼續(xù)擴(kuò)展,根據(jù)廠家的不同定義不同的工廠類用于創(chuàng)建連接器以及打印機(jī)實(shí)例。

image.png

這里因?yàn)橹粚恿?家打印機(jī),所以對2家打印廠家的不同功能進(jìn)行實(shí)現(xiàn)。
這里以其中一家舉例
打印機(jī)連接器工廠

為什么要把連接器設(shè)計(jì)的這么靈活?
因?yàn)椴煌倪B接方式的連接過程區(qū)別很大,各家三方打印機(jī)sdk也有自己的設(shè)計(jì)風(fēng)格,各家有各家的sdk連接代碼也不一樣,然后連接方式不一樣的話實(shí)現(xiàn)流程區(qū)別就更大,比如藍(lán)牙涉及到一系列的權(quán)限檢查,以及藍(lán)牙開關(guān)的檢查,以及設(shè)備綁定動作,以太網(wǎng)則直需要只需要檢查ip就可以了。

打印機(jī)工廠

到這里其實(shí)我們已經(jīng)隔離了各家廠商的打印機(jī)初始化以及連接方式的差異化。做到了隨意修改,插拔。

1.3 打印行為

打印機(jī)有不同的通訊一些這里主要是基于主流的小票采用 ESC協(xié)議 和杯貼采用的 TSPL協(xié)議 進(jìn)行的實(shí)現(xiàn)。當(dāng)然目前的設(shè)計(jì)后續(xù)需要擴(kuò)展實(shí)現(xiàn)協(xié)議方式也比較簡單。

打印行為抽象

上面依次是反白,TSPL特有的初始化打印區(qū)域,結(jié)束TSPL打印,打開錢箱,打印二維碼,打印圖片,打印空行等。
這里主要對主流的操作方式進(jìn)行了抽象,雖然是第一版,但是也cover了大部分打印機(jī)場景,后續(xù)需要擴(kuò)展基本是基于這里擴(kuò)展了。這也是我們定義服務(wù)端打印行為的基礎(chǔ)。
這樣設(shè)計(jì)的好處是,如果后續(xù)需要調(diào)整打印機(jī)的排版,客戶端不需要發(fā)版,非常靈活

實(shí)現(xiàn)

上面主要是我們對廠商的變化進(jìn)行了抽象,方便后續(xù)擴(kuò)展,上層我們我們主要的是打印機(jī)連接的實(shí)現(xiàn),異步查找,連接的過程采用訂閱者模式去監(jiān)聽查找和連接結(jié)果。目前主要實(shí)現(xiàn)了以太網(wǎng)和藍(lán)牙2種連接場景。

連接配置這個類采用建造者模式編寫,對打印機(jī)名字(INameGenerator)以及IP可進(jìn)行動態(tài)配置。
也可使用默認(rèn)配置方式

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("配置打印機(jī)連接方式:\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;
        }
    }
}

外部調(diào)用打印機(jī)初始化這個對象就可以了。主要方式是

PrintManager

1、release()釋放資源
2、startConnect()根據(jù)ConnectConfig連接打印機(jī)
3、command()和commandAsy()一個是同步調(diào)用,一個是采用異步調(diào)用的方式。

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"
}
這個是打印機(jī)入口管理類

其他的就是一些輔助工具了,利用Kotlin的擴(kuò)展函數(shù)機(jī)制,可以優(yōu)雅的實(shí)現(xiàn)程序入口鏈?zhǔn)秸{(diào)用。不過這個鏈?zhǔn)秸{(diào)用只適用于純kotlin項(xiàng)目,Java還是需要通過對象去getInstance()。

object Print

/**保存失敗日志 主要保存打印失敗的數(shù)據(jù),設(shè)置超過N天自動清理數(shù)據(jù)的邏輯等*/
val Print.record : PrintRecord
    get() = PrintRecord.getInstance()

/**保存日志 主要用來打印操作日志  設(shè)置超過N天的數(shù)據(jù)自動清理數(shù)據(jù)等*/
val Print.log : PrintLog
    get() = PrintLog.getInstance()

下層封裝基本就是這樣了,上層在加上一個任務(wù)隊(duì)列


image.png
    //打印機(jī)全部訂單并流轉(zhuǎn)所有訂單狀態(tài)為已接單
    const val PRINT_ALL_AND_PROCESS
    //打印機(jī)全部訂單
    const val PRINT_ALL
    //打印日結(jié)小票
    const val PRINT_DAILY
    //打印訂單并流轉(zhuǎn)訂單狀態(tài)為待接單
    const val PRINT_ORDER_AND_PROCESS
    //打印訂單
    const val PRINT_ORDER
    //本地類型 打印機(jī)失敗重試任務(wù)
    const val PRINT_FAILURE
    //添加打印任務(wù)
    fun addPrint(type:String? = null,
      orderId:String?= null,
      cashierId:String? = null,
      selectedDate:String? = null,
      byUser :Boolean = false) : Boolean

在初始化打印機(jī)之后,需要進(jìn)行打印任務(wù)的通過addPrint進(jìn)行任務(wù)添加,打印機(jī)隊(duì)列是一個異步阻塞隊(duì)列,在異步線程中等待新的任務(wù)。

添加打印任務(wù)可以通過支付成功的時(shí)機(jī),以及收到推送,或者輪詢等方式。
內(nèi)部還有打印錯誤,接口錯誤等重試機(jī)制。


總結(jié)

上面基本就是自研POS SDK的設(shè)計(jì)了。
調(diào)用SDK方只需要配置好ConnnectConfig調(diào)用初始化對象PrintManager的startConnect()就可以實(shí)現(xiàn)打印機(jī)連接。之后通過PrintQueue異步等待實(shí)時(shí)觸發(fā)打印任務(wù)。服務(wù)端通過使用我提供的數(shù)據(jù)文檔,可實(shí)現(xiàn)對打印數(shù)據(jù)隨意組裝,從而做到打印機(jī)SDK與業(yè)務(wù)解耦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容