ai写作辅助检测

This commit is contained in:
wyn
2026-07-15 10:55:08 +08:00
parent c80ff9b99f
commit a46df66028
4 changed files with 695 additions and 0 deletions

View File

@@ -0,0 +1,176 @@
<?php
namespace app\common;
use think\Db;
/**
* AI 写作模版句规则匹配t_ai_template_sentence
*/
class AiTemplateSentenceService
{
/** @var array<int,array>|null */
private static $ruleCache;
/**
* 按稿件分节统计模版句出现次数
*
* @param array<string,string> $sectionTexts 键abstract/introduction/methods/results/discussion/conclusion
* @return array{total_matches:int,matched_rule_count:int,items:array,by_section:array}
*/
public function countInManuscript(array $sectionTexts): array
{
$rules = $this->loadActiveRules();
if (empty($rules)) {
return [
'total_matches' => 0,
'matched_rule_count' => 0,
'items' => [],
'by_section' => [],
];
}
$normalizedSections = [];
foreach ($sectionTexts as $key => $text) {
$sectionKey = strtolower(trim((string) $key));
if ($sectionKey === '') {
continue;
}
$normalizedSections[$sectionKey] = $this->normalizeText((string) $text);
}
$allText = $this->normalizeText(implode("\n\n", array_filter($normalizedSections)));
$items = [];
$bySection = [];
foreach ($rules as $rule) {
$ruleSection = strtolower(trim((string) ($rule['section'] ?? '')));
$pattern = trim((string) ($rule['sentence_pattern'] ?? ''));
if ($pattern === '') {
continue;
}
$targetText = '';
if ($ruleSection === 'all') {
$targetText = $allText;
} elseif (isset($normalizedSections[$ruleSection])) {
$targetText = $normalizedSections[$ruleSection];
} else {
continue;
}
if ($targetText === '') {
continue;
}
$count = $this->countPatternMatches($targetText, $pattern);
if ($count <= 0) {
continue;
}
$item = [
'id' => intval($rule['id'] ?? 0),
'section' => $ruleSection,
'sentence_pattern' => $pattern,
'count' => $count,
'risk_level' => intval($rule['risk_level'] ?? 0),
'weight' => intval($rule['weight'] ?? 0),
'remark' => (string) ($rule['remark'] ?? ''),
];
$items[] = $item;
if (!isset($bySection[$ruleSection])) {
$bySection[$ruleSection] = [
'total_matches' => 0,
'matched_rule_count' => 0,
'items' => [],
];
}
$bySection[$ruleSection]['items'][] = $item;
$bySection[$ruleSection]['total_matches'] += $count;
$bySection[$ruleSection]['matched_rule_count']++;
}
usort($items, function ($a, $b) {
if ($a['count'] !== $b['count']) {
return $b['count'] - $a['count'];
}
if ($a['weight'] !== $b['weight']) {
return $b['weight'] - $a['weight'];
}
return $a['id'] - $b['id'];
});
foreach ($bySection as &$sectionStats) {
usort($sectionStats['items'], function ($a, $b) {
if ($a['count'] !== $b['count']) {
return $b['count'] - $a['count'];
}
return $b['weight'] - $a['weight'];
});
}
unset($sectionStats);
$totalMatches = 0;
foreach ($items as $item) {
$totalMatches += intval($item['count']);
}
return [
'total_matches' => $totalMatches,
'matched_rule_count' => count($items),
'items' => $items,
'by_section' => $bySection,
];
}
/**
* @return array<int,array>
*/
private function loadActiveRules(): array
{
if (self::$ruleCache !== null) {
return self::$ruleCache;
}
try {
self::$ruleCache = Db::name('ai_template_sentence')
->where('status', 1)
->field('id,sentence_pattern,section,language,risk_level,weight,remark,article_type,version')
->order('section asc,id asc')
->select();
} catch (\Throwable $e) {
self::$ruleCache = [];
}
return self::$ruleCache;
}
private function normalizeText(string $text): string
{
return ManuscriptTextCleanService::clean($text);
}
private function countPatternMatches(string $text, string $pattern): int
{
if ($text === '' || $pattern === '') {
return 0;
}
$regex = '#' . $pattern . '#iu';
$error = null;
set_error_handler(function ($errno, $errstr) use (&$error) {
$error = $errstr;
return true;
});
$count = preg_match_all($regex, $text, $matches);
restore_error_handler();
if ($count === false || $error !== null) {
return 0;
}
return intval($count);
}
}