27 KiB
27 KiB
预约系统前端对接文档
📌 文档说明
版本:v2.0
更新日期:2025-12-06
适用范围:预约房间功能前端开发
后端接口版本:预约时段优化版
📋 目录
业务流程概述
用户预约完整流程
┌─────────────────┐
│ 进入预约模块 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 【房间展示页】 │
│ 1. 选择日期 │
│ 2. 查看房间列表 │
│ 3. 查看时段状态 │
└────────┬────────┘
│ 点击【预约】
▼
┌─────────────────┐
│ 【预约页】 │
│ 1. 选择时段 │
│ 2. 填写人数 │
│ 3. 设置鸽子费 │
│ 4. 填写其他信息 │
└────────┬────────┘
│ 点击【提交】
▼
┌─────────────────┐
│ 创建预约 │
│ - 成功:跳转列表 │
│ - 失败:显示错误 │
└─────────────────┘
核心概念
时段定义
| 时段类型 | 时段名称 | 时间范围 | 代码值 |
|---|---|---|---|
| 凌晨 | Dawn | 00:00-05:59 | 0 |
| 上午 | Morning | 06:00-11:59 | 1 |
| 下午 | Afternoon | 12:00-17:59 | 2 |
| 晚上 | Evening | 18:00-23:59 | 3 |
时段状态
| 状态值 | 含义 | 展示方式 | 可否预约 |
|---|---|---|---|
| available | 可预约 | 绿色✓ | 是 |
| reserved | 已预约 | 红色✗ | 否 |
| unavailable | 不可用 | 灰色🚫 | 否 |
| using | 使用中 | 蓝色🔵 | 否 |
页面结构
页面1:房间展示页
页面元素:
┌──────────────────────────────────────┐
│ 预约房间 │
├──────────────────────────────────────┤
│ [ 今天 ] [ 明天 ] [ 12/08 ] ... │ ← 日期选择器
├──────────────────────────────────────┤
│ [ ] 只显示当前时段可用房间 │ ← 筛选开关
├──────────────────────────────────────┤
│ ┌────────────────────────────────┐ │
│ │ 101包间 [豪华包间] │ │
│ │ 📷 [房间图片] │ │
│ │ 💰 标准价:80元/时段 │ │
│ │ 会员价:60元/时段 │ │
│ │ 时段状态: │ │
│ │ 凌晨✓ 上午✗ 下午✓ 晚上🔵 │ │
│ │ [ 预约 ] 按钮 │ │
│ └────────────────────────────────┘ │
│ [更多房间...] │
└──────────────────────────────────────┘
页面2:预约页
页面元素:
┌──────────────────────────────────────┐
│ 创建预约 │
├──────────────────────────────────────┤
│ 预约日期:2025年12月06日 (只读) │
│ 房间号:101包间 (豪华包间) (只读) │
├──────────────────────────────────────┤
│ 预约时间: │
│ [ 上午 ▼ ] │ ← 下拉选择(可预约时段)
│ 时间范围:06:00-11:59 │
│ 价格:标准价80元 | 会员价60元 │
├──────────────────────────────────────┤
│ 最晚到店时间: │
│ [ 10:30 ] (时间选择器) │
├──────────────────────────────────────┤
│ 人数: │
│ ( ) 2人 ( ) 3人 ( ) 4人 │
│ ( ) 无需组局 │
├──────────────────────────────────────┤
│ 鸽子费: │
│ ( ) 0元 ( ) 10元 ( ) 20元 │
│ (•) 自定义:[ 30 ] 元 │
├──────────────────────────────────────┤
│ 组局名称:[ 周末开黑 ] │
│ 玩法类型:[ 德州扑克 ] │
│ ... 其他信息 ... │
├──────────────────────────────────────┤
│ [ 提交预约 ] 按钮 │
└──────────────────────────────────────┘
接口调用流程
时序图
前端页面 → 后端API
│
│ 1. 进入房间展示页
├────────────────→ GET /api/SQ/GetAvailableDates
│←─────────────── 返回:未来7天日期列表
│
│ 2. 获取房间列表(默认今天)
├────────────────→ GET /api/SQ/GetRoomListWithSlotsNew?date=xxx
│←─────────────── 返回:房间列表及4个时段状态
│
│ 3. 用户切换日期
├────────────────→ GET /api/SQ/GetRoomListWithSlotsNew?date=xxx
│←─────────────── 返回:新日期的房间列表
│
│ 4. 用户点击【预约】按钮
│ 携带:roomId, date, availableSlots
│ 跳转到预约页
│
│ 5. (可选)用户选择时段后实时校验
├────────────────→ POST /api/SQ/ValidateReservationBySlot
│←─────────────── 返回:是否可以预约
│
│ 6. 用户点击【提交预约】
├────────────────→ POST /api/SQ/AddSQReservationBySlot
│←─────────────── 返回:预约结果(成功/失败)
│
│ 7. 成功后跳转到"我的预约"列表
接口详细说明
接口1:获取可选日期列表
基本信息
- 接口路径:
GET /api/SQ/GetAvailableDates - 调用时机:房间展示页初始化时调用1次
- 是否需要登录:否
请求参数
无参数
返回数据
interface Response {
code: number; // 0=成功
msg: string; // 消息
data: DateItem[]; // 日期列表
}
interface DateItem {
date: number; // Unix时间戳(秒级)
dateText: string; // 文本:今天/明天/后天/日期
dateDisplay: string; // 展示文本:12月06日 周五
}
返回示例
{
"code": 0,
"msg": "ok",
"data": [
{
"date": 1733443200,
"dateText": "今天",
"dateDisplay": "周五"
},
{
"date": 1733529600,
"dateText": "明天",
"dateDisplay": "周六"
},
{
"date": 1733616000,
"dateText": "后天",
"dateDisplay": "周日"
},
{
"date": 1733702400,
"dateText": "12月09日",
"dateDisplay": "周一"
}
// ... 共7条(今天+未来6天)
]
}
接口2:获取房间列表及时段状态
基本信息
- 接口路径:
GET /api/SQ/GetRoomListWithSlotsNew - 调用时机:初始化、切换日期时调用
- 是否需要登录:否
请求参数
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
| date | number | 是 | Unix时间戳(秒级) | 1733443200 |
| showOnlyAvailable | boolean | 否 | 是否只显示当前时段可用房间 | false |
| currentTimeSlot | number | 否 | 当前时段类型(0-3),配合showOnlyAvailable | 1 |
返回数据
interface Response {
code: number;
msg: string;
data: RoomItem[];
}
interface RoomItem {
id: number; // 房间ID
name: string; // 房间名称
room_type_name: string; // 房间类型
image_url: string; // 房间图片
capacity: number; // 容纳人数
description: string; // 房间描述
standard_price_desc: string; // 标准价格说明
member_price_desc: string; // 会员价格说明
time_slots: TimeSlot[]; // 4个时段信息
status: string; // 房间状态
is_available: boolean; // 是否可用
can_reserve: boolean; // 是否可预约
}
interface TimeSlot {
slot_type: number; // 时段类型:0-3
slot_name: string; // 时段名称:凌晨/上午/下午/晚上
status: string; // 状态:available/reserved/unavailable/using
standard_price: number; // 标准价格
member_price: number; // 会员价格
price_desc_standard: string; // 标准价格说明
price_desc_member: string; // 会员价格说明
}
返回示例
{
"code": 0,
"msg": "ok",
"data": [
{
"id": 1,
"name": "101包间",
"room_type_name": "豪华包间",
"image_url": "https://example.com/room1.jpg",
"capacity": 8,
"description": "宽敞舒适的豪华包间",
"standard_price_desc": "80元/时段",
"member_price_desc": "60元/时段",
"time_slots": [
{
"slot_type": 0,
"slot_name": "凌晨",
"status": "available",
"standard_price": 60.00,
"member_price": 50.00,
"price_desc_standard": "60元/时段",
"price_desc_member": "50元/时段"
},
{
"slot_type": 1,
"slot_name": "上午",
"status": "reserved",
"standard_price": 80.00,
"member_price": 60.00,
"price_desc_standard": "80元/时段",
"price_desc_member": "60元/时段"
},
{
"slot_type": 2,
"slot_name": "下午",
"status": "available",
"standard_price": 80.00,
"member_price": 60.00,
"price_desc_standard": "80元/时段",
"price_desc_member": "60元/时段"
},
{
"slot_type": 3,
"slot_name": "晚上",
"status": "using",
"standard_price": 100.00,
"member_price": 80.00,
"price_desc_standard": "100元/时段",
"price_desc_member": "80元/时段"
}
],
"status": "using",
"is_available": false,
"can_reserve": true
}
]
}
接口3:校验是否可创建预约(可选)
基本信息
- 接口路径:
POST /api/SQ/ValidateReservationBySlot - 调用时机:用户选择时段后实时校验(可选)
- 是否需要登录:是(需要Token)
请求参数
与"创建预约"接口相同,但不会真正创建预约
返回数据
interface Response {
code: number; // 0=可以创建,500=不可以
msg: string; // 原因说明
data: {
canCreate: boolean; // 是否可以创建
reason: string; // 不能创建的原因
}
}
返回示例
// 可以创建
{
"code": 0,
"msg": "",
"data": {
"canCreate": true,
"reason": ""
}
}
// 不能创建
{
"code": 500,
"msg": "您有其它预约时间冲突",
"data": {
"canCreate": false,
"reason": "您有其它预约时间冲突"
}
}
前端使用
// 用户选择时段时实时校验
async function onSlotChange(timeSlotType) {
const data = {
room_id: this.roomId,
date: this.date,
time_slot_type: timeSlotType,
player_count: 4,
// ... 其他字段
};
try {
const response = await axios.post('/api/SQ/ValidateReservationBySlot', data, {
headers: { Authorization: `Bearer ${this.token}` }
});
if (response.data.code === 0) {
// 可以预约,显示价格等信息
this.showPriceInfo(timeSlotType);
} else {
// 不能预约,显示原因
this.$message.warning(response.data.msg);
}
} catch (error) {
console.error('校验失败', error);
}
}
接口4:创建预约(核心)
基本信息
- 接口路径:
POST /api/SQ/AddSQReservationBySlot - 调用时机:用户点击【提交预约】按钮
- 是否需要登录:是(需要Token)
请求参数
interface CreateReservationRequest {
room_id: number; // 必填,房间ID
date: number; // 必填,预约日期(Unix时间戳-秒)
time_slot_type: number; // 必填,时段类型:0-3
latest_arrival_time?: number; // 可选,最晚到店时间(Unix时间戳-秒)
is_solo_mode: boolean; // 必填,是否无需组局
player_count: number; // 必填,人数(无需组局时固定为1)
deposit_fee: number; // 必填,鸽子费(0-50整数)
title: string; // 必填,组局名称
game_type: string; // 必填,玩法类型
game_rule?: string; // 可选,游戏规则
extra_info?: string; // 可选,其他补充
is_smoking: number; // 可选,是否禁烟:0=不限,1=禁烟,2=不禁烟
gender_limit: number; // 可选,性别限制:0=不限,1=男,2=女
credit_limit?: number; // 可选,最低信誉分
min_age?: number; // 可选,最小年龄
max_age?: number; // 可选,最大年龄,0=不限
important_data?: string; // 可选,重要数据(支付相关JSON)
}
请求示例
{
"room_id": 1,
"date": 1733443200,
"time_slot_type": 1,
"latest_arrival_time": 1733461200,
"is_solo_mode": false,
"player_count": 4,
"deposit_fee": 20,
"title": "周末开黑",
"game_type": "德州扑克",
"game_rule": "经典玩法",
"extra_info": "欢迎新手",
"is_smoking": 0,
"gender_limit": 0,
"credit_limit": 3.5,
"min_age": 18,
"max_age": 0,
"important_data": "{\"paymentId\":\"ORDER123456\"}"
}
返回数据
interface Response {
code: number; // 0=成功,其他=失败
msg: string; // 消息
data?: {
reservation_id: number; // 预约ID
start_time: string; // 开始时间
end_time: string; // 结束时间
actual_price: number; // 实际价格
}
}
返回示例(成功)
{
"code": 0,
"msg": "预约成功",
"data": {
"reservation_id": 123,
"start_time": "2025-12-06 06:00:00",
"end_time": "2025-12-06 11:59:59",
"actual_price": 80.00
}
}
返回示例(失败)
// 时间冲突
{
"code": 402,
"msg": "您有其它预约时间冲突,无法创建该预约!",
"data": null
}
// 房间已被预约
{
"code": 500,
"msg": "该时间段房间已被预约",
"data": null
}
// 鸽子费超限
{
"code": 500,
"msg": "鸽子费必须在0-50元之间",
"data": null
}
前端使用
async function submitReservation() {
// 1. 构建请求数据
const data = {
room_id: this.roomId,
date: this.date,
time_slot_type: this.selectedSlot,
latest_arrival_time: this.convertToTimestamp(this.arrivalTime),
is_solo_mode: this.playerCount === 1,
player_count: this.playerCount,
deposit_fee: this.depositFee,
title: this.title,
game_type: this.gameType,
game_rule: this.gameRule,
extra_info: this.extraInfo,
is_smoking: this.isSmoking,
gender_limit: this.genderLimit,
credit_limit: this.creditLimit,
min_age: this.minAge,
max_age: this.maxAge,
important_data: this.getPaymentData()
};
// 2. 参数校验
if (!this.validate(data)) {
return;
}
// 3. 发送请求
try {
const response = await axios.post('/api/SQ/AddSQReservationBySlot', data, {
headers: { Authorization: `Bearer ${this.token}` }
});
if (response.data.code === 0) {
// 成功
this.$message.success('预约成功!');
// 可以显示预约详情
console.log('预约ID:', response.data.data.reservation_id);
console.log('时间:', response.data.data.start_time, '-', response.data.data.end_time);
console.log('价格:', response.data.data.actual_price);
// 跳转到我的预约列表
this.$router.push('/my-reservations');
} else {
// 失败
this.$message.error(response.data.msg);
}
} catch (error) {
this.$message.error('预约失败,请重试');
console.error(error);
}
}
// 参数校验
function validate(data) {
if (!data.title) {
this.$message.warning('请输入组局名称');
return false;
}
if (!data.game_type) {
this.$message.warning('请选择玩法类型');
return false;
}
if (data.deposit_fee < 0 || data.deposit_fee > 50) {
this.$message.warning('鸽子费必须在0-50元之间');
return false;
}
if (!Number.isInteger(data.deposit_fee)) {
this.$message.warning('鸽子费必须是整数');
return false;
}
return true;
}
数据结构定义
TypeScript类型定义
// ========== 基础类型 ==========
/** 时段类型枚举 */
enum TimeSlotType {
Dawn = 0, // 凌晨
Morning = 1, // 上午
Afternoon = 2, // 下午
Evening = 3 // 晚上
}
/** 时段状态枚举 */
enum TimeSlotStatus {
Available = 'available', // 可预约
Reserved = 'reserved', // 已预约
Unavailable = 'unavailable', // 不可用
Using = 'using' // 使用中
}
// ========== 日期相关 ==========
/** 日期项 */
interface DateItem {
date: number; // Unix时间戳(秒)
dateText: string; // 文本显示
dateDisplay: string; // 完整显示
}
// ========== 房间相关 ==========
/** 时段信息 */
interface TimeSlot {
slot_type: TimeSlotType; // 时段类型
slot_name: string; // 时段名称
status: TimeSlotStatus; // 时段状态
standard_price: number; // 标准价格
member_price: number; // 会员价格
price_desc_standard: string; // 标准价格说明
price_desc_member: string; // 会员价格说明
}
/** 房间信息 */
interface RoomItem {
id: number; // 房间ID
name: string; // 房间名称
room_type_name: string; // 房间类型
image_url: string; // 房间图片
capacity: number; // 容纳人数
description: string; // 房间描述
standard_price_desc: string; // 标准价格说明
member_price_desc: string; // 会员价格说明
time_slots: TimeSlot[]; // 时段列表
status: string; // 房间状态
is_available: boolean; // 是否可用
can_reserve: boolean; // 是否可预约
}
// ========== 预约相关 ==========
/** 创建预约请求 */
interface CreateReservationRequest {
room_id: number;
date: number;
time_slot_type: TimeSlotType;
latest_arrival_time?: number;
is_solo_mode: boolean;
player_count: number;
deposit_fee: number;
title: string;
game_type: string;
game_rule?: string;
extra_info?: string;
is_smoking: number;
gender_limit: number;
credit_limit?: number;
min_age?: number;
max_age?: number;
important_data?: string;
}
/** 创建预约响应 */
interface CreateReservationResponse {
reservation_id: number;
start_time: string;
end_time: string;
actual_price: number;
}
// ========== API响应 ==========
/** 通用API响应 */
interface ApiResponse<T = any> {
code: number;
msg: string;
data: T;
}
常见问题FAQ
Q1: 时间戳是秒级还是毫秒级?
A: 所有接口使用的都是秒级时间戳(Unix Timestamp),不是毫秒级。
// 正确:秒级
const timestamp = Math.floor(Date.now() / 1000);
// 错误:毫秒级
const timestamp = Date.now();
Q2: 如何判断当前是哪个时段?
A: 根据当前时间的小时数判断:
function getCurrentTimeSlot() {
const hour = new Date().getHours();
if (hour >= 0 && hour < 6) return 0; // 凌晨
if (hour >= 6 && hour < 12) return 1; // 上午
if (hour >= 12 && hour < 18) return 2; // 下午
return 3; // 晚上
}
Q3: 最晚到店时间如何处理?
A: 最晚到店时间必须在所选时段的时间范围内:
// 1. 获取时段的时间范围
const timeRanges = {
0: { min: '00:00', max: '05:59' },
1: { min: '06:00', max: '11:59' },
2: { min: '12:00', max: '17:59' },
3: { min: '18:00', max: '23:59' }
};
// 2. 限制时间选择器的范围
const range = timeRanges[selectedSlot];
<input type="time" :min="range.min" :max="range.max">
// 3. 转换为时间戳
const [hour, minute] = arrivalTime.split(':');
const d = new Date(date * 1000);
d.setHours(Number(hour), Number(minute), 0, 0);
const timestamp = Math.floor(d.getTime() / 1000);
Q4: "无需组局"模式如何处理?
A: 当选择"无需组局"时:
// 1. 人数固定为1
if (playerCount === 1) {
form.is_solo_mode = true;
form.player_count = 1;
}
// 2. 后端会拒绝其他人加入该预约
Q5: 鸽子费的限制是什么?
A: 鸽子费必须满足:
- 范围:0-50元
- 类型:整数(不能有小数)
// 校验
if (depositFee < 0 || depositFee > 50) {
alert('鸽子费必须在0-50元之间');
return false;
}
if (!Number.isInteger(depositFee)) {
alert('鸽子费必须是整数');
return false;
}
Q6: 如何处理时间冲突?
A: 后端会自动检测时间冲突,返回错误码402:
if (response.data.code === 402) {
alert('您有其它预约时间冲突,无法创建该预约!');
// 可以引导用户查看"我的预约"
}
Q7: Token如何传递?
A: 所有需要登录的接口都需要在请求头中携带Token:
// Axios示例
axios.post('/api/SQ/AddSQReservationBySlot', data, {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`
}
});
// 或配置全局拦截器
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
Q8: 如何处理已过时段?
A: 后端会自动过滤已过去的时段,前端不需要额外处理。如果某个时段已过去,它会在time_slots中显示为unavailable或不返回。
Q9: 房间图片加载失败怎么办?
A: 建议添加图片加载失败的占位图:
<img
:src="room.image_url"
:alt="room.name"
@error="handleImageError"
>
<script>
function handleImageError(e) {
e.target.src = '/default-room.png'; // 默认图片
}
</script>
Q10: 如何实现实时刷新房间状态?
A: 可以使用定时轮询或WebSocket:
// 方式1:定时轮询(简单)
let timer = null;
onMounted(() => {
loadRooms();
timer = setInterval(() => {
loadRooms();
}, 30000); // 每30秒刷新一次
});
onUnmounted(() => {
if (timer) clearInterval(timer);
});
// 方式2:WebSocket(实时性更好,需要后端支持)
// 连接WebSocket监听房间状态变化
调试技巧
1. 使用浏览器开发者工具
查看网络请求
F12 → Network → XHR
- 查看请求URL、参数、响应
- 检查状态码
- 查看响应时间
查看控制台日志
// 添加详细日志
console.log('请求参数:', data);
console.log('响应数据:', response.data);
console.error('错误信息:', error);
2. Postman测试接口
测试步骤
- 先调用登录接口获取Token
- 复制Token到环境变量
- 测试各个接口
- 保存常用请求到Collection
示例请求
GET http://localhost:5000/api/SQ/GetAvailableDates
GET http://localhost:5000/api/SQ/GetRoomListWithSlotsNew?date=1733443200
POST http://localhost:5000/api/SQ/AddSQReservationBySlot
Headers:
Authorization: Bearer eyJhbGc...
Body: { ... }
3. 常见错误排查
| 错误 | 可能原因 | 解决方法 |
|---|---|---|
| 401 Unauthorized | Token过期或无效 | 重新登录获取Token |
| 404 Not Found | 接口路径错误 | 检查URL是否正确 |
| 500 Internal Server Error | 服务器内部错误 | 查看后端日志 |
| CORS错误 | 跨域配置问题 | 联系后端配置CORS |
| 参数错误 | 参数格式不正确 | 检查参数类型和值 |
4. 开发环境代理配置
Vite配置示例
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
}
Vue CLI配置示例
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
}
附录
A. 错误码对照表
| 错误码 | 说明 | 处理建议 |
|---|---|---|
| 0 | 成功 | 继续业务流程 |
| 400 | 业务错误 | 显示错误信息 |
| 401 | 未授权 | 跳转到登录页 |
| 402 | 时间冲突 | 提示用户选择其他时段 |
| 403 | 权限不足 | 提示权限不足 |
| 404 | 资源不存在 | 提示资源不存在 |
| 500 | 系统错误 | 提示系统错误,稍后重试 |
B. 测试数据
// 测试用日期时间戳
const testDates = {
today: Math.floor(Date.now() / 1000),
tomorrow: Math.floor(Date.now() / 1000) + 86400,
nextWeek: Math.floor(Date.now() / 1000) + 604800
};
// 测试用房间ID
const testRoomIds = [1, 2, 3];
// 测试用时段类型
const testTimeSlots = [0, 1, 2, 3];
C. 联系方式
技术支持:
- 邮箱:jianweie@163.com
- 文档版本:v2.0
- 更新日期:2025-12-06
祝开发顺利! 🚀