no message
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user