1095 lines
42 KiB
PHP
1095 lines
42 KiB
PHP
<?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\FileService;
|
|
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 localizeLiuhePostImagesForAi(CommunityPost $post, array $images): array
|
|
{
|
|
$localizedImages = [];
|
|
$changed = false;
|
|
|
|
foreach ($images as $image) {
|
|
$image = trim((string) $image);
|
|
if ($image === '') {
|
|
continue;
|
|
}
|
|
|
|
if ($this->isLocalUploadImage($image)) {
|
|
$localUrl = $this->normalizeLocalUploadImageUrl($image);
|
|
$localizedImages[] = $localUrl;
|
|
$changed = $changed || $localUrl !== $image;
|
|
continue;
|
|
}
|
|
|
|
if (!preg_match('/^https?:\/\//i', $image)) {
|
|
$localUrl = FileService::getFileUrl(ltrim($image, '/'));
|
|
$localizedImages[] = $localUrl;
|
|
$changed = $changed || $localUrl !== $image;
|
|
continue;
|
|
}
|
|
|
|
$localUrl = $this->saveRemoteImageForAiAnalysis($image);
|
|
if ($localUrl === '') {
|
|
return [
|
|
'success' => false,
|
|
'error' => '图片下载到本地失败,请稍后重试',
|
|
'images' => [],
|
|
];
|
|
}
|
|
|
|
$localizedImages[] = $localUrl;
|
|
$changed = true;
|
|
}
|
|
|
|
if (empty($localizedImages)) {
|
|
return ['success' => false, 'error' => '帖子图片为空,无法分析', 'images' => []];
|
|
}
|
|
|
|
if ($changed || $localizedImages !== array_values($images)) {
|
|
$now = time();
|
|
Db::name('community_post')->where('id', (int) $post->id)->update([
|
|
'images' => json_encode($localizedImages, JSON_UNESCAPED_UNICODE),
|
|
'update_time' => $now,
|
|
]);
|
|
|
|
Db::name('community_post_image')->where('post_id', (int) $post->id)->delete();
|
|
foreach ($localizedImages as $index => $imageUrl) {
|
|
Db::name('community_post_image')->insert([
|
|
'post_id' => (int) $post->id,
|
|
'image_url' => $imageUrl,
|
|
'sort' => $index,
|
|
'create_time' => $now,
|
|
]);
|
|
}
|
|
}
|
|
|
|
return ['success' => true, 'images' => $localizedImages, 'changed' => $changed];
|
|
}
|
|
|
|
private function isLocalUploadImage(string $image): bool
|
|
{
|
|
if (!preg_match('/^https?:\/\//i', $image)) {
|
|
return str_starts_with(ltrim($image, '/'), 'uploads/');
|
|
}
|
|
|
|
$path = (string) (parse_url($image, PHP_URL_PATH) ?: '');
|
|
if (!str_starts_with($path, '/uploads/')) {
|
|
return false;
|
|
}
|
|
|
|
$imageHost = strtolower((string) (parse_url($image, PHP_URL_HOST) ?: ''));
|
|
$currentHost = strtolower((string) (parse_url(request()->domain(), PHP_URL_HOST) ?: ''));
|
|
return $imageHost !== '' && $currentHost !== '' && $imageHost === $currentHost;
|
|
}
|
|
|
|
private function normalizeLocalUploadImageUrl(string $image): string
|
|
{
|
|
if (preg_match('/^https?:\/\//i', $image)) {
|
|
return $image;
|
|
}
|
|
|
|
return FileService::getFileUrl(ltrim($image, '/'));
|
|
}
|
|
|
|
private function saveRemoteImageForAiAnalysis(string $imageUrl): string
|
|
{
|
|
$ch = curl_init($imageUrl);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_CONNECTTIMEOUT => 10,
|
|
CURLOPT_TIMEOUT => 25,
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_USERAGENT => 'Mozilla/5.0 SportEraBot/1.0',
|
|
]);
|
|
|
|
$body = curl_exec($ch);
|
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$contentType = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode !== 200 || !is_string($body) || $body === '' || strlen($body) > 8 * 1024 * 1024) {
|
|
return '';
|
|
}
|
|
|
|
$mime = strtolower(trim(explode(';', $contentType)[0] ?? ''));
|
|
$imageInfo = @getimagesizefromstring($body);
|
|
if (is_array($imageInfo) && !empty($imageInfo['mime'])) {
|
|
$mime = strtolower((string) $imageInfo['mime']);
|
|
}
|
|
|
|
$extension = match ($mime) {
|
|
'image/jpeg', 'image/jpg' => 'jpg',
|
|
'image/png' => 'png',
|
|
'image/webp' => 'webp',
|
|
'image/gif' => 'gif',
|
|
default => '',
|
|
};
|
|
|
|
if ($extension === '') {
|
|
$path = parse_url($imageUrl, PHP_URL_PATH) ?: '';
|
|
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
|
if (!in_array($extension, ['jpg', 'jpeg', 'png', 'webp', 'gif'], true)) {
|
|
return '';
|
|
}
|
|
$extension = $extension === 'jpeg' ? 'jpg' : $extension;
|
|
}
|
|
|
|
$relativeDir = 'uploads/ai_lottery_posts/' . date('Ymd');
|
|
$publicRoot = rtrim(public_path(), DIRECTORY_SEPARATOR . '/');
|
|
$targetDir = $publicRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativeDir);
|
|
if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
|
|
return '';
|
|
}
|
|
|
|
$filename = sha1($imageUrl . "\n" . $body) . '.' . $extension;
|
|
$relativeUri = $relativeDir . '/' . $filename;
|
|
$targetPath = $targetDir . DIRECTORY_SEPARATOR . $filename;
|
|
|
|
if (!is_file($targetPath) && file_put_contents($targetPath, $body) === false) {
|
|
return '';
|
|
}
|
|
|
|
return FileService::getFileUrl($relativeUri);
|
|
}
|
|
|
|
private function ensureLiuheAnalysisContent(CommunityPost $post, array $ext): array
|
|
{
|
|
$images = is_array($post->images) ? array_values($post->images) : [];
|
|
$originalImageHash = md5(json_encode($images, JSON_UNESCAPED_UNICODE));
|
|
$localizeResult = $this->localizeLiuhePostImagesForAi($post, $images);
|
|
if (empty($localizeResult['success'])) {
|
|
return [
|
|
'success' => false,
|
|
'error' => (string) ($localizeResult['error'] ?? '图片下载到本地失败'),
|
|
];
|
|
}
|
|
|
|
$images = array_values($localizeResult['images']);
|
|
$imageHash = md5(json_encode($images, JSON_UNESCAPED_UNICODE));
|
|
$cacheSource = (string) ($ext['lottery_analysis_source'] ?? '');
|
|
$cacheHash = (string) ($ext['lottery_analysis_images_hash'] ?? '');
|
|
|
|
if (
|
|
!empty($ext['lottery_analysis_content'])
|
|
&& $cacheSource === 'image_only'
|
|
&& in_array($cacheHash, [$imageHash, $originalImageHash], true)
|
|
) {
|
|
if ($cacheHash !== $imageHash) {
|
|
$ext['lottery_analysis_images_hash'] = $imageHash;
|
|
CommunityPost::where('id', (int) $post->id)->update([
|
|
'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE),
|
|
'update_time' => time(),
|
|
]);
|
|
KbSyncService::enqueue('post', 'post', (int) $post->id, 'upsert', 45);
|
|
}
|
|
return ['success' => true, 'ext' => $ext, 'from_cache' => true];
|
|
}
|
|
|
|
$result = AiService::analyzeLotteryPostImages($post->content ?: '', $images);
|
|
if (empty($result['success']) || empty($result['content'])) {
|
|
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);
|
|
}
|
|
}
|