智慧教务系统
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.
 
 
 
 
 
 

698 lines
23 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();
// 获取学员预约资格信息
$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;
}
}