ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

PHP+SQL+HTML新闻管理系统源码实战:从环境搭建到安全优化

2026/10/1 1:29:31 拓冰建站 浏览量
PHP+SQL+HTML新闻管理系统源码实战:从环境搭建到安全优化 简介这份资源是基于PHP、SQL与HTML构建的简单新闻管理系统源码面向Web开发初学者及需要快速搭建小型网站新闻模块的开发者。它提供新闻的创建、编辑、删除与查看等基础功能帮助读者理解后端逻辑、数据库存储与前端展示之间的交互流程。压缩包共2378个文件约25.54MB以2273个svg图标资源为主另含32个php脚本、8个css样式表、4个js脚本、2个html页面及1个sql建库文件覆盖页面结构、样式美化与数据操作等环节。目前已有564人学习下载。通过研读这套源码读者可掌握数据库连接配置、新闻增删改查、列表分页、关键词搜索与时间倒序排列等实现思路并熟悉前后端协作与项目部署的基本流程适合作为课程设计或练手项目的参考素材。1. 新闻管理系统源码拆解从 PHPSQLHTML 到能跑起来的第一版很多人拿到「基于phpsqlhtml的简单的新闻管理系统源码.zip」这类压缩包第一反应是解压、丢进网站根目录、改数据库配置然后刷新浏览器。结果要么白屏要么报Access denied for user rootlocalhost要么首页出来了但点进详情页 404。这套技术栈看着简单——PHP 负责逻辑、SQL 负责存储、HTML 负责展示三层分得清清楚楚但真正让它跑起来卡人的从来不是代码本身而是环境、路径、编码和数据库这四件事。这篇笔记面向两类人一是刚学完 PHP 基础语法、想找个完整项目练手的新手二是需要快速交付一个内部信息发布页、不想上重型框架的开发者。我会按「这套源码的骨架长什么样 → 数据库怎么建 → 后端接口怎么改 → 前端页面怎么接 → 出问题怎么查」的顺序把每一步落到可复制的命令和参数上。你不需要先看完所有代码跟着走一遍本地就能跑出一个能发新闻、能看列表、能进详情的版本。中间会穿插我踩过的坑比如中文乱码、SQL 注入、分页算错这些在「简单系统」里反而最容易翻车。2. 先看清骨架PHPSQLHTML 三层怎么分工2.1 目录结构与请求流向拿到源码包先别急着改配置。用tree或文件管理器把目录展开典型的简单新闻管理系统长这样news/ ├── index.php # 前台首页新闻列表 ├── detail.php # 新闻详情页 ├── admin/ │ ├── login.php # 后台登录 │ ├── news_add.php # 添加新闻 │ ├── news_list.php # 新闻管理列表 │ └── logout.php ├── config/ │ └── db.php # 数据库连接配置 ├── includes/ │ └── functions.php # 公共函数如分页、转义 ├── static/ │ ├── css/style.css │ └── images/ └── sql/ └── news.sql # 建表语句请求流向是浏览器访问index.php→ PHP 引入config/db.php连接数据库 → 执行 SQL 查询新闻列表 → 把结果集用while循环拼进 HTML 模板 → 输出给浏览器。详情页多一步从 URL 取id参数查单条记录。后台则是先session_start()判断登录态再执行增删改。这里的关键认知是PHP 文件既是控制器又是视图。简单系统不会上 MVCindex.php里上半段是 PHP 查询逻辑下半段是 HTML 输出。你改样式就动 HTML 部分改数据就动 PHP 部分别混在一起改。2.2 数据库表设计三张表就够打开sql/news.sql常见建表语句如下。如果源码里没有按这个建CREATE DATABASE IF NOT EXISTS news_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE news_db; -- 新闻分类表 CREATE TABLE category ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT 分类名 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 新闻主表 CREATE TABLE news ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL COMMENT 标题, content TEXT NOT NULL COMMENT 正文, category_id INT UNSIGNED NOT NULL DEFAULT 1, author VARCHAR(50) DEFAULT admin, create_time INT UNSIGNED NOT NULL COMMENT 时间戳, views INT UNSIGNED DEFAULT 0 COMMENT 浏览量, KEY idx_category (category_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 管理员表 CREATE TABLE admin ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(32) NOT NULL COMMENT MD5值 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO admin (username, password) VALUES (admin, MD5(admin123)); INSERT INTO category (name) VALUES (公司动态), (行业资讯), (产品更新);参数说明create_time用INT存时间戳而不是DATETIME是因为简单系统里 PHP 用time()写入、用date()格式化输出最顺手也避免时区配置问题。content用TEXT而不是VARCHAR新闻正文可能几千字VARCHAR(2000)容易截断。password存 MD5 是这类老源码的常见做法但你要知道 MD5 已经不安全后面第 5 章会讲怎么换成password_hash()。注意建库时字符集必须用utf8mb4不要用utf8。utf8在 MySQL 里是残缺的存 emoji 或某些生僻字会报Incorrect string value这是中文乱码最常见的根因之一。2.3 连接配置改对这四个参数config/db.php通常长这样?php $host 127.0.0.1; $user root; $pass your_password; $db news_db; $conn mysqli_connect($host, $user, $pass, $db); if (!$conn) { die(连接失败: . mysqli_connect_error()); } mysqli_set_charset($conn, utf8mb4);四个参数对应你的实际环境$host本地一般127.0.0.1用localhost在某些系统上会走 socket 导致连接慢$user和$pass是你 MySQL 的账号$db必须和建库名一致。最后一行mysqli_set_charset千万别删它决定 PHP 和 MySQL 之间传输的字符集删了就会出现「数据库里是好的页面上是问号」。如果你用的是 PHP 7 以上mysqli_connect报错会直接抛 Warning建议改成异常捕获mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); try { $conn new mysqli($host, $user, $pass, $db); $conn-set_charset(utf8mb4); } catch (Exception $e) { die(数据库连接异常请检查 config/db.php); }这样出错时不会把数据库账号密码路径暴露给浏览器只给一句人话提示日志里再记详细错误。3. 把后端跑通查询、分页、写入的最小实现3.1 新闻列表查询与分页参数首页index.php的核心是「查列表 分页」。先看一个能用的查询?php require config/db.php; $page isset($_GET[page]) ? max(1, intval($_GET[page])) : 1; $size 10; $offset ($page - 1) * $size; $sql SELECT id, title, author, create_time, views FROM news ORDER BY create_time DESC LIMIT ? OFFSET ?; $stmt $conn-prepare($sql); $stmt-bind_param(ii, $size, $offset); $stmt-execute(); $result $stmt-get_result(); $newsList []; while ($row $result-fetch_assoc()) { $row[create_time_text] date(Y-m-d H:i, $row[create_time]); $newsList[] $row; }逻辑说明intval($_GET[page])把 URL 参数强制转成整数max(1, ...)防止页码为 0 或负数导致OFFSET算错。LIMIT ? OFFSET ?用预处理占位符bind_param(ii, ...)里两个i表示两个整数参数这是防 SQL 注入的标准写法。date()把时间戳转成可读格式放在 PHP 层做而不是 SQL 层做是为了让 SQL 保持简单、走索引。分页总数要单独查一次$countResult $conn-query(SELECT COUNT(*) AS total FROM news); $total $countResult-fetch_assoc()[total]; $totalPages ceil($total / $size);参数怎么调$size是每页条数新闻列表一般 10 到 20 条太多首屏加载慢太少翻页频繁。ORDER BY create_time DESC依赖idx_create_time索引如果你发现列表页慢先EXPLAIN看这条 SQL 有没有走索引。3.2 详情页取单条与浏览量自增detail.php要做两件事根据id查新闻、把views加一。?php require config/db.php; $id isset($_GET[id]) ? intval($_GET[id]) : 0; if ($id 0) { header(Location: index.php); exit; } // 浏览量自增用原子操作避免并发覆盖 $conn-query(UPDATE news SET views views 1 WHERE id . $id); $stmt $conn-prepare(SELECT * FROM news WHERE id ?); $stmt-bind_param(i, $id); $stmt-execute(); $news $stmt-get_result()-fetch_assoc(); if (!$news) { die(新闻不存在或已删除); }这里有个细节UPDATE ... SET views views 1是数据库层面的原子自增不要写成先SELECT views再UPDATE views 旧值1后者在并发下会丢计数。$id虽然已经intval过但UPDATE里我仍然直接拼接因为整数转换后不存在注入风险如果你不放心同样可以用预处理。3.3 后台写入转义与字段校验添加新闻的news_add.php核心是接收表单、校验、插入?php session_start(); if (!isset($_SESSION[admin_id])) { header(Location: login.php); exit; } require ../config/db.php; $title trim($_POST[title] ?? ); $content trim($_POST[content] ?? ); $catId intval($_POST[category_id] ?? 1); $errors []; if ($title || mb_strlen($title) 200) { $errors[] 标题不能为空且不超过200字; } if ($content ) { $errors[] 正文不能为空; } if (empty($errors)) { $stmt $conn-prepare( INSERT INTO news (title, content, category_id, author, create_time) VALUES (?, ?, ?, ?, ?) ); $author $_SESSION[admin_name] ?? admin; $now time(); $stmt-bind_param(ssisi, $title, $content, $catId, $author, $now); $stmt-execute(); header(Location: news_list.php); exit; }参数说明bind_param(ssisi, ...)的类型串按顺序对应——s字符串标题、s字符串正文、i整数分类、s字符串作者、i整数时间戳。类型串写错是最隐蔽的 bug比如把$catId写成sMySQL 会做隐式转换数据量小时看不出问题数据量大了索引失效。mb_strlen而不是strlen因为中文标题按字节算会超长。注意$_POST里的内容不要直接拼进 SQL哪怕你觉得「后台只有自己人用」。SQL 注入不分前台后台一个弱密码加上注入点整库都能被拖走。4. 前端页面怎么接HTML 模板与数据循环4.1 列表页的 HTML 骨架PHP 查完数据HTML 部分负责渲染。index.php下半段!DOCTYPE html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title新闻中心/title link relstylesheet hrefstatic/css/style.css /head body div classcontainer h1新闻中心/h1 ul classnews-list ?php foreach ($newsList as $item): ? li a hrefdetail.php?id?php echo $item[id]; ? ?php echo htmlspecialchars($item[title]); ? /a span classmeta ?php echo $item[author]; ? · ?php echo $item[create_time_text]; ? · 阅读 ?php echo $item[views]; ? /span /li ?php endforeach; ? /ul div classpagination ?php for ($i 1; $i $totalPages; $i): ? a href?page?php echo $i; ? class?php echo $i $page ? active : ; ? ?php echo $i; ? /a ?php endfor; ? /div /div /body /html关键点htmlspecialchars($item[title])必须加。新闻标题里如果有人写了script不加这个函数就会直接执行这就是存储型 XSS。meta charsetutf-8要和数据库字符集一致否则中文显示成乱码。分页链接用?page拼接简单直接不需要伪静态。4.2 详情页输出与 nl2br详情页正文输出article classnews-detail h1?php echo htmlspecialchars($news[title]); ?/h1 div classmeta 发布时间?php echo date(Y-m-d H:i, $news[create_time]); ? /div div classcontent ?php echo nl2br(htmlspecialchars($news[content])); ? /div /articlenl2br把正文里的换行符转成br否则用户输入的多段文字会挤成一坨。顺序很重要先htmlspecialchars转义再nl2br加换行标签。反过来会把br也转义掉页面上直接显示br文本。4.3 后台表单与分类下拉添加新闻的表单里分类下拉要从数据库读?php $cats $conn-query(SELECT id, name FROM category ORDER BY id); ? form methodpost actionnews_add.php input typetext nametitle placeholder新闻标题 required select namecategory_id ?php while ($c $cats-fetch_assoc()): ? option value?php echo $c[id]; ? ?php echo htmlspecialchars($c[name]); ? /option ?php endwhile; ? /select textarea namecontent rows10 required/textarea button typesubmit发布/button /form表单methodpost而不是get因为正文可能很长URL 长度有限制。required是浏览器原生校验能挡掉空提交但后端仍然要再校验一次前端校验只是体验优化不能当安全边界。5. 避坑与排查简单系统里最容易翻车的五件事5.1 中文乱码从数据库到浏览器的三层字符集现象数据库里用工具看是正常中文页面上显示????或æ–°é—»。原因字符集在三个地方可能不一致——建库建表时的字符集、PHP 连接时的set_charset、HTML 的meta charset。任何一层用了utf8或gbk就会断链。解决统一成utf8mb4。建库DEFAULT CHARSET utf8mb4连接后$conn-set_charset(utf8mb4)HTML 里meta charsetutf-8。已经存进去的乱码数据改不回来只能重新录入。5.2 SQL 注入拼接字符串的代价现象新闻列表页 URL 加个?page1 OR 11返回了全部数据甚至报错暴露表名。原因源码里用了SELECT * FROM news WHERE id $id这种直接拼接$id来自$_GET没过滤。解决所有来自用户的参数要么intval强转要么用preparebind_param。字符串参数一律走预处理不要心存侥幸。老源码里addslashes不算合格防护宽字节注入能绕过。5.3 分页算错最后一页多出空白现象总共 25 条每页 10 条分页显示 3 页但第 3 页只有 5 条却渲染了 10 个空li。原因LIMIT的OFFSET算对了但循环输出时没有判断$newsList是否为空或者$totalPages用了floor而不是ceil。解决$totalPages ceil($total / $size)ceil向上取整。循环前先if (empty($newsList))给个「暂无新闻」提示。$page超过$totalPages时重定向到最后一页避免OFFSET超出返回空数组。5.4 后台越权没登录也能调接口现象直接访问admin/news_add.php没登录也能提交表单新闻被写入。原因news_add.php里只写了session_start()但没判断$_SESSION[admin_id]是否存在。或者判断了但login.php里 session 没正确写入。解决每个后台文件顶部统一加登录检查最好抽成includes/auth.php用require引入。检查逻辑是if (empty($_SESSION[admin_id])) { header(Location: login.php); exit; }exit不能省否则后面的代码还会执行。5.5 路径问题相对路径在子目录里失效现象后台admin/login.php里require ../config/db.php正常但header(Location: index.php)跳转到了admin/index.php而不是根目录首页。原因header的Location是相对当前 URL 的不是相对文件系统。在admin/下写index.php浏览器会拼成admin/index.php。解决跳转根目录用header(Location: ../index.php)或者定义常量define(BASE_URL, /news/)所有跳转用BASE_URL . index.php。静态资源引用同理static/css/style.css在子目录页面里要写成../static/css/style.css。6. 进阶技巧把 MD5 换成 password_hash 并加一层输出过滤这套源码能跑起来之后最该动的一处是密码存储。MD5 不加盐、计算快彩虹表一查就出来。PHP 5.5 以后内置了password_hash()和password_verify()改造成本极低。注册或添加管理员时$plain admin123; $hash password_hash($plain, PASSWORD_DEFAULT); // 存入数据库字段长度要 60VARCHAR(255) 稳妥 $stmt $conn-prepare(UPDATE admin SET password ? WHERE username ?); $stmt-bind_param(ss, $hash, $username); $stmt-execute();登录校验时$stmt $conn-prepare(SELECT id, username, password FROM admin WHERE username ?); $stmt-bind_param(s, $username); $stmt-execute(); $admin $stmt-get_result()-fetch_assoc(); if ($admin password_verify($password, $admin[password])) { session_start(); $_SESSION[admin_id] $admin[id]; $_SESSION[admin_name] $admin[username]; header(Location: news_list.php); } else { $error 用户名或密码错误; }注意admin表的password字段原来是CHAR(32)存 MD5改成password_hash后输出是 60 字符以上必须ALTER TABLE admin MODIFY password VARCHAR(255) NOT NULL。老数据没法自动升级只能让管理员重置密码。第二处值得加的是统一输出过滤函数放在includes/functions.phpfunction e($str) { return htmlspecialchars($str ?? , ENT_QUOTES, UTF-8); }之后所有输出用?php echo e($news[title]); ?比每次手写htmlspecialchars少打几个字也避免漏写。ENT_QUOTES同时转义单双引号防止属性值里的引号闭合逃逸。验证改造是否生效可以手动测登录时故意输错密码看是否返回统一错误提示而不是「密码错误」或「用户不存在」——后者会泄露用户名是否存在。再打开浏览器开发者工具看登录请求的响应里有没有Set-Cookie带HttpOnly没有的话在php.ini里设session.cookie_httponly 1能挡掉大部分 XSS 偷 cookie 的攻击。我自己维护这类小系统有个习惯每次改完数据库字段先在测试库跑一遍ALTER确认老数据不报错再上生产。密码字段长度这种坑我翻过一次车——线上改完VARCHAR(60)结果password_hash某次输出 61 字符新管理员直接注册失败查了半天才发现是字段长度卡在边界。所以字段宁可给宽VARCHAR(255)不差那点空间。希望帮到你。本文还有配套的精品资源点击获取