新增培训:实现课次排课P0能力
This commit is contained in:
1 parent
e340db872c
commit
fb87c95506
17 files changed
+3500
No files matched your search
+94
@@ -0,0 +1,94 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.train.domain.dto.LessonCreateDTO;
|
||||
import com.ruoyi.train.domain.dto.LessonConflictCheckDTO;
|
||||
import com.ruoyi.train.domain.dto.LessonUpdateDTO;
|
||||
import com.ruoyi.train.service.SchedulingAppService;
|
||||
|
||||
/**
|
||||
* 课次编排与冲突检测 API (ISSUE-04-01 / EPIC-04)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
public class TrainLessonController extends BaseController
|
||||
{
|
||||
private final SchedulingAppService schedulingAppService;
|
||||
|
||||
@Autowired
|
||||
public TrainLessonController(SchedulingAppService schedulingAppService)
|
||||
{
|
||||
this.schedulingAppService = schedulingAppService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增课次 (UC-07)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:lesson:add')")
|
||||
@Log(title = "新增课次", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/train/plans/{planId}/lessons")
|
||||
public AjaxResult addLesson(@PathVariable String planId, @Validated @RequestBody LessonCreateDTO dto)
|
||||
{
|
||||
Long lessonId = schedulingAppService.createLesson(planId, dto, SecurityUtils.getUsername());
|
||||
return success(lessonId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改课次 (UC-08)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:lesson:edit')")
|
||||
@Log(title = "修改课次", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/train/lessons/{lessonId}")
|
||||
public AjaxResult editLesson(@PathVariable Long lessonId, @Validated @RequestBody LessonUpdateDTO dto)
|
||||
{
|
||||
schedulingAppService.updateLesson(lessonId, dto, SecurityUtils.getUsername());
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除课次 (UC-09)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:lesson:remove')")
|
||||
@Log(title = "删除课次", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/train/lessons/{lessonId}")
|
||||
public AjaxResult removeLesson(@PathVariable Long lessonId)
|
||||
{
|
||||
schedulingAppService.deleteLesson(lessonId, SecurityUtils.getUsername());
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 排课冲突检查预检 (UC-10)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:lesson:query')")
|
||||
@PostMapping("/train/lessons/check-conflict")
|
||||
public AjaxResult checkConflict(@Validated @RequestBody LessonConflictCheckDTO dto)
|
||||
{
|
||||
return success(schedulingAppService.checkConflict(dto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取计划完整课程表聚合 (UC-11 / 编辑回显必需)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:plan:query')")
|
||||
@GetMapping("/train/plans/{planId}/timetable")
|
||||
public AjaxResult getTimetable(@PathVariable String planId)
|
||||
{
|
||||
return success(schedulingAppService.getPlanTimetable(planId));
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import com.ruoyi.train.domain.TrainLesson;
|
||||
import com.ruoyi.train.domain.dto.LessonCreateDTO;
|
||||
import com.ruoyi.train.domain.dto.LessonConflictCheckDTO;
|
||||
import com.ruoyi.train.domain.dto.LessonUpdateDTO;
|
||||
|
||||
/**
|
||||
* ISSUE-04-01 Controller 契约与冻结边界测试。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class TrainLessonControllerTest
|
||||
{
|
||||
@Test
|
||||
public void shouldExposeFrozenP0MethodsAndPermissions() throws Exception
|
||||
{
|
||||
// 1. 新增课次
|
||||
Method add = TrainLessonController.class.getMethod("addLesson", String.class, LessonCreateDTO.class);
|
||||
assertNotNull(add.getAnnotation(PostMapping.class));
|
||||
assertEquals("/train/plans/{planId}/lessons", add.getAnnotation(PostMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:lesson:add')", add.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 2. 修改课次
|
||||
Method edit = TrainLessonController.class.getMethod("editLesson", Long.class, LessonUpdateDTO.class);
|
||||
assertNotNull(edit.getAnnotation(PutMapping.class));
|
||||
assertEquals("/train/lessons/{lessonId}", edit.getAnnotation(PutMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:lesson:edit')", edit.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 3. 删除课次
|
||||
Method remove = TrainLessonController.class.getMethod("removeLesson", Long.class);
|
||||
assertNotNull(remove.getAnnotation(DeleteMapping.class));
|
||||
assertEquals("/train/lessons/{lessonId}", remove.getAnnotation(DeleteMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:lesson:remove')", remove.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 4. 排课冲突检查
|
||||
Method check = TrainLessonController.class.getMethod("checkConflict", LessonConflictCheckDTO.class);
|
||||
assertNotNull(check.getAnnotation(PostMapping.class));
|
||||
assertEquals("/train/lessons/check-conflict", check.getAnnotation(PostMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:lesson:query')", check.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 5. 计划课表日历聚合 (编辑回显)
|
||||
Method timetable = TrainLessonController.class.getMethod("getTimetable", String.class);
|
||||
assertNotNull(timetable.getAnnotation(GetMapping.class));
|
||||
assertEquals("/train/plans/{planId}/timetable", timetable.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:plan:query')", timetable.getAnnotation(PreAuthorize.class).value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controllerMustNotInventSingleGetLessonEndpoint()
|
||||
{
|
||||
// 严禁发明 GET /train/lessons/{lessonId}
|
||||
for (Method method : TrainLessonController.class.getDeclaredMethods())
|
||||
{
|
||||
if (method.isAnnotationPresent(GetMapping.class))
|
||||
{
|
||||
String[] paths = method.getAnnotation(GetMapping.class).value();
|
||||
for (String path : paths)
|
||||
{
|
||||
assertFalse("不允许发明 GET /train/lessons/{lessonId}", path.equals("/train/lessons/{lessonId}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controllerMustNotExposePersistenceDomain()
|
||||
{
|
||||
// 严禁入参或出参直接暴露 TrainLesson 数据库持久化实体
|
||||
for (Method method : TrainLessonController.class.getDeclaredMethods())
|
||||
{
|
||||
assertFalse(method.getReturnType().equals(TrainLesson.class));
|
||||
for (Class<?> parameterType : method.getParameterTypes())
|
||||
{
|
||||
assertFalse(parameterType.equals(TrainLesson.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controllerMustOnlyExposeFrozenFiveEndpoints()
|
||||
{
|
||||
int endpointCount = 0;
|
||||
for (Method method : TrainLessonController.class.getDeclaredMethods())
|
||||
{
|
||||
boolean isEndpoint = method.isAnnotationPresent(GetMapping.class)
|
||||
|| method.isAnnotationPresent(PostMapping.class)
|
||||
|| method.isAnnotationPresent(PutMapping.class)
|
||||
|| method.isAnnotationPresent(DeleteMapping.class);
|
||||
if (isEndpoint)
|
||||
{
|
||||
endpointCount++;
|
||||
}
|
||||
}
|
||||
assertEquals("Controller 必须不多不少正好暴露 5 个冻结端点", 5, endpointCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.ruoyi.train.constant;
|
||||
|
||||
/**
|
||||
* 排课与课次管理业务错误码 (API Contract V1.0 - EPIC-04 / ISSUE-04-01)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public final class LessonErrorCode
|
||||
{
|
||||
/** 教师排课时间冲突:该教师在指定时段已有其他教学任务 */
|
||||
public static final int LESSON_TEACHER_CONFLICT = 40040;
|
||||
|
||||
/** 教室排课时间冲突:该教室在指定时段已被占用 */
|
||||
public static final int LESSON_CLASSROOM_CONFLICT = 40041;
|
||||
|
||||
/** 实训场地排课时间冲突:该实训场地在指定时段已被占用 */
|
||||
public static final int LESSON_SITE_CONFLICT = 40042;
|
||||
|
||||
/** 本班课次时间重叠:同一培训班不可在同一时段上多门课 */
|
||||
public static final int LESSON_PLAN_OVERLAP = 40043;
|
||||
|
||||
/** 该课次已有学员签到打卡,禁止修改上课时间 / 严禁删除 */
|
||||
public static final int LESSON_ALREADY_HAS_ATTENDANCE = 40044;
|
||||
|
||||
/** 排课场地配置不合法:普通教室与实训场地必须二选一 */
|
||||
public static final int VENUE_CONFIG_INVALID = 40045;
|
||||
|
||||
/** 排课课次不存在 */
|
||||
public static final int LESSON_NOT_FOUND = 40440;
|
||||
|
||||
/** 授课教师不存在或已无效 */
|
||||
public static final int TEACHER_NOT_FOUND = 40401;
|
||||
|
||||
/** 教学教室不存在或已无效 */
|
||||
public static final int CLASSROOM_NOT_FOUND = 40408;
|
||||
|
||||
/** 实训场地不存在或已无效 */
|
||||
public static final int SITE_NOT_FOUND = 40409;
|
||||
|
||||
public static final String LESSON_TEACHER_CONFLICT_MESSAGE = "教师排课时间冲突:该教师在指定时段已有其他教学任务";
|
||||
public static final String LESSON_CLASSROOM_CONFLICT_MESSAGE = "教室排课时间冲突:该教室在指定时段已被占用";
|
||||
public static final String LESSON_SITE_CONFLICT_MESSAGE = "实训场地排课时间冲突:该实训场地在指定时段已被占用";
|
||||
public static final String LESSON_PLAN_OVERLAP_MESSAGE = "本班课次时间重叠:同一培训班不可在同一时段上多门课";
|
||||
public static final String LESSON_ALREADY_HAS_ATTENDANCE_MESSAGE = "该课次已有学员签到打卡,禁止修改上课时间";
|
||||
public static final String LESSON_ALREADY_HAS_ATTENDANCE_DELETE_MESSAGE = "该课次已有学员签到打卡,严禁删除";
|
||||
public static final String VENUE_CONFIG_INVALID_MESSAGE = "排课场地配置不合法:普通教室与实训场地必须二选一";
|
||||
public static final String LESSON_NOT_FOUND_MESSAGE = "排课课次不存在";
|
||||
public static final String TEACHER_NOT_FOUND_MESSAGE = "授课教师不存在或已无效";
|
||||
public static final String CLASSROOM_NOT_FOUND_MESSAGE = "教学教室不存在或已无效";
|
||||
public static final String SITE_NOT_FOUND_MESSAGE = "实训场地不存在或已无效";
|
||||
|
||||
private LessonErrorCode()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.ruoyi.train.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 课次教学活动对象 train_lesson
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
public class TrainLesson extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 课次ID (核心ID之一) */
|
||||
private Long lessonId;
|
||||
|
||||
/** 所属培训课程实例ID */
|
||||
@Excel(name = "所属培训课程实例ID")
|
||||
private Long planCourseId;
|
||||
|
||||
/** 授课教师ID (课次必要组成) */
|
||||
@Excel(name = "授课教师ID")
|
||||
private String tId;
|
||||
|
||||
/** 课次开始时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "课次开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
|
||||
/** 课次结束时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "课次结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
|
||||
/** 场地类型 (1:普通教室, 2:实训场地) */
|
||||
@Excel(name = "场地类型", readConverterExp = "1=普通教室,2=实训场地")
|
||||
private Integer venueType;
|
||||
|
||||
/** 普通教室ID */
|
||||
@Excel(name = "普通教室ID")
|
||||
private String classRoomId;
|
||||
|
||||
/** 实训场地ID */
|
||||
@Excel(name = "实训场地ID")
|
||||
private String siteId;
|
||||
|
||||
public Long getLessonId()
|
||||
{
|
||||
return lessonId;
|
||||
}
|
||||
|
||||
public void setLessonId(Long lessonId)
|
||||
{
|
||||
this.lessonId = lessonId;
|
||||
}
|
||||
|
||||
public Long getPlanCourseId()
|
||||
{
|
||||
return planCourseId;
|
||||
}
|
||||
|
||||
public void setPlanCourseId(Long planCourseId)
|
||||
{
|
||||
this.planCourseId = planCourseId;
|
||||
}
|
||||
|
||||
public String getTId()
|
||||
{
|
||||
return tId;
|
||||
}
|
||||
|
||||
public void setTId(String tId)
|
||||
{
|
||||
this.tId = tId;
|
||||
}
|
||||
|
||||
public Date getStartTime()
|
||||
{
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(Date startTime)
|
||||
{
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public Date getEndTime()
|
||||
{
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(Date endTime)
|
||||
{
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public Integer getVenueType()
|
||||
{
|
||||
return venueType;
|
||||
}
|
||||
|
||||
public void setVenueType(Integer venueType)
|
||||
{
|
||||
this.venueType = venueType;
|
||||
}
|
||||
|
||||
public String getClassRoomId()
|
||||
{
|
||||
return classRoomId;
|
||||
}
|
||||
|
||||
public void setClassRoomId(String classRoomId)
|
||||
{
|
||||
this.classRoomId = classRoomId;
|
||||
}
|
||||
|
||||
public String getSiteId()
|
||||
{
|
||||
return siteId;
|
||||
}
|
||||
|
||||
public void setSiteId(String siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
|
||||
.append("lessonId", getLessonId())
|
||||
.append("planCourseId", getPlanCourseId())
|
||||
.append("tId", getTId())
|
||||
.append("startTime", getStartTime())
|
||||
.append("endTime", getEndTime())
|
||||
.append("venueType", getVenueType())
|
||||
.append("classRoomId", getClassRoomId())
|
||||
.append("siteId", getSiteId())
|
||||
.append("createBy", getCreateBy())
|
||||
.append("createTime", getCreateTime())
|
||||
.append("updateBy", getUpdateBy())
|
||||
.append("updateTime", getUpdateTime())
|
||||
.append("remark", getRemark())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package com.ruoyi.train.domain.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 排课冲突预检请求 DTO (UC-10)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class LessonConflictCheckDTO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 课次主键ID (编辑排课预检时传入以排除自身,新增排课时传空) */
|
||||
private Long lessonId;
|
||||
|
||||
/** 培训计划ID */
|
||||
@NotBlank(message = "培训计划ID不能为空")
|
||||
private String planId;
|
||||
|
||||
/** 本期培训课程实例ID (可选) */
|
||||
private Long planCourseId;
|
||||
|
||||
/** 授课教师工号/标识 */
|
||||
@NotBlank(message = "授课教师工号/标识不能为空")
|
||||
@Size(max = 32, message = "教师工号长度不能超过32个字符")
|
||||
private String tId;
|
||||
|
||||
/** 课次开始时间 */
|
||||
@NotNull(message = "课次开始时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
|
||||
/** 课次结束时间 */
|
||||
@NotNull(message = "课次结束时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
|
||||
/** 场地类型 (1:普通教室, 2:实训场地) */
|
||||
@NotNull(message = "场地类型不能为空 (1:普通教室, 2:实训场地)")
|
||||
private Integer venueType;
|
||||
|
||||
/** 普通教室ID */
|
||||
@Size(max = 32, message = "教室ID长度不能超过32个字符")
|
||||
private String classRoomId;
|
||||
|
||||
/** 实训场地ID */
|
||||
@Size(max = 32, message = "实训场地ID长度不能超过32个字符")
|
||||
private String siteId;
|
||||
|
||||
public Long getLessonId()
|
||||
{
|
||||
return lessonId;
|
||||
}
|
||||
|
||||
public void setLessonId(Long lessonId)
|
||||
{
|
||||
this.lessonId = lessonId;
|
||||
}
|
||||
|
||||
public String getPlanId()
|
||||
{
|
||||
return planId;
|
||||
}
|
||||
|
||||
public void setPlanId(String planId)
|
||||
{
|
||||
this.planId = planId;
|
||||
}
|
||||
|
||||
public Long getPlanCourseId()
|
||||
{
|
||||
return planCourseId;
|
||||
}
|
||||
|
||||
public void setPlanCourseId(Long planCourseId)
|
||||
{
|
||||
this.planCourseId = planCourseId;
|
||||
}
|
||||
|
||||
public String getTId()
|
||||
{
|
||||
return tId;
|
||||
}
|
||||
|
||||
public void setTId(String tId)
|
||||
{
|
||||
this.tId = tId;
|
||||
}
|
||||
|
||||
public Date getStartTime()
|
||||
{
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(Date startTime)
|
||||
{
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public Date getEndTime()
|
||||
{
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(Date endTime)
|
||||
{
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public Integer getVenueType()
|
||||
{
|
||||
return venueType;
|
||||
}
|
||||
|
||||
public void setVenueType(Integer venueType)
|
||||
{
|
||||
this.venueType = venueType;
|
||||
}
|
||||
|
||||
public String getClassRoomId()
|
||||
{
|
||||
return classRoomId;
|
||||
}
|
||||
|
||||
public void setClassRoomId(String classRoomId)
|
||||
{
|
||||
this.classRoomId = classRoomId;
|
||||
}
|
||||
|
||||
public String getSiteId()
|
||||
{
|
||||
return siteId;
|
||||
}
|
||||
|
||||
public void setSiteId(String siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.ruoyi.train.domain.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 新增课次请求 DTO (UC-07)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class LessonCreateDTO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 本期培训课程实例ID */
|
||||
@NotNull(message = "本期课程实例ID不能为空")
|
||||
private Long planCourseId;
|
||||
|
||||
/** 授课教师工号/标识 */
|
||||
@NotBlank(message = "授课教师工号/标识不能为空")
|
||||
@Size(max = 32, message = "教师工号长度不能超过32个字符")
|
||||
private String tId;
|
||||
|
||||
/** 课次开始时间 */
|
||||
@NotNull(message = "课次开始时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
|
||||
/** 课次结束时间 */
|
||||
@NotNull(message = "课次结束时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
|
||||
/** 场地类型 (1:普通教室, 2:实训场地) */
|
||||
@NotNull(message = "场地类型不能为空 (1:普通教室, 2:实训场地)")
|
||||
private Integer venueType;
|
||||
|
||||
/** 普通教室ID (venueType=1 时有效且必填) */
|
||||
@Size(max = 32, message = "教室ID长度不能超过32个字符")
|
||||
private String classRoomId;
|
||||
|
||||
/** 实训场地ID (venueType=2 时有效且必填) */
|
||||
@Size(max = 32, message = "实训场地ID长度不能超过32个字符")
|
||||
private String siteId;
|
||||
|
||||
/** 课次备注 */
|
||||
@Size(max = 500, message = "备注长度不能超过500个字符")
|
||||
private String remark;
|
||||
|
||||
public Long getPlanCourseId()
|
||||
{
|
||||
return planCourseId;
|
||||
}
|
||||
|
||||
public void setPlanCourseId(Long planCourseId)
|
||||
{
|
||||
this.planCourseId = planCourseId;
|
||||
}
|
||||
|
||||
public String getTId()
|
||||
{
|
||||
return tId;
|
||||
}
|
||||
|
||||
public void setTId(String tId)
|
||||
{
|
||||
this.tId = tId;
|
||||
}
|
||||
|
||||
public Date getStartTime()
|
||||
{
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(Date startTime)
|
||||
{
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public Date getEndTime()
|
||||
{
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(Date endTime)
|
||||
{
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public Integer getVenueType()
|
||||
{
|
||||
return venueType;
|
||||
}
|
||||
|
||||
public void setVenueType(Integer venueType)
|
||||
{
|
||||
this.venueType = venueType;
|
||||
}
|
||||
|
||||
public String getClassRoomId()
|
||||
{
|
||||
return classRoomId;
|
||||
}
|
||||
|
||||
public void setClassRoomId(String classRoomId)
|
||||
{
|
||||
this.classRoomId = classRoomId;
|
||||
}
|
||||
|
||||
public String getSiteId()
|
||||
{
|
||||
return siteId;
|
||||
}
|
||||
|
||||
public void setSiteId(String siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
public String getRemark()
|
||||
{
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark)
|
||||
{
|
||||
this.remark = remark;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.ruoyi.train.domain.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 修改课次请求 DTO (UC-08)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class LessonUpdateDTO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 本期培训课程实例ID */
|
||||
@NotNull(message = "本期课程实例ID不能为空")
|
||||
private Long planCourseId;
|
||||
|
||||
/** 授课教师工号/标识 */
|
||||
@NotBlank(message = "授课教师工号/标识不能为空")
|
||||
@Size(max = 32, message = "教师工号长度不能超过32个字符")
|
||||
private String tId;
|
||||
|
||||
/** 课次开始时间 */
|
||||
@NotNull(message = "课次开始时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
|
||||
/** 课次结束时间 */
|
||||
@NotNull(message = "课次结束时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
|
||||
/** 场地类型 (1:普通教室, 2:实训场地) */
|
||||
@NotNull(message = "场地类型不能为空 (1:普通教室, 2:实训场地)")
|
||||
private Integer venueType;
|
||||
|
||||
/** 普通教室ID (venueType=1 时有效且必填) */
|
||||
@Size(max = 32, message = "教室ID长度不能超过32个字符")
|
||||
private String classRoomId;
|
||||
|
||||
/** 实训场地ID (venueType=2 时有效且必填) */
|
||||
@Size(max = 32, message = "实训场地ID长度不能超过32个字符")
|
||||
private String siteId;
|
||||
|
||||
/** 课次备注 */
|
||||
@Size(max = 500, message = "备注长度不能超过500个字符")
|
||||
private String remark;
|
||||
|
||||
public Long getPlanCourseId()
|
||||
{
|
||||
return planCourseId;
|
||||
}
|
||||
|
||||
public void setPlanCourseId(Long planCourseId)
|
||||
{
|
||||
this.planCourseId = planCourseId;
|
||||
}
|
||||
|
||||
public String getTId()
|
||||
{
|
||||
return tId;
|
||||
}
|
||||
|
||||
public void setTId(String tId)
|
||||
{
|
||||
this.tId = tId;
|
||||
}
|
||||
|
||||
public Date getStartTime()
|
||||
{
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(Date startTime)
|
||||
{
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public Date getEndTime()
|
||||
{
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(Date endTime)
|
||||
{
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public Integer getVenueType()
|
||||
{
|
||||
return venueType;
|
||||
}
|
||||
|
||||
public void setVenueType(Integer venueType)
|
||||
{
|
||||
this.venueType = venueType;
|
||||
}
|
||||
|
||||
public String getClassRoomId()
|
||||
{
|
||||
return classRoomId;
|
||||
}
|
||||
|
||||
public void setClassRoomId(String classRoomId)
|
||||
{
|
||||
this.classRoomId = classRoomId;
|
||||
}
|
||||
|
||||
public String getSiteId()
|
||||
{
|
||||
return siteId;
|
||||
}
|
||||
|
||||
public void setSiteId(String siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
public String getRemark()
|
||||
{
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark)
|
||||
{
|
||||
this.remark = remark;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.ruoyi.train.domain.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 冲突课次明细 VO。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class ConflictItemVO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 课次主键标识 */
|
||||
private Long lessonId;
|
||||
|
||||
/** 培训计划标识 */
|
||||
private String planId;
|
||||
|
||||
/** 培训班名称 */
|
||||
private String planName;
|
||||
|
||||
/** 课程名称 */
|
||||
private String courseName;
|
||||
|
||||
/** 授课教师姓名 */
|
||||
private String teacherName;
|
||||
|
||||
/** 场地展示名称 */
|
||||
private String venueName;
|
||||
|
||||
/** 课次开始时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
|
||||
/** 课次结束时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
|
||||
public Long getLessonId()
|
||||
{
|
||||
return lessonId;
|
||||
}
|
||||
|
||||
public void setLessonId(Long lessonId)
|
||||
{
|
||||
this.lessonId = lessonId;
|
||||
}
|
||||
|
||||
public String getPlanId()
|
||||
{
|
||||
return planId;
|
||||
}
|
||||
|
||||
public void setPlanId(String planId)
|
||||
{
|
||||
this.planId = planId;
|
||||
}
|
||||
|
||||
public String getPlanName()
|
||||
{
|
||||
return planName;
|
||||
}
|
||||
|
||||
public void setPlanName(String planName)
|
||||
{
|
||||
this.planName = planName;
|
||||
}
|
||||
|
||||
public String getCourseName()
|
||||
{
|
||||
return courseName;
|
||||
}
|
||||
|
||||
public void setCourseName(String courseName)
|
||||
{
|
||||
this.courseName = courseName;
|
||||
}
|
||||
|
||||
public String getTeacherName()
|
||||
{
|
||||
return teacherName;
|
||||
}
|
||||
|
||||
public void setTeacherName(String teacherName)
|
||||
{
|
||||
this.teacherName = teacherName;
|
||||
}
|
||||
|
||||
public String getVenueName()
|
||||
{
|
||||
return venueName;
|
||||
}
|
||||
|
||||
public void setVenueName(String venueName)
|
||||
{
|
||||
this.venueName = venueName;
|
||||
}
|
||||
|
||||
public Date getStartTime()
|
||||
{
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(Date startTime)
|
||||
{
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public Date getEndTime()
|
||||
{
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(Date endTime)
|
||||
{
|
||||
this.endTime = endTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.ruoyi.train.domain.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 排课冲突检测结果 VO (API Contract 1.4)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class LessonConflictVO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 是否存在冲突 */
|
||||
private Boolean hasConflict;
|
||||
|
||||
/** 冲突类型 (NONE, TEACHER, CLASSROOM, SITE, PLAN_OVERLAP) */
|
||||
private String conflictType;
|
||||
|
||||
/** 冲突描述文本 */
|
||||
private String conflictMessage;
|
||||
|
||||
/** 冲突的现有课次明细列表 */
|
||||
private List<ConflictItemVO> conflictingLessons = new ArrayList<>();
|
||||
|
||||
public Boolean getHasConflict()
|
||||
{
|
||||
return hasConflict;
|
||||
}
|
||||
|
||||
public void setHasConflict(Boolean hasConflict)
|
||||
{
|
||||
this.hasConflict = hasConflict;
|
||||
}
|
||||
|
||||
public String getConflictType()
|
||||
{
|
||||
return conflictType;
|
||||
}
|
||||
|
||||
public void setConflictType(String conflictType)
|
||||
{
|
||||
this.conflictType = conflictType;
|
||||
}
|
||||
|
||||
public String getConflictMessage()
|
||||
{
|
||||
return conflictMessage;
|
||||
}
|
||||
|
||||
public void setConflictMessage(String conflictMessage)
|
||||
{
|
||||
this.conflictMessage = conflictMessage;
|
||||
}
|
||||
|
||||
public List<ConflictItemVO> getConflictingLessons()
|
||||
{
|
||||
return conflictingLessons;
|
||||
}
|
||||
|
||||
public void setConflictingLessons(List<ConflictItemVO> conflictingLessons)
|
||||
{
|
||||
this.conflictingLessons = conflictingLessons;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.ruoyi.train.domain.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 计划完整课程表 VO (API Contract 1.2 / CG-02 聚合课表与编辑回显)。
|
||||
* 下属 days[].lessons 集合项为扩展后的 TimetableLessonItemVO。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class PlanTimetableVO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 培训计划标识 */
|
||||
private String planId;
|
||||
|
||||
/** 培训班名称 */
|
||||
private String planName;
|
||||
|
||||
/** 课表开始日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date startDate;
|
||||
|
||||
/** 课表结束日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date endDate;
|
||||
|
||||
/** 按日期分组的课次排期集合 */
|
||||
private List<TimetableDayVO> days = new ArrayList<>();
|
||||
|
||||
public String getPlanId()
|
||||
{
|
||||
return planId;
|
||||
}
|
||||
|
||||
public void setPlanId(String planId)
|
||||
{
|
||||
this.planId = planId;
|
||||
}
|
||||
|
||||
public String getPlanName()
|
||||
{
|
||||
return planName;
|
||||
}
|
||||
|
||||
public void setPlanName(String planName)
|
||||
{
|
||||
this.planName = planName;
|
||||
}
|
||||
|
||||
public Date getStartDate()
|
||||
{
|
||||
return startDate;
|
||||
}
|
||||
|
||||
public void setStartDate(Date startDate)
|
||||
{
|
||||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
public Date getEndDate()
|
||||
{
|
||||
return endDate;
|
||||
}
|
||||
|
||||
public void setEndDate(Date endDate)
|
||||
{
|
||||
this.endDate = endDate;
|
||||
}
|
||||
|
||||
public List<TimetableDayVO> getDays()
|
||||
{
|
||||
return days;
|
||||
}
|
||||
|
||||
public void setDays(List<TimetableDayVO> days)
|
||||
{
|
||||
this.days = days;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.ruoyi.train.domain.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 按日期分组的排期集合 VO。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class TimetableDayVO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 日期 (yyyy-MM-dd) */
|
||||
private String date;
|
||||
|
||||
/** 星期几 (1~7,1为周一,7为周日) */
|
||||
private Integer dayOfWeek;
|
||||
|
||||
/** 当天课次集合 */
|
||||
private List<TimetableLessonItemVO> lessons = new ArrayList<>();
|
||||
|
||||
public String getDate()
|
||||
{
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(String date)
|
||||
{
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public Integer getDayOfWeek()
|
||||
{
|
||||
return dayOfWeek;
|
||||
}
|
||||
|
||||
public void setDayOfWeek(Integer dayOfWeek)
|
||||
{
|
||||
this.dayOfWeek = dayOfWeek;
|
||||
}
|
||||
|
||||
public List<TimetableLessonItemVO> getLessons()
|
||||
{
|
||||
return lessons;
|
||||
}
|
||||
|
||||
public void setLessons(List<TimetableLessonItemVO> lessons)
|
||||
{
|
||||
this.lessons = lessons;
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
package com.ruoyi.train.domain.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 计划课表课次明细项 VO (API Contract 1.2 / CG-02 / 回显表单必需)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class TimetableLessonItemVO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 课次主键标识 */
|
||||
private Long lessonId;
|
||||
|
||||
/** 本期计划课程关联标识 (回显表单必需) */
|
||||
private Long planCourseId;
|
||||
|
||||
/** 本期课程名称 */
|
||||
private String courseName;
|
||||
|
||||
/** 授课教师工号/标识 (回显表单必需) */
|
||||
private String tId;
|
||||
|
||||
/** 授课教师姓名 */
|
||||
private String teacherName;
|
||||
|
||||
/** 课次开始时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
|
||||
/** 课次结束时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
|
||||
/** 场地类型 (1:普通教室, 2:实训场地) */
|
||||
private Integer venueType;
|
||||
|
||||
/** 普通教室物理标识 (venueType=1 时有效,回显表单必需) */
|
||||
private String classRoomId;
|
||||
|
||||
/** 普通教室名称 */
|
||||
private String classRoomName;
|
||||
|
||||
/** 实训场地物理标识 (venueType=2 时有效,回显表单必需) */
|
||||
private String siteId;
|
||||
|
||||
/** 实训场地名称 */
|
||||
private String siteName;
|
||||
|
||||
/** 场地聚合展示名称 */
|
||||
private String venueName;
|
||||
|
||||
/** 关联电子班牌 SN 号 (若有) */
|
||||
private String boardSn;
|
||||
|
||||
/** 课次备注 (回显表单必需) */
|
||||
private String remark;
|
||||
|
||||
public Long getLessonId()
|
||||
{
|
||||
return lessonId;
|
||||
}
|
||||
|
||||
public void setLessonId(Long lessonId)
|
||||
{
|
||||
this.lessonId = lessonId;
|
||||
}
|
||||
|
||||
public Long getPlanCourseId()
|
||||
{
|
||||
return planCourseId;
|
||||
}
|
||||
|
||||
public void setPlanCourseId(Long planCourseId)
|
||||
{
|
||||
this.planCourseId = planCourseId;
|
||||
}
|
||||
|
||||
public String getCourseName()
|
||||
{
|
||||
return courseName;
|
||||
}
|
||||
|
||||
public void setCourseName(String courseName)
|
||||
{
|
||||
this.courseName = courseName;
|
||||
}
|
||||
|
||||
public String getTId()
|
||||
{
|
||||
return tId;
|
||||
}
|
||||
|
||||
public void setTId(String tId)
|
||||
{
|
||||
this.tId = tId;
|
||||
}
|
||||
|
||||
public String getTeacherName()
|
||||
{
|
||||
return teacherName;
|
||||
}
|
||||
|
||||
public void setTeacherName(String teacherName)
|
||||
{
|
||||
this.teacherName = teacherName;
|
||||
}
|
||||
|
||||
public Date getStartTime()
|
||||
{
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(Date startTime)
|
||||
{
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public Date getEndTime()
|
||||
{
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(Date endTime)
|
||||
{
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public Integer getVenueType()
|
||||
{
|
||||
return venueType;
|
||||
}
|
||||
|
||||
public void setVenueType(Integer venueType)
|
||||
{
|
||||
this.venueType = venueType;
|
||||
}
|
||||
|
||||
public String getClassRoomId()
|
||||
{
|
||||
return classRoomId;
|
||||
}
|
||||
|
||||
public void setClassRoomId(String classRoomId)
|
||||
{
|
||||
this.classRoomId = classRoomId;
|
||||
}
|
||||
|
||||
public String getClassRoomName()
|
||||
{
|
||||
return classRoomName;
|
||||
}
|
||||
|
||||
public void setClassRoomName(String classRoomName)
|
||||
{
|
||||
this.classRoomName = classRoomName;
|
||||
}
|
||||
|
||||
public String getSiteId()
|
||||
{
|
||||
return siteId;
|
||||
}
|
||||
|
||||
public void setSiteId(String siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
public String getSiteName()
|
||||
{
|
||||
return siteName;
|
||||
}
|
||||
|
||||
public void setSiteName(String siteName)
|
||||
{
|
||||
this.siteName = siteName;
|
||||
}
|
||||
|
||||
public String getVenueName()
|
||||
{
|
||||
return venueName;
|
||||
}
|
||||
|
||||
public void setVenueName(String venueName)
|
||||
{
|
||||
this.venueName = venueName;
|
||||
}
|
||||
|
||||
public String getBoardSn()
|
||||
{
|
||||
return boardSn;
|
||||
}
|
||||
|
||||
public void setBoardSn(String boardSn)
|
||||
{
|
||||
this.boardSn = boardSn;
|
||||
}
|
||||
|
||||
public String getRemark()
|
||||
{
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark)
|
||||
{
|
||||
this.remark = remark;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.ruoyi.train.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.train.domain.TrainClassroomInfo;
|
||||
import com.ruoyi.train.domain.TrainLesson;
|
||||
import com.ruoyi.train.domain.TrainTeacher;
|
||||
import com.ruoyi.train.domain.TrainTrainingSite;
|
||||
import com.ruoyi.train.domain.vo.ConflictItemVO;
|
||||
import com.ruoyi.train.domain.vo.TimetableLessonItemVO;
|
||||
|
||||
/**
|
||||
* 课次教学活动 Mapper 接口 (ISSUE-04-01)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface TrainLessonMapper
|
||||
{
|
||||
/**
|
||||
* 查询课次详情。
|
||||
*
|
||||
* @param lessonId 课次ID
|
||||
* @return 课次信息
|
||||
*/
|
||||
TrainLesson selectTrainLessonById(Long lessonId);
|
||||
|
||||
/**
|
||||
* 新增课次。
|
||||
*
|
||||
* @param trainLesson 课次信息
|
||||
* @return 结果
|
||||
*/
|
||||
int insertTrainLesson(TrainLesson trainLesson);
|
||||
|
||||
/**
|
||||
* 修改课次。
|
||||
*
|
||||
* @param trainLesson 课次信息
|
||||
* @return 结果
|
||||
*/
|
||||
int updateTrainLesson(TrainLesson trainLesson);
|
||||
|
||||
/**
|
||||
* 删除课次。
|
||||
*
|
||||
* @param lessonId 课次ID
|
||||
* @return 结果
|
||||
*/
|
||||
int deleteTrainLessonById(Long lessonId);
|
||||
|
||||
/**
|
||||
* 加行级排他锁 (FOR UPDATE) 锁定稳定教师主表行。
|
||||
*
|
||||
* @param tId 教师工号
|
||||
* @return 教师主表记录
|
||||
*/
|
||||
TrainTeacher lockTeacherById(@Param("tId") String tId);
|
||||
|
||||
/**
|
||||
* 加行级排他锁 (FOR UPDATE) 锁定普通教室主表行。
|
||||
*
|
||||
* @param classRoomId 教室ID
|
||||
* @return 教室主表记录
|
||||
*/
|
||||
TrainClassroomInfo lockClassroomById(@Param("classRoomId") String classRoomId);
|
||||
|
||||
/**
|
||||
* 加行级排他锁 (FOR UPDATE) 锁定实训场地主表行。
|
||||
*
|
||||
* @param siteId 实训场地ID
|
||||
* @return 场地主表记录
|
||||
*/
|
||||
TrainTrainingSite lockSiteById(@Param("siteId") String siteId);
|
||||
|
||||
/**
|
||||
* 统计该课次下的学员考勤打卡流水数。
|
||||
*
|
||||
* @param lessonId 课次ID
|
||||
* @return 考勤记录数
|
||||
*/
|
||||
int countAttendanceByLessonId(@Param("lessonId") Long lessonId);
|
||||
|
||||
/**
|
||||
* 查询教师时间重叠冲突课次。
|
||||
*/
|
||||
List<ConflictItemVO> selectTeacherConflicts(@Param("tId") String tId,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("excludeLessonId") Long excludeLessonId);
|
||||
|
||||
/**
|
||||
* 查询普通教室时间重叠冲突课次。
|
||||
*/
|
||||
List<ConflictItemVO> selectClassroomConflicts(@Param("classRoomId") String classRoomId,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("excludeLessonId") Long excludeLessonId);
|
||||
|
||||
/**
|
||||
* 查询实训场地时间重叠冲突课次。
|
||||
*/
|
||||
List<ConflictItemVO> selectSiteConflicts(@Param("siteId") String siteId,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("excludeLessonId") Long excludeLessonId);
|
||||
|
||||
/**
|
||||
* 查询本期培训计划时间重叠冲突课次。
|
||||
*/
|
||||
List<ConflictItemVO> selectPlanConflicts(@Param("planId") String planId,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("excludeLessonId") Long excludeLessonId);
|
||||
|
||||
/**
|
||||
* 获取指定培训计划下的全部课次日程明细 (按开始时间升序排列)。
|
||||
*/
|
||||
List<TimetableLessonItemVO> selectScheduleLessonsByPlanId(@Param("planId") String planId);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.ruoyi.train.service;
|
||||
|
||||
import com.ruoyi.train.domain.dto.LessonCreateDTO;
|
||||
import com.ruoyi.train.domain.dto.LessonConflictCheckDTO;
|
||||
import com.ruoyi.train.domain.dto.LessonUpdateDTO;
|
||||
import com.ruoyi.train.domain.vo.LessonConflictVO;
|
||||
import com.ruoyi.train.domain.vo.PlanTimetableVO;
|
||||
|
||||
/**
|
||||
* 课次编排与冲突检测应用服务 (ISSUE-04-01 / EPIC-04)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface SchedulingAppService
|
||||
{
|
||||
/**
|
||||
* 新增课次 (UC-07)。
|
||||
*
|
||||
* @param planId 培训计划ID
|
||||
* @param dto 新增课次参数
|
||||
* @param operator 操作人员
|
||||
* @return 新增课次主键ID
|
||||
*/
|
||||
Long createLesson(String planId, LessonCreateDTO dto, String operator);
|
||||
|
||||
/**
|
||||
* 修改课次 (UC-08)。
|
||||
*
|
||||
* @param lessonId 课次ID
|
||||
* @param dto 修改课次参数
|
||||
* @param operator 操作人员
|
||||
*/
|
||||
void updateLesson(Long lessonId, LessonUpdateDTO dto, String operator);
|
||||
|
||||
/**
|
||||
* 删除课次 (UC-09)。
|
||||
*
|
||||
* @param lessonId 课次ID
|
||||
* @param operator 操作人员
|
||||
*/
|
||||
void deleteLesson(Long lessonId, String operator);
|
||||
|
||||
/**
|
||||
* 排课冲突检查预检 (UC-10)。
|
||||
*
|
||||
* @param dto 冲突检测参数
|
||||
* @return 冲突检测结果
|
||||
*/
|
||||
LessonConflictVO checkConflict(LessonConflictCheckDTO dto);
|
||||
|
||||
/**
|
||||
* 获取计划完整课程表聚合 (UC-11 / 编辑回显必需)。
|
||||
*
|
||||
* @param planId 培训计划ID
|
||||
* @return 完整课程表 VO
|
||||
*/
|
||||
PlanTimetableVO getPlanTimetable(String planId);
|
||||
}
|
||||
+516
@@ -0,0 +1,516 @@
|
||||
package com.ruoyi.train.service.impl;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
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 com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.train.constant.LessonErrorCode;
|
||||
import com.ruoyi.train.constant.PlanErrorCode;
|
||||
import com.ruoyi.train.domain.TrainClassPlan;
|
||||
import com.ruoyi.train.domain.TrainClassroomInfo;
|
||||
import com.ruoyi.train.domain.TrainLesson;
|
||||
import com.ruoyi.train.domain.TrainPlanCourse;
|
||||
import com.ruoyi.train.domain.TrainTeacher;
|
||||
import com.ruoyi.train.domain.TrainTrainingSite;
|
||||
import com.ruoyi.train.domain.dto.LessonCreateDTO;
|
||||
import com.ruoyi.train.domain.dto.LessonConflictCheckDTO;
|
||||
import com.ruoyi.train.domain.dto.LessonUpdateDTO;
|
||||
import com.ruoyi.train.domain.vo.ConflictItemVO;
|
||||
import com.ruoyi.train.domain.vo.LessonConflictVO;
|
||||
import com.ruoyi.train.domain.vo.PlanTimetableVO;
|
||||
import com.ruoyi.train.domain.vo.TimetableDayVO;
|
||||
import com.ruoyi.train.domain.vo.TimetableLessonItemVO;
|
||||
import com.ruoyi.train.mapper.TrainClassPlanMapper;
|
||||
import com.ruoyi.train.mapper.TrainLessonMapper;
|
||||
import com.ruoyi.train.mapper.TrainPlanCourseMapper;
|
||||
import com.ruoyi.train.service.SchedulingAppService;
|
||||
|
||||
/**
|
||||
* 课次编排与冲突检测应用服务实现类 (ISSUE-04-01 / EPIC-04)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Service
|
||||
public class SchedulingAppServiceImpl implements SchedulingAppService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(SchedulingAppServiceImpl.class);
|
||||
|
||||
private final TrainLessonMapper trainLessonMapper;
|
||||
private final TrainClassPlanMapper trainClassPlanMapper;
|
||||
private final TrainPlanCourseMapper trainPlanCourseMapper;
|
||||
|
||||
@Autowired
|
||||
public SchedulingAppServiceImpl(TrainLessonMapper trainLessonMapper,
|
||||
TrainClassPlanMapper trainClassPlanMapper,
|
||||
TrainPlanCourseMapper trainPlanCourseMapper)
|
||||
{
|
||||
this.trainLessonMapper = trainLessonMapper;
|
||||
this.trainClassPlanMapper = trainClassPlanMapper;
|
||||
this.trainPlanCourseMapper = trainPlanCourseMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增课次 (UC-07)。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createLesson(String planId, LessonCreateDTO dto, String operator)
|
||||
{
|
||||
// 1. 前置基础验证 (时间、场地二选一、班级状态、课程归属)
|
||||
validateBasicRules(planId, dto.getPlanCourseId(), dto.getVenueType(), dto.getClassRoomId(),
|
||||
dto.getSiteId(), dto.getStartTime(), dto.getEndTime());
|
||||
|
||||
// 2. 锁定稳定物理主资源行 (教师优先,教室/实训场地随后,兼具存在性校验与防并发死锁)
|
||||
Set<ResourceLockKey> lockKeys = new HashSet<>();
|
||||
lockKeys.add(new ResourceLockKey("1_TEACHER", dto.getTId()));
|
||||
if (dto.getVenueType() == 1)
|
||||
{
|
||||
lockKeys.add(new ResourceLockKey("2_CLASSROOM", dto.getClassRoomId()));
|
||||
}
|
||||
else
|
||||
{
|
||||
lockKeys.add(new ResourceLockKey("2_SITE", dto.getSiteId()));
|
||||
}
|
||||
lockPhysicalResources(lockKeys);
|
||||
|
||||
// 3. 落库新增课次 (排课冲突属于 Warning 告警级别,由 check-conflict 预检供管理员决策,不强制阻断入库)
|
||||
TrainLesson lesson = new TrainLesson();
|
||||
lesson.setPlanCourseId(dto.getPlanCourseId());
|
||||
lesson.setTId(dto.getTId());
|
||||
lesson.setStartTime(dto.getStartTime());
|
||||
lesson.setEndTime(dto.getEndTime());
|
||||
lesson.setVenueType(dto.getVenueType());
|
||||
lesson.setClassRoomId(dto.getClassRoomId());
|
||||
lesson.setSiteId(dto.getSiteId());
|
||||
lesson.setRemark(dto.getRemark());
|
||||
lesson.setCreateBy(operator);
|
||||
|
||||
trainLessonMapper.insertTrainLesson(lesson);
|
||||
log.info("成功创建课次 lessonId={}, planId={}, operator={}", lesson.getLessonId(), planId, operator);
|
||||
return lesson.getLessonId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改课次 (UC-08)。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateLesson(Long lessonId, LessonUpdateDTO dto, String operator)
|
||||
{
|
||||
TrainLesson existingLesson = trainLessonMapper.selectTrainLessonById(lessonId);
|
||||
if (existingLesson == null)
|
||||
{
|
||||
throw new ServiceException(LessonErrorCode.LESSON_NOT_FOUND_MESSAGE, LessonErrorCode.LESSON_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 获取原计划ID
|
||||
TrainPlanCourse existingPlanCourse = trainPlanCourseMapper.selectTrainPlanCourseById(existingLesson.getPlanCourseId());
|
||||
String planId = (existingPlanCourse != null) ? existingPlanCourse.getPlanId() : null;
|
||||
|
||||
// 1. 前置基础验证
|
||||
validateBasicRules(planId, dto.getPlanCourseId(), dto.getVenueType(), dto.getClassRoomId(),
|
||||
dto.getSiteId(), dto.getStartTime(), dto.getEndTime());
|
||||
|
||||
// 2. 考勤限制检测:已有考勤打卡记录,禁止修改关键时间
|
||||
int attendanceCount = trainLessonMapper.countAttendanceByLessonId(lessonId);
|
||||
if (attendanceCount > 0)
|
||||
{
|
||||
if (!existingLesson.getStartTime().equals(dto.getStartTime()) || !existingLesson.getEndTime().equals(dto.getEndTime()))
|
||||
{
|
||||
throw new ServiceException(LessonErrorCode.LESSON_ALREADY_HAS_ATTENDANCE_MESSAGE,
|
||||
LessonErrorCode.LESSON_ALREADY_HAS_ATTENDANCE);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 收集所有涉及的旧资源与新资源,统一按升序获取锁,避免交叉死锁并校验资源有效性
|
||||
Set<ResourceLockKey> lockKeys = new HashSet<>();
|
||||
lockKeys.add(new ResourceLockKey("1_TEACHER", existingLesson.getTId()));
|
||||
lockKeys.add(new ResourceLockKey("1_TEACHER", dto.getTId()));
|
||||
|
||||
if (existingLesson.getVenueType() == 1)
|
||||
{
|
||||
lockKeys.add(new ResourceLockKey("2_CLASSROOM", existingLesson.getClassRoomId()));
|
||||
}
|
||||
else
|
||||
{
|
||||
lockKeys.add(new ResourceLockKey("2_SITE", existingLesson.getSiteId()));
|
||||
}
|
||||
|
||||
if (dto.getVenueType() == 1)
|
||||
{
|
||||
lockKeys.add(new ResourceLockKey("2_CLASSROOM", dto.getClassRoomId()));
|
||||
}
|
||||
else
|
||||
{
|
||||
lockKeys.add(new ResourceLockKey("2_SITE", dto.getSiteId()));
|
||||
}
|
||||
|
||||
lockPhysicalResources(lockKeys);
|
||||
|
||||
// 4. 更新课次 (排课冲突属于 Warning 告警级别,由 check-conflict 预检供管理员决策,不强制阻断更新)
|
||||
existingLesson.setPlanCourseId(dto.getPlanCourseId());
|
||||
existingLesson.setTId(dto.getTId());
|
||||
existingLesson.setStartTime(dto.getStartTime());
|
||||
existingLesson.setEndTime(dto.getEndTime());
|
||||
existingLesson.setVenueType(dto.getVenueType());
|
||||
existingLesson.setClassRoomId(dto.getClassRoomId());
|
||||
existingLesson.setSiteId(dto.getSiteId());
|
||||
existingLesson.setRemark(dto.getRemark());
|
||||
existingLesson.setUpdateBy(operator);
|
||||
|
||||
trainLessonMapper.updateTrainLesson(existingLesson);
|
||||
log.info("成功更新课次 lessonId={}, operator={}", lessonId, operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除课次 (UC-09)。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteLesson(Long lessonId, String operator)
|
||||
{
|
||||
TrainLesson existingLesson = trainLessonMapper.selectTrainLessonById(lessonId);
|
||||
if (existingLesson == null)
|
||||
{
|
||||
throw new ServiceException(LessonErrorCode.LESSON_NOT_FOUND_MESSAGE, LessonErrorCode.LESSON_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 考勤约束:已有学员打卡考勤记录时严禁删除
|
||||
int attendanceCount = trainLessonMapper.countAttendanceByLessonId(lessonId);
|
||||
if (attendanceCount > 0)
|
||||
{
|
||||
throw new ServiceException(LessonErrorCode.LESSON_ALREADY_HAS_ATTENDANCE_DELETE_MESSAGE,
|
||||
LessonErrorCode.LESSON_ALREADY_HAS_ATTENDANCE);
|
||||
}
|
||||
|
||||
trainLessonMapper.deleteTrainLessonById(lessonId);
|
||||
log.info("成功删除课次 lessonId={}, operator={}", lessonId, operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 排课冲突检查预检 (UC-10)。
|
||||
*/
|
||||
@Override
|
||||
public LessonConflictVO checkConflict(LessonConflictCheckDTO dto)
|
||||
{
|
||||
LessonConflictVO result = new LessonConflictVO();
|
||||
|
||||
// 基础时间与场地互斥格式校验
|
||||
if (dto.getStartTime() == null || dto.getEndTime() == null || !dto.getStartTime().before(dto.getEndTime()))
|
||||
{
|
||||
result.setHasConflict(true);
|
||||
result.setConflictType("TIME_INVALID");
|
||||
result.setConflictMessage(PlanErrorCode.PLAN_TIME_INVALID_MESSAGE);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (dto.getVenueType() == null || (dto.getVenueType() != 1 && dto.getVenueType() != 2))
|
||||
{
|
||||
result.setHasConflict(true);
|
||||
result.setConflictType("VENUE_INVALID");
|
||||
result.setConflictMessage(LessonErrorCode.VENUE_CONFIG_INVALID_MESSAGE);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (dto.getVenueType() == 1 && (StringUtils.isEmpty(dto.getClassRoomId()) || StringUtils.isNotEmpty(dto.getSiteId())))
|
||||
{
|
||||
result.setHasConflict(true);
|
||||
result.setConflictType("VENUE_INVALID");
|
||||
result.setConflictMessage(LessonErrorCode.VENUE_CONFIG_INVALID_MESSAGE);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (dto.getVenueType() == 2 && (StringUtils.isEmpty(dto.getSiteId()) || StringUtils.isNotEmpty(dto.getClassRoomId())))
|
||||
{
|
||||
result.setHasConflict(true);
|
||||
result.setConflictType("VENUE_INVALID");
|
||||
result.setConflictMessage(LessonErrorCode.VENUE_CONFIG_INVALID_MESSAGE);
|
||||
return result;
|
||||
}
|
||||
|
||||
Long excludeLessonId = dto.getLessonId();
|
||||
|
||||
// 1. 检查教师冲突
|
||||
List<ConflictItemVO> teacherConflicts = trainLessonMapper.selectTeacherConflicts(
|
||||
dto.getTId(), dto.getStartTime(), dto.getEndTime(), excludeLessonId);
|
||||
if (teacherConflicts != null && !teacherConflicts.isEmpty())
|
||||
{
|
||||
result.setHasConflict(true);
|
||||
result.setConflictType("TEACHER");
|
||||
result.setConflictMessage(LessonErrorCode.LESSON_TEACHER_CONFLICT_MESSAGE);
|
||||
result.setConflictingLessons(teacherConflicts);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 2. 检查教室冲突
|
||||
if (dto.getVenueType() == 1)
|
||||
{
|
||||
List<ConflictItemVO> roomConflicts = trainLessonMapper.selectClassroomConflicts(
|
||||
dto.getClassRoomId(), dto.getStartTime(), dto.getEndTime(), excludeLessonId);
|
||||
if (roomConflicts != null && !roomConflicts.isEmpty())
|
||||
{
|
||||
result.setHasConflict(true);
|
||||
result.setConflictType("CLASSROOM");
|
||||
result.setConflictMessage(LessonErrorCode.LESSON_CLASSROOM_CONFLICT_MESSAGE);
|
||||
result.setConflictingLessons(roomConflicts);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查实训场地冲突
|
||||
if (dto.getVenueType() == 2)
|
||||
{
|
||||
List<ConflictItemVO> siteConflicts = trainLessonMapper.selectSiteConflicts(
|
||||
dto.getSiteId(), dto.getStartTime(), dto.getEndTime(), excludeLessonId);
|
||||
if (siteConflicts != null && !siteConflicts.isEmpty())
|
||||
{
|
||||
result.setHasConflict(true);
|
||||
result.setConflictType("SITE");
|
||||
result.setConflictMessage(LessonErrorCode.LESSON_SITE_CONFLICT_MESSAGE);
|
||||
result.setConflictingLessons(siteConflicts);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 检查同计划课次冲突
|
||||
if (StringUtils.isNotEmpty(dto.getPlanId()))
|
||||
{
|
||||
List<ConflictItemVO> planConflicts = trainLessonMapper.selectPlanConflicts(
|
||||
dto.getPlanId(), dto.getStartTime(), dto.getEndTime(), excludeLessonId);
|
||||
if (planConflicts != null && !planConflicts.isEmpty())
|
||||
{
|
||||
result.setHasConflict(true);
|
||||
result.setConflictType("PLAN_OVERLAP");
|
||||
result.setConflictMessage(LessonErrorCode.LESSON_PLAN_OVERLAP_MESSAGE);
|
||||
result.setConflictingLessons(planConflicts);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 无冲突
|
||||
result.setHasConflict(false);
|
||||
result.setConflictType("NONE");
|
||||
result.setConflictMessage("排课时间无冲突");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取计划完整课程表聚合 (UC-11)。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public PlanTimetableVO getPlanTimetable(String planId)
|
||||
{
|
||||
TrainClassPlan plan = trainClassPlanMapper.selectTrainClassPlanByPlanId(planId);
|
||||
if (plan == null)
|
||||
{
|
||||
throw new ServiceException(PlanErrorCode.PLAN_NOT_FOUND_MESSAGE, PlanErrorCode.PLAN_NOT_FOUND);
|
||||
}
|
||||
|
||||
PlanTimetableVO vo = new PlanTimetableVO();
|
||||
vo.setPlanId(plan.getPlanId());
|
||||
vo.setPlanName(plan.getPlanName());
|
||||
vo.setStartDate(plan.getStartTime());
|
||||
vo.setEndDate(plan.getEndTime());
|
||||
|
||||
List<TimetableLessonItemVO> lessonList = trainLessonMapper.selectScheduleLessonsByPlanId(planId);
|
||||
if (lessonList == null || lessonList.isEmpty())
|
||||
{
|
||||
vo.setDays(new ArrayList<>());
|
||||
return vo;
|
||||
}
|
||||
|
||||
// 按日期聚合课次
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
Map<String, TimetableDayVO> dayMap = new LinkedHashMap<>();
|
||||
|
||||
Calendar cal = Calendar.getInstance();
|
||||
for (TimetableLessonItemVO lesson : lessonList)
|
||||
{
|
||||
String dateStr = sdf.format(lesson.getStartTime());
|
||||
TimetableDayVO dayVO = dayMap.get(dateStr);
|
||||
if (dayVO == null)
|
||||
{
|
||||
dayVO = new TimetableDayVO();
|
||||
dayVO.setDate(dateStr);
|
||||
|
||||
cal.setTime(lesson.getStartTime());
|
||||
int dow = cal.get(Calendar.DAY_OF_WEEK);
|
||||
int dayOfWeek = (dow == Calendar.SUNDAY) ? 7 : (dow - 1);
|
||||
dayVO.setDayOfWeek(dayOfWeek);
|
||||
dayVO.setLessons(new ArrayList<>());
|
||||
|
||||
dayMap.put(dateStr, dayVO);
|
||||
}
|
||||
dayVO.getLessons().add(lesson);
|
||||
}
|
||||
|
||||
vo.setDays(new ArrayList<>(dayMap.values()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础规则验证。
|
||||
*/
|
||||
private void validateBasicRules(String planId, Long planCourseId, Integer venueType,
|
||||
String classRoomId, String siteId, Date startTime, Date endTime)
|
||||
{
|
||||
// 1. 时间检查: start < end
|
||||
if (startTime == null || endTime == null || !startTime.before(endTime))
|
||||
{
|
||||
throw new ServiceException(PlanErrorCode.PLAN_TIME_INVALID_MESSAGE, PlanErrorCode.PLAN_TIME_INVALID);
|
||||
}
|
||||
|
||||
// 2. 场地二选一互斥检查
|
||||
if (venueType == null || (venueType != 1 && venueType != 2))
|
||||
{
|
||||
throw new ServiceException(LessonErrorCode.VENUE_CONFIG_INVALID_MESSAGE, LessonErrorCode.VENUE_CONFIG_INVALID);
|
||||
}
|
||||
if (venueType == 1)
|
||||
{
|
||||
if (StringUtils.isEmpty(classRoomId) || StringUtils.isNotEmpty(siteId))
|
||||
{
|
||||
throw new ServiceException(LessonErrorCode.VENUE_CONFIG_INVALID_MESSAGE, LessonErrorCode.VENUE_CONFIG_INVALID);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (StringUtils.isEmpty(siteId) || StringUtils.isNotEmpty(classRoomId))
|
||||
{
|
||||
throw new ServiceException(LessonErrorCode.VENUE_CONFIG_INVALID_MESSAGE, LessonErrorCode.VENUE_CONFIG_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 培训计划检查
|
||||
if (StringUtils.isNotEmpty(planId))
|
||||
{
|
||||
TrainClassPlan plan = trainClassPlanMapper.selectTrainClassPlanByPlanId(planId);
|
||||
if (plan == null)
|
||||
{
|
||||
throw new ServiceException(PlanErrorCode.PLAN_NOT_FOUND_MESSAGE, PlanErrorCode.PLAN_NOT_FOUND);
|
||||
}
|
||||
if (plan.getPlanStatus() != null && (plan.getPlanStatus() == 2 || plan.getPlanStatus() == 3))
|
||||
{
|
||||
throw new ServiceException(PlanErrorCode.PLAN_STATUS_LOCKED_MESSAGE, PlanErrorCode.PLAN_STATUS_LOCKED);
|
||||
}
|
||||
if (plan.getStartTime() != null && startTime.before(plan.getStartTime()))
|
||||
{
|
||||
throw new ServiceException("课次开始时间不能早于培训计划开始时间", PlanErrorCode.PLAN_TIME_INVALID);
|
||||
}
|
||||
if (plan.getEndTime() != null && endTime.after(plan.getEndTime()))
|
||||
{
|
||||
throw new ServiceException("课次结束时间不能晚于培训计划结束时间", PlanErrorCode.PLAN_TIME_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 课程归属检查
|
||||
if (planCourseId != null)
|
||||
{
|
||||
TrainPlanCourse planCourse = trainPlanCourseMapper.selectTrainPlanCourseById(planCourseId);
|
||||
if (planCourse == null)
|
||||
{
|
||||
throw new ServiceException(PlanErrorCode.PLAN_COURSE_NOT_FOUND_MESSAGE, 40420);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(planId) && !planId.equals(planCourse.getPlanId()))
|
||||
{
|
||||
throw new ServiceException(PlanErrorCode.PLAN_COURSE_NOT_FOUND_MESSAGE, 40420);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定稳定物理主资源行 (按字典序排序加锁杜绝死锁)。
|
||||
*/
|
||||
private void lockPhysicalResources(Set<ResourceLockKey> keys)
|
||||
{
|
||||
List<ResourceLockKey> sortedKeys = new ArrayList<>(keys);
|
||||
Collections.sort(sortedKeys);
|
||||
|
||||
for (ResourceLockKey key : sortedKeys)
|
||||
{
|
||||
if ("1_TEACHER".equals(key.resourceType))
|
||||
{
|
||||
TrainTeacher teacher = trainLessonMapper.lockTeacherById(key.resourceId);
|
||||
if (teacher == null || "2".equals(teacher.getDelFlag()))
|
||||
{
|
||||
throw new ServiceException(LessonErrorCode.TEACHER_NOT_FOUND_MESSAGE,
|
||||
LessonErrorCode.TEACHER_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
else if ("2_CLASSROOM".equals(key.resourceType))
|
||||
{
|
||||
TrainClassroomInfo classroom = trainLessonMapper.lockClassroomById(key.resourceId);
|
||||
if (classroom == null || "2".equals(classroom.getDelFlag()))
|
||||
{
|
||||
throw new ServiceException(LessonErrorCode.CLASSROOM_NOT_FOUND_MESSAGE,
|
||||
LessonErrorCode.CLASSROOM_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
else if ("2_SITE".equals(key.resourceType))
|
||||
{
|
||||
TrainTrainingSite site = trainLessonMapper.lockSiteById(key.resourceId);
|
||||
if (site == null || "2".equals(site.getDelFlag()))
|
||||
{
|
||||
throw new ServiceException(LessonErrorCode.SITE_NOT_FOUND_MESSAGE,
|
||||
LessonErrorCode.SITE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源锁排序键。
|
||||
*/
|
||||
private static class ResourceLockKey implements Comparable<ResourceLockKey>
|
||||
{
|
||||
final String resourceType; // "1_TEACHER", "2_CLASSROOM", "2_SITE"
|
||||
final String resourceId;
|
||||
|
||||
ResourceLockKey(String resourceType, String resourceId)
|
||||
{
|
||||
this.resourceType = resourceType;
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
String toLockString()
|
||||
{
|
||||
return resourceType + ":" + resourceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ResourceLockKey o)
|
||||
{
|
||||
return this.toLockString().compareTo(o.toLockString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ResourceLockKey that = (ResourceLockKey) o;
|
||||
return Objects.equals(resourceType, that.resourceType) &&
|
||||
Objects.equals(resourceId, that.resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(resourceType, resourceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.train.mapper.TrainLessonMapper">
|
||||
|
||||
<resultMap type="TrainLesson" id="TrainLessonResult">
|
||||
<result property="lessonId" column="lesson_id" />
|
||||
<result property="planCourseId" column="plan_course_id" />
|
||||
<result property="tId" column="t_id" />
|
||||
<result property="startTime" column="start_time" />
|
||||
<result property="endTime" column="end_time" />
|
||||
<result property="venueType" column="venue_type" />
|
||||
<result property="classRoomId" column="class_room_id" />
|
||||
<result property="siteId" column="site_id" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
</resultMap>
|
||||
|
||||
<resultMap type="ConflictItemVO" id="ConflictItemResult">
|
||||
<result property="lessonId" column="lesson_id" />
|
||||
<result property="planId" column="plan_id" />
|
||||
<result property="planName" column="plan_name" />
|
||||
<result property="courseName" column="course_name" />
|
||||
<result property="teacherName" column="teacher_name" />
|
||||
<result property="venueName" column="venue_name" />
|
||||
<result property="startTime" column="start_time" />
|
||||
<result property="endTime" column="end_time" />
|
||||
</resultMap>
|
||||
|
||||
<resultMap type="TimetableLessonItemVO" id="TimetableLessonItemResult">
|
||||
<result property="lessonId" column="lesson_id" />
|
||||
<result property="planCourseId" column="plan_course_id" />
|
||||
<result property="courseName" column="course_name" />
|
||||
<result property="tId" column="t_id" />
|
||||
<result property="teacherName" column="teacher_name" />
|
||||
<result property="startTime" column="start_time" />
|
||||
<result property="endTime" column="end_time" />
|
||||
<result property="venueType" column="venue_type" />
|
||||
<result property="classRoomId" column="class_room_id" />
|
||||
<result property="classRoomName" column="class_room_name" />
|
||||
<result property="siteId" column="site_id" />
|
||||
<result property="siteName" column="site_name" />
|
||||
<result property="venueName" column="venue_name" />
|
||||
<result property="boardSn" column="board_sn" />
|
||||
<result property="remark" column="remark" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectTrainLessonVo">
|
||||
select lesson_id, plan_course_id, t_id, start_time, end_time, venue_type, class_room_id, site_id, create_by, create_time, update_by, update_time, remark
|
||||
from train_lesson
|
||||
</sql>
|
||||
|
||||
<select id="selectTrainLessonById" parameterType="Long" resultMap="TrainLessonResult">
|
||||
<include refid="selectTrainLessonVo"/>
|
||||
where lesson_id = #{lessonId}
|
||||
</select>
|
||||
|
||||
<insert id="insertTrainLesson" parameterType="TrainLesson" useGeneratedKeys="true" keyProperty="lessonId">
|
||||
insert into train_lesson (
|
||||
<if test="lessonId != null">lesson_id,</if>
|
||||
plan_course_id,
|
||||
t_id,
|
||||
start_time,
|
||||
end_time,
|
||||
venue_type,
|
||||
class_room_id,
|
||||
site_id,
|
||||
create_by,
|
||||
create_time,
|
||||
remark
|
||||
) values (
|
||||
<if test="lessonId != null">#{lessonId},</if>
|
||||
#{planCourseId},
|
||||
#{tId},
|
||||
#{startTime},
|
||||
#{endTime},
|
||||
#{venueType},
|
||||
#{classRoomId},
|
||||
#{siteId},
|
||||
#{createBy},
|
||||
sysdate(),
|
||||
#{remark}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateTrainLesson" parameterType="TrainLesson">
|
||||
update train_lesson
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="planCourseId != null">plan_course_id = #{planCourseId},</if>
|
||||
<if test="tId != null and tId != ''">t_id = #{tId},</if>
|
||||
<if test="startTime != null">start_time = #{startTime},</if>
|
||||
<if test="endTime != null">end_time = #{endTime},</if>
|
||||
<if test="venueType != null">venue_type = #{venueType},</if>
|
||||
class_room_id = #{classRoomId},
|
||||
site_id = #{siteId},
|
||||
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||
update_time = sysdate(),
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
</trim>
|
||||
where lesson_id = #{lessonId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteTrainLessonById" parameterType="Long">
|
||||
delete from train_lesson where lesson_id = #{lessonId}
|
||||
</delete>
|
||||
|
||||
<!-- 锁定稳定教师主表行 (FOR UPDATE) -->
|
||||
<select id="lockTeacherById" resultType="com.ruoyi.train.domain.TrainTeacher">
|
||||
select t_id, t_name, status, del_flag
|
||||
from train_teacher
|
||||
where t_id = #{tId}
|
||||
for update
|
||||
</select>
|
||||
|
||||
<!-- 锁定稳定教室主表行 (FOR UPDATE) -->
|
||||
<select id="lockClassroomById" resultType="com.ruoyi.train.domain.TrainClassroomInfo">
|
||||
select class_room_id, room_name, room_status, del_flag
|
||||
from train_classroom_info
|
||||
where class_room_id = #{classRoomId}
|
||||
for update
|
||||
</select>
|
||||
|
||||
<!-- 锁定稳定实训场地主表行 (FOR UPDATE) -->
|
||||
<select id="lockSiteById" resultType="com.ruoyi.train.domain.TrainTrainingSite">
|
||||
select site_id, site_name, capacity, del_flag
|
||||
from train_training_site
|
||||
where site_id = #{siteId}
|
||||
for update
|
||||
</select>
|
||||
|
||||
<!-- 统计课次已有考勤签到记录数 -->
|
||||
<select id="countAttendanceByLessonId" resultType="int">
|
||||
select count(*) from train_student_checkin where lesson_id = #{lessonId}
|
||||
</select>
|
||||
|
||||
<!-- 查询教师重叠课次 -->
|
||||
<select id="selectTeacherConflicts" resultMap="ConflictItemResult">
|
||||
select l.lesson_id,
|
||||
p.plan_id,
|
||||
p.plan_name,
|
||||
c.c_name as course_name,
|
||||
t.t_name as teacher_name,
|
||||
case when l.venue_type = 1 then cr.room_name else ts.site_name end as venue_name,
|
||||
l.start_time,
|
||||
l.end_time
|
||||
from train_lesson l
|
||||
inner join train_plan_course pc on l.plan_course_id = pc.plan_course_id
|
||||
inner join train_class_plan p on pc.plan_id = p.plan_id
|
||||
left join train_course c on pc.c_id = c.c_id
|
||||
left join train_teacher t on l.t_id = t.t_id
|
||||
left join train_classroom_info cr on l.class_room_id = cr.class_room_id
|
||||
left join train_training_site ts on l.site_id = ts.site_id
|
||||
where l.t_id = #{tId}
|
||||
and l.start_time < #{endTime}
|
||||
and l.end_time > #{startTime}
|
||||
<if test="excludeLessonId != null">
|
||||
and l.lesson_id != #{excludeLessonId}
|
||||
</if>
|
||||
order by l.start_time asc
|
||||
</select>
|
||||
|
||||
<!-- 查询教室重叠课次 -->
|
||||
<select id="selectClassroomConflicts" resultMap="ConflictItemResult">
|
||||
select l.lesson_id,
|
||||
p.plan_id,
|
||||
p.plan_name,
|
||||
c.c_name as course_name,
|
||||
t.t_name as teacher_name,
|
||||
cr.room_name as venue_name,
|
||||
l.start_time,
|
||||
l.end_time
|
||||
from train_lesson l
|
||||
inner join train_plan_course pc on l.plan_course_id = pc.plan_course_id
|
||||
inner join train_class_plan p on pc.plan_id = p.plan_id
|
||||
left join train_course c on pc.c_id = c.c_id
|
||||
left join train_teacher t on l.t_id = t.t_id
|
||||
left join train_classroom_info cr on l.class_room_id = cr.class_room_id
|
||||
where l.venue_type = 1
|
||||
and l.class_room_id = #{classRoomId}
|
||||
and l.start_time < #{endTime}
|
||||
and l.end_time > #{startTime}
|
||||
<if test="excludeLessonId != null">
|
||||
and l.lesson_id != #{excludeLessonId}
|
||||
</if>
|
||||
order by l.start_time asc
|
||||
</select>
|
||||
|
||||
<!-- 查询实训场地重叠课次 -->
|
||||
<select id="selectSiteConflicts" resultMap="ConflictItemResult">
|
||||
select l.lesson_id,
|
||||
p.plan_id,
|
||||
p.plan_name,
|
||||
c.c_name as course_name,
|
||||
t.t_name as teacher_name,
|
||||
ts.site_name as venue_name,
|
||||
l.start_time,
|
||||
l.end_time
|
||||
from train_lesson l
|
||||
inner join train_plan_course pc on l.plan_course_id = pc.plan_course_id
|
||||
inner join train_class_plan p on pc.plan_id = p.plan_id
|
||||
left join train_course c on pc.c_id = c.c_id
|
||||
left join train_teacher t on l.t_id = t.t_id
|
||||
left join train_training_site ts on l.site_id = ts.site_id
|
||||
where l.venue_type = 2
|
||||
and l.site_id = #{siteId}
|
||||
and l.start_time < #{endTime}
|
||||
and l.end_time > #{startTime}
|
||||
<if test="excludeLessonId != null">
|
||||
and l.lesson_id != #{excludeLessonId}
|
||||
</if>
|
||||
order by l.start_time asc
|
||||
</select>
|
||||
|
||||
<!-- 查询同培训计划班级重叠课次 -->
|
||||
<select id="selectPlanConflicts" resultMap="ConflictItemResult">
|
||||
select l.lesson_id,
|
||||
p.plan_id,
|
||||
p.plan_name,
|
||||
c.c_name as course_name,
|
||||
t.t_name as teacher_name,
|
||||
case when l.venue_type = 1 then cr.room_name else ts.site_name end as venue_name,
|
||||
l.start_time,
|
||||
l.end_time
|
||||
from train_lesson l
|
||||
inner join train_plan_course pc on l.plan_course_id = pc.plan_course_id
|
||||
inner join train_class_plan p on pc.plan_id = p.plan_id
|
||||
left join train_course c on pc.c_id = c.c_id
|
||||
left join train_teacher t on l.t_id = t.t_id
|
||||
left join train_classroom_info cr on l.class_room_id = cr.class_room_id
|
||||
left join train_training_site ts on l.site_id = ts.site_id
|
||||
where pc.plan_id = #{planId}
|
||||
and l.start_time < #{endTime}
|
||||
and l.end_time > #{startTime}
|
||||
<if test="excludeLessonId != null">
|
||||
and l.lesson_id != #{excludeLessonId}
|
||||
</if>
|
||||
order by l.start_time asc
|
||||
</select>
|
||||
|
||||
<!-- 查询指定计划下的课次日程列表 -->
|
||||
<select id="selectScheduleLessonsByPlanId" resultMap="TimetableLessonItemResult">
|
||||
select l.lesson_id,
|
||||
l.plan_course_id,
|
||||
c.c_name as course_name,
|
||||
l.t_id,
|
||||
t.t_name as teacher_name,
|
||||
l.start_time,
|
||||
l.end_time,
|
||||
l.venue_type,
|
||||
l.class_room_id,
|
||||
cr.room_name as class_room_name,
|
||||
l.site_id,
|
||||
ts.site_name,
|
||||
case when l.venue_type = 1 then cr.room_name else ts.site_name end as venue_name,
|
||||
b.board_sn,
|
||||
l.remark
|
||||
from train_lesson l
|
||||
inner join train_plan_course pc on l.plan_course_id = pc.plan_course_id
|
||||
left join train_course c on pc.c_id = c.c_id
|
||||
left join train_teacher t on l.t_id = t.t_id
|
||||
left join train_classroom_info cr on l.class_room_id = cr.class_room_id
|
||||
left join train_training_site ts on l.site_id = ts.site_id
|
||||
left join (
|
||||
select bind_classroom_id,
|
||||
case when count(*) = 1 then min(board_sn) else null end as board_sn
|
||||
from train_electronic_board
|
||||
where del_flag = '0'
|
||||
group by bind_classroom_id
|
||||
) b on (l.venue_type = 1 and b.bind_classroom_id = l.class_room_id)
|
||||
where pc.plan_id = #{planId}
|
||||
order by l.start_time asc, l.lesson_id asc
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
+1168
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user