问题描述
在博主开发一个项目时,遇到了这么一个需求,一共有护士、病人、药品、设备、病床等标签(rfid),如果在采集器范围内采集到的标签有状态变化(rfid标签离开/进入采集到的范围),就会触发事件,如果是多个标签的状态同时变化,会触发特殊的场景。
问题分析
在收到这个需求后,博主思考了下,如果使用简单的if-else来处理的话,我脑袋里已经有很多苍蝇在嗡嗡嗡的飞了,首先来说,如何去判断状态是否变化就会让人烦不胜烦,何况还有这么多种类的标签,此时使用List或是Set结构都不是很合适,再者遇到更复杂的场景,比如需要判断护士、药品、病人同时采集到30s才可以判定为用药场景,光是这一个场景,估计代码就要好几百行了。最后整个逻辑的代码量会非常的大,且if-else过多导致难以理解和维护,如果在换个开发,那简直就是灾难了。
博主在经过一下午的思考整理后,博主想到一个解决方案,也就是我们题目上说的,使用正则,因为正则对字符串处理上的强大,我们将状态的变化转换为字符串,之后通过写相应的正则来判断是否符合场景,把复杂的场景的状态通过正则来简单的处理。
算法描述
首先,我们约定的数据格式如下:data:N-100002,P-20001,M-30001,E-40001,B-50001…每个标签用英文逗号分隔,单个标签用-来分隔标签的类型,类型定义如下:
在@Getter
@AllArgsConstructor
public enum RfidTypeEnum {
N("N", "护士RFID标签类型"),
P("P", "病人RFID标签类型"),
E("E", "设备RFID标签类型"),
M("M", "药品RFID标签类型"),
B("B", "病床RFID标签类型"),
;
/**
* 标签RFID的类型
*/
private final String type;
/**
* 标签类型的描述
*/
private final String description;
/**
* 根据String转换成对应的枚举值
*/
public static RfidTypeEnum getFrom(String type) {
for (RfidTypeEnum tmp : RfidTypeEnum.values()) {
if (type.equals(tmp.getType())) {
return tmp;
}
}
return null;
}
}
我们收到当前时刻采集到的标签数据,如果包含护士标签,那么我们就将护士的状态置为0,如果不包含,则表示护士不在,状态置为1,其他的标签我们也是同样的规则,之后我们按照NPMEB顺序(即护士、病人、药品、设备、病床)将这五种标签的当前状态组合,假设接收的数据为N-100002,P-20001,那我们转换的当前状态为00111转换代码如下所示:
//此处代码为前面调用的代码
{
//...
String[] rfids = data.split(",");
List<String> rfidList = new ArrayList<>(Arrays.asList(rfids));
//当前状态
String nowStatus = castData(rfidList);
//...
}
/**
* 根据标签列表产生采集到的标签
* 根据顺序N P M E B
* 0表示在,1表示不在
* 因为状态变化出发操作与采集到的数据中有多少个无关,所以此时不关心数量
* 如果rfidList中只有一个NULL,状态全部为11111(如果采集不到数据,会接收一个NULL字符串,用于做时间间隔)
*
* @param rfidList
* @return
*/
private String castData(List<String> rfidList) {
StringBuilder result = new StringBuilder("11111");
//兼容采集不到数据的问题 字符串为"NULL"
if (rfidList.size() == 1 && "NULL".equals(rfidList.get(0))) {
return result.toString();
}
for (String rfid : rfidList) {
String type = rfid.split("-")[0];
result.replace(index.indexOf(type), index.indexOf(type) + 1, "0");
}
return result.toString();
}
之后我们将当前的状态和上一时刻的状态进行比对,假设上一时刻没有采集到任何的标签,那上一时刻的状态字符串为11111,此时,我们将状态的变化使用一个具体的数字来进行表示,比如1->1,实际意义为连续两次都没有采集到的,我们用1来进行表示,具体的状态变化的表示我们定义如下:
@Getter
@AllArgsConstructor
public enum StatusChangeEnum {
/**
* 1->1
* 一直不在
*/
A("11", 1),
/**
* 1->0
* 不在变为在
*/
B("10", 2),
/**
* 0->1
* 在变为不在
*/
C("01", 3),
/**
* 0->0
* 一直在
*/
D("00", 4),
;
private String des;
private Integer value;
/**
* 由String 转换为枚举值
* @param des
* @return
*/
public static StatusChangeEnum getFrom(String des){
for(StatusChangeEnum tmp: StatusChangeEnum.values()){
if(des.equals(tmp.getDes())){
return tmp;
}
}
return null;
}
}
具体的转换代码如下,其中historyStatus是一个全部变量,数据结构为一个List,用来存储之前的历史状态,排在最后的数据为上一时刻的状态,我们将当前的状态的字符串(00111)和上一时刻(11111)的状态字符串按照下述代码转换,得到的状态变换的statusChange的值为:22111。
//此处代码为前面调用的代码
{
//...
String statusChange = castAndOperation(nowStatus);
//...
}
/**
* 将当前状态和上一状态做比对,得到状态变化字符串
*
* @param nowStatus
* @return
*/
private String castAndOperation(String nowStatus) {
//log.info("history data: 【{}】", historyStatus);
String beforeStatus = historyStatus.get(historyStatus.size() - 1);
StringBuilder result = new StringBuilder("");
for (int i = 0; i < beforeStatus.length(); i++) {
String change = "" + beforeStatus.charAt(i) + nowStatus.charAt(i);
result.append(StatusChangeEnum.getFrom(change).getValue());
}
return result.toString();
}
到这里,我们就得到状态变化的字符串statusChange了,我们就可以使用正则表达式来判断标签数据的变换了,比如我们要判断护士的状态是否变化,我们就可以使用正则来进行判断了,因为状态变换有两种情况:0->1或1->0,因此我们只需护士所在的位置(NPMEB)数字为2或3即可。
private static final Pattern nurseChange = Pattern.compile("^[2|3]\\d{4}");
如果想要更复杂的场景,比如我们在上面讲的用药场景,需要护士、病人、药品都在,这种状态就有两种情况:1->0或0->0,即要当前的状态都需要为0,因为我们的正则表达式如下:
private static final Pattern medication = Pattern.compile("1{3}\d{2}");
------------------------------------------分割线----------
获取rfid数据
上面我们将了状态变化的部分,如果状态变化后,去触发事件时还需要标签中的rfid值,就需要我们维护另一个和historyStatus一样的数据了,我们定义为historyRfid,用来存放每次收到的数据,数据结构也是List,我们将一次接收的rfid数据按照以下格式来处理,相同类型的标签数据使用-分隔,不同类型的标签使用英文,分隔,处理代码如下:
/**
* 根据标签列表得到rfid列表
* rfid列表按照N P M E B排列
* 多个rfid按照xxx,xxx组合 英文,为分隔符
* 如果某个不存在,使用NULL标记
* 不同的类型的用-分割
* 数据示例
*
* @param rfidList
* @return
*/
private String getRfifData(List<String> rfidList) {
//去除重复的rfid
Set<String> rfidSet = new HashSet<>();
StringBuilder[] rfids = {new StringBuilder(), new StringBuilder("-"),
new StringBuilder("-"), new StringBuilder("-"), new StringBuilder("-")};
for (String rfid : rfidList) {
String[] nums = rfid.split("-");
//防止空指针异常
if (index.contains(nums[0]) && !rfidSet.contains(nums[1])) {
//将rfid拼接到对应的位置
rfids[index.indexOf(nums[0])].append(nums[1]).append(",");
//将rfid加入到集合中,用作下次判重
rfidSet.add(nums[1]);
}
}
StringBuilder result = new StringBuilder();
for (StringBuilder tem : rfids) {
if (tem.length() <= 1) {
//如果此次数据不包含此类标签,使用NULL标记
tem.append("NULL");
} else {
//替换掉多余的 ,
tem.replace(tem.lastIndexOf(","), tem.lastIndexOf(",") + 1, "");
}
result.append(tem);
}
return result.toString();
}
如果我们想要获取rfid数据,假设只获取状态变化的rfid数据,因为有两种情况:0->1或1->0,这也就需要我们从当前的接收到的数据或者上一时刻接收的数据或者两者获取rfid数据。获取代码如下,其中方法中的typeSet参数为所需的数据类型的集合:
/**
* 将上面需要获取状态变化的数据抽取出来
* 可以根据所需数据的集合获取状态未变化的rfid数据列表
*
* @param rfidList
* @param typeSet
* @return
*/
private List<String> getStatusUnchangeRfidData(List<String> rfidList, Set<String> typeSet) {
List<String> rfidDataList = new ArrayList<>();
for (String rfid : rfidList) {
String[] nums = rfid.split("-");
if (typeSet.contains(nums[0])) {
rfidDataList.add(nums[1]);
} else {
log.info("无用数据:【{}】,过滤", nums);
}
}
return rfidDataList;
}
/**
* 将上面需要获取状态变化的数据抽取出来
* 可以根据当前序列和上一序列,获取状态变化的rfid数据列表
*
* @param rfidList
* @param statusChange
* @param typeSet
* @return
*/
private List<String> getStatusChangeRfidData(List<String> rfidList, String statusChange, Set<String> typeSet) {
List<String> rfidDataList = new ArrayList<>();
//获取当前的和上一时刻的rfid列表
String beforeRfid = historyRfid.get(historyRfid.size() - 1);
String nowRfid = getRfifData(rfidList);
List<String> beforeRfids = Arrays.asList(beforeRfid.split("-"));
List<String> nowRfids = Arrays.asList(nowRfid.split("-"));
for (int i = 0; i < statusChange.length(); i++) {
int status = Integer.parseInt("" + statusChange.charAt(i));
//设置数据 只有状态发生变化才发送0->1||1->0
String[] rfidArray;
if (status == 2 || status == 3) {
//1->0 可从当前获取 0->1 从上一时刻获取
rfidArray = status == 2 ? nowRfids.get(i).split(",") : beforeRfids.get(i).split(",");
Integer dataStatus = status == 2 ? 0 : 1;
//数据异常,跳过这条数据
if ("NULL".equals(rfidArray[0])) {
continue;
}
for (String rfid : rfidArray) {
if (typeSet.contains("" + index.charAt(i))) {
rfidDataList.add(rfid);
}
}
}
}
return rfidDataList;
}
详细代码
下面我们给出方法的具体调用,因为调用的函数上面都已提供,所以这里只是接口的主体代码,具体如下所示:
//import ...
@Slf4j
public class dataTrigerTest {
private static final String index = "NPMEB";
private static final Integer cacheCount = 60;
//场景组装数据使用
private static final Set<String> commonSet = new HashSet<>();
private static final Set<String> pharmacySet = new HashSet<>();
private static final Set<String> equipmentSet = new HashSet<>();
private static final Set<String> medicationSet = new HashSet<>();
private static final Set<String> pmoveSet = new HashSet<>();
private static final Set<String> medicalWasteSet = new HashSet<>();
private static List<String> historyStatus = new ArrayList<>();
private static List<String> historyRfid = new ArrayList<>();
//初始化数据
static {
historyStatus.add("11111");
//初始时,没有rfid数据,置为NULL
historyRfid.add("NULL");
pharmacySet.add("N");
pharmacySet.add("M");
medicationSet.addAll(pharmacySet);
medicationSet.add("P");
equipmentSet.add("E");
equipmentSet.add("B");
commonSet.addAll(medicationSet);
commonSet.addAll(equipmentSet);
pmoveSet.add("P");
pmoveSet.add("B");
medicalWasteSet.addAll(pharmacySet);
}
private static final Pattern medication = Pattern.compile("^[2|4]{3}");
private static final Pattern nurseChange = Pattern.compile("^[2|3]\\d{4}");
public String processDataChange(String data) {
//process data
//data:N-100002,B-20001,...
log.info("接收的数据为:【{}】", data);
if (StringUtils.isEmpty(data)) {
log.error("接收信息为空");
return null;
}
String[] rfids = data.split(",");
List<String> rfidList = new ArrayList<>(Arrays.asList(rfids));
//当前状态变化
String nowStatus = castData(rfidList);
//当前rfid --主要为了获取状态0->1时,上一个状态的rfid
String nowRfids = getRfifData(rfidList);
log.info("转换后的数据为:【{}】【{}】", nowStatus, nowRfids);
String statusChange = castAndOperation(nowStatus);
log.info("状态变化为:【{}】", statusChange);
if (medication.matcher(statusChange).find()) {
log.info("用药场景");
} else if(nurseChange.matcher(statusChange).find()){
log.info("护士状态变化");
} else{
log.info("未触发场景");
}
historyStatus.add(nowStatus);
historyRfid.add(nowRfids);
//清除数据-->内存中保存过多数据会导致APP闪退
clearCacheRfidData();
return "OK";
}
private String castData(List<String> rfidList) {
//...
}
/**
* 详细代码参考上面代码
*/
private String getRfifData(List<String> rfidList) {
//...
}
/**
* 将当前状态和上一状态做比对,得到状态变化字符串
*
* @param nowStatus
* @return
*/
private String castAndOperation(String nowStatus) {
//...
}
/**
* 将上面需要获取状态变化的数据抽取出来
* 可以根据所需数据的集合获取String中有的rfid
* 数据形如:1001,1002-NULL-3001,3002-NULL-NULL
*
* @param rfidString
* @param typeSet
* @return
*/
private List<String> getRfidDataFromString(String rfidString, Set<String> typeSet) {
List<String> rfidDataList = new ArrayList<>();
List<String> rfidList = Arrays.asList(rfidString.split("-"));
for (String type : typeSet) {
int typeIndex = index.indexOf(type);
List<String> rfids = Arrays.asList(rfidList.get(typeIndex).split(","));
for (String rfid : rfids) {
rfidDataList.add(rfid);
}
}
return rfidDataList;
}
private List<String> getStatusUnchangeRfidData(List<String> rfidList, Set<String> typeSet) {
//...
}
private List<String> getStatusChangeRfidData(List<String> rfidList, String statusChange, Set<String> typeSet) {
//...
}
/**
* 清除存储的缓存数据,保留设定的记录数量
*/
private void clearCacheRfidData() {
if (historyStatus.size() > 200 && historyStatus.size() == historyRfid.size()) {
while (historyStatus.size() > cacheCount) {
historyStatus.remove(0);
historyRfid.remove(0);
}
log.info("清除后的状态数据:【{},{}】", historyStatus.size(), historyStatus);
log.info("清除后的rfid数据:【{},{}】", historyRfid.size(), historyRfid);
}
//两个数据不一致 需要调整
if (historyStatus.size() > 200 && historyStatus.size() != historyRfid.size()) {
int remainCount = cacheCount;
for (int i = 1; i <= cacheCount; i++) {
String status = historyStatus.get(historyStatus.size() - i);
String rfid = historyRfid.get(historyRfid.size() - i);
if (!judgeStatusAndRfid(status, rfid)) {
//此时数据异常,清空前序的所有数据
remainCount = i - 1;
break;
}
}
//只保留正确的数据或者最新的60条无误的数据
while (historyStatus.size() > remainCount) {
historyStatus.remove(0);
}
while (historyRfid.size() > remainCount) {
historyRfid.remove(0);
}
}
}
private boolean judgeStatusAndRfid(String status, String rfid) {
List<String> rfidList = Arrays.asList(rfid.split("-"));
for (int j = 0; j < 5; j++) {
//状态为0--存在,需要判断对应的rfid是否存在
//状态为1--不存在,需要判断对应的rfid是否为NULL
if ((status.charAt(j) - '0' == 0 && "NULL".equals(rfidList.get(j)))
|| (status.charAt(j) - '0' == 1 && !"NULL".equals(rfidList.get(j)))) {
return false;
}
}
return true;
}
@Test
public void testPost(){
processDataChange("N-10001");
processDataChange("NULL");
processDataChange("N-10001,P-20001,M-30001");
}
}
最后clearCacheRfidData()函数提供清除缓存的服务,因为如果缓存过多的历史数据,会占用虚拟机的过多内存导致内存溢出,因此我们会将数据进行清除,clearCacheRfidData还提供一个数据纠对的功能,如果historyStatus、historyRfid两个缓存队列中的数据数量不一致,还会将数据进行校对,只保留对的数据,清除异常的数据。
总结
走过路过,如果有用,留下一个赞呗^_^----------。