From 52a04dbf6001e3dbe493c184086e00de9f327bd6 Mon Sep 17 00:00:00 2001 From: liweijie Date: Mon, 14 Sep 2026 16:30:52 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=AD=A3=E8=AF=BE=E4=B8=8E=E5=AD=A6?= =?UTF-8?q?=E6=9C=9F=E5=85=B3=E7=B3=BB=E5=85=B3=E8=81=94=EF=BC=9B=202?= =?UTF-8?q?=E3=80=81=E5=A4=A9=E5=81=87=E5=88=A0=E9=99=A4=E4=BF=9D=E6=8A=A4?= =?UTF-8?q?=EF=BC=9B=203=E3=80=81=E6=95=99=E5=AD=A6=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E8=AE=A1=E5=88=92=E5=A2=9E=E5=8A=A0=E4=BA=86=E3=80=8C=E7=BB=93?= =?UTF-8?q?=E6=9D=9F=E5=8F=91=E5=B8=83=E3=80=8D=E3=80=82=E5=B7=B2=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E6=88=96=E5=BA=93=E9=87=8C=E7=9A=84=E3=80=8C=E5=B7=B2?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E3=80=8D=E9=83=BD=E5=8F=AF=E4=BB=A5=E7=BB=93?= =?UTF-8?q?=E6=9D=9F=EF=BC=9B=E7=BB=93=E6=9D=9F=E5=90=8E=E5=86=99=E5=85=A5?= =?UTF-8?q?=E7=BB=93=E6=9D=9F=E6=97=B6=E9=97=B4=EF=BC=8C=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E4=B9=A6=E5=92=8C=E7=8F=AD=E6=AC=A1=E8=AF=BE=E7=A8=8B=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=8F=AA=E8=AF=BB=204=E3=80=81=E7=8F=AD=E6=AC=A1?= =?UTF-8?q?=E5=AD=A6=E6=9C=9F=E6=93=8D=E4=BD=9C=E5=86=85=E5=AE=B9=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E4=BF=AE=E6=94=B9=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jwgl/ClassEventCalendarController.java | 43 ++ .../controller/jwgl/ElectiveController.java | 5 +- .../web/controller/jwgl/JYSController.java | 9 + .../jwgl/StudentRecordsController.java | 78 ++++ .../jwgl/StudentTeamTaskController.java | 43 ++ .../jwgl/TeachingTaskController.java | 9 + .../dto/calendar/ClassCalendarApplyDTO.java | 16 + .../dto/calendar/ClassCalendarEventDTO.java | 24 ++ .../java/com/roomroot/jwgl/entity/JQB.java | 59 ++- .../java/com/roomroot/jwgl/entity/XQXLB.java | 6 + .../roomroot/jwgl/entity/XYDNDXQJBXXB.java | 24 ++ .../jwgl/mapper/ClassSemesterMapper.xml | 46 ++- .../com/roomroot/jwgl/mapper/JQBMapper.java | 12 + .../com/roomroot/jwgl/mapper/XYDBMapper.java | 5 + .../com/roomroot/jwgl/mapper/XYDBMapper.xml | 15 + .../service/ClassEventCalendarService.java | 26 ++ .../jwgl/service/ClassSemesterService.java | 28 ++ .../jwgl/service/ElectiveService.java | 5 + .../jwgl/service/StudentTeamTaskService.java | 36 ++ .../service/TeachingTaskManageService.java | 5 + .../jwgl/service/TeachingTaskService.java | 7 + .../jwgl/service/TeachingTaskWriteGuard.java | 11 + .../impl/ClassEventCalendarServiceImpl.java | 303 +++++++++++++++ .../ClassSemesterCalendarServiceImpl.java | 14 + .../impl/ClassSemesterServiceImpl.java | 311 +++++++++++++-- .../service/impl/ElectiveServiceImpl.java | 40 +- .../impl/OfficeTaskBookServiceImpl.java | 16 + .../service/impl/SemesterServiceImpl.java | 85 ++++ .../impl/StudentTeamTaskServiceImpl.java | 316 +++++++++++++++ .../impl/TeachingTaskManageServiceImpl.java | 38 +- .../service/impl/TeachingTaskServiceImpl.java | 42 ++ .../impl/TeachingTaskWriteGuardImpl.java | 54 +++ .../jwgl/utils/SystemInitializationUtil.java | 4 +- .../jwgl/utils/TeachingTaskStatus.java | 23 ++ .../com/roomroot/jwgl/vo/PlanCourseVO.java | 20 + backend/sql/phase0_foundation.sql | 21 + backend/sql/phase2_class_calendar.sql | 4 + .../src/api/studentRecords/classCalendar.js | 58 ++- frontend/src/api/studentRecords/semester.js | 71 +++- .../src/api/teachBusiness/teachingTask.js | 12 + .../src/api/teachOffice/classCalendarEvent.js | 25 ++ .../components/SchoolCalendarEditor.vue | 160 +++++--- .../views/teachBusiness/calendar/index.vue | 2 +- .../views/teachBusiness/semester/index.vue | 2 +- .../shiftSemester/ClassCalendarDialog.vue | 317 ++++++++++++--- .../ClassEventCalendarDialog.vue | 154 ++++++++ .../ClassSemesterDetailDialog.vue | 172 +++++++- .../shiftSemester/ClassTeamSelectDialog.vue | 27 +- .../views/teachOffice/shiftSemester/index.vue | 367 +++++++++++++++--- .../src/views/teachOffice/taskPlan/index.vue | 62 ++- 50 files changed, 2939 insertions(+), 293 deletions(-) create mode 100644 backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ClassEventCalendarController.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarApplyDTO.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarEventDTO.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/JQBMapper.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassEventCalendarService.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TeachingTaskWriteGuard.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassEventCalendarServiceImpl.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TeachingTaskWriteGuardImpl.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/TeachingTaskStatus.java create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/PlanCourseVO.java create mode 100644 backend/sql/phase0_foundation.sql create mode 100644 backend/sql/phase2_class_calendar.sql create mode 100644 frontend/src/api/teachOffice/classCalendarEvent.js create mode 100644 frontend/src/views/teachOffice/shiftSemester/ClassEventCalendarDialog.vue diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ClassEventCalendarController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ClassEventCalendarController.java new file mode 100644 index 000000000..bdf28ce72 --- /dev/null +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ClassEventCalendarController.java @@ -0,0 +1,43 @@ +package com.roomroot.web.controller.jwgl; + +import com.roomroot.jwgl.dto.calendar.ClassCalendarApplyDTO; +import com.roomroot.jwgl.dto.calendar.ClassCalendarEventDTO; +import com.roomroot.jwgl.service.ClassEventCalendarService; +import com.roomroot.jwgl.unit.Result; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.annotation.Resource; +import java.util.List; + +/** + * 班历(假期表)。与课程任务接口 /classCalendar 分开。 + */ +@RestController +@RequestMapping("/class-calendar") +public class ClassEventCalendarController { + + @Resource + private ClassEventCalendarService classEventCalendarService; + + @GetMapping("/list") + public Result> list(@RequestParam("xydxqbh") String xydxqbh) { + return Result.success(classEventCalendarService.listOrInherit(xydxqbh)); + } + + @PostMapping("/update") + public Result update(@RequestBody ClassCalendarEventDTO event) { + classEventCalendarService.update(event); + return Result.success(); + } + + @PostMapping("/apply") + public Result apply(@RequestBody ClassCalendarApplyDTO body) { + return Result.success(classEventCalendarService.apply( + body.getSourceXydxqbh(), body.getTargetBhList(), body.getEventBhList())); + } +} diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ElectiveController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ElectiveController.java index d19e1386a..f3f4461ac 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ElectiveController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/ElectiveController.java @@ -344,8 +344,9 @@ public class ElectiveController { public Result semesterInit( @RequestParam("xydbh") String xydbh, @RequestParam(required = false) String presetKxrq, - @RequestParam(required = false) String presetJsrq) { - return Result.success(electiveService.getSemesterInit(xydbh, presetKxrq, presetJsrq)); + @RequestParam(required = false) String presetJsrq, + @RequestParam(required = false) Integer xqdc) { + return Result.success(electiveService.getSemesterInit(xydbh, presetKxrq, presetJsrq, xqdc)); } /** diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/JYSController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/JYSController.java index f8f919da0..e79cba4a5 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/JYSController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/JYSController.java @@ -545,6 +545,15 @@ public class JYSController { return Result.success("已发布,同步教研室任务书 " + count + " 份", count); } + /** + * 结束发布。结束后任务书和课程任务只读。 + */ + @PostMapping("/task/endPublish") + public Result endPublishTask(@RequestParam("bh") String bh) { + teachingTaskManageService.endPublish(bh); + return Result.success(); + } + /** * 教研室任务书列表。 */ diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/StudentRecordsController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/StudentRecordsController.java index b09161a69..eba93fadd 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/StudentRecordsController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/StudentRecordsController.java @@ -401,6 +401,84 @@ public class StudentRecordsController { return Result.success(count); } + /** + * 未毕业、且该年度学期尚未建立班次学期信息的班次。 + */ + @GetMapping("/semester/creatableClasses") + public Result> creatableClasses(@RequestParam("nd") Integer nd, + @RequestParam("xqdc") Integer xqdc) { + return Result.success(classSemesterService.listCreatableClasses(nd, xqdc)); + } + + /** + * 按学校学期带出开学/结束日期。 + */ + @GetMapping("/semester/presetDates") + public Result> presetDates(@RequestParam("nd") Integer nd, + @RequestParam("xqdc") Integer xqdc) { + return Result.success(classSemesterService.presetDates(nd, xqdc)); + } + + /** + * 为符合条件的全部班次创建班次学期,返回实际创建数。 + */ + @PostMapping("/semester/batchCreate") + public Result batchCreate(@RequestBody Map params) { + Integer nd = asInt(params.get("nd")); + Integer xqdc = asInt(params.get("xqdc")); + String kxrq = asText(params.get("kxrq")); + String jsrq = asText(params.get("jsrq")); + int count = classSemesterService.batchCreateEligible( + nd, + xqdc, + kxrq == null ? null : java.time.LocalDate.parse(kxrq), + jsrq == null ? null : java.time.LocalDate.parse(jsrq)); + return Result.success(count); + } + + /** + * 学期级预设合班,只改预设编班号。 + */ + @PostMapping("/semester/batchWaveMerge") + public Result batchWaveMerge(@RequestBody Map params) { + return Result.success(classSemesterService.batchWaveMerge(asBhList(params))); + } + + /** + * 学期级预设拆班,所选班次各自独立成波。 + */ + @PostMapping("/semester/batchWaveSplit") + public Result batchWaveSplit(@RequestBody Map params) { + return Result.success(classSemesterService.batchWaveSplit(asBhList(params))); + } + + private Integer asInt(Object value) { + if (value == null || "".equals(value)) { + return null; + } + if (value instanceof Number) { + return ((Number) value).intValue(); + } + return Integer.valueOf(String.valueOf(value)); + } + + private String asText(Object value) { + if (value == null) { + return null; + } + String text = String.valueOf(value).trim(); + return text.isEmpty() ? null : text; + } + + @SuppressWarnings("unchecked") + private List asBhList(Map params) { + Object raw = params.get("bhList"); + if (!(raw instanceof List)) { + return List.of(); + } + return (List) raw; + } + // ======================== 课程成绩查询 ======================== /** diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/StudentTeamTaskController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/StudentTeamTaskController.java index 7164fb83f..8094fc335 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/StudentTeamTaskController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/StudentTeamTaskController.java @@ -159,4 +159,47 @@ public class StudentTeamTaskController { List newBhList = studentTeamTaskService.copy(bhList, newXydbh); return Result.success(newBhList); } + + @PostMapping("/batchAutoGenerate") + public Result batchAutoGenerate(@RequestBody List xydxqbhList) { + return Result.success(studentTeamTaskService.batchAutoGenerate(xydxqbhList)); + } + + @GetMapping("/planCourses") + public Result> planCourses( + @RequestParam("xydxqbh") String xydxqbh, + @RequestParam(value = "otherOnly", defaultValue = "true") boolean otherOnly) { + return Result.success(studentTeamTaskService.listPlanCourses(xydxqbh, otherOnly)); + } + + @PostMapping("/addFromLibrary") + public Result addFromLibrary(@RequestParam("xydxqbh") String xydxqbh, + @RequestParam("kbh") String kbh) { + studentTeamTaskService.addFromLibrary(xydxqbh, kbh); + return Result.success(); + } + + @PostMapping("/addFromPlan") + public Result addFromPlan(@RequestParam("xydxqbh") String xydxqbh, + @RequestParam("planBh") String planBh) { + studentTeamTaskService.addFromPlan(xydxqbh, planBh); + return Result.success(); + } + + @PostMapping("/syncFromPlan") + public Result syncFromPlan(@RequestBody List bhList) { + return Result.success(studentTeamTaskService.syncFromPlan(bhList)); + } + + @PostMapping("/batchDelete") + public Result batchDelete(@RequestBody List bhList) { + return Result.success(studentTeamTaskService.batchDeleteGuarded(bhList)); + } + + @PostMapping("/paste") + public Result paste(@RequestBody Map params) { + List bhList = (List) params.get("bhList"); + String targetXydxqbh = (String) params.get("targetXydxqbh"); + return Result.success(studentTeamTaskService.pasteToSemester(bhList, targetXydxqbh)); + } } \ No newline at end of file diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/TeachingTaskController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/TeachingTaskController.java index 1b56835f3..e53ffd1a1 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/TeachingTaskController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/TeachingTaskController.java @@ -92,4 +92,13 @@ public class TeachingTaskController { int count = teachingTaskService.batchPublish(bh); return Result.success(count); } + + /** + * 结束发布。结束后教研室不能再改任务书和课程任务。 + */ + @PostMapping("/endPublish") + public Result endPublish(@RequestParam("bh") String bh) { + teachingTaskService.endPublish(bh); + return Result.success(); + } } \ No newline at end of file diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarApplyDTO.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarApplyDTO.java new file mode 100644 index 000000000..fd78817f1 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarApplyDTO.java @@ -0,0 +1,16 @@ +package com.roomroot.jwgl.dto.calendar; + +import lombok.Data; + +import java.util.List; + +/** + * 把班历事件应用到其它班次。eventBhList 为空时复制整个班历。 + */ +@Data +public class ClassCalendarApplyDTO { + + private String sourceXydxqbh; + private List targetBhList; + private List eventBhList; +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarEventDTO.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarEventDTO.java new file mode 100644 index 000000000..ead54af08 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/calendar/ClassCalendarEventDTO.java @@ -0,0 +1,24 @@ +package com.roomroot.jwgl.dto.calendar; + +import lombok.Data; + +/** + * 班历时间格。日期用 yyyy-MM-dd,便于复用校历编辑器。 + */ +@Data +public class ClassCalendarEventDTO { + + private String bh; + private Integer nd; + private String xydbh; + private String xydxqbh; + private String jqsj; + private String jqmc; + private Boolean jc; + private String bz; + private Boolean kpk; + private Boolean bzxs; + private Boolean zdpk; + private Boolean zk; + private String courseClass; +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/JQB.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/JQB.java index 4cd07d7ff..25de725e5 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/JQB.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/JQB.java @@ -1,19 +1,17 @@ package com.roomroot.jwgl.entity; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; -import com.roomroot.jwgl.unit.BaseEntity; import lombok.Data; -import lombok.EqualsAndHashCode; /** - * 假期表 + * 假期表。班历时间格,不继承基类,避免写入表中不存在的审计列。 */ @Data -@EqualsAndHashCode(callSuper = true) @TableName("假期表") -public class JQB extends BaseEntity { +public class JQB { /** * 编号 @@ -21,54 +19,49 @@ public class JQB extends BaseEntity { @TableId(value = "编号", type = IdType.INPUT) private String bh; - /** - * 年度 - */ + @TableField("年度") private Integer nd; - /** - * 学员队编号 - */ + @TableField("学员队编号") private String xydbh; /** - * 假期时间 + * 假期时间,库中为整型日期 yyyyMMdd。 */ + @TableField("假期时间") private Integer jqsj; - /** - * 假期名称 - */ + @TableField("假期名称") private String jqmc; - /** - * 加粗 - */ + @TableField("加粗") private Integer jc; - /** - * 备注 - */ + @TableField("备注") private String bz; - /** - * 可排课 - */ + @TableField("可排课") private Integer kpk; - /** - * 学员队学期编号 - */ + @TableField("学员队学期编号") private String xydxqbh; - /** - * 备注显示 - */ + @TableField("备注显示") private Integer bzxs; - /** - * 自动排课 - */ + @TableField("自动排课") private Integer zdpk; + /** + * 正课。与自动排课分离。 + */ + @TableField("正课") + private Integer zk; + + /** + * 节次,与校历一致,如 1-2、3-4。 + */ + @TableField("节次") + private String courseClass; + } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/XQXLB.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/XQXLB.java index 78df21e2a..716eaa943 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/XQXLB.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/XQXLB.java @@ -70,6 +70,12 @@ public class XQXLB extends BaseEntity { @TableField("自动排课") private Boolean zdpk; + /** + * 正课。与自动排课分离:默认星期一至星期五 1-6 节,周末补课也可标为正课。 + */ + @TableField("正课") + private Boolean zk; + /** * 节次 */ diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/XYDNDXQJBXXB.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/XYDNDXQJBXXB.java index 7c3272185..a83be16c9 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/XYDNDXQJBXXB.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/XYDNDXQJBXXB.java @@ -144,4 +144,28 @@ public class XYDNDXQJBXXB extends BaseEntity { @TableField("分析报告文档3编号") private String fxbgwd3bh; + /** + * 总课程数。列表聚合学员队任务表,不落库。 + */ + @TableField(exist = false) + private Integer zkcs; + + /** + * 班次名称。列表联查学员队表,不落库。 + */ + @TableField(exist = false) + private String xydmc; + + /** + * 专用教室名称。列表联查教室表,不落库。 + */ + @TableField(exist = false) + private String zyjsmc; + + /** + * 列表按预设编班号排序。仅查询参数,不落库。 + */ + @TableField(exist = false) + private Integer sortWave; + } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/ClassSemesterMapper.xml b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/ClassSemesterMapper.xml index 915d465d4..331ff0683 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/ClassSemesterMapper.xml +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/ClassSemesterMapper.xml @@ -24,6 +24,9 @@ + + + @@ -32,25 +35,50 @@ 分析报告生成时间, 分析报告文档1编号, 分析报告文档2编号, 分析报告文档3编号, DEL_FLAG + + SELECT a.编号, a.年度, a.学员队编号, a.开学日期, a.结束日期, a.学期第次, a.专用教室编号, a.教学任务编号, + a.变动时间, a.备注, a.序号标识, a.预设编班号, a.节次类别, a.系统模式, a.开放教员排课, a.JSONZD, + a.分析报告生成时间, a.分析报告文档1编号, a.分析报告文档2编号, a.分析报告文档3编号, a.DEL_FLAG, + (SELECT COUNT(*) FROM 学员队任务表 t + WHERE t.学员队学期编号 = a.编号 AND NVL(t.DEL_FLAG, 0) = 0) AS zkcs, + b.学员队名称 AS xydmc, + NVL(r1.教室名称, r2.教室名称) AS zyjsmc + FROM 学员队年度学期基本信息表 a + LEFT JOIN 学员队表 b ON a.学员队编号 = b.学员队编号 + LEFT JOIN 教室表 r1 ON a.专用教室编号 = r1.ID + LEFT JOIN 教室表 r2 ON a.专用教室编号 = r2.教室编号 AND r1.ID IS NULL + + + + + diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassEventCalendarService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassEventCalendarService.java new file mode 100644 index 000000000..6daa4658c --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassEventCalendarService.java @@ -0,0 +1,26 @@ +package com.roomroot.jwgl.service; + +import com.roomroot.jwgl.dto.calendar.ClassCalendarEventDTO; + +import java.util.List; + +/** + * 班历(假期表)服务。 + */ +public interface ClassEventCalendarService { + + /** + * 按学员队学期编号查询班历。没有记录时按校历复制一份。 + */ + List listOrInherit(String xydxqbh); + + /** + * 更新一个时间格。班次日期范围外的格子拒绝保存。 + */ + void update(ClassCalendarEventDTO event); + + /** + * 把选定或整个班历覆盖到其它班次的对应时间格。不改课程任务。 + */ + int apply(String sourceXydxqbh, List targetBhList, List eventBhList); +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassSemesterService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassSemesterService.java index f37a735e5..4413b7b1a 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassSemesterService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ClassSemesterService.java @@ -1,10 +1,13 @@ package com.roomroot.jwgl.service; import com.roomroot.jwgl.entity.XYDNDXQJBXXB; +import com.roomroot.jwgl.entity.XYDB; import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageResult; +import java.time.LocalDate; import java.util.List; +import java.util.Map; /** * 班次学期服务接口 @@ -113,4 +116,29 @@ public interface ClassSemesterService { * @return 更新数量 */ int batchUpdateDateRange(List bhList, String xqkssj, String xqjssj); + + /** + * 未毕业且指定年度学期尚未建班次学期的班次。 + */ + List listCreatableClasses(Integer nd, Integer xqdc); + + /** + * 按学校学期表带出开学/结束日期。 + */ + Map presetDates(Integer nd, Integer xqdc); + + /** + * 为符合条件的全部班次创建班次学期,返回实际创建数。 + */ + int batchCreateEligible(Integer nd, Integer xqdc, LocalDate kxrq, LocalDate jsrq); + + /** + * 学期级预设合班:所选班次写入同一预设编班号。 + */ + int batchWaveMerge(List bhList); + + /** + * 学期级预设拆班:所选班次各自独立成波。 + */ + int batchWaveSplit(List bhList); } \ No newline at end of file diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ElectiveService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ElectiveService.java index 3edea6c8d..470a67a99 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ElectiveService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/ElectiveService.java @@ -111,6 +111,11 @@ public interface ElectiveService { */ SemesterInitVO getSemesterInit(String xydbh, String presetKxrq, String presetJsrq); + /** + * 同 {@link #getSemesterInit(String, String, String)}。指定学期第次时按该次取上学期专用教室,学制半年以内仍强制第1学期。 + */ + SemesterInitVO getSemesterInit(String xydbh, String presetKxrq, String presetJsrq, Integer xqdc); + /** * 创建班次学期信息(教学班次学期信息设置窗口 - 确定按钮) *

diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/StudentTeamTaskService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/StudentTeamTaskService.java index 01ff1de35..97349cd11 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/StudentTeamTaskService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/StudentTeamTaskService.java @@ -2,6 +2,7 @@ package com.roomroot.jwgl.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.roomroot.jwgl.entity.XYDRWB; +import com.roomroot.jwgl.vo.PlanCourseVO; import java.util.List; @@ -114,4 +115,39 @@ public interface StudentTeamTaskService { * @return 新生成的编号集合 */ List copy(List bhList, String newXydbh); + + /** + * 按班次学期批量生成必修,已有课程不重复。 + */ + int batchAutoGenerate(List xydxqbhList); + + /** + * 本班人培课程。otherOnly 为 true 时只列其它学期,并标是否已加入当前学期。 + */ + List listPlanCourses(String xydxqbh, boolean otherOnly); + + /** + * 从课程库加入当前学期任务,带出课程库默认学时和类型。 + */ + void addFromLibrary(String xydxqbh, String kbh); + + /** + * 把人培中的一门课加入当前学期任务。 + */ + void addFromPlan(String xydxqbh, String planBh); + + /** + * 按人培覆盖所选任务的学时、类型等字段。 + */ + int syncFromPlan(List bhList); + + /** + * 批量删除。已发布教学任务或已排课的行拒绝删除。 + */ + int batchDeleteGuarded(List bhList); + + /** + * 复制任务到另一个班次学期,两条任务独立。 + */ + int pasteToSemester(List bhList, String targetXydxqbh); } \ No newline at end of file diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TeachingTaskManageService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TeachingTaskManageService.java index f5ac03780..15a3766b7 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TeachingTaskManageService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TeachingTaskManageService.java @@ -29,6 +29,11 @@ public interface TeachingTaskManageService { int publish(String bh); + /** + * 结束发布。结束后任务书和课程任务只读。 + */ + void endPublish(String bh); + List listOfficeBooks(String jxrwbh); List listOfficeBookCourses(String jxrwbh, String jysdh); diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TeachingTaskService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TeachingTaskService.java index 1723a4226..1f4eafa29 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TeachingTaskService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TeachingTaskService.java @@ -71,4 +71,11 @@ public interface TeachingTaskService { * @return 更新数量 */ int batchPublish(String bh); + + /** + * 结束发布。结束后教研室不能再改任务书和课程任务。 + * + * @param bh 教学任务编号 + */ + void endPublish(String bh); } \ No newline at end of file diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TeachingTaskWriteGuard.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TeachingTaskWriteGuard.java new file mode 100644 index 000000000..eb1576aa9 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TeachingTaskWriteGuard.java @@ -0,0 +1,11 @@ +package com.roomroot.jwgl.service; + +/** + * 教学任务结束后,禁止再改任务书和班次课程任务。 + */ +public interface TeachingTaskWriteGuard { + + void assertTaskWritable(String jxrwbh); + + void assertCourseTaskWritable(String xydxqbh); +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassEventCalendarServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassEventCalendarServiceImpl.java new file mode 100644 index 000000000..a316407d2 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassEventCalendarServiceImpl.java @@ -0,0 +1,303 @@ +package com.roomroot.jwgl.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.roomroot.common.exception.ServiceException; +import com.roomroot.jwgl.dto.calendar.ClassCalendarEventDTO; +import com.roomroot.jwgl.entity.JQB; +import com.roomroot.jwgl.entity.XQXLB; +import com.roomroot.jwgl.entity.XYDNDXQJBXXB; +import com.roomroot.jwgl.mapper.ClassSemesterMapper; +import com.roomroot.jwgl.mapper.JQBMapper; +import com.roomroot.jwgl.mapper.SemesterCalendarMapper; +import com.roomroot.jwgl.service.ClassEventCalendarService; +import com.roomroot.jwgl.service.TeachingTaskWriteGuard; +import com.roomroot.jwgl.utils.UuidUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import jakarta.annotation.Resource; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST; + +/** + * 班历读写假期表。没有记录时从学期校历复制,不写课程任务。 + */ +@Service +public class ClassEventCalendarServiceImpl implements ClassEventCalendarService { + + private static final DateTimeFormatter DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final DateTimeFormatter COMPACT = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final List PERIODS = Arrays.asList("1-2", "3-4", "5-6", "7-8", "9-10", "11-12"); + + @Resource + private JQBMapper jqbMapper; + + @Resource + private ClassSemesterMapper classSemesterMapper; + + @Resource + private SemesterCalendarMapper semesterCalendarMapper; + + @Resource + private TeachingTaskWriteGuard teachingTaskWriteGuard; + + @Override + @Transactional(rollbackFor = Exception.class) + public List listOrInherit(String xydxqbh) { + XYDNDXQJBXXB semester = requireSemester(xydxqbh); + List rows = listRows(xydxqbh); + if (rows.isEmpty()) { + inherit(semester); + rows = listRows(xydxqbh); + } + List result = new ArrayList<>(); + for (JQB row : rows) { + result.add(toDto(row)); + } + return result; + } + + @Override + public void update(ClassCalendarEventDTO event) { + if (event == null || event.getBh() == null || event.getBh().isEmpty()) { + throw new ServiceException("班历编号不能为空", BAD_REQUEST); + } + JQB existing = jqbMapper.selectById(event.getBh()); + if (existing == null) { + throw new ServiceException("班历时间格不存在", BAD_REQUEST); + } + teachingTaskWriteGuard.assertCourseTaskWritable(existing.getXydxqbh()); + XYDNDXQJBXXB semester = requireSemester(existing.getXydxqbh()); + LocalDate day = parseDay(event.getJqsj() == null ? formatDay(existing.getJqsj()) : event.getJqsj()); + assertInRange(semester, day); + existing.setJqmc(event.getJqmc() == null ? "" : event.getJqmc()); + existing.setBz(event.getBz()); + existing.setJc(flag(event.getJc())); + existing.setKpk(flag(event.getKpk())); + existing.setBzxs(flag(event.getBzxs())); + existing.setZdpk(flag(event.getZdpk())); + existing.setZk(flag(event.getZk())); + if (event.getCourseClass() != null && !event.getCourseClass().isEmpty()) { + existing.setCourseClass(event.getCourseClass()); + } + existing.setJqsj(Integer.valueOf(day.format(COMPACT))); + jqbMapper.updateById(existing); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int apply(String sourceXydxqbh, List targetBhList, List eventBhList) { + if (sourceXydxqbh == null || sourceXydxqbh.isEmpty() || CollectionUtils.isEmpty(targetBhList)) { + throw new ServiceException("请选择源班次和目标班次", BAD_REQUEST); + } + teachingTaskWriteGuard.assertCourseTaskWritable(sourceXydxqbh); + List sourceRows = listRows(sourceXydxqbh); + if (sourceRows.isEmpty()) { + inherit(requireSemester(sourceXydxqbh)); + sourceRows = listRows(sourceXydxqbh); + } + Set selected = eventBhList == null ? new HashSet<>() : new HashSet<>(eventBhList); + List toCopy = new ArrayList<>(); + for (JQB row : sourceRows) { + if (selected.isEmpty() || selected.contains(row.getBh())) { + toCopy.add(row); + } + } + int count = 0; + for (String targetBh : targetBhList) { + if (sourceXydxqbh.equals(targetBh)) { + continue; + } + teachingTaskWriteGuard.assertCourseTaskWritable(targetBh); + XYDNDXQJBXXB target = requireSemester(targetBh); + if (listRows(targetBh).isEmpty()) { + inherit(target); + } + for (JQB source : toCopy) { + if (overlay(target, source)) { + count++; + } + } + } + return count; + } + + private void inherit(XYDNDXQJBXXB semester) { + LocalDate start = semester.getKxrq(); + LocalDate end = semester.getJsrq(); + if (start == null || end == null) { + throw new ServiceException("班次学期缺少开学或结束日期,不能生成班历", BAD_REQUEST); + } + List schoolRows = loadSchoolRows(semester, start, end); + if (schoolRows.isEmpty()) { + generateEmptyGrid(semester, start, end); + return; + } + for (XQXLB school : schoolRows) { + LocalDate day = parseDay(school.getJqsj()); + if (day.isBefore(start) || day.isAfter(end)) { + continue; + } + insertCell(semester, day, school.getCourseClass(), school.getJqmc(), + flag(school.getJc()), flag(school.getKpk()), flag(school.getBzxs()), + flag(school.getZdpk()), flag(school.getZk()), school.getBz()); + } + } + + private List loadSchoolRows(XYDNDXQJBXXB semester, LocalDate start, LocalDate end) { + Integer schoolNd = semester.getNd() == null || semester.getXqdc() == null + ? null : semester.getNd() * 100 + semester.getXqdc(); + List rows = querySchool(schoolNd, start, end); + if (!rows.isEmpty() || semester.getNd() == null) { + return rows; + } + return querySchool(semester.getNd(), start, end); + } + + private List querySchool(Integer nd, LocalDate start, LocalDate end) { + if (nd == null) { + return List.of(); + } + return semesterCalendarMapper.selectList(new LambdaQueryWrapper() + .eq(XQXLB::getNd, nd) + .ge(XQXLB::getJqsj, start.format(DAY)) + .le(XQXLB::getJqsj, end.format(DAY))); + } + + private void generateEmptyGrid(XYDNDXQJBXXB semester, LocalDate start, LocalDate end) { + for (LocalDate day = start; !day.isAfter(end); day = day.plusDays(1)) { + boolean weekdayMain = day.getDayOfWeek().getValue() <= 5; + for (String period : PERIODS) { + boolean mainSlot = weekdayMain && ("1-2".equals(period) || "3-4".equals(period) || "5-6".equals(period)); + insertCell(semester, day, period, "", 0, mainSlot ? 1 : 0, 0, mainSlot ? 1 : 0, mainSlot ? 1 : 0, null); + } + } + } + + private boolean overlay(XYDNDXQJBXXB target, JQB source) { + LocalDate day = parseDay(formatDay(source.getJqsj())); + if (target.getKxrq() != null && day.isBefore(target.getKxrq())) { + return false; + } + if (target.getJsrq() != null && day.isAfter(target.getJsrq())) { + return false; + } + List matched = jqbMapper.selectList(new LambdaQueryWrapper() + .eq(JQB::getXydxqbh, target.getBh()) + .eq(JQB::getJqsj, source.getJqsj()) + .eq(JQB::getCourseClass, source.getCourseClass())); + if (matched.isEmpty()) { + insertCell(target, day, source.getCourseClass(), source.getJqmc(), + source.getJc(), 0, source.getBzxs(), source.getZdpk(), source.getZk(), source.getBz()); + return true; + } + for (JQB row : matched) { + row.setJqmc(source.getJqmc()); + row.setJc(source.getJc()); + row.setBz(source.getBz()); + row.setBzxs(source.getBzxs()); + row.setZdpk(source.getZdpk()); + row.setZk(source.getZk()); + row.setKpk(source.getKpk()); + jqbMapper.updateById(row); + } + return true; + } + + private void insertCell(XYDNDXQJBXXB semester, LocalDate day, String courseClass, String name, + Integer jc, Integer kpk, Integer bzxs, Integer zdpk, Integer zk, String bz) { + if (courseClass == null || courseClass.isEmpty()) { + return; + } + boolean outside = (semester.getKxrq() != null && day.isBefore(semester.getKxrq())) + || (semester.getJsrq() != null && day.isAfter(semester.getJsrq())); + JQB row = new JQB(); + row.setBh(UuidUtil.getUUID()); + row.setNd(semester.getNd()); + row.setXydbh(semester.getXydbh()); + row.setXydxqbh(semester.getBh()); + row.setJqsj(Integer.valueOf(day.format(COMPACT))); + row.setJqmc(name == null ? "" : name); + row.setJc(jc == null ? 0 : jc); + row.setBz(bz); + row.setKpk(outside ? 0 : (kpk == null ? 0 : kpk)); + row.setBzxs(bzxs == null ? 0 : bzxs); + row.setZdpk(zdpk == null ? 0 : zdpk); + row.setZk(zk == null ? 0 : zk); + row.setCourseClass(courseClass); + jqbMapper.insert(row); + } + + private List listRows(String xydxqbh) { + return jqbMapper.selectList(new LambdaQueryWrapper() + .eq(JQB::getXydxqbh, xydxqbh) + .orderByAsc(JQB::getJqsj) + .orderByAsc(JQB::getCourseClass)); + } + + private XYDNDXQJBXXB requireSemester(String xydxqbh) { + XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh); + if (semester == null || Integer.valueOf(1).equals(semester.getDelFlag())) { + throw new ServiceException("班次学期不存在", BAD_REQUEST); + } + return semester; + } + + private void assertInRange(XYDNDXQJBXXB semester, LocalDate day) { + if (semester.getKxrq() != null && day.isBefore(semester.getKxrq())) { + throw new ServiceException("班次日期范围外的格子不可排", BAD_REQUEST); + } + if (semester.getJsrq() != null && day.isAfter(semester.getJsrq())) { + throw new ServiceException("班次日期范围外的格子不可排", BAD_REQUEST); + } + } + + private ClassCalendarEventDTO toDto(JQB row) { + ClassCalendarEventDTO dto = new ClassCalendarEventDTO(); + dto.setBh(row.getBh()); + dto.setNd(row.getNd()); + dto.setXydbh(row.getXydbh()); + dto.setXydxqbh(row.getXydxqbh()); + dto.setJqsj(formatDay(row.getJqsj())); + dto.setJqmc(row.getJqmc()); + dto.setJc(Integer.valueOf(1).equals(row.getJc())); + dto.setBz(row.getBz()); + dto.setKpk(Integer.valueOf(1).equals(row.getKpk())); + dto.setBzxs(Integer.valueOf(1).equals(row.getBzxs())); + dto.setZdpk(Integer.valueOf(1).equals(row.getZdpk())); + dto.setZk(Integer.valueOf(1).equals(row.getZk())); + dto.setCourseClass(row.getCourseClass()); + return dto; + } + + private String formatDay(Integer value) { + if (value == null) { + return ""; + } + String text = String.valueOf(value); + if (text.length() != 8) { + return text; + } + return text.substring(0, 4) + "-" + text.substring(4, 6) + "-" + text.substring(6, 8); + } + + private LocalDate parseDay(String value) { + if (value == null || value.isEmpty()) { + throw new ServiceException("班历日期不能为空", BAD_REQUEST); + } + String text = value.length() >= 10 ? value.substring(0, 10) : value; + return LocalDate.parse(text, DAY); + } + + private Integer flag(Boolean value) { + return Boolean.TRUE.equals(value) ? 1 : 0; + } +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassSemesterCalendarServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassSemesterCalendarServiceImpl.java index 243397fbb..118be1b5b 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassSemesterCalendarServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassSemesterCalendarServiceImpl.java @@ -6,6 +6,7 @@ import com.roomroot.jwgl.entity.XYDRWB; import com.roomroot.jwgl.mapper.ClassSemesterCalendarMapper; import com.roomroot.jwgl.mapper.ClassSemesterMapper; import com.roomroot.jwgl.service.ClassSemesterCalendarService; +import com.roomroot.jwgl.service.TeachingTaskWriteGuard; import com.roomroot.jwgl.utils.UuidUtil; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,8 +27,19 @@ public class ClassSemesterCalendarServiceImpl implements ClassSemesterCalendarSe @Resource private ClassSemesterMapper classSemesterMapper; + @Resource + private TeachingTaskWriteGuard teachingTaskWriteGuard; + @Override public void update(XYDRWB xydrwb) { + if (xydrwb != null) { + String semesterBh = xydrwb.getXydxqbh(); + if (semesterBh == null && xydrwb.getBh() != null) { + XYDRWB existing = classSemesterCalendarMapper.selectById(xydrwb.getBh()); + semesterBh = existing == null ? null : existing.getXydxqbh(); + } + teachingTaskWriteGuard.assertCourseTaskWritable(semesterBh); + } classSemesterCalendarMapper.updateById(xydrwb); } @@ -50,6 +62,7 @@ public class ClassSemesterCalendarServiceImpl implements ClassSemesterCalendarSe failedTargetBhs.add(targetBh); continue; } + teachingTaskWriteGuard.assertCourseTaskWritable(targetBh); String newXydbh = targetSemester.getXydbh(); for (String sourceBh : sourceBhList) { @@ -142,6 +155,7 @@ public class ClassSemesterCalendarServiceImpl implements ClassSemesterCalendarSe failedTargetBhs.add(targetBh); continue; } + teachingTaskWriteGuard.assertCourseTaskWritable(targetBh); String newXydbh = targetSemester.getXydbh(); for (XYDRWB source : sourceList) { diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassSemesterServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassSemesterServiceImpl.java index 0505f5682..f7cd1b381 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassSemesterServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ClassSemesterServiceImpl.java @@ -1,11 +1,17 @@ package com.roomroot.jwgl.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.roomroot.common.exception.ServiceException; +import com.roomroot.jwgl.entity.XQ; import com.roomroot.jwgl.entity.XYDNDXQJBXXB; import com.roomroot.jwgl.entity.XYDB; +import com.roomroot.jwgl.entity.XYDRWB; import com.roomroot.jwgl.mapper.ClassSemesterCalendarMapper; import com.roomroot.jwgl.mapper.ClassSemesterMapper; import com.roomroot.jwgl.mapper.SemesterCalendarMapper; +import com.roomroot.jwgl.mapper.SemesterMapper; +import com.roomroot.jwgl.mapper.XYDBMapper; import com.roomroot.jwgl.service.ClassSemesterService; import com.roomroot.jwgl.service.ElectiveService; import com.roomroot.jwgl.unit.PageQuery; @@ -17,8 +23,16 @@ import org.springframework.util.CollectionUtils; import jakarta.annotation.Resource; import java.time.LocalDate; + +import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST; +import static com.roomroot.jwgl.utils.BasicManagementConstants.CONFLICT; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; /** * 班次学期服务实现类 @@ -38,37 +52,88 @@ public class ClassSemesterServiceImpl implements ClassSemesterService { @Resource private ElectiveService electiveService; + @Resource + private XYDBMapper xydbMapper; + + @Resource + private SemesterMapper semesterMapper; + @Transactional(rollbackFor = Exception.class) @Override public void add(XYDNDXQJBXXB xydndxqjbxxb) { - String uuid = UuidUtil.getUUID(); - xydndxqjbxxb.setBh(uuid); + if (xydndxqjbxxb == null || xydndxqjbxxb.getXydbh() == null || xydndxqjbxxb.getXydbh().isEmpty()) { + throw new ServiceException("班次编号不能为空", BAD_REQUEST); + } + assertXqdc(xydndxqjbxxb.getXqdc()); + com.roomroot.jwgl.vo.elective.SemesterInitVO init = electiveService.getSemesterInit( + xydndxqjbxxb.getXydbh(), null, null, xydndxqjbxxb.getXqdc()); + if (Boolean.TRUE.equals(init.getHalfYear()) && !Integer.valueOf(1).equals(xydndxqjbxxb.getXqdc())) { + throw new ServiceException("学制半年以内的班次,学期第次只能是第1学期", BAD_REQUEST); + } + if (xydndxqjbxxb.getZyjsbh() == null || xydndxqjbxxb.getZyjsbh().isEmpty()) { + xydndxqjbxxb.setZyjsbh(init.getZyjsbh()); + } + XYDNDXQJBXXB existing = classSemesterMapper.selectByBcAndNdAndXqdc( + xydndxqjbxxb.getXydbh(), + String.valueOf(xydndxqjbxxb.getNd()), + String.valueOf(xydndxqjbxxb.getXqdc())); + if (existing != null) { + throw new ServiceException("该班次本年度学期已存在,不能重复新建", CONFLICT); + } + if (xydndxqjbxxb.getYsbbh() == null) { + xydndxqjbxxb.setYsbbh(0); + } + if (xydndxqjbxxb.getKfjypk() == null) { + xydndxqjbxxb.setKfjypk(0); + } + if (xydndxqjbxxb.getXhbs() == null) { + xydndxqjbxxb.setXhbs(0); + } + xydndxqjbxxb.setBh(UuidUtil.getUUID()); xydndxqjbxxb.setDelFlag(0); - int insert = classSemesterMapper.insert(xydndxqjbxxb); + classSemesterMapper.insert(xydndxqjbxxb); } @Transactional(rollbackFor = Exception.class) @Override public void delete(String bh) { + assertNoCourseTasks(bh); XYDNDXQJBXXB xydndxqjbxxb = getByBh(bh); + if (xydndxqjbxxb == null) { + return; + } xydndxqjbxxb.setDelFlag(1); classSemesterMapper.updateById(xydndxqjbxxb); - classSemesterCalendarMapper.deleteByBcxqBh(bh); } @Override public void batchDelete(List bhList) { + if (bhList == null) { + return; + } for (String bh : bhList) { - XYDNDXQJBXXB xydndxqjbxxb = getByBh(bh); - if (xydndxqjbxxb != null) { - xydndxqjbxxb.setDelFlag(1); - classSemesterMapper.updateById(xydndxqjbxxb); - } + delete(bh); + } + } + + private void assertNoCourseTasks(String bh) { + Long count = classSemesterCalendarMapper.selectCount(new LambdaQueryWrapper() + .eq(XYDRWB::getXydxqbh, bh) + .eq(XYDRWB::getDelFlag, 0)); + if (count != null && count > 0) { + throw new ServiceException("该班次学期已有 " + count + " 条课程教学计划,请先删除课程任务后再删除", CONFLICT); } } @Override public void update(XYDNDXQJBXXB xydndxqjbxxb) { + if (xydndxqjbxxb != null && xydndxqjbxxb.getXydbh() != null && xydndxqjbxxb.getXqdc() != null) { + com.roomroot.jwgl.vo.elective.SemesterInitVO init = electiveService.getSemesterInit( + xydndxqjbxxb.getXydbh(), null, null, xydndxqjbxxb.getXqdc()); + if (Boolean.TRUE.equals(init.getHalfYear()) && !Integer.valueOf(1).equals(xydndxqjbxxb.getXqdc())) { + throw new ServiceException("学制半年以内的班次,学期第次只能是第1学期", BAD_REQUEST); + } + } classSemesterMapper.updateById(xydndxqjbxxb); } @@ -102,40 +167,13 @@ public class ClassSemesterServiceImpl implements ClassSemesterService { return 0; } + int xqdcNum = parseXqdc(xqdc); + int year = Integer.parseInt(nd); int count = 0; for (XYDB xydb : xydbList) { - // 校验是否已存在:根据班次编号、年度、学期第次查询 - XYDNDXQJBXXB existing = classSemesterMapper.selectByBcAndNdAndXqdc(xydb.getXydbh(), nd, xqdc); - if (existing != null) { - // 已存在,跳过 - continue; + if (insertClassSemester(xydb, year, xqdcNum, LocalDate.parse(xqkssj), LocalDate.parse(xqjssj))) { + count++; } - - // 创建学员队年度学期基本信息 - XYDNDXQJBXXB xydndxqjbxxb = new XYDNDXQJBXXB(); - String uuid = UuidUtil.getUUID(); - xydndxqjbxxb.setBh(uuid); - xydndxqjbxxb.setXydbh(xydb.getXydbh()); - xydndxqjbxxb.setNd(Integer.valueOf(nd)); - if("春季学期".equals(xqdc)){ - xqdc = "1"; - } else if("夏季学期".equals(xqdc)){ - xqdc = "2"; - } else if("秋季学期".equals(xqdc)){ - xqdc = "3"; - } - xydndxqjbxxb.setXhbs(0); - xydndxqjbxxb.setXqdc(Integer.valueOf(xqdc)); - xydndxqjbxxb.setKxrq(LocalDate.parse(xqkssj)); - xydndxqjbxxb.setJsrq(LocalDate.parse(xqjssj)); - xydndxqjbxxb.setYsbbh(0); - xydndxqjbxxb.setJclb("双节次"); - xydndxqjbxxb.setXtms(""); - xydndxqjbxxb.setKfjypk(0); - xydndxqjbxxb.setDelFlag(0); - - int insert = classSemesterMapper.insert(xydndxqjbxxb); - } return count; } @@ -159,6 +197,7 @@ public class ClassSemesterServiceImpl implements ClassSemesterService { @Override public int batchUpdateXqdc(List bhList, Integer xqdc) { + assertXqdc(xqdc); if (CollectionUtils.isEmpty(bhList)) { return 0; } @@ -210,6 +249,7 @@ public class ClassSemesterServiceImpl implements ClassSemesterService { xydndxqjbxxb.setKxrq(LocalDate.parse(xqkssj)); xydndxqjbxxb.setJsrq(LocalDate.parse(xqjssj)); classSemesterMapper.updateById(xydndxqjbxxb); + count++; // // 删除日期范围之外的日历记录 // classSemesterCalendarMapper.deleteOutsideDateRange(bh, xqkssj, xqjssj); @@ -250,4 +290,195 @@ public class ClassSemesterServiceImpl implements ClassSemesterService { return count; } + + @Override + public List listCreatableClasses(Integer nd, Integer xqdc) { + assertYearAndXqdc(nd, xqdc); + return xydbMapper.selectCreatableClasses(nd, xqdc); + } + + @Override + public Map presetDates(Integer nd, Integer xqdc) { + assertYearAndXqdc(nd, xqdc); + XQ school = findSchoolSemester(nd, xqdc); + Map result = new HashMap<>(); + if (school == null || school.getKxrq() == null || school.getJsrq() == null) { + result.put("kxrq", ""); + result.put("jsrq", ""); + return result; + } + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + result.put("kxrq", school.getKxrq().toLocalDate().format(fmt)); + result.put("jsrq", school.getJsrq().toLocalDate().format(fmt)); + return result; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public int batchCreateEligible(Integer nd, Integer xqdc, LocalDate kxrq, LocalDate jsrq) { + assertYearAndXqdc(nd, xqdc); + if (kxrq == null || jsrq == null) { + Map preset = presetDates(nd, xqdc); + if (kxrq == null && preset.get("kxrq") != null && !preset.get("kxrq").isEmpty()) { + kxrq = LocalDate.parse(preset.get("kxrq")); + } + if (jsrq == null && preset.get("jsrq") != null && !preset.get("jsrq").isEmpty()) { + jsrq = LocalDate.parse(preset.get("jsrq")); + } + } + if (kxrq == null || jsrq == null) { + throw new ServiceException("请先维护学校学期的开学/结束日期,或手工填写", BAD_REQUEST); + } + if (!jsrq.isAfter(kxrq)) { + throw new ServiceException("结束日期必须晚于开学日期", BAD_REQUEST); + } + List classes = xydbMapper.selectCreatableClasses(nd, xqdc); + int count = 0; + for (XYDB clazz : classes) { + if (insertClassSemester(clazz, nd, xqdc, kxrq, jsrq)) { + count++; + } + } + return count; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public int batchWaveMerge(List bhList) { + List rows = loadRows(bhList); + if (rows.size() < 2) { + throw new ServiceException("请至少选择两个班次进行预设合班", BAD_REQUEST); + } + Set waves = new HashSet<>(); + for (XYDNDXQJBXXB row : rows) { + if (row.getYsbbh() != null && row.getYsbbh() > 0) { + waves.add(row.getYsbbh()); + } + } + int wave = waves.size() == 1 ? waves.iterator().next() : nextWaveNo(); + int count = 0; + for (XYDNDXQJBXXB row : rows) { + row.setYsbbh(wave); + classSemesterMapper.updateById(row); + count++; + } + return count; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public int batchWaveSplit(List bhList) { + List rows = loadRows(bhList); + if (rows.isEmpty()) { + throw new ServiceException("请先选择要拆班的班次", BAD_REQUEST); + } + int next = nextWaveNo(); + int count = 0; + for (XYDNDXQJBXXB row : rows) { + row.setYsbbh(next++); + classSemesterMapper.updateById(row); + count++; + } + return count; + } + + private boolean insertClassSemester(XYDB clazz, int nd, int xqdc, LocalDate kxrq, LocalDate jsrq) { + XYDNDXQJBXXB existing = classSemesterMapper.selectByBcAndNdAndXqdc( + clazz.getXydbh(), String.valueOf(nd), String.valueOf(xqdc)); + if (existing != null) { + return false; + } + XYDNDXQJBXXB row = new XYDNDXQJBXXB(); + row.setBh(UuidUtil.getUUID()); + row.setXydbh(clazz.getXydbh()); + row.setNd(nd); + row.setXqdc(xqdc); + row.setKxrq(kxrq); + row.setJsrq(jsrq); + row.setZyjsbh(resolveRoom(clazz, xqdc)); + row.setXhbs(0); + row.setYsbbh(0); + row.setJclb(clazz.getJclb() != null && !clazz.getJclb().isEmpty() ? clazz.getJclb() : "双节次"); + row.setXtms(clazz.getXtms() == null ? "" : clazz.getXtms()); + row.setKfjypk(0); + row.setDelFlag(0); + classSemesterMapper.insert(row); + return true; + } + + private String resolveRoom(XYDB clazz, int xqdc) { + if (clazz.getZyjsbh() != null && !clazz.getZyjsbh().isEmpty()) { + return clazz.getZyjsbh(); + } + if (xqdc <= 1) { + return null; + } + List prev = classSemesterMapper.selectList(new LambdaQueryWrapper() + .eq(XYDNDXQJBXXB::getXydbh, clazz.getXydbh()) + .eq(XYDNDXQJBXXB::getXqdc, xqdc - 1) + .eq(XYDNDXQJBXXB::getDelFlag, 0) + .orderByDesc(XYDNDXQJBXXB::getNd)); + if (prev.isEmpty()) { + return null; + } + return prev.get(0).getZyjsbh(); + } + + private XQ findSchoolSemester(Integer nd, Integer xqdc) { + String key = String.valueOf(nd * 100 + xqdc); + XQ school = semesterMapper.selectById(key); + if (school != null) { + return school; + } + return semesterMapper.selectById(String.valueOf(nd)); + } + + private List loadRows(List bhList) { + if (CollectionUtils.isEmpty(bhList)) { + return List.of(); + } + return bhList.stream() + .map(classSemesterMapper::selectById) + .filter(row -> row != null && !Integer.valueOf(1).equals(row.getDelFlag())) + .toList(); + } + + private int nextWaveNo() { + int max = 0; + for (XYDNDXQJBXXB row : classSemesterMapper.selectAllValid()) { + if (row.getYsbbh() != null && row.getYsbbh() > max) { + max = row.getYsbbh(); + } + } + return max + 1; + } + + private void assertYearAndXqdc(Integer nd, Integer xqdc) { + if (nd == null || nd < 2000) { + throw new ServiceException("请选择有效年度", BAD_REQUEST); + } + assertXqdc(xqdc); + } + + private void assertXqdc(Integer xqdc) { + if (xqdc == null || xqdc < 1 || xqdc > 3) { + throw new ServiceException("学期第次只能是 1、2、3", BAD_REQUEST); + } + } + + private int parseXqdc(String xqdc) { + if (xqdc == null || xqdc.isEmpty()) { + throw new ServiceException("请选择学期第次", BAD_REQUEST); + } + if ("春季学期".equals(xqdc)) { + return 1; + } + if ("夏季学期".equals(xqdc)) { + return 2; + } + if ("秋季学期".equals(xqdc)) { + return 3; + } + return Integer.parseInt(xqdc); + } } \ No newline at end of file diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ElectiveServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ElectiveServiceImpl.java index 1c5d19927..8eac25177 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ElectiveServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/ElectiveServiceImpl.java @@ -211,7 +211,10 @@ public class ElectiveServiceImpl implements ElectiveService { wrapper.eq(JSB::getJxldh, jxldh); } if (py != null && !py.isEmpty()) { - wrapper.like(JSB::getPy, py); + wrapper.and(w -> w.like(JSB::getPy, py) + .or().like(JSB::getJsmc, py) + .or().like(JSB::getJsbh, py) + .or().like(JSB::getId, py)); } wrapper.orderByAsc(JSB::getXh); return classRoomMapper.selectList(wrapper); @@ -219,6 +222,11 @@ public class ElectiveServiceImpl implements ElectiveService { @Override public SemesterInitVO getSemesterInit(String xydbh, String presetKxrq, String presetJsrq) { + return getSemesterInit(xydbh, presetKxrq, presetJsrq, null); + } + + @Override + public SemesterInitVO getSemesterInit(String xydbh, String presetKxrq, String presetJsrq, Integer xqdc) { XYDB clazz = xydbMapper.selectById(xydbh); if (clazz == null) { throw new BusinessException("教学班次不存在:" + xydbh); @@ -230,8 +238,12 @@ public class ElectiveServiceImpl implements ElectiveService { // 判断学制是否半年以内 boolean halfYear = isHalfYear(rx, by); - // 学期第次:半年以内固定第一学期;半年以上按入学日期智能推断 - Integer xqdc = halfYear ? 1 : inferXqdc(rx, by, LocalDate.now()); + // 学期第次:半年以内固定第一学期;半年以上优先用窗口已选学期第次,否则按入学日期推断 + if (halfYear) { + xqdc = 1; + } else if (xqdc == null || xqdc < 1 || xqdc > 3) { + xqdc = inferXqdc(rx, by, LocalDate.now()); + } // 人才培养方案总学期数(半年一学期,向上取整) Integer totalXqdc = calcTotalXqdc(rx, by); @@ -246,15 +258,12 @@ public class ElectiveServiceImpl implements ElectiveService { jsrq = parseDate(presetJsrq); } - // 第二以上学期:默认预设上学期的专用教室 + // 第二以上学期优先上学期专用教室;没有则用班次专业教室 String zyjsbh = getPrevRoom(xydbh, xqdc); - String zyjsmc = null; - if (zyjsbh != null && !zyjsbh.isEmpty()) { - JSB room = classRoomMapper.selectById(zyjsbh); - if (room != null) { - zyjsmc = room.getJsmc(); - } + if (zyjsbh == null || zyjsbh.isEmpty()) { + zyjsbh = clazz.getZyjsbh(); } + String zyjsmc = roomName(zyjsbh); SemesterInitVO vo = new SemesterInitVO(); vo.setXydbh(xydbh); @@ -405,6 +414,17 @@ public class ElectiveServiceImpl implements ElectiveService { * @param xqdc 当前学期第次(<=1 时返回 null) * @return 上学期专用教室编号,不存在返回 null */ + private String roomName(String zyjsbh) { + if (zyjsbh == null || zyjsbh.isEmpty()) { + return null; + } + JSB room = classRoomMapper.selectById(zyjsbh); + if (room == null) { + room = classRoomMapper.selectOne(new LambdaQueryWrapper().eq(JSB::getJsbh, zyjsbh)); + } + return room == null ? null : room.getJsmc(); + } + private String getPrevRoom(String xydbh, Integer xqdc) { if (xqdc == null || xqdc <= 1) { return null; diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/OfficeTaskBookServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/OfficeTaskBookServiceImpl.java index 0679d8584..6a5564bbe 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/OfficeTaskBookServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/OfficeTaskBookServiceImpl.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.roomroot.jwgl.entity.JYSRWS; import com.roomroot.jwgl.mapper.OfficeTaskBookMapper; import com.roomroot.jwgl.service.OfficeTaskBookService; +import com.roomroot.jwgl.service.TeachingTaskWriteGuard; import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageResult; import com.roomroot.jwgl.utils.UuidUtil; @@ -22,8 +23,14 @@ public class OfficeTaskBookServiceImpl implements OfficeTaskBookService { @Resource private OfficeTaskBookMapper officeTaskBookMapper; + @Resource + private TeachingTaskWriteGuard teachingTaskWriteGuard; + @Override public void add(JYSRWS jysrws) { + if (jysrws != null) { + teachingTaskWriteGuard.assertTaskWritable(jysrws.getJxrwbh()); + } String uuid = UuidUtil.getUUID(); jysrws.setBh(uuid); jysrws.setDelFlag(0); @@ -37,6 +44,7 @@ public class OfficeTaskBookServiceImpl implements OfficeTaskBookService { public void delete(String bh) { JYSRWS jysrws = getByBh(bh); if (jysrws != null) { + teachingTaskWriteGuard.assertTaskWritable(jysrws.getJxrwbh()); jysrws.setDelFlag(1); officeTaskBookMapper.updateById(jysrws); } @@ -51,6 +59,14 @@ public class OfficeTaskBookServiceImpl implements OfficeTaskBookService { @Override public void update(JYSRWS jysrws) { + if (jysrws != null) { + String taskBh = jysrws.getJxrwbh(); + if (taskBh == null && jysrws.getBh() != null) { + JYSRWS existing = getByBh(jysrws.getBh()); + taskBh = existing == null ? null : existing.getJxrwbh(); + } + teachingTaskWriteGuard.assertTaskWritable(taskBh); + } officeTaskBookMapper.updateById(jysrws); } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SemesterServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SemesterServiceImpl.java index e6773798d..968a9e35b 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SemesterServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SemesterServiceImpl.java @@ -3,8 +3,15 @@ package com.roomroot.jwgl.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.roomroot.common.exception.ServiceException; +import com.roomroot.jwgl.entity.JXRW; import com.roomroot.jwgl.entity.XQ; +import com.roomroot.jwgl.entity.XQXLB; +import com.roomroot.jwgl.entity.XYDNDXQJBXXB; +import com.roomroot.jwgl.mapper.ClassSemesterMapper; +import com.roomroot.jwgl.mapper.SemesterCalendarMapper; import com.roomroot.jwgl.mapper.SemesterMapper; +import com.roomroot.jwgl.mapper.TeachingTaskMapper; import com.roomroot.jwgl.service.SemesterCalendarService; import com.roomroot.jwgl.service.SemesterService; import com.roomroot.jwgl.unit.PageQuery; @@ -14,8 +21,12 @@ import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.List; +import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST; +import static com.roomroot.jwgl.utils.BasicManagementConstants.CONFLICT; + /** * 学期服务实现类 */ @@ -31,8 +42,19 @@ public class SemesterServiceImpl extends ServiceImpl impleme @Resource SemesterCalendarService semesterCalendarService; + @Resource + private ClassSemesterMapper classSemesterMapper; + + @Resource + private TeachingTaskMapper teachingTaskMapper; + + @Resource + private SemesterCalendarMapper semesterCalendarMapper; + @Override public void add(XQ xq) { + normalizeSemester(xq); + assertNoOverlap(xq); xq.setDelFlag(0); xq.setTkbsp(true); int insert = semesterMapper.insert(xq); @@ -59,14 +81,77 @@ public class SemesterServiceImpl extends ServiceImpl impleme @Override public void delete(Integer nd) { + if (nd == null) { + throw new ServiceException("学期不能为空", BAD_REQUEST); + } + List blocked = new ArrayList<>(); + Long classSemesterCount = classSemesterMapper.selectCount(new LambdaQueryWrapper() + .eq(XYDNDXQJBXXB::getNd, nd) + .and(w -> w.isNull(XYDNDXQJBXXB::getDelFlag).or().ne(XYDNDXQJBXXB::getDelFlag, 1))); + if (classSemesterCount != null && classSemesterCount > 0) { + blocked.add("班次学期 " + classSemesterCount + " 条"); + } + Long taskCount = teachingTaskMapper.selectCount(new LambdaQueryWrapper() + .eq(JXRW::getNd, nd) + .and(w -> w.isNull(JXRW::getDelFlag).or().ne(JXRW::getDelFlag, 1))); + if (taskCount != null && taskCount > 0) { + blocked.add("教学任务 " + taskCount + " 条"); + } + Long eventCount = semesterCalendarMapper.selectCount(new LambdaQueryWrapper() + .eq(XQXLB::getNd, nd) + .isNotNull(XQXLB::getJqmc) + .ne(XQXLB::getJqmc, "")); + if (eventCount != null && eventCount > 0) { + blocked.add("校历事件 " + eventCount + " 条"); + } + if (!blocked.isEmpty()) { + throw new ServiceException("该学期已有关联数据,不能删除:" + String.join("、", blocked), CONFLICT); + } semesterMapper.deleteById(nd); } @Override public void update(XQ xq) { + normalizeSemester(xq); + assertNoOverlap(xq); semesterMapper.updateById(xq); } + /** + * 年度末两位是学期次,只允许 01/02/03,序号与末位保持一致。 + */ + private void normalizeSemester(XQ xq) { + if (xq == null || xq.getNd() == null || xq.getNd().isBlank()) { + throw new ServiceException("学期年度不能为空", BAD_REQUEST); + } + String nd = xq.getNd().trim(); + if (!nd.matches("\\d{6}")) { + throw new ServiceException("学期年度必须是6位,如202601", BAD_REQUEST); + } + String suffix = nd.substring(4); + if (!suffix.equals("01") && !suffix.equals("02") && !suffix.equals("03")) { + throw new ServiceException("一年最多三个学期,学期次只能是01、02、03", BAD_REQUEST); + } + xq.setNd(nd); + xq.setXh(Integer.parseInt(suffix)); + if (xq.getKxrq() == null || xq.getJsrq() == null) { + throw new ServiceException("开学时间和结束时间不能为空", BAD_REQUEST); + } + if (!xq.getJsrq().isAfter(xq.getKxrq())) { + throw new ServiceException("学期结束时间必须晚于开学时间", BAD_REQUEST); + } + } + + private void assertNoOverlap(XQ xq) { + Long overlap = semesterMapper.selectCount(new LambdaQueryWrapper() + .ne(XQ::getNd, xq.getNd()) + .lt(XQ::getKxrq, xq.getJsrq()) + .gt(XQ::getJsrq, xq.getKxrq())); + if (overlap != null && overlap > 0) { + throw new ServiceException("学期时间范围与已有学期重叠", CONFLICT); + } + } + @Override public XQ getByNd(Integer nd) { return semesterMapper.selectById(nd); diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/StudentTeamTaskServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/StudentTeamTaskServiceImpl.java index a8a63d3af..fa456581f 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/StudentTeamTaskServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/StudentTeamTaskServiceImpl.java @@ -3,25 +3,44 @@ package com.roomroot.jwgl.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.roomroot.common.exception.ServiceException; +import com.roomroot.jwgl.entity.JXRW; +import com.roomroot.jwgl.entity.KB; +import com.roomroot.jwgl.entity.SSKC; +import com.roomroot.jwgl.entity.SSKCXYD; import com.roomroot.jwgl.entity.XYDB; import com.roomroot.jwgl.entity.XYDNDXQJBXXB; import com.roomroot.jwgl.entity.XYDRWB; import com.roomroot.jwgl.entity.ZYJXJHB; import com.roomroot.jwgl.mapper.ClassSemesterMapper; +import com.roomroot.jwgl.mapper.KBMapper; +import com.roomroot.jwgl.mapper.SSKCMapper; +import com.roomroot.jwgl.mapper.SSKCXYDMapper; import com.roomroot.jwgl.mapper.StudentTeamTaskMapper; +import com.roomroot.jwgl.mapper.TeachingTaskMapper; import com.roomroot.jwgl.mapper.XYDBMapper; import com.roomroot.jwgl.mapper.ZYJXJHBMapper; import com.roomroot.jwgl.service.StudentTeamTaskService; +import com.roomroot.jwgl.service.TeachingTaskWriteGuard; +import com.roomroot.jwgl.utils.TeachingTaskStatus; import com.roomroot.jwgl.utils.UuidUtil; +import com.roomroot.jwgl.vo.PlanCourseVO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; +import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; +import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST; +import static com.roomroot.jwgl.utils.BasicManagementConstants.CONFLICT; + /** * 学员队任务表 Service实现 */ @@ -40,8 +59,26 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService { @Autowired private ZYJXJHBMapper zyjxjhbMapper; + @Autowired + private TeachingTaskWriteGuard teachingTaskWriteGuard; + + @Autowired + private KBMapper kbMapper; + + @Autowired + private TeachingTaskMapper teachingTaskMapper; + + @Autowired + private SSKCMapper sskcMapper; + + @Autowired + private SSKCXYDMapper sskcxydMapper; + @Override public void add(XYDRWB xydrwb) { + if (xydrwb != null) { + teachingTaskWriteGuard.assertCourseTaskWritable(xydrwb.getXydxqbh()); + } xydrwb.setBh(UuidUtil.getUUID()); xydrwb.setDelFlag(0); xydrwbMapper.insert(xydrwb); @@ -49,11 +86,23 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService { @Override public void delete(String bh) { + XYDRWB existing = xydrwbMapper.selectById(bh); + if (existing != null) { + teachingTaskWriteGuard.assertCourseTaskWritable(existing.getXydxqbh()); + } xydrwbMapper.deleteById(bh); } @Override public void update(XYDRWB xydrwb) { + if (xydrwb != null) { + String semesterBh = xydrwb.getXydxqbh(); + if (semesterBh == null && xydrwb.getBh() != null) { + XYDRWB existing = xydrwbMapper.selectById(xydrwb.getBh()); + semesterBh = existing == null ? null : existing.getXydxqbh(); + } + teachingTaskWriteGuard.assertCourseTaskWritable(semesterBh); + } xydrwbMapper.updateById(xydrwb); } @@ -168,6 +217,7 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService { } Integer nd = semester.getNd(); String xydxqbh = semester.getBh(); + teachingTaskWriteGuard.assertCourseTaskWritable(xydxqbh); // 4. 查询当前班次已存在的课程编号集合 LambdaQueryWrapper existWrapper = new LambdaQueryWrapper<>(); @@ -231,9 +281,275 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService { source.setBh(newBh); source.setXydbh(newXydbh); source.setDelFlag(0); + teachingTaskWriteGuard.assertCourseTaskWritable(source.getXydxqbh()); xydrwbMapper.insert(source); newBhList.add(newBh); } return newBhList; } + + @Transactional(rollbackFor = Exception.class) + @Override + public int batchAutoGenerate(List xydxqbhList) { + if (CollectionUtils.isEmpty(xydxqbhList)) { + return 0; + } + int count = 0; + for (String xydxqbh : xydxqbhList) { + XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh); + if (semester == null || semester.getXydbh() == null || semester.getXqdc() == null) { + continue; + } + count += autoGenerateRequiredCourses(semester.getXydbh(), semester.getXqdc()); + } + return count; + } + + @Override + public List listPlanCourses(String xydxqbh, boolean otherOnly) { + XYDNDXQJBXXB semester = requireSemester(xydxqbh); + XYDB clazz = xydbMapper.selectById(semester.getXydbh()); + if (clazz == null || clazz.getZydh() == null) { + return List.of(); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(ZYJXJHB::getZydh, clazz.getZydh()) + .eq(ZYJXJHB::getTy, 0); + if (otherOnly && semester.getXqdc() != null) { + wrapper.ne(ZYJXJHB::getXqdc, semester.getXqdc()); + } + List plans = zyjxjhbMapper.selectList(wrapper); + Set done = currentCourseCodes(xydxqbh); + List result = new ArrayList<>(); + for (ZYJXJHB plan : plans) { + PlanCourseVO vo = new PlanCourseVO(); + vo.setBh(plan.getBh()); + vo.setKbh(plan.getKbh()); + vo.setJc(plan.getJc()); + vo.setXqdc(plan.getXqdc()); + vo.setXs(plan.getXs()); + vo.setKlx(plan.getKlx()); + vo.setXf(plan.getXf()); + vo.setZks(plan.getZks()); + vo.setCompleted(done.contains(plan.getKbh())); + result.add(vo); + } + return result; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addFromLibrary(String xydxqbh, String kbh) { + XYDNDXQJBXXB semester = requireWritableSemester(xydxqbh); + KB course = kbMapper.selectById(kbh); + if (course == null) { + throw new ServiceException("课程不存在", BAD_REQUEST); + } + if (currentCourseCodes(xydxqbh).contains(kbh)) { + throw new ServiceException("当前学期已有该课程", CONFLICT); + } + XYDRWB row = new XYDRWB(); + row.setBh(UuidUtil.getUUID()); + row.setNd(semester.getNd()); + row.setXydbh(semester.getXydbh()); + row.setXydxqbh(xydxqbh); + row.setXqdc(semester.getXqdc()); + row.setKbh(course.getKbh()); + row.setJc(course.getJc()); + row.setXs(course.getXs()); + row.setKlx(course.getKlx() == null ? course.getKclx() : course.getKlx()); + row.setXf(course.getXf()); + row.setZks(course.getZks()); + row.setLlxs(course.getLlxs()); + row.setSjxs(course.getSjxs()); + row.setCjfz(course.getCjfz()); + row.setKsks(course.getKsks()); + row.setJysdh(course.getJysdh()); + row.setDelFlag(0); + xydrwbMapper.insert(row); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addFromPlan(String xydxqbh, String planBh) { + XYDNDXQJBXXB semester = requireWritableSemester(xydxqbh); + ZYJXJHB plan = zyjxjhbMapper.selectById(planBh); + if (plan == null) { + throw new ServiceException("人培课程不存在", BAD_REQUEST); + } + if (currentCourseCodes(xydxqbh).contains(plan.getKbh())) { + throw new ServiceException("当前学期已有该课程", CONFLICT); + } + xydrwbMapper.insert(fromPlan(semester, plan)); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public int syncFromPlan(List bhList) { + if (CollectionUtils.isEmpty(bhList)) { + return 0; + } + int count = 0; + for (String bh : bhList) { + XYDRWB row = xydrwbMapper.selectById(bh); + if (row == null) { + continue; + } + teachingTaskWriteGuard.assertCourseTaskWritable(row.getXydxqbh()); + ZYJXJHB plan = findPlan(row); + if (plan == null) { + continue; + } + row.setXs(plan.getXs()); + row.setKlx(plan.getKlx()); + row.setXf(plan.getXf()); + row.setJc(plan.getJc()); + row.setZks(plan.getZks()); + row.setLlxs(plan.getLlxs()); + row.setSjxs(plan.getSjxs()); + row.setJysdh(plan.getJysdh()); + row.setCjfz(plan.getCjfz()); + xydrwbMapper.updateById(row); + count++; + } + return count; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public int batchDeleteGuarded(List bhList) { + if (CollectionUtils.isEmpty(bhList)) { + return 0; + } + int count = 0; + for (String bh : bhList) { + XYDRWB row = xydrwbMapper.selectById(bh); + if (row == null) { + continue; + } + teachingTaskWriteGuard.assertCourseTaskWritable(row.getXydxqbh()); + assertDeletable(row); + xydrwbMapper.deleteById(bh); + count++; + } + return count; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public int pasteToSemester(List bhList, String targetXydxqbh) { + XYDNDXQJBXXB semester = requireWritableSemester(targetXydxqbh); + if (CollectionUtils.isEmpty(bhList)) { + return 0; + } + int count = 0; + for (String bh : bhList) { + XYDRWB source = xydrwbMapper.selectById(bh); + if (source == null) { + continue; + } + source.setBh(UuidUtil.getUUID()); + source.setXydbh(semester.getXydbh()); + source.setXydxqbh(semester.getBh()); + source.setNd(semester.getNd()); + source.setXqdc(semester.getXqdc()); + source.setDelFlag(0); + xydrwbMapper.insert(source); + count++; + } + return count; + } + + private XYDRWB fromPlan(XYDNDXQJBXXB semester, ZYJXJHB plan) { + XYDRWB row = new XYDRWB(); + row.setBh(UuidUtil.getUUID()); + row.setNd(semester.getNd()); + row.setXydbh(semester.getXydbh()); + row.setXydxqbh(semester.getBh()); + row.setXqdc(semester.getXqdc()); + row.setKbh(plan.getKbh()); + row.setKlx(plan.getKlx()); + row.setXs(plan.getXs()); + row.setJc(plan.getJc()); + row.setZks(plan.getZks()); + row.setXf(plan.getXf()); + row.setKsks(plan.getKsks()); + row.setCjfz(plan.getCjfz()); + row.setBjrxypjf(plan.getBjrxypjf()); + row.setLlxs(plan.getLlxs()); + row.setSjxs(plan.getSjxs()); + row.setKsksbxs(plan.getKsksbxs()); + row.setJysdh(plan.getJysdh()); + row.setDelFlag(0); + return row; + } + + private ZYJXJHB findPlan(XYDRWB row) { + XYDB clazz = xydbMapper.selectById(row.getXydbh()); + if (clazz == null || clazz.getZydh() == null || row.getKbh() == null) { + return null; + } + List plans = zyjxjhbMapper.selectList(new LambdaQueryWrapper() + .eq(ZYJXJHB::getZydh, clazz.getZydh()) + .eq(ZYJXJHB::getKbh, row.getKbh()) + .eq(ZYJXJHB::getTy, 0)); + for (ZYJXJHB plan : plans) { + if (Objects.equals(plan.getXqdc(), row.getXqdc())) { + return plan; + } + } + return plans.isEmpty() ? null : plans.get(0); + } + + private void assertDeletable(XYDRWB row) { + XYDNDXQJBXXB semester = classSemesterMapper.selectById(row.getXydxqbh()); + if (semester != null && semester.getJxrwbh() != null) { + JXRW task = teachingTaskMapper.selectById(semester.getJxrwbh()); + if (task != null && (TeachingTaskStatus.isPublished(task.getZt()) || TeachingTaskStatus.isEnded(task.getZt()))) { + throw new ServiceException("课程已发布到教学任务,不能删除", CONFLICT); + } + } + if (isScheduled(row)) { + throw new ServiceException("课程已排课,不能删除", CONFLICT); + } + } + + private boolean isScheduled(XYDRWB row) { + if (row.getKbh() == null || row.getXydbh() == null) { + return false; + } + List courses = sskcMapper.selectList(new LambdaQueryWrapper().eq(SSKC::getKmbh, row.getKbh())); + for (SSKC course : courses) { + Long linked = sskcxydMapper.selectCount(new LambdaQueryWrapper() + .eq(SSKCXYD::getSskcbh, course.getBh()) + .eq(SSKCXYD::getXydbh, row.getXydbh())); + if (linked != null && linked > 0) { + return true; + } + } + return false; + } + + private Set currentCourseCodes(String xydxqbh) { + return xydrwbMapper.selectList(new LambdaQueryWrapper() + .eq(XYDRWB::getXydxqbh, xydxqbh) + .eq(XYDRWB::getDelFlag, 0)) + .stream() + .map(XYDRWB::getKbh) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(HashSet::new)); + } + + private XYDNDXQJBXXB requireSemester(String xydxqbh) { + XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh); + if (semester == null) { + throw new ServiceException("班次学期不存在", BAD_REQUEST); + } + return semester; + } + + private XYDNDXQJBXXB requireWritableSemester(String xydxqbh) { + teachingTaskWriteGuard.assertCourseTaskWritable(xydxqbh); + return requireSemester(xydxqbh); + } } \ No newline at end of file diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TeachingTaskManageServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TeachingTaskManageServiceImpl.java index 2bb542c3a..035dbef69 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TeachingTaskManageServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TeachingTaskManageServiceImpl.java @@ -24,6 +24,7 @@ import com.roomroot.jwgl.mapper.XYDBMapper; import com.roomroot.jwgl.mapper.ZYJXJHBMapper; import com.roomroot.jwgl.service.StudentTeamTaskService; import com.roomroot.jwgl.service.TeachingTaskManageService; +import com.roomroot.jwgl.utils.TeachingTaskStatus; import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageResult; import com.roomroot.jwgl.utils.UuidUtil; @@ -150,6 +151,9 @@ public class TeachingTaskManageServiceImpl implements TeachingTaskManageService if (entity == null || Integer.valueOf(1).equals(entity.getDelFlag())) { throw new ServiceException("教学任务不存在", NOT_FOUND); } + if (TeachingTaskStatus.isEnded(entity.getZt())) { + throw new ServiceException("教学任务已结束,不能再修改", CONFLICT); + } fillSemester(dto); entity.setRwmc(dto.getRwmc().trim()); if (dto.getJssj() != null) { @@ -195,7 +199,10 @@ public class TeachingTaskManageServiceImpl implements TeachingTaskManageService if (entity == null || Integer.valueOf(1).equals(entity.getDelFlag())) { throw new ServiceException("教学任务不存在", NOT_FOUND); } - if (STATUS_PUBLISHED.equals(entity.getZt())) { + if (TeachingTaskStatus.isEnded(entity.getZt())) { + throw new ServiceException("该教学任务已结束", CONFLICT); + } + if (TeachingTaskStatus.isPublished(entity.getZt())) { throw new ServiceException("该教学任务已发布", CONFLICT); } generateCourseTasksAndOfficeBooks(bh); @@ -213,6 +220,35 @@ public class TeachingTaskManageServiceImpl implements TeachingTaskManageService return books.size(); } + @Transactional(rollbackFor = Exception.class) + @Override + public void endPublish(String bh) { + if (StringUtils.isEmpty(bh)) { + throw new ServiceException("教学任务编号不能为空", BAD_REQUEST); + } + JXRW entity = teachingTaskMapper.selectById(bh); + if (entity == null || Integer.valueOf(1).equals(entity.getDelFlag())) { + throw new ServiceException("教学任务不存在", NOT_FOUND); + } + if (TeachingTaskStatus.isEnded(entity.getZt())) { + throw new ServiceException("该教学任务已结束", CONFLICT); + } + if (!TeachingTaskStatus.isPublished(entity.getZt())) { + throw new ServiceException("只有已发布的教学任务才能结束", CONFLICT); + } + entity.setZt(TeachingTaskStatus.ENDED); + entity.setJssj(LocalDateTime.now()); + teachingTaskMapper.updateById(entity); + + List books = officeTaskBookMapper.selectList(new LambdaQueryWrapper() + .eq(JYSRWS::getJxrwbh, bh) + .eq(JYSRWS::getDelFlag, 0)); + for (JYSRWS book : books) { + book.setZt(TeachingTaskStatus.ENDED); + officeTaskBookMapper.updateById(book); + } + } + @Override public List listOfficeBooks(String jxrwbh) { if (StringUtils.isEmpty(jxrwbh)) { diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TeachingTaskServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TeachingTaskServiceImpl.java index 816d97bce..daafdfba4 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TeachingTaskServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TeachingTaskServiceImpl.java @@ -2,8 +2,10 @@ package com.roomroot.jwgl.service.impl; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.roomroot.common.exception.ServiceException; import com.roomroot.jwgl.entity.JXRW; import com.roomroot.jwgl.entity.JYSRWS; +import com.roomroot.jwgl.utils.TeachingTaskStatus; import com.roomroot.jwgl.mapper.TeachingTaskMapper; import com.roomroot.jwgl.mapper.OfficeTaskBookMapper; import com.roomroot.jwgl.service.TeachingTaskService; @@ -14,8 +16,12 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import jakarta.annotation.Resource; +import java.time.LocalDateTime; import java.util.List; +import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST; +import static com.roomroot.jwgl.utils.BasicManagementConstants.CONFLICT; + /** * 教学任务服务实现类 */ @@ -69,6 +75,10 @@ public class TeachingTaskServiceImpl implements TeachingTaskService { @Override public void update(JXRW jxrw) { + JXRW existing = jxrw == null ? null : getByBh(jxrw.getBh()); + if (existing != null && TeachingTaskStatus.isEnded(existing.getZt())) { + throw new ServiceException("教学任务已结束,不能再修改", CONFLICT); + } teachingTaskMapper.updateById(jxrw); } @@ -105,6 +115,9 @@ public class TeachingTaskServiceImpl implements TeachingTaskService { if (jxrw == null || jxrw.getDelFlag() != 0) { return 0; } + if (TeachingTaskStatus.isEnded(jxrw.getZt())) { + throw new ServiceException("教学任务已结束,不能再次发布", CONFLICT); + } // 更新教学任务状态为"发布" jxrw.setZt("发布"); @@ -125,4 +138,33 @@ public class TeachingTaskServiceImpl implements TeachingTaskService { return count; } + + @Transactional(rollbackFor = Exception.class) + @Override + public void endPublish(String bh) { + if (bh == null || bh.isEmpty()) { + throw new ServiceException("教学任务编号不能为空", BAD_REQUEST); + } + JXRW jxrw = teachingTaskMapper.selectById(bh); + if (jxrw == null || Integer.valueOf(1).equals(jxrw.getDelFlag())) { + throw new ServiceException("教学任务不存在", BAD_REQUEST); + } + if (TeachingTaskStatus.isEnded(jxrw.getZt())) { + throw new ServiceException("该教学任务已结束", CONFLICT); + } + if (!TeachingTaskStatus.isPublished(jxrw.getZt())) { + throw new ServiceException("只有已发布的教学任务才能结束", CONFLICT); + } + jxrw.setZt(TeachingTaskStatus.ENDED); + jxrw.setJssj(LocalDateTime.now()); + teachingTaskMapper.updateById(jxrw); + + List books = officeTaskBookMapper.selectList(new LambdaQueryWrapper() + .eq(JYSRWS::getJxrwbh, bh) + .eq(JYSRWS::getDelFlag, 0)); + for (JYSRWS book : books) { + book.setZt(TeachingTaskStatus.ENDED); + officeTaskBookMapper.updateById(book); + } + } } \ No newline at end of file diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TeachingTaskWriteGuardImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TeachingTaskWriteGuardImpl.java new file mode 100644 index 000000000..2ba6aef0a --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TeachingTaskWriteGuardImpl.java @@ -0,0 +1,54 @@ +package com.roomroot.jwgl.service.impl; + +import com.roomroot.common.exception.ServiceException; +import com.roomroot.common.utils.StringUtils; +import com.roomroot.jwgl.entity.JXRW; +import com.roomroot.jwgl.entity.XYDNDXQJBXXB; +import com.roomroot.jwgl.mapper.ClassSemesterMapper; +import com.roomroot.jwgl.mapper.TeachingTaskMapper; +import com.roomroot.jwgl.service.TeachingTaskWriteGuard; +import com.roomroot.jwgl.utils.TeachingTaskStatus; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Resource; + +import static com.roomroot.jwgl.utils.BasicManagementConstants.CONFLICT; + +/** + * 按教学任务状态拦截已结束任务的改写。 + */ +@Service +public class TeachingTaskWriteGuardImpl implements TeachingTaskWriteGuard { + + @Resource + private TeachingTaskMapper teachingTaskMapper; + + @Resource + private ClassSemesterMapper classSemesterMapper; + + @Override + public void assertTaskWritable(String jxrwbh) { + if (StringUtils.isEmpty(jxrwbh)) { + return; + } + JXRW task = teachingTaskMapper.selectById(jxrwbh); + if (task == null || Integer.valueOf(1).equals(task.getDelFlag())) { + return; + } + if (TeachingTaskStatus.isEnded(task.getZt())) { + throw new ServiceException("教学任务已结束,不能再修改任务书或课程任务", CONFLICT); + } + } + + @Override + public void assertCourseTaskWritable(String xydxqbh) { + if (StringUtils.isEmpty(xydxqbh)) { + return; + } + XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh); + if (semester == null) { + return; + } + assertTaskWritable(semester.getJxrwbh()); + } +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/SystemInitializationUtil.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/SystemInitializationUtil.java index 355da2ac3..353a3f3f8 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/SystemInitializationUtil.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/SystemInitializationUtil.java @@ -160,10 +160,10 @@ public final class SystemInitializationUtil { // 第三步:校验同一年度内的学期序号。 if (dto.getSequenceNumber() == null || dto.getSequenceNumber().intValue() < 1 - || dto.getSequenceNumber().intValue() > 10) { + || dto.getSequenceNumber().intValue() > 3) { throw new BusinessException( BAD_REQUEST, - "学期序号必须在1至10之间"); + "一年最多三个学期,学期序号必须在1至3之间"); } // 第四步:开学和结束时间均为学期初始化必填参数。 diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/TeachingTaskStatus.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/TeachingTaskStatus.java new file mode 100644 index 000000000..d5f8fc013 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/utils/TeachingTaskStatus.java @@ -0,0 +1,23 @@ +package com.roomroot.jwgl.utils; + +/** + * 教学任务状态。库中同时存在「发布」和历史值「已发布」。 + */ +public final class TeachingTaskStatus { + + public static final String DRAFT = "上报"; + public static final String PUBLISHED = "发布"; + public static final String PUBLISHED_LEGACY = "已发布"; + public static final String ENDED = "已结束"; + + private TeachingTaskStatus() { + } + + public static boolean isPublished(String status) { + return PUBLISHED.equals(status) || PUBLISHED_LEGACY.equals(status); + } + + public static boolean isEnded(String status) { + return ENDED.equals(status); + } +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/PlanCourseVO.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/PlanCourseVO.java new file mode 100644 index 000000000..59976a142 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/PlanCourseVO.java @@ -0,0 +1,20 @@ +package com.roomroot.jwgl.vo; + +import lombok.Data; + +/** + * 人培课程及在当前班次学期的完成情况。 + */ +@Data +public class PlanCourseVO { + + private String bh; + private String kbh; + private String jc; + private Integer xqdc; + private Integer xs; + private String klx; + private Float xf; + private Integer zks; + private Boolean completed; +} diff --git a/backend/sql/phase0_foundation.sql b/backend/sql/phase0_foundation.sql new file mode 100644 index 000000000..a739df236 --- /dev/null +++ b/backend/sql/phase0_foundation.sql @@ -0,0 +1,21 @@ +-- 阶段0:正课列、学期序号校正 +-- 正课与自动排课分离。回填只覆盖星期一至星期五的 1-6 节且可排课的格子。 +-- 假期时间在学期校历表中是 VARCHAR(10),直接 TO_CHAR 即可。列已存在时不要重复执行 ALTER。 + +ALTER TABLE "学期校历表" ADD "正课" TINYINT DEFAULT 0 NOT NULL; + +ALTER TABLE "假期表" ADD "正课" TINYINT DEFAULT 0 NOT NULL; + +UPDATE "学期校历表" +SET "正课" = 1 +WHERE "可排课" = 1 + AND "节次" IN ('1-2', '3-4', '5-6') + AND TO_CHAR("假期时间", 'D') IN ('2', '3', '4', '5', '6'); + +-- 年度末位即学期次(202602 -> 2)。校正现有序号与末位不一致的记录。 +UPDATE "学期" +SET "序号" = MOD("年度", 10) +WHERE "年度" >= 200001 + AND "年度" <= 210003 + AND MOD("年度", 100) BETWEEN 1 AND 3 + AND "序号" <> MOD("年度", 10); diff --git a/backend/sql/phase2_class_calendar.sql b/backend/sql/phase2_class_calendar.sql new file mode 100644 index 000000000..c64cfc3a2 --- /dev/null +++ b/backend/sql/phase2_class_calendar.sql @@ -0,0 +1,4 @@ +-- 阶段2:班历时间格需要节次。假期表原先没有该列,不新建表。 +-- 列已存在时不要重复执行。 + +ALTER TABLE "假期表" ADD "节次" VARCHAR(10) DEFAULT '' NOT NULL; diff --git a/frontend/src/api/studentRecords/classCalendar.js b/frontend/src/api/studentRecords/classCalendar.js index 96604c5d4..f77179ded 100644 --- a/frontend/src/api/studentRecords/classCalendar.js +++ b/frontend/src/api/studentRecords/classCalendar.js @@ -1,6 +1,6 @@ import request from '@/utils/request' -// ======================== 班历管理(实体 XYDRWB 学员队任务表) ======================== +// ======================== 班次教学任务(实体 XYDRWB 学员队任务表,不是班历) ======================== // 接口依据:api2.txt「班次学期日历(班历)」分组 + StudentTeamTaskController // 每个班次学期(xydxqbh)下挂若干条课程任务记录: // bh 编号、nd 年度、xydbh 学员队编号、kbh 课编号、xqdc 学期第次、jybh 教员编号、kcxh 课次序号、 @@ -103,3 +103,59 @@ export function autoGenerateRequiredCourses(xydbh, xqdc) { params: { xydbh: xydbh, xqdc: xqdc } }) } + +export function batchAutoGenerateTasks(xydxqbhList) { + return request({ + url: '/studentTeamTask/batchAutoGenerate', + method: 'post', + data: xydxqbhList + }) +} + +export function listPlanCourses(xydxqbh, otherOnly) { + return request({ + url: '/studentTeamTask/planCourses', + method: 'get', + params: { xydxqbh, otherOnly } + }) +} + +export function addTaskFromLibrary(xydxqbh, kbh) { + return request({ + url: '/studentTeamTask/addFromLibrary', + method: 'post', + params: { xydxqbh, kbh } + }) +} + +export function addTaskFromPlan(xydxqbh, planBh) { + return request({ + url: '/studentTeamTask/addFromPlan', + method: 'post', + params: { xydxqbh, planBh } + }) +} + +export function syncTaskFromPlan(bhList) { + return request({ + url: '/studentTeamTask/syncFromPlan', + method: 'post', + data: bhList + }) +} + +export function batchDeleteTasks(bhList) { + return request({ + url: '/studentTeamTask/batchDelete', + method: 'post', + data: bhList + }) +} + +export function pasteTasks(bhList, targetXydxqbh) { + return request({ + url: '/studentTeamTask/paste', + method: 'post', + data: { bhList, targetXydxqbh } + }) +} diff --git a/frontend/src/api/studentRecords/semester.js b/frontend/src/api/studentRecords/semester.js index d60be6e35..14b6c4bba 100644 --- a/frontend/src/api/studentRecords/semester.js +++ b/frontend/src/api/studentRecords/semester.js @@ -52,7 +52,7 @@ export function updateSemester(data) { }) } -// 删除班次学期(逻辑删除并级联删除其班历记录) +// 删除班次学期(逻辑删除;已有课程任务时后端拒绝) export function delSemester(bh) { return request({ url: '/student-records/semester/delete', @@ -115,3 +115,72 @@ export function batchAddFromElective(params) { params: params }) } + +// 未毕业、且该年度学期尚未建立班次学期的班次 +export function listCreatableClasses(params) { + return request({ + url: '/student-records/semester/creatableClasses', + method: 'get', + params: params + }) +} + +// 按学校学期带出开学/结束日期 +export function presetSemesterDates(params) { + return request({ + url: '/student-records/semester/presetDates', + method: 'get', + params: params + }) +} + +// 为符合条件的全部班次创建班次学期,返回实际创建数 +export function batchCreateSemester(data) { + return request({ + url: '/student-records/semester/batchCreate', + method: 'post', + data: data + }) +} + +// 学期级预设合班,只改预设编班号 +export function batchWaveMerge(data) { + return request({ + url: '/student-records/semester/batchWaveMerge', + method: 'post', + data: data + }) +} + +// 按学制带出学期第次、日期、专用教室。xqdc 为窗口已选学期第次 +export function initClassSemester(params) { + return request({ + url: '/elective/semester/init', + method: 'get', + params: params + }) +} + +export function listSemesterBuildings() { + return request({ + url: '/elective/semester/buildings', + method: 'get' + }) +} + +export function listSemesterRooms(params) { + return request({ + url: '/elective/semester/rooms', + method: 'get', + params: params + }) +} + +// 学期级预设拆班,所选班次各自独立成波 +export function batchWaveSplit(data) { + return request({ + url: '/student-records/semester/batchWaveSplit', + method: 'post', + data: data + }) +} diff --git a/frontend/src/api/teachBusiness/teachingTask.js b/frontend/src/api/teachBusiness/teachingTask.js index e83d276c1..de1c0eee8 100644 --- a/frontend/src/api/teachBusiness/teachingTask.js +++ b/frontend/src/api/teachBusiness/teachingTask.js @@ -88,3 +88,15 @@ export function publishTeachingTask(bh) { params: { bh } }) } + +/** + * 结束发布。仅已发布任务可结束,结束后任务书与班次课程任务只读。 + * POST /teachingTask/endPublish?bh= + */ +export function endPublishTeachingTask(bh) { + return request({ + url: '/teachingTask/endPublish', + method: 'post', + params: { bh } + }) +} diff --git a/frontend/src/api/teachOffice/classCalendarEvent.js b/frontend/src/api/teachOffice/classCalendarEvent.js new file mode 100644 index 000000000..8083d284a --- /dev/null +++ b/frontend/src/api/teachOffice/classCalendarEvent.js @@ -0,0 +1,25 @@ +import request from '@/utils/request' + +export function listClassCalendarEvent(xydxqbh) { + return request({ + url: '/class-calendar/list', + method: 'get', + params: { xydxqbh } + }) +} + +export function updateClassCalendarEvent(data) { + return request({ + url: '/class-calendar/update', + method: 'post', + data + }) +} + +export function applyClassCalendarEvent(data) { + return request({ + url: '/class-calendar/apply', + method: 'post', + data + }) +} diff --git a/frontend/src/views/teachBusiness/calendar/components/SchoolCalendarEditor.vue b/frontend/src/views/teachBusiness/calendar/components/SchoolCalendarEditor.vue index 17d14e6e2..e8201b74e 100644 --- a/frontend/src/views/teachBusiness/calendar/components/SchoolCalendarEditor.vue +++ b/frontend/src/views/teachBusiness/calendar/components/SchoolCalendarEditor.vue @@ -17,7 +17,7 @@

事件名称: - + 加粗显示 可排课 正课 @@ -92,6 +92,7 @@ + + diff --git a/frontend/src/views/teachOffice/shiftSemester/ClassSemesterDetailDialog.vue b/frontend/src/views/teachOffice/shiftSemester/ClassSemesterDetailDialog.vue index 8feae81b7..3b013d053 100644 --- a/frontend/src/views/teachOffice/shiftSemester/ClassSemesterDetailDialog.vue +++ b/frontend/src/views/teachOffice/shiftSemester/ClassSemesterDetailDialog.vue @@ -2,7 +2,7 @@ - - + + + + + - + - + +
学制半年以内,学期第次固定为第1学期
@@ -52,7 +69,25 @@ - + + + @@ -79,6 +114,8 @@