通过AI比对原操作文档,生成系统功能完善方案,根据方案进行增补,完善从学期新增到最终的排课功能
This commit is contained in:
@@ -45,3 +45,8 @@ htmlcov/
|
|||||||
deploy/
|
deploy/
|
||||||
.reasonix/
|
.reasonix/
|
||||||
reasonix.toml
|
reasonix.toml
|
||||||
|
.workbuddy/
|
||||||
|
|
||||||
|
# Agent scratch (screenshot/temp working dirs, local maven repo)
|
||||||
|
output/*/working/
|
||||||
|
m2repo/
|
||||||
|
|||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
package com.roomroot.web.controller.jwgl;
|
||||||
|
|
||||||
|
import com.roomroot.jwgl.dto.scheduling.SchedulingArrangeRequest;
|
||||||
|
import com.roomroot.jwgl.dto.scheduling.SchedulingCellOpRequest;
|
||||||
|
import com.roomroot.jwgl.service.SchedulingWindowService;
|
||||||
|
import com.roomroot.jwgl.unit.Result;
|
||||||
|
import com.roomroot.jwgl.vo.scheduling.SchedulingViewVO;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
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 java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实施排课窗口 Controller(阶段 5.3)。
|
||||||
|
*
|
||||||
|
* <p>组合查询 + 写操作:安排所选节次 / 删除所选节次 / 删除课程全部节次 / 彻底删除运行课程。
|
||||||
|
* 硬冲突格忽略(双占、校历/班历/场地历/教员历不可排);软提示(配当周次不符、非正课)随返回值给出。</p>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/schedulingWindow")
|
||||||
|
public class SchedulingWindowController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SchedulingWindowService schedulingWindowService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排课窗组合视图:班次头 + 课程列表(排满标绿)+ 周次×星期×节次格子。
|
||||||
|
*/
|
||||||
|
@GetMapping("/view")
|
||||||
|
public Result<SchedulingViewVO> view(@RequestParam("xydxqbh") String xydxqbh) {
|
||||||
|
return Result.success(schedulingWindowService.view(xydxqbh));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排所选节次。硬冲突格忽略并列出,软提示格照常安排并给出提示。
|
||||||
|
*/
|
||||||
|
@PostMapping("/arrange")
|
||||||
|
public Result<Map<String, Object>> arrange(@RequestBody SchedulingArrangeRequest request) {
|
||||||
|
return Result.success(schedulingWindowService.arrange(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除所选节次:仅删除该课程在这些格上的课次;已提交实施计划的课次拒绝。
|
||||||
|
*/
|
||||||
|
@PostMapping("/deleteCells")
|
||||||
|
public Result<Integer> deleteCells(@RequestBody SchedulingCellOpRequest request) {
|
||||||
|
return Result.success(schedulingWindowService.deleteCells(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除课程全部节次(课程仍留在列表)。
|
||||||
|
*/
|
||||||
|
@PostMapping("/clearCourse")
|
||||||
|
public Result<Integer> clearCourse(@RequestParam("sskcbh") String sskcbh) {
|
||||||
|
return Result.success(schedulingWindowService.clearCourse(sskcbh));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 彻底删除运行课程:课次与列表项一起删,编制侧任务保留(教员不可用)。
|
||||||
|
*/
|
||||||
|
@PostMapping("/deleteCourse")
|
||||||
|
public Result<Integer> deleteCourse(@RequestParam("sskcbh") String sskcbh) {
|
||||||
|
return Result.success(schedulingWindowService.deleteCourse(sskcbh));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排课日志查询:按课程(可叠加操作类型)。
|
||||||
|
*/
|
||||||
|
@GetMapping("/logs")
|
||||||
|
public Result<List<Map<String, Object>>> logs(@RequestParam("sskcbh") String sskcbh,
|
||||||
|
@RequestParam(value = "czlx", required = false) String czlx) {
|
||||||
|
return Result.success(schedulingWindowService.logs(sskcbh, czlx));
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-9
@@ -124,21 +124,19 @@ public class StudentTeamTaskController {
|
|||||||
/**
|
/**
|
||||||
* 自动生成必修课程
|
* 自动生成必修课程
|
||||||
* <p>
|
* <p>
|
||||||
* 根据学员队编号从学员队表获取专业代号,
|
* 按学员队学期编号定位目标班次学期,由它反查学员队编号、学期第次与年度,
|
||||||
* 再根据专业代号和学期第次查询专业教学计划表,
|
* 再按专业代号 + 学期第次查专业教学计划表补齐必修课程。
|
||||||
* 为当前班次自动生成必修课程,插入到学员队任务表。
|
|
||||||
* 已存在的课程(按课编号判断)不会重复添加。
|
* 已存在的课程(按课编号判断)不会重复添加。
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param xydbh 学员队编号
|
* @param xydxqbh 学员队学期编号
|
||||||
* @param xqdc 学期第次
|
|
||||||
* @return 新增数量
|
* @return 新增数量
|
||||||
*/
|
*/
|
||||||
@PostMapping("/autoGenerateRequiredCourses")
|
@PostMapping("/autoGenerateRequiredCourses")
|
||||||
public Result<Integer> autoGenerateRequiredCourses(
|
public Result<Integer> autoGenerateRequiredCourses(@RequestParam("xydxqbh") String xydxqbh) {
|
||||||
@RequestParam("xydbh") String xydbh,
|
// 必须按「班次学期编号」定位:同一学员队在多个年度可能有学期第次相同的班次学期,
|
||||||
@RequestParam("xqdc") Integer xqdc) {
|
// 只传 (学员队编号, 学期第次) 无法区分学年,会把课程任务写进别的年度。
|
||||||
int count = studentTeamTaskService.autoGenerateRequiredCourses(xydbh, xqdc);
|
int count = studentTeamTaskService.autoGenerateRequiredCourses(xydxqbh);
|
||||||
return Result.success(count);
|
return Result.success(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
package com.roomroot.web.controller.jwgl;
|
||||||
|
|
||||||
|
import com.roomroot.jwgl.dto.taskbook.TaskBookBhListRequest;
|
||||||
|
import com.roomroot.jwgl.dto.taskbook.TaskBookFillRequest;
|
||||||
|
import com.roomroot.jwgl.dto.taskbook.TaskBookRoomRequest;
|
||||||
|
import com.roomroot.jwgl.dto.taskbook.TaskBookTeacherRequest;
|
||||||
|
import com.roomroot.jwgl.service.TaskBookFillService;
|
||||||
|
import com.roomroot.jwgl.unit.Result;
|
||||||
|
import com.roomroot.jwgl.vo.taskbook.TaskBookRowVO;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
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 java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教研室任务书填报 Controller(阶段 4)。
|
||||||
|
*
|
||||||
|
* <p>把已生成的课程任务确认成「可以排课的运行对象」:合班、责任教员、默认场地。</p>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/taskBookFill")
|
||||||
|
public class TaskBookFillController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TaskBookFillService taskBookFillService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填报列表:某教学任务下全部课程任务行(排序 课程→责任教员→课次,含合班分组号)。
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<List<TaskBookRowVO>> list(@RequestParam("jxrwbh") String jxrwbh) {
|
||||||
|
return Result.success(taskBookFillService.list(jxrwbh));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动合班:所选行写入同一编组;科目/学时/课类型/成绩分制不同则拒绝并说明。
|
||||||
|
*/
|
||||||
|
@PostMapping("/merge")
|
||||||
|
public Result<Integer> merge(@RequestBody TaskBookBhListRequest request) {
|
||||||
|
return Result.success(taskBookFillService.merge(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按预设合班:按课程科目分组,组内按预设编班号写入同一编组。
|
||||||
|
*/
|
||||||
|
@PostMapping("/mergeByPreset")
|
||||||
|
public Result<List<Map<String, Object>>> mergeByPreset(@RequestBody TaskBookBhListRequest request) {
|
||||||
|
return Result.success(taskBookFillService.mergeByPreset(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拆班:所选行编组置 0。
|
||||||
|
*/
|
||||||
|
@PostMapping("/split")
|
||||||
|
public Result<Integer> split(@RequestBody TaskBookBhListRequest request) {
|
||||||
|
return Result.success(taskBookFillService.split(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 责任教员指定:mode = unit(责任单位教员)/ academy(全院教员)/ plan(应用教研室计划教员)。
|
||||||
|
* 同合班组其它行同步。
|
||||||
|
*/
|
||||||
|
@PostMapping("/setTeacher")
|
||||||
|
public Result<Integer> setTeacher(@RequestBody TaskBookTeacherRequest request) {
|
||||||
|
return Result.success(taskBookFillService.setTeacher(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场地指定:useSpecial=true 使用班次专用教室;否则按 jsbh 指定其它教室。同合班组同步。
|
||||||
|
*/
|
||||||
|
@PostMapping("/setRoom")
|
||||||
|
public Result<Integer> setRoom(@RequestBody TaskBookRoomRequest request) {
|
||||||
|
return Result.success(taskBookFillService.setRoom(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填报字段:计划教员 / 场地(同合班同步)/ 排课建议。
|
||||||
|
*/
|
||||||
|
@PostMapping("/fill")
|
||||||
|
public Result<Integer> fill(@RequestBody TaskBookFillRequest request) {
|
||||||
|
return Result.success(taskBookFillService.fill(request));
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package com.roomroot.web.controller.jwgl;
|
||||||
|
|
||||||
|
import com.roomroot.jwgl.entity.JYL;
|
||||||
|
import com.roomroot.jwgl.service.JYLService;
|
||||||
|
import com.roomroot.jwgl.unit.PageQuery;
|
||||||
|
import com.roomroot.jwgl.unit.PageResult;
|
||||||
|
import com.roomroot.jwgl.unit.Result;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教员历控制器(阶段 5.1)。排课冲突同时读取场地历与教员历。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/teacherCalendar/jyl")
|
||||||
|
public class TeacherCalendarController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JYLService jylService;
|
||||||
|
|
||||||
|
/** 新增教员历 */
|
||||||
|
@PostMapping("/add")
|
||||||
|
public Result<Void> add(@RequestBody JYL jyl) {
|
||||||
|
jylService.add(jyl);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除教员历(软删) */
|
||||||
|
@PostMapping("/delete")
|
||||||
|
public Result<Void> delete(@RequestParam("bh") String bh) {
|
||||||
|
jylService.delete(bh);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新教员历 */
|
||||||
|
@PostMapping("/update")
|
||||||
|
public Result<Void> update(@RequestBody JYL jyl) {
|
||||||
|
jylService.update(jyl);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据编号查询 */
|
||||||
|
@GetMapping("/get")
|
||||||
|
public Result<JYL> getById(@RequestParam("bh") String bh) {
|
||||||
|
return Result.success(jylService.getById(bh));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页查询 */
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<PageResult<JYL>> list(PageQuery query, JYL jyl) {
|
||||||
|
return Result.success(jylService.pageList(query, jyl));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询全部 */
|
||||||
|
@GetMapping("/all")
|
||||||
|
public Result<List<JYL>> all(JYL jyl) {
|
||||||
|
return Result.success(jylService.list(jyl));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按教员编号查询 */
|
||||||
|
@GetMapping("/listByJybh")
|
||||||
|
public Result<List<JYL>> listByJybh(@RequestParam("jybh") String jybh) {
|
||||||
|
return Result.success(jylService.listByJybh(jybh));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按日期查询 */
|
||||||
|
@GetMapping("/listByRq")
|
||||||
|
public Result<List<JYL>> listByRq(@RequestParam("rq") String rq) {
|
||||||
|
return Result.success(jylService.listByRq(rq));
|
||||||
|
}
|
||||||
|
}
|
||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
package com.roomroot.web.controller.jwgl;
|
||||||
|
|
||||||
|
import com.roomroot.jwgl.unit.Result;
|
||||||
|
import com.roomroot.jwgl.dto.allocation.AllocationGroupRequest;
|
||||||
|
import com.roomroot.jwgl.dto.allocation.AllocationSaveItem;
|
||||||
|
import com.roomroot.jwgl.service.TeachingAllocationService;
|
||||||
|
import com.roomroot.jwgl.vo.allocation.AllocationViewVO;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
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 java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教学配当 Controller(阶段 3)。
|
||||||
|
*
|
||||||
|
* <p>配当只做排课粗约束(周学时分布、同开同结),不直接写实施课程表。</p>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/teachingAllocation")
|
||||||
|
public class TeachingAllocationController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TeachingAllocationService teachingAllocationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配当编辑器视图:周轴(每周可排正课)+ 任务行(含铺学时建议)。
|
||||||
|
*/
|
||||||
|
@GetMapping("/view")
|
||||||
|
public Result<AllocationViewVO> view(@RequestParam("xydxqbh") String xydxqbh) {
|
||||||
|
return Result.success(teachingAllocationService.view(xydxqbh));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存配当:单条(列表 1 个元素)或多选批量;元素 saveGroup=true 时整编组同开同结。
|
||||||
|
*/
|
||||||
|
@PostMapping("/save")
|
||||||
|
public Result<Integer> save(@RequestBody List<AllocationSaveItem> items) {
|
||||||
|
return Result.success(teachingAllocationService.save(items));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优选序数上移/下移(direction: up/down),返回归一化后的任务编号顺序。
|
||||||
|
*/
|
||||||
|
@PostMapping("/move")
|
||||||
|
public Result<List<String>> move(@RequestParam("xydxqbh") String xydxqbh,
|
||||||
|
@RequestParam("bh") String bh,
|
||||||
|
@RequestParam("direction") String direction) {
|
||||||
|
return Result.success(teachingAllocationService.move(xydxqbh, bh, direction));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编组管理面板:同科目全部开课班次。
|
||||||
|
*/
|
||||||
|
@GetMapping("/groups")
|
||||||
|
public Result<List<Map<String, Object>>> groups(@RequestParam("kbh") String kbh) {
|
||||||
|
return Result.success(teachingAllocationService.groups(kbh));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编组管理:把所选任务统一改编组号。
|
||||||
|
*/
|
||||||
|
@PostMapping("/setGroup")
|
||||||
|
public Result<Integer> setGroup(@RequestBody AllocationGroupRequest request) {
|
||||||
|
return Result.success(teachingAllocationService.setGroup(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出配当 Excel(支持批量:xydxqbh 逗号分隔)。
|
||||||
|
*/
|
||||||
|
@GetMapping("/export")
|
||||||
|
public void export(@RequestParam("xydxqbh") String xydxqbh, HttpServletResponse response) throws Exception {
|
||||||
|
teachingAllocationService.exportExcel(Arrays.asList(xydxqbh.split(",")), response);
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.roomroot.jwgl.dto.allocation;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教学配当 - 编组管理改编组号请求。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AllocationGroupRequest {
|
||||||
|
|
||||||
|
/** 任务编号列表(必填) */
|
||||||
|
private List<String> bhList;
|
||||||
|
|
||||||
|
/** 目标配档编组号(必填,>=0) */
|
||||||
|
private Integer pdbz;
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package com.roomroot.jwgl.dto.allocation;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教学配当 - 单条保存项。
|
||||||
|
*
|
||||||
|
* <p>字段为 null 表示不修改。{@code saveGroup=true} 时按「同科目 + 同配档编组 + 同学期代号」
|
||||||
|
* 把同组各班次任务一起改(同开同结),限定在同一学期代号内防止跨学期串改。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AllocationSaveItem {
|
||||||
|
|
||||||
|
/** 任务编号(必填) */
|
||||||
|
private String bh;
|
||||||
|
|
||||||
|
/** 配档序号(优选序数) */
|
||||||
|
private Integer pdxh;
|
||||||
|
|
||||||
|
/** 配档起始周 */
|
||||||
|
private Integer pdqsz;
|
||||||
|
|
||||||
|
/** 配档按周:0=连排 1=按周 */
|
||||||
|
private Integer pdaz;
|
||||||
|
|
||||||
|
/** 配档占正课时间 */
|
||||||
|
private Integer pdzzksj;
|
||||||
|
|
||||||
|
/** 配档编组 */
|
||||||
|
private Integer pdbz;
|
||||||
|
|
||||||
|
/** 是否保存整个编组 */
|
||||||
|
private Boolean saveGroup;
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
package com.roomroot.jwgl.dto.scheduling;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排所选节次请求(阶段 5.3)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class SchedulingArrangeRequest {
|
||||||
|
|
||||||
|
private String xydxqbh;
|
||||||
|
|
||||||
|
/** 实施_课程编号 */
|
||||||
|
private String sskcbh;
|
||||||
|
|
||||||
|
/** 教学内容(课题) */
|
||||||
|
private String jxnr;
|
||||||
|
|
||||||
|
/** 教学要点 */
|
||||||
|
private String jxyd;
|
||||||
|
|
||||||
|
/** 教学方法,默认 理论讲授 */
|
||||||
|
private String jxff;
|
||||||
|
|
||||||
|
/** 节次调节:0 默认(一节 2 学时)/ -1 / +1 */
|
||||||
|
private Integer jcdj;
|
||||||
|
|
||||||
|
/** 教学保障备注 */
|
||||||
|
private String jxbzbz;
|
||||||
|
|
||||||
|
/** 用车信息 */
|
||||||
|
private String ycxx;
|
||||||
|
|
||||||
|
/** 班组模式:合班全部 / 分组等 */
|
||||||
|
private String bzms;
|
||||||
|
|
||||||
|
/** 场地(教室编号),缺省用课程的默认场地 */
|
||||||
|
private String jsbh;
|
||||||
|
|
||||||
|
/** 主讲教员,缺省用课程责任教员 */
|
||||||
|
private String jybh;
|
||||||
|
|
||||||
|
/** 所选格子 */
|
||||||
|
private List<Cell> cells;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Cell {
|
||||||
|
/** 日期 yyyy-MM-dd */
|
||||||
|
private String rq;
|
||||||
|
/** 节次 */
|
||||||
|
private Integer jc;
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package com.roomroot.jwgl.dto.scheduling;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除所选节次请求(阶段 5.3)。仅删除该课程在这些格上的课次。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class SchedulingCellOpRequest {
|
||||||
|
|
||||||
|
/** 实施_课程编号 */
|
||||||
|
private String sskcbh;
|
||||||
|
|
||||||
|
private List<SchedulingArrangeRequest.Cell> cells;
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package com.roomroot.jwgl.dto.taskbook;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务书批量操作请求(合班 / 按预设合班 / 拆班)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TaskBookBhListRequest {
|
||||||
|
|
||||||
|
/** 课程任务编号列表 */
|
||||||
|
private List<String> bhList;
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package com.roomroot.jwgl.dto.taskbook;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教研室填报字段请求(阶段 4:计划教员 / 场地 / 排课建议)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TaskBookFillRequest {
|
||||||
|
|
||||||
|
/** 课程任务编号 */
|
||||||
|
private String bh;
|
||||||
|
|
||||||
|
/** 教研室计划教员编号 */
|
||||||
|
private String jysjhjybh;
|
||||||
|
|
||||||
|
/** 场地(教室编号;非空时同合班行同步) */
|
||||||
|
private String jsbh;
|
||||||
|
|
||||||
|
/** 排课建议(教研室计划备注) */
|
||||||
|
private String jysjhbz;
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
package com.roomroot.jwgl.dto.taskbook;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场地指定请求(阶段 4)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TaskBookRoomRequest {
|
||||||
|
|
||||||
|
/** 课程任务编号 */
|
||||||
|
private String bh;
|
||||||
|
|
||||||
|
/** 教室编号(useSpecial=false 时必传) */
|
||||||
|
private String jsbh;
|
||||||
|
|
||||||
|
/** true=使用班次专用教室(忽略 jsbh,取班次学期.专用教室编号) */
|
||||||
|
private Boolean useSpecial;
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package com.roomroot.jwgl.dto.taskbook;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 责任教员指定请求(阶段 4:三种指定方式)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TaskBookTeacherRequest {
|
||||||
|
|
||||||
|
/** 课程任务编号 */
|
||||||
|
private String bh;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指定方式:
|
||||||
|
* unit — 责任单位教员(教员必须属于该行教研室 jysdh);
|
||||||
|
* academy — 全院教员(任意在职教员);
|
||||||
|
* plan — 应用教研室计划教员(jybh = 该行 jysjhjybh)。
|
||||||
|
*/
|
||||||
|
private String mode;
|
||||||
|
|
||||||
|
/** 责任教员编号(mode=unit / academy 时必传) */
|
||||||
|
private String jybh;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.roomroot.jwgl.entity;
|
package com.roomroot.jwgl.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.roomroot.jwgl.unit.BaseEntity;
|
import com.roomroot.jwgl.unit.BaseEntity;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -7,6 +8,9 @@ import lombok.EqualsAndHashCode;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 实施_课程学员队
|
* 实施_课程学员队
|
||||||
|
*
|
||||||
|
* <p>阶段 5 修复:原实体缺少中文列注解,LambdaQueryWrapper 会生成错误的英文列名
|
||||||
|
* (如 SSKCBH),导致 isScheduled 等查询运行时报 Invalid column。</p>
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@@ -16,46 +20,55 @@ public class SSKCXYD extends BaseEntity {
|
|||||||
/**
|
/**
|
||||||
* 编号
|
* 编号
|
||||||
*/
|
*/
|
||||||
|
@TableField("编号")
|
||||||
private String bh;
|
private String bh;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 实施_课程编号
|
* 实施_课程编号
|
||||||
*/
|
*/
|
||||||
|
@TableField("实施_课程编号")
|
||||||
private String sskcbh;
|
private String sskcbh;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 学员队编号
|
* 学员队编号
|
||||||
*/
|
*/
|
||||||
|
@TableField("学员队编号")
|
||||||
private String xydbh;
|
private String xydbh;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 简称
|
* 简称
|
||||||
*/
|
*/
|
||||||
|
@TableField("简称")
|
||||||
private String jc;
|
private String jc;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 人数
|
* 人数
|
||||||
*/
|
*/
|
||||||
|
@TableField("人数")
|
||||||
private Integer rs;
|
private Integer rs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 年度
|
* 年度
|
||||||
*/
|
*/
|
||||||
|
@TableField("年度")
|
||||||
private Integer nd;
|
private Integer nd;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 学期第次
|
* 学期第次
|
||||||
*/
|
*/
|
||||||
|
@TableField("学期第次")
|
||||||
private Integer xqdc;
|
private Integer xqdc;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 学分
|
* 学分
|
||||||
*/
|
*/
|
||||||
|
@TableField("学分")
|
||||||
private Float xf;
|
private Float xf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 学员_课程板块测评编号
|
* 学员_课程板块测评编号
|
||||||
*/
|
*/
|
||||||
|
@TableField("学员_课程板块测评编号")
|
||||||
private String xykcbkcpbh;
|
private String xykcbkcpbh;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -90,7 +90,8 @@
|
|||||||
FROM "学员队年度学期基本信息表"
|
FROM "学员队年度学期基本信息表"
|
||||||
GROUP BY "学员队编号", "年度", "学期第次"
|
GROUP BY "学员队编号", "年度", "学期第次"
|
||||||
) xq ON xq."学员队编号" = t."学员队编号"
|
) xq ON xq."学员队编号" = t."学员队编号"
|
||||||
AND xq."年度" = t."年度"
|
AND (CASE WHEN xq."年度" >= 100000 THEN xq."年度"
|
||||||
|
ELSE xq."年度" * 100 + xq."学期第次" END) = t."年度"
|
||||||
AND xq."学期第次" = t."学期第次"
|
AND xq."学期第次" = t."学期第次"
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT "科目编号", "教员编号", "年度",
|
SELECT "科目编号", "教员编号", "年度",
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.roomroot.jwgl.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.roomroot.jwgl.entity.JYL;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教员历 Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface JYLMapper extends BaseMapper<JYL> {
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import com.roomroot.jwgl.vo.jys.TeachingOfficeBookCourseVO;
|
|||||||
import com.roomroot.jwgl.vo.jys.TeachingOfficeBookVO;
|
import com.roomroot.jwgl.vo.jys.TeachingOfficeBookVO;
|
||||||
import com.roomroot.jwgl.vo.jys.TeachingTaskSquadVO;
|
import com.roomroot.jwgl.vo.jys.TeachingTaskSquadVO;
|
||||||
import com.roomroot.jwgl.vo.jys.TeachingTaskVO;
|
import com.roomroot.jwgl.vo.jys.TeachingTaskVO;
|
||||||
|
import com.roomroot.jwgl.vo.taskbook.TaskBookRowVO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
@@ -54,4 +55,10 @@ public interface TeachingTaskMapper extends BaseMapper<JXRW> {
|
|||||||
*/
|
*/
|
||||||
List<TeachingOfficeBookCourseVO> selectOfficeBookCourses(@Param("jxrwbh") String jxrwbh,
|
List<TeachingOfficeBookCourseVO> selectOfficeBookCourses(@Param("jxrwbh") String jxrwbh,
|
||||||
@Param("jysdh") String jysdh);
|
@Param("jysdh") String jysdh);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教研室任务书填报列表(阶段 4):某教学任务下全部课程任务行,
|
||||||
|
* 排序 课程 → 责任教员 → 课次,含合班分组号(编组)与教员/场地/教研室联查。
|
||||||
|
*/
|
||||||
|
List<TaskBookRowVO> selectTaskBookRows(@Param("jxrwbh") String jxrwbh);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,4 +228,41 @@
|
|||||||
ORDER BY d."学员队名称", k."课名称"
|
ORDER BY d."学员队名称", k."课名称"
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectTaskBookRows" resultType="com.roomroot.jwgl.vo.taskbook.TaskBookRowVO">
|
||||||
|
SELECT
|
||||||
|
r."编号" AS bh,
|
||||||
|
r."学员队学期编号" AS xydxqbh,
|
||||||
|
r."课编号" AS kbh,
|
||||||
|
NVL(NULLIF(r."简称", ''), k."课名称") AS kcmc,
|
||||||
|
r."课类型" AS klx,
|
||||||
|
r."学时" AS xs,
|
||||||
|
r."周课时" AS zks,
|
||||||
|
r."成绩分制" AS cjfz,
|
||||||
|
r."学员队编号" AS xydbh,
|
||||||
|
d."学员队名称" AS xydmc,
|
||||||
|
r."教员编号" AS jybh,
|
||||||
|
jy."教员姓名" AS jyxm,
|
||||||
|
r."教室编号" AS jsbh,
|
||||||
|
js."教室名称" AS jsmc,
|
||||||
|
r."课次序号" AS kcxh,
|
||||||
|
r."编组" AS bz2,
|
||||||
|
r."教研室代号" AS jysdh,
|
||||||
|
jys."教研室名称" AS jysmc,
|
||||||
|
r."教研室计划教员编号" AS jysjhjybh,
|
||||||
|
jy2."教员姓名" AS jysjhjyxm,
|
||||||
|
r."教研室计划备注" AS jysjhbz
|
||||||
|
FROM "学员队任务表" r
|
||||||
|
INNER JOIN "学员队年度学期基本信息表" s ON s."编号" = r."学员队学期编号"
|
||||||
|
LEFT JOIN "课表" k ON k."课编号" = r."课编号"
|
||||||
|
LEFT JOIN "学员队表" d ON d."学员队编号" = r."学员队编号"
|
||||||
|
LEFT JOIN "教员表" jy ON jy."教员编号" = r."教员编号"
|
||||||
|
LEFT JOIN "教员表" jy2 ON jy2."教员编号" = r."教研室计划教员编号"
|
||||||
|
LEFT JOIN "教室表" js ON js."教室编号" = r."教室编号"
|
||||||
|
LEFT JOIN "教研室表" jys ON jys."教研室代号" = r."教研室代号"
|
||||||
|
WHERE s."教学任务编号" = #{jxrwbh}
|
||||||
|
AND NVL(r."DEL_FLAG", 0) = 0
|
||||||
|
AND NVL(s."DEL_FLAG", 0) = 0
|
||||||
|
ORDER BY r."课编号", r."教员编号" NULLS LAST, r."课次序号" NULLS LAST, r."编号"
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.roomroot.jwgl.service;
|
||||||
|
|
||||||
|
import com.roomroot.jwgl.entity.JYL;
|
||||||
|
import com.roomroot.jwgl.unit.PageQuery;
|
||||||
|
import com.roomroot.jwgl.unit.PageResult;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教员历 Service(阶段 5.1)。排课冲突除教员双占外,同时读教员历不可排课格。
|
||||||
|
*/
|
||||||
|
public interface JYLService {
|
||||||
|
|
||||||
|
void add(JYL row);
|
||||||
|
|
||||||
|
void delete(String bh);
|
||||||
|
|
||||||
|
void update(JYL row);
|
||||||
|
|
||||||
|
JYL getById(String bh);
|
||||||
|
|
||||||
|
PageResult<JYL> pageList(PageQuery query, JYL condition);
|
||||||
|
|
||||||
|
List<JYL> list(JYL condition);
|
||||||
|
|
||||||
|
List<JYL> listByJybh(String jybh);
|
||||||
|
|
||||||
|
List<JYL> listByRq(String rq);
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package com.roomroot.jwgl.service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行课表批量发布/撤回(阶段 5.2)。
|
||||||
|
*
|
||||||
|
* <p>发布规则:班次下全部课程 责任教员、默认场地均已指定,否则整批失败并列出缺项;
|
||||||
|
* 只写 实施_课程 和 实施_课程学员队,不插 实施_课程表。
|
||||||
|
* 停用 convertToLessons 作为发布路径(不生成空壳课次)。</p>
|
||||||
|
*/
|
||||||
|
public interface RunningCoursePublishService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量发布某班次学期的全部课程任务为运行课程。
|
||||||
|
*
|
||||||
|
* @return 发布的 实施_课程 条数
|
||||||
|
*/
|
||||||
|
int publish(String xydxqbh);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤回:删除该班次已发布的 实施_课程 及其课次、教室、教员关联。
|
||||||
|
*
|
||||||
|
* @return 删除的 实施_课程 条数
|
||||||
|
*/
|
||||||
|
int withdraw(String xydxqbh);
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package com.roomroot.jwgl.service;
|
||||||
|
|
||||||
|
import com.roomroot.jwgl.dto.scheduling.SchedulingArrangeRequest;
|
||||||
|
import com.roomroot.jwgl.dto.scheduling.SchedulingCellOpRequest;
|
||||||
|
import com.roomroot.jwgl.vo.scheduling.SchedulingViewVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实施排课窗口(阶段 5.3)。
|
||||||
|
*
|
||||||
|
* <p>组合查询 + 写操作:安排所选节次 / 删除所选节次 / 删除课程全部节次 / 彻底删除运行课程。
|
||||||
|
* 硬冲突格忽略(双占、三类历不可排);软提示(配当周次不符、非正课)随返回值给出。</p>
|
||||||
|
*/
|
||||||
|
public interface SchedulingWindowService {
|
||||||
|
|
||||||
|
/** 排课窗组合视图:班次头 + 课程列表(排满标绿)+ 周次×星期×节次格子 */
|
||||||
|
SchedulingViewVO view(String xydxqbh);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排所选节次。硬冲突格忽略并列出,软提示格照常安排并给出提示。
|
||||||
|
*
|
||||||
|
* @return {arranged: 安排格数, ignored: [{rq,jc,reason}], warnings: [{rq,jc,reason}]}
|
||||||
|
*/
|
||||||
|
Map<String, Object> arrange(SchedulingArrangeRequest request);
|
||||||
|
|
||||||
|
/** 删除所选节次:仅删除该课程在这些格上的课次 */
|
||||||
|
int deleteCells(SchedulingCellOpRequest request);
|
||||||
|
|
||||||
|
/** 删除课程全部节次(课程仍留在列表) */
|
||||||
|
int clearCourse(String sskcbh);
|
||||||
|
|
||||||
|
/** 彻底删除运行课程:课次与列表项一起删,编制侧任务保留 */
|
||||||
|
int deleteCourse(String sskcbh);
|
||||||
|
|
||||||
|
/** 排课日志查询:按课程(可叠加操作类型) */
|
||||||
|
List<Map<String, Object>> logs(String sskcbh, String czlx);
|
||||||
|
}
|
||||||
+8
-6
@@ -92,17 +92,19 @@ public interface StudentTeamTaskService {
|
|||||||
/**
|
/**
|
||||||
* 自动生成必修课程
|
* 自动生成必修课程
|
||||||
* <p>
|
* <p>
|
||||||
* 根据学员队编号从学员队表获取专业代号,
|
* 按「学员队学期编号」定位唯一的目标班次学期,由该班次学期反查学员队编号与学期第次,
|
||||||
* 再根据专业代号和学期第次查询专业教学计划表,
|
* 再按专业代号 + 学期第次查专业教学计划表补齐必修课程。
|
||||||
* 为当前班次自动生成必修课程,插入到学员队任务表。
|
|
||||||
* 已存在的课程(按课编号判断)不会重复添加。
|
* 已存在的课程(按课编号判断)不会重复添加。
|
||||||
* </p>
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* 不再用「学员队编号 + 学期第次」定位:同一学员队在多个年度会有学期第次相同的班次学期
|
||||||
|
* (例如 2025 第3学期 与 2026 第3学期),按 (学员队, 学期第次) 查找会写错学年。
|
||||||
|
* </p>
|
||||||
*
|
*
|
||||||
* @param xydbh 学员队编号
|
* @param xydxqbh 学员队学期编号(班次学期主键)
|
||||||
* @param xqdc 学期第次
|
|
||||||
* @return 新增数量
|
* @return 新增数量
|
||||||
*/
|
*/
|
||||||
int autoGenerateRequiredCourses(String xydbh, Integer xqdc);
|
int autoGenerateRequiredCourses(String xydxqbh);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量复制课程
|
* 批量复制课程
|
||||||
|
|||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
package com.roomroot.jwgl.service;
|
||||||
|
|
||||||
|
import com.roomroot.jwgl.dto.taskbook.TaskBookBhListRequest;
|
||||||
|
import com.roomroot.jwgl.dto.taskbook.TaskBookFillRequest;
|
||||||
|
import com.roomroot.jwgl.dto.taskbook.TaskBookRoomRequest;
|
||||||
|
import com.roomroot.jwgl.dto.taskbook.TaskBookTeacherRequest;
|
||||||
|
import com.roomroot.jwgl.vo.taskbook.TaskBookRowVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教研室任务书填报(阶段 4)。
|
||||||
|
*
|
||||||
|
* <p>把已生成的课程任务确认成「可以排课的运行对象」:合班、责任教员、默认场地。</p>
|
||||||
|
*
|
||||||
|
* <p>口径约定:
|
||||||
|
* <ul>
|
||||||
|
* <li>合班分组号用已有列 学员队任务表.编组(bz2),0=未合班;不复用 预设编班号 和 课次序号。</li>
|
||||||
|
* <li>发布门禁:仅「已发布且未结束」的教学任务可填报;结束后只读(TeachingTaskWriteGuard)。</li>
|
||||||
|
* <li>责任教员 / 场地修改时,同合班组(同编组,非 0)的行同步。</li>
|
||||||
|
* <li>已发布到运行课表的行须先撤回才能拆班(运行课表属阶段 5,当前先拦截有排课数据的行)。</li>
|
||||||
|
* </ul></p>
|
||||||
|
*/
|
||||||
|
public interface TaskBookFillService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务书填报列表:某教学任务下全部课程任务行(排序 课程 → 责任教员 → 课次)。
|
||||||
|
*/
|
||||||
|
List<TaskBookRowVO> list(String jxrwbh);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动合班:所选行写入同一编组(新组号 = 现有最大编组 + 1)。
|
||||||
|
* 校验 科目、学时、课类型、成绩分制 相同,任一不同则拒绝并说明。
|
||||||
|
*
|
||||||
|
* @return 编组号
|
||||||
|
*/
|
||||||
|
Integer merge(TaskBookBhListRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按预设合班:所选行按课程科目分组,组内按 班次学期.预设编班号 写入同一编组;
|
||||||
|
* 没有预设编班号的行各成一组(不与别人强并)。
|
||||||
|
*
|
||||||
|
* @return 每组合班结果:groupNo / kbh / ysbbh / 行数
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> mergeByPreset(TaskBookBhListRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拆班:所选行编组置 0。
|
||||||
|
*
|
||||||
|
* @return 实际拆开行数
|
||||||
|
*/
|
||||||
|
int split(TaskBookBhListRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 责任教员指定(unit=责任单位教员 / academy=全院教员 / plan=应用教研室计划教员)。
|
||||||
|
* 该行编组非 0 时,同合班组其它行同步 jybh。
|
||||||
|
*
|
||||||
|
* @return 实际更新行数
|
||||||
|
*/
|
||||||
|
int setTeacher(TaskBookTeacherRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场地指定:其它教室(useSpecial=false,jsbh 必传)或 班次专用教室(useSpecial=true)。
|
||||||
|
* 该行编组非 0 时,同合班组其它行同步 jsbh。
|
||||||
|
*
|
||||||
|
* @return 实际更新行数
|
||||||
|
*/
|
||||||
|
int setRoom(TaskBookRoomRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填报字段:计划教员(jysjhjybh)、场地(jsbh,同合班同步)、排课建议(jysjhbz)。
|
||||||
|
*
|
||||||
|
* @return 实际更新行数
|
||||||
|
*/
|
||||||
|
int fill(TaskBookFillRequest request);
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
package com.roomroot.jwgl.service;
|
||||||
|
|
||||||
|
import com.roomroot.jwgl.dto.allocation.AllocationGroupRequest;
|
||||||
|
import com.roomroot.jwgl.dto.allocation.AllocationSaveItem;
|
||||||
|
import com.roomroot.jwgl.vo.allocation.AllocationViewVO;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教学配当(阶段 3)。
|
||||||
|
*
|
||||||
|
* <p>给出「哪一周上多少学时、谁和谁同开同结」,作为排课的粗约束;
|
||||||
|
* 不直接写实施课程表(SSKCB)日期节次。</p>
|
||||||
|
*/
|
||||||
|
public interface TeachingAllocationService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开某班次学期的配当编辑器视图:周轴(每周可排正课)+ 任务行(含铺学时结果)。
|
||||||
|
*/
|
||||||
|
AllocationViewVO view(String xydxqbh);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存配档字段:单条(列表 1 个元素)或多选批量。
|
||||||
|
* 元素 saveGroup=true 时按「同科目+同配档编组+同学期代号」整组一起改(同开同结)。
|
||||||
|
*
|
||||||
|
* @return 实际更新的任务行数
|
||||||
|
*/
|
||||||
|
int save(List<AllocationSaveItem> items);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优选序数上移/下移。先把该班次学期的配档序号按当前展示顺序归一化为 1..n,
|
||||||
|
* 再交换相邻两条。direction: up / down。
|
||||||
|
*
|
||||||
|
* @return 归一化后的任务编号顺序(供前端刷新)
|
||||||
|
*/
|
||||||
|
List<String> move(String xydxqbh, String bh, String direction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编组管理面板:同科目全部开课班次(跨班次学期)。
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> groups(String kbh);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编组管理:把所选任务统一改编组号。
|
||||||
|
*
|
||||||
|
* @return 实际更新行数
|
||||||
|
*/
|
||||||
|
int setGroup(AllocationGroupRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出配当 Excel(批量:每行一个任务,含班次名称与学期代号列)。
|
||||||
|
*/
|
||||||
|
void exportExcel(List<String> xydxqbhList, HttpServletResponse response) throws Exception;
|
||||||
|
}
|
||||||
+3
-1
@@ -241,8 +241,10 @@ public class ClassEventCalendarServiceImpl implements ClassEventCalendarService
|
|||||||
.eq(JQB::getJqsj, source.getJqsj())
|
.eq(JQB::getJqsj, source.getJqsj())
|
||||||
.eq(JQB::getCourseClass, source.getCourseClass()));
|
.eq(JQB::getCourseClass, source.getCourseClass()));
|
||||||
if (matched.isEmpty()) {
|
if (matched.isEmpty()) {
|
||||||
|
// 目标班次该格没有记录时按源格新建。可排课必须沿用源格:
|
||||||
|
// 之前固定传 0 会把「可排课」丢掉,使目标班次凭空多出不可排的格子。
|
||||||
insertCell(target, day, source.getCourseClass(), source.getJqmc(),
|
insertCell(target, day, source.getCourseClass(), source.getJqmc(),
|
||||||
source.getJc(), 0, source.getBzxs(), source.getZdpk(), source.getZk(), source.getBz());
|
source.getJc(), source.getKpk(), source.getBzxs(), source.getZdpk(), source.getZk(), source.getBz());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
for (JQB row : matched) {
|
for (JQB row : matched) {
|
||||||
|
|||||||
+6
-16
@@ -371,27 +371,17 @@ public class CourseRunningImportServiceImpl implements CourseRunningImportServic
|
|||||||
// 步骤2:遍历课程列表,逐个处理导入
|
// 步骤2:遍历课程列表,逐个处理导入
|
||||||
for (SSKC course : courseList) {
|
for (SSKC course : courseList) {
|
||||||
try {
|
try {
|
||||||
// 步骤3:将课程转换为课次列表(根据总学时计算课次数)
|
// 阶段 5.2 停用 convertToLessons:导入不再自动生成课次(实施_课程表),
|
||||||
List<SSKCB> lessonList = convertToLessons(course, nd);
|
// 课次改由排课窗口按节次安排(SchedulingWindowService.arrange)。
|
||||||
|
// 实施_课程 与 实施_课程学员队 数据在导入前已存在,此处仅记录导入日志。
|
||||||
|
|
||||||
// 步骤4:遍历课次列表,写入数据库
|
// 步骤3:记录导入操作日志
|
||||||
for (SSKCB lesson : lessonList) {
|
|
||||||
// 写入实施_课程表主记录
|
|
||||||
sskcbMapper.insert(lesson);
|
|
||||||
|
|
||||||
// 写入关联数据:学员队、教室、教员
|
|
||||||
insertSSKCBXYD(lesson, course.getBh(), nd);
|
|
||||||
insertSSKCBJS(lesson, nd);
|
|
||||||
insertSSKCBFZJY(lesson, nd);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 步骤5:记录导入操作日志
|
|
||||||
recordOperationLog(course.getBh(), "导入", "导入课程:" + course.getKmbh());
|
recordOperationLog(course.getBh(), "导入", "导入课程:" + course.getKmbh());
|
||||||
|
|
||||||
// 步骤6:更新成功计数
|
// 步骤4:更新成功计数
|
||||||
successCount++;
|
successCount++;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 步骤7:记录失败的课程编号
|
// 步骤5:记录失败的课程编号
|
||||||
failedList.add(course.getBh());
|
failedList.add(course.getBh());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-2
@@ -26,6 +26,7 @@ import com.roomroot.jwgl.mapper.XYDQBMapper;
|
|||||||
import com.roomroot.jwgl.service.ElectiveCourseService;
|
import com.roomroot.jwgl.service.ElectiveCourseService;
|
||||||
import com.roomroot.jwgl.unit.PageQuery;
|
import com.roomroot.jwgl.unit.PageQuery;
|
||||||
import com.roomroot.jwgl.unit.PageResult;
|
import com.roomroot.jwgl.unit.PageResult;
|
||||||
|
import com.roomroot.jwgl.utils.SemesterCodeUtil;
|
||||||
import com.roomroot.jwgl.utils.UuidUtil;
|
import com.roomroot.jwgl.utils.UuidUtil;
|
||||||
import com.roomroot.jwgl.vo.jys.ElectiveCourseStudentVO;
|
import com.roomroot.jwgl.vo.jys.ElectiveCourseStudentVO;
|
||||||
import com.roomroot.jwgl.vo.jys.ElectiveCourseVO;
|
import com.roomroot.jwgl.vo.jys.ElectiveCourseVO;
|
||||||
@@ -106,7 +107,10 @@ public class ElectiveCourseServiceImpl implements ElectiveCourseService {
|
|||||||
XYDRWB task = new XYDRWB();
|
XYDRWB task = new XYDRWB();
|
||||||
task.setBh(UuidUtil.getOriginalUUID());
|
task.setBh(UuidUtil.getOriginalUUID());
|
||||||
task.setDelFlag(0);
|
task.setDelFlag(0);
|
||||||
task.setNd(dto.getNd() != null ? dto.getNd() : LocalDate.now().getYear());
|
// 任务表/实施/课程表的「年度」口径是 6 位学期代号(见 SemesterCodeUtil 类注释);
|
||||||
|
// 前端不传 nd 时按当前日期推断春/夏/秋学期,避免落成 4 位年份导致年度连接断裂
|
||||||
|
task.setNd(dto.getNd() != null ? dto.getNd()
|
||||||
|
: SemesterCodeUtil.resolve(LocalDate.now().getYear(), SemesterCodeUtil.periodOf(LocalDate.now())));
|
||||||
task.setXydbh(xydbh);
|
task.setXydbh(xydbh);
|
||||||
task.setKbh(course.getKbh());
|
task.setKbh(course.getKbh());
|
||||||
task.setXqdc(dto.getXqdc());
|
task.setXqdc(dto.getXqdc());
|
||||||
@@ -245,7 +249,7 @@ public class ElectiveCourseServiceImpl implements ElectiveCourseService {
|
|||||||
XYDRWB task = new XYDRWB();
|
XYDRWB task = new XYDRWB();
|
||||||
task.setBh(UuidUtil.getOriginalUUID());
|
task.setBh(UuidUtil.getOriginalUUID());
|
||||||
task.setDelFlag(0);
|
task.setDelFlag(0);
|
||||||
task.setNd(LocalDate.now().getYear());
|
task.setNd(SemesterCodeUtil.resolve(LocalDate.now().getYear(), SemesterCodeUtil.periodOf(LocalDate.now())));
|
||||||
task.setXydbh(xydbh);
|
task.setXydbh(xydbh);
|
||||||
task.setKbh(course.getKbh());
|
task.setKbh(course.getKbh());
|
||||||
task.setJybh(jybh);
|
task.setJybh(jybh);
|
||||||
|
|||||||
+16
-29
@@ -12,6 +12,7 @@ import com.roomroot.jwgl.service.ElectiveService;
|
|||||||
import com.roomroot.jwgl.unit.BusinessException;
|
import com.roomroot.jwgl.unit.BusinessException;
|
||||||
import com.roomroot.jwgl.unit.PageQuery;
|
import com.roomroot.jwgl.unit.PageQuery;
|
||||||
import com.roomroot.jwgl.unit.PageResult;
|
import com.roomroot.jwgl.unit.PageResult;
|
||||||
|
import com.roomroot.jwgl.utils.SemesterCodeUtil;
|
||||||
import com.roomroot.jwgl.utils.UuidUtil;
|
import com.roomroot.jwgl.utils.UuidUtil;
|
||||||
import com.roomroot.jwgl.vo.elective.*;
|
import com.roomroot.jwgl.vo.elective.*;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -238,11 +239,16 @@ public class ElectiveServiceImpl implements ElectiveService {
|
|||||||
// 判断学制是否半年以内
|
// 判断学制是否半年以内
|
||||||
boolean halfYear = isHalfYear(rx, by);
|
boolean halfYear = isHalfYear(rx, by);
|
||||||
|
|
||||||
// 学期第次:半年以内固定第一学期;半年以上优先用窗口已选学期第次,否则按入学日期推断
|
// 学期第次:半年以内固定第一学期;半年以上优先用窗口已选学期第次,
|
||||||
|
// 未选/非法时按开学日期推断「学年内学期类别」(1 春季 / 2 夏季 / 3 秋季)。
|
||||||
|
// 注意:学期第次不是「专业总第几学期」——人培(专业教学计划表)的学期第次才是专业总学期口径,
|
||||||
|
// 不能把按半年推算出的 4~8 写进班次学期,否则学期代号(年度×100+学期第次)会越过 1/2/3 契约。
|
||||||
if (halfYear) {
|
if (halfYear) {
|
||||||
xqdc = 1;
|
xqdc = 1;
|
||||||
} else if (xqdc == null || xqdc < 1 || xqdc > 3) {
|
} else if (xqdc == null || xqdc < 1 || xqdc > 3) {
|
||||||
xqdc = inferXqdc(rx, by, LocalDate.now());
|
LocalDate preset = parseDate(presetKxrq);
|
||||||
|
LocalDate ref = preset != null ? preset : (rx != null ? rx : LocalDate.now());
|
||||||
|
xqdc = SemesterCodeUtil.periodOf(ref);
|
||||||
}
|
}
|
||||||
// 人才培养方案总学期数(半年一学期,向上取整)
|
// 人才培养方案总学期数(半年一学期,向上取整)
|
||||||
Integer totalXqdc = calcTotalXqdc(rx, by);
|
Integer totalXqdc = calcTotalXqdc(rx, by);
|
||||||
@@ -294,7 +300,10 @@ public class ElectiveServiceImpl implements ElectiveService {
|
|||||||
LocalDate by = clazz.getByrq() != null ? clazz.getByrq().toLocalDate() : null;
|
LocalDate by = clazz.getByrq() != null ? clazz.getByrq().toLocalDate() : null;
|
||||||
boolean halfYear = isHalfYear(rx, by);
|
boolean halfYear = isHalfYear(rx, by);
|
||||||
|
|
||||||
// 学期第次:半年以内只能第一学期;半年以上为空时智能推断
|
// 学期第次:半年以内只能第一学期;半年以上为空时按开学日期推断学年内学期类别,
|
||||||
|
// 传入值必须是 1/2/3(与 ClassSemesterServiceImpl.assertXqdc 同一契约)。
|
||||||
|
// 注意:学期第次是学年内学期类别(1 春季 / 2 夏季 / 3 秋季),
|
||||||
|
// 不能用「半年一学期」推算出的专业总学期(可达 6~8)写入,否则会生成非法学期代号。
|
||||||
Integer xqdc = xydndxqjbxxb.getXqdc();
|
Integer xqdc = xydndxqjbxxb.getXqdc();
|
||||||
if (halfYear) {
|
if (halfYear) {
|
||||||
if (xqdc != null && xqdc != 1) {
|
if (xqdc != null && xqdc != 1) {
|
||||||
@@ -302,7 +311,10 @@ public class ElectiveServiceImpl implements ElectiveService {
|
|||||||
}
|
}
|
||||||
xqdc = 1;
|
xqdc = 1;
|
||||||
} else if (xqdc == null) {
|
} else if (xqdc == null) {
|
||||||
xqdc = inferXqdc(rx, by, LocalDate.now());
|
LocalDate ref = xydndxqjbxxb.getKxrq() != null ? xydndxqjbxxb.getKxrq() : rx;
|
||||||
|
xqdc = SemesterCodeUtil.periodOf(ref);
|
||||||
|
} else if (xqdc < 1 || xqdc > 3) {
|
||||||
|
throw new BusinessException("学期第次只能是 1(春季)、2(夏季)、3(秋季),当前传入:" + xqdc);
|
||||||
}
|
}
|
||||||
xydndxqjbxxb.setXqdc(xqdc);
|
xydndxqjbxxb.setXqdc(xqdc);
|
||||||
|
|
||||||
@@ -382,31 +394,6 @@ public class ElectiveServiceImpl implements ElectiveService {
|
|||||||
return Math.max(1, (int) ((totalMonths + 5) / 6));
|
return Math.max(1, (int) ((totalMonths + 5) / 6));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据班次入学日期智能推断指定日期所在的学期第次(每半年为一个学期)
|
|
||||||
* <p>
|
|
||||||
* 半年以内的班次返回 1;半年以上的班次按 入学日期 到 指定日期 经过的月份
|
|
||||||
* 每 6 个月为 1 个学期递增,且不超过人才培养方案总学期数。
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
private Integer inferXqdc(LocalDate rx, LocalDate by, LocalDate target) {
|
|
||||||
if (rx == null) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (target.isBefore(rx)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
// 半年以内固定第一学期
|
|
||||||
if (by != null && isHalfYear(rx, by)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
long months = ChronoUnit.MONTHS.between(rx, target);
|
|
||||||
int xqdc = (int) (months / 6) + 1;
|
|
||||||
// 不超过总学期数
|
|
||||||
Integer total = calcTotalXqdc(rx, by);
|
|
||||||
return Math.min(xqdc, total);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取某班次指定学期第次的上一学期专用教室(用于第二以上学期的默认预设)
|
* 获取某班次指定学期第次的上一学期专用教室(用于第二以上学期的默认预设)
|
||||||
*
|
*
|
||||||
|
|||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
package com.roomroot.jwgl.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.roomroot.common.utils.StringUtils;
|
||||||
|
import com.roomroot.jwgl.entity.JYL;
|
||||||
|
import com.roomroot.jwgl.mapper.JYLMapper;
|
||||||
|
import com.roomroot.jwgl.service.JYLService;
|
||||||
|
import com.roomroot.jwgl.unit.PageQuery;
|
||||||
|
import com.roomroot.jwgl.unit.PageResult;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教员历 Service 实现。交互口径与教学场地历一致:新增生成编号 + delFlag=0,删除为软删。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class JYLServiceImpl implements JYLService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JYLMapper jylMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(JYL row) {
|
||||||
|
if (row.getJybh() == null || row.getJybh().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("请指定教员");
|
||||||
|
}
|
||||||
|
row.setBh(UUID.randomUUID().toString());
|
||||||
|
if (row.getKpk() == null) {
|
||||||
|
row.setKpk(0);
|
||||||
|
}
|
||||||
|
row.setCjsj(LocalDateTime.now());
|
||||||
|
row.setDelFlag(0);
|
||||||
|
jylMapper.insert(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(String bh) {
|
||||||
|
JYL row = jylMapper.selectById(bh);
|
||||||
|
if (row != null) {
|
||||||
|
row.setDelFlag(1);
|
||||||
|
jylMapper.updateById(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(JYL row) {
|
||||||
|
row.setDelFlag(null);
|
||||||
|
jylMapper.updateById(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JYL getById(String bh) {
|
||||||
|
return jylMapper.selectById(bh);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<JYL> pageList(PageQuery query, JYL condition) {
|
||||||
|
PageQuery q = query == null ? new PageQuery() : query;
|
||||||
|
int pageNum = q.getPageNum() == null ? 1 : q.getPageNum();
|
||||||
|
int pageSize = q.getPageSize() == null ? 10 : q.getPageSize();
|
||||||
|
com.baomidou.mybatisplus.extension.plugins.pagination.Page<JYL> page =
|
||||||
|
new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(pageNum, pageSize);
|
||||||
|
jylMapper.selectPage(page, buildWrapper(condition));
|
||||||
|
return new PageResult<>(page.getRecords(), page.getTotal(), pageNum, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<JYL> list(JYL condition) {
|
||||||
|
return jylMapper.selectList(buildWrapper(condition));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<JYL> listByJybh(String jybh) {
|
||||||
|
return jylMapper.selectList(new LambdaQueryWrapper<JYL>()
|
||||||
|
.eq(JYL::getJybh, jybh)
|
||||||
|
.eq(JYL::getDelFlag, 0)
|
||||||
|
.orderByAsc(JYL::getRq)
|
||||||
|
.orderByAsc(JYL::getJc));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<JYL> listByRq(String rq) {
|
||||||
|
return jylMapper.selectList(new LambdaQueryWrapper<JYL>()
|
||||||
|
.apply("DATE(rq) = {0}", rq)
|
||||||
|
.eq(JYL::getDelFlag, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LambdaQueryWrapper<JYL> buildWrapper(JYL condition) {
|
||||||
|
LambdaQueryWrapper<JYL> qw = new LambdaQueryWrapper<>();
|
||||||
|
qw.eq(JYL::getDelFlag, 0);
|
||||||
|
if (condition != null) {
|
||||||
|
if (StringUtils.isNotEmpty(condition.getJybh())) {
|
||||||
|
qw.eq(JYL::getJybh, condition.getJybh());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(condition.getMc())) {
|
||||||
|
qw.like(JYL::getMc, condition.getMc());
|
||||||
|
}
|
||||||
|
if (condition.getKpk() != null) {
|
||||||
|
qw.eq(JYL::getKpk, condition.getKpk());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qw.orderByAsc(JYL::getRq).orderByAsc(JYL::getJc);
|
||||||
|
return qw;
|
||||||
|
}
|
||||||
|
}
|
||||||
+255
@@ -0,0 +1,255 @@
|
|||||||
|
package com.roomroot.jwgl.service.impl;
|
||||||
|
|
||||||
|
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.SSKC;
|
||||||
|
import com.roomroot.jwgl.entity.SSKCB;
|
||||||
|
import com.roomroot.jwgl.entity.SSKCBFZJY;
|
||||||
|
import com.roomroot.jwgl.entity.SSKCBJS;
|
||||||
|
import com.roomroot.jwgl.entity.SSKCBXYD;
|
||||||
|
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.mapper.ClassSemesterMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCBFZJYMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCBJSMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCBMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCBXYDMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCXYDMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.TeachingTaskMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.XYDBMapper;
|
||||||
|
import com.roomroot.jwgl.service.RunningCoursePublishService;
|
||||||
|
import com.roomroot.jwgl.utils.SemesterCodeUtil;
|
||||||
|
import com.roomroot.jwgl.utils.TeachingTaskStatus;
|
||||||
|
import com.roomroot.jwgl.utils.UuidUtil;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST;
|
||||||
|
import static com.roomroot.jwgl.utils.BasicManagementConstants.NOT_FOUND;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行课表批量发布/撤回实现(阶段 5.2)。
|
||||||
|
*
|
||||||
|
* <p>发布 = 班次学期的课程任务(学员队任务表)按 合班组(编组 bz2)聚合写成
|
||||||
|
* 实施_课程 + 实施_课程学员队;不生成 实施_课程表 空壳课次。
|
||||||
|
* 课次由阶段 5.3 排课窗逐格安排。</p>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class RunningCoursePublishServiceImpl implements RunningCoursePublishService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ClassSemesterMapper classSemesterMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.roomroot.jwgl.mapper.StudentTeamTaskMapper studentTeamTaskMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TeachingTaskMapper jxrwMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private XYDBMapper xydbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCMapper sskcMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCXYDMapper sskcxydMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCBMapper sskcbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCBXYDMapper sskcbxydMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCBJSMapper sskcbjsMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCBFZJYMapper sskcbfzjyMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public int publish(String xydxqbh) {
|
||||||
|
XYDNDXQJBXXB semester = requireSemester(xydxqbh);
|
||||||
|
Integer ndCode = SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc());
|
||||||
|
XYDB team = xydbMapper.selectById(semester.getXydbh());
|
||||||
|
|
||||||
|
// 门禁:任务书已发布且未结束
|
||||||
|
assertTaskBookPublished(semester);
|
||||||
|
|
||||||
|
List<XYDRWB> tasks = studentTeamTaskMapper.selectList(new LambdaQueryWrapper<XYDRWB>()
|
||||||
|
.eq(XYDRWB::getXydxqbh, semester.getBh())
|
||||||
|
.eq(XYDRWB::getDelFlag, 0));
|
||||||
|
if (tasks.isEmpty()) {
|
||||||
|
throw new ServiceException("该班次学期没有课程任务可发布", NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 幂等:已有发布记录则拒绝
|
||||||
|
List<SSKCXYD> published = sskcxydMapper.selectList(new LambdaQueryWrapper<SSKCXYD>()
|
||||||
|
.eq(SSKCXYD::getXydbh, semester.getXydbh())
|
||||||
|
.eq(SSKCXYD::getNd, ndCode));
|
||||||
|
if (!published.isEmpty()) {
|
||||||
|
throw new ServiceException("该班次学期已发布 " + published.size() + " 条运行课程,请先撤回再重新发布", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 整批校验:责任教员、默认场地非空,缺项全部列出
|
||||||
|
List<String> missing = new ArrayList<>();
|
||||||
|
for (XYDRWB task : tasks) {
|
||||||
|
List<String> lack = new ArrayList<>();
|
||||||
|
if (task.getJybh() == null || task.getJybh().isEmpty()) {
|
||||||
|
lack.add("责任教员");
|
||||||
|
}
|
||||||
|
if (task.getJsbh() == null || task.getJsbh().isEmpty()) {
|
||||||
|
lack.add("默认场地");
|
||||||
|
}
|
||||||
|
if (!lack.isEmpty()) {
|
||||||
|
missing.add(task.getJc() + "(" + teamName(team) + ")缺 " + String.join("、", lack));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!missing.isEmpty()) {
|
||||||
|
throw new ServiceException("发布失败,以下课程缺项:" + String.join(";", missing), BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按合班组聚合:bz2 非 0 同组一条实施课程,未合班每任务一条
|
||||||
|
Map<String, List<XYDRWB>> groups = new LinkedHashMap<>();
|
||||||
|
for (XYDRWB task : tasks) {
|
||||||
|
String key = task.getBz2() != null && task.getBz2() != 0
|
||||||
|
? "G" + task.getBz2() + "|" + task.getKbh()
|
||||||
|
: "S" + task.getBh();
|
||||||
|
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
int count = 0;
|
||||||
|
for (List<XYDRWB> group : groups.values()) {
|
||||||
|
XYDRWB head = group.get(0);
|
||||||
|
SSKC sskc = new SSKC();
|
||||||
|
sskc.setBh(UuidUtil.getUUID());
|
||||||
|
sskc.setNd(ndCode);
|
||||||
|
sskc.setJybh(head.getJybh());
|
||||||
|
sskc.setKmbh(head.getKbh());
|
||||||
|
sskc.setJsbh(head.getJsbh());
|
||||||
|
sskc.setXs(head.getXs());
|
||||||
|
sskc.setKlx(head.getKlx());
|
||||||
|
sskc.setCjfz(head.getCjfz());
|
||||||
|
sskc.setZks(head.getZks());
|
||||||
|
sskc.setXf(head.getXf());
|
||||||
|
sskc.setLlxs(head.getLlxs());
|
||||||
|
sskc.setSjxs(head.getSjxs());
|
||||||
|
sskc.setBjrxypjf(head.getBjrxypjf());
|
||||||
|
sskc.setCjsj(now);
|
||||||
|
sskc.setBdsj(now);
|
||||||
|
sskcMapper.insert(sskc);
|
||||||
|
|
||||||
|
for (XYDRWB task : group) {
|
||||||
|
SSKCXYD link = new SSKCXYD();
|
||||||
|
link.setBh(UuidUtil.getUUID());
|
||||||
|
link.setSskcbh(sskc.getBh());
|
||||||
|
link.setXydbh(task.getXydbh());
|
||||||
|
link.setJc(team != null ? team.getJc() : null);
|
||||||
|
link.setRs(team != null ? team.getXydrs() : null);
|
||||||
|
link.setNd(ndCode);
|
||||||
|
link.setXqdc(semester.getXqdc());
|
||||||
|
link.setXf(task.getXf());
|
||||||
|
sskcxydMapper.insert(link);
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public int withdraw(String xydxqbh) {
|
||||||
|
XYDNDXQJBXXB semester = requireSemester(xydxqbh);
|
||||||
|
Integer ndCode = SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc());
|
||||||
|
|
||||||
|
// 该班次学期的发布关联
|
||||||
|
List<SSKCXYD> links = sskcxydMapper.selectList(new LambdaQueryWrapper<SSKCXYD>()
|
||||||
|
.eq(SSKCXYD::getXydbh, semester.getXydbh())
|
||||||
|
.eq(SSKCXYD::getNd, ndCode));
|
||||||
|
if (links.isEmpty()) {
|
||||||
|
throw new ServiceException("该班次学期没有已发布的运行课程", NOT_FOUND);
|
||||||
|
}
|
||||||
|
List<String> sskcBhs = links.stream().map(SSKCXYD::getSskcbh).distinct().toList();
|
||||||
|
|
||||||
|
int removed = 0;
|
||||||
|
for (String sskcbh : sskcBhs) {
|
||||||
|
// 删除该课程的课次及其教室/教员/学员队关联
|
||||||
|
List<SSKCB> lessons = sskcbMapper.selectList(new LambdaQueryWrapper<SSKCB>()
|
||||||
|
.eq(SSKCB::getSskcbh, sskcbh));
|
||||||
|
for (SSKCB lesson : lessons) {
|
||||||
|
sskcbxydMapper.delete(new LambdaQueryWrapper<SSKCBXYD>()
|
||||||
|
.eq(SSKCBXYD::getSskcbbh, lesson.getBh()));
|
||||||
|
sskcbjsMapper.delete(new LambdaQueryWrapper<SSKCBJS>()
|
||||||
|
.eq(SSKCBJS::getSskcbbh, lesson.getBh()));
|
||||||
|
sskcbfzjyMapper.delete(new LambdaQueryWrapper<SSKCBFZJY>()
|
||||||
|
.eq(SSKCBFZJY::getSskcbbh, lesson.getBh()));
|
||||||
|
}
|
||||||
|
sskcbMapper.delete(new LambdaQueryWrapper<SSKCB>()
|
||||||
|
.eq(SSKCB::getSskcbh, sskcbh));
|
||||||
|
// TODO(业务确认):课次已有调课/请假记录时应先确认,当前直接随撤回删除
|
||||||
|
|
||||||
|
// 删除本班次的关联行
|
||||||
|
sskcxydMapper.delete(new LambdaQueryWrapper<SSKCXYD>()
|
||||||
|
.eq(SSKCXYD::getSskcbh, sskcbh)
|
||||||
|
.eq(SSKCXYD::getXydbh, semester.getXydbh())
|
||||||
|
.eq(SSKCXYD::getNd, ndCode));
|
||||||
|
// 合班课程可能仍被其它班次引用:仅当不再有任何关联时删除 实施_课程 本体
|
||||||
|
Long remain = sskcxydMapper.selectCount(new LambdaQueryWrapper<SSKCXYD>()
|
||||||
|
.eq(SSKCXYD::getSskcbh, sskcbh));
|
||||||
|
if (remain == null || remain == 0) {
|
||||||
|
sskcMapper.deleteById(sskcbh);
|
||||||
|
}
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 公共 ====================
|
||||||
|
|
||||||
|
private XYDNDXQJBXXB requireSemester(String xydxqbh) {
|
||||||
|
if (xydxqbh == null || xydxqbh.isEmpty()) {
|
||||||
|
throw new ServiceException("请指定班次学期", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh);
|
||||||
|
if (semester == null) {
|
||||||
|
throw new ServiceException("班次学期不存在:" + xydxqbh, NOT_FOUND);
|
||||||
|
}
|
||||||
|
return semester;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertTaskBookPublished(XYDNDXQJBXXB semester) {
|
||||||
|
if (semester.getJxrwbh() == null || semester.getJxrwbh().isEmpty()) {
|
||||||
|
throw new ServiceException("该班次学期没有关联教学任务,不能发布", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
JXRW jxrw = jxrwMapper.selectById(semester.getJxrwbh());
|
||||||
|
if (jxrw == null || Integer.valueOf(1).equals(jxrw.getDelFlag())) {
|
||||||
|
throw new ServiceException("关联教学任务不存在,不能发布", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (!TeachingTaskStatus.isPublished(jxrw.getZt())) {
|
||||||
|
throw new ServiceException("教学任务尚未发布,不能发布运行课表", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (TeachingTaskStatus.isEnded(jxrw.getZt())) {
|
||||||
|
throw new ServiceException("教学任务已结束,不能发布运行课表", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String teamName(XYDB team) {
|
||||||
|
return team != null && team.getXydmc() != null ? team.getXydmc() : "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
+808
@@ -0,0 +1,808 @@
|
|||||||
|
package com.roomroot.jwgl.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.roomroot.common.exception.ServiceException;
|
||||||
|
import com.roomroot.common.utils.SecurityUtils;
|
||||||
|
import com.roomroot.jwgl.dto.scheduling.SchedulingArrangeRequest;
|
||||||
|
import com.roomroot.jwgl.dto.scheduling.SchedulingCellOpRequest;
|
||||||
|
import com.roomroot.jwgl.entity.JQB;
|
||||||
|
import com.roomroot.jwgl.entity.JYB;
|
||||||
|
import com.roomroot.jwgl.entity.JXCDL;
|
||||||
|
import com.roomroot.jwgl.entity.JYL;
|
||||||
|
import com.roomroot.jwgl.entity.KCJXYX_CZRZ;
|
||||||
|
import com.roomroot.jwgl.entity.SSKC;
|
||||||
|
import com.roomroot.jwgl.entity.SSKCB;
|
||||||
|
import com.roomroot.jwgl.entity.SSKCBFZJY;
|
||||||
|
import com.roomroot.jwgl.entity.SSKCBJS;
|
||||||
|
import com.roomroot.jwgl.entity.SSKCBXYD;
|
||||||
|
import com.roomroot.jwgl.entity.SSKCXYD;
|
||||||
|
import com.roomroot.jwgl.entity.XYDB;
|
||||||
|
import com.roomroot.jwgl.entity.XYDNDXQJBXXB;
|
||||||
|
import com.roomroot.jwgl.entity.XQXLB;
|
||||||
|
import com.roomroot.jwgl.mapper.XYDBMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.ClassRoomCalendarMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.ClassSemesterMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.JQBMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.JYBMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.JYLMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.KBMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.KCJXYX_CZRZMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCBFZJYMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCBJSMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCBMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCBXYDMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCXYDMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SemesterCalendarMapper;
|
||||||
|
import com.roomroot.jwgl.service.SchedulingWindowService;
|
||||||
|
import com.roomroot.jwgl.utils.JwglRoleHelper;
|
||||||
|
import com.roomroot.jwgl.utils.SemesterCodeUtil;
|
||||||
|
import com.roomroot.jwgl.utils.UuidUtil;
|
||||||
|
import com.roomroot.jwgl.vo.scheduling.SchedulingViewVO;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.DayOfWeek;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
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.FORBIDDEN;
|
||||||
|
import static com.roomroot.jwgl.utils.BasicManagementConstants.NOT_FOUND;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实施排课窗口实现(阶段 5.3)。
|
||||||
|
*
|
||||||
|
* <p>冲突口径:硬冲突(格忽略)= 教员/教室/班次双占、校历/班历/场地历/教员历不可排;
|
||||||
|
* 软提示(照常安排)= 非正课、配当周次不符。</p>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class SchedulingWindowServiceImpl implements SchedulingWindowService {
|
||||||
|
|
||||||
|
/** 默认一节 2 学时 */
|
||||||
|
private static final int HOURS_PER_LESSON = 2;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ClassSemesterMapper classSemesterMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KBMapper kbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.roomroot.jwgl.service.TeachingAllocationService teachingAllocationService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JYBMapper jybMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private XYDBMapper xydbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCMapper sskcMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCXYDMapper sskcxydMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCBMapper sskcbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCBXYDMapper sskcbxydMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCBJSMapper sskcbjsMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCBFZJYMapper sskcbfzjyMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SemesterCalendarMapper semesterCalendarMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JQBMapper jqbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ClassRoomCalendarMapper classRoomCalendarMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JYLMapper jylMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KCJXYX_CZRZMapper czrzMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JwglRoleHelper roleHelper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.roomroot.jwgl.mapper.StudentTeamTaskMapper studentTeamTaskMapper;
|
||||||
|
|
||||||
|
// ==================== 视图 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SchedulingViewVO view(String xydxqbh) {
|
||||||
|
Ctx ctx = loadCtx(xydxqbh);
|
||||||
|
SchedulingViewVO vo = new SchedulingViewVO();
|
||||||
|
vo.setXydxqbh(ctx.semester.getBh());
|
||||||
|
vo.setXydmc(ctx.team != null ? ctx.team.getXydmc() : ctx.semester.getXydbh());
|
||||||
|
vo.setZyjsbh(ctx.semester.getZyjsbh());
|
||||||
|
vo.setNdCode(ctx.ndCode);
|
||||||
|
vo.setKxrq(ctx.semester.getKxrq());
|
||||||
|
vo.setJsrq(ctx.semester.getJsrq());
|
||||||
|
|
||||||
|
// 课程列表(本班次已发布的运行课程)
|
||||||
|
List<SSKCXYD> links = ctx.links;
|
||||||
|
List<String> sskcIds = links.stream().map(SSKCXYD::getSskcbh).distinct().toList();
|
||||||
|
Map<String, SSKC> courses = sskcIds.isEmpty() ? Map.of()
|
||||||
|
: sskcMapper.selectBatchIds(sskcIds).stream().collect(Collectors.toMap(SSKC::getBh, c -> c, (a, b) -> a));
|
||||||
|
List<SSKCB> lessons = ctx.lessons;
|
||||||
|
Map<String, List<SSKCB>> lessonsByCourse = lessons.stream()
|
||||||
|
.collect(Collectors.groupingBy(SSKCB::getSskcbh));
|
||||||
|
for (String sskcbh : sskcIds) {
|
||||||
|
SSKC course = courses.get(sskcbh);
|
||||||
|
if (course == null) continue;
|
||||||
|
SchedulingViewVO.Course c = new SchedulingViewVO.Course();
|
||||||
|
c.setSskcbh(course.getBh());
|
||||||
|
c.setKbh(course.getKmbh());
|
||||||
|
c.setKcmc(resolveCourseName(course.getKmbh()));
|
||||||
|
c.setKlx(course.getKlx());
|
||||||
|
c.setXs(course.getXs());
|
||||||
|
c.setZks(course.getZks());
|
||||||
|
c.setJybh(course.getJybh());
|
||||||
|
c.setJsbh(course.getJsbh());
|
||||||
|
List<SSKCB> cl = lessonsByCourse.getOrDefault(sskcbh, List.of());
|
||||||
|
c.setScheduledHours(cl.stream().mapToInt(l -> HOURS_PER_LESSON + (l.getJcdj() == null ? 0 : l.getJcdj())).sum());
|
||||||
|
c.setFull(course.getXs() != null && c.getScheduledHours() >= course.getXs());
|
||||||
|
c.setEditable(canEdit(course, ctx));
|
||||||
|
vo.getCourses().add(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间格
|
||||||
|
buildGrid(ctx, vo);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 周次×星期×节次格子:叠加校历/班历不可排标记与已排课次 */
|
||||||
|
private void buildGrid(Ctx ctx, SchedulingViewVO vo) {
|
||||||
|
LocalDate kxrq = ctx.semester.getKxrq();
|
||||||
|
LocalDate jsrq = ctx.semester.getJsrq();
|
||||||
|
if (kxrq == null || jsrq == null) {
|
||||||
|
throw new ServiceException("班次学期缺少开学/结束日期,无法生成排课时间区", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
// 历表不可排数据(本学期代号范围内)
|
||||||
|
Set<String> schoolHard = new HashSet<>(); // 校历不可排 date#jc
|
||||||
|
Set<String> schoolSoft = new HashSet<>(); // 非正课
|
||||||
|
List<XQXLB> schoolRows = semesterCalendarMapper.selectList(new LambdaQueryWrapper<XQXLB>()
|
||||||
|
.eq(XQXLB::getNd, ctx.ndCode));
|
||||||
|
for (XQXLB row : schoolRows) {
|
||||||
|
if (row.getJqsj() == null) continue;
|
||||||
|
for (Integer jc : parsePeriods(row.getCourseClass())) {
|
||||||
|
String key = row.getJqsj() + "#" + jc;
|
||||||
|
if (Boolean.FALSE.equals(row.getKpk())) schoolHard.add(key);
|
||||||
|
else if (Boolean.FALSE.equals(row.getZk())) schoolSoft.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Set<String> classHard = new HashSet<>();
|
||||||
|
Set<String> classSoft = new HashSet<>();
|
||||||
|
List<JQB> classRows = jqbMapper.selectList(new LambdaQueryWrapper<JQB>()
|
||||||
|
.eq(JQB::getXydxqbh, ctx.semester.getBh()));
|
||||||
|
for (JQB row : classRows) {
|
||||||
|
if (row.getJqsj() == null) continue;
|
||||||
|
String date = intDate(row.getJqsj());
|
||||||
|
for (Integer jc : parsePeriods(row.getCourseClass())) {
|
||||||
|
String key = date + "#" + jc;
|
||||||
|
if (row.getKpk() != null && row.getKpk() == 0) classHard.add(key);
|
||||||
|
else if (row.getZk() != null && row.getZk() == 0) classSoft.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 已排课次
|
||||||
|
Map<String, List<SSKCB>> lessonsByGrid = ctx.lessons.stream()
|
||||||
|
.filter(l -> l.getRq() != null && l.getJc() != null)
|
||||||
|
.collect(Collectors.groupingBy(l -> l.getRq().toLocalDate().toString() + "#" + l.getJc()));
|
||||||
|
Map<String, String> courseNames = new HashMap<>();
|
||||||
|
Map<String, String> courseTeachers = new HashMap<>();
|
||||||
|
for (SSKC course : sskcMapper.selectBatchIds(ctx.links.stream().map(SSKCXYD::getSskcbh).distinct().toList())) {
|
||||||
|
courseNames.put(course.getBh(), resolveCourseName(course.getKmbh()));
|
||||||
|
courseTeachers.put(course.getBh(), safe(course.getJybh()));
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate weekStart = kxrq.with(DayOfWeek.MONDAY);
|
||||||
|
int weekNo = 1;
|
||||||
|
int[] periodCount = {8};
|
||||||
|
while (!weekStart.isAfter(jsrq)) {
|
||||||
|
SchedulingViewVO.Week week = new SchedulingViewVO.Week();
|
||||||
|
week.setWeekNo(weekNo);
|
||||||
|
week.setStartDate(weekStart);
|
||||||
|
week.setEndDate(weekStart.plusDays(6).isAfter(jsrq) ? jsrq : weekStart.plusDays(6));
|
||||||
|
for (int d = 0; d < 7; d++) {
|
||||||
|
LocalDate date = weekStart.plusDays(d);
|
||||||
|
if (date.isBefore(kxrq) || date.isAfter(jsrq)) continue;
|
||||||
|
SchedulingViewVO.Day day = new SchedulingViewVO.Day();
|
||||||
|
day.setDate(date);
|
||||||
|
day.setWeekday(date.getDayOfWeek().getValue());
|
||||||
|
for (int jc = 1; jc <= periodCount[0]; jc++) {
|
||||||
|
SchedulingViewVO.Cell cell = new SchedulingViewVO.Cell();
|
||||||
|
cell.setJc(jc);
|
||||||
|
String key = date.toString() + "#" + jc;
|
||||||
|
if (schoolHard.contains(key) || classHard.contains(key)) {
|
||||||
|
cell.setUnavailable(true);
|
||||||
|
cell.setReason("不可排课(" + (schoolHard.contains(key) ? "校历" : "班历") + ")");
|
||||||
|
} else {
|
||||||
|
if (schoolSoft.contains(key) || classSoft.contains(key)) {
|
||||||
|
cell.setWarning("非正课时段");
|
||||||
|
}
|
||||||
|
cell.setUnavailable(false);
|
||||||
|
}
|
||||||
|
for (SSKCB lesson : lessonsByGrid.getOrDefault(key, List.of())) {
|
||||||
|
SchedulingViewVO.Lesson l = new SchedulingViewVO.Lesson();
|
||||||
|
l.setBh(lesson.getBh());
|
||||||
|
l.setSskcbh(lesson.getSskcbh());
|
||||||
|
l.setKcmc(courseNames.getOrDefault(lesson.getSskcbh(), "-"));
|
||||||
|
l.setJxnr(lesson.getJxnr());
|
||||||
|
l.setJxff(lesson.getJxff());
|
||||||
|
l.setJcdj(lesson.getJcdj());
|
||||||
|
l.setYcxx(lesson.getYcxx());
|
||||||
|
l.setJyxm(courseTeachers.getOrDefault(lesson.getSskcbh(), "-"));
|
||||||
|
cell.getLessons().add(l);
|
||||||
|
}
|
||||||
|
day.getCells().add(cell);
|
||||||
|
}
|
||||||
|
week.getDays().add(day);
|
||||||
|
}
|
||||||
|
vo.getWeeks().add(week);
|
||||||
|
weekNo++;
|
||||||
|
weekStart = weekStart.plusDays(7);
|
||||||
|
}
|
||||||
|
vo.setTotalWeeks(weekNo - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 安排所选节次 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Map<String, Object> arrange(SchedulingArrangeRequest request) {
|
||||||
|
if (request == null || request.getSskcbh() == null || request.getCells() == null || request.getCells().isEmpty()) {
|
||||||
|
throw new ServiceException("请指定课程与要安排的节次", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
SSKC course = requireCourse(request.getSskcbh());
|
||||||
|
Ctx ctx = loadCtxForCourse(course);
|
||||||
|
if (!canEdit(course, ctx)) {
|
||||||
|
throw new ServiceException("没有该课程的排课权限", FORBIDDEN);
|
||||||
|
}
|
||||||
|
String jsbh = request.getJsbh() != null && !request.getJsbh().isEmpty() ? request.getJsbh() : course.getJsbh();
|
||||||
|
if (jsbh == null || jsbh.isEmpty()) {
|
||||||
|
throw new ServiceException("请指定场地(课程未设置默认场地)", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
String jybh = request.getJybh() != null && !request.getJybh().isEmpty() ? request.getJybh() : course.getJybh();
|
||||||
|
|
||||||
|
// 硬冲突判定所需数据
|
||||||
|
List<SSKCB> semesterLessons = ctx.lessons;
|
||||||
|
Map<String, List<SSKCB>> byGrid = semesterLessons.stream()
|
||||||
|
.filter(l -> l.getRq() != null && l.getJc() != null)
|
||||||
|
.collect(Collectors.groupingBy(l -> l.getRq().toLocalDate().toString() + "#" + l.getJc()));
|
||||||
|
// 课次 → 学员队 / 教室 / 教员(无占用课次时跳过,避免空 IN())
|
||||||
|
Map<String, List<String>> teamsOfLesson = new HashMap<>();
|
||||||
|
Map<String, List<String>> roomsOfLesson = new HashMap<>();
|
||||||
|
Map<String, List<String>> teachersOfLesson = new HashMap<>();
|
||||||
|
List<String> occupantIds = byGrid.values().stream().flatMap(List::stream)
|
||||||
|
.map(SSKCB::getBh).distinct().toList();
|
||||||
|
if (!occupantIds.isEmpty()) {
|
||||||
|
for (SSKCBXYD l : sskcbxydMapper.selectList(new LambdaQueryWrapper<SSKCBXYD>()
|
||||||
|
.in(SSKCBXYD::getSskcbbh, occupantIds))) {
|
||||||
|
teamsOfLesson.computeIfAbsent(l.getSskcbbh(), k -> new ArrayList<>()).add(l.getXydbh());
|
||||||
|
}
|
||||||
|
for (SSKCBJS l : sskcbjsMapper.selectList(new LambdaQueryWrapper<SSKCBJS>()
|
||||||
|
.in(SSKCBJS::getSskcbbh, occupantIds))) {
|
||||||
|
roomsOfLesson.computeIfAbsent(l.getSskcbbh(), k -> new ArrayList<>()).add(l.getJsbh());
|
||||||
|
}
|
||||||
|
for (SSKCBFZJY l : sskcbfzjyMapper.selectList(new LambdaQueryWrapper<SSKCBFZJY>()
|
||||||
|
.in(SSKCBFZJY::getSskcbbh, occupantIds))) {
|
||||||
|
teachersOfLesson.computeIfAbsent(l.getSskcbbh(), k -> new ArrayList<>()).add(l.getFzjybh());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 场地历 / 教员历 不可排
|
||||||
|
Map<String, String> roomBlocked = new HashMap<>();
|
||||||
|
for (JXCDL row : classRoomCalendarMapper.selectList(new LambdaQueryWrapper<JXCDL>()
|
||||||
|
.eq(JXCDL::getKpk, false).eq(JXCDL::getDelFlag, 0))) {
|
||||||
|
if (row.getRq() != null && row.getJsbh() != null) {
|
||||||
|
roomBlocked.put(row.getRq().toLocalDate().toString() + "#" + row.getJc() + "#" + row.getJsbh(), "场地历不可用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, String> teacherBlocked = new HashMap<>();
|
||||||
|
for (JYL row : jylMapper.selectList(new LambdaQueryWrapper<JYL>()
|
||||||
|
.eq(JYL::getKpk, 0).eq(JYL::getDelFlag, 0))) {
|
||||||
|
if (row.getRq() != null && row.getJybh() != null) {
|
||||||
|
teacherBlocked.put(row.getRq().toLocalDate().toString() + "#" + row.getJc() + "#" + row.getJybh(), "教员历不可用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 校历/班历不可排(节次级)
|
||||||
|
Set<String> schoolHard = calendarHardKeys(ctx, true);
|
||||||
|
Set<String> classHard = calendarHardKeys(ctx, false);
|
||||||
|
|
||||||
|
// 配当周次(软提示):该课程配当分布覆盖的周
|
||||||
|
Set<Integer> allocatedWeeks = allocatedWeeks(ctx, course);
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
int arranged = 0;
|
||||||
|
List<Map<String, Object>> ignored = new ArrayList<>();
|
||||||
|
List<Map<String, Object>> warnings = new ArrayList<>();
|
||||||
|
for (SchedulingArrangeRequest.Cell cell : request.getCells()) {
|
||||||
|
if (cell.getRq() == null || cell.getJc() == null) continue;
|
||||||
|
String key = cell.getRq() + "#" + cell.getJc();
|
||||||
|
List<SSKCB> occupants = byGrid.getOrDefault(key, List.of());
|
||||||
|
// 硬冲突:不可排课
|
||||||
|
if (schoolHard.contains(key)) {
|
||||||
|
ignored.add(ignoredCell(cell, "校历不可排课"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (classHard.contains(key)) {
|
||||||
|
ignored.add(ignoredCell(cell, "班历不可排课"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String roomKey = key + "#" + jsbh;
|
||||||
|
if (roomBlocked.containsKey(roomKey)) {
|
||||||
|
ignored.add(ignoredCell(cell, "场地历不可用"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (jybh != null && teacherBlocked.containsKey(key + "#" + jybh)) {
|
||||||
|
ignored.add(ignoredCell(cell, "教员历不可用"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 硬冲突:双占(本课程的已有课次视为可覆盖)
|
||||||
|
boolean ownOccupied = false;
|
||||||
|
boolean busy = false;
|
||||||
|
String busyReason = null;
|
||||||
|
for (SSKCB occupant : occupants) {
|
||||||
|
boolean own = occupant.getSskcbh().equals(course.getBh());
|
||||||
|
if (own) {
|
||||||
|
ownOccupied = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (teamsOfLesson.getOrDefault(occupant.getBh(), List.of()).stream()
|
||||||
|
.anyMatch(t -> Objects.equals(t, ctx.semester.getXydbh()))) {
|
||||||
|
busy = true; busyReason = "班次该节已有其它课程";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (roomsOfLesson.getOrDefault(occupant.getBh(), List.of()).contains(jsbh)) {
|
||||||
|
busy = true; busyReason = "教室该节已被占用";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (jybh != null && teachersOfLesson.getOrDefault(occupant.getBh(), List.of()).contains(jybh)) {
|
||||||
|
busy = true; busyReason = "教员该节已有课";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (busy) {
|
||||||
|
ignored.add(ignoredCell(cell, busyReason));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 软提示
|
||||||
|
LocalDate date = LocalDate.parse(cell.getRq());
|
||||||
|
int weekNo = weekNoOf(ctx, date);
|
||||||
|
if (allocatedWeeks != null && !allocatedWeeks.isEmpty() && !allocatedWeeks.contains(weekNo)) {
|
||||||
|
warnings.add(warnCell(cell, "配当周次不符(第 " + weekNo + " 周不在该课配当范围)"));
|
||||||
|
}
|
||||||
|
if (!ownOccupied) {
|
||||||
|
SSKCB lesson = new SSKCB();
|
||||||
|
lesson.setBh(UuidUtil.getUUID());
|
||||||
|
lesson.setSskcbh(course.getBh());
|
||||||
|
lesson.setJhsjap(0);
|
||||||
|
lesson.setJxnr(request.getJxnr());
|
||||||
|
lesson.setJxyd(request.getJxyd());
|
||||||
|
lesson.setJxff(request.getJxff() == null || request.getJxff().isEmpty() ? "理论讲授" : request.getJxff());
|
||||||
|
lesson.setRq(date.atStartOfDay());
|
||||||
|
lesson.setJc(cell.getJc());
|
||||||
|
lesson.setNd(ctx.ndCode);
|
||||||
|
lesson.setSczt(0);
|
||||||
|
lesson.setJcdj(request.getJcdj() == null ? 0 : request.getJcdj());
|
||||||
|
lesson.setJxbzbz(request.getJxbzbz());
|
||||||
|
lesson.setYcxx(request.getYcxx());
|
||||||
|
lesson.setBzms(request.getBzms());
|
||||||
|
lesson.setCjsj(now);
|
||||||
|
lesson.setBdsj(now);
|
||||||
|
sskcbMapper.insert(lesson);
|
||||||
|
// 学员队 / 教室 / 主讲教员
|
||||||
|
for (SSKCXYD link : ctx.links) {
|
||||||
|
if (!course.getBh().equals(link.getSskcbh())) continue;
|
||||||
|
SSKCBXYD lxyd = new SSKCBXYD();
|
||||||
|
lxyd.setBh(UuidUtil.getUUID());
|
||||||
|
lxyd.setSskcbbh(lesson.getBh());
|
||||||
|
lxyd.setXydbh(link.getXydbh());
|
||||||
|
lxyd.setRs(link.getRs());
|
||||||
|
lxyd.setJc(link.getJc());
|
||||||
|
lxyd.setNd(ctx.ndCode);
|
||||||
|
lxyd.setRq(lesson.getRq());
|
||||||
|
lxyd.setJc2(cell.getJc());
|
||||||
|
sskcbxydMapper.insert(lxyd);
|
||||||
|
}
|
||||||
|
SSKCBJS ljs = new SSKCBJS();
|
||||||
|
ljs.setBh(UuidUtil.getUUID());
|
||||||
|
ljs.setSskcbbh(lesson.getBh());
|
||||||
|
ljs.setJsbh(jsbh);
|
||||||
|
ljs.setNd(ctx.ndCode);
|
||||||
|
ljs.setRq(lesson.getRq());
|
||||||
|
ljs.setJc(cell.getJc());
|
||||||
|
sskcbjsMapper.insert(ljs);
|
||||||
|
if (jybh != null) {
|
||||||
|
SSKCBFZJY lfz = new SSKCBFZJY();
|
||||||
|
lfz.setBh(UuidUtil.getUUID());
|
||||||
|
lfz.setSskcbbh(lesson.getBh());
|
||||||
|
lfz.setFzjybh(jybh);
|
||||||
|
lfz.setZjy(1);
|
||||||
|
lfz.setNd(ctx.ndCode);
|
||||||
|
lfz.setRq(lesson.getRq());
|
||||||
|
lfz.setJc(cell.getJc());
|
||||||
|
sskcbfzjyMapper.insert(lfz);
|
||||||
|
}
|
||||||
|
arranged++;
|
||||||
|
logOperation(course, "安排", "安排 " + cell.getRq() + " 第" + cell.getJc() + "节"
|
||||||
|
+ (ownOccupied ? "(覆盖原课次)" : ""),
|
||||||
|
request);
|
||||||
|
} else {
|
||||||
|
// 覆盖本课程已有课次的内容
|
||||||
|
for (SSKCB occupant : occupants) {
|
||||||
|
if (!occupant.getSskcbh().equals(course.getBh())) continue;
|
||||||
|
occupant.setJxnr(request.getJxnr());
|
||||||
|
occupant.setJxyd(request.getJxyd());
|
||||||
|
occupant.setJxff(request.getJxff());
|
||||||
|
occupant.setJcdj(request.getJcdj() == null ? 0 : request.getJcdj());
|
||||||
|
occupant.setJxbzbz(request.getJxbzbz());
|
||||||
|
occupant.setBdsj(now);
|
||||||
|
sskcbMapper.updateById(occupant);
|
||||||
|
arranged++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("arranged", arranged);
|
||||||
|
result.put("ignored", ignored);
|
||||||
|
result.put("warnings", warnings);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 删除 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public int deleteCells(SchedulingCellOpRequest request) {
|
||||||
|
if (request == null || request.getSskcbh() == null || request.getCells() == null || request.getCells().isEmpty()) {
|
||||||
|
throw new ServiceException("请指定课程与要删除的节次", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
SSKC course = requireCourse(request.getSskcbh());
|
||||||
|
Ctx ctx = loadCtxForCourse(course);
|
||||||
|
if (!canEdit(course, ctx)) {
|
||||||
|
throw new ServiceException("没有该课程的排课权限", FORBIDDEN);
|
||||||
|
}
|
||||||
|
Set<String> keys = request.getCells().stream()
|
||||||
|
.filter(c -> c.getRq() != null && c.getJc() != null)
|
||||||
|
.map(c -> c.getRq() + "#" + c.getJc())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
int count = 0;
|
||||||
|
for (SSKCB lesson : sskcbMapper.selectList(new LambdaQueryWrapper<SSKCB>()
|
||||||
|
.eq(SSKCB::getSskcbh, course.getBh()))) {
|
||||||
|
if (lesson.getRq() == null || lesson.getJc() == null) continue;
|
||||||
|
String key = lesson.getRq().toLocalDate().toString() + "#" + lesson.getJc();
|
||||||
|
if (!keys.contains(key)) continue;
|
||||||
|
// 已提交实施计划的课次不在排课窗直改
|
||||||
|
if (lesson.getJhsjap() != null && lesson.getJhsjap() == 1) {
|
||||||
|
throw new ServiceException(lesson.getRq().toLocalDate() + " 第" + lesson.getJc()
|
||||||
|
+ "节已提交实施计划,请走调课申请", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
deleteLessonChildren(lesson.getBh());
|
||||||
|
sskcbMapper.deleteById(lesson.getBh());
|
||||||
|
logOperation(course, "删除", "删除课次 " + key, null);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public int clearCourse(String sskcbh) {
|
||||||
|
SSKC course = requireCourse(sskcbh);
|
||||||
|
Ctx ctx = loadCtxForCourse(course);
|
||||||
|
if (!canEdit(course, ctx)) {
|
||||||
|
throw new ServiceException("没有该课程的排课权限", FORBIDDEN);
|
||||||
|
}
|
||||||
|
List<SSKCB> lessons = sskcbMapper.selectList(new LambdaQueryWrapper<SSKCB>()
|
||||||
|
.eq(SSKCB::getSskcbh, course.getBh()));
|
||||||
|
for (SSKCB lesson : lessons) {
|
||||||
|
if (lesson.getJhsjap() != null && lesson.getJhsjap() == 1) {
|
||||||
|
throw new ServiceException("课程存在已提交实施计划的课次,请先通过调课流程处理", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (SSKCB lesson : lessons) {
|
||||||
|
deleteLessonChildren(lesson.getBh());
|
||||||
|
sskcbMapper.deleteById(lesson.getBh());
|
||||||
|
}
|
||||||
|
logOperation(course, "删除", "清空课程全部节次(" + lessons.size() + " 节)", null);
|
||||||
|
return lessons.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public int deleteCourse(String sskcbh) {
|
||||||
|
if (roleHelper.isTeacher()) {
|
||||||
|
throw new ServiceException("教员不能彻底删除运行课程", FORBIDDEN);
|
||||||
|
}
|
||||||
|
SSKC course = requireCourse(sskcbh);
|
||||||
|
Ctx ctx = loadCtxForCourse(course);
|
||||||
|
if (!canEdit(course, ctx)) {
|
||||||
|
throw new ServiceException("没有该课程的排课权限", FORBIDDEN);
|
||||||
|
}
|
||||||
|
for (SSKCB lesson : sskcbMapper.selectList(new LambdaQueryWrapper<SSKCB>()
|
||||||
|
.eq(SSKCB::getSskcbh, course.getBh()))) {
|
||||||
|
deleteLessonChildren(lesson.getBh());
|
||||||
|
sskcbMapper.deleteById(lesson.getBh());
|
||||||
|
}
|
||||||
|
sskcxydMapper.delete(new LambdaQueryWrapper<SSKCXYD>()
|
||||||
|
.eq(SSKCXYD::getSskcbh, course.getBh()));
|
||||||
|
sskcMapper.deleteById(course.getBh());
|
||||||
|
logOperation(course, "删除", "彻底删除运行课程", null);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> logs(String sskcbh, String czlx) {
|
||||||
|
LambdaQueryWrapper<KCJXYX_CZRZ> qw = new LambdaQueryWrapper<>();
|
||||||
|
qw.eq(KCJXYX_CZRZ::getTybh, sskcbh);
|
||||||
|
if (czlx != null && !czlx.isEmpty()) {
|
||||||
|
qw.eq(KCJXYX_CZRZ::getCzlx, czlx);
|
||||||
|
}
|
||||||
|
qw.orderByDesc(KCJXYX_CZRZ::getCzsj);
|
||||||
|
return czrzMapper.selectList(qw).stream().limit(200)
|
||||||
|
.map(r -> {
|
||||||
|
Map<String, Object> m = new HashMap<String, Object>();
|
||||||
|
m.put("czlx", r.getCzlx());
|
||||||
|
m.put("cznr", r.getCznr());
|
||||||
|
m.put("czrbh", r.getCzrbh());
|
||||||
|
m.put("czsj", r.getCzsj());
|
||||||
|
return m;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 公共 ====================
|
||||||
|
|
||||||
|
/** 上下文:班次学期 + 学员队 + 6 位代号 + 本班次已发布课程与课次 */
|
||||||
|
private class Ctx {
|
||||||
|
XYDNDXQJBXXB semester;
|
||||||
|
XYDB team;
|
||||||
|
Integer ndCode;
|
||||||
|
List<SSKCXYD> links;
|
||||||
|
List<SSKCB> lessons;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Ctx loadCtx(String xydxqbh) {
|
||||||
|
if (xydxqbh == null || xydxqbh.isEmpty()) {
|
||||||
|
throw new ServiceException("请指定班次学期", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh);
|
||||||
|
if (semester == null) {
|
||||||
|
throw new ServiceException("班次学期不存在:" + xydxqbh, NOT_FOUND);
|
||||||
|
}
|
||||||
|
Ctx ctx = new Ctx();
|
||||||
|
ctx.semester = semester;
|
||||||
|
ctx.team = xydbMapper.selectById(semester.getXydbh());
|
||||||
|
ctx.ndCode = SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc());
|
||||||
|
ctx.links = sskcxydMapper.selectList(new LambdaQueryWrapper<SSKCXYD>()
|
||||||
|
.eq(SSKCXYD::getXydbh, semester.getXydbh())
|
||||||
|
.eq(SSKCXYD::getNd, ctx.ndCode));
|
||||||
|
List<String> ids = ctx.links.stream().map(SSKCXYD::getSskcbh).distinct().toList();
|
||||||
|
ctx.lessons = ids.isEmpty() ? new ArrayList<>() : sskcbMapper.selectList(new LambdaQueryWrapper<SSKCB>()
|
||||||
|
.in(SSKCB::getSskcbh, ids)
|
||||||
|
.eq(SSKCB::getSczt, 0));
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Ctx loadCtxForCourse(SSKC course) {
|
||||||
|
// 通过任一关联学员队反查班次学期
|
||||||
|
List<SSKCXYD> links = sskcxydMapper.selectList(new LambdaQueryWrapper<SSKCXYD>()
|
||||||
|
.eq(SSKCXYD::getSskcbh, course.getBh()));
|
||||||
|
if (links.isEmpty()) {
|
||||||
|
throw new ServiceException("该运行课程没有关联学员队", NOT_FOUND);
|
||||||
|
}
|
||||||
|
Ctx ctx = loadCtxByTeam(links.get(0).getXydbh(), links.get(0).getNd());
|
||||||
|
if (ctx.semester == null) {
|
||||||
|
throw new ServiceException("未找到该课程所属班次学期", NOT_FOUND);
|
||||||
|
}
|
||||||
|
// 冲突判定需要全班次本学期全部课次(含其它课程),仅课程自身课次会漏判双占
|
||||||
|
List<SSKCXYD> teamLinks = sskcxydMapper.selectList(new LambdaQueryWrapper<SSKCXYD>()
|
||||||
|
.eq(SSKCXYD::getXydbh, ctx.semester.getXydbh())
|
||||||
|
.eq(SSKCXYD::getNd, ctx.ndCode));
|
||||||
|
ctx.links = teamLinks;
|
||||||
|
List<String> ids = teamLinks.stream().map(SSKCXYD::getSskcbh).distinct().toList();
|
||||||
|
ctx.lessons = ids.isEmpty() ? new ArrayList<>() : sskcbMapper.selectList(new LambdaQueryWrapper<SSKCB>()
|
||||||
|
.in(SSKCB::getSskcbh, ids)
|
||||||
|
.eq(SSKCB::getSczt, 0));
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Ctx loadCtxByTeam(String xydbh, Integer ndCode) {
|
||||||
|
List<XYDNDXQJBXXB> semesters = classSemesterMapper.selectList(new LambdaQueryWrapper<XYDNDXQJBXXB>()
|
||||||
|
.eq(XYDNDXQJBXXB::getXydbh, xydbh)
|
||||||
|
.eq(XYDNDXQJBXXB::getDelFlag, 0));
|
||||||
|
XYDNDXQJBXXB match = semesters.stream()
|
||||||
|
.filter(s -> ndCode.equals(SemesterCodeUtil.resolve(s.getNd(), s.getXqdc())))
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
Ctx ctx = new Ctx();
|
||||||
|
ctx.semester = match;
|
||||||
|
ctx.team = xydbMapper.selectById(xydbh);
|
||||||
|
ctx.ndCode = ndCode;
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SSKC requireCourse(String sskcbh) {
|
||||||
|
if (sskcbh == null || sskcbh.isEmpty()) {
|
||||||
|
throw new ServiceException("请指定运行课程", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
SSKC course = sskcMapper.selectById(sskcbh);
|
||||||
|
if (course == null) {
|
||||||
|
throw new ServiceException("运行课程不存在:" + sskcbh, NOT_FOUND);
|
||||||
|
}
|
||||||
|
return course;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排课权限:管理员/机关全部;教研室主任=责任教员属本室;教员=本人责任课程且班次开放教员排课。
|
||||||
|
*/
|
||||||
|
private boolean canEdit(SSKC course, Ctx ctx) {
|
||||||
|
if (roleHelper.isAdmin() || roleHelper.isDepartmentPersonnel()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (roleHelper.isResearchOffice()) {
|
||||||
|
String officeId = roleHelper.findResearchOfficeId();
|
||||||
|
if (officeId == null) return false;
|
||||||
|
return course.getJybh() != null && jyInOffice(course.getJybh(), officeId);
|
||||||
|
}
|
||||||
|
if (roleHelper.isTeacher()) {
|
||||||
|
if (ctx.semester != null && ctx.semester.getKfjypk() != null && ctx.semester.getKfjypk() == 0) {
|
||||||
|
return false; // 开放教员排课关闭,教员只读
|
||||||
|
}
|
||||||
|
String teacherId = roleHelper.findTeacherId();
|
||||||
|
return teacherId != null && teacherId.equals(course.getJybh());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 教员是否属指定教研室(教员表.教研室代号) */
|
||||||
|
private boolean jyInOffice(String jybh, String officeId) {
|
||||||
|
JYB teacher = jybMapper.selectById(jybh);
|
||||||
|
return teacher != null && officeId.equals(teacher.getJysdh());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteLessonChildren(String sskcbbh) {
|
||||||
|
sskcbxydMapper.delete(new LambdaQueryWrapper<SSKCBXYD>().eq(SSKCBXYD::getSskcbbh, sskcbbh));
|
||||||
|
sskcbjsMapper.delete(new LambdaQueryWrapper<SSKCBJS>().eq(SSKCBJS::getSskcbbh, sskcbbh));
|
||||||
|
sskcbfzjyMapper.delete(new LambdaQueryWrapper<SSKCBFZJY>().eq(SSKCBFZJY::getSskcbbh, sskcbbh));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校历/班历不可排格键集合 */
|
||||||
|
private Set<String> calendarHardKeys(Ctx ctx, boolean school) {
|
||||||
|
Set<String> keys = new HashSet<>();
|
||||||
|
if (school) {
|
||||||
|
for (XQXLB row : semesterCalendarMapper.selectList(new LambdaQueryWrapper<XQXLB>()
|
||||||
|
.eq(XQXLB::getNd, ctx.ndCode).eq(XQXLB::getKpk, false))) {
|
||||||
|
if (row.getJqsj() == null) continue;
|
||||||
|
for (Integer jc : parsePeriods(row.getCourseClass())) keys.add(row.getJqsj() + "#" + jc);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (JQB row : jqbMapper.selectList(new LambdaQueryWrapper<JQB>()
|
||||||
|
.eq(JQB::getXydxqbh, ctx.semester.getBh())
|
||||||
|
.eq(JQB::getKpk, 0))) {
|
||||||
|
if (row.getJqsj() == null) continue;
|
||||||
|
String date = intDate(row.getJqsj());
|
||||||
|
for (Integer jc : parsePeriods(row.getCourseClass())) keys.add(date + "#" + jc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 该课程配当分布覆盖的周号集合(无配当返回 null = 不提示) */
|
||||||
|
private Set<Integer> allocatedWeeks(Ctx ctx, SSKC course) {
|
||||||
|
try {
|
||||||
|
var view = teachingAllocationService.view(ctx.semester.getBh());
|
||||||
|
for (var task : view.getTasks()) {
|
||||||
|
if (Objects.equals(task.getKbh(), course.getKmbh()) && task.getDistribution() != null
|
||||||
|
&& !task.getDistribution().isEmpty()) {
|
||||||
|
return task.getDistribution().stream().map(d -> d.getWeek()).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("[排课窗] 读取配当失败,忽略软提示: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int weekNoOf(Ctx ctx, LocalDate date) {
|
||||||
|
LocalDate start = ctx.semester.getKxrq().with(DayOfWeek.MONDAY);
|
||||||
|
int week = 1;
|
||||||
|
while (!start.isAfter(date)) {
|
||||||
|
if (!date.isBefore(start) && !date.isAfter(start.plusDays(6))) return week;
|
||||||
|
start = start.plusDays(7);
|
||||||
|
week++;
|
||||||
|
}
|
||||||
|
return week;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> ignoredCell(SchedulingArrangeRequest.Cell cell, String reason) {
|
||||||
|
Map<String, Object> m = new HashMap<>();
|
||||||
|
m.put("rq", cell.getRq());
|
||||||
|
m.put("jc", cell.getJc());
|
||||||
|
m.put("reason", reason);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> warnCell(SchedulingArrangeRequest.Cell cell, String reason) {
|
||||||
|
Map<String, Object> m = new HashMap<>();
|
||||||
|
m.put("rq", cell.getRq());
|
||||||
|
m.put("jc", cell.getJc());
|
||||||
|
m.put("reason", reason);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logOperation(SSKC course, String czlx, String cznr, SchedulingArrangeRequest ignored) {
|
||||||
|
try {
|
||||||
|
KCJXYX_CZRZ row = new KCJXYX_CZRZ();
|
||||||
|
row.setBh(UuidUtil.getUUID());
|
||||||
|
row.setTybh(course.getBh());
|
||||||
|
row.setCzlx(czlx);
|
||||||
|
row.setCznr(cznr);
|
||||||
|
try {
|
||||||
|
row.setCzrbh(SecurityUtils.getUsername());
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
row.setCzrbh("harness");
|
||||||
|
}
|
||||||
|
row.setCzsj(LocalDateTime.now());
|
||||||
|
czrzMapper.insert(row);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[排课窗] 写排课日志失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveCourseName(String kbh) {
|
||||||
|
if (kbh == null) return "";
|
||||||
|
var kb = kbMapper.selectById(kbh);
|
||||||
|
return kb != null && kb.getKmc() != null ? kb.getKmc() : kbh;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String intDate(Integer d) {
|
||||||
|
String s = String.valueOf(d);
|
||||||
|
return s.length() == 8 ? s.substring(0, 4) + "-" + s.substring(4, 6) + "-" + s.substring(6, 8) : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Integer> parsePeriods(String label) {
|
||||||
|
List<Integer> out = new ArrayList<>();
|
||||||
|
if (label == null) return out;
|
||||||
|
for (String token : label.split("[,,]")) {
|
||||||
|
String t = token.trim();
|
||||||
|
if (t.contains("-")) {
|
||||||
|
String[] parts = t.split("-");
|
||||||
|
try {
|
||||||
|
int a = Integer.parseInt(parts[0].trim());
|
||||||
|
int b = Integer.parseInt(parts[1].trim());
|
||||||
|
for (int i = Math.min(a, b); i <= Math.max(a, b); i++) out.add(i);
|
||||||
|
} catch (Exception ignored) { }
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
out.add(Integer.parseInt(t));
|
||||||
|
} catch (Exception ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safe(String s) {
|
||||||
|
return s == null || s.isEmpty() ? "-" : s;
|
||||||
|
}
|
||||||
|
}
|
||||||
+181
-45
@@ -22,6 +22,7 @@ import com.roomroot.jwgl.mapper.XYDBMapper;
|
|||||||
import com.roomroot.jwgl.mapper.ZYJXJHBMapper;
|
import com.roomroot.jwgl.mapper.ZYJXJHBMapper;
|
||||||
import com.roomroot.jwgl.service.StudentTeamTaskService;
|
import com.roomroot.jwgl.service.StudentTeamTaskService;
|
||||||
import com.roomroot.jwgl.service.TeachingTaskWriteGuard;
|
import com.roomroot.jwgl.service.TeachingTaskWriteGuard;
|
||||||
|
import com.roomroot.jwgl.utils.SemesterCodeUtil;
|
||||||
import com.roomroot.jwgl.utils.TeachingTaskStatus;
|
import com.roomroot.jwgl.utils.TeachingTaskStatus;
|
||||||
import com.roomroot.jwgl.utils.UuidUtil;
|
import com.roomroot.jwgl.utils.UuidUtil;
|
||||||
import com.roomroot.jwgl.vo.PlanCourseVO;
|
import com.roomroot.jwgl.vo.PlanCourseVO;
|
||||||
@@ -75,12 +76,16 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
|
|||||||
private SSKCXYDMapper sskcxydMapper;
|
private SSKCXYDMapper sskcxydMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void add(XYDRWB xydrwb) {
|
public void add(XYDRWB xydrwb) {
|
||||||
if (xydrwb != null) {
|
if (xydrwb != null) {
|
||||||
teachingTaskWriteGuard.assertCourseTaskWritable(xydrwb.getXydxqbh());
|
teachingTaskWriteGuard.assertCourseTaskWritable(xydrwb.getXydxqbh());
|
||||||
}
|
}
|
||||||
xydrwb.setBh(UuidUtil.getUUID());
|
xydrwb.setBh(UuidUtil.getUUID());
|
||||||
xydrwb.setDelFlag(0);
|
xydrwb.setDelFlag(0);
|
||||||
|
// 新增表单允许留空「计划课次序号 / 序号标识 / 编组」等列,这里统一补齐非空约束,
|
||||||
|
// 否则最小表单直接插入会违反库中的非空约束而报错。
|
||||||
|
applyRequiredDefaults(xydrwb, nextSequence(xydrwb.getXydxqbh()));
|
||||||
xydrwbMapper.insert(xydrwb);
|
xydrwbMapper.insert(xydrwb);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,50 +191,41 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
|
|||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@Override
|
@Override
|
||||||
public int autoGenerateRequiredCourses(String xydbh, Integer xqdc) {
|
public int autoGenerateRequiredCourses(String xydxqbh) {
|
||||||
// 1. 根据学员队编号查询学员队表,获取专业代号
|
// 1. 按班次学期主键定位目标学期:它在库里唯一,不会跨学年串写
|
||||||
XYDB xydb = xydbMapper.selectById(xydbh);
|
if (xydxqbh == null || xydxqbh.isEmpty()) {
|
||||||
if (xydb == null || xydb.getZydh() == null) {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
String zydh = xydb.getZydh();
|
XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh);
|
||||||
|
|
||||||
// 2. 根据专业代号和学期第次查询专业教学计划表(停用=0)
|
|
||||||
LambdaQueryWrapper<ZYJXJHB> planWrapper = new LambdaQueryWrapper<>();
|
|
||||||
planWrapper.eq(ZYJXJHB::getZydh, zydh)
|
|
||||||
.eq(ZYJXJHB::getXqdc, xqdc)
|
|
||||||
.eq(ZYJXJHB::getTy, 0);
|
|
||||||
List<ZYJXJHB> planList = zyjxjhbMapper.selectList(planWrapper);
|
|
||||||
if (CollectionUtils.isEmpty(planList)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 根据学员队编号和学期第次查询学员队期表,获取年度和学员队学期编号
|
|
||||||
LambdaQueryWrapper<XYDNDXQJBXXB> semesterWrapper = new LambdaQueryWrapper<>();
|
|
||||||
semesterWrapper.eq(XYDNDXQJBXXB::getXydbh, xydbh)
|
|
||||||
.eq(XYDNDXQJBXXB::getXqdc, xqdc)
|
|
||||||
.eq(XYDNDXQJBXXB::getDelFlag, 0)
|
|
||||||
.orderByDesc(XYDNDXQJBXXB::getNd)
|
|
||||||
.last("LIMIT 1");
|
|
||||||
XYDNDXQJBXXB semester = classSemesterMapper.selectOne(semesterWrapper);
|
|
||||||
if (semester == null) {
|
if (semester == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
Integer nd = semester.getNd();
|
String xydbh = semester.getXydbh();
|
||||||
String xydxqbh = semester.getBh();
|
Integer xqdc = semester.getXqdc();
|
||||||
|
if (xydbh == null || xqdc == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务表/实施/课程表的「年度」口径是 6 位学期代号(见 SemesterCodeUtil 类注释),
|
||||||
|
// 不能直接落班次学期的 4 位年份,否则与实施链路、选修课程映射的年度连接全部断裂
|
||||||
|
Integer nd = SemesterCodeUtil.resolve(semester.getNd(), xqdc);
|
||||||
teachingTaskWriteGuard.assertCourseTaskWritable(xydxqbh);
|
teachingTaskWriteGuard.assertCourseTaskWritable(xydxqbh);
|
||||||
|
|
||||||
|
// 2. 匹配人培:匹配不到时给出明确原因,而不是静默返回 0 被当成「本学期无必修课」
|
||||||
|
PlanMatch planMatch = matchPlanCourses(xydbh, xqdc);
|
||||||
|
if (planMatch.problem() != null) {
|
||||||
|
throw new ServiceException("自动生成必修课程失败:" + planMatch.problem(), BAD_REQUEST);
|
||||||
|
}
|
||||||
|
List<ZYJXJHB> planList = planMatch.matched();
|
||||||
|
if (planList.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// 4. 查询当前班次已存在的课程编号集合
|
// 4. 查询当前班次已存在的课程编号集合
|
||||||
LambdaQueryWrapper<XYDRWB> existWrapper = new LambdaQueryWrapper<>();
|
Set<String> existKbhs = currentCourseCodes(xydxqbh);
|
||||||
existWrapper.eq(XYDRWB::getXydxqbh, xydxqbh)
|
|
||||||
.eq(XYDRWB::getDelFlag, 0);
|
|
||||||
List<XYDRWB> existList = xydrwbMapper.selectList(existWrapper);
|
|
||||||
List<String> existKbhs = existList.stream()
|
|
||||||
.map(XYDRWB::getKbh)
|
|
||||||
.filter(kbh -> kbh != null)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 5. 只添加缺少的课程
|
// 5. 只添加缺少的课程
|
||||||
|
int seq = nextSequence(xydxqbh);
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (ZYJXJHB plan : planList) {
|
for (ZYJXJHB plan : planList) {
|
||||||
if (existKbhs.contains(plan.getKbh())) {
|
if (existKbhs.contains(plan.getKbh())) {
|
||||||
@@ -242,6 +238,8 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
|
|||||||
newRecord.setXydbh(xydbh);
|
newRecord.setXydbh(xydbh);
|
||||||
newRecord.setKbh(plan.getKbh());
|
newRecord.setKbh(plan.getKbh());
|
||||||
newRecord.setXqdc(xqdc);
|
newRecord.setXqdc(xqdc);
|
||||||
|
// 阶段 4:新建任务默认写入班次专用教室作为场地
|
||||||
|
newRecord.setJsbh(semester.getZyjsbh());
|
||||||
newRecord.setKlx(plan.getKlx());
|
newRecord.setKlx(plan.getKlx());
|
||||||
newRecord.setXs(plan.getXs());
|
newRecord.setXs(plan.getXs());
|
||||||
newRecord.setJc(plan.getJc());
|
newRecord.setJc(plan.getJc());
|
||||||
@@ -255,14 +253,16 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
|
|||||||
newRecord.setKsksbxs(plan.getKsksbxs());
|
newRecord.setKsksbxs(plan.getKsksbxs());
|
||||||
newRecord.setJysdh(plan.getJysdh());
|
newRecord.setJysdh(plan.getJysdh());
|
||||||
newRecord.setXydxqbh(xydxqbh);
|
newRecord.setXydxqbh(xydxqbh);
|
||||||
newRecord.setJhkcxh(count+1);
|
newRecord.setJhkcxh(seq);
|
||||||
newRecord.setXhbs(count+1);
|
newRecord.setXhbs(seq);
|
||||||
newRecord.setBz2(0);
|
|
||||||
newRecord.setPdtbykxxbz(1);
|
|
||||||
newRecord.setPdqyzdyxs(0);
|
|
||||||
newRecord.setDelFlag(0);
|
newRecord.setDelFlag(0);
|
||||||
|
// 补齐全表的非空约束列(编组、配当同班异课选修编组、配档启用自定义学时等),
|
||||||
|
// 一次到位,避免遗漏导致插入时违反非空约束。
|
||||||
|
applyRequiredDefaults(newRecord, seq);
|
||||||
|
|
||||||
xydrwbMapper.insert(newRecord);
|
xydrwbMapper.insert(newRecord);
|
||||||
|
existKbhs.add(plan.getKbh());
|
||||||
|
seq++;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
@@ -282,6 +282,7 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
|
|||||||
source.setXydbh(newXydbh);
|
source.setXydbh(newXydbh);
|
||||||
source.setDelFlag(0);
|
source.setDelFlag(0);
|
||||||
teachingTaskWriteGuard.assertCourseTaskWritable(source.getXydxqbh());
|
teachingTaskWriteGuard.assertCourseTaskWritable(source.getXydxqbh());
|
||||||
|
applyRequiredDefaults(source, nextSequence(source.getXydxqbh()));
|
||||||
xydrwbMapper.insert(source);
|
xydrwbMapper.insert(source);
|
||||||
newBhList.add(newBh);
|
newBhList.add(newBh);
|
||||||
}
|
}
|
||||||
@@ -294,17 +295,80 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
|
|||||||
if (CollectionUtils.isEmpty(xydxqbhList)) {
|
if (CollectionUtils.isEmpty(xydxqbhList)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
int count = 0;
|
// 先统一预检,任何一个班次匹配不到人培课程都直接报错,
|
||||||
|
// 避免部分写入后因异常被整体回滚、却又没有留下任何提示。
|
||||||
|
List<String> problems = new ArrayList<>();
|
||||||
for (String xydxqbh : xydxqbhList) {
|
for (String xydxqbh : xydxqbhList) {
|
||||||
XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh);
|
XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh);
|
||||||
if (semester == null || semester.getXydbh() == null || semester.getXqdc() == null) {
|
if (semester == null || semester.getXydbh() == null || semester.getXqdc() == null) {
|
||||||
|
problems.add("班次学期 " + xydxqbh + " 不存在或缺少学员队编号/学期第次");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
count += autoGenerateRequiredCourses(semester.getXydbh(), semester.getXqdc());
|
PlanMatch match = matchPlanCourses(semester.getXydbh(), semester.getXqdc());
|
||||||
|
if (match.problem() != null) {
|
||||||
|
problems.add(semesterLabel(semester) + ":" + match.problem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!problems.isEmpty()) {
|
||||||
|
throw new ServiceException("自动生成必修课程未执行,以下班次无法匹配到人培课程:"
|
||||||
|
+ String.join(";", problems), BAD_REQUEST);
|
||||||
|
}
|
||||||
|
int count = 0;
|
||||||
|
for (String xydxqbh : xydxqbhList) {
|
||||||
|
// 直接按班次学期编号生成,避免再按 (学员队, 学期第次) 反查时落到别的学年
|
||||||
|
count += autoGenerateRequiredCourses(xydxqbh);
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 人培匹配结果:matched 为匹配到的课程;problem 非空表示无法生成的原因。 */
|
||||||
|
private record PlanMatch(List<ZYJXJHB> matched, String problem) {
|
||||||
|
static PlanMatch ok(List<ZYJXJHB> list) { return new PlanMatch(list, null); }
|
||||||
|
static PlanMatch fail(String why) { return new PlanMatch(List.of(), why); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 班次学期在报错信息里的可读标签(学员队名称 + 年度/学期第次)。 */
|
||||||
|
private String semesterLabel(XYDNDXQJBXXB semester) {
|
||||||
|
XYDB clazz = xydbMapper.selectById(semester.getXydbh());
|
||||||
|
String name = clazz != null && clazz.getXydmc() != null ? clazz.getXydmc() : semester.getXydbh();
|
||||||
|
return name + " " + semester.getNd() + " 年度学期第次 " + semester.getXqdc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按专业代号 + 学期第次匹配人培(专业教学计划表,停用=0)。
|
||||||
|
*
|
||||||
|
* <p>口径说明(经业务确认保留等值匹配):人培的「学期第次」是专业总第几学期
|
||||||
|
* (1..学期数,学期数 = 学年制 × 2,见专业表);而班次学期的「学期第次」是
|
||||||
|
* 学年内学期类别(1 春季 / 2 夏季 / 3 秋季,见 ClassSemesterServiceImpl.assertXqdc)。
|
||||||
|
* 两者口径不同,因此匹配不到时必须把原因讲清楚,不能静默返回 0。</p>
|
||||||
|
*/
|
||||||
|
private PlanMatch matchPlanCourses(String xydbh, Integer xqdc) {
|
||||||
|
XYDB clazz = xydbMapper.selectById(xydbh);
|
||||||
|
String zydh = clazz == null ? null : clazz.getZydh();
|
||||||
|
if (zydh == null || zydh.isEmpty()) {
|
||||||
|
return PlanMatch.fail("学员队 " + xydbh + " 未关联专业");
|
||||||
|
}
|
||||||
|
List<ZYJXJHB> allEnabled = zyjxjhbMapper.selectList(new LambdaQueryWrapper<ZYJXJHB>()
|
||||||
|
.eq(ZYJXJHB::getZydh, zydh)
|
||||||
|
.eq(ZYJXJHB::getTy, 0));
|
||||||
|
if (allEnabled.isEmpty()) {
|
||||||
|
return PlanMatch.fail("专业 " + zydh + " 尚无启用状态的人才培养方案数据");
|
||||||
|
}
|
||||||
|
List<ZYJXJHB> matched = new ArrayList<>();
|
||||||
|
for (ZYJXJHB plan : allEnabled) {
|
||||||
|
if (xqdc != null && xqdc.equals(plan.getXqdc())) {
|
||||||
|
matched.add(plan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matched.isEmpty()) {
|
||||||
|
return PlanMatch.fail(String.format(
|
||||||
|
"专业 %s 的人培中没有「学期第次 = %d」的课程(人培的学期第次是专业总第几学期 1..学期数,"
|
||||||
|
+ "而班次学期的学期第次是学年内学期类别 1 春季 / 2 夏季 / 3 秋季,两者口径不同,"
|
||||||
|
+ "请核对人培数据或改用「计划内必修添加」手动选取)", zydh, xqdc));
|
||||||
|
}
|
||||||
|
return PlanMatch.ok(matched);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<PlanCourseVO> listPlanCourses(String xydxqbh, boolean otherOnly) {
|
public List<PlanCourseVO> listPlanCourses(String xydxqbh, boolean otherOnly) {
|
||||||
XYDNDXQJBXXB semester = requireSemester(xydxqbh);
|
XYDNDXQJBXXB semester = requireSemester(xydxqbh);
|
||||||
@@ -350,10 +414,12 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
|
|||||||
}
|
}
|
||||||
XYDRWB row = new XYDRWB();
|
XYDRWB row = new XYDRWB();
|
||||||
row.setBh(UuidUtil.getUUID());
|
row.setBh(UuidUtil.getUUID());
|
||||||
row.setNd(semester.getNd());
|
row.setNd(SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc()));
|
||||||
row.setXydbh(semester.getXydbh());
|
row.setXydbh(semester.getXydbh());
|
||||||
row.setXydxqbh(xydxqbh);
|
row.setXydxqbh(xydxqbh);
|
||||||
row.setXqdc(semester.getXqdc());
|
row.setXqdc(semester.getXqdc());
|
||||||
|
// 阶段 4:新建任务默认写入班次专用教室作为场地
|
||||||
|
row.setJsbh(semester.getZyjsbh());
|
||||||
row.setKbh(course.getKbh());
|
row.setKbh(course.getKbh());
|
||||||
row.setJc(course.getJc());
|
row.setJc(course.getJc());
|
||||||
row.setXs(course.getXs());
|
row.setXs(course.getXs());
|
||||||
@@ -366,6 +432,7 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
|
|||||||
row.setKsks(course.getKsks());
|
row.setKsks(course.getKsks());
|
||||||
row.setJysdh(course.getJysdh());
|
row.setJysdh(course.getJysdh());
|
||||||
row.setDelFlag(0);
|
row.setDelFlag(0);
|
||||||
|
applyRequiredDefaults(row, nextSequence(xydxqbh));
|
||||||
xydrwbMapper.insert(row);
|
xydrwbMapper.insert(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +447,9 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
|
|||||||
if (currentCourseCodes(xydxqbh).contains(plan.getKbh())) {
|
if (currentCourseCodes(xydxqbh).contains(plan.getKbh())) {
|
||||||
throw new ServiceException("当前学期已有该课程", CONFLICT);
|
throw new ServiceException("当前学期已有该课程", CONFLICT);
|
||||||
}
|
}
|
||||||
xydrwbMapper.insert(fromPlan(semester, plan));
|
XYDRWB row = fromPlan(semester, plan);
|
||||||
|
applyRequiredDefaults(row, nextSequence(xydxqbh));
|
||||||
|
xydrwbMapper.insert(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -443,6 +512,7 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
int seq = nextSequence(targetXydxqbh);
|
||||||
for (String bh : bhList) {
|
for (String bh : bhList) {
|
||||||
XYDRWB source = xydrwbMapper.selectById(bh);
|
XYDRWB source = xydrwbMapper.selectById(bh);
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
@@ -451,10 +521,15 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
|
|||||||
source.setBh(UuidUtil.getUUID());
|
source.setBh(UuidUtil.getUUID());
|
||||||
source.setXydbh(semester.getXydbh());
|
source.setXydbh(semester.getXydbh());
|
||||||
source.setXydxqbh(semester.getBh());
|
source.setXydxqbh(semester.getBh());
|
||||||
source.setNd(semester.getNd());
|
source.setNd(SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc()));
|
||||||
source.setXqdc(semester.getXqdc());
|
source.setXqdc(semester.getXqdc());
|
||||||
source.setDelFlag(0);
|
source.setDelFlag(0);
|
||||||
|
// 粘贴到新班次后应排在末尾,不复用源班的课次序号,避免与目标班次已有行重号
|
||||||
|
source.setJhkcxh(seq);
|
||||||
|
source.setXhbs(seq);
|
||||||
|
applyRequiredDefaults(source, seq);
|
||||||
xydrwbMapper.insert(source);
|
xydrwbMapper.insert(source);
|
||||||
|
seq++;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
@@ -463,10 +538,12 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
|
|||||||
private XYDRWB fromPlan(XYDNDXQJBXXB semester, ZYJXJHB plan) {
|
private XYDRWB fromPlan(XYDNDXQJBXXB semester, ZYJXJHB plan) {
|
||||||
XYDRWB row = new XYDRWB();
|
XYDRWB row = new XYDRWB();
|
||||||
row.setBh(UuidUtil.getUUID());
|
row.setBh(UuidUtil.getUUID());
|
||||||
row.setNd(semester.getNd());
|
row.setNd(SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc()));
|
||||||
row.setXydbh(semester.getXydbh());
|
row.setXydbh(semester.getXydbh());
|
||||||
row.setXydxqbh(semester.getBh());
|
row.setXydxqbh(semester.getBh());
|
||||||
row.setXqdc(semester.getXqdc());
|
row.setXqdc(semester.getXqdc());
|
||||||
|
// 阶段 4:新建任务默认写入班次专用教室作为场地
|
||||||
|
row.setJsbh(semester.getZyjsbh());
|
||||||
row.setKbh(plan.getKbh());
|
row.setKbh(plan.getKbh());
|
||||||
row.setKlx(plan.getKlx());
|
row.setKlx(plan.getKlx());
|
||||||
row.setXs(plan.getXs());
|
row.setXs(plan.getXs());
|
||||||
@@ -540,6 +617,65 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
|
|||||||
.collect(Collectors.toCollection(HashSet::new));
|
.collect(Collectors.toCollection(HashSet::new));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 补齐学员队任务表里「非空且无库默认值」的列。
|
||||||
|
* <p>
|
||||||
|
* 这些列漏填时插入会直接违反非空约束:计划课次序号、序号标识、编组、
|
||||||
|
* 配当同班异课选修编组、配档启用自定义学时、学时、课类型、教研室代号。
|
||||||
|
* 其余非空列在库里有默认值,交给库处理。
|
||||||
|
*/
|
||||||
|
private void applyRequiredDefaults(XYDRWB row, Integer sequence) {
|
||||||
|
if (row == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (row.getJhkcxh() == null) {
|
||||||
|
row.setJhkcxh(sequence == null ? 1 : sequence);
|
||||||
|
}
|
||||||
|
if (row.getXhbs() == null) {
|
||||||
|
row.setXhbs(row.getJhkcxh());
|
||||||
|
}
|
||||||
|
if (row.getBz2() == null) {
|
||||||
|
row.setBz2(0);
|
||||||
|
}
|
||||||
|
if (row.getPdtbykxxbz() == null) {
|
||||||
|
row.setPdtbykxxbz(1);
|
||||||
|
}
|
||||||
|
if (row.getPdqyzdyxs() == null) {
|
||||||
|
row.setPdqyzdyxs(0);
|
||||||
|
}
|
||||||
|
if (row.getXs() == null) {
|
||||||
|
row.setXs(0);
|
||||||
|
}
|
||||||
|
if (row.getKlx() == null) {
|
||||||
|
row.setKlx("");
|
||||||
|
}
|
||||||
|
if (row.getJysdh() == null) {
|
||||||
|
row.setJysdh("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取该班次学期下一个可用的课次序号。计划课次序号与序号标识都要非空,故取两者较大值 +1。
|
||||||
|
*/
|
||||||
|
private int nextSequence(String xydxqbh) {
|
||||||
|
if (xydxqbh == null || xydxqbh.isEmpty()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int max = 0;
|
||||||
|
List<XYDRWB> rows = xydrwbMapper.selectList(new LambdaQueryWrapper<XYDRWB>()
|
||||||
|
.eq(XYDRWB::getXydxqbh, xydxqbh)
|
||||||
|
.eq(XYDRWB::getDelFlag, 0));
|
||||||
|
for (XYDRWB r : rows) {
|
||||||
|
if (r.getJhkcxh() != null && r.getJhkcxh() > max) {
|
||||||
|
max = r.getJhkcxh();
|
||||||
|
}
|
||||||
|
if (r.getXhbs() != null && r.getXhbs() > max) {
|
||||||
|
max = r.getXhbs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max + 1;
|
||||||
|
}
|
||||||
|
|
||||||
private XYDNDXQJBXXB requireSemester(String xydxqbh) {
|
private XYDNDXQJBXXB requireSemester(String xydxqbh) {
|
||||||
XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh);
|
XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh);
|
||||||
if (semester == null) {
|
if (semester == null) {
|
||||||
|
|||||||
+378
@@ -0,0 +1,378 @@
|
|||||||
|
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.taskbook.TaskBookBhListRequest;
|
||||||
|
import com.roomroot.jwgl.dto.taskbook.TaskBookFillRequest;
|
||||||
|
import com.roomroot.jwgl.dto.taskbook.TaskBookRoomRequest;
|
||||||
|
import com.roomroot.jwgl.dto.taskbook.TaskBookTeacherRequest;
|
||||||
|
import com.roomroot.jwgl.entity.JYB;
|
||||||
|
import com.roomroot.jwgl.entity.JXRW;
|
||||||
|
import com.roomroot.jwgl.entity.XYDNDXQJBXXB;
|
||||||
|
import com.roomroot.jwgl.entity.XYDRWB;
|
||||||
|
import com.roomroot.jwgl.mapper.ClassSemesterMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.JYBMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.StudentTeamTaskMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.TeachingTaskMapper;
|
||||||
|
import com.roomroot.jwgl.service.TaskBookFillService;
|
||||||
|
import com.roomroot.jwgl.service.TeachingTaskWriteGuard;
|
||||||
|
import com.roomroot.jwgl.utils.SemesterCodeUtil;
|
||||||
|
import com.roomroot.jwgl.utils.TeachingTaskStatus;
|
||||||
|
import com.roomroot.jwgl.vo.taskbook.TaskBookRowVO;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST;
|
||||||
|
import static com.roomroot.jwgl.utils.BasicManagementConstants.CONFLICT;
|
||||||
|
import static com.roomroot.jwgl.utils.BasicManagementConstants.NOT_FOUND;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教研室任务书填报实现(阶段 4)。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class TaskBookFillServiceImpl implements TaskBookFillService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private StudentTeamTaskMapper taskMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ClassSemesterMapper classSemesterMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TeachingTaskMapper jxrwMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JYBMapper jybMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TeachingTaskWriteGuard guard;
|
||||||
|
|
||||||
|
// ==================== 列表 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TaskBookRowVO> list(String jxrwbh) {
|
||||||
|
if (jxrwbh == null || jxrwbh.isEmpty()) {
|
||||||
|
throw new ServiceException("请指定教学任务", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
JXRW jxrw = jxrwMapper.selectById(jxrwbh);
|
||||||
|
if (jxrw == null || Integer.valueOf(1).equals(jxrw.getDelFlag())) {
|
||||||
|
throw new ServiceException("教学任务不存在:" + jxrwbh, NOT_FOUND);
|
||||||
|
}
|
||||||
|
List<TaskBookRowVO> rows = jxrwMapper.selectTaskBookRows(jxrwbh);
|
||||||
|
boolean frozen = isFrozen(jxrw);
|
||||||
|
rows.forEach(r -> r.setFrozen(frozen));
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 填报门禁:仅「已发布且未结束」的教学任务可填报,其余只读 */
|
||||||
|
private boolean isFrozen(JXRW jxrw) {
|
||||||
|
return !TeachingTaskStatus.isPublished(jxrw.getZt()) || TeachingTaskStatus.isEnded(jxrw.getZt());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 合班 / 拆班 ====================
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public Integer merge(TaskBookBhListRequest request) {
|
||||||
|
List<XYDRWB> rows = loadRows(request);
|
||||||
|
assertAllFillable(rows);
|
||||||
|
assertSameXq(rows);
|
||||||
|
assertMergeCompatible(rows);
|
||||||
|
int groupNo = nextGroupNo();
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
for (XYDRWB row : rows) {
|
||||||
|
row.setBz2(groupNo);
|
||||||
|
row.setBdsj(now);
|
||||||
|
taskMapper.updateById(row);
|
||||||
|
}
|
||||||
|
return groupNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 合班只允许同一学期(6 位学期代号相同):跨学期合班没有业务意义 */
|
||||||
|
private void assertSameXq(List<XYDRWB> rows) {
|
||||||
|
Integer first = xqCodeOf(rows.get(0));
|
||||||
|
for (XYDRWB row : rows) {
|
||||||
|
if (!Objects.equals(xqCodeOf(row), first)) {
|
||||||
|
throw new ServiceException("任务 " + row.getBh() + "(" + row.getXydbh() + ")与第一行不在同一学期,不能合班",
|
||||||
|
BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer xqCodeOf(XYDRWB row) {
|
||||||
|
XYDNDXQJBXXB semester = requireSemester(row.getXydxqbh());
|
||||||
|
return SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手动合班校验:科目、学时、课类型、成绩分制必须相同 */
|
||||||
|
private void assertMergeCompatible(List<XYDRWB> rows) {
|
||||||
|
XYDRWB first = rows.get(0);
|
||||||
|
for (XYDRWB row : rows) {
|
||||||
|
List<String> diff = new ArrayList<>();
|
||||||
|
if (!Objects.equals(row.getKbh(), first.getKbh())) {
|
||||||
|
diff.add("科目(" + row.getKbh() + " ≠ " + first.getKbh() + ")");
|
||||||
|
}
|
||||||
|
if (!Objects.equals(row.getXs(), first.getXs())) {
|
||||||
|
diff.add("学时(" + row.getXs() + " ≠ " + first.getXs() + ")");
|
||||||
|
}
|
||||||
|
if (!Objects.equals(row.getKlx(), first.getKlx())) {
|
||||||
|
diff.add("课类型(" + row.getKlx() + " ≠ " + first.getKlx() + ")");
|
||||||
|
}
|
||||||
|
if (!Objects.equals(norm(row.getCjfz()), norm(first.getCjfz()))) {
|
||||||
|
diff.add("成绩分制(" + row.getCjfz() + " ≠ " + first.getCjfz() + ")");
|
||||||
|
}
|
||||||
|
if (!diff.isEmpty()) {
|
||||||
|
throw new ServiceException("任务 " + row.getBh() + "(" + row.getXydbh() + ")与第一行不合班:" + String.join("、", diff),
|
||||||
|
BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String norm(String s) {
|
||||||
|
return s == null || s.isEmpty() ? "" : s.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int nextGroupNo() {
|
||||||
|
int max = 0;
|
||||||
|
for (Integer bz2 : taskMapper.selectList(new LambdaQueryWrapper<XYDRWB>()
|
||||||
|
.isNotNull(XYDRWB::getBz2)).stream().map(XYDRWB::getBz2).toList()) {
|
||||||
|
if (bz2 != null && bz2 > max) {
|
||||||
|
max = bz2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> mergeByPreset(TaskBookBhListRequest request) {
|
||||||
|
List<XYDRWB> rows = loadRows(request);
|
||||||
|
assertAllFillable(rows);
|
||||||
|
assertSameXq(rows);
|
||||||
|
// 行按 课程科目(kbh) → 预设编班号(ysbbh,空则各班次学期独立) 分组
|
||||||
|
Map<String, List<XYDRWB>> groups = new LinkedHashMap<>();
|
||||||
|
for (XYDRWB row : rows) {
|
||||||
|
XYDNDXQJBXXB semester = requireSemester(row.getXydxqbh());
|
||||||
|
Integer ysbbh = semester.getYsbbh();
|
||||||
|
String key = row.getKbh() + "|" + (ysbbh == null ? "row:" + row.getXydxqbh() : ysbbh);
|
||||||
|
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(row);
|
||||||
|
}
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
int groupNo = nextGroupNo();
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, List<XYDRWB>> entry : groups.entrySet()) {
|
||||||
|
List<XYDRWB> group = entry.getValue();
|
||||||
|
for (XYDRWB row : group) {
|
||||||
|
row.setBz2(groupNo);
|
||||||
|
row.setBdsj(now);
|
||||||
|
taskMapper.updateById(row);
|
||||||
|
}
|
||||||
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
|
item.put("groupNo", groupNo);
|
||||||
|
item.put("kbh", group.get(0).getKbh());
|
||||||
|
item.put("ysbbh", requireSemester(group.get(0).getXydxqbh()).getYsbbh());
|
||||||
|
item.put("count", group.size());
|
||||||
|
result.add(item);
|
||||||
|
groupNo++;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public int split(TaskBookBhListRequest request) {
|
||||||
|
List<XYDRWB> rows = loadRows(request);
|
||||||
|
assertAllFillable(rows);
|
||||||
|
// TODO(阶段 5):行已发布到运行课表的须先撤回,再拆班。阶段 5 落地后在此校验。
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
int count = 0;
|
||||||
|
for (XYDRWB row : rows) {
|
||||||
|
if (row.getBz2() != null && row.getBz2() != 0) {
|
||||||
|
row.setBz2(0);
|
||||||
|
row.setBdsj(now);
|
||||||
|
taskMapper.updateById(row);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 责任教员 / 场地 / 填报 ====================
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public int setTeacher(TaskBookTeacherRequest request) {
|
||||||
|
if (request == null || request.getBh() == null || request.getBh().isEmpty()) {
|
||||||
|
throw new ServiceException("请指定课程任务", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
XYDRWB row = requireRow(request.getBh());
|
||||||
|
assertFillable(row);
|
||||||
|
String mode = request.getMode() == null ? "" : request.getMode();
|
||||||
|
String jybh;
|
||||||
|
if ("plan".equalsIgnoreCase(mode)) {
|
||||||
|
// 应用教研室计划教员:jybh ← 该行 jysjhjybh
|
||||||
|
jybh = row.getJysjhjybh();
|
||||||
|
if (jybh == null || jybh.isEmpty()) {
|
||||||
|
throw new ServiceException("该行没有教研室计划教员,请先在填报字段中指定", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
} else if ("unit".equalsIgnoreCase(mode) || "academy".equalsIgnoreCase(mode)) {
|
||||||
|
jybh = request.getJybh();
|
||||||
|
if (jybh == null || jybh.isEmpty()) {
|
||||||
|
throw new ServiceException("请选择教员", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
JYB teacher = jybMapper.selectById(jybh);
|
||||||
|
if (teacher == null) {
|
||||||
|
throw new ServiceException("教员不存在:" + jybh, NOT_FOUND);
|
||||||
|
}
|
||||||
|
if ("unit".equalsIgnoreCase(mode)) {
|
||||||
|
if (row.getJysdh() == null || row.getJysdh().isEmpty()) {
|
||||||
|
throw new ServiceException("该行没有归属教研室,无法按责任单位指定", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (!row.getJysdh().equals(teacher.getJysdh())) {
|
||||||
|
throw new ServiceException("教员 " + teacher.getJyxm() + " 不属于该行教研室,不能按责任单位指定", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ServiceException("指定方式只支持 unit / academy / plan", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
return updateGroupField(row, "jybh", jybh);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public int setRoom(TaskBookRoomRequest request) {
|
||||||
|
if (request == null || request.getBh() == null || request.getBh().isEmpty()) {
|
||||||
|
throw new ServiceException("请指定课程任务", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
XYDRWB row = requireRow(request.getBh());
|
||||||
|
assertFillable(row);
|
||||||
|
String jsbh;
|
||||||
|
if (Boolean.TRUE.equals(request.getUseSpecial())) {
|
||||||
|
XYDNDXQJBXXB semester = requireSemester(row.getXydxqbh());
|
||||||
|
jsbh = semester.getZyjsbh();
|
||||||
|
if (jsbh == null || jsbh.isEmpty()) {
|
||||||
|
throw new ServiceException("该班次学期没有专用教室,请改选其它教室", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jsbh = request.getJsbh();
|
||||||
|
if (jsbh == null || jsbh.isEmpty()) {
|
||||||
|
throw new ServiceException("请选择教室,或选择使用班次专用教室", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updateGroupField(row, "jsbh", jsbh);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public int fill(TaskBookFillRequest request) {
|
||||||
|
if (request == null || request.getBh() == null || request.getBh().isEmpty()) {
|
||||||
|
throw new ServiceException("请指定课程任务", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
XYDRWB row = requireRow(request.getBh());
|
||||||
|
assertFillable(row);
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
int count;
|
||||||
|
if (request.getJysjhjybh() != null) {
|
||||||
|
row.setJysjhjybh(request.getJysjhjybh());
|
||||||
|
}
|
||||||
|
if (request.getJysjhbz() != null) {
|
||||||
|
row.setJysjhbz(request.getJysjhbz());
|
||||||
|
}
|
||||||
|
row.setBdsj(now);
|
||||||
|
taskMapper.updateById(row);
|
||||||
|
count = 1;
|
||||||
|
// 场地按合班组同步
|
||||||
|
if (request.getJsbh() != null) {
|
||||||
|
count = updateGroupField(row, "jsbh", request.getJsbh());
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同合班组联动更新:行编组非 0 时,同编组全部行(全局组号唯一)同步该字段;
|
||||||
|
* 编组为 0(未合班)只改本行。
|
||||||
|
*/
|
||||||
|
private int updateGroupField(XYDRWB row, String field, String value) {
|
||||||
|
List<XYDRWB> targets;
|
||||||
|
if (row.getBz2() != null && row.getBz2() != 0) {
|
||||||
|
targets = taskMapper.selectList(new LambdaQueryWrapper<XYDRWB>()
|
||||||
|
.eq(XYDRWB::getBz2, row.getBz2())
|
||||||
|
.eq(XYDRWB::getDelFlag, 0));
|
||||||
|
} else {
|
||||||
|
targets = List.of(row);
|
||||||
|
}
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
for (XYDRWB target : targets) {
|
||||||
|
if ("jybh".equals(field)) {
|
||||||
|
target.setJybh(value);
|
||||||
|
} else if ("jsbh".equals(field)) {
|
||||||
|
target.setJsbh(value);
|
||||||
|
} else {
|
||||||
|
throw new ServiceException("不支持同步字段:" + field, BAD_REQUEST);
|
||||||
|
}
|
||||||
|
target.setBdsj(now);
|
||||||
|
taskMapper.updateById(target);
|
||||||
|
}
|
||||||
|
return targets.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 公共 ====================
|
||||||
|
|
||||||
|
private List<XYDRWB> loadRows(TaskBookBhListRequest request) {
|
||||||
|
if (request == null || request.getBhList() == null || request.getBhList().isEmpty()) {
|
||||||
|
throw new ServiceException("请先选择课程任务行", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
List<XYDRWB> rows = new ArrayList<>();
|
||||||
|
for (String bh : request.getBhList()) {
|
||||||
|
rows.add(requireRow(bh));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private XYDRWB requireRow(String bh) {
|
||||||
|
XYDRWB row = taskMapper.selectById(bh);
|
||||||
|
if (row == null || Integer.valueOf(1).equals(row.getDelFlag())) {
|
||||||
|
throw new ServiceException("课程任务不存在:" + bh, NOT_FOUND);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private XYDNDXQJBXXB requireSemester(String xydxqbh) {
|
||||||
|
XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh);
|
||||||
|
if (semester == null) {
|
||||||
|
throw new ServiceException("班次学期不存在:" + xydxqbh, NOT_FOUND);
|
||||||
|
}
|
||||||
|
return semester;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 填报门禁:仅「已发布且未结束」的教学任务可填报 */
|
||||||
|
private void assertFillable(XYDRWB row) {
|
||||||
|
XYDNDXQJBXXB semester = requireSemester(row.getXydxqbh());
|
||||||
|
JXRW jxrw = semester.getJxrwbh() == null || semester.getJxrwbh().isEmpty()
|
||||||
|
? null : jxrwMapper.selectById(semester.getJxrwbh());
|
||||||
|
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("教学任务尚未发布,不能填报", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
guard.assertTaskWritable(semester.getJxrwbh());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertAllFillable(List<XYDRWB> rows) {
|
||||||
|
for (XYDRWB row : rows) {
|
||||||
|
assertFillable(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+655
@@ -0,0 +1,655 @@
|
|||||||
|
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.allocation.AllocationGroupRequest;
|
||||||
|
import com.roomroot.jwgl.dto.allocation.AllocationSaveItem;
|
||||||
|
import com.roomroot.jwgl.entity.JQB;
|
||||||
|
import com.roomroot.jwgl.entity.KB;
|
||||||
|
import com.roomroot.jwgl.entity.XQXLB;
|
||||||
|
import com.roomroot.jwgl.entity.XYDB;
|
||||||
|
import com.roomroot.jwgl.entity.XYDNDXQJBXXB;
|
||||||
|
import com.roomroot.jwgl.entity.XYDRWB;
|
||||||
|
import com.roomroot.jwgl.mapper.JQBMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.KBMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SemesterCalendarMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.StudentTeamTaskMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.XYDBMapper;
|
||||||
|
import com.roomroot.jwgl.service.TeachingAllocationService;
|
||||||
|
import com.roomroot.jwgl.service.TeachingTaskWriteGuard;
|
||||||
|
import com.roomroot.jwgl.mapper.ClassSemesterMapper;
|
||||||
|
import com.roomroot.jwgl.utils.ExcelExportUtil;
|
||||||
|
import com.roomroot.jwgl.utils.SemesterCodeUtil;
|
||||||
|
import com.roomroot.jwgl.vo.allocation.AllocationTaskVO;
|
||||||
|
import com.roomroot.jwgl.vo.allocation.AllocationViewVO;
|
||||||
|
import com.roomroot.jwgl.vo.allocation.AllocationWeekVO;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.DayOfWeek;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST;
|
||||||
|
import static com.roomroot.jwgl.utils.BasicManagementConstants.NOT_FOUND;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教学配当 Service 实现。
|
||||||
|
*
|
||||||
|
* <p>口径约定(详见各方法注释):
|
||||||
|
* <ul>
|
||||||
|
* <li>「是否连排课」不新增列,用已有 配档按周 pdaz:0=连排(单科独进)、1=按周(非连排);
|
||||||
|
* 周课时 ≥ 20 默认 0,否则默认 1。</li>
|
||||||
|
* <li>「每周可排正课」= 该周 可排课=1 且 正课=1 的格数 × 2 学时;
|
||||||
|
* 数据源优先级 班历 > 校历 > 默认(15 格 = 30 学时)。</li>
|
||||||
|
* <li>铺学时只是粗约束建议:非连排各自成组按起始周+优选序数独立铺;
|
||||||
|
* 连排课合成一组串行铺且优先级低于非连排;不写实施课程表。</li>
|
||||||
|
* <li>「保存整个编组」限定 同科目+同配档编组+同学期代号(6 位),防止跨学期串改。</li>
|
||||||
|
* </ul></p>
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class TeachingAllocationServiceImpl implements TeachingAllocationService {
|
||||||
|
|
||||||
|
/** 双节次一格按 2 学时计 */
|
||||||
|
private static final int HOURS_PER_CELL = 2;
|
||||||
|
/** 班历/校历都没有时的默认每周可排正课:周一~五 1-2/3-4/5-6 共 15 格 */
|
||||||
|
private static final int DEFAULT_WEEKLY_MAIN_CELLS = 15;
|
||||||
|
/** 周课时达到该值默认按连排(配档按周=0) */
|
||||||
|
private static final int CONTINUOUS_ZKS_THRESHOLD = 20;
|
||||||
|
/** 默认每周学时 */
|
||||||
|
private static final int DEFAULT_WEEKLY_HOURS = DEFAULT_WEEKLY_MAIN_CELLS * HOURS_PER_CELL;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private StudentTeamTaskMapper xydrwbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ClassSemesterMapper classSemesterMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private XYDBMapper xydbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JQBMapper jqbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SemesterCalendarMapper semesterCalendarMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KBMapper kbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TeachingTaskWriteGuard teachingTaskWriteGuard;
|
||||||
|
|
||||||
|
// ==================== 视图 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AllocationViewVO view(String xydxqbh) {
|
||||||
|
XYDNDXQJBXXB semester = requireSemester(xydxqbh);
|
||||||
|
XYDB clazz = xydbMapper.selectById(semester.getXydbh());
|
||||||
|
|
||||||
|
AllocationViewVO vo = new AllocationViewVO();
|
||||||
|
vo.setXydxqbh(xydxqbh);
|
||||||
|
vo.setXydbh(semester.getXydbh());
|
||||||
|
vo.setXydmc(clazz != null ? clazz.getXydmc() : semester.getXydbh());
|
||||||
|
vo.setNdCode(SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc()));
|
||||||
|
vo.setKxrq(semester.getKxrq());
|
||||||
|
vo.setJsrq(semester.getJsrq());
|
||||||
|
vo.setFrozen(isFrozen(xydxqbh));
|
||||||
|
|
||||||
|
List<AllocationWeekVO> weeks = buildWeeks(semester);
|
||||||
|
vo.setWeeks(weeks);
|
||||||
|
vo.setTotalWeeks(weeks.size());
|
||||||
|
|
||||||
|
List<XYDRWB> tasks = listTasks(xydxqbh);
|
||||||
|
Map<String, AllocationTaskVO> voMap = new LinkedHashMap<>();
|
||||||
|
for (XYDRWB task : tasks) {
|
||||||
|
voMap.put(task.getBh(), toVO(task));
|
||||||
|
}
|
||||||
|
allocate(weeks, tasks, voMap);
|
||||||
|
vo.setTasks(new ArrayList<>(voMap.values()));
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isFrozen(String xydxqbh) {
|
||||||
|
try {
|
||||||
|
teachingTaskWriteGuard.assertCourseTaskWritable(xydxqbh);
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<XYDRWB> listTasks(String xydxqbh) {
|
||||||
|
List<XYDRWB> tasks = xydrwbMapper.selectList(new LambdaQueryWrapper<XYDRWB>()
|
||||||
|
.eq(XYDRWB::getXydxqbh, xydxqbh)
|
||||||
|
.eq(XYDRWB::getDelFlag, 0));
|
||||||
|
for (XYDRWB task : tasks) {
|
||||||
|
// 连排缺省:周课时 >= 20 默认连排(配档按周=0),否则按周
|
||||||
|
if (task.getPdaz() == null) {
|
||||||
|
task.setPdaz(defaultPdaz(task.getZks()));
|
||||||
|
}
|
||||||
|
if (task.getPdbz() == null) {
|
||||||
|
task.setPdbz(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tasks.sort(Comparator
|
||||||
|
.comparing((XYDRWB t) -> t.getPdxh() == null ? Integer.MAX_VALUE : t.getPdxh())
|
||||||
|
.thenComparing(XYDRWB::getBh));
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int defaultPdaz(Integer zks) {
|
||||||
|
return zks != null && zks >= CONTINUOUS_ZKS_THRESHOLD ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AllocationTaskVO toVO(XYDRWB task) {
|
||||||
|
AllocationTaskVO vo = new AllocationTaskVO();
|
||||||
|
vo.setBh(task.getBh());
|
||||||
|
vo.setKbh(task.getKbh());
|
||||||
|
vo.setKcmc(resolveCourseName(task));
|
||||||
|
vo.setKlx(task.getKlx());
|
||||||
|
vo.setXs(task.getXs());
|
||||||
|
vo.setZks(task.getZks());
|
||||||
|
vo.setPdxh(task.getPdxh());
|
||||||
|
vo.setPdqsz(task.getPdqsz());
|
||||||
|
vo.setPdaz(task.getPdaz());
|
||||||
|
vo.setPdzzksj(task.getPdzzksj());
|
||||||
|
vo.setPdbz(task.getPdbz());
|
||||||
|
vo.setJysdh(task.getJysdh());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveCourseName(XYDRWB task) {
|
||||||
|
if (task.getJc() != null && !task.getJc().isEmpty()) {
|
||||||
|
return task.getJc();
|
||||||
|
}
|
||||||
|
if (task.getKbh() == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
KB kb = kbMapper.selectById(task.getKbh());
|
||||||
|
return kb != null && kb.getKmc() != null ? kb.getKmc() : task.getKbh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 周轴与每周可排正课 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 学期起止按周切分(周一为周界,覆盖 [开学日期, 结束日期]),并统计每周可排正课。
|
||||||
|
*/
|
||||||
|
private List<AllocationWeekVO> buildWeeks(XYDNDXQJBXXB semester) {
|
||||||
|
LocalDate kxrq = semester.getKxrq();
|
||||||
|
LocalDate jsrq = semester.getJsrq();
|
||||||
|
if (kxrq == null || jsrq == null) {
|
||||||
|
throw new ServiceException("班次学期缺少开学/结束日期,无法生成配当周轴", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
boolean hasClassCalendar = jqbMapper.selectCount(new LambdaQueryWrapper<JQB>()
|
||||||
|
.eq(JQB::getXydxqbh, semester.getBh())) > 0;
|
||||||
|
Integer schoolNd = SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc());
|
||||||
|
|
||||||
|
List<AllocationWeekVO> weeks = new ArrayList<>();
|
||||||
|
LocalDate weekStart = kxrq.with(DayOfWeek.MONDAY);
|
||||||
|
int weekNo = 1;
|
||||||
|
while (!weekStart.isAfter(jsrq)) {
|
||||||
|
LocalDate weekEnd = weekStart.plusDays(6);
|
||||||
|
AllocationWeekVO week = new AllocationWeekVO();
|
||||||
|
week.setWeekNo(weekNo);
|
||||||
|
week.setStartDate(weekStart);
|
||||||
|
week.setEndDate(weekEnd.isAfter(jsrq) ? jsrq : weekEnd);
|
||||||
|
|
||||||
|
int hours;
|
||||||
|
String source;
|
||||||
|
if (hasClassCalendar) {
|
||||||
|
hours = countClassMainHours(semester.getBh(), weekStart, weekEnd) * HOURS_PER_CELL;
|
||||||
|
source = "class";
|
||||||
|
if (hours <= 0) {
|
||||||
|
// 班历未覆盖该周时按默认容量兜底,避免连排计算被卡死
|
||||||
|
hours = DEFAULT_WEEKLY_HOURS;
|
||||||
|
source = "default";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int cells = countSchoolMainCells(schoolNd, weekStart, weekEnd);
|
||||||
|
if (cells > 0) {
|
||||||
|
hours = cells * HOURS_PER_CELL;
|
||||||
|
source = "school";
|
||||||
|
} else {
|
||||||
|
hours = DEFAULT_WEEKLY_HOURS;
|
||||||
|
source = "default";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
week.setAvailableHours(hours);
|
||||||
|
week.setSource(source);
|
||||||
|
weeks.add(week);
|
||||||
|
weekNo++;
|
||||||
|
weekStart = weekStart.plusDays(7);
|
||||||
|
}
|
||||||
|
return weeks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 班历:该周 可排课=1 且 正课=1 的格数 */
|
||||||
|
private int countClassMainHours(String xydxqbh, LocalDate start, LocalDate end) {
|
||||||
|
List<JQB> rows = jqbMapper.selectList(new LambdaQueryWrapper<JQB>()
|
||||||
|
.eq(JQB::getXydxqbh, xydxqbh)
|
||||||
|
.ge(JQB::getJqsj, toIntDay(start))
|
||||||
|
.le(JQB::getJqsj, toIntDay(end)));
|
||||||
|
return (int) rows.stream()
|
||||||
|
.filter(r -> Integer.valueOf(1).equals(r.getKpk()) && Integer.valueOf(1).equals(r.getZk()))
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校历:该周 可排课=1 且 正课=1 的格数 */
|
||||||
|
private int countSchoolMainCells(Integer schoolNd, LocalDate start, LocalDate end) {
|
||||||
|
if (schoolNd == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
List<XQXLB> rows = semesterCalendarMapper.selectList(new LambdaQueryWrapper<XQXLB>()
|
||||||
|
.eq(XQXLB::getNd, schoolNd)
|
||||||
|
.ge(XQXLB::getJqsj, start.toString())
|
||||||
|
.le(XQXLB::getJqsj, end.toString()));
|
||||||
|
return (int) rows.stream()
|
||||||
|
.filter(r -> Boolean.TRUE.equals(r.getKpk()) && Boolean.TRUE.equals(r.getZk()))
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer toIntDay(LocalDate day) {
|
||||||
|
return Integer.valueOf(day.toString().replace("-", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 铺学时 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 铺学时(粗约束建议,不落库):
|
||||||
|
* 非连排(pdaz=1)各自成组,从配档起始周起每周铺周课时,按优选序数排序依次铺;
|
||||||
|
* 连排(pdaz=0)合成一组串行铺(单科独进,每周占满剩余容量),优先级低于非连排。
|
||||||
|
*/
|
||||||
|
private void allocate(List<AllocationWeekVO> weeks, List<XYDRWB> tasks, Map<String, AllocationTaskVO> voMap) {
|
||||||
|
int totalWeeks = weeks.size();
|
||||||
|
int[] capacity = new int[Math.max(totalWeeks, 1)];
|
||||||
|
for (int i = 0; i < totalWeeks; i++) {
|
||||||
|
capacity[i] = weeks.get(i).getAvailableHours() == null ? 0 : weeks.get(i).getAvailableHours();
|
||||||
|
}
|
||||||
|
Map<Integer, Integer> continuousUsed = new HashMap<>();
|
||||||
|
|
||||||
|
List<XYDRWB> nonContinuous = new ArrayList<>();
|
||||||
|
List<XYDRWB> continuous = new ArrayList<>();
|
||||||
|
for (XYDRWB task : tasks) {
|
||||||
|
(Integer.valueOf(0).equals(task.getPdaz()) ? continuous : nonContinuous).add(task);
|
||||||
|
}
|
||||||
|
nonContinuous.sort(Comparator
|
||||||
|
.comparing((XYDRWB t) -> t.getPdqsz() == null ? 1 : t.getPdqsz())
|
||||||
|
.thenComparing(t -> t.getPdxh() == null ? Integer.MAX_VALUE : t.getPdxh())
|
||||||
|
.thenComparing(XYDRWB::getBh));
|
||||||
|
continuous.sort(Comparator
|
||||||
|
.comparing((XYDRWB t) -> t.getPdxh() == null ? Integer.MAX_VALUE : t.getPdxh())
|
||||||
|
.thenComparing(t -> t.getPdqsz() == null ? 1 : t.getPdqsz())
|
||||||
|
.thenComparing(XYDRWB::getBh));
|
||||||
|
|
||||||
|
for (XYDRWB task : nonContinuous) {
|
||||||
|
fillByWeek(voMap.get(task.getBh()), task, capacity, totalWeeks, null);
|
||||||
|
}
|
||||||
|
int cursor = 1;
|
||||||
|
for (XYDRWB task : continuous) {
|
||||||
|
cursor = fillContinuous(voMap.get(task.getBh()), task, capacity, totalWeeks, continuousUsed, cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 非连排:各自独立铺,不与其它任务抢容量(粗约束) */
|
||||||
|
private void fillByWeek(AllocationTaskVO vo, XYDRWB task, int[] capacity, int totalWeeks, Map<Integer, Integer> used) {
|
||||||
|
if (vo == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int zks = task.getZks() != null && task.getZks() > 0 ? task.getZks() : 2;
|
||||||
|
int remaining = task.getXs() != null ? task.getXs() : 0;
|
||||||
|
int week = task.getPdqsz() != null && task.getPdqsz() > 0 ? task.getPdqsz() : 1;
|
||||||
|
while (remaining > 0 && week <= totalWeeks) {
|
||||||
|
int hours = Math.min(remaining, zks);
|
||||||
|
vo.getDistribution().add(new AllocationTaskVO.WeekHours(week, hours));
|
||||||
|
if (used != null) {
|
||||||
|
used.merge(week, hours, Integer::sum);
|
||||||
|
}
|
||||||
|
remaining -= hours;
|
||||||
|
week++;
|
||||||
|
}
|
||||||
|
finish(vo, remaining, totalWeeks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 连排:串行铺,每周占满剩余容量(单科独进),组内不重叠 */
|
||||||
|
private int fillContinuous(AllocationTaskVO vo, XYDRWB task, int[] capacity, int totalWeeks,
|
||||||
|
Map<Integer, Integer> used, int cursor) {
|
||||||
|
if (vo == null) {
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
int remaining = task.getXs() != null ? task.getXs() : 0;
|
||||||
|
int week = Math.max(cursor, task.getPdqsz() != null && task.getPdqsz() > 0 ? task.getPdqsz() : 1);
|
||||||
|
while (remaining > 0 && week <= totalWeeks) {
|
||||||
|
int left = capacity[week - 1] - used.getOrDefault(week, 0);
|
||||||
|
if (left <= 0) {
|
||||||
|
week++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int hours = Math.min(remaining, left);
|
||||||
|
vo.getDistribution().add(new AllocationTaskVO.WeekHours(week, hours));
|
||||||
|
used.merge(week, hours, Integer::sum);
|
||||||
|
remaining -= hours;
|
||||||
|
week++;
|
||||||
|
}
|
||||||
|
finish(vo, remaining, totalWeeks);
|
||||||
|
return Math.max(cursor, vo.getEndWeek() == null ? cursor : vo.getEndWeek());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finish(AllocationTaskVO vo, int overflow, int totalWeeks) {
|
||||||
|
List<AllocationTaskVO.WeekHours> dist = vo.getDistribution();
|
||||||
|
if (!dist.isEmpty()) {
|
||||||
|
vo.setStartWeek(dist.get(0).getWeek());
|
||||||
|
vo.setEndWeek(dist.get(dist.size() - 1).getWeek());
|
||||||
|
}
|
||||||
|
if (overflow > 0) {
|
||||||
|
vo.setOverflowHours(overflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 保存 / 排序 / 编组 ====================
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public int save(List<AllocationSaveItem> items) {
|
||||||
|
if (items == null || items.isEmpty()) {
|
||||||
|
throw new ServiceException("请先选择要保存的配当行", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
int count = 0;
|
||||||
|
for (AllocationSaveItem item : items) {
|
||||||
|
count += saveOne(item);
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int saveOne(AllocationSaveItem item) {
|
||||||
|
if (item == null || item.getBh() == null || item.getBh().isEmpty()) {
|
||||||
|
throw new ServiceException("保存项缺少任务编号", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
validate(item);
|
||||||
|
XYDRWB task = xydrwbMapper.selectById(item.getBh());
|
||||||
|
if (task == null || Integer.valueOf(1).equals(task.getDelFlag())) {
|
||||||
|
throw new ServiceException("课程任务不存在:" + item.getBh(), NOT_FOUND);
|
||||||
|
}
|
||||||
|
teachingTaskWriteGuard.assertCourseTaskWritable(task.getXydxqbh());
|
||||||
|
|
||||||
|
List<XYDRWB> targets;
|
||||||
|
if (Boolean.TRUE.equals(item.getSaveGroup())) {
|
||||||
|
Integer pdbz = task.getPdbz() == null ? 0 : task.getPdbz();
|
||||||
|
// 同科目 + 同配档编组 + 同学期代号:同开同结;限定学期代号防止跨学期串改
|
||||||
|
targets = xydrwbMapper.selectList(new LambdaQueryWrapper<XYDRWB>()
|
||||||
|
.eq(XYDRWB::getKbh, task.getKbh())
|
||||||
|
.eq(XYDRWB::getPdbz, pdbz)
|
||||||
|
.eq(XYDRWB::getNd, task.getNd())
|
||||||
|
.eq(XYDRWB::getDelFlag, 0));
|
||||||
|
} else {
|
||||||
|
targets = List.of(task);
|
||||||
|
}
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
for (XYDRWB t : targets) {
|
||||||
|
if (item.getPdxh() != null) {
|
||||||
|
t.setPdxh(item.getPdxh());
|
||||||
|
}
|
||||||
|
if (item.getPdqsz() != null) {
|
||||||
|
t.setPdqsz(item.getPdqsz());
|
||||||
|
}
|
||||||
|
if (item.getPdaz() != null) {
|
||||||
|
t.setPdaz(item.getPdaz());
|
||||||
|
}
|
||||||
|
if (item.getPdzzksj() != null) {
|
||||||
|
t.setPdzzksj(item.getPdzzksj());
|
||||||
|
}
|
||||||
|
if (item.getPdbz() != null) {
|
||||||
|
t.setPdbz(item.getPdbz());
|
||||||
|
}
|
||||||
|
t.setBdsj(now);
|
||||||
|
xydrwbMapper.updateById(t);
|
||||||
|
}
|
||||||
|
return targets.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validate(AllocationSaveItem item) {
|
||||||
|
if (item.getPdxh() != null && item.getPdxh() < 1) {
|
||||||
|
throw new ServiceException("配档序号必须 ≥ 1", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (item.getPdqsz() != null && item.getPdqsz() < 1) {
|
||||||
|
throw new ServiceException("配档起始周必须 ≥ 1", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (item.getPdaz() != null && item.getPdaz() != 0 && item.getPdaz() != 1) {
|
||||||
|
throw new ServiceException("配档按周只能为 0(连排)或 1(按周)", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (item.getPdzzksj() != null && item.getPdzzksj() != 0 && item.getPdzzksj() != 1) {
|
||||||
|
throw new ServiceException("配档占正课时间只能为 0 或 1", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (item.getPdbz() != null && item.getPdbz() < 0) {
|
||||||
|
throw new ServiceException("配档编组必须 ≥ 0", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public List<String> move(String xydxqbh, String bh, String direction) {
|
||||||
|
teachingTaskWriteGuard.assertCourseTaskWritable(xydxqbh);
|
||||||
|
boolean up = "up".equalsIgnoreCase(direction);
|
||||||
|
if (!up && !"down".equalsIgnoreCase(direction)) {
|
||||||
|
throw new ServiceException("方向只支持 up / down", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
List<XYDRWB> tasks = listTasks(xydxqbh);
|
||||||
|
if (tasks.isEmpty()) {
|
||||||
|
throw new ServiceException("该班次学期没有课程任务", NOT_FOUND);
|
||||||
|
}
|
||||||
|
// 归一化优选序数 1..n(与展示顺序一致)
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
int idx = -1;
|
||||||
|
for (int i = 0; i < tasks.size(); i++) {
|
||||||
|
XYDRWB t = tasks.get(i);
|
||||||
|
if (!Objects.equals(t.getPdxh(), i + 1)) {
|
||||||
|
t.setPdxh(i + 1);
|
||||||
|
t.setBdsj(now);
|
||||||
|
xydrwbMapper.updateById(t);
|
||||||
|
}
|
||||||
|
if (t.getBh().equals(bh)) {
|
||||||
|
idx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (idx < 0) {
|
||||||
|
throw new ServiceException("课程任务不属于该班次学期:" + bh, NOT_FOUND);
|
||||||
|
}
|
||||||
|
int other = up ? idx - 1 : idx + 1;
|
||||||
|
if (other < 0) {
|
||||||
|
throw new ServiceException("已经是最前面了", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (other >= tasks.size()) {
|
||||||
|
throw new ServiceException("已经是最后面了", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
XYDRWB a = tasks.get(idx);
|
||||||
|
XYDRWB b = tasks.get(other);
|
||||||
|
Integer tmp = a.getPdxh();
|
||||||
|
a.setPdxh(b.getPdxh());
|
||||||
|
b.setPdxh(tmp);
|
||||||
|
a.setBdsj(now);
|
||||||
|
b.setBdsj(now);
|
||||||
|
xydrwbMapper.updateById(a);
|
||||||
|
xydrwbMapper.updateById(b);
|
||||||
|
// 返回顺序必须反映交换后的新顺序(供前端直接刷新),而不是归一化时的旧顺序
|
||||||
|
java.util.Collections.swap(tasks, idx, other);
|
||||||
|
|
||||||
|
return tasks.stream().map(XYDRWB::getBh).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> groups(String kbh) {
|
||||||
|
if (kbh == null || kbh.isEmpty()) {
|
||||||
|
throw new ServiceException("请指定课编号", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
List<XYDRWB> tasks = xydrwbMapper.selectList(new LambdaQueryWrapper<XYDRWB>()
|
||||||
|
.eq(XYDRWB::getKbh, kbh)
|
||||||
|
.eq(XYDRWB::getDelFlag, 0));
|
||||||
|
tasks.sort(Comparator
|
||||||
|
.comparing((XYDRWB t) -> t.getNd() == null ? 0 : t.getNd())
|
||||||
|
.thenComparing(t -> t.getPdbz() == null ? 0 : t.getPdbz())
|
||||||
|
.thenComparing(XYDRWB::getBh));
|
||||||
|
List<Map<String, Object>> rows = new ArrayList<>();
|
||||||
|
for (XYDRWB task : tasks) {
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("bh", task.getBh());
|
||||||
|
row.put("kbh", task.getKbh());
|
||||||
|
row.put("kcmc", resolveCourseName(task));
|
||||||
|
row.put("nd", task.getNd());
|
||||||
|
XYDB clazz = task.getXydbh() == null ? null : xydbMapper.selectById(task.getXydbh());
|
||||||
|
row.put("xydbh", task.getXydbh());
|
||||||
|
row.put("xydmc", clazz != null ? clazz.getXydmc() : task.getXydbh());
|
||||||
|
row.put("xs", task.getXs());
|
||||||
|
row.put("zks", task.getZks());
|
||||||
|
row.put("pdxh", task.getPdxh());
|
||||||
|
row.put("pdqsz", task.getPdqsz());
|
||||||
|
row.put("pdaz", task.getPdaz() == null ? defaultPdaz(task.getZks()) : task.getPdaz());
|
||||||
|
row.put("pdbz", task.getPdbz() == null ? 0 : task.getPdbz());
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public int setGroup(AllocationGroupRequest request) {
|
||||||
|
if (request == null || request.getBhList() == null || request.getBhList().isEmpty()) {
|
||||||
|
throw new ServiceException("请先选择要改编组的任务", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (request.getPdbz() == null || request.getPdbz() < 0) {
|
||||||
|
throw new ServiceException("配档编组必须 ≥ 0", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
int count = 0;
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
for (String bh : request.getBhList()) {
|
||||||
|
XYDRWB task = xydrwbMapper.selectById(bh);
|
||||||
|
if (task == null || Integer.valueOf(1).equals(task.getDelFlag())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
teachingTaskWriteGuard.assertCourseTaskWritable(task.getXydxqbh());
|
||||||
|
if (!Objects.equals(task.getPdbz(), request.getPdbz())) {
|
||||||
|
task.setPdbz(request.getPdbz());
|
||||||
|
task.setBdsj(now);
|
||||||
|
xydrwbMapper.updateById(task);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 导出 ====================
|
||||||
|
|
||||||
|
/** 导出行(固定列,便于 ExcelExportUtil 取值) */
|
||||||
|
public static class AllocationExportRow {
|
||||||
|
private final String xydmc;
|
||||||
|
private final Integer ndCode;
|
||||||
|
private final String kbh;
|
||||||
|
private final String kcmc;
|
||||||
|
private final String klx;
|
||||||
|
private final Integer xs;
|
||||||
|
private final Integer zks;
|
||||||
|
private final Integer pdxh;
|
||||||
|
private final Integer pdqsz;
|
||||||
|
private final String pdazText;
|
||||||
|
private final Integer pdzzksj;
|
||||||
|
private final Integer pdbz;
|
||||||
|
private final Integer startWeek;
|
||||||
|
private final Integer endWeek;
|
||||||
|
private final Integer overflowHours;
|
||||||
|
|
||||||
|
AllocationExportRow(String xydmc, Integer ndCode, XYDRWB task, AllocationTaskVO vo) {
|
||||||
|
this.xydmc = xydmc;
|
||||||
|
this.ndCode = ndCode;
|
||||||
|
this.kbh = task.getKbh();
|
||||||
|
this.kcmc = vo.getKcmc();
|
||||||
|
this.klx = task.getKlx();
|
||||||
|
this.xs = task.getXs();
|
||||||
|
this.zks = task.getZks();
|
||||||
|
this.pdxh = task.getPdxh();
|
||||||
|
this.pdqsz = task.getPdqsz();
|
||||||
|
this.pdazText = Objects.equals(task.getPdaz(), 0) ? "连排" : "按周";
|
||||||
|
this.pdzzksj = task.getPdzzksj();
|
||||||
|
this.pdbz = task.getPdbz() == null ? 0 : task.getPdbz();
|
||||||
|
this.startWeek = vo.getStartWeek();
|
||||||
|
this.endWeek = vo.getEndWeek();
|
||||||
|
this.overflowHours = vo.getOverflowHours();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getXydmc() { return xydmc; }
|
||||||
|
public Integer getNdCode() { return ndCode; }
|
||||||
|
public String getKbh() { return kbh; }
|
||||||
|
public String getKcmc() { return kcmc; }
|
||||||
|
public String getKlx() { return klx; }
|
||||||
|
public Integer getXs() { return xs; }
|
||||||
|
public Integer getZks() { return zks; }
|
||||||
|
public Integer getPdxh() { return pdxh; }
|
||||||
|
public Integer getPdqsz() { return pdqsz; }
|
||||||
|
public String getPdazText() { return pdazText; }
|
||||||
|
public Integer getPdzzksj() { return pdzzksj; }
|
||||||
|
public Integer getPdbz() { return pdbz; }
|
||||||
|
public Integer getStartWeek() { return startWeek; }
|
||||||
|
public Integer getEndWeek() { return endWeek; }
|
||||||
|
public Integer getOverflowHours() { return overflowHours; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void exportExcel(List<String> xydxqbhList, HttpServletResponse response) throws Exception {
|
||||||
|
if (xydxqbhList == null || xydxqbhList.isEmpty()) {
|
||||||
|
throw new ServiceException("请先选择要导出的班次学期", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
List<AllocationExportRow> rows = new ArrayList<>();
|
||||||
|
for (String xydxqbh : xydxqbhList) {
|
||||||
|
XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh);
|
||||||
|
if (semester == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
XYDB clazz = xydbMapper.selectById(semester.getXydbh());
|
||||||
|
String xydmc = clazz != null ? clazz.getXydmc() : semester.getXydbh();
|
||||||
|
Integer ndCode = SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc());
|
||||||
|
AllocationViewVO view = view(xydxqbh);
|
||||||
|
Map<String, AllocationTaskVO> voMap = new HashMap<>();
|
||||||
|
view.getTasks().forEach(t -> voMap.put(t.getBh(), t));
|
||||||
|
for (XYDRWB task : listTasks(xydxqbh)) {
|
||||||
|
rows.add(new AllocationExportRow(xydmc, ndCode, task, voMap.get(task.getBh())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
throw new ServiceException("所选班次学期没有课程任务可导出", NOT_FOUND);
|
||||||
|
}
|
||||||
|
String[] headers = {"班次名称", "学期代号", "课编号", "课程", "课类型", "学时", "周课时",
|
||||||
|
"配档序号", "配档起始周", "连排/按周", "占正课时间", "配档编组", "计算起始周", "计算结束周", "溢出学时"};
|
||||||
|
byte[] bytes = ExcelExportUtil.export("教学配当", headers, rows,
|
||||||
|
AllocationExportRow::getXydmc, AllocationExportRow::getNdCode, AllocationExportRow::getKbh,
|
||||||
|
AllocationExportRow::getKcmc, AllocationExportRow::getKlx, AllocationExportRow::getXs,
|
||||||
|
AllocationExportRow::getZks, AllocationExportRow::getPdxh, AllocationExportRow::getPdqsz,
|
||||||
|
AllocationExportRow::getPdazText, AllocationExportRow::getPdzzksj, AllocationExportRow::getPdbz,
|
||||||
|
AllocationExportRow::getStartWeek, AllocationExportRow::getEndWeek, AllocationExportRow::getOverflowHours);
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
response.setCharacterEncoding("utf-8");
|
||||||
|
String fileName = java.net.URLEncoder.encode("教学配当.xlsx", java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
|
||||||
|
response.getOutputStream().write(bytes);
|
||||||
|
response.getOutputStream().flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 公共 ====================
|
||||||
|
|
||||||
|
private XYDNDXQJBXXB requireSemester(String xydxqbh) {
|
||||||
|
if (xydxqbh == null || xydxqbh.isEmpty()) {
|
||||||
|
throw new ServiceException("请指定班次学期", BAD_REQUEST);
|
||||||
|
}
|
||||||
|
XYDNDXQJBXXB semester = classSemesterMapper.selectById(xydxqbh);
|
||||||
|
if (semester == null) {
|
||||||
|
throw new ServiceException("班次学期不存在:" + xydxqbh, NOT_FOUND);
|
||||||
|
}
|
||||||
|
return semester;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -379,7 +379,7 @@ public class TeachingTaskManageServiceImpl implements TeachingTaskManageService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
studentTeamTaskService.autoGenerateRequiredCourses(squad.getXydbh(), squad.getXqdc());
|
studentTeamTaskService.autoGenerateRequiredCourses(squad.getBh());
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
// 课程任务已存在或计划缺失时,仍继续生成教研室任务书
|
// 课程任务已存在或计划缺失时,仍继续生成教研室任务书
|
||||||
}
|
}
|
||||||
|
|||||||
+245
-2
@@ -2,11 +2,26 @@ package com.roomroot.jwgl.service.impl;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.roomroot.jwgl.entity.JQB;
|
||||||
|
import com.roomroot.jwgl.entity.JXCDL;
|
||||||
|
import com.roomroot.jwgl.entity.SSKC;
|
||||||
|
import com.roomroot.jwgl.entity.SSKCB;
|
||||||
|
import com.roomroot.jwgl.entity.SSKCXYD;
|
||||||
import com.roomroot.jwgl.entity.TimetableConflictResult;
|
import com.roomroot.jwgl.entity.TimetableConflictResult;
|
||||||
|
import com.roomroot.jwgl.entity.XYDB;
|
||||||
|
import com.roomroot.jwgl.entity.XQXLB;
|
||||||
|
import com.roomroot.jwgl.mapper.ClassRoomCalendarMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.JQBMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.KBMapper;
|
||||||
import com.roomroot.jwgl.mapper.SSKCBFZJYMapper;
|
import com.roomroot.jwgl.mapper.SSKCBFZJYMapper;
|
||||||
import com.roomroot.jwgl.mapper.SSKCBJSMapper;
|
import com.roomroot.jwgl.mapper.SSKCBJSMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCBMapper;
|
||||||
import com.roomroot.jwgl.mapper.SSKCBXYDMapper;
|
import com.roomroot.jwgl.mapper.SSKCBXYDMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SSKCXYDMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.SemesterCalendarMapper;
|
||||||
import com.roomroot.jwgl.mapper.TimetableConflictResultMapper;
|
import com.roomroot.jwgl.mapper.TimetableConflictResultMapper;
|
||||||
|
import com.roomroot.jwgl.mapper.XYDBMapper;
|
||||||
import com.roomroot.jwgl.service.TimetableConflictService;
|
import com.roomroot.jwgl.service.TimetableConflictService;
|
||||||
import com.roomroot.jwgl.unit.PageResult;
|
import com.roomroot.jwgl.unit.PageResult;
|
||||||
import com.roomroot.jwgl.unit.conflict.TimetableConflictDimension;
|
import com.roomroot.jwgl.unit.conflict.TimetableConflictDimension;
|
||||||
@@ -17,6 +32,7 @@ import jakarta.annotation.Resource;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
@@ -47,6 +63,10 @@ public class TimetableConflictServiceImpl implements TimetableConflictService {
|
|||||||
@Resource
|
@Resource
|
||||||
private SSKCBXYDMapper sskcbxydMapper;
|
private SSKCBXYDMapper sskcbxydMapper;
|
||||||
|
|
||||||
|
/** 实施_课程学员队(注意与 SSKCBXYDMapper=实施_课程表_学员队 是两张表) */
|
||||||
|
@Resource
|
||||||
|
private com.roomroot.jwgl.mapper.SSKCXYDMapper sskcxydMapper;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private SSKCBFZJYMapper sskcbfzjyMapper;
|
private SSKCBFZJYMapper sskcbfzjyMapper;
|
||||||
|
|
||||||
@@ -59,6 +79,28 @@ public class TimetableConflictServiceImpl implements TimetableConflictService {
|
|||||||
@Resource
|
@Resource
|
||||||
private TimetableConflictResultMapper conflictResultMapper;
|
private TimetableConflictResultMapper conflictResultMapper;
|
||||||
|
|
||||||
|
// 阶段 5.1:EVENT_CONFLICT / 场地历不可用 需要读取的表
|
||||||
|
@Resource
|
||||||
|
private SemesterCalendarMapper semesterCalendarMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JQBMapper jqbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ClassRoomCalendarMapper classRoomCalendarMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCBMapper sskcbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSKCMapper sskcMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private XYDBMapper xydbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KBMapper kbMapper;
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// 内存缓存(不需要持久化到数据库,临时保存每次检查结果)
|
// 内存缓存(不需要持久化到数据库,临时保存每次检查结果)
|
||||||
// Key 格式:"<年度>_<维度编码>" 例:"2026_TEACHER_CONFLICT"
|
// Key 格式:"<年度>_<维度编码>" 例:"2026_TEACHER_CONFLICT"
|
||||||
@@ -210,9 +252,19 @@ public class TimetableConflictServiceImpl implements TimetableConflictService {
|
|||||||
case TEAM_CONFLICT -> sskcbxydMapper.selectTeamConflictDetails(nd);
|
case TEAM_CONFLICT -> sskcbxydMapper.selectTeamConflictDetails(nd);
|
||||||
case ELECTIVE_REQUIRED_CONFLICT -> sskcbxydMapper.selectElectiveRequiredConflictDetails(nd);
|
case ELECTIVE_REQUIRED_CONFLICT -> sskcbxydMapper.selectElectiveRequiredConflictDetails(nd);
|
||||||
case TEACHER_CONFLICT -> sskcbfzjyMapper.selectTeacherConflictDetails(nd);
|
case TEACHER_CONFLICT -> sskcbfzjyMapper.selectTeacherConflictDetails(nd);
|
||||||
case CLASSROOM_CONFLICT -> sskcbjsMapper.selectClassroomConflictDetails(nd);
|
case CLASSROOM_CONFLICT -> {
|
||||||
case GUARANTEE_CONFLICT, EVENT_CONFLICT -> Collections.emptyList();
|
// 阶段 5.1:教室重复占用 + 场地历不可用
|
||||||
|
List<TimetableConflictDetailVO> dup = sskcbjsMapper.selectClassroomConflictDetails(nd);
|
||||||
|
dup.addAll(checkClassroomCalendarConflicts(nd));
|
||||||
|
yield dup;
|
||||||
|
}
|
||||||
|
case EVENT_CONFLICT -> checkEventConflicts(nd);
|
||||||
|
case GUARANTEE_CONFLICT -> Collections.emptyList();
|
||||||
};
|
};
|
||||||
|
// 本检查器的维度全部为硬冲突(双占 / 三类历不可排);软提示由排课窗实时计算
|
||||||
|
if (details != null) {
|
||||||
|
details.forEach(d -> d.setLevel("hard"));
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[课表冲突检查][维度:{}][年度:{}] 执行SQL失败: {}",
|
log.error("[课表冲突检查][维度:{}][年度:{}] 执行SQL失败: {}",
|
||||||
dimension.getCode(), nd, e.getMessage(), e);
|
dimension.getCode(), nd, e.getMessage(), e);
|
||||||
@@ -435,4 +487,195 @@ public class TimetableConflictServiceImpl implements TimetableConflictService {
|
|||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// 阶段 5.1:EVENT_CONFLICT(校历/班历不可排课格)与场地历不可用
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EVENT_CONFLICT:读取 学期校历表、假期表 的不可排课格,与已排课次求交。
|
||||||
|
* 校历不可排课格上有已排课次 → 硬冲突;班历对某学员队不可排课格上有该队已排课次 → 硬冲突。
|
||||||
|
*/
|
||||||
|
private List<TimetableConflictDetailVO> checkEventConflicts(Integer nd) {
|
||||||
|
List<TimetableConflictDetailVO> result = new ArrayList<>();
|
||||||
|
// 已排课次(未删除、有日期)
|
||||||
|
List<SSKCB> lessons = sskcbMapper.selectList(new LambdaQueryWrapper<SSKCB>()
|
||||||
|
.eq(SSKCB::getNd, nd)
|
||||||
|
.eq(SSKCB::getSczt, 0)
|
||||||
|
.isNotNull(SSKCB::getRq));
|
||||||
|
if (lessons.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Map<String, String> courseNames = loadCourseNames(nd);
|
||||||
|
|
||||||
|
// ---- 校历:可排课=0 的格 ----
|
||||||
|
Map<String, List<SSKCB>> byGrid = lessons.stream()
|
||||||
|
.filter(l -> l.getRq() != null && l.getJc() != null)
|
||||||
|
.collect(Collectors.groupingBy(l -> l.getRq().toLocalDate().toString() + "#" + l.getJc()));
|
||||||
|
List<XQXLB> schoolUnavailable = semesterCalendarMapper.selectList(new LambdaQueryWrapper<XQXLB>()
|
||||||
|
.eq(XQXLB::getNd, nd)
|
||||||
|
.eq(XQXLB::getKpk, false));
|
||||||
|
for (XQXLB grid : schoolUnavailable) {
|
||||||
|
for (Integer jc : parsePeriods(grid.getCourseClass())) {
|
||||||
|
List<SSKCB> hits = byGrid.get(grid.getJqsj() + "#" + jc);
|
||||||
|
if (hits != null && !hits.isEmpty()) {
|
||||||
|
result.add(buildEventDetail("SCHOOL|" + grid.getJqsj() + "#" + jc, "校历不可排课格",
|
||||||
|
grid.getJqsj(), jc, hits, courseNames,
|
||||||
|
"校历标记该日期节次不可排课(" + safeName(grid.getJqmc()) + "),但已有课次安排"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 班历:对某学员队可排课=0 的格 ----
|
||||||
|
List<JQB> classUnavailable = jqbMapper.selectList(new LambdaQueryWrapper<JQB>()
|
||||||
|
.eq(JQB::getNd, nd)
|
||||||
|
.eq(JQB::getKpk, 0));
|
||||||
|
if (!classUnavailable.isEmpty()) {
|
||||||
|
Map<String, SSKCB> lessonByBh = lessons.stream()
|
||||||
|
.collect(Collectors.toMap(SSKCB::getBh, l -> l, (a, b) -> a));
|
||||||
|
List<SSKCXYD> links = sskcxydMapper.selectList(new LambdaQueryWrapper<SSKCXYD>()
|
||||||
|
.in(SSKCXYD::getSskcbh, lessonByBh.keySet()));
|
||||||
|
Map<String, String> teamNames = xydbMapper.selectList(new LambdaQueryWrapper<>()).stream()
|
||||||
|
.collect(Collectors.toMap(XYDB::getXydbh, XYDB::getXydmc, (a, b) -> a));
|
||||||
|
Map<String, List<String>> teamsOfLesson = new HashMap<>();
|
||||||
|
for (SSKCXYD link : links) {
|
||||||
|
teamsOfLesson.computeIfAbsent(link.getSskcbh(), k -> new ArrayList<>()).add(link.getXydbh());
|
||||||
|
}
|
||||||
|
for (JQB grid : classUnavailable) {
|
||||||
|
if (grid.getJqsj() == null || grid.getCourseClass() == null) continue;
|
||||||
|
String dateStr = intDateToString(grid.getJqsj());
|
||||||
|
for (Integer jc : parsePeriods(grid.getCourseClass())) {
|
||||||
|
for (Map.Entry<String, List<String>> e : teamsOfLesson.entrySet()) {
|
||||||
|
if (!e.getValue().contains(grid.getXydbh())) continue;
|
||||||
|
SSKCB lesson = lessonByBh.get(e.getKey());
|
||||||
|
if (lesson == null || lesson.getRq() == null || lesson.getJc() == null) continue;
|
||||||
|
if (!dateStr.equals(lesson.getRq().toLocalDate().toString()) || lesson.getJc() != jc) continue;
|
||||||
|
TimetableConflictDetailVO d = buildEventDetail(
|
||||||
|
"CLASS|" + grid.getXydbh() + "|" + dateStr + "#" + jc, "班历不可排课格",
|
||||||
|
dateStr, jc, List.of(lesson), courseNames,
|
||||||
|
"班历标记该日期节次对该学员队不可排课,但已有课次安排");
|
||||||
|
d.setXydNames(teamNames.getOrDefault(grid.getXydbh(), grid.getXydbh()));
|
||||||
|
result.add(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CLASSROOM_CONFLICT 追加:教学场地历 标记不可排课(可排课=0)但该教室该格已有课次 */
|
||||||
|
private List<TimetableConflictDetailVO> checkClassroomCalendarConflicts(Integer nd) {
|
||||||
|
List<TimetableConflictDetailVO> result = new ArrayList<>();
|
||||||
|
List<SSKCB> lessons = sskcbMapper.selectList(new LambdaQueryWrapper<SSKCB>()
|
||||||
|
.eq(SSKCB::getNd, nd)
|
||||||
|
.eq(SSKCB::getSczt, 0)
|
||||||
|
.isNotNull(SSKCB::getRq));
|
||||||
|
if (lessons.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Map<String, SSKCB> lessonByBh = lessons.stream()
|
||||||
|
.collect(Collectors.toMap(SSKCB::getBh, l -> l, (a, b) -> a));
|
||||||
|
List<JXCDL> unavailable = classRoomCalendarMapper.selectList(new LambdaQueryWrapper<JXCDL>()
|
||||||
|
.eq(JXCDL::getKpk, false)
|
||||||
|
.eq(JXCDL::getDelFlag, 0)
|
||||||
|
.isNotNull(JXCDL::getRq));
|
||||||
|
if (unavailable.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Map<String, List<String>> roomsOfLesson = new HashMap<>();
|
||||||
|
for (var link : sskcbjsMapper.selectList(new LambdaQueryWrapper<com.roomroot.jwgl.entity.SSKCBJS>()
|
||||||
|
.in(com.roomroot.jwgl.entity.SSKCBJS::getSskcbbh, lessonByBh.keySet()))) {
|
||||||
|
roomsOfLesson.computeIfAbsent(link.getSskcbbh(), k -> new ArrayList<>()).add(link.getJsbh());
|
||||||
|
}
|
||||||
|
Map<String, String> courseNames = loadCourseNames(nd);
|
||||||
|
Map<String, String> roomNames = new HashMap<>();
|
||||||
|
for (JXCDL row : unavailable) {
|
||||||
|
if (row.getRq() == null || row.getJsbh() == null) continue;
|
||||||
|
String dateStr = row.getRq().toLocalDate().toString();
|
||||||
|
roomNames.put(row.getJsbh(), safeName(row.getMc()));
|
||||||
|
for (Map.Entry<String, List<String>> e : roomsOfLesson.entrySet()) {
|
||||||
|
if (!e.getValue().contains(row.getJsbh())) continue;
|
||||||
|
SSKCB lesson = lessonByBh.get(e.getKey());
|
||||||
|
if (lesson.getRq() == null || lesson.getJc() == null) continue;
|
||||||
|
if (!dateStr.equals(lesson.getRq().toLocalDate().toString()) || lesson.getJc() != row.getJc()) continue;
|
||||||
|
TimetableConflictDetailVO d = buildEventDetail(
|
||||||
|
"ROOMCAL|" + row.getJsbh() + "|" + dateStr + "#" + row.getJc(), "场地历不可用",
|
||||||
|
dateStr, row.getJc(), List.of(lesson), courseNames,
|
||||||
|
"教学场地历标记该教室该格不可排课,但已有课次安排");
|
||||||
|
d.setClassroomNames(safeName(row.getMc()));
|
||||||
|
result.add(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构建 不可排课格 类冲突明细 */
|
||||||
|
private TimetableConflictDetailVO buildEventDetail(String resourceId, String resourceName,
|
||||||
|
String dateStr, Integer jc, List<SSKCB> lessons,
|
||||||
|
Map<String, String> courseNames, String reason) {
|
||||||
|
String courses = lessons.stream()
|
||||||
|
.map(l -> courseNames.getOrDefault(l.getSskcbh(), l.getSskcbh()))
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.joining(";"));
|
||||||
|
return TimetableConflictDetailVO.builder()
|
||||||
|
.dimensionCode("EVENT_CONFLICT")
|
||||||
|
.dimensionTitle("日历冲突")
|
||||||
|
.resourceId(resourceId)
|
||||||
|
.resourceName(resourceName)
|
||||||
|
.rq(LocalDate.parse(dateStr).atStartOfDay())
|
||||||
|
.jc(jc)
|
||||||
|
.occupiedLessonCount(lessons.size())
|
||||||
|
.courseNames(courses)
|
||||||
|
.conflictSskcbBhs(lessons.stream().map(SSKCB::getBh).collect(Collectors.joining(",")))
|
||||||
|
.reason(reason)
|
||||||
|
.level("hard")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 实施_课程编号 → 课程名称(经 实施_课程.科目编号 → 课表.课名称) */
|
||||||
|
private Map<String, String> loadCourseNames(Integer nd) {
|
||||||
|
Map<String, String> names = new HashMap<>();
|
||||||
|
List<SSKC> courses = sskcMapper.selectList(new LambdaQueryWrapper<SSKC>().eq(SSKC::getNd, nd));
|
||||||
|
if (courses.isEmpty()) return names;
|
||||||
|
Set<String> kbhs = courses.stream().map(SSKC::getKmbh).collect(Collectors.toSet());
|
||||||
|
Map<String, String> kbhNames = kbMapper.selectBatchIds(kbhs).stream()
|
||||||
|
.collect(Collectors.toMap(com.roomroot.jwgl.entity.KB::getKbh, k -> safeName(k.getKmc()), (a, b) -> a));
|
||||||
|
for (SSKC c : courses) {
|
||||||
|
names.put(c.getBh(), kbhNames.getOrDefault(c.getKmbh(), c.getKmbh()));
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析历表「节次」标签为节次号集合:"1-2"→[1,2],"3"→[3],无法解析返回空 */
|
||||||
|
private List<Integer> parsePeriods(String label) {
|
||||||
|
List<Integer> out = new ArrayList<>();
|
||||||
|
if (label == null) return out;
|
||||||
|
for (String token : label.split("[,,]")) {
|
||||||
|
String t = token.trim();
|
||||||
|
if (t.contains("-")) {
|
||||||
|
String[] parts = t.split("-");
|
||||||
|
try {
|
||||||
|
int a = Integer.parseInt(parts[0].trim());
|
||||||
|
int b = Integer.parseInt(parts[1].trim());
|
||||||
|
for (int i = Math.min(a, b); i <= Math.max(a, b); i++) out.add(i);
|
||||||
|
} catch (Exception ignored) { }
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
out.add(Integer.parseInt(t));
|
||||||
|
} catch (Exception ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String intDateToString(Integer d) {
|
||||||
|
if (d == null) return null;
|
||||||
|
String s = String.valueOf(d);
|
||||||
|
if (s.length() == 8) return s.substring(0, 4) + "-" + s.substring(4, 6) + "-" + s.substring(6, 8);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safeName(String s) {
|
||||||
|
return s == null || s.isEmpty() ? "-" : s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
package com.roomroot.jwgl.utils;
|
package com.roomroot.jwgl.utils;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 学期代号工具。
|
* 学期代号工具。
|
||||||
*
|
*
|
||||||
* <p>系统内学期统一用 6 位学期代号表示,形如 {@code 202601}:前 4 位为年份,后 2 位为学期第次。
|
* <p>系统内学期统一用 6 位学期代号表示,形如 {@code 202601}:前 4 位为年份,后 2 位为学期第次。
|
||||||
*
|
*
|
||||||
* <p>但「学员队年度学期基本信息表」的 {@code 年度} 列在历史上出现过两种口径:
|
* <p>「年度」列的口径约定(按表区分):
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>4 位年份 + 学期第次,例如 年度=2026、学期第次=1 → 2026 * 100 + 1 = 202601</li>
|
* <li>课程/任务/实施类表(学员队任务表、实施_课程、实施_课程表、课程表):存 <b>6 位学期代号</b>;</li>
|
||||||
* <li>直接存 6 位学期代号,例如 年度=202503</li>
|
* <li>学员队年度学期基本信息表(班次学期):存 <b>4 位年份</b> + 学期第次列。</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
|
* 班次学期表的历史数据里「年度」出现过直接存 6 位代号的情况(如 202503),
|
||||||
* 把 6 位值再套 {@code nd * 100 + xqdc} 会溢出(202503 * 100 + 1 = 20250301),
|
* 把 6 位值再套 {@code nd * 100 + xqdc} 会溢出(202503 * 100 + 1 = 20250301),
|
||||||
* 所以各处都需要先判别口径。本工具把两种口径统一解析为 6 位学期代号,
|
* 所以各处都需要先判别口径。本工具把两种口径统一解析为 6 位学期代号,
|
||||||
* 同时提供「写库时统一为 4 位年份」的规范化方法,避免新增数据继续产生双口径。
|
* 同时提供「写库时统一为 4 位年份」的规范化方法。
|
||||||
*/
|
*/
|
||||||
public final class SemesterCodeUtil {
|
public final class SemesterCodeUtil {
|
||||||
|
|
||||||
@@ -22,6 +25,30 @@ public final class SemesterCodeUtil {
|
|||||||
private SemesterCodeUtil() {
|
private SemesterCodeUtil() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按日期推断「学年内学期类别」:2-5 月春季(1)、6-8 月夏季(2)、9 月-次年 1 月秋季(3)。
|
||||||
|
*
|
||||||
|
* <p>学期第次在全系统的契约是 1/2/3(见 {@code ClassSemesterServiceImpl.assertXqdc} 与
|
||||||
|
* 前端批量建班接口「xqdc 传 春季学期/夏季学期/秋季学期」),它与「专业总第几学期」
|
||||||
|
* (人培/专业教学计划表的口径,1..学期数)是两套不同的编号,不能混用。</p>
|
||||||
|
*
|
||||||
|
* @param date 用于判断学年内位置的日期(优先开学日期,其次入学日期)
|
||||||
|
* @return 1 春季 / 2 夏季 / 3 秋季;日期为空时返回 1
|
||||||
|
*/
|
||||||
|
public static Integer periodOf(LocalDate date) {
|
||||||
|
if (date == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int month = date.getMonthValue();
|
||||||
|
if (month >= 2 && month <= 5) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (month >= 6 && month <= 8) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析为 6 位学期代号,兼容 4 位年份与 6 位学期代号两种历史口径。
|
* 解析为 6 位学期代号,兼容 4 位年份与 6 位学期代号两种历史口径。
|
||||||
*
|
*
|
||||||
|
|||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
package com.roomroot.jwgl.vo.allocation;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教学配当 - 单条课程任务的配当行(含铺学时结果)。
|
||||||
|
*
|
||||||
|
* <p>铺学时结果只是「粗约束建议」,不写实施课程表(SSKCB),见方案阶段 3「明确不做」。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AllocationTaskVO {
|
||||||
|
|
||||||
|
/** 任务编号 */
|
||||||
|
private String bh;
|
||||||
|
|
||||||
|
/** 课编号 */
|
||||||
|
private String kbh;
|
||||||
|
|
||||||
|
/** 课程名(简称优先,缺省取课名称) */
|
||||||
|
private String kcmc;
|
||||||
|
|
||||||
|
/** 课类型 */
|
||||||
|
private String klx;
|
||||||
|
|
||||||
|
/** 学时 */
|
||||||
|
private Integer xs;
|
||||||
|
|
||||||
|
/** 周课时 */
|
||||||
|
private Integer zks;
|
||||||
|
|
||||||
|
/** 配档序号(优选序数) */
|
||||||
|
private Integer pdxh;
|
||||||
|
|
||||||
|
/** 配档起始周 */
|
||||||
|
private Integer pdqsz;
|
||||||
|
|
||||||
|
/** 配档按周:0=连排(单科独进) 1=按周(非连排) */
|
||||||
|
private Integer pdaz;
|
||||||
|
|
||||||
|
/** 配档占正课时间 */
|
||||||
|
private Integer pdzzksj;
|
||||||
|
|
||||||
|
/** 配档编组 */
|
||||||
|
private Integer pdbz;
|
||||||
|
|
||||||
|
/** 教研室代号 */
|
||||||
|
private String jysdh;
|
||||||
|
|
||||||
|
/** 计算起始周(铺学时结果) */
|
||||||
|
private Integer startWeek;
|
||||||
|
|
||||||
|
/** 计算结束周(铺学时结果) */
|
||||||
|
private Integer endWeek;
|
||||||
|
|
||||||
|
/** 溢出学时(超出学期周数没铺完的部分) */
|
||||||
|
private Integer overflowHours;
|
||||||
|
|
||||||
|
/** 每周铺学时(weekNo -> 学时),仅含 >0 的周 */
|
||||||
|
private List<WeekHours> distribution = new ArrayList<>();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class WeekHours {
|
||||||
|
private Integer week;
|
||||||
|
private Integer hours;
|
||||||
|
|
||||||
|
public WeekHours() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public WeekHours(Integer week, Integer hours) {
|
||||||
|
this.week = week;
|
||||||
|
this.hours = hours;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
package com.roomroot.jwgl.vo.allocation;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教学配当编辑器 - 打开视图。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AllocationViewVO {
|
||||||
|
|
||||||
|
/** 班次学期编号 */
|
||||||
|
private String xydxqbh;
|
||||||
|
|
||||||
|
/** 学员队编号 */
|
||||||
|
private String xydbh;
|
||||||
|
|
||||||
|
/** 班次(学员队)名称 */
|
||||||
|
private String xydmc;
|
||||||
|
|
||||||
|
/** 学期代号(6 位) */
|
||||||
|
private Integer ndCode;
|
||||||
|
|
||||||
|
/** 学期起 */
|
||||||
|
private LocalDate kxrq;
|
||||||
|
|
||||||
|
/** 学期止 */
|
||||||
|
private LocalDate jsrq;
|
||||||
|
|
||||||
|
/** 总周数 */
|
||||||
|
private Integer totalWeeks;
|
||||||
|
|
||||||
|
/** 是否冻结(已发布/已结束的教学任务,只读) */
|
||||||
|
private Boolean frozen;
|
||||||
|
|
||||||
|
/** 周视图 */
|
||||||
|
private List<AllocationWeekVO> weeks = new ArrayList<>();
|
||||||
|
|
||||||
|
/** 任务配当行(已铺学时) */
|
||||||
|
private List<AllocationTaskVO> tasks = new ArrayList<>();
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.roomroot.jwgl.vo.allocation;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教学配当 - 周视图行。
|
||||||
|
*
|
||||||
|
* <p>「每周可排正课」口径:该周内 可排课=1 且 正课=1 的时间格数 × 2 学时(双节次一格按 2 学时计)。
|
||||||
|
* 数据源优先级:班历(假期表)> 校历(学期校历表)> 默认值(周一~五 1-2/3-4/5-6 共 15 格 = 30 学时)。
|
||||||
|
* 班历已继承校历并允许人工补正课,故直接用班历即等价于「校历正课 − 不可排课 + 班历补正课」。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AllocationWeekVO {
|
||||||
|
|
||||||
|
/** 周次(从 1 起) */
|
||||||
|
private Integer weekNo;
|
||||||
|
|
||||||
|
/** 周起(周一) */
|
||||||
|
private LocalDate startDate;
|
||||||
|
|
||||||
|
/** 周止(周日) */
|
||||||
|
private LocalDate endDate;
|
||||||
|
|
||||||
|
/** 该周可排正课学时 */
|
||||||
|
private Integer availableHours;
|
||||||
|
|
||||||
|
/** 容量来源:class-班历 / school-校历 / default-默认 */
|
||||||
|
private String source;
|
||||||
|
}
|
||||||
+2
-1
@@ -34,7 +34,8 @@ public class SemesterInitVO {
|
|||||||
private Boolean halfYear;
|
private Boolean halfYear;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认/推断的学期第次
|
* 默认/推断的学期第次(学年内学期类别:1 春季 / 2 夏季 / 3 秋季;
|
||||||
|
* 不是「专业总第几学期」,人培/专业教学计划表的学期第次才是专业总学期口径)
|
||||||
*/
|
*/
|
||||||
private Integer xqdc;
|
private Integer xqdc;
|
||||||
|
|
||||||
|
|||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
package com.roomroot.jwgl.vo.scheduling;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实施排课窗口视图(阶段 5.3):班次头 + 课程列表 + 周次×星期×节次 格子。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class SchedulingViewVO {
|
||||||
|
|
||||||
|
private String xydxqbh;
|
||||||
|
|
||||||
|
private String xydmc;
|
||||||
|
|
||||||
|
/** 班次专用教室编号 */
|
||||||
|
private String zyjsbh;
|
||||||
|
|
||||||
|
/** 6 位学期代号 */
|
||||||
|
private Integer ndCode;
|
||||||
|
|
||||||
|
private LocalDate kxrq;
|
||||||
|
|
||||||
|
private LocalDate jsrq;
|
||||||
|
|
||||||
|
private Integer totalWeeks;
|
||||||
|
|
||||||
|
/** 课程列表(已排满 full=true,前端标绿) */
|
||||||
|
private List<Course> courses = new ArrayList<>();
|
||||||
|
|
||||||
|
/** 时间区:按周切分,格子叠加校历/班历不可排与已排课次 */
|
||||||
|
private List<Week> weeks = new ArrayList<>();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Course {
|
||||||
|
private String sskcbh;
|
||||||
|
private String kbh;
|
||||||
|
private String kcmc;
|
||||||
|
private String klx;
|
||||||
|
private Integer xs;
|
||||||
|
private Integer zks;
|
||||||
|
private String jybh;
|
||||||
|
private String jyxm;
|
||||||
|
private String jsbh;
|
||||||
|
private String jsmc;
|
||||||
|
/** 已排学时(每节 2 学时 + 节次调节) */
|
||||||
|
private Integer scheduledHours;
|
||||||
|
/** 是否已排满(scheduledHours >= xs) */
|
||||||
|
private Boolean full;
|
||||||
|
/** 当前用户是否可编辑该课程 */
|
||||||
|
private Boolean editable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Week {
|
||||||
|
private Integer weekNo;
|
||||||
|
private LocalDate startDate;
|
||||||
|
private LocalDate endDate;
|
||||||
|
private List<Day> days = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Day {
|
||||||
|
private LocalDate date;
|
||||||
|
private Integer weekday;
|
||||||
|
/** 该日不可排原因(校历整日/班历整日,节次级见格子) */
|
||||||
|
private String unavailableReason;
|
||||||
|
private List<Cell> cells = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Cell {
|
||||||
|
private Integer jc;
|
||||||
|
/** 硬冲突:不可排课 */
|
||||||
|
private Boolean unavailable;
|
||||||
|
/** 硬冲突原因(不可排课来源:校历/班历等) */
|
||||||
|
private String reason;
|
||||||
|
/** 软提示(非正课 / 配当周次不符) */
|
||||||
|
private String warning;
|
||||||
|
private List<Lesson> lessons = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Lesson {
|
||||||
|
private String bh;
|
||||||
|
private String sskcbh;
|
||||||
|
private String kcmc;
|
||||||
|
private String jxnr;
|
||||||
|
private String jxff;
|
||||||
|
private Integer jcdj;
|
||||||
|
private String jyxm;
|
||||||
|
private String jsmc;
|
||||||
|
private String ycxx;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.roomroot.jwgl.vo.taskbook;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教研室任务书填报列表行(阶段 4)。
|
||||||
|
*
|
||||||
|
* <p>一行 = 一条课程任务(学员队任务表)。列表排序:课程 → 责任教员 → 课次;
|
||||||
|
* 返回 合班分组号(bz2,0=未合班)供前端同色连显。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TaskBookRowVO {
|
||||||
|
|
||||||
|
/** 课程任务编号(学员队任务表.编号) */
|
||||||
|
private String bh;
|
||||||
|
|
||||||
|
/** 学员队学期编号 */
|
||||||
|
private String xydxqbh;
|
||||||
|
|
||||||
|
/** 课编号(科目) */
|
||||||
|
private String kbh;
|
||||||
|
|
||||||
|
/** 课程名称(简称优先,缺省取课表课名称) */
|
||||||
|
private String kcmc;
|
||||||
|
|
||||||
|
/** 课类型 */
|
||||||
|
private String klx;
|
||||||
|
|
||||||
|
/** 学时 */
|
||||||
|
private Integer xs;
|
||||||
|
|
||||||
|
/** 周课时 */
|
||||||
|
private Integer zks;
|
||||||
|
|
||||||
|
/** 成绩分制 */
|
||||||
|
private String cjfz;
|
||||||
|
|
||||||
|
/** 学员队编号 */
|
||||||
|
private String xydbh;
|
||||||
|
|
||||||
|
/** 学员队名称 */
|
||||||
|
private String xydmc;
|
||||||
|
|
||||||
|
/** 责任教员编号 */
|
||||||
|
private String jybh;
|
||||||
|
|
||||||
|
/** 责任教员姓名 */
|
||||||
|
private String jyxm;
|
||||||
|
|
||||||
|
/** 场地(教室编号) */
|
||||||
|
private String jsbh;
|
||||||
|
|
||||||
|
/** 场地名称 */
|
||||||
|
private String jsmc;
|
||||||
|
|
||||||
|
/** 课次序号 */
|
||||||
|
private Integer kcxh;
|
||||||
|
|
||||||
|
/** 合班分组号(0=未合班) */
|
||||||
|
private Integer bz2;
|
||||||
|
|
||||||
|
/** 教研室代号 */
|
||||||
|
private String jysdh;
|
||||||
|
|
||||||
|
/** 教研室名称 */
|
||||||
|
private String jysmc;
|
||||||
|
|
||||||
|
/** 教研室计划教员编号 */
|
||||||
|
private String jysjhjybh;
|
||||||
|
|
||||||
|
/** 教研室计划教员姓名 */
|
||||||
|
private String jysjhjyxm;
|
||||||
|
|
||||||
|
/** 排课建议(教研室计划备注) */
|
||||||
|
private String jysjhbz;
|
||||||
|
|
||||||
|
/** 是否只读(教学任务未发布或已结束) */
|
||||||
|
private Boolean frozen;
|
||||||
|
}
|
||||||
+6
@@ -132,4 +132,10 @@ public class TimetableConflictDetailVO {
|
|||||||
* 统一从学员队表的「所属单位」列取值(WM_CONCAT 去重)。
|
* 统一从学员队表的「所属单位」列取值(WM_CONCAT 去重)。
|
||||||
*/
|
*/
|
||||||
private String responsibleDept;
|
private String responsibleDept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 冲突等级(阶段 5.1):hard-硬冲突(双占 / 三类历不可排,深红);soft-软提示(橙)。
|
||||||
|
* 本检查器的维度全部为硬冲突;软提示(配当周次不符、非正课)由排课窗实时计算。
|
||||||
|
*/
|
||||||
|
private String level;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,12 +95,14 @@ export function batchPresetSplit(bhList) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动生成必修课程(按学员队+学期第次从专业教学计划生成班历记录)
|
// 自动生成必修课程(按学员队学期编号 xydxqbh 定位唯一的目标班次学期)
|
||||||
export function autoGenerateRequiredCourses(xydbh, xqdc) {
|
// 不再传 (学员队编号, 学期第次):同一学员队在多个年度会有学期第次相同的班次学期,
|
||||||
|
// 只传这两个参数无法区分学年,会把课程任务写进别的年度。
|
||||||
|
export function autoGenerateRequiredCourses(xydxqbh) {
|
||||||
return request({
|
return request({
|
||||||
url: '/studentTeamTask/autoGenerateRequiredCourses',
|
url: '/studentTeamTask/autoGenerateRequiredCourses',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
params: { xydbh: xydbh, xqdc: xqdc }
|
params: { xydxqbh: xydxqbh }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 教学配当(阶段 3)
|
||||||
|
// 配当只做排课粗约束(周学时分布、同开同结),不直接写实施课程表。
|
||||||
|
|
||||||
|
// 配当编辑器视图:周轴(每周可排正课)+ 任务行(含铺学时建议)
|
||||||
|
export function allocationView(xydxqbh) {
|
||||||
|
return request({
|
||||||
|
url: '/teachingAllocation/view',
|
||||||
|
method: 'get',
|
||||||
|
params: { xydxqbh }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配当:单条(列表 1 个元素)或多选批量;元素 saveGroup=true 时整编组同开同结
|
||||||
|
export function allocationSave(items) {
|
||||||
|
return request({
|
||||||
|
url: '/teachingAllocation/save',
|
||||||
|
method: 'post',
|
||||||
|
data: items
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优选序数上移/下移(direction: up/down),返回归一化后的任务编号顺序
|
||||||
|
export function allocationMove(xydxqbh, bh, direction) {
|
||||||
|
return request({
|
||||||
|
url: '/teachingAllocation/move',
|
||||||
|
method: 'post',
|
||||||
|
params: { xydxqbh, bh, direction }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编组管理面板:同科目全部开课班次
|
||||||
|
export function allocationGroups(kbh) {
|
||||||
|
return request({
|
||||||
|
url: '/teachingAllocation/groups',
|
||||||
|
method: 'get',
|
||||||
|
params: { kbh }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编组管理:把所选任务统一改编组号
|
||||||
|
export function allocationSetGroup(data) {
|
||||||
|
return request({
|
||||||
|
url: '/teachingAllocation/setGroup',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出配当 Excel(xydxqbh 逗号分隔,支持批量)
|
||||||
|
export function allocationExport(xydxqbhList) {
|
||||||
|
return request({
|
||||||
|
url: '/teachingAllocation/export',
|
||||||
|
method: 'get',
|
||||||
|
params: { xydxqbh: xydxqbhList.join(',') },
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教研室任务书填报(阶段 4)
|
||||||
|
* 接口依据:/taskBookFill/*(前端 baseURL 为 /api,此处不重复 /api 前缀)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 填报列表 GET /taskBookFill/list?jxrwbh= */
|
||||||
|
export function taskBookList(jxrwbh) {
|
||||||
|
return request({
|
||||||
|
url: '/taskBookFill/list',
|
||||||
|
method: 'get',
|
||||||
|
params: { jxrwbh }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手动合班 POST /taskBookFill/merge body: { bhList } */
|
||||||
|
export function taskBookMerge(bhList) {
|
||||||
|
return request({
|
||||||
|
url: '/taskBookFill/merge',
|
||||||
|
method: 'post',
|
||||||
|
data: { bhList }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按预设合班 POST /taskBookFill/mergeByPreset body: { bhList } */
|
||||||
|
export function taskBookMergeByPreset(bhList) {
|
||||||
|
return request({
|
||||||
|
url: '/taskBookFill/mergeByPreset',
|
||||||
|
method: 'post',
|
||||||
|
data: { bhList }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拆班 POST /taskBookFill/split body: { bhList } */
|
||||||
|
export function taskBookSplit(bhList) {
|
||||||
|
return request({
|
||||||
|
url: '/taskBookFill/split',
|
||||||
|
method: 'post',
|
||||||
|
data: { bhList }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 责任教员指定 POST /taskBookFill/setTeacher body: { bh, mode, jybh } */
|
||||||
|
export function taskBookSetTeacher(data) {
|
||||||
|
return request({
|
||||||
|
url: '/taskBookFill/setTeacher',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 场地指定 POST /taskBookFill/setRoom body: { bh, jsbh, useSpecial } */
|
||||||
|
export function taskBookSetRoom(data) {
|
||||||
|
return request({
|
||||||
|
url: '/taskBookFill/setRoom',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 填报字段 POST /taskBookFill/fill body: { bh, jysjhjybh, jsbh, jysjhbz } */
|
||||||
|
export function taskBookFill(data) {
|
||||||
|
return request({
|
||||||
|
url: '/taskBookFill/fill',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -442,6 +442,10 @@ export default {
|
|||||||
cellClass(wIdx, col) {
|
cellClass(wIdx, col) {
|
||||||
const classes = []
|
const classes = []
|
||||||
if (this.selectedKeys.includes(this.cellKey(wIdx, col.colIndex))) classes.push('is-selected')
|
if (this.selectedKeys.includes(this.cellKey(wIdx, col.colIndex))) classes.push('is-selected')
|
||||||
|
// 班历:班次日期范围外的格子不可排,给出明确视觉提示
|
||||||
|
if (this.mode === 'class' && this.isOutOfRange(wIdx, col.dayIndex)) {
|
||||||
|
classes.push('is-out-of-range')
|
||||||
|
}
|
||||||
const ev = this.getEvent(wIdx, col.colIndex)
|
const ev = this.getEvent(wIdx, col.colIndex)
|
||||||
// 可排课的全部白色;不可排课且事件名非空才标红
|
// 可排课的全部白色;不可排课且事件名非空才标红
|
||||||
if (ev && ev.name && !ev.schedulable) {
|
if (ev && ev.name && !ev.schedulable) {
|
||||||
@@ -449,6 +453,30 @@ export default {
|
|||||||
}
|
}
|
||||||
return classes
|
return classes
|
||||||
},
|
},
|
||||||
|
// 时间格对应的实际日期
|
||||||
|
cellDate(wIdx, dayIndex) {
|
||||||
|
if (!this.calendarStart) return null
|
||||||
|
const d = new Date(this.calendarStart)
|
||||||
|
d.setDate(this.calendarStart.getDate() + wIdx * 7 + dayIndex)
|
||||||
|
return d
|
||||||
|
},
|
||||||
|
// 班历只有 [开学日期, 结束日期] 内的格子会落库,范围外的格子无法保存
|
||||||
|
isOutOfRange(wIdx, dayIndex) {
|
||||||
|
if (!this.startDate || !this.endDate) return false
|
||||||
|
const d = this.cellDate(wIdx, dayIndex)
|
||||||
|
if (!d) return false
|
||||||
|
const at = v => new Date(v.getFullYear(), v.getMonth(), v.getDate()).getTime()
|
||||||
|
return at(d) < at(this.startDate) || at(d) > at(this.endDate)
|
||||||
|
},
|
||||||
|
// 返回所选时间格里落在班次日期范围外的格子数
|
||||||
|
countOutOfRangeSelected() {
|
||||||
|
let n = 0
|
||||||
|
this.selectedKeys.forEach(key => {
|
||||||
|
const pos = this.parseCellKey(key)
|
||||||
|
if (pos && this.isOutOfRange(pos.wIdx, pos.dayIndex)) n++
|
||||||
|
})
|
||||||
|
return n
|
||||||
|
},
|
||||||
/* ---------- 点击 / 拖拽多选 ---------- */
|
/* ---------- 点击 / 拖拽多选 ---------- */
|
||||||
onGridMouseDown(e) {
|
onGridMouseDown(e) {
|
||||||
const cell = e.target.closest('.sce-cell')
|
const cell = e.target.closest('.sce-cell')
|
||||||
@@ -548,6 +576,14 @@ export default {
|
|||||||
this.$message.warning('请输入事件名')
|
this.$message.warning('请输入事件名')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 班历只在班次日期范围内落库,范围外的格子直接提示,避免落成含糊的“保存失败”
|
||||||
|
if (this.mode === 'class') {
|
||||||
|
const outside = this.countOutOfRangeSelected()
|
||||||
|
if (outside > 0) {
|
||||||
|
this.$message.warning(`所选 ${outside} 个时间格在班次日期范围外,不可排课`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
const keys = [...this.selectedKeys]
|
const keys = [...this.selectedKeys]
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
const prev = this.events[key]
|
const prev = this.events[key]
|
||||||
@@ -876,6 +912,14 @@ export default {
|
|||||||
// 可排课白色,不可排课且有事件标红
|
// 可排课白色,不可排课且有事件标红
|
||||||
&.no-schedule { background: #fde2e2; }
|
&.no-schedule { background: #fde2e2; }
|
||||||
|
|
||||||
|
// 班历:班次日期范围外的格子不可排
|
||||||
|
&.is-out-of-range {
|
||||||
|
background: #f4f4f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
.sce-date-num { color: #dcdfe6; }
|
||||||
|
}
|
||||||
|
|
||||||
&.is-selected {
|
&.is-selected {
|
||||||
outline: 2px solid var(--edu-green-primary, #00875a);
|
outline: 2px solid var(--edu-green-primary, #00875a);
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="课类型">
|
<el-form-item label="课类型" prop="klx">
|
||||||
<el-input v-model="editForm.klx" placeholder="如 必修/选修/实践" clearable />
|
<el-input v-model="editForm.klx" placeholder="如 必修/选修/实践" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
<div class="form-section-title">学时学分</div>
|
<div class="form-section-title">学时学分</div>
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="学时">
|
<el-form-item label="学时" prop="xs">
|
||||||
<el-input-number v-model="editForm.xs" :min="1" controls-position="right" style="width: 100%" />
|
<el-input-number v-model="editForm.xs" :min="1" controls-position="right" style="width: 100%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -233,7 +233,7 @@
|
|||||||
<div class="form-section-title">教研室计划</div>
|
<div class="form-section-title">教研室计划</div>
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="教研室代号">
|
<el-form-item label="教研室代号" prop="jysdh">
|
||||||
<el-input v-model="editForm.jysdh" placeholder="教研室代号" clearable />
|
<el-input v-model="editForm.jysdh" placeholder="教研室代号" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -418,7 +418,11 @@ export default {
|
|||||||
editMode: 'add',
|
editMode: 'add',
|
||||||
editForm: this.createEmptyEditForm(),
|
editForm: this.createEmptyEditForm(),
|
||||||
editRules: {
|
editRules: {
|
||||||
jc: [{ required: true, message: '请输入课程简称', trigger: 'blur' }]
|
jc: [{ required: true, message: '请输入课程简称', trigger: 'blur' }],
|
||||||
|
// 以下三列在库中「非空且无默认值」,留空会写入无意义值,这里前置拦截
|
||||||
|
xs: [{ required: true, message: '请输入学时', trigger: 'blur' }],
|
||||||
|
klx: [{ required: true, message: '请输入课类型', trigger: 'blur' }],
|
||||||
|
jysdh: [{ required: true, message: '请输入教研室代号', trigger: 'blur' }]
|
||||||
},
|
},
|
||||||
|
|
||||||
// ==================== 复制到其它班次 ====================
|
// ==================== 复制到其它班次 ====================
|
||||||
@@ -427,6 +431,7 @@ export default {
|
|||||||
copyNote: '',
|
copyNote: '',
|
||||||
clipboardCount: 0,
|
clipboardCount: 0,
|
||||||
planMap: {},
|
planMap: {},
|
||||||
|
planByKbh: {},
|
||||||
libraryVisible: false,
|
libraryVisible: false,
|
||||||
libraryKeyword: '',
|
libraryKeyword: '',
|
||||||
libraryRows: [],
|
libraryRows: [],
|
||||||
@@ -532,19 +537,31 @@ export default {
|
|||||||
]).then(([taskRes, planRes]) => {
|
]).then(([taskRes, planRes]) => {
|
||||||
this.records = (taskRes && taskRes.data) || []
|
this.records = (taskRes && taskRes.data) || []
|
||||||
const plans = (planRes && planRes.data) || []
|
const plans = (planRes && planRes.data) || []
|
||||||
const map = {}
|
// 同一课编号可能出现在人培的多个学期第次里,按「课编号@学期第次」精确配对,
|
||||||
|
// 再退化为只按课编号配对,避免拿别的学期的人培行来判「人培差异」。
|
||||||
|
const exact = {}
|
||||||
|
const byKbh = {}
|
||||||
plans.forEach(item => {
|
plans.forEach(item => {
|
||||||
if (item.kbh) map[item.kbh] = item
|
if (!item.kbh) return
|
||||||
|
if (item.xqdc !== undefined && item.xqdc !== null && item.xqdc !== '') {
|
||||||
|
exact[item.kbh + '@' + item.xqdc] = item
|
||||||
|
}
|
||||||
|
if (!byKbh[item.kbh]) byKbh[item.kbh] = item
|
||||||
})
|
})
|
||||||
this.planMap = map
|
this.planMap = exact
|
||||||
|
this.planByKbh = byKbh
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.records = []
|
this.records = []
|
||||||
this.loading = false
|
this.loading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
findPlan(row) {
|
||||||
|
if (!row || !row.kbh) return null
|
||||||
|
return this.planMap[row.kbh + '@' + row.xqdc] || this.planByKbh[row.kbh] || null
|
||||||
|
},
|
||||||
isPlanMismatch(row) {
|
isPlanMismatch(row) {
|
||||||
const plan = row && row.kbh ? this.planMap[row.kbh] : null
|
const plan = this.findPlan(row)
|
||||||
if (!plan) return false
|
if (!plan) return false
|
||||||
return String(row.xs || '') !== String(plan.xs || '')
|
return String(row.xs || '') !== String(plan.xs || '')
|
||||||
|| String(row.klx || '') !== String(plan.klx || '')
|
|| String(row.klx || '') !== String(plan.klx || '')
|
||||||
@@ -674,7 +691,7 @@ export default {
|
|||||||
/* ---------- 自动生成必修课程 ---------- */
|
/* ---------- 自动生成必修课程 ---------- */
|
||||||
handleAutoGenerate() {
|
handleAutoGenerate() {
|
||||||
const s = this.semester || {}
|
const s = this.semester || {}
|
||||||
if (!s.xydbh || !s.xqdc) {
|
if (!s.bh || !s.xydbh || !s.xqdc) {
|
||||||
this.$message.warning('当前班次学期缺少学员队编号或学期第次,无法自动生成')
|
this.$message.warning('当前班次学期缺少学员队编号或学期第次,无法自动生成')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -683,7 +700,9 @@ export default {
|
|||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
autoGenerateRequiredCourses(s.xydbh, s.xqdc).then(res => {
|
// 传班次学期编号:同一学员队在多个年度可能有学期第次相同的班次学期,
|
||||||
|
// 只用 (学员队编号, 学期第次) 会写进别的学年。
|
||||||
|
autoGenerateRequiredCourses(s.bh).then(res => {
|
||||||
const count = (res && res.data) || 0
|
const count = (res && res.data) || 0
|
||||||
this.$message.success(`已自动生成 ${count} 门必修课程`)
|
this.$message.success(`已自动生成 ${count} 门必修课程`)
|
||||||
this.loadData()
|
this.loadData()
|
||||||
|
|||||||
@@ -0,0 +1,452 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:visible="visible"
|
||||||
|
:title="`教学配当 - ${semesterName}`"
|
||||||
|
width="96%"
|
||||||
|
top="4vh"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@update:visible="val => $emit('update:visible', val)"
|
||||||
|
>
|
||||||
|
<div v-loading="loading" class="allocation-page">
|
||||||
|
<!-- ==================== 1. 学期概览 ==================== -->
|
||||||
|
<el-card shadow="never" class="block-card">
|
||||||
|
<div class="meta-line">
|
||||||
|
<span>学期:{{ view.kxrq }} ~ {{ view.jsrq }}</span>
|
||||||
|
<span class="ml">共 {{ view.totalWeeks }} 周</span>
|
||||||
|
<span class="ml">
|
||||||
|
学期代号:
|
||||||
|
<el-tag size="mini" type="info">{{ view.ndCode }}</el-tag>
|
||||||
|
</span>
|
||||||
|
<span v-if="view.frozen" class="ml frozen-tip">
|
||||||
|
<el-tag size="mini" type="danger">教学任务已发布/已结束,配当只读</el-tag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 每周可排正课:时间轴按周、月刻度 -->
|
||||||
|
<div class="week-axis">
|
||||||
|
<table class="axis-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="sticky-col first">周次</th>
|
||||||
|
<th v-for="w in view.weeks" :key="'wn' + w.weekNo" class="week-col"
|
||||||
|
:class="{ 'month-start': isMonthStart(w) }">
|
||||||
|
W{{ w.weekNo }}
|
||||||
|
<div class="week-date">{{ shortDate(w.startDate) }}</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="sticky-col first">可排正课</th>
|
||||||
|
<th v-for="w in view.weeks" :key="'wh' + w.weekNo" class="week-col hours-cell"
|
||||||
|
:class="{ 'month-start': isMonthStart(w) }"
|
||||||
|
:title="sourceText(w.source)">
|
||||||
|
{{ w.availableHours }}h
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- ==================== 2. 工具栏 ==================== -->
|
||||||
|
<div class="list-header">
|
||||||
|
<div class="list-title">课程配当(铺学时为排课粗约束建议,不写实施课表)</div>
|
||||||
|
<div class="list-actions">
|
||||||
|
<el-button size="small" type="primary" :disabled="!selection.length || view.frozen"
|
||||||
|
@click="handleSaveSelected">保存所选</el-button>
|
||||||
|
<el-button size="small" icon="el-icon-refresh" @click="loadData">刷新</el-button>
|
||||||
|
<el-button size="small" icon="el-icon-download" @click="handleExportImage">导出图像</el-button>
|
||||||
|
<el-button size="small" icon="el-icon-download" @click="handleExportExcel">导出 Excel</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== 3. 配当表格(行=任务,列=周) ==================== -->
|
||||||
|
<div class="alloc-table-wrap" ref="allocTableWrap">
|
||||||
|
<table class="alloc-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="sticky-col c-check">
|
||||||
|
<el-checkbox :disabled="view.frozen" @change="val => handleCheckAll(val)" />
|
||||||
|
</th>
|
||||||
|
<th class="sticky-col c-idx">序</th>
|
||||||
|
<th class="sticky-col c-name">课程</th>
|
||||||
|
<th class="sticky-col c-num">学时</th>
|
||||||
|
<th class="sticky-col c-num">周课时</th>
|
||||||
|
<th class="sticky-col c-num" title="优选序数">序数</th>
|
||||||
|
<th class="sticky-col c-num" title="配档起始周">起周</th>
|
||||||
|
<th class="sticky-col c-mode">连排/按周</th>
|
||||||
|
<th class="sticky-col c-num" title="配档编组(0=不编组)">编组</th>
|
||||||
|
<th class="sticky-col c-ops">操作</th>
|
||||||
|
<th v-for="w in view.weeks" :key="'tc' + w.weekNo"
|
||||||
|
class="week-col" :class="{ 'month-start': isMonthStart(w) }">
|
||||||
|
{{ w.weekNo }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="row in view.tasks" :key="row.bh" :class="{ 'row-overflow': row.overflowHours > 0 }">
|
||||||
|
<td class="sticky-col c-check">
|
||||||
|
<el-checkbox v-model="row._checked" :disabled="view.frozen" @change="syncSelection" />
|
||||||
|
</td>
|
||||||
|
<td class="sticky-col c-idx">{{ row.pdxh || '-' }}</td>
|
||||||
|
<td class="sticky-col c-name" :title="row.kbh">{{ row.kcmc }}</td>
|
||||||
|
<td class="sticky-col c-num">{{ row.xs }}</td>
|
||||||
|
<td class="sticky-col c-num">{{ row.zks }}</td>
|
||||||
|
<td class="sticky-col c-num">
|
||||||
|
<el-input-number v-model="row.pdxh" :min="1" :controls="false" size="mini"
|
||||||
|
class="tiny-input" :disabled="view.frozen" />
|
||||||
|
</td>
|
||||||
|
<td class="sticky-col c-num">
|
||||||
|
<el-input-number v-model="row.pdqsz" :min="1" :max="view.totalWeeks" :controls="false"
|
||||||
|
size="mini" class="tiny-input" :disabled="view.frozen" />
|
||||||
|
</td>
|
||||||
|
<td class="sticky-col c-mode">
|
||||||
|
<el-select v-model="row.pdaz" size="mini" class="mode-select" :disabled="view.frozen">
|
||||||
|
<el-option label="按周" :value="1" />
|
||||||
|
<el-option label="连排" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</td>
|
||||||
|
<td class="sticky-col c-num">
|
||||||
|
<el-input-number v-model="row.pdbz" :min="0" :controls="false" size="mini"
|
||||||
|
class="tiny-input" :disabled="view.frozen" />
|
||||||
|
</td>
|
||||||
|
<td class="sticky-col c-ops">
|
||||||
|
<el-button type="text" size="mini" :disabled="view.frozen" @click="handleSaveRow(row)">保存</el-button>
|
||||||
|
<el-button type="text" size="mini" :disabled="view.frozen"
|
||||||
|
@click="handleSaveRow(row, true)">整组</el-button>
|
||||||
|
<el-button type="text" size="mini" :disabled="view.frozen" @click="handleMove(row, 'up')">↑</el-button>
|
||||||
|
<el-button type="text" size="mini" :disabled="view.frozen" @click="handleMove(row, 'down')">↓</el-button>
|
||||||
|
<el-button type="text" size="mini" @click="openGroupPanel(row)">编组</el-button>
|
||||||
|
</td>
|
||||||
|
<td v-for="w in view.weeks" :key="'cell' + row.bh + w.weekNo"
|
||||||
|
class="week-col cell" :class="cellClass(row, w)">
|
||||||
|
{{ cellHours(row, w.weekNo) || '' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!view.tasks.length">
|
||||||
|
<td :colspan="10 + view.weeks.length" class="empty-row">
|
||||||
|
该班次学期暂无课程任务,请先在「班次教学任务」中生成/添加课程。
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== 4. 编组管理面板 ==================== -->
|
||||||
|
<el-dialog
|
||||||
|
:visible.sync="groupPanelVisible"
|
||||||
|
:title="`编组管理 - ${groupCourse}`"
|
||||||
|
width="720px"
|
||||||
|
append-to-body
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<div class="group-panel">
|
||||||
|
<div class="group-tip">同科目全部开课班次;勾选后统一改编组号(同组同开同结)。</div>
|
||||||
|
<el-table ref="groupTableRef" :data="groupList" border size="mini" max-height="320"
|
||||||
|
@selection-change="val => groupSelection = val">
|
||||||
|
<el-table-column type="selection" width="45" align="center" />
|
||||||
|
<el-table-column prop="xydmc" label="班次" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="nd" label="学期代号" width="90" align="center" />
|
||||||
|
<el-table-column prop="xs" label="学时" width="60" align="center" />
|
||||||
|
<el-table-column prop="zks" label="周课时" width="70" align="center" />
|
||||||
|
<el-table-column prop="pdqsz" label="起始周" width="70" align="center" />
|
||||||
|
<el-table-column label="连排/按周" width="90" align="center">
|
||||||
|
<template slot-scope="{ row }">{{ row.pdaz === 0 ? '连排' : '按周' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="pdbz" label="当前编组" width="80" align="center" />
|
||||||
|
</el-table>
|
||||||
|
<div class="group-ops">
|
||||||
|
<span>目标编组号:</span>
|
||||||
|
<el-input-number v-model="groupTarget" :min="0" :controls="false" size="mini" class="tiny-input" />
|
||||||
|
<el-button size="mini" type="primary" :disabled="!groupSelection.length" @click="applyGroup">应用</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
allocationView,
|
||||||
|
allocationSave,
|
||||||
|
allocationMove,
|
||||||
|
allocationGroups,
|
||||||
|
allocationSetGroup,
|
||||||
|
allocationExport
|
||||||
|
} from '@/api/teachBusiness/allocation'
|
||||||
|
|
||||||
|
const SOURCE_TEXT = { class: '班历', school: '校历', default: '默认(30学时)' }
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TeachingAllocationDialog',
|
||||||
|
props: {
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
/** 选中的班次学期记录(含 bh/xydbh/xqdc/nd/xydmc/kxrq/jsrq) */
|
||||||
|
semester: { type: Object, default: null }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
view: { weeks: [], tasks: [], totalWeeks: 0, frozen: false, ndCode: '', kxrq: '', jsrq: '', xydmc: '' },
|
||||||
|
selection: [],
|
||||||
|
groupPanelVisible: false,
|
||||||
|
groupCourse: '',
|
||||||
|
groupKbh: '',
|
||||||
|
groupList: [],
|
||||||
|
groupSelection: [],
|
||||||
|
groupTarget: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
semesterName() {
|
||||||
|
const s = this.semester || {}
|
||||||
|
return s.xydmc || s.xydbh || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(val) {
|
||||||
|
if (val) {
|
||||||
|
this.loadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadData() {
|
||||||
|
const s = this.semester || {}
|
||||||
|
if (!s.bh) {
|
||||||
|
this.$message.warning('缺少班次学期编号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.loading = true
|
||||||
|
allocationView(s.bh).then(res => {
|
||||||
|
const view = (res && res.data) || {}
|
||||||
|
view.tasks = (view.tasks || []).map(t => ({ ...t, _checked: false }))
|
||||||
|
this.view = view
|
||||||
|
this.selection = []
|
||||||
|
}).finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---------- 选择 ---------- */
|
||||||
|
handleCheckAll(val) {
|
||||||
|
this.view.tasks.forEach(t => { t._checked = !!val && !this.view.frozen })
|
||||||
|
this.syncSelection()
|
||||||
|
},
|
||||||
|
syncSelection() {
|
||||||
|
this.selection = this.view.tasks.filter(t => t._checked)
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---------- 保存 ---------- */
|
||||||
|
buildItem(row, saveGroup) {
|
||||||
|
return {
|
||||||
|
bh: row.bh,
|
||||||
|
pdxh: row.pdxh,
|
||||||
|
pdqsz: row.pdqsz,
|
||||||
|
pdaz: row.pdaz,
|
||||||
|
pdbz: row.pdbz,
|
||||||
|
saveGroup: !!saveGroup
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSaveRow(row, saveGroup) {
|
||||||
|
if (saveGroup) {
|
||||||
|
this.$confirm(`将按「同科目 + 编组 ${row.pdbz || 0}」把本学期所有同组任务一起修改(同开同结),是否继续?`, '保存整个编组', {
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
this.doSave([this.buildItem(row, true)], `已保存整编组`)
|
||||||
|
}).catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.doSave([this.buildItem(row, false)], '已保存')
|
||||||
|
},
|
||||||
|
handleSaveSelected() {
|
||||||
|
if (!this.selection.length) {
|
||||||
|
this.$message.warning('请先勾选要保存的行')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.doSave(this.selection.map(row => this.buildItem(row, false)), `已保存 ${this.selection.length} 条`)
|
||||||
|
},
|
||||||
|
doSave(items, okText) {
|
||||||
|
allocationSave(items).then(res => {
|
||||||
|
this.$message.success(`${okText}(更新 ${res && res.data || 0} 行)`)
|
||||||
|
this.loadData()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---------- 上移/下移 ---------- */
|
||||||
|
handleMove(row, direction) {
|
||||||
|
const s = this.semester || {}
|
||||||
|
allocationMove(s.bh, row.bh, direction).then(() => {
|
||||||
|
this.loadData()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---------- 编组管理 ---------- */
|
||||||
|
openGroupPanel(row) {
|
||||||
|
this.groupKbh = row.kbh
|
||||||
|
this.groupCourse = `${row.kcmc}(${row.kbh})`
|
||||||
|
this.groupTarget = row.pdbz || 0
|
||||||
|
this.groupSelection = []
|
||||||
|
allocationGroups(row.kbh).then(res => {
|
||||||
|
this.groupList = (res && res.data) || []
|
||||||
|
this.groupPanelVisible = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
applyGroup() {
|
||||||
|
allocationSetGroup({
|
||||||
|
bhList: this.groupSelection.map(item => item.bh),
|
||||||
|
pdbz: this.groupTarget
|
||||||
|
}).then(res => {
|
||||||
|
this.$message.success(`已更新 ${res && res.data || 0} 行`)
|
||||||
|
this.groupPanelVisible = false
|
||||||
|
this.loadData()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---------- 单元格 ---------- */
|
||||||
|
cellHours(row, weekNo) {
|
||||||
|
const hit = (row.distribution || []).find(item => item.week === weekNo)
|
||||||
|
return hit ? hit.hours : 0
|
||||||
|
},
|
||||||
|
cellClass(row, w) {
|
||||||
|
const hours = this.cellHours(row, w.weekNo)
|
||||||
|
return {
|
||||||
|
'cell-active': hours > 0,
|
||||||
|
'cell-overflow-week': hours > w.availableHours
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isMonthStart(w) {
|
||||||
|
return w.startDate.getDate() <= 7
|
||||||
|
},
|
||||||
|
shortDate(dateStr) {
|
||||||
|
return dateStr ? String(dateStr).slice(5) : ''
|
||||||
|
},
|
||||||
|
sourceText(source) {
|
||||||
|
return `来源:${SOURCE_TEXT[source] || source}`
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---------- 导出 ---------- */
|
||||||
|
handleExportExcel() {
|
||||||
|
const s = this.semester || {}
|
||||||
|
if (!s.bh) return
|
||||||
|
allocationExport([s.bh]).then(blob => {
|
||||||
|
this.saveBlob(blob, `教学配当_${this.semesterName}.xlsx`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleExportImage() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const wrap = this.$refs.allocTableWrap
|
||||||
|
if (!wrap) return
|
||||||
|
const table = wrap.querySelector('table')
|
||||||
|
const width = table.scrollWidth
|
||||||
|
const height = table.scrollHeight
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
canvas.width = width * dpr
|
||||||
|
canvas.height = height * dpr
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
ctx.scale(dpr, dpr)
|
||||||
|
ctx.fillStyle = '#fff'
|
||||||
|
ctx.fillRect(0, 0, width, height)
|
||||||
|
// 表格自绘(无 html2canvas 依赖):逐行输出文本网格
|
||||||
|
const rows = this.view.tasks
|
||||||
|
const weeks = this.view.weeks
|
||||||
|
const lineH = 24
|
||||||
|
ctx.font = '12px sans-serif'
|
||||||
|
let y = 20
|
||||||
|
ctx.fillText(`${this.semesterName} 教学配当 ${this.view.kxrq}~${this.view.jsrq}(共${weeks.length}周)`, 10, y)
|
||||||
|
y += lineH
|
||||||
|
ctx.fillText('周次', 10, y)
|
||||||
|
weeks.forEach(w => {
|
||||||
|
ctx.fillText('W' + w.weekNo, 260 + (w.weekNo - 1) * 34, y)
|
||||||
|
})
|
||||||
|
y += lineH
|
||||||
|
ctx.fillText('可排正课', 10, y)
|
||||||
|
weeks.forEach(w => {
|
||||||
|
ctx.fillText(w.availableHours + 'h', 260 + (w.weekNo - 1) * 34, y)
|
||||||
|
})
|
||||||
|
y += lineH
|
||||||
|
rows.forEach(row => {
|
||||||
|
ctx.fillText(String(row.pdxh || '-'), 10, y)
|
||||||
|
ctx.fillText(String(row.kcmc || '').slice(0, 12), 60, y)
|
||||||
|
ctx.fillText(`学时${row.xs}/周${row.zks}`, 150, y)
|
||||||
|
weeks.forEach(w => {
|
||||||
|
const h = this.cellHours(row, w.weekNo)
|
||||||
|
if (h > 0) {
|
||||||
|
ctx.fillStyle = '#409eff'
|
||||||
|
ctx.fillRect(254 + (w.weekNo - 1) * 34, y - 12, 30, 16)
|
||||||
|
ctx.fillStyle = '#fff'
|
||||||
|
ctx.fillText(String(h), 260 + (w.weekNo - 1) * 34, y)
|
||||||
|
ctx.fillStyle = '#000'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
y += lineH
|
||||||
|
})
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.download = `教学配当_${this.semesterName}.png`
|
||||||
|
link.href = canvas.toDataURL('image/png')
|
||||||
|
link.click()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
saveBlob(blob, fileName) {
|
||||||
|
const url = window.URL.createObjectURL(new Blob([blob]))
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = fileName
|
||||||
|
link.click()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.allocation-page {
|
||||||
|
.block-card { margin-bottom: 8px; }
|
||||||
|
.meta-line {
|
||||||
|
display: flex; align-items: center; flex-wrap: wrap;
|
||||||
|
font-size: 13px; color: #606266; margin-bottom: 8px;
|
||||||
|
.ml { margin-left: 16px; }
|
||||||
|
.frozen-tip { margin-left: auto; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-axis { overflow-x: auto; }
|
||||||
|
.axis-table, .alloc-table {
|
||||||
|
border-collapse: collapse; font-size: 12px; width: max-content; min-width: 100%;
|
||||||
|
th, td { border: 1px solid #ebeef5; padding: 4px 6px; text-align: center; }
|
||||||
|
}
|
||||||
|
.week-col { min-width: 34px; }
|
||||||
|
.month-start { border-left: 2px solid #dcdfe6 !important; }
|
||||||
|
.hours-cell { color: #409eff; font-weight: 600; }
|
||||||
|
|
||||||
|
.alloc-table-wrap { overflow: auto; max-height: 52vh; border: 1px solid #ebeef5; }
|
||||||
|
.alloc-table {
|
||||||
|
.sticky-col { position: sticky; background: #fafafa; z-index: 2; }
|
||||||
|
.c-check { left: 0; width: 36px; }
|
||||||
|
.c-idx { left: 36px; width: 36px; }
|
||||||
|
.c-name { left: 72px; min-width: 120px; max-width: 160px; text-align: left; }
|
||||||
|
.c-num { min-width: 56px; }
|
||||||
|
.c-mode { min-width: 74px; }
|
||||||
|
.c-ops { left: 486px; min-width: 150px; white-space: nowrap; }
|
||||||
|
.cell { min-width: 34px; color: #909399; }
|
||||||
|
.cell-active { background: #d9ecff; color: #1f5faa; font-weight: 600; }
|
||||||
|
.cell-overflow-week { background: #fde2e2; }
|
||||||
|
.row-overflow .c-name { color: #f56c6c; }
|
||||||
|
.empty-row { padding: 16px; color: #909399; text-align: center; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; margin: 4px 0 8px;
|
||||||
|
.list-title { font-size: 13px; font-weight: 600; color: #303133; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiny-input { width: 52px; }
|
||||||
|
.tiny-input ::v-deep input { padding: 0 4px; text-align: center; }
|
||||||
|
.mode-select { width: 66px; }
|
||||||
|
|
||||||
|
.group-panel {
|
||||||
|
.group-tip { font-size: 12px; color: #909399; margin-bottom: 8px; }
|
||||||
|
.group-ops { margin-top: 10px; display: flex; align-items: center; gap: 8px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -63,6 +63,7 @@
|
|||||||
<div class="right-group">
|
<div class="right-group">
|
||||||
<el-button type="success" plain icon="el-icon-date" :disabled="!currentRow" @click="handleEditEventCalendar">编辑班历</el-button>
|
<el-button type="success" plain icon="el-icon-date" :disabled="!currentRow" @click="handleEditEventCalendar">编辑班历</el-button>
|
||||||
<el-button type="success" plain icon="el-icon-calendar" :disabled="!currentRow" @click="handleEditCalendar">编辑班次教学任务</el-button>
|
<el-button type="success" plain icon="el-icon-calendar" :disabled="!currentRow" @click="handleEditCalendar">编辑班次教学任务</el-button>
|
||||||
|
<el-button type="success" plain icon="el-icon-data-line" :disabled="!currentRow" @click="handleAllocation">教学配当</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -170,6 +171,12 @@
|
|||||||
@update:visible="val => eventCalendarVisible = val"
|
@update:visible="val => eventCalendarVisible = val"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TeachingAllocationDialog
|
||||||
|
:visible="allocationVisible"
|
||||||
|
:semester="allocationSemester"
|
||||||
|
@update:visible="val => allocationVisible = val"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- ==================== 批量创建班次学期弹窗 ==================== -->
|
<!-- ==================== 批量创建班次学期弹窗 ==================== -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:visible="batchAddVisible"
|
:visible="batchAddVisible"
|
||||||
@@ -320,6 +327,7 @@
|
|||||||
import ClassSemesterDetailDialog from './ClassSemesterDetailDialog.vue'
|
import ClassSemesterDetailDialog from './ClassSemesterDetailDialog.vue'
|
||||||
import ClassCalendarDialog from './ClassCalendarDialog.vue'
|
import ClassCalendarDialog from './ClassCalendarDialog.vue'
|
||||||
import ClassEventCalendarDialog from './ClassEventCalendarDialog.vue'
|
import ClassEventCalendarDialog from './ClassEventCalendarDialog.vue'
|
||||||
|
import TeachingAllocationDialog from './TeachingAllocationDialog.vue'
|
||||||
import { batchAutoGenerateTasks } from '@/api/studentRecords/classCalendar'
|
import { batchAutoGenerateTasks } from '@/api/studentRecords/classCalendar'
|
||||||
import { listSemester, allSemester, addSemester, updateSemester, delSemester, batchDeleteSemester, batchUpdateDateRange, batchUpdateJxrwbh, batchCreateSemester, batchUpdateXqdc, batchUpdateKfjypk, batchWaveMerge, batchWaveSplit, listCreatableClasses, presetSemesterDates } from '@/api/studentRecords/semester'
|
import { listSemester, allSemester, addSemester, updateSemester, delSemester, batchDeleteSemester, batchUpdateDateRange, batchUpdateJxrwbh, batchCreateSemester, batchUpdateXqdc, batchUpdateKfjypk, batchWaveMerge, batchWaveSplit, listCreatableClasses, presetSemesterDates } from '@/api/studentRecords/semester'
|
||||||
import { listAllSemester } from '@/api/teachBusiness/semester'
|
import { listAllSemester } from '@/api/teachBusiness/semester'
|
||||||
@@ -330,7 +338,8 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
ClassSemesterDetailDialog,
|
ClassSemesterDetailDialog,
|
||||||
ClassCalendarDialog,
|
ClassCalendarDialog,
|
||||||
ClassEventCalendarDialog
|
ClassEventCalendarDialog,
|
||||||
|
TeachingAllocationDialog
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -628,6 +637,16 @@ export default {
|
|||||||
this.eventCalendarVisible = true
|
this.eventCalendarVisible = true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleAllocation(row) {
|
||||||
|
const target = row || this.currentRow
|
||||||
|
if (!target) {
|
||||||
|
this.$message.warning('请先在列表中选择一条班次学期信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.allocationSemester = target
|
||||||
|
this.allocationVisible = true
|
||||||
|
},
|
||||||
|
|
||||||
handleBatchGenerateTasks() {
|
handleBatchGenerateTasks() {
|
||||||
if (!this.selection.length) {
|
if (!this.selection.length) {
|
||||||
this.$message.warning('请先勾选班次学期')
|
this.$message.warning('请先勾选班次学期')
|
||||||
|
|||||||
@@ -63,10 +63,11 @@
|
|||||||
<el-table-column prop="jssj" label="结束时间" width="180" align="center" :formatter="fmtDateTime" />
|
<el-table-column prop="jssj" label="结束时间" width="180" align="center" :formatter="fmtDateTime" />
|
||||||
<el-table-column prop="cjsj" label="创建时间" width="180" align="center" :formatter="fmtDateTime" />
|
<el-table-column prop="cjsj" label="创建时间" width="180" align="center" :formatter="fmtDateTime" />
|
||||||
<el-table-column prop="jcxqscsj" label="教材需求生成时间" align="center" :formatter="fmtDateTime" />
|
<el-table-column prop="jcxqscsj" label="教材需求生成时间" align="center" :formatter="fmtDateTime" />
|
||||||
<el-table-column label="操作" width="380" align="center" fixed="right">
|
<el-table-column label="操作" width="450" align="center" fixed="right">
|
||||||
<template slot-scope="{ row }">
|
<template slot-scope="{ row }">
|
||||||
<el-button type="text" size="small" @click="handleDetail(row)">详情</el-button>
|
<el-button type="text" size="small" @click="handleDetail(row)">详情</el-button>
|
||||||
<el-button type="text" size="small" :disabled="isTaskLocked(row)" @click="handleEdit(row)">编辑</el-button>
|
<el-button type="text" size="small" :disabled="isTaskLocked(row)" @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button type="text" size="small" @click="handleTaskBook(row)">任务书填报</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -186,6 +187,14 @@
|
|||||||
<el-button @click="detailVisible = false">关闭</el-button>
|
<el-button @click="detailVisible = false">关闭</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- ==================== 6. 任务书填报对话框(阶段 4) ==================== -->
|
||||||
|
<task-book-fill-dialog
|
||||||
|
:visible.sync="taskBookVisible"
|
||||||
|
:jxrwbh="taskBookRow.bh"
|
||||||
|
:task-name="taskBookRow.rwmc"
|
||||||
|
:task-status="taskBookRow.zt"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -207,9 +216,11 @@ import {
|
|||||||
endPublishTeachingTask
|
endPublishTeachingTask
|
||||||
} from '@/api/teachBusiness/teachingTask'
|
} from '@/api/teachBusiness/teachingTask'
|
||||||
import { listAllSemester } from '@/api/teachBusiness/semester'
|
import { listAllSemester } from '@/api/teachBusiness/semester'
|
||||||
|
import TaskBookFillDialog from './TaskBookFillDialog.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TaskPlan',
|
name: 'TaskPlan',
|
||||||
|
components: { TaskBookFillDialog },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// ==================== 1. 查询条件 ====================
|
// ==================== 1. 查询条件 ====================
|
||||||
@@ -252,7 +263,11 @@ export default {
|
|||||||
// ==================== 4. 详情 ====================
|
// ==================== 4. 详情 ====================
|
||||||
detailVisible: false,
|
detailVisible: false,
|
||||||
detailLoading: false,
|
detailLoading: false,
|
||||||
detailData: {}
|
detailData: {},
|
||||||
|
|
||||||
|
// ==================== 5. 任务书填报(阶段 4) ====================
|
||||||
|
taskBookVisible: false,
|
||||||
|
taskBookRow: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -441,6 +456,11 @@ export default {
|
|||||||
isTaskPublished(row) {
|
isTaskPublished(row) {
|
||||||
return row && (row.zt === '发布' || row.zt === '已发布')
|
return row && (row.zt === '发布' || row.zt === '已发布')
|
||||||
},
|
},
|
||||||
|
/* ---------- 任务书填报(阶段 4) ---------- */
|
||||||
|
handleTaskBook(row) {
|
||||||
|
this.taskBookRow = row || {}
|
||||||
|
this.taskBookVisible = true
|
||||||
|
},
|
||||||
isTaskEnded(row) {
|
isTaskEnded(row) {
|
||||||
return row && row.zt === '已结束'
|
return row && row.zt === '已结束'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const CompressionPlugin = require('compression-webpack-plugin')
|
|||||||
|
|
||||||
const name = process.env.VUE_APP_TITLE || '教学管理信息系统' // 网页标题
|
const name = process.env.VUE_APP_TITLE || '教学管理信息系统' // 网页标题
|
||||||
|
|
||||||
const baseUrl = 'http://10.1.1.193:8080' // 后端接口
|
const baseUrl = 'http://127.0.0.1:8080' // 后端接口
|
||||||
|
|
||||||
const port = 80 // 固定开发服务器端口,避免多实例端口混乱
|
const port = 80 // 固定开发服务器端口,避免多实例端口混乱
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user