deploy: auto commit repo-root changes 2026-07-05 12:33:57
This commit is contained in:
@@ -2,77 +2,34 @@
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\common\service\ai\AiService;
|
||||
use app\common\model\match\MatchEvent;
|
||||
use app\common\model\match\MatchHistory;
|
||||
use app\common\model\match\MatchRecent;
|
||||
use app\common\model\article\Article;
|
||||
use app\common\enum\YesNoEnum;
|
||||
use app\common\model\ai\AiUnlock;
|
||||
use app\common\service\ai\AiAnalysisRequestService;
|
||||
|
||||
class AiController extends BaseApiController
|
||||
{
|
||||
public array $notNeedLogin = ['matchPredict', 'articleAnalysis'];
|
||||
public array $notNeedLogin = [];
|
||||
|
||||
/**
|
||||
* AI分析统一入口
|
||||
*/
|
||||
public function analyze()
|
||||
{
|
||||
$type = $this->request->get('type/s', '');
|
||||
$params = $this->request->get();
|
||||
$result = AiAnalysisRequestService::analyze($type, $params, $this->userId);
|
||||
return $this->respondAnalysis($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 赛事AI预测
|
||||
*/
|
||||
public function matchPredict()
|
||||
{
|
||||
$matchId = $this->request->get('match_id/d');
|
||||
if (empty($matchId)) {
|
||||
return $this->fail('参数缺失');
|
||||
}
|
||||
|
||||
$cacheKey = 'ai_match_predict_' . $matchId;
|
||||
$cached = cache($cacheKey);
|
||||
if ($cached) {
|
||||
$cached['from_cache'] = true;
|
||||
return $this->data($cached);
|
||||
}
|
||||
|
||||
$match = MatchEvent::where('id', $matchId)->where('is_show', 1)->findOrEmpty();
|
||||
if ($match->isEmpty()) {
|
||||
return $this->fail('赛事不存在');
|
||||
}
|
||||
$data = $match->toArray();
|
||||
|
||||
$homeTeam = $data['home_team'];
|
||||
$awayTeam = $data['away_team'];
|
||||
|
||||
// 组装分析数据
|
||||
$matchData = [
|
||||
'match_info' => [
|
||||
'home_team' => $homeTeam,
|
||||
'away_team' => $awayTeam,
|
||||
'league' => $data['league_name'] ?? '',
|
||||
'match_time' => date('Y-m-d H:i', $data['match_time'] ?? time()),
|
||||
'status' => $data['status'] ?? 0,
|
||||
],
|
||||
'head_to_head' => MatchHistory::where(function ($query) use ($homeTeam, $awayTeam) {
|
||||
$query->where([['home_team', '=', $homeTeam], ['away_team', '=', $awayTeam]])
|
||||
->whereOr([['home_team', '=', $awayTeam], ['away_team', '=', $homeTeam]]);
|
||||
})->order('match_time desc')->limit(5)->select()->toArray(),
|
||||
'home_recent' => MatchRecent::where('team_name', $homeTeam)
|
||||
->order('match_time desc')->limit(5)->select()->toArray(),
|
||||
'away_recent' => MatchRecent::where('team_name', $awayTeam)
|
||||
->order('match_time desc')->limit(5)->select()->toArray(),
|
||||
];
|
||||
|
||||
$result = AiService::predictMatch($matchId, $matchData, $this->userId);
|
||||
if (!$result['success']) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败');
|
||||
}
|
||||
|
||||
$responseData = [
|
||||
'analysis' => $result['data'],
|
||||
'from_cache' => false,
|
||||
'match_info' => $matchData['match_info'],
|
||||
];
|
||||
|
||||
cache($cacheKey, $responseData, 6 * 3600);
|
||||
|
||||
return $this->data($responseData);
|
||||
$result = AiAnalysisRequestService::analyzeMatchFull(
|
||||
$this->request->get('match_id/d'),
|
||||
$this->userId
|
||||
);
|
||||
return $this->respondAnalysis($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,73 +37,12 @@ class AiController extends BaseApiController
|
||||
*/
|
||||
public function matchPredictSection()
|
||||
{
|
||||
$matchId = $this->request->get('match_id/d');
|
||||
$section = $this->request->get('section/s', '');
|
||||
if (empty($matchId) || empty($section)) {
|
||||
return $this->fail('参数缺失');
|
||||
}
|
||||
|
||||
if (!$this->userId) {
|
||||
return $this->fail('请先登录', [], 0, 401);
|
||||
}
|
||||
|
||||
// 解锁检查:只有本人解锁过的才免费,其他人的缓存不算
|
||||
$isUnlocked = AiUnlock::isUnlocked($this->userId, $matchId);
|
||||
if (!$isUnlocked) {
|
||||
if ($section === 'prediction') {
|
||||
$unlockResult = AiUnlock::unlock($this->userId, $matchId);
|
||||
if (!$unlockResult['ok']) {
|
||||
return $this->fail($unlockResult['msg']);
|
||||
}
|
||||
} else {
|
||||
return $this->fail('请先解锁该赛事分析');
|
||||
}
|
||||
}
|
||||
|
||||
$match = MatchEvent::where('id', $matchId)->where('is_show', 1)->findOrEmpty();
|
||||
if ($match->isEmpty()) {
|
||||
return $this->fail('赛事不存在');
|
||||
}
|
||||
$data = $match->toArray();
|
||||
|
||||
$homeTeam = $data['home_team'];
|
||||
$awayTeam = $data['away_team'];
|
||||
|
||||
$matchData = [
|
||||
'match_info' => [
|
||||
'home_team' => $homeTeam,
|
||||
'away_team' => $awayTeam,
|
||||
'league' => $data['league_name'] ?? '',
|
||||
'match_time' => date('Y-m-d H:i', $data['match_time'] ?? time()),
|
||||
'status' => $data['status'] ?? 0,
|
||||
'home_score' => $data['home_score'] ?? 0,
|
||||
'away_score' => $data['away_score'] ?? 0,
|
||||
'home_odds' => $data['home_odds'] ?? 0,
|
||||
'draw_odds' => $data['draw_odds'] ?? 0,
|
||||
'away_odds' => $data['away_odds'] ?? 0,
|
||||
],
|
||||
'head_to_head' => MatchHistory::where(function ($query) use ($homeTeam, $awayTeam) {
|
||||
$query->where([['home_team', '=', $homeTeam], ['away_team', '=', $awayTeam]])
|
||||
->whereOr([['home_team', '=', $awayTeam], ['away_team', '=', $homeTeam]]);
|
||||
})->order('match_time desc')->limit(5)->select()->toArray(),
|
||||
'home_recent' => MatchRecent::where('team_name', $homeTeam)
|
||||
->order('match_time desc')->limit(5)->select()->toArray(),
|
||||
'away_recent' => MatchRecent::where('team_name', $awayTeam)
|
||||
->order('match_time desc')->limit(5)->select()->toArray(),
|
||||
];
|
||||
|
||||
$result = AiService::predictMatchSection($matchId, $section, $matchData, $this->userId);
|
||||
if (!$result['success']) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败');
|
||||
}
|
||||
|
||||
return $this->data([
|
||||
'section' => $section,
|
||||
'data' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
'match_info' => $matchData['match_info'],
|
||||
'remain_count' => AiUnlock::getRemainCount($this->userId),
|
||||
]);
|
||||
$result = AiAnalysisRequestService::analyzeMatchSection(
|
||||
$this->request->get('match_id/d'),
|
||||
$this->request->get('section/s', ''),
|
||||
$this->userId
|
||||
);
|
||||
return $this->respondAnalysis($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,25 +50,11 @@ class AiController extends BaseApiController
|
||||
*/
|
||||
public function articleAnalysis()
|
||||
{
|
||||
$articleId = $this->request->get('article_id/d');
|
||||
if (empty($articleId)) {
|
||||
return $this->fail('参数缺失');
|
||||
}
|
||||
|
||||
$article = Article::where(['id' => $articleId, 'is_show' => YesNoEnum::YES])->findOrEmpty();
|
||||
if ($article->isEmpty()) {
|
||||
return $this->fail('文章不存在');
|
||||
}
|
||||
|
||||
$articleData = $article->toArray();
|
||||
$result = AiService::analyzeArticle($articleId, $articleData, $this->userId);
|
||||
if (!$result['success']) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败');
|
||||
}
|
||||
return $this->data([
|
||||
'analysis' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
]);
|
||||
$result = AiAnalysisRequestService::analyzeArticle(
|
||||
$this->request->get('article_id/d'),
|
||||
$this->userId
|
||||
);
|
||||
return $this->respondAnalysis($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,28 +62,11 @@ class AiController extends BaseApiController
|
||||
*/
|
||||
public function userCredibility()
|
||||
{
|
||||
$targetUserId = $this->request->get('user_id/d');
|
||||
if (empty($targetUserId)) {
|
||||
return $this->fail('参数缺失');
|
||||
}
|
||||
|
||||
// 组装用户数据(简化版,后续可扩展)
|
||||
$userData = [
|
||||
'user_id' => $targetUserId,
|
||||
'register_days' => 30,
|
||||
'post_count' => 0,
|
||||
'comment_count' => 0,
|
||||
'like_received' => 0,
|
||||
];
|
||||
|
||||
$result = AiService::analyzeCredibility($targetUserId, $userData, $this->userId);
|
||||
if (!$result['success']) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败');
|
||||
}
|
||||
return $this->data([
|
||||
'analysis' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
]);
|
||||
$result = AiAnalysisRequestService::analyzeUserCredibility(
|
||||
$this->request->get('user_id/d'),
|
||||
$this->userId
|
||||
);
|
||||
return $this->respondAnalysis($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,23 +74,12 @@ class AiController extends BaseApiController
|
||||
*/
|
||||
public function lotteryAnalysis()
|
||||
{
|
||||
$type = $this->request->get('type', 'ssq');
|
||||
|
||||
// 简化版彩票数据
|
||||
$lotteryData = [
|
||||
'lottery_type' => $type,
|
||||
'recent_draws' => [],
|
||||
'description' => $type === 'ssq' ? '双色球' : '大乐透',
|
||||
];
|
||||
|
||||
$result = AiService::analyzeLottery($lotteryData, $this->userId);
|
||||
if (!$result['success']) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败');
|
||||
$params = $this->request->get();
|
||||
if (empty($params['code']) && !empty($params['type'])) {
|
||||
$params['code'] = $params['type'];
|
||||
}
|
||||
return $this->data([
|
||||
'analysis' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
]);
|
||||
$result = AiAnalysisRequestService::analyze('lottery', $params, $this->userId);
|
||||
return $this->respondAnalysis($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,4 +98,16 @@ class AiController extends BaseApiController
|
||||
'remain_count' => $remain,
|
||||
]);
|
||||
}
|
||||
|
||||
private function respondAnalysis(array $result)
|
||||
{
|
||||
if (empty($result['success'])) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败', [
|
||||
'remain_count' => $result['remain_count'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
unset($result['success'], $result['error'], $result['code']);
|
||||
return $this->data($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use app\common\model\user\User;
|
||||
use app\common\model\ai\AiAnalysis;
|
||||
use app\common\enum\user\AccountLogEnum;
|
||||
use app\common\logic\AccountLogLogic;
|
||||
use app\common\service\ai\AiAnalysisRequestService;
|
||||
use app\common\service\ai\AiService;
|
||||
use app\common\service\ai\KbSyncService;
|
||||
use app\common\service\chat\PrivateChatService;
|
||||
@@ -585,56 +586,20 @@ class CommunityController extends BaseApiController
|
||||
|
||||
public function aiAnalysis()
|
||||
{
|
||||
try {
|
||||
$postId = $this->request->get('post_id/d', 0);
|
||||
if ($postId <= 0) {
|
||||
return $this->fail('参数缺失');
|
||||
}
|
||||
$result = AiAnalysisRequestService::analyzePost(
|
||||
$this->request->get('post_id/d', 0),
|
||||
$this->userId,
|
||||
$this->request->get('force/d', 0) === 1
|
||||
);
|
||||
|
||||
$post = CommunityPost::where('id', $postId)->where('status', 1)->findOrEmpty();
|
||||
if ($post->isEmpty()) {
|
||||
return $this->fail('帖子不存在');
|
||||
}
|
||||
|
||||
$data = $post->toArray();
|
||||
$ext = is_array($data['ext'] ?? null) ? $data['ext'] : (json_decode((string) ($data['ext'] ?? ''), true) ?: []);
|
||||
$tags = $this->getPostTags($postId);
|
||||
$isTrumpPost = $this->isTrumpPost($tags);
|
||||
$isLiuhePost = $this->isLiuhePost($tags, $ext);
|
||||
if ($isTrumpPost) {
|
||||
return $this->fail('特朗普帖子仅支持翻译');
|
||||
}
|
||||
if (!$isLiuhePost) {
|
||||
return $this->fail('该帖子类型无需AI分析');
|
||||
}
|
||||
|
||||
$analysis = $this->ensureLiuheAnalysisContent($post, $ext);
|
||||
if (!$analysis['success']) {
|
||||
return $this->fail($analysis['error'] ?? '图片分析失败');
|
||||
}
|
||||
$ext = $analysis['ext'];
|
||||
$data['tags'] = array_values($tags);
|
||||
$data['ext'] = $ext;
|
||||
if (!empty($ext['translated_content'])) {
|
||||
$data['translated_content'] = (string) $ext['translated_content'];
|
||||
}
|
||||
if (!empty($ext['lottery_analysis_content'])) {
|
||||
$data['lottery_analysis_content'] = (string) $ext['lottery_analysis_content'];
|
||||
}
|
||||
|
||||
$force = $this->request->get('force/d', 0) === 1;
|
||||
$result = AiService::analyzePost($postId, $data, $this->userId, true, $force);
|
||||
if (!$result['success']) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败');
|
||||
}
|
||||
|
||||
return $this->data([
|
||||
'analysis' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
if (empty($result['success'])) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败', [
|
||||
'remain_count' => $result['remain_count'] ?? null,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail('AI分析异常: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
unset($result['success'], $result['error'], $result['code']);
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
public function aiAnalysisResult()
|
||||
|
||||
@@ -10,11 +10,11 @@ use app\common\model\lottery\LotteryDrawResult;
|
||||
use app\common\model\lottery\LotteryAiAnalysis;
|
||||
use app\common\model\lottery\LotteryNumberMapping;
|
||||
use app\common\model\lottery\LotteryRegion;
|
||||
use app\common\service\ai\AiService;
|
||||
use app\common\service\ai\AiAnalysisRequestService;
|
||||
|
||||
class LotteryController extends BaseApiController
|
||||
{
|
||||
public array $notNeedLogin = ['categoryList', 'regionList', 'drawList', 'drawDetail', 'latestDraw', 'aiPredict', 'gameList', 'latestDrawResult', 'drawResultList', 'aiHistory', 'aiHotCold', 'aiTrend', 'aiStats', 'aiColorZodiac', 'aiAnalysisHistory'];
|
||||
public array $notNeedLogin = ['categoryList', 'regionList', 'drawList', 'drawDetail', 'latestDraw', 'gameList', 'latestDrawResult', 'drawResultList', 'aiHistory', 'aiAnalysisHistory'];
|
||||
|
||||
/**
|
||||
* 彩种游戏列表(一级分类 + 二级彩种)
|
||||
@@ -341,87 +341,7 @@ class LotteryController extends BaseApiController
|
||||
*/
|
||||
public function aiPredict()
|
||||
{
|
||||
$categoryId = $this->request->get('category_id/d', 0);
|
||||
$code = $this->request->get('code', '');
|
||||
|
||||
// 支持两种查找方式:category_id(旧表)或 code(新表)
|
||||
if (!empty($code)) {
|
||||
$game = LotteryGame::where('code', $code)->findOrEmpty();
|
||||
if ($game->isEmpty()) {
|
||||
return $this->fail('彩种不存在');
|
||||
}
|
||||
|
||||
$recentDraws = LotteryDrawResult::where('code', $code)
|
||||
->where('draw_code', '<>', '')
|
||||
->order('draw_time desc, issue desc')
|
||||
->limit(30)
|
||||
->field('issue,draw_time,draw_code')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($recentDraws as &$d) {
|
||||
$d['numbers'] = $d['draw_code'] ? explode(',', $d['draw_code']) : [];
|
||||
}
|
||||
|
||||
$lotteryData = [
|
||||
'lottery_type' => $code,
|
||||
'lottery_name' => $game['name'],
|
||||
'draw_rule' => $game['template'] ?? '',
|
||||
'recent_draws' => $recentDraws,
|
||||
];
|
||||
|
||||
$result = AiService::analyzeLottery($lotteryData, $this->userId);
|
||||
if (!$result['success']) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败');
|
||||
}
|
||||
return $this->data([
|
||||
'analysis' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
'category_name' => $game['name'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (empty($categoryId)) {
|
||||
return $this->fail('参数缺失');
|
||||
}
|
||||
|
||||
$category = LotteryCategory::where('id', $categoryId)->findOrEmpty();
|
||||
if ($category->isEmpty()) {
|
||||
return $this->fail('彩种不存在');
|
||||
}
|
||||
|
||||
// 获取最近30期已开奖数据
|
||||
$recentDraws = LotteryDraw::where('category_id', $categoryId)
|
||||
->where('status', 1)
|
||||
->where('is_show', 1)
|
||||
->order('draw_date desc, period desc')
|
||||
->limit(30)
|
||||
->field('period,draw_date,numbers,special_number,zodiac,color')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($recentDraws as &$d) {
|
||||
$d['numbers'] = is_string($d['numbers']) ? json_decode($d['numbers'], true) : $d['numbers'];
|
||||
$d['zodiac'] = is_string($d['zodiac']) ? json_decode($d['zodiac'], true) : $d['zodiac'];
|
||||
$d['color'] = is_string($d['color']) ? json_decode($d['color'], true) : $d['color'];
|
||||
}
|
||||
|
||||
$lotteryData = [
|
||||
'lottery_type' => $category['code'],
|
||||
'lottery_name' => $category['name'],
|
||||
'draw_rule' => $category['draw_rule'],
|
||||
'recent_draws' => $recentDraws,
|
||||
];
|
||||
|
||||
$result = AiService::analyzeLottery($lotteryData, $this->userId);
|
||||
if (!$result['success']) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败');
|
||||
}
|
||||
return $this->data([
|
||||
'analysis' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
'category_name' => $category['name'],
|
||||
]);
|
||||
return $this->respondAnalysis(AiAnalysisRequestService::analyzeLotteryOverview($this->request->get(), $this->userId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -465,29 +385,7 @@ class LotteryController extends BaseApiController
|
||||
*/
|
||||
public function aiHotCold()
|
||||
{
|
||||
$code = $this->request->get('code', '');
|
||||
if (empty($code)) {
|
||||
return $this->fail('参数缺失');
|
||||
}
|
||||
|
||||
$game = LotteryGame::where('code', $code)->findOrEmpty();
|
||||
if ($game->isEmpty()) {
|
||||
return $this->fail('彩种不存在');
|
||||
}
|
||||
|
||||
$recentDraws = $this->getRecentDrawCodes($code, 30);
|
||||
$lotteryData = [
|
||||
'lottery_type' => $code,
|
||||
'lottery_name' => $game['name'],
|
||||
'draw_rule' => $game['template'] ?? '',
|
||||
'recent_draws' => $recentDraws,
|
||||
];
|
||||
|
||||
$result = AiService::analyzeLotteryModule($lotteryData, 'hot_cold', $this->userId);
|
||||
if (!$result['success']) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败');
|
||||
}
|
||||
return $this->data($result['data']);
|
||||
return $this->respondLotteryModule('hot_cold');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -495,29 +393,7 @@ class LotteryController extends BaseApiController
|
||||
*/
|
||||
public function aiTrend()
|
||||
{
|
||||
$code = $this->request->get('code', '');
|
||||
if (empty($code)) {
|
||||
return $this->fail('参数缺失');
|
||||
}
|
||||
|
||||
$game = LotteryGame::where('code', $code)->findOrEmpty();
|
||||
if ($game->isEmpty()) {
|
||||
return $this->fail('彩种不存在');
|
||||
}
|
||||
|
||||
$recentDraws = $this->getRecentDrawCodes($code, 30);
|
||||
$lotteryData = [
|
||||
'lottery_type' => $code,
|
||||
'lottery_name' => $game['name'],
|
||||
'draw_rule' => $game['template'] ?? '',
|
||||
'recent_draws' => $recentDraws,
|
||||
];
|
||||
|
||||
$result = AiService::analyzeLotteryModule($lotteryData, 'trend', $this->userId);
|
||||
if (!$result['success']) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败');
|
||||
}
|
||||
return $this->data($result['data']);
|
||||
return $this->respondLotteryModule('trend');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -525,29 +401,7 @@ class LotteryController extends BaseApiController
|
||||
*/
|
||||
public function aiStats()
|
||||
{
|
||||
$code = $this->request->get('code', '');
|
||||
if (empty($code)) {
|
||||
return $this->fail('参数缺失');
|
||||
}
|
||||
|
||||
$game = LotteryGame::where('code', $code)->findOrEmpty();
|
||||
if ($game->isEmpty()) {
|
||||
return $this->fail('彩种不存在');
|
||||
}
|
||||
|
||||
$recentDraws = $this->getRecentDrawCodes($code, 30);
|
||||
$lotteryData = [
|
||||
'lottery_type' => $code,
|
||||
'lottery_name' => $game['name'],
|
||||
'draw_rule' => $game['template'] ?? '',
|
||||
'recent_draws' => $recentDraws,
|
||||
];
|
||||
|
||||
$result = AiService::analyzeLotteryModule($lotteryData, 'stats', $this->userId);
|
||||
if (!$result['success']) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败');
|
||||
}
|
||||
return $this->data($result['data']);
|
||||
return $this->respondLotteryModule('stats');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -555,42 +409,7 @@ class LotteryController extends BaseApiController
|
||||
*/
|
||||
public function aiColorZodiac()
|
||||
{
|
||||
$code = $this->request->get('code', '');
|
||||
if (empty($code)) {
|
||||
return $this->fail('参数缺失');
|
||||
}
|
||||
|
||||
$game = LotteryGame::where('code', $code)->findOrEmpty();
|
||||
if ($game->isEmpty()) {
|
||||
return $this->fail('彩种不存在');
|
||||
}
|
||||
|
||||
if (($game['template'] ?? '') !== 'HK6') {
|
||||
return $this->fail('该彩种不支持波色生肖分析');
|
||||
}
|
||||
|
||||
$recentDraws = $this->getRecentDrawCodes($code, 30);
|
||||
|
||||
$year = (int) date('Y');
|
||||
$colorMap = LotteryNumberMapping::getMapByType($year, LotteryNumberMapping::TYPE_COLOR);
|
||||
$zodiacMap = LotteryNumberMapping::getMapByType($year, LotteryNumberMapping::TYPE_ZODIAC);
|
||||
|
||||
$lotteryData = [
|
||||
'lottery_type' => $code,
|
||||
'lottery_name' => $game['name'],
|
||||
'draw_rule' => $game['template'] ?? '',
|
||||
'recent_draws' => $recentDraws,
|
||||
'number_mapping' => [
|
||||
'color' => $colorMap,
|
||||
'zodiac' => $zodiacMap,
|
||||
],
|
||||
];
|
||||
|
||||
$result = AiService::analyzeLotteryModule($lotteryData, 'color_zodiac', $this->userId);
|
||||
if (!$result['success']) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败');
|
||||
}
|
||||
return $this->data($result['data']);
|
||||
return $this->respondLotteryModule('color_zodiac');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -638,4 +457,36 @@ class LotteryController extends BaseApiController
|
||||
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
private function respondLotteryModule(string $module)
|
||||
{
|
||||
$result = AiAnalysisRequestService::analyzeLotteryModule(
|
||||
$this->request->get('code', ''),
|
||||
$module,
|
||||
$this->userId
|
||||
);
|
||||
if (empty($result['success'])) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败', [
|
||||
'remain_count' => $result['remain_count'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
$data = is_array($result['data'] ?? null) ? $result['data'] : [];
|
||||
$data['remain_count'] = $result['remain_count'] ?? 0;
|
||||
$data['unlocked'] = $result['unlocked'] ?? true;
|
||||
$data['charged'] = $result['charged'] ?? false;
|
||||
return $this->data($data);
|
||||
}
|
||||
|
||||
private function respondAnalysis(array $result)
|
||||
{
|
||||
if (empty($result['success'])) {
|
||||
return $this->fail($result['error'] ?? 'AI分析失败', [
|
||||
'remain_count' => $result['remain_count'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
unset($result['success'], $result['error'], $result['code']);
|
||||
return $this->data($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model\ai;
|
||||
|
||||
use app\common\model\BaseModel;
|
||||
|
||||
class AiQuotaUnlock extends BaseModel
|
||||
{
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace app\common\model\ai;
|
||||
|
||||
use app\common\model\BaseModel;
|
||||
use app\common\model\user\User;
|
||||
use app\common\service\ai\AiQuotaService;
|
||||
|
||||
class AiUnlock extends BaseModel
|
||||
@@ -13,7 +12,9 @@ class AiUnlock extends BaseModel
|
||||
*/
|
||||
public static function isUnlocked(int $userId, int $matchId): bool
|
||||
{
|
||||
return self::where('user_id', $userId)->where('match_id', $matchId)->count() > 0;
|
||||
$analysisKey = AiQuotaService::makeKey(AiQuotaService::TYPE_MATCH, $matchId);
|
||||
return AiQuotaService::hasUnlocked($userId, $analysisKey)
|
||||
|| self::where('user_id', $userId)->where('match_id', $matchId)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,42 +23,20 @@ class AiUnlock extends BaseModel
|
||||
*/
|
||||
public static function unlock(int $userId, int $matchId): array
|
||||
{
|
||||
// 已解锁则直接返回
|
||||
if (self::isUnlocked($userId, $matchId)) {
|
||||
return ['ok' => true, 'msg' => '已解锁'];
|
||||
}
|
||||
|
||||
// 检查今日剩余次数
|
||||
$user = User::findOrEmpty($userId);
|
||||
if ($user->isEmpty()) {
|
||||
return ['ok' => false, 'msg' => '用户不存在'];
|
||||
}
|
||||
|
||||
// VIP用户不限次数
|
||||
if (AiQuotaService::isVip($user)) {
|
||||
$analysisKey = AiQuotaService::makeKey(AiQuotaService::TYPE_MATCH, $matchId);
|
||||
$result = AiQuotaService::unlock($userId, AiQuotaService::TYPE_MATCH, $analysisKey, $matchId);
|
||||
if (!empty($result['ok']) && self::where('user_id', $userId)->where('match_id', $matchId)->count() <= 0) {
|
||||
self::create([
|
||||
'user_id' => $userId,
|
||||
'match_id' => $matchId,
|
||||
]);
|
||||
return ['ok' => true, 'msg' => 'VIP解锁'];
|
||||
}
|
||||
|
||||
$freeCount = AiQuotaService::resetDailyIfNeeded($user);
|
||||
|
||||
if ($freeCount <= 0) {
|
||||
return ['ok' => false, 'msg' => '今日免费次数已用完,升级VIP可无限使用'];
|
||||
}
|
||||
|
||||
// 扣次数 + 写解锁记录
|
||||
$user->ai_free_count = $freeCount - 1;
|
||||
$user->save();
|
||||
|
||||
self::create([
|
||||
'user_id' => $userId,
|
||||
'match_id' => $matchId,
|
||||
]);
|
||||
|
||||
return ['ok' => true, 'msg' => '解锁成功', 'remain' => $user->ai_free_count];
|
||||
return [
|
||||
'ok' => (bool) ($result['ok'] ?? false),
|
||||
'msg' => (string) ($result['msg'] ?? ''),
|
||||
'remain' => $result['remain'] ?? AiQuotaService::getRemainCount($userId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,759 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\ai;
|
||||
|
||||
use app\common\enum\YesNoEnum;
|
||||
use app\common\model\article\Article;
|
||||
use app\common\model\community\CommunityPost;
|
||||
use app\common\model\community\CommunityTag;
|
||||
use app\common\model\lottery\LotteryCategory;
|
||||
use app\common\model\lottery\LotteryDraw;
|
||||
use app\common\model\lottery\LotteryDrawResult;
|
||||
use app\common\model\lottery\LotteryGame;
|
||||
use app\common\model\lottery\LotteryNumberMapping;
|
||||
use app\common\model\match\MatchEvent;
|
||||
use app\common\model\match\MatchHistory;
|
||||
use app\common\model\match\MatchRecent;
|
||||
use app\common\service\FileService;
|
||||
use think\facade\Db;
|
||||
|
||||
class AiAnalysisRequestService
|
||||
{
|
||||
private const LIUHE_TAGS = ['旧澳六合', '新澳六合'];
|
||||
private const MATCH_SECTIONS = ['prediction', 'factors', 'odds', 'summary'];
|
||||
private const LOTTERY_MODULES = ['hot_cold', 'trend', 'stats', 'color_zodiac'];
|
||||
|
||||
public static function analyze(string $type, array $params, int $userId): array
|
||||
{
|
||||
$type = strtolower(str_replace('-', '_', trim($type)));
|
||||
|
||||
return match ($type) {
|
||||
'match', 'match_section' => self::analyzeMatchSection(
|
||||
(int) ($params['match_id'] ?? 0),
|
||||
(string) ($params['section'] ?? 'prediction'),
|
||||
$userId
|
||||
),
|
||||
'match_full', 'match_predict' => self::analyzeMatchFull((int) ($params['match_id'] ?? 0), $userId),
|
||||
'article' => self::analyzeArticle((int) ($params['article_id'] ?? 0), $userId),
|
||||
'post' => self::analyzePost((int) ($params['post_id'] ?? 0), $userId, (int) ($params['force'] ?? 0) === 1),
|
||||
'lottery' => self::analyzeLotteryOverview($params, $userId),
|
||||
'lottery_module' => self::analyzeLotteryModule(
|
||||
(string) ($params['code'] ?? ''),
|
||||
(string) ($params['module'] ?? ''),
|
||||
$userId
|
||||
),
|
||||
'credibility', 'user_credibility' => self::analyzeUserCredibility((int) ($params['user_id'] ?? 0), $userId),
|
||||
default => self::fail('不支持的AI分析类型'),
|
||||
};
|
||||
}
|
||||
|
||||
public static function analyzeMatchFull(int $matchId, int $userId): array
|
||||
{
|
||||
if ($matchId <= 0) {
|
||||
return self::fail('参数缺失');
|
||||
}
|
||||
|
||||
$matchData = self::buildMatchData($matchId, false);
|
||||
if (empty($matchData['success'])) {
|
||||
return $matchData;
|
||||
}
|
||||
|
||||
$quota = self::ensureQuota($userId, AiQuotaService::TYPE_MATCH, $matchId, $matchId);
|
||||
if (empty($quota['success'])) {
|
||||
return $quota;
|
||||
}
|
||||
|
||||
$result = AiService::predictMatch($matchId, $matchData['data'], $userId);
|
||||
if (!$result['success']) {
|
||||
return self::fail($result['error'] ?? 'AI分析失败', 0, $quota['quota']);
|
||||
}
|
||||
|
||||
return self::withQuota([
|
||||
'analysis' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
'match_info' => $matchData['data']['match_info'],
|
||||
], $quota['quota']);
|
||||
}
|
||||
|
||||
public static function analyzeMatchSection(int $matchId, string $section, int $userId): array
|
||||
{
|
||||
$section = trim($section);
|
||||
if ($matchId <= 0 || $section === '') {
|
||||
return self::fail('参数缺失');
|
||||
}
|
||||
if (!in_array($section, self::MATCH_SECTIONS, true)) {
|
||||
return self::fail('无效的分析模块');
|
||||
}
|
||||
|
||||
$matchData = self::buildMatchData($matchId, true);
|
||||
if (empty($matchData['success'])) {
|
||||
return $matchData;
|
||||
}
|
||||
|
||||
$quota = self::ensureQuota($userId, AiQuotaService::TYPE_MATCH, $matchId, $matchId);
|
||||
if (empty($quota['success'])) {
|
||||
return $quota;
|
||||
}
|
||||
|
||||
$result = AiService::predictMatchSection($matchId, $section, $matchData['data'], $userId);
|
||||
if (!$result['success']) {
|
||||
return self::fail($result['error'] ?? 'AI分析失败', 0, $quota['quota']);
|
||||
}
|
||||
|
||||
return self::withQuota([
|
||||
'section' => $section,
|
||||
'data' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
'match_info' => $matchData['data']['match_info'],
|
||||
], $quota['quota']);
|
||||
}
|
||||
|
||||
public static function analyzeArticle(int $articleId, int $userId): array
|
||||
{
|
||||
if ($articleId <= 0) {
|
||||
return self::fail('参数缺失');
|
||||
}
|
||||
|
||||
$article = Article::where(['id' => $articleId, 'is_show' => YesNoEnum::YES])->findOrEmpty();
|
||||
if ($article->isEmpty()) {
|
||||
return self::fail('文章不存在');
|
||||
}
|
||||
|
||||
$quota = self::ensureQuota($userId, AiQuotaService::TYPE_ARTICLE, $articleId, $articleId);
|
||||
if (empty($quota['success'])) {
|
||||
return $quota;
|
||||
}
|
||||
|
||||
$result = AiService::analyzeArticle($articleId, $article->toArray(), $userId);
|
||||
if (!$result['success']) {
|
||||
return self::fail($result['error'] ?? 'AI分析失败', 0, $quota['quota']);
|
||||
}
|
||||
|
||||
return self::withQuota([
|
||||
'analysis' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
], $quota['quota']);
|
||||
}
|
||||
|
||||
public static function analyzeUserCredibility(int $targetUserId, int $userId): array
|
||||
{
|
||||
if ($targetUserId <= 0) {
|
||||
return self::fail('参数缺失');
|
||||
}
|
||||
|
||||
$quota = self::ensureQuota($userId, AiQuotaService::TYPE_CREDIBILITY, $targetUserId, $targetUserId);
|
||||
if (empty($quota['success'])) {
|
||||
return $quota;
|
||||
}
|
||||
|
||||
$userData = [
|
||||
'user_id' => $targetUserId,
|
||||
'register_days' => 30,
|
||||
'post_count' => 0,
|
||||
'comment_count' => 0,
|
||||
'like_received' => 0,
|
||||
];
|
||||
|
||||
$result = AiService::analyzeCredibility($targetUserId, $userData, $userId);
|
||||
if (!$result['success']) {
|
||||
return self::fail($result['error'] ?? 'AI分析失败', 0, $quota['quota']);
|
||||
}
|
||||
|
||||
return self::withQuota([
|
||||
'analysis' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
], $quota['quota']);
|
||||
}
|
||||
|
||||
public static function analyzePost(int $postId, int $userId, bool $force = false): array
|
||||
{
|
||||
if ($postId <= 0) {
|
||||
return self::fail('参数缺失');
|
||||
}
|
||||
|
||||
try {
|
||||
$post = CommunityPost::where('id', $postId)->where('status', 1)->findOrEmpty();
|
||||
if ($post->isEmpty()) {
|
||||
return self::fail('帖子不存在');
|
||||
}
|
||||
|
||||
$data = $post->toArray();
|
||||
$ext = is_array($data['ext'] ?? null) ? $data['ext'] : (json_decode((string) ($data['ext'] ?? ''), true) ?: []);
|
||||
$tags = self::getPostTags($postId);
|
||||
if (self::isTrumpPost($tags)) {
|
||||
return self::fail('特朗普帖子仅支持翻译');
|
||||
}
|
||||
if (!self::isLiuhePost($tags, $ext)) {
|
||||
return self::fail('该帖子类型无需AI分析');
|
||||
}
|
||||
|
||||
$quota = self::ensureQuota($userId, AiQuotaService::TYPE_POST, $postId, $postId);
|
||||
if (empty($quota['success'])) {
|
||||
return $quota;
|
||||
}
|
||||
|
||||
$analysis = self::ensureLiuheAnalysisContent($post, $ext);
|
||||
if (!$analysis['success']) {
|
||||
return self::fail($analysis['error'] ?? '图片分析失败', 0, $quota['quota']);
|
||||
}
|
||||
|
||||
$ext = $analysis['ext'];
|
||||
$data['tags'] = array_values($tags);
|
||||
$data['ext'] = $ext;
|
||||
if (!empty($ext['translated_content'])) {
|
||||
$data['translated_content'] = (string) $ext['translated_content'];
|
||||
}
|
||||
if (!empty($ext['lottery_analysis_content'])) {
|
||||
$data['lottery_analysis_content'] = (string) $ext['lottery_analysis_content'];
|
||||
}
|
||||
|
||||
$result = AiService::analyzePost($postId, $data, $userId, true, $force);
|
||||
if (!$result['success']) {
|
||||
return self::fail($result['error'] ?? 'AI分析失败', 0, $quota['quota']);
|
||||
}
|
||||
|
||||
return self::withQuota([
|
||||
'analysis' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
], $quota['quota']);
|
||||
} catch (\Throwable $e) {
|
||||
return self::fail('AI分析异常: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function analyzeLotteryOverview(array $params, int $userId): array
|
||||
{
|
||||
$prepared = self::buildLotteryOverviewData($params);
|
||||
if (empty($prepared['success'])) {
|
||||
return $prepared;
|
||||
}
|
||||
|
||||
$quota = self::ensureQuota(
|
||||
$userId,
|
||||
AiQuotaService::TYPE_LOTTERY,
|
||||
self::lotteryQuotaRef($prepared['code'], $prepared['latest_issue']),
|
||||
self::crcRefId($prepared['code'] . ':' . $prepared['latest_issue'])
|
||||
);
|
||||
if (empty($quota['success'])) {
|
||||
return $quota;
|
||||
}
|
||||
|
||||
$result = AiService::analyzeLottery($prepared['lottery_data'], $userId);
|
||||
if (!$result['success']) {
|
||||
return self::fail($result['error'] ?? 'AI分析失败', 0, $quota['quota']);
|
||||
}
|
||||
|
||||
return self::withQuota([
|
||||
'analysis' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
'category_name' => $prepared['category_name'],
|
||||
], $quota['quota']);
|
||||
}
|
||||
|
||||
public static function analyzeLotteryModule(string $code, string $module, int $userId): array
|
||||
{
|
||||
$code = trim($code);
|
||||
$module = trim($module);
|
||||
if ($code === '' || $module === '') {
|
||||
return self::fail('参数缺失');
|
||||
}
|
||||
if (!in_array($module, self::LOTTERY_MODULES, true)) {
|
||||
return self::fail('无效的分析模块');
|
||||
}
|
||||
|
||||
$prepared = self::buildLotteryModuleData($code, $module);
|
||||
if (empty($prepared['success'])) {
|
||||
return $prepared;
|
||||
}
|
||||
|
||||
$quota = self::ensureQuota(
|
||||
$userId,
|
||||
AiQuotaService::TYPE_LOTTERY,
|
||||
self::lotteryQuotaRef($code, $prepared['latest_issue']),
|
||||
self::crcRefId($code . ':' . $prepared['latest_issue'])
|
||||
);
|
||||
if (empty($quota['success'])) {
|
||||
return $quota;
|
||||
}
|
||||
|
||||
$result = AiService::analyzeLotteryModule($prepared['lottery_data'], $module, $userId);
|
||||
if (!$result['success']) {
|
||||
return self::fail($result['error'] ?? 'AI分析失败', 0, $quota['quota']);
|
||||
}
|
||||
|
||||
return self::withQuota([
|
||||
'module' => $module,
|
||||
'data' => $result['data'],
|
||||
'from_cache' => $result['from_cache'] ?? false,
|
||||
], $quota['quota']);
|
||||
}
|
||||
|
||||
private static function buildMatchData(int $matchId, bool $includeOdds): array
|
||||
{
|
||||
$match = MatchEvent::where('id', $matchId)->where('is_show', 1)->findOrEmpty();
|
||||
if ($match->isEmpty()) {
|
||||
return self::fail('赛事不存在');
|
||||
}
|
||||
|
||||
$data = $match->toArray();
|
||||
$homeTeam = $data['home_team'];
|
||||
$awayTeam = $data['away_team'];
|
||||
$matchInfo = [
|
||||
'home_team' => $homeTeam,
|
||||
'away_team' => $awayTeam,
|
||||
'league' => $data['league_name'] ?? '',
|
||||
'match_time' => date('Y-m-d H:i', $data['match_time'] ?? time()),
|
||||
'status' => $data['status'] ?? 0,
|
||||
];
|
||||
|
||||
if ($includeOdds) {
|
||||
$matchInfo += [
|
||||
'home_score' => $data['home_score'] ?? 0,
|
||||
'away_score' => $data['away_score'] ?? 0,
|
||||
'home_odds' => $data['home_odds'] ?? 0,
|
||||
'draw_odds' => $data['draw_odds'] ?? 0,
|
||||
'away_odds' => $data['away_odds'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'match_info' => $matchInfo,
|
||||
'head_to_head' => MatchHistory::where(function ($query) use ($homeTeam, $awayTeam) {
|
||||
$query->where([['home_team', '=', $homeTeam], ['away_team', '=', $awayTeam]])
|
||||
->whereOr([['home_team', '=', $awayTeam], ['away_team', '=', $homeTeam]]);
|
||||
})->order('match_time desc')->limit(5)->select()->toArray(),
|
||||
'home_recent' => MatchRecent::where('team_name', $homeTeam)
|
||||
->order('match_time desc')->limit(5)->select()->toArray(),
|
||||
'away_recent' => MatchRecent::where('team_name', $awayTeam)
|
||||
->order('match_time desc')->limit(5)->select()->toArray(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private static function buildLotteryOverviewData(array $params): array
|
||||
{
|
||||
$categoryId = (int) ($params['category_id'] ?? 0);
|
||||
$code = trim((string) ($params['code'] ?? ''));
|
||||
|
||||
if ($code !== '') {
|
||||
$game = LotteryGame::where('code', $code)->findOrEmpty();
|
||||
if ($game->isEmpty()) {
|
||||
return self::fail('彩种不存在');
|
||||
}
|
||||
|
||||
$recentDraws = LotteryDrawResult::where('code', $code)
|
||||
->where('draw_code', '<>', '')
|
||||
->order('draw_time desc, issue desc')
|
||||
->limit(30)
|
||||
->field('issue,draw_time,draw_code')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($recentDraws as &$draw) {
|
||||
$draw['numbers'] = $draw['draw_code'] ? explode(',', $draw['draw_code']) : [];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'code' => $code,
|
||||
'latest_issue' => (string) ($recentDraws[0]['issue'] ?? ''),
|
||||
'category_name' => $game['name'],
|
||||
'lottery_data' => [
|
||||
'lottery_type' => $code,
|
||||
'lottery_name' => $game['name'],
|
||||
'draw_rule' => $game['template'] ?? '',
|
||||
'recent_draws' => $recentDraws,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($categoryId <= 0) {
|
||||
return self::fail('参数缺失');
|
||||
}
|
||||
|
||||
$category = LotteryCategory::where('id', $categoryId)->findOrEmpty();
|
||||
if ($category->isEmpty()) {
|
||||
return self::fail('彩种不存在');
|
||||
}
|
||||
|
||||
$recentDraws = LotteryDraw::where('category_id', $categoryId)
|
||||
->where('status', 1)
|
||||
->where('is_show', 1)
|
||||
->order('draw_date desc, period desc')
|
||||
->limit(30)
|
||||
->field('period,draw_date,numbers,special_number,zodiac,color')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($recentDraws as &$draw) {
|
||||
$draw['numbers'] = is_string($draw['numbers']) ? json_decode($draw['numbers'], true) : $draw['numbers'];
|
||||
$draw['zodiac'] = is_string($draw['zodiac']) ? json_decode($draw['zodiac'], true) : $draw['zodiac'];
|
||||
$draw['color'] = is_string($draw['color']) ? json_decode($draw['color'], true) : $draw['color'];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'code' => (string) $category['code'],
|
||||
'latest_issue' => (string) ($recentDraws[0]['period'] ?? ''),
|
||||
'category_name' => $category['name'],
|
||||
'lottery_data' => [
|
||||
'lottery_type' => $category['code'],
|
||||
'lottery_name' => $category['name'],
|
||||
'draw_rule' => $category['draw_rule'],
|
||||
'recent_draws' => $recentDraws,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private static function buildLotteryModuleData(string $code, string $module): array
|
||||
{
|
||||
$game = LotteryGame::where('code', $code)->findOrEmpty();
|
||||
if ($game->isEmpty()) {
|
||||
return self::fail('彩种不存在');
|
||||
}
|
||||
|
||||
if ($module === 'color_zodiac' && ($game['template'] ?? '') !== 'HK6') {
|
||||
return self::fail('该彩种不支持波色生肖分析');
|
||||
}
|
||||
|
||||
$recentDraws = self::getRecentDrawCodes($code, 30);
|
||||
$lotteryData = [
|
||||
'lottery_type' => $code,
|
||||
'lottery_name' => $game['name'],
|
||||
'draw_rule' => $game['template'] ?? '',
|
||||
'recent_draws' => $recentDraws,
|
||||
];
|
||||
|
||||
if ($module === 'color_zodiac') {
|
||||
$year = (int) date('Y');
|
||||
$lotteryData['number_mapping'] = [
|
||||
'color' => LotteryNumberMapping::getMapByType($year, LotteryNumberMapping::TYPE_COLOR),
|
||||
'zodiac' => LotteryNumberMapping::getMapByType($year, LotteryNumberMapping::TYPE_ZODIAC),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'latest_issue' => (string) ($recentDraws[0]['issue'] ?? ''),
|
||||
'lottery_data' => $lotteryData,
|
||||
];
|
||||
}
|
||||
|
||||
private static function getRecentDrawCodes(string $code, int $limit = 30): array
|
||||
{
|
||||
$rows = LotteryDrawResult::where('code', $code)
|
||||
->where('draw_code', '<>', '')
|
||||
->order('draw_time desc, issue desc')
|
||||
->limit($limit)
|
||||
->field('issue,draw_time,draw_code')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$row['numbers'] = $row['draw_code'] ? array_map('intval', explode(',', $row['draw_code'])) : [];
|
||||
unset($row['draw_code']);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
private static function ensureQuota(int $userId, string $analysisType, $refId, int $dbRefId): array
|
||||
{
|
||||
$analysisKey = AiQuotaService::makeKey($analysisType, $refId);
|
||||
$quota = AiQuotaService::unlock($userId, $analysisType, $analysisKey, $dbRefId);
|
||||
if (empty($quota['ok'])) {
|
||||
return self::fail((string) ($quota['msg'] ?? '今日免费次数已用完'), (int) ($quota['code'] ?? 0), $quota);
|
||||
}
|
||||
|
||||
return ['success' => true, 'quota' => $quota];
|
||||
}
|
||||
|
||||
private static function withQuota(array $payload, array $quota): array
|
||||
{
|
||||
return array_merge(['success' => true], $payload, [
|
||||
'remain_count' => $quota['remain'] ?? 0,
|
||||
'unlocked' => $quota['unlocked'] ?? true,
|
||||
'charged' => $quota['charged'] ?? false,
|
||||
]);
|
||||
}
|
||||
|
||||
private static function fail(string $error, int $code = 0, array $quota = []): array
|
||||
{
|
||||
$result = ['success' => false, 'error' => $error, 'code' => $code];
|
||||
if (array_key_exists('remain', $quota)) {
|
||||
$result['remain_count'] = $quota['remain'];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function lotteryQuotaRef(string $code, string $latestIssue): string
|
||||
{
|
||||
return $latestIssue !== '' ? $code . ':' . $latestIssue : $code;
|
||||
}
|
||||
|
||||
private static function crcRefId(string $value): int
|
||||
{
|
||||
return (int) sprintf('%u', crc32($value));
|
||||
}
|
||||
|
||||
private static function getPostTags(int $postId): array
|
||||
{
|
||||
$tagIds = Db::name('community_post_tag')->where('post_id', $postId)->column('tag_id');
|
||||
if (empty($tagIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(CommunityTag::whereIn('id', $tagIds)->column('name'));
|
||||
}
|
||||
|
||||
private static function isTrumpPost(array $tags): bool
|
||||
{
|
||||
return in_array('特朗普', $tags, true);
|
||||
}
|
||||
|
||||
private static function isLiuhePost(array $tags, array $ext = []): bool
|
||||
{
|
||||
foreach (self::LIUHE_TAGS as $tag) {
|
||||
if (in_array($tag, $tags, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$lotteryKey = (string) ($ext['lottery_key'] ?? '');
|
||||
return in_array($lotteryKey, ['a6', 'xa6'], true);
|
||||
}
|
||||
|
||||
private static function localizeLiuhePostImagesForAi(CommunityPost $post, array $images): array
|
||||
{
|
||||
$localizedImages = [];
|
||||
$changed = false;
|
||||
|
||||
foreach ($images as $image) {
|
||||
$image = trim((string) $image);
|
||||
if ($image === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (self::isLocalUploadImage($image)) {
|
||||
$localUrl = self::normalizeLocalUploadImageUrl($image);
|
||||
$localizedImages[] = $localUrl;
|
||||
$changed = $changed || $localUrl !== $image;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!preg_match('/^https?:\/\//i', $image)) {
|
||||
$localUrl = FileService::getFileUrl(ltrim($image, '/'));
|
||||
$localizedImages[] = $localUrl;
|
||||
$changed = $changed || $localUrl !== $image;
|
||||
continue;
|
||||
}
|
||||
|
||||
$localUrl = self::saveRemoteImageForAiAnalysis($image);
|
||||
if ($localUrl === '') {
|
||||
return ['success' => false, 'error' => '图片下载到本地失败,请稍后重试', 'images' => []];
|
||||
}
|
||||
|
||||
$localizedImages[] = $localUrl;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if (empty($localizedImages)) {
|
||||
return ['success' => false, 'error' => '帖子图片为空,无法分析', 'images' => []];
|
||||
}
|
||||
|
||||
if ($changed || $localizedImages !== array_values($images)) {
|
||||
$now = time();
|
||||
Db::name('community_post')->where('id', (int) $post->id)->update([
|
||||
'images' => json_encode($localizedImages, JSON_UNESCAPED_UNICODE),
|
||||
'update_time' => $now,
|
||||
]);
|
||||
|
||||
Db::name('community_post_image')->where('post_id', (int) $post->id)->delete();
|
||||
foreach ($localizedImages as $index => $imageUrl) {
|
||||
Db::name('community_post_image')->insert([
|
||||
'post_id' => (int) $post->id,
|
||||
'image_url' => $imageUrl,
|
||||
'sort' => $index,
|
||||
'create_time' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => true, 'images' => $localizedImages, 'changed' => $changed];
|
||||
}
|
||||
|
||||
private static function isLocalUploadImage(string $image): bool
|
||||
{
|
||||
if (!preg_match('/^https?:\/\//i', $image)) {
|
||||
return str_starts_with(ltrim($image, '/'), 'uploads/');
|
||||
}
|
||||
|
||||
$path = (string) (parse_url($image, PHP_URL_PATH) ?: '');
|
||||
if (!str_starts_with($path, '/uploads/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$imageHost = strtolower((string) (parse_url($image, PHP_URL_HOST) ?: ''));
|
||||
$currentHost = strtolower((string) (parse_url(request()->domain(), PHP_URL_HOST) ?: ''));
|
||||
return $imageHost !== '' && $currentHost !== '' && $imageHost === $currentHost;
|
||||
}
|
||||
|
||||
private static function normalizeLocalUploadImageUrl(string $image): string
|
||||
{
|
||||
if (preg_match('/^https?:\/\//i', $image)) {
|
||||
return $image;
|
||||
}
|
||||
|
||||
return FileService::getFileUrl(ltrim($image, '/'));
|
||||
}
|
||||
|
||||
private static function saveRemoteImageForAiAnalysis(string $imageUrl): string
|
||||
{
|
||||
$ch = curl_init($imageUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_TIMEOUT => 25,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_USERAGENT => 'Mozilla/5.0 SportEraBot/1.0',
|
||||
]);
|
||||
|
||||
$body = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200 || !is_string($body) || $body === '' || strlen($body) > 8 * 1024 * 1024) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$mime = strtolower(trim(explode(';', $contentType)[0] ?? ''));
|
||||
$imageInfo = @getimagesizefromstring($body);
|
||||
if (is_array($imageInfo) && !empty($imageInfo['mime'])) {
|
||||
$mime = strtolower((string) $imageInfo['mime']);
|
||||
}
|
||||
|
||||
$extension = match ($mime) {
|
||||
'image/jpeg', 'image/jpg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/webp' => 'webp',
|
||||
'image/gif' => 'gif',
|
||||
default => '',
|
||||
};
|
||||
|
||||
if ($extension === '') {
|
||||
$path = parse_url($imageUrl, PHP_URL_PATH) ?: '';
|
||||
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
if (!in_array($extension, ['jpg', 'jpeg', 'png', 'webp', 'gif'], true)) {
|
||||
return '';
|
||||
}
|
||||
$extension = $extension === 'jpeg' ? 'jpg' : $extension;
|
||||
}
|
||||
|
||||
$relativeDir = 'uploads/ai_lottery_posts/' . date('Ymd');
|
||||
$publicRoot = rtrim(public_path(), DIRECTORY_SEPARATOR . '/');
|
||||
$targetDir = $publicRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativeDir);
|
||||
if (!is_dir($targetDir) && !@mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
|
||||
return '';
|
||||
}
|
||||
if (!is_writable($targetDir)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$filename = sha1($imageUrl . "\n" . $body) . '.' . $extension;
|
||||
$relativeUri = $relativeDir . '/' . $filename;
|
||||
$targetPath = $targetDir . DIRECTORY_SEPARATOR . $filename;
|
||||
|
||||
if (!is_file($targetPath) && @file_put_contents($targetPath, $body) === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return FileService::getFileUrl($relativeUri);
|
||||
}
|
||||
|
||||
private static function ensureLiuheAnalysisContent(CommunityPost $post, array $ext): array
|
||||
{
|
||||
$images = is_array($post->images) ? array_values($post->images) : [];
|
||||
$originalImageHash = md5(json_encode($images, JSON_UNESCAPED_UNICODE));
|
||||
$localizeResult = self::localizeLiuhePostImagesForAi($post, $images);
|
||||
if (empty($localizeResult['success'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => (string) ($localizeResult['error'] ?? '图片下载到本地失败'),
|
||||
];
|
||||
}
|
||||
|
||||
$images = array_values($localizeResult['images']);
|
||||
$imageHash = md5(json_encode($images, JSON_UNESCAPED_UNICODE));
|
||||
$cacheSource = (string) ($ext['lottery_analysis_source'] ?? '');
|
||||
$cacheHash = (string) ($ext['lottery_analysis_images_hash'] ?? '');
|
||||
|
||||
if (
|
||||
!empty($ext['lottery_analysis_content'])
|
||||
&& $cacheSource === 'image_only'
|
||||
&& in_array($cacheHash, [$imageHash, $originalImageHash], true)
|
||||
) {
|
||||
if ($cacheHash !== $imageHash) {
|
||||
$ext['lottery_analysis_images_hash'] = $imageHash;
|
||||
CommunityPost::where('id', (int) $post->id)->update([
|
||||
'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
KbSyncService::enqueue('post', 'post', (int) $post->id, 'upsert', 45);
|
||||
}
|
||||
return ['success' => true, 'ext' => $ext, 'from_cache' => true];
|
||||
}
|
||||
|
||||
$result = AiService::analyzeLotteryPostImages($post->content ?: '', $images);
|
||||
if (empty($result['success']) || empty($result['content'])) {
|
||||
$fallbackContent = self::buildLiuheImageAnalysisFallback($images, (string) ($result['error'] ?? ''));
|
||||
if ($fallbackContent === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => (string) ($result['error'] ?? '帖子图片分析失败'),
|
||||
];
|
||||
}
|
||||
|
||||
$result = [
|
||||
'success' => true,
|
||||
'content' => $fallbackContent,
|
||||
'tokens' => 0,
|
||||
'cost_ms' => (int) ($result['cost_ms'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
$ext['lottery_analysis_content'] = trim((string) $result['content']);
|
||||
$ext['lottery_analysis_source'] = 'image_only';
|
||||
$ext['lottery_analysis_images_hash'] = $imageHash;
|
||||
$ext['lottery_analysis_generated_at'] = time();
|
||||
|
||||
CommunityPost::where('id', (int) $post->id)->update([
|
||||
'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
KbSyncService::enqueue('post', 'post', (int) $post->id, 'upsert', 45);
|
||||
|
||||
return ['success' => true, 'ext' => $ext, 'from_cache' => false];
|
||||
}
|
||||
|
||||
private static function buildLiuheImageAnalysisFallback(array $images, string $error): string
|
||||
{
|
||||
$error = trim($error);
|
||||
$isTimeout = $error !== '' && (stripos($error, 'timed out') !== false
|
||||
|| stripos($error, 'timeout') !== false
|
||||
|| stripos($error, 'cURL错误') !== false);
|
||||
if (!$isTimeout) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return implode("\n", [
|
||||
'图片已下载并替换为站内链接,但视觉模型在限定时间内未返回完整识别结果。',
|
||||
'当前可确认信息:帖子包含 ' . count($images) . ' 张本地化图片,可在原帖图片区域查看。',
|
||||
'识别状态:图片文字、号码、生肖、波色等细项暂未可靠识别。',
|
||||
'处理建议:以下分析只能结合帖子文案和图片展示场景进行保守解读,不能视为开奖结果或中奖承诺。',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,21 @@
|
||||
|
||||
namespace app\common\service\ai;
|
||||
|
||||
use app\common\model\ai\AiQuotaUnlock;
|
||||
use app\common\model\user\User;
|
||||
use think\facade\Db;
|
||||
|
||||
class AiQuotaService
|
||||
{
|
||||
public const DAILY_FREE_COUNT = 3;
|
||||
public const VIP_REMAIN_COUNT = 999;
|
||||
|
||||
public const TYPE_MATCH = 'match';
|
||||
public const TYPE_ARTICLE = 'article';
|
||||
public const TYPE_POST = 'post';
|
||||
public const TYPE_LOTTERY = 'lottery';
|
||||
public const TYPE_CREDIBILITY = 'credibility';
|
||||
|
||||
public static function isVip(User $user): bool
|
||||
{
|
||||
return (int) $user->getData('is_vip') === 1 && (int) $user->getData('vip_expire_time') > time();
|
||||
@@ -41,4 +49,132 @@ class AiQuotaService
|
||||
|
||||
return self::resetDailyIfNeeded($user);
|
||||
}
|
||||
|
||||
public static function makeKey(string $analysisType, $refId = 0, array $parts = []): string
|
||||
{
|
||||
$segments = array_merge([$analysisType, (string) $refId], array_map(static function ($part) {
|
||||
return trim((string) $part);
|
||||
}, $parts));
|
||||
$segments = array_values(array_filter($segments, static fn($part) => $part !== ''));
|
||||
return self::normalizeKey(implode(':', $segments), $analysisType);
|
||||
}
|
||||
|
||||
public static function hasUnlocked(int $userId, string $analysisKey): bool
|
||||
{
|
||||
if ($userId <= 0 || $analysisKey === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return AiQuotaUnlock::where('user_id', $userId)
|
||||
->where('analysis_key', self::normalizeKey($analysisKey))
|
||||
->count() > 0;
|
||||
}
|
||||
|
||||
public static function unlock(int $userId, string $analysisType, string $analysisKey, int $refId = 0): array
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'msg' => '请先登录',
|
||||
'code' => 401,
|
||||
'remain' => 0,
|
||||
'unlocked' => false,
|
||||
'charged' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$analysisType = trim($analysisType);
|
||||
$analysisKey = self::normalizeKey($analysisKey, $analysisType);
|
||||
if ($analysisType === '' || $analysisKey === '') {
|
||||
return ['ok' => false, 'msg' => 'AI分析类型缺失', 'remain' => self::getRemainCount($userId)];
|
||||
}
|
||||
|
||||
return Db::transaction(function () use ($userId, $analysisType, $analysisKey, $refId) {
|
||||
$user = User::where('id', $userId)->lock(true)->findOrEmpty();
|
||||
if ($user->isEmpty()) {
|
||||
return ['ok' => false, 'msg' => '用户不存在', 'remain' => 0];
|
||||
}
|
||||
|
||||
$existing = AiQuotaUnlock::where('user_id', $userId)
|
||||
->where('analysis_key', $analysisKey)
|
||||
->findOrEmpty();
|
||||
if (!$existing->isEmpty()) {
|
||||
return [
|
||||
'ok' => true,
|
||||
'msg' => '已解锁',
|
||||
'remain' => self::getRemainCountForUser($user),
|
||||
'unlocked' => true,
|
||||
'charged' => false,
|
||||
];
|
||||
}
|
||||
|
||||
if (self::isVip($user)) {
|
||||
self::createUnlockRecord($userId, $analysisType, $analysisKey, $refId);
|
||||
return [
|
||||
'ok' => true,
|
||||
'msg' => 'VIP解锁',
|
||||
'remain' => self::VIP_REMAIN_COUNT,
|
||||
'unlocked' => true,
|
||||
'charged' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$freeCount = self::resetDailyIfNeeded($user);
|
||||
if ($freeCount <= 0) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'msg' => '今日免费次数已用完,升级VIP可无限使用',
|
||||
'remain' => 0,
|
||||
'unlocked' => false,
|
||||
'charged' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$remain = $freeCount - 1;
|
||||
$user->save(['ai_free_count' => $remain]);
|
||||
self::createUnlockRecord($userId, $analysisType, $analysisKey, $refId);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'msg' => '解锁成功',
|
||||
'remain' => $remain,
|
||||
'unlocked' => true,
|
||||
'charged' => true,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private static function createUnlockRecord(int $userId, string $analysisType, string $analysisKey, int $refId): void
|
||||
{
|
||||
AiQuotaUnlock::create([
|
||||
'user_id' => $userId,
|
||||
'analysis_type' => $analysisType,
|
||||
'analysis_key' => $analysisKey,
|
||||
'ref_id' => $refId,
|
||||
]);
|
||||
}
|
||||
|
||||
private static function getRemainCountForUser(User $user): int
|
||||
{
|
||||
if (self::isVip($user)) {
|
||||
return self::VIP_REMAIN_COUNT;
|
||||
}
|
||||
|
||||
return self::resetDailyIfNeeded($user);
|
||||
}
|
||||
|
||||
private static function normalizeKey(string $analysisKey, string $analysisType = 'ai'): string
|
||||
{
|
||||
$analysisKey = trim(preg_replace('/\s+/', '', $analysisKey) ?? '');
|
||||
if ($analysisKey === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (strlen($analysisKey) <= 190) {
|
||||
return $analysisKey;
|
||||
}
|
||||
|
||||
$prefix = trim($analysisType) ?: 'ai';
|
||||
return $prefix . ':' . sha1($analysisKey);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user