
简介这是一份基于ThinkPHP框架的新闻发布系统前后台完整资源面向PHP初学者与Web项目实战开发者帮助理解MVC分层、数据交互及富文本编辑器集成。压缩包仅6.43MB却包含1117个文件414个PHP文件承载核心业务逻辑503个GIF多为界面效果或操作演示另有HTML/CSS/JS前端页面、SQL数据库脚本及UEditor扩展组件便于按模块对照学习。已有4735人学习下载。资源围绕文章后台管理展开涵盖数据表设计、Article控制器与模型编写、模板渲染、图片上传接口配置以及前端列表分页和搜索功能同时涉及SQL注入、XSS防护与缓存策略等安全优化要点。配合内容预览中的IndexAction等关键文件可直接部署运行也可作为课程设计的参考蓝图。1. 用 ThinkPHP 搭新闻系统三天交付前后台前段时间接了一个新闻发布网站的单子需求不复杂但很琐碎前台列表页、详情页后台要有登录、栏目管理、新闻发布和审核状态切换。原本以为要用原生 PHP 慢慢写结果翻出之前收藏的这套 ThinkPHP 新闻发布系统资源前台后台都有按 MVC 结构拆得很清楚从环境配置到能提交新闻前后用了三天。这套资源适合两类人一类是刚学 ThinkPHP 想找个完整项目练手的新手另一类是像我一样要快速交付中小型网站的开发。它把数据库、后台登录、UEditor 富文本编辑器全部串了起来相当于给你搭好骨架你只需要改改样式和业务细节就能上线。原始需求里常见的栏目管理、新闻发布、状态审核都在里面基本不用自己从零造轮子。2. 环境搭建与项目初始化MVC 目录结构、入口文件与数据库连接2.1 先说 MVCThinkPHP 的项目目录到底怎么组织刚开始用 ThinkPHP后面简称 TP的人最容易被目录结构劝退。它的 MVC 分层其实很直观M 是模型对应数据库表的操作V 是视图负责 HTML 页面渲染C 是控制器接收请求、调用模型、把数据抛给视图。这套新闻系统把前后台都放在同一个项目里后台代码和前台代码分别放在不同的控制器目录通过 URL 前段区分。常见的目录结构如下tp_news/ ├── app/ │ ├── controller/ # 控制器前台和后台的类都放在这 │ ├── model/ # 模型对应新闻、栏目、管理员等表 │ └── view/ # 模板HTML 页面 ├── config/ │ └── database.php # 数据库连接配置 ├── public/ │ ├── index.php # 入口文件 │ └── static/ # css、js、图片 ├── route/ │ └── app.php # 路由定义 └── runtime/ # 缓存和日志目录实际项目里还会多出 extend、vendor 之类的内容。你要记住的核心是控制器只做调度不直接写 SQL模型层封装数据查询视图层只负责输出 HTML。很多新手图省事在控制器里用原生 SQL 一把梭短期看起来快后面改一个字段名要搜遍全站。你既然用了 MVC 框架就顺着它的规矩走后面维护成本会低很多。2.2 环境准备与入口文件配置这套系统跑的是 PHP 7 环境本地调试建议直接用集成环境常见的集成包自带 Apache MySQL PHP省去自己配环境的麻烦。解压源码后把 Web 根目录指向 public 目录访问域名就能看到首页。如果看到的是目录列表而不是页面说明入口文件没有被正确解析或者伪静态规则没配。入口文件 public/index.php 很简单它的作用只有两个引入框架启动文件、初始化应用。?php // public/index.php namespace think; require __DIR__ . /../vendor/autoload.php; // 开启调试模式上线前记得关掉 $app new \think\App(); $app-initialize();如果你用的是精简版 PHP 环境很可能缺少 vendor 目录里的依赖文件。这时在项目根目录运行 composer install 拉一遍依赖就能解决。没装 Composer 的读者可以找一套带完整 vendor 的压缩包直接解压这也是这套资源的优势之一离线环境也能跑起来调试。2.3 数据库连接配置config/database.php 的关键参数框架跑起来之后第一件事是改数据库配置。文件位置在 config/database.php核心参数是主机、库名、账号、密码还有一个容易忽略的 prefix。// config/database.php return [ type mysql, hostname 127.0.0.1, database tp_news, username root, password your_password, hostport 3306, prefix tp_, charset utf8mb4, ];不少人在这一步翻车prefix 填了 tp_但导入 SQL 时表名没有带这个前缀然后模型查询报“表不存在”。解决办法很简单要么把 SQL 里的表名都加上 tp_ 前缀要么把 prefix 改成空字符串。我一般习惯保留一个固定前缀因为同一个数据库里可能还有别的应用加了前缀不容易互相覆盖。字符集也要注意老项目很多还是 utf8建议你直接改成 utf8mb4否则以后用户往标题里粘 emoji 表情存进数据库会显示成乱码而且报错很隐蔽。SQL 文件一般在源码包的 sql 目录下用 phpMyAdmin 导入或者命令行导入都行mysql -uroot -p tp_news sql/tp_news.sql导入完成后用后台初始账号登进去第一件事就是改密码。初始密码一般写在 README 或 SQL 文件的注释里这个别忽略线上环境用默认密码等于裸奔。2.4 URL 重写与调试模式的选择ThinkPHP 默认的 URL 访问方式是 index.php?s/index/news/readid1 这种模式能跑但地址非常丑对 SEO 也不友好。这套系统自带伪静态规则Apache 下在 public 目录放一个 .htaccessNginx 下在站点配置里加一段 rewrite比较常见的写法是location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } }配好伪静态之后访问地址会变成 news/read/id/1 这种简洁形式前后台链接看起来也专业很多。调试模式是新手经常忽略的地方。这套资源里调试开关在 config/app.php 中设置为 app_debug true 后页面上会直接显示报错信息。本地开发一定要开着代码出问题一目了然。上线前务必关掉否则 SQL 错误、文件路径全都会暴露给访问者这些信息对攻击者来说等于送上门的地图。3. 数据库设计与模型层新闻表字段、索引和查询封装3.1 新闻表字段设计预留哪些状态位才能不发散做新闻系统最忌讳的就是把状态直接设计成「发布/未发布」两个枚举值。到了项目后期你会发现需求里还要塞「草稿」「待审核」「定时发布」字段改来改去非常痛苦。这套系统的 news 表字段设计得很常规但状态位的取舍值得参考字段名类型说明idint(11)主键titlevarchar(200)新闻标题contenttext正文内容category_idint(11)栏目IDstatustinyint(1)0草稿 1待审核 2已发布 3已下架viewsint(11)浏览量create_timeint(10)创建时间时间戳update_timeint(10)更新时间用 tinyint 做 status 而不是直接用布尔值的原因很简单新闻发布流程天然带审核这个动作。管理员编辑完保存为草稿提交后进入待审核审核通过才能对外展示。如果一开始就设计成两个状态后面加审核环节就得改表结构SQL 迁移在小项目里还能忍但没必要给自己埋这个坑。3.2 建表 SQL 与索引设计建表语句直接看代码下面的结构基本能对应到这套系统的核心表CREATE TABLE tp_news ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT 标题, content text COMMENT 正文, category_id int(11) NOT NULL DEFAULT 0 COMMENT 栏目ID, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 状态0草稿 1待审核 2已发布 3已下架, views int(11) NOT NULL DEFAULT 0 COMMENT 浏览量, create_time int(10) NOT NULL DEFAULT 0, update_time int(10) NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_category_status (category_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;索引这里有个细节值得说idx_category_status 是联合索引。前台列表页最常见的操作是「按栏目展示已发布的新闻」查询条件同时带上 category_id 和 status这一个索引能同时覆盖两个字段的过滤。如果单独建两个单列索引MySQL 一般只会选择其中一个另一个字段的过滤就得回表数据量大了性能差距很明显。create_time 用 int(10) 时间戳而不是 datetime是 TP 框架里比较常见的做法。排序和范围查询都走索引PHP 端格式化输出即可。如果你更喜欢 datetime 的可读性可以改但要记得同步修改模型里的写入逻辑。3.3 模型层与 Db 类什么时候用模型什么时候直接查这套系统查询时同时用到了模型和 Db 类。区别在于模型适合封装业务逻辑Db 类适合快速查询。比如前台列表页只需要取几条数据展示出来直接上 Db 类// 前台列表查询 use think\Db; $list Db::name(news) -where(status, 2) -where(category_id, $categoryId) -order(create_time, desc) -limit(20) -select();代码逻辑很直白查已发布状态、按栏目过滤、按创建时间倒序、取 20 条。这里的 status 2 是硬编码的魔法数字实际开发中建议在模型里定义常量比如 News::STATUS_PUBLISHED不然代码里到处是裸的数字过两个月你自己都不知道 2 代表什么。创建新闻、更新状态这类带业务逻辑的操作推荐单独写一个 News 模型// app/model/News.php namespace app\model; use think\Model; class News extends Model { const STATUS_DRAFT 0; const STATUS_PENDING 1; const STATUS_PUBLISHED 2; const STATUS_OFFLINE 3; public function publish($id) { $news $this-find($id); if (!$news || $news-status ! self::STATUS_PENDING) { return false; } $news-status self::STATUS_PUBLISHED; $news-save(); return true; } }这个封装的意义在于所有状态流转的判断都收口在模型里控制器只需要调用 publish 方法不需要关心状态流转的细节。多个控制器调用同一套逻辑时不会出现有的地方忘了判断待审核状态。3.4 模型事件与过度设计的边界ThinkPHP 模型支持 after_update、after_insert 等事件钩子比如新闻审核通过后要更新栏目的统计字段、写操作日志都可以挂在模型事件里。但项目初期不建议过度设计。事件写多了整条业务链路会变得像黑匣子数据莫名被修改时很难定位是哪段钩子触发的。我一般只把「状态变更 日志写入」这层逻辑放进模型其他页面展示逻辑全部放在控制器里保持直观。这套系统的代码风格也是这个路子没有堆砌太多魔术逻辑读起来比较舒服。4. 前台页面与 UEditor 集成富文本、图片上传和列表渲染4.1 为什么选 UEditor部署成本最低的富文本方案前台页面本身结构简单真正的重头戏在后台的富文本编辑器。市面上编辑器五花八门百度 UEditor 虽然官方维护频率低但依然是国内很多中小项目的首选——它不依赖 npm 构建直接把静态资源丢到 public 目录就能跑而且图片上传、视频上传的后端逻辑都有成套的 PHP 代码。对一套强调「易学易用」的 TP 资源来说这是最对味的选择。4.2 UEditor 的引入与服务端配置把 UEditor 解压后放到 public/ueditor后台发布页面的模板里引入三段资源link relstylesheet href/ueditor/themes/default/css/ueditor.css script src/ueditor/ueditor.config.js/script script src/ueditor/ueditor.all.min.js/script script var ue UE.getEditor(container); /scripttextarea 那边注意给个 idcontainer 的空标签UEditor 会自动把它替换成编辑区。ueditor.config.js 里可以设置编辑器高度、工具栏按钮等默认配置一般够用不需要动太多。服务端上传配置在 UEditor 的 PHP 目录里是一个 config.json 文件。图片上传路径、允许的扩展名、大小限制都在里面。开发时记得把图片访问路径的根目录设置成 /upload/ 这种公开可访问的地址否则上传成功但是前端展示时读不到文件。4.3 图片上传返回格式最常见的翻车点UEditor 的图片上传通过 controller.php 这个入口文件处理它返回给前端的数据格式有严格规定必须是 state、url、title、original 这几个字段的 JSON。很多人在这里翻车是因为自定义上传接口返回了 {code:0, data:{url:...}} 这种常见接口格式UEditor 不认表现为编辑器能打开但点图片上传一直没反应控制台报 parse error。解决方法是把上传接口的返回值改成 UEditor 的格式// UEditor 后端返回格式 $result [ state SUCCESS, url /upload/20250101/xxxx.jpg, title xxxx.jpg, original 原文件名.jpg ]; echo json_encode($result);注意 state 字段必须是英文字符串 SUCCESS且不能有别的字段混在里面。如果你给 UEditor 的 config.json 里配置了自定义上传地址后端再按通用 JSON 格式返回前端解析必挂。以后看到编辑器上传没反应先开浏览器的 Network 面板看响应体再对着 state 字段找问题基本都是这个套路。4.4 前台列表与详情页渲染前台模板的渲染逻辑不复杂控制器里查出数据assign 给模板模板用 foreach 循环输出// 控制器 public function index() { $categoryId input(get.category_id, 0); $where [status 2]; if ($categoryId) { $where[category_id] $categoryId; } $list Db::name(news) -where($where) -order(create_time, desc) -paginate(10); $this-assign(list, $list); return $this-fetch(); }!-- 模板列表页 -- ul classnews-list {volist namelist idvo} li a href/news/read/id/{$vo.id}{$vo.title}/a span{$vo.create_time|dateY-m-d,###}/span /li {/volist} /ul {$list|raw}paginate(10) 会自动生成分页对象模板里 {$list|raw} 输出分页链接。这里有个易踩的坑模板里的时间格式化用了 TP 自带的 date 模板修饰符第三个参数 ### 表示把当前值作为时间戳传入。如果你改了数据库字段类型比如把 create_time 改成 datetime这里的格式化方式就要换成 {$vo.create_time|strtotime} 之类的转换不然页面上显示的是 1970 年。前台详情页逻辑更简单按 id 查一条数据$news Db::name(news)-where(id, $id)-where(status, 2)-find(); if (!$news) { $this-error(内容不存在或未发布); } // 浏览量自增 Db::name(news)-where(id, $id)-setInc(views);浏览量的自增用 setInc 一步搞定不需要先查再写。注意顺序先查状态再自增浏览量避免把草稿的浏览量也加上去。5. 后台管理实战与常见问题排查登录认证、发布审核和踩坑记录5.1 后台登录认证与 session 控制后台所有的控制器都继承一个 AdminBase 基类在初始化方法里统一判断登录状态。这样做的好处是权限判断收敛在一个地方不用每个控制器里重复写判断逻辑。// application/admin/controller/AdminBase.php namespace app\admin\controller; use think\Controller; class AdminBase extends Controller { protected function initialize() { if (!session(?admin_id)) { $this-redirect(admin/login/index); } } }登录成功后写入 session密码校验用的是 password_hash 和 password_verify 这对组合比直接存 MD5 靠谱得多。这套系统如果你拿到源码看 admin 登录控制器的逻辑就能看到完整的用法。5.2 发布、编辑、审核的状态流转后台的操作流程大致是编辑创建新闻 → 保存草稿 → 提交审核状态流转到待审核 → 管理员在列表页点击通过/驳回 → 通过后状态变为已发布前台才可见。状态流转如果散落在各个控制器方法的 if else 里很容易出现漏改。你看下源码里的 Admin News 控制器一般会封装一个 changeStatus 方法// 后台审核操作 public function audit() { $id input(post.id); $action input(post.action); // pass 或 reject if ($action pass) { Db::name(news)-where(id, $id)-update([ status News::STATUS_PUBLISHED, update_time time() ]); } else { Db::name(news)-where(id, $id)-update([ status News::STATUS_DRAFT, update_time time() ]); } return json([code 1, msg 操作成功]); }驳回退回草稿而不是直接删除这个设计很关键编辑可以继续修改再重新提交避免有效内容被误删。5.3 踩坑记录这套系统最常见的五个问题下面几条都是实际部署和二次开发中会高频遇到的问题按现象、原因、解决的方式整理出来每条都是付过学费的。坑一登录后台立刻被踢回登录页现象输入账号密码点登录跳转后台首页后马上又跳回登录页像死循环一样。原因session 没生效。最常见是 TP 的 session 配置里驱动选的是 file但 runtime/session 目录没有写权限。另一种可能是服务器开了多个 PHP 进程session 文件互相覆盖。解决把 runtime 目录权限设为 755 或 777确认服务器能正常创建 session 文件。如果还不行打开 config/session.php 把驱动改成 redis 试试前提是装了 redis 扩展。坑二UEditor 图片上传后前台无法访问现象上传返回成功但前台文章里的图片显示裂图直接访问图片 URL 返回 404。原因Nginx 的 root 配置指向了 public 目录而图片实际存放在 public/upload 下按理论不应该 404但如果 UEditor 的配置文件里图片保存路径写成了 /data/upload文件被写到了 public 之外的目录nginx 自然访问不到。解决打开 UEditor 的 config.json把图片路径相关配置改成相对于 public 的路径例如 /upload/image/。同时确认上传目录有写权限。坑三去掉了 index.php页面全部 404现象伪静态配置完访问首页正常点击详情页链接 404或者后台链接全部打不开。原因Nginx 伪静态规则只写了 location /没有把 /admin 等后台路径也走 rewrite或者 rewrite 规则顺序写错了。解决Nginx 的 rewrite 规则要放在 server 块中最外层同时确认 public/.htaccess 文件存在。Apache 环境检查 mod_rewrite 是否开启。可以把 Nginx 配置改成下面这样覆盖所有路径location / { if (-e $request_filename) { break; } rewrite ^(.*)$ /index.php?s$1 last; }坑四导入 SQL 时提示表已存在或者字段冲突现象第二次导入时报错或者导入后页面报某个字段不存在。原因数据库里残留了同名表尤其第一次导入失败后MySQL 会自动把建了一半的表留在库里。解决导入前先 DROP 掉原有的表或者用 phpMyAdmin 的「导入时清空已有表」选项。正规做法是重新建一个空数据库然后一次性导入干净的 SQL。坑五修改了代码页面不生效现象改了控制器、模板文件刷新页面还是老样子。原因TP 的模板缓存和 runtime 目录缓存没清。很多新手不知道 runtime 目录是干嘛的改完代码不清理就会看到一份「过期」的页面。解决手动删除 runtime/cache 和 runtime/temp 目录下的缓存文件或者直接删掉整个 runtime 目录让它重建。本地开发时可以关闭模板缓存在 config/template.php 里设置 cache false省得每次都要手工清。5.4 权限与操作日志小系统也要留一手后台系统如果面向多人管理至少要把操作日志做了。谁在什么时候审核了一篇文章、改动过什么字段这些信息在出问题时是唯一的追溯线索。不需要设计太复杂的日志表简单的操作记录表就够了CREATE TABLE tp_operation_log ( id int(11) NOT NULL AUTO_INCREMENT, admin_id int(11) NOT NULL, action varchar(100) NOT NULL, target_id int(11) NOT NULL, detail varchar(500) NOT NULL, create_time int(10) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;每次审核、删除操作调用一个公共的日志写入方法把管理员 ID、操作类型、对象 ID 记下来成本很低收益很大。6. 性能优化与部署技巧慢查询、模板缓存与上线检查清单后台跑通之后收尾阶段有几个优化动作值得做一遍尤其是上线前。先开慢查询日志。TP 的数据库配置里有一个 debug true 开关开启后会在页面上输出 SQL 执行时间。不要只在出了问题才开我一般开发最后一天会开着这个跑一遍所有页面专门抓执行时间超过 100ms 的查询。新闻列表页如果数据量过万没加索引的 where 条件会在日志里非常难看。列表查询千万别用 select() 全量查改成 paginate(10) 或者 limit(20)分页对用户体验影响很大。内容表查询建议加个缓存$list Db::name(news) -where(status, 2) -cache(news_list_ . $categoryId, 60) -select();cache 方法是 TP 内置的查询缓存这里的意思是缓存 60 秒。新闻系统的更新频率不高缓存 5 分钟都问题不大。注意缓存键别乱写带上分类 ID 避免不同栏目串数据。模板缓存那一层开发期关掉上线前打开。TP 的模板引擎默认开启缓存你在 config/template.php 里确认一下缓存开启状态。关掉缓存的写法是// config/template.php cache false,但这个只适合本地改样式用上线前务必改回 true不然后台操作系统会自动清理模板缓存虽然不影响功能但无谓地增加 IO 开销。上线前我习惯把快速检查清单跑一遍关闭 app_debug、确认错误页面不暴露 SQL 信息、修改默认后台密码、删除 install 相关脚本、检查 runtime 目录不可被外部访问。这套系统默认的初始密码一定要在上线当天换掉不是危言耸听扫描器扫到默认密码是分分钟的事。做完上面的优化和检查这个新闻系统基本就能扛住日活几千量级的小站了。有一次交付后一周用户反馈图片加载变慢排查下来是没给静态资源做缓存策略加了一行 Nginx 配置解决了。从那以后我每次上线前都要强制走一遍静态资源过期时间的设置顺手看看编辑器上传目录有没有不该在里面的文件。建站容易守住和优化才是长期的事希望这些经验能让你少走几步弯路希望帮到你。本文还有配套的精品资源点击获取