排课功能优化

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 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);
}
@@ -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 GUID,deleteById 直绑字符串会报
// 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,11 +600,12 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
lesson.setCjsj(now);
lesson.setBdsj(now);
sskcbMapper.insert(lesson);
// 学员队 / 教室 / 主讲教员
// 学员队(教学班次,可临时增删)
if (xydbhs.isEmpty()) {
for (SSKCXYD link : ctx.links) {
if (!course.getBh().equals(link.getSskcbh())) continue;
SSKCBXYD lxyd = new SSKCBXYD();
lxyd.setBh(UuidUtil.getUUID());
lxyd.setBh(UuidUtil.getOriginalUUID());
lxyd.setSskcbbh(lesson.getBh());
lxyd.setXydbh(link.getXydbh());
lxyd.setRs(link.getRs());
@@ -429,25 +615,77 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
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);
}
}
// 教学场地(可多场地)
for (String room : rooms) {
SSKCBJS ljs = new SSKCBJS();
ljs.setBh(UuidUtil.getUUID());
ljs.setBh(UuidUtil.getOriginalUUID());
ljs.setSskcbbh(lesson.getBh());
ljs.setJsbh(jsbh);
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;
@@ -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));
// 必须走显式 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;
}
@@ -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<>();
}
}
@@ -58,3 +58,47 @@ export function listSchedulingLogs(sskcbh, czlx) {
params: { sskcbh, czlx }
})
}
// 编辑课程教学任务信息(学时/周课时/课程简称/责任教员/默认场地)
export function updateSchedulingCourse(data) {
return request({
url: '/schedulingWindow/course/update',
method: 'post',
data
})
}
// 教学班次(合班)当前列表 + 候选班次
export function getCourseTeams(sskcbh) {
return request({
url: '/schedulingWindow/course/teams',
method: 'get',
params: { sskcbh }
})
}
// 教学保障选项(保障类别 + 保障资源,含总量与单位)
export function getSupportOptions() {
return request({
url: '/schedulingWindow/support/options',
method: 'get'
})
}
// 批量发布【运行课表】(手册 12.1)
export function publishRunningCourses(xydxqbhs) {
return request({
url: '/schedulingWindow/publish',
method: 'post',
data: { xydxqbhs }
})
}
// 批量删除【运行课表】(手册 12.2)
export function withdrawRunningCourses(xydxqbhs) {
return request({
url: '/schedulingWindow/withdraw',
method: 'post',
data: { xydxqbhs }
})
}
@@ -0,0 +1,32 @@
import request from '@/utils/request'
// 教员历(后端 /teacherCalendar/jyl)。
// 教员历 = 教员自己的工作时间表:标出的时段「不可排课」。
// 排课窗口的硬冲突「教员历不可用」与批量冲突检查的教员时间冲突都读这张表。
// 按教员编号查询该教员已登记的教员历(返回全部,前端按周过滤)
export function listTeacherCalendarByJybh(jybh) {
return request({
url: '/teacherCalendar/jyl/listByJybh',
method: 'get',
params: { jybh }
})
}
// 批量保存(按 教员编号+日期+节次 幂等覆盖)
export function batchSaveTeacherCalendar(list) {
return request({
url: '/teacherCalendar/jyl/batchSave',
method: 'post',
data: list
})
}
// 批量删除(教员历表无 del_flag 审计列,为物理删除)
export function batchDeleteTeacherCalendar(bhs) {
return request({
url: '/teacherCalendar/jyl/batchDelete',
method: 'post',
data: bhs
})
}
File diff suppressed because it is too large Load Diff
@@ -99,11 +99,12 @@ import { listAllSemester } from '@/api/teachBusiness/semester'
// 四类检查卡片定义(标题/描述与后端 TimetableConflictDimension 枚举一致,作为页面布局常量;
// 检查状态与冲突数完全来自后端接口)
// 注意:教员卡片含「教员历不可排」、教室卡片含「场地历不可用」,与排课窗口硬冲突口径一致
const CARD_DEFS = [
{ key: 'TEAM_CONFLICT', label: '教学班时间冲突检查', desc: '同一教学班次在同一时间段被安排多门课程' },
{ key: 'ELECTIVE_REQUIRED_CONFLICT', label: '教学班必修与选修时间冲突检查', desc: '教学班次必修课与选修课时间重叠' },
{ key: 'TEACHER_CONFLICT', label: '教员时间冲突检查', desc: '同一教员在同一时间段被安排多门课程' },
{ key: 'CLASSROOM_CONFLICT', label: '教室时间冲突检查', desc: '同一教室在同一时间段被多门课程占用' }
{ key: 'TEACHER_CONFLICT', label: '教员时间冲突检查', desc: '同一教员在同一时间段被安排多门课程,或教员历不可排课时段已有课程' },
{ key: 'CLASSROOM_CONFLICT', label: '教室时间冲突检查', desc: '同一教室在同一时间段被多门课程占用,或场地历不可用时段已有课程' }
]
export default {
@@ -64,6 +64,8 @@
<el-button type="success" plain icon="el-icon-date" :disabled="!currentRow" @click="handleEditEventCalendar">编辑班历</el-button>
<el-button type="success" plain icon="el-icon-calendar" :disabled="!currentRow" @click="handleEditCalendar">编辑班次教学任务</el-button>
<el-button type="success" plain icon="el-icon-data-line" :disabled="!currentRow" @click="handleAllocation">教学配当</el-button>
<el-button type="primary" plain icon="el-icon-upload2" :disabled="!selection.length" @click="handlePublishRunning">发布运行课表</el-button>
<el-button type="danger" plain icon="el-icon-delete-solid" :disabled="!selection.length" @click="handleWithdrawRunning">删除运行课表</el-button>
</div>
</div>
@@ -333,6 +335,7 @@ import { batchAutoGenerateTasks } from '@/api/studentRecords/classCalendar'
import { listSemester, allSemester, addSemester, updateSemester, delSemester, batchDeleteSemester, batchUpdateDateRange, batchUpdateJxrwbh, batchCreateSemester, batchUpdateXqdc, batchUpdateKfjypk, batchWaveMerge, batchWaveSplit, listCreatableClasses, presetSemesterDates } from '@/api/studentRecords/semester'
import { listAllSemester } from '@/api/teachBusiness/semester'
import { listTeachingTask } from '@/api/teachBusiness/teachingTask'
import { publishRunningCourses, withdrawRunningCourses } from '@/api/teachBusiness/schedulingWindow'
export default {
name: 'ShiftSemester',
@@ -346,6 +349,8 @@ export default {
return {
loading: false,
saving: false,
/** 运行课表批量发布/删除进行中 */
runningPublishing: false,
// ==================== 查询条件 ====================
searchForm: { xydbh: '', xqdc: undefined },
@@ -663,6 +668,70 @@ export default {
this.allocationVisible = true
},
// 批量发布【运行课表】(手册 12.1):把课程任务发布为运行课程,之后才能在排课窗口排课
handlePublishRunning() {
const rows = this.selection.slice()
if (!rows.length) {
this.$message.warning('请先勾选班次学期')
return
}
const ids = rows.map(item => item.bh || item.xydxqbh).filter(Boolean)
this.$confirm(
`将发布所选 ${ids.length} 个班次学期的课程到运行课表。<br/>发布前要求:教学任务已发布未结束、每门课程已指定责任教员与默认场地。`,
'批量发布【运行课表】',
{ type: 'warning', dangerouslyUseHTMLString: true }
).then(() => {
this.batchRunningPublish(publishRunningCourses, ids, '发布')
}).catch(() => {})
},
// 批量删除【运行课表】(手册 12.2):撤回运行课程,已排课次一并删除
handleWithdrawRunning() {
const rows = this.selection.slice()
if (!rows.length) {
this.$message.warning('请先勾选班次学期')
return
}
const ids = rows.map(item => item.bh || item.xydxqbh).filter(Boolean)
this.$confirm(
`将从运行课表删除所选 ${ids.length} 个班次学期的全部课程。<br/><b>这些班次的排课信息(已排课次)将一并删除</b>,编制侧任务保留。`,
'批量删除【运行课表】',
{ type: 'warning', dangerouslyUseHTMLString: true }
).then(() => {
this.batchRunningPublish(withdrawRunningCourses, ids, '删除')
}).catch(() => {})
},
// 批量发布/删除公共处理:逐条展示成功与失败原因
batchRunningPublish(api, ids, action) {
this.runningPublishing = true
api(ids).then(res => {
const list = (res && res.data) || []
const ok = list.filter(i => i.success)
const fail = list.filter(i => !i.success)
const okText = ok.length
? `成功 ${ok.length} 个:` + ok.map(i => `${this.semesterNameOf(i.xydxqbh)}(${i.message})`).join(';')
: ''
const failText = fail.length
? `失败 ${fail.length} 个:` + fail.map(i => `${this.semesterNameOf(i.xydxqbh)}(${i.message})`).join(';')
: ''
const html = [okText, failText].filter(Boolean).join('<br/>')
if (fail.length) {
this.$alert(html, `运行课表${action}结果`, { dangerouslyUseHTMLString: true })
} else {
this.$message.success(`运行课表${action}完成:成功 ${ok.length} 个`)
}
this.loadList()
}).finally(() => {
this.runningPublishing = false
})
},
semesterNameOf(bh) {
const row = this.selection.find(i => (i.bh || i.xydxqbh) === bh)
return row ? (row.xydmc || row.bh) : bh
},
handleBatchGenerateTasks() {
if (!this.selection.length) {
this.$message.warning('请先勾选班次学期')
@@ -0,0 +1,461 @@
<template>
<el-dialog
:visible="dialogVisible"
:title="`教员历——${teacherTitle}`"
width="92%"
top="4vh"
:close-on-click-modal="false"
class="teacher-calendar-dialog"
@update:visible="val => dialogVisible = val"
>
<div class="dialog-toolbar">
<span class="toolbar-label">学期</span>
<el-select
v-model="nd"
size="small"
filterable
:loading="semesterLoading"
placeholder="请选择学期"
class="semester-select"
@change="onSemesterChange"
>
<el-option
v-for="item in semesterOptions"
:key="item.nd"
:label="getSemesterName(item.nd)"
:value="item.nd"
/>
</el-select>
<span class="toolbar-label">周次</span>
<el-select v-model="weekIndex" size="small" class="week-select" :disabled="!weeks.length">
<el-option
v-for="(w, i) in weeks"
:key="'w-' + i"
:label="`第 ${i + 1} 周(${w.rangeText})`"
:value="i"
/>
</el-select>
<el-button size="small" :disabled="weekIndex <= 0" @click="handlePrevWeek">上一周</el-button>
<el-button size="small" :disabled="weekIndex >= weeks.length - 1" @click="handleNextWeek">下一周</el-button>
<el-button size="small" type="warning" plain @click="handleClearWeek">清除本周标记</el-button>
<span class="hint">点击格子标记「不可排课」。标记后,排课窗口会判为硬冲突「教员历不可用」,批量冲突检查的「教员时间冲突」也会报出。</span>
</div>
<div v-loading="loading" class="editor-wrap">
<table v-if="dayColumns.length" class="tcal-table">
<thead>
<tr>
<th class="jc-col">节次</th>
<th
v-for="d in dayColumns"
:key="d.dateStr"
:class="{ weekend: d.weekend }"
>
<span class="day-label">{{ d.weekLabel }}</span>
<span class="day-date">{{ d.dateStr.slice(5) }}</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="jc in JC_LIST" :key="'jc-' + jc">
<td class="jc-col">
第{{ jc }}节<span class="slot-hint">{{ slotGroup(jc) }}</span>
</td>
<td
v-for="d in dayColumns"
:key="d.dateStr + '#' + jc"
class="tcal-cell"
:class="{ marked: isMarked(d.dateStr, jc), weekend: d.weekend }"
@click="toggleCell(d.dateStr, jc)"
>
<span v-if="isMarked(d.dateStr, jc)">不可排课</span>
</td>
</tr>
</tbody>
</table>
<el-empty v-else description="请选择学期(学期需先维护开学/结束日期)" :image-size="60" />
</div>
<div slot="footer" class="dialog-footer">
<span class="pending-tip">
本次改动:新增/更新 {{ addCount }} 格,取消 {{ removeCount }} 格
</span>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
</div>
</el-dialog>
</template>
<script>
import { listAllSemester, getSemester } from '@/api/teachBusiness/semester'
import {
listTeacherCalendarByJybh,
batchSaveTeacherCalendar,
batchDeleteTeacherCalendar
} from '@/api/teachOffice/teacherCalendar'
// 节次 1-12:与 实施_课程表.节次 / 教学场地历.节次 同一套编号,
// 冲突检查按 (教员 + 日期 + 节次) 精确比对,所以这里按单节次存储,不做 1-2 节合并。
const JC_LIST = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
const SLOT_GROUP = {
1: '上午', 2: '上午', 3: '上午', 4: '上午',
5: '下午', 6: '下午', 7: '下午', 8: '下午',
9: '晚上', 10: '晚上', 11: '夜间', 12: '夜间'
}
const WEEK_LABELS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
function pad2(n) {
return n < 10 ? '0' + n : '' + n
}
function fmtDate(d) {
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
}
export default {
name: 'TeacherCalendarDialog',
props: {
visible: { type: Boolean, default: false },
// 教员行(教员表):需要 jybh / jyxm
teacher: { type: Object, default: null }
},
data() {
return {
JC_LIST,
nd: '',
semesterOptions: [],
semesterLoading: false,
startDate: null,
endDate: null,
weeks: [],
weekIndex: 0,
// 'yyyy-MM-dd#jc' -> 记录编号(新增未保存的格值为 null)
marked: {},
// 载入时的原始快照:'yyyy-MM-dd#jc' -> 记录编号
original: {},
loading: false,
saving: false
}
},
computed: {
dialogVisible: {
get() { return this.visible },
set(val) { this.$emit('update:visible', val) }
},
teacherTitle() {
const t = this.teacher || {}
return t.jyxm || t.jybh || '教员历'
},
jybh() {
const t = this.teacher || {}
return t.jybh || ''
},
dayColumns() {
const w = this.weeks[this.weekIndex]
if (!w || !w.days) return []
return w.days.map((d, i) => ({
dateStr: d,
weekLabel: WEEK_LABELS[i],
weekend: i >= 5
}))
},
addCount() {
return Object.keys(this.marked).filter(k => !this.original[k]).length
},
removeCount() {
return Object.keys(this.original).filter(k => !(k in this.marked)).length
}
},
watch: {
visible(val) {
if (val) {
this.loadSemesters()
}
},
jybh(val) {
if (val && this.visible) {
this.loadMarks()
}
}
},
methods: {
slotGroup(jc) {
return SLOT_GROUP[jc] || ''
},
isMarked(dateStr, jc) {
return Object.prototype.hasOwnProperty.call(this.marked, dateStr + '#' + jc)
},
toggleCell(dateStr, jc) {
const key = dateStr + '#' + jc
if (Object.prototype.hasOwnProperty.call(this.marked, key)) {
this.$delete(this.marked, key)
} else {
this.$set(this.marked, key, null)
}
},
handlePrevWeek() {
if (this.weekIndex > 0) this.weekIndex -= 1
},
handleNextWeek() {
if (this.weekIndex < this.weeks.length - 1) this.weekIndex += 1
},
handleClearWeek() {
const cols = this.dayColumns
if (!cols.length) return
cols.forEach(d => {
this.JC_LIST.forEach(jc => {
const key = d.dateStr + '#' + jc
if (Object.prototype.hasOwnProperty.call(this.marked, key)) {
this.$delete(this.marked, key)
}
})
})
this.$message.info('已清除本周标记,记得点「保存」')
},
/* ---------- 学期与周次 ---------- */
loadSemesters() {
if (this.semesterOptions.length && this.nd) {
this.loadMarks()
return
}
this.semesterLoading = true
listAllSemester().then(response => {
const list = Array.isArray(response.data) ? response.data : (response.data && response.data.records) || []
this.semesterOptions = list
if (!this.nd) {
const current = list.find(i => i.dqxq) || list[0]
if (current) this.nd = current.nd
}
return this.loadSemesterRange()
}).catch(() => {
this.semesterOptions = []
}).finally(() => {
this.semesterLoading = false
})
},
onSemesterChange() {
this.weekIndex = 0
return this.loadSemesterRange()
},
loadSemesterRange() {
if (!this.nd) return Promise.resolve()
return getSemester(this.nd).then(res => {
const data = res.data || res || {}
const kx = (data.kxrq || '').toString().slice(0, 10)
const jx = (data.jsrq || '').toString().slice(0, 10)
if (kx && jx) {
this.startDate = new Date(kx.replace(/-/g, '/'))
this.endDate = new Date(jx.replace(/-/g, '/'))
this.buildWeeks()
} else {
this.weeks = []
}
return this.loadMarks()
}).catch(() => {
this.weeks = []
this.$message.warning('学期详情获取失败,请确认该学期已维护开学/结束日期')
return this.loadMarks()
})
},
getMonday(date) {
const day = date.getDay()
const diff = date.getDate() - day + (day === 0 ? -6 : 1)
const d = new Date(date)
d.setDate(diff)
return d
},
buildWeeks() {
if (!this.startDate || !this.endDate) return
const start = this.getMonday(new Date(this.startDate))
const end = new Date(this.endDate)
const days = Math.round((end - start) / (24 * 3600 * 1000)) + 1
const totalWeeks = Math.max(1, Math.ceil(days / 7))
const weeks = []
for (let w = 0; w < totalWeeks; w++) {
const wkStart = new Date(start)
wkStart.setDate(start.getDate() + w * 7)
const wkEnd = new Date(wkStart)
wkEnd.setDate(wkStart.getDate() + 6)
const dayList = []
for (let i = 0; i < 7; i++) {
const d = new Date(wkStart)
d.setDate(wkStart.getDate() + i)
dayList.push(fmtDate(d))
}
weeks.push({
rangeText: pad2(wkStart.getMonth() + 1) + '-' + pad2(wkStart.getDate()) +
'至' + pad2(wkEnd.getMonth() + 1) + '-' + pad2(wkEnd.getDate()),
days: dayList
})
}
this.weeks = weeks
if (this.weekIndex > weeks.length - 1) this.weekIndex = 0
},
/** 学期代号 -> 学期名称(如 202601 -> 2026年春季学期) */
getSemesterName(nd) {
if (!nd) return ''
const s = String(nd)
const xn = s.slice(0, 4)
const xq = s.slice(4)
const map = { '01': '春季学期', '02': '夏季学期', '03': '秋季学期' }
return xn + '年' + (map[xq] || '第' + xq + '学期')
},
/* ---------- 教员历读取与保存 ---------- */
loadMarks() {
if (!this.jybh) {
this.marked = {}
this.original = {}
return Promise.resolve()
}
this.loading = true
return listTeacherCalendarByJybh(this.jybh).then(res => {
const list = Array.isArray(res.data) ? res.data : (res.data && res.data.records) || []
const marked = {}
const original = {}
list.forEach(row => {
if (!row || !row.rq || row.jc === null || row.jc === undefined) return
const key = String(row.rq).slice(0, 10) + '#' + row.jc
marked[key] = row.bh
original[key] = row.bh
})
this.marked = marked
this.original = original
}).catch(() => {
this.marked = {}
this.original = {}
}).finally(() => {
this.loading = false
})
},
handleSave() {
if (!this.jybh) {
this.$message.warning('缺少教员编号,无法保存')
return
}
const rows = []
const del = []
Object.keys(this.marked).forEach(k => {
if (this.original[k]) return // 原有记录且未改动
const parts = k.split('#')
rows.push({
jybh: this.jybh,
// JYL.rq 是 LocalDateTime,Jackson 只认 ISO-8601,必须带 'T';
// 用 'yyyy-MM-dd HH:mm:ss'(场地历 JXCDL 是 java.util.Date 才吃这种)会 400。
rq: parts[0] + 'T00:00:00',
jc: Number(parts[1]),
kpk: 0, // 教员历的语义:标出的时段不可排课
mc: '不可排课',
bz: null
})
})
Object.keys(this.original).forEach(k => {
if (!(k in this.marked)) del.push(this.original[k])
})
if (!rows.length && !del.length) {
this.$message.info('没有改动需要保存')
return
}
this.saving = true
const tasks = []
if (rows.length) tasks.push(batchSaveTeacherCalendar(rows))
if (del.length) tasks.push(batchDeleteTeacherCalendar(del))
Promise.all(tasks).then(() => {
this.$message.success('已保存:新增/更新 ' + rows.length + ' 格,取消 ' + del.length + ' 格')
return this.loadMarks()
}).catch(() => {
this.$message.error('教员历保存失败')
}).finally(() => {
this.saving = false
})
}
}
}
</script>
<style scoped>
.dialog-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
}
.toolbar-label {
font-size: 13px;
color: #606266;
}
.semester-select {
width: 180px;
}
.week-select {
width: 200px;
}
.hint {
color: #909399;
font-size: 12px;
}
.editor-wrap {
height: 68vh;
overflow: auto;
}
.tcal-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.tcal-table th,
.tcal-table td {
border: 1px solid #ebeef5;
text-align: center;
font-size: 12px;
color: #303133;
height: 34px;
}
.tcal-table thead th {
background: #f5f7fa;
color: #606266;
font-weight: 500;
position: sticky;
top: 0;
z-index: 1;
}
.jc-col {
width: 96px;
background: #fafafa;
color: #606266;
}
.slot-hint {
display: block;
font-size: 11px;
color: #909399;
}
.day-date {
font-size: 11px;
color: #909399;
}
.weekend {
background: #fdf6ec;
}
.tcal-cell {
cursor: pointer;
}
.tcal-cell:hover {
background: #ecf5ff;
}
.tcal-cell.marked {
background: #fef0f0;
color: #f56c6c;
font-weight: 500;
}
.dialog-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.pending-tip {
margin-right: auto;
font-size: 12px;
color: #909399;
}
</style>
@@ -78,10 +78,11 @@
@click="handleCreateAccount(row)">创建账号</el-button>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="200">
<el-table-column label="操作" align="center" fixed="right" width="260">
<template slot-scope="{ row }">
<el-button type="text" size="small" icon="el-icon-view" @click="handleViewAttribute(row)">查看属性</el-button>
<el-button type="text" size="small" icon="el-icon-edit" @click="handleEdit(row)">编辑</el-button>
<el-button type="text" size="small" icon="el-icon-date" @click="handleTeacherCalendar(row)">教员历</el-button>
<el-button type="text" size="small" class="danger-text" @click="handleDisable(row)">离职</el-button>
</template>
</el-table-column>
@@ -700,6 +701,12 @@
<!-- 对齐结果清单弹窗 -->
<org-sync-result-dialog ref="alignResult" title="对齐结果" />
<!-- 教员历编辑弹窗(标记不可排课时段,供排课窗口与冲突检查使用) -->
<teacher-calendar-dialog
:visible.sync="teacherCalendarVisible"
:teacher="teacherCalendarRow"
/>
</div>
</template>
@@ -723,10 +730,11 @@ import { getDicts } from '@/api/system/dict/data'
import { listRole } from '@/api/system/role'
import { saveAs } from 'file-saver'
import OrgSyncResultDialog from '@/components/OrgSyncResultDialog'
import TeacherCalendarDialog from './components/TeacherCalendarDialog.vue'
export default {
name: 'Teacher',
components: { OrgSyncResultDialog },
components: { OrgSyncResultDialog, TeacherCalendarDialog },
data() {
return {
// 查询条件(仅后端 JYBMapper.xml 支持的字段:jyxm 模糊 / jysdh 等值 / zc 模糊 / jylb 等值)
@@ -790,6 +798,10 @@ export default {
attributeData: {},
attributeForm: {},
attributeLoading: false,
// 教员历(标记不可排课时段)
teacherCalendarVisible: false,
teacherCalendarRow: null,
attributeSaving: false,
attributeJybh: '',
// 字典选项
@@ -1086,6 +1098,17 @@ export default {
return payload
},
// ==================== 教员历 ====================
/** 打开该教员的教员历编辑弹窗(标记不可排课时段) */
handleTeacherCalendar(row) {
if (!row || !row.jybh) {
this.$message.warning('该教员缺少教员工号,无法维护教员历')
return
}
this.teacherCalendarRow = row
this.teacherCalendarVisible = true
},
// ==================== 编辑 ====================
handleEdit(row) {
this.editDialogVisible = true