17 KiB
17 KiB
预约时间自由选择 - 改动文档
1. 需求概述
1.1 当前问题
- 用户只能选择固定时段(凌晨0-6、上午6-12、下午12-18、晚上18-24)
- 无法灵活选择具体的开始和结束时间
- 跨时段预约显示不直观
1.2 改动目标
- 时段显示优化:如果预约时间为15:00-20:00,房间列表页应显示该房间"下午"和"晚上"都被预定
- 自由时间选择:创建预约界面允许用户自由选择开始时间和结束时间
2. 改动前分析
2.1 当前时段定义
时段类型 | 时间范围 | 对应值
--------|----------|-------
凌晨 | 00:00-06:00 | 0
上午 | 06:00-12:00 | 1
下午 | 12:00-18:00 | 2
晚上 | 18:00-24:00 | 3
2.2 当前预约创建流程
flowchart TD
A[用户选择日期] --> B[选择房间]
B --> C[显示可用时段]
C --> D{选择固定时段}
D -->|凌晨| E1[设置 00:00-06:00]
D -->|上午| E2[设置 06:00-12:00]
D -->|下午| E3[设置 12:00-18:00]
D -->|晚上| E4[设置 18:00-24:00]
E1 --> F[提交预约]
E2 --> F
E3 --> F
E4 --> F
F --> G[后端创建预约]
2.3 当前前端时间计算逻辑
文件: pages/appointment/appointment-page.vue
// 当前代码:根据时段类型计算固定时间
const calculateTimeFromSlot = () => {
const date = new Date(selectedDate.value);
let startHour, endHour;
switch (selectedTimeSlot.value) {
case 0: // 凌晨
startHour = 0;
endHour = 6;
break;
case 1: // 上午
startHour = 6;
endHour = 12;
break;
case 2: // 下午
startHour = 12;
endHour = 18;
break;
case 3: // 晚上
startHour = 18;
endHour = 24;
break;
}
// ... 设置开始和结束时间
};
2.4 当前房间时段状态显示
文件: pages/appointment/book-room-page.vue
┌─────────────────────────────────────────┐
│ 房间名称 │
├─────────────────────────────────────────┤
│ [凌晨] [上午] [下午] [晚上] │
│ 🟢 🟢 🟠 🟢 │
│ │
│ 🟢 可预约 🟠 已预约 ⚫ 不可用 🔵 使用中 │
└─────────────────────────────────────────┘
当前问题:预约15:00-20:00时,仅显示一个时段被占用
2.5 当前后端时段判断逻辑
文件: CoreCms.Net.Services/SQ/SQRoomsServices.cs
// 后端已有重叠判断逻辑(可复用)
bool isReserved = reservations?.Any(r =>
r.start_time < slotEnd && r.end_time > slotStart) ?? false;
分析:后端逻辑已支持跨时段检测,问题在于前端只能选择单一时段。
3. 改动后方案
3.1 新的预约创建流程
flowchart TD
A[用户选择日期] --> B[选择房间]
B --> C[显示房间详情]
C --> D[选择开始时间]
D --> E[选择结束时间]
E --> F{时间校验}
F -->|结束时间 > 开始时间| G[计算跨越时段]
F -->|无效| H[提示错误]
G --> I[检查时段冲突]
I -->|无冲突| J[提交预约]
I -->|有冲突| K[提示时段已被预约]
J --> L[后端创建预约]
L --> M[更新房间时段显示]
3.2 新的时间选择UI设计
改动前(固定时段选择):
┌─────────────────────────────────────────┐
│ 选择时段 │
├─────────────────────────────────────────┤
│ ○ 凌晨 (00:00-06:00) │
│ ○ 上午 (06:00-12:00) │
│ ● 下午 (12:00-18:00) ← 只能单选 │
│ ○ 晚上 (18:00-24:00) │
└─────────────────────────────────────────┘
改动后(自由时间选择):
┌─────────────────────────────────────────┐
│ 选择时间 │
├─────────────────────────────────────────┤
│ 开始时间: [15:00 ▼] ← 时间选择器 │
│ 结束时间: [20:00 ▼] ← 时间选择器 │
│ │
│ 预计时长: 5小时 │
│ 跨越时段: 下午、晚上 │
│ │
│ 💡 提示:结束时间必须晚于开始时间 │
└─────────────────────────────────────────┘
3.3 新的房间列表时段显示
改动前(15:00-20:00预约后):
┌─────────────────────────────────────────┐
│ [凌晨] [上午] [下午] [晚上] │
│ 🟢 🟢 🟠 🟢 ← 只显示下午 │
└─────────────────────────────────────────┘
改动后(15:00-20:00预约后):
┌─────────────────────────────────────────┐
│ [凌晨] [上午] [下午] [晚上] │
│ 🟢 🟢 🟠 🟠 ← 下午+晚上 │
└─────────────────────────────────────────┘
3.4 时段重叠判断逻辑
flowchart LR
subgraph 预约时间 15:00-20:00
P1[15:00] --> P2[20:00]
end
subgraph 时段判断
S1[凌晨 0-6] --> R1{重叠?}
S2[上午 6-12] --> R2{重叠?}
S3[下午 12-18] --> R3{重叠?}
S4[晚上 18-24] --> R4{重叠?}
end
R1 -->|否| N1[🟢]
R2 -->|否| N2[🟢]
R3 -->|是| Y3[🟠]
R4 -->|是| Y4[🟠]
重叠判断公式:
预约与时段重叠 = (预约开始时间 < 时段结束时间) AND (预约结束时间 > 时段开始时间)
示例:预约15:00-20:00
- 凌晨(0-6): 15 < 6 = false → 不重叠 🟢
- 上午(6-12): 15 < 12 = false → 不重叠 🟢
- 下午(12-18): 15 < 18 = true AND 20 > 12 = true → 重叠 🟠
- 晚上(18-24): 15 < 24 = true AND 20 > 18 = true → 重叠 🟠
4. 需要改动的文件清单
4.1 前端改动
| 序号 | 文件路径 | 改动内容 | 复杂度 |
|---|---|---|---|
| 1 | pages/appointment/appointment-page.vue |
将固定时段选择改为时间选择器 | ⭐⭐⭐ |
| 2 | pages/appointment/book-room-page.vue |
时段状态显示逻辑优化(可能无需改动,后端已支持) | ⭐ |
| 3 | components/com/page/reservation-item.vue |
预约详情显示时间格式调整 | ⭐ |
4.2 后端改动
| 序号 | 文件路径 | 改动内容 | 复杂度 |
|---|---|---|---|
| 1 | SQController.cs |
接收自定义开始/结束时间参数 | ⭐⭐ |
| 2 | SQRoomsServices.cs |
时段状态判断逻辑已支持,可能需微调 | ⭐ |
| 3 | SQReservationsServices.cs |
预约冲突检测逻辑验证 | ⭐ |
5. 详细改动说明
5.1 appointment-page.vue 改动
改动前代码
<template>
<!-- 时段选择(单选按钮) -->
<view class="time-slot-selector">
<view v-for="slot in timeSlotOptions" :key="slot.value"
:class="['slot-item', selectedTimeSlot === slot.value ? 'active' : '']"
@click="selectTimeSlot(slot.value)">
{{ slot.label }}
</view>
</view>
</template>
<script setup>
const selectedTimeSlot = ref(null);
const timeSlotOptions = ref([]);
const calculateTimeFromSlot = () => {
switch (selectedTimeSlot.value) {
case 0: startHour = 0; endHour = 6; break;
case 1: startHour = 6; endHour = 12; break;
case 2: startHour = 12; endHour = 18; break;
case 3: startHour = 18; endHour = 24; break;
}
};
</script>
改动后代码
<template>
<!-- 时间选择器 -->
<view class="time-picker-container">
<view class="time-picker-row">
<text class="label">开始时间</text>
<picker mode="time" :value="startTime" @change="onStartTimeChange">
<view class="picker-value">{{ startTime || '请选择' }}</view>
</picker>
</view>
<view class="time-picker-row">
<text class="label">结束时间</text>
<picker mode="time" :value="endTime" @change="onEndTimeChange">
<view class="picker-value">{{ endTime || '请选择' }}</view>
</picker>
</view>
<view class="time-info">
<text>预计时长: {{ calculateDuration() }}</text>
<text>跨越时段: {{ calculateCrossSlots() }}</text>
</view>
</view>
</template>
<script setup>
const startTime = ref('');
const endTime = ref('');
// 开始时间变更
const onStartTimeChange = (e) => {
startTime.value = e.detail.value;
validateTimeRange();
};
// 结束时间变更
const onEndTimeChange = (e) => {
endTime.value = e.detail.value;
validateTimeRange();
};
// 计算时长
const calculateDuration = () => {
if (!startTime.value || !endTime.value) return '-';
const start = parseTime(startTime.value);
const end = parseTime(endTime.value);
const hours = (end - start) / (1000 * 60 * 60);
return `${hours}小时`;
};
// 计算跨越的时段
const calculateCrossSlots = () => {
if (!startTime.value || !endTime.value) return '-';
const slots = [];
const startHour = parseInt(startTime.value.split(':')[0]);
const endHour = parseInt(endTime.value.split(':')[0]);
if (startHour < 6 || endHour > 0 && endHour <= 6) slots.push('凌晨');
if ((startHour < 12 && endHour > 6) || (startHour >= 6 && startHour < 12)) slots.push('上午');
if ((startHour < 18 && endHour > 12) || (startHour >= 12 && startHour < 18)) slots.push('下午');
if (endHour > 18 || startHour >= 18) slots.push('晚上');
return slots.join('、') || '-';
};
// 验证时间范围
const validateTimeRange = () => {
if (startTime.value && endTime.value) {
const start = parseTime(startTime.value);
const end = parseTime(endTime.value);
if (end <= start) {
uni.showToast({ title: '结束时间必须晚于开始时间', icon: 'none' });
return false;
}
}
return true;
};
// 构建预约数据(提交时)
const buildReservationData = () => {
const date = new Date(selectedDate.value);
const [startH, startM] = startTime.value.split(':').map(Number);
const [endH, endM] = endTime.value.split(':').map(Number);
const startDateTime = new Date(date);
startDateTime.setHours(startH, startM, 0, 0);
const endDateTime = new Date(date);
endDateTime.setHours(endH, endM, 0, 0);
return {
start_time: startDateTime.toISOString(),
end_time: endDateTime.toISOString(),
// ... 其他字段
};
};
</script>
5.2 后端 SQController.cs 改动
改动说明
后端接口已支持接收 start_time 和 end_time 参数,主要验证逻辑需确认:
// 添加预约时的冲突检测
[HttpPost("AddSQReservation")]
public async Task<IActionResult> AddSQReservation([FromBody] AddReservationRequest request)
{
// 验证时间有效性
if (request.end_time <= request.start_time)
{
return BadRequest("结束时间必须晚于开始时间");
}
// 检查时段冲突(已有逻辑)
var conflicts = await _reservationService.CheckTimeConflict(
request.room_id,
request.start_time,
request.end_time
);
if (conflicts.Any())
{
return BadRequest("所选时段已被预约");
}
// 创建预约...
}
5.3 后端时段显示逻辑验证
文件: SQRoomsServices.cs
当前逻辑已正确支持跨时段检测:
// 判断预约是否与时段重叠
bool isReserved = reservations?.Any(r =>
r.start_time < slotEnd && r.end_time > slotStart) ?? false;
验证场景:
- 预约 15:00-20:00
- 下午时段(12:00-18:00):
15:00 < 18:00 && 20:00 > 12:00= true ✅ - 晚上时段(18:00-24:00):
15:00 < 24:00 && 20:00 > 18:00= true ✅
结论:后端逻辑无需改动,已支持跨时段显示。
6. 数据流对比
6.1 改动前数据流
sequenceDiagram
participant U as 用户
participant F as 前端
participant B as 后端
participant DB as 数据库
U->>F: 选择日期
U->>F: 选择房间
F->>B: 获取房间时段状态
B->>DB: 查询已有预约
DB-->>B: 返回预约列表
B-->>F: 返回4个时段状态
F-->>U: 显示可选时段
U->>F: 选择"下午"时段
F->>F: calculateTimeFromSlot()
Note over F: 固定设置 12:00-18:00
F->>B: 提交预约(12:00-18:00)
B->>DB: 保存预约
DB-->>B: 保存成功
B-->>F: 返回成功
F-->>U: 预约成功
6.2 改动后数据流
sequenceDiagram
participant U as 用户
participant F as 前端
participant B as 后端
participant DB as 数据库
U->>F: 选择日期
U->>F: 选择房间
F->>B: 获取房间时段状态
B->>DB: 查询已有预约
DB-->>B: 返回预约列表
B-->>F: 返回4个时段状态
F-->>U: 显示时间选择器
U->>F: 选择开始时间 15:00
U->>F: 选择结束时间 20:00
F->>F: validateTimeRange()
F->>F: calculateCrossSlots()
Note over F: 显示"下午、晚上"
F->>B: 提交预约(15:00-20:00)
B->>B: 检查时段冲突
B->>DB: 保存预约
DB-->>B: 保存成功
B-->>F: 返回成功
F-->>U: 预约成功
7. 测试用例
7.1 时间选择测试
| 测试场景 | 开始时间 | 结束时间 | 预期结果 |
|---|---|---|---|
| 正常选择 | 15:00 | 20:00 | 成功,显示跨越"下午、晚上" |
| 同时段选择 | 14:00 | 17:00 | 成功,显示跨越"下午" |
| 跨三时段 | 10:00 | 20:00 | 成功,显示跨越"上午、下午、晚上" |
| 无效时间 | 20:00 | 15:00 | 失败,提示"结束时间必须晚于开始时间" |
| 相同时间 | 15:00 | 15:00 | 失败,提示"结束时间必须晚于开始时间" |
7.2 时段冲突测试
| 已有预约 | 新预约 | 预期结果 |
|---|---|---|
| 15:00-20:00 | 10:00-14:00 | 成功(无重叠) |
| 15:00-20:00 | 14:00-16:00 | 失败(重叠) |
| 15:00-20:00 | 19:00-22:00 | 失败(重叠) |
| 15:00-20:00 | 20:00-22:00 | 成功(边界无重叠) |
7.3 房间列表显示测试
| 预约时间 | 凌晨 | 上午 | 下午 | 晚上 |
|---|---|---|---|---|
| 02:00-05:00 | 🟠 | 🟢 | 🟢 | 🟢 |
| 10:00-14:00 | 🟢 | 🟠 | 🟠 | 🟢 |
| 15:00-20:00 | 🟢 | 🟢 | 🟠 | 🟠 |
| 22:00-04:00 | 🟠 | 🟢 | 🟢 | 🟠 |
8. 工作量评估
| 模块 | 工作内容 | 复杂度 |
|---|---|---|
| 前端-预约页面 | 时间选择器组件替换、时间计算逻辑 | ⭐⭐⭐ |
| 前端-房间列表 | 验证显示逻辑(可能无需改动) | ⭐ |
| 前端-预约详情 | 时间格式显示调整 | ⭐ |
| 后端-控制器 | 参数验证、冲突检测 | ⭐⭐ |
| 后端-服务层 | 逻辑验证(可能无需改动) | ⭐ |
| 测试 | 各场景测试 | ⭐⭐ |
总体评估:中等复杂度改动,主要工作集中在前端预约页面的UI和逻辑重构。
9. 风险与注意事项
- 向后兼容:已有的固定时段预约数据仍可正常显示
- 边界条件:跨天预约(如23:00-02:00)需特殊处理
- 时间精度:建议时间选择以30分钟为最小单位
- 用户体验:需添加明确的时长提示和时段跨越提示
- 数据验证:前后端都需要进行时间有效性验证
10. 附录:时段判断工具函数
/**
* 判断时间范围跨越哪些时段
* @param {number} startHour 开始小时 (0-23)
* @param {number} endHour 结束小时 (0-24)
* @returns {Array} 跨越的时段数组
*/
function getCrossedSlots(startHour, endHour) {
const slots = [];
const slotRanges = [
{ name: '凌晨', start: 0, end: 6 },
{ name: '上午', start: 6, end: 12 },
{ name: '下午', start: 12, end: 18 },
{ name: '晚上', start: 18, end: 24 }
];
for (const slot of slotRanges) {
// 重叠判断:预约开始 < 时段结束 AND 预约结束 > 时段开始
if (startHour < slot.end && endHour > slot.start) {
slots.push(slot.name);
}
}
return slots;
}
// 使用示例
getCrossedSlots(15, 20); // ['下午', '晚上']
getCrossedSlots(10, 14); // ['上午', '下午']
getCrossedSlots(2, 5); // ['凌晨']