ARTICLE DETAIL

资讯详情

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

PHP美容院预约系统:并发控制与状态管理实战

PHP美容院预约系统:并发控制与状态管理实战 简介这是一份面向计算机专业本科生的毕业设计实战项目资源聚焦美容院客户预约管理场景解决传统人工排班低效、客户信息难追溯、服务流程不透明等运营痛点。系统基于PHP开发完整实现用户注册登录、客户预约与状态跟踪、美容服务分类管理、员工排班与绩效记录等核心模块兼顾安全性密码加密存储与用户体验Ajax异步交互、短信/邮件提醒。资源包为ZIP格式大小25.01MB包含可运行的前后端代码、数据库脚本及基础配置说明主要文件类型涵盖.php业务逻辑文件、.sql数据库结构与初始数据、HTML/CSS/JS前端页面及配置文档结构清晰便于二次开发与功能拓展。已有836人学习下载适合PHP初学者通过真实业务场景掌握Web全栈开发流程快速复现一个具备登录鉴权、CRUD操作、预约冲突校验等典型功能的中小型管理系统。1. 这不是又一个“登录列表”的PHP练手项目而是一套能跑通真实预约闭环的美容院业务系统很多毕业设计选题写着“XX管理系统”实际只做到用户增删改查和静态页面跳转一到“客户下午三点要来做光子嫩肤但美容师A刚被调去接待VIP客户系统能否自动把预约挪给有空档且擅长该项目的美容师B”就卡住。这个美容院管理系统不同——它从数据库字段设计开始就锚定真实业务约束服务项绑定技能标签如“射频紧肤”→“持证操作员≥2人”、员工排班表与预约时间轴双向校验、同一时段同一服务类型不可超员预约。它用原生PHPMySQL实现不依赖Laravel等框架的ORM魔法所有SQL语句可追踪、可调试、可压测。适合两类人一是需要交出可演示、可答辩、能讲清每行代码逻辑的本科生二是想补全Web开发全链路能力从密码加盐存储到Ajax异步冲突检测的转行者。它解决的不是“能不能显示数据”而是“当12个客户同时抢周五晚8点的SPA房时系统如何保证不超卖、不漏通知、不崩库”。2. 数据库设计与PHP后端逻辑为什么预约状态必须用整型枚举而非字符串以及如何用事务锁死并发冲突2.1 核心表结构设计从业务规则反推字段约束美容院预约的核心矛盾是资源有限性——每个服务项目对应固定时长、固定人力、固定设备。若数据库仅用VARCHAR(20)存status如已预约、已取消后续查询统计、状态流转、权限控制将陷入字符串匹配泥潭。本系统采用整型枚举TINYINT(1)状态码含义业务约束说明0待确认客户提交后需管理员人工审核1已确认可进入日历视图占用员工/房间资源2已完成不再参与排班计算计入消费历史3已取消释放资源触发退款/扣费逻辑4已过期超过预约时间未到场自动置为该状态提示状态字段必须设为NOT NULL DEFAULT 0避免NULL值导致WHERE条件失效。所有状态变更必须通过UPDATE ... SET status ? WHERE id ? AND status ?的乐观锁写法防止A用户取消时B用户正修改造成状态错乱。服务表services关键字段设计体现业务强约束CREATE TABLE services ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 服务名称如小气泡清洁, duration_min TINYINT UNSIGNED NOT NULL COMMENT 耗时分钟用于计算时段, price DECIMAL(8,2) NOT NULL COMMENT 价格, required_staff_count TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT 最少需几人操作, skill_tags VARCHAR(100) COMMENT 技能标签逗号分隔如洁面,仪器操作, is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT 是否启用 );skill_tags虽用VARCHAR存储但实际使用中需在PHP层做explode(,, $tags)解析并与员工表中的skills字段同样逗号分隔做交集匹配确保预约分配时“持证人员可用”。2.2 预约创建的原子性保障三步事务与时间槽校验客户提交预约请求时系统必须一次性完成①检查员工当日该时段是否空闲②检查服务所需技能人员是否充足③插入预约记录并锁定资源。任何一步失败全部回滚。核心PHP逻辑如下?php // start_transaction.php $pdo-beginTransaction(); try { // 步骤1查询指定日期、时段内该服务所需的全部员工按技能匹配 $stmt $pdo-prepare( SELECT s.id as staff_id, s.name FROM staff s WHERE s.is_active 1 AND s.id IN ( SELECT staff_id FROM staff_skills WHERE skill_name IN (?) ) AND s.id NOT IN ( -- 排除已被其他预约占用的员工 SELECT staff_id FROM appointments WHERE date ? AND time_slot ? AND status IN (0,1) ) LIMIT ? ); $stmt-execute([$service_skill_tags, $date, $time_slot, $required_staff_count]); $available_staff $stmt-fetchAll(PDO::FETCH_ASSOC); if (count($available_staff) $required_staff_count) { throw new Exception(时段内无足够技能员工可用); } // 步骤2插入预约主记录status0 待确认 $stmt $pdo-prepare( INSERT INTO appointments (customer_id, service_id, date, time_slot, status, created_at) VALUES (?, ?, ?, ?, 0, NOW()) ); $stmt-execute([$customer_id, $service_id, $date, $time_slot]); $appointment_id $pdo-lastInsertId(); // 步骤3为每位分配的员工插入关联记录appointments_staff foreach ($available_staff as $staff) { $stmt $pdo-prepare( INSERT INTO appointments_staff (appointment_id, staff_id) VALUES (?, ?) ); $stmt-execute([$appointment_id, $staff[staff_id]]); } $pdo-commit(); echo json_encode([success true, id $appointment_id]); } catch (Exception $e) { $pdo-rollback(); echo json_encode([success false, error $e-getMessage()]); }参数说明$service_skill_tags是从services.skill_tags取出的字符串如仪器操作,洁面需在PHP中str_replace(, ,, $tags)统一逗号格式$time_slot采用HH:MM格式如14:00数据库中time_slot字段为VARCHAR(5)便于前端日历组件渲染appointments_staff关联表的存在使单次预约可绑定多名员工如SPA需1名按摩师1名助理避免在appointments主表中冗余存储员工ID。2.3 用户密码安全bcrypt而非md5且强制加盐策略学生项目常犯错误是直接md5($_POST[password])存库。本系统采用PHP内置password_hash()函数生成符合$2y$10$前缀的bcrypt哈希// register.php $password_hash password_hash($_POST[password], PASSWORD_DEFAULT); // 存入数据库 users.password 字段VARCHAR(255)验证时不用md5()比对而用password_verify()// login.php $stmt $pdo-prepare(SELECT id, password, role FROM users WHERE username ?); $stmt-execute([$_POST[username]]); $user $stmt-fetch(PDO::FETCH_ASSOC); if ($user password_verify($_POST[password], $user[password])) { // 登录成功设置session $_SESSION[user_id] $user[id]; $_SESSION[role] $user[role]; // customer, staff, admin } else { die(用户名或密码错误); }注意PASSWORD_DEFAULT会随PHP版本升级自动选用更强算法如PHP 9.x可能切换至Argon2无需手动修改代码。若需兼容旧系统可显式指定PASSWORD_BCRYPT并传入[cost 12]参数提升迭代轮数。3. 前端交互与实时性用原生JavaScriptAjax实现无刷新预约冲突检测与日历渲染3.1 预约页面的动态时段生成基于服务时长与营业时间计算可用Slot美容院每日营业时间为9:00-21:00但不同服务耗时差异大“基础护理”30分钟“全身SPA”120分钟。前端不能硬编码option14:00/option而需根据所选服务动态计算。核心逻辑在appointment.js中// 根据服务ID获取时长生成时段选项 function generateTimeSlots(serviceId) { const duration serviceDurations[serviceId]; // 从PHP注入的JS对象获取 const openTime 9 * 60; // 9:00转分钟 const closeTime 21 * 60; // 21:00转分钟 const slots []; for (let time openTime; time closeTime - duration; time 30) { const hour Math.floor(time / 60); const min time % 60; const formatted ${hour.toString().padStart(2,0)}:${min.toString().padStart(2,0)}; slots.push(formatted); } return slots; } // 绑定服务选择事件 document.getElementById(service_id).addEventListener(change, function() { const slots generateTimeSlots(this.value); const select document.getElementById(time_slot); select.innerHTML ; // 清空原有选项 slots.forEach(slot { const opt document.createElement(option); opt.value slot; opt.textContent slot; select.appendChild(opt); }); });关键点serviceDurations对象由PHP在页面输出时注入script const serviceDurations ?php echo json_encode($service_durations); ?; /script其中$service_durations来自数据库查询SELECT id, duration_min FROM services WHERE is_active1。3.2 实时冲突检测Ajax校验时段是否已被占用用户选择日期时段后立即向后端发起轻量级校验避免提交后才提示“该时段已满”。后端接口check_availability.php?php // check_availability.php require_once config.php; // 包含PDO连接 $date $_GET[date] ?? ; $time_slot $_GET[time_slot] ?? ; $service_id (int)$_GET[service_id]; if (!$date || !$time_slot || !$service_id) { die(json_encode([available false, msg 参数缺失])); } // 查询该时段内此服务所需的最少员工数是否满足 $stmt $pdo-prepare( SELECT COUNT(*) as occupied_count FROM appointments a JOIN appointments_staff asf ON a.id asf.appointment_id WHERE a.date ? AND a.time_slot ? AND a.status IN (0,1) AND a.service_id ? ); $stmt-execute([$date, $time_slot, $service_id]); $result $stmt-fetch(PDO::FETCH_ASSOC); // 获取服务所需人数 $stmt $pdo-prepare(SELECT required_staff_count FROM services WHERE id ?); $stmt-execute([$service_id]); $required $stmt-fetch(PDO::FETCH_ASSOC)[required_staff_count]; $available ($result[occupied_count] $required); echo json_encode([ available $available, msg $available ? 时段可用 : 该时段已被预约满员 ]);前端调用// 检查时段可用性 function checkAvailability() { const date document.getElementById(date).value; const time document.getElementById(time_slot).value; const serviceId document.getElementById(service_id).value; fetch(check_availability.php?date${date}time_slot${time}service_id${serviceId}) .then(res res.json()) .then(data { const msgEl document.getElementById(availability_msg); msgEl.textContent data.msg; msgEl.className data.available ? success : error; document.getElementById(submit_btn).disabled !data.available; }); }注意此接口不返回具体占用详情如“张技师已约”仅返回布尔值提示语既保护员工隐私又降低接口复杂度。真实项目中可扩展为返回占用率百分比如occupied_count / required_staff_count。3.3 日历视图渲染用CSS Grid构建可点击的月度网格管理员需快速查看某日所有预约前端采用纯CSS Grid实现响应式日历避免引入FullCalendar等重型库!-- calendar.html -- div classcalendar-grid !-- 表头周一至周日 -- div classcalendar-header一/div div classcalendar-header二/div div classcalendar-header三/div div classcalendar-header四/div div classcalendar-header五/div div classcalendar-header六/div div classcalendar-header日/div !-- 日历主体35个格子5周×7天 -- ?php for($i1; $i35; $i): ? div classcalendar-day>/* calendar.css */ .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; max-width: 800px; margin: 0 auto; } .calendar-header { background: #f0f0f0; padding: 8px; text-align: center; font-weight: bold; } .calendar-day { border: 1px solid #ddd; min-height: 80px; padding: 4px; cursor: pointer; position: relative; } .calendar-day:hover { background: #f9f9f9; } .calendar-day[data-date?php echo date(Y-m-d); ?] { background: #e3f2fd; /* 今日高亮 */ } /* 预约条目用绝对定位嵌入 */ .appointment-item { position: absolute; top: 2px; left: 2px; right: 2px; font-size: 12px; padding: 2px 4px; background: #4caf50; color: white; border-radius: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }技巧每个.calendar-day通过>CREATE TABLE reminders ( id INT PRIMARY KEY AUTO_INCREMENT, appointment_id INT NOT NULL, email VARCHAR(100) NOT NULL, send_time DATETIME NOT NULL COMMENT 计划发送时间, status ENUM(pending,sent,failed) DEFAULT pending, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_send_time (send_time), INDEX idx_status (status) );CLI脚本send_reminders.php需命令行执行#!/usr/bin/env php ?php // send_reminders.php require_once config.php; // 只查1小时内需发送且状态为pending的记录 $stmt $pdo-prepare( SELECT r.id, r.email, a.date, a.time_slot, s.name as service_name FROM reminders r JOIN appointments a ON r.appointment_id a.id JOIN services s ON a.service_id s.id WHERE r.send_time NOW() AND r.status pending LIMIT 50 ); $stmt-execute(); $reminders $stmt-fetchAll(PDO::FETCH_ASSOC); foreach ($reminders as $r) { $subject 【美容院】您的预约提醒; $body 您好您预约的{$r[service_name]}将于{$r[date]} {$r[time_slot]}进行请提前10分钟到达。\n\n如需调整请登录系统操作。; // 使用PHPMailer需composer require phpmailer/phpmailer $mail new PHPMailer\PHPMailer\PHPMailer(); $mail-isSMTP(); $mail-Host smtp.gmail.com; // 生产环境应从配置文件读取 $mail-SMTPAuth true; $mail-Username getenv(SMTP_USER) ?: your_appgmail.com; // 从环境变量读取 $mail-Password getenv(SMTP_PASS) ?: app_password_here; // App Password非邮箱密码 $mail-SMTPSecure tls; $mail-Port 587; $mail-setFrom(no-replybeauty-spa.com, 美容院系统); $mail-addAddress($r[email]); $mail-Subject $subject; $mail-Body $body; if ($mail-send()) { $stmt $pdo-prepare(UPDATE reminders SET status sent WHERE id ?); $stmt-execute([$r[id]]); } else { error_log(邮件发送失败: . $mail-ErrorInfo); $stmt $pdo-prepare(UPDATE reminders SET status failed WHERE id ?); $stmt-execute([$r[id]]); } }安全实践SMTP密码绝不硬编码通过getenv()从系统环境变量读取Gmail需开启“两步验证”后生成App Password而非使用邮箱登录密码Cron设置* * * * * /usr/bin/php /var/www/beauty-system/send_reminders.php /var/log/beauty-reminders.log 214.2 MySQL性能瓶颈定位针对预约查询的索引优化与慢日志分析当预约数据超万条SELECT * FROM appointments WHERE date 2024-06-15会变慢。需建立复合索引-- 为高频查询字段建立联合索引 ALTER TABLE appointments ADD INDEX idx_date_status (date, status), ADD INDEX idx_customer_date (customer_id, date);启用MySQL慢查询日志my.cnfslow_query_log ON slow_query_log_file /var/log/mysql/mysql-slow.log long_query_time 1 # 超过1秒即记录 log_queries_not_using_indexes ON分析日志常用命令# 查看最慢的10条SQL mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log # 查看未走索引的查询 mysqldumpslow -s c -t 10 -g appointments /var/log/mysql/mysql-slow.log典型优化案例原查询SELECT * FROM appointments WHERE status 1 AND date 2024-06-01未用索引因status在date前。优化后建索引ADD INDEX idx_status_date (status, date)查询速度从1.2秒降至0.008秒。4.3 防止XSS与CSRF在表单提交中嵌入一次性Token所有修改类操作预约、取消、员工排班必须校验CSRF Token。PHP生成与验证// 在表单页如appointment_form.php if (empty($_SESSION[csrf_token])) { $_SESSION[csrf_token] bin2hex(random_bytes(32)); } ? form methodpost actioncreate_appointment.php input typehidden namecsrf_token value?php echo $_SESSION[csrf_token]; ? !-- 其他字段 -- /form// create_appointment.php 开头校验 if (!hash_equals($_SESSION[csrf_token] ?? , $_POST[csrf_token] ?? )) { die(CSRF token无效); } // 执行业务逻辑... unset($_SESSION[csrf_token]); // 一次性Token用完即销毁注意hash_equals()用于防时序攻击比更安全。前端JavaScript提交时也需携带该Token如Ajax请求头中添加X-CSRF-Token: ?php echo $_SESSION[csrf_token]; ?。本文还有配套的精品资源点击获取
返回列表