field(ArticleModel::listFields()); if ($status !== 'all') { $query->where('status', $status); } if ($keyword) { $query->where('title', 'like', "%{$keyword}%"); } $paginator = $query->order('is_top', 'desc') ->order('create_at', 'desc') ->paginate(['list_rows' => $limit, 'page' => $page]); $rows = []; foreach ($paginator->items() as $a) { $rows[] = [ 'id' => $a->id, 'title' => $a->title, 'is_top' => $a->is_top, 'is_recommend'=> $a->is_recommend, 'category' => $a->category->name ?? '-', 'status' => $a->status, 'view_count' => (int) $a->view_count, 'comment_count' => (int) $a->comment_count, 'create_at' => $a->create_at, 'edit_url' => (string) url('articles/backend/article/edit', ['id' => $a->id]), ]; } // ↓↓↓ 临时诊断标记(确认 Web 实际加载的是不是本文件 + 真实 SQL),定位后移除 $diag = [ 'mtime' => date('Y-m-d H:i:s', filemtime(__FILE__)), 'sql' => (string) ArticleModel::fetchSql(true)->order('id', 'desc')->limit(1)->select(), ]; // ↑↑↑ 临时诊断标记 return json(['code' => 0, 'msg' => '', 'count' => $paginator->total(), 'data' => $rows, '__diag' => $diag]); } return View::fetch('article/index', ['status' => $status, 'keyword' => $keyword]); } public function save() { if (Request::isPost()) { return $this->update(0); } $categories = ArticleCategory::where('delete_at', 0)->where('status', 1)->select(); $tags = ArticleTag::where('delete_at', 0)->select(); return View::fetch('article/edit', [ 'article' => null, 'categories' => $categories, 'tags' => $tags, 'articleTags' => [], 'tagNamesStr' => '', ]); } public function edit($id = null) { $article = ArticleModel::with(['tags'])->find($id); if (!$article) { // 用 url() 生成:绑定独立域名后自动切换,不要硬编码 '/articles/backend/...' return redirect((string) url('articles/backend/article/index')); } $categories = ArticleCategory::where('delete_at', 0)->where('status', 1)->select(); $tags = ArticleTag::where('delete_at', 0)->select(); $articleTags = $article->tags->column('id'); $tagNamesStr = implode(',', $article->tags->column('name')); return View::fetch('article/edit', [ 'article' => $article, 'categories' => $categories, 'tags' => $tags, 'articleTags' => $articleTags, 'tagNamesStr' => $tagNamesStr, ]); } public function update($id = 0) { $data = Request::post(); $id = $id ?: (int) ($data['id'] ?? 0); $data['update_at'] = time(); $data['is_top'] = isset($data['is_top']) ? 1 : 0; $data['is_recommend']= isset($data['is_recommend']) ? 1 : 0; $validate = new Validate([ 'title|标题' => 'require|max:255', 'content|内容' => 'require', 'cid|分类' => 'require|number', ]); if (!$validate->check($data)) { return json(['code' => 0, 'msg' => $validate->getError()]); } // 定时发布:前端传 "Y-m-d H:i" 字符串,统一转时间戳(0 = 立即发布) if (!empty($data['publish_time'])) { $ts = strtotime((string) $data['publish_time']); $data['publish_time'] = $ts ?: 0; } // 正文白名单净化:正文在前台是 |raw 直出(富文本必须原样输出), // 所以必须在入库前净化,防止来源不可信时形成存储型 XSS。 if (!empty($data['content'])) { $data['content'] = HtmlSanitizer::clean((string) $data['content']); } // 封面图地址校验:必须是 http(s):// 或 / 开头的 URL,避免填成裸字符串导致坏图 if (!empty($data['cover_image']) && !preg_match('#^(https?://|/)#i', (string) $data['cover_image'])) { return json(['code' => 0, 'msg' => '封面图地址格式不正确(须为 http(s):// 或 / 开头)']); } // 字段白名单:防止通过表单注入 uid / view_count / delete_at 等非表单字段 $allow = ['title', 'content', 'summary', 'cover_image', 'uid', 'author', 'cid', 'status', 'is_top', 'is_recommend', 'publish_time', 'create_at', 'update_at']; // 处理标签 $tagIds = []; if (!empty($data['tags'])) { $tagNames = is_array($data['tags']) ? $data['tags'] : explode(',', $data['tags']); foreach ($tagNames as $name) { $name = trim($name); if ($name === '') { continue; } $tag = ArticleTag::where('delete_at', 0)->where('name', $name)->find(); if (!$tag) { // alias 用作 URL 标识:中文名不能直接当别名,用稳定的短哈希兜底 $tag = ArticleTag::create([ 'name' => $name, 'alias' => 'tag-' . substr(md5($name), 0, 8), 'create_at' => time(), 'update_at' => time(), ]); } $tagIds[] = $tag->id; } } try { if ($id) { $article = ArticleModel::find($id); if (!$article) { return json(['code' => 0, 'msg' => '文章不存在']); } $article->allowField($allow)->save($data); } else { if (empty($data['uid'])) { $data['uid'] = $this->auth->info['uid'] ?? 0; } if (empty($data['author'])) { $data['author'] = $this->auth->info['nickname'] ?? ''; } $data['create_at'] = time(); $article = ArticleModel::create($data, $allow); $id = $article->id; } } catch (\Throwable $e) { return json(['code' => 0, 'msg' => '保存失败:' . $e->getMessage()]); } ArticleArticleTag::syncTags($id, $tagIds); return json([ 'code' => 1, 'msg' => '保存成功', 'url' => (string) url('articles/backend/article/index'), ]); } public function delete($id = 0) { $id = $id ?: (int) Request::post('id', 0); $article = ArticleModel::find($id); if ($article) { $article->delete(); // 一并清理标签关联与评论,避免孤儿数据 ArticleArticleTag::where('aid', $id)->delete(); ArticleComment::where('aid', $id)->delete(); return json(['code' => 1, 'msg' => '已删除']); } return json(['code' => 0, 'msg' => '文章不存在']); } /** * 批量删除(软删 + 清理标签关联与评论) */ public function batchDelete() { $ids = (array) Request::post('ids/a', []); $ids = array_values(array_filter(array_map('intval', $ids))); if (!$ids) { return json(['code' => 0, 'msg' => '请选择要删除的文章']); } $count = 0; foreach ($ids as $id) { $article = ArticleModel::find($id); if (!$article) { continue; } $article->delete(); ArticleArticleTag::where('aid', $id)->delete(); ArticleComment::where('aid', $id)->delete(); $count++; } return json(['code' => 1, 'msg' => '已删除 ' . $count . ' 篇']); } /** * 批量设置状态(1=发布 0=草稿) */ public function batchStatus() { $ids = (array) Request::post('ids/a', []); $ids = array_values(array_filter(array_map('intval', $ids))); $status = (int) Request::post('status', 1); if (!$ids) { return json(['code' => 0, 'msg' => '请选择文章']); } if (!in_array($status, [0, 1], true)) { return json(['code' => 0, 'msg' => '状态值不合法']); } $count = ArticleModel::whereIn('id', $ids) ->update(['status' => $status, 'update_at' => time()]); return json(['code' => 1, 'msg' => '已更新 ' . $count . ' 篇']); } /** * 审核前台投稿(pass=1 通过并发布 / pass=0 拒绝并退回草稿) */ public function audit() { $id = (int) Request::post('id', 0); $pass = (int) Request::post('pass', 1); $article = ArticleModel::find($id); if (!$article) { return json(['code' => 0, 'msg' => '文章不存在']); } if ((int) $article->status !== 2) { return json(['code' => 0, 'msg' => '该文章不是待审核状态']); } $article->save(['status' => $pass ? 1 : 0, 'update_at' => time()]); return json(['code' => 1, 'msg' => $pass ? '审核通过,已发布' : '已拒绝,退回草稿']); } public function recyclebin() { $page = Request::param('page', 1); $limit = Request::param('limit', 15); if (Request::isAjax()) { $list = ArticleModel::onlyTrashed() ->order('delete_at', 'desc') ->paginate(['list_rows' => $limit, 'page' => $page]); $rows = array_map(function ($a) { return [ 'id' => $a->id, 'title' => $a->title, 'delete_at' => $a->delete_at, ]; }, $list->items()); return json(['code' => 0, 'msg' => '', 'count' => $list->total(), 'data' => $rows]); } return View::fetch('article/recyclebin'); } public function restore($id = 0) { $id = $id ?: (int) Request::post('id', 0); $article = ArticleModel::onlyTrashed()->find($id); if ($article) { $article->restore(); return json(['code' => 1, 'msg' => '已恢复']); } return json(['code' => 0, 'msg' => '记录不存在']); } }