package com.yonge.cooleshow.biz.dal.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.yonge.cooleshow.auth.api.client.SysUserFeignService;
import com.yonge.cooleshow.auth.api.entity.SysUser;
import com.yonge.cooleshow.biz.dal.dao.*;
import com.yonge.cooleshow.biz.dal.dto.PracticeScheduleDto;
import com.yonge.cooleshow.biz.dal.dto.UserAccountRecordDto;
import com.yonge.cooleshow.biz.dal.dto.req.OrderReq;
import com.yonge.cooleshow.biz.dal.dto.search.HomeworkSearch;
import com.yonge.cooleshow.biz.dal.dto.search.MyCourseSearch;
import com.yonge.cooleshow.biz.dal.dto.search.PracticeTeacherSearch;
import com.yonge.cooleshow.biz.dal.entity.*;
import com.yonge.cooleshow.biz.dal.enums.*;
import com.yonge.cooleshow.biz.dal.service.*;
import com.yonge.cooleshow.common.enums.PostStatusEnum;
import com.yonge.cooleshow.common.enums.YesOrNoEnum;
import com.yonge.toolset.payment.util.DistributedLock;
import com.yonge.toolset.mybatis.support.PageUtil;
import com.yonge.cooleshow.biz.dal.support.WrapperUtil;
import com.yonge.cooleshow.biz.dal.vo.*;
import com.yonge.cooleshow.biz.dal.vo.res.OrderCreateRes;
import com.yonge.cooleshow.common.constant.SysConfigConstant;
import com.yonge.cooleshow.common.entity.HttpResponseResult;
import com.yonge.cooleshow.common.enums.CacheNameEnum;
import com.yonge.toolset.base.exception.BizException;
import com.yonge.toolset.base.page.PageInfo;
import com.yonge.toolset.thirdparty.message.MessageSenderPluginContext;
import com.yonge.toolset.utils.date.DateUtil;
import com.yonge.toolset.utils.obj.ObjectUtil;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.yonge.cooleshow.biz.dal.support.WrapperUtil.inInterSection;

/**
 * 老师课程表(CourseSchedule)表服务实现类
 *
 * @author hgw
 * @since 2022-03-18 15:29:11
 */
@Service("courseScheduleService")
public class CourseScheduleServiceImpl extends ServiceImpl<CourseScheduleDao, CourseSchedule> implements CourseScheduleService {

    private final static Logger log = LoggerFactory.getLogger(CourseScheduleServiceImpl.class);

    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private SysUserFeignService sysUserFeignService;
    @Autowired
    private HolidaysFestivalsService holidaysFestivalsService;
    @Autowired
    private SysConfigService sysConfigService;
    @Autowired
    private CourseScheduleRepliedDao repliedDao;
    @Autowired
    private CourseScheduleStudentPaymentDao paymentDao;
    @Autowired
    private TeacherFreeTimeService teacherFreeTimeService;
    @Autowired
    private CourseGroupService courseGroupService;
    @Autowired
    private CourseScheduleStudentPaymentService courseScheduleStudentPaymentService;
    @Autowired
    private CourseScheduleTeacherSalaryService courseScheduleTeacherSalaryService;
    @Autowired
    private TeacherFreeTimeDao teacherFreeTimeDao;
    @Autowired
    private SysMessageService sysMessageService;
    @Autowired
    private HolidaysFestivalsDao holidaysFestivalsDao;
    @Autowired
    private PianoRoomTimeDao pianoRoomTimeDao;
    @Autowired
    private UserBindingTeacherDao userBindingTeacherDao;
    @Autowired
    private CourseScheduleRecordDao recordDao;
    @Autowired
    private ImGroupService imGroupService;
    @Autowired
    private ImUserFriendService imUserFriendService;
    @Autowired
    private UserAccountService userAccountService;
    @Autowired
    private UserAccountRecordService userAccountRecordService;
    @Autowired
    private UserOrderService userOrderService;
    
    @Autowired
    private StudentService studentService;

    @Override
    public CourseScheduleDao getDao() {
        return this.baseMapper;
    }

    /**
     * 老师端-首页-我的课程-直播课
     *
     * @param param 传入参数
     *              <p> - classDate 年月
     *              <p> - row 条数
     *              <p> - page 页数
     *              <p> - status 课程状态 NOT_START未开始 ING进行中 COMPLETE已完成
     *              <p> - subjectId 声部id
     */
    public PageInfo<TeacherLiveCourseInfoVo> queryTeacherLiveCourse(Map<String, Object> param) {
        String status = WrapperUtil.toStr(param, "status");
        if (StringUtils.isNotBlank(status)) {
            CourseScheduleEnum.existCourseState(status, "查询条件错误，课程状态不正确");
        }
        param.put("status", status);
        String classDate = WrapperUtil.toStr(param, "classDate", "查询时间不能为空!");
        //本月的第一天
        LocalDate firstDay;
        try {
            String[] classDateSp = classDate.split("-");
            firstDay = LocalDate.of(Integer.parseInt(classDateSp[0]), Integer.parseInt(classDateSp[1]), 1);
        } catch (Exception e) {
            throw new BizException("查询时间格式不正确 [" + classDate + "]");
        }
        //本月的最后一天
        LocalDate lastDay = firstDay.with(TemporalAdjusters.lastDayOfMonth());
        //查询该月的所有课程
        param.put("startDate", firstDay.toString());
        param.put("endDate", lastDay.toString());
        param.put("teacherId", getSysUser().getId());
        param.put("type", CourseScheduleEnum.LIVE.getCode());
        param.put("groupState", String.join(",", CourseGroupEnum.ING.getCode(), CourseGroupEnum.COMPLETE.getCode()));
        log.info("queryTeacherLiveCourse：{}", param);
        Page<TeacherLiveCourseInfoVo> pageInfo = PageUtil.getPageInfo(param);
        pageInfo.setAsc("a.start_time_");
        IPage<TeacherLiveCourseInfoVo> page = baseMapper.queryLiveTeacherCourse(pageInfo, param);
        return PageUtil.pageInfo(page);
    }

    /**
     * 校验该学生大于当前时间未开始和进行中的课程时间和传入时间段有没有交集
     *
     * @param studentId 学生id
     * @param startTime 新增课程 开始时间
     * @param endTime   新增课程 结束时间
     * @return true 被占用 false 没有被占用
     */
    public boolean checkStudentCourseTime(Long studentId, Date startTime, Date endTime) {
        WrapperUtil.checkObj(studentId, "学生id不能为空!");
        WrapperUtil.checkObj(startTime, "开始时间不能为空!");
        //获取日期-目前上课没有跨天情况
        String ymd = DateUtil.format(startTime, DateUtil.DEFAULT_PATTERN);
        //查询大于当前时间并未开始的课程
        Map<String, Object> param = new HashMap<>();
        param.put("studentId", studentId);
        param.put("greaterDate", new Date());
        param.put("classDate", ymd);
        param.put("statusList", Lists.newArrayList(CourseScheduleEnum.NOT_START.getCode(), CourseScheduleEnum.ING.getCode()));
        List<CourseSchedule> list = baseMapper.queryStudentCourse(param);
        return checkCourseTime(list, CourseSchedule::getStartTime, CourseSchedule::getEndTime, startTime, endTime);
    }

    /**
     * 校验老师这个时间段有没有被占用-校验课时
     *
     * @param teacherId 老师id
     * @param startTime 新增课程 开始时间
     * @param endTime   新增课程 结束时间
     * @return true 被占用 false 没有被占用
     */
    public boolean checkTeacherCourseTime(Long teacherId, Date startTime, Date endTime) {
        WrapperUtil.checkObj(teacherId, "老师id不能为空!");
        WrapperUtil.checkObj(startTime, "开始时间不能为空!");
        //获取日期-目前上课没有跨天情况
        String ymd = DateUtil.format(startTime, DateUtil.DEFAULT_PATTERN);
        //查询老师当天未开始、进行中的课程
        List<CourseSchedule> list = getTeacherCourseTime(teacherId, ymd, ymd);
        return checkCourseTime(list, CourseSchedule::getStartTime, CourseSchedule::getEndTime, startTime, endTime);
    }

    /**
     * 检查传入时间列表的时间是否与新增课程时间有交集
     * <p>案例1：
     * <p>list列表
     * <p> 8:00 ~ 09:00 课程
     * <p> 9:00 ~ 10:00 课程
     * <p>新增课程 09:10~ 09:40
     * <p>返回 true
     *
     * <p>案例2：
     * <p>list列表
     * <p> 8:00 ~ 09:00 课程
     * <p> 9:00 ~ 10:00 课程
     * <p>新增课程 10:00~ 10:40
     * <p>返回 false
     *
     * @param list         时间列表
     * @param startTimeFun 获取现课程开始时间
     * @param endTimeFun   获取现课程结束时间
     * @param startTime    新增课程开始时间
     * @param endTime      新增课程结束时间
     * @return true 有交集 false 没有交集
     */
    public <T> boolean checkCourseTime(List<T> list, Function<T, Date> startTimeFun, Function<T, Date> endTimeFun, Date startTime, Date endTime) {
        WrapperUtil.checkObj(startTime, "开始时间不能为空!");
        WrapperUtil.checkObj(endTime, "结束时间不能为空!");
        // 如果没有课程，直接返回false
        if (CollectionUtils.isEmpty(list)) {
            return false;
        }
        boolean result;
        //匹配时间是否有交集
        for (T t : list) {
            result = WrapperUtil.inInterSection(startTimeFun.apply(t), endTimeFun.apply(t), startTime, endTime);
            if (result) {
                return true;
            }
        }
        return false;
    }

    /**
     * 创建直播课时选课时的日历
     * <P>主要查询该老师目前的课程时间
     * <P>根据传入的单课时时长将时间分片后来匹配数据库中课程时间，得到每日空余时间
     *
     * @param param 传入参数
     *              <p> - teacherId 老师id
     *              <p> - year 年
     *              <p> - month 月
     *              <p> - singleCourseMinutes 单课时时长(不包含休息时间)
     *              <p> - freeCourseMinutes 单课时休息时长
     * @return 返回传入时间当月每日的剩余时间段
     */
    public List<CourseCalendarEntity> createLiveCourseCalendar(Map<String, Object> param) {
        //校验数据
        Long teacherId = WrapperUtil.toLong(param, "teacherId", "老师id不能为空!");
        Integer year = WrapperUtil.toInt(param, "year", "日历的时间年份不能为空!");
        Integer month = WrapperUtil.toInt(param, "month", "日历的时间月份不能为空!");
        Integer singleCourseMinutes = WrapperUtil.toInt(param, "singleCourseMinutes", "单课时时长不能为空!");
        Integer freeCourseMinutes = WrapperUtil.toInt(param, "freeCourseMinutes");
        if (Objects.isNull(freeCourseMinutes)) {
            freeCourseMinutes = 0;
        }
        if (singleCourseMinutes < 25) {
            throw new BizException("单课时时长不能小于25分钟!");
        }
        //获取每月1号
        LocalDate firstDay = LocalDate.of(year, month, 1);
        //根据firstDay计算出该月的最后一天
        LocalDate lastDay = firstDay.with(TemporalAdjusters.lastDayOfMonth());
        //获取老师的课程  key：日期-年月日 value：课程时间-开始时间，结束时间
        Map<String, List<CourseTimeEntity>> nowCourseMap = getCourseListMap(getTeacherCourseTime(teacherId, firstDay.toString(), lastDay.toString()));
        //生成日历数据
        List<CourseCalendarEntity> courseCalendarEntities = generateCourseData(firstDay, lastDay, singleCourseMinutes, freeCourseMinutes);
        //对比课程数据，筛选出空余的课程时间
        opsCourseCalendarData(nowCourseMap, courseCalendarEntities, 0);
        return courseCalendarEntities;
    }

    /**
     * 对比课程数据，筛选出空余的课程时间
     *
     * @param nowCourseMap           现有课程
     * @param courseCalendarEntities 课程时间日历
     * @param skipHoliday            是否跳过节假日  0:不跳过 1跳过
     */
    private void opsCourseCalendarData(Map<String, List<CourseTimeEntity>> nowCourseMap, List<CourseCalendarEntity> courseCalendarEntities, Integer skipHoliday) {
        //储存节假日数据，避免重复查询
        Map<Integer, List<String>> holidayMap = new HashMap<>();
        courseCalendarEntities.forEach(calendarEntity -> {
            //获取课程数据,并剔除交集时间段数据
            List<CourseTimeEntity> nowCourse = nowCourseMap.get(calendarEntity.getDate());
            if (CollectionUtils.isEmpty(nowCourse)) {
                //当日没课
                calendarEntity.setFullCourse(0);//  0:未满 1满
            } else {
                //将日历时间与课程时间进行对比，如果有交集则将日历的时间数据删除
                if (CollectionUtils.isEmpty(calendarEntity.getCourseTime())) {
                    return;
                }
                Iterator<CourseTimeEntity> iterator = calendarEntity.getCourseTime().iterator();
                while (iterator.hasNext()) {
                    CourseTimeEntity next = iterator.next();
                    boolean flag = checkCourseTime(nowCourse, CourseTimeEntity::getStartTime, CourseTimeEntity::getEndTime, next.getStartTime(), next.getEndTime());
                    if (flag) {
                        iterator.remove();
                    }
                }
                //如果日历时间集合数据删没了，那么就证明当天课程已经满了  0:未满 1满
                if (CollectionUtils.isEmpty(calendarEntity.getCourseTime())) {
                    calendarEntity.setFullCourse(1);
                } else {
                    calendarEntity.setFullCourse(0);
                }
            }
            //获取节假日信息
            int calendarYear = LocalDate.parse(calendarEntity.getDate()).getYear();
            List<String> holiday = Optional.of(calendarYear)
                    .map(holidayMap::get)
                    .filter(CollectionUtils::isNotEmpty)
                    .orElseGet(() -> {
                        List<String> list = getHoliday(calendarYear);
                        holidayMap.put(calendarYear, list);
                        return list;
                    });
            //是否是节假日 1是 0:不是
            int holidayFlag = holiday.contains(calendarEntity.getDate()) ? 1 : 0;
            calendarEntity.setHoliday(holidayFlag);

            //是节假日又有需要跳过节假日
            if (holidayFlag == 1 && skipHoliday == 1) {
                calendarEntity.setSkipHoliday(1);//跳过节假日
                calendarEntity.setCourseTime(null);//清空课程时间
            } else {
                calendarEntity.setSkipHoliday(0);//不跳过节假日
            }
        });
    }

