no message

This commit is contained in:
hajimi
2026-06-11 12:15:29 +08:00
parent 10ebe39c30
commit 96efa1d905
5859 changed files with 815501 additions and 5 deletions
@@ -0,0 +1,285 @@
<?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_NICKNAME_PREFIX = 'AI评论员';
private const VIRTUAL_POOL_SIZE = 20;
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
{
$existing = User::where('account', 'like', self::VIRTUAL_ACCOUNT_PREFIX . '%')
->order('id', 'asc')
->column('id');
$existing = array_values(array_map('intval', $existing));
$missing = max(0, $targetCount - count($existing));
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::VIRTUAL_NICKNAME_PREFIX . str_pad((string) $accountIndex, 2, '0', STR_PAD_LEFT),
'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 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);
}
}