568 lines
22 KiB
PHP
568 lines
22 KiB
PHP
<?php
|
||
|
||
namespace app\common\service\ai;
|
||
|
||
use app\common\model\ai\AiAnalysis;
|
||
use app\common\model\ai\AiConfig;
|
||
use app\common\model\ai\AiLog;
|
||
use app\common\model\lottery\LotteryAiAnalysis;
|
||
use app\common\service\FileService;
|
||
use app\common\service\MTranServerService;
|
||
|
||
class AiService
|
||
{
|
||
private const LOTTERY_POST_IMAGE_TIMEOUT = 25;
|
||
|
||
|
||
// 生成类 AI 统一走 DeepSeekClient 的 OpenAI Responses 配置。
|
||
private static function gptModelOptions(array $overrides = []): array
|
||
{
|
||
return DeepSeekClient::defaultModelOptions($overrides);
|
||
}
|
||
|
||
private static function defaultModelName(): string
|
||
{
|
||
return (string) self::gptModelOptions()['model'];
|
||
}
|
||
|
||
/**
|
||
* 赛事预测(完整,保留兼容)
|
||
*/
|
||
public static function predictMatch(int $matchId, array $matchData, int $userId = 0): array
|
||
{
|
||
// 检查缓存
|
||
$cache = AiAnalysis::getCache(AiAnalysis::TYPE_MATCH_PREDICT, $matchId);
|
||
if ($cache) {
|
||
$cacheData = json_decode($cache['result'], true);
|
||
if (is_array($cacheData) && self::isKbEnhancedMatchPayload($cacheData)) {
|
||
return ['success' => true, 'data' => $cacheData, 'from_cache' => true];
|
||
}
|
||
}
|
||
|
||
$result = KbRagService::analyzeMatch($matchId, $matchData, $userId);
|
||
|
||
self::saveResult(AiAnalysis::TYPE_MATCH_PREDICT, $matchId, '', $result, $userId);
|
||
|
||
if (!$result['success']) {
|
||
return ['success' => false, 'error' => $result['error']];
|
||
}
|
||
|
||
$data = $result['parsed'] ?? self::buildFallbackMatch($matchData);
|
||
return ['success' => true, 'data' => $data, 'from_cache' => false];
|
||
}
|
||
|
||
// ── 分段赛事预测 ──
|
||
|
||
protected static array $sectionPrompts = [
|
||
'prediction' => '你是专业体育赛事分析师。请基于数据预测胜负和比分。
|
||
要求输出严格JSON:
|
||
{"prediction":{"result":"主胜/平局/客胜","confidence":75,"score_predict":"2-1","reasoning":"简述理由"},"analysis":{"home_strength":78,"away_strength":72}}',
|
||
'factors' => '你是专业体育赛事分析师。请分析影响比赛的关键因素和风险。
|
||
要求输出严格JSON:
|
||
{"key_factors":["利好因素1","利好因素2","利好因素3"],"risk_factors":["风险1","风险2"]}',
|
||
'odds' => '你是专业体育赛事分析师。请分析赔率并给出投注建议。
|
||
要求输出严格JSON:
|
||
{"odds_opinion":{"value_bet":"主胜/平局/客胜/无","explanation":"赔率分析说明"}}',
|
||
'summary' => '你是专业体育赛事分析师。请给出简洁的分析总结。
|
||
要求输出严格JSON:
|
||
{"summary":"完整的分析总结,200字左右"}',
|
||
];
|
||
|
||
public static function predictMatchSection(int $matchId, string $section, array $matchData, int $userId = 0): array
|
||
{
|
||
$validSections = ['prediction', 'factors', 'odds', 'summary'];
|
||
if (!in_array($section, $validSections)) {
|
||
return ['success' => false, 'error' => '无效的分析模块'];
|
||
}
|
||
|
||
$cacheKey = AiAnalysis::TYPE_MATCH_PREDICT * 100 + array_search($section, $validSections);
|
||
$cache = AiAnalysis::getCache($cacheKey, $matchId);
|
||
if ($cache) {
|
||
$cacheData = json_decode($cache['result'], true);
|
||
if (is_array($cacheData) && self::isKbEnhancedMatchPayload($cacheData)) {
|
||
return ['success' => true, 'data' => $cacheData, 'from_cache' => true, 'section' => $section];
|
||
}
|
||
}
|
||
|
||
$result = KbRagService::analyzeMatchSection($matchId, $section, $matchData, $userId);
|
||
|
||
self::saveResult($cacheKey, $matchId, '', $result, $userId, 6);
|
||
|
||
if (!$result['success']) {
|
||
return ['success' => false, 'error' => $result['error'], 'section' => $section];
|
||
}
|
||
|
||
$data = $result['parsed'] ?? [];
|
||
return ['success' => true, 'data' => $data, 'from_cache' => false, 'section' => $section];
|
||
}
|
||
|
||
/**
|
||
* 资讯AI分析
|
||
*/
|
||
public static function analyzeArticle(int $articleId, array $articleData, int $userId = 0): array
|
||
{
|
||
$cache = AiAnalysis::getCache(AiAnalysis::TYPE_ARTICLE_ANALYSIS, $articleId);
|
||
if ($cache) {
|
||
return ['success' => true, 'data' => json_decode($cache['result'], true), 'from_cache' => true];
|
||
}
|
||
|
||
$result = KbRagService::analyzeArticle($articleId, $articleData, $userId);
|
||
|
||
$cacheHours = (int) AiConfig::getVal('article_analysis_cache_hours', '24');
|
||
self::saveResult(AiAnalysis::TYPE_ARTICLE_ANALYSIS, $articleId, '', $result, $userId, $cacheHours);
|
||
|
||
if (!$result['success']) {
|
||
return ['success' => false, 'error' => $result['error']];
|
||
}
|
||
|
||
$data = $result['parsed'] ?? self::buildFallbackArticle($articleData);
|
||
return ['success' => true, 'data' => $data, 'from_cache' => false];
|
||
}
|
||
|
||
/**
|
||
* 帖子AI分析
|
||
*/
|
||
public static function analyzePost(int $postId, array $postData, int $userId = 0, bool $isLotteryPost = false, bool $force = false): array
|
||
{
|
||
$cache = $force ? null : AiAnalysis::getCache(AiAnalysis::TYPE_POST_ANALYSIS, $postId);
|
||
if ($cache) {
|
||
$cacheData = json_decode($cache['result'], true);
|
||
if (is_array($cacheData) && self::isValidPostAnalysisCache($cacheData, $isLotteryPost)) {
|
||
return ['success' => true, 'data' => $cacheData, 'from_cache' => true];
|
||
}
|
||
}
|
||
|
||
$result = KbRagService::analyzePost($postId, $postData, $userId, $isLotteryPost);
|
||
self::saveResult(AiAnalysis::TYPE_POST_ANALYSIS, $postId, '', $result, $userId, 12);
|
||
|
||
if (!$result['success']) {
|
||
return ['success' => false, 'error' => $result['error'] ?? 'AI分析失败'];
|
||
}
|
||
|
||
return [
|
||
'success' => true,
|
||
'data' => $result['parsed'] ?? self::buildFallbackPost($postData),
|
||
'from_cache' => false,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 用户可信度分析
|
||
*/
|
||
public static function analyzeCredibility(int $targetUserId, array $userData, int $requestUserId = 0): array
|
||
{
|
||
$cache = AiAnalysis::getCache(AiAnalysis::TYPE_USER_CREDIBILITY, $targetUserId);
|
||
if ($cache) {
|
||
return ['success' => true, 'data' => json_decode($cache['result'], true), 'from_cache' => true];
|
||
}
|
||
|
||
$defaultPrompt = '你是体育社区内容审核AI。请根据用户的历史数据评估其可信度。
|
||
|
||
要求输出严格JSON格式:
|
||
{"credibility_score":75,"risk_level":"low/medium/high","is_suspicious":false,"analysis":"分析说明,100字左右","dimension_scores":{"hit_rate":80,"stability":85,"rationality":65,"activity":70},"suggestions":["建议1","建议2"]}';
|
||
$systemPrompt = AiConfig::getVal('credibility_analysis_prompt', $defaultPrompt);
|
||
|
||
$userMsg = "请分析以下用户数据并评估可信度:\n" . json_encode($userData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
|
||
$client = new DeepSeekClient();
|
||
$result = $client->chatJson($systemPrompt, $userMsg);
|
||
|
||
self::saveResult(AiAnalysis::TYPE_USER_CREDIBILITY, $targetUserId, '', $result, $requestUserId, 24);
|
||
|
||
if (!$result['success']) {
|
||
return ['success' => false, 'error' => $result['error']];
|
||
}
|
||
|
||
$data = $result['parsed'] ?? self::buildFallbackCredibility();
|
||
return ['success' => true, 'data' => $data, 'from_cache' => false];
|
||
}
|
||
|
||
/**
|
||
* 彩票概率分析
|
||
*/
|
||
public static function analyzeLottery(array $lotteryData, int $userId = 0): array
|
||
{
|
||
$refId = crc32(json_encode($lotteryData));
|
||
|
||
$cache = AiAnalysis::getCache(AiAnalysis::TYPE_LOTTERY_ANALYSIS, $refId);
|
||
if ($cache) {
|
||
return ['success' => true, 'data' => json_decode($cache['result'], true), 'from_cache' => true];
|
||
}
|
||
|
||
$result = KbRagService::analyzeLotteryOverview($lotteryData, $userId);
|
||
|
||
self::saveResult(AiAnalysis::TYPE_LOTTERY_ANALYSIS, $refId, '', $result, $userId, 12);
|
||
|
||
if (!$result['success']) {
|
||
return ['success' => false, 'error' => $result['error']];
|
||
}
|
||
|
||
$data = $result['parsed'] ?? ['disclaimer' => '分析暂时不可用,请稍后重试'];
|
||
return ['success' => true, 'data' => $data, 'from_cache' => false];
|
||
}
|
||
|
||
public static function translateCommunityPost(string $content): array
|
||
{
|
||
if ($content === '') {
|
||
return ['success' => false, 'error' => '帖子内容为空'];
|
||
}
|
||
|
||
return MTranServerService::translate($content, 'zh-Hans', 'auto');
|
||
}
|
||
|
||
/**
|
||
* 资讯翻译
|
||
*/
|
||
public static function translateArticle(string $title, string $abstract, string $content): array
|
||
{
|
||
$title = trim($title);
|
||
$abstract = trim($abstract);
|
||
$content = trim($content);
|
||
|
||
if ($title === '' && $abstract === '' && $content === '') {
|
||
return ['success' => false, 'error' => '文章内容为空'];
|
||
}
|
||
|
||
if (mb_strlen($content) > 5000) {
|
||
$content = mb_substr($content, 0, 5000) . '...';
|
||
}
|
||
|
||
$parts = [];
|
||
if ($title !== '') {
|
||
$translatedTitle = MTranServerService::translate($title, 'zh-Hans', 'auto');
|
||
if (!$translatedTitle['success']) {
|
||
return $translatedTitle;
|
||
}
|
||
$parts[] = '标题:' . trim((string) $translatedTitle['content']);
|
||
}
|
||
|
||
if ($abstract !== '') {
|
||
$translatedAbstract = MTranServerService::translate($abstract, 'zh-Hans', 'auto');
|
||
if (!$translatedAbstract['success']) {
|
||
return $translatedAbstract;
|
||
}
|
||
$parts[] = '摘要:' . trim((string) $translatedAbstract['content']);
|
||
}
|
||
|
||
if ($content !== '') {
|
||
$translatedContentLines = [];
|
||
foreach (self::splitTextForTranslation($content) as $contentLine) {
|
||
$translatedContent = MTranServerService::translate($contentLine, 'zh-Hans', 'auto');
|
||
if (!$translatedContent['success']) {
|
||
return $translatedContent;
|
||
}
|
||
$translatedContentLines[] = trim((string) $translatedContent['content']);
|
||
}
|
||
$translatedContentLines = array_values(array_filter($translatedContentLines, static fn($line) => $line !== ''));
|
||
if (!empty($translatedContentLines)) {
|
||
$firstLine = array_shift($translatedContentLines);
|
||
$parts[] = '正文:' . $firstLine;
|
||
foreach ($translatedContentLines as $line) {
|
||
$parts[] = $line;
|
||
}
|
||
}
|
||
}
|
||
|
||
$finalText = trim(implode("\n", $parts));
|
||
if ($finalText === '') {
|
||
return ['success' => false, 'error' => '翻译结果为空'];
|
||
}
|
||
|
||
return [
|
||
'success' => true,
|
||
'content' => $finalText,
|
||
'tokens' => 0,
|
||
'cost_ms' => 0,
|
||
];
|
||
}
|
||
|
||
protected static function splitTextForTranslation(string $text): array
|
||
{
|
||
$normalized = trim(preg_replace('/\r\n|\r/u', "\n", $text));
|
||
if ($normalized === '') {
|
||
return [];
|
||
}
|
||
|
||
$manualLines = array_values(array_filter(array_map(static function ($line) {
|
||
$line = preg_replace('/\s+/u', ' ', trim($line));
|
||
return $line === '' ? '' : $line;
|
||
}, explode("\n", $normalized))));
|
||
|
||
if (count($manualLines) > 1) {
|
||
$segments = [];
|
||
foreach ($manualLines as $line) {
|
||
$segments = array_merge($segments, self::splitTextForTranslation($line));
|
||
}
|
||
return $segments;
|
||
}
|
||
|
||
$flatText = preg_replace('/\s+/u', ' ', $normalized);
|
||
preg_match_all('/[^。!?.!?]+[。!?.!?]?/u', $flatText, $matches);
|
||
$segments = array_values(array_filter(array_map('trim', $matches[0] ?? [])));
|
||
|
||
return !empty($segments) ? $segments : [$flatText];
|
||
}
|
||
|
||
/**
|
||
* 资讯评论生成
|
||
*/
|
||
public static function generateArticleComment(string $title, string $abstract, string $content, string $persona = ''): array
|
||
{
|
||
$title = trim($title);
|
||
$abstract = trim($abstract);
|
||
$content = trim($content);
|
||
|
||
if ($title === '' && $abstract === '' && $content === '') {
|
||
return ['success' => false, 'error' => '文章内容为空'];
|
||
}
|
||
|
||
if (mb_strlen($content) > 3500) {
|
||
$content = mb_substr($content, 0, 3500) . '...';
|
||
}
|
||
|
||
$systemPrompt = AiConfig::getVal(
|
||
'article_comment_prompt',
|
||
'你是一名专业、克制、真实的体育资讯评论员。请根据给定资讯内容生成1条中文评论,要求自然、具体、像真实用户发言,评论长度控制在20到60字之间。不要输出标题、前缀、解释、表情、标签、项目符号,不要提及自己是AI,不要复述全文。'
|
||
);
|
||
|
||
$personaText = $persona !== '' ? "评论视角:{$persona}\n" : '';
|
||
$userMsg = $personaText . "标题:{$title}\n摘要:{$abstract}\n正文:{$content}\n\n请输出1条适合发布在评论区的中文评论。";
|
||
|
||
$client = new DeepSeekClient();
|
||
return $client->chat($systemPrompt, $userMsg, ['max_tokens' => 200, 'temperature' => 0.75]);
|
||
}
|
||
|
||
public static function analyzeLotteryPostImages(string $content, array $images): array
|
||
{
|
||
if (empty($images)) {
|
||
return ['success' => false, 'error' => '帖子图片为空,无法分析'];
|
||
}
|
||
|
||
$imageUrls = array_values(array_filter(array_map(function ($image) {
|
||
if (!$image) {
|
||
return '';
|
||
}
|
||
return FileService::getFileUrl($image);
|
||
}, $images)));
|
||
|
||
if (empty($imageUrls)) {
|
||
return ['success' => false, 'error' => '帖子图片为空,无法分析'];
|
||
}
|
||
|
||
$systemPrompt = AiConfig::getVal(
|
||
'community_lottery_image_analysis_prompt',
|
||
'你是彩票图片内容快速抽取助手。只提取当前图片中清晰可见的文字、期号、号码、生肖、波色、单双、大小、五行、推荐组合、标题结论等有效信息。'
|
||
. '不要做长篇分析,不要引用历史或记忆,不要承诺中奖。'
|
||
. '看不清或图片里没有的信息直接写“未识别”。'
|
||
. '请按“图片X:有效信息:...;不确定:...”的格式输出,总字数控制在300字以内。'
|
||
);
|
||
$options = self::gptModelOptions([
|
||
'max_tokens' => 600,
|
||
'temperature' => 0.3,
|
||
'timeout' => self::LOTTERY_POST_IMAGE_TIMEOUT,
|
||
'reasoning_effort' => 'low',
|
||
'retry_times' => 0,
|
||
'retry_sleep_ms' => 0,
|
||
]);
|
||
|
||
$client = new DeepSeekClient();
|
||
$userMessage = "帖子内容:\n" . ($content ?: '无文字内容') . "\n\n请快速抽取图片里的有效信息,只输出可见事实和不确定项,不要展开分析。";
|
||
$result = $client->chatWithImages(
|
||
$systemPrompt,
|
||
$userMessage,
|
||
$imageUrls,
|
||
$options
|
||
);
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 保存分析结果
|
||
*/
|
||
protected static function saveResult(int $type, int $refId, string $prompt, array $result, int $userId, int $cacheHours = 6): void
|
||
{
|
||
$status = $result['success'] ? AiAnalysis::STATUS_SUCCESS : AiAnalysis::STATUS_FAILED;
|
||
$resultJson = $result['success'] ? json_encode($result['parsed'] ?? $result['content'], JSON_UNESCAPED_UNICODE) : '';
|
||
|
||
AiAnalysis::create([
|
||
'type' => $type,
|
||
'ref_id' => $refId,
|
||
'prompt' => mb_substr($prompt, 0, 5000),
|
||
'result' => $resultJson,
|
||
'model' => (string) ($result['model'] ?? self::defaultModelName()),
|
||
'tokens_used' => $result['tokens'] ?? 0,
|
||
'status' => $status,
|
||
'error_msg' => $result['error'] ?? '',
|
||
'expire_time' => time() + $cacheHours * 3600,
|
||
]);
|
||
|
||
AiLog::create([
|
||
'user_id' => $userId,
|
||
'type' => $type,
|
||
'ref_id' => $refId,
|
||
'tokens_used' => $result['tokens'] ?? 0,
|
||
'cost_ms' => $result['cost_ms'] ?? 0,
|
||
'status' => $status,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 彩票分模块分析(热冷号/趋势/统计)
|
||
*/
|
||
public static function analyzeLotteryModule(array $lotteryData, string $module, int $userId = 0): array
|
||
{
|
||
$latestIssue = $lotteryData['recent_draws'][0]['issue'] ?? '';
|
||
$cacheKey = $lotteryData['lottery_type'] . ':' . $module . ':' . $latestIssue;
|
||
$refId = crc32($cacheKey);
|
||
|
||
$cache = AiAnalysis::getCache(AiAnalysis::TYPE_LOTTERY_ANALYSIS, $refId);
|
||
if ($cache) {
|
||
$cacheData = json_decode($cache['result'], true);
|
||
if (is_array($cacheData) && !empty($cacheData)) {
|
||
return ['success' => true, 'data' => $cacheData, 'from_cache' => true];
|
||
}
|
||
}
|
||
|
||
$result = KbRagService::analyzeLotteryModule($lotteryData, $module, $userId);
|
||
|
||
if (!$result['success']) {
|
||
self::saveResult(AiAnalysis::TYPE_LOTTERY_ANALYSIS, $refId, '', $result, $userId, 12);
|
||
return ['success' => false, 'error' => $result['error']];
|
||
}
|
||
|
||
$data = $result['parsed'] ?? null;
|
||
if (!is_array($data) || empty($data)) {
|
||
$result['success'] = false;
|
||
$result['error'] = 'AI返回格式异常';
|
||
self::saveResult(AiAnalysis::TYPE_LOTTERY_ANALYSIS, $refId, '', $result, $userId, 12);
|
||
return ['success' => false, 'error' => $result['error']];
|
||
}
|
||
|
||
self::saveResult(AiAnalysis::TYPE_LOTTERY_ANALYSIS, $refId, '', $result, $userId, 12);
|
||
|
||
// 持久化到彩票AI分析历史表
|
||
if ($latestIssue) {
|
||
$history = LotteryAiAnalysis::create([
|
||
'code' => $lotteryData['lottery_type'],
|
||
'issue' => $latestIssue,
|
||
'module' => $module,
|
||
'result' => json_encode($data, JSON_UNESCAPED_UNICODE),
|
||
]);
|
||
KbSyncService::enqueue('lottery', 'ai_history', (int) $history->id, 'upsert', 90);
|
||
}
|
||
|
||
return ['success' => true, 'data' => $data, 'from_cache' => false];
|
||
}
|
||
|
||
/**
|
||
* 赛事预测降级数据
|
||
*/
|
||
protected static function buildFallbackMatch(array $matchData): array
|
||
{
|
||
return [
|
||
'prediction' => [
|
||
'result' => '待分析',
|
||
'confidence' => 50,
|
||
'score_predict' => '-',
|
||
'reasoning' => 'AI分析暂时不可用,请稍后重试',
|
||
],
|
||
'analysis' => [
|
||
'home_strength' => 50,
|
||
'away_strength' => 50,
|
||
'key_factors' => ['数据加载中'],
|
||
'risk_factors' => ['AI服务响应异常'],
|
||
],
|
||
'odds_opinion' => ['value_bet' => '无', 'explanation' => '暂无数据'],
|
||
'summary' => '当前AI分析服务暂时不可用,请稍后重试。',
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 资讯分析降级数据
|
||
*/
|
||
protected static function buildFallbackArticle(array $articleData): array
|
||
{
|
||
return [
|
||
'overall_score' => 60,
|
||
'summary' => $articleData['desc'] ?? '暂无摘要',
|
||
'leagues_and_teams' => ['leagues' => [], 'teams' => [], 'match_context' => ''],
|
||
'upcoming_matches' => [],
|
||
'head_to_head' => ['available' => false, 'records' => [], 'summary' => ''],
|
||
'betting_analysis' => [
|
||
'prediction' => '谨慎观望',
|
||
'confidence' => 50,
|
||
'reasoning' => 'AI分析暂时不可用,请稍后重试。',
|
||
'risk_level' => 'medium',
|
||
'key_factors' => [],
|
||
'value_bet' => '无明显价值',
|
||
],
|
||
'key_points' => ['详细分析正在生成中'],
|
||
'risk_warnings' => [],
|
||
'opposite_views' => [],
|
||
'sentiment' => ['label' => 'neutral', 'confidence' => 50],
|
||
'tags' => [],
|
||
'evidence_list' => [],
|
||
'similar_history' => [],
|
||
'from_kb' => false,
|
||
'retrieval_meta' => ['hit_count' => 0, 'domains' => [], 'top_scores' => []],
|
||
];
|
||
}
|
||
|
||
protected static function buildFallbackPost(array $postData): array
|
||
{
|
||
return [
|
||
'overall_score' => 60,
|
||
'summary' => self::truncatePlainText((string) ($postData['content'] ?? '当前帖子信息不足,暂时无法给出稳定结论。'), 120),
|
||
'credibility_signals' => [],
|
||
'risk_warning' => ['AI分析暂时不可用,请稍后重试。'],
|
||
'evidence_list' => [],
|
||
'similar_history' => [],
|
||
'from_kb' => false,
|
||
'retrieval_meta' => ['hit_count' => 0, 'domains' => [], 'top_scores' => []],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 可信度分析降级数据
|
||
*/
|
||
protected static function buildFallbackCredibility(): array
|
||
{
|
||
return [
|
||
'credibility_score' => 50,
|
||
'risk_level' => 'medium',
|
||
'is_suspicious' => false,
|
||
'analysis' => '可信度分析服务暂时不可用',
|
||
'dimension_scores' => ['hit_rate' => 50, 'stability' => 50, 'rationality' => 50, 'activity' => 50],
|
||
'suggestions' => [],
|
||
];
|
||
}
|
||
|
||
protected static function truncatePlainText(string $text, int $length): string
|
||
{
|
||
$text = trim(preg_replace('/\s+/u', ' ', $text) ?: '');
|
||
return mb_strlen($text) > $length ? mb_substr($text, 0, $length) . '...' : $text;
|
||
}
|
||
|
||
protected static function isKbEnhancedMatchPayload(array $payload): bool
|
||
{
|
||
return isset($payload['retrieval_meta']) || isset($payload['evidence_list']) || isset($payload['from_kb']);
|
||
}
|
||
|
||
protected static function isValidPostAnalysisCache(array $payload, bool $isLotteryPost): bool
|
||
{
|
||
if (empty($payload)) {
|
||
return false;
|
||
}
|
||
|
||
if (!$isLotteryPost) {
|
||
return true;
|
||
}
|
||
|
||
return self::isKbEnhancedMatchPayload($payload)
|
||
&& isset($payload['next_prediction'])
|
||
&& is_array($payload['next_prediction'])
|
||
&& ($payload['analysis_source'] ?? '') === 'image_free_v1';
|
||
}
|
||
}
|