model = new \app\admin\model\FcPost; } /** * 查看信息列表 * * 支持置顶优先排序与通用条件筛选,去除顶部多余 Tab 依赖 * * @return string|\think\response\Json * @throws \think\Exception * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\ModelNotFoundException * @throws \think\exception\DbException */ public function index() { // 设置过滤方法 $this->request->filter(['strip_tags', 'trim']); if (false === $this->request->isAjax()) { return $this->view->fetch(); } // 如果发送的来源是 Selectpage,则转发到 Selectpage if ($this->request->request('keyField')) { return $this->selectpage(); } [$where, $sort, $order, $offset, $limit] = $this->buildparams(); // 默认按 ID 倒序时,优先将置顶记录排在前面 $orderRaw = ($sort === 'id' && strtolower($order) === 'desc') ? 'is_top DESC, id DESC' : "{$sort} {$order}"; $list = $this->model ->where($where) ->orderRaw($orderRaw) ->paginate($limit); $result = ['total' => $list->total(), 'rows' => $list->items()]; return json($result); } /** * 顶部标签数量(只统计未删除的信息) * * @return array */ protected function tabCounts() { $row = Db::name('fc_post')->whereNull('deletetime')->field( 'COUNT(*) AS total,' . 'SUM(audit_status = 0) AS pending,' . 'SUM(audit_status = 1) AS passed,' . 'SUM(audit_status = 2) AS rejected,' . 'SUM(status = 0) AS offline' )->find(); return [ 'all' => isset($row['total']) ? (int)$row['total'] : 0, 'pending' => isset($row['pending']) ? (int)$row['pending'] : 0, 'passed' => isset($row['passed']) ? (int)$row['passed'] : 0, 'rejected' => isset($row['rejected']) ? (int)$row['rejected'] : 0, 'offline' => isset($row['offline']) ? (int)$row['offline'] : 0, ]; } /** * 标签数量(供前端增量刷新,避免整页重载) */ public function counts() { $this->success('', null, $this->tabCounts()); } /** * 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法 * 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑 * 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改 */ /** * 审核信息 */ /** * 查看信息详情或进入审核工作台 * * @param int|null $ids 信息ID * @return string|\think\response\Json */ public function detail($ids = null) { $id = $this->request->param('ids', 0) ?: $this->request->param('id/d', 0); if (!$id) { $this->error('缺少参数ID'); } $row = $this->model->where('id', $id)->find(); if (!$row) { $this->error('信息不存在或已删除'); } // 补齐分类名称 if ($row->category_id) { $category = Db::name('fc_category')->where('id', $row->category_id)->find(); $row->category_name = $category ? $category['name'] : '未分类'; } else { $row->category_name = '未分类'; } // 解析多图相册 $mediaList = []; $rawMedia = $row->getData('media'); if (!empty($rawMedia)) { $decoded = is_string($rawMedia) ? json_decode($rawMedia, true) : $rawMedia; if (is_array($decoded)) { $mediaList = array_values(array_filter($decoded)); } else { $mediaList = array_values(array_filter(explode(',', (string)$rawMedia))); } } if (empty($mediaList) && !empty($row->cover)) { $mediaList[] = $row->cover; } // 结构化媒体列表(识别图片与短视频) $mediaItems = []; foreach ($mediaList as $m) { $url = (string)$m; $isVideo = (bool)preg_match('/\.(mp4|mov|avi|webm|mkv)(\?|$)/i', $url); $mediaItems[] = [ 'url' => $url, 'is_video' => $isVideo, ]; } // 获取用户画像与关联商家门店资料 $userInfo = [ 'id' => $row->user_id, 'nickname' => $row->user_name ?: '未知用户', 'mobile' => '', 'avatar' => '/assets/img/avatar.png', 'post_count' => 0 ]; $user = Db::name('user')->where('id', $row->user_id)->field('id,nickname,mobile,avatar')->find(); if ($user) { $userInfo['nickname'] = $user['nickname'] ?: $userInfo['nickname']; $userInfo['mobile'] = $user['mobile'] ?: ''; $userInfo['avatar'] = $user['avatar'] ?: $userInfo['avatar']; $userInfo['post_count'] = Db::name('fc_post')->where('user_id', $user['id'])->count(); } $merchantInfo = null; if (!empty($row->user_id)) { $merchantInfo = Db::name('fc_merchant')->where('user_id', $row->user_id)->find(); } // 支持请求 JSON 详情 if ($this->request->param('format') === 'json') { $data = [ 'id' => $row->id, 'title' => $row->title, 'content' => $row->content, 'category_name' => $row->category_name, 'identity_type' => isset($row->identity_type) && (int)$row->identity_type === 2 ? '商家' : '个人', 'price' => $row->price, 'price_unit' => $row->price_unit, 'area' => $row->area, 'address' => $row->address, 'contact' => $row->contact, 'phone' => $row->phone, 'wechat' => $row->wechat, 'tags' => $row->tags ? explode(',', $row->tags) : [], 'is_paid' => $row->is_paid, 'amount' => $row->amount, 'pay_type' => $row->pay_type, 'audit_status' => $row->audit_status, 'audit_reason' => $row->audit_reason, 'status' => $row->status, 'is_top' => $row->is_top, 'views' => $row->views, 'likes' => $row->likes, 'createtime_text' => date('Y-m-d H:i:s', $row->createtime), 'media_list' => $mediaList, 'user' => $userInfo, 'merchant' => $merchantInfo ]; $this->success('获取成功', null, $data); } // 区分模式:audit=专属审核界面,view=纯净详情档案界面(信息列表默认只读详情,审核由信息审核菜单承接) $mode = $this->request->param('mode', 'view'); $this->view->assign('mode', $mode); $this->view->assign('row', $row); $this->view->assign('userInfo', $userInfo); $this->view->assign('merchantInfo', $merchantInfo); $this->view->assign('mediaList', $mediaList); $this->view->assign('mediaItems', $mediaItems); $this->view->assign('queue', $this->auditQueue($row->getData())); // 如果是审核模式渲染 audit.html,详情模式渲染 detail.html if ($mode === 'audit') { return $this->view->fetch('audit'); } return $this->view->fetch('detail'); } public function audit() { $ids = $this->request->post('ids'); $auditStatus = (int)$this->request->post('auditStatus'); $reason = trim((string)$this->request->post('reason', '')); if (!in_array($auditStatus, [1, 2], true)) { $this->error('审核状态仅支持通过(1)/驳回(2)'); } $ids = $this->normalizeIds($ids); if (!$ids) { $this->error('请选择要审核的信息'); } if ($auditStatus === 2 && $reason === '') { $this->error('请填写驳回原因'); } if (mb_strlen($reason, 'utf-8') > 200) { $reason = mb_substr($reason, 0, 200, 'utf-8'); } $posts = Db::name('fc_post')->where('id', 'in', $ids)->whereNull('deletetime')->select(); if (!$posts) { $this->error('信息不存在或已删除'); } $changed = 0; $skipped = 0; Db::startTrans(); try { foreach ($posts as $post) { $from = (int)$post['audit_status']; // 已是目标状态:不再重复更新、不再重复计数、不再重复发通知 if ($from === $auditStatus) { $skipped++; continue; } Db::name('fc_post')->where('id', $post['id'])->update([ 'audit_status' => $auditStatus, 'audit_reason' => $reason, 'updatetime' => time(), ]); // 分类信息量只在「非通过 → 通过」时 +1,「通过 → 驳回」时 -1 if (!empty($post['category_id'])) { if ($auditStatus === 1 && $from !== 1) { Db::name('fc_category')->where('id', $post['category_id'])->setInc('post_count'); } elseif ($auditStatus === 2 && $from === 1) { Db::name('fc_category') ->where('id', $post['category_id']) ->where('post_count', '>', 0) ->setDec('post_count'); } } Db::name('fc_message')->insert([ 'user_id' => $post['user_id'], 'type' => 'system', 'title' => $auditStatus === 1 ? '审核通过' : '审核驳回', 'content' => $auditStatus === 1 ? '您发布的信息「' . $post['title'] . '」已审核通过' : '您发布的信息「' . $post['title'] . '」审核未通过,原因:' . $reason, 'isread' => 0, 'createtime' => time(), ]); $changed++; } Db::commit(); } catch (\Exception $e) { Db::rollback(); $this->error('审核失败,请稍后重试'); } AdminLog::setTitle('审核信息 #' . implode(',', $ids) . ':' . ($auditStatus === 1 ? '通过' : '驳回') . ($reason ? '(' . $reason . ')' : '')); $msg = ($auditStatus === 1 ? '已通过 ' : '已驳回 ') . $changed . ' 条'; if ($skipped) { $msg .= ',跳过 ' . $skipped . ' 条(已是该状态)'; } $this->success($msg); } /** * 置顶 / 取消置顶 * POST fc/post/top {ids:'1,2', isTop:1|0} * 说明:置顶信息在用户端主页与列表排在非置顶之前(Home::feed / Post::mine 均按 is_top desc 排序) */ public function top() { $ids = $this->normalizeIds($this->request->post('ids')); $isTop = (int)$this->request->post('isTop', 1); if (!in_array($isTop, [0, 1], true)) { $this->error('置顶状态仅支持置顶(1)/取消置顶(0)'); } if (!$ids) { $this->error('请选择要操作的信息'); } $posts = Db::name('fc_post')->where('id', 'in', $ids)->whereNull('deletetime')->select(); if (!$posts) { $this->error('信息不存在或已删除'); } $changed = 0; $skipped = 0; $blocked = 0; Db::startTrans(); try { foreach ($posts as $post) { if ((int)$post['is_top'] === $isTop) { $skipped++; continue; } // 只有已通过且已上架的信息才允许置顶,避免用户收到「已置顶」但实际看不到 if ($isTop === 1 && ((int)$post['audit_status'] !== 1 || (int)$post['status'] !== 1)) { $blocked++; continue; } Db::name('fc_post')->where('id', $post['id'])->update([ 'is_top' => $isTop, 'updatetime' => time(), ]); Db::name('fc_message')->insert([ 'user_id' => $post['user_id'], 'type' => 'system', 'title' => $isTop === 1 ? '信息已置顶' : '已取消置顶', 'content' => $isTop === 1 ? '您发布的信息「' . $post['title'] . '」已被置顶,将在首页优先展示' : '您发布的信息「' . $post['title'] . '」已取消置顶', 'isread' => 0, 'createtime' => time(), ]); $changed++; } Db::commit(); } catch (\Exception $e) { Db::rollback(); $this->error('操作失败,请稍后重试'); } if (!$changed) { if ($blocked) { $this->error('仅「已通过且已上架」的信息可以置顶'); } $this->error($isTop === 1 ? '所选信息已全部置顶' : '所选信息均未置顶'); } AdminLog::setTitle('信息置顶 #' . implode(',', $ids) . ':' . ($isTop === 1 ? '置顶' : '取消置顶')); $msg = ($isTop === 1 ? '置顶成功 ' : '已取消置顶 ') . $changed . ' 条'; if ($skipped) { $msg .= ',跳过 ' . $skipped . ' 条'; } if ($blocked) { $msg .= ',' . $blocked . ' 条未通过审核或已下架未处理'; } $this->success($msg); } /** * 下架信息(必须填写原因) * POST fc/post/offline {ids:'1,2', reason:'违规内容'} */ public function offline() { $ids = $this->normalizeIds($this->request->post('ids')); $reason = trim((string)$this->request->post('reason', '')); if (!$ids) { $this->error('请选择要下架的信息'); } if ($reason === '') { $this->error('请填写下架原因'); } if (mb_strlen($reason, 'utf-8') > 200) { $reason = mb_substr($reason, 0, 200, 'utf-8'); } $posts = Db::name('fc_post')->where('id', 'in', $ids)->whereNull('deletetime')->select(); if (!$posts) { $this->error('信息不存在或已删除'); } $changed = 0; $skipped = 0; $blocked = 0; Db::startTrans(); try { foreach ($posts as $post) { if ((int)$post['status'] === 0) { $skipped++; continue; } if ((int)$post['audit_status'] !== 1) { $blocked++; continue; } Db::name('fc_post')->where('id', $post['id'])->update([ 'status' => 0, 'is_top' => 0, 'updatetime' => time(), ]); Db::name('fc_message')->insert([ 'user_id' => $post['user_id'], 'type' => 'system', 'title' => '信息已下架', 'content' => '您发布的信息「' . $post['title'] . '」已被下架,原因:' . $reason, 'isread' => 0, 'createtime' => time(), ]); $changed++; } Db::commit(); } catch (\Exception $e) { Db::rollback(); $this->error('下架失败,请稍后重试'); } if (!$changed) { if ($blocked) { $this->error('仅「已通过」的信息可以下架'); } $this->error('所选信息已是下架状态'); } AdminLog::setTitle('下架信息 #' . implode(',', $ids) . '(' . $reason . ')'); $msg = '下架成功 ' . $changed . ' 条'; if ($skipped) { $msg .= ',跳过 ' . $skipped . ' 条(已是下架状态)'; } if ($blocked) { $msg .= ',' . $blocked . ' 条未通过审核未处理'; } $this->success($msg); } /** * 恢复上架 * POST fc/post/online {ids:'1,2'} */ public function online() { $ids = $this->normalizeIds($this->request->post('ids')); if (!$ids) { $this->error('请选择要上架的信息'); } $posts = Db::name('fc_post')->where('id', 'in', $ids)->whereNull('deletetime')->select(); if (!$posts) { $this->error('信息不存在或已删除'); } $changed = 0; $skipped = 0; $blocked = 0; Db::startTrans(); try { foreach ($posts as $post) { if ((int)$post['status'] === 1) { $skipped++; continue; } if ((int)$post['audit_status'] !== 1) { $blocked++; continue; } Db::name('fc_post')->where('id', $post['id'])->update([ 'status' => 1, 'updatetime' => time(), ]); Db::name('fc_message')->insert([ 'user_id' => $post['user_id'], 'type' => 'system', 'title' => '信息已恢复上架', 'content' => '您发布的信息「' . $post['title'] . '」已恢复上架', 'isread' => 0, 'createtime' => time(), ]); $changed++; } Db::commit(); } catch (\Exception $e) { Db::rollback(); $this->error('上架失败,请稍后重试'); } if (!$changed) { if ($blocked) { $this->error('仅「已通过」的信息可以上架'); } $this->error('所选信息已是上架状态'); } AdminLog::setTitle('恢复上架 #' . implode(',', $ids)); $msg = '已恢复上架 ' . $changed . ' 条'; if ($skipped) { $msg .= ',跳过 ' . $skipped . ' 条(已在上架状态)'; } $this->success($msg); } /** * 审核队列位置(供审核工作台显示「第 x / y 条」和上/下一条) * GET fc/post/queue?id=123 */ public function queue() { $id = (int)$this->request->param('id', 0); $row = Db::name('fc_post')->where('id', $id)->whereNull('deletetime')->find(); if (!$row) { $this->error('信息不存在或已删除'); } $this->success('', null, $this->auditQueue($row)); } /** * 待审核队列按「提交时间正序」排列,与小程序审核队列保持一致 * * @param array $row * @return array */ protected function auditQueue(array $row) { $base = function () { return Db::name('fc_post')->where('audit_status', 0)->whereNull('deletetime'); }; $total = (int)$base()->count(); if (empty($row['id'])) { return ['index' => 0, 'total' => $total, 'prev' => 0, 'next' => 0]; } $createtime = isset($row['createtime']) ? (int)$row['createtime'] : 0; $id = (int)$row['id']; $index = (int)$base() ->whereRaw("createtime < {$createtime} OR (createtime = {$createtime} AND id <= {$id})") ->count(); $prev = (int)$base() ->whereRaw("createtime < {$createtime} OR (createtime = {$createtime} AND id < {$id})") ->order('createtime desc, id desc') ->value('id'); $next = (int)$base() ->whereRaw("createtime > {$createtime} OR (createtime = {$createtime} AND id > {$id})") ->order('createtime asc, id asc') ->value('id'); return ['index' => $index, 'total' => $total, 'prev' => $prev, 'next' => $next]; } /** * 统一整理 ids 入参(支持数组 / 逗号字符串),去重并转成整数 * * @param mixed $ids * @return array */ protected function normalizeIds($ids) { $ids = is_array($ids) ? $ids : explode(',', (string)$ids); $ids = array_map('intval', $ids); $ids = array_filter($ids); return array_values(array_unique($ids)); } }