mahjong_group/docs/1.0.0/预约时间自由选择_改动文档_开发版.md
2026-01-01 14:39:23 +08:00

666 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 预约时间自由选择 - 开发文档
> 文档版本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. **并发控制**创建预约时需考虑并发冲突建议后端加锁