# 预约时间自由选择 - 开发文档
> 文档版本: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
{{ slot.label }}
```
```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
开始时间
{{ startTime || '请选择开始时间' }}
结束时间
{{ endTime || '请选择结束时间' }}
预计时长
{{ calculateDuration() }}
跨越时段
{{ calculateCrossSlots() }}
{{ timeError }}
```
**Script部分**:
```javascript
```
**Style部分**:
```scss
```
### 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 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. **并发控制**:创建预约时需考虑并发冲突,建议后端加锁