分享一下最近剛做的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)建連接器以及打印機實例或衡。
這里因為只對接了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ù)隊列
//打印機全部訂單并流轉(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ù)解耦绽族。