You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
455 lines
15 KiB
455 lines
15 KiB
<?php
|
|
// +----------------------------------------------------------------------
|
|
// | 课程预约服务类
|
|
// +----------------------------------------------------------------------
|
|
|
|
namespace app\service\api\student;
|
|
|
|
use app\model\student\Student;
|
|
use app\model\customer_resources\CustomerResources;
|
|
use think\facade\Db;
|
|
use core\base\BaseService;
|
|
use core\exception\CommonException;
|
|
|
|
/**
|
|
* 课程预约服务类
|
|
*/
|
|
class CourseBookingService extends BaseService
|
|
{
|
|
/**
|
|
* 获取可预约课程列表
|
|
* @param array $params
|
|
* @return array
|
|
*/
|
|
public function getAvailableCourses($params)
|
|
{
|
|
$studentId = $params['student_id'];
|
|
|
|
// 验证学员权限
|
|
$this->checkStudentPermission($studentId);
|
|
|
|
// 构建查询条件
|
|
$where = [
|
|
['cs.deleted_at', '=', 0],
|
|
['cs.status', '=', 'pending'], // 待开始的课程
|
|
['cs.course_date', '>=', date('Y-m-d')] // 未来的课程
|
|
];
|
|
|
|
// 日期筛选
|
|
if (!empty($params['date'])) {
|
|
$where[] = ['cs.course_date', '=', $params['date']];
|
|
}
|
|
|
|
// 日期范围筛选
|
|
if (!empty($params['start_date']) && !empty($params['end_date'])) {
|
|
$where[] = ['cs.course_date', 'between', [$params['start_date'], $params['end_date']]];
|
|
}
|
|
|
|
// 教练筛选
|
|
if (!empty($params['coach_id'])) {
|
|
$where[] = ['cs.coach_id', '=', $params['coach_id']];
|
|
}
|
|
|
|
// 场地筛选
|
|
if (!empty($params['venue_id'])) {
|
|
$where[] = ['cs.venue_id', '=', $params['venue_id']];
|
|
}
|
|
|
|
// 查询可预约的课程安排
|
|
$availableCourses = Db::table('school_course_schedule cs')
|
|
->leftJoin('school_course c', 'cs.course_id = c.id')
|
|
->leftJoin('school_personnel p', 'cs.coach_id = p.id')
|
|
->leftJoin('school_venue v', 'cs.venue_id = v.id')
|
|
->where($where)
|
|
->field('
|
|
cs.id,
|
|
cs.course_date,
|
|
cs.time_slot,
|
|
COALESCE(cs.start_time,
|
|
CASE
|
|
WHEN cs.time_slot REGEXP "^[0-9]{2}:[0-9]{2}-[0-9]{2}:[0-9]{2}$"
|
|
THEN SUBSTRING_INDEX(cs.time_slot, "-", 1)
|
|
ELSE "09:00"
|
|
END
|
|
) as start_time,
|
|
COALESCE(cs.end_time,
|
|
CASE
|
|
WHEN cs.time_slot REGEXP "^[0-9]{2}:[0-9]{2}-[0-9]{2}:[0-9]{2}$"
|
|
THEN SUBSTRING_INDEX(cs.time_slot, "-", -1)
|
|
ELSE "10:00"
|
|
END
|
|
) as end_time,
|
|
cs.max_students,
|
|
cs.available_capacity,
|
|
c.course_name,
|
|
c.course_type,
|
|
c.duration,
|
|
p.name as coach_name,
|
|
v.venue_name,
|
|
cs.status
|
|
')
|
|
->order('cs.course_date asc, cs.start_time asc')
|
|
->select()
|
|
->toArray();
|
|
|
|
// 处理每个课程的预约状态
|
|
foreach ($availableCourses as &$course) {
|
|
// 计算已预约人数
|
|
$bookedCount = Db::table('school_person_course_schedule')
|
|
->where('schedule_id', $course['id'])
|
|
->where('deleted_at', 0)
|
|
->where('status', 0) // 0-待上课
|
|
->count();
|
|
|
|
$course['current_students'] = $bookedCount;
|
|
$course['max_students'] = $course['max_students'] ?: 10; // 默认最大10人
|
|
|
|
// 检查该学员是否已预约此时段
|
|
$isBooked = Db::table('school_person_course_schedule')
|
|
->where('student_id', $studentId)
|
|
->where('schedule_id', $course['id'])
|
|
->where('deleted_at', 0)
|
|
->where('status', '!=', 3) // 3-取消
|
|
->find();
|
|
|
|
// 确定课程状态
|
|
if ($isBooked) {
|
|
$course['booking_status'] = 'booked';
|
|
} elseif ($bookedCount >= $course['max_students']) {
|
|
$course['booking_status'] = 'full';
|
|
} else {
|
|
$course['booking_status'] = 'available';
|
|
}
|
|
|
|
// 计算时长
|
|
$course['duration'] = 60; // 默认60分钟
|
|
}
|
|
|
|
return [
|
|
'list' => $availableCourses,
|
|
'total' => count($availableCourses)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 创建课程预约
|
|
* @param array $data
|
|
* @return array
|
|
*/
|
|
public function createBooking($data)
|
|
{
|
|
$studentId = $data['student_id'];
|
|
$scheduleId = $data['schedule_id'];
|
|
|
|
// 验证学员权限
|
|
$this->checkStudentPermission($studentId);
|
|
|
|
// 检查课程安排是否存在
|
|
$courseSchedule = Db::table('school_course_schedule')
|
|
->where('id', $scheduleId)
|
|
->where('deleted_at', 0)
|
|
->find();
|
|
|
|
if (!$courseSchedule) {
|
|
throw new CommonException('课程安排不存在');
|
|
}
|
|
|
|
// 检查预约冲突
|
|
$conflictCheck = $this->checkBookingConflict([
|
|
'student_id' => $studentId,
|
|
'booking_date' => $data['course_date'],
|
|
'time_slot' => $data['time_slot']
|
|
]);
|
|
|
|
if ($conflictCheck['has_conflict']) {
|
|
throw new CommonException('该时段已有预约冲突');
|
|
}
|
|
|
|
// 检查课程容量
|
|
$bookedCount = Db::table('school_person_course_schedule')
|
|
->where('schedule_id', $scheduleId)
|
|
->where('deleted_at', 0)
|
|
->where('status', '!=', 3) // 非取消状态
|
|
->count();
|
|
|
|
$maxStudents = $courseSchedule['max_students'] ?: 10;
|
|
if ($bookedCount >= $maxStudents) {
|
|
throw new CommonException('该课程已满员');
|
|
}
|
|
|
|
// 检查是否已预约过
|
|
$existingBooking = Db::table('school_person_course_schedule')
|
|
->where('student_id', $studentId)
|
|
->where('schedule_id', $scheduleId)
|
|
->where('deleted_at', 0)
|
|
->where('status', '!=', 3)
|
|
->find();
|
|
|
|
if ($existingBooking) {
|
|
throw new CommonException('您已预约过此课程');
|
|
}
|
|
|
|
// 创建预约记录
|
|
$bookingData = [
|
|
'student_id' => $studentId,
|
|
'schedule_id' => $scheduleId,
|
|
'course_date' => $data['course_date'],
|
|
'time_slot' => $data['time_slot'],
|
|
'person_type' => 'student',
|
|
'course_type' => 3, // 3-预约课程
|
|
'status' => 0, // 0-待上课
|
|
'remark' => $data['note'] ?? '',
|
|
'created_at' => date('Y-m-d H:i:s'),
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
'deleted_at' => 0
|
|
];
|
|
|
|
try {
|
|
$bookingId = Db::table('school_person_course_schedule')->insertGetId($bookingData);
|
|
|
|
if (!$bookingId) {
|
|
throw new CommonException('预约创建失败');
|
|
}
|
|
|
|
// TODO: 发送预约成功消息通知
|
|
|
|
return [
|
|
'booking_id' => $bookingId,
|
|
'status' => 'success',
|
|
'message' => '预约创建成功'
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
throw new CommonException('预约创建失败:' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取我的预约列表
|
|
* @param array $params
|
|
* @return array
|
|
*/
|
|
public function getMyBookingList($params)
|
|
{
|
|
$studentId = $params['student_id'];
|
|
|
|
// 验证学员权限
|
|
$this->checkStudentPermission($studentId);
|
|
|
|
// 构建查询条件
|
|
$where = [
|
|
['pcs.student_id', '=', $studentId],
|
|
['pcs.deleted_at', '=', 0],
|
|
['pcs.course_type', '=', 3] // 3-预约课程
|
|
];
|
|
|
|
// 状态筛选
|
|
if (!empty($params['status'])) {
|
|
$where[] = ['pcs.status', '=', $params['status']];
|
|
}
|
|
|
|
// 日期范围筛选
|
|
if (!empty($params['start_date']) && !empty($params['end_date'])) {
|
|
$where[] = ['pcs.course_date', 'between', [$params['start_date'], $params['end_date']]];
|
|
}
|
|
|
|
// 查询预约列表
|
|
$bookingList = Db::table('school_person_course_schedule pcs')
|
|
->leftJoin('school_course_schedule cs', 'pcs.schedule_id = cs.id')
|
|
->leftJoin('school_course c', 'cs.course_id = c.id')
|
|
->leftJoin('school_personnel p', 'cs.coach_id = p.id')
|
|
->leftJoin('school_venue v', 'cs.venue_id = v.id')
|
|
->where($where)
|
|
->field('
|
|
pcs.id,
|
|
pcs.course_date as booking_date,
|
|
pcs.time_slot,
|
|
pcs.status,
|
|
pcs.cancel_reason,
|
|
pcs.remark,
|
|
COALESCE(cs.start_time,
|
|
CASE
|
|
WHEN pcs.time_slot REGEXP "^[0-9]{2}:[0-9]{2}-[0-9]{2}:[0-9]{2}$"
|
|
THEN SUBSTRING_INDEX(pcs.time_slot, "-", 1)
|
|
ELSE "09:00"
|
|
END
|
|
) as start_time,
|
|
COALESCE(cs.end_time,
|
|
CASE
|
|
WHEN pcs.time_slot REGEXP "^[0-9]{2}:[0-9]{2}-[0-9]{2}:[0-9]{2}$"
|
|
THEN SUBSTRING_INDEX(pcs.time_slot, "-", -1)
|
|
ELSE "10:00"
|
|
END
|
|
) as end_time,
|
|
c.course_name as course_type,
|
|
p.name as coach_name,
|
|
v.venue_name,
|
|
pcs.created_at
|
|
')
|
|
->order('pcs.course_date desc, pcs.created_at desc')
|
|
->select()
|
|
->toArray();
|
|
|
|
// 处理数据格式
|
|
foreach ($bookingList as &$booking) {
|
|
$booking['status_text'] = $this->getBookingStatusText($booking['status']);
|
|
}
|
|
|
|
return [
|
|
'list' => $bookingList,
|
|
'total' => count($bookingList)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 取消课程预约
|
|
* @param array $data
|
|
* @return bool
|
|
*/
|
|
public function cancelBooking($data)
|
|
{
|
|
$bookingId = $data['booking_id'];
|
|
$cancelReason = $data['cancel_reason'] ?? '';
|
|
|
|
// 查询预约记录
|
|
$booking = Db::table('school_person_course_schedule')
|
|
->where('id', $bookingId)
|
|
->where('deleted_at', 0)
|
|
->find();
|
|
|
|
if (!$booking) {
|
|
throw new CommonException('预约记录不存在');
|
|
}
|
|
|
|
// 验证学员权限
|
|
$this->checkStudentPermission($booking['student_id']);
|
|
|
|
// 检查预约状态
|
|
if ($booking['status'] != 0) { // 0-待上课
|
|
throw new CommonException('当前预约状态不允许取消');
|
|
}
|
|
|
|
// 检查取消时间限制(上课前6小时)
|
|
$courseDateTime = $booking['course_date'] . ' ' . ($booking['start_time'] ?: '09:00');
|
|
$courseTimestamp = strtotime($courseDateTime);
|
|
$currentTimestamp = time();
|
|
|
|
if ($courseTimestamp - $currentTimestamp < 6 * 3600) {
|
|
throw new CommonException('上课前6小时内不允许取消预约');
|
|
}
|
|
|
|
// 更新预约状态为取消
|
|
$result = Db::table('school_person_course_schedule')
|
|
->where('id', $bookingId)
|
|
->update([
|
|
'status' => 3, // 3-取消
|
|
'cancel_reason' => $cancelReason,
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
if ($result === false) {
|
|
throw new CommonException('取消预约失败');
|
|
}
|
|
|
|
// TODO: 发送取消预约消息通知
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 检查预约冲突
|
|
* @param array $data
|
|
* @return array
|
|
*/
|
|
public function checkBookingConflict($data)
|
|
{
|
|
$studentId = $data['student_id'];
|
|
$bookingDate = $data['booking_date'];
|
|
$timeSlot = $data['time_slot'];
|
|
|
|
// 查询同一时间段的预约
|
|
$conflictBooking = Db::table('school_person_course_schedule')
|
|
->where('student_id', $studentId)
|
|
->where('course_date', $bookingDate)
|
|
->where('time_slot', $timeSlot)
|
|
->where('deleted_at', 0)
|
|
->where('status', '!=', 3) // 非取消状态
|
|
->find();
|
|
|
|
return [
|
|
'has_conflict' => !empty($conflictBooking),
|
|
'conflict_booking' => $conflictBooking
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 检查学员权限(确保只能操作自己的预约)
|
|
* @param int $studentId
|
|
* @return bool
|
|
*/
|
|
private function checkStudentPermission($studentId)
|
|
{
|
|
$customerId = $this->getUserId();
|
|
|
|
// 检查学员是否属于当前用户
|
|
$student = (new Student())
|
|
->where('id', $studentId)
|
|
->where('user_id', $customerId)
|
|
->where('deleted_at', 0)
|
|
->find();
|
|
|
|
if (!$student) {
|
|
throw new CommonException('无权限访问该学员信息');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 获取预约状态文本
|
|
* @param int $status
|
|
* @return string
|
|
*/
|
|
private function getBookingStatusText($status)
|
|
{
|
|
$statusMap = [
|
|
0 => '待上课',
|
|
1 => '已完成',
|
|
2 => '请假',
|
|
3 => '已取消'
|
|
];
|
|
|
|
return $statusMap[$status] ?? '未知状态';
|
|
}
|
|
|
|
/**
|
|
* 获取当前登录用户ID
|
|
* @return int
|
|
*/
|
|
private function getUserId()
|
|
{
|
|
// 从request中获取memberId(由ApiCheckToken中间件设置)
|
|
$memberId = request()->memberId();
|
|
if ($memberId) {
|
|
return $memberId;
|
|
}
|
|
|
|
// 如果没有中间件设置,尝试解析token
|
|
$token = request()->header('token');
|
|
if ($token) {
|
|
try {
|
|
$loginService = new \app\service\api\login\LoginService();
|
|
$tokenInfo = $loginService->parseToken($token);
|
|
if (!empty($tokenInfo) && isset($tokenInfo['member_id'])) {
|
|
return $tokenInfo['member_id'];
|
|
}
|
|
} catch (\Exception $e) {
|
|
// token解析失败,抛出异常
|
|
throw new CommonException('用户未登录或token无效');
|
|
}
|
|
}
|
|
|
|
// 如果都没有,抛出异常
|
|
throw new CommonException('用户未登录');
|
|
}
|
|
}
|