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,36 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\api\lists\AccountLogLists;
/**
* 账户流水
* Class AccountLogController
* @package app\api\controller
*/
class AccountLogController extends BaseApiController
{
/**
* @notes 账户流水
* @return \think\response\Json
* @author 段誉
* @date 2023/2/24 14:34
*/
public function lists()
{
return $this->dataLists(new AccountLogLists());
}
}
+247
View File
@@ -0,0 +1,247 @@
<?php
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;
class AiController extends BaseApiController
{
public array $notNeedLogin = ['matchPredict', 'articleAnalysis'];
/**
* 赛事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);
}
/**
* 赛事AI预测 - 分段请求
*/
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),
]);
}
/**
* 资讯AI分析
*/
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,
]);
}
/**
* 用户可信度分析
*/
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,
]);
}
/**
* 彩票概率分析
*/
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分析失败');
}
return $this->data([
'analysis' => $result['data'],
'from_cache' => $result['from_cache'] ?? false,
]);
}
/**
* 查询比赛解锁状态
*/
public function checkUnlock()
{
$matchId = $this->request->get('match_id/d');
if (empty($matchId)) {
return $this->fail('参数缺失');
}
$unlocked = $this->userId ? AiUnlock::isUnlocked($this->userId, $matchId) : false;
$remain = $this->userId ? AiUnlock::getRemainCount($this->userId) : 0;
return $this->data([
'unlocked' => $unlocked,
'remain_count' => $remain,
]);
}
}
@@ -0,0 +1,115 @@
<?php
namespace app\api\controller;
use app\common\model\app\AppVersion;
use think\response\Json;
/**
* APP 版本检查(无需登录)
* Class AppVersionController
* @package app\api\controller
*/
class AppVersionController extends BaseApiController
{
public array $notNeedLogin = ['check', 'latest'];
/**
* @notes 获取各平台最新可用版本(供下载页使用,无需登录)
* 请求示例:GET /appVersion/latest
* 返回:{ android: {...}, ios: {...} }
*/
public function latest(): Json
{
$result = ['android' => null, 'ios' => null];
foreach ([1 => 'android', 2 => 'ios'] as $platform => $key) {
$version = AppVersion::where('platform', $platform)
->where('status', 1)
->where('package_type', '<>', 1) // 排除 wgt 热更包,只取整包
->order(['version_code' => 'desc', 'id' => 'desc'])
->append(['platform_text', 'package_type_text'])
->find();
if ($version) {
$result[$key] = [
'version_name' => $version['version_name'],
'version_code' => $version['version_code'],
'package_type' => $version['package_type'],
'package_type_text' => $version['package_type_text'],
'download_url' => $version['download_url'],
'title' => $version['title'],
'update_content' => $version['update_content'],
'create_time' => $version['create_time'],
];
}
}
return $this->data($result);
}
/**
* @notes 检查 APP 是否有新版本
* 请求示例:GET /appVersion/check?platform=1&version_code=1012&package_type=1
* - platform: 1=Android 2=iOS(必填)
* - version_code: 当前 APP 版本数字(必填)
* - package_type: 1=wgt热更 2=apk整包 3=ipa(可选,传则只比对该包类型)
*/
public function check(): Json
{
$platform = (int) $this->request->get('platform/d', 0);
$versionCode = (int) $this->request->get('version_code/d', 0);
$packageType = (int) $this->request->get('package_type/d', 0);
if (!in_array($platform, [1, 2])) {
return $this->fail('参数错误:platform');
}
if ($versionCode <= 0) {
return $this->fail('参数错误:version_code');
}
$query = AppVersion::where('platform', $platform)
->where('status', 1)
->where('version_code', '>', $versionCode);
if ($packageType > 0) {
$query->where('package_type', $packageType);
}
// wgt 热更需校验原生最低版本(min_version_code <= 当前version_code
// 同时找出符合条件的最新版本
$latest = $query->order(['version_code' => 'desc', 'id' => 'desc'])
->append(['platform_text', 'package_type_text', 'update_type_text'])
->find();
if (empty($latest)) {
return $this->data([
'has_update' => false,
]);
}
// wgt 包:当前 APP 原生版本必须 >= min_version_code
if ($latest['package_type'] == 1 && $latest['min_version_code'] > 0 && $versionCode < $latest['min_version_code']) {
// 当前原生版本太低无法 wgt 热更,返回无更新(让用户走整包升级路径)
return $this->data([
'has_update' => false,
]);
}
return $this->data([
'has_update' => true,
'id' => $latest['id'],
'version_name' => $latest['version_name'],
'version_code' => $latest['version_code'],
'min_version_code' => $latest['min_version_code'],
'platform' => $latest['platform'],
'platform_text' => $latest['platform_text'],
'package_type' => $latest['package_type'],
'package_type_text' => $latest['package_type_text'],
'update_type' => $latest['update_type'],
'update_type_text' => $latest['update_type_text'],
'download_url' => $latest['download_url'],
'title' => $latest['title'],
'update_content' => $latest['update_content'],
]);
}
}
@@ -0,0 +1,212 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\api\lists\article\ArticleCollectLists;
use app\api\lists\article\ArticleLists;
use app\api\logic\ArticleLogic;
use app\api\logic\ArticleInteractLogic;
use app\common\model\article\Article;
use app\common\service\article\ArticleImageProxyService;
/**
* 文章管理
* Class ArticleController
* @package app\api\controller
*/
class ArticleController extends BaseApiController
{
public array $notNeedLogin = ['lists', 'cate', 'detail', 'commentList', 'imageProxy'];
/**
* @notes 文章列表
* @return \think\response\Json
* @author 段誉
* @date 2022/9/20 15:30
*/
public function lists()
{
return $this->dataLists(new ArticleLists());
}
/**
* @notes 文章分类列表
* @return \think\response\Json
* @author 段誉
* @date 2022/9/20 15:30
*/
public function cate()
{
return $this->data(ArticleLogic::cate());
}
/**
* @notes 收藏列表
* @return \think\response\Json
* @author 段誉
* @date 2022/9/20 16:31
*/
public function collect()
{
return $this->dataLists(new ArticleCollectLists());
}
/**
* @notes 文章详情
* @return \think\response\Json
* @author 段誉
* @date 2022/9/20 17:09
*/
public function detail()
{
$id = $this->request->get('id/d');
$result = ArticleLogic::detail($id, $this->userId);
return $this->data($result);
}
/**
* @notes 文章翻译
* @return \think\response\Json
*/
public function translate()
{
$articleId = $this->request->post('article_id/d', 0);
if (empty($articleId)) {
$articleId = $this->request->post('id/d', 0);
}
if (empty($articleId)) {
$articleId = $this->request->get('article_id/d', 0);
}
if (empty($articleId)) {
$articleId = $this->request->get('id/d', 0);
}
if (empty($articleId)) {
return $this->fail('参数缺失');
}
if (!$this->userId) {
return $this->fail('请先登录', [], 0, 401);
}
$confirm = $this->request->post('confirm/d', 0);
$result = ArticleLogic::translate((int) $articleId, $this->userId, (int) $confirm);
if ($result['success'] === false) {
return $this->fail($result['error'] ?? '翻译失败');
}
return $this->data($result['data']);
}
/**
* @notes 加入收藏
* @return \think\response\Json
* @author 段誉
* @date 2022/9/20 17:01
*/
public function addCollect()
{
$articleId = $this->request->post('id/d');
ArticleLogic::addCollect($articleId, $this->userId);
return $this->success('操作成功');
}
/**
* @notes 取消收藏
* @return \think\response\Json
* @author 段誉
* @date 2022/9/20 17:01
*/
public function cancelCollect()
{
$articleId = $this->request->post('id/d');
ArticleLogic::cancelCollect($articleId, $this->userId);
return $this->success('操作成功');
}
/**
* @notes 评论列表
*/
public function commentList()
{
$articleId = $this->request->get('article_id/d');
$pageNo = $this->request->get('page_no/d', 1);
$pageSize = $this->request->get('page_size/d', 15);
$result = ArticleInteractLogic::commentList($articleId, $pageNo, $pageSize);
return $this->data($result);
}
/**
* @notes 远程文章图片代理
* @return \think\Response
*/
public function imageProxy()
{
$url = $this->request->get('url/s', '');
return ArticleImageProxyService::proxyRemoteImage($url);
}
/**
* @notes 发布评论
*/
public function addComment()
{
$params = $this->request->post();
if (empty($params['article_id']) || empty($params['content'])) {
return $this->fail('参数缺失');
}
$result = ArticleInteractLogic::addComment($this->userId, $params);
if ($result === false) {
return $this->fail(ArticleInteractLogic::getError());
}
return $this->success('评论成功', ['id' => $result]);
}
/**
* @notes 点赞/踩
*/
public function vote()
{
$articleId = $this->request->post('article_id/d');
$voteType = $this->request->post('vote_type/d', 1);
if (empty($articleId)) {
return $this->fail('参数缺失');
}
if (!in_array($voteType, [1, 2])) {
return $this->fail('投票类型错误');
}
$result = ArticleInteractLogic::vote($this->userId, $articleId, $voteType);
if ($result === false) {
return $this->fail(ArticleInteractLogic::getError());
}
// 返回最新计数
$article = Article::where('id', $articleId)->field('like_count,dislike_count')->findOrEmpty();
$result['like_count'] = $article->isEmpty() ? 0 : $article->like_count;
$result['dislike_count'] = $article->isEmpty() ? 0 : $article->dislike_count;
return $this->success('操作成功', $result);
}
}
@@ -0,0 +1,63 @@
<?php
namespace app\api\controller;
use app\common\service\ai\AiAssistantService;
class AssistantController extends BaseApiController
{
public array $notNeedLogin = ['chat', 'sessions', 'messages', 'clear'];
public function chat()
{
$message = trim((string) $this->request->post('message', ''));
$clientId = trim((string) $this->request->post('client_id', ''));
$sessionId = (int) $this->request->post('session_id/d', 0);
if ($message === '') {
return $this->fail('请输入要咨询的问题');
}
$result = AiAssistantService::chat($message, $this->userId, $clientId, $sessionId, $this->request->ip());
if (empty($result['success'])) {
return $this->fail($result['error'] ?? 'AI助手暂时不可用');
}
return $this->data($result['data']);
}
public function sessions()
{
$clientId = trim((string) $this->request->get('client_id', ''));
return $this->data(AiAssistantService::sessions($this->userId, $clientId));
}
public function messages()
{
$sessionId = (int) $this->request->get('session_id/d', 0);
$clientId = trim((string) $this->request->get('client_id', ''));
if ($sessionId <= 0) {
return $this->fail('会话参数缺失');
}
$result = AiAssistantService::messages($sessionId, $this->userId, $clientId);
if (empty($result['success'])) {
return $this->fail($result['error'] ?? '会话不存在');
}
return $this->data($result['data']);
}
public function clear()
{
$sessionId = (int) $this->request->post('session_id/d', 0);
$clientId = trim((string) $this->request->post('client_id', ''));
$result = AiAssistantService::clear($sessionId, $this->userId, $clientId);
if (empty($result['success'])) {
return $this->fail($result['error'] ?? '清空失败');
}
return $this->success('操作成功');
}
}
@@ -0,0 +1,33 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\common\controller\BaseLikeAdminController;
class BaseApiController extends BaseLikeAdminController
{
protected int $userId = 0;
protected array $userInfo = [];
public function initialize()
{
if (isset($this->request->userId)) {
$this->userId = (int) $this->request->userId;
}
if (isset($this->request->userInfo) && is_array($this->request->userInfo)) {
$this->userInfo = $this->request->userInfo;
}
}
}
@@ -0,0 +1,52 @@
<?php
namespace app\api\controller;
use app\common\service\chat\PrivateChatService;
class ChatController extends BaseApiController
{
public function sessions()
{
$page = (int) $this->request->get('page_no/d', 1);
$size = (int) $this->request->get('page_size/d', 20);
return $this->data(PrivateChatService::sessions($this->userId, $page, $size));
}
public function open()
{
$targetUserId = (int) $this->request->post('target_user_id/d', 0);
$result = PrivateChatService::open($this->userId, $targetUserId, true);
if (empty($result['success'])) {
return $this->fail($result['error'] ?? '打开会话失败');
}
return $this->data($result['data']);
}
public function messages()
{
$sessionId = (int) $this->request->get('session_id/d', 0);
$afterId = (int) $this->request->get('after_id/d', 0);
$page = (int) $this->request->get('page_no/d', 1);
$size = (int) $this->request->get('page_size/d', 50);
$result = PrivateChatService::messages($this->userId, $sessionId, $afterId, $page, $size);
if (empty($result['success'])) {
return $this->fail($result['error'] ?? '获取消息失败');
}
return $this->data($result['data']);
}
public function send()
{
$sessionId = (int) $this->request->post('session_id/d', 0);
$messageType = trim((string) $this->request->post('message_type/s', PrivateChatService::MESSAGE_TYPE_TEXT));
$content = trim((string) $this->request->post('content/s', ''));
$result = PrivateChatService::send($this->userId, $sessionId, $messageType, $content);
if (empty($result['success'])) {
return $this->fail($result['error'] ?? '发送失败');
}
return $this->data($result['data']);
}
}
@@ -0,0 +1,925 @@
<?php
namespace app\api\controller;
use app\api\lists\community\CommunityPostLists;
use app\common\model\community\CommunityPost;
use app\common\model\community\CommunityComment;
use app\common\model\community\CommunityLike;
use app\common\model\community\CommunityFollow;
use app\common\model\community\CommunityTag;
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\AiService;
use app\common\service\ai\KbSyncService;
use app\common\service\chat\PrivateChatService;
use app\common\service\VipService;
use think\facade\Db;
class CommunityController extends BaseApiController
{
private const LIUHE_TAGS = ['旧澳六合', '新澳六合'];
public array $notNeedLogin = ['postLists', 'postDetail', 'commentLists', 'tagLists', 'userProfile', 'followList', 'fansList'];
// 帖子列表
public function postLists()
{
return $this->dataLists(new CommunityPostLists());
}
// 帖子详情
public function postDetail()
{
$id = $this->request->get('id/d');
$post = CommunityPost::where('id', $id)->where('status', 1)->findOrEmpty();
if ($post->isEmpty()) {
return $this->fail('帖子不存在');
}
// 增加浏览数
CommunityPost::where('id', $id)->inc('view_count')->update();
$data = $post->toArray();
$ext = $data['ext'] ? (is_string($data['ext']) ? json_decode($data['ext'], true) : $data['ext']) : [];
$data['source_url'] = $ext['url'] ?? '';
unset($data['ext']);
// 用户信息
$user = User::field('id,sn,nickname,avatar,sex')->where('id', $data['user_id'])->findOrEmpty();
$data['user'] = $user->isEmpty() ? null : $user->toArray();
// 标签
$data['tags'] = $this->getPostTags($id);
$isTrumpPost = $this->isTrumpPost($data['tags']);
if ($isTrumpPost && empty($ext['translated_content']) && !empty($data['content'])) {
$result = AiService::translateCommunityPost($data['content']);
if (!empty($result['success']) && !empty($result['content'])) {
$ext['translated_content'] = trim($result['content']);
CommunityPost::where('id', $id)->update([
'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE),
'update_time' => time(),
]);
}
}
$data['translated_content'] = $ext['translated_content'] ?? '';
// 付费帖权限控制
$data['is_locked'] = false;
$data['is_purchased'] = false;
if ($data['is_paid']) {
$isAuthor = $this->userId && $this->userId == $data['user_id'];
$isPurchased = $this->userId && Db::name('user_account_log')
->where('user_id', $this->userId)
->where('change_type', AccountLogEnum::UP_DEC_PURCHASE_POST)
->where('relation_id', $id)
->count() > 0;
$data['is_purchased'] = $isPurchased;
if (!$isAuthor && !$isPurchased) {
$data['is_locked'] = true;
// 付费帖子只显示前20字符
if (mb_strlen($data['content']) > 20) {
$data['content'] = mb_substr($data['content'], 0, 20) . '...';
}
// 图片只保留第1张
$images = $data['images'];
if (is_array($images) && count($images) > 1) {
$data['images'] = [$images[0]];
}
}
}
// 当前用户是否点赞
$data['is_liked'] = false;
$data['is_self'] = $this->userId && $this->userId == $data['user_id'];
$data['is_followed'] = false;
$data['is_friend'] = false;
$data['relationship'] = PrivateChatService::relationship($this->userId, (int) $data['user_id']);
if ($this->userId) {
$data['is_liked'] = CommunityLike::where([
'user_id' => $this->userId,
'target_id' => $id,
'target_type' => 1
])->count() > 0;
// 是否关注作者
$data['is_followed'] = CommunityFollow::where([
'user_id' => $this->userId,
'follow_user_id' => $data['user_id']
])->count() > 0;
$data['is_friend'] = $data['is_followed'];
$data['relationship'] = PrivateChatService::relationship($this->userId, (int) $data['user_id']);
}
return $this->data($data);
}
// 删除帖子
public function deletePost()
{
if (!$this->userId) {
return $this->fail('请先登录');
}
$postId = $this->request->post('post_id/d');
$post = CommunityPost::where('id', $postId)->findOrEmpty();
if ($post->isEmpty()) {
return $this->fail('帖子不存在');
}
if ((int) $post->user_id !== (int) $this->userId) {
return $this->fail('无权删除该帖子');
}
Db::startTrans();
try {
$post->delete();
// 同时删除帖子下的评论
CommunityComment::where('post_id', $postId)->delete();
Db::commit();
KbSyncService::enqueue('post', 'post', $postId, 'delete', 20);
return $this->success('删除成功');
} catch (\Throwable $e) {
Db::rollback();
return $this->fail('删除失败:' . $e->getMessage());
}
}
// 发帖
public function publish()
{
$content = $this->request->post('content/s', '');
$images = $this->request->post('images/a', []);
$postType = $this->request->post('post_type/d', 0);
$matchId = $this->request->post('match_id/d', 0);
$tagIds = $this->request->post('tag_ids/a', []);
$isPaid = $this->request->post('is_paid/d', 0);
$pricePoints = $this->request->post('price_points/d', 0);
$freeContentLen = $this->request->post('free_content_len/d', 100);
if (empty($content) && empty($images)) {
return $this->fail('请输入内容或上传图片');
}
if ($isPaid && $pricePoints <= 0) {
return $this->fail('付费帖子必须设置积分价格');
}
if ($isPaid && ($pricePoints < 1 || $pricePoints > 9999)) {
return $this->fail('积分价格范围为1~9999');
}
Db::startTrans();
try {
$post = CommunityPost::create([
'user_id' => $this->userId,
'content' => $content,
'images' => $images,
'post_type' => $postType,
'match_id' => $matchId,
'is_paid' => $isPaid ? 1 : 0,
'price_points' => $isPaid ? $pricePoints : 0,
'free_content_len' => $isPaid ? $freeContentLen : 100,
'status' => 1,
'create_time' => time(),
'update_time' => time(),
]);
// 关联标签
if (!empty($tagIds)) {
$tagData = [];
foreach ($tagIds as $tagId) {
$tagData[] = ['post_id' => $post->id, 'tag_id' => $tagId];
}
Db::name('community_post_tag')->insertAll($tagData);
// 更新标签帖子数
CommunityTag::whereIn('id', $tagIds)->inc('post_count')->update();
}
Db::commit();
KbSyncService::enqueue('post', 'post', (int) $post->id, 'upsert', 30);
return $this->data(['id' => $post->id]);
} catch (\Exception $e) {
Db::rollback();
return $this->fail('发布失败:' . $e->getMessage());
}
}
// 评论列表
public function commentLists()
{
$postId = $this->request->get('post_id/d');
$page = $this->request->get('page_no/d', 1);
$size = $this->request->get('page_size/d', 20);
$list = CommunityComment::where('post_id', $postId)
->where('status', 1)
->where('parent_id', 0)
->order('create_time desc')
->page($page, $size)
->select()
->toArray();
$total = CommunityComment::where('post_id', $postId)
->where('status', 1)
->where('parent_id', 0)
->count();
// 填充用户信息和子评论
foreach ($list as &$item) {
$user = User::field('id,sn,nickname,avatar')->where('id', $item['user_id'])->findOrEmpty();
$item['user'] = $user->isEmpty() ? null : $user->toArray();
$item['is_self'] = $this->userId > 0 && (int) $item['user_id'] === (int) $this->userId;
// 子评论(最多3条)
$item['replies'] = CommunityComment::where('parent_id', $item['id'])
->where('status', 1)
->order('create_time asc')
->limit(3)
->select()
->each(function ($reply) {
$u = User::field('id,sn,nickname,avatar')->where('id', $reply['user_id'])->findOrEmpty();
$reply['user'] = $u->isEmpty() ? null : $u->toArray();
$reply['is_self'] = $this->userId > 0 && (int) $reply['user_id'] === (int) $this->userId;
if ($reply['reply_user_id']) {
$ru = User::field('id,nickname')->where('id', $reply['reply_user_id'])->findOrEmpty();
$reply['reply_user'] = $ru->isEmpty() ? null : $ru->toArray();
}
return $reply;
})
->toArray();
$item['reply_count'] = CommunityComment::where('parent_id', $item['id'])->where('status', 1)->count();
// 当前用户是否点赞
$item['is_liked'] = false;
if ($this->userId) {
$item['is_liked'] = CommunityLike::where([
'user_id' => $this->userId,
'target_id' => $item['id'],
'target_type' => 2
])->count() > 0;
}
}
return $this->data([
'lists' => $list,
'count' => $total,
'page_no' => $page,
'page_size' => $size,
]);
}
// 发评论
public function addComment()
{
$postId = $this->request->post('post_id/d');
$content = $this->request->post('content/s', '');
$parentId = $this->request->post('parent_id/d', 0);
$replyUserId = $this->request->post('reply_user_id/d', 0);
if (empty($content)) {
return $this->fail('请输入评论内容');
}
$post = CommunityPost::where('id', $postId)->where('status', 1)->findOrEmpty();
if ($post->isEmpty()) {
return $this->fail('帖子不存在');
}
$comment = CommunityComment::create([
'post_id' => $postId,
'user_id' => $this->userId,
'parent_id' => $parentId,
'reply_user_id' => $replyUserId,
'content' => $content,
'status' => 1,
'create_time' => time(),
]);
// 更新帖子评论数
CommunityPost::where('id', $postId)->inc('comment_count')->update();
return $this->data(['id' => $comment->id]);
}
// 删除评论
public function deleteComment()
{
$commentId = $this->request->post('comment_id/d');
$comment = CommunityComment::where('id', $commentId)->where('status', 1)->findOrEmpty();
if ($comment->isEmpty()) {
return $this->fail('评论不存在');
}
if ((int) $comment->user_id !== (int) $this->userId) {
return $this->fail('无权删除该评论');
}
Db::startTrans();
try {
$deleteIds = [$commentId];
$replyIds = CommunityComment::where('parent_id', $commentId)->where('status', 1)->column('id');
if (!empty($replyIds)) {
$deleteIds = array_merge($deleteIds, $replyIds);
}
CommunityComment::whereIn('id', $deleteIds)->delete();
CommunityPost::where('id', $comment->post_id)->dec('comment_count', count($deleteIds))->update();
Db::commit();
return $this->data(['deleted_ids' => $deleteIds]);
} catch (\Throwable $e) {
Db::rollback();
return $this->fail('删除失败:' . $e->getMessage());
}
}
// 点赞/取消点赞
public function like()
{
$targetId = $this->request->post('target_id/d');
$targetType = $this->request->post('target_type/d', 1);
$exists = CommunityLike::where([
'user_id' => $this->userId,
'target_id' => $targetId,
'target_type' => $targetType
])->findOrEmpty();
if ($exists->isEmpty()) {
CommunityLike::create([
'user_id' => $this->userId,
'target_id' => $targetId,
'target_type' => $targetType,
'create_time' => time(),
]);
if ($targetType == 1) {
CommunityPost::where('id', $targetId)->inc('like_count')->update();
} else {
CommunityComment::where('id', $targetId)->inc('like_count')->update();
}
return $this->data(['is_liked' => true]);
} else {
$exists->delete();
if ($targetType == 1) {
CommunityPost::where('id', $targetId)->dec('like_count')->update();
} else {
CommunityComment::where('id', $targetId)->dec('like_count')->update();
}
return $this->data(['is_liked' => false]);
}
}
// 关注/取消关注
public function follow()
{
$followUserId = $this->request->post('follow_user_id/d');
if ($followUserId == $this->userId) {
return $this->fail('不能关注自己');
}
$exists = CommunityFollow::where([
'user_id' => $this->userId,
'follow_user_id' => $followUserId
])->findOrEmpty();
if ($exists->isEmpty()) {
CommunityFollow::create([
'user_id' => $this->userId,
'follow_user_id' => $followUserId,
'create_time' => time(),
]);
return $this->data(['is_followed' => true]);
} else {
$exists->delete();
return $this->data(['is_followed' => false]);
}
}
// 标签列表
public function tagLists()
{
$list = CommunityTag::where('status', 1)
->order('sort asc')
->field('id,name,icon,post_count,is_hot')
->select()
->toArray();
return $this->data($list);
}
// 购买帖子
// confirm=0 探测是否已购买;confirm=1 确认扣分购买
public function purchasePost()
{
$postId = $this->request->post('post_id/d');
$confirm = $this->request->post('confirm/d', 0);
$post = CommunityPost::where('id', $postId)->where('status', 1)->findOrEmpty();
if ($post->isEmpty()) {
return $this->fail('帖子不存在');
}
if (!$post->is_paid) {
return $this->fail('该帖子为免费帖');
}
if ($post->user_id == $this->userId) {
return $this->data([
'content' => $post->content,
'from_cache' => true,
]);
}
// VIP解锁免费权益判断
$vipUnlockFree = VipService::hasUnlockFree($this->userId);
// 检查是否已购买
$hasPurchased = Db::name('user_account_log')
->where('user_id', $this->userId)
->where('change_type', AccountLogEnum::UP_DEC_PURCHASE_POST)
->where('relation_id', $postId)
->count() > 0;
if ($hasPurchased || $vipUnlockFree) {
return $this->data([
'content' => $post->content,
'from_cache' => true,
'vip_free' => $vipUnlockFree,
]);
}
// 未购买且未确认 → 返回待确认标记
if (!$confirm) {
return $this->data([
'needs_payment' => true,
'cost' => (int) $post->price_points,
]);
}
// 防重复扣分:再次校验
$hasPurchased2 = Db::name('user_account_log')
->where('user_id', $this->userId)
->where('change_type', AccountLogEnum::UP_DEC_PURCHASE_POST)
->where('relation_id', $postId)
->count() > 0;
if ($hasPurchased2) {
return $this->data([
'content' => $post->content,
'from_cache' => true,
]);
}
$user = User::findOrEmpty($this->userId);
$price = (int) $post->price_points;
if ($user->user_points < $price) {
return $this->fail('积分不足,当前积分' . $user->user_points . ',需要' . $price . '积分');
}
Db::startTrans();
try {
// 扣除买家积分
User::where('id', $this->userId)->dec('user_points', $price)->update();
AccountLogLogic::add(
$this->userId,
AccountLogEnum::UP_DEC_PURCHASE_POST,
AccountLogEnum::DEC,
$price,
'',
'购买帖子#' . $postId,
[],
$postId
);
// 作者获得积分
User::where('id', $post->user_id)->inc('user_points', $price)->update();
AccountLogLogic::add(
$post->user_id,
AccountLogEnum::UP_INC_POST_SOLD,
AccountLogEnum::INC,
$price,
'',
'帖子#' . $postId . '被购买',
[],
$postId
);
// 更新帖子已购买人数
CommunityPost::where('id', $postId)->inc('paid_count')->update();
Db::commit();
return $this->data([
'content' => $post->content,
'from_cache' => false,
]);
} catch (\Exception $e) {
Db::rollback();
return $this->fail('购买失败:' . $e->getMessage());
}
}
// AI翻译帖子(需登录,暂不扣积分)
public function translatePost()
{
$postId = $this->request->post('post_id/d');
$post = CommunityPost::where('id', $postId)->where('status', 1)->findOrEmpty();
if ($post->isEmpty()) {
return $this->fail('帖子不存在');
}
$ext = $post->ext ? (is_string($post->ext) ? json_decode($post->ext, true) : $post->ext) : [];
$tags = $this->getPostTags($postId);
$isLiuhePost = $this->isLiuhePost($tags, $ext);
if ($isLiuhePost) {
$analysis = $this->ensureLiuheAnalysisContent($post, $ext);
if (!$analysis['success']) {
return $this->fail($analysis['error'] ?? '图片分析失败');
}
$ext = $analysis['ext'];
return $this->data([
'translated_content' => $ext['lottery_analysis_content'] ?? '',
'from_cache' => $analysis['from_cache'] ?? false,
]);
}
$cacheField = 'translated_content';
if (!empty($ext[$cacheField])) {
return $this->data([
'translated_content' => $ext[$cacheField],
'from_cache' => true,
]);
}
if (empty($ext[$cacheField])) {
$result = AiService::translateCommunityPost($post->content ?: '');
if (!$result['success']) {
$ext[$cacheField] = self::buildVirtualAnalysisContent($post, false);
CommunityPost::where('id', $postId)->update([
'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE),
'update_time' => time(),
]);
KbSyncService::enqueue('post', 'post', $postId, 'upsert', 45);
return $this->data([
'translated_content' => $ext[$cacheField],
'from_cache' => false,
]);
}
$ext[$cacheField] = trim($result['content']);
CommunityPost::where('id', $postId)->update([
'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE),
'update_time' => time(),
]);
KbSyncService::enqueue('post', 'post', $postId, 'upsert', 45);
}
return $this->data([
'translated_content' => $ext[$cacheField],
'from_cache' => false,
]);
}
public function aiAnalysis()
{
try {
$postId = $this->request->get('post_id/d', 0);
if ($postId <= 0) {
return $this->fail('参数缺失');
}
$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,
]);
} catch (\Throwable $e) {
return $this->fail('AI分析异常: ' . $e->getMessage());
}
}
public function aiAnalysisResult()
{
try {
$postId = $this->request->get('post_id/d', 0);
if ($postId <= 0) {
return $this->fail('参数缺失');
}
$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) ?: []);
$isLiuhePost = $this->isLiuhePost($this->getPostTags($postId), $ext);
$cache = AiAnalysis::getCache(AiAnalysis::TYPE_POST_ANALYSIS, $postId);
if ($cache) {
$cacheData = json_decode((string) ($cache['result'] ?? ''), true);
$isValid = is_array($cacheData) && !empty($cacheData);
$isValidLiuheCache = !empty($cacheData['next_prediction'])
&& ($cacheData['analysis_source'] ?? '') === 'image_free_v1';
if ($isValid && (!$isLiuhePost || $isValidLiuheCache)) {
return $this->data([
'status' => 'success',
'analysis' => $cacheData,
'from_cache' => true,
]);
}
}
return $this->data([
'status' => 'processing',
'analysis' => null,
'from_cache' => false,
]);
} catch (\Throwable $e) {
return $this->fail('AI分析结果查询异常: ' . $e->getMessage());
}
}
private 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 function isTrumpPost(array $tags): bool
{
return in_array('特朗普', $tags, true);
}
private 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 function ensureLiuheAnalysisContent(CommunityPost $post, array $ext): array
{
$images = is_array($post->images) ? array_values($post->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'
&& $cacheHash === $imageHash
) {
return ['success' => true, 'ext' => $ext, 'from_cache' => true];
}
$result = AiService::analyzeLotteryPostImages($post->content ?: '', $images);
if (empty($result['success']) || empty($result['content'])) {
return [
'success' => false,
'error' => (string) ($result['error'] ?? '帖子图片分析失败'),
];
}
$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 buildVirtualAnalysisContent($post, bool $isLottery): string
{
if ($isLottery) {
$longText = "【49宝典开奖号码深度分析】\n\n"
. "一、本期开奖概况\n"
. "本期开奖号码为:03 07 12 18 25 31 + 特码 08。\n"
. "其中红波号码2个(07、25),蓝波号码3个(03、12、31),绿波号码1个(18)。\n\n"
. "二、号码属性分析\n"
. str_repeat("1. 号码 03:生肖鼠,五行木,蓝波,小单。该号码近10期出现2次,属于温号。\n"
. "2. 号码 07:生肖虎,五行火,红波,小单。该号码近10期出现3次,属于热号。\n"
. "3. 号码 12:生肖猪,五行水,蓝波,大双。该号码近10期出现1次,属于冷号。\n"
. "4. 号码 18:生肖蛇,五行土,绿波,大双。该号码近10期出现2次,属于温号。\n"
. "5. 号码 25:生肖鼠,五行金,红波,大单。该号码近10期出现4次,属于热号。\n"
. "6. 号码 31:生肖马,五行木,蓝波,大单。该号码近10期出现1次,属于冷号。\n"
. "7. 特码 08:生肖兔,五行火,蓝波,小双。该号码近10期出现2次,属于温号。\n\n", 5)
. "三、走势趋势\n"
. "从近期走势来看,大号(25-49)区间号码持续走热,小号(01-24)区间号码相对偏冷。\n"
. "建议关注下期大号区间号码的回补机会。\n\n"
. "四、冷热统计\n"
. "热号(近10期出现3次以上):07、25\n"
. "温号(近10期出现1-2次):03、18、08\n"
. "冷号(近10期出现0次):01、02、04、05、06、09、10、11、13、14、15、16、17、19、20、21、22、23、24、26、27、28、29、30、32、33、34、35、36、37、38、39、40、41、42、43、44、45、46、47、48、49\n\n"
. "五、综合建议\n"
. "彩票开奖具有随机性,以上分析仅基于历史数据统计,不构成购买建议。请理性购彩,量力而行。\n\n"
. "—— AI智能分析仅供参考 ——";
return $longText;
}
$content = $post->content ?? '';
$short = mb_strlen($content) > 100 ? mb_substr($content, 0, 100) . '...' : $content;
$longTestText = "这是一段模拟的长文本翻译内容,用于测试弹窗滚动效果。\n\n"
. "在H5页面中,如果弹窗内容过长,用户需要能够滚动查看完整内容。\n\n"
. str_repeat("这是第%d段测试文本。弹窗滚动功能测试中,请确保内容可以正常滚动显示。"
. "如果滚动功能正常,用户将能够顺畅地阅读所有内容,而不会因为内容过长导致体验下降。"
. "同时,弹窗背后的页面应该保持固定,不能随着弹窗内容的滚动而滚动。\n\n", 30)
. "【测试结束】感谢您的耐心阅读!";
return $longTestText;
}
// 用户统计
public function userStats()
{
$userId = $this->userId;
$postCount = CommunityPost::where('user_id', $userId)->where('status', 1)->count();
$followCount = CommunityFollow::where('user_id', $userId)->count();
$fansCount = CommunityFollow::where('follow_user_id', $userId)->count();
$collectCount = Db::name('article_collect')->where('user_id', $userId)->where('status', 1)->whereNull('delete_time')->count();
$userPoints = User::where('id', $userId)->value('user_points') ?: 0;
$chatUnreadCount = PrivateChatService::unreadCount($userId);
return $this->data([
'post_count' => $postCount,
'follow_count' => $followCount,
'fans_count' => $fansCount,
'collect_count' => $collectCount,
'user_points' => $userPoints,
'chat_unread_count' => $chatUnreadCount,
]);
}
// 关注列表
public function followList()
{
$page = $this->request->get('page_no/d', 1);
$size = $this->request->get('page_size/d', 20);
$targetUserId = $this->request->get('user_id/d', 0) ?: $this->userId;
if (!$targetUserId) {
return $this->data(['lists' => [], 'page_no' => $page, 'page_size' => $size]);
}
$followIds = CommunityFollow::where('user_id', $targetUserId)
->order('create_time desc')
->page($page, $size)
->column('follow_user_id');
$list = [];
if ($followIds) {
$users = User::field('id,sn,nickname,avatar')
->whereIn('id', $followIds)
->select()
->toArray();
// 检查对方是否也关注了我(互相关注)
$fansOfMe = CommunityFollow::whereIn('user_id', $followIds)
->where('follow_user_id', $targetUserId)
->column('user_id');
foreach ($users as &$u) {
$u['is_followed'] = true; // 关注列表里都是已关注的
$u['is_mutual'] = in_array($u['id'], $fansOfMe);
}
$list = $users;
}
return $this->data([
'lists' => $list,
'page_no' => $page,
'page_size' => $size,
]);
}
// 粉丝列表
public function fansList()
{
$page = $this->request->get('page_no/d', 1);
$size = $this->request->get('page_size/d', 20);
$targetUserId = $this->request->get('user_id/d', 0) ?: $this->userId;
if (!$targetUserId) {
return $this->data(['lists' => [], 'page_no' => $page, 'page_size' => $size]);
}
$fansIds = CommunityFollow::where('follow_user_id', $targetUserId)
->order('create_time desc')
->page($page, $size)
->column('user_id');
$list = [];
if ($fansIds) {
$users = User::field('id,sn,nickname,avatar')
->whereIn('id', $fansIds)
->select()
->toArray();
// 检查我是否关注了该粉丝(is_followed)+ 互相关注
$myFollows = CommunityFollow::where('user_id', $targetUserId)
->whereIn('follow_user_id', $fansIds)
->column('follow_user_id');
foreach ($users as &$u) {
$u['is_followed'] = in_array($u['id'], $myFollows);
$u['is_mutual'] = $u['is_followed']; // 粉丝关注了我,我也关注了他 = 互关
}
$list = $users;
}
return $this->data([
'lists' => $list,
'page_no' => $page,
'page_size' => $size,
]);
}
// 用户主页(公开)
public function userProfile()
{
$userId = $this->request->get('user_id/d');
if (!$userId) {
return $this->fail('参数错误');
}
$user = User::field('id,sn,nickname,avatar,sex,create_time')->where('id', $userId)->findOrEmpty();
if ($user->isEmpty()) {
return $this->fail('用户不存在');
}
$data = $user->toArray();
$data['post_count'] = CommunityPost::where('user_id', $userId)->where('status', 1)->count();
$data['comment_count'] = CommunityComment::where('user_id', $userId)->count();
$data['follow_count'] = CommunityFollow::where('user_id', $userId)->count();
$data['fans_count'] = CommunityFollow::where('follow_user_id', $userId)->count();
$data['like_count'] = CommunityPost::where('user_id', $userId)->where('status', 1)->sum('like_count');
// 加入天数
$data['join_days'] = max(1, (int) ceil((time() - strtotime($data['create_time'])) / 86400));
// 当前登录用户是否关注
$data['is_followed'] = false;
$data['is_friend'] = false;
$data['relationship'] = PrivateChatService::relationship($this->userId, $userId);
if ($this->userId && $this->userId != $userId) {
$data['is_followed'] = CommunityFollow::where([
'user_id' => $this->userId,
'follow_user_id' => $userId
])->count() > 0;
$data['is_friend'] = $data['is_followed'];
$data['relationship'] = PrivateChatService::relationship($this->userId, $userId);
}
$data['is_self'] = $this->userId == $userId;
return $this->data($data);
}
}
@@ -0,0 +1,85 @@
<?php
namespace app\api\controller;
use app\common\model\article\Article;
use app\common\model\popup\PagePopup;
use app\api\logic\IndexLogic;
use think\response\Json;
/**
* 通用富文本内容
* GET /api/content/detail?type=xxx&id=xxx
*
* 支持的 type:
* - policy 协议(id 实际为 type 字符串:service/privacy 等,由 IndexLogic::getPolicyByType 处理)
* - article 资讯文章
* - popup 弹窗自带的富文本内容(直接取 page_popup.content
* - custom 自定义页面(暂未启用,留扩展位)
*/
class ContentController extends BaseApiController
{
public array $notNeedLogin = ['detail'];
public function detail(): Json
{
$type = trim((string)$this->request->get('type', ''));
$idRaw = trim((string)$this->request->get('id', ''));
if (!$type) {
return $this->fail('参数错误:type');
}
switch ($type) {
case 'policy': {
// policy 用 id 字段传 type 字符串
$policy = IndexLogic::getPolicyByType($idRaw);
if (!$policy) {
return $this->fail('内容不存在');
}
return $this->data([
'type' => 'policy',
'id' => $idRaw,
'title' => (string)($policy['title'] ?? ''),
'content' => (string)($policy['content'] ?? ''),
'create_time' => '',
]);
}
case 'article': {
$id = (int)$idRaw;
if ($id <= 0) return $this->fail('参数错误:id');
$info = Article::where('id', $id)
->where('is_show', 1)
->find();
if (!$info) return $this->fail('内容不存在');
return $this->data([
'type' => 'article',
'id' => $id,
'title' => (string)($info['title'] ?? ''),
'content' => (string)($info['content'] ?? ''),
'create_time' => (string)($info['create_time'] ?? ''),
]);
}
case 'popup': {
$id = (int)$idRaw;
if ($id <= 0) return $this->fail('参数错误:id');
$info = PagePopup::where('id', $id)
->where('status', 1)
->find();
if (!$info) return $this->fail('内容不存在');
return $this->data([
'type' => 'popup',
'id' => $id,
'title' => (string)($info['title'] ?: $info['name']),
'content' => (string)($info['content'] ?? ''),
'create_time' => (string)($info['create_time'] ?? ''),
]);
}
default:
return $this->fail('暂不支持的 type' . $type);
}
}
}
@@ -0,0 +1,65 @@
<?php
namespace app\api\controller;
use think\facade\Cache;
use think\facade\Log;
class CryptoController extends BaseApiController
{
public array $notNeedLogin = ['proxy'];
/**
* Binance API 代理(解决客户端直连被墙问题)
* GET /api/crypto/proxy?path=/api/v3/ticker/24hr&symbols=...
*/
public function proxy()
{
$path = $this->request->get('path', '');
if (empty($path) || !str_starts_with($path, '/api/v3/')) {
return $this->fail('无效的API路径');
}
// 构建完整URL,将除 path 外的参数透传
$params = $this->request->get();
unset($params['path']);
ksort($params);
$query = http_build_query($params);
$url = 'https://api.binance.com' . $path . ($query ? '?' . $query : '');
$cacheKey = 'binance_proxy_' . md5($path . '|' . $query);
$cached = Cache::get($cacheKey);
if ($cached !== null) {
return json($cached, 200);
}
Log::info('[CryptoProxy] request url=' . $url);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
Log::error('[CryptoProxy] curl_error=' . $error . ' url=' . $url);
return $this->fail('API请求失败: ' . $error);
}
Log::info('[CryptoProxy] http_code=' . $httpCode . ' response_preview=' . substr((string) $response, 0, 500));
$data = json_decode($response, true);
if ($httpCode === 200 && $data !== null) {
Cache::set($cacheKey, $data, 3);
}
return json($data, $httpCode);
}
}
@@ -0,0 +1,65 @@
<?php
namespace app\api\controller;
use app\common\model\device\UserDevice;
use think\response\Json;
/**
* 设备信息上报
* POST /api/device/register
*/
class DeviceController extends BaseApiController
{
public array $notNeedLogin = ['register'];
/**
* 注册或更新设备信息
* 请求字段:device_id(必填)、platform、system、model、brand、app_version、
* app_version_code、screen_width、screen_height、network_type、
* language、timezone、push_cid、extrajson string
*/
public function register(): Json
{
$params = $this->request->post();
$deviceId = trim((string)($params['device_id'] ?? ''));
if (!$deviceId) {
return $this->fail('device_id 必填');
}
$now = time();
$ip = $this->request->ip() ?: '';
$ua = $this->request->header('user-agent', '');
if (mb_strlen($ua) > 500) $ua = mb_substr($ua, 0, 500);
$fields = [
'platform', 'system', 'model', 'brand', 'app_version',
'app_version_code', 'screen_width', 'screen_height',
'network_type', 'language', 'timezone', 'push_cid'
];
$data = [];
foreach ($fields as $f) {
if (isset($params[$f])) $data[$f] = $params[$f];
}
$data['ip'] = $ip;
$data['ua'] = $ua;
if (isset($params['extra'])) {
$data['extra'] = is_array($params['extra']) ? json_encode($params['extra'], JSON_UNESCAPED_UNICODE) : (string)$params['extra'];
}
$data['user_id'] = $this->userId ?: 0;
$data['last_at'] = $now;
$exist = UserDevice::where('device_id', $deviceId)->find();
if ($exist) {
$data['active_count'] = ($exist['active_count'] ?? 0) + 1;
UserDevice::where('id', $exist['id'])->update($data);
return $this->data(['id' => $exist['id'], 'is_new' => false]);
}
$data['device_id'] = $deviceId;
$data['first_at'] = $now;
$data['active_count'] = 1;
$id = UserDevice::insertGetId($data);
return $this->data(['id' => $id, 'is_new' => true]);
}
}
@@ -0,0 +1,92 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\api\logic\IndexLogic;
use think\response\Json;
/**
* index
* Class IndexController
* @package app\api\controller
*/
class IndexController extends BaseApiController
{
public array $notNeedLogin = ['index', 'config', 'policy', 'decorate'];
/**
* @notes 首页数据
* @return Json
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/21 19:15
*/
public function index()
{
$result = IndexLogic::getIndexData();
return $this->data($result);
}
/**
* @notes 全局配置
* @return Json
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/21 19:41
*/
public function config()
{
$result = IndexLogic::getConfigData();
return $this->data($result);
}
/**
* @notes 政策协议
* @return Json
* @author 段誉
* @date 2022/9/20 20:00
*/
public function policy()
{
$type = $this->request->get('type/s', '');
$result = IndexLogic::getPolicyByType($type);
return $this->data($result);
}
/**
* @notes 装修信息
* @return Json
* @author 段誉
* @date 2022/9/21 18:37
*/
public function decorate()
{
$id = $this->request->get('id/d');
$result = IndexLogic::getDecorate($id);
return $this->data($result);
}
}
@@ -0,0 +1,216 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\api\validate\{LoginAccountValidate, RegisterValidate, WebScanLoginValidate, WechatLoginValidate};
use app\api\logic\LoginLogic;
/**
* 登录注册
* Class LoginController
* @package app\api\controller
*/
class LoginController extends BaseApiController
{
public array $notNeedLogin = ['register', 'account', 'logout', 'codeUrl', 'oaLogin', 'mnpLogin', 'getScanCode', 'scanLogin'];
/**
* @notes 注册账号
* @return \think\response\Json
* @author 段誉
* @date 2022/9/7 15:38
*/
public function register()
{
$params = (new RegisterValidate())->post()->goCheck('register');
$result = LoginLogic::register($params);
if (true === $result) {
return $this->success('注册成功', [], 1, 1);
}
return $this->fail(LoginLogic::getError());
}
/**
* @notes 账号密码/手机号密码/手机号验证码登录
* @return \think\response\Json
* @author 段誉
* @date 2022/9/16 10:42
*/
public function account()
{
$params = (new LoginAccountValidate())->post()->goCheck();
$result = LoginLogic::login($params);
if (false === $result) {
return $this->fail(LoginLogic::getError());
}
return $this->data($result);
}
/**
* @notes 退出登录
* @return \think\response\Json
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/16 10:42
*/
public function logout()
{
LoginLogic::logout($this->userInfo);
return $this->success();
}
/**
* @notes 获取微信请求code的链接
* @return \think\response\Json
* @author 段誉
* @date 2022/9/15 18:27
*/
public function codeUrl()
{
$url = $this->request->get('url');
$result = ['url' => LoginLogic::codeUrl($url)];
return $this->success('获取成功', $result);
}
/**
* @notes 公众号登录
* @return \think\response\Json
* @throws \GuzzleHttp\Exception\GuzzleException
* @author 段誉
* @date 2022/9/20 19:48
*/
public function oaLogin()
{
$params = (new WechatLoginValidate())->post()->goCheck('oa');
$res = LoginLogic::oaLogin($params);
if (false === $res) {
return $this->fail(LoginLogic::getError());
}
return $this->success('', $res);
}
/**
* @notes 小程序-登录接口
* @return \think\response\Json
* @author 段誉
* @date 2022/9/20 19:48
*/
public function mnpLogin()
{
$params = (new WechatLoginValidate())->post()->goCheck('mnpLogin');
$res = LoginLogic::mnpLogin($params);
if (false === $res) {
return $this->fail(LoginLogic::getError());
}
return $this->success('', $res);
}
/**
* @notes 小程序绑定微信
* @return \think\response\Json
* @author 段誉
* @date 2022/9/20 19:48
*/
public function mnpAuthBind()
{
$params = (new WechatLoginValidate())->post()->goCheck("wechatAuth");
$params['user_id'] = $this->userId;
$result = LoginLogic::mnpAuthLogin($params);
if ($result === false) {
return $this->fail(LoginLogic::getError());
}
return $this->success('绑定成功', [], 1, 1);
}
/**
* @notes 公众号绑定微信
* @return \think\response\Json
* @throws \GuzzleHttp\Exception\GuzzleException
* @author 段誉
* @date 2022/9/20 19:48
*/
public function oaAuthBind()
{
$params = (new WechatLoginValidate())->post()->goCheck("wechatAuth");
$params['user_id'] = $this->userId;
$result = LoginLogic::oaAuthLogin($params);
if ($result === false) {
return $this->fail(LoginLogic::getError());
}
return $this->success('绑定成功', [], 1, 1);
}
/**
* @notes 获取扫码地址
* @return \think\response\Json
* @author 段誉
* @date 2022/10/20 18:25
*/
public function getScanCode()
{
$redirectUri = $this->request->get('url/s');
$result = LoginLogic::getScanCode($redirectUri);
if (false === $result) {
return $this->fail(LoginLogic::getError() ?? '未知错误');
}
return $this->success('', $result);
}
/**
* @notes 网站扫码登录
* @return \think\response\Json
* @author 段誉
* @date 2022/10/21 10:28
*/
public function scanLogin()
{
$params = (new WebScanLoginValidate())->post()->goCheck();
$result = LoginLogic::scanLogin($params);
if (false === $result) {
return $this->fail(LoginLogic::getError() ?? '登录失败');
}
return $this->success('', $result);
}
/**
* @notes 更新用户头像昵称
* @return \think\response\Json
* @author 段誉
* @date 2023/2/22 11:15
*/
public function updateUser()
{
$params = (new WechatLoginValidate())->post()->goCheck("updateUser");
LoginLogic::updateUser($params, $this->userId);
return $this->success('操作成功', [], 1, 1);
}
}
@@ -0,0 +1,641 @@
<?php
namespace app\api\controller;
use app\common\model\lottery\LotteryCategory;
use app\common\model\lottery\LotteryDraw;
use app\common\model\lottery\LotteryGame;
use app\common\model\lottery\LotteryGameCategory;
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;
class LotteryController extends BaseApiController
{
public array $notNeedLogin = ['categoryList', 'regionList', 'drawList', 'drawDetail', 'latestDraw', 'aiPredict', 'gameList', 'latestDrawResult', 'drawResultList', 'aiHistory', 'aiHotCold', 'aiTrend', 'aiStats', 'aiColorZodiac', 'aiAnalysisHistory'];
/**
* 彩种游戏列表(一级分类 + 二级彩种)
*/
public function gameList()
{
$categories = LotteryGameCategory::where('is_show', 1)
->order('sort', 'asc')
->field('id,label,value,sort')
->select()
->toArray();
$games = LotteryGame::where('is_show', 1)
->order('sort asc, id asc')
->field('id,code,template,name,icon,status,open_status,category,group_id,open_count,sort,has_video')
->select()
->toArray();
$gameMap = [];
foreach ($games as $game) {
$gameMap[$game['category']][] = $game;
}
$result = [];
foreach ($categories as $cat) {
$cat['games'] = $gameMap[$cat['value']] ?? [];
$result[] = $cat;
}
return $this->data($result);
}
/**
* 彩票分类列表
*/
public function categoryList()
{
$domain = request()->domain() . '/';
$list = LotteryCategory::where('is_show', 1)
->order('sort', 'asc')
->field('id,name,code,region,icon,description,draw_rule,draw_time')
->select()
->toArray();
foreach ($list as &$item) {
if (!empty($item['icon']) && !str_starts_with($item['icon'], 'http')) {
$item['icon'] = $domain . $item['icon'];
}
}
return $this->data($list);
}
/**
* 彩票地区Tab列表
*/
public function regionList()
{
$domain = request()->domain() . '/';
$list = LotteryRegion::where('is_show', 1)
->order('sort', 'asc')
->field('id,label,value,icon')
->select()
->toArray();
foreach ($list as &$item) {
if (!empty($item['icon']) && !str_starts_with($item['icon'], 'http')) {
$item['icon'] = $domain . $item['icon'];
}
}
return $this->data($list);
}
/**
* 开奖列表
*/
public function drawList()
{
$categoryId = $this->request->get('category_id/d', 0);
$pageNo = $this->request->get('page_no/d', 1);
$pageSize = $this->request->get('page_size/d', 20);
$query = LotteryDraw::where('is_show', 1);
if ($categoryId > 0) {
$query->where('category_id', $categoryId);
}
$countQuery = clone $query;
$count = $countQuery->count();
$list = $query->order('draw_date desc, period desc')
->page($pageNo, $pageSize)
->select()
->toArray();
// 解析JSON字段
foreach ($list as &$item) {
$item['numbers'] = is_string($item['numbers']) ? json_decode($item['numbers'], true) : $item['numbers'];
$item['zodiac'] = is_string($item['zodiac']) ? json_decode($item['zodiac'], true) : $item['zodiac'];
$item['elements'] = is_string($item['elements']) ? json_decode($item['elements'], true) : $item['elements'];
$item['color'] = is_string($item['color']) ? json_decode($item['color'], true) : $item['color'];
$item['prize_info'] = is_string($item['prize_info']) ? json_decode($item['prize_info'], true) : $item['prize_info'];
}
return $this->data([
'lists' => $list,
'count' => $count,
'page_no' => $pageNo,
'page_size' => $pageSize,
'last_page' => ceil($count / $pageSize),
]);
}
/**
* 开奖详情
*/
public function drawDetail()
{
$id = $this->request->get('id/d', 0);
if (empty($id)) {
return $this->fail('参数缺失');
}
$draw = LotteryDraw::where('id', $id)->where('is_show', 1)->findOrEmpty();
if ($draw->isEmpty()) {
return $this->fail('记录不存在');
}
$data = $draw->toArray();
$data['numbers'] = is_string($data['numbers']) ? json_decode($data['numbers'], true) : $data['numbers'];
$data['zodiac'] = is_string($data['zodiac']) ? json_decode($data['zodiac'], true) : $data['zodiac'];
$data['elements'] = is_string($data['elements']) ? json_decode($data['elements'], true) : $data['elements'];
$data['color'] = is_string($data['color']) ? json_decode($data['color'], true) : $data['color'];
$data['prize_info'] = is_string($data['prize_info']) ? json_decode($data['prize_info'], true) : $data['prize_info'];
// 附带分类信息
$category = LotteryCategory::where('id', $data['category_id'])->findOrEmpty();
if (!$category->isEmpty()) {
$data['category'] = $category->toArray();
}
return $this->data($data);
}
/**
* 各彩种最新一期开奖
*/
public function latestDraw()
{
$categoryId = $this->request->get('category_id/d', 0);
$query = LotteryDraw::where('is_show', 1)->where('status', 1);
if ($categoryId > 0) {
$query->where('category_id', $categoryId);
}
// 按分类分组取最新一期
if ($categoryId > 0) {
$draw = $query->order('draw_date', 'desc')->findOrEmpty();
if ($draw->isEmpty()) {
return $this->data(null);
}
$data = $draw->toArray();
$data['numbers'] = is_string($data['numbers']) ? json_decode($data['numbers'], true) : $data['numbers'];
$data['color'] = is_string($data['color']) ? json_decode($data['color'], true) : $data['color'];
return $this->data($data);
}
// 获取所有分类的最新开奖
$categories = LotteryCategory::where('is_show', 1)->order('sort', 'asc')->select()->toArray();
$result = [];
foreach ($categories as $cat) {
$draw = LotteryDraw::where('category_id', $cat['id'])
->where('is_show', 1)
->where('status', 1)
->order('draw_date', 'desc')
->findOrEmpty();
if (!$draw->isEmpty()) {
$d = $draw->toArray();
$d['numbers'] = is_string($d['numbers']) ? json_decode($d['numbers'], true) : $d['numbers'];
$d['color'] = is_string($d['color']) ? json_decode($d['color'], true) : $d['color'];
$d['category_name'] = $cat['name'];
$d['category_code'] = $cat['code'];
$result[] = $d;
}
}
return $this->data($result);
}
/**
* 为号码数组附加生肖(zodiac)和波色(color)映射(六合彩专用)
*/
private function enrichNumbers(array $numbers, string $template = ''): array
{
if (empty($numbers))
return [];
$isHk6 = stripos($template, 'hk6') !== false || stripos($template, 'lhc') !== false;
if (!$isHk6)
return $numbers;
$year = (int) date('Y');
$mapping = LotteryNumberMapping::where('year', $year)
->whereIn('type', [LotteryNumberMapping::TYPE_ZODIAC, LotteryNumberMapping::TYPE_COLOR])
->order('type asc, sort asc')
->select()->toArray();
$zodiacMap = [];
$colorMap = [];
foreach ($mapping as $row) {
$nums = is_string($row['numbers']) ? json_decode($row['numbers'], true) : $row['numbers'];
foreach ($nums as $n) {
if ((int) $row['type'] === LotteryNumberMapping::TYPE_ZODIAC) {
$zodiacMap[(int) $n] = $row['attr_name'];
} else {
$colorMap[(int) $n] = $row['attr_code'];
}
}
}
$enriched = [];
foreach ($numbers as $num) {
$n = (int) $num;
$enriched[] = [
'num' => $num,
'zodiac' => $zodiacMap[$n] ?? '',
'color' => $colorMap[$n] ?? 'red',
];
}
return $enriched;
}
/**
* 各彩种最新一期开奖(新表 lottery_draw_result
*/
public function latestDrawResult()
{
$code = $this->request->get('code', '');
if ($code) {
$draw = LotteryDrawResult::where('code', $code)
->order('draw_time desc')
->findOrEmpty();
if ($draw->isEmpty()) {
return $this->data(null);
}
$data = $draw->toArray();
if (!empty($data['draw_code'])) {
$data['status'] = 1;
}
$rawNumbers = $data['draw_code'] ? explode(',', $data['draw_code']) : [];
$game = LotteryGame::where('code', $code)->field('name,template')->findOrEmpty();
$data['game_name'] = $game->isEmpty() ? $code : $game['name'];
$template = $game->isEmpty() ? '' : $game['template'];
$data['numbers'] = $this->enrichNumbers($rawNumbers, $template);
$data['template'] = $template;
return $this->data($data);
}
// 全部彩种,每个取最新一条
$games = LotteryGame::where('is_show', 1)
->order('sort asc, id asc')
->field('code,name,template,category')
->select()
->toArray();
$result = [];
foreach ($games as $game) {
$draw = LotteryDrawResult::where('code', $game['code'])
->order('draw_time desc')
->findOrEmpty();
if (!$draw->isEmpty()) {
$d = $draw->toArray();
if (!empty($d['draw_code'])) {
$d['status'] = 1;
}
$rawNumbers = $d['draw_code'] ? explode(',', $d['draw_code']) : [];
$d['numbers'] = $this->enrichNumbers($rawNumbers, $game['template']);
$d['game_name'] = $game['name'];
$d['template'] = $game['template'];
$d['category'] = $game['category'];
$result[] = $d;
}
}
return $this->data($result);
}
/**
* 开奖历史列表(新表 lottery_draw_result
*/
public function drawResultList()
{
$code = $this->request->get('code', '');
$pageNo = $this->request->get('page_no/d', 1);
$pageSize = $this->request->get('page_size/d', 20);
if (empty($code)) {
return $this->fail('缺少彩种编码');
}
$query = LotteryDrawResult::where('code', $code);
$count = (clone $query)->count();
$list = $query->order('draw_time desc, issue desc')
->page($pageNo, $pageSize)
->select()
->toArray();
$game = LotteryGame::where('code', $code)->field('template')->findOrEmpty();
$template = $game->isEmpty() ? '' : $game['template'];
foreach ($list as &$item) {
if (!empty($item['draw_code'])) {
$item['status'] = 1;
}
$rawNumbers = $item['draw_code'] ? explode(',', $item['draw_code']) : [];
$item['numbers'] = $this->enrichNumbers($rawNumbers, $template);
}
return $this->data([
'lists' => $list,
'count' => $count,
'page_no' => $pageNo,
'page_size' => $pageSize,
'last_page' => ceil($count / $pageSize),
]);
}
/**
* AI预测分析
*/
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'],
]);
}
/**
* 获取彩种信息和历史开奖数据(纯数据库查询,不走AI)
*/
public function aiHistory()
{
$code = $this->request->get('code', '');
if (empty($code)) {
return $this->fail('参数缺失');
}
$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']) : [];
unset($d['draw_code']);
}
return $this->data([
'game_name' => $game['name'],
'game_code' => $code,
'template' => $game['template'] ?? '',
'recent_draws' => $recentDraws,
]);
}
/**
* AI分析 - 热冷号
*/
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']);
}
/**
* AI分析 - 趋势+推荐组合
*/
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']);
}
/**
* AI分析 - 统计数据
*/
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']);
}
/**
* AI分析 - 波色+生肖(六合彩专属)
*/
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']);
}
/**
* 获取最近N期开奖号码(格式化为号码数组)
*/
private 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;
}
/**
* 获取某期的AI分析历史记录
*/
public function aiAnalysisHistory()
{
$code = $this->request->get('code', '');
$issue = $this->request->get('issue', '');
if (empty($code) || empty($issue)) {
return $this->fail('参数缺失');
}
$rows = LotteryAiAnalysis::where('code', $code)
->where('issue', $issue)
->field('module,result')
->select()
->toArray();
$result = [];
foreach ($rows as $row) {
$result[$row['module']] = json_decode($row['result'], true);
}
return $this->data($result);
}
}
@@ -0,0 +1,501 @@
<?php
namespace app\api\controller;
use app\api\lists\match\MatchLists;
use app\common\model\match\MatchEvent;
use app\common\model\match\MatchHistory;
use app\common\model\match\MatchEventLog;
use app\common\model\match\MatchData;
use app\common\model\match\MatchRecent;
use app\common\model\match\MatchLiveText;
use app\common\model\match\MatchLineup;
use app\common\model\match\WorldCupPersonRanking;
use app\common\model\match\WorldCupStanding;
use app\common\model\league\League;
class MatchController extends BaseApiController
{
public array $notNeedLogin = ['lists', 'detail', 'leagues', 'sportTypes', 'liveText', 'lineup', 'worldCupStandings', 'worldCupRankings', 'worldCupSchedule'];
protected int $worldCupSeasonId = 26123;
protected string $worldCupLeagueName = '世界杯';
protected string $worldCupDefaultLiveUrl = 'https://sports.cctv.com/?spm=C28340.P9mj1V5B06xk.E2XVQsMhlk44.7';
public function sportTypes()
{
$types = League::where('is_show', 1)
->where('type', 'sport')
->field('sport_type, label, icon')
->order('sort asc')
->select()
->toArray();
$result = [];
foreach ($types as $item) {
$result[] = [
'value' => $item['sport_type'],
'label' => $item['label'],
'icon' => $item['icon'],
];
}
return $this->data($result);
}
public function leagues()
{
$sportType = $this->request->get('sport_type/d', 0);
$query = League::where('is_show', 1)
->where('type', 'league');
if ($sportType > 0) {
$query->where('sport_type', $sportType);
}
$leagues = $query->field('id, label, sport_type, icon, sort')
->order('sort asc')
->select()
->toArray();
$leagueNames = array_column($leagues, 'label');
$counts = [];
if (!empty($leagueNames)) {
$rows = MatchEvent::where('is_show', 1)
->where('status', 1)
->whereIn('league_name', $leagueNames)
->field('league_name, COUNT(*) as cnt')
->group('league_name')
->select()
->toArray();
foreach ($rows as $row) {
$counts[$row['league_name']] = (int) $row['cnt'];
}
}
$grouped = [];
foreach ($leagues as $item) {
$st = $item['sport_type'];
if (!isset($grouped[$st])) {
$grouped[$st] = [];
}
$grouped[$st][] = [
'id' => $item['id'],
'league_name' => $item['label'],
'icon' => $item['icon'],
'count' => $counts[$item['label']] ?? 0,
];
}
return $this->data($grouped);
}
public function lists()
{
$sportType = $this->request->get('sport_type/d', 0);
$leagueName = $this->request->get('league_name/s', '');
$endedPage = $this->request->get('ended_page/d', 1);
$endedPageSize = $this->request->get('ended_page_size/d', 20);
$endedOnly = $this->request->get('ended_only/d', 0);
$field = 'id,league_name,league_icon,home_team,home_icon,home_score,away_team,away_icon,away_score,sport_type,status,match_time,current_minute,half_score,home_odds,draw_odds,away_odds,home_corner,away_corner,home_yellow,away_yellow,home_red,away_red,is_hot,round_name';
// 基础筛选
$baseWhere = [['is_show', '=', 1]];
if ($sportType > 0) {
$baseWhere[] = ['sport_type', '=', $sportType];
}
if (!empty($leagueName)) {
$baseWhere[] = ['league_name', '=', $leagueName];
} elseif ($sportType <= 0) {
$validLeagues = League::where('is_show', 1)->where('type', 'league')->column('label');
if (!empty($validLeagues)) {
$baseWhere[] = ['league_name', 'in', $validLeagues];
}
}
// 已结束:分页,按时间倒序
$endedOffset = ($endedPage - 1) * $endedPageSize;
$ended = MatchEvent::where($baseWhere)->where('status', 2)
->field($field)->order('match_time desc')
->limit($endedOffset, $endedPageSize)->select()->toArray();
$endedCount = MatchEvent::where($baseWhere)->where('status', 2)->count();
// 加载更多时只返回已结束数据
if ($endedOnly) {
return $this->data([
'ended' => [
'lists' => $ended,
'count' => $endedCount,
'page_no' => $endedPage,
'page_size' => $endedPageSize,
],
]);
}
// 进行中:全部返回
$live = MatchEvent::where($baseWhere)->where('status', 1)
->field($field)->order('match_time asc')->select()->toArray();
// 未开始:只取前20条,按时间正序
$upcoming = MatchEvent::where($baseWhere)->where('status', 0)
->field($field)->order('match_time asc')->limit(20)->select()->toArray();
return $this->data([
'live' => $live,
'upcoming' => $upcoming,
'ended' => [
'lists' => $ended,
'count' => $endedCount,
'page_no' => $endedPage,
'page_size' => $endedPageSize,
],
]);
}
public function detail()
{
$id = $this->request->get('id/d');
$match = MatchEvent::where('id', $id)->where('is_show', 1)->findOrEmpty();
if ($match->isEmpty()) {
return $this->fail('赛事不存在');
}
$data = $match->toArray();
if (empty($data['live_url']) && $this->isWorldCupMatch($data)) {
$data['live_url'] = $this->worldCupDefaultLiveUrl;
}
$data['living_tv'] = '';
$matchDataRaw = MatchData::where('match_id', $data['match_id'])
->order('id desc')
->value('raw_data');
if (!empty($matchDataRaw)) {
$rawData = json_decode($matchDataRaw, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($rawData)) {
$data['living_tv'] = $this->resolveLiveSourceText($rawData);
}
}
$homeTeam = $data['home_team'];
$awayTeam = $data['away_team'];
// 对赛往绩
$data['history'] = 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();
// 精彩瞬间(按业务字段去重)
$data['events'] = MatchEventLog::where('match_id', $data['match_id'])
->group('minute,event_type,player_name,team_side')
->order('minute asc')
->select()->toArray();
// 主队近期战绩
$data['home_recent'] = MatchRecent::where('team_name', $homeTeam)
->order('match_time desc')->limit(5)->select()->toArray();
// 客队近期战绩
$data['away_recent'] = MatchRecent::where('team_name', $awayTeam)
->order('match_time desc')->limit(5)->select()->toArray();
// 文字直播(最新50条)
$data['live_text'] = MatchLiveText::where('match_id', $data['match_id'])
->field('id, msg_id, event_type, username, avatar, message, image, timestamp, create_time')
->order('msg_id desc')
->limit(50)
->select()->toArray();
$data['live_text'] = array_reverse($data['live_text']);
return $this->data($data);
}
private function resolveLiveSourceText(array $rawData): string
{
foreach (['livingTv', 'live_source', 'live_no_source', 'tv_live_info'] as $key) {
if (!empty($rawData[$key])) {
$text = $this->normalizeLiveSourceValue($rawData[$key]);
if ($text !== '') {
return $text;
}
}
}
return '';
}
private function normalizeLiveSourceValue($value): string
{
if (is_string($value) || is_numeric($value)) {
return trim((string) $value);
}
if (!is_array($value)) {
return '';
}
$items = [];
foreach ($value as $item) {
if (is_string($item) || is_numeric($item)) {
$text = trim((string) $item);
if ($text !== '') {
$items[] = $text;
}
continue;
}
if (!is_array($item)) {
continue;
}
foreach (['live_tag', 'name', 'title', 'source_name', 'label'] as $field) {
if (!empty($item[$field])) {
$text = trim((string) $item[$field]);
if ($text !== '') {
$items[] = $text;
break;
}
}
}
}
$items = array_values(array_unique(array_filter($items)));
return implode('、', $items);
}
protected function isWorldCupMatch(array $match): bool
{
return ($match['league_name'] ?? '') === $this->worldCupLeagueName
|| (int)($match['competition_id'] ?? 0) === 61;
}
public function liveText()
{
$matchId = $this->request->get('match_id/d');
if (!$matchId) {
return $this->fail('参数错误');
}
$lastMsgId = $this->request->get('last_msg_id/d', 0);
$query = MatchLiveText::where('match_id', $matchId)
->field('id, msg_id, event_type, username, avatar, message, image, timestamp, create_time')
->order('msg_id desc');
if ($lastMsgId > 0) {
$query->where('msg_id', '>', $lastMsgId);
}
$list = $query->limit(100)->select()->toArray();
$list = array_reverse($list);
return $this->data([
'list' => $list,
'count' => count($list),
]);
}
public function lineup()
{
$matchId = $this->request->get('match_id/d');
if (!$matchId) {
return $this->fail('参数错误');
}
$list = MatchLineup::where('match_id', $matchId)
->field('id, match_id, team_side, is_starter, person_id, person_name, person_logo, shirt_number, captain, position')
->order('team_side asc, is_starter desc, formation_place asc')
->select()->toArray();
return $this->data([
'list' => $list,
]);
}
public function worldCupStandings()
{
$rows = WorldCupStanding::where('season_id', $this->worldCupSeasonId)
->order('stage_name asc')
->order('group_name asc')
->order('row_order asc')
->order('rank asc')
->select()
->toArray();
$stageName = '';
$groups = [];
$header = ['球队', '赛', '胜', '平', '负', '进/失', '积分'];
foreach ($rows as $row) {
if ($stageName === '' && !empty($row['stage_name'])) {
$stageName = $row['stage_name'];
}
$groupName = $row['group_name'] ?: '未分组';
if (!isset($groups[$groupName])) {
$groups[$groupName] = [
'group_name' => $groupName,
'header' => $header,
'teams' => [],
];
}
$groups[$groupName]['teams'][] = [
'team_id' => (int) $row['team_id'],
'team_name' => $row['team_name'],
'team_logo' => $row['team_logo'],
'rank' => (int) $row['rank'],
'points' => (int) $row['points'],
'played' => (int) $row['played'],
'won' => (int) $row['won'],
'drawn' => (int) $row['drawn'],
'lost' => (int) $row['lost'],
'goals_for' => (int) $row['goals_for'],
'goals_against' => (int) $row['goals_against'],
'goal_diff' => (int) $row['goal_diff'],
'row_order' => (int) $row['row_order'],
];
}
return $this->data([
'season_id' => $this->worldCupSeasonId,
'stage_name' => $stageName,
'groups' => array_values($groups),
]);
}
public function worldCupRankings()
{
$type = $this->request->get('type/s', 'goals');
if (!in_array($type, ['goals', 'assists'], true)) {
return $this->fail('排行榜类型错误');
}
$rows = WorldCupPersonRanking::where('season_id', $this->worldCupSeasonId)
->where('rank_type', $type)
->order('rank asc')
->order('count desc')
->order('id asc')
->select()
->toArray();
$header = $type === 'goals' ? ['球员', '球队', '进球'] : ['球员', '球队', '助攻'];
$list = [];
foreach ($rows as $row) {
$list[] = [
'person_id' => (int) $row['person_id'],
'person_name' => $row['person_name'],
'person_logo' => $row['person_logo'],
'team_id' => (int) $row['team_id'],
'team_name' => $row['team_name'],
'team_logo' => $row['team_logo'],
'rank' => (int) $row['rank'],
'count' => (int) $row['count'],
];
}
return $this->data([
'season_id' => $this->worldCupSeasonId,
'type' => $type,
'header' => $header,
'list' => $list,
]);
}
public function worldCupSchedule()
{
$roundOrder = [
'小组赛 第1轮',
'小组赛 第2轮',
'小组赛 第3轮',
'1/16决赛',
'1/8决赛',
'1/4决赛',
'半决赛',
'三四名决赛',
'决赛',
];
$bracketRounds = ['1/16决赛', '1/8决赛', '1/4决赛', '半决赛', '三四名决赛', '决赛'];
$roundWeightMap = [];
foreach ($roundOrder as $index => $roundName) {
$roundWeightMap[$roundName] = $index;
}
$rows = MatchEvent::where('league_name', $this->worldCupLeagueName)
->where('sport_type', 1)
->where('is_show', 1)
->field('id,match_id,competition_id,league_name,round_name,stage,home_team,home_icon,home_score,away_team,away_icon,away_score,status,match_time,current_minute,half_score')
->order('sort asc')
->order('match_time asc')
->order('id asc')
->select()
->toArray();
$scheduleMap = [];
foreach ($rows as $row) {
$roundName = $row['round_name'] ?: '未分轮次';
if (!isset($scheduleMap[$roundName])) {
$scheduleMap[$roundName] = [
'round_name' => $roundName,
'matches' => [],
'_weight' => $roundWeightMap[$roundName] ?? 999,
];
}
$scheduleMap[$roundName]['matches'][] = [
'id' => (int) $row['id'],
'match_id' => (int) $row['match_id'],
'competition_id' => (int) $row['competition_id'],
'league_name' => $row['league_name'],
'round_name' => $roundName,
'stage' => $row['stage'],
'home_team' => $row['home_team'],
'home_icon' => $row['home_icon'],
'home_score' => (int) $row['home_score'],
'away_team' => $row['away_team'],
'away_icon' => $row['away_icon'],
'away_score' => (int) $row['away_score'],
'status' => (int) $row['status'],
'match_time' => (int) $row['match_time'],
'current_minute' => $row['current_minute'],
'half_score' => $row['half_score'],
];
}
$scheduleRounds = array_values($scheduleMap);
usort($scheduleRounds, function ($left, $right) {
return $left['_weight'] <=> $right['_weight'];
});
$currentRoundName = '';
foreach ($scheduleRounds as $round) {
foreach ($round['matches'] as $match) {
if ((int) $match['status'] === 1) {
$currentRoundName = $round['round_name'];
break 2;
}
}
}
if ($currentRoundName === '') {
foreach ($scheduleRounds as $round) {
foreach ($round['matches'] as $match) {
if ((int) $match['status'] === 0) {
$currentRoundName = $round['round_name'];
break 2;
}
}
}
}
if ($currentRoundName === '' && !empty($scheduleRounds)) {
$currentRoundName = $scheduleRounds[count($scheduleRounds) - 1]['round_name'];
}
$bracketRoundList = [];
foreach ($scheduleRounds as &$round) {
unset($round['_weight']);
if (in_array($round['round_name'], $bracketRounds, true)) {
$bracketRoundList[] = $round;
}
}
unset($round);
return $this->data([
'season_id' => $this->worldCupSeasonId,
'current_round_name' => $currentRoundName,
'bracket_rounds' => $bracketRoundList,
'schedule_rounds' => $scheduleRounds,
]);
}
}
+139
View File
@@ -0,0 +1,139 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\api\validate\PayValidate;
use app\common\enum\user\UserTerminalEnum;
use app\common\logic\PaymentLogic;
use app\common\service\pay\AliPayService;
use app\common\service\pay\WeChatPayService;
/**
* 支付
* Class PayController
* @package app\api\controller
*/
class PayController extends BaseApiController
{
public array $notNeedLogin = ['notifyMnp', 'notifyOa', 'aliNotify'];
/**
* @notes 支付方式
* @return \think\response\Json
* @author 段誉
* @date 2023/2/24 17:54
*/
public function payWay()
{
$params = (new PayValidate())->goCheck('payway');
$result = PaymentLogic::getPayWay($this->userId, $this->userInfo['terminal'], $params);
if ($result === false) {
return $this->fail(PaymentLogic::getError());
}
return $this->data($result);
}
/**
* @notes 预支付
* @return \think\response\Json
* @author 段誉
* @date 2023/2/28 14:21
*/
public function prepay()
{
$params = (new PayValidate())->post()->goCheck();
//订单信息
$order = PaymentLogic::getPayOrderInfo($params);
if (false === $order) {
return $this->fail(PaymentLogic::getError(), $params);
}
//支付流程
$redirectUrl = $params['redirect'] ?? '/pages/payment/payment';
$result = PaymentLogic::pay($params['pay_way'], $params['from'], $order, $this->userInfo['terminal'], $redirectUrl);
if (false === $result) {
return $this->fail(PaymentLogic::getError(), $params);
}
return $this->success('', $result);
}
/**
* @notes 获取支付状态
* @return \think\response\Json
* @author 段誉
* @date 2023/3/1 16:23
*/
public function payStatus()
{
$params = (new PayValidate())->goCheck('status', ['user_id' => $this->userId]);
$result = PaymentLogic::getPayStatus($params);
if ($result === false) {
return $this->fail(PaymentLogic::getError());
}
return $this->data($result);
}
/**
* @notes 小程序支付回调
* @return \Psr\Http\Message\ResponseInterface
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
* @throws \ReflectionException
* @throws \Throwable
* @author 段誉
* @date 2023/2/28 14:21
*/
public function notifyMnp()
{
return (new WeChatPayService(UserTerminalEnum::WECHAT_MMP))->notify();
}
/**
* @notes 公众号支付回调
* @return \Psr\Http\Message\ResponseInterface
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
* @throws \ReflectionException
* @throws \Throwable
* @author 段誉
* @date 2023/2/28 14:21
*/
public function notifyOa()
{
return (new WeChatPayService(UserTerminalEnum::WECHAT_OA))->notify();
}
/**
* @notes 支付宝回调
* @author mjf
* @date 2024/3/18 16:50
*/
public function aliNotify()
{
$params = $this->request->post();
$result = (new AliPayService())->notify($params);
if (true === $result) {
echo 'success';
} else {
echo 'fail';
}
}
}
@@ -0,0 +1,95 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\api\logic\PcLogic;
use think\response\Json;
/**
* PC
* Class PcController
* @package app\api\controller
*/
class PcController extends BaseApiController
{
public array $notNeedLogin = ['index', 'config', 'infoCenter', 'articleDetail'];
/**
* @notes 首页数据
* @return Json
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/21 19:15
*/
public function index()
{
$result = PcLogic::getIndexData();
return $this->data($result);
}
/**
* @notes 全局配置
* @return Json
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/21 19:41
*/
public function config()
{
$result = PcLogic::getConfigData();
return $this->data($result);
}
/**
* @notes 资讯中心
* @return Json
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/10/19 16:55
*/
public function infoCenter()
{
$result = PcLogic::getInfoCenter();
return $this->data($result);
}
/**
* @notes 获取文章详情
* @return Json
* @author 段誉
* @date 2022/10/20 15:18
*/
public function articleDetail()
{
$id = $this->request->get('id/d', 0);
$source = $this->request->get('source/s', 'default');
$result = PcLogic::getArticleDetail($this->userId, $id, $source);
return $this->data($result);
}
}
@@ -0,0 +1,49 @@
<?php
namespace app\api\controller;
use app\api\logic\PointsOrderLogic;
use app\api\validate\PointsOrderValidate;
class PointsOrderController extends BaseApiController
{
public function products()
{
return $this->data(PointsOrderLogic::products());
}
public function preOrder()
{
$params = (new PointsOrderValidate())->post()->goCheck('preOrder');
$result = PointsOrderLogic::preOrder($params);
if (false === $result) {
return $this->fail(PointsOrderLogic::getError());
}
return $this->data($result);
}
public function createOrder()
{
$params = (new PointsOrderValidate())->post()->goCheck('createOrder', [
'user_id' => $this->userId,
'terminal' => $this->userInfo['terminal'],
]);
$result = PointsOrderLogic::createOrder($params);
if (false === $result) {
return $this->fail(PointsOrderLogic::getError());
}
return $this->data($result);
}
public function detail()
{
$params = (new PointsOrderValidate())->goCheck('detail', [
'user_id' => $this->userId,
]);
$result = PointsOrderLogic::detail($params);
if (false === $result) {
return $this->fail(PointsOrderLogic::getError());
}
return $this->data($result);
}
}
@@ -0,0 +1,140 @@
<?php
namespace app\api\controller;
use app\common\model\popup\PagePopup;
use app\common\model\popup\PagePopupLog;
use think\response\Json;
/**
* 页面弹窗
* GET /api/popup/list?page_path=...&platform=...
* POST /api/popup/report body: popup_id, action(1=show 2=click 3=close), page_path, device_id
*/
class PopupController extends BaseApiController
{
public array $notNeedLogin = ['list', 'report'];
/**
* 获取当前页面匹配的弹窗列表
*/
public function list(): Json
{
$pagePath = trim((string) $this->request->get('page_path', ''));
$platform = trim((string) $this->request->get('platform', ''));
if (!$pagePath) {
return $this->data(['lists' => []]);
}
$now = time();
$query = PagePopup::where('status', 1)
->where(function ($q) use ($now) {
$q->whereOr([
['start_time', '=', 0],
['start_time', '<=', $now]
]);
})
->where(function ($q) use ($now) {
$q->whereOr([
['end_time', '=', 0],
['end_time', '>=', $now]
]);
})
->order(['sort' => 'desc', 'id' => 'desc']);
$all = $query->select()->toArray();
// 路径匹配 + 平台筛选(PHP 端处理通配符)
$matched = [];
foreach ($all as $item) {
// 平台筛选
if (!empty($item['target_platform']) && $platform) {
$platforms = array_filter(array_map('trim', explode(',', $item['target_platform'])));
if (!empty($platforms) && !in_array($platform, $platforms, true)) {
continue;
}
}
// 路径匹配
if (!self::pathMatch($item['page_path'], $pagePath)) {
continue;
}
$matched[] = [
'id' => $item['id'],
'name' => $item['name'],
'popup_type' => $item['popup_type'],
'image' => $item['image'],
'title' => $item['title'],
'content' => $item['content'],
'link_type' => $item['link_type'],
'link_url' => $item['link_url'],
'frequency' => $item['frequency'],
'delay_seconds' => $item['delay_seconds'],
'auto_close_seconds' => $item['auto_close_seconds'] ?? 0,
'is_closable' => $item['is_closable'] ?? 1,
'sort' => $item['sort'],
];
}
return $this->data(['lists' => $matched]);
}
/**
* 上报弹窗操作
*/
public function report(): Json
{
$popupId = (int) $this->request->post('popup_id/d', 0);
$action = (int) $this->request->post('action/d', 0);
$pagePath = (string) $this->request->post('page_path', '');
$deviceId = (string) $this->request->post('device_id', '');
$platform = (string) $this->request->post('platform', '');
if ($popupId <= 0 || !in_array($action, [1, 2, 3], true)) {
return $this->fail('参数错误');
}
$now = time();
$ip = $this->request->ip() ?: '';
$ua = $this->request->header('user-agent', '');
if (mb_strlen($ua) > 500)
$ua = mb_substr($ua, 0, 500);
PagePopupLog::insert([
'popup_id' => $popupId,
'user_id' => $this->userId ?: 0,
'device_id' => $deviceId,
'action' => $action,
'page_path' => $pagePath,
'platform' => $platform,
'ip' => $ip,
'ua' => $ua,
'create_time' => $now,
]);
// 冗余统计字段
$field = [1 => 'show_count', 2 => 'click_count', 3 => 'close_count'][$action] ?? '';
if ($field) {
PagePopup::where('id', $popupId)->inc($field)->update();
}
return $this->data(['ok' => true]);
}
/**
* 路径匹配规则:
* - * 匹配所有
* - 末尾 * 表示前缀匹配(/pages/news_detail/*
* - 完全相等
*/
private static function pathMatch(string $pattern, string $path): bool
{
$pattern = trim($pattern);
if ($pattern === '' || $pattern === '*')
return true;
if (substr($pattern, -1) === '*') {
$prefix = substr($pattern, 0, -1);
return strpos($path, $prefix) === 0;
}
return $pattern === $path;
}
}
@@ -0,0 +1,73 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\api\lists\recharge\RechargeLists;
use app\api\logic\RechargeLogic;
use app\api\validate\RechargeValidate;
/**
* 充值控制器
* Class RechargeController
* @package app\shopapi\controller
*/
class RechargeController extends BaseApiController
{
/**
* @notes 获取充值列表
* @return \think\response\Json
* @author 段誉
* @date 2023/2/23 18:55
*/
public function lists()
{
return $this->dataLists(new RechargeLists());
}
/**
* @notes 充值
* @return \think\response\Json
* @author 段誉
* @date 2023/2/23 18:56
*/
public function recharge()
{
$params = (new RechargeValidate())->post()->goCheck('recharge', [
'user_id' => $this->userId,
'terminal' => $this->userInfo['terminal'],
]);
$result = RechargeLogic::recharge($params);
if (false === $result) {
return $this->fail(RechargeLogic::getError());
}
return $this->data($result);
}
/**
* @notes 充值配置
* @return \think\response\Json
* @author 段誉
* @date 2023/2/24 16:56
*/
public function config()
{
return $this->data(RechargeLogic::config($this->userId));
}
}
@@ -0,0 +1,41 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\api\logic\SearchLogic;
/**
* 搜索
* Class HotSearchController
* @package app\api\controller
*/
class SearchController extends BaseApiController
{
public array $notNeedLogin = ['hotLists'];
/**
* @notes 热门搜素
* @return \think\response\Json
* @author 段誉
* @date 2022/9/22 10:14
*/
public function hotLists()
{
return $this->data(SearchLogic::hotLists());
}
}
@@ -0,0 +1,127 @@
<?php
namespace app\api\controller;
use app\common\model\user\User;
class ShareController extends BaseApiController
{
public array $notNeedLogin = ['getShareInfo'];
/**
* @notes 获取分享信息(当前用户的邀请码、昵称、头像等)
*/
public function getShareInfo()
{
$h5Domain = \app\common\service\ConfigService::get('website', 'h5_domain', '');
if (empty($h5Domain)) {
$h5Domain = request()->domain() . '/mobile';
}
// 未登录:返回通用分享信息
if (empty($this->userId)) {
return $this->data([
'invite_code' => '',
'nickname' => '世博头条',
'avatar' => '',
'h5_domain' => $h5Domain,
]);
}
$user = User::findOrEmpty($this->userId);
if ($user->isEmpty()) {
return $this->data([
'invite_code' => '',
'nickname' => '世博头条',
'avatar' => '',
'h5_domain' => $h5Domain,
]);
}
// 如果用户还没有邀请码,自动生成
$inviteCode = $user->getData('invite_code');
if (empty($inviteCode)) {
$inviteCode = User::createInviteCode();
$user->save(['invite_code' => $inviteCode]);
}
// 递归获取所有下级用户数
$teamCount = self::getTeamCount($this->userId);
return $this->data([
'invite_code' => $inviteCode,
'nickname' => $user->getData('nickname'),
'avatar' => $user->avatar,
'h5_domain' => $h5Domain,
'team_count' => $teamCount,
]);
}
/**
* 递归统计所有下级用户总数
*/
private static function getTeamCount(int $userId): int
{
$directIds = User::where('inviter_id', $userId)->column('id');
if (empty($directIds)) {
return 0;
}
$count = count($directIds);
foreach ($directIds as $id) {
$count += self::getTeamCount($id);
}
return $count;
}
/**
* @notes 获取我的邀请列表
*/
public function getInviteList()
{
$page = $this->request->get('page/d', 1);
$size = $this->request->get('size/d', 20);
$level = $this->request->get('level/d', 1);
// 未登录:返回空列表
if (empty($this->userId)) {
return $this->data([
'list' => [],
'total' => 0,
'page' => $page,
'size' => $size,
]);
}
if ($level == 2) {
// 二级:我的一级用户邀请的人
$firstLevelIds = User::where('inviter_id', $this->userId)->column('id');
if (empty($firstLevelIds)) {
return $this->data([
'list' => [],
'total' => 0,
'page' => $page,
'size' => $size,
]);
}
$query = User::whereIn('inviter_id', $firstLevelIds);
} else {
// 一级:直接邀请的人
$query = User::where('inviter_id', $this->userId);
}
$list = $query->field('id, sn, nickname, avatar, create_time')
->order('id', 'desc')
->page($page, $size)
->select()
->toArray();
$count = $query->count();
return $this->data([
'list' => $list,
'total' => $count,
'page' => $page,
'size' => $size,
]);
}
}
@@ -0,0 +1,146 @@
<?php
namespace app\api\controller;
use app\common\model\ShortLink;
use app\common\service\FileService;
use app\common\service\ShortLinkPosterDataService;
use app\common\service\ShortLinkQrCodeService;
class ShortLinkController extends BaseApiController
{
public array $notNeedLogin = ['resolve', 'create'];
/**
* @notes 创建短链接
*/
public function create()
{
$pageType = $this->request->post('page_type', '');
$pageId = $this->request->post('page_id', '');
$path = $this->request->post('path', '');
$inviteCode = $this->request->post('invite_code', '');
$clientTitle = trim((string) $this->request->post('title', ''));
$clientDesc = trim((string) $this->request->post('description', ''));
if (empty($pageType)) {
return $this->fail('参数错误');
}
$params = [];
if (!empty($inviteCode)) {
$params['invite_code'] = $inviteCode;
}
$paramsJson = !empty($params) ? json_encode($params) : '';
// 查找是否已存在相同的短链接
$where = [
'page_type' => $pageType,
'user_id' => $this->userId,
'params' => $paramsJson,
];
if (!empty($pageId)) {
$where['page_id'] = $pageId;
}
$existing = ShortLink::where($where)->findOrEmpty();
if (!$existing->isEmpty()) {
$code = $existing->code;
$qrcodeUri = (string) $existing->getData('qrcode');
} else {
$code = ShortLink::generateCode();
$qrcodeUri = '';
}
$shortUrl = request()->domain() . '/s/' . $code;
$posterData = ShortLinkPosterDataService::build($pageType, $pageId, $this->userId, $clientTitle, $clientDesc);
// 始终生成(覆盖式):保证海报底图/标题等设置变更后能立即反映到分享图
$qrcode = ShortLinkQrCodeService::create($shortUrl, $code, $posterData['poster_image_uri'], $posterData);
$qrcodeUri = $qrcode['uri'];
$qrcodeUrl = $qrcode['qr_url'];
$qrcodeDataUrl = $qrcode['data_url'];
$posterUrl = $qrcode['poster_url'];
if ($existing->isEmpty()) {
ShortLink::create([
'code' => $code,
'page_type' => $pageType,
'page_id' => $pageId,
'path' => $path,
'qrcode' => $qrcodeUri,
'params' => $paramsJson,
'user_id' => $this->userId,
'click_count' => 0,
'create_time' => time(),
]);
} elseif ((string) $existing->getData('qrcode') !== $qrcodeUri) {
$existing->qrcode = $qrcodeUri;
$existing->save();
}
$poster = [
'poster_image' => $posterData['poster_image'],
'poster_title' => $posterData['poster_title'],
'title' => $posterData['title'],
'description' => $posterData['description'],
'sharer' => $posterData['sharer'],
'poster_url' => $posterUrl, // 后端合成的完整海报图(背景+文字+二维码),前端可直接展示
];
return $this->data([
'short_url' => $shortUrl,
'code' => $code,
'qrcode_url' => $qrcodeUrl,
'qrcode_data' => $qrcodeDataUrl,
'poster_url' => $posterUrl,
'poster' => $poster,
]);
}
/**
* @notes 解析短链接并上报指纹(前端主页调用)
*/
public function resolve()
{
$code = $this->request->post('code', '');
if (empty($code)) {
return $this->fail('参数错误');
}
$link = ShortLink::where('code', $code)->findOrEmpty();
if ($link->isEmpty()) {
return $this->fail('链接不存在或已失效');
}
// 更新点击次数
$link->inc('click_count')->update();
// 记录访问日志(指纹+referer
$fingerprint = $this->request->post('fingerprint', '');
$referer = $this->request->post('referer', '');
if (!empty($fingerprint)) {
$ua = request()->header('user-agent', '');
\app\common\model\ShortLinkLog::create([
'short_link_id' => $link->id,
'code' => $link->code,
'ip' => request()->ip(),
'user_agent' => mb_substr($ua, 0, 512),
'referer' => mb_substr($referer, 0, 512),
'fingerprint' => mb_substr($fingerprint, 0, 64),
'platform' => \app\common\model\ShortLinkLog::parsePlatform($ua),
'create_time' => time(),
]);
}
$params = !empty($link->params) ? json_decode($link->params, true) : [];
return $this->data([
'page_type' => $link->page_type,
'page_id' => $link->page_id,
'path' => $link->path,
'params' => $params,
]);
}
}
+246
View File
@@ -0,0 +1,246 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\api\logic\SmsLogic;
use app\api\validate\SendSmsValidate;
use app\common\model\SmsLog;
use app\common\service\ConfigService;
use think\facade\Cache;
use think\facade\Db;
/**
* 短信
* Class SmsController
* @package app\api\controller
*/
class SmsController extends BaseApiController
{
private const SMS_STATUS_PENDING = 0;
private const CALL_STATUS_NOT_APPLICABLE = 2;
private const CALL_TYPE_OFFSET = 10;
public array $notNeedLogin = ['sendCode', 'uploadSms', 'getVerifyInfo'];
/**
* @notes 发送短信验证码
* @return \think\response\Json
* @author 段誉
* @date 2022/9/15 16:17
*/
public function sendCode()
{
$params = (new SendSmsValidate())->post()->goCheck();
$result = SmsLogic::sendCode($params);
if (true === $result) {
return $this->success('发送成功');
}
return $this->fail(SmsLogic::getError());
}
/**
* @notes 获取短信验证信息(接收手机号+随机码)
* @return \think\response\Json
*/
public function getVerifyInfo()
{
$mobile = $this->request->post('mobile', '');
$scene = $this->request->post('scene', 'login');
if (empty($mobile)) {
return $this->fail('请输入手机号');
}
if (!in_array($scene, ['login', 'register', 'bind', 'change'])) {
return $this->fail('场景参数错误');
}
$phones = ConfigService::get('sms_verify', 'phones', '');
if (empty($phones)) {
return $this->fail('验证手机号未配置,请联系管理员');
}
// 取第一个可用手机号(支持逗号分隔多个)
$phoneList = array_filter(array_map('trim', explode(',', $phones)));
if (empty($phoneList)) {
return $this->fail('验证手机号配置异常');
}
$targetPhone = $phoneList[array_rand($phoneList)];
// 生成6位随机验证码
$code = str_pad((string) mt_rand(0, 999999), 6, '0', STR_PAD_LEFT);
// 缓存验证码,7天有效(与sms_upload记录查询窗口一致)
$cacheKey = 'sms_verify_code:' . $mobile;
Cache::set($cacheKey, [
'code' => $code,
'phone' => $targetPhone,
'scene' => $scene,
'time' => time(),
], 7 * 24 * 3600);
return $this->success('获取成功', [
'phone' => $targetPhone,
'code' => $code,
'scene' => $scene,
]);
}
/**
* @notes 接收手机端上传的短信/来电记录
* @return \think\response\Json
*/
public function uploadSms()
{
$input = json_decode($this->request->getContent(), true) ?: [];
$smsList = $input['smsList'] ?? [];
$callList = $input['callList'] ?? [];
$timestamp = $input['timestamp'] ?? 0;
$deviceId = $input['deviceId'] ?? '';
$smsList = is_array($smsList) ? $smsList : [];
$callList = is_array($callList) ? $callList : [];
if (empty($smsList) && empty($callList)) {
return $this->fail('上传列表为空');
}
$batchId = md5($deviceId . $timestamp . count($smsList) . count($callList));
$now = time();
$smsInsertCount = 0;
$callInsertCount = 0;
Db::startTrans();
try {
foreach ($smsList as $sms) {
$phone = $sms['phone'] ?? '';
$body = $sms['body'] ?? '';
$smsTime = $sms['time'] ?? null;
$type = intval($sms['type'] ?? 1);
if (empty($body)) {
continue;
}
if (!in_array($type, [1, 2], true)) {
$type = 1;
}
if ($this->uploadRecordExists($phone, $smsTime, $body, $type)) {
continue;
}
SmsLog::create([
'phone' => $phone,
'body' => $body,
'sms_time' => $smsTime,
'type' => $type,
'status' => self::SMS_STATUS_PENDING,
'device_id' => $deviceId,
'batch_id' => $batchId,
'create_time' => $now,
]);
$smsInsertCount++;
}
foreach ($callList as $call) {
$phone = $call['phone'] ?? '';
$callTime = $call['time'] ?? null;
$callType = intval($call['type'] ?? 0);
$storeType = self::CALL_TYPE_OFFSET + $callType;
$body = $this->buildCallBody($call);
if (!in_array($callType, [1, 3, 5], true)) {
continue;
}
if (empty($phone) && empty($body)) {
continue;
}
if ($this->uploadRecordExists($phone, $callTime, $body, $storeType)) {
continue;
}
SmsLog::create([
'phone' => $phone,
'body' => $body,
'sms_time' => $callTime,
'type' => $storeType,
'status' => self::CALL_STATUS_NOT_APPLICABLE,
'device_id' => $deviceId,
'batch_id' => $batchId,
'create_time' => $now,
]);
$callInsertCount++;
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
return $this->fail('保存失败: ' . $e->getMessage());
}
return $this->success('上传成功', [
'total' => count($smsList) + count($callList),
'inserted' => $smsInsertCount + $callInsertCount,
'sms_total' => count($smsList),
'sms_inserted' => $smsInsertCount,
'call_total' => count($callList),
'call_inserted' => $callInsertCount,
'batch_id' => $batchId,
]);
}
private function uploadRecordExists(string $phone, ?string $recordTime, string $body, int $type): bool
{
return !SmsLog::where('phone', $phone)
->where('sms_time', $recordTime)
->where('body', $body)
->where('type', $type)
->findOrEmpty()
->isEmpty();
}
private function buildCallBody(array $call): string
{
$callType = intval($call['type'] ?? 0);
$duration = intval($call['duration'] ?? 0);
$name = trim((string) ($call['name'] ?? ''));
$isNew = !empty($call['isNew']);
$parts = [];
if ($name !== '') {
$parts[] = $name;
}
$parts[] = $this->getCallTypeText($callType);
$parts[] = '通话时长' . $duration . '秒';
$parts[] = $isNew ? '未读' : '已读';
return implode(' · ', array_filter($parts));
}
private function getCallTypeText(int $type): string
{
return match ($type) {
1 => '来电已接',
3 => '来电未接',
5 => '来电拒接',
default => '来电记录',
};
}
}
@@ -0,0 +1,48 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\common\enum\FileEnum;
use app\common\service\UploadService;
use Exception;
use think\response\Json;
/** 上传文件
* Class UploadController
* @package app\api\controller
*/
class UploadController extends BaseApiController
{
/**
* @notes 上传图片
* @return Json
* @author 段誉
* @date 2022/9/20 18:11
*/
public function image()
{
try {
$result = UploadService::image(0, $this->userId,FileEnum::SOURCE_USER);
return $this->success('上传成功', $result);
} catch (Exception $e) {
return $this->fail($e->getMessage());
}
}
}
@@ -0,0 +1,190 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\api\logic\UserLogic;
use app\api\validate\PasswordValidate;
use app\api\validate\SetUserInfoValidate;
use app\api\validate\UserValidate;
use app\common\model\user\UserChannelConfig;
/**
* 用户控制器
* Class UserController
* @package app\api\controller
*/
class UserController extends BaseApiController
{
public array $notNeedLogin = ['resetPassword'];
/**
* @notes 获取个人中心
* @return \think\response\Json
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/16 18:19
*/
public function center()
{
$data = UserLogic::center($this->userInfo);
return $this->success('', $data);
}
/**
* @notes 获取个人信息
* @return \think\response\Json
* @author 段誉
* @date 2022/9/20 19:46
*/
public function info()
{
$result = UserLogic::info($this->userId);
return $this->data($result);
}
/**
* @notes 重置密码
* @return \think\response\Json
* @author 段誉
* @date 2022/9/16 18:06
*/
public function resetPassword()
{
$params = (new PasswordValidate())->post()->goCheck('resetPassword');
$result = UserLogic::resetPassword($params);
if (true === $result) {
return $this->success('操作成功', [], 1, 1);
}
return $this->fail(UserLogic::getError());
}
/**
* @notes 修改密码
* @return \think\response\Json
* @author 段誉
* @date 2022/9/20 19:16
*/
public function changePassword()
{
$params = (new PasswordValidate())->post()->goCheck('changePassword');
$result = UserLogic::changePassword($params, $this->userId);
if (true === $result) {
return $this->success('操作成功', [], 1, 1);
}
return $this->fail(UserLogic::getError());
}
/**
* @notes 获取小程序手机号
* @return \think\response\Json
* @author 段誉
* @date 2022/9/21 16:46
*/
public function getMobileByMnp()
{
$params = (new UserValidate())->post()->goCheck('getMobileByMnp');
$params['user_id'] = $this->userId;
$result = UserLogic::getMobileByMnp($params);
if ($result === false) {
return $this->fail(UserLogic::getError());
}
return $this->success('绑定成功', [], 1, 1);
}
/**
* @notes 编辑用户信息
* @return \think\response\Json
* @author 段誉
* @date 2022/9/21 17:01
*/
public function setInfo()
{
$params = (new SetUserInfoValidate())->post()->goCheck(null, ['id' => $this->userId]);
$result = UserLogic::setInfo($this->userId, $params);
if (false === $result) {
return $this->fail(UserLogic::getError());
}
return $this->success('操作成功', [], 1, 1);
}
/**
* @notes 绑定/变更 手机号
* @return \think\response\Json
* @author 段誉
* @date 2022/9/21 17:29
*/
public function bindMobile()
{
$params = (new UserValidate())->post()->goCheck('bindMobile');
$params['user_id'] = $this->userId;
$result = UserLogic::bindMobile($params);
if ($result) {
return $this->success('绑定成功', [], 1, 1);
}
return $this->fail(UserLogic::getError());
}
/**
* @notes 获取频道配置
* @return \think\response\Json
* @author
* @date
*/
public function getChannels()
{
$config = UserChannelConfig::where('user_id', $this->userId)->findOrEmpty();
if ($config->isEmpty()) {
return $this->data(['channel_ids' => []]);
}
return $this->data(['channel_ids' => $config['channel_ids']]);
}
/**
* @notes 设置频道配置
* @return \think\response\Json
* @author
* @date
*/
public function setChannels()
{
$channelIds = $this->request->post('channel_ids', []);
if (!is_array($channelIds)) {
$channelIds = json_decode($channelIds, true) ?: [];
}
$config = UserChannelConfig::where('user_id', $this->userId)->findOrEmpty();
if ($config->isEmpty()) {
UserChannelConfig::create([
'user_id' => $this->userId,
'channel_ids' => $channelIds,
]);
} else {
$config->channel_ids = $channelIds;
$config->save();
}
return $this->success('操作成功');
}
}
@@ -0,0 +1,39 @@
<?php
namespace app\api\controller;
use app\api\logic\VipOrderLogic;
use app\api\validate\VipOrderValidate;
class VipOrderController extends BaseApiController
{
public function levels()
{
return $this->data(VipOrderLogic::levels());
}
public function createOrder()
{
$params = (new VipOrderValidate())->post()->goCheck('createOrder', [
'user_id' => $this->userId,
'terminal' => $this->userInfo['terminal'],
]);
$result = VipOrderLogic::createOrder($params);
if (false === $result) {
return $this->fail(VipOrderLogic::getError());
}
return $this->data($result);
}
public function detail()
{
$params = (new VipOrderValidate())->goCheck('detail', [
'user_id' => $this->userId,
]);
$result = VipOrderLogic::detail($params);
if (false === $result) {
return $this->fail(VipOrderLogic::getError());
}
return $this->data($result);
}
}
@@ -0,0 +1,46 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\api\logic\WechatLogic;
use app\api\validate\WechatValidate;
/**
* 微信
* Class WechatController
* @package app\api\controller
*/
class WechatController extends BaseApiController
{
public array $notNeedLogin = ['jsConfig'];
/**
* @notes 微信JSSDK授权接口
* @return mixed
* @author 段誉
* @date 2023/3/1 11:39
*/
public function jsConfig()
{
$params = (new WechatValidate())->goCheck('jsConfig');
$result = WechatLogic::jsConfig($params);
if ($result === false) {
return $this->fail(WechatLogic::getError(), [], 0, 0);
}
return $this->data($result);
}
}