Files
YwxAppThink/addon/wxchat/service/SafetyService.php
T

118 lines
3.5 KiB
PHP

<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace addon\wxchat\service;
/**
* 内容安全服务:敏感词过滤 + 风险评分
* 敏感词存放于 wxapp_wxchat_banwords,缺失时使用内置兜底词库
*/
class SafetyService
{
private static $instance = null;
/** @var array|null [['word'=>, 'level'=>, 'replace'=>], ...] */
private $words = null;
public static function instance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 加载敏感词(带缓存 + 兜底)
*/
private function loadWords(): array
{
if ($this->words !== null) {
return $this->words;
}
$this->words = [];
try {
$rows = \think\facade\Db::name('wxchat_banwords')
->where('status', 1)
->order('level', 'desc')
->select();
foreach ($rows as $r) {
$this->words[] = [
'word' => $r['word'],
'level' => (int)$r['level'],
'replace' => $r['replace_char'] ?: '**',
];
}
} catch (\Throwable $e) {
// 表可能未创建,忽略,使用兜底词库
}
// 兜底词库
foreach (['fuck', 'shit', '赌博', '色情', '诈骗', '代开发票'] as $w) {
$this->words[] = ['word' => $w, 'level' => 1, 'replace' => '**'];
}
return $this->words;
}
/**
* 过滤敏感词(level=1 替换为 **;level=2 命中则整段清空并返回命中标记)
* @return string 过滤后的文本
*/
public function filter(string $text): string
{
if ($text === '') {
return $text;
}
foreach ($this->loadWords() as $w) {
if ($w['level'] >= 2) {
if (mb_stripos($text, $w['word']) !== false) {
// 高危词:整段清空(拦截)
return '';
}
} else {
$text = $this->mbStrReplace($w['word'], $w['replace'], $text);
}
}
return $text;
}
/**
* 内容安全检测:返回是否命中高危词
*/
public function check(string $text): array
{
$hit = [];
foreach ($this->loadWords() as $w) {
if (mb_stripos($text, $w['word']) !== false) {
$hit[] = $w['word'];
if ($w['level'] >= 2) {
return ['blocked' => true, 'hit' => $hit];
}
}
}
return ['blocked' => false, 'hit' => $hit];
}
/**
* 多字节安全的字符串替换
*/
private function mbStrReplace($search, $replace, $subject): string
{
if ($search === '') {
return $subject;
}
$parts = mb_split(preg_quote($search), $subject);
if ($parts === false) {
return str_ireplace($search, $replace, $subject);
}
return implode($replace, $parts);
}
}