fix(预约系统): 增强日期时间验证并修复跨天预约处理

- 添加日期和时间格式的正则验证
- 改进时间有效性检查逻辑,防止无效时间输入
- 修复跨天预约处理中的变量命名冲突问题
- 优化时间冲突检测SQL查询条件
- 增加XSS防护措施,对VIP客户搜索结果显示进行转义
This commit is contained in:
2025-12-12 02:45:53 +08:00
parent ae557aa5c2
commit 5438b944b8
2 changed files with 110 additions and 27 deletions
+57 -16
View File
@@ -91,9 +91,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
throw new Exception('请选择一个套餐');
}
// 验证日期和时间格式
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $appointment_date)) {
throw new Exception('预约日期格式不正确');
}
if (!preg_match('/^\d{2}:\d{2}$/', $appointment_time)) {
throw new Exception('预约时间格式不正确');
}
// 计算预约时间范围
$start_time = $appointment_date . ' ' . $appointment_time . ':00';
$end_time = date('Y-m-d H:i:s', strtotime($start_time . " +{$duration} minutes"));
$start_timestamp = strtotime($start_time);
if ($start_timestamp === false) {
throw new Exception('预约时间无效,请检查日期和时间');
}
$end_time = date('Y-m-d H:i:s', $start_timestamp + $duration * 60);
// 验证结束时间是否有效
if ($end_time === false) {
throw new Exception('计算结束时间失败');
}
// 检查时间冲突
// 两个时间段重叠的条件:现有预约的开始时间 < 新预约的结束时间 AND 现有预约的结束时间 > 新预约的开始时间
@@ -185,17 +202,26 @@ $all_bookings = $stmt2->fetchAll(PDO::FETCH_ASSOC);
// 按日期组织预约数据,处理跨天预约情况
$bookings_by_date = [];
foreach ($all_bookings as $booking) {
$start_date = $booking['date'];
$start_time = $booking['start_time'];
$end_time = $booking['end_time'];
$booking_date = $booking['date']; // 使用不同的变量名避免覆盖外层$start_date
$booking_start_time = $booking['start_time'];
$booking_end_time = $booking['end_time'];
// 将预约添加到开始日期
$bookings_by_date[$start_date][] = $booking;
$bookings_by_date[$booking_date][] = $booking;
// 检查是否是跨天预约(结束时间早于开始时间)
if (strtotime($end_time) < strtotime($start_time)) {
// 检查是否是跨天预约(结束时间早于开始时间,表示跨天
// 注意:这里比较的是时间字符串(HH:MM格式),需要转换为可比较的格式
$start_timestamp = strtotime($booking_start_time);
$end_timestamp = strtotime($booking_end_time);
// #region agent log
$log_data = json_encode(['location' => 'index.php:196', 'message' => 'Checking cross-day booking', 'data' => ['booking_date' => $booking_date, 'start_time' => $booking_start_time, 'end_time' => $booking_end_time, 'start_ts' => $start_timestamp, 'end_ts' => $end_timestamp], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'G']);
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
// #endregion
if ($start_timestamp !== false && $end_timestamp !== false && $end_timestamp < $start_timestamp) {
// 计算第二天的日期
$next_date = date('Y-m-d', strtotime($start_date . ' +1 day'));
$next_date = date('Y-m-d', strtotime($booking_date . ' +1 day'));
// 创建第二天的预约记录副本
$next_day_booking = $booking;
@@ -1214,20 +1240,35 @@ $packages_json = json_encode(array_map(function($package) {
const carModel = vip.car_model || '';
const carNumber = vip.car_number || '';
// 转义引号,防止JS语法错误
const escapedId = id.toString().replace(/'/g, "\\'");
const escapedName = name.replace(/'/g, "\\'");
const escapedPhone = phone.replace(/'/g, "\\'");
// 转义HTML和引号,防止XSS和JS语法错误
const escapeHtml = (str) => {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
};
const escapeJs = (str) => {
return str.replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\\/g, '\\\\');
};
// 高亮处理
const highlightedName = name.replace(new RegExp(`(${term})`, 'gi'), '<span class="highlight">$1</span>');
const highlightedPhone = phone.replace(new RegExp(`(${term})`, 'gi'), '<span class="highlight">$1</span>');
const escapedId = escapeJs(id.toString());
const escapedName = escapeJs(name);
const escapedPhone = escapeJs(phone);
// 高亮处理(先转义HTML,再进行高亮)
const escapedNameHtml = escapeHtml(name);
const escapedPhoneHtml = escapeHtml(phone);
const escapedTerm = escapeHtml(term);
const highlightedName = escapedNameHtml.replace(new RegExp(`(${escapedTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), '<span class="highlight">$1</span>');
const highlightedPhone = escapedPhoneHtml.replace(new RegExp(`(${escapedTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), '<span class="highlight">$1</span>');
const escapedCarModel = escapeHtml(carModel);
const escapedCarNumber = escapeHtml(carNumber);
html += `
<div class="vip-search-item" onclick="selectVIPCustomer('${escapedId}', '${escapedName}', '${escapedPhone}')">
<div class="customer-name">${highlightedName}</div>
<div class="customer-phone">${highlightedPhone}</div>
${carModel || carNumber ? `<div class="customer-car">${carModel} ${carNumber}</div>` : ''}
${carModel || carNumber ? `<div class="customer-car">${escapedCarModel} ${escapedCarNumber}</div>` : ''}
</div>
`;
});
+53 -11
View File
@@ -60,15 +60,50 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$start_time_str = '';
$end_time_str = '';
// 验证日期格式
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $booking_date)) {
throw new Exception('预约日期格式不正确');
}
if (strpos($time_slot, '-') !== false) {
// 格式:09:00-10:00
list($start_time_str, $end_time_str) = explode('-', $time_slot);
$start_time = $booking_date . ' ' . trim($start_time_str) . ':00';
$end_time = $booking_date . ' ' . trim($end_time_str) . ':00';
$start_time_str = trim($start_time_str);
$end_time_str = trim($end_time_str);
// 验证时间格式
if (!preg_match('/^\d{2}:\d{2}$/', $start_time_str) || !preg_match('/^\d{2}:\d{2}$/', $end_time_str)) {
throw new Exception('时间段格式不正确');
}
$start_time = $booking_date . ' ' . $start_time_str . ':00';
$end_time = $booking_date . ' ' . $end_time_str . ':00';
// 验证时间有效性
$start_timestamp = strtotime($start_time);
$end_timestamp = strtotime($end_time);
if ($start_timestamp === false || $end_timestamp === false) {
throw new Exception('时间段无效');
}
if ($end_timestamp <= $start_timestamp) {
throw new Exception('结束时间必须晚于开始时间');
}
} else {
// 格式:09:00,使用默认时长
$start_time = $booking_date . ' ' . trim($time_slot) . ':00';
$end_time = date('Y-m-d H:i:s', strtotime($start_time) + $duration * 60);
$time_slot = trim($time_slot);
if (!preg_match('/^\d{2}:\d{2}$/', $time_slot)) {
throw new Exception('时间格式不正确');
}
$start_time = $booking_date . ' ' . $time_slot . ':00';
$start_timestamp = strtotime($start_time);
if ($start_timestamp === false) {
throw new Exception('开始时间无效');
}
$end_time = date('Y-m-d H:i:s', $start_timestamp + $duration * 60);
if ($end_time === false) {
throw new Exception('计算结束时间失败');
}
}
// #region agent log
@@ -98,16 +133,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
// 检查时间冲突
// 两个时间段重叠的条件:现有预约的开始时间 < 新预约的结束时间 AND 现有预约的结束时间 > 新预约的开始时间
// #region agent log
$log_data = json_encode(['location' => 'process_booking.php:100', 'message' => 'Checking time conflict', 'data' => ['start_time' => $start_time, 'end_time' => $end_time], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'H']);
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
// #endregion
$stmt = $pdo->prepare("SELECT COUNT(*) FROM bookings
WHERE status != '已取消'
AND (
(start_time <= ? AND end_time > ?)
OR (start_time < ? AND end_time >= ?)
OR (start_time >= ? AND end_time <= ?)
)");
$stmt->execute([$start_time, $start_time, $end_time, $end_time, $start_time, $end_time]);
AND start_time < ?
AND end_time > ?");
$stmt->execute([$end_time, $start_time]);
$conflict_count = $stmt->fetchColumn();
// #region agent log
$log_data = json_encode(['location' => 'process_booking.php:110', 'message' => 'Time conflict check result', 'data' => ['conflict_count' => $conflict_count], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'H']);
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
// #endregion
if ($stmt->fetchColumn() > 0) {
if ($conflict_count > 0) {
throw new Exception('该时间段已被预约,请选择其他时间');
}