排课功能优化

This commit is contained in:
2026-09-17 17:14:31 +08:00
parent a958bcf059
commit 351df2cba2
26 changed files with 2699 additions and 190 deletions
@@ -1,7 +1,10 @@
package com.roomroot.web.controller.jwgl;
import com.roomroot.common.exception.ServiceException;
import com.roomroot.jwgl.dto.scheduling.SchedulingArrangeRequest;
import com.roomroot.jwgl.dto.scheduling.SchedulingCellOpRequest;
import com.roomroot.jwgl.dto.scheduling.SchedulingCourseEditRequest;
import com.roomroot.jwgl.service.RunningCoursePublishService;
import com.roomroot.jwgl.service.SchedulingWindowService;
import com.roomroot.jwgl.unit.Result;
import com.roomroot.jwgl.vo.scheduling.SchedulingViewVO;
@@ -14,6 +17,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -30,6 +35,9 @@ public class SchedulingWindowController {
@Autowired
private SchedulingWindowService schedulingWindowService;
@Autowired
private RunningCoursePublishService runningCoursePublishService;
/**
* 排课窗组合视图:班次头 + 课程列表(排满标绿)+ 周次×星期×节次格子。
*/
@@ -84,4 +92,91 @@ public class SchedulingWindowController {
@RequestParam(value = "czlx", required = false) String czlx) {
return Result.success(schedulingWindowService.logs(sskcbh, czlx));
}
/**
* 编辑课程教学任务信息(手册 12.3.1):学时/周课时/课程简称/责任教员/默认场地。
*/
@PreAuthorize("@ss.hasPermi('teach:schedulingWindow:edit')")
@PostMapping("/course/update")
public Result<Map<String, Object>> updateCourse(@RequestBody SchedulingCourseEditRequest request) {
return Result.success(schedulingWindowService.updateCourse(request));
}
/**
* 教学班次(合班)当前列表 + 候选班次(手册 12.3.2.2)。
*/
@PreAuthorize("@ss.hasPermi('teach:schedulingWindow:list')")
@GetMapping("/course/teams")
public Result<Map<String, Object>> courseTeams(@RequestParam("sskcbh") String sskcbh) {
return Result.success(schedulingWindowService.courseTeams(sskcbh));
}
/**
* 教学保障选项:保障类别 + 保障资源(含总量与计量单位,手册 12.3.2.5)。
*/
@PreAuthorize("@ss.hasPermi('teach:schedulingWindow:list')")
@GetMapping("/support/options")
public Result<Map<String, Object>> supportOptions() {
return Result.success(schedulingWindowService.supportOptions());
}
/**
* 批量发布【运行课表】(手册 12.1):把班次学期的课程任务发布为运行课程。
*
* <p>发布前要求教学任务已发布未结束、责任教员与默认场地齐备;逐个班次独立事务,
* 单个失败不影响其它班次,失败原因逐条返回。</p>
*/
@PreAuthorize("@ss.hasPermi('teach:schedulingWindow:edit')")
@PostMapping("/publish")
public Result<List<Map<String, Object>>> publish(@RequestBody PublishRequest request) {
return Result.success(batchPublish(request, true));
}
/**
* 批量删除【运行课表】(手册 12.2):撤回该班次学期的运行课程,已排课次一并删除。
*/
@PreAuthorize("@ss.hasPermi('teach:schedulingWindow:edit')")
@PostMapping("/withdraw")
public Result<List<Map<String, Object>>> withdraw(@RequestBody PublishRequest request) {
return Result.success(batchPublish(request, false));
}
/** 批量发布/撤回公共实现 */
private List<Map<String, Object>> batchPublish(PublishRequest request, boolean publish) {
if (request == null || request.getXydxqbhs() == null || request.getXydxqbhs().isEmpty()) {
throw new ServiceException("请选择要操作的班次学期", 400);
}
List<Map<String, Object>> results = new ArrayList<>();
for (String xydxqbh : request.getXydxqbhs()) {
Map<String, Object> item = new HashMap<>();
item.put("xydxqbh", xydxqbh);
try {
int count = publish
? runningCoursePublishService.publish(xydxqbh)
: runningCoursePublishService.withdraw(xydxqbh);
item.put("success", true);
item.put("count", count);
item.put("message", publish ? ("已发布 " + count + " 条运行课程") : ("已删除 " + count + " 条运行课程"));
} catch (Exception e) {
item.put("success", false);
item.put("count", 0);
item.put("message", e.getMessage() == null ? "操作失败" : e.getMessage());
}
results.add(item);
}
return results;
}
/** 批量发布/删除请求体 */
public static class PublishRequest {
private List<String> xydxqbhs;
public List<String> getXydxqbhs() {
return xydxqbhs;
}
public void setXydxqbhs(List<String> xydxqbhs) {
this.xydxqbhs = xydxqbhs;
}
}
}
@@ -70,4 +70,16 @@ public class TeacherCalendarController {
public Result<List<JYL>> listByRq(@RequestParam("rq") String rq) {
return Result.success(jylService.listByRq(rq));
}
/** 批量保存教员历(教员历编辑器整片提交,按 教员+日期+节次 幂等覆盖) */
@PostMapping("/batchSave")
public Result<Integer> batchSave(@RequestBody List<JYL> list) {
return Result.success(jylService.batchSave(list));
}
/** 批量删除教员历(本表无 del_flag,物理删除) */
@PostMapping("/batchDelete")
public Result<Integer> batchDelete(@RequestBody List<String> bhs) {
return Result.success(jylService.batchDelete(bhs));
}
}
@@ -42,9 +42,32 @@ public class SchedulingArrangeRequest {
/** 主讲教员,缺省用课程责任教员 */
private String jybh;
/** 教学班次(学员队编号,本课程默认全部合班班次,可临时增删) */
private List<String> xydbhs;
/** 辅讲教员编号(可多个) */
private List<String> fzjybhs;
/** 教学场地(可多场地),为空则用 jsbh / 课程默认场地 */
private List<String> jsbhs;
/** 教学保障需求(结构化:类别→资源→数量) */
private List<Support> supports;
/** 所选格子 */
private List<Cell> cells;
/** 教学保障需求项 */
@Data
public static class Support {
/** 保障提供明细编号(保障资源库) */
private String bztgmxbh;
/** 需求数量 */
private Integer sl;
/** 备注 */
private String bz;
}
@Data
public static class Cell {
/** 日期 yyyy-MM-dd */
@@ -0,0 +1,31 @@
package com.roomroot.jwgl.dto.scheduling;
import lombok.Data;
/**
* 排课窗口内「编辑课程教学任务信息」请求(手册 12.3.1)。
*
* <p>只允许改运行编制的可调项:学时、周课时、课程简称、责任教员、默认场地。
* 课程简称写回学员队任务表(编制侧)后运行课程同步显示。</p>
*/
@Data
public class SchedulingCourseEditRequest {
/** 实施_课程编号 */
private String sskcbh;
/** 学时 */
private Integer xs;
/** 周课时 */
private Integer zks;
/** 课程简称 */
private String jc;
/** 责任教员编号 */
private String jybh;
/** 默认教学场地(教室编号) */
private String jsbh;
}
@@ -1,5 +1,6 @@
package com.roomroot.jwgl.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -22,66 +23,79 @@ public class BZTGMX {
/**
* 保障类别编号
*/
@TableField("保障类别编号")
private String bzlbbh;
/**
* 名称
*/
@TableField("名称")
private String mc;
/**
* 单位
*/
@TableField("单位")
private String dw;
/**
* 数量
*/
@TableField("数量")
private Integer sl;
/**
* 备注
*/
@TableField("备注")
private String bz;
/**
* 停用
*/
@TableField("停用")
private Integer ty;
/**
* 健康状态
*/
@TableField("健康状态")
private Float jkzt;
/**
* 详细
*/
@TableField("详细")
private String xx;
/**
* 耗材
*/
@TableField("耗材")
private Integer hc;
/**
* 拼音
*/
@TableField("拼音")
private String py;
/**
* 资源类别
*/
@TableField("资源类别")
private String zylb;
/**
* JSONZD
*/
@TableField("JSONZD")
private String jsonzd;
/**
* 危险品
*/
@TableField("危险品")
private Integer wxp;
}
@@ -1,5 +1,6 @@
package com.roomroot.jwgl.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -24,36 +25,43 @@ public class KCBBZMX {
/**
* 课程表编号
*/
@TableField("课程表编号")
private String kcbbh;
/**
* 保障提供明细编号
*/
@TableField("保障提供明细编号")
private String bztgmxbh;
/**
* 数量
*/
@TableField("数量")
private Integer sl;
/**
* 备注
*/
@TableField("备注")
private String bz;
/**
* 年度
*/
@TableField("年度")
private Integer nd;
/**
* 日期
*/
@TableField("日期")
private LocalDateTime rq;
/**
* 节次
*/
@TableField("节次")
private Integer jc;
}
@@ -12,6 +12,18 @@ public interface SSKCMapper extends BaseMapper<SSKC> {
List<SSKC> selectBySemester(@Param("nd") Integer nd, @Param("xqdc") Integer xqdc);
/**
* 按年度查询全部实施_课程(不做停用过滤)。
* <p>禁用 {@code selectList(wrapper)} 读本表:中文列名 + VARBINARY 主键下,
* MyBatis-Plus 自动 SQL 的列标签与 resultMap 列名不匹配,会整行返回 null 元素。</p>
*/
List<SSKC> selectByNd(@Param("nd") Integer nd);
/**
* 按科目编号查询实施_课程(同样绕开 selectList,原因见上)。
*/
List<SSKC> selectByKmbh(@Param("kmbh") String kmbh);
List<SSKC> selectByClassBh(@Param("classBh") String classBh, @Param("nd") Integer nd);
SSKC selectByCourseBhAndClassBh(@Param("courseBh") String courseBh, @Param("classBh") String classBh, @Param("nd") Integer nd);
@@ -20,4 +32,20 @@ public interface SSKCMapper extends BaseMapper<SSKC> {
* 按编号批量查询(编号为 VARBINARY GUID,不能用 selectBatchIds——主键 typeHandler 对 IN 参数不生效)
*/
List<SSKC> selectByBhs(@Param("list") List<String> bhs);
/**
* 按编号批量删除(同上:编号为 VARBINARY GUIDdeleteById 直绑字符串会报
* dm.jdbc.driver.DMException: Invalid hexadecimal digits)。
*
* @return 删除行数
*/
int deleteByBhs(@Param("list") List<String> bhs);
/**
* 按编号更新运行编制可调项(学时 / 周课时 / 责任教员 / 默认场地)。
* 同样绕开 GUID 主键直接绑定:updateById 会对主键绑字符串而失败。
* 传 null 的字段不更新。
*/
int updateBasicByBh(@Param("bh") String bh, @Param("xs") Integer xs, @Param("zks") Integer zks,
@Param("jybh") String jybh, @Param("jsbh") String jsbh);
}
@@ -2,9 +2,46 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.roomroot.jwgl.mapper.SSKCMapper">
<!--
注意:达梦业务表列名为中文,MyBatis 的 autoMapping 按列标签匹配 Java 属性名,
"科目编号" 无法自动映射到 kmbh、"学时" 无法映射到 xs,会全部读出 null。
因此这里必须逐列显式声明;autoMapping 仅作兜底。
-->
<resultMap id="SSKCMap" type="com.roomroot.jwgl.entity.SSKC" autoMapping="true">
<id column="编号" property="bh" jdbcType="VARBINARY"
typeHandler="com.roomroot.jwgl.handler.DmGuidTypeHandler"/>
<result column="年度" property="nd"/>
<result column="教员编号" property="jybh"/>
<result column="教员课次序号" property="jykcxh"/>
<result column="科目编号" property="kmbh"/>
<result column="教室编号" property="jsbh"/>
<result column="学时" property="xs"/>
<result column="课类型" property="klx"/>
<result column="课程标准编号" property="ktbzbh"/>
<result column="课程课时系数" property="kcksxs"/>
<result column="课程学员测评状态" property="kcxycpzt"/>
<result column="学分" property="xf"/>
<result column="创建时间" property="cjsj"/>
<result column="变动时间" property="bdsj"/>
<result column="课表变动时间" property="kbbdsj"/>
<result column="测评结果样式编号" property="cpjgygbh"/>
<result column="理论学时" property="llxs"/>
<result column="实践学时" property="sjxs"/>
<result column="考核学时" property="khxs"/>
<result column="运行学时" property="yxxs"/>
<result column="运行理论学时" property="yxllxs"/>
<result column="运行实践学时" property="yxsjxs"/>
<result column="运行考核学时" property="yxkhxs"/>
<result column="成绩分制" property="cjfz"/>
<result column="不计入学员平均分" property="bjrxypjf"/>
<result column="周课时" property="zks"/>
<result column="预计测评结束时间" property="yjcpjssj"/>
<result column="分值权重标准编号" property="fzqzbzbh"/>
<result column="JSONZD" property="jsonzd"/>
<result column="评学综合得分" property="pxxhdf"/>
<result column="确认评学综合" property="qrpxzh"/>
<result column="责任教员编号" property="zrjybh"/>
<result column="教员评学_结束状态" property="jypxjszt"/>
</resultMap>
<select id="selectBySemester" resultMap="SSKCMap">
@@ -12,6 +49,24 @@
WHERE 年度 = #{nd} AND 学期第次 = #{xqdc} AND 停用 = 0
</select>
<!--
按年度查询全部实施_课程。
注意:本表列名为中文、主键为 VARBINARY(16)MyBatis-Plus 自动 SQLBaseMapper.selectList 等)
的列标签与 resultMap 的列名对不上,会让每一行都判为"未匹配任何列"而返回 null 元素
MyBatis returnInstanceForEmptyRow 默认 false 时会丢弃整行)。
因此凡是要按条件读本表,一律走显式 XML,不要用 selectList。
-->
<select id="selectByNd" resultMap="SSKCMap">
SELECT * FROM 实施_课程
WHERE 年度 = #{nd}
</select>
<!-- 同上:按科目编号查询,禁用 selectList -->
<select id="selectByKmbh" resultMap="SSKCMap">
SELECT * FROM 实施_课程
WHERE 科目编号 = #{kmbh}
</select>
<!--
达梦:实施_课程.编号 为 VARBINARY(16),实施_课程学员队.实施_课程编号 为 CHAR(36)。
禁止 编号 = 实施_课程编号:达梦会把 CHAR 当十六进制转 VARBINARYUUID 中的 '-' 报 -6147。
@@ -40,4 +95,26 @@
</foreach>
</select>
<!-- 批量删除:同一原因,deleteById 直绑字符串会报 Invalid hexadecimal digits -->
<delete id="deleteByBhs">
DELETE FROM 实施_课程
WHERE LOWER(RAWTOHEX("编号")) IN
<foreach collection="list" item="bh" open="(" separator="," close=")">
LOWER(REPLACE(TRIM(#{bh}), '-', ''))
</foreach>
</delete>
<!-- 更新运行编制可调项:同样绕开 GUID 主键直接绑定 -->
<update id="updateBasicByBh">
UPDATE 实施_课程
<set>
<if test="xs != null">"学时" = #{xs},</if>
<if test="zks != null">"周课时" = #{zks},</if>
<if test="jybh != null and jybh != ''">"教员编号" = #{jybh},</if>
<if test="jsbh != null and jsbh != ''">"教室编号" = #{jsbh},</if>
"变动时间" = CURRENT_TIMESTAMP
</set>
WHERE LOWER(RAWTOHEX("编号")) = LOWER(REPLACE(TRIM(#{bh}), '-', ''))
</update>
</mapper>
@@ -26,4 +26,19 @@ public interface JYLService {
List<JYL> listByJybh(String jybh);
List<JYL> listByRq(String rq);
/**
* 批量保存教员历(供教员历编辑器整片提交)。
* <p>按 (教员编号 + 日期 + 节次) 幂等覆盖:已有则更新,没有则新增。</p>
*
* @return 实际写入/更新的条数
*/
int batchSave(List<JYL> list);
/**
* 批量删除教员历(本表无 del_flag 审计列,为物理删除)。
*
* @return 实际删除条数
*/
int batchDelete(List<String> bhs);
}
@@ -2,6 +2,7 @@ package com.roomroot.jwgl.service;
import com.roomroot.jwgl.dto.scheduling.SchedulingArrangeRequest;
import com.roomroot.jwgl.dto.scheduling.SchedulingCellOpRequest;
import com.roomroot.jwgl.dto.scheduling.SchedulingCourseEditRequest;
import com.roomroot.jwgl.vo.scheduling.SchedulingViewVO;
import java.util.List;
@@ -36,4 +37,13 @@ public interface SchedulingWindowService {
/** 排课日志查询:按课程(可叠加操作类型) */
List<Map<String, Object>> logs(String sskcbh, String czlx);
/** 编辑课程教学任务信息:学时/周课时/课程简称/责任教员/默认场地(手册 12.3.1) */
Map<String, Object> updateCourse(SchedulingCourseEditRequest request);
/** 教学班次(合班)当前列表 + 候选班次(手册 12.3.2.2 */
Map<String, Object> courseTeams(String sskcbh);
/** 教学保障选项:保障类别 + 保障资源(含总量与计量单位,手册 12.3.2.5) */
Map<String, Object> supportOptions();
}
@@ -19,7 +19,8 @@ import java.util.Map;
* <li>合班分组号用已有列 学员队任务表.编组(bz2),0=未合班;不复用 预设编班号 和 课次序号。</li>
* <li>发布门禁:仅「已发布且未结束」的教学任务可填报;结束后只读(TeachingTaskWriteGuard)。</li>
* <li>责任教员 / 场地修改时,同合班组(同编组,非 0)的行同步。</li>
* <li>已发布到运行课表的行须先撤回才能拆班(运行课表属阶段 5,当前先拦截有排课数据的行)。</li>
* <li>已发布到运行课表的行须先撤回才能拆班:按「实施_课程学员队」(学员队编号, 年度=6 位学期代号) 判定,
* 与运行课表发布的幂等门禁同源;拆班会让任务书合班组与已固化的运行课程不一致。</li>
* </ul></p>
*/
public interface TaskBookFillService {
@@ -5,13 +5,19 @@ 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.BusinessException;
import com.roomroot.jwgl.unit.PageQuery;
import com.roomroot.jwgl.unit.PageResult;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
@@ -81,6 +87,86 @@ public class JYLServiceImpl implements JYLService {
.apply("DATE(rq) = {0}", rq));
}
/**
* 批量保存:按 (教员编号 + 日期 + 节次) 幂等覆盖。
* <p>先按涉及的教员 + 日期区间一次性把已有记录拉回来判重,避免逐行查库。</p>
*/
@Override
public int batchSave(List<JYL> list) {
if (list == null || list.isEmpty()) {
return 0;
}
Set<String> jybhSet = new HashSet<>();
LocalDate minDay = null;
LocalDate maxDay = null;
for (JYL item : list) {
if (item == null || StringUtils.isEmpty(item.getJybh())) {
throw new BusinessException("教员历缺少教员编号");
}
if (item.getRq() == null || item.getJc() == null) {
throw new BusinessException("教员历缺少日期或节次");
}
item.setJybh(item.getJybh().trim());
jybhSet.add(item.getJybh());
LocalDate d = item.getRq().toLocalDate();
if (minDay == null || d.isBefore(minDay)) {
minDay = d;
}
if (maxDay == null || d.isAfter(maxDay)) {
maxDay = d;
}
}
Map<String, JYL> existing = new HashMap<>();
for (JYL row : jylMapper.selectList(new LambdaQueryWrapper<JYL>()
.in(JYL::getJybh, jybhSet)
.ge(JYL::getRq, minDay.atStartOfDay())
.lt(JYL::getRq, maxDay.plusDays(1).atStartOfDay()))) {
if (row == null || row.getRq() == null || row.getJc() == null) {
continue;
}
existing.put(cellKey(row.getJybh(), row.getRq().toLocalDate(), row.getJc()), row);
}
int affected = 0;
for (JYL item : list) {
JYL old = existing.get(cellKey(item.getJybh(), item.getRq().toLocalDate(), item.getJc()));
if (old == null) {
if (item.getKpk() == null) {
// 教员历的语义是"标出不可排课的时段",新标记默认不可排课
item.setKpk(0);
}
add(item);
} else {
if (item.getKpk() != null) {
old.setKpk(item.getKpk());
}
if (StringUtils.isNotEmpty(item.getMc())) {
old.setMc(item.getMc());
}
old.setBz(item.getBz());
update(old);
}
affected++;
}
return affected;
}
@Override
public int batchDelete(List<String> bhs) {
if (bhs == null || bhs.isEmpty()) {
return 0;
}
List<String> ids = bhs.stream().filter(StringUtils::isNotEmpty).collect(java.util.stream.Collectors.toList());
if (ids.isEmpty()) {
return 0;
}
return jylMapper.delete(new LambdaQueryWrapper<JYL>().in(JYL::getBh, ids));
}
/** 判重键:教员编号|yyyy-MM-dd|节次 */
private static String cellKey(String jybh, LocalDate day, Integer jc) {
return (jybh == null ? "" : jybh.trim()) + "|" + day + "|" + jc;
}
private LambdaQueryWrapper<JYL> buildWrapper(JYL condition) {
LambdaQueryWrapper<JYL> qw = new LambdaQueryWrapper<>();
if (condition != null) {
@@ -138,7 +138,10 @@ public class RunningCoursePublishServiceImpl implements RunningCoursePublishServ
for (List<XYDRWB> group : groups.values()) {
XYDRWB head = group.get(0);
SSKC sskc = new SSKC();
sskc.setBh(UuidUtil.getUUID());
// 实施_课程.编号 是 VARBINARY(16)MyBatis 走 @TableId 元数据不带 typeHandler
// 必须绑 32 位无横线十六进制,带横线会被达梦当普通字符串写入并报 String truncated。
String sskcBh = UuidUtil.getUUID();
sskc.setBh(sskcBh);
sskc.setNd(ndCode);
sskc.setJybh(head.getJybh());
sskc.setKmbh(head.getKbh());
@@ -153,12 +156,27 @@ public class RunningCoursePublishServiceImpl implements RunningCoursePublishServ
sskc.setBjrxypjf(head.getBjrxypjf());
sskc.setCjsj(now);
sskc.setBdsj(now);
// 以下非空列必须显式赋值(实施_课程共 26 个 NOT NULL 列,无库级缺省)
sskc.setJykcxh(1);
sskc.setKcksxs(1.0f);
sskc.setKcxycpzt("未开始");
sskc.setKbbdsj(now);
sskc.setKhxs(head.getKsks() == null ? 0 : head.getKsks());
sskc.setYxxs(0);
sskc.setYxllxs(0);
sskc.setYxsjxs(0);
sskc.setYxkhxs(0);
sskc.setPxxhdf(0f);
sskc.setQrpxzh(0);
sskc.setZrjybh(head.getJybh());
sskc.setJypxjszt(0);
sskcMapper.insert(sskc);
for (XYDRWB task : group) {
SSKCXYD link = new SSKCXYD();
link.setBh(UuidUtil.getUUID());
link.setSskcbh(sskc.getBh());
// 实施_课程学员队.编号/实施_课程编号 是 CHAR(36),约定存带横线 36 位 UUID
link.setBh(UuidUtil.getOriginalUUID());
link.setSskcbh(UuidUtil.fromBytes(UuidUtil.toBytes(sskcBh)));
link.setXydbh(task.getXydbh());
link.setJc(team != null ? team.getJc() : null);
link.setRs(team != null ? team.getXydrs() : null);
@@ -213,7 +231,9 @@ public class RunningCoursePublishServiceImpl implements RunningCoursePublishServ
Long remain = sskcxydMapper.selectCount(new LambdaQueryWrapper<SSKCXYD>()
.eq(SSKCXYD::getSskcbh, sskcbh));
if (remain == null || remain == 0) {
sskcMapper.deleteById(sskcbh);
// 注意:实施_课程.编号为 VARBINARY GUIDdeleteById 直绑字符串会报
// dm.jdbc.driver.DMException: Invalid hexadecimal digits,必须走 RAWTOHEX
sskcMapper.deleteByBhs(java.util.List.of(sskcbh));
}
removed++;
}
@@ -5,10 +5,16 @@ 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.dto.scheduling.SchedulingCourseEditRequest;
import com.roomroot.jwgl.entity.BZLB;
import com.roomroot.jwgl.entity.BZTGMX;
import com.roomroot.jwgl.entity.JCSJB;
import com.roomroot.jwgl.entity.JQB;
import com.roomroot.jwgl.entity.JSB;
import com.roomroot.jwgl.entity.JYB;
import com.roomroot.jwgl.entity.JXCDL;
import com.roomroot.jwgl.entity.JYL;
import com.roomroot.jwgl.entity.KCBBZMX;
import com.roomroot.jwgl.entity.KCJXYX_CZRZ;
import com.roomroot.jwgl.entity.SSKC;
import com.roomroot.jwgl.entity.SSKCB;
@@ -18,7 +24,13 @@ 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.entity.XQXLB;
import com.roomroot.jwgl.mapper.BZLBMapper;
import com.roomroot.jwgl.mapper.BZTGMXMapper;
import com.roomroot.jwgl.mapper.ClassRoomMapper;
import com.roomroot.jwgl.mapper.JCSJBMapper;
import com.roomroot.jwgl.mapper.KCBBZMXMapper;
import com.roomroot.jwgl.mapper.XYDBMapper;
import com.roomroot.jwgl.mapper.ClassRoomCalendarMapper;
import com.roomroot.jwgl.mapper.ClassSemesterMapper;
@@ -40,6 +52,7 @@ import com.roomroot.jwgl.utils.SemesterCodeUtil;
import com.roomroot.jwgl.utils.PeriodUtil;
import com.roomroot.jwgl.utils.UuidUtil;
import com.roomroot.jwgl.vo.scheduling.SchedulingViewVO;
import com.roomroot.common.core.domain.entity.SysUser;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -52,6 +65,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -129,6 +143,24 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
@Resource
private com.roomroot.jwgl.mapper.StudentTeamTaskMapper studentTeamTaskMapper;
@Resource
private JCSJBMapper jcsjbMapper;
@Resource
private ClassRoomMapper classRoomMapper;
@Resource
private BZLBMapper bzlbMapper;
@Resource
private BZTGMXMapper bztgmxMapper;
@Resource
private KCBBZMXMapper kcbbzMapper;
@Resource
private com.roomroot.system.service.ISysUserService userService;
// ==================== 视图 ====================
@Override
@@ -147,6 +179,13 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
List<String> sskcIds = links.stream().map(SSKCXYD::getSskcbh).distinct().toList();
Map<String, SSKC> courses = sskcIds.isEmpty() ? Map.of()
: sskcMapper.selectByBhs(sskcIds).stream().collect(Collectors.toMap(SSKC::getBh, c -> c, (a, b) -> a));
// 编制侧任务(排课建议 / 简称 / 计划教员)
Map<String, XYDRWB> taskByKbh = new LinkedHashMap<>();
for (XYDRWB task : studentTeamTaskMapper.selectList(new LambdaQueryWrapper<XYDRWB>()
.eq(XYDRWB::getXydxqbh, ctx.semester.getBh())
.eq(XYDRWB::getDelFlag, 0))) {
taskByKbh.putIfAbsent(task.getKbh(), task);
}
List<SSKCB> lessons = ctx.lessons;
Map<String, List<SSKCB>> lessonsByCourse = lessons.stream()
.collect(Collectors.groupingBy(SSKCB::getSskcbh));
@@ -162,6 +201,25 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
c.setZks(course.getZks());
c.setJybh(course.getJybh());
c.setJsbh(course.getJsbh());
XYDRWB task = taskByKbh.get(course.getKmbh());
if (task != null) {
c.setJc(task.getJc());
c.setJysjhbz(task.getJysjhbz());
c.setJysjhjybh(task.getJysjhjybh());
c.setJysjhjyxm(resolveTeacherName(task.getJysjhjybh()));
}
if (c.getJybh() != null) c.setJyxm(resolveTeacherName(c.getJybh()));
if (c.getJsbh() != null) c.setJsmc(resolveRoomName(c.getJsbh()));
// ⑤ 合班信息:该运行课程关联的全部班次
for (SSKCXYD link : links) {
if (!course.getBh().equals(link.getSskcbh())) continue;
SchedulingViewVO.Team t = new SchedulingViewVO.Team();
t.setXydbh(link.getXydbh());
t.setXydxqbh(ctx.semester.getBh());
t.setXydmc(link.getXydbh() != null && ctx.team != null && ctx.team.getXydbh().equals(link.getXydbh())
? ctx.team.getXydmc() : resolveTeamName(link.getXydbh()));
c.getTeams().add(t);
}
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());
@@ -213,14 +271,55 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
.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.selectByBhs(ctx.links.stream().map(SSKCXYD::getSskcbh).distinct().toList())) {
List<String> courseBhList = ctx.links.stream().map(SSKCXYD::getSskcbh).distinct().toList();
for (SSKC course : courseBhList.isEmpty() ? List.<SSKC>of() : sskcMapper.selectByBhs(courseBhList)) {
courseNames.put(course.getBh(), resolveCourseName(course.getKmbh()));
courseTeachers.put(course.getBh(), safe(course.getJybh()));
courseTeachers.put(course.getBh(), resolveTeacherName(course.getJybh()));
}
// 课次明细:学员队 / 教室 / 教员(供 ② 冲突显示选项与双击回填使用)
Map<String, List<String>> teamsOfLesson = new HashMap<>();
Map<String, List<String>> roomsOfLesson = new HashMap<>();
Map<String, List<String>> teachersOfLesson = new HashMap<>();
Map<String, String> mainTeacherOfLesson = new HashMap<>();
List<String> allLessonIds = ctx.lessons.stream().map(SSKCB::getBh).distinct().toList();
if (!allLessonIds.isEmpty()) {
for (SSKCBXYD l : sskcbxydMapper.selectList(new LambdaQueryWrapper<SSKCBXYD>()
.in(SSKCBXYD::getSskcbbh, allLessonIds))) {
teamsOfLesson.computeIfAbsent(l.getSskcbbh(), k -> new ArrayList<>()).add(l.getXydbh());
}
for (SSKCBJS l : sskcbjsMapper.selectList(new LambdaQueryWrapper<SSKCBJS>()
.in(SSKCBJS::getSskcbbh, allLessonIds))) {
roomsOfLesson.computeIfAbsent(l.getSskcbbh(), k -> new ArrayList<>()).add(l.getJsbh());
}
for (SSKCBFZJY l : sskcbfzjyMapper.selectList(new LambdaQueryWrapper<SSKCBFZJY>()
.in(SSKCBFZJY::getSskcbbh, allLessonIds))) {
teachersOfLesson.computeIfAbsent(l.getSskcbbh(), k -> new ArrayList<>()).add(l.getFzjybh());
if (l.getZjy() != null && l.getZjy() == 1) {
mainTeacherOfLesson.put(l.getSskcbbh(), l.getFzjybh());
} else {
mainTeacherOfLesson.putIfAbsent(l.getSskcbbh(), l.getFzjybh());
}
}
}
// 场地历 / 教员历 不可用(按 date#jc 聚合,供冲突显示选项分类着色)
Map<String, List<String>> blockedRoomsByCell = 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) continue;
blockedRoomsByCell.computeIfAbsent(row.getRq().toLocalDate() + "#" + row.getJc(), k -> new ArrayList<>())
.add(row.getJsbh());
}
Map<String, List<String>> blockedTeachersByCell = new HashMap<>();
for (JYL row : jylMapper.selectList(new LambdaQueryWrapper<JYL>().eq(JYL::getKpk, 0))) {
if (row.getRq() == null || row.getJybh() == null) continue;
blockedTeachersByCell.computeIfAbsent(row.getRq().toLocalDate() + "#" + row.getJc(), k -> new ArrayList<>())
.add(row.getJybh());
}
LocalDate weekStart = kxrq.with(DayOfWeek.MONDAY);
int weekNo = 1;
int[] periodCount = {8};
int[] periodCount = {resolvePeriodCount()};
vo.setPeriods(resolvePeriods());
while (!weekStart.isAfter(jsrq)) {
SchedulingViewVO.Week week = new SchedulingViewVO.Week();
week.setWeekNo(weekNo);
@@ -238,23 +337,41 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
String key = date.toString() + "#" + jc;
if (schoolHard.contains(key) || classHard.contains(key)) {
cell.setUnavailable(true);
cell.setReason("不可排课(" + (schoolHard.contains(key) ? "校历" : "班历") + "");
cell.setCalendarKind(schoolHard.contains(key) ? "SCHOOL" : "CLASS");
cell.setReason("不可排课(" + ("SCHOOL".equals(cell.getCalendarKind()) ? "校历" : "班历") + "");
} else {
if (schoolSoft.contains(key) || classSoft.contains(key)) {
cell.setWarning("非正课时段");
cell.setSoft(true);
}
cell.setUnavailable(false);
}
cell.setBlockedRooms(blockedRoomsByCell.getOrDefault(key, new ArrayList<>()));
cell.setBlockedTeachers(blockedTeachersByCell.getOrDefault(key, new ArrayList<>()));
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.setJxyd(lesson.getJxyd());
l.setJxff(lesson.getJxff());
l.setJcdj(lesson.getJcdj());
l.setYcxx(lesson.getYcxx());
l.setJxbzbz(lesson.getJxbzbz());
l.setBzms(lesson.getBzms());
l.setJyxm(courseTeachers.getOrDefault(lesson.getSskcbh(), "-"));
l.setJybh(mainTeacherOfLesson.get(lesson.getBh()));
if (l.getJybh() != null) {
l.setJyxm(resolveTeacherName(l.getJybh()));
}
l.setXydbhs(teamsOfLesson.getOrDefault(lesson.getBh(), new ArrayList<>()));
l.setJsbhs(roomsOfLesson.getOrDefault(lesson.getBh(), new ArrayList<>()));
l.setJybhs(teachersOfLesson.getOrDefault(lesson.getBh(), new ArrayList<>()));
if (!l.getJsbhs().isEmpty()) {
l.setJsbh(l.getJsbhs().get(0));
l.setJsmc(resolveRoomName(l.getJsbhs().get(0)));
}
cell.getLessons().add(l);
}
day.getCells().add(cell);
@@ -268,6 +385,34 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
vo.setTotalWeeks(weekNo - 1);
}
/** 节次字典:节次时间表展开为 1..N,供前端显示控制 */
private List<SchedulingViewVO.Period> resolvePeriods() {
List<SchedulingViewVO.Period> periods = new ArrayList<>();
try {
for (JCSJB row : jcsjbMapper.selectList(new LambdaQueryWrapper<JCSJB>())) {
String label = row.getJc() != null && !row.getJc().isEmpty() ? row.getJc() : row.getJcsy();
for (Integer jc : PeriodUtil.parsePeriods(row.getJcsy())) {
if (periods.stream().anyMatch(p -> p.getJc().equals(jc))) continue;
SchedulingViewVO.Period p = new SchedulingViewVO.Period();
p.setJc(jc);
p.setLabel(label);
p.setSd(row.getSd());
periods.add(p);
}
}
} catch (Exception e) {
log.debug("[排课窗] 读取节次字典失败,退化为默认 1-8: {}", e.getMessage());
}
periods.sort(Comparator.comparing(SchedulingViewVO.Period::getJc));
return periods;
}
/** 最大节次数:优先取节次时间表,缺省 8 */
private int resolvePeriodCount() {
List<SchedulingViewVO.Period> periods = resolvePeriods();
return periods.isEmpty() ? 8 : periods.get(periods.size() - 1).getJc();
}
// ==================== 安排所选节次 ====================
@Override
@@ -281,11 +426,51 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
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()) {
// 教学场地(支持多场地):jsbhs > jsbh > 课程默认场地
List<String> rooms = new ArrayList<>();
if (request.getJsbhs() != null) {
request.getJsbhs().stream().filter(s -> s != null && !s.isEmpty()).distinct().forEach(rooms::add);
}
if (rooms.isEmpty() && request.getJsbh() != null && !request.getJsbh().isEmpty()) {
rooms.add(request.getJsbh());
}
if (rooms.isEmpty() && course.getJsbh() != null && !course.getJsbh().isEmpty()) {
rooms.add(course.getJsbh());
}
if (rooms.isEmpty()) {
throw new ServiceException("请指定场地(课程未设置默认场地)", BAD_REQUEST);
}
String jsbh = rooms.get(0);
String jybh = request.getJybh() != null && !request.getJybh().isEmpty() ? request.getJybh() : course.getJybh();
List<String> fzjybhs = request.getFzjybhs() == null ? List.of()
: request.getFzjybhs().stream().filter(s -> s != null && !s.isEmpty()).distinct().toList();
// 教学保障:数量不得超过资源库总量
List<KCBBZMX> supportRows = new ArrayList<>();
if (request.getSupports() != null) {
for (SchedulingArrangeRequest.Support s : request.getSupports()) {
if (s == null || s.getBztgmxbh() == null || s.getBztgmxbh().isEmpty()) continue;
if (s.getSl() == null || s.getSl() <= 0) {
throw new ServiceException("教学保障需求数量必须大于 0", BAD_REQUEST);
}
BZTGMX res = bztgmxMapper.selectById(s.getBztgmxbh());
if (res == null) {
throw new ServiceException("教学保障资源不存在:" + s.getBztgmxbh(), BAD_REQUEST);
}
if (res.getSl() != null && s.getSl() > res.getSl()) {
throw new ServiceException("教学保障「" + res.getMc() + "」需求数量 " + s.getSl()
+ " 超过总量 " + res.getSl() + (res.getDw() == null ? "" : res.getDw()), BAD_REQUEST);
}
KCBBZMX row = new KCBBZMX();
row.setBztgmxbh(s.getBztgmxbh());
row.setSl(s.getSl());
row.setBz(s.getBz());
row.setNd(ctx.ndCode);
supportRows.add(row);
}
}
// 教学班次(可临时增删):为空用课程的全部合班班次
List<String> xydbhs = request.getXydbhs() == null ? List.of()
: request.getXydbhs().stream().filter(s -> s != null && !s.isEmpty()).distinct().toList();
// 硬冲突判定所需数据
List<SSKCB> semesterLessons = ctx.lessons;
@@ -351,9 +536,9 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
ignored.add(ignoredCell(cell, "班历不可排课"));
continue;
}
String roomKey = key + "#" + jsbh;
if (roomBlocked.containsKey(roomKey)) {
ignored.add(ignoredCell(cell, "场地历不可用"));
String blockedRoom = rooms.stream().filter(r -> roomBlocked.containsKey(key + "#" + r)).findFirst().orElse(null);
if (blockedRoom != null) {
ignored.add(ignoredCell(cell, "场地历不可用(教室 " + blockedRoom + ""));
continue;
}
if (jybh != null && teacherBlocked.containsKey(key + "#" + jybh)) {
@@ -375,7 +560,7 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
busy = true; busyReason = "班次该节已有其它课程";
break;
}
if (roomsOfLesson.getOrDefault(occupant.getBh(), List.of()).contains(jsbh)) {
if (roomsOfLesson.getOrDefault(occupant.getBh(), List.of()).stream().anyMatch(rooms::contains)) {
busy = true; busyReason = "教室该节已被占用";
break;
}
@@ -396,7 +581,7 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
}
if (!ownOccupied) {
SSKCB lesson = new SSKCB();
lesson.setBh(UuidUtil.getUUID());
lesson.setBh(UuidUtil.getOriginalUUID());
lesson.setSskcbh(course.getBh());
lesson.setJhsjap(0);
lesson.setSjsjap(0);
@@ -415,39 +600,92 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
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);
// 学员队(教学班次,可临时增删)
if (xydbhs.isEmpty()) {
for (SSKCXYD link : ctx.links) {
if (!course.getBh().equals(link.getSskcbh())) continue;
SSKCBXYD lxyd = new SSKCBXYD();
lxyd.setBh(UuidUtil.getOriginalUUID());
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);
}
} else {
for (String xydbh : xydbhs) {
SSKCXYD link = ctx.links.stream()
.filter(k -> course.getBh().equals(k.getSskcbh()) && xydbh.equals(k.getXydbh()))
.findFirst().orElse(null);
XYDB team = xydbMapper.selectById(xydbh);
SSKCBXYD lxyd = new SSKCBXYD();
lxyd.setBh(UuidUtil.getOriginalUUID());
lxyd.setSskcbbh(lesson.getBh());
lxyd.setXydbh(xydbh);
lxyd.setRs(link != null ? link.getRs() : (team != null ? team.getXydrs() : null));
lxyd.setJc(link != null ? link.getJc() : (team != null ? team.getJc() : null));
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);
// 教学场地(可多场地)
for (String room : rooms) {
SSKCBJS ljs = new SSKCBJS();
ljs.setBh(UuidUtil.getOriginalUUID());
ljs.setSskcbbh(lesson.getBh());
ljs.setJsbh(room);
ljs.setNd(ctx.ndCode);
ljs.setRq(lesson.getRq());
ljs.setJc(cell.getJc());
sskcbjsMapper.insert(ljs);
}
// 授课教员:主讲 zjy=1 + 辅讲 zjy=0
if (jybh != null) {
SSKCBFZJY lfz = new SSKCBFZJY();
lfz.setBh(UuidUtil.getUUID());
lfz.setBh(UuidUtil.getOriginalUUID());
lfz.setSskcbbh(lesson.getBh());
lfz.setFzjybh(jybh);
lfz.setZjy(1);
lfz.setNd(ctx.ndCode);
// 参与学员测评 / 学员测评平均分 为非空列:新排课次默认参与、分数待评
lfz.setCyxycp(1);
lfz.setXycppjf(0f);
lfz.setRq(lesson.getRq());
lfz.setJc(cell.getJc());
sskcbfzjyMapper.insert(lfz);
}
for (String fzjybh : fzjybhs) {
if (fzjybh.equals(jybh)) continue;
SSKCBFZJY lfz = new SSKCBFZJY();
lfz.setBh(UuidUtil.getOriginalUUID());
lfz.setSskcbbh(lesson.getBh());
lfz.setFzjybh(fzjybh);
lfz.setZjy(0);
lfz.setNd(ctx.ndCode);
lfz.setCyxycp(1);
lfz.setXycppjf(0f);
lfz.setRq(lesson.getRq());
lfz.setJc(cell.getJc());
sskcbfzjyMapper.insert(lfz);
}
// 教学保障明细
for (KCBBZMX template : supportRows) {
KCBBZMX row = new KCBBZMX();
row.setBh(UuidUtil.getOriginalUUID());
row.setKcbbh(lesson.getBh());
row.setBztgmxbh(template.getBztgmxbh());
row.setSl(template.getSl());
row.setBz(template.getBz());
row.setNd(template.getNd());
row.setRq(lesson.getRq());
row.setJc(cell.getJc());
kcbbzMapper.insert(row);
}
arranged++;
logOperation(course, "安排", "安排 " + cell.getRq() + "" + cell.getJc() + ""
+ (ownOccupied ? "(覆盖原课次)" : ""),
@@ -551,7 +789,8 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
}
sskcxydMapper.delete(new LambdaQueryWrapper<SSKCXYD>()
.eq(SSKCXYD::getSskcbh, course.getBh()));
sskcMapper.deleteById(course.getBh());
// 实施_课程.编号为 VARBINARY GUIDdeleteById 会报 Invalid hexadecimal digits,须走 RAWTOHEX
sskcMapper.deleteByBhs(java.util.List.of(course.getBh()));
logOperation(course, "删除", "彻底删除运行课程", null);
return 1;
}
@@ -570,11 +809,160 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
m.put("czlx", r.getCzlx());
m.put("cznr", r.getCznr());
m.put("czrbh", r.getCzrbh());
m.put("czrmc", resolveOperatorName(r.getCzrbh()));
m.put("czsj", r.getCzsj());
return m;
}).collect(Collectors.toList());
}
/** 操作人显示名:按登录名取用户昵称,取不到则回显登录名 */
private String resolveOperatorName(String username) {
if (username == null || username.isEmpty()) return "-";
try {
SysUser user = userService.selectUserByUserName(username);
if (user != null && user.getNickName() != null) {
return user.getNickName();
}
} catch (Exception ignore) {
// 取不到昵称时回显登录名
}
return username;
}
// ==================== 编辑课程教学任务信息(手册 12.3.1 ====================
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> updateCourse(SchedulingCourseEditRequest request) {
if (request == null || request.getSskcbh() == null || request.getSskcbh().isEmpty()) {
throw new ServiceException("请指定运行课程", BAD_REQUEST);
}
SSKC course = requireCourse(request.getSskcbh());
Ctx ctx = loadCtxForCourse(course);
if (!canEdit(course, ctx)) {
throw new ServiceException("没有该课程的信息修改权限", FORBIDDEN);
}
boolean changed = false;
Integer newXs = null;
Integer newZks = null;
String newJy = null;
String newJs = null;
if (request.getXs() != null && !request.getXs().equals(course.getXs())) {
newXs = request.getXs(); changed = true;
}
if (request.getZks() != null && !request.getZks().equals(course.getZks())) {
newZks = request.getZks(); changed = true;
}
if (request.getJybh() != null && !request.getJybh().isEmpty() && !request.getJybh().equals(course.getJybh())) {
newJy = request.getJybh(); changed = true;
}
if (request.getJsbh() != null && !request.getJsbh().isEmpty() && !request.getJsbh().equals(course.getJsbh())) {
newJs = request.getJsbh(); changed = true;
}
if (changed) {
// 实施_课程.编号为 VARBINARY GUIDupdateById 会对主键绑字符串而报 Invalid hexadecimal digits
sskcMapper.updateBasicByBh(course.getBh(), newXs, newZks, newJy, newJs);
}
// 课程简称回写编制侧任务表(同一班次学期 + 同一课程编号)
if (request.getJc() != null && ctx.semester != null) {
for (XYDRWB task : studentTeamTaskMapper.selectList(new LambdaQueryWrapper<XYDRWB>()
.eq(XYDRWB::getXydxqbh, ctx.semester.getBh())
.eq(XYDRWB::getKbh, course.getKmbh())
.eq(XYDRWB::getDelFlag, 0))) {
task.setJc(request.getJc());
task.setBdsj(LocalDateTime.now());
studentTeamTaskMapper.updateById(task);
changed = true;
}
}
if (changed) {
logOperation(course, "修改", "编辑课程教学任务信息(学时=" + (newXs != null ? newXs : course.getXs())
+ ",周课时=" + (newZks != null ? newZks : course.getZks())
+ ",简称=" + request.getJc() + "", null);
}
Map<String, Object> result = new HashMap<>();
result.put("updated", changed);
return result;
}
// ==================== 教学班次(合班)与候选班次(手册 12.3.2.2 ====================
@Override
public Map<String, Object> courseTeams(String sskcbh) {
SSKC course = requireCourse(sskcbh);
Ctx ctx = loadCtxForCourse(course);
Map<String, Object> result = new HashMap<>();
// 当前教学班次:该运行课程关联的学员队
List<Map<String, Object>> current = new ArrayList<>();
for (SSKCXYD link : ctx.links) {
if (!course.getBh().equals(link.getSskcbh())) continue;
Map<String, Object> m = new HashMap<>();
m.put("xydbh", link.getXydbh());
m.put("xydmc", resolveTeamName(link.getXydbh()));
m.put("rs", link.getRs());
current.add(m);
}
// 候选班次:同学期开设同一课程编号的其它班次学期
List<Map<String, Object>> candidates = new ArrayList<>();
java.util.LinkedHashSet<String> seen = new java.util.LinkedHashSet<>();
for (XYDRWB task : studentTeamTaskMapper.selectList(new LambdaQueryWrapper<XYDRWB>()
.eq(XYDRWB::getKbh, course.getKmbh())
.eq(XYDRWB::getDelFlag, 0))) {
if (Objects.equals(task.getXydxqbh(), ctx.semester == null ? null : ctx.semester.getBh())) continue;
if (task.getXydxqbh() == null || !seen.add(task.getXydxqbh())) continue;
XYDNDXQJBXXB semester = classSemesterMapper.selectById(task.getXydxqbh());
if (semester == null) continue;
Map<String, Object> m = new HashMap<>();
m.put("xydbh", task.getXydbh());
m.put("xydxqbh", task.getXydxqbh());
m.put("xydmc", resolveTeamName(task.getXydbh()));
m.put("nd", semester.getNd());
m.put("xqdc", semester.getXqdc());
candidates.add(m);
}
result.put("current", current);
result.put("candidates", candidates);
return result;
}
// ==================== 教学保障选项(手册 12.3.2.5 ====================
@Override
public Map<String, Object> supportOptions() {
Map<String, Object> result = new HashMap<>();
List<Map<String, Object>> categories = new ArrayList<>();
try {
for (BZLB lb : bzlbMapper.selectList(new LambdaQueryWrapper<BZLB>())) {
if (lb.getTy() != null && lb.getTy() != 0) continue;
Map<String, Object> m = new HashMap<>();
m.put("bh", lb.getBh());
m.put("mc", lb.getMc());
categories.add(m);
}
} catch (Exception e) {
log.debug("[排课窗] 读取保障类别失败: {}", e.getMessage());
}
List<Map<String, Object>> resources = new ArrayList<>();
try {
for (BZTGMX mx : bztgmxMapper.selectList(new LambdaQueryWrapper<BZTGMX>())) {
if (mx.getTy() != null && mx.getTy() != 0) continue;
Map<String, Object> m = new HashMap<>();
m.put("bh", mx.getBh());
m.put("mc", mx.getMc());
m.put("dw", mx.getDw());
m.put("total", mx.getSl());
m.put("bzlbbh", mx.getBzlbbh());
m.put("zylb", mx.getZylb());
resources.add(m);
}
} catch (Exception e) {
log.debug("[排课窗] 读取保障资源失败: {}", e.getMessage());
}
result.put("categories", categories);
result.put("resources", resources);
return result;
}
// ==================== 公共 ====================
/** 上下文:班次学期 + 学员队 + 6 位代号 + 本班次已发布课程与课次 */
@@ -688,6 +1076,7 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
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));
kcbbzMapper.delete(new LambdaQueryWrapper<KCBBZMX>().eq(KCBBZMX::getKcbbh, sskcbbh));
}
/** 校历/班历不可排格键集合 */
@@ -757,7 +1146,8 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
private void logOperation(SSKC course, String czlx, String cznr, SchedulingArrangeRequest ignored) {
try {
KCJXYX_CZRZ row = new KCJXYX_CZRZ();
row.setBh(UuidUtil.getUUID());
// 课程教学运行_操作日志.编号 为 CHAR(36),按项目约定存带横线 36 位 UUID
row.setBh(UuidUtil.getOriginalUUID());
row.setTybh(course.getBh());
row.setCzlx(czlx);
row.setCznr(cznr);
@@ -779,6 +1169,39 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
return kb != null && kb.getKmc() != null ? kb.getKmc() : kbh;
}
/** 教员姓名(失败返回编号) */
private String resolveTeacherName(String jybh) {
if (jybh == null || jybh.isEmpty()) return null;
try {
JYB jyb = jybMapper.selectById(jybh);
return jyb != null && jyb.getJyxm() != null ? jyb.getJyxm() : jybh;
} catch (Exception e) {
return jybh;
}
}
/** 教室名称(失败返回编号) */
private String resolveRoomName(String jsbh) {
if (jsbh == null || jsbh.isEmpty()) return null;
try {
List<JSB> rooms = classRoomMapper.selectList(new LambdaQueryWrapper<JSB>().eq(JSB::getJsbh, jsbh));
return rooms.isEmpty() ? jsbh : (rooms.get(0).getJsmc() != null ? rooms.get(0).getJsmc() : jsbh);
} catch (Exception e) {
return jsbh;
}
}
/** 学员队名称(失败返回编号) */
private String resolveTeamName(String xydbh) {
if (xydbh == null || xydbh.isEmpty()) return null;
try {
XYDB team = xydbMapper.selectById(xydbh);
return team != null && team.getXydmc() != null ? team.getXydmc() : xydbh;
} catch (Exception e) {
return xydbh;
}
}
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;
@@ -595,8 +595,11 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService {
if (row.getKbh() == null || row.getXydbh() == null) {
return false;
}
List<SSKC> courses = sskcMapper.selectList(new LambdaQueryWrapper<SSKC>().eq(SSKC::getKmbh, row.getKbh()));
// 注意:实施_课程 为中文列名 + VARBINARY 主键,selectList 会整行返回 null 元素,
// 这里必须走显式 XML(否则下面的 course.getBh() 会 NPE)。
List<SSKC> courses = sskcMapper.selectByKmbh(row.getKbh());
for (SSKC course : courses) {
if (course == null) continue;
Long linked = sskcxydMapper.selectCount(new LambdaQueryWrapper<SSKCXYD>()
.eq(SSKCXYD::getSskcbh, course.getBh())
.eq(SSKCXYD::getXydbh, row.getXydbh()));
@@ -8,10 +8,12 @@ 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.SSKCXYD;
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.SSKCXYDMapper;
import com.roomroot.jwgl.mapper.StudentTeamTaskMapper;
import com.roomroot.jwgl.mapper.TeachingTaskMapper;
import com.roomroot.jwgl.service.TaskBookFillService;
@@ -25,10 +27,12 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST;
import static com.roomroot.jwgl.utils.BasicManagementConstants.CONFLICT;
@@ -55,6 +59,9 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
@Resource
private TeachingTaskWriteGuard guard;
@Resource
private SSKCXYDMapper sskcxydMapper;
// ==================== 列表 ====================
@Override
@@ -191,7 +198,8 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
public int split(TaskBookBhListRequest request) {
List<XYDRWB> rows = loadRows(request);
assertAllFillable(rows);
// TODO(阶段 5):行已发布到运行课表的须先撤回,再拆班。阶段 5 落地后在此校验
// 阶段 5 已落地:行所属班次学期若已发布到运行课表,合班组已被固化成运行课程,必须先撤回
assertNotPublishedToRunning(rows);
LocalDateTime now = LocalDateTime.now();
int count = 0;
for (XYDRWB row : rows) {
@@ -375,4 +383,33 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
assertFillable(row);
}
}
/**
* 阶段 5 反向门禁:拆班前确认行所在班次学期尚未发布到运行课表。
*
* <p>判定信号与 {@code RunningCoursePublishServiceImpl#publish} 的幂等门禁同源:
* 「实施_课程学员队」按 (学员队编号, 年度=6 位学期代号) 命中即说明该班次学期已整批发布。
* 发布后合班组已固化为运行课程的 实施_课程.编号,拆班会让任务书与运行课表不一致。</p>
*
* <p>不用「实施_课程表.教学保障备注」或「课表变动时间」判定:前者由 logOperation 只更新自己
* 那门课,后者发布时不写,两者都不可靠。</p>
*/
private void assertNotPublishedToRunning(List<XYDRWB> rows) {
Set<String> checked = new HashSet<>();
for (XYDRWB row : rows) {
String xydxqbh = row.getXydxqbh();
if (xydxqbh == null || xydxqbh.isEmpty() || !checked.add(xydxqbh)) {
continue;
}
XYDNDXQJBXXB semester = requireSemester(xydxqbh);
Integer ndCode = SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc());
Long published = sskcxydMapper.selectCount(new LambdaQueryWrapper<SSKCXYD>()
.eq(SSKCXYD::getXydbh, semester.getXydbh())
.eq(SSKCXYD::getNd, ndCode));
if (published != null && published > 0) {
throw new ServiceException("班次学期 " + semester.getXydbh() + "" + ndCode + ")已发布 "
+ published + " 条运行课程,请先在运行课表撤回后再拆班", CONFLICT);
}
}
}
}
@@ -4,8 +4,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.JYL;
import com.roomroot.jwgl.entity.SSKC;
import com.roomroot.jwgl.entity.SSKCB;
import com.roomroot.jwgl.entity.SSKCBFZJY;
import com.roomroot.jwgl.entity.SSKCXYD;
import com.roomroot.jwgl.entity.TimetableConflictResult;
import com.roomroot.jwgl.entity.XYDB;
@@ -14,6 +16,7 @@ import com.roomroot.jwgl.entity.XQXLB;
import com.roomroot.jwgl.mapper.ClassRoomCalendarMapper;
import com.roomroot.jwgl.mapper.ClassSemesterMapper;
import com.roomroot.jwgl.mapper.JQBMapper;
import com.roomroot.jwgl.mapper.JYLMapper;
import com.roomroot.jwgl.mapper.KBMapper;
import com.roomroot.jwgl.mapper.SSKCBFZJYMapper;
import com.roomroot.jwgl.mapper.SSKCBJSMapper;
@@ -96,6 +99,9 @@ public class TimetableConflictServiceImpl implements TimetableConflictService {
@Resource
private ClassRoomCalendarMapper classRoomCalendarMapper;
@Resource
private JYLMapper jylMapper;
@Resource
private SSKCBMapper sskcbMapper;
@@ -258,7 +264,13 @@ public class TimetableConflictServiceImpl implements TimetableConflictService {
details = switch (dimension) {
case TEAM_CONFLICT -> sskcbxydMapper.selectTeamConflictDetails(nd);
case ELECTIVE_REQUIRED_CONFLICT -> sskcbxydMapper.selectElectiveRequiredConflictDetails(nd);
case TEACHER_CONFLICT -> sskcbfzjyMapper.selectTeacherConflictDetails(nd);
case TEACHER_CONFLICT -> {
// 口径统一(与排课窗口硬冲突一致):教员时间冲突 = 教员双占 + 教员历不可排
List<TimetableConflictDetailVO> teacherDup =
new ArrayList<>(sskcbfzjyMapper.selectTeacherConflictDetails(nd));
teacherDup.addAll(checkTeacherCalendarConflicts(nd));
yield teacherDup;
}
case CLASSROOM_CONFLICT -> {
// 阶段 5.1:教室重复占用 + 场地历不可用
List<TimetableConflictDetailVO> dup = sskcbjsMapper.selectClassroomConflictDetails(nd);
@@ -626,17 +638,92 @@ public class TimetableConflictServiceImpl implements TimetableConflictService {
return result;
}
/** 构建 不可排课格 类冲突明细 */
/**
* TEACHER_CONFLICT 追加:教员历 标记不可排课(可排课=0)但该教员该格已有课次。
* <p>
* 与 {@link #checkClassroomCalendarConflicts} 同为"不可排课格"类检查,只是主体换成教员:
* 排课窗口硬冲突里含「教员历不可用」,批量检查此前缺失该维度,此处补齐以保证两套口径一致。
* </p>
* <p>
* 匹配口径与排课窗口一致:按 <b>日期(天) + 节次 + 教员编号</b> 三元组,不比对具体时分。
* </p>
*/
private List<TimetableConflictDetailVO> checkTeacherCalendarConflicts(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;
}
// 教员历 无"删除状态"列,只需 可排课 = 0
List<JYL> unavailable = jylMapper.selectList(new LambdaQueryWrapper<JYL>()
.eq(JYL::getKpk, 0)
.isNotNull(JYL::getRq));
if (unavailable.isEmpty()) {
return result;
}
Set<String> lessonIds = lessons.stream().map(SSKCB::getBh).collect(Collectors.toSet());
// 课次 → 教员编号集合(主讲 + 辅讲统一纳入检查)
Map<String, List<String>> teachersOfLesson = new HashMap<>();
for (SSKCBFZJY link : sskcbfzjyMapper.selectList(new LambdaQueryWrapper<SSKCBFZJY>()
.in(SSKCBFZJY::getSskcbbh, lessonIds))) {
if (link.getFzjybh() == null) continue;
teachersOfLesson.computeIfAbsent(link.getSskcbbh(), k -> new ArrayList<>()).add(link.getFzjybh());
}
if (teachersOfLesson.isEmpty()) {
return result;
}
Map<String, String> courseNames = loadCourseNames(nd);
// 按 教员编号#日期#节次 → 该格已有课次,便于一次聚合出明细
Map<String, List<SSKCB>> lessonsByTeacherCell = new HashMap<>();
Map<String, SSKCB> lessonByBh = lessons.stream()
.collect(Collectors.toMap(SSKCB::getBh, l -> l, (a, b) -> a));
for (Map.Entry<String, List<String>> e : teachersOfLesson.entrySet()) {
SSKCB lesson = lessonByBh.get(e.getKey());
if (lesson == null || lesson.getRq() == null || lesson.getJc() == null) continue;
String dateKey = lesson.getRq().toLocalDate().toString() + "#" + lesson.getJc();
for (String jybh : e.getValue()) {
lessonsByTeacherCell.computeIfAbsent(jybh + "#" + dateKey, k -> new ArrayList<>()).add(lesson);
}
}
for (JYL row : unavailable) {
if (row.getRq() == null || row.getJybh() == null) continue;
String dateStr = row.getRq().toLocalDate().toString();
String key = row.getJybh() + "#" + dateStr + "#" + row.getJc();
List<SSKCB> hit = lessonsByTeacherCell.get(key);
if (hit == null || hit.isEmpty()) continue;
TimetableConflictDetailVO d = buildEventDetail(
"TEACHERCAL|" + row.getJybh() + "|" + dateStr + "#" + row.getJc(),
safeName(row.getMc()), dateStr, row.getJc(), hit, courseNames,
"教员历标记该教员该时段不可排课,但已有课次安排",
TimetableConflictDimension.TEACHER_CONFLICT);
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) {
return buildEventDetail(resourceId, resourceName, dateStr, jc, lessons, courseNames, reason,
TimetableConflictDimension.EVENT_CONFLICT);
}
/** 构建 不可排课格 类冲突明细(可指定归属维度,使明细归属与其所在卡片一致) */
private TimetableConflictDetailVO buildEventDetail(String resourceId, String resourceName,
String dateStr, Integer jc, List<SSKCB> lessons,
Map<String, String> courseNames, String reason,
TimetableConflictDimension dimension) {
String courses = lessons.stream()
.map(l -> courseNames.getOrDefault(l.getSskcbh(), l.getSskcbh()))
.distinct()
.collect(Collectors.joining(""));
return TimetableConflictDetailVO.builder()
.dimensionCode("EVENT_CONFLICT")
.dimensionTitle("日历冲突")
.dimensionCode(dimension.getCode())
.dimensionTitle(dimension.getTitle())
.resourceId(resourceId)
.resourceName(resourceName)
.rq(LocalDate.parse(dateStr).atStartOfDay())
@@ -652,13 +739,27 @@ public class TimetableConflictServiceImpl implements TimetableConflictService {
/** 实施_课程编号 → 课程名称(经 实施_课程.科目编号 → 课表.课名称) */
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));
// 必须走显式 XMLselectList 在本表(中文列名 + VARBINARY 主键)会整行返回 null 元素
List<SSKC> courses = sskcMapper.selectByNd(nd);
if (courses == null || courses.isEmpty()) {
return names;
}
Set<String> kbhs = courses.stream()
.filter(Objects::nonNull)
.map(SSKC::getKmbh)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<String, String> kbhNames = new HashMap<>();
if (!kbhs.isEmpty()) {
for (com.roomroot.jwgl.entity.KB k : kbMapper.selectBatchIds(kbhs)) {
if (k == null || k.getKbh() == null) continue;
kbhNames.put(k.getKbh(), safeName(k.getKmc()));
}
}
for (SSKC c : courses) {
names.put(c.getBh(), kbhNames.getOrDefault(c.getKmbh(), c.getKmbh()));
if (c == null) continue;
names.put(c.getBh(), c.getKmbh() == null ? safeName(c.getBh())
: kbhNames.getOrDefault(c.getKmbh(), safeName(c.getKmbh())));
}
return names;
}
@@ -52,33 +52,48 @@ public enum TimetableConflictDimension {
/**
* 教员时间冲突检查。
* <p>检查逻辑:同一教员(主讲+辅讲统一在此维度)在同一日期同一节次被安排多门课程 → 冲突</p>
* <p>检查逻辑(两部分,均为硬冲突):
* <ol>
* <li>同一教员(主讲+辅讲统一在此维度)在同一日期同一节次被安排多门课程 → 冲突</li>
* <li>教员历(教员工作时间表)标记"可排课=0"的日期节次上,该教员仍有已排课次 → 冲突</li>
* </ol>
* </p>
* <p>涉及数据表:
* <ol>
* <li>实施_课程表 SSKCB —— 主表,日期/节次/年度/删除状态</li>
* <li>实施_课程表_辅助教员 SSKCBFZJY —— 课次与教员的关联表(含主讲ZJY=1 与辅讲ZJY=0),获取教员编号(FZJYBH)</li>
* <li>教员历 JYL —— 获取教员"可排课"标记(可排课=0 表示不可排)</li>
* <li>教员表 JYB / 教研室人员 JYSX —— 翻译中文姓名展示</li>
* </ol>
* </p>
* <p><b>口径说明:</b>排课窗口的硬冲突包含「教员历不可用」,为此本维度同样纳入教员历检查,
* 使批量冲突检查与排课窗口判断一致。实现方式与「场地历不可用」并入
* {@link #CLASSROOM_CONFLICT} 保持一致,不再单开卡片,避免同一事实被重复计数。</p>
*/
TEACHER_CONFLICT("TEACHER_CONFLICT",
"教员时间冲突检查",
"同一教员在同一时间段被安排多门课程"),
"同一教员在同一时间段被安排多门课程,或教员历不可排课时段已有课程"),
/**
* 教室时间冲突检查。
* <p>检查逻辑:同一教室(场地)在同一日期同一节次被多门课程占用 → 冲突</p>
* <p>检查逻辑(两部分,均为硬冲突):
* <ol>
* <li>同一教室(场地)在同一日期同一节次被多门课程占用 → 冲突</li>
* <li>教学场地历标记"可排课=0"的教室节次上仍有已排课次 → 冲突(场地历不可用)</li>
* </ol>
* </p>
* <p>涉及数据表:
* <ol>
* <li>实施_课程表 SSKCB —— 主表,日期/节次/年度/删除状态</li>
* <li>实施_课程表_教室 SSKCBJS —— 课次与教室的关联表,获取教室编号(JSBH)</li>
* <li>教学场地历 JXCDL —— 获取场地"可排课"标记</li>
* <li>教室表 JSB —— 翻译中文名称(含容纳人数/设施等信息)</li>
* </ol>
* </p>
*/
CLASSROOM_CONFLICT("CLASSROOM_CONFLICT",
"教室时间冲突检查",
"同一教室在同一时间段被多门课程占用"),
"同一教室在同一时间段被多门课程占用,或场地历不可用时段已有课程"),
/**
* 教学保障资源冲突检查(预留/按需开启)。
@@ -34,11 +34,26 @@ public class SchedulingViewVO {
/** 时间区:按周切分,格子叠加校历/班历不可排与已排课次 */
private List<Week> weeks = new ArrayList<>();
/** 节次字典(节次时间表),供前端显示控制(7-8 节 / 晚上 / 夜间) */
private List<Period> periods = new ArrayList<>();
@Data
public static class Period {
/** 节次号 */
private Integer jc;
/** 节次名称/简称 */
private String label;
/** 时段:上午 / 下午 / 晚上 */
private String sd;
}
@Data
public static class Course {
private String sskcbh;
private String kbh;
private String kcmc;
/** 课程简称(任务表 简称,格内优先显示) */
private String jc;
private String klx;
private Integer xs;
private Integer zks;
@@ -46,12 +61,27 @@ public class SchedulingViewVO {
private String jyxm;
private String jsbh;
private String jsmc;
/** 排课建议(教研室任务书填报的 教研室计划备注) */
private String jysjhbz;
/** 教研室计划教员编号 / 姓名 */
private String jysjhjybh;
private String jysjhjyxm;
/** 已排学时(每节 2 学时 + 节次调节) */
private Integer scheduledHours;
/** 是否已排满(scheduledHours >= xs */
private Boolean full;
/** 当前用户是否可编辑该课程 */
private Boolean editable;
/** 合班班次(⑤ 合班信息) */
private List<Team> teams = new ArrayList<>();
}
/** 班次学期(学员队 + 学期)简项 */
@Data
public static class Team {
private String xydxqbh;
private String xydbh;
private String xydmc;
}
@Data
@@ -78,8 +108,16 @@ public class SchedulingViewVO {
private Boolean unavailable;
/** 硬冲突原因(不可排课来源:校历/班历等) */
private String reason;
/** 不可排来源类别:SCHOOL 校历 / CLASS 班历 */
private String calendarKind;
/** 软提示(非正课 / 配当周次不符) */
private String warning;
/** 是否为非正课时段 */
private Boolean soft;
/** 该节次被场地历标记不可用的教室编号 */
private List<String> blockedRooms = new ArrayList<>();
/** 该节次被教员历标记不可用的教员编号 */
private List<String> blockedTeachers = new ArrayList<>();
private List<Lesson> lessons = new ArrayList<>();
}
@@ -89,10 +127,21 @@ public class SchedulingViewVO {
private String sskcbh;
private String kcmc;
private String jxnr;
private String jxyd;
private String jxff;
private Integer jcdj;
private String jyxm;
private String jybh;
private String jsmc;
private String jsbh;
private String jxbzbz;
private String bzms;
private String ycxx;
/** 本次课次的学员队(教学班次) */
private List<String> xydbhs = new ArrayList<>();
/** 本次课次的教室(可多场地) */
private List<String> jsbhs = new ArrayList<>();
/** 本次课次的授课教员(含主讲/辅讲) */
private List<String> jybhs = new ArrayList<>();
}
}