    /**
     * 获取老师未开始、进行中的课程
     *
     * @param teacherId 老师id
     * @param startDate 开始时间
     * @param endDate   结束时间
     * @return 课程列表
     */
    private List<CourseSchedule> getTeacherCourseTime(Long teacherId, String startDate, String endDate) {
        //未开始、进行中的课程
        List<String> statusList = Lists.newArrayList(CourseScheduleEnum.NOT_START.getCode(), CourseScheduleEnum.ING.getCode());
        //查询该老师指定时间段的课程
        return this.list(Wrappers.<CourseSchedule>lambdaQuery()
                .eq(CourseSchedule::getTeacherId, teacherId)
                .in(CourseSchedule::getStatus, statusList)
                .ge(CourseSchedule::getClassDate, startDate)
                .le(CourseSchedule::getClassDate, endDate)
        );
    }

    /**
     * 根据开始日期 结束日期 单结课时长 生成一个周期的每日课程时间数据
     *
     * @param firstDay            开始日期年月日
     * @param lastDay             最后一天的日期年月日
     * @param singleCourseMinutes 单结课时长
     * @param freeCourseMinutes   单课时休息时长
     */
    public List<CourseCalendarEntity> generateCourseData(LocalDate firstDay, LocalDate lastDay, Integer singleCourseMinutes, Integer freeCourseMinutes) {
        //获取上课的最早时间和最晚时间,将每日时间切片
        String sysStartTime = sysConfigService.findConfigValue(SysConfigConstant.COURSE_START_SETTING);
        String sysEndTime = sysConfigService.findConfigValue(SysConfigConstant.COURSE_END_SETTING);
        if (StringUtils.isBlank(sysStartTime) || StringUtils.isBlank(sysEndTime)) {
            throw new BizException("未查询到平台设置的每日最早上/最晚上课时间设置！");
        }
        //每日最早上课时间
        Date dayStartTime = DateUtil.toDateTime(firstDay + " " + sysStartTime + ":00");
        //每日最晚下课时间
        Date dayEndTime = DateUtil.toDateTime(firstDay + " " + sysEndTime + ":00");
        //每日日期数据
        List<CourseCalendarEntity> list = new ArrayList<>();
        //生成每日课程时间段 -根据单节课时长 及每日最早最晚时间 生成一天的时间切片
        List<CourseTimeEntity> dayTime = generateDayTime(singleCourseMinutes, freeCourseMinutes, dayStartTime, dayEndTime);
        int addDay = 0;
        //获取每日日期数据
        while (firstDay.isBefore(lastDay) || firstDay.isEqual(lastDay)) {
            //将每日上课时间时间添加到日历中
            CourseCalendarEntity entity = opsCourseDayTime(addDay, dayTime);
            //获取当前日期
            entity.setDate(firstDay.toString());
            list.add(entity);
            firstDay = firstDay.plusDays(1L);
            addDay++;
        }

        return list;
    }

    /**
     * 生成每日课程时间段数据
     * <p>根据单节课时长 及每日最早最晚时间 生成一天的时间切片
     *
     * @param singleCourseMinutes 单节课时长
     * @param freeCourseMinutes   单课时休息时长
     * @param startTime           每日最早上课时间
     * @param endTime             每日最晚下课时间
     */
    public List<CourseTimeEntity> generateDayTime(Integer singleCourseMinutes, Integer freeCourseMinutes, Date startTime, Date endTime) {
        List<CourseTimeEntity> result = new ArrayList<>();
        //划分每日课程时间
        Date edate = startTime;
        boolean flag = true;
        while (flag) {
            if (edate.getTime() > endTime.getTime()) {
                flag = false;
            } else {
                CourseTimeEntity timeEntity = new CourseTimeEntity();
                timeEntity.setStartTime(edate);
                //加时间得到课程结束时间
                edate = DateUtil.addMinutes(edate, singleCourseMinutes);
                timeEntity.setEndTime(edate);
                //再加上单节课休息时间
                edate = DateUtil.addMinutes(edate, freeCourseMinutes);
                result.add(timeEntity);
            }
        }
        result.remove(result.size() - 1);
        return result;
    }

    /**
     * 根据年份获取当年节假日
     *
     * @param year 年
     * @return 年月日
     */
    @Override
    public List<String> getHoliday(int year) {
        HolidaysFestivals holidaysFestivals = holidaysFestivalsService.queryByYear(year);
        if (Objects.isNull(holidaysFestivals)) {
            return Collections.emptyList();
        }
        if (StringUtils.isBlank(holidaysFestivals.getHolidaysFestivalsJson())) {
            return Collections.emptyList();
        }
        return WrapperUtil.toList(holidaysFestivals.getHolidaysFestivalsJson());
    }

    /**
     * 生成陪练课日历-用于学生购买指定老师陪练课
     *
     * @param param 传入参数
     *              <p> - teacherId 老师id
     *              <p> - studentId 学生id
     *              <p> - year 年
     *              <p> - month 月
     * @return 返回传入时间当月每日的剩余时间段
     */
    public List<CourseCalendarEntity> createPracticeCourseCalender(Map<String, Object> param) {
        //校验数据
        Long teacherId = WrapperUtil.toLong(param, "teacherId", "老师id不能为空!");
        Long studentId = WrapperUtil.toLong(param, "studentId", "学生id不能为空!");
        Integer year = WrapperUtil.toInt(param, "year", "日历的时间年份不能为空!");
        Integer month = WrapperUtil.toInt(param, "month", "日历的时间月份不能为空!");

        //获取开始日期
        LocalDate firstDay = LocalDate.of(year, month, 1);
        //根据firstDay计算出该月的最后一天
        LocalDate lastDay = firstDay.with(TemporalAdjusters.lastDayOfMonth());
        //查询老师陪练课设置
        TeacherFreeTime teacherTime = teacherFreeTimeService.getOne(Wrappers.<TeacherFreeTime>lambdaQuery()
                .eq(TeacherFreeTime::getDefaultFlag, true)
                .eq(TeacherFreeTime::getTeacherId, teacherId)
        );
        Optional.ofNullable(teacherTime).orElseThrow(() -> new BizException("未查询到老师陪练课设置！"));
        //根据老师设置的陪练课时间生成当月日历
        List<CourseCalendarEntity> courseCalendarEntities = generatePracticeCalendar(teacherTime, firstDay, lastDay);
        //查询老师及当前学生的课时
        Map<String, List<CourseTimeEntity>> nowCourseMap = getAllPracticeCourseTime(teacherId, studentId, firstDay.toString(), lastDay.toString());
        //和生成的日历对比,筛选出空余的课程时间
        opsCourseCalendarData(nowCourseMap, courseCalendarEntities, teacherTime.getSkipHolidayFlag() ? 1 : 0);
        return courseCalendarEntities;
    }

    /**
     * 生成老师陪练课日历
     * <p>陪练课的日历需要查询老师设置的陪练课时间，根据设置的时间来生成日历
     */
    private List<CourseCalendarEntity> generatePracticeCalendar(TeacherFreeTime teacherTime, LocalDate firstDay, LocalDate lastDay) {
        //将老师设置的陪练课时间结构化   key:：数字周几 1~7  value：具体的时间 开始时间-结束时间
        Map<Integer, JSONArray> teacherPracticeTime = getTeacherPracticeTime(teacherTime);
        //每日日期数据
        List<CourseCalendarEntity> list = new ArrayList<>();

        //查询系统配置开课/结束时间
        String star = sysConfigService.findConfigValue(SysConfigConstant.COURSE_START_SETTING);//系统开课时间
        String end = sysConfigService.findConfigValue(SysConfigConstant.COURSE_END_SETTING);//系统关课时间

        //获取每日日期数据
        while (firstDay.isBefore(lastDay) || firstDay.isEqual(lastDay)) {
            CourseCalendarEntity entity = new CourseCalendarEntity();
            //获取当前日期
            entity.setDate(firstDay.toString());
            //获取当前日期周几
            int weekNum = firstDay.getDayOfWeek().getValue();
            JSONArray jsonArray = teacherPracticeTime.get(weekNum);
            if (Objects.nonNull(jsonArray)) {
                List<CourseTimeEntity> timeEntities = jsonArray.stream()
                        .map(t -> getCourseTimeEntity(entity, (JSONObject) t))
                        .filter(f -> checkTime(f, star, end))
                        .collect(Collectors.toList());
                entity.setCourseTime(timeEntities);
            }
            list.add(entity);
            firstDay = firstDay.plusDays(1L);
        }
        return list;
    }

    private CourseTimeEntity getCourseTimeEntity(CourseCalendarEntity entity, JSONObject t) {
        //拼接时间
        BiFunction<String, String, Date> canCatTime = (date, time) -> DateUtil.toDateTime(date + " " + time);
        CourseTimeEntity timeEntity = new CourseTimeEntity();
        timeEntity.setStartTime(canCatTime.apply(entity.getDate(), t.getString("startTime")));
        timeEntity.setEndTime(canCatTime.apply(entity.getDate(), t.getString("endTime")));
        return timeEntity;
    }

    private boolean checkTime(CourseTimeEntity entity, String star, String end) {
        Date startTime = entity.getStartTime();
        Date endTime = entity.getEndTime();
        String classDate = DateUtil.dateToString(entity.getStartTime());
        Date s = DateUtil.strToDate(classDate + " " + star + ":00");
        Date e = DateUtil.strToDate(classDate + " " + end + ":00");
        if (startTime.before(s) || endTime.after(e)) {
            return false;
        }
        return true;
    }

    /**
     * 将每日上课时间时间添加到日历中
     *
     * @param addDay       添加的天数
     * @param timeEntities 每日上课时间集合
     */
    private CourseCalendarEntity opsCourseDayTime(int addDay, List<CourseTimeEntity> timeEntities) {
        CourseCalendarEntity entity = new CourseCalendarEntity();
        List<CourseTimeEntity> times = new ArrayList<>();
        timeEntities.forEach(time -> {
            CourseTimeEntity courseTimeEntity = new CourseTimeEntity();
            courseTimeEntity.setStartTime(DateUtil.addDays(time.getStartTime(), addDay));
            courseTimeEntity.setEndTime(DateUtil.addDays(time.getEndTime(), addDay));
            times.add(courseTimeEntity);
        });
        entity.setCourseTime(times);
        return entity;
    }

    /**
     * 将老师设置的陪练课时间结构化
     *
     * @return key:：数字周几 1~7  value：具体的时间 开始时间-结束时间
     */
    private Map<Integer, JSONArray> getTeacherPracticeTime(TeacherFreeTime teacherTime) {
        Map<Integer, JSONArray> teacherPracticeTime = new HashMap<>();
        //将老师设置的陪练课时间放入map中
        BiConsumer<String, Integer> timeCon = (timeStr, weekNum) -> {
            Optional.ofNullable(timeStr)
                    .map(JSONObject::parseArray)
                    .filter(CollectionUtils::isNotEmpty)
                    .ifPresent(objects -> teacherPracticeTime.put(weekNum, objects));
        };
        timeCon.accept(teacherTime.getMonday(), 1);
        timeCon.accept(teacherTime.getTuesday(), 2);
        timeCon.accept(teacherTime.getWednesday(), 3);
        timeCon.accept(teacherTime.getThursday(), 4);
        timeCon.accept(teacherTime.getFriday(), 5);
        timeCon.accept(teacherTime.getSaturday(), 6);
        timeCon.accept(teacherTime.getSunday(), 7);
        return teacherPracticeTime;
    }

    /**
     * 查询老师及当前学生的课时和目前直播课排课锁定在缓存的的课时
     *
     * @param teacherId 老师id
     * @param studentId 学员id
     * @param startDate 开始日期
     * @param endDate   结束日期
     */
    private Map<String, List<CourseTimeEntity>> getAllPracticeCourseTime(Long teacherId, Long studentId, String startDate, String endDate) {
        //获取老师未开始、进行中的课程
        List<CourseSchedule> courseList = getTeacherCourseTime(teacherId, startDate, endDate);
        if (CollectionUtils.isEmpty(courseList)) {
            courseList = new ArrayList<>();
        }
        //查询学生的课程;
        Map<String, Object> param = new HashMap<>();
        param.put("studentId", studentId);
        param.put("startClassDate", startDate);
        param.put("endClassDate", endDate);
        param.put("statusList", Lists.newArrayList(CourseScheduleEnum.NOT_START.getCode(), CourseScheduleEnum.ING.getCode()));
        List<CourseSchedule> studentCourse = baseMapper.queryStudentCourse(param);
        //将数据合并
        if (CollectionUtils.isNotEmpty(studentCourse)) {
            courseList.addAll(studentCourse);
        }
        //查询当前老师直播课当前排课锁定的课时缓存-因为在学生购买老师课程时候老师可能正在排直播课，并且也将时间已经预排好了，所以需要查询缓存中的课时
        RMap<Long, List<CourseTimeEntity>> lockTimeCache = courseGroupService.getLiveLockTimeCache(teacherId);
        if (lockTimeCache.isExists()) {
            List<CourseSchedule> lockCourseList = new ArrayList<>();
            List<CourseTimeEntity> timeEntities = lockTimeCache.get(teacherId);
            if (CollectionUtils.isNotEmpty(timeEntities)) {
                timeEntities.forEach(courseTimeEntity -> {
                    CourseSchedule lockCourse = new CourseSchedule();
                    String ymd = DateUtil.dateToString(courseTimeEntity.getStartTime());
                    lockCourse.setClassDate(DateUtil.toDate(ymd));
                    lockCourse.setStartTime(courseTimeEntity.getStartTime());
                    lockCourse.setEndTime(courseTimeEntity.getEndTime());
                    lockCourseList.add(lockCourse);
                });
            }
            if (CollectionUtils.isNotEmpty(lockCourseList)) {
                courseList.addAll(lockCourseList);
            }
        }
        return getCourseListMap(courseList);
    }

    /**
     * 将课时数据结构化
     *
     * @param courseList 课程列表
     * @return key：课程日期-年月日  value：课程
     */
    private Map<String, List<CourseTimeEntity>> getCourseListMap(List<CourseSchedule> courseList) {
        Map<String, List<CourseTimeEntity>> nowCourse = new HashMap<>();
        if (CollectionUtils.isNotEmpty(courseList)) {
            WrapperUtil.groupList(courseList, CourseSchedule::getClassDate)
                    .forEach((k, v) -> {
                        List<CourseTimeEntity> value = new ArrayList<>();
                        v.forEach(course -> {
                            CourseTimeEntity courseTimeEntity = new CourseTimeEntity();
                            courseTimeEntity.setStartTime(course.getStartTime());
                            courseTimeEntity.setEndTime(course.getEndTime());
                            value.add(courseTimeEntity);
                        });
                        nowCourse.put(DateUtil.dateToString(k), value);
                    });
        }
        return nowCourse;
    }

