666 lines
18 KiB
Markdown
666 lines
18 KiB
Markdown
# 预约时间自由选择 - 开发文档
|
||
|
||
> 文档版本:v1.0
|
||
> 更新日期:2024年
|
||
> 文档类型:开发人员版
|
||
|
||
---
|
||
|
||
## 一、改动概述
|
||
|
||
### 1.1 需求说明
|
||
1. 将固定时段选择(6小时一段)改为自由时间选择
|
||
2. 房间列表正确显示跨时段预约的占用状态
|
||
|
||
### 1.2 影响范围评估
|
||
- **前端**:主要改动,需重构时间选择UI
|
||
- **后端**:少量改动,逻辑已基本支持
|
||
|
||
---
|
||
|
||
## 二、前端改动详情
|
||
|
||
### 2.1 appointment-page.vue(主要改动)
|
||
|
||
**文件路径**:`/uniapp/mahjong_group/pages/appointment/appointment-page.vue`
|
||
|
||
#### 2.1.1 移除旧的时段选择代码
|
||
|
||
**需删除的部分**:
|
||
|
||
```vue
|
||
<!-- 删除:固定时段选择UI -->
|
||
<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>
|
||
```
|
||
|
||
```javascript
|
||
// 删除:固定时段计算函数
|
||
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.1.2 新增时间选择器代码
|
||
|
||
**Template部分**:
|
||
|
||
```vue
|
||
<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" v-if="startTime && endTime">
|
||
<view class="info-item">
|
||
<text class="info-label">预计时长</text>
|
||
<text class="info-value">{{ calculateDuration() }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="info-label">跨越时段</text>
|
||
<text class="info-value">{{ calculateCrossSlots() }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 错误提示 -->
|
||
<view class="time-error" v-if="timeError">
|
||
<text>{{ timeError }}</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
```
|
||
|
||
**Script部分**:
|
||
|
||
```javascript
|
||
<script setup>
|
||
import { ref, computed } from 'vue';
|
||
|
||
// 响应式数据
|
||
const startTime = ref('');
|
||
const endTime = ref('');
|
||
const timeError = ref('');
|
||
|
||
// 开始时间变更
|
||
const onStartTimeChange = (e) => {
|
||
startTime.value = e.detail.value;
|
||
validateTimeRange();
|
||
};
|
||
|
||
// 结束时间变更
|
||
const onEndTimeChange = (e) => {
|
||
endTime.value = e.detail.value;
|
||
validateTimeRange();
|
||
};
|
||
|
||
// 验证时间范围
|
||
const validateTimeRange = () => {
|
||
timeError.value = '';
|
||
|
||
if (!startTime.value || !endTime.value) {
|
||
return true;
|
||
}
|
||
|
||
const [startH, startM] = startTime.value.split(':').map(Number);
|
||
const [endH, endM] = endTime.value.split(':').map(Number);
|
||
|
||
const startMinutes = startH * 60 + startM;
|
||
const endMinutes = endH * 60 + endM;
|
||
|
||
if (endMinutes <= startMinutes) {
|
||
timeError.value = '结束时间必须晚于开始时间';
|
||
return false;
|
||
}
|
||
|
||
// 可选:最短时长检查(如最少1小时)
|
||
if (endMinutes - startMinutes < 60) {
|
||
timeError.value = '预约时长不能少于1小时';
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
};
|
||
|
||
// 计算时长
|
||
const calculateDuration = () => {
|
||
if (!startTime.value || !endTime.value) return '-';
|
||
|
||
const [startH, startM] = startTime.value.split(':').map(Number);
|
||
const [endH, endM] = endTime.value.split(':').map(Number);
|
||
|
||
const startMinutes = startH * 60 + startM;
|
||
const endMinutes = endH * 60 + endM;
|
||
const diffMinutes = endMinutes - startMinutes;
|
||
|
||
if (diffMinutes <= 0) return '-';
|
||
|
||
const hours = Math.floor(diffMinutes / 60);
|
||
const minutes = diffMinutes % 60;
|
||
|
||
if (minutes === 0) {
|
||
return `${hours}小时`;
|
||
}
|
||
return `${hours}小时${minutes}分钟`;
|
||
};
|
||
|
||
// 计算跨越的时段
|
||
const calculateCrossSlots = () => {
|
||
if (!startTime.value || !endTime.value) return '-';
|
||
|
||
const [startH] = startTime.value.split(':').map(Number);
|
||
const [endH] = endTime.value.split(':').map(Number);
|
||
|
||
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 (startH < slot.end && endH > slot.start) {
|
||
slots.push(slot.name);
|
||
}
|
||
}
|
||
|
||
return slots.join('、') || '-';
|
||
};
|
||
|
||
// 构建预约数据(提交时调用)
|
||
const buildReservationData = () => {
|
||
if (!validateTimeRange()) {
|
||
return null;
|
||
}
|
||
|
||
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: formatDateTime(startDateTime),
|
||
end_time: formatDateTime(endDateTime),
|
||
// ... 其他字段保持不变
|
||
};
|
||
};
|
||
|
||
// 格式化日期时间
|
||
const formatDateTime = (date) => {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
const seconds = '00';
|
||
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||
};
|
||
|
||
// 修改提交函数
|
||
const submitReservation = async () => {
|
||
const data = buildReservationData();
|
||
if (!data) {
|
||
uni.showToast({ title: timeError.value || '请选择正确的时间', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
// 调用API提交
|
||
// ...
|
||
};
|
||
</script>
|
||
```
|
||
|
||
**Style部分**:
|
||
|
||
```scss
|
||
<style lang="scss">
|
||
.time-picker-container {
|
||
padding: 20rpx;
|
||
background-color: #fff;
|
||
border-radius: 16rpx;
|
||
margin: 20rpx 0;
|
||
}
|
||
|
||
.time-picker-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 24rpx 0;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.label {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.picker-value {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
padding: 10rpx 20rpx;
|
||
background-color: #f5f5f5;
|
||
border-radius: 8rpx;
|
||
}
|
||
}
|
||
|
||
.time-info {
|
||
margin-top: 20rpx;
|
||
padding: 20rpx;
|
||
background-color: #f8f8f8;
|
||
border-radius: 12rpx;
|
||
|
||
.info-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 10rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.info-label {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 24rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.time-error {
|
||
margin-top: 16rpx;
|
||
padding: 16rpx;
|
||
background-color: #fff2f0;
|
||
border-radius: 8rpx;
|
||
|
||
text {
|
||
font-size: 24rpx;
|
||
color: #ff4d4f;
|
||
}
|
||
}
|
||
</style>
|
||
```
|
||
|
||
### 2.2 book-room-page.vue(验证,可能无需改动)
|
||
|
||
**文件路径**:`/uniapp/mahjong_group/pages/appointment/book-room-page.vue`
|
||
|
||
当前时段显示已从后端获取数据,后端已支持跨时段检测,**理论上无需前端改动**。
|
||
|
||
**验证方法**:
|
||
1. 创建一个15:00-20:00的预约
|
||
2. 查看房间列表,确认下午和晚上都显示为已预约状态
|
||
|
||
如显示不正确,检查后端`SQRoomsServices.cs`中的时段状态返回逻辑。
|
||
|
||
---
|
||
|
||
## 三、后端改动详情
|
||
|
||
### 3.1 SQController.cs(验证/微调)
|
||
|
||
**文件路径**:`/server/CoreCms.Net.Web.WebApi/Controllers/SQController.cs`
|
||
|
||
#### 3.1.1 AddSQReservation 接口验证
|
||
|
||
当前接口已支持接收`start_time`和`end_time`参数,主要验证:
|
||
|
||
```csharp
|
||
[HttpPost("AddSQReservation")]
|
||
public async Task<IActionResult> AddSQReservation([FromBody] AddReservationDto dto)
|
||
{
|
||
// 验证时间有效性(建议新增)
|
||
if (dto.end_time <= dto.start_time)
|
||
{
|
||
return BadRequest(new { code = 400, msg = "结束时间必须晚于开始时间" });
|
||
}
|
||
|
||
// 验证最短时长(可选,如最少1小时)
|
||
var duration = (dto.end_time - dto.start_time).TotalHours;
|
||
if (duration < 1)
|
||
{
|
||
return BadRequest(new { code = 400, msg = "预约时长不能少于1小时" });
|
||
}
|
||
|
||
// 验证最长时长(可选,如最多12小时)
|
||
if (duration > 12)
|
||
{
|
||
return BadRequest(new { code = 400, msg = "预约时长不能超过12小时" });
|
||
}
|
||
|
||
// ... 现有逻辑
|
||
}
|
||
```
|
||
|
||
### 3.2 SQRoomsServices.cs(验证,无需改动)
|
||
|
||
**文件路径**:`/server/CoreCms.Net.Services/SQ/SQRoomsServices.cs`
|
||
|
||
当前时段状态判断逻辑已正确支持跨时段检测:
|
||
|
||
```csharp
|
||
// 现有代码(已支持跨时段)
|
||
bool isReserved = reservations?.Any(r =>
|
||
r.start_time < slotEnd && r.end_time > slotStart) ?? false;
|
||
```
|
||
|
||
**逻辑验证**:
|
||
|
||
| 预约时间 | 时段 | 判断公式 | 结果 |
|
||
|:--------:|:----:|:--------:|:----:|
|
||
| 15:00-20:00 | 下午(12-18) | 15 < 18 && 20 > 12 | true ✓ |
|
||
| 15:00-20:00 | 晚上(18-24) | 15 < 24 && 20 > 18 | true ✓ |
|
||
| 15:00-20:00 | 上午(6-12) | 15 < 12 | false ✓ |
|
||
|
||
**结论**:后端逻辑无需改动。
|
||
|
||
---
|
||
|
||
## 四、签到功能代码参考
|
||
|
||
### 4.1 前端签到组件
|
||
|
||
**文件路径**:`/uniapp/mahjong_group/components/com/page/qiandao-popup.vue`
|
||
|
||
**签到按钮显示条件**(reservation-item.vue):
|
||
|
||
```javascript
|
||
// 签到按钮显示条件
|
||
const isQianDaoVisible = computed(() => {
|
||
const item = props.reservation;
|
||
|
||
// 1. 预约状态必须为 1(已锁定)
|
||
if (item.status !== 1) return false;
|
||
|
||
// 2. 只有发起者可签到
|
||
if (item.role !== 1) return false;
|
||
|
||
// 3. 时间范围检查
|
||
const now = new Date();
|
||
const startTime = new Date(item.start_time);
|
||
const endTime = new Date(item.end_time);
|
||
const tenMinutes = 10 * 60 * 1000;
|
||
|
||
const timeToStart = startTime.getTime() - now.getTime();
|
||
const timeToEnd = endTime.getTime() - now.getTime();
|
||
|
||
// 可签到:开始前10分钟内 或 已开始但未结束
|
||
return (timeToStart <= tenMinutes && timeToStart > 0) ||
|
||
(timeToStart <= 0 && timeToEnd > 0);
|
||
});
|
||
```
|
||
|
||
### 4.2 后端签到接口
|
||
|
||
**文件路径**:`/server/CoreCms.Net.Web.WebApi/Controllers/SQController.cs`
|
||
**方法位置**:第1205-1431行 `CheckInReservation`
|
||
|
||
**请求格式**:
|
||
|
||
```json
|
||
POST /api/sq/checkinreservation
|
||
{
|
||
"reservation_id": 123,
|
||
"attendeds": [
|
||
{ "user_id": 2, "isAttended": true },
|
||
{ "user_id": 3, "isAttended": false }
|
||
]
|
||
}
|
||
```
|
||
|
||
**核心处理逻辑**:
|
||
|
||
```csharp
|
||
// 1. 更新预约状态为"进行中"
|
||
reservation.status = 2;
|
||
|
||
// 2. 处理参与者
|
||
foreach (var attendee in dto.attendeds)
|
||
{
|
||
if (attendee.isAttended)
|
||
{
|
||
// 已到场:信誉+0.2,标记为已赴约
|
||
participant.is_arrive = 1;
|
||
user.credit_score = Math.Min(5, user.credit_score + 0.2);
|
||
}
|
||
else
|
||
{
|
||
// 未到场:信誉-0.5,鸽子+1,踢出
|
||
participant.is_arrive = 2;
|
||
participant.status = 1; // 已退出
|
||
user.credit_score = Math.Max(0, user.credit_score - 0.5);
|
||
user.dove_count += 1;
|
||
}
|
||
}
|
||
|
||
// 3. 处理押金退款
|
||
if (reservation.deposit_fee > 0)
|
||
{
|
||
// 已到场的参与者:发起退款
|
||
// 未到场的参与者:不退款
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 五、数据库相关
|
||
|
||
### 5.1 相关表结构
|
||
|
||
**SQReservations(预约表)**:
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|:----:|:----:|:----:|
|
||
| id | int | 主键 |
|
||
| status | int | 0=待开始,1=已锁定,2=进行中,3=已结束,4=已取消 |
|
||
| start_time | datetime | 开始时间 |
|
||
| end_time | datetime | 结束时间 |
|
||
| room_id | int | 房间ID |
|
||
| deposit_fee | decimal | 押金金额 |
|
||
|
||
**SQReservationParticipants(参与者表)**:
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|:----:|:----:|:----:|
|
||
| id | int | 主键 |
|
||
| reservation_id | int | 预约ID |
|
||
| user_id | int | 用户ID |
|
||
| role | int | 0=参与者,1=发起者 |
|
||
| status | int | 0=正常,1=已退出 |
|
||
| is_arrive | int | 0=默认,1=已赴约,2=未赴约 |
|
||
| is_refund | int | 退款状态 |
|
||
|
||
### 5.2 无需数据库改动
|
||
|
||
本次改动不涉及数据库结构变更,现有字段已满足需求:
|
||
- `start_time` / `end_time` 已支持任意时间
|
||
- 时段状态在服务层动态计算
|
||
|
||
---
|
||
|
||
## 六、测试用例
|
||
|
||
### 6.1 时间选择功能测试
|
||
|
||
| 用例ID | 测试场景 | 输入 | 预期结果 |
|
||
|:------:|:--------:|:----:|:--------:|
|
||
| T001 | 正常选择 | 开始15:00,结束20:00 | 成功,显示"5小时",跨越"下午、晚上" |
|
||
| T002 | 同时段 | 开始14:00,结束17:00 | 成功,显示"3小时",跨越"下午" |
|
||
| T003 | 跨三时段 | 开始10:00,结束20:00 | 成功,显示"10小时",跨越"上午、下午、晚上" |
|
||
| T004 | 时间倒置 | 开始20:00,结束15:00 | 失败,提示"结束时间必须晚于开始时间" |
|
||
| T005 | 时间相同 | 开始15:00,结束15:00 | 失败,提示"结束时间必须晚于开始时间" |
|
||
| T006 | 时长过短 | 开始15:00,结束15:30 | 失败,提示"预约时长不能少于1小时" |
|
||
|
||
### 6.2 时段冲突测试
|
||
|
||
| 用例ID | 已有预约 | 新预约 | 预期结果 |
|
||
|:------:|:--------:|:------:|:--------:|
|
||
| T101 | 15:00-20:00 | 10:00-14:00 | 成功(无重叠) |
|
||
| T102 | 15:00-20:00 | 14:00-16:00 | 失败(重叠) |
|
||
| T103 | 15:00-20:00 | 19:00-22:00 | 失败(重叠) |
|
||
| T104 | 15:00-20:00 | 20:00-22:00 | 成功(边界无重叠) |
|
||
| T105 | 15:00-20:00 | 12:00-22:00 | 失败(完全包含) |
|
||
|
||
### 6.3 房间列表显示测试
|
||
|
||
| 用例ID | 预约时间 | 凌晨 | 上午 | 下午 | 晚上 |
|
||
|:------:|:--------:|:----:|:----:|:----:|:----:|
|
||
| T201 | 02:00-05:00 | 🟠 | 🟢 | 🟢 | 🟢 |
|
||
| T202 | 05:00-08:00 | 🟠 | 🟠 | 🟢 | 🟢 |
|
||
| T203 | 10:00-14:00 | 🟢 | 🟠 | 🟠 | 🟢 |
|
||
| T204 | 15:00-20:00 | 🟢 | 🟢 | 🟠 | 🟠 |
|
||
| T205 | 08:00-22:00 | 🟢 | 🟠 | 🟠 | 🟠 |
|
||
|
||
### 6.4 签到功能测试
|
||
|
||
| 用例ID | 测试场景 | 预期结果 |
|
||
|:------:|:--------:|:--------:|
|
||
| T301 | 开始前11分钟签到 | 签到按钮不显示 |
|
||
| T302 | 开始前10分钟签到 | 签到按钮显示,可签到 |
|
||
| T303 | 开始后5分钟签到 | 签到按钮显示,可签到 |
|
||
| T304 | 结束后签到 | 签到按钮不显示 |
|
||
| T305 | 参与者尝试签到 | 签到按钮不显示 |
|
||
| T306 | 重复签到 | 提示"已签到,无法重复签到" |
|
||
|
||
---
|
||
|
||
## 七、改动文件清单
|
||
|
||
### 7.1 必须改动
|
||
|
||
| 序号 | 文件路径 | 改动类型 | 说明 |
|
||
|:----:|:--------:|:--------:|:----:|
|
||
| 1 | `pages/appointment/appointment-page.vue` | 修改 | 时间选择器重构 |
|
||
|
||
### 7.2 验证确认
|
||
|
||
| 序号 | 文件路径 | 改动类型 | 说明 |
|
||
|:----:|:--------:|:--------:|:----:|
|
||
| 2 | `pages/appointment/book-room-page.vue` | 验证 | 确认跨时段显示正确 |
|
||
| 3 | `SQController.cs` | 验证 | 确认时间参数接收正确 |
|
||
| 4 | `SQRoomsServices.cs` | 验证 | 确认跨时段检测正确 |
|
||
|
||
### 7.3 可选优化
|
||
|
||
| 序号 | 文件路径 | 改动类型 | 说明 |
|
||
|:----:|:--------:|:--------:|:----:|
|
||
| 5 | `SQController.cs` | 新增 | 添加时长限制验证 |
|
||
|
||
---
|
||
|
||
## 八、工具函数
|
||
|
||
### 8.1 跨时段计算(前端)
|
||
|
||
```javascript
|
||
/**
|
||
* 判断时间范围跨越哪些时段
|
||
* @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) {
|
||
if (startHour < slot.end && endHour > slot.start) {
|
||
slots.push(slot.name);
|
||
}
|
||
}
|
||
|
||
return slots;
|
||
}
|
||
|
||
// 使用示例
|
||
getCrossedSlots(15, 20); // ['下午', '晚上']
|
||
getCrossedSlots(10, 14); // ['上午', '下午']
|
||
getCrossedSlots(2, 5); // ['凌晨']
|
||
```
|
||
|
||
### 8.2 时长格式化(前端)
|
||
|
||
```javascript
|
||
/**
|
||
* 格式化时长显示
|
||
* @param {number} minutes 分钟数
|
||
* @returns {string} 格式化后的时长
|
||
*/
|
||
function formatDuration(minutes) {
|
||
if (minutes <= 0) return '-';
|
||
|
||
const hours = Math.floor(minutes / 60);
|
||
const mins = minutes % 60;
|
||
|
||
if (hours === 0) {
|
||
return `${mins}分钟`;
|
||
}
|
||
if (mins === 0) {
|
||
return `${hours}小时`;
|
||
}
|
||
return `${hours}小时${mins}分钟`;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 九、注意事项
|
||
|
||
1. **向后兼容**:已有预约数据不受影响,仍可正常显示
|
||
2. **时间精度**:UniApp的time picker默认支持分钟级别选择
|
||
3. **时区处理**:前后端统一使用本地时间,避免时区转换问题
|
||
4. **边界条件**:特别注意24:00的处理(可视为次日00:00)
|
||
5. **并发控制**:创建预约时需考虑并发冲突,建议后端加锁
|