This commit is contained in:
root
2026-10-16 13:51:00 +08:00
parent 6bf949949c
commit 8fc899d0a9
3830 changed files with 11228 additions and 188377 deletions
@@ -0,0 +1,83 @@
"""敏感词服务:DFA 算法,词库 Redis 缓存 60s(后台维护后即时生效,对齐《02-技术文档》6.7)"""
import json
import time
import redis.asyncio as aioredis
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.common import SensitiveWord
CACHE_KEY = "words:sensitive"
CACHE_TTL = 60
class DFAMatcher:
"""确定性有穷自动机敏感词匹配器"""
def __init__(self) -> None:
self.root: dict = {}
def add(self, word: str) -> None:
node = self.root
for ch in word.strip().lower():
if not ch:
continue
node = node.setdefault(ch, {})
node["\x00"] = True # 词结束标记
def find(self, text: str) -> str | None:
"""返回命中的第一个敏感词,未命中返回 None"""
text = text.lower()
n = len(text)
for i in range(n):
node = self.root
j = i
while j < n and text[j] in node:
node = node[text[j]]
j += 1
if "\x00" in node:
return text[i:j]
return None
# 进程内缓存(matcher, 构建时间戳)
_local_cache: tuple[DFAMatcher, float] | None = None
async def get_matcher(db: AsyncSession, redis: aioredis.Redis) -> DFAMatcher:
"""获取 DFA 匹配器:进程缓存 → Redis → DB,60s 重建"""
global _local_cache
now = time.time()
if _local_cache and now - _local_cache[1] < CACHE_TTL:
return _local_cache[0]
words: list[str] = []
cached = await redis.get(CACHE_KEY)
if cached:
words = json.loads(cached)
else:
rows = (await db.execute(select(SensitiveWord.word))).scalars().all()
words = [w for w in rows if w]
await redis.set(CACHE_KEY, json.dumps(words, ensure_ascii=False), ex=CACHE_TTL)
matcher = DFAMatcher()
for w in words:
matcher.add(w)
_local_cache = (matcher, now)
return matcher
async def find_sensitive(db: AsyncSession, redis: aioredis.Redis, text: str) -> str | None:
"""检查文本,返回命中的敏感词或 None"""
if not text:
return None
matcher = await get_matcher(db, redis)
return matcher.find(text)
async def invalidate_cache(redis: aioredis.Redis) -> None:
"""后台增删词库后调用:清 Redis 缓存,进程缓存等 60s 内自然过期"""
global _local_cache
_local_cache = None
await redis.delete(CACHE_KEY)