forked from liweijie/education
排课功能优化
This commit is contained in:
+23
@@ -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 */
|
||||
|
||||
+31
@@ -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 GUID,deleteById 直绑字符串会报
|
||||
* 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 自动 SQL(BaseMapper.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 当十六进制转 VARBINARY,UUID 中的 '-' 报 -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);
|
||||
}
|
||||
|
||||
+10
@@ -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();
|
||||
}
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+86
@@ -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) {
|
||||
|
||||
+24
-4
@@ -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 GUID,deleteById 直绑字符串会报
|
||||
// dm.jdbc.driver.DMException: Invalid hexadecimal digits,必须走 RAWTOHEX
|
||||
sskcMapper.deleteByBhs(java.util.List.of(sskcbh));
|
||||
}
|
||||
removed++;
|
||||
}
|
||||
|
||||
+458
-35
@@ -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 GUID,deleteById 会报 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 GUID,updateById 会对主键绑字符串而报 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;
|
||||
|
||||
+4
-1
@@ -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()));
|
||||
|
||||
+38
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+111
-10
@@ -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));
|
||||
// 必须走显式 XML:selectList 在本表(中文列名 + 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;
|
||||
}
|
||||
|
||||
+19
-4
@@ -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",
|
||||
"教室时间冲突检查",
|
||||
"同一教室在同一时间段被多门课程占用"),
|
||||
"同一教室在同一时间段被多门课程占用,或场地历不可用时段已有课程"),
|
||||
|
||||
/**
|
||||
* 教学保障资源冲突检查(预留/按需开启)。
|
||||
|
||||
+49
@@ -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<>();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user