51 Commits

Author SHA1 Message Date
wsh5485 1f0cf4acaa fix: 排除已取消的订单在待处理预约查询中
修改SQL查询条件,添加status != '已取消'过滤,确保已取消的订单不会出现在待处理预约列表中
2025-12-06 03:25:56 +08:00
wsh5485 26dd84bea2 docs: 统一导航菜单中"待处理预约"的文案
将多个页面导航菜单中的"待预约处理"统一修改为"待处理预约",保持文案一致性
2025-12-06 03:19:18 +08:00
wsh5485 a372464299 docs: 添加系统功能文档VERSION_FEATURES.md
添加详细的系统功能文档,包含核心功能模块介绍、技术实现细节、系统架构、界面优化、安装配置说明和使用指南
2025-12-06 03:09:41 +08:00
wsh5485 6906d8da2a docs: 重构README文档使其更简洁清晰
- 简化项目描述和功能说明
- 优化目录结构和内容组织
- 移除冗余的技术细节和安装步骤
- 更新项目结构说明
- 改进使用指南部分
2025-12-06 03:04:34 +08:00
wsh5485 decbc95d28 docs: 统一更新页面标题为"张老师撸车工作室"
更新所有页面的标题和头部显示,将原来的"洗车预约系统"统一改为"张老师撸车工作室",保持品牌一致性
2025-12-06 03:00:40 +08:00
wsh5485 7f2fe3dd21 style(packages): 优化表单样式和交互体验
- 移除渐变边框效果,简化卡片悬停样式
- 为表单添加内联样式,提升视觉一致性和用户体验
- 调整服务项目输入框布局,使其更紧凑易用
2025-12-06 02:51:56 +08:00
wsh5485 ae3ed1e58f feat(ui): 更新页面标题和样式,增强移动端体验
refactor: 优化套餐管理页面布局和交互效果
2025-12-06 02:45:16 +08:00
wsh5485 bb3ebffb37 统一packages.php与pending_bookings.php的样式 2025-12-06 02:17:42 +08:00
wsh5485 9d8fa49206 美化packages.php界面:1)添加了套餐卡片网格布局;2)优化了表单样式和按钮设计;3)增强了套餐卡片的视觉层次;4)改进了空状态显示;5)添加了响应式设计支持 2025-12-06 02:08:38 +08:00
wsh5485 b5cacb9e0b 注释掉pending_bookings.php中无实际意义的applyCustomDuration按钮 2025-12-06 02:04:21 +08:00
wsh5485 cb9eb599ec refactor: 移除自定义时长输入框及相关按钮 2025-12-06 02:02:38 +08:00
wsh5485 6344947fa7 在pending_bookings.php中为时长输入框添加分钟单位标识 2025-12-06 02:02:16 +08:00
wsh5485 5c0132a209 删除重复的时长输入框:1)将customDuration_xxx输入框修改为同时用于显示和提交;2)删除独立的duration_xxx输入框;3)更新相关的JavaScript函数和label引用 2025-12-06 01:56:05 +08:00
wsh5485 5ace9b86d8 修复日期显示不一致问题:1)在index.php中使用本地时区获取日期字符串,避免toISOString()导致的时区偏差;2)在vip.php中同样修改默认预约日期的获取方式 2025-12-06 01:52:06 +08:00
wsh5485 565fc310b7 修复日期显示问题:1)在index.php和pending_bookings.php中添加时区设置;2)在db_connect.php中添加全局时区设置,确保系统一致性 2025-12-06 01:48:43 +08:00
wsh5485 77ae32095e 统一时间选择样式:将index.php的时间选择样式更新为pending_bookings.php的样式 2025-12-06 01:45:39 +08:00
wsh5485 ca554456b0 优化套餐信息区域排版:1)改进容器样式,增加圆角和阴影;2)美化套餐元数据标签;3)将服务列表改为无序列表结构;4)添加响应式布局和悬停效果 2025-12-06 01:41:49 +08:00
wsh5485 d387487bc5 修复data-services属性JSON解析错误,增加对逗号分隔字符串的支持 2025-12-06 01:39:07 +08:00
wsh5485 44e4bc64d8 修复自定义时长输入框未随套餐选择更新的问题 2025-12-06 01:37:50 +08:00
wsh5485 aefd3df9ae 修复套餐选择后默认服务时长未带出问题:1)优化元素获取方式,确保能找到正确的表单和输入框;2)添加全面的错误处理和调试信息;3)触发change事件确保所有依赖功能更新 2025-12-06 01:33:18 +08:00
wsh5485 9a256cda0a 修复套餐选择后默认服务时长未带出的问题:1)使用dataset属性获取套餐数据;2)优化selectDuration函数的表单查找逻辑;3)添加调试日志以便排查问题 2025-12-06 01:28:47 +08:00
wsh5485 0245f2c822 使用data-duration属性修复服务时长选择按钮状态更新问题 2025-12-06 01:24:48 +08:00
wsh5485 f297a12a7a 修复selectDuration函数,确保套餐默认服务时间正确显示 2025-12-06 01:17:33 +08:00
wsh5485 466f16e313 修复套餐选择默认时间问题并添加快捷选择服务时长功能 2025-12-06 01:14:37 +08:00
wsh5485 a6356ea2a2 修复已过期和已预约时间段可点击的问题 2025-12-06 01:10:27 +08:00
wsh5485 ff23ac6bbb 修复时间段选择没有反应的问题 2025-12-06 01:07:54 +08:00
wsh5485 d0abed0f86 修复套餐默认时间和时间段选择问题 2025-12-06 01:06:11 +08:00
wsh5485 3944051b25 更新待预约页面:添加套餐选择、日历和空闲时间段显示功能 2025-12-06 01:02:00 +08:00
wsh5485 a9b50046c1 调整webhook日志级别,仅在错误时记录完整信息 2025-12-05 18:18:01 +08:00
wsh5485 caac9899a5 fix: 修正导航栏活动链接状态
修复导航栏中“预约洗车”和“预约管理”链接的活动状态错误,确保当前页面链接显示为激活状态
2025-12-05 18:14:06 +08:00
wsh5485 6ffabd9384 fix: 修复会话启动位置以避免重复启动
将 session_start() 移到文件开头,避免在显示预约成功信息时重复启动会话
2025-12-05 18:12:41 +08:00
wsh5485 fc3b6aa4cc feat(导航): 添加待预约处理链接并更新活动状态
在导航栏中添加"待预约处理"链接,并将"预约管理"设为当前活动状态
2025-12-05 18:10:36 +08:00
wsh5485 ac40ce0d56 refactor(database): 移除旧的数据库脚本文件
移除不再使用的create_wps_form_table.sql和carwash_db.sql数据库脚本文件,
这些文件已被新的数据库结构和迁移脚本所替代
2025-12-05 18:07:56 +08:00
wsh5485 2590e1fc22 创建待预约页面并更新导航栏 2025-12-05 18:07:00 +08:00
wsh5485 2fb3c70f87 合并SQL文件并移除webhook中的建表代码 2025-12-05 17:51:35 +08:00
wsh5485 5c9f4c1a4f 添加自动编号字段支持:在数据库表和数据存储逻辑中添加auto_number字段 2025-12-05 17:44:57 +08:00
wsh5485 b2ad942b2c 添加对WPS绑定验证请求(bind事件)的处理 2025-12-05 17:38:10 +08:00
wsh5485 0d20bc09bc 修复MySQL 1293错误:将updated_at字段改为DATETIME类型并移除CURRENT_TIMESTAMP属性 2025-12-05 17:30:15 +08:00
wsh5485 8d1612b956 修复MySQL 5.7兼容性问题:将DATETIME字段改回TIMESTAMP字段,使用兼容的默认值 2025-12-05 17:28:28 +08:00
wsh5485 3893065393 更新create_wps_form_table.sql文件,使其与webhook.php中的表结构一致 2025-12-05 17:26:45 +08:00
wsh5485 bafbc8344b 修复webhook.php中的TIMESTAMP限制问题:将TIMESTAMP字段改为DATETIME字段 2025-12-05 17:23:06 +08:00
wsh5485 b4ab3c625a 修复创建表失败的问题:解决多个TIMESTAMP列使用CURRENT_TIMESTAMP的错误 2025-12-05 17:19:08 +08:00
wsh5485 678c79c5a7 fix(test): 修正测试页面中日志文件路径错误
将测试页面中的日志文件路径从相对路径改为绝对路径,确保能正确加载日志文件
2025-12-05 17:17:21 +08:00
wsh5485 016a5bf6b9 fix: 确保日志目录存在以避免写入失败
在写入日志前检查目录是否存在,不存在时自动创建,防止因目录缺失导致的日志写入失败
2025-12-05 17:11:42 +08:00
wsh5485 1a196f968e test: 更新测试网页中的webhook URL为生产环境地址 2025-12-05 17:10:21 +08:00
wsh5485 6a43cec22e 添加配置文件模板config.php.env和安装文档INSTALL.md,从Git中移除config.php 2025-12-05 17:06:43 +08:00
wsh5485 93dfc4e5cd fix: 更新数据库配置并添加404页面
更新生产环境数据库连接配置
添加默认nginx 404错误页面
2025-12-05 17:03:26 +08:00
wsh5485 537a95989e refactor(webhook): 统一使用licensePlate类型的车牌号字段
移除对多个车牌号字段的支持,仅保留qid为t2u2i4的licensePlate类型字段
2025-12-05 16:28:18 +08:00
wsh5485 88346ca1ec refactor(webhook): 根据实际WPS表单字段调整数据库结构和映射
更新数据库表结构和字段映射以匹配实际WPS表单字段
移除不再使用的字段,仅保留实际存在的表单字段
调整SQL语句和绑定逻辑以匹配新字段结构
2025-12-05 15:31:02 +08:00
wsh5485 5d320e0ccf refactor: 将日志文件路径移动到log目录下
修改日志文件存储位置,便于统一管理日志文件
2025-12-05 14:00:35 +08:00
wsh5485 5bbb0877d5 feat: 添加WPS表单处理功能及相关测试
实现WPS表单Webhook接收端点,包括数据解析、验证和存储功能
添加数据库表创建脚本和测试用例
包含Webhook测试页面和日志功能
2025-12-05 13:58:57 +08:00
18 changed files with 3177 additions and 650 deletions
+7
View File
@@ -0,0 +1,7 @@
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
+92
View File
@@ -0,0 +1,92 @@
# 洗车预约系统安装指南
## 1. 环境要求
- PHP 5.6 或更高版本
- MySQL 5.5 或更高版本
- Web服务器(如Apache、Nginx等)
## 2. 安装步骤
### 2.1 克隆项目
首先,将项目克隆到您的本地或服务器上:
```bash
git clone [项目仓库地址]
cd carwash_order
```
### 2.2 配置数据库
1. 创建数据库:
- 使用MySQL客户端(如phpMyAdmin、Navicat等)创建一个新的数据库
- 数据库名称建议使用 `carwash_booking`
2. 导入数据库结构:
- 执行 `carwash_db.sql` 文件,创建所需的表结构和初始数据
### 2.3 配置数据库连接
1. 复制配置文件模板:
-`config.php.env` 重命名为 `config.php`
2. 修改数据库连接信息:
- 使用文本编辑器打开 `config.php` 文件
- 根据您的数据库配置修改以下参数:
```php
// 数据库主机地址
$host = 'localhost'; // 通常为localhost,如使用远程数据库请填写IP地址
// 数据库用户名
$username = 'your_username'; // 替换为您的MySQL用户名
// 数据库密码
$password = 'your_password'; // 替换为您的MySQL密码
// 数据库名称
$database = 'carwash_booking'; // 替换为您创建的数据库名称
```
### 2.4 部署到Web服务器
- 将项目文件上传到Web服务器的根目录或虚拟主机的文档根目录
- 确保Web服务器具有对项目文件的读取权限
- 确保PHP可以执行项目中的PHP文件
## 3. 访问系统
在浏览器中输入您的域名或IP地址,即可访问洗车预约系统。
例如:
```
http://localhost/carwash_order/
```
## 4. 常见问题
### 4.1 数据库连接失败
- 检查 `config.php` 文件中的数据库连接信息是否正确
- 确保MySQL服务正在运行
- 确保数据库用户具有足够的权限
### 4.2 页面显示错误
- 检查PHP版本是否符合要求
- 检查Web服务器的错误日志
- 确保所有项目文件都已正确上传
## 5. 其他配置
- 如果您需要修改时区设置,可以在PHP文件中添加:`date_default_timezone_set('Asia/Shanghai');`
- 如果您需要修改会话超时时间,可以在PHP文件中添加:`ini_set('session.gc_maxlifetime', 3600);`
## 6. 注意事项
- 不要将 `config.php` 文件提交到Git仓库,该文件包含敏感的数据库连接信息
- 定期备份数据库和项目文件
- 确保使用强密码保护数据库和服务器
如有其他问题,请参考项目的README.md文件或联系技术支持。
+52 -468
View File
@@ -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服务
```
#### 使用WAMPWindows用户)
```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 # 预约公告页面
├── index.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搜索调试页面
├── announcement.php # 今日待办页面
├── db_connect.php # 数据库连接
├── style.css # 样式文件
├── merged_db.sql # 数据库结构
── test/ # 测试工具目录
```
## 🗄️ 数据库结构
## 使用说明
### 主要数据表
### 客户预约
1. 访问主页面,选择客户类型(普通/VIP)
2. 填写客户信息或搜索VIP客户
3. 选择服务套餐和预约时间
4. 提交预约
#### 1. vip_customersVIP客户表)
```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
---
**洗车店订单管理系统** - 让洗车店管理更简单、更高效! 🚗✨
**张老师撸车工作室** - 专业洗车服务预约管理系统
+170
View File
@@ -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开发
+3 -2
View File
@@ -58,7 +58,7 @@ 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">
<style>
/* 公告页面特有样式 */
@@ -274,10 +274,11 @@ try {
<body>
<div class="container">
<header class="header">
<h1>🚗 洗车店管理系统</h1>
<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>
+3 -2
View File
@@ -74,7 +74,7 @@ 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>
@@ -84,10 +84,11 @@ try {
<body>
<div class="container">
<header class="header">
<h1>🚗 洗车预约系统 - 预约管理</h1>
<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>
-7
View File
@@ -1,7 +0,0 @@
<?php
// config.php - 数据库配置文件
$host = 'localhost';
$username = 'root';
$password = '';
$database = 'carwash_booking';
?>
+16
View File
@@ -0,0 +1,16 @@
<?php
// config.php.env - 数据库配置文件模板
// 请将此文件重命名为 config.php 并根据实际情况修改以下配置
// 数据库主机地址
$host = 'localhost';
// 数据库用户名
$username = 'your_username';
// 数据库密码
$password = 'your_password';
// 数据库名称
$database = 'carwash_booking';
?>
+1
View File
@@ -1,5 +1,6 @@
<?php
// db_connect.php - 数据库连接文件
date_default_timezone_set('Asia/Shanghai');
require_once 'config.php';
try {
+83 -7
View File
@@ -1,4 +1,5 @@
<?php
date_default_timezone_set('Asia/Shanghai');
session_start();
require_once 'db_connect.php';
@@ -198,7 +199,7 @@ $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">
<style>
@@ -310,10 +311,11 @@ $packages_json = json_encode(array_map(function($package) {
<body>
<div class="container">
<header class="header">
<h1>🚗 洗车预约系统</h1>
<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>
@@ -435,13 +437,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,6 +515,71 @@ $packages_json = json_encode(array_map(function($package) {
});
</script>
<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" id="timeSlots" style="display: none;">
<h3>🕐 选择时间段</h3>
<div class="quick-duration">
@@ -518,8 +592,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动态生成 -->
@@ -1805,6 +1879,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 +1960,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');
}
+31
View File
@@ -63,6 +63,37 @@ CREATE TABLE IF NOT EXISTS bookings (
FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE SET NULL
);
-- 创建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;
-- 插入示例套餐数据
INSERT INTO packages (package_name, description, base_duration, price, services) VALUES
('基础洗车', '基础外观清洗', 30, 50.00, '外观冲洗,泡沫清洁,内饰吸尘'),
+123 -82
View File
@@ -51,23 +51,34 @@ $packages = $stmt->fetchAll();
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="format-detection" content="telephone=no">
<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>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>张老师撸车(私家车库)工作室</title>
<link rel="stylesheet" href="style.css">
<style>
/* 套餐管理页面特定样式 */
.pending-bookings-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 加载动画 */
.loading { display: none; }
/* 页面淡入效果 */
body {
animation: fadeIn 0.5s ease;
}
</style>
</head>
<body>
<div class="container">
<div class="pending-bookings-container">
<header class="header">
<h1>🚗 洗车预约系统 - 套餐管理</h1>
<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 active">套餐管理</a>
<a href="vip.php" class="nav-link">VIP管理</a>
<a href="announcement.php" class="nav-link">今日待办</a>
@@ -82,62 +93,75 @@ $packages = $stmt->fetchAll();
<?php endif; ?>
<div class="package-form-container">
<h2>添加新套餐</h2>
<form method="POST" class="form">
<h2>📋 添加新套餐</h2>
<form method="POST" class="package-form" style="background: white; border-radius: 8px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<input type="hidden" name="action" value="add">
<div class="form-group">
<label for="package_name">套餐名称</label>
<input type="text" id="package_name" name="package_name" required>
<div class="form-group" style="margin-bottom: 16px;">
<label for="package_name" class="required" style="display: block; font-weight: 600; margin-bottom: 6px; color: #333;">套餐名称</label>
<input type="text" id="package_name" name="package_name" required placeholder="如:标准洗车套餐" class="form-control" style="width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
</div>
<div class="form-group">
<label for="description">套餐描述</label>
<textarea id="description" name="description" rows="3"></textarea>
<div class="form-group" style="margin-bottom: 16px;">
<label for="description" style="display: block; font-weight: 600; margin-bottom: 6px; color: #333;">套餐描述</label>
<textarea id="description" name="description" rows="3" placeholder="详细描述套餐内容和特点" class="form-control" style="width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; resize: vertical;"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="base_duration">基础时长(分钟):</label>
<input type="number" id="base_duration" name="base_duration" min="15" step="15" value="60" required>
</div>
<div class="form-group">
<label for="price">价格(元):</label>
<input type="number" id="price" name="price" min="0" step="0.01" required>
<div class="form-row" style="display: flex; gap: 16px; margin-bottom: 16px;">
<div class="form-group" style="flex: 1; margin-bottom: 0;">
<label for="base_duration" class="required" style="display: block; font-weight: 600; margin-bottom: 6px; color: #333;">基础时长</label>
<div class="input-group" style="display: flex; align-items: center;">
<input type="number" id="base_duration" name="base_duration" min="15" step="15" value="60" required class="form-control" style="flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px 0 0 4px; font-size: 14px; border-right: none;">
<span class="input-group-addon" style="padding: 10px 12px; background: #f5f5f5; border: 1px solid #ddd; border-radius: 0 4px 4px 0; font-size: 14px; color: #666;">分钟</span>
</div>
</div>
<div class="form-group">
<label>服务项目:</label>
<div class="services-container">
<div class="service-item">
<input type="text" name="services[]" placeholder="服务项目1" value="外观清洗">
<button type="button" class="btn-remove" onclick="removeService(this)">删除</button>
</div>
<div class="service-item">
<input type="text" name="services[]" placeholder="服务项目2" value="内饰清洁">
<button type="button" class="btn-remove" onclick="removeService(this)">删除</button>
<div class="form-group" style="flex: 1; margin-bottom: 0;">
<label for="price" class="required" style="display: block; font-weight: 600; margin-bottom: 6px; color: #333;">价格</label>
<div class="input-group" style="display: flex; align-items: center;">
<span class="input-group-addon" style="padding: 10px 12px; background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px 0 0 4px; font-size: 14px; color: #666;">¥</span>
<input type="number" id="price" name="price" min="0" step="0.01" required placeholder="0.00" class="form-control" style="flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 0 4px 4px 0; font-size: 14px; border-left: none;">
</div>
</div>
<button type="button" class="btn-add" onclick="addService()">+ 添加服务项目</button>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">添加套餐</button>
<div class="form-group" style="margin-bottom: 16px;">
<label style="display: block; font-weight: 600; margin-bottom: 6px; color: #333;">服务项目</label>
<div class="services-container" style="margin-bottom: 8px;">
<div class="service-item" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center;">
<input type="text" name="services[]" placeholder="如:外观清洗" value="外观清洗" class="form-control" style="flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
<button type="button" class="btn-danger btn-sm" onclick="removeService(this)" style="padding: 6px 12px; background: #ff4d4f; color: white; border: none; border-radius: 4px; font-size: 12px; cursor: pointer;"><span>删除</span></button>
</div>
<div class="service-item" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center;">
<input type="text" name="services[]" placeholder="如:内饰清洁" value="内饰清洁" class="form-control" style="flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
<button type="button" class="btn-danger btn-sm" onclick="removeService(this)" style="padding: 6px 12px; background: #ff4d4f; color: white; border: none; border-radius: 4px; font-size: 12px; cursor: pointer;"><span>删除</span></button>
</div>
</div>
<button type="button" class="btn-outline btn-sm" onclick="addService()" style="padding: 6px 12px; background: white; color: #1890ff; border: 1px solid #1890ff; border-radius: 4px; font-size: 12px; cursor: pointer;"><span>+ 添加服务项目</span></button>
</div>
<div class="form-actions" style="margin-top: 24px; display: flex; justify-content: flex-end;">
<button type="submit" class="btn-primary btn-lg" style="padding: 10px 24px; background: #1890ff; color: white; border: none; border-radius: 4px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.3s;">
<span>✨ 添加套餐</span>
</button>
</div>
</form>
</div>
<div class="packages-list">
<h2>套餐列表</h2>
<h2>📊 套餐列表</h2>
<?php if (empty($packages)): ?>
<div class="empty-state">
<div class="empty-icon">📦</div>
<p class="empty-message">暂无套餐数据</p>
<p class="empty-submessage">点击上方"添加套餐"按钮创建第一个套餐吧!</p>
</div>
<?php else: ?>
<div class="packages-grid">
<?php foreach ($packages as $package): ?>
<div class="package-card <?= $package['is_active'] ? '' : 'inactive' ?>">
<div class="package-card <?= $package['is_active'] ? '' : 'inactive' ?>" data-package-id="<?= $package['id'] ?>">
<div class="package-header">
<h3><?= htmlspecialchars($package['package_name']) ?></h3>
<h3 class="package-title"><?= htmlspecialchars($package['package_name']) ?></h3>
<div class="package-status">
<span class="status-badge <?= $package['is_active'] ? 'active' : 'inactive' ?>">
<?= $package['is_active'] ? '启用' : '禁用' ?>
@@ -151,12 +175,12 @@ $packages = $stmt->fetchAll();
<div class="package-details">
<div class="detail-item">
<span class="detail-label">基础时长</span>
<span class="detail-value"><?= $package['base_duration'] ?>分钟</span>
<span class="detail-label">时长</span>
<span class="detail-value"><strong><?= $package['base_duration'] ?></strong>分钟</span>
</div>
<div class="detail-item">
<span class="detail-label">价格</span>
<span class="detail-value">¥<?= number_format($package['price'], 2) ?></span>
<span class="detail-label">价格</span>
<span class="detail-value price"><strong>¥<?= number_format($package['price'], 2) ?></strong></span>
</div>
</div>
@@ -165,7 +189,7 @@ $packages = $stmt->fetchAll();
if ($services && !empty(trim($services[0]))):
?>
<div class="package-services">
<span class="detail-label">包含服务</span>
<span class="detail-label">包含服务</span>
<div class="services-tags">
<?php foreach ($services as $service): ?>
<?php if (trim($service)): ?>
@@ -177,75 +201,88 @@ $packages = $stmt->fetchAll();
<?php endif; ?>
<div class="package-actions">
<button class="btn btn-sm" onclick="editPackage(<?= $package['id'] ?>)">编辑</button>
<button class="btn-primary btn-sm" onclick="editPackage(<?= $package['id'] ?>)"><span>✏️ 编辑</span></button>
<form method="POST" style="display: inline;" onsubmit="return confirmDelete()">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= $package['id'] ?>">
<button type="submit" class="btn btn-danger btn-sm">删除</button>
<button type="submit" class="btn-danger btn-sm"><span>🗑️ 删除</span></button>
</form>
</div>
<!-- 编辑表单 -->
<form method="POST" class="edit-form" id="edit-form-<?= $package['id'] ?>" style="display: none;">
<form method="POST" class="edit-form" id="edit-form-<?= $package['id'] ?>" style="display: none; background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-top: 16px;">
<input type="hidden" name="action" value="update">
<input type="hidden" name="id" value="<?= $package['id'] ?>">
<div class="form-group">
<label>套餐名称:</label>
<input type="text" name="package_name" value="<?= htmlspecialchars($package['package_name']) ?>" required>
<h4 style="margin-top: 0; margin-bottom: 16px; font-size: 16px; font-weight: 600; color: #333;">✏️ 编辑套餐</h4>
<div class="form-group" style="margin-bottom: 16px;">
<label class="required" style="display: block; font-weight: 600; margin-bottom: 6px; color: #333;">套餐名称</label>
<input type="text" name="package_name" value="<?= htmlspecialchars($package['package_name']) ?>" required placeholder="请输入套餐名称" class="form-control" style="width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
</div>
<div class="form-group">
<label>套餐描述</label>
<textarea name="description" rows="2"><?= htmlspecialchars($package['description']) ?></textarea>
<div class="form-group" style="margin-bottom: 16px;">
<label style="display: block; font-weight: 600; margin-bottom: 6px; color: #333;">套餐描述</label>
<textarea name="description" rows="2" placeholder="请输入套餐描述" class="form-control" style="width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; resize: vertical;"><?= htmlspecialchars($package['description']) ?></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>基础时长(分钟):</label>
<input type="number" name="base_duration" min="15" step="15" value="<?= $package['base_duration'] ?>" required>
</div>
<div class="form-group">
<label>价格(元):</label>
<input type="number" name="price" min="0" step="0.01" value="<?= $package['price'] ?>" required>
<div class="form-row" style="display: flex; gap: 16px; margin-bottom: 16px;">
<div class="form-group" style="flex: 1; margin-bottom: 0;">
<label class="required" style="display: block; font-weight: 600; margin-bottom: 6px; color: #333;">基础时长</label>
<div class="input-group" style="display: flex; align-items: center;">
<input type="number" name="base_duration" min="15" step="15" value="<?= $package['base_duration'] ?>" required class="form-control" style="flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px 0 0 4px; font-size: 14px; border-right: none;">
<span class="input-group-addon" style="padding: 10px 12px; background: #f5f5f5; border: 1px solid #ddd; border-radius: 0 4px 4px 0; font-size: 14px; color: #666;">分钟</span>
</div>
</div>
<div class="form-group">
<label>服务项目:</label>
<div class="services-container">
<div class="form-group" style="flex: 1; margin-bottom: 0;">
<label class="required" style="display: block; font-weight: 600; margin-bottom: 6px; color: #333;">价格</label>
<div class="input-group" style="display: flex; align-items: center;">
<span class="input-group-addon" style="padding: 10px 12px; background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px 0 0 4px; font-size: 14px; color: #666;">¥</span>
<input type="number" name="price" min="0" step="0.01" value="<?= $package['price'] ?>" required placeholder="0.00" class="form-control" style="flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 0 4px 4px 0; font-size: 14px; border-left: none;">
</div>
</div>
</div>
<div class="form-group" style="margin-bottom: 16px;">
<label style="display: block; font-weight: 600; margin-bottom: 6px; color: #333;">服务项目</label>
<div class="services-container" style="margin-bottom: 8px;">
<?php
$services = explode(',', $package['services']);
foreach ($services as $service):
if (trim($service)):
?>
<div class="service-item">
<input type="text" name="services[]" value="<?= htmlspecialchars(trim($service)) ?>">
<button type="button" class="btn-remove" onclick="removeService(this)">删除</button>
<div class="service-item" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center;">
<input type="text" name="services[]" value="<?= htmlspecialchars(trim($service)) ?>" placeholder="请输入服务项目" class="form-control" style="flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
<button type="button" class="btn-danger btn-sm" onclick="removeService(this)" style="padding: 6px 12px; background: #ff4d4f; color: white; border: none; border-radius: 4px; font-size: 12px; cursor: pointer;"><span>删除</span></button>
</div>
<?php
endif;
endforeach;
?>
</div>
<button type="button" class="btn-add" onclick="addService()">+ 添加服务项目</button>
<button type="button" class="btn-outline btn-sm" onclick="addService()" style="padding: 6px 12px; background: white; color: #1890ff; border: 1px solid #1890ff; border-radius: 4px; font-size: 12px; cursor: pointer;"><span>+ 添加服务项目</span></button>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="is_active" <?= $package['is_active'] ? 'checked' : '' ?>>
启用此套餐
<div class="form-group" style="margin-bottom: 16px;">
<label class="checkbox-label" style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" name="is_active" <?= $package['is_active'] ? 'checked' : '' ?> style="margin-right: 8px;">
<span class="checkbox-text" style="font-weight: normal; color: #333;">启用此套餐</span>
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">保存更改</button>
<button type="button" class="btn" onclick="cancelEdit(<?= $package['id'] ?>)">取消</button>
<div class="form-actions" style="display: flex; gap: 12px; justify-content: flex-end;">
<button type="submit" class="btn-primary" style="padding: 8px 16px; background: #1890ff; color: white; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; transition: background 0.3s;">
<span>💾 保存更改</span>
</button>
<button type="button" class="btn-outline" onclick="cancelEdit(<?= $package['id'] ?>)" style="padding: 8px 16px; background: white; color: #666; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; cursor: pointer; transition: all 0.3s;">
<span>❌ 取消</span>
</button>
</div>
</form>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
@@ -255,9 +292,13 @@ $packages = $stmt->fetchAll();
const container = document.querySelector('.services-container');
const serviceItem = document.createElement('div');
serviceItem.className = 'service-item';
serviceItem.style.display = 'flex';
serviceItem.style.gap = '8px';
serviceItem.style.marginBottom = '8px';
serviceItem.style.alignItems = 'center';
serviceItem.innerHTML = `
<input type="text" name="services[]" placeholder="服务项目">
<button type="button" class="btn-remove" onclick="removeService(this)">删除</button>
<input type="text" name="services[]" placeholder="如:外观清洗" class="form-control" style="flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;" required>
<button type="button" class="btn-danger btn-sm" onclick="removeService(this)" style="padding: 6px 12px; background: #ff4d4f; color: white; border: none; border-radius: 4px; font-size: 12px; cursor: pointer;"><span>删除</span></button>
`;
container.appendChild(serviceItem);
}
+1094
View File
File diff suppressed because it is too large Load Diff
+717 -5
View File
@@ -878,6 +878,85 @@ body {
margin: 0;
max-height: calc(100vh - 20px);
}
/* 套餐管理页面移动端优化 */
.pending-bookings-container {
padding: var(--el-spacing-small);
}
.pending-card {
padding: var(--el-spacing-base);
}
.pending-header h2 {
font-size: var(--el-font-size-large);
}
.package-card {
padding: var(--el-spacing-base);
margin-bottom: var(--el-spacing-base);
}
.package-header {
flex-direction: column;
align-items: flex-start;
gap: var(--el-spacing-small);
}
.package-details {
grid-template-columns: 1fr;
gap: var(--el-spacing-small);
}
.services-tags {
justify-content: flex-start;
gap: var(--el-spacing-small);
}
.service-tag {
font-size: var(--el-font-size-small);
padding: 3px 6px;
}
.package-actions {
flex-direction: column;
gap: var(--el-spacing-small);
}
/* 服务项目移动端布局 */
.service-item {
flex-direction: column;
align-items: stretch !important;
gap: var(--el-spacing-small) !important;
}
.service-item div {
flex: none !important;
}
/* 表单移动端优化 */
.form-group {
margin-bottom: var(--el-spacing-base);
}
.form-control {
padding: 14px 16px;
font-size: 16px; /* 防止iOS缩放 */
}
.input-group {
flex-direction: column;
}
.input-group .form-control {
border-radius: var(--el-border-radius-base);
margin-bottom: var(--el-spacing-small);
}
.input-group .btn {
border-radius: var(--el-border-radius-base);
width: 100%;
}
}
/* 小屏幕手机 */
@@ -963,6 +1042,171 @@ body {
to { transform: translateX(0); }
}
/* 页面加载动画 */
.page-fade-in {
animation: pageFadeIn 0.6s ease-out;
}
@keyframes pageFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 套餐卡片入场动画 */
.package-card {
animation: cardFadeIn 0.4s ease-out forwards;
opacity: 0;
transform: translateY(20px);
}
@keyframes cardFadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
/* 卡片延迟动画,为每个卡片添加不同的延迟 */
.package-card:nth-child(1) { animation-delay: 0.1s; }
.package-card:nth-child(2) { animation-delay: 0.2s; }
.package-card:nth-child(3) { animation-delay: 0.3s; }
.package-card:nth-child(4) { animation-delay: 0.4s; }
.package-card:nth-child(5) { animation-delay: 0.5s; }
.package-card:nth-child(6) { animation-delay: 0.6s; }
/* 按钮波纹效果 */
.btn {
position: relative;
overflow: hidden;
}
.btn::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.btn:hover::after {
width: 300px;
height: 300px;
}
.btn-primary::after {
background: rgba(255, 255, 255, 0.5);
}
/* 表单元素动画 */
.form-control {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 服务标签悬停动画 */
.service-tag {
transition: all 0.3s ease;
}
.service-tag:hover {
transform: translateY(-2px);
box-shadow: var(--el-shadow-light);
}
/* 状态徽章动画 */
.status-badge {
transition: all 0.3s ease;
}
/* 空状态动画 */
.empty-message {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 0.8;
}
50% {
opacity: 1;
}
100% {
opacity: 0.8;
}
}
/* 表单基础样式 */
.form-control {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-base);
font-size: var(--el-font-size-base);
color: var(--el-text-primary);
background-color: var(--el-bg-color);
transition: all 0.3s ease;
outline: none;
box-sizing: border-box;
}
.form-control:focus {
border-color: var(--el-primary-color);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.form-control:hover:not(:disabled) {
border-color: var(--el-primary-light);
}
.form-control:disabled {
background-color: var(--el-bg-color-page);
opacity: 0.7;
cursor: not-allowed;
}
/* 表单组样式 */
.form-group {
margin-bottom: var(--el-spacing-large);
}
.form-group label {
display: block;
margin-bottom: var(--el-spacing-small);
font-weight: 500;
color: var(--el-text-primary);
font-size: var(--el-font-size-base);
}
.form-group label.required::after {
content: " *";
color: var(--el-danger-color);
}
/* 输入组样式 */
.input-group {
display: flex;
align-items: stretch;
width: 100%;
gap: var(--el-spacing-small);
}
.input-group .form-control {
flex: 1;
margin-bottom: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group .btn {
flex: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* 表单操作区域 */
.form-actions {
display: flex;
@@ -1110,18 +1354,486 @@ label {
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--el-bg-color-page);
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--el-border-color);
border-radius: 3px;
background: #ccc;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--el-text-secondary);
background: #999;
}
/* 套餐管理页面样式 */
.package-form-container {
margin-bottom: var(--el-spacing-extra-large);
background: var(--el-bg-color);
border-radius: var(--el-border-radius-base);
box-shadow: var(--el-shadow-light);
padding: var(--el-spacing-large);
}
.package-form-container h2,
.packages-list h2 {
margin: 0 0 var(--el-spacing-large) 0;
font-size: var(--el-font-size-large);
font-weight: 600;
color: var(--el-text-primary);
padding-bottom: var(--el-spacing-base);
border-bottom: 1px solid var(--el-border-lighter);
}
.packages-list {
background: var(--el-bg-color);
border-radius: var(--el-border-radius-base);
box-shadow: var(--el-shadow-light);
padding: var(--el-spacing-large);
}
/* 套餐卡片网格布局 */
.packages-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--el-spacing-large);
margin-top: var(--el-spacing-large);
}
/* 套餐卡片样式 */
.package-card {
background: var(--el-bg-color);
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
padding: var(--el-spacing-large);
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
border: 2px solid transparent;
background-clip: padding-box;
}
.package-card:hover {
transform: translateY(-6px);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
}
.package-card.inactive {
opacity: 0.7;
border: 1px dashed var(--el-border-color);
}
.package-card.inactive::before {
content: '已禁用';
position: absolute;
top: 16px;
right: -30px;
background: var(--el-danger-color);
color: white;
padding: 4px 32px;
font-size: 12px;
font-weight: 600;
transform: rotate(45deg);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1;
letter-spacing: 0.5px;
}
.package-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--el-spacing-base);
padding-bottom: var(--el-spacing-small);
border-bottom: 2px solid var(--el-border-color-lighter);
position: relative;
z-index: 2;
}
.package-title {
margin: 0;
font-size: 1.35rem;
font-weight: 700;
color: var(--el-text-primary);
flex: 1;
margin-right: var(--el-spacing-base);
line-height: 1.4;
position: relative;
display: inline-block;
}
/* 标题悬停下划线动画 */
.package-title::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: var(--el-primary-color);
transition: width 0.3s ease;
}
.package-card:hover .package-title::after {
width: 100%;
}
.package-status {
display: flex;
align-items: center;
}
.status-badge {
padding: 4px 12px;
border-radius: var(--el-border-radius-round);
font-size: var(--el-font-size-extra-small);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.status-badge.active {
background: rgba(103, 194, 58, 0.1);
color: var(--el-success-color);
border: 1px solid rgba(103, 194, 58, 0.2);
}
.status-badge.inactive {
background: rgba(245, 108, 108, 0.1);
color: var(--el-danger-color);
border: 1px solid rgba(245, 108, 108, 0.2);
}
.package-description {
margin: 0 0 var(--el-spacing-base) 0;
color: var(--el-text-secondary);
font-size: var(--el-font-size-small);
line-height: 1.6;
padding: 0 var(--el-spacing-small);
}
.package-details {
margin: var(--el-spacing-base) 0;
padding: var(--el-spacing-large);
background: linear-gradient(135deg, rgba(246, 248, 250, 0.8), rgba(238, 242, 246, 0.8));
border-radius: 10px;
border: 1px solid var(--el-border-color-lighter);
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
/* 详情区域悬停效果 */
.package-card:hover .package-details {
background: linear-gradient(135deg, rgba(240, 244, 248, 0.9), rgba(228, 232, 236, 0.9));
transform: scale(1.02);
}
/* 渐变光效 */
.package-details::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.4), transparent);
transition: left 0.6s ease;
}
.package-card:hover .package-details::before {
left: 100%;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--el-spacing-small);
font-size: var(--el-font-size-small);
}
.detail-item:last-child {
margin-bottom: 0;
}
.detail-label {
color: var(--el-text-secondary);
font-weight: 500;
font-size: var(--el-font-size-extra-small);
}
.detail-value {
color: var(--el-text-primary);
font-weight: 600;
}
.detail-value.price {
color: var(--el-danger-color);
font-size: var(--el-font-size-base);
}
/* 服务项目标签 */
.package-services {
margin: var(--el-spacing-base) 0;
}
.package-services .detail-label {
display: block;
margin-bottom: var(--el-spacing-small);
font-size: var(--el-font-size-small);
color: var(--el-text-regular);
font-weight: 500;
}
.services-tags {
display: flex;
flex-wrap: wrap;
gap: var(--el-spacing-small);
}
.service-tag {
background: var(--el-border-extra-light);
color: var(--el-text-regular);
padding: 6px 16px;
border-radius: 20px;
font-size: var(--el-font-size-extra-small);
font-weight: 600;
border: 1px solid var(--el-border-lighter);
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
line-height: 1.5;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
position: relative;
overflow: hidden;
}
.service-tag:hover {
background: var(--el-primary-color);
color: white;
border-color: var(--el-primary-color);
transform: translateY(-2px) scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* 服务标签悬停光效 */
.service-tag::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.4s ease;
}
.service-tag:hover::before {
left: 100%;
}
/* 套餐操作按钮 */
.package-actions {
margin-top: var(--el-spacing-base);
display: flex;
gap: var(--el-spacing-small);
justify-content: flex-end;
padding-top: var(--el-spacing-base);
border-top: 1px solid var(--el-border-lighter);
}
/* 空状态样式 */
.empty-state {
text-align: center;
padding: var(--el-spacing-extra-large) 0;
background: var(--el-bg-color);
border-radius: var(--el-border-radius-base);
box-shadow: var(--el-shadow-light);
margin-top: var(--el-spacing-large);
}
.empty-icon {
font-size: 64px;
margin-bottom: var(--el-spacing-base);
opacity: 0.5;
filter: grayscale(100%);
transition: all 0.3s ease;
}
.empty-state:hover .empty-icon {
opacity: 0.8;
filter: grayscale(0%);
transform: scale(1.05);
}
.empty-message {
font-size: var(--el-font-size-large);
font-weight: 600;
color: var(--el-text-secondary);
margin: 0 0 var(--el-spacing-small) 0;
}
.empty-submessage {
font-size: var(--el-font-size-base);
color: var(--el-text-placeholder);
margin: 0;
}
/* 编辑表单样式 */
.edit-form {
margin-top: var(--el-spacing-base);
border-top: 2px solid var(--el-border-lighter);
padding-top: var(--el-spacing-large);
animation: fadeIn 0.3s ease;
}
.edit-form h4 {
margin: 0 0 var(--el-spacing-base) 0;
font-size: var(--el-font-size-base);
font-weight: 600;
color: var(--el-text-primary);
}
/* 服务项目样式 */
.services-container {
margin-bottom: var(--el-spacing-small);
}
.service-item {
display: flex;
align-items: center;
gap: var(--el-spacing-small);
margin-bottom: var(--el-spacing-small);
}
.service-item input {
flex: 1;
}
/* 复选框标签样式 */
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
margin-bottom: var(--el-spacing-small);
}
.checkbox-text {
margin-left: var(--el-spacing-small);
color: var(--el-text-primary);
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.packages-grid {
grid-template-columns: 1fr;
gap: var(--el-spacing-base);
}
.package-header {
flex-direction: column;
align-items: flex-start;
}
.package-status {
margin-top: var(--el-spacing-small);
align-self: flex-start;
}
.form-row {
flex-direction: column;
}
.form-row .form-group {
width: 100%;
}
.package-actions {
flex-direction: column;
gap: var(--el-spacing-base);
}
.package-actions button {
width: 100%;
}
.package-form-container,
.packages-list {
padding: var(--el-spacing-base);
}
.package-card {
padding: var(--el-spacing-base);
}
}
/* 表单样式增强 */
.form-row {
display: flex;
gap: 20px;
margin-bottom: var(--el-spacing-large);
}
.form-row .form-group {
flex: 1;
margin-bottom: 0;
}
/* 按钮悬停效果增强 */
.btn {
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.btn:hover::before {
width: 300px;
height: 300px;
}
.btn span {
position: relative;
z-index: 1;
}
+124
View File
@@ -0,0 +1,124 @@
<?php
// 测试数据库连接和表创建功能
require_once 'config.php';
// 记录日志的函数
function log_test_message($message, $type = 'info') {
$log_file = 'test_db.log';
$timestamp = date('Y-m-d H:i:s');
$log_entry = "[$timestamp] [$type] $message\n";
file_put_contents($log_file, $log_entry, FILE_APPEND);
echo $log_entry;
}
echo "=== 测试数据库连接和表创建功能 ===\n";
// 测试数据库连接
try {
$pdo = new PDO("mysql:host=$host;dbname=$database;charset=utf8mb4", $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
log_test_message("数据库连接成功");
} catch(PDOException $e) {
log_test_message("数据库连接失败: " . $e->getMessage(), 'error');
exit(1);
}
// 测试创建表
try {
$sql = "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 '车型',
wash_type VARCHAR(100) COMMENT '洗车类型',
wash_packages VARCHAR(255) COMMENT '洗车套餐',
wash_services TEXT COMMENT '洗车服务',
additional_services TEXT COMMENT '额外服务',
need_wax VARCHAR(10) COMMENT '是否需要打蜡',
wax_type VARCHAR(50) COMMENT '蜡的类型',
cleaning_needs TEXT COMMENT '清洁需求',
interior_cleaning VARCHAR(10) COMMENT '是否需要内饰清洁',
exterior_cleaning VARCHAR(10) COMMENT '是否需要外观清洁',
vip_member VARCHAR(10) COMMENT '是否为VIP会员',
vip_number VARCHAR(50) COMMENT 'VIP卡号',
payment_method VARCHAR(50) COMMENT '支付方式',
special_requests TEXT COMMENT '特殊要求',
technician VARCHAR(50) COMMENT '指定技师',
status VARCHAR(20) DEFAULT 'pending' COMMENT '状态',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
INDEX idx_rid (rid),
INDEX idx_license_plate (license_plate),
INDEX idx_date (date),
INDEX idx_mobile (mobile)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
$pdo->exec($sql);
log_test_message("表创建成功");
} catch(PDOException $e) {
log_test_message("表创建失败: " . $e->getMessage(), 'error');
exit(1);
}
// 测试插入数据
try {
$sql = "INSERT INTO wps_form_submissions (
rid, form_id, form_title, creator_id, create_time, update_time,
mobile, name, license_plate, date, time_slot, car_type
) VALUES (
:rid, :form_id, :form_title, :creator_id, :create_time, :update_time,
:mobile, :name, :license_plate, :date, :time_slot, :car_type
)";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':rid', 'test_rid_' . time());
$stmt->bindValue(':form_id', 'test_form_id');
$stmt->bindValue(':form_title', '测试表单');
$stmt->bindValue(':creator_id', 'test_creator_id');
$stmt->bindValue(':create_time', date('Y-m-d H:i:s'));
$stmt->bindValue(':update_time', date('Y-m-d H:i:s'));
$stmt->bindValue(':mobile', '13800138000');
$stmt->bindValue(':name', '测试用户');
$stmt->bindValue(':license_plate', '京A12345');
$stmt->bindValue(':date', date('Y-m-d'));
$stmt->bindValue(':time_slot', '14:00-15:00');
$stmt->bindValue(':car_type', '轿车');
$stmt->execute();
$insert_id = $pdo->lastInsertId();
log_test_message("测试数据插入成功,ID: " . $insert_id);
} catch(PDOException $e) {
log_test_message("测试数据插入失败: " . $e->getMessage(), 'error');
exit(1);
}
// 测试查询数据
try {
$sql = "SELECT * FROM wps_form_submissions ORDER BY id DESC LIMIT 1";
$stmt = $pdo->query($sql);
$row = $stmt->fetch();
if ($row) {
log_test_message("测试数据查询成功");
log_test_message("查询结果: " . print_r($row, true));
} else {
log_test_message("测试数据查询失败: 没有找到数据", 'error');
}
} catch(PDOException $e) {
log_test_message("测试数据查询失败: " . $e->getMessage(), 'error');
exit(1);
}
echo "\n=== 所有测试完成 ===\n";
+263
View File
@@ -0,0 +1,263 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WPS表单Webhook测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.container {
background-color: #f5f5f5;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
.test-section {
margin: 20px 0;
padding: 15px;
background-color: white;
border-radius: 4px;
border: 1px solid #ddd;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: #45a049;
}
.response {
margin-top: 10px;
padding: 10px;
background-color: #f0f0f0;
border-radius: 4px;
white-space: pre-wrap;
font-family: monospace;
}
.log-section {
margin-top: 30px;
}
#log-content {
height: 300px;
overflow-y: auto;
background-color: #2d2d2d;
color: #f0f0f0;
padding: 10px;
border-radius: 4px;
font-family: monospace;
}
</style>
</head>
<body>
<div class="container">
<h1>WPS表单Webhook测试工具</h1>
<div class="test-section">
<h2>1. 验证请求测试</h2>
<p>模拟WPS表单的验证请求,Webhook应返回绑定码</p>
<button onclick="testVerification()">发送验证请求</button>
<div class="response" id="verification-response"></div>
</div>
<div class="test-section">
<h2>2. 表单数据测试</h2>
<p>模拟WPS表单发送数据,Webhook应接收并处理数据</p>
<button onclick="testFormData()">发送表单数据</button>
<div class="response" id="form-data-response"></div>
</div>
<div class="log-section">
<h2>3. Webhook日志</h2>
<button onclick="loadLog()">加载日志</button>
<button onclick="clearLog()">清空日志</button>
<div id="log-content"></div>
</div>
</div>
<script>
// Webhook URL
const webhookUrl = 'http://carwash.com/webhook.php';
// 发送验证请求
async function testVerification() {
const responseDiv = document.getElementById('verification-response');
responseDiv.innerHTML = '发送中...';
try {
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: '' // 空请求体,模拟验证请求
});
const data = await response.json();
responseDiv.innerHTML = '响应: ' + JSON.stringify(data, null, 2);
} catch (error) {
responseDiv.innerHTML = '错误: ' + error.message;
}
}
// 发送表单数据
async function testFormData() {
const responseDiv = document.getElementById('form-data-response');
responseDiv.innerHTML = '发送中...';
// 模拟WPS实际表单数据格式
const formData = {
"rid": "lAFP6N1Tap",
"formId": "20251205123619412736007",
"formTitle": "张老师撸车(私家车库)预约撸车表单",
"aid": "20251205131620432762589",
"eventTs": Date.now(),
"messageTs": Date.now(),
"creatorId": "344573037",
"creatorName": "吴展鹏",
"event": "create_answer",
"version": 2,
"answerContents": [
{
"qid": "eo513g",
"type": "input",
"title": "怎么称呼您",
"value": "吴展鹏"
},
{
"qid": "xKFUcp",
"type": "input",
"title": "车型",
"value": "比亚迪宋PRODMi"
},
{
"qid": "NFJDDT",
"type": "input",
"title": "车牌号",
"value": "新AF10365-普通输入"
},
{
"qid": "t2u2i4",
"type": "licensePlate",
"title": "车牌号",
"value": "新AF10365-车牌专用"
},
{
"qid": "7jgfmh",
"type": "telphone",
"title": "请输入手机号",
"value": "18699627661"
},
{
"qid": "3j6opi",
"type": "select",
"title": "是否有车衣",
"value": ["否,无车衣"]
},
{
"qid": "ej48lk",
"type": "select",
"title": "有无自己撸车习惯",
"value": ["有"]
},
{
"qid": "1tnljp",
"type": "select",
"title": "撸车经验",
"value": ["我是老司机啦"]
},
{
"qid": "x02g35",
"type": "select",
"title": "洗车频率",
"value": ["每2周一次(每月1-2次)"]
},
{
"qid": "54dbo7",
"type": "select",
"title": "请选择年龄段",
"value": ["26~30"]
},
{
"qid": "rwxgkc",
"type": "autoNum",
"title": "自动编号",
"value": "zlslc202512002"
},
{
"qid": "r3ft9n",
"type": "input",
"title": "备注内容",
"value": "谢谢"
}
]
};
try {
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
const data = await response.json();
responseDiv.innerHTML = '响应: ' + JSON.stringify(data, null, 2);
} catch (error) {
responseDiv.innerHTML = '错误: ' + error.message;
}
}
// 加载日志
async function loadLog() {
const logContent = document.getElementById('log-content');
logContent.innerHTML = '加载中...';
try {
const response = await fetch('/log/wps_form_webhook.log');
if (response.ok) {
const text = await response.text();
logContent.innerHTML = text;
} else {
logContent.innerHTML = '无法加载日志文件: ' + response.statusText;
}
} catch (error) {
logContent.innerHTML = '错误: ' + error.message;
}
}
// 清空日志
async function clearLog() {
const logContent = document.getElementById('log-content');
try {
const response = await fetch('webhook.php?action=clear_log', {
method: 'POST'
});
if (response.ok) {
logContent.innerHTML = '日志已清空';
} else {
logContent.innerHTML = '清空日志失败';
}
} catch (error) {
logContent.innerHTML = '错误: ' + error.message;
}
}
</script>
</body>
</html>
+8 -3
View File
@@ -141,16 +141,17 @@ 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">
</head>
<body>
<div class="container">
<header class="header">
<h1>🚗 洗车预约系统 - VIP管理</h1>
<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>
@@ -921,7 +922,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 = '';
+316
View File
@@ -0,0 +1,316 @@
<?php
// WPS表单Webhook接收端点
// 设置响应头
header('Content-Type: application/json; charset=utf-8');
// 绑定码(根据WPS表单要求提供)
$bind_code = '20251205123619412736007';
// 日志文件路径
$log_file = 'log/wps_form_webhook.log';
// 加载数据库配置
require_once 'config.php';
// 数据库连接函数
function get_db_connection() {
global $host, $username, $password, $database;
try {
$pdo = new PDO("mysql:host=$host;dbname=$database;charset=utf8mb4", $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
return $pdo;
} catch(PDOException $e) {
log_message("数据库连接失败: " . $e->getMessage(), 'error');
return null;
}
}
// 字段对照表(根据WPS表单实际字段)
$field_mapping = array(
// 题目标题 => array(qid, type)
'日期' => array('0AmAeI', 'date'),
'单选项' => array('y0Rvqm', 'select'),
'图片和附件' => array('FOS5GT', 'file'),
'等级' => array('0wJSrH', 'star'),
'填写ID' => array('vwS7ci', 'input'),
'提交时间' => array('yjbQYS', 'date'),
'答题时间(秒)' => array('AA2kq8', 'input'),
'车牌号' => array('t2u2i4', 'licensePlate'), // 只使用这个车牌号字段(qid: t2u2i4, type: licensePlate
'车牌号2' => array('7nAOz7', 'input'), // 不使用
'车牌号(普通)' => array('NFJDDT', 'input'), // 不使用
'怎么称呼您' => array('eo513g', 'input'),
'车型' => array('xKFUcp', 'input'),
'请输入手机号' => array('7jgfmh', 'input'),
'是否有车衣' => array('3j6opi', 'select'),
'有无自己撸车习惯' => array('ej48lk', 'select'),
'撸车经验' => array('1tnljp', 'select'),
'洗车频率' => array('x02g35', 'select'),
'请选择年龄段' => array('54dbo7', 'select'),
'提交者' => array('T9OiCe', 'contact'),
'自动编号' => array('rwxgkc', 'autoNum'),
'备注内容' => array('r3ft9n', 'input')
);
// 反向映射:qid => array(title, type)
$qid_mapping = array();
foreach ($field_mapping as $title => $info) {
$qid = $info[0];
$type = $info[1];
$qid_mapping[$qid] = array('title' => $title, 'type' => $type);
}
// 记录日志的函数
function log_message($message, $type = 'info') {
global $log_file;
$timestamp = date('Y-m-d H:i:s');
$log_entry = "[$timestamp] [$type] $message\n";
// 检查日志目录是否存在,如果不存在则创建
$log_dir = dirname($log_file);
if (!is_dir($log_dir)) {
mkdir($log_dir, 0755, true);
}
file_put_contents($log_file, $log_entry, FILE_APPEND);
}
// 获取请求方法
$method = $_SERVER['REQUEST_METHOD'];
// 获取请求数据
$request_body = file_get_contents('php://input');
// 记录所有请求(仅记录方法)
log_message("收到请求 - 方法: $method");
// 处理验证请求
if ($method == 'POST') {
// 尝试解析请求体
$data = json_decode($request_body, true);
// 如果解析失败,可能是验证请求
if ($data === null) {
// 返回绑定码进行验证
log_message("返回绑定码进行验证");
echo json_encode(array('bind_code' => $bind_code));
exit;
}
// 检查是否是绑定验证请求
if (isset($data['event']) && $data['event'] === 'bind') {
log_message("收到绑定验证请求");
echo json_encode(array('bind_code' => $bind_code));
exit;
}
// 处理实际的表单数据
log_message("处理表单数据");
// 解析WPS表单数据结构
$form_data = array(
'rid' => isset($data['rid']) ? $data['rid'] : '',
'form_id' => isset($data['formId']) ? $data['formId'] : '',
'form_title' => isset($data['formTitle']) ? $data['formTitle'] : '',
'aid' => isset($data['aid']) ? $data['aid'] : '',
'event_ts' => isset($data['eventTs']) ? $data['eventTs'] : 0,
'message_ts' => isset($data['messageTs']) ? $data['messageTs'] : 0,
'creator_id' => isset($data['creatorId']) ? $data['creatorId'] : '',
'creator_name' => isset($data['creatorName']) ? $data['creatorName'] : '',
'event' => isset($data['event']) ? $data['event'] : '',
'version' => isset($data['version']) ? $data['version'] : 1,
'answers' => array()
);
// 解析表单字段
if (isset($data['answerContents']) && is_array($data['answerContents'])) {
foreach ($data['answerContents'] as $content) {
$qid = isset($content['qid']) ? $content['qid'] : '';
$type = isset($content['type']) ? $content['type'] : '';
$title = isset($content['title']) ? $content['title'] : '';
$value = isset($content['value']) ? $content['value'] : '';
// 将数组值转换为字符串,便于处理
if (is_array($value)) {
$value = implode(', ', $value);
}
$field = array(
'qid' => $qid,
'type' => $type,
'title' => $title,
'value' => $value
);
$form_data['answers'][] = $field;
// 按标题索引存储,便于直接访问
if (!empty($title)) {
$form_data['answers_by_title'][$title] = $value;
}
// 按qid索引存储,便于精确访问
if (!empty($qid)) {
$form_data['answers_by_qid'][$qid] = $value;
}
// 使用字段对照表检查并标准化字段
if (!empty($qid) && isset($qid_mapping[$qid])) {
$standard_title = $qid_mapping[$qid]['title'];
$standard_type = $qid_mapping[$qid]['type'];
// 存储标准化字段
$form_data['standardized_answers'][$standard_title] = array(
'qid' => $qid,
'type' => $standard_type,
'title' => $title, // 保留原始标题
'value' => $value,
'standard_title' => $standard_title
);
}
}
}
// 在这里可以添加数据处理逻辑,例如:
// 1. 将数据保存到数据库
// 2. 发送通知
// 3. 其他业务逻辑
// 示例:访问特定字段
// 通过标题访问
if (isset($form_data['answers_by_title']['怎么称呼您'])) {
$name = $form_data['answers_by_title']['怎么称呼您'];
log_message("客户姓名: $name");
}
// 注意:由于有两个"车牌号"字段,通过标题访问可能获取到任意一个
// 建议使用qid或标准化字段访问
if (isset($form_data['answers_by_title']['车牌号'])) {
$license_plate = $form_data['answers_by_title']['车牌号'];
log_message("车牌号(通过标题): $license_plate");
}
// 通过qid精确访问特定的车牌号字段
if (isset($form_data['answers_by_qid']['t2u2i4'])) {
$license_plate_standard = $form_data['answers_by_qid']['t2u2i4'];
log_message("车牌号(通过qid t2u2i4 - 标准): $license_plate_standard");
}
// 通过标准化字段访问
if (isset($form_data['standardized_answers']['怎么称呼您'])) {
$name_standard = $form_data['standardized_answers']['怎么称呼您']['value'];
log_message("客户姓名(标准化): $name_standard");
}
if (isset($form_data['standardized_answers']['车型'])) {
$car_model = $form_data['standardized_answers']['车型']['value'];
log_message("车型: $car_model");
}
if (isset($form_data['standardized_answers']['请输入手机号'])) {
$phone = $form_data['standardized_answers']['请输入手机号']['value'];
log_message("手机号: $phone");
}
// 返回成功响应
$response = array(
'code' => 200,
'message' => '数据接收成功',
'received_at' => date('Y-m-d H:i:s'),
'processed_fields' => count($form_data['answers'])
);
// 将数据存储到数据库
store_form_data_to_db($form_data);
log_message("返回成功响应");
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请求'));
}
log_message("请求处理完成\n" . str_repeat('-', 50) . "\n");
// 将表单数据存储到数据库
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;
}
// 准备SQL语句(只包含实际WPS表单存在的字段)
$sql = "INSERT INTO wps_form_submissions (
rid, form_id, form_title, creator_id, create_time, update_time,
mobile, name, license_plate, date, time_slot, car_type,
has_car_coat, car_wash_habit, car_wash_experience, wash_frequency,
age_group, remarks, auto_number, status, updated_at
) VALUES (
:rid, :form_id, :form_title, :creator_id, :create_time, :update_time,
:mobile, :name, :license_plate, :date, :time_slot, :car_type,
:has_car_coat, :car_wash_habit, :car_wash_experience, :wash_frequency,
:age_group, :remarks, :auto_number, :status, :updated_at
)";
$stmt = $pdo->prepare($sql);
// 绑定参数
$stmt->bindValue(':rid', $form_data['rid']);
$stmt->bindValue(':form_id', $form_data['form_id']);
$stmt->bindValue(':form_title', $form_data['form_title']);
$stmt->bindValue(':creator_id', $form_data['creator_id']);
$stmt->bindValue(':create_time', date('Y-m-d H:i:s'));
$stmt->bindValue(':update_time', date('Y-m-d H:i:s'));
$stmt->bindValue(':updated_at', date('Y-m-d H:i:s'));
// 绑定表单字段值(只绑定实际存在的字段)
$stmt->bindValue(':mobile', isset($form_data['answers_by_title']['请输入手机号']) ? $form_data['answers_by_title']['请输入手机号'] : '');
$stmt->bindValue(':name', isset($form_data['answers_by_title']['怎么称呼您']) ? $form_data['answers_by_title']['怎么称呼您'] : '');
// 处理车牌号字段(只使用qid为t2u2i4、type为licensePlate的车牌号字段)
$license_plate = '';
if (isset($form_data['answers_by_qid']['t2u2i4'])) {
$license_plate = $form_data['answers_by_qid']['t2u2i4'];
}
$stmt->bindValue(':license_plate', $license_plate);
// 日期处理
$date = isset($form_data['answers_by_title']['日期']) ? $form_data['answers_by_title']['日期'] : '';
$stmt->bindValue(':date', $date ? date('Y-m-d', strtotime($date)) : null);
// 绑定实际存在的其他字段
$stmt->bindValue(':time_slot', isset($form_data['answers_by_title']['单选项']) ? $form_data['answers_by_title']['单选项'] : '');
$stmt->bindValue(':car_type', isset($form_data['answers_by_title']['车型']) ? $form_data['answers_by_title']['车型'] : '');
$stmt->bindValue(':has_car_coat', isset($form_data['answers_by_title']['是否有车衣']) ? $form_data['answers_by_title']['是否有车衣'] : '');
$stmt->bindValue(':car_wash_habit', isset($form_data['answers_by_title']['有无自己撸车习惯']) ? $form_data['answers_by_title']['有无自己撸车习惯'] : '');
$stmt->bindValue(':car_wash_experience', isset($form_data['answers_by_title']['撸车经验']) ? $form_data['answers_by_title']['撸车经验'] : '');
$stmt->bindValue(':wash_frequency', isset($form_data['answers_by_title']['洗车频率']) ? $form_data['answers_by_title']['洗车频率'] : '');
$stmt->bindValue(':age_group', isset($form_data['answers_by_title']['请选择年龄段']) ? $form_data['answers_by_title']['请选择年龄段'] : '');
$stmt->bindValue(':remarks', isset($form_data['answers_by_title']['备注内容']) ? $form_data['answers_by_title']['备注内容'] : '');
$stmt->bindValue(':auto_number', isset($form_data['answers_by_title']['自动编号']) ? $form_data['answers_by_title']['自动编号'] : '');
$stmt->bindValue(':status', 'pending');
// 执行SQL
$stmt->execute();
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;
}
}
?>