按照模块化搭积木的思想。
要实现一款POS机,需要存储模块、配置文件操作模块,通信模块,卡操作模块,界面显示模块。
如果这些都具备了,实现一完整的pos机岂不是很简单?就像搭积木一样。
再在android的框架下按照一种MVVM架构去实现,轻而易举且能做到结构清晰。
先来看结果,操作记录有多简单,
Record01 rec = new Record01(); rec.recType = 1;//赋值 RecordApi api = RecordApi.getInstance(); api.saveRec01(rec);//存储 long unrec = api.getRecUnNum();//获取未上传记录数 Log.d("un send rec num:",String.valueOf(unrec)); //依此上传未上传记录 Record01 rdrec; for(int i =0 ;i < unrec;i++){ rdrec = api.readRecNotServer(1+i); Log.d("recDebug:",rdrec.toString()); } //删除已上传记录: RecordApi api = RecordApi.getInstance(); api.decRec01(unrec);
目前有这个打算从零开始构建一个Android上运行的POS机应用。
先从构建各个模块开始。银联8583解析的模块有了,操作配置文件的已经有了,前文介绍过了。通信的简单,Android上有各种开源的库,其中的retrofit配合Rxjava就很好用。卡操作模块打算用JNI做封装,因此卡操作模块也简单。重要的就是数据存储模块了,
以及后续对MVVM架构模式做个探究,再加上界面就完整了。
这里主要针对数据存储做个封装,让接口更好用。
Android上对本地数据库的操作很简单,且有很多开源的第三方ORM库可以用,如litepal ,greendao,realm,OrmLite等。
其中的greendao据说是性能很好,它能够支持数千条记录的CRUD每秒,和OrmLite相比,GreenDAO要快几乎4.5倍。
Realm 则是一个移动数据库,可运行于手机、平板和可穿戴设备之上。可以让你的应用更快速,带来难以想象的体验。其目标是为了代替 CoreData 和 SQLite 数据库。 Realm非常易用,不是在SQLite基础上的ORM,它有自己的数据查询引擎。是完全重新开发的数据库,速度非常快,并且支持跨平台,数据加密,数据迁移,支持json,流式api等 。
Android官方也有操作sqllite的ROOM框架。
这里我采用了国人做的一个ORM框架litepal,我觉得它操作简单好用,且升级维护表结构也简单。
只所以要封装,是因为本身的数据的 增,删,改,查,无论用哪种框架或者直接用原生的sqlhelper都不难。
但是这还不能满足我们的要求。
我们的需求是,数据存储不能一直顺序存,存满指定数目要从头覆盖存储。一条一条的覆盖已经上传过的记录。
且从不能删除记录,记录只能打过上传标记被覆盖,不能删除记录表里的记录。
还要能查询未上传记录数目,按顺序依次上传未上传的记录数量。
因此,本次封装实现原理,新建一个数据目录表。一个记录表对应一个记录目录表。
记录目录表里就一条记录,用来跟踪记录表里当前记录写到的ID位置,记录上传ID的位置。
且要考虑同一条记录可能需要上传至多个后台(平台)。没上传之前不能被覆盖。
可能有人说直接在一个记录表里就可以搞定了,或者写几个sql语句可以实现。但是数据量很大时效率是个问题。
以下为实现:
package com.example.yang.testmvvm.database.table; import org.litepal.annotation.Column; import org.litepal.crud.LitePalSupport; /** * Created by yangyongzhen on 2018/08/07 * * RecordDir,记录表对应的目录表,用来对记录表进行管理。 * RecordDir表,记录了当前记录的写的位置及记录读的位置。 * 可据此实现,查询未上传记录数量,依次上传未上传记录, * 记录顺序存储,存满指定容量后从头覆盖存储的方式。 * 删除记录操作只更新记录目录表的读的位置,从不真正的从记录表删除数据,保证数据的安全性。 */ public class RecordDir extends LitePalSupport { @Column(nullable = false) //Litepal数据库自动生成的自增的ID private long recNO; private long writeID; private long readID1; private long readID2; private long readID3; private int mode; private int upDateFlag; private long curWriteID; private long curReadID; @Column(ignore = true) public static final long MaxRecNO = 10; public long getRecNO() { return recNO; } public long getWriteID() { return writeID; } public long getReadID1() { return readID1; } public long getReadID2() { return readID2; } public long getReadID3() { return readID3; } public long getCurReadID() { return curReadID; } public int getMode() { return mode; } public long getCurWriteID() { return curWriteID; } public void setRecNO(long recNO) { this.recNO = recNO; } public void setWriteID(long writeID) { this.writeID = writeID; } public void setReadID1(long readID1) { this.readID1 = readID1; } public void setReadID2(long readID2) { this.readID2 = readID2; } public void setReadID3(long readID3) { this.readID3 = readID3; } public void setMode(int mode) { this.mode = mode; } public void setCurWriteID(long curWriteID) { this.curWriteID = curWriteID; } public void setCurReadID(long curReadID) { this.curReadID = curReadID; } public int getUpDateFlag() { return upDateFlag; } public void setUpDateFlag(int upDateFlag) { this.upDateFlag = upDateFlag; } @Override public String toString() { return "RecordDir{" + "recNO=" + recNO + ", writeID=" + writeID + ", readID1=" + readID1 + ", readID2=" + readID2 + ", readID3=" + readID3 + ", mode=" + mode + ", upDateFlag=" + upDateFlag + ", curWriteID=" + curWriteID + ", curReadID=" + curReadID + '}'; } }
记录操作的接口:
package com.example.yang.testmvvm.database; import android.content.Context; import android.util.Log; import com.example.yang.testmvvm.app.App; import com.example.yang.testmvvm.database.table.Record01; import com.example.yang.testmvvm.database.table.RecordDir; import org.litepal.LitePal; /** * Created by yangyongzhen on 2018/08/07 * 实现记录的常用操作接口:记录存储,查询未上传记录数,读取未上传记录,删除记录 */ public class RecordApi { public RecordDir recDir01; private static Context context; private static RecordApi instance = null; private RecordApi(Context contxt){ context = contxt; recDir01 = LitePal.find(RecordDir.class, 1); if(recDir01 == null){ recDir01 = new RecordDir(); recDir01.setWriteID(0); recDir01.setReadID1(0); recDir01.setRecNO(0); recDir01.setUpDateFlag(0); recDir01.save(); } } /** * 保存记录 * @param rec * @return */ public int saveRec01( Record01 rec){ if(rec == null){ return 1; } if(recDir01.getWriteID()+1 > RecordDir.MaxRecNO){ if((recDir01.getWriteID() + 1 - RecordDir.MaxRecNO) == recDir01.getReadID1()){ return 2;//记录满 } recDir01.setWriteID(1); recDir01.setUpDateFlag(1); rec.update(1); } else { if(recDir01.getUpDateFlag() == 1){ if((recDir01.getWriteID() + 1) == recDir01.getReadID1()){ return 3;//记录满 } rec.update(recDir01.getWriteID()+1); recDir01.setCurWriteID(recDir01.getWriteID()+1); recDir01.setRecNO(recDir01.getRecNO() + 1); recDir01.setWriteID(recDir01.getWriteID() + 1); recDir01.update(1); } else { rec.save(); recDir01.setRecNO(recDir01.getRecNO() + 1); recDir01.setWriteID(recDir01.getWriteID() + 1); recDir01.setCurWriteID(recDir01.getWriteID()+1); recDir01.update(1); } } Log.d("WriteRec:",recDir01.toString()); return 0; } /** * 删除记录,实际上只更改记录目录表的读指针,并不删除记录表的数据 * 记录表的数据采取循环存储,循环覆盖的模式,保证安全性 * @param recnum * @return */ public int decRec01(long recnum){ long id = recDir01.getReadID1(); if(recDir01.getWriteID() == recDir01.getReadID1()){ return 0; } if((id + recnum) > RecordDir.MaxRecNO){ if((id + recnum - RecordDir.MaxRecNO) > recDir01.getWriteID() ){ recDir01.setReadID1(recDir01.getWriteID()); recDir01.setCurReadID(recDir01.getWriteID()); recDir01.update(1); return 0; } recDir01.setReadID1(id + recnum - RecordDir.MaxRecNO); recDir01.setCurReadID(id + recnum - RecordDir.MaxRecNO); recDir01.update(1); }else { if(recDir01.getWriteID() > recDir01.getReadID1()){ if(id + recnum > recDir01.getWriteID()){ recDir01.setReadID1(recDir01.getWriteID()); recDir01.setCurReadID(recDir01.getWriteID()); recDir01.update(1); return 0; } } recDir01.setReadID1(id + recnum); recDir01.setCurReadID(id + recnum); recDir01.update(1); } return 0; } /** * 获取未上传的记录条数 * @return */ public long getRecUnNum() { long num = 0; if(recDir01.getWriteID() == recDir01.getReadID1()){ num = 0; return num; } if(recDir01.getUpDateFlag() == 0){ num = (recDir01.getWriteID() - recDir01.getReadID1()); }else{ if(recDir01.getWriteID() > recDir01.getReadID1()){ num = (recDir01.getWriteID() - recDir01.getReadID1()); }else{ num = RecordDir.MaxRecNO - recDir01.getReadID1() + recDir01.getWriteID(); } } return num; } /** * 读取未上传的记录数据,顺序读取 * sn取值 1-到-->未上传记录数目 * @param sn * @return */ public Record01 readRecNotServer(long sn){ Record01 rec; long id = recDir01.getReadID1(); if((id + sn) > RecordDir.MaxRecNO){ if(id + sn - RecordDir.MaxRecNO > recDir01.getWriteID()){ return null; } rec = LitePal.find(Record01.class, id + sn - RecordDir.MaxRecNO ); }else { if(recDir01.getReadID1() < recDir01.getWriteID()){ if((id + sn) > recDir01.getWriteID()){ return null; } } rec = LitePal.find(Record01.class, recDir01.getReadID1() + sn); } return rec; } public static RecordApi getInstance(){ if(instance == null){ instance = new RecordApi(App.getContext()); } return instance; } }
一个测试 demo:
btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //String anotherName = "John Doe"; //myViewModel.getName().setValue(anotherName); //myViewModel.loginmodel.login("qq8864","123456", LoginType.TYPE_LOGIN); Record01 rec = new Record01(); rec.recType = 1; RecordApi api = RecordApi.getInstance(); api.saveRec01(rec); long unrec = api.getRecUnNum(); Record01 rdrec; Log.d("un send rec num:",String.valueOf(unrec)); for(int i =0 ;i < unrec;i++){ rdrec = api.readRecNotServer(1+i); Log.d("recDebug:",rdrec.toString()); } } }); btn2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //mThread = new LongTimeWork(); //mThread.start(); /* sysCfg.ver.value = "1234"; sysCfg.ip.value = "218.28.133.181"; sysCfg.saveConfig(); sysCfg.printConfig(); sysCfg.ver.value = "5678"; sysCfg.saveConfig(); sysCfg.printConfig(); */ RecordApi api = RecordApi.getInstance(); api.decRec01(1); Log.d("del rec:",String.format("curwriteid:%d,curreadid:%d",api.recDir01.getCurWriteID(),api.recDir01.getCurReadID())); } });
测试日志:
D/WriteRec:: RecordDir{recNO=81, writeID=9, readID1=2, readID2=0, readID3=0, mode=0, upDateFlag=1, curWriteID=9, curReadID=2} D/un send rec num:: 7 D/recDebug:: Record01{id=3, physn=0, recType=1, psamTid='null', lineID=0, dealTime='null', cardAsn='null', dealMoney=0, oldBalance=0} D/recDebug:: Record01{id=4, physn=0, recType=1, psamTid='null', lineID=0, dealTime='null', cardAsn='null', dealMoney=0, oldBalance=0} D/recDebug:: Record01{id=5, physn=0, recType=1, psamTid='null', lineID=0, dealTime='null', cardAsn='null', dealMoney=0, oldBalance=0} D/recDebug:: Record01{id=6, physn=0, recType=1, psamTid='null', lineID=0, dealTime='null', cardAsn='null', dealMoney=0, oldBalance=0} D/recDebug:: Record01{id=7, physn=0, recType=1, psamTid='null', lineID=0, dealTime='null', cardAsn='null', dealMoney=0, oldBalance=0} D/recDebug:: Record01{id=8, physn=0, recType=1, psamTid='null', lineID=0, dealTime='null', cardAsn='null', dealMoney=0, oldBalance=0} D/recDebug:: Record01{id=9, physn=0, recType=1, psamTid='null', lineID=0, dealTime='null', cardAsn='null', dealMoney=0, oldBalance=0}
至此,操作记录存储以及上传未上传记录变得很简单了。
首先一开始先定义好Recod01表结构。
存记录时:
Record01 rec = new Record01(); rec.recType = 1;//赋值 RecordApi api = RecordApi.getInstance(); api.saveRec01(rec);//存储 long unrec = api.getRecUnNum();//获取未上传记录数 Record01 rdrec; Log.d("un send rec num:",String.valueOf(unrec)); //依此上传未上传记录 for(int i =0 ;i < unrec;i++){ rdrec = api.readRecNotServer(1+i); Log.d("recDebug:",rdrec.toString()); }
//删除已上传记录:
RecordApi api = RecordApi.getInstance(); api.decRec01(unrec);