Files

1020 lines
33 KiB
PHP

<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace addon\wxchat\controller\api;
use addon\wxchat\model\WxchatMoment as MomentModel;
use think\facade\Log;
use think\Request;
use ywxapp\controller\FrontendBase;
header('Access-Control-Allow-Origin: *'); // 允许所有域
header('Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT');
header('Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization');
header('Access-Control-Allow-Credentials: true');
/**
* Moment 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Moment extends FrontendBase
{
private $uid = 0;
/**
* Summary of noNeedLogin
* @var array
*/
protected $noNeedLogin = [];
/**
* Summary of noNeedVerify
* @var array
*/
protected $noNeedVerify = ['*'];
/**
* 控制器初始化 initialize
* @return void
*/
protected function initialize()
{
if ($this->auth->isLogin) {
$this->uid = $this->auth->model->uid;
}
}
/**
* 获取动态详情
* @param int $id
* @return Json
*/
public function red(int $id)
{
try {
// 查询动态详情
$info = MomentModel::with([
'member' =>
function ($query) {
$query->field('id,username,nickname,avatar,introduction,follower_count,following_count,dynamic_count');
},
'media' =>
function ($query) {
$query->field('id,mid,type,url,thumbnail_url,width,height,duration,sort_order')->order('sort_order', 'asc');
},
'hashtags' =>
function ($query) {
$query->field('hashtags.id,hashtags.name,hashtags.description');
},
])->where('id', $id)
->where('status', 1)
->find();
if (! $info) {
$this->error();
}
// 增加浏览量
$this->increaseViewCount($id, $this->uid);
// 获取当前用户与动态的交互状态
$interaction = $this->getUserInteraction($id, $this->uid);
// 获取评论列表(分页)
$page = Request::param('page', 1);
$limit = Request::param('limit', 20);
$comments = $this->getComments($id, $page, $limit, $this->uid);
// 构建响应数据
$response = [
'moment' => $this->formatDynamic($info, $interaction),
'comments' => $comments['list'],
'comment_total' => $comments['total'],
'comment_pages' => $comments['pages'],
];
$this->success($response, 'success');
} catch (\Exception $e) {
$this->error('服务器错误:' . $e->getMessage());
}
}
/**
* 动态广场列表(推荐/关注/附近/热门)
*/
public function index()
{
$uid = $this->auth->model->uid;
$scope = Request::param('scope', 'recommend');
$page = max(1, (int) Request::param('page', 1));
$pageSize = max(1, min(50, (int) Request::param('page_size', 10)));
$query = MomentModel::with([
'member' =>
function ($q) {
$q->field('id,username,nickname,avatar,introduction');
},
'media' =>
function ($q) {
$q->field('id,mid,type,url,thumbnail_url,sort_order')->order('sort_order', 'asc');
},
'hashtags' =>
function ($q) {
$q->field('hashtags.id,hashtags.name');
},
])->where('status', 1);
if ($scope == 'follow') {
$ids = UserFollowModel::where('follower_id', $uid)->column('following_id');
if (empty($ids)) {
$this->success(['list' => [], 'total' => 0, 'page' => $page]);
return;
}
$query->where('uid', 'in', $ids)->order('create_at', 'desc');
} elseif ($scope == 'hot') {
$query->order('like_count', 'desc')->order('comment_count', 'desc');
} else {
if ($scope == 'nearby') {
$query->where('latitude', '>', 0)->where('longitude', '>', 0);
}
$query->order('create_at', 'desc');
}
$total = $query->count();
$list = $query->page($page, $pageSize)->select();
$result = [];
foreach ($list as $info) {
$result[] = $this->formatDynamic($info, $this->getUserInteraction($info->id, $uid));
}
$this->success(['list' => $result, 'total' => $total, 'page' => $page]);
}
/**
* 我的动态列表(获赞页复用)
*/
public function mine()
{
$uid = $this->uid;
$page = max(1, (int) Request::param('page', 1));
$pageSize = max(1, min(50, (int) Request::param('page_size', 10)));
$query = MomentModel::with([
'member' =>
function ($q) {
$q->field('id,username,nickname,avatar,introduction');
},
'media' =>
function ($q) {
$q->field('id,mid,type,url,thumbnail_url,sort_order')->order('sort_order', 'asc');
},
'hashtags' =>
function ($q) {
$q->field('hashtags.id,hashtags.name');
},
])->where('uid', $uid)->where('status', 1)->order('create_at', 'desc');
$total = $query->count();
$list = $query->page($page, $pageSize)->select();
$result = [];
foreach ($list as $info) {
$result[] = $this->formatDynamic($info, $this->getUserInteraction($info->id, $uid));
}
$this->success(['list' => $result, 'total' => $total, 'page' => $page]);
}
/**
* 我的收藏列表
*/
public function collectedList()
{
$uid = $this->uid;
$page = max(1, (int) Request::param('page', 1));
$pageSize = max(1, min(50, (int) Request::param('page_size', 10)));
$mids = Db::name('wxchat_moment_collects')
->where('uid', $uid)
->order('create_at', 'desc')
->column('mid');
if (empty($mids)) {
$this->success(['list' => [], 'total' => 0, 'page' => $page]);
return;
}
$total = count($mids);
$slice = array_slice($mids, ($page - 1) * $pageSize, $pageSize);
$list = MomentModel::with([
'member' =>
function ($q) {
$q->field('id,username,nickname,avatar,introduction');
},
'media' =>
function ($q) {
$q->field('id,mid,type,url,thumbnail_url,sort_order')->order('sort_order', 'asc');
},
'hashtags' =>
function ($q) {
$q->field('hashtags.id,hashtags.name');
},
])->where('id', 'in', $slice)->where('status', 1)->select();
$map = [];
foreach ($list as $info) {
$map[$info->id] = $info;
}
$result = [];
foreach ($slice as $mid) {
if (isset($map[$mid])) {
$result[] = $this->formatDynamic($map[$mid], $this->getUserInteraction($mid, $uid));
}
}
$this->success(['list' => $result, 'total' => $total, 'page' => $page]);
}
/**
* 我的浏览记录
*/
public function historyList()
{
$uid = $this->uid;
$page = max(1, (int) Request::param('page', 1));
$pageSize = max(1, min(50, (int) Request::param('page_size', 10)));
$mids = Db::name('wxchat_view_history')
->where('uid', $uid)
->order('create_at', 'desc')
->column('mid');
if (empty($mids)) {
$this->success(['list' => [], 'total' => 0, 'page' => $page]);
return;
}
$mids = array_values(array_unique($mids));
$total = count($mids);
$slice = array_slice($mids, ($page - 1) * $pageSize, $pageSize);
$list = MomentModel::with([
'member' =>
function ($q) {
$q->field('id,username,nickname,avatar,introduction');
},
'media' =>
function ($q) {
$q->field('id,mid,type,url,thumbnail_url,sort_order')->order('sort_order', 'asc');
},
'hashtags' =>
function ($q) {
$q->field('hashtags.id,hashtags.name');
},
])->where('id', 'in', $slice)->where('status', 1)->select();
$map = [];
foreach ($list as $info) {
$map[$info->id] = $info;
}
$result = [];
foreach ($slice as $mid) {
if (isset($map[$mid])) {
$result[] = $this->formatDynamic($map[$mid], $this->getUserInteraction($mid, $uid));
}
}
$this->success(['list' => $result, 'total' => $total, 'page' => $page]);
}
/**
* 发布动态
* @return Json
*/
public function create()
{
try {
$data = Request::post();
$validate = Validate::rule([
'content' => 'max:2000',
'type' => 'require|in:1,2,3',
'is_public' => 'in:0,1',
'media' => 'array',
'hashtags' => 'array',
]);
if (! $validate->check($data)) {
$this->error($validate->getError());
}
// 内容与媒体至少存在其一
if (empty($data['content']) && empty($data['media'])) {
$this->error('请填写内容或添加图片');
}
Db::startTrans();
try {
// 创建动态
$info = new MomentModel();
$info->uid = $this->uid;
$info->content = $data['content'] ?? '';
$info->type = $data['type'];
$info->location = $data['location'] ?? null;
$info->latitude = $data['latitude'] ?? null;
$info->longitude = $data['longitude'] ?? null;
$info->is_public = $data['is_public'] ?? 1;
$info->save();
$infoId = $info->id;
// 保存媒体文件
if (! empty($data['media'])) {
$mediaData = [];
foreach ($data['media'] as $index => $media) {
$mediaData[] = [
'dynamic_id' => $infoId,
'type' => $media['type'],
'url' => $media['url'],
'thumbnail_url' => $media['thumbnail_url'] ?? null,
'width' => $media['width'] ?? null,
'height' => $media['height'] ?? null,
'duration' => $media['duration'] ?? null,
'sort_order' => $index,
];
}
DynamicMediaModel::insertAll($mediaData);
}
// 处理话题标签
if (! empty($data['hashtags'])) {
$hashtagData = [];
foreach ($data['hashtags'] as $hashtagName) {
// 查找或创建标签
$hashtag = HashtagModel::where('name', $hashtagName)->find();
if (! $hashtag) {
$hashtag = new HashtagModel();
$hashtag->name = $hashtagName;
$hashtag->save();
}
// 关联动态和标签
$hashtagData[] = [
'dynamic_id' => $infoId,
'hashtag_id' => $hashtag->id,
];
// 更新标签计数
$hashtag->dynamic_count = Db::raw('dynamic_count + 1');
$hashtag->save();
}
if (! empty($hashtagData)) {
Db::name('dynamic_hashtags')->insertAll($hashtagData);
}
}
// 更新用户动态计数
UserModel::where('id', $this->uid)->inc('dynamic_count')->update();
Db::commit();
return json([
'code' => 200,
'message' => '动态发布成功',
'data' => [
'id' => $infoId,
],
]);
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
} catch (\Exception $e) {
return json([
'code' => 500,
'message' => '发布失败:' . $e->getMessage(),
'data' => null,
]);
}
}
/**
* 发布评论
* @return Json
*/
public function comment(): Json
{
try {
$data = Request::post();
// 验证数据
$validate = Validate::rule([
'dynamic_id' => 'require|integer|gt:0',
'content' => 'require|max:1000',
'parent_id' => 'integer|gt:0',
'reply_to_user_id' => 'integer|gt:0',
]);
if (! $validate->check($data)) {
return json([
'code' => 400,
'message' => $validate->getError(),
'data' => null,
]);
}
// 检查动态是否存在
$info = MomentModel::where('id', $data['dynamic_id'])
->where('status', 1)
->find();
if (! $info) {
return json([
'code' => 404,
'message' => '动态不存在',
'data' => null,
]);
}
// 开始事务
Db::startTrans();
try {
// 创建评论
$comment = new CommentModel();
$comment->dynamic_id = $data['dynamic_id'];
$comment->user_id = $this->uid;
$comment->parent_id = $data['parent_id'] ?? 0;
$comment->reply_to_user_id = $data['reply_to_user_id'] ?? null;
$comment->content = $data['content'];
$comment->save();
$commentId = $comment->id;
// 更新动态评论数
$info->comment_count = Db::raw('comment_count + 1');
$info->save();
// 如果是回复评论,更新父评论的回复数
if (! empty($data['parent_id'])) {
CommentModel::where('id', $data['parent_id'])
->inc('reply_count')
->update();
}
Db::commit();
// 获取完整的评论信息
$comment = CommentModel::with(['member' =>
function ($query) {
$query->field('id,username,nickname,avatar');
}])->where('id', $commentId)->find();
return json([
'code' => 200,
'message' => '评论成功',
'data' => $this->formatComment($comment),
]);
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
} catch (\Exception $e) {
return json([
'code' => 500,
'message' => '评论失败:' . $e->getMessage(),
'data' => null,
]);
}
}
/**
* 点赞动态
* @return Json
*/
public function like(): Json
{
try {
$infoId = Request::post('dynamic_id', 0);
if (! $infoId) {
return json([
'code' => 400,
'message' => '动态ID不能为空',
'data' => null,
]);
}
// 检查动态是否存在
$info = MomentModel::where('id', $infoId)
->where('status', 1)
->find();
if (! $info) {
return json([
'code' => 404,
'message' => '动态不存在',
'data' => null,
]);
}
// 检查是否已点赞
$like = DynamicLikeModel::where('dynamic_id', $infoId)
->where('user_id', $this->uid)
->find();
if ($like) {
// 取消点赞
$like->delete();
$info->like_count = Db::raw('like_count - 1');
$info->save();
$liked = false;
$message = '取消点赞成功';
} else {
// 点赞
$like = new DynamicLikeModel();
$like->dynamic_id = $infoId;
$like->user_id = $this->uid;
$like->save();
$info->like_count = Db::raw('like_count + 1');
$info->save();
$liked = true;
$message = '点赞成功';
}
return json([
'code' => 200,
'message' => $message,
'data' => [
'liked' => $liked,
'like_count' => $info->like_count,
],
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'message' => '操作失败:' . $e->getMessage(),
'data' => null,
]);
}
}
/**
* 收藏动态
* @return Json
*/
public function collect(): Json
{
try {
$infoId = Request::post('dynamic_id', 0);
$folderId = Request::post('folder_id', 0);
if (! $infoId) {
return json([
'code' => 400,
'message' => '动态ID不能为空',
'data' => null,
]);
}
// 检查动态是否存在
$info = MomentModel::where('id', $infoId)
->where('status', 1)
->find();
if (! $info) {
return json([
'code' => 404,
'message' => '动态不存在',
'data' => null,
]);
}
// 检查是否已收藏
$collect = DynamicCollectModel::where('dynamic_id', $infoId)
->where('user_id', $this->uid)
->find();
if ($collect) {
// 取消收藏
$collect->delete();
$info->collect_count = Db::raw('collect_count - 1');
$info->save();
$collected = false;
$message = '取消收藏成功';
} else {
// 收藏
$collect = new DynamicCollectModel();
$collect->dynamic_id = $infoId;
$collect->user_id = $this->uid;
$collect->folder_id = $folderId;
$collect->save();
$info->collect_count = Db::raw('collect_count + 1');
$info->save();
$collected = true;
$message = '收藏成功';
}
return json([
'code' => 200,
'message' => $message,
'data' => [
'collected' => $collected,
'collect_count' => $info->collect_count,
],
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'message' => '操作失败:' . $e->getMessage(),
'data' => null,
]);
}
}
/**
* 关注用户
* @return Json
*/
public function follow(): Json
{
try {
$followingId = Request::post('following_id', 0);
if (! $followingId) {
return json([
'code' => 400,
'message' => '用户ID不能为空',
'data' => null,
]);
}
if ($this->uid == $followingId) {
return json([
'code' => 400,
'message' => '不能关注自己',
'data' => null,
]);
}
// 检查用户是否存在
$user = UserModel::where('id', $followingId)
->where('status', 1)
->find();
if (! $user) {
return json([
'code' => 404,
'message' => '用户不存在',
'data' => null,
]);
}
// 检查是否已关注
$follow = UserFollowModel::where('follower_id', $this->uid)
->where('following_id', $followingId)
->find();
if ($follow) {
// 取消关注
$follow->delete();
// 更新用户关注数
UserModel::where('id', $this->uid)
->dec('following_count')
->update();
UserModel::where('id', $followingId)
->dec('follower_count')
->update();
$followed = false;
$message = '取消关注成功';
} else {
// 关注
$follow = new UserFollowModel();
$follow->follower_id = $this->uid;
$follow->following_id = $followingId;
$follow->save();
// 更新用户关注数
UserModel::where('id', $this->uid)
->inc('following_count')
->update();
UserModel::where('id', $followingId)
->inc('follower_count')
->update();
$followed = true;
$message = '关注成功';
}
return json([
'code' => 200,
'message' => $message,
'data' => [
'followed' => $followed,
],
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'message' => '操作失败:' . $e->getMessage(),
'data' => null,
]);
}
}
/**
* 获取评论列表
* @param int $infoId
* @param int $page
* @param int $limit
* @param int $this->uid
* @return array
*/
private function getComments(int $infoId, int $page, int $limit, int $uid = 0)
{
// 查询顶级评论
$query = CommentModel::with([
'member' =>
function ($query) {
$query->field('id,username,nickname,avatar');
},
'replyToUser' =>
function ($query) {
$query->field('id,username,nickname');
},
])->where('dynamic_id', $infoId)
->where('parent_id', 0)
->where('status', 1)
->order('is_top', 'desc')
->order('create_at', 'desc');
$total = $query->count();
$pages = ceil($total / $limit);
$comments = $query->page($page, $limit)->select();
$formattedComments = [];
foreach ($comments as $comment) {
$formattedComment = $this->formatComment($comment);
// 获取评论的回复
$replies = CommentModel::with([
'member' =>
function ($query) {
$query->field('id,username,nickname,avatar');
},
'replyToUser' =>
function ($query) {
$query->field('id,username,nickname');
},
])->where('parent_id', $comment->id)
->where('status', 1)
->order('create_at', 'asc')
->limit(5) // 只获取前5条回复
->select();
$formattedComment['replies'] = [];
foreach ($replies as $reply) {
$formattedComment['replies'][] = $this->formatComment($reply);
}
$formattedComments[] = $formattedComment;
}
return [
'list' => $formattedComments,
'total' => $total,
'pages' => $pages,
];
}
/**
* 增加浏览量
* @param int $infoId
* @param int $userId
*/
private function increaseViewCount(int $infoId, int $userId = 0): void
{
try {
// 更新动态浏览量
MomentModel::where('id', $infoId)
->inc('view_count')
->update();
// 记录浏览历史
$viewHistory = new ViewHistoryModel();
$viewHistory->dynamic_id = $infoId;
$viewHistory->user_id = $userId ?: null;
$viewHistory->ip_address = Request::ip();
$viewHistory->user_agent = Request::header('user-agent');
$viewHistory->save();
} catch (\Exception $e) {
// 浏览记录失败不影响主流程
}
}
/**
* 获取用户与动态的交互状态
* @param int $infoId
* @param int $userId
* @return array
*/
private function getUserInteraction(int $infoId, int $userId): array
{
if (! $userId) {
return [
'liked' => false,
'collected' => false,
'followed' => false,
];
}
$liked = DynamicLikeModel::where('dynamic_id', $infoId)
->where('user_id', $userId)
->find() ? true : false;
$collected = DynamicCollectModel::where('dynamic_id', $infoId)
->where('user_id', $userId)
->find() ? true : false;
// 获取动态作者ID
$info = MomentModel::where('id', $infoId)->find();
$followed = false;
if ($info && $info->user_id != $userId) {
$followed = UserFollowModel::where('follower_id', $userId)
->where('following_id', $info->user_id)
->find() ? true : false;
}
return [
'liked' => $liked,
'collected' => $collected,
'followed' => $followed,
];
}
/**
* 格式化动态数据
* @param MomentModel $info
* @param array $interaction
* @return array
*/
private function formatDynamic(MomentModel $info, array $interaction): array
{
$user = $info->user;
$media = [];
$images = [];
$videoUrl = '';
foreach ($info->media as $m) {
$mType = ($m->type == 2 || $m->type == 'video') ? 'video' : 'image';
$mItem = [
'type' => $mType,
'url' => $m->url,
'thumbnail' => $m->thumbnail_url,
];
$media[] = $mItem;
if ($mType == 'image') {
$images[] = $m->url;
} elseif ($videoUrl === '') {
$videoUrl = $m->url;
}
}
$type = 'text';
if ($videoUrl !== '') {
$type = 'video';
} elseif (count($images) > 0) {
$type = 'image';
}
$hashtags = [];
foreach ($info->hashtags as $h) {
$hashtags[] = ['id' => $h->id, 'name' => $h->name];
}
$isLiked = ! empty($interaction['liked']);
$isCollect = ! empty($interaction['collected']);
$isFollow = ! empty($interaction['followed']);
return [
'id' => $info->id,
'uid' => $info->uid,
'userId' => $user ? $user->id : 0,
'userName' => $user ? $user->nickname : '',
'userAvatar' => $user ? $user->avatar : '',
'userVip' => false,
'content' => $info->content,
'images' => $images,
'videoUrl' => $videoUrl,
'type' => $type,
'location' => $info->location ?: '',
'time' => (int) $info->create_at * 1000,
'likes' => (int) $info->like_count,
'comments' => (int) $info->comment_count,
'shares' => (int) $info->share_count,
'isLiked' => $isLiked,
'isPinned' => false,
'tags' => array_map(function ($h) { return $h['name']; }, $hashtags),
'hot' => (int) $info->like_count >= 50,
'distance' => 0,
// 详情页扩展字段
'media' => $media,
'hashtags' => $hashtags,
'stats' => [
'like_count' => (int) $info->like_count,
'comment_count' => (int) $info->comment_count,
'share_count' => (int) $info->share_count,
'collect_count' => (int) $info->collect_count,
'view_count' => (int) $info->view_count,
],
'interaction' => [
'liked' => $isLiked,
'collected' => $isCollect,
'followed' => $isFollow,
],
'view' => (int) $info->view_count,
];
}
/**
* 格式化评论数据
* @param CommentModel $comment
* @return array
*/
private function formatComment(CommentModel $comment): array
{
$replyTo = null;
if ($comment->reply_to_user_id && $comment->replyToUser) {
$replyTo = [
'id' => $comment->replyToUser->id,
'username' => $comment->replyToUser->username,
'nickname' => $comment->replyToUser->nickname,
];
}
return [
'id' => $comment->id,
'member' => [
'id' => $comment->user->id,
'username' => $comment->user->username,
'nickname' => $comment->user->nickname,
'avatar' => $comment->user->avatar,
],
'reply_to' => $replyTo,
'content' => $comment->content,
'like_count' => $comment->like_count,
'reply_count' => $comment->reply_count,
'is_top' => $comment->is_top,
'create_time' => (int) $comment->create_at,
'create_time_formatted' => date('Y-m-d H:i:s', (int) $comment->create_at),
];
}
public function upload()
{
$request = $this->request;
// 记录请求的详细信息
Log::record('请求方法: ' . $request->method());
Log::record('请求URL: ' . $request->url());
Log::record('请求参数: ' . json_encode($request->param()));
Log::record('请求头信息: ' . json_encode($request->header()));
Log::record('请求头信息: ' . json_encode($request->file()));
Log::record('请求体信息: ' . json_encode($request->post())); // 对于POST请求
Log::record('请求IP: ' . $request->ip());
Log::record('请求时间: ' . date('Y-m-d H:i:s'));
// 获取表单上传文件
$files = request()->file('file');
// $savename = \think\facade\Filesystem::disk('local')->putFile( 'topic', $files);
$savename = \think\facade\Filesystem::putFile('topic', $files);
$disk =\think\facade\Filesystem::getDefaultDriver();
$disk = \think\facade\Filesystem::getDiskConfig($disk) ;
$this->success([
'fid' => 0,
'name' => $files->getOriginalName(),
'url' =>$disk['url'] . DIRECTORY_SEPARATOR . $savename,
], '上传成功');
}
}