ARTICLE DETAIL

资讯详情

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

PHP+MySQL打造非遗文化展示与体验预约系统:以广州榄雕为例

PHP+MySQL打造非遗文化展示与体验预约系统:以广州榄雕为例 广州榄雕是非物质文化遗产项目中很有代表性的传统技艺围绕它开发一个“文化展示 体验预约”的系统是目前不少计算机毕业设计和课程设计里比较亮眼的选题。它不是一个简单的信息展示站而是要把非遗内容、用户注册、线上预约、后台审核、课程管理串成一条完整的业务链路。这篇文章使用 PHP MySQL 作为后端技术栈从数据库设计到前台展示、预约提交、后台审核完整走一遍开发流程。无论你是正在找毕业设计题目的学生还是想把本地管理系统练熟的开发者都可以按这篇文章的思路落地一个可运行的项目。为了让内容更贴近真实开发我会把“广州榄雕非遗文化展示和体验预约系统”拆成两个核心部分一部分是面向浏览者的非遗文化展示和预约入口另一部分是面向管理员的体验课程管理和预约审核后台。前端页面可以用 PHP 直接渲染也保留改造为 Vue 前后端分离的思路我把两种方式的衔接点也写清楚。1. 非遗文化展示与体验预约系统能做什么1.1 功能需求分析先不急着写代码我们先把需求理清楚。一个合格的“非遗文化展示 体验预约”系统至少包含下面几块能力第一文化展示模块。系统要展示广州榄雕的历史背景、代表作品、传承人、工艺流程等信息。比较常见的做法是做一个图文列表页用户进入首页后能浏览所有非遗项目点击进入详情页查看完整介绍同时在详情页展示相关的体验课程。第二在线预约模块。用户看到某个体验课程后可以选择日期、时间段、人数填写联系方式和备注提交预约申请。提交后预约状态默认为“待审核”管理员在后台审核通过或拒绝。第三用户管理模块。用户需要注册和登录才能预约。如果没有登录预约前要跳转到登录页面登录成功后用户可以在“我的预约”中查看自己的预约记录和审核状态也可以取消尚未开始的预约。第四后台管理模块。管理员登录后台后可以维护非遗项目内容、体验课程、预约记录并且处理预约审核。管理员不需要像普通用户一样注册直接在数据库或初始化脚本里创建账号即可。1.2 为什么用 PHP 来实现这类管理系统很多毕设系统会纠结语言选型项目标题里也出现了 PHP、Python、Vue 等标签。对于这类“管理系统”和“网页制作”场景PHP 的优势其实很明显部署简单本地用集成环境就可以跑起来不需要像 Python 那样做额外的虚拟环境配置也不需要手写 Node 服务。PHP 和 MySQL 配合非常成熟PDO 预处理方式可以很好地防止 SQL 注入代码门槛较低。动态网页渲染能力强一个 PHP 文件既能做业务逻辑又能输出 HTML适合快速完成功能演示。网上 PHP 管理系统的学习资料非常多遇到报错容易检索。如果你的导师或答辩老师更希望看到前后端分离架构也可以把 PHP 当成后端接口层只提供 JSON 数据前端使用 Vue 来渲染。文章最后我会给一个最简单的接口改造示例方便你横向扩展。1.3 系统整体流程整个预约流程可以概括为游客访问非遗文化展示页面浏览榄雕项目介绍。用户点击体验课程进入预约表单。如果未登录系统引导用户先登录已登录用户直接填写表单。用户提交预约后系统校验预约日期、时间段和名额。预约成功生成预约单号状态为“待审核”。管理员登录后台查看预约列表审核通过或拒绝。用户进入“我的预约”查看审核结果。这个流程并不复杂但把用户角色、业务状态和数据库表关系都串起来了是典型的“管理系统”实战案例。2. 开发环境准备与项目结构2.1 本地 PHP 运行环境为了快速搭建本地开发环境建议使用 PHPStudy 这类集成环境软件它内置了 Apache/Nginx、PHP 和 MySQL可以减少很多环境配置成本。版本方面不需要刻意追求最新以当前常见环境为例PHP 使用 7.4 或者 8.0 都可以MySQL 使用 5.7 或 8.0 均可。如果你在自己电脑上使用的是其他版本功能上也没有太大区别重点是确保 PHP 开启了pdo_mysql扩展。安装好集成环境后将项目代码放在网站根目录下例如phpstudy_pro/WWW/lan_diao然后在浏览器中访问http://localhost/lan_diao/public/index.php如果使用的是 Apache 或 Nginx还需要把虚拟主机或站点根目录指向项目的public目录这样更方便管理入口文件。2.2 项目目录规划为了让代码结构清晰同时又不至于让新手感到过于复杂我建议使用下面这套轻量目录结构lan_diao/ ├── config/ │ └── config.php # 数据库等全局配置 ├── public/ │ ├── index.php # 前台入口 │ ├── css/ │ └── js/ ├── app/ │ ├── controllers/ │ │ ├── HeritageController.php # 非遗展示 │ │ ├── AppointmentController.php # 预约 │ │ ├── UserController.php # 用户登录注册 │ │ └── AdminController.php # 后台管理 │ ├── models/ │ │ ├── HeritageModel.php │ │ ├── AppointmentModel.php │ │ └── UserModel.php │ └── views/ │ ├── heritage/ │ ├── appointment/ │ ├── user/ │ └── admin/ ├── uploads/ │ └── images/ # 非遗图片上传目录 └── db.php # PDO 连接封装如果你觉得 M V C 分层概念还比较陌生也可以先使用扁平目录把所有 PHP 页面放在一个目录下例如index.php、heritage_list.php、appointment_add.php等。我个人更推荐上面这个结构因为后期扩展功能、拆分为 Vue 接口时会更顺手。2.3 先准备好数据库在开发前先创建数据库。使用数据库管理工具 phpMyAdmin 或命令行都可以。下面给出命令行创建方式CREATE DATABASE lan_diao DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;数据库名我设置为lan_diao。字符集使用utf8mb4可以正常存储生僻字和特殊符号避免非遗介绍中出现生僻字乱码。3. 数据库设计核心表结构与建表 SQL数据库是这类管理系统的核心。如果表结构设计不合理后面做预约冲突校验时会非常被动。这里我设计了 5 张表管理员表、用户表、非遗项目表、体验课程表、预约记录表。3.1 表结构总览表名说明关键字段admin_user后台管理员username, password, statususer前台注册用户username, password, phone, emailheritage非遗项目与文化展示内容title, category, cover_image, contentexperience体验课程/活动title, max_people, price, locationappointment预约记录order_no, user_id, experience_id, appointment_date, time_slot, status这里有一个设计细节非遗文化展示内容heritage和体验课程experience是分开的因为一条非遗文化可以对应多个体验课程比如“广州榄雕历史文化”可以配一个参观讲解课也可以配一个动手雕刻体验课。分开建表更灵活。3.2 建表 SQLUSE lan_diao; -- 管理员表 CREATE TABLE admin_user ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT 管理员账号, password VARCHAR(255) NOT NULL COMMENT password_hash 加密结果, real_name VARCHAR(50) NOT NULL DEFAULT COMMENT 管理员姓名, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, last_login_time DATETIME DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT管理员表; -- 前台用户表 CREATE TABLE user ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(255) NOT NULL COMMENT password_hash 加密结果, phone VARCHAR(20) NOT NULL DEFAULT COMMENT 手机号, email VARCHAR(100) NOT NULL DEFAULT COMMENT 邮箱, avatar VARCHAR(255) NOT NULL DEFAULT COMMENT 头像地址, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT前台用户表; -- 非遗项目表 CREATE TABLE heritage ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL COMMENT 非遗项目标题, category VARCHAR(50) NOT NULL DEFAULT COMMENT 分类如 榄雕历史/传承人/工艺流程, cover_image VARCHAR(255) NOT NULL DEFAULT COMMENT 封面图片, intro VARCHAR(500) NOT NULL DEFAULT COMMENT 一句话简介, content LONGTEXT COMMENT 详细介绍, view_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 浏览次数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1发布 0下架, sort_order INT NOT NULL DEFAULT 0 COMMENT 排序值越小越靠前, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT非遗项目表; -- 体验课程表 CREATE TABLE experience ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, heritage_id INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 关联非遗项目ID, title VARCHAR(100) NOT NULL COMMENT 体验课程名称, cover_image VARCHAR(255) NOT NULL DEFAULT , intro VARCHAR(500) NOT NULL DEFAULT , location VARCHAR(255) NOT NULL DEFAULT COMMENT 体验地点, duration INT UNSIGNED NOT NULL DEFAULT 90 COMMENT 课程时长分钟, max_people INT UNSIGNED NOT NULL DEFAULT 10 COMMENT 每场最大预约人数, price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 体验价格0表示公益免费, content LONGTEXT COMMENT 课程详细介绍, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT体验课程表; -- 预约记录表 CREATE TABLE appointment ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 预约单号, user_id INT UNSIGNED NOT NULL COMMENT 用户ID, experience_id INT UNSIGNED NOT NULL COMMENT 体验课程ID, appointment_date DATE NOT NULL COMMENT 预约日期, time_slot VARCHAR(20) NOT NULL COMMENT 时间段如 09:00-11:00, people_count INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 预约人数, contact_name VARCHAR(50) NOT NULL COMMENT 联系人, contact_phone VARCHAR(20) NOT NULL COMMENT 联系电话, remark VARCHAR(500) NOT NULL DEFAULT COMMENT 备注, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1已通过 2已拒绝 3已完成 4已取消, reject_reason VARCHAR(255) NOT NULL DEFAULT COMMENT 拒绝原因, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_exp_date (experience_id, appointment_date, time_slot), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约记录表;3.3 索引与引擎说明预约表里的组合索引idx_exp_date非常重要。实际查询预约记录时最常用的条件就是“某个体验课程在某个日期某个时间段内已经预约了多少人”这个组合索引可以直接命中查询速度会明显提升。另外所有表都要使用 InnoDB 引擎。原因很直接InnoDB 支持事务和行级锁在预约提交场景中我们要避免两个用户同时抢同一个时间段导致预约人数超员。如果使用 MyISAM表锁会导致并发能力差事务控制也不好实现。4. 公共层开发数据库连接与公共函数4.1 数据库连接封装在项目根目录创建一个db.php文件统一封装 PDO 连接。这样做的好处是每个页面如果需要数据库操作直接调用db()函数即可不需要重复写连接代码。?php // db.php function db(): PDO { static $pdo null; if ($pdo null) { $config require __DIR__ . /config/config.php; $dsn sprintf( mysql:host%s;port%s;dbname%s;charset%s, $config[host], $config[port], $config[dbname], $config[charset] ); $pdo new PDO($dsn, $config[username], $config[password], [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, ]); } return $pdo; }config/config.php文件内容?php // config/config.php return [ host 127.0.0.1, port 3306, dbname lan_diao, username root, password root, charset utf8mb4, ];这里有一个容易被忽略的点PDO 构造方法的参数列表中charset要写在 DSN 里不要在连接成功后单独执行SET NAMES这样更规范。同时开启ERRMODE_EXCEPTION这样 SQL 出错时能直接抛出异常方便调试。4.2 公共函数建立一个helpers.php放入一些通用的函数。比如获取当前登录用户、判断用户是否登录、格式化日期等。后面多个控制器和页面都会用到。?php // helpers.php function is_logged_in(): bool { return !empty($_SESSION[user_id]); } function current_user(): ?array { if (empty($_SESSION[user_id])) { return null; } $stmt db()-prepare(SELECT id, username, phone, email, avatar FROM user WHERE id ?); $stmt-execute([$_SESSION[user_id]]); return $stmt-fetch() ?: null; } function require_login(): void { if (!is_logged_in()) { header(Location: /login.php?redirect . urlencode($_SERVER[REQUEST_URI])); exit; } } function e(?string $value): string { return htmlspecialchars($value ?? , ENT_QUOTES, UTF-8); }其中e()函数是输出前转义函数。所有从数据库中读取并输出到页面的内容都应该经过e()处理目的是防止 XSS 攻击。这是一个非常值得养成习惯的安全操作。4.3 用户登录注册基础逻辑用户登录注册逻辑比较标准这里只展示登录验证片段?php // login.php 核心逻辑 session_start(); if ($_SERVER[REQUEST_METHOD] POST) { $username trim($_POST[username] ?? ); $password $_POST[password] ?? ; $stmt db()-prepare(SELECT * FROM user WHERE username ? AND status 1 LIMIT 1); $stmt-execute([$username]); $user $stmt-fetch(); if ($user password_verify($password, $user[password])) { $_SESSION[user_id] $user[id]; header(Location: /index.php); exit; } $error 用户名或密码错误; }注册时密码一定不要明文存储要使用password_hash()生成哈希值$hashed password_hash($password, PASSWORD_DEFAULT); $stmt db()-prepare(INSERT INTO user (username, password, phone) VALUES (?, ?, ?)); $stmt-execute([$username, $hashed, $phone]);后台管理员登录逻辑与用户登录一致只是判断的字段改成了管理员表并把admin_id写入 session。5. 前台非遗文化展示模块5.1 非遗项目列表页首页的核心是把非遗项目列出来。使用一个很简单的查询注意只查询status 1的已发布内容下架内容不在前台出现。?php // index.php require __DIR__ . /db.php; require __DIR__ . /helpers.php; $stmt db()-query( SELECT id, title, category, cover_image, intro, view_count FROM heritage WHERE status 1 ORDER BY sort_order ASC, id DESC ); $heritageList $stmt-fetchAll(); ? !DOCTYPE html html langzh-CN head meta charsetUTF-8 title广州榄雕非遗文化展示与体验预约/title /head body div classcontainer h1广州榄雕非遗文化展示/h1 ?php if (empty($heritageList)): ? p暂无非遗内容请在后台添加。/p ?php else: ? div classheritage-grid ?php foreach ($heritageList as $item): ? div classheritage-card ?php if ($item[cover_image]): ? img src? e($item[cover_image]) ? alt? e($item[title]) ? ?php endif; ? h2? e($item[title]) ?/h2 p classcategory? e($item[category]) ?/p p classintro? e($item[intro]) ?/p a hrefheritage_detail.php?id? (int)$item[id] ?查看详情/a /div ?php endforeach; ? /div ?php endif; ? /div /body /html这段代码包含了一个很基础的模板循环。列表页不需要复杂的分页因为非遗项目通常数量有限但如果后续内容多了建议加上 LIMIT 分页。5.2 非遗详情页详情页接收id参数查询对应项目并且把浏览量加一。浏览量加一的处理方式需要注意不要先查询一次再更新一次直接使用 SQL 的view_count view_count 1原子更新避免并发时计数不准确。?php // heritage_detail.php require __DIR__ . /db.php; require __DIR__ . /helpers.php; $id (int)($_GET[id] ?? 0); $stmt db()-prepare(SELECT * FROM heritage WHERE id ? AND status 1 LIMIT 1); $stmt-execute([$id]); $heritage $stmt-fetch(); if (!$heritage) { http_response_code(404); exit(非遗项目不存在或已下架); } db()-prepare(UPDATE heritage SET view_count view_count 1 WHERE id ?)-execute([$id]); ? !DOCTYPE html html langzh-CN head meta charsetUTF-8 title? e($heritage[title]) ?/title /head body article h1? e($heritage[title]) ?/h1 p浏览 ? (int)$heritage[view_count] ? 次/p div? $heritage[content] ?/div /article /body /html这里有一个需要特别说明的地方content字段如果保存的是富文本内容那么输出时不应该经过htmlspecialchars否则 HTML 标签会被全部转义。实际开发中要么在富文本入库前做好白名单过滤要么只允许管理员在后台录入内容降低 XSS 风险。如果是普通文本则应该继续使用e()。5.3 相关体验课程展示非遗详情页除了展示文化介绍还应该把关联的体验课程列出来用户看完文化内容后可以直接预约。体验课程表里有heritage_id字段查询时按该字段关联即可。$stmt db()-prepare( SELECT id, title, intro, max_people, price, duration, location FROM experience WHERE heritage_id ? AND status 1 ORDER BY id DESC ); $stmt-execute([$heritage[id]]); $experienceList $stmt-fetchAll();每个体验课程卡片上都放一个“立即预约”按钮跳转到appointment_add.php?experience_id课程ID。6. 体验预约模块开发体验预约是这套系统的核心业务也是最容易在答辩或测试时暴露问题的地方。下面我从预约表单、提交逻辑、防超员、我的预约四个方面来讲。6.1 预约流程设计预约时需要确定几个关键信息体验课程从哪个课程入口进入就带着哪个课程的 id。预约日期用户选择未来某一天。时间段为了简化管理建议预设固定时间段例如09:00-11:00、14:00-16:00、16:30-18:30不要让用户任意输入。预约人数用户本次预约几个人参加默认 1。联系人、联系电话、备注方便管理员联系和确认。时间段为什么要求固定选项因为如果不固定后台审核时很难判断场次冲突也难统计每个时段的名额。固定时间段后名额校验只需要对experience_id appointment_date time_slot这三个字段做判断。6.2 预约表单与提交逻辑在appointment_add.php中先根据experience_id查询课程信息如果课程不存在或下架直接提示错误。用户点击提交后进行基础校验和业务校验。?php // appointment_add.php 核心逻辑 session_start(); require __DIR__ . /db.php; require __DIR__ . /helpers.php; require_login(); $experienceId (int)($_GET[experience_id] ?? $_POST[experience_id] ?? 0); $stmt db()-prepare(SELECT * FROM experience WHERE id ? AND status 1 LIMIT 1); $stmt-execute([$experienceId]); $experience $stmt-fetch(); if (!$experience) { exit(体验课程不存在或已下架); } $allowSlots [09:00-11:00, 14:00-16:00, 16:30-18:30]; if ($_SERVER[REQUEST_METHOD] POST) { $date trim($_POST[appointment_date] ?? ); $timeSlot trim($_POST[time_slot] ?? ); $peopleCount (int)($_POST[people_count] ?? 1); $contactName trim($_POST[contact_name] ?? ); $contactPhone trim($_POST[contact_phone] ?? ); $remark trim($_POST[remark] ?? ); $errors []; if (strtotime($date) false || strtotime($date) strtotime(date(Y-m-d))) { $errors[] 请选择有效的预约日期; } if (!in_array($timeSlot, $allowSlots, true)) { $errors[] 请选择合法的时间段; } if ($peopleCount 1 || $peopleCount 10) { $errors[] 预约人数必须在1到10之间; } if ($contactName ) { $errors[] 请填写联系人; } if (!preg_match(/^1[3-9]\d{9}$/, $contactPhone)) { $errors[] 请填写正确的手机号; } if (empty($errors)) { // 执行事务校验与插入 } else { foreach ($errors as $err) { echo p stylecolor:red . e($err) . /p; } } } ?这里有两个细节值得关注第一时间段的合法性一定要做白名单校验避免用户通过构造请求绕过前端限制提交任意内容第二手机号校验要用正则保证后台联系时信息准确。6.3 事务处理防止超员预约这是预约系统最重要的技术点。如果只做普通 INSERT两个用户同时提交同一个课程、同一个日期、同一个时间段就有可能出现预约人数超出max_people的情况。解决思路是使用事务 SELECT ... FOR UPDATE行锁。先查体验课程并锁定该行再统计当前已预约人数判断是否还有名额最后插入预约记录并提交。try { $pdo db(); $pdo-beginTransaction(); // 锁定体验课程记录防止并发修改 $stmt $pdo-prepare(SELECT * FROM experience WHERE id ? AND status 1 LIMIT 1 FOR UPDATE); $stmt-execute([$experienceId]); $experience $stmt-fetch(); if (!$experience) { throw new RuntimeException(体验课程不存在或已下架); } // 统计该时段已预约人数待审核和已通过都占用名额 $stmt $pdo-prepare( SELECT COALESCE(SUM(people_count), 0) AS total FROM appointment WHERE experience_id ? AND appointment_date ? AND time_slot ? AND status IN (0, 1) ); $stmt-execute([$experienceId, $date, $timeSlot]); $booked (int) $stmt-fetch()[total]; if ($booked $peopleCount $experience[max_people]) { throw new RuntimeException(该时间段剩余名额不足请选择其他时段); } // 生成预约单号 $orderNo date(YmdHis) . mt_rand(1000, 9999); $stmt $pdo-prepare( INSERT INTO appointment (order_no, user_id, experience_id, appointment_date, time_slot, people_count, contact_name, contact_phone, remark, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0) ); $stmt-execute([ $orderNo, $_SESSION[user_id], $experienceId, $date, $timeSlot, $peopleCount, $contactName, $contactPhone, $remark ]); $pdo-commit(); header(Location: appointment_my.php); exit; } catch (Throwable $e) { $pdo-rollBack(); $error $e-getMessage(); }这里有几个点要解释清楚第一FOR UPDATE是 InnoDB 的行级排他锁。事务提交或回滚后锁自动释放。它保证同一个体验课程在同一时刻只有一个请求能执行到后续的名额判断和插入。第二统计已预约人数时状态取了IN (0, 1)也就是“待审核”和“已通过”都会占用名额。如果某个预约被拒绝或用户主动取消就释放名额。第三order_no作为预约单号在表结构里已经加了唯一约束这样即使并发情况下生成订单号重复数据库也会拦截避免脏数据。6.4 我的预约与取消操作用户提交预约后需要有一个页面能看到自己的预约列表。查询时根据user_id过滤并且关联体验课程名称。?php // appointment_my.php 核心逻辑 session_start(); require __DIR__ . /db.php; require __DIR__ . /helpers.php; require_login(); $user current_user(); $stmt db()-prepare( SELECT a.*, e.title AS experience_title, e.location FROM appointment a LEFT JOIN experience e ON a.experience_id e.id WHERE a.user_id ? ORDER BY a.create_time DESC ); $stmt-execute([$user[id]]); $appointments $stmt-fetchAll();展示时状态字段可以用一个数组映射成中文$statusMap [ 0 待审核, 1 已通过, 2 已拒绝, 3 已完成, 4 已取消, ];取消预约的操作相对简单用户只能取消“待审核”或“已通过”状态的预约而且应该在预约开始前取消。取消时修改状态为4即可$stmt db()-prepare( UPDATE appointment SET status 4 WHERE id ? AND user_id ? AND status IN (0, 1) ); $stmt-execute([$appointmentId, $user[id]]);注意SQL 的 WHERE 条件里必须同时带上user_id防止用户通过修改请求参数操作其他人的预约。这就是最基础的数据权限控制。7. 后台管理模块开发7.1 管理员登录后台的入口建议单独放在admin/目录下与前台页面隔离。登录逻辑与前台用户登录类似但 session 键名使用admin_id并且进入后台每个页面时都要校验。?php // admin/index.php 进入后台前的统一校验 session_start(); if (empty($_SESSION[admin_id])) { header(Location: admin_login.php); exit; }登录成功时可以顺手更新管理员最后登录时间$stmt db()-prepare(UPDATE admin_user SET last_login_time NOW() WHERE id ?); $stmt-execute([$admin[id]]);7.2 非遗与体验课程管理这部分本质是 CRUD不需要过度设计。以非遗项目新增为例需要考虑图片上传。图片上传是管理系统常见的扩展点这里给出一个简化的文件上传处理// admin/heritage_save.php 图片上传部分 $targetDir __DIR__ . /../../uploads/images/; if (!is_dir($targetDir)) { mkdir($targetDir, 0775, true); } $fileName date(YmdHis) . _ . mt_rand(1000, 9999) . . . strtolower(pathinfo($_FILES[cover_image][name], PATHINFO_EXTENSION)); $targetFile $targetDir . $fileName; // 仅允许常见图片格式 $allowTypes [jpg, jpeg, png, gif, webp]; $ext strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); if (!in_array($ext, $allowTypes, true)) { exit(不支持的图片格式); } if (move_uploaded_file($_FILES[cover_image][tmp_name], $targetFile)) { $coverImage /uploads/images/ . $fileName; } else { $coverImage ; }保存到数据库时cover_image字段存放的是浏览器可访问的 URL 路径而不是服务器绝对路径。这一点很重要否则前端页面img标签无法正常显示图片。非遗项目和体验课程的管理页面结构相似列表页显示所有记录编辑页更新内容删除操作需要二次确认避免误删。对于管理后台建议使用简单清晰的表格布局再配合一个后台管理模板例如 Layui 或 AdminLTE能让界面专业很多。7.3 预约审核后台预约列表需要连表查询把体验课程和用户信息带出来?php // admin/appointment_manage.php 核心逻辑 $stmt db()-query( SELECT a.*, e.title AS experience_title, u.username FROM appointment a LEFT JOIN experience e ON a.experience_id e.id LEFT JOIN user u ON a.user_id u.id ORDER BY a.create_time DESC ); $appointments $stmt-fetchAll();审核时管理员点击“通过”或“拒绝”提交到appointment_review.php$id (int)($_POST[id] ?? 0); $status (int)($_POST[status] ?? 0); $rejectReason trim($_POST[reject_reason] ?? ); if ($status 1) { $rejectReason ; } $stmt db()-prepare(UPDATE appointment SET status ?, reject_reason ? WHERE id ?); $stmt-execute([$status, $rejectReason, $id]);这里建议在状态变更时增加一次校验如果操作的是“通过”可以重新检查该时段剩余名额防止管理员在用户取消后又超额通过。毕竟审核也是异步操作直接用事务提交会更稳妥。8. 常见问题与排查思路PHP 管理系统开发中报错和异常情况非常多。我整理了几个高频问题你可以直接对照排查。问题现象常见原因解决思路连接数据库报 1045 Access denied数据库账号或密码错误检查 config 配置使用 phpMyAdmin 验证账号连接数据库报 Connection refusedMySQL 服务未启动或端口不是 3306启动 MySQL核对端口配置页面中文显示乱码页面、数据库、连接字符集不一致统一使用 utf8mb4并在 HTML head 设置 charset图片上传后访问 404上传文件保存路径与访问 URL 不一致确认uploads根目录映射关系预约提示“名额不足”但明明有人取消取消预约状态未更新或统计状态范围不对检查 status 状态范围确认已取消不占名额后台登录后刷新又跳到登录页session 未开启或 session 保存异常检查每个后台文件是否执行 session_start()当前页面报未定义函数 db()未引入 db.php 或文件路径不对使用 require_once 引入公共层文件富文本内容显示为 HTML 源码输出前使用了 htmlspecialchars富文本直接输出普通文本转义输出8.1 环境与连接类问题本地开发最常见的是数据库连接失败原因通常集中在三处MySQL 没有启动、账号密码不对、配置中 host 写错。我建议在db()函数里开启ERRMODE_EXCEPTION这样报错信息里会直接显示连接失败原因不用猜测。如果使用 PHP 8 环境部分老式mysql_*函数已经移除应统一使用 PDO 或 mysqli。本文所有示例都使用 PDO兼容 PHP 7 和 PHP 8。8.2 预约功能典型问题预约功能的“超员”问题很难通过页面测试暴露通常需要并发模拟。这里推荐一个简单的自查方式连续打开两个浏览器窗口用不同账号同时提交同一个时间段观察是否会出现超过max_people的记录。如果出现说明事务FOR UPDATE没写对或者代码在beginTransaction之前先执行了其他查询。另一个常见问题是状态混乱。如果预约状态机不统一比如把“待审核”设成 1那统计名额时就要对应修改IN条件。建议把状态常量提前写好不要在使用时随手写数字。8.3 文件上传与编码问题图片上传失败大多是目录权限问题。Linux 环境下uploads/images目录需要有写权限可以用chmod -R 775 uploadsWindows 下一般不会遇到这个问题但如果使用 Nginx需要确保client_max_body_size配置足够大否则上传稍大的图片会直接报 413 错误。9. 工程化与安全建议很多毕设项目能跑通但经不起推敲主要是因为忽略了安全和工程化细节。下面这些建议不需要增加太多工作量却能在答辩和后续维护中明显加分。9.1 安全底线预处理、哈希、权限第一所有 SQL 语句都要使用 PDO 预处理严禁直接拼接用户输入。尤其是后台管理页面更要警惕通过查询参数构造 SQL 注入。第二所有密码必须使用password_hash()加密存储登录时用password_verify()校验。控制台中不要出现明文密码。第三后台管理页面必须做独立登录校验。建议把后台相关页面统一放到一个目录下在入口文件中统一校验admin_id避免遗漏。第四用户操作自己的数据时必须携带user_id作为 WHERE 条件。预约取消、修改个人信息等接口都要这样做防止水平越权。9.2 状态与数据一致性预约状态建议使用状态机思想来管理0待审核1已通过2已拒绝3已完成4已取消不是所有状态之间都能随意跳转。例如已拒绝的预约不能又改成已完成已取消的预约也不能再审核通过。如果你希望在代码中体现这一层约束可以在更新前先查询当前状态再判断是否允许目标状态$allowTransition [ 0 [1, 2, 4], 1 [3, 4], 2 [], 3 [], 4 [], ]; if (!in_array($newStatus, $allowTransition[$currentStatus] ?? [], true)) { throw new RuntimeException(非法的状态变更); }9.3 再次谈谈 Vue 前端改造如果你希望把系统改造成 Vue PHP 接口的前后端分离架构其实不需要重写业务逻辑。只需要让 PHP 文件只输出 JSON不再输出 HTML然后在页面中通过 Vue 的axios或fetch去请求这些接口即可。一个最简单的 JSON 接口示例?php // api/heritage_list.php require __DIR__ . /../db.php; header(Content-Type: application/json; charsetutf-8); $stmt db()-query(SELECT id, title, category, cover_image, intro FROM heritage WHERE status 1); echo json_encode([code 0, data $stmt-fetchAll()]);前端 Vue 部分可以是这样的思路// Vue 3 示例 const app Vue.createApp({ data() { return { heritageList: [] }; }, mounted() { fetch(/api/heritage_list.php) .then(res res.json()) .then(res { if (res.code 0) { this.heritageList res.data; } }); } }); app.mount(#app);在正式比赛或毕设答辩中能说清楚“为什么用 PHP 做后端、为什么用 Vue 做渲染”是一个加分项。需要特别注意的是前后端分离后登录态可以继续使用 session也可以通过接口返回 token。如果只是本地毕设演示session 方案足够简单稳定不需要引入 JWT 等复杂机制。9.4 部署与备份本地开发完成后如果要部署到服务器建议使用宝塔面板或直接使用 Nginx PHP-FPM。部署前重点检查三件事数据库连接配置是否修改为服务器参数。uploads目录权限是否正确。关闭 PHP 错误展示将错误日志写入文件。数据库备份是最容易被忽略的工作。开发期间可以每天用命令行导出一次mysqldump -u root -p lan_diao lan_diao_backup.sql恢复时使用mysql -u root -p lan_diao lan_diao_backup.sql涉及线上数据结构的修改一定先备份再在测试环境验证 SQL 语句最后在生产环境执行。10. 总结与下一步学习建议通过这篇文章我们完整搭建了一个基于 PHP MySQL 的广州榄雕非遗文化展示和体验预约系统。在功能上它覆盖了
返回列表