package cn.iocoder.yudao.module.im.service.sensitiveword; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.cache.CacheUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordPageReqVO; import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordSaveReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO; import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper; import com.github.houbb.sensitive.word.bs.SensitiveWordBs; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import jakarta.annotation.Resource; import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.time.Duration; import java.time.LocalDateTime; import java.util.List; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSITIVE_WORD_BLOCKED; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_DUPLICATED; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS; /** * IM 敏感词 Service 实现类 *
* 词库匹配交给 houbb sensitive-word 库(trie 树 + 全/半角 / 大小写 / 繁简体 / 数字风格规范化) * * @author 芋道源码 */ @Service @Validated @Slf4j public class ImSensitiveWordServiceImpl implements ImSensitiveWordService { @Resource private ImSensitiveWordMapper sensitiveWordMapper; /** * 缓存条目 */ @Data @AllArgsConstructor private static class SensitiveWordBsCache { /** * 敏感词检测器 */ private SensitiveWordBs bs; /** * 构建本实例时数据库里的 max(update_time),作为下次刷新比对的基线 */ private LocalDateTime maxUpdateTime; } /** * 租户 → SensitiveWordBs 实例的本地缓存 *
* 每分钟触发一次异步 reload,先读 max(update_time),没变就复用旧实例(避免 trie 重建),变了才重新读词库 + 重建。
* 单实例 CRUD 后另外通过 {@link #invalidateSensitiveWordBsCaches()} 立即让本机失效,多实例靠定时刷新最长 1 分钟内收敛。
*/
@SuppressWarnings({"Convert2Diamond", "NullableProblems"})
private final LoadingCache
* 有租户上下文:仅失效该租户。无租户上下文(如系统级 / 跨租户清理):兜底失效所有租户。
*/
private void invalidateSensitiveWordBsCaches() {
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId != null) {
sensitiveWordBsCaches.invalidate(tenantId);
return;
}
sensitiveWordBsCaches.invalidateAll();
}
@Override
public void validateText(String text) {
if (StrUtil.isBlank(text)) {
return;
}
SensitiveWordBs bs = sensitiveWordBsCaches.getUnchecked(TenantContextHolder.getRequiredTenantId()).getBs();
if (bs.contains(text)) {
throw exception(MESSAGE_SENSITIVE_WORD_BLOCKED);
}
}
// ==================== 管理后台 ====================
@Override
public PageResult