attachAuthors($topics); $this->attachAuthors($hot); // 当前会员未读消息 $unread = 0; if ($member = $this->member()) { $unread = ForumMessage::unreadCount((int) $member['uid']); } $this->assign([ 'boards' => $boards, 'topics' => $topics, 'hot' => $hot, 'unread' => $unread, 'typeMap'=> ForumTopic::$typeMap, ]); return $this->fetch('index/index'); } /** * 版块帖子列表 */ public function board($slug = '') { $board = ForumBoard::findBySlug($slug); if (!$board) { $this->assign('msg', '版块不存在'); return $this->fetch('index/notfound'); } $type = $this->request->param('type', ''); $order = $this->request->param('order', 'new'); $page = (int) $this->request->param('page', 1); $topics = ForumTopic::listFront((int) $board->id, $type, $order, $page, 20); $this->attachAuthors($topics); $this->assign([ 'board' => $board, 'topics' => $topics, 'type' => $type, 'order' => $order, 'typeMap'=> ForumTopic::$typeMap, ]); return $this->fetch('index/board'); } /** * 帖子详情 + 回复列表 */ public function topic($id = 0) { $topic = ForumTopic::findById((int) $id); if (!$topic || (int) $topic->status !== 1) { $this->assign('msg', '帖子不存在、待审核或已下架'); return $this->fetch('index/notfound'); } ForumTopic::addViews((int) $topic->id); $replies = ForumReply::listByTopic((int) $topic->id); $board = ForumBoard::find((int) $topic->board_id); // 作者信息(复用会员表),带默认值避免模板内联查库 $author = Db::name('member')->where('id', $topic->user_id) ->field('id,nickname,avatar,intro')->find(); if (!$author) { $author = ['id' => $topic->user_id, 'nickname' => '匿名', 'avatar' => '', 'intro' => '']; } // 每条回复附带用户信息 $uids = array_column($replies->toArray(), 'user_id'); $users = []; if (!empty($uids)) { $users = Db::name('member')->where('id', 'in', array_unique($uids)) ->column('nickname,avatar', 'id'); } $replyList = []; foreach ($replies as $rp) { $u = $users[$rp['user_id']] ?? ['nickname' => '匿名', 'avatar' => '']; $replyList[] = [ 'id' => $rp['id'], 'user_id' => $rp['user_id'], 'floor' => $rp['floor'], 'content' => $rp['content'], 'create_at' => $rp['create_at'], 'nickname' => $u['nickname'] ?: '匿名', 'avatar' => $u['avatar'] ?: '', ]; } $this->assign([ 'topic' => $topic, 'author' => $author, 'replyList' => $replyList, 'board' => $board, 'related' => \addon\forum\service\ForumSearchService::related((int) $topic->id, 6), 'typeName' => ForumTopic::$typeMap[$topic->type] ?? '', ]); return $this->fetch('index/topic'); } /** * 发帖页 */ public function post() { if (!$this->requireLogin()) { return redirect('/user/login'); } $boards = ForumBoard::enabledList(); $this->assign('boards', $boards); $this->assign('typeMap', ForumTopic::$typeMap); $cfg = \ywxapp\service\AddonService::config('forum'); $this->assign('editorHeight', (int) ($cfg['editor_height'] ?? 360)); return $this->fetch('index/post'); } /** * 提交发帖 */ public function doPost() { if (!$this->requireLogin()) { return $this->error('请先登录'); } $member = $this->member(); $data = $this->request->post(); $title = trim((string) ($data['title'] ?? '')); $content = trim((string) ($data['content'] ?? '')); $boardId = (int) ($data['board_id'] ?? 0); $type = (int) ($data['type'] ?? 0); $lat = (float) ($data['lat'] ?? 0); $lng = (float) ($data['lng'] ?? 0); if ($title === '' || $content === '' || $boardId < 1) { return $this->error('请填写完整信息'); } if (!isset(ForumTopic::$typeMap[$type])) { $type = ForumTopic::TYPE_ASK; } $needAudit = (int) config('forum.need_audit'); $status = $needAudit ? 0 : 1; // 0待审 1正常 $auditReason = ''; // 机审(云审):开启 need_audit 且无 AI 审核服务时,按策略直接通过/转人工 if ($needAudit) { $ai = new \addon\wxchat\service\AiAuditService(); $res = $ai->audit($title . "\n" . $content); if ($res['code'] === 0) { if (!empty($res['data']['blocked'])) { // 机审命中违规:直接驳回,不进入人工队列 $status = -1; $auditReason = '机审不通过:' . ($res['data']['reason'] ?? '内容违规'); } // 机审通过:保持待审(status=0),等待人工复核 } // AI 服务不可用(code!=0 且非 blocked):维持待审,转人工队列 } \addon\forum\library\ForumSchema::ensure(); $topicId = ForumTopic::insertGetId([ 'board_id' => $boardId, 'user_id' => $member['id'], 'title' => $title, 'content' => $content, 'type' => $type, 'status' => $status, 'lat' => $lat, 'lng' => $lng, 'audit_reason' => $auditReason, 'create_at' => time(), 'update_at' => time(), ]); ForumBoard::incr($boardId, 'topic_count', 1); \addon\forum\service\ForumSearchService::sync($topicId); if ($status === -1) { return $this->success(['id' => $topicId], $auditReason); } return $this->success(['id' => $topicId], $needAudit ? '发布成功,等待审核' : '发布成功'); } /** * 提交回复 */ public function doReply() { if (!$this->requireLogin()) { return $this->error('请先登录'); } $member = $this->member(); $data = $this->request->post(); $topicId = (int) ($data['topic_id'] ?? 0); $content = trim((string) ($data['content'] ?? '')); if ($topicId < 1 || $content === '') { return $this->error('回复内容不能为空'); } $topic = ForumTopic::findById($topicId); if (!$topic) { return $this->error('帖子不存在'); } $floor = ForumReply::countByTopic($topicId) + 1; $replyId = ForumReply::insertGetId([ 'topic_id' => $topicId, 'user_id' => $member['id'], 'content' => $content, 'floor' => $floor, 'status' => 1, 'create_at' => time(), 'update_at' => time(), ]); // 更新帖子回复计数 + 最后回复 ForumTopic::where('id', $topicId)->inc('reply_count', 1)->update([ 'last_reply_id' => $replyId, 'last_reply_uid' => $member['id'], 'last_reply_time' => time(), ]); ForumBoard::incr((int) $topic->board_id, 'reply_count', 1); // 给帖子作者发消息(非自己) if ($topic->user_id != $member['id']) { ForumMessage::insert([ 'user_id' => $topic->user_id, 'from_uid' => $member['id'], 'type' => 'reply', 'topic_id' => $topicId, 'reply_id' => $replyId, 'content' => mb_substr(strip_tags($content), 0, 120), 'is_read' => 0, 'create_at' => time(), ]); } return $this->success(['floor' => $floor], '回复成功'); } /** * 搜索 */ public function search() { $kw = trim((string) $this->request->param('q', '')); $list = []; if ($kw !== '') { $list = \addon\forum\service\ForumSearchService::search($kw, (int) $this->request->param('page', 1), 20); } $this->assign('kw', $kw); $this->assign('list', $list); return $this->fetch('index/search'); } /** * 附近帖子(GEO/LBS):由前端定位传入 lat/lng,展示半径内帖子 */ public function nearby() { $lat = (float) $this->request->param('lat', 0); $lng = (float) $this->request->param('lng', 0); $radius = (float) $this->request->param('radius', 10); $list = \addon\forum\service\ForumSearchService::nearby($lat, $lng, $radius, 30); $this->assign('list', $list); $this->assign('lat', $lat); $this->assign('lng', $lng); $this->assign('radius', $radius); return $this->fetch('index/nearby'); } /** * 批量给帖子列表(Paginator)注入 author 字段,避免模板读取不存在的关联属性 * @param \think\Paginator $paginator */ private function attachAuthors($collection) { if (!is_iterable($collection)) { return; } $uids = []; foreach ($collection as $t) { $uids[] = $t['user_id'] ?? ($t->user_id ?? 0); } $uids = array_filter(array_unique($uids)); $users = []; if (!empty($uids)) { $users = Db::name('member')->where('id', 'in', $uids) ->column('nickname,avatar', 'id'); } foreach ($collection as $t) { $uid = $t['user_id'] ?? ($t->user_id ?? 0); $u = $users[$uid] ?? ['nickname' => '匿名', 'avatar' => '']; $t->author = [ 'nickname' => $u['nickname'] ?: '匿名', 'avatar' => $u['avatar'] ?: '', ]; } } /** * 用户主页(展示其论坛数据) */ public function user($id = 0) { $user = Db::name('member')->where('id', (int) $id)->field('id,nickname,avatar,intro')->find(); if (!$user) { $this->assign('msg', '用户不存在'); return $this->fetch('index/notfound'); } $topics = ForumTopic::where('user_id', (int) $id) ->where('status', 1) ->order('id desc') ->limit(20)->select(); $this->assign('member', $user); $this->assign('topics', $topics); return $this->fetch('index/user'); } }