checkStudentPermission($studentId); // 构建查询条件 $where = [ ['cs.deleted_at', '=', 0], ['cs.status', '=', 'pending'], // 待开始的课程 ['cs.course_date', '>=', date('Y-m-d')] // 未来的课程 ]; // 日期筛选 if (!empty($params['date']) && $params['date'] !== 'undefined') { $where[] = ['cs.course_date', '=', $params['date']]; } // 日期范围筛选 if (!empty($params['start_date']) && $params['start_date'] !== 'undefined' && !empty($params['end_date']) && $params['end_date'] !== 'undefined') { $where[] = ['cs.course_date', 'between', [$params['start_date'], $params['end_date']]]; } // 教练筛选 if (!empty($params['coach_id']) && $params['coach_id'] !== 'undefined') { $where[] = ['cs.coach_id', '=', $params['coach_id']]; } // 场地筛选 if (!empty($params['venue_id']) && $params['venue_id'] !== 'undefined') { $where[] = ['cs.venue_id', '=', $params['venue_id']]; } // 课程类型筛选 if (!empty($params['course_type']) && $params['course_type'] !== 'undefined') { $where[] = ['c.course_type', '=', $params['course_type']]; } // 查询可预约的课程安排 $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(); // 获取学员预约资格信息 $studentQualification = $this->checkStudentQualification($studentId); // 处理每个课程的预约状态 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'; } elseif (!$studentQualification['can_book']) { $course['booking_status'] = 'no_permission'; $course['no_permission_reason'] = $studentQualification['reason']; } 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); // 检查学员预约资格 $studentQualification = $this->checkStudentQualification($studentId); if (!$studentQualification['can_book']) { throw new CommonException($studentQualification['reason']); } // 检查课程安排是否存在 $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('您已预约过此课程'); } // 判断预约类型(正式课程或试听课) $bookingType = $studentQualification['has_valid_course'] ? 1 : 4; // 1-正式课程, 4-试听课 // 开启事务 Db::startTrans(); try { // 创建预约记录 $bookingData = [ 'student_id' => $studentId, 'schedule_id' => $scheduleId, 'course_date' => $data['course_date'], 'time_slot' => $data['time_slot'], 'person_type' => 'student', 'course_type' => $bookingType, 'status' => 0, // 0-待上课 'remark' => $data['remark'] ?? '', 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), 'deleted_at' => 0 ]; $bookingId = Db::table('school_person_course_schedule')->insertGetId($bookingData); if (!$bookingId) { throw new CommonException('预约创建失败'); } // 预约时不扣减课时,等实际上课时再扣减 // 只记录预约信息,包含关联的课程ID(用于后续课时扣减) if ($bookingType == 1 && !empty($studentQualification['best_course'])) { // 更新预约记录,关联学员课程ID Db::table('school_person_course_schedule') ->where('id', $bookingId) ->update(['student_course_id' => $studentQualification['best_course']['id']]); } // 提交事务 Db::commit(); // TODO: 发送预约成功消息通知 return [ 'booking_id' => $bookingId, 'status' => 'success', 'message' => '预约创建成功', 'booking_type' => $bookingType == 1 ? 'formal' : 'trial' ]; } catch (\Exception $e) { // 回滚事务 Db::rollback(); 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] // 移除course_type限制,因为我们改成了1(正式课程)和4(试听课) ]; // 状态筛选 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小时) // 从time_slot中提取开始时间 $startTime = '09:00'; // 默认开始时间 if (!empty($booking['time_slot']) && preg_match('/^(\d{2}:\d{2})-\d{2}:\d{2}$/', $booking['time_slot'], $matches)) { $startTime = $matches[1]; } $courseDateTime = $booking['course_date'] . ' ' . $startTime; $courseTimestamp = strtotime($courseDateTime); $currentTimestamp = time(); if ($courseTimestamp - $currentTimestamp < 6 * 3600) { throw new CommonException('上课前6小时内不允许取消预约'); } // 开启事务 Db::startTrans(); try { // 更新预约状态为取消 $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('取消预约失败'); } // 预约时不扣减课时,取消预约时也不需要恢复课时 // 课时只在实际签到上课时扣减 // 这里只需要更新预约状态为取消即可 // 提交事务 Db::commit(); // TODO: 发送取消预约消息通知 return true; } catch (\Exception $e) { // 回滚事务 Db::rollback(); throw new CommonException('取消预约失败:' . $e->getMessage()); } } /** * 检查预约冲突 * @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('用户未登录'); } /** * 检查学员预约资格 * @param int $studentId * @return array */ public function checkStudentQualification($studentId) { // 查询学员有效的正式课程 $validCourses = Db::table('school_student_courses') ->where('student_id', $studentId) ->where('status', 1) // 有效状态 ->where('start_date', '<=', date('Y-m-d')) ->where('end_date', '>=', date('Y-m-d')) ->select() ->toArray(); // 检查是否有剩余课时的课程 $hasValidCourse = false; $bestCourse = null; $totalRemainingHours = 0; foreach ($validCourses as $course) { $totalHours = $course['total_hours'] + $course['gift_hours']; $usedHours = $course['use_total_hours'] + $course['use_gift_hours']; $remainingHours = $totalHours - $usedHours; if ($remainingHours > 0) { $hasValidCourse = true; $totalRemainingHours += $remainingHours; // 选择剩余课时最多的课程 if (!$bestCourse || $remainingHours > ($bestCourse['remaining_hours'] ?? 0)) { $bestCourse = $course; $bestCourse['remaining_hours'] = $remainingHours; } } } // 如果有有效课程,直接允许预约 if ($hasValidCourse) { return [ 'can_book' => true, 'has_valid_course' => true, 'total_remaining_hours' => $totalRemainingHours, 'best_course' => $bestCourse, 'reason' => '' ]; } // 没有有效课程,检查试听课次数 $student = Db::table('school_student') ->where('id', $studentId) ->where('deleted_at', 0) ->find(); if (!$student) { return [ 'can_book' => false, 'has_valid_course' => false, 'trial_class_count' => 0, 'reason' => '学员信息不存在' ]; } $trialClassCount = $student['trial_class_count'] ?? 0; if ($trialClassCount > 0) { return [ 'can_book' => true, 'has_valid_course' => false, 'trial_class_count' => $trialClassCount, 'reason' => '' ]; } // 既没有有效课程,也没有试听课次数 return [ 'can_book' => false, 'has_valid_course' => false, 'trial_class_count' => 0, 'reason' => '课时不足且无试听课次数,请联系客服购买课程' ]; } /** * 扣减正式课课时 * @param int $studentId * @param array $courseInfo * @return bool */ private function deductCourseHours($studentId, $courseInfo) { $courseId = $courseInfo['id']; // 优先扣减赠课课时,再扣减购买课时 if ($courseInfo['gift_hours'] > $courseInfo['use_gift_hours']) { // 扣减赠课课时 $result = Db::table('school_student_courses') ->where('id', $courseId) ->inc('use_gift_hours', 1) ->update(); } else { // 扣减购买课时 $result = Db::table('school_student_courses') ->where('id', $courseId) ->inc('use_total_hours', 1) ->update(); } if ($result === false) { throw new CommonException('扣减课时失败'); } return true; } /** * 扣减试听课次数 * @param int $studentId * @return bool */ private function deductTrialClassCount($studentId) { $result = Db::table('school_student') ->where('id', $studentId) ->where('trial_class_count', '>', 0) ->dec('trial_class_count', 1) ->update(); if ($result === false) { throw new CommonException('扣减试听课次数失败'); } return true; } /** * 恢复正式课课时 * @param int $studentId * @param int $studentCourseId * @return bool */ private function restoreCourseHours($studentId, $studentCourseId) { if (!$studentCourseId) { return false; // 如果没有关联的学员课程ID,无法恢复 } $courseInfo = Db::table('school_student_courses') ->where('id', $studentCourseId) ->find(); if (!$courseInfo) { return false; } // 优先恢复赠课课时,再恢复购买课时 if ($courseInfo['use_gift_hours'] > 0) { // 恢复赠课课时 $result = Db::table('school_student_courses') ->where('id', $studentCourseId) ->dec('use_gift_hours', 1) ->update(); } else { // 恢复购买课时 $result = Db::table('school_student_courses') ->where('id', $studentCourseId) ->dec('use_total_hours', 1) ->update(); } return $result !== false; } /** * 恢复试听课次数 * @param int $studentId * @return bool */ private function restoreTrialClassCount($studentId) { $result = Db::table('school_student') ->where('id', $studentId) ->inc('trial_class_count', 1) ->update(); return $result !== false; } }