自研安卓POS

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

1僵缺、背景

公司業(yè)務(wù)是類似于瑞幸咖啡云茸,所以門店的初期開店成本很高,但是三方的Pos一體機確實挺貴的谤饭,2萬一套标捺,還每年需要3000的服務(wù)費懊纳,我們用pad自己實現(xiàn)箫锤,加上打印機插勤,掃碼槍等外設(shè)成本也才3000左右答毫,也就是一個門店可以節(jié)約1.7萬左右年鸳,1000家就是1700萬灌危,甚至于迭代成熟了卤妒,我們可以賣pos機給三方用驯击,實現(xiàn)盈利硝清,對接成本也低屋谭,體驗也不錯脚囊。

2、設(shè)計

基于解耦的思路桐磁,把打印機對接封裝成了一個黑盒的module悔耘。上層業(yè)務(wù)模塊依賴于約定好的接口文檔,第一版文檔比較簡單我擂〕囊裕考慮到海外一些國家的流量問題,設(shè)計通信數(shù)據(jù)結(jié)構(gòu)的原則模仿protobuffer對重復(fù)的key進行了壓縮校摩,value采用數(shù)組對應(yīng)看峻,優(yōu)點是數(shù)據(jù)量越大,壓縮越明顯衙吩,缺點有很明顯排查問題不直觀互妓。同時,對于不同的標(biāo)志位坤塞,采用位操作表示车猬,Java中Int有32個2進制位可以表示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)的配置  采用位運算
    //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)的配置  采用位運算
    //config默認(rèn)值傳0表示無配置
    cupConfig:[1,1,1,1,1,1......]
    } 
  ]
   "code": 0,
   "msg": "success",
   "success": true
}

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

1.1 抽象打印機

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

這里打印機的連接方式可能分為USB,藍(lán)牙,以太網(wǎng),共享熱點等方式承绸。
目前主要考慮USB,藍(lán)牙挣轨,以太網(wǎng)三種支持?jǐn)U展军熏。海外網(wǎng)絡(luò)基礎(chǔ)建設(shè)比較復(fù)雜,不像國內(nèi)原材料豐富卷扮,基建完善荡澎。所以實際場景可能是以太網(wǎng)為主均践,藍(lán)牙輔助的場景居多。
因為以太網(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 打印機生產(chǎn)廠商

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

image.png

這里因為只對接了2家打印機焦影,所以對2家打印廠家的不同功能進行實現(xiàn)。
這里以其中一家舉例
打印機連接器工廠

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

打印機工廠

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

1.3 打印行為

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

打印行為抽象

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

實現(xiàn)

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

連接配置這個類采用建造者模式編寫,對打印機名字(INameGenerator)以及IP可進行動態(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("配置打印機連接方式:\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)用打印機初始化這個對象就可以了刃泌。主要方式是

PrintManager

1凡壤、release()釋放資源
2、startConnect()根據(jù)ConnectConfig連接打印機
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"
}
這個是打印機入口管理類

其他的就是一些輔助工具了俗扇,利用Kotlin的擴展函數(shù)機制硝烂,可以優(yōu)雅的實現(xiàn)程序入口鏈?zhǔn)秸{(diào)用。不過這個鏈?zhǔn)秸{(diào)用只適用于純kotlin項目铜幽,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ù)隊列


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

在初始化打印機之后狮杨,需要進行打印任務(wù)的通過addPrint進行任務(wù)添加,打印機隊列是一個異步阻塞隊列到忽,在異步線程中等待新的任務(wù)橄教。

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


總結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末姨涡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子吧慢,更是在濱河造成了極大的恐慌涛漂,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異匈仗,居然都是意外死亡瓢剿,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門悠轩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來间狂,“玉大人,你說我怎么就攤上這事火架〖螅” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵何鸡,是天一觀的道長纺弊。 經(jīng)常有香客問我,道長骡男,這世上最難降的妖魔是什么淆游? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮隔盛,結(jié)果婚禮上犹菱,老公的妹妹穿的比我還像新娘。我一直安慰自己吮炕,他們只是感情好腊脱,可當(dāng)我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著来屠,像睡著了一般虑椎。 火紅的嫁衣襯著肌膚如雪震鹉。 梳的紋絲不亂的頭發(fā)上俱笛,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機與錄音传趾,去河邊找鬼迎膜。 笑死,一個胖子當(dāng)著我的面吹牛浆兰,可吹牛的內(nèi)容都是我干的磕仅。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼簸呈,長吁一口氣:“原來是場噩夢啊……” “哼榕订!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蜕便,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤劫恒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體两嘴,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡丛楚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了憔辫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片趣些。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖贰您,靈堂內(nèi)的尸體忽然破棺而出坏平,到底是詐尸還是另有隱情,我是刑警寧澤锦亦,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布功茴,位于F島的核電站,受9級特大地震影響孽亲,放射性物質(zhì)發(fā)生泄漏坎穿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一返劲、第九天 我趴在偏房一處隱蔽的房頂上張望玲昧。 院中可真熱鬧,春花似錦篮绿、人聲如沸孵延。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尘应。三九已至,卻和暖如春吼虎,著一層夾襖步出監(jiān)牢的瞬間犬钢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工思灰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留玷犹,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓洒疚,卻偏偏與公主長得像歹颓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子油湖,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容