Files
sbnews/server/app/common/service/article/ArticleAiCommentService.php
T

355 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common\service\article;
use app\common\model\article\Article;
use app\common\model\article\ArticleAiCommentTask;
use app\common\model\article\ArticleComment;
use app\common\model\user\User;
use app\common\service\ai\AiService;
use think\facade\Config;
use think\facade\Db;
class ArticleAiCommentService
{
private const VIRTUAL_ACCOUNT_PREFIX = 'ai_comment_';
private const VIRTUAL_POOL_SIZE = 20;
private const VIRTUAL_NICKNAME_PREFIXES = [
'看台',
'深夜',
'边线',
'北区',
'南看台',
'东门',
'西街',
'慢热',
'小酒馆',
'补时',
'角旗',
'传中',
'临场',
'半场',
'球门后',
'旧球鞋',
'赛点',
'风向',
'追球',
'回防',
];
private const VIRTUAL_NICKNAME_SUFFIXES = [
'阿泽',
'小顾',
'老韩',
'阿凯',
'小唐',
'阿林',
'阿松',
'阿北',
'阿岳',
'阿诚',
];
public static function tick(int $seedLimit = 10, int $processLimit = 20): array
{
$virtualUsers = self::ensureVirtualUsers(self::VIRTUAL_POOL_SIZE);
$seeded = self::seedTasks($virtualUsers, $seedLimit);
$processed = self::processDueTasks($processLimit);
return [
'virtual_user_count' => count($virtualUsers),
'seeded' => $seeded,
'processed' => $processed,
];
}
protected static function seedTasks(array $virtualUsers, int $limit = 10): array
{
if (empty($virtualUsers)) {
return ['created' => 0, 'skipped' => 0];
}
$articles = Article::field('id,title,desc,content,author,ext,click_virtual,click_actual,comment_count,create_time,published_at')
->where('is_show', 1)
->whereNull('delete_time')
->where('create_time', '>=', strtotime('-14 days'))
->orderRaw('comment_count asc, (click_actual + click_virtual) desc, id desc')
->limit($limit)
->select()
->toArray();
$created = 0;
$skipped = 0;
foreach ($articles as $index => $article) {
$articleId = (int) ($article['id'] ?? 0);
if ($articleId <= 0) {
$skipped++;
continue;
}
$existingPending = ArticleAiCommentTask::where('article_id', $articleId)
->whereIn('status', [0, 1])
->count();
if ($existingPending > 0) {
$skipped++;
continue;
}
$desiredCount = self::resolveTaskCount($article);
if ($desiredCount <= 0) {
$skipped++;
continue;
}
for ($i = 0; $i < $desiredCount; $i++) {
$virtualUserId = $virtualUsers[($articleId + $index + $i) % count($virtualUsers)];
$comment = self::generateCommentContent($article, $virtualUserId);
if ($comment === '') {
continue;
}
$runAt = time() + random_int(600, 43200);
ArticleAiCommentTask::create([
'article_id' => $articleId,
'virtual_user_id' => $virtualUserId,
'comment_content' => $comment,
'run_at' => $runAt,
'status' => 0,
'create_time' => time(),
'update_time' => time(),
]);
$created++;
}
}
return compact('created', 'skipped');
}
protected static function processDueTasks(int $limit = 20): array
{
$tasks = ArticleAiCommentTask::where('status', 0)
->where('run_at', '<=', time())
->order('run_at', 'asc')
->limit($limit)
->select()
->toArray();
$done = 0;
$failed = 0;
foreach ($tasks as $task) {
$articleId = (int) ($task['article_id'] ?? 0);
$content = trim((string) ($task['comment_content'] ?? ''));
$virtualUserId = (int) ($task['virtual_user_id'] ?? 0);
if ($articleId <= 0 || $content === '' || $virtualUserId <= 0) {
self::markTaskFailed((int) $task['id'], '任务数据不完整');
$failed++;
continue;
}
$article = Article::where('id', $articleId)
->where('is_show', 1)
->whereNull('delete_time')
->findOrEmpty();
if ($article->isEmpty()) {
self::markTaskFailed((int) $task['id'], '文章不存在或已下架');
$failed++;
continue;
}
Db::startTrans();
try {
ArticleComment::create([
'article_id' => $articleId,
'user_id' => $virtualUserId,
'parent_id' => 0,
'reply_user_id' => 0,
'content' => $content,
'is_show' => 1,
'create_time' => time(),
'update_time' => time(),
]);
Article::where('id', $articleId)->inc('comment_count')->update();
ArticleAiCommentTask::where('id', $task['id'])->update([
'status' => 1,
'update_time' => time(),
]);
Db::commit();
$done++;
} catch (\Throwable $e) {
Db::rollback();
self::markTaskFailed((int) $task['id'], $e->getMessage());
$failed++;
}
}
return compact('done', 'failed');
}
protected static function ensureVirtualUsers(int $targetCount = self::VIRTUAL_POOL_SIZE): array
{
$existingRows = User::where('account', 'like', self::VIRTUAL_ACCOUNT_PREFIX . '%')
->order('id', 'asc')
->field('id,account,nickname')
->select()
->toArray();
$existing = [];
foreach ($existingRows as $row) {
$existing[] = (int) $row['id'];
}
$missing = max(0, $targetCount - count($existing));
$now = time();
foreach ($existingRows as $row) {
$index = self::parseAccountIndex((string) ($row['account'] ?? ''));
if ($index <= 0) {
continue;
}
$expectedNickname = self::buildVirtualNickname($index);
if (($row['nickname'] ?? '') !== $expectedNickname) {
User::update([
'id' => (int) $row['id'],
'nickname' => $expectedNickname,
'update_time' => $now,
]);
}
}
if ($missing > 0) {
$avatar = (string) Config::get('project.default_image.user_avatar', '');
$passwordSalt = (string) Config::get('project.unique_identification', 'sport-era');
for ($i = 1; $i <= $missing; $i++) {
$accountIndex = count($existing) + $i;
$sn = User::createUserSn('9');
$password = create_password(bin2hex(random_bytes(6)), $passwordSalt);
$user = User::create([
'sn' => $sn,
'avatar' => $avatar,
'real_name' => '',
'nickname' => self::buildVirtualNickname($accountIndex),
'account' => self::VIRTUAL_ACCOUNT_PREFIX . str_pad((string) $accountIndex, 4, '0', STR_PAD_LEFT),
'password' => $password,
'mobile' => '',
'sex' => 0,
'channel' => 0,
'is_disable' => 0,
'is_new_user' => 0,
'user_money' => 0,
'total_recharge_amount' => 0,
]);
$existing[] = (int) $user->id;
}
}
return $existing;
}
protected static function buildVirtualNickname(int $index): string
{
$zeroBased = max(0, $index - 1);
$prefix = self::VIRTUAL_NICKNAME_PREFIXES[$zeroBased % count(self::VIRTUAL_NICKNAME_PREFIXES)];
$suffix = self::VIRTUAL_NICKNAME_SUFFIXES[intdiv($zeroBased, count(self::VIRTUAL_NICKNAME_PREFIXES)) % count(self::VIRTUAL_NICKNAME_SUFFIXES)];
return $prefix . $suffix;
}
protected static function parseAccountIndex(string $account): int
{
if (!preg_match('/(\d+)$/', $account, $matches)) {
return 0;
}
return (int) $matches[1];
}
protected static function resolveTaskCount(array $article): int
{
$commentCount = (int) ($article['comment_count'] ?? 0);
$clickCount = (int) ($article['click_actual'] ?? 0) + (int) ($article['click_virtual'] ?? 0);
if ($commentCount >= 20) {
return 0;
}
if ($clickCount >= 500) {
return 2;
}
return 1;
}
protected static function generateCommentContent(array $article, int $virtualUserId): string
{
$title = trim((string) ($article['title'] ?? ''));
$abstract = trim((string) ($article['desc'] ?? ''));
$content = self::normalizeContent((string) ($article['content'] ?? ''));
if ($title === '' && $abstract === '' && $content === '') {
return '';
}
$personaPool = [
'资深球迷',
'战术观察员',
'数据控',
'赛事评论员',
'球迷视角',
];
$persona = $personaPool[$virtualUserId % count($personaPool)];
$result = AiService::generateArticleComment($title, $abstract, $content, $persona);
if (empty($result['success'])) {
return self::fallbackComment($title, $abstract, $content);
}
$comment = trim((string) ($result['content'] ?? ''));
$comment = preg_replace('/^评论[:]\s*/u', '', $comment);
$comment = preg_replace('/^[“"‘’\'\']|[”"‘’\'\']$/u', '', $comment);
$comment = preg_replace('/\s+/u', ' ', $comment);
$comment = trim($comment, " \t\n\r\0\x0B,。,.!?;:");
if ($comment === '' || mb_strlen($comment) < 8) {
return self::fallbackComment($title, $abstract, $content);
}
return mb_substr($comment, 0, 120);
}
protected static function fallbackComment(string $title, string $abstract, string $content): string
{
$focus = $title ?: ($abstract ?: mb_substr($content, 0, 18));
if ($focus === '') {
return '';
}
return mb_substr($focus, 0, 18) . '这条信息量挺足,后续关键数据变化值得继续关注。';
}
protected static function markTaskFailed(int $taskId, string $message): void
{
if ($taskId <= 0) {
return;
}
ArticleAiCommentTask::where('id', $taskId)->update([
'status' => 2,
'error_msg' => mb_substr($message, 0, 255),
'update_time' => time(),
]);
}
protected static function normalizeContent(string $content): string
{
if ($content === '') {
return '';
}
$content = strip_tags($content);
$content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$content = preg_replace('/\s+/u', ' ', $content);
return trim($content);
}
}