短信代码分析
package com.fairyalliance.smartanswer; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.provider.Telephony; import android.util.Log; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.text.SimpleDateFormat; import java.util.Locale; import CyberWinPHP.Cyber_CPU.Cyber_Public_Var; import java.util.regex.Matcher; // ===================== 规则实体【修正,增加level】===================== class SmsRuleItem { public String data_id; //规则id public List<String> numberRegexList; public List<String> bodyRegexList; /** 预警紧急等级 */ public int level; //2026-06-08-08 public List<String> Extractreglist; } // ===================== 短信输出记录实体 ===================== class SmsRecordItem { public String smsAddress; public String smsBody; public long smsDateTs; public int level; /** 预警紧急等级 */ public String rule_id; //规则id } /** * 独立静态短信规则&短信读取工具模块 * 【注意】本类仅做纯数据处理,不启动后台、不注册广播;上层(Activity/Service)负责调用、权限申请、网络请求、后台保活 */ public final class CyberWinEnterpriseAutoSmsRuleHelper { private static final String TAG = "cwSmsRuleHelper"; public static final Gson GSON = new Gson(); // ========== 全局静态变量:内存中保存服务器下发全部规则 ========== public static List<SmsRuleItem> GLOBAL_SMS_RULES = new ArrayList<>(); private CyberWinEnterpriseAutoSmsRuleHelper() { // 禁止实例化,纯静态类 } /** * 函数1:从服务器下载并加载规则到全局静态变量 GLOBAL_SMS_RULES * @param localHttpApi 接口地址 */ public static void loadRuleFromServer(String localHttpApi) { new Thread(() -> { try { //规整本地标准时间 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); String nowTime = dateFormat.format(System.currentTimeMillis()); //替换填写你本地电脑内网固定IP+你搭建好的接口路径,例http://192.168.1.105:8080/callnotify //原生GET请求携带来电号码+时间两个参数推送到本地服务 URL url = new URL(localHttpApi +"&client_time="+nowTime+"&mer_id=77&store_id=72&eco_type=phonecall"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(3000); int code = conn.getResponseCode(); writelog("本地推送","下载规则","时间"+nowTime+"返回状态码"+code); String responseBody = ""; InputStream is = null; try { if(code >= 200 && code < 300){ is = conn.getInputStream(); }else{ is = conn.getErrorStream(); // 错误状态码拿错误流 } // 流转字符串 BufferedReader br = new BufferedReader(new InputStreamReader(is,"UTF-8")); StringBuilder sb = new StringBuilder(); String line; while ((line = br.readLine()) != null) { sb.append(line); } responseBody = sb.toString(); br.close(); writelog("本地推送","下载规则","时间"+nowTime+"返回数据:"+responseBody); loadRuleFromServerJson(responseBody); } finally { conn.disconnect(); } } catch (Exception e) { writelog("本地推送","失败下载规则","推送本地服务异常:"+e.getMessage()); } }).start(); } /** * 函数1:从服务器下载并加载规则到全局静态变量 GLOBAL_SMS_RULES * @param jsonStr 服务器返回完整JSON字符串 * @return true解析成功;false解析失败 */ public static boolean loadRuleFromServerJson(String jsonStr) { if (jsonStr == null || jsonStr.trim().isEmpty()) { Log.e(TAG, "loadRuleFromServerJson: json为空"); writelog("本地推送","loadRule","数据json为空:"+jsonStr); return false; } try { Type listType = new TypeToken<List<SmsRuleItem>>() {}.getType(); List<SmsRuleItem> tempList = GSON.fromJson(jsonStr, listType); if(tempList == null){ return false; } // 赋值给全局静态变量 GLOBAL_SMS_RULES.clear(); GLOBAL_SMS_RULES.addAll(tempList); writelog("本地推送","loadRule","共 " + GLOBAL_SMS_RULES.size() + " 条规则"); // Log.i(TAG, "加载规则完成,共 " + GLOBAL_SMS_RULES.size() + " 条规则"); return true; } catch (Exception e) { // Log.e(TAG, "解析规则JSON异常", e); writelog("本地推送","loadRule","解析规则JSON异常 " + e.getMessage() + " "); return false; } } /** * 函数2:读取短信,按【时间区间+最大读取条数】读取,倒序(最新短信优先) * * @param context 上下文,需要READ_SMS权限 * @param maxReadCount 最多读取多少条,例如300;传<=0代表不限制条数 * @param startTimeTs 开始时间戳(毫秒);传null:不限制开始,从现在往前读 * @param endTimeTs 结束时间戳(毫秒);传null:不限制结束 * @return 原始读取到的短信列表(未过滤,原始全部短信,rule_id/level字段为空) */ public static List<SmsRecordItem> readSmsList(Context context, int maxReadCount, Long startTimeTs, Long endTimeTs) { List<SmsRecordItem> result = new ArrayList<>(); ContentResolver resolver = context.getContentResolver(); Uri smsUri = Telephony.Sms.CONTENT_URI; // 查询字段 String[] projection = { Telephony.Sms.ADDRESS, Telephony.Sms.BODY, Telephony.Sms.DATE }; // where条件拼装时间过滤 StringBuilder whereSb = new StringBuilder(); List<String> whereArgsList = new ArrayList<>(); if(startTimeTs != null){ whereSb.append(Telephony.Sms.DATE).append(" >= ? "); whereArgsList.add(String.valueOf(startTimeTs)); } if(endTimeTs != null){ if(whereSb.length()>0){ whereSb.append(" AND "); } whereSb.append(Telephony.Sms.DATE).append(" <= ? "); whereArgsList.add(String.valueOf(endTimeTs)); } String selection = whereSb.length()>0 ? whereSb.toString() : null; String[] selectionArgs = whereArgsList.isEmpty() ? null : whereArgsList.toArray(new String[0]); // ORDER BY date DESC:最新短信排在最前面 String sortOrder = Telephony.Sms.DATE + " DESC"; Cursor cursor = null; try { cursor = resolver.query(smsUri, projection, selection, selectionArgs, sortOrder); if(cursor == null){ return result; } int idxAddr = cursor.getColumnIndex(Telephony.Sms.ADDRESS); int idxBody = cursor.getColumnIndex(Telephony.Sms.BODY); int idxDate = cursor.getColumnIndex(Telephony.Sms.DATE); while (cursor.moveToNext()){ // 达到最大读取条数直接退出循环 if(maxReadCount > 0 && result.size() >= maxReadCount){ break; } SmsRecordItem item = new SmsRecordItem(); item.smsAddress = cursor.getString(idxAddr); item.smsBody = cursor.getString(idxBody); item.smsDateTs = cursor.getLong(idxDate); //原始读取,level、rule_id为空,匹配之后才填充 item.level = 0; item.rule_id = null; result.add(item); } } catch (Exception e) { // Log.e(TAG,"读取短信数据库异常",e); writelog("本地推送","readSmsList","读取短信数据库异常 " + e.getMessage() + " "); } finally { if(cursor != null){ cursor.close(); } } return result; } /** * 附加工具方法:使用【全局GLOBAL_SMS_RULES】对短信做正则匹配过滤 * 输入原始短信列表,输出命中规则的完整SmsRecordItem(回填rule_id、level) * 一条短信命中多条规则,返回多条记录 * @param rawSmsList readSmsList返回原始短信 * @return 命中结果列表,每条携带rule_id、level,可直接Gson序列化上传服务器 */ public static List<SmsRecordItem> filterSmsByGlobalRule(List<SmsRecordItem> rawSmsList){ List<SmsRecordItem> out = new ArrayList<>(); if(rawSmsList == null || rawSmsList.isEmpty()){ // writelog("本地推送","过滤","短信为空 "); return out; } // writelog("本地推送","过滤","短信有数据 "); for(SmsRecordItem rawSms : rawSmsList){ //writelog("本地推送","过滤","号码: "+rawSms.smsAddress); int ruleCount = 0; //匹配成功的正则数量 for(SmsRuleItem rule : GLOBAL_SMS_RULES){ // writelog("本地推送","s规则","number: "+.GSON.toJson(rule.numberRegexList)); // writelog("本地推送","s规则","body: "+GSON.toJson(rawSms.bodyRegexList)); //规则校验:两个列表同时为空,直接跳过该规则 boolean numListEmpty = (rule.numberRegexList == null || rule.numberRegexList.isEmpty()); boolean bodyListEmpty = (rule.bodyRegexList == null || rule.bodyRegexList.isEmpty()); ruleCount = ruleCount +1; writelog("本地推送","s2规则","判断:"+bodyListEmpty+",原始: "+GSON.toJson(rule.bodyRegexList)+"numListEmpty="+numListEmpty+","+GSON.toJson(rule.numberRegexList)); String res判断="规则:"+ruleCount+"条 "; if(numListEmpty && bodyListEmpty){ res判断 = res判断+ "同时为空"; } if(numListEmpty && bodyListEmpty){ // writelog("本地推送","s规则","无规则 "); continue; } boolean matchNumber = false; if(!numListEmpty){ // matchNumber = matchAnyRegexOne(rawSms.smsAddress, rule.numberRegexList); matchNumber = matchAnyRegexALL(rawSms.smsAddress, rule.numberRegexList); //matchAnyRegexALL } boolean matchBody = false; if(!bodyListEmpty){ matchBody = matchAnyRegexALL(rawSms.smsBody, rule.bodyRegexList); } //核心逻辑:(号码非空且命中) OR (正文非空且命中) boolean ruleHit = false; if(!numListEmpty && matchNumber){ ruleHit = true; } if(!bodyListEmpty && matchBody){ ruleHit = true; } //2026-08-08 同时满足 if(!bodyListEmpty && !numListEmpty){ res判断 = res判断+ "同时非空ruleHit="+ruleHit; //非空 要求全部满足 if(matchNumber && matchBody){ ruleHit = true; }else{ ruleHit = false; } res判断 = res判断+ ",同时非空,后:ruleHit="+ruleHit; } writelog("本地推送","s2规则",res判断); if(ruleHit){ //复制一份对象,回填规则id与等级,不要修改原始对象 SmsRecordItem hitItem = new SmsRecordItem(); hitItem.smsAddress = rawSms.smsAddress; hitItem.smsBody = rawSms.smsBody; hitItem.smsDateTs = rawSms.smsDateTs; hitItem.rule_id = rule.data_id; hitItem.level = rule.level; out.add(hitItem); //❗不break,允许一条短信命中多条不同规则 } } } return out; } /** * 工具:判断文本是否匹配列表中**任意一条正则** */ private static boolean matchAnyRegexOne(String text, List<String> regexList){ if(text == null || regexList == null || regexList.isEmpty()){ return false; } for(String reg : regexList){ try{ // 仅当首尾都是斜杠,才剥离PHP定界符;干净正则原样使用 String realReg = reg; if (realReg.startsWith("/") && realReg.endsWith("/")) { realReg = realReg.substring(1, realReg.length() - 1); } if(Pattern.compile(realReg).matcher(text).find()){ return true; } }catch (Exception e){ // Log.w(TAG,"正则表达式错误:"+reg,e); writelog("本地推送","matchAnyRegexOne","正则表达式错误 " +reg + " "); } } return false; } /** * 且逻辑:所有【非空】正则项全部匹配成功才返回true * 空字符串直接跳过,不计入有效数量;正则异常直接返回false */ private static boolean matchAnyRegexALL(String text, List<String> regexList){ if(text == null || regexList == null || regexList.isEmpty()){ return false; } int validCount = 0; //有效、非空的正则数量 int hitCount = 0; //匹配成功的正则数量 for(String reg : regexList){ //过滤空字符串,直接跳过,不参与运算 if(reg == null || reg.trim().length() == 0){ continue; } validCount ++; try{ String realReg = reg; //兼容PHP旧的首尾定界符/ if (realReg.startsWith("/") && realReg.endsWith("/")) { realReg = realReg.substring(1, realReg.length() - 1); } boolean hit = Pattern.compile(realReg).matcher(text).find(); if(hit){ hitCount++; }else{ //任意一条有效正则匹配失败,直接整体失败 return false; } }catch (Exception e){ writelog("本地推送","matchAnyRegexALL","正则表达式错误 " + reg + " "); //正则编译异常,直接判定不通过 return false; } } //必须:存在有效正则,并且成功数等于全部有效正则 return validCount > 0 && hitCount == validCount; } //=================== 日志占位,你原有writelog保留 =================== // private static void writelog(String tag1, String tag2, String msg){ // Log.d("writelog", tag1+"|"+tag2+"|"+msg); //} private static void writelog(String type, String name, String msg) { try { CyberWinLogToFile.init( Cyber_Public_Var.cyber_main_instance); // 1. 时间格式化:yyyy-MM-dd HH:mm:ss // SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); // String time = sdf.format(new Date()); // 2. 日志内容 // String logContent = time + " | " + type + " | " + name + " | " + msg + "\n"; String logContent = type + " | " + name + " | " + msg + "\n"; CyberWinLogToFile.d_windows(type,name,logContent); } catch (Exception e) { // 不处理,避免崩溃 } } }一、整体架构设计优点
- 职责边界清晰,单一职责原则该类为final 纯静态工具类,构造函数私有禁止实例化;只负责短信读取、规则解析、正则匹配数据处理。不做权限申请、不注册广播、不启动 Service 保活,把平台相关操作全部交给上层调用方,解耦做得很好,模块可复用,更换上层业务组件无需改动本工具代码。
- 实体类拆分合理单独定义
SmsRuleItem规则实体、SmsRecordItem短信记录实体;规则实体扩展level预警等级、Extractreglist提取正则,业务字段和数据模型一一对应,JSON 序列化反序列化直接适配,方便后续上传上报。 - 全局内存缓存规则使用静态集合
GLOBAL_SMS_RULES内存缓存服务器下发规则,不需要每次匹配都走网络 / 磁盘 IO;上层只需要调用一次下载接口,后续大量短信匹配直接读取内存,性能高。
二、网络下载规则模块优点
- 网络请求隔离子线程
loadRuleFromServer内部新建 Thread 执行 GET 网络请求,严格避免主线程网络异常崩溃,符合 Android 网络开发规范。 - 异常流兼容处理HTTP 状态码非 2xx 时,不取
getInputStream(),改用getErrorStream()读取服务端错误返回,不会直接抛出流关闭异常,可以拿到服务端报错内容用于排查。 - 入参校验完备
loadRuleFromServerJson对空 JSON、空白字符串做前置拦截;Gson 解析做异常捕获,解析失败返回布尔值给上层,上层可以做重试逻辑。 - 携带业务参数请求接口附带客户端时间、商户 ID、门店 ID、业务类型,方便服务端做日志排查、多租户区分。
三、短信数据库读取模块优点
- 时间区间过滤查询动态拼装 SQLite where 条件,支持起止时间戳筛选短信,不用全量读取全部短信库,减少内存占用;支持最大读取条数限制,防止一次性加载上千条短信造成 OOM 内存溢出。
- 倒序查询
sortOrder = date DESC,优先拿到最新收到的短信,符合业务预警场景。 - Cursor 健壮处理判空 cursor,finally 块强制关闭游标,杜绝 SQLite 游标泄漏;读取列优先用
getColumnIndex()获取下标,不硬编码索引,系统字段变更也不会数组越界。 - 原始数据隔离读取出来的
SmsRecordItem只填充短信原始信息,level、rule_id 初始置空;匹配规则之后复制新对象回填,不会污染原始短信数据。
四、正则匹配核心逻辑优点
- 两套正则匹配函数区分语义
matchAnyRegexOne:或逻辑,列表中任意一条正则命中即成功matchAnyRegexALL:且逻辑,列表所有有效正则必须全部命中才返回 true 两个方法职责完全分开,适配两种业务规则场景。
- 兼容 PHP 正则定界符自动识别并剥离正则首尾
/符号,服务端 PHP 直接下发原生正则字符串,不用服务端额外做格式转换,前后端对接更方便。 - 过滤空正则
matchAnyRegexALL自动跳过集合内空字符串正则,空项不参与运算,避免配置脏数据导致匹配失效。 - 异常防护正则编译失败捕获异常,不会直接崩溃;正则出错直接返回 false,跳过这条规则。
- 规则逻辑完备
- 号码列表、正文列表同时为空的规则直接跳过,过滤无效脏规则
- 号码、正文列表其中一个为空,只校验非空那一侧;两者同时非空时,强制要求号码 + 正文全部命中才算规则命中,业务逻辑可控。
- 支持一条短信命中多条规则命中后不 break 循环,复制新对象输出多条记录,不会出现只匹配第一条规则丢失预警的问题。
五、日志与稳定性优点
- 全链路埋点日志网络下载、JSON 解析、短信读取、正则匹配各个分支都写入日志,便于线上排查:规则有没有下载成功、多少条规则、正则报错、读取短信异常都可以追溯。
- 日志写入做 try‑catch 保护写入文件日志全部包裹异常捕获,就算日志组件初始化失败,不会打断业务主流程,不会造成 App 崩溃。
- 大量异常捕获网络、数据库、Gson 解析、正则编译全部捕获 Exception,工具类内部吞异常输出日志,把错误交给上层日志,工具本身不会导致应用闪退。
六、扩展性优点
- 规则实体预留
Extractreglist提取正则字段,后续可以直接扩展短信内容提取逻辑,不需要大改现有代码。 - 预警 level 等级字段已经落地,上层可以根据等级做分级告警,区分普通、紧急预警。
- 返回实体直接支持 Gson 序列化,拿到
filterSmsByGlobalRule输出集合,直接 toJson 即可上传给服务器。 - 所有对外方法都是 public static,上层 Service/Activity 都可以直接调用,接入成本低。
人人皆为创造者,共创方能共成长
每个人都是使用者,也是创造者;是数字世界的消费者,更是价值的生产者与分享者。在智能时代的浪潮里,单打独斗的发展模式早已落幕,唯有开放连接、创意共创、利益共享,才能让个体价值汇聚成生态合力,让技术与创意双向奔赴,实现平台与伙伴的快速成长、共赢致远。
原创永久分成,共赴星辰大海
原创创意共创、永久收益分成,是东方仙盟始终坚守的核心理念。我们坚信,每一份原创智慧都值得被尊重与回馈,以永久分成锚定共创初心,让创意者长期享有价值红利,携手万千伙伴向着科技星辰大海笃定前行,拥抱硅基 生命与数字智能交融的未来,共筑跨越时代的数字文明共同体。
东方仙盟:拥抱知识开源,共筑数字新生态
在全球化与数字化浪潮中,东方仙盟始终秉持开放协作、知识共享的理念,积极拥抱开源技术与开放标准。我们相信,唯有打破技术壁垒、汇聚全球智慧,才能真正推动行业的可持续发展。
开源赋能中小商户:通过将前端异常检测、跨系统数据互联等核心能力开源化,东方仙盟为全球中小商户提供了低成本、高可靠的技术解决方案,让更多商家能够平等享受数字转型的红利。
共建行业标准:我们积极参与国际技术社区,与全球开发者、合作伙伴共同制定开放协议 与技术规范,推动跨境零售、文旅、餐饮等多业态的系统互联互通,构建更加公平、高效的数字生态。
知识普惠,共促发展:通过开源社区 、技术文档与培训体系,东方仙盟致力于将前沿技术转化为可落地的行业实践,赋能全球合作伙伴,共同培育创新人才,推动数字经济 的普惠式增长
阿雪技术观
在科技发展浪潮中,我们不妨积极投身技术共享。不满足于做受益者,更要主动担当贡献者 。无论是分享代码、撰写技术博客,还是参与开源项目 维护改进,每一个微小举动都可能蕴含推动技术进步的巨大能量。东方仙盟是汇聚力量的天地,我们携手在此探索硅基 生命,为科技进步添砖加瓦。
Hey folks, in this wild tech - driven world, why not dive headfirst into the whole tech - sharing scene? Don't just be the one reaping all the benefits; step up and be a contributor too. Whether you're tossing out your code snippets , hammering out some tech blogs, or getting your hands dirty with maintaining and sprucing up open - source projects, every little thing you do might just end up being a massive force that pushes tech forward. And guess what? The Eastern FairyAlliance is this awesome place where we all come together. We're gonna team up and explore the whole silicon - based life thing, and in the process, we'll be fueling the growth of technology