    /**
     * 获取当前登录人信息
     */
    private SysUser getSysUser() {
        return Optional.ofNullable(sysUserFeignService.queryUserInfo())
                .orElseThrow(() -> new BizException("用户不存在"));
    }

    private SysUser getSysUser(Long userId) {
        return Optional.ofNullable(userId)
                .map(sysUserFeignService::queryUserById)
                .orElseThrow(() -> new BizException("用户不存在"));
    }

    /**
     * 老师端-首页-我的课程-陪练课
     * search:{"classMonth":"2022-03","status":"COMPLETE","subjectId":1}
     * <p>
     * 老师端-首页-课后评价
     * search:{"classMonth":"2022-03","replied":0,"studentName":"测试王"}
     * <p>
     * 老师端-我的-我的主页
     * search:{"classMonth":"2022-03"}
     * <p>
     * 老师端-课表
     * search:{"classDate":"2022-03-27"}
     * search:{"classMonth":"2022-03"}
     * <p>
     * 老师端-课表-陪练课
     * search:{"classDate":"2022-03-27"}
     *
     * @Description: 根据老师id查询购课学员
     * @Author: cy
     * @Date: 2022/4/11
     */
    public IPage<MyCourseVo> queryTeacherPracticeCourse(IPage<MyCourseVo> page, MyCourseSearch search) {
        Integer replied = search.getReplied();
        if (replied != null) {
            search.setStatus(CourseScheduleEnum.COMPLETE.getCode());
            List<MyCourseVo> list = baseMapper.queryTeacherPracticeCourse(page, monthToDate(search));
            if (replied == 0) {//未评价
                return page.setRecords(list.stream().filter(s -> {
                    return s.getTeacherReplied() == 0;
                }).collect(Collectors.toList()));
            }

            if (replied == 1) {//已评价
                return page.setRecords(list.stream().filter(s -> {
                    return s.getTeacherReplied() == 1;
                }).collect(Collectors.toList()));
            }
        }

        return page.setRecords(baseMapper.queryTeacherPracticeCourse(page, monthToDate(search)));
    }

    @Override
    public Integer countTeacherNoDecorateHomework(HomeworkSearch homeworkSearch) {

        // 陪练课 课程状态为完成，有学生考勤记录 没有布置作业
        return baseMapper.countTeacherNoDecorateHomework(homeworkSearch);
    }

    /**
     * 按月份计算开始结束日期
     *
     * @param search
     * @return
     */
    public MyCourseSearch monthToDate(MyCourseSearch search) {
        String classMonth = search.getClassMonth();
        if (StringUtils.isBlank(classMonth)) {
            return search;
        }
        String[] classDateSp = classMonth.split("-");
        LocalDate firstDay;
        try {
            firstDay = LocalDate.of(Integer.parseInt(classDateSp[0]), Integer.parseInt(classDateSp[1]), 1);
        } catch (Exception e) {
            throw new BizException("查询时间格式不正确 [" + classMonth + "]");
        }
        //本月的最后一天
        LocalDate lastDay = firstDay.with(TemporalAdjusters.lastDayOfMonth());
        search.setStartDate(firstDay.toString());
        search.setEndDate(lastDay.toString());
        return search;
    }

    /**
     * @Description: 查询老师约课日历
     * @Author: cy
     * @Date: 2022/4/13
     */
    public Set<String> queryCourseSchedule(MyCourseSearch search) {
        return baseMapper.queryCourseSchedule(monthToDate(search));
    }

    /**
     * @Description: 根据学生id查询老师
     * @Author: cy
     * @Date: 2022/4/13
     */
    public IPage<MyCourseVo> queryStudentPracticeCourse(IPage<MyCourseVo> page, MyCourseSearch search) {
        return page.setRecords(baseMapper.queryStudentPracticeCourse(page, monthToDate(search)));
    }

    @Override
    public boolean checkTeacherCourseSchedule(Long teacherId, Long courseId) {
        CourseSchedule courseSchedule = this.getById(courseId);
        if (courseSchedule == null) {
            return false;
        }
        return teacherId.equals(courseSchedule.getTeacherId());
    }

    /**
     * @Description: 查询学生约课日历
     * @Author: cy
     * @Date: 2022/4/14
     */
    public Set<String> queryCourseScheduleStudent(MyCourseSearch search) {
        return baseMapper.queryCourseScheduleStudent(monthToDate(search));
    }

    @Override
    public boolean checkStudentCourseSchedule(Long studentId, Long courseId) {
        List<CourseScheduleStudentPayment> paymentList = courseScheduleStudentPaymentService.getByCourseId(courseId);
        if (CollectionUtils.isEmpty(paymentList)) {
            return false;
        }
        for (CourseScheduleStudentPayment courseScheduleStudentPayment : paymentList) {
            if (courseScheduleStudentPayment != null && studentId.equals(courseScheduleStudentPayment.getUserId())) {
                return true;
            }
        }
        return false;
    }

    /**
     * @Description: 老师端-课表-日历-用户
     * @Author: cy
     * @Date: 2022/4/18
     */
    @Override
    public Map<String, Object> queryCourseUser(MyCourseSearch search) {
        List<CourseStudent> studentList = baseMapper.queryCourseUser(search);

        Map<String, String> sysConfig = new HashMap<>();
        //提前XX分钟创建/进入陪练课房间时间
        sysConfig.put("practiceStartTime", sysConfigService.findConfigValue(SysConfigConstant.PRE_CREATE_PRACTICE_ROOM_MINUTE));
        //陪练课结束后，XX分钟关闭房间
        sysConfig.put("practiceEndTime", sysConfigService.findConfigValue(SysConfigConstant.DESTROY_EXPIRED_PRACTICE_ROOM_MINUTE));
        //提前XX分钟创建/进入直播房间的时间
        sysConfig.put("liveStartTime", sysConfigService.findConfigValue(SysConfigConstant.PRE_CREATE_LIVE_ROOM_MINUTE));
        //直播结束后，XX分钟关闭房间
        sysConfig.put("liveEndTime", sysConfigService.findConfigValue(SysConfigConstant.DESTROY_EXPIRED_LIVE_ROOM_MINUTE));
        //提前XX分钟创建/进入琴房课房间时间
        sysConfig.put("pianoStartTime", sysConfigService.findConfigValue(SysConfigConstant.PRE_CREATE_PIANO_ROOM_MINUTE));
        //琴房课结束后，XX分钟关闭房间
        sysConfig.put("pianoEndTime", sysConfigService.findConfigValue(SysConfigConstant.DESTROY_EXPIRED_PIANO_ROOM_MINUTE));

        Map map = new HashMap();
        map.put("sysConfig", sysConfig);
        map.put("studentList", studentList);
        return map;
    }

    /**
     * @Description: 学生端-首页-陪练课老师列表
     * @Author: cy
     * @Date: 2022/4/19
     */
    @Override
    public IPage<PracticeTeacherVo> teacherList(IPage<PracticeTeacherVo> page, PracticeTeacherSearch search) {
        if (search.getScreen() != null && search.getScreen()) {
            List<Long> teacherIdList = getTeacherId(30);
            if (CollectionUtils.isEmpty(teacherIdList)) {
                return null;
            }
            search.setTeacherIdList(teacherIdList);
            return page.setRecords(baseMapper.teacherList(page, search));
        } else {
            search.setTeacherIdList(baseMapper.teacherIdList());
            return page.setRecords(baseMapper.teacherList(page, search));
        }
    }

    /**
     * @Description: 学生端-课表-日历-用户
     * @Author: cy
     * @Date: 2022/4/20
     */
    public Map<String, Object> queryCourseTeacher(MyCourseSearch search) {
        List<CourseStudent> teacherList = baseMapper.queryCourseTeacher(search);
        Map<String, String> sysConfig = new HashMap<>();
        //提前XX分钟创建/进入陪练课房间时间
        sysConfig.put("practiceStartTime", sysConfigService.findConfigValue(SysConfigConstant.PRE_CREATE_PRACTICE_ROOM_MINUTE));
        //陪练课结束后，XX分钟关闭房间
        sysConfig.put("practiceEndTime", sysConfigService.findConfigValue(SysConfigConstant.DESTROY_EXPIRED_PRACTICE_ROOM_MINUTE));
        //提前XX分钟创建/进入直播房间的时间
        sysConfig.put("liveStartTime", sysConfigService.findConfigValue(SysConfigConstant.PRE_CREATE_LIVE_ROOM_MINUTE));
        //直播结束后，XX分钟关闭房间
        sysConfig.put("liveEndTime", sysConfigService.findConfigValue(SysConfigConstant.DESTROY_EXPIRED_LIVE_ROOM_MINUTE));
        //提前XX分钟创建/进入琴房课房间时间
        sysConfig.put("pianoStartTime", sysConfigService.findConfigValue(SysConfigConstant.PRE_CREATE_PIANO_ROOM_MINUTE));
        //琴房课结束后，XX分钟关闭房间
        sysConfig.put("pianoEndTime", sysConfigService.findConfigValue(SysConfigConstant.DESTROY_EXPIRED_PIANO_ROOM_MINUTE));

        Map map = new HashMap();
        map.put("sysConfig", sysConfig);
        map.put("studentList", teacherList);
        return map;
    }

    /**
     * 学生-查询直播课
     *
     * @param param 传入参数
     *              <p> - studentId 学生id
     *              <p> - courseState 课程状态 NOT_START未开始 ING进行中 COMPLETE已完成 CANCEL已取消
     *              <p> - classDate 查询时间-年月
     *              <p> - subjectId 声部id
     */
    @Override
    public PageInfo<CourseStudent> queryStudentLiveCourse(Map<String, Object> param) {
        String classDate = WrapperUtil.toStr(param, "classDate");
        String[] classDateSp = classDate.split("-");
        LocalDate firstDay;
        try {
            firstDay = LocalDate.of(Integer.parseInt(classDateSp[0]), Integer.parseInt(classDateSp[1]), 1);
        } catch (Exception e) {
            throw new BizException("查询时间格式不正确 [" + classDate + "]");
        }
        //本月的最后一天
        LocalDate lastDay = firstDay.with(TemporalAdjusters.lastDayOfMonth());
        param.put("type", CourseScheduleEnum.LIVE.getCode());
        param.put("startDate", firstDay.toString());
        param.put("endDate", lastDay.toString());
        param.put("orderState", OrderStatusEnum.PAID.getCode());
        Page<CourseStudent> pageInfo = PageUtil.getPageInfo(param);
        pageInfo.setAsc(" cs.start_time_");
        return PageUtil.pageInfo(baseMapper.queryStudentLiveCourse(pageInfo, param));
    }

    /**
     * 批量检查老师课时在数据库是否重复
     *
     * @param teacherId 老师id
     * @param timeList  时间集合
     */
    private <T> void batchCheckTeacherCourseTime(Long teacherId, List<T> timeList, Function<T, Date> startTimeFun, Function<T, Date> endTimeFun) {
        //再校验数据库中课程时间和传入时间是否有交集
        timeList.forEach(o -> {
            boolean checkDataTime = this.checkTeacherCourseTime(teacherId, startTimeFun.apply(o), endTimeFun.apply(o));
            if (checkDataTime) {
                throw new BizException("预计安排在" + DateUtil.dateToString(startTimeFun.apply(o), "yyyy年MM月dd号 HH点mm分") + "的课程时间存在冲突!");
            }
        });
    }

    /**
     * 批量检查学生课时在数据库是否重复
     *
     * @param studentId 老师id
     * @param timeList  时间集合
     */
    private <T> void batchCheckStudentCourseTime(Long studentId, List<T> timeList, Function<T, Date> startTimeFun, Function<T, Date> endTimeFun) {
        //再校验数据库中课程时间和传入时间是否有交集
        timeList.forEach(o -> {
            boolean checkDataTime = this.checkStudentCourseTime(studentId, startTimeFun.apply(o), endTimeFun.apply(o));
            if (checkDataTime) {
                SysUser sysUser = sysUserFeignService.queryUserById(studentId);
                throw new BizException("学生:" + sysUser.getUsername() + ",预计安排在" + DateUtil.dateToString(startTimeFun.apply(o), "yyyy年MM月dd号 HH点mm分") + "的课程时间存在冲突!");
            }
        });
    }

