背景
企业微信SaaS产品,每个企业可以自己配置敏感词库。员工在跟客户聊天时,如果发送的消息包含敏感词,系统会拦截或者记录下来。
需求其实很明确:
-
支持多租户(每个企业有自己的词库)
-
匹配要快(不能影响聊天体验)
-
更新要及时(修改敏感词后尽快生效)
-
不能丢日志(谁发了什么敏感词都要记录)
核心技术选型
一开始我以为他们会用什么Lucene或者ES做全文检索,结果看了代码发现,就用了个Hutool的WordTree,加上Redis版本控制。
简单,但有效。
DFA算法到底是什么?
DFA(Deterministic Finite Automaton)其实就是个状态机。想象一下,把所有敏感词构建成一棵树:
根
/ | \
赌 毒 诈
/ | \
博 品 骗
匹配的时候,从根节点开始,一个字一个字往下走。走到叶子节点,说明匹配上了。
相比正则或者逐个contains(),这种方式的优势是:文本只需要遍历一次,不管有多少个敏感词。
Hutool的WordTree就是这种算法的封装,用起来超简单:
java
WordTree tree = new WordTree();
tree.addWords("敏感词1", "2", "3");
List<String> result = tree.matchAll("今天有人邀我敏感词1");
// 输出: [敏感词1]
系统架构
三层缓存设计?不,是两层
项目里用了「本地缓存+Redis版本控制」的设计,挺巧妙的。
一开始我以为会用Redis存整个词库,但那样每次匹配都要查Redis,太慢了。
实际的做法是:
-
本地Map 存每个企业的
WordTree(热点数据,极速访问) -
Redis 只存一个版本号(用来判断本地缓存是否过期)
java
// 本地缓存
private static final Map<Long, SensitiveWordVo> SENSITIVE_CACHE = new HashMap<>();
// 获取当前版本
String versionKey = RedisEnum.SENSITIVE_WORD_VERSION_.name() + customId;
Long version = Long.parseLong(redisHelper.get(versionKey));
// 版本比对
SensitiveWordVo cached = SENSITIVE_CACHE.get(customId);
if (cached == null || !Objects.equals(cached.getVersion(), version)) {
// 过期了,重新从数据库加载
cached = init(customId, version);
}
多租户怎么搞?
很简单,就是在所有Key上加上customId:
-
本地缓存Key:
customId -
Redis版本Key:
SENSITIVE_WORD_VERSION_:{customId} -
数据库查询条件:
customId = xxx
这样每个企业的数据完全隔离,互不影响。
完整流程走一遍
发送消息时的拦截过程
-
员工发送消息 → 消息到了
HookChatService -
提取文本内容 → 如果是文本/混合文本,提取内容
-
校验敏感词 → 调用
CommonService.checkSensitiveWord() -
匹配词库 →
SensitiveWordUtil.getFoundAllSensitive() -
判断是否拦截 → 看规则里的
isIntercept字段 -
记录日志 → 存到
robot_sensitive_rule_log表 -
返回结果 → 拦截的话给前端提示
修改敏感词后的刷新过程
管理员在后台加了个敏感词:
-
保存数据库 →
RobotSensitiveRuleService.saveModel() -
更新版本号 →
redisHelper.incr(),版本号+1 -
等待下次使用 → 下次匹配时发现版本变了,重新加载
这里用的是懒加载,不是推送,简单但有效。
代码里值得借鉴的细节
1. 规则与匹配分离
每个敏感词规则是一条记录,包含多个关键词:
java
public class RobotSensitiveRule {
private String ruleTitle; // 规则名称,比如"政治敏感"
private String ruleKeys; // "铭感词1|敏感词2|敏感词3"(用|分隔)
private Integer isIntercept; // 1=拦截,0=只记录
private Integer status; // 1=生效
}
然后每个规则对应一个WordTree,这样日志里能知道是触发了哪条规则。
2. 敏感词和违规词是两套
看代码时发现有ruleType字段:
-
1= 敏感词(可以配置是否拦截) -
2= 违规词(强制拦截,还支持包含匹配)
一开始觉得没必要,后来想了想,这是业务需求的差异:
-
敏感词:比如内部机密,可能只记录不拦截
-
违规词:比如赌博,一定不能发出去
3. 日志里的高亮显示
记录日志时,他们会把敏感词用HTML标签包起来:
java
String highlighted = content;
for (String word : matchedWords) {
highlighted = highlighted.replace(word,
"<font color='red'>" + word + "</font>");
}
log.setSendMsg(highlighted);
这样在后台看日志时,敏感词是红色的,一目了然。
总结
这套敏感词过滤系统,技术上并不复杂,但每一个设计决策都有它的道理:
-
DFA算法 → 匹配快
-
双层缓存 → 性能与一致性的平衡
-
多租户隔离 → 简单直接的方案
-
规则灵活配置 → 满足不同业务需求
很多时候,好的架构不是用了多么牛的技术,而是用最简单的方案解决了问题。
希望这篇文章对你有帮助。如果觉得有用,欢迎分享~