fix(预约系统): 增强日期时间验证并修复跨天预约处理
- 添加日期和时间格式的正则验证 - 改进时间有效性检查逻辑,防止无效时间输入 - 修复跨天预约处理中的变量命名冲突问题 - 优化时间冲突检测SQL查询条件 - 增加XSS防护措施,对VIP客户搜索结果显示进行转义
This commit is contained in:
@@ -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
@@ -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('该时间段已被预约,请选择其他时间');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user