    /**
     * @Description: 陪练课购买记录
     * @Author: cy
     * @Date: 2022/5/16
     */
    @Transactional(rollbackFor = Exception.class)
    public HttpResponseResult<OrderCreateRes> buyPracticeCourseTran(OrderReq.OrderReqInfo orderReqInfo) {
        log.info("buyPracticeCourse  param:{}", JSON.toJSONString(orderReqInfo));
        Long studentId = orderReqInfo.getUserId();

        //校验学生信息
        getSysUser(studentId);

        ObjectMapper objectMapper = new ObjectMapper();
        PracticeScheduleDto scheduleDto = objectMapper.convertValue(orderReqInfo.getBizContent(), PracticeScheduleDto.class);
        List<CourseScheduleDate> dateList = scheduleDto.getClassTime();
        Integer courseNum = scheduleDto.getCourseNum();//课程数

        //校验课时数
        if (dateList.size() != courseNum) {
            throw new BizException("课程数与课时数不符");
        }
        BigDecimal price = baseMapper.selectPrice(scheduleDto.getTeacherId(), scheduleDto.getSubjectId());//老师设置声部价格
        BigDecimal decimal = new BigDecimal(courseNum);//选购课程节数
        BigDecimal multiply = price.multiply(decimal);//预计总价
        if (multiply.compareTo(scheduleDto.getCoursePrice()) != 0) {
            throw new BizException("价格异常。预计价格:{},实际价格:{}", multiply, orderReqInfo.getActualPrice());
        }

        //获取老师锁课缓存
        RMap<Long, List<CourseTimeEntity>> map = courseGroupService.getExpireLiveLockTimeCache(scheduleDto.getTeacherId());
        if (map.isExists()) {
            List<CourseTimeEntity> courseTimeCache = map.get(scheduleDto.getTeacherId());
            //校验缓存中的时间和当前自动生成的时间有没有重复
            dateList.forEach(item ->
            {
                if (checkCourseTime(courseTimeCache, CourseTimeEntity::getStartTime, CourseTimeEntity::getEndTime, item.getStartTime(), item.getEndTime())) {
                    throw new BizException("与缓存中时间存在中途");
                }
            });
        }

        //批量检查老师课时在数据库是否重复
        batchCheckTeacherCourseTime(scheduleDto.getTeacherId(), dateList, CourseScheduleDate::getStartTime, CourseScheduleDate::getEndTime);
        //批量检查学生课时在数据库是否重复
        batchCheckStudentCourseTime(studentId, dateList, CourseScheduleDate::getStartTime, CourseScheduleDate::getEndTime);

        String orderNo = orderReqInfo.getOrderNo();
        scheduleDto.setType(CourseScheduleEnum.PRACTICE.getCode());
        scheduleDto.setStatus(CourseGroupEnum.NOT_SALE.getCode());
        scheduleDto.setMixStudentNum(1);
        scheduleDto.setStudentId(studentId);

        //写入course_group
        baseMapper.addCourseGroup(scheduleDto);
        Long groupId = scheduleDto.getGroupId();

        //根据老师id获取默认配置声部原价
        TeacherSubjectPrice teacherSubjectPrice = teacherFreeTimeDao.selectSubjectPrice(scheduleDto.getTeacherId(), scheduleDto.getSubjectId());
        BigDecimal subjectPrice = teacherSubjectPrice.getSubjectPrice();

        //每课实际价格
        Map<Integer, BigDecimal> courseAveragePrice = WrapperUtil.getAveragePrice(scheduleDto.getCourseNum(), orderReqInfo.getActualPrice());

        List<CourseScheduleDate> classTime = scheduleDto.getClassTime();
        for (int i = 0; i < classTime.size(); i++) {
            CourseScheduleDate date = classTime.get(i);
            CourseSchedule schedule = new CourseSchedule();
            schedule.setClassDate(date.getClassDate());
            schedule.setStartTime(date.getStartTime());
            schedule.setEndTime(date.getEndTime());
            schedule.setCourseGroupId(groupId);
            schedule.setType(CourseScheduleEnum.PRACTICE.getCode());
            schedule.classNum(i + 1);
            schedule.setTeacherId(scheduleDto.getTeacherId());
            schedule.setLock(1);
            schedule.setStatus(CourseScheduleEnum.NOT_START.getCode());
            schedule.setCreatedBy(scheduleDto.getStudentId());
            //写入course_schedule
            baseMapper.insert(schedule);

            Long scheduleId = schedule.getId();
            CourseScheduleStudentPayment payment = new CourseScheduleStudentPayment();
            payment.setUserId(scheduleDto.getStudentId());
            payment.setCourseId(scheduleId);
            payment.setCourseGroupId(groupId);
            payment.setCourseType(CourseScheduleEnum.PRACTICE.getCode());
            payment.setOrderNo(orderNo);
            payment.setOriginalPrice(subjectPrice);//原价
            payment.setExpectPrice(subjectPrice);//预计价格
            payment.setActualPrice(courseAveragePrice.get(i + 1));//实际价格
            //写入course_schedule_student_payment
            courseScheduleStudentPaymentService.save(payment);
        }

        OrderCreateRes orderCreateRes = new OrderCreateRes();
        orderCreateRes.setRes(true);
        orderCreateRes.setMerchId(scheduleDto.getTeacherId());
        orderCreateRes.setBizId(groupId);
        orderCreateRes.setOriginalPrice(scheduleDto.getCoursePrice());
        orderCreateRes.setExpectPrice(scheduleDto.getCoursePrice());
        orderCreateRes.setGoodNum(scheduleDto.getCourseNum());
        orderCreateRes.setGoodType(GoodTypeEnum.PRACTICE);
        HttpResponseResult<OrderCreateRes> httpResponseResult = new HttpResponseResult<>();
        httpResponseResult.setData(orderCreateRes);
        log.info("buyPracticeCourse  return {}", httpResponseResult);
        return httpResponseResult;
    }

    /**
     * @Description: 学生购买陪练课
     * @Author: cy
     * @Date: 2022/4/21
     */
    public HttpResponseResult<OrderCreateRes> buyPracticeCourse(OrderReq.OrderReqInfo orderReqInfo) {
        ObjectMapper objectMapper = new ObjectMapper();
        PracticeScheduleDto scheduleDto = objectMapper.convertValue(orderReqInfo.getBizContent(), PracticeScheduleDto.class);

        RLock lock = redissonClient.getLock("teacherId:" + scheduleDto.getTeacherId());
        try {
            if (lock.tryLock(10, 60, TimeUnit.SECONDS)) {
                return buyPracticeCourseTran(orderReqInfo);
            }
        } catch (InterruptedException e) {
            throw new BizException("创建陪练课失败:{}", e.getMessage());
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
        return HttpResponseResult.failed();
    }

    /**
     * 学生购买陪练课-成功-回调
     */
    @Transactional(rollbackFor = Exception.class)
    public void buyPracticeCourseSuccess(UserOrderDetailVo orderParam) {
        log.info("buyPracticeCourseSuccess  param:{}", JSON.toJSONString(orderParam));
        String orderNo = orderParam.getOrderNo();
        List<CourseScheduleStudentPaymentVo> paymentList = paymentDao.selectPaymentList(orderNo);
        if (CollectionUtils.isEmpty(paymentList)) {
            throw new BizException("订单不存在!");
        }

        //lock_改为0,课程组状态改为ING
        List<Long> scheduleIds = paymentList.stream().map(CourseScheduleStudentPayment::getCourseId).collect(Collectors.toList());
        baseMapper.updateLock(scheduleIds);
        UpdateWrapper<CourseGroup> warp = new UpdateWrapper<>();
        warp.set("status_", "ING").eq("id_", paymentList.get(0).getCourseGroupId());
        courseGroupService.update(warp);

        //查询陪练课服务费
        String practiceServiceRateStr = sysConfigService.findConfigValue(SysConfigConstant.PRACTICE_SERVICE_FEE);
        BigDecimal practiceServiceRate = new BigDecimal(practiceServiceRateStr).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);

        //写course_schedule_teacher_salary
        Long teacherId = null;
        Long studentId = null;
        Date now = new Date();
        List<CourseScheduleTeacherSalary> teacherSalaryList = new ArrayList<>();
        for (CourseScheduleStudentPaymentVo payment : paymentList) {
            BigDecimal expectPrice = payment.getExpectPrice();//预计价格

            CourseScheduleTeacherSalary teacherSalary = new CourseScheduleTeacherSalary();
            teacherSalary.setTeacherId(payment.getTeacherId());
            teacherSalary.setStudentId(payment.getUserId());
            teacherSalary.setCourseScheduleId(payment.getCourseId());
            teacherSalary.setCourseGroupType(payment.getCourseType());
            teacherSalary.setCourseGroupId(payment.getCourseGroupId());
            teacherSalary.setClassNum(payment.getClassNum());
            teacherSalary.setExpectSalary(expectPrice);//预计薪水
            BigDecimal actualSalary = expectPrice.subtract(expectPrice.multiply(practiceServiceRate)).setScale(2, RoundingMode.HALF_UP);
            teacherSalary.setActualSalary(actualSalary);//实际薪水(扣除手续费)
            teacherSalary.setReduceSalary(expectPrice.subtract(actualSalary));//扣除薪水 = 预计 - 实际
            teacherSalary.setReduceSalaryRemark(SysConfigConstant.PRACTICE_SERVICE_FEE);//扣除原因
            teacherSalary.setStatus(TeacherSalaryEnum.NOT_START.getCode());
            teacherSalary.setCreateTime(now);

            if (teacherSalary.getExpectSalary().compareTo(BigDecimal.ZERO) > 0) {
                teacherSalaryList.add(teacherSalary);
            }

            teacherId = payment.getTeacherId();
            studentId = payment.getUserId();
        }
        if (CollectionUtils.isNotEmpty(teacherSalaryList)) {
            courseScheduleTeacherSalaryService.getDao().insertBatch(teacherSalaryList);
            log.info("buyPracticeCourseSuccess ok");
            //写老师账户
            addTeacherAccount(orderParam, teacherSalaryList);
        }

        Set<Long> studentIds = new HashSet();
        studentIds.add(studentId);
        try {
            imUserFriendService.saveUserFriend(teacherId, studentIds);
            log.info("保存用户通讯录成功,teacherId:{},studentIds:{}", teacherId, studentIds);
        } catch (Exception e) {
            log.error("保存用户通讯录失败,teacherId:{},studentIds:{},e:{}", teacherId, studentIds, e);
        }

        //消息推送
        try {
            practiceSend(paymentList.get(0).getTeacherId(), orderParam.getUserId(), paymentList.size() + "", orderNo);
        } catch (Exception e) {
            log.error("陪练课消息推送失败");
        }
    }

    //写入老师账户
    private void addTeacherAccount(UserOrderDetailVo orderParam, List<CourseScheduleTeacherSalary> teacherSalaryList) {
        //获取账期时间
        Date accountPeriodTime = userOrderService.getAccountPeriodTime(orderParam);

        //老师待入账
        List<UserAccountRecordDto> userAccountRecordDtos = new ArrayList<>();
        for (CourseScheduleTeacherSalary salary : teacherSalaryList) {
            UserAccountRecordDto dto = new UserAccountRecordDto(orderParam.getMerchId(), PostStatusEnum.WAIT, salary.getActualSalary(),
                    InOrOutEnum.IN, AccountBizTypeEnum.PRACTICE, salary.getCourseScheduleId(), orderParam.getGoodName(), orderParam.getOrderNo(), orderParam.getUserId(), accountPeriodTime);
            userAccountRecordDtos.add(dto);
        }

        if (CollectionUtils.isNotEmpty(userAccountRecordDtos)) {
            userAccountService.accountRecord(userAccountRecordDtos);
        }
    }

    /**
     * 学生购买陪练课-失败-回调
     */
    @Transactional(rollbackFor = Exception.class)
    public void buyPracticeCourseFailed(UserOrderDetailVo orderParam) {
        String orderNo = orderParam.getOrderNo();
        List<CourseScheduleStudentPayment> paymentList = paymentDao.selectList(Wrappers.<CourseScheduleStudentPayment>lambdaQuery()
                .eq(CourseScheduleStudentPayment::getOrderNo, orderNo));
        if (CollectionUtils.isEmpty(paymentList)) {
            throw new BizException("订单不存在!");
        }

        List<Long> scheduleIds = paymentList.stream().map(CourseScheduleStudentPayment::getCourseId).collect(Collectors.toList());
        Long courseGroupId = paymentList.get(0).getCourseGroupId();

        //删除course_schedule_student_payment数据
        courseScheduleStudentPaymentService.remove(Wrappers.<CourseScheduleStudentPayment>lambdaQuery()
                .eq(CourseScheduleStudentPayment::getOrderNo, orderNo));
        //删除course_schedule数据
        this.removeByIds(scheduleIds);
        //删除course_group数据
        courseGroupService.removeById(courseGroupId);
        log.info("buyPracticeCourseFailed ok");
    }

    /**
     * @Description: 老师端-课表-日历-调课
     * @Author: cy
     * @Date: 2022/4/21
     */
    @Override
    public void courseAdjust(CourseAdjustVo adjustVo, Long teacherId) {
        Integer courseId = adjustVo.getCourseId();
        Date classDate = adjustVo.getClassDate();
        Date startTime = adjustVo.getStartTime();
        Date endTime = adjustVo.getEndTime();
        Date now = new Date();

        if (endTime == null) {
            CourseSchedule courseSchedule = baseMapper.selectById(courseId);
            Integer minutes = Math.toIntExact(ChronoUnit.MINUTES.between(Instant.ofEpochMilli(courseSchedule.getStartTime().getTime()), Instant.ofEpochMilli(courseSchedule.getEndTime().getTime())));
            endTime = DateUtil.offsetMinute(startTime, minutes);
            adjustVo.setEndTime(endTime);
        }

        //校验时间是否为未来时刻
        if (startTime.before(now) || endTime.before(now)) {
            throw new BizException("排课日期必须大于当前日期");
        }
        if (!(DateUtil.dateToString(classDate).equals(DateUtil.dateToString(startTime))) ||
                !(DateUtil.dateToString(startTime).equals(DateUtil.dateToString(endTime)))) {
            throw new BizException("排课日期必须为同天");
        }

        //校验课程是否存在
        CourseSchedule schedule = baseMapper.selectOne(Wrappers.<CourseSchedule>lambdaQuery()
                .eq(CourseSchedule::getId, courseId)
                .eq(CourseSchedule::getLock, 0)
                .eq(CourseSchedule::getStatus, CourseScheduleEnum.NOT_START)
                .eq(CourseSchedule::getType, CourseScheduleEnum.PRACTICE));
        if (ObjectUtil.isEmpty(schedule)) {
            throw new BizException("无法调整该课程,课程不存在");
        }

        //校验系统配置上下课时间
        String star = sysConfigService.findConfigValue(SysConfigConstant.COURSE_START_SETTING);//系统开课时间
        String end = sysConfigService.findConfigValue(SysConfigConstant.COURSE_END_SETTING);//系统关课时间
        Date s = DateUtil.strToDate(DateUtil.dateToString(classDate) + " " + star + ":00");
        Date e = DateUtil.strToDate(DateUtil.dateToString(classDate) + " " + end + ":00");
        if (startTime.before(s) || endTime.after(e)) {
            throw new BizException("调整时间必须在{}到{}之间", star, end);
        }

        //查询是否有人购买
        CourseScheduleStudentPayment studentPayment = paymentDao.selectOne(Wrappers.<CourseScheduleStudentPayment>lambdaQuery()
                .eq(CourseScheduleStudentPayment::getCourseId, courseId)
                .eq(CourseScheduleStudentPayment::getCourseType, CourseScheduleEnum.PRACTICE));
        if (ObjectUtil.isEmpty(studentPayment)) {
            throw new BizException("课程无人购买");
        }

        //课程改为过去式避免校验时报错
        baseMapper.update(null, Wrappers.<CourseSchedule>lambdaUpdate()
                .eq(CourseSchedule::getId, adjustVo.getCourseId())
                .set(CourseSchedule::getStartTime, DateUtil.getYesterday())
                .set(CourseSchedule::getEndTime, DateUtil.getYesterday()));

        CourseTimeEntity timeEntity = new CourseTimeEntity();
        timeEntity.setStartTime(startTime);
        timeEntity.setEndTime(endTime);
        List<CourseTimeEntity> timeList = Arrays.asList(timeEntity);
        //校验老师课程是否冲突
        this.batchCheckTeacherCourseTime(teacherId, timeList, CourseTimeEntity::getStartTime, CourseTimeEntity::getEndTime);
        //校验学生课程是否冲突
        this.batchCheckTeacherCourseTime(studentPayment.getUserId(), timeList, CourseTimeEntity::getStartTime, CourseTimeEntity::getEndTime);

        baseMapper.courseAdjust(adjustVo);

        // 课程调整后给学生发消息
        sendCourseAdjustMessage(teacherId,studentPayment.getUserId(),adjustVo.getCourseId(),adjustVo.getStartTime());
    }

