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); } }