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
+19
View File
@@ -0,0 +1,19 @@
<?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
// +----------------------------------------------------------------------
return [
'middleware' => [
app\api\http\middleware\InitMiddleware::class, // 初始化
app\api\http\middleware\LoginMiddleware::class, // 登录验证
],
];
@@ -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);
}
}
@@ -0,0 +1,56 @@
<?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
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace app\api\http\middleware;
use app\common\exception\ControllerExtendException;
use app\api\controller\BaseApiController;
use think\exception\ClassNotFoundException;
use think\exception\HttpException;
class InitMiddleware
{
/**
* @notes 初始化
* @param $request
* @param \Closure $next
* @return mixed
* @throws ControllerExtendException
* @author 段誉
* @date 2022/9/6 18:17
*/
public function handle($request, \Closure $next)
{
//获取控制器
try {
$controller = str_replace('.', '\\', $request->controller());
$controller = '\\app\\api\\controller\\' . $controller . 'Controller';
$controllerClass = invoke($controller);
if (($controllerClass instanceof BaseApiController) === false) {
throw new ControllerExtendException($controller, '404');
}
} catch (ClassNotFoundException $e) {
throw new HttpException(404, 'controller not exists:' . $e->getClass());
}
//创建控制器对象
$request->controllerObject = invoke($controller);
return $next($request);
}
}
@@ -0,0 +1,96 @@
<?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
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\api\http\middleware;
use app\common\cache\UserTokenCache;
use app\common\service\JsonService;
use app\api\service\UserTokenService;
use think\facade\Config;
class LoginMiddleware
{
/**
* @notes 登录验证
* @param $request
* @param \Closure $next
* @return mixed|\think\response\Json
* @author 令狐冲
* @date 2021/7/1 17:33
*/
public function handle($request, \Closure $next)
{
$token = $request->header('token');
if ($token === 'null' || $token === 'undefined' || $token === '') {
$token = null;
}
//判断接口是否免登录
$isNotNeedLogin = $request->controllerObject->isNotNeedLogin();
// GET请求视为读操作,无token时以游客身份放行
$isReadRequest = $request->isGet();
//不直接判断$isNotNeedLogin结果,使不需要登录的接口通过,为了兼容某些接口可以登录或不登录访问
if (empty($token) && !$isNotNeedLogin) {
if ($isReadRequest) {
// GET请求无token,以游客身份放行
$request->userInfo = [];
$request->userId = 0;
return $next($request);
}
//写操作没有token需要登录才能访问, 指定show为0,前端不弹出此报错
return JsonService::fail('请求参数缺token', [], 0, 0);
}
$userInfo = (new UserTokenCache())->getUserInfo($token);
if (empty($userInfo) && !$isNotNeedLogin) {
if ($isReadRequest) {
// GET请求token无效,以游客身份放行
$request->userInfo = [];
$request->userId = 0;
return $next($request);
}
//写操作token过期无效需要登录才能访问
return JsonService::fail('登录超时,请重新登录', [], -1, 0);
}
//token临近过期,自动续期
if ($userInfo) {
//获取临近过期自动续期时长
$beExpireDuration = Config::get('project.user_token.be_expire_duration');
//token续期
if (time() > ($userInfo['expire_time'] - $beExpireDuration)) {
$result = UserTokenService::overtimeToken($token);
//续期失败(数据表被删除导致)
if (empty($result)) {
if ($isReadRequest) {
$request->userInfo = [];
$request->userId = 0;
return $next($request);
}
return JsonService::fail('登录过期', [], -1);
}
}
}
//给request赋值,用于控制器
$request->userInfo = $userInfo;
$request->userId = $userInfo['user_id'] ?? 0;
return $next($request);
}
}
+98
View File
@@ -0,0 +1,98 @@
<?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\lists;
use app\common\enum\user\AccountLogEnum;
use app\common\model\user\UserAccountLog;
/**
* 账户流水列表
* Class AccountLogLists
* @package app\shopapi\lists
*/
class AccountLogLists extends BaseApiDataLists
{
/**
* @notes 搜索条件
* @return array
* @author 段誉
* @date 2023/2/24 14:43
*/
public function queryWhere()
{
// 指定用户
$where[] = ['user_id', '=', $this->userId];
// 用户余额明细
if (isset($this->params['type']) && $this->params['type'] == 'um') {
$where[] = ['change_type', 'in', AccountLogEnum::getUserMoneyChangeType()];
}
// 用户积分明细
if (isset($this->params['type']) && $this->params['type'] == 'up') {
$where[] = ['change_type', 'in', AccountLogEnum::getUserPointsChangeType()];
}
// 变动类型
if (!empty($this->params['action'])) {
$where[] = ['action', '=', $this->params['action']];
}
return $where;
}
/**
* @notes 获取列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2023/2/24 14:43
*/
public function lists(): array
{
$field = 'change_type,change_amount,action,create_time,remark';
$lists = UserAccountLog::field($field)
->where($this->queryWhere())
->order('id', 'desc')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
foreach ($lists as &$item) {
$item['type_desc'] = AccountLogEnum::getChangeTypeDesc($item['change_type']);
$symbol = $item['action'] == AccountLogEnum::DEC ? '-' : '+';
$item['change_amount_desc'] = $symbol . $item['change_amount'];
}
return $lists;
}
/**
* @notes 获取数量
* @return int
* @author 段誉
* @date 2023/2/24 14:44
*/
public function count(): int
{
return UserAccountLog::where($this->queryWhere())->count();
}
}
+39
View File
@@ -0,0 +1,39 @@
<?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\lists;
use app\common\lists\BaseDataLists;
abstract class BaseApiDataLists extends BaseDataLists
{
protected array $userInfo = [];
protected int $userId = 0;
public string $export;
public function __construct()
{
parent::__construct();
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;
}
$this->export = $this->request->get('export', '');
}
}
@@ -0,0 +1,79 @@
<?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\lists\article;
use app\api\lists\BaseApiDataLists;
use app\common\enum\YesNoEnum;
use app\common\model\article\Article;
/**
* 文章收藏列表
* Class ArticleCollectLists
* @package app\api\lists\article
*/
class ArticleCollectLists extends BaseApiDataLists
{
/**
* @notes 获取收藏列表
* @return array
* @author 段誉
* @date 2022/9/20 16:29
*/
public function lists(): array
{
$field = "c.id,c.article_id,a.title,a.image,a.desc,a.is_show,
a.click_virtual, a.click_actual,a.create_time, c.create_time as collect_time";
$lists = (new Article())->alias('a')
->join('article_collect c', 'c.article_id = a.id')
->field($field)
->where([
'c.user_id' => $this->userId,
'c.status' => YesNoEnum::YES,
'a.is_show' => YesNoEnum::YES,
])
->order(['sort' => 'desc', 'c.id' => 'desc'])
->limit($this->limitOffset, $this->limitLength)
->append(['click'])
->hidden(['click_virtual', 'click_actual'])
->select()->toArray();
foreach ($lists as &$item) {
$item['collect_time'] = date('Y-m-d H:i', $item['collect_time']);
}
return $lists;
}
/**
* @notes 获取收藏数量
* @return int
* @author 段誉
* @date 2022/9/20 16:29
*/
public function count(): int
{
return (new Article())->alias('a')
->join('article_collect c', 'c.article_id = a.id')
->where([
'c.user_id' => $this->userId,
'c.status' => YesNoEnum::YES,
'a.is_show' => YesNoEnum::YES,
])
->count();
}
}
@@ -0,0 +1,155 @@
<?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\lists\article;
use app\api\lists\BaseApiDataLists;
use app\api\logic\ArticleLogic;
use app\common\enum\YesNoEnum;
use app\common\lists\ListsSearchInterface;
use app\common\model\article\Article;
use app\common\model\article\ArticleCollect;
use app\common\service\article\ArticleImageProxyService;
use app\common\model\user\UserChannelConfig;
/**
* 文章列表
* Class ArticleLists
* @package app\api\lists\article
*/
class ArticleLists extends BaseApiDataLists implements ListsSearchInterface
{
/**
* @notes 搜索条件
* @return \string[][]
* @author 段誉
* @date 2022/9/16 18:54
*/
public function setSearch(): array
{
return [
'=' => ['cid']
];
}
/**
* @notes 自定查询条件
* @return array
* @author 段誉
* @date 2022/10/25 16:53
*/
public function queryWhere()
{
$where[] = ['is_show', '=', 1];
if (!empty($this->params['keyword'])) {
$where[] = ['title', 'like', '%' . $this->params['keyword'] . '%'];
}
if (isset($this->params['is_recommend']) && $this->params['is_recommend'] !== '') {
$where[] = ['is_recommend', '=', $this->params['is_recommend']];
}
if (!empty($this->params['is_mychannel'])) {
if ($this->userId) {
$config = UserChannelConfig::where('user_id', $this->userId)->findOrEmpty();
$channelIds = [];
if (!$config->isEmpty()) {
$raw = $config['channel_ids'];
if (is_string($raw)) {
$channelIds = json_decode($raw, true) ?: [];
} elseif (is_array($raw)) {
$channelIds = $raw;
}
}
if (!empty($channelIds)) {
$where[] = ['cid', 'in', $channelIds];
} else {
$where[] = ['id', '=', 0];
}
} else {
$where[] = ['id', '=', 0];
}
}
return $where;
}
/**
* @notes 获取文章列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/16 18:55
*/
public function lists(): array
{
$orderRaw = 'published_at desc, id desc';
$sortType = $this->params['sort'] ?? 'default';
$cid = (int) ($this->params['cid'] ?? 0);
// 最新排序
if ($sortType == 'new') {
$orderRaw = 'published_at desc, id desc';
}
// 最热排序
if ($sortType == 'hot') {
$orderRaw = 'click_actual + click_virtual desc, id desc';
}
$field = 'id,cid,title,desc,content,author,image,is_video,duration,video_url,click_virtual,click_actual,create_time,ext';
$result = Article::field($field)
->withCount(['comments' => 'comment_count'])
->where($this->queryWhere())
->where($this->searchWhere)
->orderRaw($orderRaw)
->append(['click'])
->hidden(['click_virtual', 'click_actual'])
->limit($this->limitOffset, $this->limitLength)
->select()->toArray();
$articleIds = array_column($result, 'id');
$collectIds = ArticleCollect::where(['user_id' => $this->userId, 'status' => YesNoEnum::YES])
->whereIn('article_id', $articleIds)
->column('article_id');
foreach ($result as &$item) {
$item['collect'] = in_array($item['id'], $collectIds);
if (!empty($item['content'])) {
$item['content'] = mb_substr(strip_tags($item['content']), 0, 100);
}
$item['translated_content'] = $cid === 22 ? ArticleLogic::getCachedTranslatedContent($item) : '';
$item = ArticleImageProxyService::rewriteArticlePayload($item);
unset($item['ext']);
}
return $result;
}
/**
* @notes 获取文章数量
* @return int
* @author 段誉
* @date 2022/9/16 18:55
*/
public function count(): int
{
return Article::where($this->searchWhere)
->where($this->queryWhere())
->count();
}
}
@@ -0,0 +1,253 @@
<?php
namespace app\api\lists\community;
use app\api\lists\BaseApiDataLists;
use app\common\model\community\CommunityPost;
use app\common\model\community\CommunityFollow;
use app\common\model\community\CommunityTag;
use app\common\model\user\User;
use app\common\enum\user\AccountLogEnum;
use app\common\service\ai\AiService;
use app\common\service\VipService;
use think\facade\Cache;
use think\facade\Db;
use app\common\model\community\CommunityLike;
class CommunityPostLists extends BaseApiDataLists
{
private const LIUHE_TAGS = ['旧澳六合', '新澳六合'];
private function buildQuery()
{
$query = CommunityPost::where($this->queryWhere());
$trumpTagId = (int) CommunityTag::where('name', '特朗普')->value('id');
if ($trumpTagId > 0) {
$query->whereRaw(
"NOT (id IN (SELECT post_id FROM la_community_post_tag WHERE tag_id = {$trumpTagId})" .
" AND TRIM(IFNULL(content, '')) = ''" .
" AND IFNULL(images, '[]') <> '[]')"
);
}
return $query;
}
private function queryWhere(): array
{
$where = [['status', '=', 1]];
if (!empty($this->params['post_type'])) {
$where[] = ['post_type', '=', $this->params['post_type']];
}
if (!empty($this->params['user_id'])) {
$where[] = ['user_id', '=', $this->params['user_id']];
}
if (!empty($this->params['follow']) && $this->userId) {
$followUserIds = CommunityFollow::where('user_id', $this->userId)
->column('follow_user_id');
if ($followUserIds) {
$where[] = ['user_id', 'in', $followUserIds];
} else {
$where[] = ['id', '=', 0];
}
}
if (!empty($this->params['tag_id'])) {
$postIds = Db::name('community_post_tag')
->where('tag_id', $this->params['tag_id'])
->column('post_id');
if ($postIds) {
$where[] = ['id', 'in', $postIds];
} else {
$where[] = ['id', '=', 0];
}
}
return $where;
}
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 getLiuheAnalysisContent(array $ext): string
{
if (($ext['lottery_analysis_source'] ?? '') !== 'image_only') {
return '';
}
return (string) ($ext['lottery_analysis_content'] ?? '');
}
public function lists(): array
{
$list = $this->buildQuery()
->field('id,origin_id,user_id,content,images,ext,post_type,match_id,is_paid,price_points,paid_count,view_count,like_count,comment_count,share_count,is_top,is_hot,is_recommend,create_time')
->orderRaw('is_top DESC, is_hot DESC, create_time DESC')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
// 批量预加载:用户、标签、翻译状态
$postIds = array_column($list, 'id');
$userIds = array_unique(array_column($list, 'user_id'));
// 批量查询用户 → id 为 key
$userMap = [];
if ($userIds) {
$users = User::field('id,sn,nickname,avatar,sex')->whereIn('id', $userIds)->select()->toArray();
foreach ($users as $u) {
$userMap[$u['id']] = $u;
}
}
// 批量查询帖子标签 → post_id => [tag_name, ...]
$tagMap = [];
if ($postIds) {
$postTags = Db::name('community_post_tag')->whereIn('post_id', $postIds)->select()->toArray();
$tagIdsByPost = [];
foreach ($postTags as $pt) {
$tagIdsByPost[$pt['post_id']][] = $pt['tag_id'];
}
$allTagIds = array_unique(array_column($postTags, 'tag_id'));
$tagNames = $allTagIds ? CommunityTag::whereIn('id', $allTagIds)->column('name', 'id') : [];
foreach ($tagIdsByPost as $pid => $tids) {
$tagMap[$pid] = array_values(array_intersect_key($tagNames, array_flip($tids)));
}
}
// 已付费翻译的帖子ID集合(带缓存)
$translatedPostIds = [];
if ($this->userId) {
$translatedPostIds = self::getUserTranslatedPostIds($this->userId);
}
// 批量查询已购买的付费帖子ID
$purchasedPostIds = [];
if ($this->userId && $postIds) {
$purchasedPostIds = Db::name('user_account_log')
->where('user_id', $this->userId)
->where('change_type', AccountLogEnum::UP_DEC_PURCHASE_POST)
->whereIn('relation_id', $postIds)
->column('relation_id');
}
// VIP权益:解锁帖子免费、翻译免费
$vipUnlockFree = $this->userId ? VipService::hasUnlockFree($this->userId) : false;
$vipTranslateFree = $this->userId ? VipService::hasTranslateFree($this->userId) : false;
foreach ($list as &$item) {
$ext = $item['ext'] ?? '';
$ext = $ext ? (is_string($ext) ? json_decode($ext, true) : $ext) : [];
// 判断是否已解锁(自己发的 或 已购买 或 VIP解锁免费)
$item['is_unlocked'] = 0;
if ($item['is_paid']) {
if ($item['user_id'] == $this->userId || in_array($item['id'], $purchasedPostIds) || $vipUnlockFree) {
$item['is_unlocked'] = 1;
}
}
// 付费帖子:未解锁 → 截断内容并隐藏全文
if ($item['is_paid'] && !$item['is_unlocked']) {
$item['content_short'] = mb_substr($item['content'], 0, 20) . '...';
$item['content'] = $item['content_short'];
} elseif (mb_strlen($item['content']) > 120) {
$item['content_short'] = mb_substr($item['content'], 0, 120) . '...';
} else {
$item['content_short'] = $item['content'];
}
$item['user'] = $userMap[$item['user_id']] ?? null;
$item['tags'] = $tagMap[$item['id']] ?? [];
$item['source_url'] = $ext['url'] ?? '';
$item['is_liked'] = false;
if ($this->userId) {
$item['is_liked'] = CommunityLike::where([
'user_id' => $this->userId,
'target_id' => $item['id'],
'target_type' => 1
])->count() > 0;
}
$item['translated_content'] = '';
$isTrumpPost = in_array('特朗普', $item['tags']);
$isLiuhePost = $this->isLiuhePost($item['tags'], $ext);
if ($isTrumpPost) {
if (empty($ext['translated_content']) && !empty($item['content'])) {
$result = AiService::translateCommunityPost($item['content']);
if (!empty($result['success']) && !empty($result['content'])) {
$ext['translated_content'] = trim($result['content']);
CommunityPost::where('id', $item['id'])->update([
'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE),
'update_time' => time(),
]);
}
}
$item['translated_content'] = $ext['translated_content'] ?? '';
} elseif ((!empty($item['origin_id']) || $isLiuhePost) && (in_array($item['id'], $translatedPostIds) || $vipTranslateFree)) {
$item['translated_content'] = $isLiuhePost
? $this->getLiuheAnalysisContent($ext)
: (string) ($ext['translated_content'] ?? '');
}
unset($item['ext']);
}
return $list;
}
public function count(): int
{
return $this->buildQuery()->count();
}
/**
* 获取用户已付费翻译的帖子ID集合(优先缓存)
*/
public static function getUserTranslatedPostIds(int $userId): array
{
$cacheKey = 'translate_paid:' . $userId;
$postIds = Cache::get($cacheKey);
if ($postIds !== null) {
return $postIds;
}
$remarks = Db::name('user_account_log')
->where('user_id', $userId)
->where('change_type', AccountLogEnum::UP_DEC_TRANSLATE_POST)
->column('remark');
$postIds = [];
foreach ($remarks as $remark) {
if (preg_match('/(?:翻译|分析)帖子#(\d+)/', $remark, $m)) {
$postIds[] = (int) $m[1];
}
}
Cache::set($cacheKey, $postIds, 3600);
return $postIds;
}
/**
* 翻译扣分后刷新缓存:追加新帖子ID
*/
public static function refreshTranslateCache(int $userId, int $postId): void
{
$cacheKey = 'translate_paid:' . $userId;
$postIds = Cache::get($cacheKey);
if (is_array($postIds)) {
if (!in_array($postId, $postIds)) {
$postIds[] = $postId;
Cache::set($cacheKey, $postIds, 3600);
}
} else {
Cache::delete($cacheKey);
}
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace app\api\lists\match;
use app\api\lists\BaseApiDataLists;
use app\common\model\match\MatchEvent;
use app\common\model\league\League;
class MatchLists extends BaseApiDataLists
{
private function queryWhere(): array
{
$where = [['is_show', '=', 1]];
if (!empty($this->params['sport_type'])) {
$where[] = ['sport_type', '=', $this->params['sport_type']];
} else {
$validSportTypes = League::where('is_show', 1)
->where('type', 'sport')
->where('sport_type', '>', 0)
->column('sport_type');
if (!empty($validSportTypes)) {
$where[] = ['sport_type', 'in', $validSportTypes];
}
}
if (!empty($this->params['league_name'])) {
$where[] = ['league_name', '=', $this->params['league_name']];
} elseif (empty($this->params['sport_type'])) {
$validLeagues = League::where('is_show', 1)
->where('type', 'league')
->column('label');
if (!empty($validLeagues)) {
$where[] = ['league_name', 'in', $validLeagues];
}
}
if (isset($this->params['status']) && $this->params['status'] !== '') {
$where[] = ['status', '=', $this->params['status']];
}
return $where;
}
public function lists(): array
{
return MatchEvent::where($this->queryWhere())
->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')
->orderRaw('FIELD(status, 1, 0, 3, 2), CASE WHEN status = 2 THEN -match_time ELSE match_time END ASC')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
}
public function count(): int
{
return MatchEvent::where($this->queryWhere())->count();
}
}
@@ -0,0 +1,72 @@
<?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\lists\recharge;
use app\api\lists\BaseApiDataLists;
use app\common\enum\PayEnum;
use app\common\model\recharge\RechargeOrder;
/**
* 充值记录列表
* Class RechargeLists
* @package app\api\lists\recharge
*/
class RechargeLists extends BaseApiDataLists
{
/**
* @notes 获取列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2023/2/23 18:43
*/
public function lists(): array
{
$lists = RechargeOrder::field('order_amount,create_time')
->where([
'user_id' => $this->userId,
'pay_status' => PayEnum::ISPAID
])
->order('id', 'desc')
->select()
->toArray();
foreach($lists as &$item) {
$item['tips'] = '充值' . format_amount($item['order_amount']) . '元';
}
return $lists;
}
/**
* @notes 获取数量
* @return int
* @author 段誉
* @date 2023/2/23 18:43
*/
public function count(): int
{
return RechargeOrder::where([
'user_id' => $this->userId,
'pay_status' => PayEnum::ISPAID
])
->count();
}
}
@@ -0,0 +1,196 @@
<?php
namespace app\api\logic;
use app\common\logic\BaseLogic;
use app\common\model\article\Article;
use app\common\model\article\ArticleComment;
use app\common\model\article\ArticleVote;
use app\common\model\user\User;
use think\facade\Db;
/**
* 文章互动逻辑(评论/点赞/踩)
* Class ArticleInteractLogic
* @package app\api\logic
*/
class ArticleInteractLogic extends BaseLogic
{
/**
* @notes 评论列表
*/
public static function commentList($articleId, $pageNo = 1, $pageSize = 15)
{
$where = [
'article_id' => $articleId,
'parent_id' => 0,
'is_show' => 1,
];
$count = ArticleComment::where($where)->count();
$list = ArticleComment::where($where)
->order('id', 'desc')
->page($pageNo, $pageSize)
->select()
->toArray();
$userIds = array_column($list, 'user_id');
// 取子评论的user_id
$commentIds = array_column($list, 'id');
$replies = [];
if (!empty($commentIds)) {
$repliesRaw = ArticleComment::where('parent_id', 'in', $commentIds)
->where('is_show', 1)
->order('id', 'asc')
->select()
->toArray();
$replyUserIds = array_column($repliesRaw, 'user_id');
$replyTargetUserIds = array_column($repliesRaw, 'reply_user_id');
$userIds = array_merge($userIds, $replyUserIds, $replyTargetUserIds);
foreach ($repliesRaw as $r) {
$replies[$r['parent_id']][] = $r;
}
}
// 批量获取用户信息
$userIds = array_unique(array_filter($userIds));
$users = [];
if (!empty($userIds)) {
$usersRaw = User::whereIn('id', $userIds)
->field('id,nickname,avatar')
->select()
->toArray();
foreach ($usersRaw as $u) {
$users[$u['id']] = $u;
}
}
$defaultUser = ['nickname' => '匿名用户', 'avatar' => ''];
foreach ($list as &$item) {
$u = $users[$item['user_id']] ?? $defaultUser;
$item['nickname'] = $u['nickname'];
$item['avatar'] = $u['avatar'];
$item['reply_list'] = [];
if (isset($replies[$item['id']])) {
foreach ($replies[$item['id']] as $reply) {
$ru = $users[$reply['user_id']] ?? $defaultUser;
$reply['nickname'] = $ru['nickname'];
$reply['avatar'] = $ru['avatar'];
$reply['reply_nickname'] = '';
if ($reply['reply_user_id'] > 0) {
$tu = $users[$reply['reply_user_id']] ?? $defaultUser;
$reply['reply_nickname'] = $tu['nickname'];
}
$item['reply_list'][] = $reply;
}
}
}
unset($item);
return [
'count' => $count,
'lists' => $list,
'page_no' => $pageNo,
'page_size' => $pageSize,
];
}
/**
* @notes 发布评论
*/
public static function addComment($userId, $params)
{
Db::startTrans();
try {
$comment = ArticleComment::create([
'article_id' => $params['article_id'],
'user_id' => $userId,
'parent_id' => $params['parent_id'] ?? 0,
'reply_user_id' => $params['reply_user_id'] ?? 0,
'content' => $params['content'],
]);
Article::where('id', $params['article_id'])
->inc('comment_count')
->update();
Db::commit();
return $comment->id;
} catch (\Exception $e) {
Db::rollback();
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 点赞/踩
*/
public static function vote($userId, $articleId, $voteType)
{
Db::startTrans();
try {
$existing = ArticleVote::where([
'article_id' => $articleId,
'user_id' => $userId,
])->findOrEmpty();
if (!$existing->isEmpty()) {
if ($existing->vote_type == $voteType) {
// 取消
$field = $voteType == ArticleVote::VOTE_LIKE ? 'like_count' : 'dislike_count';
$existing->delete();
Article::where('id', $articleId)->where($field, '>', 0)->dec($field)->update();
Db::commit();
return ['action' => 'cancel', 'vote_type' => $voteType];
} else {
// 切换: 旧的减,新的加
$oldField = $existing->vote_type == ArticleVote::VOTE_LIKE ? 'like_count' : 'dislike_count';
$newField = $voteType == ArticleVote::VOTE_LIKE ? 'like_count' : 'dislike_count';
$existing->vote_type = $voteType;
$existing->save();
Article::where('id', $articleId)->where($oldField, '>', 0)->dec($oldField)->update();
Article::where('id', $articleId)->inc($newField)->update();
Db::commit();
return ['action' => 'switch', 'vote_type' => $voteType];
}
} else {
// 新增
ArticleVote::create([
'article_id' => $articleId,
'user_id' => $userId,
'vote_type' => $voteType,
]);
$field = $voteType == ArticleVote::VOTE_LIKE ? 'like_count' : 'dislike_count';
Article::where('id', $articleId)->inc($field)->update();
Db::commit();
return ['action' => 'add', 'vote_type' => $voteType];
}
} catch (\Exception $e) {
Db::rollback();
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 获取用户对文章的投票状态
*/
public static function getVoteStatus($userId, $articleId)
{
if (empty($userId)) {
return 0;
}
$vote = ArticleVote::where([
'article_id' => $articleId,
'user_id' => $userId,
])->findOrEmpty();
return $vote->isEmpty() ? 0 : $vote->vote_type;
}
}
+355
View File
@@ -0,0 +1,355 @@
<?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\logic;
use app\common\enum\user\AccountLogEnum;
use app\common\enum\YesNoEnum;
use app\common\logic\AccountLogLogic;
use app\common\logic\BaseLogic;
use app\common\model\article\Article;
use app\common\model\article\ArticleCate;
use app\common\model\article\ArticleCollect;
use app\api\logic\ArticleInteractLogic;
use app\common\service\article\ArticleImageProxyService;
use app\common\service\ai\AiService;
use app\common\service\ai\KbSyncService;
use app\common\model\user\User;
use think\facade\Db;
/**
* 文章逻辑
* Class ArticleLogic
* @package app\api\logic
*/
class ArticleLogic extends BaseLogic
{
/**
* @notes 文章详情
* @param $articleId
* @param $userId
* @return array
* @author 段誉
* @date 2022/9/20 17:09
*/
public static function detail($articleId, $userId)
{
$article = Article::getArticleDetailArr($articleId);
if (empty($article)) {
return [];
}
$ext = self::decodeExt($article['ext'] ?? []);
$translatedContent = self::getTranslatedContentForUser($articleId, $userId, $ext);
if ($translatedContent === '' && (int) ($article['cid'] ?? 0) === 22) {
$translatedContent = self::autoTranslateArticle($article);
}
if ($translatedContent !== '') {
$article['translated_content'] = $translatedContent;
} elseif (!empty($ext['translated_content'])) {
$article['translated_content'] = (string) $ext['translated_content'];
}
$article['collect'] = ArticleCollect::isCollectArticle($userId, $articleId);
$article['vote_status'] = ArticleInteractLogic::getVoteStatus($userId, $articleId);
$article = ArticleImageProxyService::rewriteArticlePayload($article);
return $article;
}
/**
* @notes 加入收藏
* @param $userId
* @param $articleId
* @author 段誉
* @date 2022/9/20 16:52
*/
public static function addCollect($articleId, $userId)
{
$where = ['user_id' => $userId, 'article_id' => $articleId];
$collect = ArticleCollect::where($where)->findOrEmpty();
if ($collect->isEmpty()) {
ArticleCollect::create([
'user_id' => $userId,
'article_id' => $articleId,
'status' => YesNoEnum::YES
]);
} else {
ArticleCollect::update([
'id' => $collect['id'],
'status' => YesNoEnum::YES
]);
}
}
/**
* @notes 取消收藏
* @param $articleId
* @param $userId
* @author 段誉
* @date 2022/9/20 16:59
*/
public static function cancelCollect($articleId, $userId)
{
ArticleCollect::update(['status' => YesNoEnum::NO], [
'user_id' => $userId,
'article_id' => $articleId,
'status' => YesNoEnum::YES
]);
}
/**
* @notes 文章分类
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/23 14:11
*/
public static function cate()
{
return ArticleCate::field('id,name')
->where('is_show', '=', 1)
->order(['sort' => 'desc', 'id' => 'desc'])
->select()->toArray();
}
/**
* @notes 文章翻译
* @param int $articleId
* @return array
*/
public static function translate(int $articleId, int $userId = 0, int $confirm = 0): array
{
$article = Article::where(['id' => $articleId, 'is_show' => YesNoEnum::YES])->findOrEmpty();
if ($article->isEmpty()) {
return ['success' => false, 'error' => '文章不存在'];
}
$articleData = $article->toArray();
$ext = self::decodeExt($articleData['ext'] ?? []);
$translatedContent = self::getTranslatedContentForUser($articleId, $userId, $ext);
$translatePoints = 5;
$remark = 'AI翻译资讯#' . $articleId;
$hasPaid = false;
if ($userId) {
$hasPaid = Db::name('user_account_log')
->where('user_id', $userId)
->where('change_type', AccountLogEnum::UP_DEC_TRANSLATE_POST)
->where('remark', $remark)
->count() > 0;
}
if ($hasPaid && $translatedContent !== '') {
return [
'success' => true,
'data' => [
'translated_content' => $translatedContent,
'from_cache' => true,
],
];
}
if ($hasPaid) {
$confirm = 1;
}
if (!$confirm) {
return [
'success' => true,
'data' => [
'needs_payment' => true,
'cost' => $translatePoints,
],
];
}
if (!$userId) {
return ['success' => false, 'error' => '请先登录'];
}
if (!$hasPaid) {
$user = User::findOrEmpty($userId);
if ($user->isEmpty()) {
return ['success' => false, 'error' => '用户不存在'];
}
if (($user->user_points ?? 0) < $translatePoints) {
return ['success' => false, 'error' => '积分不足,当前积分' . ($user->user_points ?? 0) . ',翻译需要' . $translatePoints . '积分'];
}
}
if ($translatedContent !== '') {
if (!$hasPaid) {
User::where('id', $userId)->dec('user_points', $translatePoints)->update();
AccountLogLogic::add(
$userId,
AccountLogEnum::UP_DEC_TRANSLATE_POST,
AccountLogEnum::DEC,
$translatePoints,
'',
$remark
);
}
return [
'success' => true,
'data' => [
'translated_content' => $translatedContent,
'from_cache' => false,
],
];
}
$title = trim((string) ($articleData['title'] ?? ''));
$abstract = trim((string) ($articleData['abstract'] ?? $articleData['desc'] ?? ''));
$content = self::normalizeContent((string) ($articleData['content'] ?? ''));
$result = AiService::translateArticle($title, $abstract, $content);
if (!$result['success']) {
return ['success' => false, 'error' => $result['error'] ?? '翻译失败'];
}
$translatedContent = trim((string) ($result['content'] ?? ''));
if ($translatedContent === '') {
return ['success' => false, 'error' => '翻译结果为空'];
}
$ext['translated_content'] = $translatedContent;
Article::where('id', $articleId)->update([
'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE),
'update_time' => time(),
]);
KbSyncService::enqueue('article', 'article', $articleId, 'upsert', 45);
if (!$hasPaid) {
User::where('id', $userId)->dec('user_points', $translatePoints)->update();
AccountLogLogic::add(
$userId,
AccountLogEnum::UP_DEC_TRANSLATE_POST,
AccountLogEnum::DEC,
$translatePoints,
'',
$remark
);
}
return [
'success' => true,
'data' => [
'translated_content' => $translatedContent,
'from_cache' => false,
],
];
}
/**
* @notes 自动翻译并落库文章译文缓存
* @param array $articleData
* @return string
*/
public static function autoTranslateArticle(array $articleData): string
{
$articleId = (int) ($articleData['id'] ?? 0);
if ($articleId <= 0) {
return '';
}
$ext = self::decodeExt($articleData['ext'] ?? []);
if (!empty($ext['translated_content'])) {
return (string) $ext['translated_content'];
}
$title = trim((string) ($articleData['title'] ?? ''));
$abstract = trim((string) ($articleData['abstract'] ?? $articleData['desc'] ?? ''));
$content = self::normalizeContent((string) ($articleData['content'] ?? ''));
if ($title === '' && $abstract === '' && $content === '') {
return '';
}
$result = AiService::translateArticle($title, $abstract, $content);
if (empty($result['success'])) {
return '';
}
$translatedContent = trim((string) ($result['content'] ?? ''));
if ($translatedContent === '') {
return '';
}
$ext['translated_content'] = $translatedContent;
Article::where('id', $articleId)->update([
'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE),
'update_time' => time(),
]);
KbSyncService::enqueue('article', 'article', $articleId, 'upsert', 45);
return $translatedContent;
}
/**
* @notes 仅读取已缓存的文章翻译内容
* @param array $articleData
* @return string
*/
public static function getCachedTranslatedContent(array $articleData): string
{
$ext = self::decodeExt($articleData['ext'] ?? []);
return !empty($ext['translated_content']) ? (string) $ext['translated_content'] : '';
}
protected static function getTranslatedContentForUser(int $articleId, int $userId, array $ext): string
{
if (empty($ext['translated_content']) || !$userId) {
return '';
}
$hasPaid = Db::name('user_account_log')
->where('user_id', $userId)
->where('change_type', AccountLogEnum::UP_DEC_TRANSLATE_POST)
->where('remark', 'AI翻译资讯#' . $articleId)
->count() > 0;
return $hasPaid ? (string) $ext['translated_content'] : '';
}
protected static function decodeExt($ext): array
{
if (is_array($ext)) {
return $ext;
}
if (is_string($ext) && $ext !== '') {
$data = json_decode($ext, true);
return is_array($data) ? $data : [];
}
return [];
}
protected static function normalizeContent(string $content): string
{
if ($content === '') {
return '';
}
$content = str_ireplace(['<br>', '<br/>', '<br />'], "\n", $content);
$content = str_ireplace(['</p>', '</div>', '</li>', '</h1>', '</h2>', '</h3>'], "\n", $content);
$content = strip_tags($content);
$content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$content = preg_replace('/\r\n|\r/u', "\n", $content);
$content = preg_replace('/\n{3,}/u', "\n\n", $content);
return trim($content);
}
}
+160
View File
@@ -0,0 +1,160 @@
<?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\logic;
use app\common\logic\BaseLogic;
use app\common\model\article\Article;
use app\common\model\decorate\DecoratePage;
use app\common\model\decorate\DecorateTabbar;
use app\common\service\ConfigService;
use app\common\service\FileService;
/**
* index
* Class IndexLogic
* @package app\api\logic
*/
class IndexLogic extends BaseLogic
{
/**
* @notes 首页数据
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/21 19:15
*/
public static function getIndexData()
{
// 装修配置
$decoratePage = DecoratePage::findOrEmpty(1);
// 首页文章
$field = [
'id', 'title', 'desc', 'abstract', 'image',
'author', 'click_actual', 'click_virtual', 'create_time'
];
$article = Article::field($field)
->where(['is_show' => 1])
->order(['id' => 'desc'])
->limit(20)->append(['click'])
->hidden(['click_actual', 'click_virtual'])
->select()->toArray();
return [
'page' => $decoratePage,
'article' => $article
];
}
/**
* @notes 获取政策协议
* @param string $type
* @return array
* @author 段誉
* @date 2022/9/20 20:00
*/
public static function getPolicyByType(string $type)
{
return [
'title' => ConfigService::get('agreement', $type . '_title', ''),
'content' => ConfigService::get('agreement', $type . '_content', ''),
];
}
/**
* @notes 装修信息
* @param $id
* @return array
* @author 段誉
* @date 2022/9/21 18:37
*/
public static function getDecorate($id)
{
return DecoratePage::field(['type', 'name', 'data', 'meta'])
->findOrEmpty($id)->toArray();
}
/**
* @notes 获取配置
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/21 19:38
*/
public static function getConfigData()
{
// 底部导航
$tabbar = DecorateTabbar::getTabbarLists();
// 导航颜色
$style = ConfigService::get('tabbar', 'style', config('project.decorate.tabbar_style'));
// 登录配置
$loginConfig = [
// 登录方式
'login_way' => ConfigService::get('login', 'login_way', config('project.login.login_way')),
// 注册强制绑定手机
'coerce_mobile' => ConfigService::get('login', 'coerce_mobile', config('project.login.coerce_mobile')),
// 政策协议
'login_agreement' => ConfigService::get('login', 'login_agreement', config('project.login.login_agreement')),
// 第三方登录 开关
'third_auth' => ConfigService::get('login', 'third_auth', config('project.login.third_auth')),
// 微信授权登录
'wechat_auth' => ConfigService::get('login', 'wechat_auth', config('project.login.wechat_auth')),
// qq授权登录
'qq_auth' => ConfigService::get('login', 'qq_auth', config('project.login.qq_auth')),
];
// 网址信息
$website = [
'h5_favicon' => FileService::getFileUrl(ConfigService::get('website', 'h5_favicon')),
'shop_name' => ConfigService::get('website', 'shop_name'),
'shop_logo' => FileService::getFileUrl(ConfigService::get('website', 'shop_logo')),
];
// H5配置
$webPage = [
// 渠道状态 0-关闭 1-开启
'status' => ConfigService::get('web_page', 'status', 1),
// 关闭后渠道后访问页面 0-空页面 1-自定义链接
'page_status' => ConfigService::get('web_page', 'page_status', 0),
// 自定义链接
'page_url' => ConfigService::get('web_page', 'page_url', ''),
'url' => request()->domain() . '/mobile'
];
// 备案信息
$copyright = ConfigService::get('copyright', 'config', []);
return [
'domain' => FileService::getFileUrl(),
'style' => $style,
'tabbar' => $tabbar,
'login' => $loginConfig,
'website' => $website,
'webPage' => $webPage,
'version'=> config('project.version'),
'copyright' => $copyright,
];
}
}
+511
View File
@@ -0,0 +1,511 @@
<?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\logic;
use app\common\cache\WebScanLoginCache;
use app\common\logic\BaseLogic;
use app\api\service\{UserTokenService, WechatUserService};
use app\common\enum\{LoginEnum, user\UserTerminalEnum, YesNoEnum};
use app\common\service\{
ConfigService,
FileService,
wechat\WeChatConfigService,
wechat\WeChatMnpService,
wechat\WeChatOaService,
wechat\WeChatRequestService
};
use app\common\model\user\{User, UserAuth};
use think\facade\{Db, Config};
/**
* 登录逻辑
* Class LoginLogic
* @package app\api\logic
*/
class LoginLogic extends BaseLogic
{
/**
* @notes 账号密码注册
* @param array $params
* @return bool
* @author 段誉
* @date 2022/9/7 15:37
*/
public static function register(array $params)
{
try {
$userSn = User::createUserSn();
$passwordSalt = Config::get('project.unique_identification');
$password = create_password($params['password'], $passwordSalt);
$avatar = ConfigService::get('default_image', 'user_avatar');
$inviteCode = User::createInviteCode();
$inviterId = 0;
if (!empty($params['inviter_code'])) {
$inviter = User::where('invite_code', $params['inviter_code'])->find();
if ($inviter) {
$inviterId = $inviter->id;
}
}
User::create([
'sn' => $userSn,
'avatar' => $avatar,
'nickname' => '用户' . $userSn,
'account' => $params['account'],
'password' => $password,
'channel' => $params['channel'],
'invite_code' => $inviteCode,
'inviter_id' => $inviterId,
]);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 账号/手机号登录,手机号验证码
* @param $params
* @return array|false
* @author 段誉
* @date 2022/9/6 19:26
*/
public static function login($params)
{
try {
// 账号/手机号 密码登录
$where = ['account|mobile' => $params['account']];
if ($params['scene'] == LoginEnum::MOBILE_CAPTCHA) {
//手机验证码登录
$where = ['mobile' => $params['account']];
}
// 验证码登录:只要上传表里存在该手机号记录即可视为通过
$verifyScene = '';
$smsMatched = false;
if ($params['scene'] == LoginEnum::MOBILE_CAPTCHA) {
$verifyInfo = \think\facade\Cache::get('sms_verify_info:' . $params['account']);
if ($verifyInfo && is_array($verifyInfo)) {
$verifyScene = $verifyInfo['scene'] ?? 'login';
}
$userMobile = $params['account'];
$record = \app\common\model\SmsLog::where(function ($query) use ($userMobile) {
$query->where('phone', $userMobile)
->whereOr('phone', '+86' . $userMobile)
->whereOr('phone', '86' . $userMobile);
})
->order('sms_time', 'desc')
->findOrEmpty();
$smsMatched = !$record->isEmpty();
if ($smsMatched && (int)$record->status === 0) {
$record->status = 1;
$record->save();
}
}
$user = User::where($where)->findOrEmpty();
if ($user->isEmpty()) {
// 验证码登录场景:用户不存在则自动注册,无论验证码是否匹配都允许注册
if ($params['scene'] == LoginEnum::MOBILE_CAPTCHA) {
// is_bind_mobile: sms_upload匹配=1,缓存码匹配但sms_upload不匹配=0,都不匹配=0
$isPhoneReal = $smsMatched ? 1 : 0;
$userSn = User::createUserSn();
$avatar = ConfigService::get('default_image', 'user_avatar');
$inviteCode = User::createInviteCode();
$inviterId = 0;
if (!empty($params['inviter_code'])) {
$inviter = User::where('invite_code', $params['inviter_code'])->find();
if ($inviter) {
$inviterId = $inviter->id;
}
}
$user = User::create([
'sn' => $userSn,
'avatar' => $avatar,
'nickname' => '用户' . $userSn,
'account' => $params['account'],
'mobile' => $params['account'],
'channel' => $params['terminal'],
'invite_code' => $inviteCode,
'inviter_id' => $inviterId,
'is_bind_mobile' => $isPhoneReal,
]);
} else {
throw new \Exception('用户不存在');
}
} else {
// 已有账号:验证码登录必须通过 sms_upload 验证
if ($params['scene'] == LoginEnum::MOBILE_CAPTCHA && !$smsMatched) {
throw new \Exception('验证码错误,请使用密码登录');
}
if ($params['scene'] == LoginEnum::MOBILE_CAPTCHA && $smsMatched) {
$user->is_bind_mobile = 1;
}
}
// 验证码验证成功才清除缓存
if ($params['scene'] == LoginEnum::MOBILE_CAPTCHA && $smsMatched) {
\think\facade\Cache::delete('sms_verify_code:' . $params['account']);
\think\facade\Cache::delete('sms_verify_info:' . $params['account']);
}
//更新登录信息
$user->login_time = time();
$user->login_ip = request()->ip();
$user->save();
//设置token
$userInfo = UserTokenService::setToken($user->id, $params['terminal']);
//返回登录信息
$avatar = $user->avatar ?: Config::get('project.default_image.user_avatar');
$avatar = FileService::getFileUrl($avatar);
return [
'nickname' => $userInfo['nickname'],
'sn' => $userInfo['sn'],
'mobile' => $userInfo['mobile'],
'avatar' => $avatar,
'token' => $userInfo['token'],
'is_real_phone' => $user->is_bind_mobile ?? 0,
'verify_scene' => $verifyScene,
];
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 退出登录
* @param $userInfo
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/16 17:56
*/
public static function logout($userInfo)
{
//token不存在,不注销
if (!isset($userInfo['token'])) {
return false;
}
//设置token过期
return UserTokenService::expireToken($userInfo['token']);
}
/**
* @notes 获取微信请求code的链接
* @param string $url
* @return string
* @author 段誉
* @date 2022/9/20 19:47
*/
public static function codeUrl(string $url)
{
return (new WeChatOaService())->getCodeUrl($url);
}
/**
* @notes 公众号登录
* @param array $params
* @return array|false
* @throws \GuzzleHttp\Exception\GuzzleException
* @author 段誉
* @date 2022/9/20 19:47
*/
public static function oaLogin(array $params)
{
Db::startTrans();
try {
//通过code获取微信 openid
$response = (new WeChatOaService())->getOaResByCode($params['code']);
$userServer = new WechatUserService($response, UserTerminalEnum::WECHAT_OA);
$userInfo = $userServer->getResopnseByUserInfo()->authUserLogin()->getUserInfo();
// 更新登录信息
self::updateLoginInfo($userInfo['id']);
Db::commit();
return $userInfo;
} catch (\Exception $e) {
Db::rollback();
self::$error = $e->getMessage();
return false;
}
}
/**
* @notes 小程序-静默登录
* @param array $params
* @return array|false
* @author 段誉
* @date 2022/9/20 19:47
*/
public static function silentLogin(array $params)
{
try {
//通过code获取微信 openid
$response = (new WeChatMnpService())->getMnpResByCode($params['code']);
$userServer = new WechatUserService($response, UserTerminalEnum::WECHAT_MMP);
$userInfo = $userServer->getResopnseByUserInfo('silent')->getUserInfo();
if (!empty($userInfo)) {
// 更新登录信息
self::updateLoginInfo($userInfo['id']);
}
return $userInfo;
} catch (\Exception $e) {
self::$error = $e->getMessage();
return false;
}
}
/**
* @notes 小程序-授权登录
* @param array $params
* @return array|false
* @author 段誉
* @date 2022/9/20 19:47
*/
public static function mnpLogin(array $params)
{
Db::startTrans();
try {
//通过code获取微信 openid
$response = (new WeChatMnpService())->getMnpResByCode($params['code']);
$userServer = new WechatUserService($response, UserTerminalEnum::WECHAT_MMP);
$userInfo = $userServer->getResopnseByUserInfo()->authUserLogin()->getUserInfo();
// 更新登录信息
self::updateLoginInfo($userInfo['id']);
Db::commit();
return $userInfo;
} catch (\Exception $e) {
Db::rollback();
self::$error = $e->getMessage();
return false;
}
}
/**
* @notes 更新登录信息
* @param $userId
* @throws \Exception
* @author 段誉
* @date 2022/9/20 19:46
*/
public static function updateLoginInfo($userId)
{
$user = User::findOrEmpty($userId);
if ($user->isEmpty()) {
throw new \Exception('用户不存在');
}
$time = time();
$user->login_time = $time;
$user->login_ip = request()->ip();
$user->update_time = $time;
$user->save();
}
/**
* @notes 小程序端绑定微信
* @param array $params
* @return bool
* @author 段誉
* @date 2022/9/20 19:46
*/
public static function mnpAuthLogin(array $params)
{
try {
//通过code获取微信openid
$response = (new WeChatMnpService())->getMnpResByCode($params['code']);
$response['user_id'] = $params['user_id'];
$response['terminal'] = UserTerminalEnum::WECHAT_MMP;
return self::createAuth($response);
} catch (\Exception $e) {
self::$error = $e->getMessage();
return false;
}
}
/**
* @notes 公众号端绑定微信
* @param array $params
* @return bool
* @throws \GuzzleHttp\Exception\GuzzleException
* @author 段誉
* @date 2022/9/16 10:43
*/
public static function oaAuthLogin(array $params)
{
try {
//通过code获取微信openid
$response = (new WeChatOaService())->getOaResByCode($params['code']);
$response['user_id'] = $params['user_id'];
$response['terminal'] = UserTerminalEnum::WECHAT_OA;
return self::createAuth($response);
} catch (\Exception $e) {
self::$error = $e->getMessage();
return false;
}
}
/**
* @notes 生成授权记录
* @param $response
* @return bool
* @throws \Exception
* @author 段誉
* @date 2022/9/16 10:43
*/
public static function createAuth($response)
{
//先检查openid是否有记录
$isAuth = UserAuth::where('openid', '=', $response['openid'])->findOrEmpty();
if (!$isAuth->isEmpty()) {
throw new \Exception('该微信已被绑定');
}
if (isset($response['unionid']) && !empty($response['unionid'])) {
//在用unionid找记录,防止生成两个账号,同个unionid的问题
$userAuth = UserAuth::where(['unionid' => $response['unionid']])
->findOrEmpty();
if (!$userAuth->isEmpty() && $userAuth->user_id != $response['user_id']) {
throw new \Exception('该微信已被绑定');
}
}
//如果没有授权,直接生成一条微信授权记录
UserAuth::create([
'user_id' => $response['user_id'],
'openid' => $response['openid'],
'unionid' => $response['unionid'] ?? '',
'terminal' => $response['terminal'],
]);
return true;
}
/**
* @notes 获取扫码登录地址
* @return array|false
* @author 段誉
* @date 2022/10/20 18:23
*/
public static function getScanCode($redirectUri)
{
try {
$config = WeChatConfigService::getOpConfig();
$appId = $config['app_id'];
$redirectUri = UrlEncode($redirectUri);
// 设置有效时间标记状态, 超时扫码不可登录
$state = MD5(time() . rand(10000, 99999));
(new WebScanLoginCache())->setScanLoginState($state);
// 扫码地址
$url = WeChatRequestService::getScanCodeUrl($appId, $redirectUri, $state);
return ['url' => $url];
} catch (\Exception $e) {
self::$error = $e->getMessage();
return false;
}
}
/**
* @notes 网站扫码登录
* @param $params
* @return array|false
* @author 段誉
* @date 2022/10/21 10:28
*/
public static function scanLogin($params)
{
Db::startTrans();
try {
// 通过code 获取 access_token,openid,unionid等信息
$userAuth = WeChatRequestService::getUserAuthByCode($params['code']);
if (empty($userAuth['openid']) || empty($userAuth['access_token'])) {
throw new \Exception('获取用户授权信息失败');
}
// 获取微信用户信息
$response = WeChatRequestService::getUserInfoByAuth($userAuth['access_token'], $userAuth['openid']);
// 生成用户或更新用户信息
$userServer = new WechatUserService($response, UserTerminalEnum::PC);
$userInfo = $userServer->getResopnseByUserInfo()->authUserLogin()->getUserInfo();
// 更新登录信息
self::updateLoginInfo($userInfo['id']);
Db::commit();
return $userInfo;
} catch (\Exception $e) {
Db::rollback();
self::$error = $e->getMessage();
return false;
}
}
/**
* @notes 更新用户信息
* @param $params
* @param $userId
* @return User
* @author 段誉
* @date 2023/2/22 11:19
*/
public static function updateUser($params, $userId)
{
return User::where(['id' => $userId])->update([
'nickname' => $params['nickname'],
'avatar' => FileService::setFileUrl($params['avatar']),
'is_new_user' => YesNoEnum::NO
]);
}
}
+277
View File
@@ -0,0 +1,277 @@
<?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\logic;
use app\common\enum\YesNoEnum;
use app\common\logic\BaseLogic;
use app\common\model\article\Article;
use app\common\model\article\ArticleCate;
use app\common\model\article\ArticleCollect;
use app\common\model\article\ArticleComment;
use app\common\model\decorate\DecoratePage;
use app\common\service\ConfigService;
use app\common\service\FileService;
/**
* index
* Class IndexLogic
* @package app\api\logic
*/
class PcLogic extends BaseLogic
{
/**
* @notes 首页数据
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/21 19:15
*/
public static function getIndexData()
{
// 装修配置
$decoratePage = DecoratePage::findOrEmpty(4);
// 最新资讯
$newArticle = self::getLimitArticle('new', 7);
// 全部资讯
$allArticle = self::getLimitArticle('all', 5);
// 热门资讯
$hotArticle = self::getLimitArticle('hot', 8);
return [
'page' => $decoratePage,
'all' => $allArticle,
'new' => $newArticle,
'hot' => $hotArticle
];
}
/**
* @notes 获取文章
* @param string $sortType
* @param int $limit
* @return mixed
* @author 段誉
* @date 2022/10/19 9:53
*/
public static function getLimitArticle(string $sortType, int $limit = 0, int $cate = 0, int $excludeId = 0)
{
// 查询字段
$field = [
'id',
'cid',
'title',
'desc',
'abstract',
'image',
'author',
'click_actual',
'click_virtual',
'create_time'
];
// 排序条件
$orderRaw = 'published_at desc, id desc';
if ($sortType == 'new') {
$orderRaw = 'published_at desc, id desc';
}
if ($sortType == 'hot') {
$orderRaw = 'click_actual + click_virtual desc, id desc';
}
// 查询条件
$where[] = ['is_show', '=', YesNoEnum::YES];
if (!empty($cate)) {
$where[] = ['cid', '=', $cate];
}
if (!empty($excludeId)) {
$where[] = ['id', '<>', $excludeId];
}
$article = Article::field($field)
->withCount(['comments' => 'comment_count'])
->where($where)
->append(['click'])
->orderRaw($orderRaw)
->hidden(['click_actual', 'click_virtual']);
if ($limit) {
$article->limit($limit);
}
return $article->select()->toArray();
}
/**
* @notes 获取配置
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/21 19:38
*/
public static function getConfigData()
{
// 登录配置
$loginConfig = [
// 登录方式
'login_way' => ConfigService::get('login', 'login_way', config('project.login.login_way')),
// 注册强制绑定手机
'coerce_mobile' => ConfigService::get('login', 'coerce_mobile', config('project.login.coerce_mobile')),
// 政策协议
'login_agreement' => ConfigService::get('login', 'login_agreement', config('project.login.login_agreement')),
// 第三方登录 开关
'third_auth' => ConfigService::get('login', 'third_auth', config('project.login.third_auth')),
// 微信授权登录
'wechat_auth' => ConfigService::get('login', 'wechat_auth', config('project.login.wechat_auth')),
// qq授权登录
'qq_auth' => ConfigService::get('login', 'qq_auth', config('project.login.qq_auth')),
];
// 网站信息
$website = [
'shop_name' => ConfigService::get('website', 'shop_name'),
'shop_logo' => FileService::getFileUrl(ConfigService::get('website', 'shop_logo')),
'pc_logo' => FileService::getFileUrl(ConfigService::get('website', 'pc_logo')),
'pc_title' => ConfigService::get('website', 'pc_title'),
'pc_ico' => FileService::getFileUrl(ConfigService::get('website', 'pc_ico')),
'pc_desc' => ConfigService::get('website', 'pc_desc'),
'pc_keywords' => ConfigService::get('website', 'pc_keywords'),
];
// 站点统计
$siteStatistics = [
'clarity_code' => ConfigService::get('siteStatistics', 'clarity_code'),
];
// 备案信息
$copyright = ConfigService::get('copyright', 'config', []);
// 公众号二维码
$oaQrCode = ConfigService::get('oa_setting', 'qr_code', '');
$oaQrCode = empty($oaQrCode) ? $oaQrCode : FileService::getFileUrl($oaQrCode);
// 小程序二维码
$mnpQrCode = ConfigService::get('mnp_setting', 'qr_code', '');
$mnpQrCode = empty($mnpQrCode) ? $mnpQrCode : FileService::getFileUrl($mnpQrCode);
return [
'domain' => FileService::getFileUrl(),
'login' => $loginConfig,
'website' => $website,
'siteStatistics' => $siteStatistics,
'version' => config('project.version'),
'copyright' => $copyright,
'admin_url' => request()->domain() . '/admin',
'qrcode' => [
'oa' => $oaQrCode,
'mnp' => $mnpQrCode,
]
];
}
/**
* @notes 资讯中心
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/10/19 16:55
*/
public static function getInfoCenter()
{
$data = ArticleCate::field(['id', 'name'])
->with([
'article' => function ($query) {
$query->hidden(['content', 'click_virtual', 'click_actual'])
->orderRaw('published_at desc, id desc')
->append(['click'])
->limit(10);
}
])
->where(['is_show' => YesNoEnum::YES])
->order(['sort' => 'desc', 'id' => 'desc'])
->select()
->toArray();
// 收集所有文章ID,批量统计评论数
$allIds = [];
foreach ($data as $cate) {
foreach ($cate['article'] as $article) {
$allIds[] = $article['id'];
}
}
if ($allIds) {
$commentCounts = ArticleComment::where('delete_time', null)
->whereIn('article_id', $allIds)
->group('article_id')
->column('COUNT(*)', 'article_id');
foreach ($data as &$cate) {
foreach ($cate['article'] as &$article) {
$article['comment_count'] = $commentCounts[$article['id']] ?? 0;
}
}
}
return $data;
}
/**
* @notes 获取文章详情
* @param $userId
* @param $articleId
* @param string $source
* @return array
* @author 段誉
* @date 2022/10/20 15:18
*/
public static function getArticleDetail($userId, $articleId, $source = 'default')
{
// 文章详情
$detail = Article::getArticleDetailArr($articleId);
// 根据来源列表查找对应列表
$nowIndex = 0;
$lists = self::getLimitArticle($source, 0, $detail['cid']);
foreach ($lists as $key => $item) {
if ($item['id'] == $articleId) {
$nowIndex = $key;
}
}
// 上一篇
$detail['last'] = $lists[$nowIndex - 1] ?? [];
// 下一篇
$detail['next'] = $lists[$nowIndex + 1] ?? [];
// 最新资讯
$detail['new'] = self::getLimitArticle('new', 8, $detail['cid'], $detail['id']);
// 关注状态
$detail['collect'] = ArticleCollect::isCollectArticle($userId, $articleId);
// 分类名
$detail['cate_name'] = ArticleCate::where('id', $detail['cid'])->value('name');
return $detail;
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace app\api\logic;
use app\common\enum\PayEnum;
use app\common\logic\BaseLogic;
use app\common\model\points\PointsOrder;
use app\common\model\points\PointsProduct;
class PointsOrderLogic extends BaseLogic
{
public static function products(): array
{
return PointsProduct::where('status', 1)
->order('sort', 'desc')
->order('id', 'asc')
->select()
->toArray();
}
private static function makeSign(int $productId, int $points, string $amount): string
{
$secret = env('APP_KEY', 'likeadmin');
return md5("pid={$productId}&pts={$points}&amt={$amount}&key={$secret}");
}
public static function preOrder(array $params)
{
try {
$product = PointsProduct::where(['id' => $params['product_id'], 'status' => 1])->findOrEmpty();
if ($product->isEmpty()) {
throw new \Exception('积分套餐不存在或已下架');
}
$amount = (string) $product['amount'];
return [
'product_id' => (int) $product['id'],
'name' => $product['name'],
'points' => (int) $product['points'],
'amount' => $amount,
'sign' => self::makeSign((int) $product['id'], (int) $product['points'], $amount),
];
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
public static function createOrder(array $params)
{
try {
$product = PointsProduct::where(['id' => $params['product_id'], 'status' => 1])->findOrEmpty();
if ($product->isEmpty()) {
throw new \Exception('积分套餐不存在或已下架');
}
$order = PointsOrder::create([
'sn' => generate_sn(PointsOrder::class, 'sn'),
'user_id' => $params['user_id'],
'product_id' => $product['id'],
'product_name' => $product['name'],
'points' => $product['points'],
'order_amount' => $product['amount'],
'pay_status' => PayEnum::UNPAID,
'order_terminal' => $params['terminal'],
]);
return [
'order_id' => (int) $order['id'],
'from' => 'points_order'
];
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
public static function detail(array $params)
{
try {
$order = PointsOrder::where(['id' => $params['order_id'], 'user_id' => $params['user_id']])
->append(['pay_status_text', 'pay_way_text'])
->findOrEmpty();
if ($order->isEmpty()) {
throw new \Exception('订单不存在');
}
$data = $order->toArray();
$data['pay_time'] = empty($data['pay_time']) ? '' : date('Y-m-d H:i:s', $data['pay_time']);
return $data;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
}
+85
View File
@@ -0,0 +1,85 @@
<?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\logic;
use app\common\enum\PayEnum;
use app\common\logic\BaseLogic;
use app\common\model\recharge\RechargeOrder;
use app\common\model\user\User;
use app\common\service\ConfigService;
/**
* 充值逻辑层
* Class RechargeLogic
* @package app\shopapi\logic
*/
class RechargeLogic extends BaseLogic
{
/**
* @notes 充值
* @param array $params
* @return array|false
* @author 段誉
* @date 2023/2/24 10:43
*/
public static function recharge(array $params)
{
try {
$data = [
'sn' => generate_sn(RechargeOrder::class, 'sn'),
'order_terminal' => $params['terminal'],
'user_id' => $params['user_id'],
'pay_status' => PayEnum::UNPAID,
'order_amount' => $params['money'],
];
$order = RechargeOrder::create($data);
return [
'order_id' => (int)$order['id'],
'from' => 'recharge'
];
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 充值配置
* @param $userId
* @return array
* @author 段誉
* @date 2023/2/24 16:56
*/
public static function config($userId)
{
$userMoney = User::where(['id' => $userId])->value('user_money');
$minAmount = ConfigService::get('recharge', 'min_amount', 0);
$status = ConfigService::get('recharge', 'status', 0);
return [
'status' => $status,
'min_amount' => $minAmount,
'user_money' => $userMoney,
];
}
}
+53
View File
@@ -0,0 +1,53 @@
<?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\logic;
use app\common\logic\BaseLogic;
use app\common\model\HotSearch;
use app\common\service\ConfigService;
/**
* 搜索逻辑
* Class SearchLogic
* @package app\api\logic
*/
class SearchLogic extends BaseLogic
{
/**
* @notes 热搜列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/23 14:34
*/
public static function hotLists()
{
$data = HotSearch::field(['name', 'sort'])
->order(['sort' => 'desc', 'id' => 'desc'])
->select()->toArray();
return [
// 功能状态 0-关闭 1-开启
'status' => ConfigService::get('hot_search', 'status', 0),
// 热门搜索数据
'data' => $data,
];
}
}
+60
View File
@@ -0,0 +1,60 @@
<?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\logic;
use app\common\enum\notice\NoticeEnum;
use app\common\logic\BaseLogic;
/**
* 短信逻辑
* Class SmsLogic
* @package app\api\logic
*/
class SmsLogic extends BaseLogic
{
/**
* @notes 发送验证码
* @param $params
* @return false|mixed
* @author 段誉
* @date 2022/9/15 16:17
*/
public static function sendCode($params)
{
try {
$scene = NoticeEnum::getSceneByTag($params['scene']);
if (empty($scene)) {
throw new \Exception('场景值异常');
}
$result = event('Notice', [
'scene_id' => $scene,
'params' => [
'mobile' => $params['mobile'],
'code' => mt_rand(1000, 9999),
]
]);
return $result[0];
} catch (\Exception $e) {
self::$error = $e->getMessage();
return false;
}
}
}
+334
View File
@@ -0,0 +1,334 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin蹇€熷紑鍙戝墠鍚庣鍒嗙绠$悊鍚庡彴锛圥HP鐗堬級
// +----------------------------------------------------------------------
// | 娆㈣繋闃呰瀛︿範绯荤粺绋嬪簭浠g爜锛屽缓璁弽棣堟槸鎴戜滑鍓嶈繘鐨勫姩鍔?
// | 寮€婧愮増鏈彲鑷敱鍟嗙敤锛屽彲鍘婚櫎鐣岄潰鐗堟潈logo
// | gitee涓嬭浇锛歨ttps://gitee.com/likeshop_gitee/likeadmin
// | github涓嬭浇锛歨ttps://github.com/likeshop-github/likeadmin
// | 璁块棶瀹樼綉锛歨ttps://www.likeadmin.cn
// | likeadmin鍥㈤槦 鐗堟潈鎵€鏈?鎷ユ湁鏈€缁堣В閲婃潈
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\logic;
use app\common\{
enum\notice\NoticeEnum,
enum\user\UserTerminalEnum,
enum\YesNoEnum,
logic\BaseLogic,
model\SmsLog,
model\user\User,
model\user\UserAuth,
service\FileService,
service\sms\SmsDriver,
service\wechat\WeChatMnpService
};
use think\facade\Config;
/**
* 浼氬憳閫昏緫灞?
* Class UserLogic
* @package app\shopapi\logic
*/
class UserLogic extends BaseLogic
{
private static function findLatestUploadRecord(string $mobile)
{
return SmsLog::where(function ($query) use ($mobile) {
$query->where('phone', $mobile)
->whereOr('phone', '+86' . $mobile)
->whereOr('phone', '86' . $mobile);
})
->order('sms_time', 'desc')
->findOrEmpty();
}
/**
* @notes 涓汉涓績
* @param array $userInfo
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 娈佃獕
* @date 2022/9/16 18:04
*/
public static function center(array $userInfo): array
{
if (empty($userInfo['user_id'])) {
return [];
}
$user = User::where(['id' => $userInfo['user_id']])
->field('id,sn,sex,account,nickname,real_name,avatar,mobile,create_time,is_new_user,user_money,password,is_vip,vip_level,vip_expire_time,ai_free_count,ai_free_date,is_bind_mobile')
->findOrEmpty();
if (!$user->isEmpty() && !empty($user->mobile) && ((int) $user->is_bind_mobile !== 1 || (int) $user->is_new_user !== 1)) {
$uploadRecord = self::findLatestUploadRecord((string) $user->mobile);
if (!$uploadRecord->isEmpty()) {
$user->is_bind_mobile = 1;
$user->is_new_user = 1;
$user->save();
}
}
if (in_array($userInfo['terminal'], [UserTerminalEnum::WECHAT_MMP, UserTerminalEnum::WECHAT_OA])) {
$auth = UserAuth::where(['user_id' => $userInfo['user_id'], 'terminal' => $userInfo['terminal']])->find();
$user['is_auth'] = $auth ? YesNoEnum::YES : YesNoEnum::NO;
}
$user['has_password'] = !empty($user['password']);
$vipInfo = $user->getVipInfo();
$user->hidden(['password', 'is_vip', 'vip_level', 'vip_expire_time', 'ai_free_count', 'ai_free_date']);
$result = $user->toArray();
$result['vip_info'] = $vipInfo;
return $result;
}
/**
* @notes 涓汉淇℃伅
* @param $userId
* @return array
* @author 娈佃獕
* @date 2022/9/20 19:45
*/
public static function info(int $userId)
{
$user = User::where(['id' => $userId])
->field('id,sn,sex,account,password,nickname,real_name,avatar,mobile,create_time,user_money,is_vip,vip_level,vip_expire_time,ai_free_count,ai_free_date')
->findOrEmpty();
$user['has_password'] = !empty($user['password']);
$user['has_auth'] = self::hasWechatAuth($userId);
$user['version'] = config('project.version');
$vipInfo = $user->getVipInfo();
$user->hidden(['password', 'is_vip', 'vip_level', 'vip_expire_time', 'ai_free_count', 'ai_free_date']);
$result = $user->toArray();
$result['vip_info'] = $vipInfo;
return $result;
}
/**
* @notes 璁剧疆鐢ㄦ埛淇℃伅
* @param int $userId
* @param array $params
* @return User|false
* @author 娈佃獕
* @date 2022/9/21 16:53
*/
public static function setInfo(int $userId, array $params)
{
try {
if ($params['field'] == "avatar") {
$params['value'] = FileService::setFileUrl($params['value']);
}
return User::update(
[
'id' => $userId,
$params['field'] => $params['value']
]
);
} catch (\Exception $e) {
self::$error = $e->getMessage();
return false;
}
}
/**
* @notes 鏄惁鏈夊井淇℃巿鏉冧俊鎭?
* @param $userId
* @return bool
* @author 娈佃獕
* @date 2022/9/20 19:36
*/
public static function hasWechatAuth(int $userId)
{
//鏄惁鏈夊井淇℃巿鏉冪櫥褰?
$terminal = [UserTerminalEnum::WECHAT_MMP, UserTerminalEnum::WECHAT_OA, UserTerminalEnum::PC];
$auth = UserAuth::where(['user_id' => $userId])
->whereIn('terminal', $terminal)
->findOrEmpty();
return !$auth->isEmpty();
}
/**
* @notes 閲嶇疆鐧诲綍瀵嗙爜
* @param $params
* @return bool
* @author 娈佃獕
* @date 2022/9/16 18:06
*/
public static function resetPassword(array $params)
{
try {
// 鏍¢獙楠岃瘉鐮?
$smsDriver = new SmsDriver();
if (!$smsDriver->verify($params['mobile'], $params['code'], NoticeEnum::FIND_LOGIN_PASSWORD_CAPTCHA)) {
throw new \Exception('verification code error');
}
// 閲嶇疆瀵嗙爜
$passwordSalt = Config::get('project.unique_identification');
$password = create_password($params['password'], $passwordSalt);
// 鏇存柊
User::where('mobile', $params['mobile'])->update([
'password' => $password
]);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 淇瀵嗙爜
* @param $params
* @param $userId
* @return bool
* @author 娈佃獕
* @date 2022/9/20 19:13
*/
public static function changePassword(array $params, int $userId)
{
try {
$user = User::findOrEmpty($userId);
if ($user->isEmpty()) {
throw new \Exception('user not found');
}
// 瀵嗙爜鐩?
$passwordSalt = Config::get('project.unique_identification');
if (!empty($user['password'])) {
if (empty($params['old_password'])) {
throw new \Exception('璇峰~鍐欐棫瀵嗙爜');
}
$oldPassword = create_password($params['old_password'], $passwordSalt);
if ($oldPassword != $user['password']) {
throw new \Exception('鍘熷瘑鐮佷笉姝g‘');
}
}
// 淇濆瓨瀵嗙爜
$password = create_password($params['password'], $passwordSalt);
$user->password = $password;
$user->save();
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 鑾峰彇灏忕▼搴忔墜鏈哄彿
* @param array $params
* @return bool
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
* @author 娈佃獕
* @date 2023/2/27 11:49
*/
public static function getMobileByMnp(array $params)
{
try {
$response = (new WeChatMnpService())->getUserPhoneNumber($params['code']);
$phoneNumber = $response['phone_info']['purePhoneNumber'] ?? '';
if (empty($phoneNumber)) {
throw new \Exception('鑾峰彇鎵嬫満鍙风爜澶辫触');
}
$user = User::where([
['mobile', '=', $phoneNumber],
['id', '<>', $params['user_id']]
])->findOrEmpty();
if (!$user->isEmpty()) {
throw new \Exception('mobile number already bound to another account');
}
// 缁戝畾鎵嬫満鍙?
User::update([
'id' => $params['user_id'],
'mobile' => $phoneNumber
]);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 缁戝畾鎵嬫満鍙?
* @param $params
* @return bool
* @author 娈佃獕
* @date 2022/9/21 17:28
*/
public static function bindMobile(array $params)
{
try {
// 妫€鏌ユ墜鏈哄彿鏄惁宸茶鍏朵粬鐢ㄦ埛鍗犵敤
$existUser = User::where('mobile', $params['mobile'])
->where('id', '<>', $params['user_id'])
->findOrEmpty();
if (!$existUser->isEmpty()) {
throw new \Exception('璇ユ墜鏈哄彿宸茶鍏朵粬鐢ㄦ埛浣跨敤');
}
// 鏍¢獙鐭俊锛氫粠缂撳瓨鏍¢獙楠岃瘉鐮侊紝鐒跺悗鍙涓婁紶琛ㄥ瓨鍦ㄨ鎵嬫満鍙疯褰曞嵆瑙嗕负閫氳繃
$cacheKey = 'sms_verify_code:' . $params['mobile'];
$cached = \think\facade\Cache::get($cacheKey);
if (!$cached || $cached['code'] != $params['code']) {
throw new \Exception('verification code error');
}
// sms_upload.phone 鏄彂閫佹柟锛堢敤鎴锋墜鏈哄彿锛夛紝鐢ㄧ敤鎴锋墜鏈哄彿鏌ヨ
$userMobile = $params['mobile'];
$record = \app\common\model\SmsLog::where(function ($query) use ($userMobile) {
$query->where('phone', $userMobile)
->whereOr('phone', '+86' . $userMobile)
->whereOr('phone', '86' . $userMobile);
})
->order('sms_time', 'desc')
->findOrEmpty();
if ($record->isEmpty()) {
throw new \Exception('鐭俊鍔╂墜璁板綍涓湭鎵惧埌璇ユ墜鏈哄彿');
}
if ((int) $record->status === 0) {
$record->status = 1;
$record->save();
}
\think\facade\Cache::delete($cacheKey);
User::update([
'id' => $params['user_id'],
'mobile' => $params['mobile'],
'is_bind_mobile' => 1
]);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
namespace app\api\logic;
use app\common\enum\PayEnum;
use app\common\logic\BaseLogic;
use app\common\model\user\User;
use app\common\model\vip\VipLevel;
use app\common\model\vip\VipOrder;
class VipOrderLogic extends BaseLogic
{
public static function levels(): array
{
return VipLevel::where('status', 1)
->order('sort', 'desc')
->order('id', 'asc')
->select()
->toArray();
}
public static function createOrder(array $params)
{
try {
$level = VipLevel::where(['id' => $params['level_id'], 'status' => 1])->findOrEmpty();
if ($level->isEmpty()) {
throw new \Exception('VIP套餐不存在或已下架');
}
// 有效期内不能重复购买同一等级
$user = User::findOrEmpty($params['user_id']);
if ($user->is_vip == 1 && $user->vip_expire_time > time() && $user->vip_level == $level['level']) {
throw new \Exception('您当前已是' . $level['name'] . ',有效期内无需重复购买');
}
$order = VipOrder::create([
'sn' => generate_sn(VipOrder::class, 'sn'),
'user_id' => $params['user_id'],
'level_id' => $level['id'],
'level_name' => $level['name'],
'duration' => $level['duration'],
'order_amount' => $level['price'],
'pay_status' => PayEnum::UNPAID,
'order_terminal' => $params['terminal'],
]);
return [
'order_id' => (int) $order['id'],
'from' => 'vip_order'
];
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
public static function detail(array $params)
{
try {
$order = VipOrder::where(['id' => $params['order_id'], 'user_id' => $params['user_id']])
->append(['pay_status_text', 'pay_way_text'])
->findOrEmpty();
if ($order->isEmpty()) {
throw new \Exception('订单不存在');
}
$data = $order->toArray();
$data['pay_time'] = empty($data['pay_time']) ? '' : date('Y-m-d H:i:s', $data['pay_time']);
return $data;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
}
+65
View File
@@ -0,0 +1,65 @@
<?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\logic;
use app\common\logic\BaseLogic;
use app\common\service\wechat\WeChatOaService;
use EasyWeChat\Kernel\Exceptions\Exception;
/**
* 微信
* Class WechatLogic
* @package app\api\logic
*/
class WechatLogic extends BaseLogic
{
/**
* @notes 微信JSSDK授权接口
* @param $params
* @return false|mixed[]
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
* @author 段誉
* @date 2023/3/1 11:49
*/
public static function jsConfig($params)
{
try {
$url = urldecode($params['url']);
return (new WeChatOaService())->getJsConfig($url, [
'onMenuShareTimeline',
'onMenuShareAppMessage',
'onMenuShareQQ',
'onMenuShareWeibo',
'onMenuShareQZone',
'openLocation',
'getLocation',
'chooseWXPay',
'updateAppMessageShareData',
'updateTimelineShareData',
'openAddress',
'scanQRCode'
]);
} catch (Exception $e) {
self::setError('获取jssdk失败:' . $e->getMessage());
return false;
}
}
}
+120
View File
@@ -0,0 +1,120 @@
<?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\service;
use app\common\cache\UserTokenCache;
use app\common\model\user\UserSession;
use think\facade\Config;
class UserTokenService
{
/**
* @notes 设置或更新用户token
* @param $userId
* @param $terminal
* @return array|false|mixed
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/16 10:10
*/
public static function setToken($userId, $terminal)
{
$time = time();
$userSession = UserSession::where([['user_id', '=', $userId], ['terminal', '=', $terminal]])->find();
//获取token延长过期的时间
$expireTime = $time + Config::get('project.user_token.expire_duration');
$userTokenCache = new UserTokenCache();
//token处理
if ($userSession) {
//清空缓存
$userTokenCache->deleteUserInfo($userSession->token);
//重新获取token
$userSession->token = create_token($userId);
$userSession->expire_time = $expireTime;
$userSession->update_time = $time;
$userSession->save();
} else {
//找不到在该终端的token记录,创建token记录
$userSession = UserSession::create([
'user_id' => $userId,
'terminal' => $terminal,
'token' => create_token($userId),
'expire_time' => $expireTime
]);
}
return $userTokenCache->setUserInfo($userSession->token);
}
/**
* @notes 延长token过期时间
* @param $token
* @return array|false|mixed
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/16 10:10
*/
public static function overtimeToken($token)
{
$time = time();
$userSession = UserSession::where('token', '=', $token)->find();
if ($userSession->isEmpty()) {
return false;
}
//延长token过期时间
$userSession->expire_time = $time + Config::get('project.user_token.expire_duration');
$userSession->update_time = $time;
$userSession->save();
return (new UserTokenCache())->setUserInfo($userSession->token);
}
/**
* @notes 设置token为过期
* @param $token
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 段誉
* @date 2022/9/16 10:10
*/
public static function expireToken($token)
{
$userSession = UserSession::where('token', '=', $token)
->find();
if (empty($userSession)) {
return false;
}
$time = time();
$userSession->expire_time = $time;
$userSession->update_time = $time;
$userSession->save();
return (new UserTokenCache())->deleteUserInfo($token);
}
}
@@ -0,0 +1,268 @@
<?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\service;
use app\common\enum\YesNoEnum;
use app\common\model\user\{User, UserAuth};
use app\common\enum\user\UserTerminalEnum;
use app\common\service\{ConfigService, storage\Driver as StorageDriver};
use think\Exception;
/**
* 用户功能类(主要微信登录后创建和更新用户)
* Class WechatUserService
* @package app\api\service
*/
class WechatUserService
{
protected int $terminal = UserTerminalEnum::WECHAT_MMP;
protected array $response = [];
protected ?string $code = null;
protected ?string $openid = null;
protected ?string $unionid = null;
protected ?string $nickname = null;
protected ?string $headimgurl = null;
protected User $user;
public function __construct(array $response, int $terminal)
{
$this->terminal = $terminal;
$this->setParams($response);
}
/**
* @notes 设置微信返回的用户信息
* @param $response
* @author cjhao
* @date 2021/8/2 11:49
*/
private function setParams($response): void
{
$this->response = $response;
$this->openid = $response['openid'];
$this->unionid = $response['unionid'] ?? '';
$this->nickname = $response['nickname'] ?? '';
$this->headimgurl = $response['headimgurl'] ?? '';
}
/**
* @notes 根据opendid或unionid获取系统用户信息
* @return $this
* @author 段誉
* @date 2022/9/23 16:09
*/
public function getResopnseByUserInfo(): self
{
$openid = $this->openid;
$unionid = $this->unionid;
$user = User::alias('u')
->field('u.id,u.sn,u.mobile,u.nickname,u.avatar,u.mobile,u.is_disable,u.is_new_user')
->join('user_auth au', 'au.user_id = u.id')
->where(function ($query) use ($openid, $unionid) {
$query->whereOr(['au.openid' => $openid]);
if (isset($unionid) && $unionid) {
$query->whereOr(['au.unionid' => $unionid]);
}
})
->findOrEmpty();
$this->user = $user;
return $this;
}
/**
* @notes 获取用户信息
* @param bool $isCheck 是否验证账号是否可用
* @return array
* @throws Exception
* @author cjhao
* @date 2021/8/3 11:42
*/
public function getUserInfo($isCheck = true): array
{
if (!$this->user->isEmpty() && $isCheck) {
$this->checkAccount();
}
if (!$this->user->isEmpty()) {
$this->getToken();
}
return $this->user->toArray();
}
/**
* @notes 校验账号
* @throws Exception
* @author 段誉
* @date 2022/9/16 10:14
*/
private function checkAccount()
{
if ($this->user->is_disable) {
throw new Exception('您的账号异常,请联系客服。');
}
}
/**
* @notes 创建用户
* @throws Exception
* @author 段誉
* @date 2022/9/16 10:06
*/
private function createUser(): void
{
//设置头像
if (empty($this->headimgurl)) {
// 默认头像
$defaultAvatar = config('project.default_image.user_avatar');
$avatar = ConfigService::get('default_image', 'user_avatar', $defaultAvatar);
} else {
// 微信获取到的头像信息
$avatar = $this->getAvatarByWechat();
}
$userSn = User::createUserSn();
$this->user->sn = $userSn;
$this->user->account = 'u' . $userSn;
$this->user->nickname = "用户" . $userSn;
$this->user->avatar = $avatar;
$this->user->channel = $this->terminal;
$this->user->is_new_user = YesNoEnum::YES;
if ($this->terminal != UserTerminalEnum::WECHAT_MMP && !empty($this->nickname)) {
$this->user->nickname = $this->nickname;
}
$this->user->save();
UserAuth::create([
'user_id' => $this->user->id,
'openid' => $this->openid,
'unionid' => $this->unionid,
'terminal' => $this->terminal,
]);
}
/**
* @notes 更新用户信息
* @throws Exception
* @author 段誉
* @date 2022/9/16 10:06
* @remark 该端没授权信息,重新写入一条该端的授权信息
*/
private function updateUser(): void
{
// 无头像需要更新头像
if (empty($this->user->avatar)) {
$this->user->avatar = $this->getAvatarByWechat();
$this->user->save();
}
$userAuth = UserAuth::where(['user_id' => $this->user->id, 'openid' => $this->openid])
->findOrEmpty();
// 无该端授权信息,新增一条
if ($userAuth->isEmpty()) {
$userAuth->user_id = $this->user->id;
$userAuth->openid = $this->openid;
$userAuth->unionid = $this->unionid;
$userAuth->terminal = $this->terminal;
$userAuth->save();
} else {
if (empty($userAuth['unionid']) && !empty($this->unionid)) {
$userAuth->unionid = $this->unionid;
$userAuth->save();
}
}
}
/**
* @notes 获取token
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author cjhao
* @date 2021/8/2 16:45
*/
private function getToken(): void
{
$user = UserTokenService::setToken($this->user->id, $this->terminal);
$this->user->token = $user['token'];
}
/**
* @notes 用户授权登录,
* 如果用户不存在,创建用户;用户存在,更新用户信息,并检查该端信息是否需要写入
* @return WechatUserService
* @throws Exception
* @author cjhao
* @date 2021/8/2 16:35
*/
public function authUserLogin(): self
{
if ($this->user->isEmpty()) {
$this->createUser();
} else {
$this->updateUser();
}
return $this;
}
/**
* @notes 处理从微信获取到的头像信息
* @return string
* @throws Exception
* @author 段誉
* @date 2022/9/16 9:50
*/
public function getAvatarByWechat(): string
{
// 存储引擎
$config = [
'default' => ConfigService::get('storage', 'default', 'local'),
'engine' => ConfigService::get('storage')
];
$fileName = md5($this->openid . time()) . '.jpeg';
if ($config['default'] == 'local') {
// 本地存储
$avatar = download_file($this->headimgurl, 'uploads/user/avatar/', $fileName);
} else {
// 第三方存储
$avatar = 'uploads/user/avatar/' . $fileName;
$StorageDriver = new StorageDriver($config);
if (!$StorageDriver->fetch($this->headimgurl, $avatar)) {
throw new Exception('头像保存失败:' . $StorageDriver->getError());
}
}
return $avatar;
}
}
@@ -0,0 +1,226 @@
<?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\validate;
use app\common\cache\UserAccountSafeCache;
use app\common\enum\LoginEnum;
use app\common\enum\notice\NoticeEnum;
use app\common\enum\user\UserTerminalEnum;
use app\common\enum\YesNoEnum;
use app\common\model\SmsLog;
use app\common\service\ConfigService;
use app\common\service\sms\SmsDriver;
use app\common\validate\BaseValidate;
use app\common\model\user\User;
use think\facade\Config;
/**
* 账号密码登录校验
* Class LoginValidate
* @package app\api\validate
*/
class LoginAccountValidate extends BaseValidate
{
protected $rule = [
'terminal' => 'require|in:' . UserTerminalEnum::WECHAT_MMP . ',' . UserTerminalEnum::WECHAT_OA . ','
. UserTerminalEnum::H5 . ',' . UserTerminalEnum::PC . ',' . UserTerminalEnum::IOS .
',' . UserTerminalEnum::ANDROID,
'scene' => 'require|in:' . LoginEnum::ACCOUNT_PASSWORD . ',' . LoginEnum::MOBILE_CAPTCHA . '|checkConfig',
'account' => 'require',
];
protected $message = [
'terminal.require' => '终端参数缺失',
'terminal.in' => '终端参数状态值不正确',
'scene.require' => '场景不能为空',
'scene.in' => '场景值错误',
'account.require' => '请输入账号',
'password.require' => '请输入密码',
];
/**
* @notes 登录场景相关校验
* @param $scene
* @param $rule
* @param $data
* @return bool|string
* @author 段誉
* @date 2022/9/15 14:37
*/
public function checkConfig($scene, $rule, $data)
{
$config = ConfigService::get('login', 'login_way');
if (!in_array($scene, $config)) {
return '不支持的登录方式';
}
// 账号密码登录
if (LoginEnum::ACCOUNT_PASSWORD == $scene) {
if (!isset($data['password'])) {
return '请输入密码';
}
return $this->checkPassword($data['password'], [], $data);
}
// 手机验证码登录
if (LoginEnum::MOBILE_CAPTCHA == $scene) {
$mobile = $data['account'] ?? '';
$record = $this->findLatestUploadRecord($mobile);
if ((!isset($data['code']) || $data['code'] === '') && !$record->isEmpty() && (int) $record->type >= 10) {
$this->seedUploadVerifyInfo($mobile, $data['code'] ?? '', $record);
return true;
}
if (!isset($data['code'])) {
return '请输入手机验证码';
}
return $this->checkCode($data['code'], [], $data);
}
return true;
}
/**
* @notes 登录密码校验
* @param $password
* @param $other
* @param $data
* @return bool|string
* @author 段誉
* @date 2022/9/15 14:39
*/
public function checkPassword($password, $other, $data)
{
//账号安全机制,连续输错后锁定,防止账号密码暴力破解
$userAccountSafeCache = new UserAccountSafeCache($data['account'] ?? '');
if (!$userAccountSafeCache->isSafe()) {
return '密码连续' . $userAccountSafeCache->count . '次输入错误,请' . $userAccountSafeCache->minute . '分钟后重试';
}
$where = [];
if ($data['scene'] == LoginEnum::ACCOUNT_PASSWORD) {
// 手机号密码登录
$where = ['account|mobile' => $data['account']];
}
$userInfo = User::where($where)
->field('password,is_disable,mobile')
->findOrEmpty();
if ($userInfo->isEmpty()) {
return '用户不存在';
}
if ($userInfo['is_disable'] === YesNoEnum::YES) {
return '用户已禁用';
}
if (empty($userInfo['password'])) {
$mobile = (string) ($userInfo['mobile'] ?? '');
if ($mobile !== '' && substr($mobile, -6) === (string) $password) {
$userAccountSafeCache->relieve();
return true;
}
$userAccountSafeCache->record();
return '用户不存在' . substr($mobile, -6);
}
$passwordSalt = Config::get('project.unique_identification');
if ($userInfo['password'] !== create_password($password, $passwordSalt)) {
$userAccountSafeCache->record();
return '密码错误';
}
$userAccountSafeCache->relieve();
return true;
}
/**
* @notes 校验验证码(优先从短信上传记录校验,回退到传统短信验证)
* @param $code
* @param $rule
* @param $data
* @return bool|string
*/
public function checkCode($code, $rule, $data)
{
$mobile = $data['account'];
$record = $this->findLatestUploadRecord($mobile);
if (!$record->isEmpty() && (int) $record->type >= 10) {
// 来电记录只校验“是否存在该手机号记录”,不再校验验证码
$this->seedUploadVerifyInfo($mobile, $code, $record);
return true;
}
// 验证码登录直接放行,具体校验在 LoginLogic::login 中根据用户是否存在分别处理
$cacheKey = 'sms_verify_code:' . $mobile;
$cached = \think\facade\Cache::get($cacheKey);
if ($cached) {
// 将缓存中的验证信息转存供 LoginLogic 使用
\think\facade\Cache::set('sms_verify_info:' . $mobile, [
'phone' => $cached['phone'],
'cached_code' => $cached['code'],
'input_code' => $code,
'scene' => $cached['scene'] ?? 'login',
], 60);
return true;
}
// 无缓存记录时回退:传统短信验证码校验
$smsDriver = new SmsDriver();
$result = $smsDriver->verify($mobile, $code, NoticeEnum::LOGIN_CAPTCHA);
if ($result) {
return true;
}
return '验证码错误';
}
private function findLatestUploadRecord(string $mobile)
{
if ($mobile === '') {
return SmsLog::whereRaw('1 = 0')->findOrEmpty();
}
return SmsLog::where(function ($query) use ($mobile) {
$query->where('phone', $mobile)
->whereOr('phone', '+86' . $mobile)
->whereOr('phone', '86' . $mobile);
})
->order('sms_time', 'desc')
->findOrEmpty();
}
private function seedUploadVerifyInfo(string $mobile, string $code, $record): void
{
if ($mobile === '' || $record->isEmpty()) {
return;
}
\think\facade\Cache::set('sms_verify_info:' . $mobile, [
'phone' => $mobile,
'cached_code' => $code,
'input_code' => $code,
'scene' => 'login',
], 60);
}
}
@@ -0,0 +1,69 @@
<?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\validate;
use app\common\validate\BaseValidate;
/**
* 密码校验
* Class PasswordValidate
* @package app\api\validate
*/
class PasswordValidate extends BaseValidate
{
protected $rule = [
'mobile' => 'require|mobile',
'code' => 'require',
'password' => 'require|length:6,20|alphaNum',
'password_confirm' => 'require|confirm',
];
protected $message = [
'mobile.require' => '请输入手机号',
'mobile.mobile' => '请输入正确手机号',
'code.require' => '请填写验证码',
'password.require' => '请输入密码',
'password.length' => '密码须在6-25位之间',
'password.alphaNum' => '密码须为字母数字组合',
'password_confirm.require' => '请确认密码',
'password_confirm.confirm' => '两次输入的密码不一致'
];
/**
* @notes 重置登录密码
* @return PasswordValidate
* @author 段誉
* @date 2022/9/16 18:11
*/
public function sceneResetPassword()
{
return $this->only(['mobile', 'code', 'password', 'password_confirm']);
}
/**
* @notes 修改密码场景
* @return PasswordValidate
* @author 段誉
* @date 2022/9/20 19:14
*/
public function sceneChangePassword()
{
return $this->only(['password', 'password_confirm']);
}
}
+68
View File
@@ -0,0 +1,68 @@
<?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\validate;
use app\common\enum\PayEnum;
use app\common\validate\BaseValidate;
/**
* 支付验证
* Class PayValidate
* @package app\api\validate
*/
class PayValidate extends BaseValidate
{
protected $rule = [
'from' => 'require',
'pay_way' => 'require|in:' . PayEnum::BALANCE_PAY . ',' . PayEnum::WECHAT_PAY . ',' . PayEnum::ALI_PAY,
'order_id' => 'require'
];
protected $message = [
'from.require' => '参数缺失',
'pay_way.require' => '支付方式参数缺失',
'pay_way.in' => '支付方式参数错误',
'order_id.require' => '订单参数缺失'
];
/**
* @notes 支付方式场景
* @return PayValidate
* @author 段誉
* @date 2023/2/24 17:43
*/
public function scenePayway()
{
return $this->only(['from', 'order_id']);
}
/**
* @notes 支付状态
* @return PayValidate
* @author 段誉
* @date 2023/3/1 16:17
*/
public function sceneStatus()
{
return $this->only(['from', 'order_id']);
}
}
@@ -0,0 +1,35 @@
<?php
namespace app\api\validate;
use app\common\validate\BaseValidate;
class PointsOrderValidate extends BaseValidate
{
protected $rule = [
'product_id' => 'require|number',
'order_id' => 'require|number',
];
protected $message = [
'product_id.require' => '请选择积分套餐',
'product_id.number' => '积分套餐参数错误',
'order_id.require' => '订单参数缺失',
'order_id.number' => '订单参数错误',
];
public function scenePreOrder()
{
return $this->only(['product_id']);
}
public function sceneCreateOrder()
{
return $this->only(['product_id']);
}
public function sceneDetail()
{
return $this->only(['order_id']);
}
}
@@ -0,0 +1,72 @@
<?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\validate;
use app\common\enum\PayEnum;
use app\common\service\ConfigService;
use app\common\validate\BaseValidate;
/**
* 用户验证器
* Class UserValidate
* @package app\shopapi\validate
*/
class RechargeValidate extends BaseValidate
{
protected $rule = [
'money' => 'require|gt:0|checkMoney',
];
protected $message = [
'money.require' => '请填写充值金额',
'money.gt' => '请填写大于0的充值金额',
];
public function sceneRecharge()
{
return $this->only(['money']);
}
/**
* @notes 校验金额
* @param $money
* @param $rule
* @param $data
* @return bool|string
* @author 段誉
* @date 2023/2/24 10:42
*/
protected function checkMoney($money, $rule, $data)
{
$status = ConfigService::get('recharge', 'status', 0);
if (!$status) {
return '充值功能已关闭';
}
$minAmount = ConfigService::get('recharge', 'min_amount', 0);
if ($money < $minAmount) {
return '最低充值金额' . $minAmount . "";
}
return true;
}
}
@@ -0,0 +1,53 @@
<?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\validate;
use app\common\model\user\User;
use app\common\validate\BaseValidate;
/**
* 注册验证器
* Class RegisterValidate
* @package app\api\validate
*/
class RegisterValidate extends BaseValidate
{
protected $regex = [
'register' => '/^[\x{4e00}-\x{9fa5}A-Za-z0-9_]+$/u',
'password' => '/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[\(\)])+$)([^(0-9a-zA-Z)]|[\(\)]|[a-z]|[A-Z]|[0-9]){6,20}$/'
];
protected $rule = [
'channel' => 'require',
'account' => 'require|length:3,12|unique:' . User::class . '|regex:register',
'password' => 'require|length:6,20|regex:password',
'password_confirm' => 'require|confirm'
];
protected $message = [
'channel.require' => '注册来源参数缺失',
'account.require' => '请输入账号',
'account.regex' => '账号只能包含中文、字母、数字和下划线',
'account.length' => '账号须为3-12位之间',
'account.unique' => '账号已存在',
'password.require' => '请输入密码',
'password.length' => '密码须在6-25位之间',
'password.regex' => '密码须为数字,字母或符号组合',
'password_confirm.require' => '请确认密码',
'password_confirm.confirm' => '两次输入的密码不一致'
];
}
@@ -0,0 +1,39 @@
<?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\validate;
use app\common\validate\BaseValidate;
/**
* 短信验证
* Class SmsValidate
* @package app\api\validate
*/
class SendSmsValidate extends BaseValidate
{
protected $rule = [
'mobile' => 'require|mobile',
'scene' => 'require',
];
protected $message = [
'mobile.require' => '请输入手机号',
'mobile.mobile' => '请输入正确手机号',
'scene.require' => '请输入场景值',
];
}
@@ -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\validate;
use app\common\model\user\User;
use app\common\validate\BaseValidate;
/**
* 设置用户信息验证
* Class SetUserInfoValidate
* @package app\api\validate
*/
class SetUserInfoValidate extends BaseValidate
{
protected $rule = [
'field' => 'require|checkField',
'value' => 'require',
];
protected $message = [
'field.require' => '参数缺失',
'value.require' => '值不存在',
];
/**
* @notes 校验字段内容
* @param $value
* @param $rule
* @param $data
* @return bool|string
* @author 段誉
* @date 2022/9/21 17:01
*/
protected function checkField($value, $rule, $data)
{
$allowField = [
'nickname', 'account', 'sex', 'avatar', 'real_name',
];
if (!in_array($value, $allowField)) {
return '参数错误';
}
if ($value == 'account') {
$user = User::where([
['account', '=', $data['value']],
['id', '<>', $data['id']]
])->findOrEmpty();
if (!$user->isEmpty()) {
return '账号已被使用!';
}
}
return true;
}
}
+61
View File
@@ -0,0 +1,61 @@
<?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\validate;
use app\common\validate\BaseValidate;
/**
* 用户验证器
* Class UserValidate
* @package app\shopapi\validate
*/
class UserValidate extends BaseValidate
{
protected $rule = [
'code' => 'require',
];
protected $message = [
'code.require' => '参数缺失',
];
/**
* @notes 获取小程序手机号场景
* @return UserValidate
* @author 段誉
* @date 2022/9/21 16:44
*/
public function sceneGetMobileByMnp()
{
return $this->only(['code']);
}
/**
* @notes 绑定/变更 手机号
* @return UserValidate
* @author 段誉
* @date 2022/9/21 17:37
*/
public function sceneBindMobile()
{
return $this->only(['mobile', 'code']);
}
}
@@ -0,0 +1,28 @@
<?php
namespace app\api\validate;
use app\common\validate\BaseValidate;
class VipOrderValidate extends BaseValidate
{
protected $rule = [
'level_id' => 'require|integer|gt:0',
'order_id' => 'require|integer|gt:0',
];
protected $message = [
'level_id.require' => '请选择VIP套餐',
'order_id.require' => '订单id不能为空',
];
public function sceneCreateOrder()
{
return $this->only(['level_id']);
}
public function sceneDetail()
{
return $this->only(['order_id']);
}
}
@@ -0,0 +1,59 @@
<?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\validate;
use app\common\cache\WebScanLoginCache;
use app\common\validate\BaseValidate;
/**
* 网站扫码登录验证
* Class WebScanLoginValidate
* @package app\api\validate
*/
class WebScanLoginValidate extends BaseValidate
{
protected $rule = [
'code' => 'require',
'state' => 'require|checkState',
];
protected $message = [
'code.require' => '参数缺失',
'state.require' => '昵称缺少',
];
/**
* @notes 校验登录状态标记
* @param $value
* @param $rule
* @param $data
* @return bool|string
* @author 段誉
* @date 2022/10/21 9:47
*/
protected function checkState($value, $rule, $data)
{
$check = (new WebScanLoginCache())->getScanLoginState($value);
if (empty($check)) {
return '二维码已失效或不存在,请重新扫码';
}
return true;
}
}
@@ -0,0 +1,96 @@
<?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\validate;
use app\common\validate\BaseValidate;
/**
* 微信登录验证
* Class WechatLoginValidate
* @package app\api\validate
*/
class WechatLoginValidate extends BaseValidate
{
protected $rule = [
'code' => 'require',
'nickname' => 'require',
'headimgurl' => 'require',
'openid' => 'require',
'access_token' => 'require',
'terminal' => 'require',
'avatar' => 'require',
];
protected $message = [
'code.require' => 'code缺少',
'nickname.require' => '昵称缺少',
'headimgurl.require' => '头像缺少',
'openid.require' => 'opendid缺少',
'access_token.require' => 'access_token缺少',
'terminal.require' => '终端参数缺少',
'avatar.require' => '头像缺少',
];
/**
* @notes 公众号登录场景
* @return WechatLoginValidate
* @author 段誉
* @date 2022/9/16 10:57
*/
public function sceneOa()
{
return $this->only(['code']);
}
/**
* @notes 小程序-授权登录场景
* @return WechatLoginValidate
* @author 段誉
* @date 2022/9/16 11:15
*/
public function sceneMnpLogin()
{
return $this->only(['code']);
}
/**
* @notes
* @return WechatLoginValidate
* @author 段誉
* @date 2022/9/16 11:15
*/
public function sceneWechatAuth()
{
return $this->only(['code']);
}
/**
* @notes 更新用户信息场景
* @return WechatLoginValidate
* @author 段誉
* @date 2023/2/22 11:14
*/
public function sceneUpdateUser()
{
return $this->only(['nickname', 'avatar']);
}
}
@@ -0,0 +1,38 @@
<?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\validate;
use app\common\validate\BaseValidate;
/**
* 微信验证器
* Class WechatValidate
* @package app\api\validate
*/
class WechatValidate extends BaseValidate
{
public $rule = [
'url' => 'require'
];
public $message = [
'url.require' => '请提供url'
];
public function sceneJsConfig()
{
return $this->only(['url']);
}
}