Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c0e7cbb5f | |||
| 2c5ac2b943 | |||
| c64651d6c7 | |||
| 9cd2b31648 | |||
| 89a22c7b11 | |||
| 5438b944b8 | |||
| ae557aa5c2 | |||
| 905bbc5934 | |||
| 58fbb9f1e1 | |||
| 31899dfea8 | |||
| a6c48fc681 | |||
| a714f0a526 | |||
| b963c2b513 | |||
| a672e1d7bc | |||
| 1f0cf4acaa | |||
| 26dd84bea2 | |||
| a372464299 | |||
| 6906d8da2a | |||
| decbc95d28 | |||
| 7f2fe3dd21 | |||
| ae3ed1e58f | |||
| bb3ebffb37 | |||
| 9d8fa49206 | |||
| b5cacb9e0b | |||
| cb9eb599ec | |||
| 6344947fa7 | |||
| 5c0132a209 | |||
| 5ace9b86d8 | |||
| 565fc310b7 | |||
| 77ae32095e | |||
| ca554456b0 | |||
| d387487bc5 | |||
| 44e4bc64d8 | |||
| aefd3df9ae | |||
| 9a256cda0a | |||
| 0245f2c822 | |||
| f297a12a7a | |||
| 466f16e313 | |||
| a6356ea2a2 | |||
| ff23ac6bbb | |||
| d0abed0f86 | |||
| 3944051b25 | |||
| a9b50046c1 | |||
| caac9899a5 | |||
| 6ffabd9384 | |||
| fc3b6aa4cc | |||
| ac40ce0d56 | |||
| 2590e1fc22 |
@@ -1,497 +1,81 @@
|
||||
# 🚗 洗车店订单管理系统
|
||||
# 张老师撸车工作室 - 洗车预约管理系统
|
||||
|
||||
一个功能完整的洗车店订单管理系统,支持VIP客户管理、预约处理、套餐管理等功能。
|
||||
一个专为洗车店设计的现代化预约管理系统,支持客户预约、套餐管理、VIP服务等功能。
|
||||
|
||||
## 📋 目录
|
||||
## 主要功能
|
||||
|
||||
- [系统概述](#-系统概述)
|
||||
- [主要功能](#-主要功能)
|
||||
- [技术架构](#-技术架构)
|
||||
- [系统要求](#-系统要求)
|
||||
- [安装指南](#-安装指南)
|
||||
- [项目结构](#-项目结构)
|
||||
- [功能模块](#-功能模块)
|
||||
- [数据库结构](#-数据库结构)
|
||||
- [API接口](#-api接口)
|
||||
- [使用指南](#-使用指南)
|
||||
- [调试工具](#-调试工具)
|
||||
- [常见问题](#-常见问题)
|
||||
- [版本历史](#-版本历史)
|
||||
- **预约管理**:洗车预约创建、状态管理、历史记录查询
|
||||
- **套餐管理**:洗车服务套餐配置、价格和时长设置
|
||||
- **VIP客户管理**:VIP客户信息管理、历史预约记录查询
|
||||
- **数据持久化**:基于MySQL的可靠数据存储
|
||||
- **响应式设计**:适配各种设备屏幕
|
||||
|
||||
## 🎯 系统概述
|
||||
## 技术栈
|
||||
|
||||
本系统是一个专为洗车店设计的现代化订单管理平台,提供完整的客户管理、预约处理、VIP服务等功能。系统采用PHP + MySQL架构,界面简洁易用,功能完善稳定。
|
||||
- **前端**:HTML5, CSS3, JavaScript (jQuery)
|
||||
- **后端**:PHP 7.4+
|
||||
- **数据库**:MySQL 5.7+
|
||||
- **架构**:MVC模式,PDO数据库连接
|
||||
|
||||
### 核心特性
|
||||
## 快速安装
|
||||
|
||||
- 🏆 **VIP客户管理** - 完整的VIP客户信息管理和历史预约查询功能
|
||||
- 📝 **预约管理** - 完整的洗车预约生命周期管理
|
||||
- 📦 **套餐管理** - 灵活的洗车服务套餐配置
|
||||
- 💾 **数据持久化** - 可靠的MySQL数据库存储
|
||||
- 🔍 **搜索功能** - 强大的客户搜索功能
|
||||
- ️ **调试工具** - 完善的开发调试和监控工具
|
||||
### 1. 环境要求
|
||||
|
||||
## 🚀 主要功能
|
||||
- PHP 7.4+ (启用PDO扩展)
|
||||
- MySQL 5.7+
|
||||
- Apache/Nginx Web服务器
|
||||
|
||||
### 1. 客户管理
|
||||
- **客户信息录入** - 姓名、手机号、车型、车牌号等基本信息
|
||||
- **VIP客户管理** - 专门的VIP客户管理功能
|
||||
- **客户搜索** - 支持姓名、手机号模糊搜索
|
||||
- **VIP客户预约记录** - 自动显示VIP客户最近一次预约信息
|
||||
### 2. 部署步骤
|
||||
|
||||
### 2. 预约管理
|
||||
- **预约创建** - 快速创建洗车预约
|
||||
- **预约状态** - 待确认、已确认、进行中、已完成、已取消等状态管理
|
||||
- **预约详情** - 完整的预约信息记录
|
||||
- **VIP客户特殊处理** - 自动关联VIP客户信息和历史记录
|
||||
1. **克隆/上传代码**到Web服务器根目录
|
||||
2. **导入数据库**:
|
||||
- 创建数据库 `carwash_booking`
|
||||
- 导入 `merged_db.sql` 文件
|
||||
3. **配置数据库连接**:
|
||||
- 复制 `config.php.env` 为 `config.php`
|
||||
- 修改数据库连接参数
|
||||
|
||||
### 3. 套餐管理
|
||||
- **服务套餐** - 预设洗车服务套餐
|
||||
- **价格管理** - 灵活的价格设置
|
||||
- **套餐配置** - 服务项目和时长配置
|
||||
### 3. 访问系统
|
||||
|
||||
### 4. VIP专享功能
|
||||
- **VIP预约历史查询** - 自动显示VIP客户最近一次预约详情
|
||||
- **首次到店提示** - 为首次预约的VIP客户提供特别提示
|
||||
- **客户信息自动填充** - 选择VIP客户后自动填充相关信息
|
||||
打开浏览器访问:`http://localhost/carwash_order/`
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 前端技术
|
||||
- **HTML5** - 现代化的页面结构
|
||||
- **CSS3** - 响应式样式设计
|
||||
- **JavaScript** - 动态交互和AJAX请求
|
||||
- **Bootstrap** - UI组件库(可选)
|
||||
|
||||
### 后端技术
|
||||
- **PHP 7.4+** - 服务器端脚本语言
|
||||
- **MySQL 5.7+** - 关系型数据库
|
||||
- **PDO** - 数据库抽象层
|
||||
- **RESTful API** - 标准API设计
|
||||
|
||||
### 核心组件
|
||||
- **数据库连接** (`db_connect.php`) - 统一的数据库连接管理
|
||||
- **配置管理** (`config.php`) - 系统配置参数
|
||||
- **主页面** (`index.php`) - 系统主入口和预约创建页面
|
||||
- **VIP管理** (`get_vip_customers.php`, `get_vip_last_booking.php`) - VIP客户相关功能
|
||||
|
||||
## 💻 系统要求
|
||||
|
||||
### 服务器环境
|
||||
- **PHP**: 7.4 或更高版本
|
||||
- **MySQL**: 5.7 或更高版本
|
||||
- **Web服务器**: Apache 2.4+ 或 Nginx 1.18+
|
||||
- **PHP扩展**: PDO, PDO_MySQL, JSON
|
||||
|
||||
### 开发环境
|
||||
- **操作系统**: Windows 10+, macOS 10.14+, Linux Ubuntu 18+
|
||||
- **推荐工具**: XAMPP, WAMP, LAMP 或 Docker
|
||||
- **浏览器**: Chrome 80+, Firefox 75+, Safari 13+
|
||||
|
||||
## 🔧 安装指南
|
||||
|
||||
### 1. 环境准备
|
||||
|
||||
#### 使用XAMPP(推荐)
|
||||
```bash
|
||||
# 下载并安装XAMPP
|
||||
# 启动Apache和MySQL服务
|
||||
```
|
||||
|
||||
#### 使用WAMP(Windows用户)
|
||||
```bash
|
||||
# 下载并安装WAMP
|
||||
# 启动WAMP服务
|
||||
```
|
||||
|
||||
### 2. 项目部署
|
||||
|
||||
#### 项目文件放置
|
||||
```bash
|
||||
# 将项目文件复制到Web服务器根目录
|
||||
# Windows (XAMPP): C:\xampp\htdocs\carwash_order\
|
||||
# Linux/macOS: /var/www/html/carwash_order/
|
||||
```
|
||||
|
||||
#### 数据库配置
|
||||
```sql
|
||||
-- 1. 创建数据库
|
||||
CREATE DATABASE carwash_booking CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 2. 导入数据库结构
|
||||
SOURCE carwash_db.sql;
|
||||
|
||||
-- 3. 验证表创建
|
||||
SHOW TABLES;
|
||||
```
|
||||
|
||||
#### 配置文件设置
|
||||
编辑 `config.php`:
|
||||
```php
|
||||
<?php
|
||||
$host = 'localhost'; // 数据库主机
|
||||
$username = 'root'; // 数据库用户名
|
||||
$password = ''; // 数据库密码
|
||||
$database = 'carwash_booking'; // 数据库名称
|
||||
?>
|
||||
```
|
||||
|
||||
### 3. 权限设置
|
||||
|
||||
#### Linux/macOS权限
|
||||
```bash
|
||||
# 设置目录权限
|
||||
chmod -R 755 /var/www/html/carwash_order/
|
||||
chown -R www-data:www-data /var/www/html/carwash_order/
|
||||
```
|
||||
|
||||
#### Windows权限
|
||||
确保IIS或Apache用户有读取项目文件的权限。
|
||||
|
||||
### 4. 访问测试
|
||||
|
||||
打开浏览器访问:
|
||||
```
|
||||
http://localhost/carwash_order/
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
carwash_order/
|
||||
├── README.md # 项目说明文档
|
||||
├── carwash_db.sql # 数据库结构和初始数据
|
||||
├── config.php # 系统配置文件
|
||||
├── db_connect.php # 数据库连接文件
|
||||
├── index.php # 主页面入口
|
||||
├── style.css # 样式文件
|
||||
├── announcement.php # 预约公告页面
|
||||
├── bookings.php # 预约管理页面
|
||||
├── packages.php # 套餐管理页面
|
||||
├── vip.php # VIP客户管理页面
|
||||
├── process_booking.php # 预约处理API
|
||||
├── get_bookings.php # 获取预约列表API
|
||||
├── get_vip_customers.php # 获取VIP客户列表API
|
||||
├── get_vip_customer.php # 获取单个VIP客户信息API
|
||||
├── get_vip_last_booking.php # 获取VIP客户最近预约记录API
|
||||
├── add_payment_status.php # 支付状态添加API
|
||||
├── update_booking.php # 预约更新API
|
||||
│
|
||||
├── test/ # 测试文件目录
|
||||
│ ├── SOLUTIONS.md # 解决方案文档
|
||||
│ ├── VIP_Function_Fix_Report.md # VIP功能修复报告
|
||||
│ ├── VIP_Search_Fix_Report.md # VIP搜索修复报告
|
||||
│ ├── debug_vip.php # VIP调试工具
|
||||
│ ├── debug_vip_db.php # VIP数据库调试工具
|
||||
│ ├── test.php # 基础测试文件
|
||||
│ ├── test_24hour_booking.php # 24小时预约测试
|
||||
│ ├── test_db_connection.php # 数据库连接测试
|
||||
│ ├── test_filters.php # 过滤器测试
|
||||
│ ├── test_update_booking.php # 预约更新测试
|
||||
│ ├── test_vip.php # VIP功能测试
|
||||
│ ├── test_vip_ajax.html # VIP AJAX测试
|
||||
│ ├── test_vip_booking_history.php # VIP预约历史测试
|
||||
│ ├── test_vip_debug_panel.html # VIP调试面板
|
||||
│ ├── test_vip_entries.php # VIP条目测试
|
||||
│ ├── test_vip_fix.html # VIP修复测试
|
||||
│ ├── test_vip_loading.php # VIP加载测试
|
||||
│ ├── test_vip_search.html # VIP搜索测试
|
||||
│ ├── test_vip_search_simple.html # VIP简单搜索测试
|
||||
│ ├── verify_vip_data.php # VIP数据验证
|
||||
│ ├── vip_debug_page.html # VIP调试页面
|
||||
│ ├── vip_functionality_test.html # VIP功能测试
|
||||
│ └── vip_search_debug.html # VIP搜索调试页面
|
||||
├── index.php # 预约页面入口
|
||||
├── bookings.php # 预约管理页面
|
||||
├── packages.php # 套餐管理页面
|
||||
├── vip.php # VIP客户管理页面
|
||||
├── announcement.php # 今日待办页面
|
||||
├── db_connect.php # 数据库连接
|
||||
├── style.css # 样式文件
|
||||
├── merged_db.sql # 数据库结构
|
||||
└── test/ # 测试工具目录
|
||||
```
|
||||
|
||||
## 🗄️ 数据库结构
|
||||
## 使用说明
|
||||
|
||||
### 主要数据表
|
||||
### 客户预约
|
||||
1. 访问主页面,选择客户类型(普通/VIP)
|
||||
2. 填写客户信息或搜索VIP客户
|
||||
3. 选择服务套餐和预约时间
|
||||
4. 提交预约
|
||||
|
||||
#### 1. vip_customers(VIP客户表)
|
||||
```sql
|
||||
CREATE TABLE vip_customers (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
customer_name VARCHAR(100) NOT NULL COMMENT '客户姓名',
|
||||
phone VARCHAR(20) NOT NULL COMMENT '联系电话',
|
||||
car_model VARCHAR(50) COMMENT '车型',
|
||||
car_number VARCHAR(20) COMMENT '车牌号',
|
||||
email VARCHAR(100) COMMENT '邮箱地址',
|
||||
birthday DATE COMMENT '生日',
|
||||
notes TEXT COMMENT '备注信息',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NULL,
|
||||
-- 复合唯一索引:确保手机号和车牌号组合唯一
|
||||
UNIQUE INDEX idx_phone_car_number (phone, car_number)
|
||||
);
|
||||
```
|
||||
### 套餐管理
|
||||
1. 进入套餐管理页面
|
||||
2. 创建新套餐或编辑现有套餐
|
||||
3. 配置套餐名称、价格、时长和服务项目
|
||||
|
||||
#### 2. bookings(预约表)
|
||||
```sql
|
||||
CREATE TABLE bookings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
customer_name VARCHAR(100) NOT NULL,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
car_model VARCHAR(50) NOT NULL,
|
||||
car_number VARCHAR(20) NOT NULL,
|
||||
package_id INT,
|
||||
custom_services TEXT COMMENT '自定义服务内容',
|
||||
start_time DATETIME NOT NULL,
|
||||
end_time DATETIME NOT NULL,
|
||||
duration INT NOT NULL COMMENT '实际服务时长(分钟)',
|
||||
total_price DECIMAL(10,2) NOT NULL,
|
||||
notes TEXT,
|
||||
status ENUM('待确认', '已确认', '进行中', '已完成', '已取消') DEFAULT '待确认',
|
||||
member_type ENUM('普通客户', 'VIP会员') DEFAULT '普通客户' COMMENT '会员类型',
|
||||
source ENUM('抖音', '微信', '快手', '朋友介绍', '其他') DEFAULT '其他' COMMENT '客户来源',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NULL,
|
||||
FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE SET NULL
|
||||
);
|
||||
```
|
||||
### VIP管理
|
||||
1. 进入VIP管理页面
|
||||
2. 添加或编辑VIP客户信息
|
||||
3. 查询VIP客户历史预约记录
|
||||
|
||||
#### 3. packages(套餐表)
|
||||
```sql
|
||||
CREATE TABLE packages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
package_name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
base_duration INT NOT NULL COMMENT '基础服务时长(分钟)',
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
services TEXT NOT NULL COMMENT '包含的服务项目(用逗号分隔)',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NULL
|
||||
);
|
||||
```
|
||||
## 许可证
|
||||
|
||||
## 🔌 API接口
|
||||
|
||||
### VIP客户相关接口
|
||||
|
||||
#### 1. 获取VIP客户列表
|
||||
```http
|
||||
GET /get_vip_customers.php
|
||||
```
|
||||
|
||||
**参数**: 无
|
||||
**返回**: JSON格式的VIP客户列表
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"customer_name": "张总",
|
||||
"phone": "13900139001",
|
||||
"car_model": "奔驰S500",
|
||||
"car_number": "京V88888",
|
||||
"email": "zhang@example.com",
|
||||
"birthday": "1980-05-15",
|
||||
"is_active": 1
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 2. 获取单个VIP客户信息
|
||||
```http
|
||||
GET /get_vip_customer.php?id=1
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `id`: VIP客户ID
|
||||
|
||||
**返回**: JSON格式的单个VIP客户信息
|
||||
|
||||
#### 3. 获取VIP客户最近预约记录
|
||||
```http
|
||||
GET /get_vip_last_booking.php?vip_id=1
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `vip_id`: VIP客户ID
|
||||
|
||||
**返回**: JSON格式的最近预约信息或首次到店标识
|
||||
|
||||
**响应示例** (有预约记录):
|
||||
```json
|
||||
{
|
||||
"has_booking": true,
|
||||
"appointment_date": "2024-12-21",
|
||||
"appointment_time": "14:00",
|
||||
"package_name": "VIP套餐",
|
||||
"duration": 180
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例** (首次到店):
|
||||
```json
|
||||
{
|
||||
"has_booking": false
|
||||
}
|
||||
```
|
||||
|
||||
### 预约管理相关接口
|
||||
|
||||
#### 4. 获取预约列表
|
||||
```http
|
||||
GET /get_bookings.php
|
||||
```
|
||||
|
||||
**参数**: 无
|
||||
**返回**: JSON格式的预约列表
|
||||
|
||||
#### 5. 处理预约创建
|
||||
```http
|
||||
POST /process_booking.php
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- 客户信息(姓名、手机号、车型等)
|
||||
- 预约日期和时间
|
||||
- 选择的套餐
|
||||
|
||||
**返回**: JSON格式的预约处理结果
|
||||
|
||||
#### 6. 更新预约信息
|
||||
```http
|
||||
POST /update_booking.php
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `id`: 预约ID
|
||||
- 其他需要更新的预约信息
|
||||
|
||||
**返回**: JSON格式的预约更新结果
|
||||
|
||||
#### 7. 添加支付状态
|
||||
```http
|
||||
POST /add_payment_status.php
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `booking_id`: 预约ID
|
||||
- `status`: 支付状态
|
||||
- `amount`: 支付金额
|
||||
|
||||
**返回**: JSON格式的支付状态添加结果
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 基础操作流程
|
||||
|
||||
#### 1. VIP客户预约
|
||||
```
|
||||
1. 打开系统主页 (index.php)
|
||||
2. 选择客户类型为"VIP客户"
|
||||
3. 在VIP搜索框中输入关键词搜索客户
|
||||
4. 选择目标VIP客户
|
||||
5. 系统自动显示客户信息和最近一次预约记录(如有)
|
||||
6. 选择服务套餐和预约时间
|
||||
7. 提交预约
|
||||
```
|
||||
|
||||
#### 2. 普通客户预约
|
||||
```
|
||||
1. 打开系统主页 (index.php)
|
||||
2. 选择客户类型为"普通客户"
|
||||
3. 填写客户基本信息
|
||||
4. 选择服务套餐和预约时间
|
||||
5. 提交预约
|
||||
```
|
||||
|
||||
### VIP客户特殊功能
|
||||
|
||||
#### VIP客户最近预约显示
|
||||
当选择VIP客户后,系统会自动查询并显示该客户的最近一次预约信息,包括:
|
||||
- 预约日期
|
||||
- 预约时间
|
||||
- 选择的套餐
|
||||
- 服务时长
|
||||
|
||||
如果是首次预约,则显示"该VIP首次到店"的提示信息。
|
||||
|
||||
## 🛠️ 调试工具
|
||||
|
||||
### 1. VIP预约历史测试
|
||||
- **文件**: `test/test_vip_booking_history.php`
|
||||
- **功能**: 测试VIP客户预约历史查询功能
|
||||
- **使用**: 浏览器访问或命令行执行
|
||||
|
||||
### 2. VIP功能测试
|
||||
- **文件**: `test/test_vip.php`
|
||||
- **功能**: 测试VIP客户管理相关功能
|
||||
- **使用**: 浏览器访问
|
||||
|
||||
### 3. 数据库连接测试
|
||||
- **文件**: `test/test_db_connection.php`
|
||||
- **功能**: 测试数据库连接是否正常
|
||||
- **使用**: 命令行执行
|
||||
|
||||
### 4. 其他测试工具
|
||||
- **VIP调试工具**: `test/debug_vip.php`
|
||||
- **VIP搜索调试**: `test/vip_search_debug.html`
|
||||
- **数据验证工具**: `test/verify_vip_data.php`
|
||||
- **功能测试页面**: `test/vip_functionality_test.html`
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### 安装问题
|
||||
|
||||
#### Q: PHP版本不兼容怎么办?
|
||||
A: 确保使用PHP 7.4或更高版本,检查php.ini配置是否启用PDO扩展。
|
||||
|
||||
#### Q: 数据库连接失败?
|
||||
A: 检查config.php中的数据库配置,确保MySQL服务正在运行,用户权限正确。
|
||||
|
||||
### 功能问题
|
||||
|
||||
#### Q: VIP客户搜索不到?
|
||||
A:
|
||||
1. 检查数据库中是否有VIP客户数据
|
||||
2. 查看浏览器控制台是否有JavaScript错误
|
||||
3. 使用调试工具验证数据加载
|
||||
|
||||
#### Q: VIP客户预约记录不显示?
|
||||
A:
|
||||
1. 确认该VIP客户已有预约记录
|
||||
2. 检查get_vip_last_booking.php是否正常工作
|
||||
3. 查看数据库中bookings表与vip_customers表的phone字段是否匹配
|
||||
|
||||
#### Q: 页面显示错误"SQLSTATE[42S22]: Column not found"?
|
||||
A: 确保数据库结构与代码中的表结构一致,特别是bookings表使用start_time字段而不是appointment_date字段。
|
||||
|
||||
## 📝 版本历史
|
||||
|
||||
### v3.0.0 (最新版)
|
||||
- ✨ **新增**: VIP客户最近预约记录显示功能
|
||||
- 🐛 **修复**: 数据库查询错误,优化表结构匹配
|
||||
- 🔧 **改进**: VIP客户与预约记录的关联逻辑
|
||||
- � **增强**: 首次到店提示功能
|
||||
|
||||
### v2.1.0 (2024-01-15)
|
||||
- ✨ **新增**: VIP客户搜索功能优化
|
||||
- 🐛 **修复**: VIP客户数据加载时序问题
|
||||
- 🔧 **改进**: 异步处理机制优化
|
||||
|
||||
### v2.0.0 (2024-01-10)
|
||||
- ✨ **新增**: VIP客户管理系统
|
||||
- ✨ **新增**: 订单状态管理
|
||||
- ✨ **新增**: 套餐配置功能
|
||||
|
||||
### v1.0.0 (2024-01-01)
|
||||
- 🎉 **初始版本**: 基础订单管理功能
|
||||
- ✨ **新增**: 客户信息管理
|
||||
- ✨ **新增**: 订单创建和处理
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 [MIT License](LICENSE) 开源协议。
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢所有为这个项目贡献代码、反馈问题和提出建议的开发者们!
|
||||
MIT License
|
||||
|
||||
---
|
||||
|
||||
**洗车店订单管理系统** - 让洗车店管理更简单、更高效! 🚗✨
|
||||
**张老师撸车工作室** - 专业洗车服务预约管理系统
|
||||
@@ -0,0 +1,170 @@
|
||||
# 张老师撸车工作室 - 洗车预约管理系统
|
||||
|
||||
## 版本功能汇总
|
||||
|
||||
### 1. 核心预约管理系统
|
||||
|
||||
**功能特点:**
|
||||
- **普通客户/VIP客户区分预约**:支持选择普通客户或VIP客户进行预约
|
||||
- **套餐选择**:可选择预设的洗车套餐
|
||||
- **时间冲突检测**:自动检查并避免预约时间冲突
|
||||
- **预约状态管理**:支持待确认、已确认、进行中、已完成、已取消等状态
|
||||
- **预约记录查询与筛选**:可按状态筛选和搜索预约记录
|
||||
- **预约时间更新**:支持修改已预约的时间
|
||||
- **付款状态管理**:可标记预约的付款状态(已付款/未付款)
|
||||
|
||||
**技术实现:**
|
||||
- 基于PHP和MySQL开发
|
||||
- 使用PDO进行数据库操作
|
||||
- 时间冲突算法确保资源合理分配
|
||||
- 表单验证和错误处理机制
|
||||
|
||||
### 2. 套餐管理系统
|
||||
|
||||
**功能特点:**
|
||||
- **套餐CRUD操作**:支持添加、编辑、删除和查看套餐
|
||||
- **套餐内容管理**:可定义套餐名称、描述、基础时长、价格和包含的服务项目
|
||||
- **套餐状态控制**:可启用/禁用套餐
|
||||
- **服务项目管理**:支持添加多个服务项目到套餐中
|
||||
- **可视化套餐列表**:卡片式展示,包含套餐详细信息
|
||||
|
||||
**界面优化:**
|
||||
- 现代化卡片式设计
|
||||
- 服务项目标签展示
|
||||
- 悬停效果和微动画
|
||||
- 响应式设计支持移动端
|
||||
|
||||
### 3. VIP客户管理系统
|
||||
|
||||
**功能特点:**
|
||||
- **VIP客户信息录入**:支持添加VIP客户基本信息
|
||||
- **VIP客户查询与管理**:可查看、编辑和删除VIP客户
|
||||
- **VIP客户识别**:基于手机号和车牌号组合唯一识别
|
||||
- **VIP客户预约**:支持快速选择VIP客户进行预约
|
||||
- **数据验证**:防止重复录入VIP客户
|
||||
|
||||
**技术实现:**
|
||||
- 复合唯一索引确保数据完整性
|
||||
- 表单验证和错误处理
|
||||
- VIP客户信息自动填充功能
|
||||
|
||||
### 4. 待预约处理系统(WPS表单集成)
|
||||
|
||||
**功能特点:**
|
||||
- **WPS表单数据同步**:自动获取WPS表单提交的预约请求
|
||||
- **预约转换功能**:可将WPS表单提交转换为正式预约
|
||||
- **套餐选择与价格计算**:支持为WPS表单提交选择套餐和计算价格
|
||||
- **时间选择与冲突检测**:转换时自动检测时间冲突
|
||||
- **状态管理**:标记已处理的表单提交
|
||||
|
||||
**技术实现:**
|
||||
- 数据库表设计支持WPS表单数据存储
|
||||
- 预约转换逻辑确保数据完整性
|
||||
- 错误处理和日志记录
|
||||
|
||||
### 5. 公告与待办列表系统
|
||||
|
||||
**功能特点:**
|
||||
- **今日预约概览**:展示当天所有预约记录
|
||||
- **状态统计**:显示待处理和已完成的预约数量
|
||||
- **日期选择**:支持查看不同日期的预约记录
|
||||
- **预约详情展示**:包含客户信息、套餐、时间和服务项目
|
||||
- **待办事项管理**:直观展示需要处理的预约
|
||||
|
||||
**界面优化:**
|
||||
- 响应式设计支持移动端
|
||||
- 统计卡片展示关键指标
|
||||
- 时间线式预约列表
|
||||
- 简洁明了的状态标识
|
||||
|
||||
### 6. 系统架构与技术栈
|
||||
|
||||
**技术栈:**
|
||||
- **后端语言**:PHP 7+
|
||||
- **数据库**:MySQL
|
||||
- **前端技术**:HTML5, CSS3, JavaScript, jQuery
|
||||
- **数据库连接**:PDO (PHP Data Objects)
|
||||
- **版本控制**:Git
|
||||
|
||||
**系统特点:**
|
||||
- 模块化设计,功能分离清晰
|
||||
- 数据库设计合理,关系明确
|
||||
- 安全性考虑(输入验证、SQL注入防护)
|
||||
- 响应式设计,支持桌面和移动设备
|
||||
- 用户友好的界面和交互体验
|
||||
|
||||
### 7. 界面与用户体验优化
|
||||
|
||||
**优化内容:**
|
||||
- **现代化UI设计**:卡片式布局、渐变效果、阴影处理
|
||||
- **响应式设计**:适配不同屏幕尺寸
|
||||
- **动画效果**:页面加载淡入、元素过渡动画
|
||||
- **表单体验**:统一的表单样式、输入验证、占位符提示
|
||||
- **交互反馈**:按钮悬停效果、操作成功/失败提示
|
||||
|
||||
**移动端适配:**
|
||||
- 响应式布局调整
|
||||
- 触摸友好的元素大小
|
||||
- 优化的移动端表单
|
||||
- 适配不同移动设备
|
||||
|
||||
## 版本信息
|
||||
|
||||
**当前版本:** v1.0.0
|
||||
|
||||
**发布日期:** 2024年
|
||||
|
||||
**主要改进:**
|
||||
1. 完整的预约管理流程
|
||||
2. 套餐管理功能
|
||||
3. VIP客户管理系统
|
||||
4. WPS表单集成
|
||||
5. 响应式界面设计
|
||||
6. 用户体验优化
|
||||
|
||||
## 系统要求
|
||||
|
||||
- PHP 7.0+
|
||||
- MySQL 5.6+
|
||||
- Web服务器(Apache/Nginx)
|
||||
- 支持PDO的PHP环境
|
||||
- 浏览器支持:Chrome、Firefox、Safari、Edge (最新版本)
|
||||
|
||||
## 安装与配置
|
||||
|
||||
1. 导入数据库脚本 `merged_db.sql`
|
||||
2. 配置数据库连接信息 `config.php`
|
||||
3. 将项目部署到Web服务器
|
||||
4. 访问系统首页开始使用
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. **添加套餐**:在套餐管理页面添加洗车套餐
|
||||
2. **管理VIP客户**:录入和管理VIP客户信息
|
||||
3. **创建预约**:在首页选择客户类型和套餐,填写预约信息
|
||||
4. **处理待预约**:在待预约页面处理WPS表单提交
|
||||
5. **查看公告**:在公告页面查看今日待办和统计信息
|
||||
|
||||
## 功能流程图
|
||||
|
||||
1. **预约流程**:选择客户类型 → 填写/选择客户信息 → 选择套餐 → 选择时间 → 确认预约
|
||||
2. **套餐管理**:添加套餐 → 编辑套餐 → 启用/禁用套餐 → 查看套餐列表
|
||||
3. **VIP管理**:录入VIP客户 → 查询/编辑VIP信息 → 使用VIP快速预约
|
||||
4. **WPS表单处理**:接收表单提交 → 转换为预约 → 标记为已处理
|
||||
|
||||
## 数据统计与分析
|
||||
|
||||
系统支持通过数据库查询获取以下统计数据:
|
||||
- 每日/每周/每月预约量
|
||||
- 不同套餐的销售情况
|
||||
- VIP客户数量和活跃度
|
||||
- 客户来源分析
|
||||
|
||||
## 未来计划
|
||||
|
||||
1. 增加支付功能集成
|
||||
2. 实现短信通知系统
|
||||
3. 添加数据可视化图表
|
||||
4. 完善会员积分系统
|
||||
5. 支持多门店管理
|
||||
6. 移动端App开发
|
||||
+86
-38
@@ -58,12 +58,13 @@ try {
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="description" content="今日洗车待办列表,直观展示今日需要处理的洗车预约">
|
||||
<meta name="keywords" content="公告,今日待办,预约列表,洗车管理">
|
||||
<title><?php echo $page_title; ?></title>
|
||||
<title>张老师撸车(私家车库)工作室</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="mobile-nav.js" defer></script>
|
||||
<style>
|
||||
/* 公告页面特有样式 */
|
||||
.announcement-container {
|
||||
max-width: 1200px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -245,6 +246,29 @@ try {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 客服备注样式 */
|
||||
.service-notes {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #007bff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.service-notes h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.service-notes p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.no-bookings {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
@@ -274,25 +298,42 @@ try {
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>🚗 洗车店管理系统</h1>
|
||||
<button class="mobile-menu-toggle" onclick="toggleMobileMenu()">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
<h1>🚗 张老师撸车工作室 - 今日待办</h1>
|
||||
<nav class="nav">
|
||||
<a href="index.php" class="nav-link">预约洗车</a>
|
||||
<a href="bookings.php" class="nav-link">预约管理</a>
|
||||
<a href="pending_bookings.php" class="nav-link">待处理预约</a>
|
||||
<a href="packages.php" class="nav-link">套餐管理</a>
|
||||
<a href="vip.php" class="nav-link">VIP管理</a>
|
||||
<a href="announcement.php" class="nav-link active">今日待办</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- 移动端导航菜单 -->
|
||||
<div class="mobile-nav-overlay" onclick="closeMobileMenu()"></div>
|
||||
<nav class="mobile-nav">
|
||||
<a href="index.php" class="nav-link">预约洗车</a>
|
||||
<a href="bookings.php" class="nav-link">预约管理</a>
|
||||
<a href="pending_bookings.php" class="nav-link">待处理预约</a>
|
||||
<a href="packages.php" class="nav-link">套餐管理</a>
|
||||
<a href="vip.php" class="nav-link">VIP管理</a>
|
||||
<a href="announcement.php" class="nav-link active">今日待办</a>
|
||||
</nav>
|
||||
|
||||
<div class="announcement-container">
|
||||
<!-- 页面标题区 -->
|
||||
<div class="page-header">
|
||||
<div class="page-header enhanced-card">
|
||||
<h1>📋 待办列表</h1>
|
||||
<div class="subtitle"><?php echo date('Y年m月d日', strtotime($selected_date)); ?> - <?php echo date('l', strtotime($selected_date)); ?></div>
|
||||
</div>
|
||||
|
||||
<!-- 日期选择器 -->
|
||||
<div style="background: white; border-radius: 10px; padding: 20px; margin-bottom: 30px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); text-align: center;">
|
||||
<div class="enhanced-card" style="text-align: center; padding: 20px; margin-bottom: 30px;">
|
||||
<form method="GET" style="display: flex; justify-content: center; align-items: center; gap: 15px; flex-wrap: wrap;">
|
||||
<label for="selected_date" style="font-weight: 600; color: #333;">选择日期:</label>
|
||||
<input type="date" id="selected_date" name="selected_date" value="<?php echo $selected_date; ?>" style="padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 5px; font-size: 1rem; width: 200px; transition: border-color 0.3s ease;">
|
||||
@@ -301,7 +342,7 @@ try {
|
||||
</div>
|
||||
|
||||
<!-- 统计信息卡片 -->
|
||||
<div class="stats-card">
|
||||
<div class="stats-card enhanced-card">
|
||||
<div class="stat-item stat-pending">
|
||||
<div class="stat-label">待处理</div>
|
||||
<div class="stat-number"><?php echo $pending_count; ?></div>
|
||||
@@ -352,48 +393,45 @@ try {
|
||||
if (!empty($booking['services'])) {
|
||||
$services = explode(',', $booking['services']);
|
||||
}
|
||||
if (!empty($booking['custom_services'])) {
|
||||
$services[] = $booking['custom_services'];
|
||||
}
|
||||
?>
|
||||
<div class="booking-card <?php echo $card_class; ?>">
|
||||
<div class="enhanced-card booking-card <?php echo $card_class; ?>">
|
||||
<div class="booking-time">
|
||||
🕒 <?php echo date('H:i', strtotime($booking['start_time'])); ?> - <?php echo date('H:i', strtotime($booking['end_time'])); ?>
|
||||
<span class="booking-status status-<?php echo strtolower($booking['status']); ?>">
|
||||
<span class="status-badge status-<?php echo strtolower($booking['status']); ?>">
|
||||
<?php echo $booking['status']; ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="booking-details">
|
||||
<div class="detail-row">
|
||||
<div class="booking-details details-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">客户姓名</span>
|
||||
<span class="detail-value"><?php echo htmlspecialchars($booking['customer_name']); ?></span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">联系电话</span>
|
||||
<span class="detail-value"><?php echo htmlspecialchars($booking['phone']); ?></span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">车型</span>
|
||||
<span class="detail-value"><?php echo htmlspecialchars($booking['car_model']); ?></span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">车牌号</span>
|
||||
<span class="detail-value"><?php echo htmlspecialchars($booking['car_number']); ?></span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">套餐</span>
|
||||
<span class="detail-value"><?php echo htmlspecialchars($booking['package_name'] ?: '自定义套餐'); ?></span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">时长</span>
|
||||
<span class="detail-value"><?php echo $booking['duration']; ?> 分钟</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">价格</span>
|
||||
<span class="detail-value">¥<?php echo $booking['total_price']; ?></span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">会员类型</span>
|
||||
<span class="detail-value"><?php echo $booking['member_type']; ?></span>
|
||||
</div>
|
||||
@@ -409,11 +447,19 @@ try {
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 客服备注 -->
|
||||
<?php if (!empty($booking['custom_services'])): ?>
|
||||
<div class="service-notes">
|
||||
<h4>客服备注:</h4>
|
||||
<p><?php echo htmlspecialchars($booking['custom_services']); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 备注信息 -->
|
||||
<?php if (!empty($booking['notes'])): ?>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">备注</span>
|
||||
<span class="detail-label">客户备注</span>
|
||||
<span class="detail-value"><?php echo htmlspecialchars($booking['notes']); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
@@ -429,18 +475,19 @@ try {
|
||||
// 移动端优化脚本
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 为按钮添加触摸反馈
|
||||
const buttons = document.querySelectorAll('.btn');
|
||||
buttons.forEach(btn => {
|
||||
var buttons = document.querySelectorAll('.btn');
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
var btn = buttons[i];
|
||||
btn.addEventListener('touchstart', function() {
|
||||
this.style.transform = 'translateY(1px)';
|
||||
});
|
||||
btn.addEventListener('touchend', function() {
|
||||
this.style.transform = 'translateY(-2px)';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 检测设备类型
|
||||
const isMobile = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
var isMobile = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
if (isMobile) {
|
||||
document.body.classList.add('mobile-device');
|
||||
}
|
||||
@@ -451,8 +498,8 @@ try {
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
// 将倒计时添加到页面标题旁边
|
||||
const pageHeader = document.querySelector('.page-header');
|
||||
const refreshInfo = document.createElement('div');
|
||||
var pageHeader = document.querySelector('.page-header');
|
||||
var refreshInfo = document.createElement('div');
|
||||
refreshInfo.style.cssText = 'font-size:0.9rem;color:#666;margin-top:5px;';
|
||||
|
||||
if (pageHeader) {
|
||||
@@ -465,28 +512,29 @@ try {
|
||||
|
||||
// 显示北京时间及距离下次刷新倒计时
|
||||
function updateBeijingTime() {
|
||||
const now = new Date();
|
||||
const beijingTime = new Date(now.getTime() + 8 * 60 * 60 * 1000); // 转为北京时间
|
||||
const hours = String(beijingTime.getUTCHours()).padStart(2, '0');
|
||||
const minutes = String(beijingTime.getUTCMinutes()).padStart(2, '0');
|
||||
const seconds = String(beijingTime.getUTCSeconds()).padStart(2, '0');
|
||||
var now = new Date();
|
||||
var beijingTime = new Date(now.getTime() + 8 * 60 * 60 * 1000); // 转为北京时间
|
||||
var hours = String(beijingTime.getUTCHours()).padStart(2, '0');
|
||||
var minutes = String(beijingTime.getUTCMinutes()).padStart(2, '0');
|
||||
var seconds = String(beijingTime.getUTCSeconds()).padStart(2, '0');
|
||||
|
||||
// 计算距离下次刷新的剩余时间
|
||||
const refreshInterval = 5 * 60 * 1000; // 5分钟
|
||||
const elapsed = now.getTime() % refreshInterval;
|
||||
const remaining = refreshInterval - elapsed;
|
||||
const remMinutes = Math.floor(remaining / 60000);
|
||||
const remSeconds = Math.floor((remaining % 60000) / 1000);
|
||||
var refreshInterval = 5 * 60 * 1000; // 5分钟
|
||||
var elapsed = now.getTime() % refreshInterval;
|
||||
var remaining = refreshInterval - elapsed;
|
||||
var remMinutes = Math.floor(remaining / 60000);
|
||||
var remSeconds = Math.floor((remaining % 60000) / 1000);
|
||||
|
||||
// 更新显示
|
||||
if (refreshInfo) {
|
||||
refreshInfo.textContent = `北京时间 ${hours}:${minutes}:${seconds} | 下次刷新 ${remMinutes}分${remSeconds}秒`;
|
||||
refreshInfo.textContent = '北京时间 ' + hours + ':' + minutes + ':' + seconds + ' | 下次刷新 ' + remMinutes + '分' + remSeconds + '秒';
|
||||
}
|
||||
}
|
||||
|
||||
// 每秒更新一次
|
||||
setInterval(updateBeijingTime, 1000);
|
||||
updateBeijingTime(); // 初始化显示
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
/**
|
||||
* 预约信息模板文件
|
||||
* 此文件用于定义预约成功后发送给客户的信息模板
|
||||
* 用户可以根据自己的需求修改此模板
|
||||
*/
|
||||
|
||||
// 预约成功信息模板配置
|
||||
$booking_templates = [
|
||||
// 基本模板
|
||||
'basic' => [
|
||||
'title' => '预约成功!',
|
||||
'message' => "预约成功!\n\n" .
|
||||
"客户:{customer_name}\n" .
|
||||
"手机号:{phone}\n" .
|
||||
"车牌号:{car_number}\n" .
|
||||
"车型:{car_model}\n" .
|
||||
"\n" .
|
||||
"预约时间:{start_time}\n" .
|
||||
"服务项目:{package_name}\n" .
|
||||
"服务时长:{duration}分钟\n" .
|
||||
"总价:{total_price}元\n" .
|
||||
"\n" .
|
||||
"感谢您的预约!"
|
||||
],
|
||||
// 详细模板
|
||||
'detailed' => [
|
||||
'title' => '【张老师撸车工作室】预约确认',
|
||||
'message' => "尊敬的 {customer_name} 先生/女士:\n\n" .
|
||||
"恭喜您,您的洗车预约已成功确认!\n\n" .
|
||||
"🔹 客户信息:\n" .
|
||||
" - 姓名:{customer_name}\n" .
|
||||
" - 手机号:{phone}\n" .
|
||||
" - 车牌号:{car_number}\n" .
|
||||
" - 车型:{car_model}\n" .
|
||||
" - 会员类型:{member_type}\n" .
|
||||
"\n" .
|
||||
"🔹 预约详情:\n" .
|
||||
" - 预约时间:{date} {time_range}\n" .
|
||||
" - 服务项目:{package_name}\n" .
|
||||
" - 自定义服务:{custom_services}\n" .
|
||||
" - 服务时长:{duration}分钟\n" .
|
||||
" - 总价:¥{total_price}\n" .
|
||||
" - 支付状态:{payment_status}\n" .
|
||||
"\n" .
|
||||
"🔹 备注信息:\n" .
|
||||
" {notes}\n" .
|
||||
"\n" .
|
||||
"🔹 温馨提示:\n" .
|
||||
" 1. 请提前10分钟到达洗车地点\n" .
|
||||
" 2. 如需改期或取消,请至少提前2小时联系我们\n" .
|
||||
" 3. 联系电话:138-0013-8000\n" .
|
||||
"\n" .
|
||||
"感谢您选择张老师撸车工作室!\n" .
|
||||
"祝您用车愉快!🚗✨"
|
||||
],
|
||||
// 简洁模板
|
||||
'simple' => [
|
||||
'title' => '预约确认',
|
||||
'message' => "{customer_name},您的洗车预约已确认:\n" .
|
||||
"时间:{start_time}\n" .
|
||||
"项目:{package_name}\n" .
|
||||
"总价:{total_price}元\n" .
|
||||
"如需调整请联系我们。"
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* 生成预约信息
|
||||
* @param array $booking_data 预约数据数组
|
||||
* @param string $template_name 使用的模板名称
|
||||
* @return string 生成的预约信息
|
||||
*/
|
||||
function generateBookingMessage($booking_data, $template_name = 'basic') {
|
||||
global $booking_templates;
|
||||
|
||||
// 检查模板是否存在
|
||||
if (!isset($booking_templates[$template_name])) {
|
||||
$template_name = 'basic'; // 默认使用基本模板
|
||||
}
|
||||
|
||||
$template = $booking_templates[$template_name]['message'];
|
||||
|
||||
// 定义替换字段
|
||||
$replacements = [
|
||||
'{customer_name}' => $booking_data['customer_name'] ?? '未知客户',
|
||||
'{phone}' => $booking_data['phone'] ?? '未提供',
|
||||
'{car_model}' => $booking_data['car_model'] ?? '未提供',
|
||||
'{car_number}' => $booking_data['car_number'] ?? '未提供',
|
||||
'{member_type}' => $booking_data['member_type'] ?? '普通客户',
|
||||
'{package_name}' => $booking_data['package_name'] ?? '未选择套餐',
|
||||
'{custom_services}' => $booking_data['custom_services'] ?? '无',
|
||||
'{start_time}' => $booking_data['start_time'] ?? '未设置',
|
||||
'{end_time}' => $booking_data['end_time'] ?? '未设置',
|
||||
'{date}' => $booking_data['date'] ?? (isset($booking_data['start_time']) ? date('Y年m月d日', strtotime($booking_data['start_time'])) : '未设置'),
|
||||
'{time_range}' => $booking_data['time_range'] ?? (isset($booking_data['start_time'], $booking_data['end_time']) ? date('H:i', strtotime($booking_data['start_time'])) . ' - ' . date('H:i', strtotime($booking_data['end_time'])) : '未设置'),
|
||||
'{duration}' => $booking_data['duration'] ?? '未设置',
|
||||
'{total_price}' => $booking_data['total_price'] ?? '0.00',
|
||||
'{notes}' => $booking_data['notes'] ?? '无',
|
||||
'{status}' => $booking_data['status'] ?? '未确认',
|
||||
'{payment_status}' => $booking_data['payment_status'] ?? '未支付',
|
||||
'{source}' => $booking_data['source'] ?? '未知',
|
||||
'{created_at}' => $booking_data['created_at'] ?? date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// 替换模板中的占位符
|
||||
$message = str_replace(array_keys($replacements), array_values($replacements), $template);
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定模板的标题
|
||||
* @param string $template_name 模板名称
|
||||
* @return string 模板标题
|
||||
*/
|
||||
function getTemplateTitle($template_name = 'basic') {
|
||||
global $booking_templates;
|
||||
|
||||
if (isset($booking_templates[$template_name])) {
|
||||
return $booking_templates[$template_name]['title'];
|
||||
}
|
||||
|
||||
return $booking_templates['basic']['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的模板名称
|
||||
* @return array 模板名称数组
|
||||
*/
|
||||
function getAvailableTemplates() {
|
||||
global $booking_templates;
|
||||
return array_keys($booking_templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例用法:
|
||||
* 1. 包含此文件:require_once 'booking_template.php';
|
||||
* 2. 准备预约数据:
|
||||
* $booking_data = [
|
||||
* 'customer_name' => '张三',
|
||||
* 'phone' => '138-0013-8000',
|
||||
* 'car_model' => '特斯拉 Model 3',
|
||||
* 'car_number' => '京A12345',
|
||||
* 'member_type' => 'VIP会员',
|
||||
* 'package_name' => '精致洗车套餐',
|
||||
* 'start_time' => '2023-10-01 10:00:00',
|
||||
* 'end_time' => '2023-10-01 11:00:00',
|
||||
* 'duration' => 60,
|
||||
* 'total_price' => '128.00',
|
||||
* 'notes' => '车顶上有行李架,请小心清洗',
|
||||
* 'status' => '已确认',
|
||||
* 'payment_status' => '已付款',
|
||||
* 'source' => '微信'
|
||||
* ];
|
||||
* 3. 生成预约信息:
|
||||
* $message = generateBookingMessage($booking_data, 'detailed');
|
||||
* 4. 输出或使用生成的信息:
|
||||
* echo $message;
|
||||
*/
|
||||
?>
|
||||
+211
-12
@@ -25,6 +25,13 @@ if (isset($_POST['action']) && isset($_POST['booking_id'])) {
|
||||
$stmt = $pdo->prepare("UPDATE bookings SET start_time = ?, end_time = ? WHERE id = ?");
|
||||
$stmt->execute([$new_start_time, $new_end_time, $booking_id]);
|
||||
$success_message = '预约时间更新成功!';
|
||||
} elseif ($action == 'update_notes' && isset($_POST['notes_content'])) {
|
||||
// 更新客服备注
|
||||
$notes_content = $_POST['notes_content'];
|
||||
$stmt = $pdo->prepare("UPDATE bookings SET custom_services = ? WHERE id = ?");
|
||||
$stmt->execute([$notes_content, $booking_id]);
|
||||
echo 'success';
|
||||
exit();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error_message = '更新失败:' . $e->getMessage();
|
||||
@@ -34,7 +41,7 @@ if (isset($_POST['action']) && isset($_POST['booking_id'])) {
|
||||
// 获取所有预约,支持状态筛选和搜索功能
|
||||
try {
|
||||
// 构建查询,支持筛选和搜索
|
||||
$query = "SELECT b.*, p.package_name FROM bookings b LEFT JOIN packages p ON b.package_id = p.id WHERE 1=1 ";
|
||||
$query = "SELECT b.*, p.package_name, p.package_reminder FROM bookings b LEFT JOIN packages p ON b.package_id = p.id WHERE 1=1 ";
|
||||
$params = [];
|
||||
|
||||
// 状态筛选
|
||||
@@ -74,9 +81,10 @@ try {
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="description" content="洗车预约管理列表,查看和管理所有预约记录">
|
||||
<meta name="keywords" content="预约管理,洗车预约,预约列表">
|
||||
<title>预约列表 - 洗车预约系统</title>
|
||||
<title>张老师撸车(私家车库)工作室</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="mobile-nav.js" defer></script>
|
||||
|
||||
<!-- Favicon for mobile devices -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚗</text></svg>">
|
||||
@@ -84,15 +92,32 @@ try {
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>🚗 洗车预约系统 - 预约管理</h1>
|
||||
<button class="mobile-menu-toggle" onclick="toggleMobileMenu()" aria-label="菜单">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
<h1>🚗 张老师撸车工作室 - 预约管理</h1>
|
||||
<nav class="nav">
|
||||
<a href="index.php" class="nav-link">预约洗车</a>
|
||||
<a href="bookings.php" class="nav-link active">预约管理</a>
|
||||
<a href="pending_bookings.php" class="nav-link">待处理预约</a>
|
||||
<a href="packages.php" class="nav-link">套餐管理</a>
|
||||
<a href="vip.php" class="nav-link">VIP管理</a>
|
||||
<a href="announcement.php" class="nav-link">今日待办</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- 移动端导航菜单 -->
|
||||
<div class="mobile-nav-overlay" onclick="closeMobileMenu()"></div>
|
||||
<nav class="mobile-nav">
|
||||
<a href="index.php" class="nav-link">预约洗车</a>
|
||||
<a href="bookings.php" class="nav-link active">预约管理</a>
|
||||
<a href="pending_bookings.php" class="nav-link">待处理预约</a>
|
||||
<a href="packages.php" class="nav-link">套餐管理</a>
|
||||
<a href="vip.php" class="nav-link">VIP管理</a>
|
||||
<a href="announcement.php" class="nav-link">今日待办</a>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($success_message)): ?>
|
||||
<div class="success-message"><?php echo $success_message; ?></div>
|
||||
@@ -104,11 +129,11 @@ try {
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card enhanced-card">
|
||||
<h2>所有预约 (共 <?php echo count($bookings); ?> 条)</h2>
|
||||
|
||||
<!-- 筛选和搜索区域 -->
|
||||
<div class="filter-search-area">
|
||||
<div class="filter-search-area enhanced-card">
|
||||
<form method="GET" class="filter-form">
|
||||
<div class="filter-group">
|
||||
<label for="status_filter">状态筛选:</label>
|
||||
@@ -136,7 +161,7 @@ try {
|
||||
<div class="empty-message">暂无预约记录</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($bookings as $booking): ?>
|
||||
<div class="package-card">
|
||||
<div class="package-card enhanced-card">
|
||||
<div class="package-header">
|
||||
<h3><?php echo htmlspecialchars($booking['customer_name']); ?> 的预约</h3>
|
||||
<div class="package-status">
|
||||
@@ -213,17 +238,31 @@ try {
|
||||
|
||||
<?php if ($booking['notes']): ?>
|
||||
<div class="package-description">
|
||||
<span class="detail-label">备注:</span>
|
||||
<span class="detail-label">客户备注:</span>
|
||||
<span><?php echo htmlspecialchars($booking['notes']); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($booking['custom_services']): ?>
|
||||
<div class="package-description">
|
||||
<span class="detail-label">客服备注:</span>
|
||||
<span><?php echo htmlspecialchars($booking['custom_services']); ?></span>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="openEditNotesModal(<?php echo $booking['id']; ?>, '<?php echo htmlspecialchars($booking['custom_services']); ?>')" style="margin-left: 10px;">修改</button>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="package-description">
|
||||
<span class="detail-label">客服备注:</span>
|
||||
<span style="color: #999;">无</span>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="openEditNotesModal(<?php echo $booking['id']; ?>, '<?php echo htmlspecialchars($booking['custom_services']); ?>')" style="margin-left: 10px;">添加</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="package-meta">
|
||||
<span>预约时间:<?php echo $booking['created_at']; ?></span>
|
||||
</div>
|
||||
|
||||
<?php if ($booking['status'] !== '已完成' && $booking['status'] !== '已取消'): ?>
|
||||
<div class="package-actions">
|
||||
<div class="package-actions">
|
||||
<?php if ($booking['status'] !== '已完成' && $booking['status'] !== '已取消'): ?>
|
||||
<!-- 状态更新按钮 - 使用AJAX -->
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-success" onclick="updateStatus(<?php echo $booking['id']; ?>, '已确认')">确认</button>
|
||||
@@ -239,8 +278,49 @@ try {
|
||||
<br>
|
||||
<!-- 修改预约时间按钮 -->
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="openEditModal(<?php echo $booking['id']; ?>, '<?php echo date('Y-m-d\TH:i', strtotime($booking['start_time'])); ?>', '<?php echo date('Y-m-d\TH:i', strtotime($booking['end_time'])); ?>')">修改预约时间</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<br>
|
||||
<?php endif; ?>
|
||||
<!-- 转换为VIP客户按钮 -->
|
||||
<?php
|
||||
// 检查该客户是否已经是VIP
|
||||
$stmt_check_vip = $pdo->prepare("SELECT id FROM vip_customers WHERE phone = ? AND car_number = ? AND is_active = 1");
|
||||
$stmt_check_vip->execute([$booking['phone'], $booking['car_number']]);
|
||||
$is_vip = $stmt_check_vip->fetch() !== false;
|
||||
?>
|
||||
<?php if (!$is_vip && $booking['member_type'] === '普通客户'): ?>
|
||||
<form method="POST" action="vip.php" style="display: inline; margin-top: 10px;">
|
||||
<input type="hidden" name="action" value="convert_from_booking">
|
||||
<input type="hidden" name="booking_id" value="<?php echo $booking['id']; ?>">
|
||||
<button type="submit" class="btn btn-sm" style="background-color: #ffd700; color: #333; font-weight: bold;"
|
||||
onclick="return confirm('确定要将该客户转换为VIP客户吗?\n\n客户:<?php echo htmlspecialchars($booking['customer_name']); ?>\n手机号:<?php echo htmlspecialchars($booking['phone']); ?>\n车牌号:<?php echo htmlspecialchars($booking['car_number']); ?>')">
|
||||
👑 转换为VIP客户
|
||||
</button>
|
||||
</form>
|
||||
<br>
|
||||
<?php endif; ?>
|
||||
<!-- 复制预约信息按钮 -->
|
||||
<?php
|
||||
// 计算服务时长(分钟)
|
||||
$start = strtotime($booking['start_time']);
|
||||
$end = strtotime($booking['end_time']);
|
||||
$duration = round(($end - $start) / 60);
|
||||
// 获取车友备注(如果不存在则为空字符串)
|
||||
$notes = $booking['notes'] ?? '';
|
||||
?>
|
||||
<button type="button" class="btn btn-sm btn-copy"
|
||||
data-id="<?php echo $booking['id']; ?>"
|
||||
data-customer-name="<?php echo htmlspecialchars($booking['customer_name']); ?>"
|
||||
data-phone="<?php echo htmlspecialchars($booking['phone']); ?>"
|
||||
data-car-model="<?php echo htmlspecialchars($booking['car_model']); ?>"
|
||||
data-car-number="<?php echo htmlspecialchars($booking['car_number']); ?>"
|
||||
data-package-name="<?php echo htmlspecialchars($booking['package_name'] ?? '未选择套餐'); ?>"
|
||||
data-date="<?php echo date('Y-m-d', strtotime($booking['start_time'])); ?>"
|
||||
data-time="<?php echo date('H:i', strtotime($booking['start_time'])) . ' - ' . date('H:i', strtotime($booking['end_time'])); ?>"
|
||||
data-duration="<?php echo $duration; ?>"
|
||||
data-notes="<?php echo htmlspecialchars($notes); ?>"
|
||||
data-package-reminder="<?php echo htmlspecialchars($booking['package_reminder'] ?? ''); ?>"
|
||||
onclick="copyMessage(this)">复制预约信息</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
@@ -250,6 +330,27 @@ try {
|
||||
<a href="index.php" class="btn">返回预约页面</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑客服备注模态框 -->
|
||||
<div id="editNotesModal" style="display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4);">
|
||||
<div style="background-color: #fefefe; margin: 15% auto; padding: 20px; border-radius: 8px; width: 90%; max-width: 500px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);">
|
||||
<div class="modal-header">
|
||||
<h3>编辑客服备注</h3>
|
||||
<span class="close-modal" onclick="closeEditNotesModal()">×</span>
|
||||
</div>
|
||||
<form id="editNotesForm">
|
||||
<input type="hidden" id="editNotesBookingId" name="booking_id">
|
||||
<div class="form-group">
|
||||
<label for="editNotesContent">客服备注内容:</label>
|
||||
<textarea id="editNotesContent" name="notes_content" rows="5" placeholder="请输入客服备注内容..." style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;"></textarea>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-sm" onclick="closeEditNotesModal()">取消</button>
|
||||
<button type="submit" class="btn btn-sm btn-primary">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改预约时间模态框 -->
|
||||
<div id="editTimeModal" class="modal">
|
||||
@@ -461,15 +562,56 @@ try {
|
||||
document.getElementById('editTimeModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// 编辑客服备注相关函数
|
||||
function openEditNotesModal(bookingId, notesContent) {
|
||||
document.getElementById('editNotesBookingId').value = bookingId;
|
||||
document.getElementById('editNotesContent').value = notesContent;
|
||||
document.getElementById('editNotesModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeEditNotesModal() {
|
||||
document.getElementById('editNotesModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// 点击模态框外部关闭模态框
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('editTimeModal');
|
||||
const notesModal = document.getElementById('editNotesModal');
|
||||
if (event.target == modal) {
|
||||
closeEditModal();
|
||||
} else if (event.target == notesModal) {
|
||||
closeEditNotesModal();
|
||||
}
|
||||
}
|
||||
|
||||
// 表单提交处理 - 使用AJAX
|
||||
// 编辑客服备注表单提交处理 - 使用AJAX
|
||||
document.getElementById('editNotesForm').addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const bookingId = document.getElementById('editNotesBookingId').value;
|
||||
const notesContent = document.getElementById('editNotesContent').value;
|
||||
|
||||
// 创建AJAX请求
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', 'bookings.php', true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
// 刷新页面以显示更新后的备注
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('保存失败,请重试');
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
alert('网络错误,请重试');
|
||||
};
|
||||
|
||||
// 发送请求
|
||||
xhr.send('action=update_notes&booking_id=' + bookingId + '¬es_content=' + encodeURIComponent(notesContent));
|
||||
});
|
||||
|
||||
// 修改预约时间表单提交处理 - 使用AJAX
|
||||
document.getElementById('editTimeForm').addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -600,6 +742,63 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<!-- 单独的JavaScript块用于复制功能 -->
|
||||
<!-- 复制预约信息功能 -->
|
||||
<script>
|
||||
function copyMessage(button) {
|
||||
try {
|
||||
// 从按钮的data属性获取预约信息
|
||||
var id = button.dataset.id;
|
||||
var customer_name = button.dataset.customerName;
|
||||
var phone = button.dataset.phone;
|
||||
var car_model = button.dataset.carModel;
|
||||
var car_number = button.dataset.carNumber;
|
||||
var package_name = button.dataset.packageName;
|
||||
var date = button.dataset.date;
|
||||
var time = button.dataset.time;
|
||||
var duration = button.dataset.duration;
|
||||
var notes = button.dataset.notes;
|
||||
var package_reminder = button.dataset.packageReminder;
|
||||
|
||||
var msg = "【张老师撸车工作室】预约确认\n\n";
|
||||
msg += "客户姓名:" + customer_name + "\n";
|
||||
msg += "联系方式:" + phone + "\n";
|
||||
msg += "车型:" + car_model + "\n";
|
||||
msg += "车牌号:" + car_number + "\n";
|
||||
msg += "预约日期:" + date + "\n";
|
||||
msg += "套餐:" + package_name + "\n";
|
||||
msg += "预约时间:" + time + "\n";
|
||||
msg += "预计服务时长:" + duration + "分钟\n";
|
||||
msg += "车友备注:" + notes + "\n";
|
||||
msg += "" + (package_reminder || "") + "\n\n";
|
||||
|
||||
|
||||
// 复制到剪贴板
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = msg;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
alert('预约信息已复制到剪贴板!');
|
||||
} catch (err) {
|
||||
alert('复制失败,请手动复制!');
|
||||
}
|
||||
|
||||
document.body.removeChild(ta);
|
||||
} catch (e) {
|
||||
alert('复制过程中发生错误,请稍后重试!');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
// 移动端优化脚本
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 为按钮添加触摸反馈
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
-- carwash_db.sql - 数据库创建脚本
|
||||
CREATE DATABASE IF NOT EXISTS carwash_booking;
|
||||
USE carwash_booking;
|
||||
|
||||
-- 创建VIP客户表
|
||||
CREATE TABLE IF NOT EXISTS vip_customers (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
customer_name VARCHAR(100) NOT NULL COMMENT '客户姓名',
|
||||
phone VARCHAR(20) NOT NULL COMMENT '联系电话',
|
||||
car_model VARCHAR(50) COMMENT '车型',
|
||||
car_number VARCHAR(20) COMMENT '车牌号',
|
||||
email VARCHAR(100) COMMENT '邮箱地址',
|
||||
birthday DATE COMMENT '生日',
|
||||
notes TEXT COMMENT '备注信息',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NULL
|
||||
);
|
||||
|
||||
-- 更新VIP客户表的索引结构
|
||||
-- 1. 移除phone字段的UNIQUE约束(如果存在)
|
||||
ALTER TABLE vip_customers DROP INDEX IF EXISTS phone;
|
||||
|
||||
-- 2. 添加phone和car_number的复合唯一索引
|
||||
ALTER TABLE vip_customers ADD UNIQUE INDEX idx_phone_car_number (phone, car_number);
|
||||
|
||||
-- 3. 验证索引添加成功
|
||||
SHOW INDEX FROM vip_customers;
|
||||
|
||||
-- 创建套餐表
|
||||
CREATE TABLE IF NOT EXISTS packages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
package_name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
base_duration INT NOT NULL COMMENT '基础服务时长(分钟)',
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
services TEXT NOT NULL COMMENT '包含的服务项目(用逗号分隔)',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NULL
|
||||
);
|
||||
|
||||
-- 修改预约表支持时间段、会员类型和来源
|
||||
DROP TABLE IF EXISTS bookings;
|
||||
CREATE TABLE IF NOT EXISTS bookings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
customer_name VARCHAR(100) NOT NULL,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
car_model VARCHAR(50) NOT NULL,
|
||||
car_number VARCHAR(20) NOT NULL,
|
||||
package_id INT,
|
||||
custom_services TEXT COMMENT '自定义服务内容',
|
||||
start_time DATETIME NOT NULL,
|
||||
end_time DATETIME NOT NULL,
|
||||
duration INT NOT NULL COMMENT '实际服务时长(分钟)',
|
||||
total_price DECIMAL(10,2) NOT NULL,
|
||||
notes TEXT,
|
||||
status ENUM('待确认', '已确认', '进行中', '已完成', '已取消') DEFAULT '待确认',
|
||||
member_type ENUM('普通客户', 'VIP会员') DEFAULT '普通客户' COMMENT '会员类型',
|
||||
source ENUM('抖音', '微信', '快手', '朋友介绍', '其他') DEFAULT '其他' COMMENT '客户来源',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NULL,
|
||||
FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- 插入示例套餐数据
|
||||
INSERT INTO packages (package_name, description, base_duration, price, services) VALUES
|
||||
('基础洗车', '基础外观清洗', 30, 50.00, '外观冲洗,泡沫清洁,内饰吸尘'),
|
||||
('精洗套餐', '全面深度清洗', 90, 150.00, '外观精洗,内饰深度清洁,轮胎清洁,打蜡'),
|
||||
('VIP套餐', '顶级豪华洗护', 180, 300.00, '全套精洗,抛光打蜡,内饰护理,发动机清洁,真皮护理');
|
||||
|
||||
-- 插入示例预约数据(包含会员类型和来源)
|
||||
INSERT INTO bookings (customer_name, phone, car_model, car_number, package_id, start_time, end_time, duration, total_price, notes, member_type, source) VALUES
|
||||
('张三', '13800138001', '大众朗逸', '京A12345', 1, '2024-12-20 09:00:00', '2024-12-20 09:30:00', 30, 50.00, '第一次来', '普通客户', '抖音'),
|
||||
('李四', '13800138002', '丰田凯美瑞', '京B67890', 2, '2024-12-20 10:30:00', '2024-12-20 12:00:00', 90, 150.00, '需要特别清洗内饰', 'VIP会员', '朋友介绍'),
|
||||
('王五', '13800138003', '宝马X3', '沪C88888', 3, '2024-12-21 14:00:00', '2024-12-21 17:00:00', 180, 300.00, 'VIP客户,定期保养', 'VIP会员', '微信'),
|
||||
('赵六', '13800138004', '本田雅阁', '粤A66666', 1, '2024-12-21 09:30:00', '2024-12-21 10:00:00', 30, 50.00, '快手看到广告来的', '普通客户', '快手');
|
||||
|
||||
-- 插入示例VIP客户数据
|
||||
INSERT INTO vip_customers (customer_name, phone, car_model, car_number, email, birthday, notes) VALUES
|
||||
('张总', '13900139001', '奔驰S500', '京V88888', 'zhang@example.com', '1980-05-15', '长期VIP客户,商务人士'),
|
||||
('王女士', '13900139002', '奥迪A6L', '京A66666', 'wang@example.com', '1985-03-20', '女企业家,每月定期保养'),
|
||||
('李先生', '13900139003', 'BMW X5', '沪B99999', 'li@example.com', '1978-11-10', '房地产公司老板,对服务要求较高');
|
||||
@@ -1,30 +0,0 @@
|
||||
-- 创建WPS表单数据表
|
||||
CREATE TABLE IF NOT EXISTS wps_form_submissions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
rid VARCHAR(50) NOT NULL COMMENT '表单提交ID',
|
||||
form_id VARCHAR(50) NOT NULL COMMENT '表单ID',
|
||||
form_title VARCHAR(255) NOT NULL COMMENT '表单标题',
|
||||
creator_id VARCHAR(50) NOT NULL COMMENT '创建者ID',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间',
|
||||
update_time DATETIME NOT NULL COMMENT '更新时间',
|
||||
mobile VARCHAR(20) COMMENT '请输入手机号',
|
||||
name VARCHAR(255) COMMENT '怎么称呼您',
|
||||
license_plate VARCHAR(20) COMMENT '车牌号',
|
||||
date DATE COMMENT '日期',
|
||||
time_slot VARCHAR(20) COMMENT '时间段',
|
||||
car_type VARCHAR(50) COMMENT '车型',
|
||||
has_car_coat VARCHAR(10) COMMENT '是否有车衣',
|
||||
car_wash_habit VARCHAR(10) COMMENT '有无自己撸车习惯',
|
||||
car_wash_experience VARCHAR(50) COMMENT '撸车经验',
|
||||
wash_frequency VARCHAR(50) COMMENT '洗车频率',
|
||||
age_group VARCHAR(20) COMMENT '请选择年龄段',
|
||||
remarks TEXT COMMENT '备注内容',
|
||||
auto_number VARCHAR(50) COMMENT '自动编号',
|
||||
status VARCHAR(20) DEFAULT 'pending' COMMENT '状态',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
|
||||
updated_at DATETIME NOT NULL COMMENT '记录更新时间',
|
||||
INDEX idx_rid (rid),
|
||||
INDEX idx_license_plate (license_plate),
|
||||
INDEX idx_date (date),
|
||||
INDEX idx_mobile (mobile)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
// db_connect.php - 数据库连接文件
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
require_once 'config.php';
|
||||
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
/**
|
||||
* 预约信息模板使用示例
|
||||
* 此文件展示如何使用 booking_template.php 来生成预约信息
|
||||
*/
|
||||
|
||||
// 包含模板文件
|
||||
require_once 'booking_template.php';
|
||||
|
||||
// 示例1:使用基本模板
|
||||
function example_basic_template() {
|
||||
echo "=== 示例1:使用基本模板 ===\n\n";
|
||||
|
||||
// 准备预约数据
|
||||
$booking_data = [
|
||||
'customer_name' => '张三',
|
||||
'phone' => '138-0013-8000',
|
||||
'car_number' => '京A12345',
|
||||
'car_model' => '特斯拉 Model 3',
|
||||
'start_time' => '2023-10-01 10:00:00',
|
||||
'package_name' => '精致洗车套餐',
|
||||
'duration' => 60,
|
||||
'total_price' => '128.00'
|
||||
];
|
||||
|
||||
// 生成预约信息
|
||||
$message = generateBookingMessage($booking_data, 'basic');
|
||||
|
||||
// 输出结果
|
||||
echo "模板标题:" . getTemplateTitle('basic') . "\n";
|
||||
echo "模板内容:\n" . $message . "\n\n";
|
||||
}
|
||||
|
||||
// 示例2:使用详细模板
|
||||
function example_detailed_template() {
|
||||
echo "=== 示例2:使用详细模板 ===\n\n";
|
||||
|
||||
// 准备完整的预约数据
|
||||
$booking_data = [
|
||||
'customer_name' => '李四',
|
||||
'phone' => '139-0013-9000',
|
||||
'car_model' => '宝马 5系',
|
||||
'car_number' => '沪B67890',
|
||||
'member_type' => 'VIP会员',
|
||||
'package_name' => '深度清洁套餐',
|
||||
'custom_services' => '内饰消毒 + 轮胎养护',
|
||||
'start_time' => '2023-10-02 14:30:00',
|
||||
'end_time' => '2023-10-02 16:00:00',
|
||||
'duration' => 90,
|
||||
'total_price' => '298.00',
|
||||
'notes' => '车辆右前门有轻微划痕,清洗时请注意',
|
||||
'status' => '已确认',
|
||||
'payment_status' => '已付款',
|
||||
'source' => '抖音'
|
||||
];
|
||||
|
||||
// 生成预约信息
|
||||
$message = generateBookingMessage($booking_data, 'detailed');
|
||||
|
||||
// 输出结果
|
||||
echo "模板标题:" . getTemplateTitle('detailed') . "\n";
|
||||
echo "模板内容:\n" . $message . "\n\n";
|
||||
}
|
||||
|
||||
// 示例3:使用简洁模板
|
||||
function example_simple_template() {
|
||||
echo "=== 示例3:使用简洁模板 ===\n\n";
|
||||
|
||||
// 准备简约的预约数据
|
||||
$booking_data = [
|
||||
'customer_name' => '王五',
|
||||
'phone' => '137-0013-7000',
|
||||
'car_number' => '粤C54321',
|
||||
'car_model' => '本田 CR-V',
|
||||
'start_time' => '2023-10-03 09:30:00',
|
||||
'package_name' => '快速洗车',
|
||||
'duration' => 30,
|
||||
'total_price' => '38.00'
|
||||
];
|
||||
|
||||
// 生成预约信息
|
||||
$message = generateBookingMessage($booking_data, 'simple');
|
||||
|
||||
// 输出结果
|
||||
echo "模板标题:" . getTemplateTitle('simple') . "\n";
|
||||
echo "模板内容:\n" . $message . "\n\n";
|
||||
}
|
||||
|
||||
// 示例4:自定义模板数据
|
||||
function example_custom_data() {
|
||||
echo "=== 示例4:自定义模板数据 ===\n\n";
|
||||
|
||||
// 从数据库获取的示例预约数据
|
||||
$db_booking_data = [
|
||||
'id' => 123,
|
||||
'customer_name' => '赵六',
|
||||
'phone' => '136-0013-6000',
|
||||
'car_model' => '丰田 凯美瑞',
|
||||
'car_number' => '苏D98765',
|
||||
'member_type' => '普通会员',
|
||||
'package_id' => 2,
|
||||
'package_name' => '打蜡套餐',
|
||||
'custom_services' => '',
|
||||
'start_time' => '2023-10-04 16:00:00',
|
||||
'end_time' => '2023-10-04 17:30:00',
|
||||
'duration' => 90,
|
||||
'total_price' => '198.00',
|
||||
'notes' => '',
|
||||
'status' => '已确认',
|
||||
'payment_status' => '未付款',
|
||||
'source' => '朋友介绍',
|
||||
'created_at' => '2023-09-25 14:20:00',
|
||||
'updated_at' => '2023-09-25 14:20:00'
|
||||
];
|
||||
|
||||
// 生成预约信息
|
||||
$message = generateBookingMessage($db_booking_data, 'detailed');
|
||||
|
||||
// 输出结果
|
||||
echo "模板标题:" . getTemplateTitle('detailed') . "\n";
|
||||
echo "模板内容:\n" . $message . "\n\n";
|
||||
}
|
||||
|
||||
// 示例5:将模板集成到现有系统中
|
||||
function example_integration() {
|
||||
echo "=== 示例5:将模板集成到现有系统中 ===\n\n";
|
||||
|
||||
// 模拟从数据库获取预约数据
|
||||
function getBookingFromDatabase($booking_id) {
|
||||
// 这里应该是实际的数据库查询
|
||||
return [
|
||||
'id' => $booking_id,
|
||||
'customer_name' => '孙七',
|
||||
'phone' => '135-0013-5000',
|
||||
'car_model' => '大众 帕萨特',
|
||||
'car_number' => '浙E34567',
|
||||
'member_type' => '普通客户',
|
||||
'package_name' => '普通洗车',
|
||||
'start_time' => '2023-10-05 11:00:00',
|
||||
'end_time' => '2023-10-05 11:30:00',
|
||||
'duration' => 30,
|
||||
'total_price' => '58.00',
|
||||
'notes' => '',
|
||||
'status' => '已确认',
|
||||
'payment_status' => '已付款',
|
||||
'source' => '其他'
|
||||
];
|
||||
}
|
||||
|
||||
// 模拟发送预约确认信息
|
||||
function sendBookingConfirmation($booking_id) {
|
||||
// 获取预约数据
|
||||
$booking = getBookingFromDatabase($booking_id);
|
||||
|
||||
// 生成预约信息
|
||||
$template_name = $booking['member_type'] == 'VIP会员' ? 'detailed' : 'basic';
|
||||
$message = generateBookingMessage($booking, $template_name);
|
||||
$title = getTemplateTitle($template_name);
|
||||
|
||||
// 这里应该是实际的发送逻辑(短信、微信等)
|
||||
echo "向 {$booking['phone']} 发送信息:\n";
|
||||
echo "标题:{$title}\n";
|
||||
echo "内容:\n{$message}\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
$booking_id = 456;
|
||||
sendBookingConfirmation($booking_id);
|
||||
}
|
||||
|
||||
// 运行所有示例
|
||||
example_basic_template();
|
||||
example_detailed_template();
|
||||
example_simple_template();
|
||||
example_custom_data();
|
||||
example_integration();
|
||||
|
||||
// 显示可用模板
|
||||
echo "\n=== 可用模板列表 ===\n";
|
||||
echo "当前系统中可用的预约信息模板:\n";
|
||||
$templates = getAvailableTemplates();
|
||||
foreach ($templates as $template) {
|
||||
echo "- {$template} (标题:" . getTemplateTitle($template) . ")\n";
|
||||
}
|
||||
|
||||
// 如何自定义模板的说明
|
||||
echo "\n=== 如何自定义模板 ===\n";
|
||||
echo "1. 打开 booking_template.php 文件\n";
|
||||
echo "2. 在 \$booking_templates 数组中添加新的模板或修改现有模板\n";
|
||||
echo "3. 每个模板包含 title 和 message 两个字段\n";
|
||||
echo "4. 使用 {字段名} 作为占位符,系统会自动替换为实际数据\n";
|
||||
echo "5. 可使用的占位符包括:\n";
|
||||
echo " - {customer_name}:客户姓名\n";
|
||||
echo " - {phone}:手机号\n";
|
||||
echo " - {car_model}:车型\n";
|
||||
echo " - {car_number}:车牌号\n";
|
||||
echo " - {member_type}:会员类型\n";
|
||||
echo " - {package_name}:服务项目\n";
|
||||
echo " - {custom_services}:自定义服务\n";
|
||||
echo " - {start_time}:预约开始时间\n";
|
||||
echo " - {end_time}:预约结束时间\n";
|
||||
echo " - {date}:预约日期\n";
|
||||
echo " - {time_range}:时间范围\n";
|
||||
echo " - {duration}:服务时长\n";
|
||||
echo " - {total_price}:总价\n";
|
||||
echo " - {notes}:备注\n";
|
||||
echo " - {status}:预约状态\n";
|
||||
echo " - {payment_status}:支付状态\n";
|
||||
echo " - {source}:来源渠道\n";
|
||||
echo " - {created_at}:创建时间\n";
|
||||
?>
|
||||
+71
-74
@@ -1,12 +1,9 @@
|
||||
<?php
|
||||
// 配置数据库连接参数
|
||||
$servername = "localhost";
|
||||
$username = "root";
|
||||
$password = "";
|
||||
$dbname = "car_wash_db";
|
||||
// 加载数据库配置
|
||||
require_once 'db_connect.php';
|
||||
|
||||
// 设置响应头为JSON
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// 获取请求参数
|
||||
$phone = $_GET['phone'] ?? '';
|
||||
@@ -20,82 +17,82 @@ if (empty($phone)) {
|
||||
'pending' => 0,
|
||||
'completed' => 0,
|
||||
'bookings' => []
|
||||
]);
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 创建数据库连接
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
try {
|
||||
// 准备SQL查询 - 查询用户的预约记录(使用新的数据库结构)
|
||||
$sql = "SELECT b.*, p.package_name
|
||||
FROM bookings b
|
||||
LEFT JOIN packages p ON b.package_id = p.id
|
||||
WHERE b.phone = ?
|
||||
ORDER BY b.start_time DESC, b.id DESC";
|
||||
|
||||
// 检查连接
|
||||
if ($conn->connect_error) {
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$phone]);
|
||||
$results = $stmt->fetchAll();
|
||||
|
||||
$bookings = [];
|
||||
$pending = 0;
|
||||
$completed = 0;
|
||||
|
||||
// 处理查询结果
|
||||
foreach ($results as $row) {
|
||||
// 格式化日期和时间
|
||||
$booking_date = $row['start_time'] ? date('Y-m-d', strtotime($row['start_time'])) : '';
|
||||
$time_slot = '';
|
||||
if ($row['start_time'] && $row['end_time']) {
|
||||
$start_time = date('H:i', strtotime($row['start_time']));
|
||||
$end_time = date('H:i', strtotime($row['end_time']));
|
||||
$time_slot = $start_time . '-' . $end_time;
|
||||
}
|
||||
|
||||
$booking = [
|
||||
'id' => $row['id'],
|
||||
'booking_date' => $booking_date,
|
||||
'phone' => $row['phone'],
|
||||
'car_number' => $row['car_number'] ?? '',
|
||||
'car_model' => $row['car_model'] ?? '',
|
||||
'time_slot' => $time_slot,
|
||||
'status' => $row['status'] ?? '未知',
|
||||
'package_name' => $row['package_name'] ?? '',
|
||||
'notes' => $row['notes'] ?? ''
|
||||
];
|
||||
|
||||
// 统计不同状态的预约数量
|
||||
if ($booking['status'] === '待确认' || $booking['status'] === '已确认' || $booking['status'] === '进行中') {
|
||||
$pending++;
|
||||
} elseif ($booking['status'] === '已完成') {
|
||||
$completed++;
|
||||
}
|
||||
|
||||
$bookings[] = $booking;
|
||||
}
|
||||
|
||||
// 计算总预约数
|
||||
$total = count($bookings);
|
||||
|
||||
// 构建响应数据
|
||||
$response = [
|
||||
'success' => true,
|
||||
'total' => $total,
|
||||
'pending' => $pending,
|
||||
'completed' => $completed,
|
||||
'bookings' => $bookings
|
||||
];
|
||||
|
||||
// 返回JSON数据
|
||||
echo json_encode($response, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '数据库连接失败',
|
||||
'message' => '查询失败:' . $e->getMessage(),
|
||||
'total' => 0,
|
||||
'pending' => 0,
|
||||
'completed' => 0,
|
||||
'bookings' => []
|
||||
]);
|
||||
exit;
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
// 准备SQL查询 - 查询用户的预约记录
|
||||
// 假设预约表名为bookings,套餐表名为packages
|
||||
$sql = "SELECT b.*, p.package_name
|
||||
FROM bookings b
|
||||
LEFT JOIN packages p ON b.package_id = p.id
|
||||
WHERE b.phone = ?
|
||||
ORDER BY b.booking_date DESC, b.id DESC";
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param("s", $phone);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
$bookings = [];
|
||||
$pending = 0;
|
||||
$completed = 0;
|
||||
|
||||
// 处理查询结果
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$booking = [
|
||||
'id' => $row['id'],
|
||||
'booking_date' => $row['booking_date'],
|
||||
'phone' => $row['phone'],
|
||||
'car_number' => $row['car_number'] ?? '',
|
||||
'car_model' => $row['car_model'] ?? '',
|
||||
'time_slot' => $row['time_slot'] ?? '',
|
||||
'status' => $row['status'] ?? '未知',
|
||||
'package_name' => $row['package_name'] ?? '',
|
||||
'notes' => $row['notes'] ?? ''
|
||||
];
|
||||
|
||||
// 统计不同状态的预约数量
|
||||
if ($booking['status'] === '待服务') {
|
||||
$pending++;
|
||||
} elseif ($booking['status'] === '已完成') {
|
||||
$completed++;
|
||||
}
|
||||
|
||||
$bookings[] = $booking;
|
||||
}
|
||||
|
||||
// 计算总预约数
|
||||
$total = count($bookings);
|
||||
|
||||
// 构建响应数据
|
||||
$response = [
|
||||
'success' => true,
|
||||
'total' => $total,
|
||||
'pending' => $pending,
|
||||
'completed' => $completed,
|
||||
'bookings' => $bookings
|
||||
];
|
||||
|
||||
// 返回JSON数据
|
||||
echo json_encode($response);
|
||||
|
||||
// 关闭数据库连接
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
// 引入数据库连接文件
|
||||
require 'db_connect.php';
|
||||
|
||||
// 获取请求参数
|
||||
$date = $_GET['date'];
|
||||
|
||||
// 验证日期格式
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
echo json_encode(['error' => 'Invalid date format']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// 查询指定日期的总预约时长,只计算有效预约
|
||||
$sql = "SELECT SUM(duration) as total_duration FROM bookings WHERE DATE(start_time) = ? AND status NOT IN ('已完成', '已取消')";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$date]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
// 获取总时长,如果没有预约则返回0
|
||||
$total_duration = $row['total_duration'] ? $row['total_duration'] : 0;
|
||||
|
||||
// 返回结果
|
||||
echo json_encode(['total_duration' => $total_duration]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['error' => 'Database query failed: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
require_once 'db_connect.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['error' => '无效的套餐ID'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT * FROM packages WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$package = $stmt->fetch();
|
||||
|
||||
if (!$package) {
|
||||
echo json_encode(['error' => '套餐不存在'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode($package, JSON_UNESCAPED_UNICODE);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['error' => '获取套餐信息失败:' . $e->getMessage()], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
session_start();
|
||||
require_once 'db_connect.php';
|
||||
|
||||
@@ -8,7 +9,12 @@ $success_message = '';
|
||||
// 处理表单提交
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
$customer_type = $_POST['customer_type'];
|
||||
// 验证并获取客户类型
|
||||
$customer_type = isset($_POST['customer_type']) ? $_POST['customer_type'] : '';
|
||||
if (!in_array($customer_type, ['vip', 'new'])) {
|
||||
throw new Exception('无效的客户类型');
|
||||
}
|
||||
|
||||
$vip_id = isset($_POST['vip_id']) ? (int)$_POST['vip_id'] : 0;
|
||||
|
||||
// 如果选择VIP客户,从VIP表获取信息
|
||||
@@ -24,25 +30,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// 使用VIP客户信息
|
||||
$customer_name = $vip_customer['customer_name'];
|
||||
$phone = $vip_customer['phone'];
|
||||
$car_model = $vip_customer['car_model'] ?: $car_model; // 允许覆盖
|
||||
$car_number = $vip_customer['car_number'] ?: $car_number; // 允许覆盖
|
||||
// #region agent log
|
||||
$log_data = json_encode(['location' => 'index.php:28', 'message' => 'VIP customer data', 'data' => ['vip_id' => $vip_id, 'has_car_model' => isset($vip_customer['car_model']), 'has_car_number' => isset($vip_customer['car_number'])], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'A']);
|
||||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||||
// #endregion
|
||||
// VIP客户信息优先,但允许通过POST覆盖(如果用户想修改)
|
||||
$car_model = isset($_POST['car_model']) && trim($_POST['car_model']) ? trim($_POST['car_model']) : (isset($vip_customer['car_model']) && $vip_customer['car_model'] ? $vip_customer['car_model'] : '');
|
||||
$car_number = isset($_POST['car_number']) && trim($_POST['car_number']) ? trim($_POST['car_number']) : (isset($vip_customer['car_number']) && $vip_customer['car_number'] ? $vip_customer['car_number'] : '');
|
||||
$member_type = 'VIP会员';
|
||||
} else {
|
||||
// 新客户录入
|
||||
$customer_name = trim($_POST['customer_name']);
|
||||
$phone = trim($_POST['phone']);
|
||||
$customer_name = trim($_POST['customer_name'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
$car_model = trim($_POST['car_model'] ?? '');
|
||||
$car_number = trim($_POST['car_number'] ?? '');
|
||||
}
|
||||
|
||||
$car_model = trim($_POST['car_model']);
|
||||
$car_number = trim($_POST['car_number']);
|
||||
$package_id = (int)$_POST['package_id'];
|
||||
$package_id = (int)($_POST['package_id'] ?? 0);
|
||||
$custom_services = trim($_POST['custom_services'] ?? '');
|
||||
$appointment_date = $_POST['appointment_date'];
|
||||
$appointment_time = $_POST['appointment_time'];
|
||||
$duration = (int)$_POST['duration'];
|
||||
$appointment_date = $_POST['appointment_date'] ?? '';
|
||||
$appointment_time = $_POST['appointment_time'] ?? '';
|
||||
$duration = (int)($_POST['duration'] ?? 60);
|
||||
$notes = trim($_POST['notes'] ?? '');
|
||||
$member_type = $_POST['member_type'];
|
||||
$source = $_POST['source'];
|
||||
|
||||
// 验证member_type和source
|
||||
$allowed_member_types = ['普通客户', 'VIP会员'];
|
||||
$member_type = isset($_POST['member_type']) && in_array($_POST['member_type'], $allowed_member_types) ? $_POST['member_type'] : '普通客户';
|
||||
|
||||
$allowed_sources = ['抖音', '微信', '快手', '朋友介绍', '其他'];
|
||||
$source = isset($_POST['source']) && in_array($_POST['source'], $allowed_sources) ? $_POST['source'] : '其他';
|
||||
|
||||
// 验证必填字段
|
||||
if (empty($customer_name) || empty($phone) || empty($car_model) ||
|
||||
@@ -76,32 +91,79 @@ 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 现有预约的结束时间 > 新预约的开始时间
|
||||
// #region agent log
|
||||
$log_data = json_encode(['location' => 'index.php:98', 'message' => 'Checking time conflict', 'data' => ['start_time' => $start_time, 'end_time' => $end_time, 'duration' => $duration], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'F']);
|
||||
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' => 'index.php:107', 'message' => 'Time conflict check result', 'data' => ['conflict_count' => $conflict_count], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'F']);
|
||||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||||
// #endregion
|
||||
|
||||
if ($stmt->fetchColumn() > 0) {
|
||||
if ($conflict_count > 0) {
|
||||
throw new Exception('该时间段已被预约,请选择其他时间');
|
||||
}
|
||||
|
||||
// 检查客户是否有历史预约记录,如果有则自动转为普通客户
|
||||
// #region agent log
|
||||
$log_data = json_encode(['location' => 'index.php:136', 'message' => 'Checking customer booking history', 'data' => ['phone' => $phone, 'car_number' => $car_number, 'current_member_type' => $member_type], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'I']);
|
||||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||||
// #endregion
|
||||
// 检查该手机号和车牌号组合是否有历史预约记录
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM bookings
|
||||
WHERE phone = ? AND car_number = ?");
|
||||
$stmt->execute([$phone, $car_number]);
|
||||
$has_booking_history = $stmt->fetchColumn() > 0;
|
||||
|
||||
// 如果客户有历史预约记录,自动转为普通客户(除非是VIP客户模式)
|
||||
if ($has_booking_history && $member_type === 'VIP会员' && $customer_type !== 'vip') {
|
||||
$member_type = '普通客户';
|
||||
// #region agent log
|
||||
$log_data = json_encode(['location' => 'index.php:144', 'message' => 'Auto converted to regular customer', 'data' => ['reason' => 'has_booking_history', 'phone' => $phone, 'car_number' => $car_number], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'I']);
|
||||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||||
// #endregion
|
||||
}
|
||||
|
||||
// 插入预约记录
|
||||
// 对于0元订单,自动标记为已付款
|
||||
$payment_status = ($total_price <= 0) ? '已付款' : '未付款';
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO bookings
|
||||
(customer_name, phone, car_model, car_number, package_id, custom_services,
|
||||
start_time, end_time, duration, total_price, notes, member_type, source)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
start_time, end_time, duration, total_price, notes, member_type, source, payment_status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
|
||||
$stmt->execute([$customer_name, $phone, $car_model, $car_number, $package_id, $custom_services,
|
||||
$start_time, $end_time, $duration, $total_price, $notes, $member_type, $source]);
|
||||
$start_time, $end_time, $duration, $total_price, $notes, $member_type, $source, $payment_status]);
|
||||
|
||||
$success_message = "预约提交成功!";
|
||||
|
||||
@@ -160,17 +222,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;
|
||||
@@ -198,8 +269,9 @@ $packages_json = json_encode(array_map(function($package) {
|
||||
<meta name="description" content="洗车预约系统 - 在线预约洗车服务">
|
||||
<meta name="keywords" content="洗车,预约,在线预约,汽车美容">
|
||||
<link rel="apple-touch-icon" href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkyIiBoZWlnaHQ9IjE5MiIgdmlld0JveD0iMCAwIDE5MiAxOTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxOTIiIGhlaWdodD0iMTkyIiByeD0iMjQiIGZpbGw9IiMzMDk1RjQiLz4KPHN2ZyB4PSI0OCIgeT0iNDgiIHdpZHRoPSI5NiIgaGVpZ2h0PSI5NiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJ3aGl0ZSI+CjxwYXRoIGQ9Ik0yMS4yIDQuNEMyMS42IDMuNiAyMi4xIDMuMSAyMi41IDIuN0MyMy40IDIuMSAyNC41IDIuMSAyNS4yIDIuN0MyNS44IDMuMSAyNi4zIDMuNiAyNi43IDQuNEMyNy4xIDUuMSAyNy4xIDYuMiAyNi43IDcuMUMyNi4zIDcuOCAyNS44IDguMyAyNS4yIDguN0MyNC43IDkuMSAyMy42IDkuMSAyMi45IDguN0MyMi4zIDguMyAyMS44IDcuOCAyMS40IDcuMUMyMS4wIDYuMiAyMS4wIDUuMSAyMS4yIDQuNFoiLz4KPHN2ZyB4PSIyMCIgeT0iMTIiIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJ3aGl0ZSI+CjxwYXRoIGQ9Ik0yMSAyMy41QzIwLjUgMjMuNSAyMCAyMyAyMCAyMi41VjEyQzIwIDExLjUgMjAuNSAxMSAyMSAxMUg5QzguNSAxMSAxMi41IDEwLjUgMTIgMTBIMjBWMTBCMjAgMTAuNSAyMC41IDExIDIxIDExVjIzLjVaIi8+CjxwYXRoIGQ9Ik0xOCAyMFYxN0gxNFY4SDVWMTNIMTlWMTVIMTlWMjBaIi8+CjxwYXRoIGQ9Ik04IDEwSDVWN0g4VjEwWiIvPgo8L3N2Zz4KPC9zdmc+">
|
||||
<title>洗车预约系统</title>
|
||||
<title>张老师撸车(私家车库)工作室</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="mobile-nav.js" defer></script>
|
||||
|
||||
<style>
|
||||
/* VIP搜索结果样式 */
|
||||
@@ -310,15 +382,32 @@ $packages_json = json_encode(array_map(function($package) {
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>🚗 洗车预约系统</h1>
|
||||
<button class="mobile-menu-toggle" onclick="toggleMobileMenu()" aria-label="菜单">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
<h1>🚗 张老师撸车工作室</h1>
|
||||
<nav class="nav">
|
||||
<a href="index.php" class="nav-link active">预约洗车</a>
|
||||
<a href="bookings.php" class="nav-link">预约管理</a>
|
||||
<a href="pending_bookings.php" class="nav-link">待处理预约</a>
|
||||
<a href="packages.php" class="nav-link">套餐管理</a>
|
||||
<a href="vip.php" class="nav-link">VIP管理</a>
|
||||
<a href="announcement.php" class="nav-link">今日待办</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- 移动端导航菜单 -->
|
||||
<div class="mobile-nav-overlay" onclick="closeMobileMenu()"></div>
|
||||
<nav class="mobile-nav">
|
||||
<a href="index.php" class="nav-link active">预约洗车</a>
|
||||
<a href="bookings.php" class="nav-link">预约管理</a>
|
||||
<a href="pending_bookings.php" class="nav-link">待处理预约</a>
|
||||
<a href="packages.php" class="nav-link">套餐管理</a>
|
||||
<a href="vip.php" class="nav-link">VIP管理</a>
|
||||
<a href="announcement.php" class="nav-link">今日待办</a>
|
||||
</nav>
|
||||
|
||||
<?php if ($message): ?>
|
||||
<div class="message error-message" style="background-color: #fee; color: #c33; border-color: #fcc;">
|
||||
@@ -398,7 +487,7 @@ $packages_json = json_encode(array_map(function($package) {
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="calendar" id="calendarContainer">
|
||||
<div class="calendar enhanced-card" id="calendarContainer">
|
||||
<!-- 日期将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
|
||||
@@ -435,13 +524,20 @@ $packages_json = json_encode(array_map(function($package) {
|
||||
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
|
||||
const date = new Date(pageStartMonday);
|
||||
date.setDate(pageStartMonday.getDate() + dayOffset);
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
// 确保使用本地时区获取日期字符串
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const dateStr = `${year}-${month}-${day}`;
|
||||
const dateDisplay = `${date.getMonth() + 1}/${date.getDate()}`;
|
||||
const weekday = ['日', '一', '二', '三', '四', '五', '六'][date.getDay()];
|
||||
|
||||
// 获取今天的日期字符串用于比较
|
||||
const today = new Date();
|
||||
const todayStr = today.toISOString().split('T')[0];
|
||||
const todayYear = today.getFullYear();
|
||||
const todayMonth = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const todayDay = String(today.getDate()).padStart(2, '0');
|
||||
const todayStr = `${todayYear}-${todayMonth}-${todayDay}`;
|
||||
const isToday = dateStr === todayStr;
|
||||
|
||||
// 获取预约数量
|
||||
@@ -506,7 +602,72 @@ $packages_json = json_encode(array_map(function($package) {
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="time-slots" id="timeSlots" style="display: none;">
|
||||
<style>
|
||||
/* 统一时间选择样式为pending_bookings.php的样式 */
|
||||
.calendar-day {
|
||||
padding: 15px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.calendar-day:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.calendar-day.selected {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.time-slots-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.time-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.time-slot {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.time-slot.available {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.time-slot.booked {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.time-slot.past {
|
||||
background: #e9ecef;
|
||||
color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.time-slot.selected {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.time-slot:hover.available {
|
||||
background: #c3e6cb;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="time-slots enhanced-card" id="timeSlots" style="display: none;">
|
||||
<h3>🕐 选择时间段</h3>
|
||||
<div class="quick-duration">
|
||||
<span>快捷时长:</span>
|
||||
@@ -518,8 +679,8 @@ $packages_json = json_encode(array_map(function($package) {
|
||||
<button type="button" class="duration-btn" onclick="selectDuration(480)">8小时</button>
|
||||
<button type="button" class="duration-btn" onclick="selectDuration(600)">10小时</button>
|
||||
<button type="button" class="duration-btn" onclick="selectDuration(720)">12小时</button>
|
||||
<input type="number" id="customDuration" min="30" step="30" value="60" style="width: 80px; margin-left: 10px;">
|
||||
<button type="button" class="btn btn-sm" onclick="applyCustomDuration()">确定</button>
|
||||
<!-- <input type="number" id="customDuration" min="30" step="30" value="60" style="width: 80px; margin-left: 10px;"> -->
|
||||
<!-- <button type="button" class="btn btn-sm" onclick="applyCustomDuration()">确定</button> -->
|
||||
</div>
|
||||
<div class="time-grid" id="timeGrid">
|
||||
<!-- 时间段将通过JavaScript动态生成 -->
|
||||
@@ -527,13 +688,13 @@ $packages_json = json_encode(array_map(function($package) {
|
||||
</div>
|
||||
|
||||
<!-- 预约详情显示区域 -->
|
||||
<div id="bookingDetails" class="booking-details" style="display: none;">
|
||||
<div id="bookingDetails" class="booking-details enhanced-card" style="display: none;">
|
||||
<h3 id="detailsDateTitle"></h3>
|
||||
<div id="bookingDetailsContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="booking-form-section">
|
||||
<div class="booking-form-section enhanced-card">
|
||||
<h2>📋 预约信息</h2>
|
||||
<form method="POST" class="form" id="bookingForm">
|
||||
<div class="form-row">
|
||||
@@ -1116,20 +1277,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>
|
||||
`;
|
||||
});
|
||||
@@ -1805,6 +1981,7 @@ $packages_json = json_encode(array_map(function($package) {
|
||||
const slotDiv = document.createElement('div');
|
||||
slotDiv.className = `time-slot ${isPast ? 'past' : ''} ${isBooked ? 'booked' : 'available'}`;
|
||||
slotDiv.textContent = timeString;
|
||||
slotDiv.dataset.time = timeString; // 添加数据属性,与pending_bookings.php保持一致
|
||||
slotDiv.onclick = () => selectTimeSlot(timeString);
|
||||
|
||||
timeGrid.appendChild(slotDiv);
|
||||
@@ -1885,7 +2062,8 @@ $packages_json = json_encode(array_map(function($package) {
|
||||
slot.classList.remove('selected');
|
||||
});
|
||||
|
||||
const slotElement = document.querySelector(`[onclick="selectTimeSlot('${time}')"]`);
|
||||
// 使用data-time属性查找元素,与pending_bookings.php保持一致
|
||||
const slotElement = document.querySelector(`[data-time="${time}"]`);
|
||||
if (slotElement) {
|
||||
slotElement.classList.add('selected');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// 移动端导航菜单控制
|
||||
function toggleMobileMenu() {
|
||||
const toggle = document.querySelector('.mobile-menu-toggle');
|
||||
const overlay = document.querySelector('.mobile-nav-overlay');
|
||||
const nav = document.querySelector('.mobile-nav');
|
||||
|
||||
if (!toggle || !overlay || !nav) return;
|
||||
|
||||
toggle.classList.toggle('active');
|
||||
overlay.classList.toggle('active');
|
||||
nav.classList.toggle('active');
|
||||
document.body.style.overflow = nav.classList.contains('active') ? 'hidden' : '';
|
||||
}
|
||||
|
||||
function closeMobileMenu() {
|
||||
const toggle = document.querySelector('.mobile-menu-toggle');
|
||||
const overlay = document.querySelector('.mobile-nav-overlay');
|
||||
const nav = document.querySelector('.mobile-nav');
|
||||
|
||||
if (!toggle || !overlay || !nav) return;
|
||||
|
||||
toggle.classList.remove('active');
|
||||
overlay.classList.remove('active');
|
||||
nav.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 点击导航链接后关闭菜单
|
||||
const mobileNavLinks = document.querySelectorAll('.mobile-nav .nav-link');
|
||||
mobileNavLinks.forEach(link => {
|
||||
link.addEventListener('click', closeMobileMenu);
|
||||
});
|
||||
|
||||
// 点击遮罩层关闭菜单
|
||||
const overlay = document.querySelector('.mobile-nav-overlay');
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', closeMobileMenu);
|
||||
}
|
||||
|
||||
// ESC键关闭菜单
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeMobileMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// 防止双击缩放(iOS Safari)
|
||||
let lastTouchEnd = 0;
|
||||
document.addEventListener('touchend', function(event) {
|
||||
const now = Date.now();
|
||||
if (now - lastTouchEnd <= 300) {
|
||||
event.preventDefault();
|
||||
}
|
||||
lastTouchEnd = now;
|
||||
}, false);
|
||||
|
||||
// 优化触摸反馈
|
||||
document.addEventListener('touchstart', function(e) {
|
||||
const target = e.target;
|
||||
if (target.classList.contains('btn') ||
|
||||
target.classList.contains('time-slot') ||
|
||||
target.classList.contains('calendar-day') ||
|
||||
target.classList.contains('nav-link')) {
|
||||
target.style.transition = 'transform 0.1s';
|
||||
target.style.transform = 'scale(0.97)';
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchend', function(e) {
|
||||
const target = e.target;
|
||||
if (target.classList.contains('btn') ||
|
||||
target.classList.contains('time-slot') ||
|
||||
target.classList.contains('calendar-day') ||
|
||||
target.classList.contains('nav-link')) {
|
||||
setTimeout(() => {
|
||||
target.style.transform = '';
|
||||
}, 100);
|
||||
}
|
||||
}, { passive: true });
|
||||
});
|
||||
|
||||
+1222
-198
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+205
-111
@@ -1,111 +1,207 @@
|
||||
<?php
|
||||
// 配置数据库连接参数
|
||||
$servername = "localhost";
|
||||
$username = "root";
|
||||
$password = "";
|
||||
$dbname = "car_wash_db";
|
||||
// 加载数据库配置
|
||||
require_once 'db_connect.php';
|
||||
// #region agent log
|
||||
$log_data = json_encode(['location' => 'process_booking.php:3', 'message' => 'Database connection check', 'data' => ['has_pdo' => isset($pdo), 'using_db_connect' => true], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'D']);
|
||||
// 确保日志目录存在
|
||||
$log_dir = '.cursor';
|
||||
if (!file_exists($log_dir)) {
|
||||
mkdir($log_dir, 0777, true);
|
||||
}
|
||||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||||
// #endregion
|
||||
|
||||
// 检查表单提交
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// 获取表单数据
|
||||
$customer_name = $_POST['customer_name'] ?? '';
|
||||
$phone = $_POST['phone'] ?? '';
|
||||
$car_model = $_POST['car_model'] ?? '';
|
||||
$car_number = $_POST['car_number'] ?? '';
|
||||
$booking_date = $_POST['booking_date'] ?? '';
|
||||
$time_slot = $_POST['time_slot'] ?? '';
|
||||
$package_id = $_POST['package_id'] ?? '';
|
||||
$notes = $_POST['notes'] ?? '';
|
||||
$source = $_POST['source'] ?? ''; // 来源标识,用于确定返回页面
|
||||
|
||||
// 验证必填字段
|
||||
$errors = [];
|
||||
|
||||
if (empty($customer_name)) {
|
||||
$errors[] = '请输入客户姓名';
|
||||
}
|
||||
|
||||
if (empty($phone)) {
|
||||
$errors[] = '请输入手机号码';
|
||||
} elseif (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
|
||||
$errors[] = '请输入正确的手机号码';
|
||||
}
|
||||
|
||||
if (empty($car_number)) {
|
||||
$errors[] = '请输入车牌号';
|
||||
}
|
||||
|
||||
if (empty($booking_date)) {
|
||||
$errors[] = '请选择预约日期';
|
||||
}
|
||||
|
||||
if (empty($time_slot)) {
|
||||
$errors[] = '请选择预约时间';
|
||||
}
|
||||
|
||||
if (empty($package_id)) {
|
||||
$errors[] = '请选择洗车套餐';
|
||||
}
|
||||
|
||||
// 验证日期是否为过去
|
||||
$current_date = date('Y-m-d');
|
||||
if ($booking_date < $current_date) {
|
||||
$errors[] = '不能选择过去的日期';
|
||||
}
|
||||
|
||||
// 如果有错误,返回错误信息
|
||||
if (!empty($errors)) {
|
||||
$error_message = implode('\n', $errors);
|
||||
echo "<script>
|
||||
alert('$error_message');
|
||||
window.history.back();
|
||||
</script>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// 创建数据库连接
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
|
||||
// 检查连接
|
||||
if ($conn->connect_error) {
|
||||
echo "<script>
|
||||
alert('数据库连接失败,请稍后重试');
|
||||
window.history.back();
|
||||
</script>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// 检查是否已经存在相同的预约(同一天、同一时间段、同一车牌号)
|
||||
$check_sql = "SELECT * FROM bookings
|
||||
WHERE booking_date = ? AND time_slot = ? AND car_number = ? AND status != '已取消'";
|
||||
$check_stmt = $conn->prepare($check_sql);
|
||||
$check_stmt->bind_param("sss", $booking_date, $time_slot, $car_number);
|
||||
$check_stmt->execute();
|
||||
$check_result = $check_stmt->get_result();
|
||||
|
||||
if ($check_result->num_rows > 0) {
|
||||
echo "<script>
|
||||
alert('该时间段已存在相同车牌号的预约');
|
||||
window.history.back();
|
||||
</script>";
|
||||
$check_stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
$check_stmt->close();
|
||||
|
||||
// 插入预约记录
|
||||
$status = '待服务'; // 默认为待服务状态
|
||||
$create_time = date('Y-m-d H:i:s');
|
||||
|
||||
$insert_sql = "INSERT INTO bookings
|
||||
(customer_name, phone, car_model, car_number, booking_date, time_slot, package_id, notes, status, create_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
$stmt = $conn->prepare($insert_sql);
|
||||
$stmt->bind_param("ssssssssss", $customer_name, $phone, $car_model, $car_number, $booking_date, $time_slot, $package_id, $notes, $status, $create_time);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
try {
|
||||
// 获取表单数据
|
||||
$customer_name = $_POST['customer_name'] ?? '';
|
||||
$phone = $_POST['phone'] ?? '';
|
||||
$car_model = $_POST['car_model'] ?? '';
|
||||
$car_number = $_POST['car_number'] ?? '';
|
||||
$booking_date = $_POST['booking_date'] ?? '';
|
||||
$time_slot = $_POST['time_slot'] ?? '';
|
||||
$package_id = $_POST['package_id'] ?? '';
|
||||
$notes = $_POST['notes'] ?? '';
|
||||
$source = $_POST['source'] ?? '其他'; // 来源标识,用于确定返回页面
|
||||
$duration = isset($_POST['duration']) ? (int)$_POST['duration'] : 60; // 默认60分钟
|
||||
$total_price = isset($_POST['total_price']) ? (float)$_POST['total_price'] : 0;
|
||||
|
||||
// 验证必填字段
|
||||
if (empty($customer_name)) {
|
||||
throw new Exception('请输入客户姓名');
|
||||
}
|
||||
|
||||
if (empty($phone)) {
|
||||
throw new Exception('请输入手机号码');
|
||||
} elseif (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
|
||||
throw new Exception('请输入正确的手机号码');
|
||||
}
|
||||
|
||||
if (empty($car_number)) {
|
||||
throw new Exception('请输入车牌号');
|
||||
}
|
||||
|
||||
if (empty($booking_date)) {
|
||||
throw new Exception('请选择预约日期');
|
||||
}
|
||||
|
||||
if (empty($time_slot)) {
|
||||
throw new Exception('请选择预约时间');
|
||||
}
|
||||
|
||||
if (empty($package_id)) {
|
||||
throw new Exception('请选择洗车套餐');
|
||||
}
|
||||
|
||||
// 验证日期是否为过去
|
||||
$current_date = date('Y-m-d');
|
||||
if ($booking_date < $current_date) {
|
||||
throw new Exception('不能选择过去的日期');
|
||||
}
|
||||
|
||||
// 解析时间段,转换为start_time和end_time
|
||||
// time_slot格式可能是 "09:00-10:00" 或 "09:00"
|
||||
$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_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,使用默认时长
|
||||
$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
|
||||
$log_data = json_encode(['location' => 'process_booking.php:70', 'message' => 'Time conversion', 'data' => ['booking_date' => $booking_date, 'time_slot' => $time_slot, 'start_time' => $start_time, 'end_time' => $end_time, 'duration' => $duration], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'E']);
|
||||
// 确保日志目录存在
|
||||
$log_dir = '.cursor';
|
||||
if (!file_exists($log_dir)) {
|
||||
mkdir($log_dir, 0777, true);
|
||||
}
|
||||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||||
// #endregion
|
||||
|
||||
// 获取套餐信息以获取价格和默认时长
|
||||
$stmt = $pdo->prepare("SELECT * FROM packages WHERE id = ? AND is_active = 1");
|
||||
$stmt->execute([$package_id]);
|
||||
$package = $stmt->fetch();
|
||||
|
||||
if (!$package) {
|
||||
throw new Exception('选择的套餐无效');
|
||||
}
|
||||
|
||||
// 如果未提供价格,使用套餐价格
|
||||
if ($total_price <= 0) {
|
||||
$total_price = $package['price'];
|
||||
}
|
||||
|
||||
// 如果未提供时长,使用套餐默认时长
|
||||
if ($duration <= 0) {
|
||||
$duration = $package['base_duration'];
|
||||
// 重新计算结束时间
|
||||
$end_time = date('Y-m-d H:i:s', strtotime($start_time) + $duration * 60);
|
||||
}
|
||||
|
||||
// 检查时间冲突
|
||||
// 两个时间段重叠的条件:现有预约的开始时间 < 新预约的结束时间 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']);
|
||||
// 确保日志目录存在
|
||||
$log_dir = '.cursor';
|
||||
if (!file_exists($log_dir)) {
|
||||
mkdir($log_dir, 0777, true);
|
||||
}
|
||||
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 > ?");
|
||||
$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']);
|
||||
// 确保日志目录存在
|
||||
$log_dir = '.cursor';
|
||||
if (!file_exists($log_dir)) {
|
||||
mkdir($log_dir, 0777, true);
|
||||
}
|
||||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||||
// #endregion
|
||||
|
||||
if ($conflict_count > 0) {
|
||||
throw new Exception('该时间段已被预约,请选择其他时间');
|
||||
}
|
||||
|
||||
// 对于0元订单,自动标记为已付款
|
||||
$payment_status = ($total_price <= 0) ? '已付款' : '未付款';
|
||||
|
||||
// 插入预约记录(使用新的数据库结构)
|
||||
$stmt = $pdo->prepare("INSERT INTO bookings
|
||||
(customer_name, phone, car_model, car_number, package_id, custom_services,
|
||||
start_time, end_time, duration, total_price, notes, member_type, source, payment_status, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
|
||||
$member_type = '普通客户'; // 默认普通客户,VIP客户应该通过index.php添加
|
||||
|
||||
$stmt->execute([
|
||||
$customer_name,
|
||||
$phone,
|
||||
$car_model,
|
||||
$car_number,
|
||||
$package_id,
|
||||
'', // custom_services
|
||||
$start_time,
|
||||
$end_time,
|
||||
$duration,
|
||||
$total_price,
|
||||
$notes,
|
||||
$member_type,
|
||||
$source,
|
||||
$payment_status,
|
||||
'待确认' // status
|
||||
]);
|
||||
|
||||
// 预约成功
|
||||
$success_message = '预约添加成功!';
|
||||
|
||||
@@ -116,19 +212,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
alert('$success_message');
|
||||
window.location.href = '$redirect_url';
|
||||
</script>";
|
||||
} else {
|
||||
// 预约失败
|
||||
|
||||
} catch (Exception $e) {
|
||||
$error_message = $e->getMessage();
|
||||
echo "<script>
|
||||
alert('预约添加失败,请稍后重试');
|
||||
alert('$error_message');
|
||||
window.history.back();
|
||||
</script>";
|
||||
}
|
||||
|
||||
// 关闭数据库连接
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
} else {
|
||||
// 不是POST请求,重定向到首页
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
+20
-1
@@ -62,7 +62,26 @@ try {
|
||||
}
|
||||
|
||||
// 计算新的持续时间(分钟)
|
||||
$duration = $start_dt->diff($end_dt)->i + ($start_dt->diff($end_dt)->h * 60);
|
||||
// #region agent log
|
||||
$log_data = json_encode(['location' => 'update_booking.php:65', 'message' => 'Calculating duration', 'data' => ['start_time' => $new_start_time, 'end_time' => $new_end_time], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'C']);
|
||||
// 确保日志目录存在
|
||||
$log_dir = '.cursor';
|
||||
if (!file_exists($log_dir)) {
|
||||
mkdir($log_dir, 0777, true);
|
||||
}
|
||||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||||
// #endregion
|
||||
$interval = $start_dt->diff($end_dt);
|
||||
$duration = ($interval->days * 24 * 60) + ($interval->h * 60) + $interval->i;
|
||||
// #region agent log
|
||||
$log_data = json_encode(['location' => 'update_booking.php:67', 'message' => 'Duration calculated', 'data' => ['duration' => $duration, 'days' => $interval->days, 'hours' => $interval->h, 'minutes' => $interval->i], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'C']);
|
||||
// 确保日志目录存在
|
||||
$log_dir = '.cursor';
|
||||
if (!file_exists($log_dir)) {
|
||||
mkdir($log_dir, 0777, true);
|
||||
}
|
||||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||||
// #endregion
|
||||
|
||||
// 更新预约时间和持续时间
|
||||
$stmt = $pdo->prepare("UPDATE bookings SET start_time = ?, end_time = ?, duration = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
|
||||
|
||||
@@ -94,6 +94,56 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$stmt = $pdo->prepare("DELETE FROM vip_customers WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$success_message = "VIP客户删除成功!";
|
||||
|
||||
} elseif ($action === 'convert_from_booking') {
|
||||
// 从预约记录转换为VIP客户
|
||||
$booking_id = isset($_POST['booking_id']) ? (int)$_POST['booking_id'] : 0;
|
||||
|
||||
if ($booking_id <= 0) {
|
||||
throw new Exception('无效的预约ID');
|
||||
}
|
||||
|
||||
// 获取预约信息
|
||||
$stmt = $pdo->prepare("SELECT customer_name, phone, car_model, car_number FROM bookings WHERE id = ?");
|
||||
$stmt->execute([$booking_id]);
|
||||
$booking = $stmt->fetch();
|
||||
|
||||
if (!$booking) {
|
||||
throw new Exception('预约记录不存在');
|
||||
}
|
||||
|
||||
// 检查是否已经是VIP客户
|
||||
$stmt = $pdo->prepare("SELECT id FROM vip_customers WHERE phone = ? AND car_number = ?");
|
||||
$stmt->execute([$booking['phone'], $booking['car_number']]);
|
||||
if ($stmt->fetch()) {
|
||||
throw new Exception('该客户已经是VIP客户');
|
||||
}
|
||||
|
||||
// 插入VIP客户
|
||||
$stmt = $pdo->prepare("INSERT INTO vip_customers
|
||||
(customer_name, phone, car_model, car_number, notes)
|
||||
VALUES (?, ?, ?, ?, ?)");
|
||||
try {
|
||||
$notes = '从预约记录 #' . $booking_id . ' 自动转换';
|
||||
$stmt->execute([
|
||||
$booking['customer_name'],
|
||||
$booking['phone'],
|
||||
$booking['car_model'] ?? '',
|
||||
$booking['car_number'] ?? '',
|
||||
$notes
|
||||
]);
|
||||
|
||||
// 更新该客户的所有预约记录的member_type为VIP会员
|
||||
$stmt = $pdo->prepare("UPDATE bookings SET member_type = 'VIP会员' WHERE phone = ? AND car_number = ?");
|
||||
$stmt->execute([$booking['phone'], $booking['car_number']]);
|
||||
|
||||
$success_message = "客户已成功转换为VIP客户!";
|
||||
} catch (PDOException $e) {
|
||||
if ($e->errorInfo[1] == 1062) {
|
||||
throw new Exception('该客户已经是VIP客户');
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
@@ -141,21 +191,39 @@ try {
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="description" content="VIP客户管理,录入和管理VIP客户信息">
|
||||
<meta name="keywords" content="VIP管理,客户管理,会员管理">
|
||||
<title>VIP管理 - 洗车预约系统</title>
|
||||
<title>张老师撸车(私家车库)工作室</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="mobile-nav.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>🚗 洗车预约系统 - VIP管理</h1>
|
||||
<button class="mobile-menu-toggle" onclick="toggleMobileMenu()" aria-label="菜单">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
<h1>🚗 张老师撸车工作室 - VIP管理</h1>
|
||||
<nav class="nav">
|
||||
<a href="index.php" class="nav-link">预约洗车</a>
|
||||
<a href="bookings.php" class="nav-link">预约管理</a>
|
||||
<a href="pending_bookings.php" class="nav-link">待处理预约</a>
|
||||
<a href="packages.php" class="nav-link">套餐管理</a>
|
||||
<a href="vip.php" class="nav-link active">VIP管理</a>
|
||||
<a href="announcement.php" class="nav-link">今日待办</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- 移动端导航菜单 -->
|
||||
<div class="mobile-nav-overlay" onclick="closeMobileMenu()"></div>
|
||||
<nav class="mobile-nav">
|
||||
<a href="index.php" class="nav-link">预约洗车</a>
|
||||
<a href="bookings.php" class="nav-link">预约管理</a>
|
||||
<a href="pending_bookings.php" class="nav-link">待处理预约</a>
|
||||
<a href="packages.php" class="nav-link">套餐管理</a>
|
||||
<a href="vip.php" class="nav-link active">VIP管理</a>
|
||||
<a href="announcement.php" class="nav-link">今日待办</a>
|
||||
</nav>
|
||||
|
||||
<?php if (isset($success_message)): ?>
|
||||
<div class="success-message"><?php echo $success_message; ?></div>
|
||||
@@ -173,7 +241,7 @@ try {
|
||||
|
||||
<div class="vip-management">
|
||||
<!-- VIP搜索表单 -->
|
||||
<div class="card search-container">
|
||||
<div class="card search-container enhanced-card">
|
||||
<h2>🔍 VIP客户搜索</h2>
|
||||
<form method="GET" class="form search-form">
|
||||
<div class="form-row">
|
||||
@@ -194,7 +262,7 @@ try {
|
||||
</div>
|
||||
|
||||
<!-- VIP录入表单 -->
|
||||
<div class="card">
|
||||
<div class="card enhanced-card">
|
||||
<h2>➕ 录入新VIP客户</h2>
|
||||
<form method="POST" class="form">
|
||||
<input type="hidden" name="action" value="add_vip">
|
||||
@@ -247,8 +315,66 @@ try {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 从预约转换为VIP -->
|
||||
<div class="card enhanced-card">
|
||||
<h2>📋 从预约记录转换为VIP客户</h2>
|
||||
<p style="color: #666; margin-bottom: 15px;">选择预约记录,快速将普通客户转换为VIP客户</p>
|
||||
<?php
|
||||
// 获取可以转换为VIP的普通客户预约记录(最近30天,且不是VIP客户)
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT DISTINCT b.id, b.customer_name, b.phone, b.car_model, b.car_number,
|
||||
COUNT(*) as booking_count,
|
||||
MAX(b.start_time) as last_booking_time
|
||||
FROM bookings b
|
||||
LEFT JOIN vip_customers v ON b.phone = v.phone AND b.car_number = v.car_number AND v.is_active = 1
|
||||
WHERE b.member_type = '普通客户'
|
||||
AND v.id IS NULL
|
||||
AND b.start_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||
GROUP BY b.phone, b.car_number, b.customer_name, b.car_model
|
||||
ORDER BY last_booking_time DESC
|
||||
LIMIT 20");
|
||||
$stmt->execute();
|
||||
$convertible_bookings = $stmt->fetchAll();
|
||||
} catch (Exception $e) {
|
||||
$convertible_bookings = [];
|
||||
}
|
||||
?>
|
||||
|
||||
<?php if (empty($convertible_bookings)): ?>
|
||||
<div class="empty-message">暂无可转换的普通客户</div>
|
||||
<?php else: ?>
|
||||
<div style="max-height: 400px; overflow-y: auto;">
|
||||
<?php foreach ($convertible_bookings as $booking): ?>
|
||||
<div style="padding: 12px; border: 1px solid #e0e0e0; border-radius: 6px; margin-bottom: 10px; background: #f9f9f9;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="flex: 1;">
|
||||
<strong><?php echo htmlspecialchars($booking['customer_name']); ?></strong><br>
|
||||
<span style="color: #666; font-size: 0.9em;">
|
||||
手机号:<?php echo htmlspecialchars($booking['phone']); ?> |
|
||||
车牌号:<?php echo htmlspecialchars($booking['car_number']); ?><br>
|
||||
<?php if ($booking['car_model']): ?>
|
||||
车型:<?php echo htmlspecialchars($booking['car_model']); ?> |
|
||||
<?php endif; ?>
|
||||
预约次数:<?php echo $booking['booking_count']; ?>次
|
||||
</span>
|
||||
</div>
|
||||
<form method="POST" style="margin-left: 15px;">
|
||||
<input type="hidden" name="action" value="convert_from_booking">
|
||||
<input type="hidden" name="booking_id" value="<?php echo $booking['id']; ?>">
|
||||
<button type="submit" class="btn btn-sm" style="background-color: #ffd700; color: #333; font-weight: bold;"
|
||||
onclick="return confirm('确定要将 <?php echo htmlspecialchars($booking['customer_name']); ?> 转换为VIP客户吗?')">
|
||||
👑 转为VIP
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- VIP客户列表 -->
|
||||
<div class="card">
|
||||
<div class="card enhanced-card">
|
||||
<h2>👑 VIP客户列表 (共 <?php echo count($vip_customers); ?> 位)</h2>
|
||||
|
||||
<?php if (empty($vip_customers)): ?>
|
||||
@@ -256,7 +382,7 @@ try {
|
||||
<?php else: ?>
|
||||
<div class="vip-grid">
|
||||
<?php foreach ($vip_customers as $vip): ?>
|
||||
<div class="vip-card">
|
||||
<div class="vip-card enhanced-card">
|
||||
<div class="vip-header">
|
||||
<h3><?php echo htmlspecialchars($vip['customer_name']); ?></h3>
|
||||
<div class="vip-status">👑 VIP</div>
|
||||
@@ -413,13 +539,19 @@ try {
|
||||
<option value="">请选择套餐</option>
|
||||
<?php
|
||||
// 查询所有可用的洗车套餐
|
||||
$package_result = $conn->query("SELECT id, package_name, price FROM packages ORDER BY price ASC");
|
||||
if ($package_result && $package_result->num_rows > 0) {
|
||||
while ($package = $package_result->fetch_assoc()) {
|
||||
// #region agent log
|
||||
$log_data = json_encode(['location' => 'vip.php:417', 'message' => 'Loading packages', 'data' => ['using_pdo' => isset($pdo), 'using_conn' => isset($conn)], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'B']);
|
||||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||||
// #endregion
|
||||
try {
|
||||
$stmt = $pdo->query("SELECT id, package_name, price FROM packages WHERE is_active = 1 ORDER BY price ASC");
|
||||
$packages = $stmt->fetchAll();
|
||||
foreach ($packages as $package) {
|
||||
echo "<option value='{$package['id']}'>{$package['package_name']} - ¥{$package['price']}</option>";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo "<option value=''>加载套餐失败</option>";
|
||||
}
|
||||
$package_result->free_result();
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
@@ -921,7 +1053,11 @@ try {
|
||||
document.getElementById('booking_car_number').value = carNumber || '';
|
||||
|
||||
// 设置默认预约日期为今天
|
||||
document.getElementById('booking_date').value = new Date().toISOString().split('T')[0];
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
document.getElementById('booking_date').value = `${year}-${month}-${day}`;
|
||||
|
||||
// 清空其他字段
|
||||
document.getElementById('booking_time_slot').value = '';
|
||||
|
||||
+7
-5
@@ -82,9 +82,8 @@ $method = $_SERVER['REQUEST_METHOD'];
|
||||
// 获取请求数据
|
||||
$request_body = file_get_contents('php://input');
|
||||
|
||||
// 记录所有请求
|
||||
// 记录所有请求(仅记录方法)
|
||||
log_message("收到请求 - 方法: $method");
|
||||
log_message("请求内容: $request_body");
|
||||
|
||||
// 处理验证请求
|
||||
if ($method == 'POST') {
|
||||
@@ -108,7 +107,6 @@ if ($method == 'POST') {
|
||||
|
||||
// 处理实际的表单数据
|
||||
log_message("处理表单数据");
|
||||
log_message("原始表单数据: " . print_r($data, true), 'data');
|
||||
|
||||
// 解析WPS表单数据结构
|
||||
$form_data = array(
|
||||
@@ -174,8 +172,6 @@ if ($method == 'POST') {
|
||||
}
|
||||
}
|
||||
|
||||
log_message("解析后的表单数据: " . print_r($form_data, true), 'data');
|
||||
|
||||
// 在这里可以添加数据处理逻辑,例如:
|
||||
// 1. 将数据保存到数据库
|
||||
// 2. 发送通知
|
||||
@@ -233,7 +229,9 @@ if ($method == 'POST') {
|
||||
echo json_encode($response);
|
||||
} else {
|
||||
// 处理非POST请求
|
||||
// 在错误时记录完整请求
|
||||
log_message("不支持的请求方法: $method", 'error');
|
||||
log_message("完整请求内容: $request_body", 'error');
|
||||
http_response_code(405); // 方法不允许
|
||||
echo json_encode(array('error' => '只支持POST请求'));
|
||||
}
|
||||
@@ -245,6 +243,8 @@ function store_form_data_to_db($form_data) {
|
||||
try {
|
||||
$pdo = get_db_connection();
|
||||
if (!$pdo) {
|
||||
// 在错误时记录完整表单数据
|
||||
log_message("数据库连接失败,表单数据: " . print_r($form_data, true), 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -305,7 +305,9 @@ function store_form_data_to_db($form_data) {
|
||||
log_message("表单数据成功存储到数据库,插入ID: " . $pdo->lastInsertId());
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
// 在错误时记录完整表单数据
|
||||
log_message("数据存储失败: " . $e->getMessage(), 'error');
|
||||
log_message("表单数据: " . print_r($form_data, true), 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user