ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PHP+MySQL实现旅游景点网站管理系统:毕业设计全流程指南

PHP+MySQL实现旅游景点网站管理系统:毕业设计全流程指南 旅游景点网站管理系统很适合作为 PHPMySQL 毕业设计题目因为它的功能边界非常清晰前台面向游客和注册用户展示景点、新闻、提供搜索、详情、收藏和评论后台面向管理员完成景点、分类、新闻、评论和用户的维护。整套系统要涉及建库建表、登录会话、文件上传、分页搜索、权限校验和前端数据回显恰好覆盖 Web 开发课程中大部分核心知识点也方便在答辩时把每个功能点讲透。这篇文章以一套可运行的原生 PHP 项目为例从需求拆分、数据库设计、环境搭建、核心代码实现、运行验证到常见问题排查完整走一遍适合作为毕业设计代码实现和论文技术章节的参考底稿。1. 先把需求拆清楚系统给谁用要做什么毕业设计最常见的翻车原因不是代码写不出来而是需求没拆清楚就开始建表。旅游景点网站管理系统看起来简单实际涉及两类用户、两套界面和多个业务动作。如果不先把角色和使用场景列出来后面很容易出现“前台页面做了一堆后台用不上后台功能又覆盖不了前台业务”的混乱情况。1.1 两种角色对应两套界面系统的第一刀应该切在角色上。普通游客可以浏览景点列表、搜索景点、查看景点详情和新闻公告注册用户可以登录后收藏景点、发表评论。管理员则通过独立后台登录入口进入管理界面负责维护景点信息、景点分类、新闻公告、用户状态和评论审核。这里要注意管理员前后台如果是同一个登录入口权限区分会变得很别扭。推荐的做法是前台用户表和后台管理员表分开会话变量也分开。前台登录用$_SESSION[uid]后台登录用$_SESSION[admin_id]互不干扰。这样实现起来简单权限也更清楚。1.2 功能模块拆分把需求落到功能模块一个用于毕业设计的系统可以拆成以下模块模块所属端核心功能用户注册登录前台注册、登录、退出、个人基本信息校验景点展示前台景点列表、分类筛选、关键词搜索、分页景点详情前台详细介绍、图片、门票价格、开放时间、浏览量统计收藏管理前台添加收藏、取消收藏、查看我的收藏评论留言前台发表评论、查看已审核评论新闻公告前台新闻列表、新闻详情管理员登录后台独立登录、会话校验、退出景点管理后台景点新增、编辑、删除、上下架、图片上传分类管理后台景点分类新增、编辑、删除、排序评论审核后台评论通过、驳回、删除用户管理后台用户列表、启用、禁用数据统计后台景点数量、用户数量、评论数量等简单统计如果只需要满足常见毕业设计评分标准这个范围已经足够。不要在初期就加入在线支付、多级评论、地图定位等功能除非题目明确要求。功能过多意味着数据表变多、字段变多、答辩追问点也变多复杂度会快速上升。1.3 为什么选原生 PHP MySQL 而不是直接套框架很多学生会问现在开发都用 Laravel、ThinkPHP为什么毕业设计还写原生 PHP原因有三点。第一原生 PHP 让每个知识点都能被看见。分页怎么写、会话怎么存、预处理语句怎么防注入这些在答辩时都能清楚地展示给评委。第二框架会隐式处理很多初始化工作一旦框架版本和 PHP 版本对不上报错信息对新手很不友好。第三毕业设计文档里需要写系统流程图、数据流图、核心代码说明原生代码更容易对照讲解。MySQL 作为配套数据库适合这种结构化、关系明确的业务场景。项目里统一使用 PDO 连接 MySQL这样 PHP 7.4、PHP 8.x、MySQL 5.7、MySQL 8.x 之间切换时代码改动量最小。2. 数据库设计是首先要完成的工程图纸很多项目失败在“边写代码边改表”。数据库表一旦确定后续所有 SQL、页面字段、后台表单都围绕它展开。表改一次查询语句、表单、回显逻辑全部要跟着改成本非常高。所以数据库设计应该放在最前面先画清楚实体关系再建表。2.1 确定实体和关系这个系统需要的核心实体有七个管理员、前台用户、景点分类、景点、新闻、评论、收藏。它们之间的关系很清晰一个分类下有多个景点景点表通过category_id关联分类表。一个用户可收藏多个景点一个景点可被多个用户收藏所以收藏表用来记录“用户-景点”关系。一个用户可发表多条评论一个景点可收到多条评论评论表同时保存user_id和scenic_id。管理员和前台用户之间没有业务关联各自独立。实体的核心含义要先说明白scenic_spot是前台展示的主体comment和favorite都围绕它展开news是辅助内容admin和user分别服务后台和前台登录。2.2 表字段设计要点以景点表为例字段设计直接决定详情页能展示哪些信息。下表是推荐字段字段类型含义和设计理由idINT UNSIGNED主键景点唯一标识category_idINT UNSIGNED关联景点分类用于前台分类筛选nameVARCHAR(100)景点名称长度给足imageVARCHAR(255)封面图路径只存相对路径ticket_priceDECIMAL(10,2)门票价格用精确小数避免浮点误差open_timeVARCHAR(50)开放时间实际项目里是一个显示字符串更灵活addressVARCHAR(255)地址introTEXT列表页使用的中等长度简介detailLONGTEXT详情页完整内容可能包含图文visit_countINT UNSIGNED浏览数详情页访问时自增sort_orderINT排序值越小越靠前statusTINYINT1 上架、0 下架不要直接物理删除created_atDATETIME创建时间updated_atDATETIME更新时间这里有一个容易忽略的点门票价格一定要用DECIMAL(10,2)不要用FLOAT。FLOAT是近似值在做价格比较、统计时可能产生 0.1 加 0.2 不等于 0.3 这类问题。而日期时间字段统一用DATETIME代码里插入时使用NOW()避免 PHP 和 MySQL 时区不一致导致的时间错乱。2.3 完整建表 SQL建库建表脚本建议放在项目根目录的sql/init.sql文件中方便重复初始化。下面这套 SQL 可以直接用于开发环境CREATE DATABASE IF NOT EXISTS travel_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE travel_system; CREATE TABLE admin ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 管理员账号, password VARCHAR(255) NOT NULL COMMENT 密码哈希, real_name VARCHAR(50) DEFAULT COMMENT 姓名, last_login_time DATETIME DEFAULT NULL COMMENT 最后登录时间, created_at DATETIME NOT NULL COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_admin_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT管理员表; CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(255) NOT NULL COMMENT 密码哈希, nickname VARCHAR(50) DEFAULT COMMENT 昵称, phone VARCHAR(20) DEFAULT COMMENT 手机号, email VARCHAR(100) DEFAULT COMMENT 邮箱, avatar VARCHAR(255) DEFAULT COMMENT 头像路径, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常 0禁用, created_at DATETIME NOT NULL COMMENT 注册时间, PRIMARY KEY (id), UNIQUE KEY uk_user_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT前台用户表; CREATE TABLE scenic_category ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名称, sort_order INT NOT NULL DEFAULT 0 COMMENT 排序值, created_at DATETIME NOT NULL COMMENT 创建时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT景点分类表; CREATE TABLE scenic_spot ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, category_id INT UNSIGNED NOT NULL COMMENT 分类ID, name VARCHAR(100) NOT NULL COMMENT 景点名称, image VARCHAR(255) DEFAULT COMMENT 封面图, address VARCHAR(255) DEFAULT COMMENT 地址, ticket_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 门票价格, open_time VARCHAR(50) DEFAULT COMMENT 开放时间, intro TEXT COMMENT 景点简介, detail LONGTEXT COMMENT 详细内容, visit_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 浏览量, sort_order INT NOT NULL DEFAULT 0 COMMENT 排序值, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1上架 0下架, created_at DATETIME NOT NULL COMMENT 创建时间, updated_at DATETIME DEFAULT NULL COMMENT 更新时间, PRIMARY KEY (id), KEY idx_category_status (category_id, status), KEY idx_visit_count (visit_count) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT景点表; CREATE TABLE news ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 标题, cover_image VARCHAR(255) DEFAULT COMMENT 封面图, content LONGTEXT COMMENT 内容, source VARCHAR(50) DEFAULT COMMENT 来源, views INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 浏览量, created_at DATETIME NOT NULL COMMENT 发布时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT新闻公告表; CREATE TABLE comment ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, scenic_id INT UNSIGNED NOT NULL COMMENT 景点ID, user_id INT UNSIGNED NOT NULL COMMENT 用户ID, content VARCHAR(500) NOT NULL COMMENT 评论内容, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0待审核 1通过 2驳回, created_at DATETIME NOT NULL COMMENT 评论时间, PRIMARY KEY (id), KEY idx_comment_scenic (scenic_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT景点评论表; CREATE TABLE favorite ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, user_id INT UNSIGNED NOT NULL COMMENT 用户ID, scenic_id INT UNSIGNED NOT NULL COMMENT 景点ID, created_at DATETIME NOT NULL COMMENT 收藏时间, PRIMARY KEY (id), UNIQUE KEY uk_user_scenic (user_id, scenic_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收藏表;2.4 索引、唯一约束和外键取舍这张表设计里有几个细节要理解第一user.username和admin.username设置唯一约束防止重复账号。业务上注册时要先查一次用户名是否存在但唯一约束是数据库层面的最后一道防线。第二favorite表设置唯一索引uk_user_scenic(user_id, scenic_id)避免同一用户重复收藏同一个景点。代码里配合INSERT IGNORE使用即使重复点击也不会产生脏数据。第三comment表加(scenic_id, status)组合索引因为前台详情页要查“某个景点下状态为通过的评论”后台要查“所有待审核评论”这些查询条件正好命中索引。关于外键这里没有为comment、favorite建物理外键约束。原因不是外键没有用而是如果删除景点时直接删除评论和收藏物理外键会导致删除顺序受限。毕业设计阶段使用应用层控制删除逻辑更灵活也更容易讲解。生产项目里如果需要强一致性再考虑ON DELETE CASCADE或软删除方案。3. 环境准备把 PHP 和 MySQL 在本地跑起来很多代码本身没问题问题出在环境不一致。在本地搭环境时先确认 PHP 版本、MySQL 版本、扩展是否开启再开始写代码能省掉大量排查时间。3.1 学习环境推荐 XAMPP 或 phpStudy学习环境推荐用集成环境Windows 上常见的是 XAMPP 和 phpStudy。这类工具把 Apache、PHP、MySQL、phpMyAdmin 打包在一起安装后启动 Apache 和 MySQL 两个服务即可。PHP 版本选择建议如果跟随教材或课程使用 PHP 8.0 或 8.1 都可以如果学校机房环境较老实际落地前要先确认机房安装了什么版本。下面的示例代码统一使用 PDO 和 PHP 7.4 以上兼容写法在 PHP 8.x 下也能正常执行。MySQL 版本方面MySQL 5.7 和 8.x 都常见。代码里没有使用 MySQL 8 的私有特性两个版本都能跑。唯一的差异是 MySQL 8 默认认证插件是caching_sha2_password部分老旧 PHP 驱动连接时会报Authentication plugin相关错误这个问题在后面的排查章节单独讲。3.2 验证环境是否可用启动 XAMPP 后先做下面三个检查php -v确认当前 PHP CLI 版本。再打开浏览器访问http://localhost如果显示 XAMPP 欢迎页说明 Apache 正常。接着访问http://localhost/phpmyadmin如果能打开 MySQL 管理界面说明数据库服务正常。如果想要更精确地确认 PHP 和 MySQL 的连接能力可以在项目根目录创建临时文件phpinfo.php?php phpinfo();在浏览器访问该文件搜索pdo_mysql确认扩展已经开启。这个扩展是后面 PDO 连接 MySQL 的前提。验证完记得删除该临时文件。3.3 创建数据库、导入初始数据和管理员账号打开 phpMyAdmin执行sql/init.sql的建库建表脚本。如果使用命令行可以这样导入mysql -u root -p sql/init.sql导入后需要插入管理员账号。管理员密码不能直接存明文推荐用 PHP 的password_hash()生成哈希。临时创建一个gen_hash.php?php echo password_hash(admin123, PASSWORD_DEFAULT), PHP_EOL;执行这个文件复制输出的哈希字符串然后执行INSERT INTO admin (username, password, real_name, created_at) VALUES (admin, 替换为上面生成的哈希值, 管理员, NOW());同时插入几条初始分类和景点数据方便前台页面有内容可以展示INSERT INTO scenic_category (name, sort_order, created_at) VALUES (自然风光, 1, NOW()), (人文古迹, 2, NOW()), (主题乐园, 3, NOW());3.4 检测环境是否就绪的清单检查项验证方式结果Apache 服务访问 http://localhost显示欢迎页MySQL 服务访问 http://localhost/phpmyadmin能登录数据库管理界面PHP CLI执行 php -v显示版本号pdo_mysql 扩展访问 phpinfo.php 并搜索扩展已启用数据库执行 init.sql7 张表创建成功管理员账号查询 admin 表有一条密码为哈希的记录这套清单可以在写代码之前全部确认避免后面把“环境没装好”和“代码写错”混在一起排查。4. 项目骨架目录结构、数据库连接和公共函数毕业设计代码不建议把所有文件堆在根目录也不要一上来就搞复杂的框架分层。合理的做法是前台页面放根目录后台页面放admin/目录公共代码放config/和includes/上传文件放uploads/。目录清晰答辩时能直接讲代码组织方式。4.1 推荐目录结构travel_system/ ├─ config/ │ ├─ database.php 数据库连接 │ └─ common.php 公共函数 ├─ admin/ │ ├─ login.php 管理员登录 │ ├─ index.php 后台首页 │ ├─ scenic_list.php 景点列表 │ ├─ scenic_add.php 景点新增 │ ├─ scenic_edit.php 景点编辑 │ ├─ category_list.php 分类管理 │ ├─ comment_list.php 评论审核 │ └─ user_list.php 用户管理 ├─ includes/ │ ├─ header.php 前台公共头部 │ └─ footer.php 前台公共底部 ├─ uploads/ 景点图片上传目录 ├─ sql/ │ └─ init.sql 建库建表脚本 ├─ index.php 前台首页 ├─ list.php 景点列表 ├─ detail.php 景点详情 ├─ login.php 用户登录 ├─ register.php 用户注册 └─ favorite.php 收藏处理4.2 使用 PDO 统一数据库连接数据库连接封装成单例函数项目中所有页面都通过db()获取连接对象?php // config/database.php define(DB_HOST, 127.0.0.1); define(DB_PORT, 3306); define(DB_NAME, travel_system); define(DB_USER, root); define(DB_PASS, 123456); function db(): PDO { static $pdo null; if ($pdo null) { $dsn sprintf( mysql:host%s;port%s;dbname%s;charsetutf8mb4, DB_HOST, DB_PORT, DB_NAME ); $pdo new PDO($dsn, DB_USER, DB_PASS, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ]); } return $pdo; }这里有三点值得解释。第一DSN 中必须带charsetutf8mb4否则中文可能乱码。第二ATTR_ERRMODE设为ERRMODE_EXCEPTIONSQL 出错时直接抛异常开发阶段更容易发现错误。第三ATTR_EMULATE_PREPARES设为false让 MySQL 使用真正预处理SQL 注入防护更可靠。4.3 公共函数统一处理输出和权限前台输出用户数据时必须做 HTML 转义防止 XSS。公共函数放在config/common.php?php // config/common.php function e($value): string { return htmlspecialchars((string)$value, ENT_QUOTES, UTF-8); } function current_user(): ?array { if (!isset($_SESSION[uid])) { return null; } static $user null; if ($user null) { $stmt db()-prepare( SELECT id, username, nickname, avatar FROM user WHERE id ? ); $stmt-execute([$_SESSION[uid]]); $user $stmt-fetch() ?: null; } return $user; } function require_admin(): void { if (empty($_SESSION[admin_id])) { header(Location: login.php); exit; } } function redirect(string $url): void { header(Location: . $url); exit; }页面里统一这样引入?php session_start(); require_once __DIR__ . /config/database.php; require_once __DIR__ . /config/common.php;session_start()放在文件最前面并且保证在此之前没有任何输出否则会报headers already sent错误。4.4 前台页面模板复用前台多个页面都要输出顶部导航、用户登录状态和底部版权栏。把重复部分拆到includes/header.php和includes/footer.php。includes/header.php里可以输出登录状态?php $currentUser current_user(); ? !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title?php echo isset($pageTitle) ? e($pageTitle) : 景点网站; ?/title /head body header classsite-header div classcontainer a hrefindex.php classlogo景点网/a nav a hrefindex.php首页/a a hreflist.php景点列表/a ?php if ($currentUser): ? span你好?php echo e($currentUser[nickname]); ?/span a hreffavorite.php我的收藏/a a hreflogout.php退出/a ?php else: ? a hreflogin.php登录/a a hrefregister.php注册/a ?php endif; ? /nav /div /header main classcontainer页面文件使用前先设置$pageTitle?php $pageTitle 景点详情; ?底部模板/main footer classsite-footer p旅游景点网站管理系统 · 毕业设计示例/p /footer /body /html模板复用的好处是改一次导航所有前台页面同步生效后台页面也可以照这个思路拆公共头部。5. 前台核心功能注册、登录、列表、详情和收藏前台功能是答辩时演示效果最直观的部分。下面按“用户注册登录、景点列表、景点详情、收藏、评论”顺序实现。5.1 用户注册先校验再写库注册页register.php要做两件事显示注册表单处理提交。处理逻辑统一放同一个文件的顶部成功后再重定向到登录页。?php session_start(); require_once __DIR__ . /config/database.php; require_once __DIR__ . /config/common.php; if ($_SERVER[REQUEST_METHOD] POST) { $username trim($_POST[username] ?? ); $password $_POST[password] ?? ; $confirm $_POST[confirm_password] ?? ; if ($username || $password ) { exit(用户名和密码不能为空); } if (strlen($username) 3 || strlen($username) 20) { exit(用户名长度需要在 3 到 20 个字符之间); } if (strlen($password) 6) { exit(密码长度不能少于 6 位); } if ($password ! $confirm) { exit(两次输入的密码不一致); } $pdo db(); $stmt $pdo-prepare(SELECT id FROM user WHERE username ?); $stmt-execute([$username]); if ($stmt-fetch()) { exit(用户名已存在); } $hash password_hash($password, PASSWORD_DEFAULT); $stmt $pdo-prepare( INSERT INTO user (username, password, nickname, created_at) VALUES (?, ?, ?, NOW()) ); $stmt-execute([$username, $hash, $username]); redirect(login.php); } ?这里要特别强调密码不能明文存储。password_hash()生成的是带随机盐的哈希入库后即使数据库泄露攻击者也无法直接看到原始密码。登录校验时用password_verify()两句配套使用。5.2 登录会话只保存必要信息登录页处理逻辑?php session_start(); require_once __DIR__ . /config/database.php; require_once __DIR__ . /config/common.php; if ($_SERVER[REQUEST_METHOD] POST) { $username trim($_POST[username] ?? ); $password $_POST[password] ?? ; if ($username || $password ) { exit(请输入用户名和密码); } $pdo db(); $stmt $pdo-prepare( SELECT id, username, password, status FROM user WHERE username ? ); $stmt-execute([$username]); $user $stmt-fetch(); if (!$user) { exit(用户不存在); } if ($user[status] ! 1) { exit(账号已被禁用请联系管理员); } if (!password_verify($password, $user[password])) { exit(密码错误); } $_SESSION[uid] (int)$user[id]; $_SESSION[username] $user[username]; redirect(index.php); } ?会话里只存uid和username不存密码也不存完整用户信息。每次需要当前用户资料时再通过current_user()从数据库读取。这样用户被禁用、昵称修改后页面能立即反映最新状态。退出登录就是清空会话?php session_start(); $_SESSION []; session_destroy(); header(Location: login.php); exit;5.3 景点列表、分类筛选和分页前台list.php是最容易拿分也最容易写乱的地方。分页的完整逻辑要同时处理当前页码、每页条数、总记录数、总页数和搜索关键词。?php session_start(); require_once __DIR__ . /config/database.php; require_once __DIR__ . /config/common.php; $page max(1, (int)($_GET[page] ?? 1)); $perPage 8; $keyword trim($_GET[keyword] ?? ); $categoryId (int)($_GET[category_id] ?? 0); $where WHERE s.status 1; $params []; if ($categoryId 0) { $where . AND s.category_id ?; $params[] $categoryId; } if ($keyword ! ) { $where . AND (s.name LIKE ? OR s.intro LIKE ?); $like % . $keyword . %; $params[] $like; $params[] $like; } $countSql SELECT COUNT(*) FROM scenic_spot s $where; $stmt $pdo-prepare($countSql); $stmt-execute($params); $total (int)$stmt-fetchColumn(); $pages max(1, ceil($total / $perPage)); $offset ($page - 1) * $perPage; $sql SELECT s.id, s.name, s.image, s.ticket_price, s.address, s.visit_count, c.name AS category_name FROM scenic_spot s LEFT JOIN scenic_category c ON s.category_id c.id $where ORDER BY s.sort_order ASC, s.id DESC LIMIT $offset, $perPage; $stmt $pdo-prepare($sql); $stmt-execute($params); $list $stmt-fetchAll(); ?分页代码这里用max(1, ...)防止页码小于 1。$offset用于LIMIT的偏移量$pages用于生成分页链接。前台表单也可以把keyword和category_id一起带到下一页的 URL 中防止翻页后搜索条件丢失。要特别注意的是$where里同时拼了$params查询总数和查询列表用了同一份条件顺序必须一致否则参数会错位。这就是为什么把条件拼接放在前面而不是在两条 SQL 里各写一次。5.4 景点详情和浏览量自增详情页接收id参数先查询景点如果不存在或已下架就跳回首页。?php session_start(); require_once __DIR__ . /config/database.php; require_once __DIR__ . /config/common.php; $id (int)($_GET[id] ?? 0); if ($id 0) { redirect(index.php); } $pdo db(); $stmt $pdo-prepare( SELECT s.*, c.name AS category_name FROM scenic_spot s LEFT JOIN scenic_category c ON s.category_id c.id WHERE s.id ? AND s.status 1 ); $stmt-execute([$id]); $spot $stmt-fetch(); if (!$spot) { redirect(index.php); } $pdo-prepare( UPDATE scenic_spot SET visit_count visit_count 1 WHERE id ? )-execute([$id]);详情页输出时所有动态值都用e()转义h1?php echo e($spot[name]); ?/h1 p分类?php echo e($spot[category_name]); ?/p p价格?php echo e($spot[ticket_price]); ? 元/p p地址?php echo e($spot[address]); ?/p p浏览数?php echo (int)$spot[visit_count]; ?/p img src?php echo e($spot[image]); ? alt景点图片 div?php echo $spot[detail]; ?/div详情内容如果是富文本本身允许包含 HTML所以不能走e()转义但这也是 XSS 风险点。安全做法是后台编辑时只允许指定标签或者使用 HTML 过滤库。毕业设计阶段至少要做到“详情内容仅管理员可编辑”并明确写出这个限制。5.5 收藏唯一索引配合 INSERT IGNORE收藏功能用独立文件处理通过action参数区分添加和取消?php session_start(); require_once __DIR__ . /config/database.php; if (!isset($_SESSION[uid])) { redirect(login.php); } $scenicId (int)($_GET[scenic_id] ?? 0); $action $_GET[action] ?? add; if ($scenicId 0) { redirect(list.php); } $pdo db(); $stmt $pdo-prepare(SELECT id FROM scenic_spot WHERE id ? AND status 1); $stmt-execute([$scenicId]); if (!$stmt-fetch()) { redirect(list.php); } if ($action add) { $stmt $pdo-prepare( INSERT IGNORE INTO favorite (user_id, scenic_id, created_at) VALUES (?, ?, NOW()) ); $stmt-execute([$_SESSION[uid], $scenicId]); } elseif ($action remove) { $stmt $pdo-prepare( DELETE FROM favorite WHERE user_id ? AND scenic_id ? ); $stmt-execute([$_SESSION[uid], $scenicId]); } redirect(detail.php?id . $scenicId);“我的收藏”页面把favorite和scenic_spot做内连接查询$stmt $pdo-prepare( SELECT s.id, s.name, s.image, s.ticket_price, f.created_at AS favorite_time FROM favorite f INNER JOIN scenic_spot s ON f.scenic_id s.id WHERE f.user_id ? AND s.status 1 ORDER BY f.created_at DESC ); $stmt-execute([$_SESSION[uid]]); $favorites $stmt-fetchAll();涉及列表页和详情页时要注意收藏列表只显示仍处于上架状态的景点因此查询条件里带上s.status 1避免用户收藏的景点被下架后仍然显示。5.6 评论前台提交后台审核后公开发表评论要登录内容先插入comment表状态为待审核管理员审核通过后才在前台展示?php session_start(); require_once __DIR__ . /config/database.php; require_once __DIR__ . /config/common.php; if (!isset($_SESSION[uid])) { redirect(login.php); } if ($_SERVER[REQUEST_METHOD] POST) { $scenicId (int)($_POST[scenic_id] ?? 0); $content trim($_POST[content] ?? ); if ($scenicId 0 || $content ) { exit(评论内容不能为空); } if (mb_strlen($content) 500) { exit(评论内容不能超过 500 字); } $stmt $pdo-prepare( INSERT INTO comment (scenic_id, user_id, content, status, created_at) VALUES (?, ?, ?, 0, NOW()) ); $stmt-execute([$scenicId, $_SESSION[uid], $content]); redirect(detail.php?id . $scenicId); }前台详情页只查询已审核通过的评论$stmt $pdo-prepare( SELECT c.content, c.created_at, u.nickname FROM comment c INNER JOIN user u ON c.user_id u.id WHERE c.scenic_id ? AND c.status 1 ORDER BY c.created_at DESC ); $stmt-execute([$id]); $comments $stmt-fetchAll();这里用mb_strlen判断中文字符长度而不是strlen因为strlen会把一个中文当成 3 个字节。6. 后台管理登录校验、景点维护和评论审核后台的代码思想比具体页面更重要每个后台页面都必须先做登录校验每个写操作都必须先验证提交数据。6.1 管理员登录和统一权限校验后台登录页基本逻辑与前台登录一致只是换了一张admin表会话变量换成admin_id?php session_start(); require_once __DIR__ . /../config/database.php; require_once __DIR__ . /../config/common.php; if ($_SERVER[REQUEST_METHOD] POST) { $username trim($_POST[username] ?? ); $password $_POST[password] ?? ; if ($username || $password ) { exit(请输入账号和密码); } $stmt db()-prepare( SELECT id, username, password, real_name FROM admin WHERE username ? ); $stmt-execute([$username]); $admin $stmt-fetch(); if (!$admin || !password_verify($password, $admin[password])) { exit(账号或密码错误); } $_SESSION[admin_id] (int)$admin[id]; $_SESSION[admin_name] $admin[real_name] ?: $admin[username]; db()-prepare( UPDATE admin SET last_login_time NOW() WHERE id ? )-execute([$admin[id]]); redirect(index.php); } ?后台每个管理页面顶部第一件事就是require_admin()?php session_start(); require_once __DIR__ . /../config/database.php; require_once __DIR__ . /../config/common.php; require_admin();不需要在每一个页面重复写“判断是否登录”一个require_admin()函数
返回列表