1. 新增视频图标及上传示例视频文件 2. 调整开发环境后端域名 3. 新增结构化媒体列表功能,区分图片与视频 4. 新增短视频弹窗预览功能,支持在审核和详情页播放 5. 调整图片和视频文件大小限制,放宽至15MB和100MB 6. 优化详情页,支持视频播放、图片视频混合画廊展示 7. 优化后台审核和详情页面,新增短视频质检模块
606 行
21 KiB
PHP
606 行
21 KiB
PHP
<?php
|
||
|
||
namespace app\admin\controller\fc;
|
||
|
||
use app\admin\model\AdminLog;
|
||
use app\common\controller\Backend;
|
||
use think\Db;
|
||
|
||
/**
|
||
* 同城-信息
|
||
*
|
||
* @icon fa fa-circle-o
|
||
*/
|
||
class Post extends Backend
|
||
{
|
||
|
||
/**
|
||
* FcPost模型对象
|
||
* @var \app\admin\model\FcPost
|
||
*/
|
||
protected $model = null;
|
||
|
||
/**
|
||
* 无需鉴权的方法(只读辅助接口;审核/置顶/下架/上架必须走权限节点)
|
||
* @var array
|
||
*/
|
||
protected $noNeedRight = ['detail', 'queue', 'counts'];
|
||
|
||
public function _initialize()
|
||
{
|
||
parent::_initialize();
|
||
$this->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));
|
||
}
|
||
|
||
|
||
}
|