    //{老师昵称}已将{课程类型陪练课、琴房课}{课程名称}课调整到{上课时间yyyy-MM-dd HH:mm:ss}，请注意查看
    private void sendCourseAdjustMessage(Long teacherId,Long studentId,Integer courseScheduleId,Date date) {
        //  陪练课调整模板
        try {
            //  发送消息
            SysUser teacher = sysUserFeignService.queryUserById(teacherId);
            SysUser student = sysUserFeignService.queryUserById(studentId);

            CourseSchedule courseSchedule = this.getById(courseScheduleId);
            CourseGroup courseGroup = courseGroupService.getById(courseSchedule.getCourseGroupId());

            String courseName = courseGroup.getName() + "-第" + courseSchedule.getClassNum() + "课";

            String dateString = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().format(
                    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

            Map<Long, String> receivers = new HashMap<>();
            receivers.put(studentId, student.getPhone());

            sysMessageService.batchSendMessage(MessageSenderPluginContext.MessageSender.JIGUANG, MessageTypeEnum.PRACTICE_ADJUST, receivers, null,
                                               0, null, ClientEnum.STUDENT.getCode(), teacher.getUsername(),"陪练课",courseName,dateString);

        } catch (Exception e) {
            log.error("陪练课调整发送消息失败--> {}", e.fillInStackTrace());
        }
    }

    /**
     * @Description: 首页-直播课&视频课
     * @Author: cy
     * @Date: 2022/4/22
     */
    @Override
    public StudentHomePage queryLiveAndVideo(Long studentId, Long teacherId, YesOrNoEnum appAuditVersion) {
        StudentHomePage homePage = new StudentHomePage();
        if (appAuditVersion == null) {
            appAuditVersion = YesOrNoEnum.NO;
        }
        if (teacherId == null) {
        	Student student = studentService.getById(studentId);
        	Long subjectId = null;
            if (student != null && student.getSubjectId() != null) {
            	subjectId = Long.parseLong(student.getSubjectId());
            }
            
            homePage.setLiveList(baseMapper.selectLive(appAuditVersion, subjectId));
            homePage.setVideoList(baseMapper.selectVideo(appAuditVersion, subjectId));
            homePage.setRecentCourses(baseMapper.selectRecentCourses(studentId));
        }
        if (studentId == null) {
            StudentHomePage.RecentCourses coursesPractice = baseMapper.selectRecentCoursesPractice(teacherId);
            StudentHomePage.RecentCourses coursesLive = baseMapper.selectRecentCoursesLive(teacherId);//直播课&琴房课共用coursesLive

            if (coursesPractice == null && coursesLive != null) {
                homePage.setRecentCourses(coursesLive);
            }
            if (coursesPractice != null && coursesLive == null) {
                homePage.setRecentCourses(coursesPractice);
            }

            if (coursesPractice != null && coursesLive != null) {
                Date practiceTime = coursesPractice.getCourseStartTime();
                Date liveTime = coursesLive.getCourseStartTime();

                if (practiceTime.compareTo(liveTime) == -1) {
                    homePage.setRecentCourses(coursesPractice);
                } else {
                    homePage.setRecentCourses(coursesLive);
                }
            }
        }
        return homePage;
    }

    /**
     * @Description: 查询老师陪练课配置
     * @Author: cy
     * @Date: 2022/4/26
     */
    public List<TeacherSubjectPrice> teacherSubjectPrice(Long teacherId) {
        TeacherFreeTime teacherFreeTime = teacherFreeTimeDao.selectOne(Wrappers.<TeacherFreeTime>lambdaQuery()
                .eq(TeacherFreeTime::getTeacherId, teacherId)
                .eq(TeacherFreeTime::getEnableFlag, 1)
                .eq(TeacherFreeTime::getDefaultFlag, 1));

        if (teacherFreeTime == null) {
            log.info("未查到陪练课配置,老师id:{}", teacherId);
            return new ArrayList<>();
        }

        if ((teacherFreeTime.getMonday() == null || teacherFreeTime.getMonday().equals("[]")) &&
                (teacherFreeTime.getThursday() == null || teacherFreeTime.getThursday().equals("[]")) &&
                (teacherFreeTime.getWednesday() == null || teacherFreeTime.getWednesday().equals("[]")) &&
                (teacherFreeTime.getThursday() == null || teacherFreeTime.getThursday().equals("[]")) &&
                (teacherFreeTime.getFriday() == null || teacherFreeTime.getFriday().equals("[]")) &&
                (teacherFreeTime.getSaturday() == null || teacherFreeTime.getSaturday().equals("[]")) &&
                (teacherFreeTime.getSunday() == null || teacherFreeTime.getSunday().equals("[]"))) {
            return new ArrayList<>();
        }

        List<TeacherSubjectPrice> list = teacherFreeTimeDao.selectPriceByTeacherId(teacherId);
        return list;
    }

    @Override
    public Integer getWeekNotStart(Long userId) {
        return baseMapper.selectWeekNotStartCourseSchedule(userId);
    }

    @Override
    public Integer getHomeworkNotDecorate(Long userId) {
        return baseMapper.selectHomeworkNotDecorate(userId);
    }

    @Override
    public Integer getNotRepliedCourseSchedule(Long userId) {
        return baseMapper.selectNotRepliedCourseSchedule(userId);
    }

    @Override
    public Integer getWeekStudentRepliedCourseSchedule(Long userId) {
        return baseMapper.selectWeekStudentRepliedCourseSchedule(userId);
    }

    @Override
    public void sendTodayNotRepliedAndNotDecorateHomework() {
        List<TodayNotRepliedAndNotDecorateHomeworkVo> list = baseMapper.selectTodayNotRepliedAndNotDecorateHomework();
        for (TodayNotRepliedAndNotDecorateHomeworkVo info : list) {
            if (info.getRepliedNum() == 0 && info.getDecorateNum() == 0) {
                continue;
            }
            try {
                Map<Long, String> teacherReceivers = new HashMap<>();
                teacherReceivers.put(info.getTeacherId(), info.getPhone());
                sysMessageService.batchSendMessage(MessageSenderPluginContext.MessageSender.JIGUANG, MessageTypeEnum.NOT_EVALUATE_STUDENT_PRACTICE,
                        teacherReceivers, null, 0, null, ClientEnum.TEACHER.getCode(),
                        info.getRepliedNum(), info.getDecorateNum());
            } catch (Exception e) {
                log.warn("老师当日陪练课未评价/布置作业消息发送失败,{}", e.getMessage());
            }
        }
    }

    /**
     * @Description: 购买陪练课消息推送
     * @Author: cy
     * @Date: 2022/5/6
     */
    public void practiceSend(Long teacherId, Long studentId, String courseNum, String orderNo) {
        //查询老师&学生信息
        SysUser teacher = sysUserFeignService.queryUserById(teacherId);
        SysUser student = sysUserFeignService.queryUserById(studentId);

        //消息接收者(Key:用户编号 value:消息接收对象)
        Map<Long, String> teacherReceivers = new HashMap<>();
        teacherReceivers.put(teacherId, teacher.getPhone());
        Map<Long, String> studentReceivers = new HashMap<>();
        studentReceivers.put(studentId, student.getPhone());
        Map<Long, String> teacherSms = new HashMap<>();
        teacherSms.put(teacherId, teacher.getPhone());
        Map<Long, String> studentSms = new HashMap<>();
        studentSms.put(studentId, student.getPhone());

        //老师端-学生买陪练课
        String teacherUrl = sysMessageService.selectConfigUrl(MessageTypeEnum.STUDENT_BUY_PRACTICE.getCode());
        sysMessageService.batchSendMessage(MessageSenderPluginContext.MessageSender.JIGUANG, MessageTypeEnum.STUDENT_BUY_PRACTICE,
                teacherReceivers, null, 0, teacherUrl, ClientEnum.TEACHER.getCode(),
                student.getUsername(), courseNum);
        log.info("send success {}", MessageTypeEnum.STUDENT_BUY_PRACTICE);

        //老师端-学生买陪练课(短信)
        sysMessageService.batchSendMessage(MessageSenderPluginContext.MessageSender.AWSMS, MessageTypeEnum.SMS_STUDENT_BUY_PRACTICE,
                teacherSms, null, 0, null, null,
                student.getUsername(), courseNum);
        log.info("send success {}", MessageTypeEnum.SMS_STUDENT_BUY_PRACTICE);

        //学生端-买陪练课
        String studentUrl = sysMessageService.selectConfigUrl(MessageTypeEnum.PRACTICE_BUY.getCode());
        sysMessageService.batchSendMessage(MessageSenderPluginContext.MessageSender.JIGUANG, MessageTypeEnum.PRACTICE_BUY,
                studentReceivers, null, 0, studentUrl, ClientEnum.STUDENT.getCode(),
                teacher.getUsername(), courseNum);
        log.info("send success {}", MessageTypeEnum.PRACTICE_BUY);

        //学生端-买陪练课(短信)
        String studentSmsUrl = sysMessageService.selectConfigUrl(MessageTypeEnum.SMS_PRACTICE_BUY.getCode());
        sysMessageService.batchSendMessage(MessageSenderPluginContext.MessageSender.AWSMS, MessageTypeEnum.SMS_PRACTICE_BUY,
                studentSms, null, 0, null, null,
                teacher.getUsername(), courseNum, studentSmsUrl);
        log.info("send success {}", MessageTypeEnum.SMS_PRACTICE_BUY);

        List<String> startTimeList = baseMapper.selectStartTime(orderNo);
        for (String startTime : startTimeList) {
            //老师端-陪练课开课提醒
            sysMessageService.batchSendMessage(MessageSenderPluginContext.MessageSender.JIGUANG, MessageTypeEnum.STUDENT_PRACTICE_REMINDER,
                    teacherReceivers, DateUtil.offsetMinute(startTime, -20), 0, null, ClientEnum.TEACHER.getCode(),
                    student.getUsername());

            //学生端-陪练课开课提醒
            sysMessageService.batchSendMessage(MessageSenderPluginContext.MessageSender.JIGUANG, MessageTypeEnum.PRACTICE_REMIND,
                    studentReceivers, DateUtil.offsetMinute(startTime, -20), 0, null, ClientEnum.STUDENT.getCode(),
                    teacher.getUsername());
        }
        log.info("send success {}", MessageTypeEnum.PRACTICE_REMIND);
    }

    /**
     * @Description: 明日课程提醒(每晚9点)
     * @Author: cy
     * @Date: 2022/5/6
     */
    public void courseRemind() {
        String tomorrow = new SimpleDateFormat("yyyy-MM-dd").format(DateUtil.addDays1(new Date(), 1));
        //查询明天有课的老师
        List<SysUser> userList = baseMapper.selectTeacher(tomorrow);
        if (CollectionUtils.isNotEmpty(userList)) {
            for (SysUser sysUser : userList) {
                Map<Long, String> teacherReceivers = new HashMap<>();
                teacherReceivers.put(sysUser.getId(), sysUser.getPhone());
                Map<Long, String> teacherSms = new HashMap<>();
                teacherSms.put(sysUser.getId(), sysUser.getPhone());

                Integer liveCount = 0;
                Integer practiceCount = 0;
                List<CountVo> typeCount = baseMapper.selectTypeCount(sysUser.getId(), tomorrow);
                if (CollectionUtils.isNotEmpty(typeCount)) {
                    for (CountVo countVo : typeCount) {
                        if (countVo.getType().equals(CourseScheduleEnum.LIVE.getCode())) {
                            liveCount = countVo.getCount();
                        }
                        if (countVo.getType().equals(CourseScheduleEnum.PRACTICE.getCode())) {
                            practiceCount = countVo.getCount();
                        }
                    }
                }

                //老师端-明日课程提醒
                if (liveCount != 0 || practiceCount != 0) {
                    String teacherUrl = sysMessageService.selectConfigUrl(MessageTypeEnum.TOMORROW_COURSE_REMINDER.getCode());
                    sysMessageService.batchSendMessage(MessageSenderPluginContext.MessageSender.JIGUANG, MessageTypeEnum.TOMORROW_COURSE_REMINDER,
                            teacherReceivers, null, 0, teacherUrl, ClientEnum.TEACHER.getCode(),
                            practiceCount.toString(), liveCount.toString());
                    log.info("send success {}", MessageTypeEnum.TOMORROW_COURSE_REMINDER);

                    //老师端-明日课程提醒(短信)
                    sysMessageService.batchSendMessage(MessageSenderPluginContext.MessageSender.AWSMS, MessageTypeEnum.SMS_TOMORROW_COURSE_REMINDER,
                            teacherSms, null, 0, null, null,
                            practiceCount, liveCount);
                    log.info("send success {}", MessageTypeEnum.SMS_STUDENT_BUY_PRACTICE);
                }
            }
        }
    }

    /**
     * @Description: 查询课程配置
     * @Author: cy
     * @Date: 2022/5/10
     */
    public Map<String, String> selectRoomConfig() {
        Map<String, String> sysConfig = new HashMap<>();
        //提前XX分钟创建/进入陪练课房间时间
        sysConfig.put("practiceStartTime", sysConfigService.findConfigValue(SysConfigConstant.PRE_CREATE_PRACTICE_ROOM_MINUTE));
        //陪练课结束后，XX分钟关闭房间
        sysConfig.put("practiceEndTime", sysConfigService.findConfigValue(SysConfigConstant.DESTROY_EXPIRED_PRACTICE_ROOM_MINUTE));
        //提前XX分钟创建/进入直播房间的时间
        sysConfig.put("liveStartTime", sysConfigService.findConfigValue(SysConfigConstant.PRE_CREATE_LIVE_ROOM_MINUTE));
        //直播结束后，XX分钟关闭房间
        sysConfig.put("liveEndTime", sysConfigService.findConfigValue(SysConfigConstant.DESTROY_EXPIRED_LIVE_ROOM_MINUTE));
        //提前XX分钟创建/进入琴房课房间时间
        sysConfig.put("pianoStartTime", sysConfigService.findConfigValue(SysConfigConstant.PRE_CREATE_PIANO_ROOM_MINUTE));
        //琴房课结束后，XX分钟关闭房间
        sysConfig.put("pianoEndTime", sysConfigService.findConfigValue(SysConfigConstant.DESTROY_EXPIRED_PIANO_ROOM_MINUTE));
        return sysConfig;
    }

    /**
     * 获取未来30天有空老师id
     *
     * @param amount 未来天数
     * @return
     */
    public List<Long> getTeacherId(Integer amount) {
        if (amount == null) {
            amount = 30;
        }
        //查询所有过审的老师时间配置
        List<TeacherFreeTime> teacherFreeTime = teacherFreeTimeDao.getTeacherFreeTime();

        //老师配置-跳过节假日
        List<TeacherFreeTime> skipHoliday = teacherFreeTime.stream().filter(item -> item.getSkipHolidayFlag()).collect(Collectors.toList());
        //老师配置
        List<TeacherFreeTime> noSkipHoliday = teacherFreeTime.stream().filter(item -> !item.getSkipHolidayFlag()).collect(Collectors.toList());

        //查询节假日
        HolidaysFestivals holidays = holidaysFestivalsDao.queryByYear(Integer.parseInt(DateUtil.getYear(new Date())));
        String[] strs = holidays.getHolidaysFestivalsJson().split(",");
        List<String> holiday = Arrays.asList(strs);

        //未来30天日期
        List<String> futureDays = DateUtil.getFutureDays(amount);
        //未来30天日期-跳过节假日
        List<String> futureDaysNoVacation = futureDays.stream().filter(item -> !holiday.contains(item)).collect(Collectors.toList());

        List<Long> teacherIdList = new ArrayList<>();
        getAllTimeConfig(skipHoliday, futureDaysNoVacation, teacherIdList);//跳过节假日配置
        getAllTimeConfig(noSkipHoliday, futureDays, teacherIdList);//未跳过节假日配置

        return teacherIdList;
    }

    /**
     * 获取未来30天有空老师id
     *
     * @param teacherFreeTime 老师配置
     * @param futureDay       未来30天日期
     * @param idList          未约满老师id集合
     * @return
     */
    public List<Long> getAllTimeConfig(List<TeacherFreeTime> teacherFreeTime, List<String> futureDay, List<Long> idList) {
        List<String> statusList = Lists.newArrayList(CourseScheduleEnum.NOT_START.getCode(), CourseScheduleEnum.ING.getCode());

        for (TeacherFreeTime freeTimeConfig : teacherFreeTime) {//遍历老师配置
            //根据老师id查询未开始、进行中的课程
            List<CourseSchedule> scheduleList = this.list(Wrappers.<CourseSchedule>lambdaQuery()
                    .eq(CourseSchedule::getTeacherId, freeTimeConfig.getTeacherId())
                    .in(CourseSchedule::getStatus, statusList)
                    .gt(CourseSchedule::getClassDate, DateUtil.getNowStr()));

            //获取一周配置格式化
            List<DateVo> monday = JSONObject.parseArray(freeTimeConfig.getMonday(), DateVo.class);
            List<DateVo> tuesday = JSONObject.parseArray(freeTimeConfig.getTuesday(), DateVo.class);
            List<DateVo> wednesday = JSONObject.parseArray(freeTimeConfig.getWednesday(), DateVo.class);
            List<DateVo> thursday = JSONObject.parseArray(freeTimeConfig.getThursday(), DateVo.class);
            List<DateVo> friday = JSONObject.parseArray(freeTimeConfig.getFriday(), DateVo.class);
            List<DateVo> saturday = JSONObject.parseArray(freeTimeConfig.getSaturday(), DateVo.class);
            List<DateVo> sunday = JSONObject.parseArray(freeTimeConfig.getSunday(), DateVo.class);

            //存储未来30天所有时间
            List<DateChangeVo> rList = new ArrayList<>();

            for (String day : futureDay) {
                switch (DateUtil.getWeek(day)) {
                    case "monday":
                        splicingDate(day, monday, rList);
                        break;
                    case "tuesday":
                        splicingDate(day, tuesday, rList);
                        break;
                    case "wednesday":
                        splicingDate(day, wednesday, rList);
                        break;
                    case "thursday":
                        splicingDate(day, thursday, rList);
                        break;
                    case "friday":
                        splicingDate(day, friday, rList);
                        break;
                    case "saturday":
                        splicingDate(day, saturday, rList);
                        break;
                    case "sunday":
                        splicingDate(day, sunday, rList);
                        break;
                }
            }

            //记录冲突索引
            List<Integer> indexList = new ArrayList<>();

            for (int i = 0; i < rList.size(); i++) {//遍历未来30天所有时间
                DateChangeVo dateChange = rList.get(i);
                for (CourseSchedule schedule : scheduleList) {//遍历已排课时间
                    //校验当前时间 与 排课时间是否冲突  记录冲突时间索引
                    if (schedule.getStartTime() != null && schedule.getEndTime() != null) {
                        if (inInterSection(dateChange.getStartTime(), dateChange.getEndTime(), schedule.getStartTime(), schedule.getEndTime())) {
                            indexList.add(i);
                            continue;
                        }
                    }
                }
            }

            //冲突数<配置数 则有空档期 记录老师id
            if (indexList.size() < rList.size()) {
                idList.add(freeTimeConfig.getTeacherId());
            }
        }
        return idList;
    }

    /**
     * 拼接日期时间
     *
     * @param day         日期
     * @param strDateList 时间集合
     * @param rList       接受容器
     * @return
     */
    public List<DateChangeVo> splicingDate(String day, List<DateVo> strDateList, List<DateChangeVo> rList) {
        if (CollectionUtils.isNotEmpty(strDateList)) {
            for (DateVo vo : strDateList) {
                if (vo != null) {
                    DateChangeVo dateChange = new DateChangeVo();
                    dateChange.setStartTime(DateUtil.strToDate(day + " " + vo.getStartTime()));
                    dateChange.setEndTime(DateUtil.strToDate(day + " " + vo.getEndTime()));
                    rList.add(dateChange);
                }
            }
        }
        return rList;
    }

    /**
     * @Description: 定时修改课程状态
     * @Author: cy
     * @Date: 2022/5/17
     */
    @Transactional(rollbackFor = Exception.class)
    public void scheduleTask() {
        //查询房间配置时间
        LocalDateTime liveMinute = LocalDateTime.now().plusMinutes(Long.parseLong(sysConfigService.findConfigValue(SysConfigConstant.PRE_CREATE_LIVE_ROOM_MINUTE)));
        LocalDateTime practiceMinute = LocalDateTime.now().plusMinutes(Long.parseLong(sysConfigService.findConfigValue(SysConfigConstant.PRE_CREATE_PRACTICE_ROOM_MINUTE)));
        LocalDateTime pianoMinute = LocalDateTime.now().plusMinutes(Long.parseLong(sysConfigService.findConfigValue(SysConfigConstant.PRE_CREATE_PIANO_ROOM_MINUTE)));

        //课程开始(开课时间 ≤ (NOW + 提前进入房间时间(分)) && NOW ≤ 结束时间)
        List<CourseSchedule> liveStart = baseMapper.selectList(Wrappers.<CourseSchedule>lambdaQuery()
                .eq(CourseSchedule::getLock, 0)
                .eq(CourseSchedule::getStatus, CourseScheduleEnum.NOT_START)
                .eq(CourseSchedule::getType, CourseScheduleEnum.LIVE)
                .le(CourseSchedule::getStartTime, liveMinute)
                .ge(CourseSchedule::getEndTime, LocalDateTime.now()));
        List<CourseSchedule> practiceStart = baseMapper.selectList(Wrappers.<CourseSchedule>lambdaQuery()
                .eq(CourseSchedule::getLock, 0)
                .eq(CourseSchedule::getStatus, CourseScheduleEnum.NOT_START)
                .eq(CourseSchedule::getType, CourseScheduleEnum.PRACTICE)
                .le(CourseSchedule::getStartTime, practiceMinute)
                .ge(CourseSchedule::getEndTime, LocalDateTime.now()));
        List<CourseSchedule> pianoStart = baseMapper.selectList(Wrappers.<CourseSchedule>lambdaQuery()
                .eq(CourseSchedule::getLock, 0)
                .eq(CourseSchedule::getStatus, CourseScheduleEnum.NOT_START)
                .eq(CourseSchedule::getType, CourseScheduleEnum.PIANO_ROOM_CLASS)
                .le(CourseSchedule::getStartTime, pianoMinute)
                .ge(CourseSchedule::getEndTime, LocalDateTime.now()));
        if (CollectionUtils.isEmpty(liveStart)) {
            liveStart = new ArrayList<>();
        }
        if (CollectionUtils.isEmpty(practiceStart)) {
            practiceStart = new ArrayList<>();
        }
        if (CollectionUtils.isEmpty(pianoStart)) {
            pianoStart = new ArrayList<>();
        }
        List<CourseSchedule> courseStart = Stream.of(liveStart, practiceStart, pianoStart).flatMap(Collection::stream).collect(Collectors.toList());
        if (CollectionUtils.isNotEmpty(courseStart)) {
            //课程状态更新为ING
            baseMapper.updateStartTime(courseStart);
        }

        //课程结束(NOW ≥ 结束时间)
        List<CourseScheduleStudentVo> userList = paymentDao.selectUser();
        if (CollectionUtils.isNotEmpty(userList)) {
            List<CourseScheduleStudentVo> practiceList = userList.stream().filter(s -> s.getType().equals(CourseScheduleEnum.PRACTICE.getCode())).collect(Collectors.toList());
            if (CollectionUtils.isNotEmpty(practiceList)) {//陪练课
                //老师课酬状态改为待结算
                courseScheduleTeacherSalaryService.update(null, Wrappers.<CourseScheduleTeacherSalary>lambdaUpdate()
                        .set(CourseScheduleTeacherSalary::getStatus, TeacherSalaryEnum.WAIT.getCode())
                        .in(CourseScheduleTeacherSalary::getCourseScheduleId, practiceList.stream().map(CourseScheduleStudentVo::getCourseId).collect(Collectors.toList())));

                //创建课程评论
                List<Long> ids = practiceList.stream().map(CourseScheduleStudentVo::getCourseId).collect(Collectors.toList());
                repliedDao.deleteBatch(ids);
                repliedDao.insertBatch(practiceList);
            }

            //更新琴房课学生最近结课时间
            List<CourseScheduleStudentVo> pianoUserList = userList.stream().filter((CourseScheduleStudentVo s) -> s.getType().equals(CourseScheduleEnum.PIANO_ROOM_CLASS.getCode())).collect(Collectors.toList());
            if (CollectionUtils.isNotEmpty(pianoUserList)) {
                pianoRoomTimeDao.updateEndTime(pianoUserList);
            }

            //统计琴房课消耗时长
            List<CourseScheduleRecord> recordList = recordDao.sumCourseTime();
            if (CollectionUtils.isNotEmpty(recordList)) {
                //时长消费记录
                String formula = sysConfigService.findConfigValue(SysConfigConstant.PIANO_ROOM_TIME_FORMULA);
                List<CourseScheduleRecord> records = recordList.stream()
                        .map(s -> {
                            Integer studentCount = s.getStudentCount();
                            Integer n = (Integer) WrapperUtil.strToFormula(formula, "n", String.valueOf(studentCount + 1));
                            if (n == null) {
                                throw new BizException("公式转换失败");
                            }
                            s.setConsumTime(s.getSingleCourseTime() * n);
                            return s;
                        })
                        .collect(Collectors.toList());
                recordDao.insertBatch(records);

                //释放冻结课时
                Map<Long, List<CourseScheduleRecord>> collect = records.stream().collect(Collectors.groupingBy(CourseScheduleRecord::getTeacherId, Collectors.toList()));
                collect.forEach((key, list) -> {
                    //入琴房账户
                    DistributedLock.of(redissonClient)
                            .runIfLockToFunction(CacheNameEnum.LOCK_CHANGE_PIANO_ROOM_ACCOUNT.getRedisKey(key)
                                    , (o) -> {
                                        PianoRoomTime pianoRoomTime = pianoRoomTimeDao.selectOne(Wrappers.<PianoRoomTime>lambdaQuery().eq(PianoRoomTime::getTeacherId, key));
                                        Integer frozenTime = pianoRoomTime.getFrozenTime();

                                        PianoRoomTime roomTime = new PianoRoomTime();
                                        roomTime.setFrozenTime(frozenTime - list.stream().mapToInt(CourseScheduleRecord::getConsumTime).sum());
                                        pianoRoomTimeDao.update(roomTime, Wrappers.<PianoRoomTime>lambdaQuery().eq(PianoRoomTime::getTeacherId, key));
                                        return null;
                                    }, null, 10l);
                });
            }

            List<CourseScheduleStudentVo> courseList = userList.stream()
                    .filter(s -> (s.getType().equals(CourseScheduleEnum.PRACTICE.getCode()) || s.getType().equals(CourseScheduleEnum.PIANO_ROOM_CLASS.getCode())))
                    .collect(Collectors.toList());

            //清除缓存
            for (CourseScheduleStudentVo practice : courseList) {
                //清除学生统计缓存
                redissonClient.getBucket(CacheNameEnum.STUDENT_TOTAL.getRedisKey(practice.getStudentId())).delete();
                //清除老师统计缓存
                redissonClient.getBucket(CacheNameEnum.TEACHER_TOTAL.getRedisKey(practice.getTeacherId())).delete();
            }
            //课程状态更新为COMPLETE
            baseMapper.updateEndTime(userList);
        }

        //查完完成的课程
        List<CourseCompleteVo> completeList = baseMapper.selectComplete();
        if (CollectionUtils.isNotEmpty(completeList)) {
            List<Long> ids = completeList.stream().filter(s -> {
                return s.getCourseCount().equals(s.getCourseNum());
            }).map(s -> {
                return s.getId();
            }).collect(Collectors.toList());
            if (CollectionUtils.isNotEmpty(ids)) {
                //同步课程组状态
                courseGroupService.getDao().updateBatch(ids);
            }
        }
    }

    /**
     * @Description: 定时修改课酬状态
     * @Author: cy
     * @Date: 2022/5/24
     */
    @Transactional(rollbackFor = Exception.class)
    public void teacherSalaryTask() {
        //课程结算日期(天)
        Integer settlementDay = Integer.valueOf(sysConfigService.findConfigValue(SysConfigConstant.COURSE_SETTLEMENT_TIME_DAY));
        //获取n天前日期
        String day = DateUtil.getDayAgoOrAftString(-settlementDay);

        List<UserAccountRecordDto> list = baseMapper.selectWaitCourse(day);
        if (CollectionUtils.isNotEmpty(list)) {
            for (UserAccountRecordDto dto : list) {
                UserAccountRecordVo record = userAccountRecordService.detail(dto.getOrderNo(), dto.getBizType(), dto.getBizId());
                if (null != record) {
                    userAccountService.accountChange(record, PostStatusEnum.RECORDED);
                }
            }
            Set<Long> bizIds = list.stream().map(UserAccountRecordDto::getBizId).collect(Collectors.toSet());
            courseScheduleTeacherSalaryService.update(null, Wrappers.<CourseScheduleTeacherSalary>lambdaUpdate()
                    .set(CourseScheduleTeacherSalary::getStatus, TeacherSalaryEnum.COMPLETE.getCode())
                    .set(CourseScheduleTeacherSalary::getSettlementTime, new Date())
                    .in(CourseScheduleTeacherSalary::getCourseScheduleId, bizIds));
        }

    }

    /**
     * @Description: 查询琴房剩余时长、冻结时长、统计学员人数
     * @Author: cy
     * @Date: 2022/5/27
     */
    public PianoRoomTimeVo selectRemainTime(Long teacherId) {
        PianoRoomTimeVo roomTimeVo = pianoRoomTimeDao.selectRemainTime(teacherId);
        if (roomTimeVo != null) {
            return roomTimeVo;
        } else {
            PianoRoomTimeVo pianoRoomTimeVo = new PianoRoomTimeVo();
            pianoRoomTimeVo.setTeacherId(teacherId);
            pianoRoomTimeVo.setStudentCount(pianoRoomTimeDao.countStudent(teacherId));
            return pianoRoomTimeVo;
        }
    }

    /**
     * @Description: 我的学员&课内学员
     * @Author: cy
     * @Date: 2022/5/27
     */
    public PageInfo<CourseStudentVo> selectStudent(Map<String, Object> param) {
        Integer courseId = (Integer) param.get("courseId");
        if (courseId != null) {
            List<CourseScheduleStudentPayment> studentList = paymentDao.selectList(Wrappers.<CourseScheduleStudentPayment>lambdaQuery().eq(CourseScheduleStudentPayment::getCourseId, courseId));
            param.put("studentList", studentList);//根据课程id查询报课学员
        }
        return PageUtil.pageInfo(userBindingTeacherDao.selectStudent(PageUtil.getPageInfo(param), param));
    }

    /**
     * @Description: 排课
     * @Author: cy
     * @Date: 2022/5/27
     */
    public void arrangeCourse(ArrangeCourseVo arrangeCourseVo, Long teacherId) {
        DistributedLock.of(redissonClient)
                .runIfLockCanGet(CacheNameEnum.LOCK_EXECUTE_ORDER.getRedisKey("teacherId:" + teacherId)
                        , () -> this.checkArrangeCourse(arrangeCourseVo, teacherId), 60L, TimeUnit.SECONDS);
    }

    @Transactional(rollbackFor = Exception.class)
    public void checkArrangeCourse(ArrangeCourseVo arrangeCourseVo, Long teacherId) {
        Integer classNum = arrangeCourseVo.getClassNum();//课时数
        Integer singleClssTime = arrangeCourseVo.getSingleClssTime();//单课时长
        List<Long> studentIds = arrangeCourseVo.getStudentIds();//学员id集合

        String formula = sysConfigService.findConfigValue(SysConfigConstant.PIANO_ROOM_TIME_FORMULA);
        Integer n = (Integer) WrapperUtil.strToFormula(formula, "n", String.valueOf(studentIds.size() + 1));//人数计算 公式n*(n-1) n包含老师
        if (n == null) {
            throw new BizException("公式转换失败");
        }
        Integer consumTime = classNum * singleClssTime * n;//消耗时长 课程数*单课时长*人数

        List<CourseTimeEntity> timeList = arrangeCourseVo.getTimeList();//选课时间
        Integer consumeTime = arrangeCourseVo.getConsumeTime();

        log.info("classNum:" + classNum);
        log.info("singleClssTime:" + singleClssTime);
        log.info("n:" + n);
        log.info("消耗时长:" + consumTime);
        log.info("传入时长:" + consumeTime);

        Integer configValue = Integer.valueOf(sysConfigService.findConfigValue(SysConfigConstant.PIANO_ROOM_MAX_STUDENTS));
        if (studentIds.size() > configValue) {
            throw new BizException("成课学员人数超过房间最大容量{}", configValue);
        }

        //校验课时
        if (timeList.size() != classNum) {
            throw new BizException("课时数与排课数不符");
        }

        //校验消耗时长
        if (!consumTime.equals(consumeTime)) {
            throw new BizException("时长计算错误");
        }

        //校验上下课时间
        for (int i = 0; i < timeList.size(); i++) {
            if (timeList.get(i).getStartTime().before(new Date())) {
                throw new BizException("上课时间必须大于当前时间");
            }
            if (!DateUtil.offsetMinute(timeList.get(i).getStartTime(), singleClssTime).equals(timeList.get(i).getEndTime())) {
                throw new BizException("第{}节课结束时间计算错误", i + 1);
            }
        }

        //校验时长
        PianoRoomTime pianoRoomTime = pianoRoomTimeDao.selectOne(Wrappers.<PianoRoomTime>lambdaQuery().eq(PianoRoomTime::getTeacherId, teacherId));
        if (pianoRoomTime == null) {
            throw new BizException("未查询到老师剩余时长");
        }
        Integer remainTime = pianoRoomTime.getRemainTime();
        Integer frozenTime = pianoRoomTime.getFrozenTime();
        if (consumTime > remainTime) {
            throw new BizException("剩余时长不足");
        }

        //校验学员是否绑定
        List<UserBindingTeacher> bindingTeachers = userBindingTeacherDao.selectList(Wrappers.<UserBindingTeacher>lambdaQuery().eq(UserBindingTeacher::getTeacherId, teacherId));
        if (CollectionUtils.isEmpty(bindingTeachers)) {
            throw new BizException("无绑定学员");
        }
        List<Long> studentList = bindingTeachers.stream().map(UserBindingTeacher::getStudentId).collect(Collectors.toList());
        for (Long studentId : studentIds) {
            if (!studentList.contains(studentId)) {
                throw new BizException("学员id:{},未绑定", studentId);
            }
        }

        //校验传入时间是否交集
        if (timeList.size() > 1) {
            for (int i = 0; i < timeList.size(); i++) {
                if (i == timeList.size() - 1) {
                    break;
                }
                CourseTimeEntity o = timeList.get(i);
                List<CourseTimeEntity> newList = timeList.subList(i + 1, timeList.size());
                boolean checkParamTime = this.checkCourseTime(newList, CourseTimeEntity::getStartTime, CourseTimeEntity::getEndTime, o.getStartTime(), o.getEndTime());
                if (checkParamTime) {
                    throw new BizException(DateUtil.dateToString(o.getStartTime(), "yyyy年MM月dd号 HH点mm分") + "的课程时间重复!");
                }
            }
        }

        //批量检查老师课时在数据库是否重复
        this.batchCheckTeacherCourseTime(teacherId, timeList, CourseTimeEntity::getStartTime, CourseTimeEntity::getEndTime);

        //校验购买的课程组每节课时间是否和自己的课时冲突
        for (Long studentId : studentIds) {
            this.batchCheckStudentCourseTime(studentId, timeList, CourseTimeEntity::getStartTime, CourseTimeEntity::getEndTime);
        }

        //添加课程组
        CourseGroup courseGroup = new CourseGroup();
        courseGroup.setType(CourseScheduleEnum.PIANO_ROOM_CLASS.getCode());
        courseGroup.setTeacherId(teacherId);
        courseGroup.setName(arrangeCourseVo.getCourseName());
        courseGroup.setSubjectId(arrangeCourseVo.getSubjectId());
        courseGroup.setSingleCourseMinutes(singleClssTime);
        courseGroup.setCourseNum(classNum);
        courseGroup.setStatus(CourseGroupEnum.ING.getCode());
        courseGroup.setCreatedBy(teacherId);
        courseGroupService.getDao().insert(courseGroup);

        for (int i = 0; i < timeList.size(); i++) {
            //添加课程
            CourseTimeEntity courseTimeEntity = timeList.get(i);
            CourseSchedule schedule = new CourseSchedule();
            schedule.setCourseGroupId(courseGroup.getId());
            schedule.setType(CourseScheduleEnum.PIANO_ROOM_CLASS.getCode());
            schedule.classNum(i + 1);
            schedule.setTeacherId(teacherId);
            schedule.setClassDate(DateUtil.trunc(courseTimeEntity.getStartTime()));
            schedule.setStartTime(courseTimeEntity.getStartTime());
            schedule.setEndTime(courseTimeEntity.getEndTime());
            schedule.setLock(0);
            schedule.setStatus(CourseScheduleEnum.NOT_START.getCode());
            schedule.setCreatedBy(teacherId);
            schedule.setSingleCourseTime(singleClssTime);
            baseMapper.insert(schedule);

            //添加payment
            for (Long studentId : studentIds) {
                CourseScheduleStudentPayment payment = new CourseScheduleStudentPayment();
                payment.setCourseGroupId(courseGroup.getId());
                payment.setCourseId(schedule.getId());
                payment.setUserId(studentId);
                payment.setCourseType(CourseScheduleEnum.PIANO_ROOM_CLASS.getCode());
                courseScheduleStudentPaymentService.save(payment);
            }
        }

        //扣减piano_room_time
        DistributedLock.of(redissonClient)
                .runIfLockToFunction(CacheNameEnum.LOCK_CHANGE_PIANO_ROOM_ACCOUNT.getRedisKey(teacherId)
                        , (o) -> {
                            //校验时长
                            PianoRoomTime pianoRoomTimeLock = pianoRoomTimeDao.selectOne(Wrappers.<PianoRoomTime>lambdaQuery().eq(PianoRoomTime::getTeacherId, teacherId));
                            if (pianoRoomTimeLock == null) {
                                throw new BizException("未查询到老师剩余时长");
                            }
                            Integer remainTimeLock = pianoRoomTimeLock.getRemainTime();
                            Integer frozenTimeLock = pianoRoomTimeLock.getFrozenTime();
                            if (consumTime > remainTimeLock) {
                                throw new BizException("剩余时长不足");
                            }

                            PianoRoomTime roomTimeLock = new PianoRoomTime();
                            roomTimeLock.setRemainTime(remainTimeLock - consumTime);
                            roomTimeLock.setFrozenTime(frozenTimeLock + consumTime);
                            pianoRoomTimeDao.update(roomTimeLock, Wrappers.<PianoRoomTime>lambdaQuery().eq(PianoRoomTime::getTeacherId, teacherId));
                            return null;
                        }, null, 10l);

        //创建群聊
        try {
            imGroupService.autoCreate(courseGroup.getId(), CourseScheduleEnum.PIANO_ROOM_CLASS.getCode());
        } catch (Exception e) {
            log.error("琴房课程组id:{},创建群聊失败:{}", courseGroup.getId(), e);
            e.printStackTrace();
        }

        for (Long studentId : studentIds) {
            //清除学生缓存
            redissonClient.getBucket(CacheNameEnum.STUDENT_TOTAL.getRedisKey(studentId)).delete();
            //发生推送通知
            pianoSend(teacherId, studentId);
        }
        //清除老师缓存
        redissonClient.getBucket(CacheNameEnum.TEACHER_TOTAL.getRedisKey(teacherId)).delete();
    }

    /**
     * @Description: 根据月份查询消耗记录
     * @Author: cy
     * @Date: 2022/5/30
     */
    public Map<String, Object> selectConsumeTime(String month, Long teacherId) {
        Map<String, Object> map = new HashMap();
        map.put("consumeTime", recordDao.countTimeByTeacherId(teacherId, month));
        return map;
    }

    /**
     * @Description: 消耗时长详情
     * @Author: cy
     * @Date: 2022/5/30
     */
    @Override
    public PageInfo<CourseScheduleRecordVo> selectConsumeTimeList(Map<String, Object> param) {
        param.put("type", CourseScheduleEnum.PIANO_ROOM_CLASS.getCode());
        return PageUtil.pageInfo(recordDao.selectConsumeTimeList(PageUtil.getPageInfo(param), param));
    }

    /**
     * @Description: 琴房课列表
     * @Author: cy
     * @Date: 2022/6/7
     */
    @Override
    public PageInfo<CourseScheduleRecordVo> selectCourseList(Map<String, Object> param) {
        param.put("type", CourseScheduleEnum.PIANO_ROOM_CLASS.getCode());
        return PageUtil.pageInfo(recordDao.selectCourseList(PageUtil.getPageInfo(param), param));
    }

    /**
     * @Description: 排课推送
     * @Author: cy
     * @Date: 2022/5/31
     */
    public void pianoSend(Long teacherId, Long studentId) {
        try {
            //查询老师&学生信息
            SysUser teacher = sysUserFeignService.queryUserById(teacherId);
            SysUser student = sysUserFeignService.queryUserById(studentId);

            //消息接收者(Key:用户编号 value:消息接收对象)
            Map<Long, String> studentReceivers = new HashMap<>();
            studentReceivers.put(studentId, student.getPhone());

            String pianoUrl = sysMessageService.selectConfigUrl(MessageTypeEnum.ARRANGE_PIANO_ROOM_CLASS.getCode());
            sysMessageService.batchSendMessage(MessageSenderPluginContext.MessageSender.JIGUANG, MessageTypeEnum.ARRANGE_PIANO_ROOM_CLASS,
                    studentReceivers, null, 0, pianoUrl, ClientEnum.STUDENT.getCode(),
                    teacher.getUsername());
            log.info("老师:{},学员:{},排课推送成功", teacherId, studentId);
        } catch (Exception e) {
            log.error("老师:{},学员:{},排课推送失败,{}", teacherId, studentId, e);
        }
    }

    /**
     * @Description: 删除课程
     * @Author: cy
     * @Date: 2022/5/31
     */
    @Transactional(rollbackFor = Exception.class)
    public void deleteCourse(Map<String, Object> param) {
        Long courseId = WrapperUtil.toLong(param, "courseId", "课程id不能为空!");
        Long teacherId = WrapperUtil.toLong(param, "teacherId", "老师id不能为空!");

        //校验课程是否存在
        CourseSchedule schedule = baseMapper.selectOne(Wrappers.<CourseSchedule>lambdaQuery()
                .eq(CourseSchedule::getId, courseId)
                .eq(CourseSchedule::getTeacherId, teacherId)
                .eq(CourseSchedule::getType, CourseScheduleEnum.PIANO_ROOM_CLASS.getCode()));
        if (schedule == null) {
            throw new BizException("课程不存在");
        }

        //课程是否未开始
        if (!schedule.getStatus().equals(CourseScheduleEnum.NOT_START.getCode())) {
            throw new BizException("只能删除未开始的课程");
        }

        //计算课时
        List<CourseScheduleStudentPayment> studentPaymentList = paymentDao.selectList(Wrappers.<CourseScheduleStudentPayment>lambdaQuery().eq(CourseScheduleStudentPayment::getCourseId, courseId));

        String formula = sysConfigService.findConfigValue(SysConfigConstant.PIANO_ROOM_TIME_FORMULA);
        Integer n = (Integer) WrapperUtil.strToFormula(formula, "n", String.valueOf(studentPaymentList.size() + 1));//人数计算 公式n*(n-1) n包含老师
        if (n == null) {
            throw new BizException("公式转换失败");
        }
        Integer time = schedule.getSingleCourseTime() * n;//消耗时长 课程数*单课时长*人数

        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("teacherId", teacherId);
        paramMap.put("time", time);

        //更新老师琴房时长账户
        DistributedLock.of(redissonClient)
                .runIfLockToFunction(CacheNameEnum.LOCK_CHANGE_PIANO_ROOM_ACCOUNT.getRedisKey(teacherId)
                        , (objectMap) -> {
                            Long objectId = Long.parseLong(objectMap.get("teacherId").toString());
                            Integer objectTime = Integer.parseInt(objectMap.get("time").toString());

                            PianoRoomTime pianoRoomTime = pianoRoomTimeDao.selectOne(Wrappers.<PianoRoomTime>lambdaQuery()
                                    .eq(PianoRoomTime::getTeacherId, objectId));
                            if (pianoRoomTime == null) {
                                throw new BizException("未查到剩余时长");
                            }
                            if (pianoRoomTime.getFrozenTime() - objectTime < 0) {
                                throw new BizException("冻结时长计算错误");
                            }
                            pianoRoomTimeDao.update(null, Wrappers.<PianoRoomTime>lambdaUpdate()
                                    .eq(PianoRoomTime::getTeacherId, objectId)
                                    .set(PianoRoomTime::getRemainTime, pianoRoomTime.getRemainTime() + objectTime)
                                    .set(PianoRoomTime::getFrozenTime, pianoRoomTime.getFrozenTime() - objectTime));
                            return null;
                        }, paramMap, 10l);
        //删除课程
        baseMapper.deleteById(courseId);
        //删除payment
        paymentDao.delete(Wrappers.<CourseScheduleStudentPayment>lambdaQuery().eq(CourseScheduleStudentPayment::getCourseId, courseId));

        //更新组课程数
        Long groupId = schedule.getCourseGroupId();
        Integer count = baseMapper.selectCount(Wrappers.<CourseSchedule>lambdaQuery().eq(CourseSchedule::getCourseGroupId, groupId));
        if (count == 0) {//组内课程为0,删除课程组
            courseGroupService.getDao().deleteById(groupId);
        } else {
            courseGroupService.update(null, Wrappers.<CourseGroup>lambdaUpdate()
                    .eq(CourseGroup::getId, groupId)
                    .set(CourseGroup::getCourseNum, count));
        }
    }

    /**
     * @Description: 调整上课时间
     * @Author: cy
     * @Date: 2022/5/31
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateCourseTime(Map<String, Object> param) {
        Long courseId = WrapperUtil.toLong(param, "courseId", "课程id不能为空!");
        Long teacherId = WrapperUtil.toLong(param, "teacherId", "老师id不能为空!");
        String startTimeStr = WrapperUtil.toStr(param, "startTime", "开始时间不能为空!");
        String endTimeStr = WrapperUtil.toStr(param, "endTime", "结束时间不能为空!");

        Date startTime = DateUtil.strToDate(startTimeStr);
        Date endTime = DateUtil.strToDate(endTimeStr);
        CourseTimeEntity courseTime = new CourseTimeEntity();
        courseTime.setStartTime(startTime);
        courseTime.setEndTime(endTime);
        List<CourseTimeEntity> timeList = Arrays.asList(courseTime);

        //校验系统配置上下课时间
        String classDate = DateUtil.dateToString(startTime);
        String star = sysConfigService.findConfigValue(SysConfigConstant.COURSE_START_SETTING);//系统开课时间
        String end = sysConfigService.findConfigValue(SysConfigConstant.COURSE_END_SETTING);//系统关课时间
        Date s = DateUtil.strToDate(classDate + " " + star + ":00");
        Date e = DateUtil.strToDate(classDate + " " + end + ":00");
        if (startTime.before(s) || endTime.after(e)) {
            throw new BizException("排课时间区间为{}~{}", star, end);
        }

        if (startTime.before(new Date())) {
            throw new BizException("上课时间必须大于当前时间");
        }

        CourseSchedule courseSchedule = baseMapper.selectOne(Wrappers.<CourseSchedule>lambdaQuery()
                .eq(CourseSchedule::getId, courseId)
                .eq(CourseSchedule::getTeacherId, teacherId)
                .eq(CourseSchedule::getStatus, CourseScheduleEnum.NOT_START.getCode())
                .eq(CourseSchedule::getType, CourseScheduleEnum.PIANO_ROOM_CLASS.getCode()));
        if (courseSchedule == null) {
            throw new BizException("无法修改课程");
        }

        //校验上下课时间
        Integer singleCourseTime = courseSchedule.getSingleCourseTime();
        if (!DateUtil.offsetMinute(startTime, singleCourseTime).equals(endTime)) {
            throw new BizException("课程结束时间计算错误");
        }

        //课程改为过去式避免校验时报错
        baseMapper.update(null, Wrappers.<CourseSchedule>lambdaUpdate()
                .eq(CourseSchedule::getId, courseId)
                .set(CourseSchedule::getStartTime, DateUtil.getYesterday())
                .set(CourseSchedule::getEndTime, DateUtil.getYesterday()));

        //批量检查老师课时在数据库是否重复
        this.batchCheckTeacherCourseTime(teacherId, timeList, CourseTimeEntity::getStartTime, CourseTimeEntity::getEndTime);

        //校验购买的课程组每节课时间是否和自己的课时冲突
        List<CourseScheduleStudentPayment> studentPayments = paymentDao.selectList(Wrappers.<CourseScheduleStudentPayment>lambdaQuery()
                .eq(CourseScheduleStudentPayment::getCourseId, courseId)
                .eq(CourseScheduleStudentPayment::getCourseType, CourseScheduleEnum.PIANO_ROOM_CLASS.getCode()));
        if (CollectionUtils.isNotEmpty(studentPayments)) {
            for (CourseScheduleStudentPayment payment : studentPayments) {
                this.batchCheckStudentCourseTime(payment.getUserId(), timeList, CourseTimeEntity::getStartTime, CourseTimeEntity::getEndTime);
            }
        }

        //更新时间
        baseMapper.update(null, Wrappers.<CourseSchedule>lambdaUpdate()
                .eq(CourseSchedule::getId, courseId)
                .set(CourseSchedule::getClassDate, DateUtil.trunc(startTime))
                .set(CourseSchedule::getStartTime, startTime)
                .set(CourseSchedule::getEndTime, endTime));

        if (CollectionUtils.isNotEmpty(studentPayments)) {
            SysUser teacher = sysUserFeignService.queryUserById(teacherId);

            CourseSchedule schedule = this.getById(courseId);
            CourseGroup courseGroup = courseGroupService.getById(courseSchedule.getCourseGroupId());

            String courseName = courseGroup.getName() + "-第" + courseSchedule.getClassNum() + "课";

            String dateString = startTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().format(
                    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

            for (CourseScheduleStudentPayment payment : studentPayments) {
                sendPianoCourseAdjustMessage(teacher,payment.getUserId(),courseName,dateString);

            }
        }
    }

    //{老师昵称}已将{课程类型琴房课}{课程名称}课调整到{上课时间yyyy-MM-dd HH:mm:ss}，请注意查看
    private void sendPianoCourseAdjustMessage(SysUser teacher,Long studentId,String courseName,String dateString) {
        //  琴房课调整模板
        try {
            //  发送消息
            SysUser student = sysUserFeignService.queryUserById(studentId);

            Map<Long, String> receivers = new HashMap<>();
            receivers.put(studentId, student.getPhone());
            sysMessageService.batchSendMessage(MessageSenderPluginContext.MessageSender.JIGUANG, MessageTypeEnum.PIANO_ROOM_ADJUST, receivers, null,
                    0, null, ClientEnum.STUDENT.getCode(), teacher.getUsername(),"琴房课",courseName,dateString);

        } catch (Exception e) {
            log.error("琴房课调整发送消息失败--> {}", e.fillInStackTrace());
        }
    }

    /**
     * @Description: 调整学员
     * @Author: cy
     * @Date: 2022/5/31
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateCourseStudent(Map<String, Object> param) {
        Long courseId = WrapperUtil.toLong(param, "courseId", "课程id不能为空!");
        Long teacherId = WrapperUtil.toLong(param, "teacherId", "老师id不能为空!");
        List<Integer> studentIdsList = (List<Integer>) param.get("studentIds");
        List<Long> studentIds = JSONArray.parseArray(studentIdsList.toString(), Long.class);

        Integer configValue = Integer.valueOf(sysConfigService.findConfigValue(SysConfigConstant.PIANO_ROOM_MAX_STUDENTS));
        if (studentIds.size() > configValue) {
            throw new BizException("成课学员人数超过房间最大容量{}", configValue);
        }

        if (CollectionUtils.isEmpty(studentIds)) {
            throw new BizException("学生id不能为空");
        }

        List<UserBindingTeacher> bindingTeachers = userBindingTeacherDao.selectList(Wrappers.<UserBindingTeacher>lambdaQuery().eq(UserBindingTeacher::getTeacherId, teacherId));
        if (CollectionUtils.isEmpty(bindingTeachers)) {
            throw new BizException("未绑定学员");
        }
        List<Long> students = bindingTeachers.stream().map(UserBindingTeacher::getStudentId).collect(Collectors.toList());
        if (!students.containsAll(studentIds)) {
            throw new BizException("存在未绑定学员");
        }

        CourseSchedule courseSchedule = baseMapper.selectOne(Wrappers.<CourseSchedule>lambdaQuery()
                .eq(CourseSchedule::getId, courseId)
                .eq(CourseSchedule::getTeacherId, teacherId)
                .eq(CourseSchedule::getStatus, CourseScheduleEnum.NOT_START.getCode())
                .eq(CourseSchedule::getType, CourseScheduleEnum.PIANO_ROOM_CLASS.getCode()));
        if (courseSchedule == null) {
            throw new BizException("无法修改课程");
        }
        Integer singleCourseTime = courseSchedule.getSingleCourseTime();
        Long groupId = courseSchedule.getCourseGroupId();

        List<CourseScheduleStudentPayment> paymentList = paymentDao.selectList(Wrappers.<CourseScheduleStudentPayment>lambdaQuery()
                .eq(CourseScheduleStudentPayment::getCourseId, courseId)
                .eq(CourseScheduleStudentPayment::getCourseType, CourseScheduleEnum.PIANO_ROOM_CLASS.getCode()));
        if (CollectionUtils.isEmpty(paymentList)) {
            throw new BizException("未查到排课学员");
        }

        //更新老师琴房时长账户
        DistributedLock.of(redissonClient)
                .runIfLockToFunction(CacheNameEnum.LOCK_CHANGE_PIANO_ROOM_ACCOUNT.getRedisKey(teacherId)
                        , (o) -> {
                            PianoRoomTime pianoRoomTime = pianoRoomTimeDao.selectOne(Wrappers.<PianoRoomTime>lambdaQuery().eq(PianoRoomTime::getTeacherId, teacherId));
                            if (pianoRoomTime == null) {
                                throw new BizException("未查到剩余时长");
                            }
                            Integer frozenTime = pianoRoomTime.getFrozenTime();//冻结时长
                            Integer remainTime = pianoRoomTime.getRemainTime();//剩余时长

                            String formula = sysConfigService.findConfigValue(SysConfigConstant.PIANO_ROOM_TIME_FORMULA);
                            Integer after = (Integer) WrapperUtil.strToFormula(formula, "n", String.valueOf(studentIds.size() + 1));//变更后
                            Integer before = (Integer) WrapperUtil.strToFormula(formula, "n", String.valueOf(paymentList.size() + 1));//变更前
                            if (after == null || before == null) {
                                throw new BizException("公式转换失败");
                            }
                            int diffTime = singleCourseTime * (after - before);//人数变更后单课耗时
                            if (remainTime - diffTime < 0) {
                                throw new BizException("剩余时长不足");
                            }
                            pianoRoomTimeDao.update(null, Wrappers.<PianoRoomTime>lambdaUpdate()
                                    .eq(PianoRoomTime::getTeacherId, teacherId)
                                    .set(PianoRoomTime::getRemainTime, remainTime - diffTime)
                                    .set(PianoRoomTime::getFrozenTime, frozenTime + diffTime));
                            return null;
                        }, null, 10l);

        //删除原学员
        paymentDao.delete(Wrappers.<CourseScheduleStudentPayment>lambdaQuery().eq(CourseScheduleStudentPayment::getCourseId, courseId));

        CourseTimeEntity courseTime = new CourseTimeEntity();
        courseTime.setStartTime(courseSchedule.getStartTime());
        courseTime.setEndTime(courseSchedule.getEndTime());
        List<CourseTimeEntity> timeList = Arrays.asList(courseTime);

        List<CourseScheduleStudentPayment> payments = new ArrayList<>();
        for (Long studentId : studentIds) {
            //校验购买的课程组每节课时间是否和自己的课时冲突
            this.batchCheckStudentCourseTime(studentId, timeList, CourseTimeEntity::getStartTime, CourseTimeEntity::getEndTime);

            CourseScheduleStudentPayment payment = new CourseScheduleStudentPayment();
            payment.setUserId(studentId);
            payment.setCourseId(courseId);
            payment.setCourseGroupId(groupId);
            payment.setCourseType(CourseScheduleEnum.PIANO_ROOM_CLASS.getCode());
            payment.setCreatedTime(new Date());
            payment.setUpdatedTime(new Date());
            payments.add(payment);
        }

        //重新新增学员
        paymentDao.insertBatch(payments);
    }

    @Override
    public IPage<PianoClassVo> queryPianoClass(IPage<PianoClassVo> page, MyCourseSearch search) {
        monthToDate(search);
        return page.setRecords(baseMapper.queryPianoClass(page, search));
    }
}