Merge remote-tracking branch 'origin/main'

This commit is contained in:
liumengyu
2026-08-21 17:47:39 +08:00
857 changed files with 10838 additions and 5 deletions
@@ -0,0 +1,167 @@
package com.roomroot.jwgl.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 课表冲突检查结果实体。
* <p>
* 对应数据库表:课表_冲突检查结果(方案 A 落库表)。
* 每条记录 = 一次检查中的一条冲突明细,与 TimetableConflictDetailVO 字段一一对应,
* 额外加:年度、检查批次号、编号主键、创建时间 4 个落库字段。
* </p>
* <p>
* 命名风格与项目其他实体保持一致:
* 属性=拼音/缩写,列名=中文,显式 @TableField 映射
* </p>
*
* @author management
*/
@Data
@TableName("课表_冲突检查结果")
public class TimetableConflictResult {
// =========================================================
// 落库扩展字段
// =========================================================
/**
* 主键编号(BIGINT 自增,对应表"编号"列,IDENTITY(1,1)
*/
@TableId(value = "编号", type = IdType.AUTO)
private Long bh;
/**
* 年度(学期过滤,例:2026)
*/
@TableField("年度")
private Integer nd;
/**
* 检查批次号(同一次"执行全部检查"共用,例:20260821-153000-123
*/
@TableField("检查批次号")
private String jcpch;
/**
* 创建时间(DEFAULT SYSDATE,MP 不做填充,数据库默认值即可)
*/
@TableField("创建时间")
private LocalDateTime cjsj;
// =========================================================
// 维度信息
// =========================================================
/**
* 维度编码,对应 TimetableConflictDimension.code
* 取值:TEAM_CONFLICT / ELECTIVE_REQUIRED_CONFLICT / TEACHER_CONFLICT / CLASSROOM_CONFLICT
*/
@TableField("维度编码")
private String wdbm;
/**
* 维度标题(中文,例:教员时间冲突)
*/
@TableField("维度标题")
private String wdbt;
// =========================================================
// 冲突资源信息
// =========================================================
/**
* 资源编号(学员队编号/教员编号/教室编号)
*/
@TableField("资源编号")
private String zybh;
/**
* 资源中文名(例:张教授、101多媒体教室)
*/
@TableField("资源名称")
private String zymc;
// =========================================================
// 日期节次
// =========================================================
/**
* 冲突日期
*/
@TableField("日期")
private LocalDateTime rq;
/**
* 冲突节次(1-N
*/
@TableField("节次")
private Integer jc;
// =========================================================
// 冲突课次信息
// =========================================================
/**
* 占用课次数(同一资源同时被排了几门课,一般 ≥2)
*/
@TableField("占用课次数")
private Integer zykcsl;
/**
* 课程名称列表(WM_CONCAT 拼接,例:高等数学;大学英语)
*/
@TableField("课程名称列表")
private String kcmclb;
/**
* 冲突课次编号列表(= 实施_课程表.编号,逗号分隔,便于跳转到课次详情)
*/
@TableField("冲突课次编号列表")
private String ctkcbhlb;
/**
* 冲突原因中文整句(直接用于弹框展示)
*/
@TableField("冲突原因")
private String ctyy;
// =========================================================
// UI 明细表格 5 个业务字段(冲突号/班次/教员/场地/责任单位)
// =========================================================
/**
* 冲突号(规则:YYYYMMDD_节次2位_资源编号,例:20260910_03_XYD00001
*/
@TableField("冲突号")
private String cth;
/**
* 班次名称列表(学员队名称,WM_CONCAT 去重拼接)
*/
@TableField("班次名称列表")
private String bcmclb;
/**
* 教员姓名列表(含【主】【辅】标注)
*/
@TableField("教员姓名列表")
private String jyxmlb;
/**
* 场地/教室名称列表
*/
@TableField("场地名称列表")
private String cdmclb;
/**
* 责任单位列表(班次的所属单位聚合)
*/
@TableField("责任单位列表")
private String zrdwlb;
}
@@ -2,6 +2,7 @@ package com.roomroot.jwgl.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.roomroot.jwgl.entity.SSKCBFZJY;
import com.roomroot.jwgl.vo.timetableconflict.TimetableConflictDetailVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -13,4 +14,12 @@ public interface SSKCBFZJYMapper extends BaseMapper<SSKCBFZJY> {
List<SSKCBFZJY> selectBySskcbh(@Param("sskcbh") String sskcbh);
int deleteBySskcbbh(@Param("sskcbbh") String sskcbbh);
/**
* 【课表冲突检查】教员时间冲突明细查询。
*
* @param nd 年度
* @return 教员时间冲突明细列表
*/
List<TimetableConflictDetailVO> selectTeacherConflictDetails(@Param("nd") Integer nd);
}
@@ -3,11 +3,112 @@
<mapper namespace="com.roomroot.jwgl.mapper.SSKCBFZJYMapper">
<select id="selectBySskcbh" resultType="com.roomroot.jwgl.entity.SSKCBFZJY">
SELECT * FROM 实施_课程表_辅助教员 WHERE 实施_课程表编号 = #{sskcbh}
SELECT * FROM "实施_课程表_辅助教员" WHERE "实施_课程表编号" = #{sskcbh}
</select>
<delete id="deleteBySskcbbh">
DELETE FROM 实施_课程表_辅助教员 WHERE 实施_课程表编号 = #{sskcbbh}
DELETE FROM "实施_课程表_辅助教员" WHERE "实施_课程表编号" = #{sskcbbh}
</delete>
<!-- ============================================================ -->
<!-- 课表冲突检查 - 维度3:教员时间冲突明细查询 -->
<!-- 检查逻辑:同一教员(主讲+辅讲统一查)在同一日期节次,被安排多个课次 -->
<!-- 用到的表: -->
<!-- 1. 实施_课程表 SSKCB → 日期/节次/年度/删除状态 -->
<!-- 2. 实施_课程表_辅助教员 SSKCBFZJY → 教员编号(FZJYBH) -->
<!-- 3. 教员表/教研室人员表 JYB/JYSX → 翻译教员姓名 -->
<!-- 4. 课表 KB → 翻译课程名称 -->
<!-- ============================================================ -->
<select id="selectTeacherConflictDetails"
resultType="com.roomroot.jwgl.vo.timetableconflict.TimetableConflictDetailVO">
SELECT
/* 固定维度编码:教员时间冲突 */
'TEACHER_CONFLICT' AS dimension_code,
'教员时间冲突' AS dimension_title,
/* 发生冲突的资源(教员)*/
g."辅助教员编号" AS resource_id,
COALESCE(j."教员姓名", g."辅助教员编号") AS resource_name,
/* 冲突日期节次:统一取自主表 s */
g."日期" AS rq,
g."节次" AS jc,
/* 占用课次数:同一教员同一时间排了几门不同的课 */
g."占用课次数" AS occupied_lesson_count,
/* 冲突课程名 + 主讲/辅讲标注 */
g."课程名称列表" AS course_names,
/* 冲突课次编号列表,逗号分隔 */
g."课次编号列表" AS conflict_sskcb_bhs,
/* 中文原因 */
'教员【' || COALESCE(j."教员姓名", g."辅助教员编号") || '】在 '
|| TO_CHAR(g."日期", 'YYYY-MM-DD') || ' 第' || CAST(g."节次" AS VARCHAR) || '节'
|| ' 被同时安排 ' || CAST(g."占用课次数" AS VARCHAR) || ' 门课程:'
|| COALESCE(g."课程名称列表", '(课次号:' || g."课次编号列表" || '')
AS reason,
/* ========== UI 明细新增 5 字段 ========== */
/* 冲突号:日期8位_节次2位_辅助教员编号 */
TO_CHAR(g."日期", 'YYYYMMDD')
|| '_' || LPAD(CAST(g."节次" AS VARCHAR), 2, '0')
|| '_' || g."辅助教员编号" AS conflict_no,
/* 班次名称列表(这些课次涉及的所有学员队/班次)*/
COALESCE(g.xyd_names_list, '(无)') AS xyd_names,
/* 教员姓名列表(含【主/辅】,该冲突课次涉及的所有教员)*/
COALESCE(g.teacher_names_list, '(无)') AS teacher_names,
/* 场地/教室名称列表 */
COALESCE(g.classroom_names_list, '(无)') AS classroom_names,
/* 责任单位(从班次所属单位聚合)*/
COALESCE(g.responsible_dept_list, '(无)')AS responsible_dept
FROM (
/*
* 内层:按教员编号+日期+节次 GROUP BY,筛选出现>1课次的冲突
*
* 关键修正:
* 1. 关联链路:SSKCB → 实施_课程 c → 课表 k;必须 CAST(c.编号 AS CHAR(36))
* 2. 主讲/辅讲字段:真实列名「主教员」(1主/0辅)
* 3. 日期节次:统一取主表 s
*
* 新增 JOIN(同维度1/2):为明细表格聚合"该冲突课次涉及的所有班次/教员/场地"
* - INNER JOIN f 是"维度过滤主关联"(只保留该教员的课次做分组)
* - 额外 LEFT JOIN xyd/fz/js 三层是"聚合展示用"(把同一冲突组涉及的所有信息都拼出来)
*/
SELECT
f."辅助教员编号",
s."日期",
s."节次",
COUNT(DISTINCT s."编号") AS "占用课次数",
/* 原课程名称列表(课程名 + 主/辅标注)*/
WM_CONCAT(DISTINCT
CASE f."主教员" WHEN 1 THEN '【主】' ELSE '【辅】' END
|| COALESCE(k."课名称", '未命名课程')
) AS "课程名称列表",
WM_CONCAT(DISTINCT s."编号") AS "课次编号列表",
/* ========== 新增 4 个聚合字段(明细表格用)========== */
WM_CONCAT(DISTINCT xyd_info."学员队名称") AS xyd_names_list,
WM_CONCAT(DISTINCT
CASE fz_info."主教员" WHEN 1 THEN '【主】' ELSE '【辅】' END
|| jy_info."教员姓名"
) AS teacher_names_list,
WM_CONCAT(DISTINCT js_info."教室名称") AS classroom_names_list,
WM_CONCAT(DISTINCT xyd_info."所属单位") AS responsible_dept_list
FROM "实施_课程表" s
/* 维度过滤主关联:只看该教员的课次(GROUP BY 维度)*/
INNER JOIN "实施_课程表_辅助教员" f ON f."实施_课程表编号" = s."编号"
/* 课表信息三层关联 */
LEFT JOIN "实施_课程" c ON CAST(c."编号" AS CHAR(36)) = s."实施_课程编号"
LEFT JOIN "课表" k ON k."科目代码" = c."科目编号"
/* ========== 新增 JOIN:聚合该冲突课次的班次/教员/场地所有信息 ========== */
LEFT JOIN "实施_课程表_学员队" xyd_link ON xyd_link."实施_课程表编号" = s."编号"
LEFT JOIN "学员队表" xyd_info ON xyd_info."学员队编号" = xyd_link."学员队编号"
LEFT JOIN "实施_课程表_辅助教员" fz_info ON fz_info."实施_课程表编号" = s."编号"
LEFT JOIN "教员表" jy_info ON jy_info."教员编号" = fz_info."辅助教员编号"
LEFT JOIN "实施_课程表_教室" js_link ON js_link."实施_课程表编号" = s."编号"
LEFT JOIN "教室表" js_info ON js_info."教室编号" = js_link."教室编号"
WHERE s."删除状态" = 0
AND s."年度" = #{nd}
GROUP BY f."辅助教员编号", s."日期", s."节次"
HAVING COUNT(DISTINCT s."编号") > 1
) g
/* 翻译教员姓名:教员表主键「教员编号」,姓名字段「教员姓名」 */
LEFT JOIN "教员表" j ON j."教员编号" = g."辅助教员编号"
ORDER BY g."日期", g."节次", g."辅助教员编号"
</select>
</mapper>
@@ -2,6 +2,7 @@ package com.roomroot.jwgl.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.roomroot.jwgl.entity.SSKCBJS;
import com.roomroot.jwgl.vo.timetableconflict.TimetableConflictDetailVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -13,4 +14,12 @@ public interface SSKCBJSMapper extends BaseMapper<SSKCBJS> {
List<SSKCBJS> selectBySskcbh(@Param("sskcbh") String sskcbh);
int deleteBySskcbbh(@Param("sskcbbh") String sskcbbh);
/**
* 【课表冲突检查】教室时间冲突明细查询。
*
* @param nd 年度
* @return 教室时间冲突明细列表
*/
List<TimetableConflictDetailVO> selectClassroomConflictDetails(@Param("nd") Integer nd);
}
@@ -3,11 +3,108 @@
<mapper namespace="com.roomroot.jwgl.mapper.SSKCBJSMapper">
<select id="selectBySskcbh" resultType="com.roomroot.jwgl.entity.SSKCBJS">
SELECT * FROM 实施_课程表_教室 WHERE 实施_课程表编号 = #{sskcbh}
SELECT * FROM "实施_课程表_教室" WHERE "实施_课程表编号" = #{sskcbh}
</select>
<delete id="deleteBySskcbbh">
DELETE FROM 实施_课程表_教室 WHERE 实施_课程表编号 = #{sskcbbh}
DELETE FROM "实施_课程表_教室" WHERE "实施_课程表编号" = #{sskcbbh}
</delete>
<!-- ============================================================ -->
<!-- 课表冲突检查 - 维度4:教室(教学场地)时间冲突明细查询 -->
<!-- 检查逻辑:同一教室同一日期节次,被安排多个不同课次 -->
<!-- 用到的表: -->
<!-- 1. 实施_课程表 SSKCB → 日期/节次/年度/删除状态 -->
<!-- 2. 实施_课程表_教室 SSKCBJS → 教室编号(JSBH) -->
<!-- 3. 教室表 JSB → 翻译教室中文名 -->
<!-- 4. 课表 KB → 翻译课程名称 -->
<!-- ============================================================ -->
<select id="selectClassroomConflictDetails"
resultType="com.roomroot.jwgl.vo.timetableconflict.TimetableConflictDetailVO">
SELECT
/* 固定维度编码:教室时间冲突 */
'CLASSROOM_CONFLICT' AS dimension_code,
'教室时间冲突' AS dimension_title,
/* 发生冲突的资源(教室)*/
g."教室编号" AS resource_id,
/* 翻译教室名称(教室表主键「教室编号」,名称「教室名称」)*/
COALESCE(r."教室名称", g."教室编号") AS resource_name,
/* 冲突的日期节次:统一取自主表 s */
g."日期" AS rq,
g."节次" AS jc,
/* 同一教室同一时间占用课次数 */
g."占用课次数" AS occupied_lesson_count,
/* 冲突的课程名称列表 */
g."课程名称列表" AS course_names,
/* 冲突的课次编号列表 */
g."课次编号列表" AS conflict_sskcb_bhs,
/* 中文原因 */
'教室【' || COALESCE(r."教室名称", g."教室编号") || '】在 '
|| TO_CHAR(g."日期", 'YYYY-MM-DD') || ' 第' || CAST(g."节次" AS VARCHAR) || '节'
|| ' 被同时安排 ' || CAST(g."占用课次数" AS VARCHAR) || ' 门课程:'
|| COALESCE(g."课程名称列表", '(课次号:' || g."课次编号列表" || '')
AS reason,
/* ========== UI 明细新增 5 字段 ========== */
/* 冲突号:日期8位_节次2位_教室编号 */
TO_CHAR(g."日期", 'YYYYMMDD')
|| '_' || LPAD(CAST(g."节次" AS VARCHAR), 2, '0')
|| '_' || g."教室编号" AS conflict_no,
/* 班次名称列表(这些课次涉及的所有学员队/班次)*/
COALESCE(g.xyd_names_list, '(无)') AS xyd_names,
/* 教员姓名列表(含【主/辅】标注)*/
COALESCE(g.teacher_names_list, '(无)') AS teacher_names,
/* 场地/教室名称(虽然维度是教室,这里仍统一返回所有场地名,便于合班情况)*/
COALESCE(g.classroom_names_list, '(无)') AS classroom_names,
/* 责任单位(班次所属单位聚合)*/
COALESCE(g.responsible_dept_list, '(无)')AS responsible_dept
FROM (
/*
* 内层:按教室编号+日期+节次 GROUP BY,筛选 COUNT(DISTINCT 课次) > 1
*
* 关键修正:
* 1. 三层关联 JOIN:SSKCB → 实施_课程 → 课表;CAST(ss.编号 AS CHAR(36))
* 2. 日期节次统一取主表 s
*
* 新增 JOIN(同其他维度):聚合该冲突课次的班次/教员/场地所有明细信息
* - INNER JOIN c 作为"维度过滤主关联"(只保留该教室的课次做分组)
* - xyd_link/fz_info/js_link 作为"聚合展示用"JOINWM_CONCAT DISTINCT
*/
SELECT
c."教室编号",
s."日期",
s."节次",
COUNT(DISTINCT s."编号") AS "占用课次数",
WM_CONCAT(DISTINCT k."课名称") AS "课程名称列表",
WM_CONCAT(DISTINCT s."编号") AS "课次编号列表",
/* ========== 新增 4 个聚合字段(明细表格用)========== */
WM_CONCAT(DISTINCT xyd_info."学员队名称") AS xyd_names_list,
WM_CONCAT(DISTINCT
CASE fz_info."主教员" WHEN 1 THEN '【主】' ELSE '【辅】' END
|| jy_info."教员姓名"
) AS teacher_names_list,
WM_CONCAT(DISTINCT js_info."教室名称") AS classroom_names_list,
WM_CONCAT(DISTINCT xyd_info."所属单位") AS responsible_dept_list
FROM "实施_课程表" s
/* 维度过滤主关联:教室维度分组 */
INNER JOIN "实施_课程表_教室" c ON c."实施_课程表编号" = s."编号"
/* 课表信息三层关联(ss=实施_课程别名,避免与聚合JOIN的别名冲突)*/
LEFT JOIN "实施_课程" ss ON CAST(ss."编号" AS CHAR(36)) = s."实施_课程编号"
LEFT JOIN "课表" k ON k."科目代码" = ss."科目编号"
/* ========== 新增 JOIN:聚合该冲突课次的班次/教员/场地所有信息 ========== */
LEFT JOIN "实施_课程表_学员队" xyd_link ON xyd_link."实施_课程表编号" = s."编号"
LEFT JOIN "学员队表" xyd_info ON xyd_info."学员队编号" = xyd_link."学员队编号"
LEFT JOIN "实施_课程表_辅助教员" fz_info ON fz_info."实施_课程表编号" = s."编号"
LEFT JOIN "教员表" jy_info ON jy_info."教员编号" = fz_info."辅助教员编号"
LEFT JOIN "实施_课程表_教室" js_link ON js_link."实施_课程表编号" = s."编号"
LEFT JOIN "教室表" js_info ON js_info."教室编号" = js_link."教室编号"
WHERE s."删除状态" = 0
AND s."年度" = #{nd}
GROUP BY c."教室编号", s."日期", s."节次"
HAVING COUNT(DISTINCT s."编号") > 1
) g
/* 修正:教室表主键是「教室编号」,不是「编号」 */
LEFT JOIN "教室表" r ON r."教室编号" = g."教室编号"
ORDER BY g."日期", g."节次", g."教室编号"
</select>
</mapper>
@@ -3,6 +3,7 @@ package com.roomroot.jwgl.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.roomroot.jwgl.entity.SSKCBXYD;
import com.roomroot.jwgl.vo.elective.ClassScheduleVO;
import com.roomroot.jwgl.vo.timetableconflict.TimetableConflictDetailVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -17,4 +18,20 @@ public interface SSKCBXYDMapper extends BaseMapper<SSKCBXYD> {
/** 查询班次关联的课程表(联表) */
List<ClassScheduleVO> selectScheduleByXydbh(@Param("xydbh") String xydbh, @Param("nd") Integer nd);
/**
* 【课表冲突检查】教学班时间冲突明细查询。
*
* @param nd 年度(学期过滤)
* @return 教学班时间冲突明细列表
*/
List<TimetableConflictDetailVO> selectTeamConflictDetails(@Param("nd") Integer nd);
/**
* 【课表冲突检查】教学班必修与选修时间冲突明细查询。
*
* @param nd 年度
* @return 必修选修冲突明细列表
*/
List<TimetableConflictDetailVO> selectElectiveRequiredConflictDetails(@Param("nd") Integer nd);
}
@@ -30,7 +30,13 @@
k."课名称"
FROM "实施_课程表" s
JOIN "实施_课程表_学员队" t ON t."实施_课程表编号" = s."编号"
LEFT JOIN "课表" k ON k."课编号" = s."课程科目编号"
<!--
修正:三层关联 SSKCB → 实施_课程 → 课表
- 类型转换:c."编号" 是 VARBINARYs."实施_课程编号" 是 CHAR,必须 CAST 才能匹配上
- 科目关联:KB.科目代码 = SSKC.科目编号
-->
LEFT JOIN "实施_课程" c ON CAST(c."编号" AS CHAR(36)) = s."实施_课程编号"
LEFT JOIN "课表" k ON k."科目代码" = c."科目编号"
WHERE t."学员队编号" = #{xydbh}
<if test="nd != null">
AND s."年度" = #{nd}
@@ -38,4 +44,187 @@
ORDER BY s."日期", s."节次"
</select>
<!-- ============================================================ -->
<!-- 课表冲突检查 - 维度1:教学班时间冲突明细查询 -->
<!-- 检查逻辑:同一班次 + 同一日期 + 同一节次,出现 >1 个不同课次 -->
<!-- 用到的表: -->
<!-- 1. 实施_课程表 SSKCB → 日期(RQ)/节次(JC)/年度/删除状态(SCZT=0正常) -->
<!-- 2. 实施_课程表_学员队 SSKCBXYD → 学员队编号(班次编号) -->
<!-- 3. 学员队表(或班次表) XYDB → 翻译班次中文名 -->
<!-- 4. 课表 KB → 翻译课程名称 -->
<!-- ============================================================ -->
<select id="selectTeamConflictDetails"
resultType="com.roomroot.jwgl.vo.timetableconflict.TimetableConflictDetailVO">
SELECT
/* 固定维度编码:教学班时间冲突 */
'TEAM_CONFLICT' AS dimension_code,
'教学班时间冲突' AS dimension_title,
/* 发生冲突的资源(班次)*/
g."学员队编号" AS resource_id,
COALESCE(x."学员队名称", g."学员队编号") AS resource_name,
/* 冲突日期节次(统一从主表 SSKCB(s) 取)*/
g."日期" AS rq,
g."节次" AS jc,
/* 占用课次数(同一班次同一时间有多少门课) */
g."占用课次数" AS occupied_lesson_count,
/* 冲突课程名称拼接 */
g."课程名称列表" AS course_names,
/* 冲突课次编号拼接(前端跳转用)*/
g."课次编号列表" AS conflict_sskcb_bhs,
/* 冲突原因说明 */
'教学班次【' || COALESCE(x."学员队名称", g."学员队编号") || '】在 '
|| TO_CHAR(g."日期", 'YYYY-MM-DD') || ' 第' || CAST(g."节次" AS VARCHAR) || '节'
|| ' 被同时安排 ' || CAST(g."占用课次数" AS VARCHAR) || ' 门课程:'
|| COALESCE(g."课程名称列表", '(课次号:' || g."课次编号列表" || '')
AS reason,
/* =========================================================
* 以下为 UI 明细表格新增的 5 个字段(对应截图8列)
* 列别名统一用"下划线命名" → MyBatis resultType 自动转驼峰匹配 VO 属性
* ========================================================= */
/* 冲突号:日期8位_节次2位补0_资源编号(学员队编号)*/
TO_CHAR(g."日期", 'YYYYMMDD')
|| '_' || LPAD(CAST(g."节次" AS VARCHAR), 2, '0')
|| '_' || g."学员队编号" AS conflict_no,
/* 班次名称列表(该冲突涉及的所有学员队/班次) */
COALESCE(g.xyd_names_list, '(无)') AS xyd_names,
/* 教员姓名列表(含【主/辅】标注)*/
COALESCE(g.teacher_names_list, '(无)') AS teacher_names,
/* 场地/教室名称列表 */
COALESCE(g.classroom_names_list, '(无)') AS classroom_names,
/* 责任单位(班次的所属单位)*/
COALESCE(g.responsible_dept_list, '(无)')AS responsible_dept
FROM (
/*
* 内层子查询:按班次+日期+节次 GROUP BY,筛选 COUNT(DISTINCT 课次) > 1 的冲突组
*
* 新增:除原有课程信息 JOIN 外,再 LEFT JOIN 三类业务信息用于明细展示(均加 DISTINCT,避免 JOIN 膨胀影响聚合)
* ① 班次+责任单位:SSKCB 再次 JOIN SSKCBXYD + 学员队表(拿到所有涉及班次的名称和所属单位)
* ② 教员:SSKCB → SSKCBFZJY + 教员表(带【主】【辅】标注)
* ③ 场地/教室:SSKCB → SSKCBJS + 教室表
*/
SELECT
t."学员队编号",
s."日期",
s."节次",
COUNT(DISTINCT s."编号") AS "占用课次数",
/* 原有用字段:课程名、课次号 */
WM_CONCAT(DISTINCT k."课名称") AS "课程名称列表",
WM_CONCAT(DISTINCT s."编号") AS "课次编号列表",
/* ========== 新增 4 个聚合字段(对应 UI 明细 4 列)========== */
/* 1. 班次名称列表:一个课次可能多个班上,DISTINCT 去重再拼接 */
WM_CONCAT(DISTINCT xyd_info."学员队名称") AS xyd_names_list,
/* 2. 教员姓名列表:带【主】【辅】角色区分 */
WM_CONCAT(DISTINCT
CASE fz_info."主教员" WHEN 1 THEN '【主】' ELSE '【辅】' END
|| jy_info."教员姓名"
) AS teacher_names_list,
/* 3. 场地/教室名称列表 */
WM_CONCAT(DISTINCT js_info."教室名称") AS classroom_names_list,
/* 4. 责任单位:学员队表所属单位字段聚合(DISTINCT 去重)*/
WM_CONCAT(DISTINCT xyd_info."所属单位") AS responsible_dept_list
FROM "实施_课程表" s
/* 维度过滤用:主关联"教学班(学员队)" —— 教学班冲突就是按这个维度 GROUP BY */
INNER JOIN "实施_课程表_学员队" t ON t."实施_课程表编号" = s."编号"
/* 课表信息三层关联 */
LEFT JOIN "实施_课程" c ON CAST(c."编号" AS CHAR(36)) = s."实施_课程编号"
LEFT JOIN "课表" k ON k."科目代码" = c."科目编号"
/* ========== 新增 JOIN:聚合该冲突课次涉及的所有班次/教员/教室信息 ========== */
LEFT JOIN "实施_课程表_学员队" xyd_link ON xyd_link."实施_课程表编号" = s."编号"
LEFT JOIN "学员队表" xyd_info ON xyd_info."学员队编号" = xyd_link."学员队编号"
LEFT JOIN "实施_课程表_辅助教员" fz_info ON fz_info."实施_课程表编号" = s."编号"
LEFT JOIN "教员表" jy_info ON jy_info."教员编号" = fz_info."辅助教员编号"
LEFT JOIN "实施_课程表_教室" js_link ON js_link."实施_课程表编号" = s."编号"
LEFT JOIN "教室表" js_info ON js_info."教室编号" = js_link."教室编号"
WHERE s."删除状态" = 0
AND s."年度" = #{nd}
GROUP BY t."学员队编号", s."日期", s."节次"
HAVING COUNT(DISTINCT s."编号") > 1
) g
/* 翻译班次中文名(学员队表) */
LEFT JOIN "学员队表" x ON x."学员队编号" = g."学员队编号"
ORDER BY g."日期", g."节次", g."学员队编号"
</select>
<!-- ============================================================ -->
<!-- 课表冲突检查 - 维度2:教学班必修与选修时间冲突 -->
<!-- 检查逻辑:同一班次同一日期节次,同时出现"必修"和"选修"两类课程 -->
<!-- 用到的表: -->
<!-- 1. 实施_课程表 SSKCB → 日期/节次/年度/删除状态 -->
<!-- 2. 实施_课程表_学员队 SSKCBXYD → 班次编号 -->
<!-- 3. 课表 KB → 课程类型(必修/选修/考查) -->
<!-- 4. 学员队表 XYDB → 翻译班次中文名 -->
<!-- ============================================================ -->
<select id="selectElectiveRequiredConflictDetails"
resultType="com.roomroot.jwgl.vo.timetableconflict.TimetableConflictDetailVO">
SELECT
'ELECTIVE_REQUIRED_CONFLICT' AS dimension_code,
'教学班必修与选修时间冲突' AS dimension_title,
g."学员队编号" AS resource_id,
COALESCE(x."学员队名称", g."学员队编号") AS resource_name,
/* 日期节次统一取自主表 s */
g."日期" AS rq,
g."节次" AS jc,
(g."必修课次数" + g."选修课次数") AS occupied_lesson_count,
(g."必修课程名" || '' || g."选修课程名") AS course_names,
(g."必修课次编号" || ',' || g."选修课次编号") AS conflict_sskcb_bhs,
'教学班次【' || COALESCE(x."学员队名称", g."学员队编号") || '】在 '
|| TO_CHAR(g."日期", 'YYYY-MM-DD') || ' 第' || CAST(g."节次" AS VARCHAR) || '节'
|| ' 同时安排了必修课:' || COALESCE(g."必修课程名", '(无)')
|| ' 与 选修课:' || COALESCE(g."选修课程名", '(无)')
AS reason,
/* ========== UI 明细新增 5 字段 ========== */
/* 冲突号:日期8位_节次2位_学员队编号 */
TO_CHAR(g."日期", 'YYYYMMDD')
|| '_' || LPAD(CAST(g."节次" AS VARCHAR), 2, '0')
|| '_' || g."学员队编号" AS conflict_no,
COALESCE(g.xyd_names_list, '(无)') AS xyd_names,
COALESCE(g.teacher_names_list, '(无)') AS teacher_names,
COALESCE(g.classroom_names_list, '(无)') AS classroom_names,
COALESCE(g.responsible_dept_list, '(无)')AS responsible_dept
FROM (
/*
* 按班次+日期+节次分组,分别统计"必修课"和"选修课"
* 新增 JOIN:同维度1LEFT JOIN 班次/教员/教室三类信息(WM_CONCAT DISTINCT 聚合)
*/
SELECT
t."学员队编号",
s."日期",
s."节次",
COUNT(DISTINCT CASE WHEN k."课程类型" = '必修' THEN s."编号" END) AS "必修课次数",
COUNT(DISTINCT CASE WHEN k."课程类型" = '选修' THEN s."编号" END) AS "选修课次数",
WM_CONCAT(DISTINCT CASE WHEN k."课程类型" = '必修' THEN k."课名称" END) AS "必修课程名",
WM_CONCAT(DISTINCT CASE WHEN k."课程类型" = '选修' THEN k."课名称" END) AS "选修课程名",
WM_CONCAT(DISTINCT CASE WHEN k."课程类型" = '必修' THEN s."编号" END) AS "必修课次编号",
WM_CONCAT(DISTINCT CASE WHEN k."课程类型" = '选修' THEN s."编号" END) AS "选修课次编号",
/* ========== 新增 4 个聚合字段 ========== */
WM_CONCAT(DISTINCT xyd_info."学员队名称") AS xyd_names_list,
WM_CONCAT(DISTINCT
CASE fz_info."主教员" WHEN 1 THEN '【主】' ELSE '【辅】' END
|| jy_info."教员姓名"
) AS teacher_names_list,
WM_CONCAT(DISTINCT js_info."教室名称") AS classroom_names_list,
WM_CONCAT(DISTINCT xyd_info."所属单位") AS responsible_dept_list
FROM "实施_课程表" s
INNER JOIN "实施_课程表_学员队" t ON t."实施_课程表编号" = s."编号"
LEFT JOIN "实施_课程" c ON CAST(c."编号" AS CHAR(36)) = s."实施_课程编号"
LEFT JOIN "课表" k ON k."科目代码" = c."科目编号"
/* ========== 新增 JOIN:三类业务信息 ========== */
LEFT JOIN "实施_课程表_学员队" xyd_link ON xyd_link."实施_课程表编号" = s."编号"
LEFT JOIN "学员队表" xyd_info ON xyd_info."学员队编号" = xyd_link."学员队编号"
LEFT JOIN "实施_课程表_辅助教员" fz_info ON fz_info."实施_课程表编号" = s."编号"
LEFT JOIN "教员表" jy_info ON jy_info."教员编号" = fz_info."辅助教员编号"
LEFT JOIN "实施_课程表_教室" js_link ON js_link."实施_课程表编号" = s."编号"
LEFT JOIN "教室表" js_info ON js_info."教室编号" = js_link."教室编号"
WHERE s."删除状态" = 0
AND s."年度" = #{nd}
GROUP BY t."学员队编号", s."日期", s."节次"
/* 关键:只有必修 >0 并且 选修 >0 才算冲突 */
HAVING COUNT(DISTINCT CASE WHEN k."课程类型" = '必修' THEN s."编号" END) > 0
AND COUNT(DISTINCT CASE WHEN k."课程类型" = '选修' THEN s."编号" END) > 0
) g
/* 修正:学员队表主键是「学员队编号」,不是「编号」 */
LEFT JOIN "学员队表" x ON x."学员队编号" = g."学员队编号"
ORDER BY g."日期", g."节次"
</select>
</mapper>
@@ -0,0 +1,82 @@
package com.roomroot.jwgl.mapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.roomroot.jwgl.entity.TimetableConflictResult;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 课表冲突检查结果 Mapper。
* <p>
* 对应表:课表_冲突检查结果。
* 继承 MyBatis-Plus BaseMapper 自动获得 insert / selectList / delete 等 CRUD 方法;
* 额外保留 2 个便捷方法:按年度+批次号清理、按年度+维度编码查最新批次。
* </p>
*
* @author management
*/
@Mapper
public interface TimetableConflictResultMapper extends BaseMapper<TimetableConflictResult> {
/**
* 按年度 + 检查批次号清理历史记录(DELETE,非逻辑删除)。
* <p>
* 等价于 MP 的 delete(new LambdaQueryWrapper<TimetableConflictResult>()
* .eq(TimetableConflictResult::getNd, nd)
* .eq(TimetableConflictResult::getJcpch, jcpch));
* 这里提供 XML 版本只是为了在"批处理前后先清旧批次"场景更直观,
* 不实现也行,Service 层用 MP wrapper 即可。
* </p>
*
* @param nd 年度
* @param jcpch 检查批次号
* @return 删除条数
*/
int deleteByNdAndJcpch(@Param("nd") Integer nd, @Param("jcpch") String jcpch);
/**
* 查询指定年度的所有检查批次号(倒序,最新排第一)。
* <p>
* 给前端"查看某次检查历史"的下拉框用。
* </p>
*
* @param nd 年度
* @return 批次号列表
*/
List<String> selectJcpchListByNd(@Param("nd") Integer nd);
/**
* 【默认走 MP BaseMapper】等价说明:
* <ul>
* <li>insert(entity) → 单条插入一条冲突结果</li>
* <li>insertBatch(entityList) → 需要 Service 层自己循环或用 MP IService 的 saveBatch
* 此处 BaseMapper 没有批量方法。</li>
* <li>selectList(wrapper) → 按年度/维度/批次号查询明细列表</li>
* <li>delete(wrapper) → 按条件 deletereset 场景清该年度所有)</li>
* </ul>
*
* 另:以下 2 个方法只是接口声明,提供语义化 API;
* 若不准备写 TimetableConflictResultMapper.xml 实现,
* 调用方请直接使用 MP BaseMapper 默认方法即可(推荐,更省事)。
*/
default int deleteByNd(Integer nd) {
return delete(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TimetableConflictResult>()
.eq(TimetableConflictResult::getNd, nd));
}
default List<TimetableConflictResult> listByNdAndJcpch(Integer nd, String jcpch) {
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TimetableConflictResult> qw
= new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
qw.eq(TimetableConflictResult::getNd, nd);
if (jcpch != null && !jcpch.isEmpty()) {
qw.eq(TimetableConflictResult::getJcpch, jcpch);
}
qw.orderByAsc(TimetableConflictResult::getRq)
.orderByAsc(TimetableConflictResult::getJc)
.orderByAsc(TimetableConflictResult::getWdbm);
return selectList(qw);
}
}
@@ -0,0 +1,144 @@
package com.roomroot.jwgl.service;
import com.roomroot.jwgl.entity.TimetableConflictResult;
import com.roomroot.jwgl.unit.PageResult;
import com.roomroot.jwgl.unit.conflict.TimetableConflictDimension;
import com.roomroot.jwgl.vo.timetableconflict.TimetableConflictCardVO;
import com.roomroot.jwgl.vo.timetableconflict.TimetableConflictDetailVO;
import com.roomroot.jwgl.vo.timetableconflict.TimetableConflictSummaryVO;
import java.util.List;
/**
* 课表冲突检查服务接口。
* <p>
* 现在支持两套"存储/读取"机制,调用方可按需选择:
* <ul>
* <li><b>内存模式(默认,兼容旧逻辑)</b>:检查结果只存在 ConcurrentHashMapJVM 重启丢失。
* 方法签名不带 jcpch/useDb。</li>
* <li><b>落库模式(方案 A</b>:检查结果同时写"课表_冲突检查结果"表,
* 分页可按 useDb=true / jcpch 指定某一批次从 DB 读。</li>
* </ul>
* </p>
*
* @author management
*/
public interface TimetableConflictService {
/**
* 加载课表冲突检查页面的初始化汇总(卡片列表)。
* <p>
* 用于 checkAll / reset 内部生成卡片结构;同时也可以返回"已落库历史检查结果回填"的状态,
* 若该年度该维度在 DB 有最新批次也会自动填充 checked=true。
* </p>
*/
TimetableConflictSummaryVO loadSummary(Integer nd);
// =========================================================
// 检查相关(兼容旧签名 + 新签名)
// =========================================================
/**
* 【旧签名,内存模式】单维度检查。
* 等价于 {@code checkDimension(nd, dimension, null, false)}。
*/
default TimetableConflictCardVO checkDimension(Integer nd, TimetableConflictDimension dimension) {
return checkDimension(nd, dimension, null, false);
}
/**
* 【完整签名】单维度检查。
*
* @param nd 年度
* @param dimension 维度枚举
* @param jcpch 检查批次号(=null 则内部自动生成;checkAll 调用时所有维度传同一个批次号)
* @param writeToDb true=同时写入落库表"课表_冲突检查结果"false=只写内存缓存
* @return 卡片结果 VO(内部会保证 conflictCount / checked 的正确性)
*/
TimetableConflictCardVO checkDimension(Integer nd, TimetableConflictDimension dimension,
String jcpch, boolean writeToDb);
/**
* 【旧签名,内存模式】全部维度检查。等价 checkAll(nd, false)。
*/
default TimetableConflictSummaryVO checkAll(Integer nd) {
return checkAll(nd, false);
}
/**
* 【完整签名】全部维度检查。
*
* @param nd 年度
* @param writeToDb true=写落库表(自动生成统一批次号,所有维度共用;返回 summary 会带上最后生成的批次号),false=只写内存
* @return 汇总 VO
*/
TimetableConflictSummaryVO checkAll(Integer nd, boolean writeToDb);
/**
* 返回 checkAll(writeToDb=true) 生成的批次号(如果 writeToDb=false,返回 null)。
* 提供给 Controller 层把"本次检查批次号"放到 Result.message / data 扩展字段给前端用。
*/
String getLastBatchNo();
// =========================================================
// 重置
// =========================================================
/**
* 【旧签名】重置:清内存 + 不清 DB。
*/
default TimetableConflictSummaryVO reset(Integer nd) {
return reset(nd, false);
}
/**
* 重置检查结果。
*
* @param nd 年度
* @param clearDb true=同时 DELETE 落库表里该年度的所有批次;false=只清内存缓存(保持历史)
*/
TimetableConflictSummaryVO reset(Integer nd, boolean clearDb);
// =========================================================
// 分页查询明细
// =========================================================
/**
* 【旧签名,内存模式】分页查询明细。
* 等价于 {@code getDetailsPage(nd, dimensionCode, pageNum, pageSize, false, null)}。
*/
default PageResult<TimetableConflictDetailVO> getDetailsPage(Integer nd, String dimensionCode,
Integer pageNum, Integer pageSize) {
return getDetailsPage(nd, dimensionCode, pageNum, pageSize, false, null);
}
/**
* 【完整签名】分页查询明细。
*
* @param nd 年度
* @param dimensionCode 维度编码,null / "ALL"=合并全部维度
* @param pageNum 页码
* @param pageSize 每页大小
* @param useDb true=从"课表_冲突检查结果"表查;false=从内存缓存查(旧行为)
* @param jcpch useDb=true 时可选:具体查哪一个批次号;null 自动取该年度 DB 里"最新批次号"(MAX 创建时间对应批次号)
*/
PageResult<TimetableConflictDetailVO> getDetailsPage(Integer nd, String dimensionCode,
Integer pageNum, Integer pageSize,
boolean useDb, String jcpch);
// =========================================================
// 便捷方法:批次号列表查询、实体→VO 转换等
// =========================================================
/**
* 查询指定年度的所有检查批次号(倒序,最新批次排第一个)。
* 给前端下拉框"选择某次历史检查查看详情"。
*/
List<String> listBatchNos(Integer nd);
/**
* 把一条"课表_冲突检查结果"实体转成前端明细表格需要的 TimetableConflictDetailVO。
*/
TimetableConflictDetailVO toDetailVO(TimetableConflictResult entity);
}
@@ -0,0 +1,438 @@
package com.roomroot.jwgl.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.roomroot.jwgl.entity.TimetableConflictResult;
import com.roomroot.jwgl.mapper.SSKCBFZJYMapper;
import com.roomroot.jwgl.mapper.SSKCBJSMapper;
import com.roomroot.jwgl.mapper.SSKCBXYDMapper;
import com.roomroot.jwgl.mapper.TimetableConflictResultMapper;
import com.roomroot.jwgl.service.TimetableConflictService;
import com.roomroot.jwgl.unit.PageResult;
import com.roomroot.jwgl.unit.conflict.TimetableConflictDimension;
import com.roomroot.jwgl.vo.timetableconflict.TimetableConflictCardVO;
import com.roomroot.jwgl.vo.timetableconflict.TimetableConflictDetailVO;
import com.roomroot.jwgl.vo.timetableconflict.TimetableConflictSummaryVO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 课表冲突检查服务实现类。
* <p>
* 现在支持:
* <ul>
* <li><b>内存模式(兼容旧前端)</b>ConcurrentHashMap 缓存(默认行为,不改代码也能跑);</li>
* <li><b>落库模式(方案 A</b>:检查结果写入【课表_冲突检查结果】表,分页可用 useDb=true 查询。</li>
* </ul>
* </p>
*
* @author management
*/
@Slf4j
@Service
public class TimetableConflictServiceImpl implements TimetableConflictService {
// =====================================================================
// 依赖注入
// =====================================================================
@Resource
private SSKCBXYDMapper sskcbxydMapper;
@Resource
private SSKCBFZJYMapper sskcbfzjyMapper;
@Resource
private SSKCBJSMapper sskcbjsMapper;
/**
* 方案 A 落库表 Mapper(继承 MP BaseMapper,开箱自带 insert / selectList / delete)。
*/
@Resource
private TimetableConflictResultMapper conflictResultMapper;
// =====================================================================
// 内存缓存(不需要持久化到数据库,临时保存每次检查结果)
// Key 格式:"<年度>_<维度编码>" 例:"2026_TEACHER_CONFLICT"
// =====================================================================
private final ConcurrentHashMap<String, List<TimetableConflictDetailVO>> detailCache
= new ConcurrentHashMap<>();
private String cacheKey(Integer nd, String dimensionCode) {
return nd + "_" + dimensionCode;
}
/** 最近一次 checkAll(writeToDb=true) 生成的批次号 */
private volatile String lastBatchNo;
private static final DateTimeFormatter BATCH_FMT
= DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss-SSS");
// =====================================================================
// 工具:生成批次号 / VO ↔ Entity 转换
// =====================================================================
private String generateBatchNo(Integer nd) {
// 格式:{年度}_{yyyyMMdd-HHmmss-SSS},例:2026_20260821-153000-123
String ts = LocalDateTime.now().format(BATCH_FMT);
return nd + "_" + ts;
}
private TimetableConflictResult toResultEntity(Integer nd, String jcpch,
TimetableConflictDetailVO vo) {
TimetableConflictResult e = new TimetableConflictResult();
e.setNd(nd);
e.setJcpch(jcpch);
e.setWdbm(vo.getDimensionCode());
e.setWdbt(vo.getDimensionTitle());
e.setZybh(vo.getResourceId());
e.setZymc(vo.getResourceName());
e.setRq(vo.getRq());
e.setJc(vo.getJc());
e.setZykcsl(vo.getOccupiedLessonCount());
e.setKcmclb(vo.getCourseNames());
e.setCtkcbhlb(vo.getConflictSskcbBhs());
e.setCtyy(vo.getReason());
e.setCth(vo.getConflictNo());
e.setBcmclb(vo.getXydNames());
e.setJyxmlb(vo.getTeacherNames());
e.setCdmclb(vo.getClassroomNames());
e.setZrdwlb(vo.getResponsibleDept());
return e;
}
@Override
public TimetableConflictDetailVO toDetailVO(TimetableConflictResult e) {
if (e == null) return null;
return TimetableConflictDetailVO.builder()
.dimensionCode(e.getWdbm())
.dimensionTitle(e.getWdbt())
.resourceId(e.getZybh())
.resourceName(e.getZymc())
.rq(e.getRq())
.jc(e.getJc())
.occupiedLessonCount(e.getZykcsl())
.courseNames(e.getKcmclb())
.conflictSskcbBhs(e.getCtkcbhlb())
.reason(e.getCtyy())
.conflictNo(e.getCth())
.xydNames(e.getBcmclb())
.teacherNames(e.getJyxmlb())
.classroomNames(e.getCdmclb())
.responsibleDept(e.getZrdwlb())
.build();
}
// =====================================================================
// 接口实现:初始化汇总
// =====================================================================
@Override
public TimetableConflictSummaryVO loadSummary(Integer nd) {
TimetableConflictSummaryVO summary = new TimetableConflictSummaryVO();
summary.setNd(nd);
for (TimetableConflictDimension dim : TimetableConflictDimension.values()) {
TimetableConflictCardVO card = TimetableConflictCardVO.builder()
.dimensionCode(dim.getCode())
.title(dim.getTitle())
.description(dim.getDescription())
.checked(false)
.conflictCount(0)
.build();
// 1) 优先:内存缓存有就回填(最高优先级:刚点过检查的结果)
List<TimetableConflictDetailVO> cached = detailCache.get(cacheKey(nd, dim.getCode()));
if (cached != null) {
card.setChecked(true);
card.setConflictCount(cached.size());
} else {
// 2) 兜底:DB 里该年度最新批次有数据就回填(避免"重启完 JVM 卡片全变未检查")
Long dbCnt = countLatestInDb(nd, dim.getCode());
if (dbCnt != null && dbCnt > 0) {
card.setChecked(true);
card.setConflictCount(dbCnt.intValue());
}
}
summary.getCards().add(card);
}
summary.recalculateTotal();
return summary;
}
/**
* DB 辅助:返回某年度某维度在"最新批次"中的冲突条数。
*/
private Long countLatestInDb(Integer nd, String wdbm) {
// 1. 查该年度在 DB 中"最新批次号"
List<String> batchNos = listBatchNos(nd);
if (batchNos.isEmpty()) return null;
String latest = batchNos.get(0);
// 2. 按 nd+jcpch+wdbm count
LambdaQueryWrapper<TimetableConflictResult> qw = new LambdaQueryWrapper<>();
qw.eq(TimetableConflictResult::getNd, nd)
.eq(TimetableConflictResult::getJcpch, latest)
.eq(TimetableConflictResult::getWdbm, wdbm);
try {
return conflictResultMapper.selectCount(qw);
} catch (Exception ex) {
log.warn("[课表冲突检查] 回填DB历史冲突数失败(nd={},wdbm={}): {}", nd, wdbm, ex.getMessage());
return null;
}
}
// =====================================================================
// 接口实现:单维度检查(完整签名 = 内存 + 可选落库)
// =====================================================================
@Override
public TimetableConflictCardVO checkDimension(Integer nd, TimetableConflictDimension dimension,
String jcpch, boolean writeToDb) {
if (dimension == null) {
throw new IllegalArgumentException("维度不能为空");
}
// 1. 跑聚合 SQL(和旧版完全一样,保证结果一致性)
List<TimetableConflictDetailVO> details;
long startMs = System.currentTimeMillis();
try {
details = switch (dimension) {
case TEAM_CONFLICT -> sskcbxydMapper.selectTeamConflictDetails(nd);
case ELECTIVE_REQUIRED_CONFLICT -> sskcbxydMapper.selectElectiveRequiredConflictDetails(nd);
case TEACHER_CONFLICT -> sskcbfzjyMapper.selectTeacherConflictDetails(nd);
case CLASSROOM_CONFLICT -> sskcbjsMapper.selectClassroomConflictDetails(nd);
case GUARANTEE_CONFLICT, EVENT_CONFLICT -> Collections.emptyList();
};
} catch (Exception e) {
log.error("[课表冲突检查][维度:{}][年度:{}] 执行SQL失败: {}",
dimension.getCode(), nd, e.getMessage(), e);
details = Collections.emptyList();
}
if (details == null) details = Collections.emptyList();
long costMs = System.currentTimeMillis() - startMs;
log.info("[课表冲突检查][维度:{}][年度:{}] SQL完成, {} 条冲突, 耗时 {} ms",
dimension.getCode(), nd, details.size(), costMs);
// 2. 写内存缓存(保持原逻辑,旧前端不动也能看)
detailCache.put(cacheKey(nd, dimension.getCode()), details);
// 3. 可选:写落库表(方案 A
if (writeToDb) {
String batch = jcpch;
if (batch == null || batch.isEmpty()) {
batch = generateBatchNo(nd);
}
persistOneDimension(nd, batch, dimension.getCode(), details);
}
// 4. 返回卡片
return TimetableConflictCardVO.builder()
.dimensionCode(dimension.getCode())
.title(dimension.getTitle())
.description(dimension.getDescription())
.checked(true)
.conflictCount(details.size())
.build();
}
/**
* 把一个维度的明细持久化到【课表_冲突检查结果】。
* 幂等策略:先 DELETE (nd, jcpch, wdbm) 再 INSERT 明细,保证同一批次重跑不重复。
*/
private void persistOneDimension(Integer nd, String jcpch, String wdbm,
List<TimetableConflictDetailVO> details) {
long start = System.currentTimeMillis();
// 1) 幂等清理
LambdaQueryWrapper<TimetableConflictResult> qw = new LambdaQueryWrapper<>();
qw.eq(TimetableConflictResult::getNd, nd)
.eq(TimetableConflictResult::getJcpch, jcpch)
.eq(TimetableConflictResult::getWdbm, wdbm);
conflictResultMapper.delete(qw);
// 2) 逐条 INSERT(一次检查一个维度最多几千条,直接循环够用;要更高性能改成 SqlSession Batch
int ok = 0, fail = 0;
for (TimetableConflictDetailVO vo : details) {
try {
conflictResultMapper.insert(toResultEntity(nd, jcpch, vo));
ok++;
} catch (Exception e) {
fail++;
if (fail <= 3) {
log.warn("[课表冲突检查][落库] nd={},jcpch={},wdbm={},插入冲突号={}失败: {}",
nd, jcpch, wdbm, vo.getConflictNo(), e.getMessage());
}
}
}
log.info("[课表冲突检查][落库][维度:{}] nd={},jcpch={},成功{}条/失败{}条,耗时{}ms",
wdbm, nd, jcpch, ok, fail, (System.currentTimeMillis() - start));
}
// =====================================================================
// 接口实现:全部维度检查(完整签名)
// =====================================================================
@Override
public TimetableConflictSummaryVO checkAll(Integer nd, boolean writeToDb) {
TimetableConflictSummaryVO summary = loadSummary(nd);
// 1) writeToDb 模式:统一生成一个批次号,6 个维度共用
String batchNo = null;
if (writeToDb) {
batchNo = generateBatchNo(nd);
lastBatchNo = batchNo;
log.info("[课表冲突检查][checkAll] nd={}, 生成批次号={}", nd, batchNo);
} else {
lastBatchNo = null;
}
// 2) 按卡片顺序串行检查
for (int i = 0; i < summary.getCards().size(); i++) {
TimetableConflictCardVO old = summary.getCards().get(i);
TimetableConflictDimension dim = TimetableConflictDimension.of(old.getDimensionCode());
if (dim == null) continue;
TimetableConflictCardVO newCard = checkDimension(nd, dim, batchNo, writeToDb);
summary.getCards().set(i, newCard);
}
summary.recalculateTotal();
summary.setFullyChecked(true);
return summary;
}
@Override
public String getLastBatchNo() {
return lastBatchNo;
}
// =====================================================================
// 接口实现:重置
// =====================================================================
@Override
public TimetableConflictSummaryVO reset(Integer nd, boolean clearDb) {
// 1) 清内存缓存
Iterator<Map.Entry<String, List<TimetableConflictDetailVO>>> it
= detailCache.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, List<TimetableConflictDetailVO>> e = it.next();
if (e.getKey().startsWith(nd + "_")) {
it.remove();
}
}
lastBatchNo = null;
log.info("[课表冲突检查][年度:{}] 已重置内存缓存", nd);
// 2) 可选:清落库表(方案A 硬删除,不是逻辑删除——重置意为"不留任何检查痕迹")
if (clearDb) {
int del = conflictResultMapper.deleteByNd(nd);
log.info("[课表冲突检查][年度:{}] 已清空落库表记录 {} 条", nd, del);
}
return loadSummary(nd);
}
// =====================================================================
// 接口实现:明细分页(支持 useDb)
// =====================================================================
@Override
public PageResult<TimetableConflictDetailVO> getDetailsPage(Integer nd, String dimensionCode,
Integer pageNum, Integer pageSize,
boolean useDb, String jcpch) {
// 兜底分页参数
if (pageNum == null || pageNum < 1) pageNum = 1;
if (pageSize == null || pageSize < 1) pageSize = 20;
List<TimetableConflictDetailVO> all = useDb
? buildListFromDb(nd, dimensionCode, jcpch)
: buildListFromCache(nd, dimensionCode);
// 排序(不管缓存/DB,统一按 日期 → 节次 → 维度 排)
all.sort(Comparator
.comparing((TimetableConflictDetailVO v) -> v.getRq() == null ? "" : v.getRq().toString())
.thenComparing(v -> v.getJc() == null ? 0 : v.getJc())
.thenComparing(v -> v.getDimensionCode() == null ? "" : v.getDimensionCode()));
// 手动分页
long total = all.size();
int fromIndex = Math.min((pageNum - 1) * pageSize, (int) total);
int toIndex = Math.min(fromIndex + pageSize, (int) total);
List<TimetableConflictDetailVO> pageData = total == 0
? Collections.emptyList()
: all.subList(fromIndex, toIndex);
return new PageResult<>(pageData, total, pageNum, pageSize);
}
private List<TimetableConflictDetailVO> buildListFromCache(Integer nd, String dimensionCode) {
List<TimetableConflictDetailVO> all = new ArrayList<>();
for (TimetableConflictDimension dim : TimetableConflictDimension.values()) {
if (dimensionCode != null && !"ALL".equalsIgnoreCase(dimensionCode)
&& !dim.getCode().equalsIgnoreCase(dimensionCode)) {
continue;
}
List<TimetableConflictDetailVO> part = detailCache.get(cacheKey(nd, dim.getCode()));
if (part != null && !part.isEmpty()) all.addAll(part);
}
return all;
}
private List<TimetableConflictDetailVO> buildListFromDb(Integer nd, String dimensionCode,
String jcpch) {
// 批次号没传 → 取该年度最新批次
String batch = jcpch;
if (batch == null || batch.isEmpty()) {
List<String> batches = listBatchNos(nd);
if (batches.isEmpty()) {
return Collections.emptyList();
}
batch = batches.get(0);
}
// 从 DB 拉该 nd+jcpch 的所有结果行
List<TimetableConflictResult> rows = conflictResultMapper.listByNdAndJcpch(nd, batch);
if (rows == null || rows.isEmpty()) return Collections.emptyList();
// 按 dimensionCode 过滤(null/ALL=保留全部),再实体→VO
return rows.stream()
.filter(r -> {
if (dimensionCode == null || "ALL".equalsIgnoreCase(dimensionCode)) return true;
return dimensionCode.equalsIgnoreCase(r.getWdbm());
})
.map(this::toDetailVO)
.collect(Collectors.toList());
}
// =====================================================================
// 便捷方法:批次号列表(倒序,最新排第一)
// =====================================================================
@Override
public List<String> listBatchNos(Integer nd) {
// 用 MP QueryWrapperSELECT DISTINCT "检查批次号" FROM 表 WHERE "年度" = ? ORDER BY MAX("创建时间") DESC
QueryWrapper<TimetableConflictResult> qw = new QueryWrapper<>();
qw.select("检查批次号 AS jcpch")
.eq("年度", nd)
.groupBy("检查批次号")
.orderByDesc("MAX(创建时间)");
try {
List<Object> objs = conflictResultMapper.selectObjs(qw);
List<String> res = new ArrayList<>(objs.size());
for (Object o : objs) {
if (o != null) res.add(String.valueOf(o));
}
return res;
} catch (Exception e) {
// 表若不存在 / 列名报错 → 不影响主流程,返回空
log.warn("[课表冲突检查] 查询批次号列表异常(nd={}): {}", nd, e.getMessage());
return Collections.emptyList();
}
}
}
@@ -0,0 +1,149 @@
package com.roomroot.jwgl.unit.conflict;
import lombok.Getter;
/**
* 课表冲突检查维度枚举。
* <p>
* 对应页面"教学资源冲突检查"的6个卡片,每个枚举值代表一种检查维度。
* 用于 Controller → Service → Mapper 之间传递"检查哪一类"的标识。
* </p>
* <p>
* 设计说明:
* <ul>
* <li>前4项是已明确的页面卡片,后2项是预留(保障资源/活动事件),可按需启用</li>
* <li>每个维度都对应一张或多张数据表的 GROUP BY + HAVING COUNT 聚合查询</li>
* </ul>
*
* @author management
*/
@Getter
public enum TimetableConflictDimension {
/**
* 教学班时间冲突检查。
* <p>检查逻辑:同一教学班次(学员队)在同一日期同一节次被安排多门课程 → 冲突</p>
* <p>涉及数据表:
* <ol>
* <li>实施_课程表 SSKCB —— 主表,获取日期(RQ)、节次(JC)、年度(ND)、删除状态(SCZT)</li>
* <li>实施_课程表_学员队 SSKCBXYD —— 课次与教学班次(学员队)的关联表,获取教学班次编号(XYDBH)</li>
* <li>学员队表 XYDB(辅助关联,用于翻译中文名称展示)</li>
* </ol>
* </p>
*/
TEAM_CONFLICT("TEAM_CONFLICT",
"教学班时间冲突检查",
"同一教学班次在同一时间段被安排多门课程"),
/**
* 教学班必修与选修时间冲突检查。
* <p>检查逻辑:同一教学班次在同一日期同一节次,既有"必修课"又有"选修课" → 冲突</p>
* <p>涉及数据表:
* <ol>
* <li>实施_课程表 SSKCB —— 主表,日期/节次/年度/删除状态</li>
* <li>实施_课程表_学员队 SSKCBXYD —— 课次与教学班次的关联</li>
* <li>课程表科目 KB / 课程科目表 KC —— 获取课程的"课程类型"字段(必修/选修/考查等)</li>
* </ol>
* </p>
*/
ELECTIVE_REQUIRED_CONFLICT("ELECTIVE_REQUIRED_CONFLICT",
"教学班必修与选修时间冲突检查",
"教学班次必修课与选修课时间重叠"),
/**
* 教员时间冲突检查。
* <p>检查逻辑:同一教员(主讲+辅讲统一在此维度)在同一日期同一节次被安排多门课程 → 冲突</p>
* <p>涉及数据表:
* <ol>
* <li>实施_课程表 SSKCB —— 主表,日期/节次/年度/删除状态</li>
* <li>实施_课程表_辅助教员 SSKCBFZJY —— 课次与教员的关联表(含主讲ZJY=1 与辅讲ZJY=0),获取教员编号(FZJYBH)</li>
* <li>教员表 JYB / 教研室人员 JYSX —— 翻译中文姓名展示</li>
* </ol>
* </p>
*/
TEACHER_CONFLICT("TEACHER_CONFLICT",
"教员时间冲突检查",
"同一教员在同一时间段被安排多门课程"),
/**
* 教室时间冲突检查。
* <p>检查逻辑:同一教室(场地)在同一日期同一节次被多门课程占用 → 冲突</p>
* <p>涉及数据表:
* <ol>
* <li>实施_课程表 SSKCB —— 主表,日期/节次/年度/删除状态</li>
* <li>实施_课程表_教室 SSKCBJS —— 课次与教室的关联表,获取教室编号(JSBH)</li>
* <li>教室表 JSB —— 翻译中文名称(含容纳人数/设施等信息)</li>
* </ol>
* </p>
*/
CLASSROOM_CONFLICT("CLASSROOM_CONFLICT",
"教室时间冲突检查",
"同一教室在同一时间段被多门课程占用"),
/**
* 教学保障资源冲突检查(预留/按需开启)。
* <p>检查逻辑:同一保障项目在同一日期同一节次,申请总数量 > 可提供能力阈值 → 冲突</p>
* <p>涉及数据表:
* <ol>
* <li>课程表保障明细表 KCBBZMX / 实施保障表 —— 课次申请的保障项目及数量</li>
* <li>保障项目表 BZLB / BZMX —— 项目可提供能力阈值</li>
* </ol>
* </p>
*/
GUARANTEE_CONFLICT("GUARANTEE_CONFLICT",
"教学保障资源冲突检查",
"同一保障项目同一时间申请数量超过可用能力"),
/**
* 院历/班历活动事件冲突检查(预留/按需开启)。
* <p>检查逻辑:被"不可排课"活动事件覆盖的日期节次范围,却依然存在已落地课次 → 冲突</p>
* <p>涉及数据表:
* <ol>
* <li>学期校历表 XQ / 学员队期班历 XYJXQJB —— 标记"是否可排课"的活动事件集合</li>
* <li>实施_课程表 SSKCB + 实施_课程表_学员队 SSKCBXYD —— 当前已排课的日期节次</li>
* </ol>
* </p>
*/
EVENT_CONFLICT("EVENT_CONFLICT",
"院历/班历活动事件冲突检查",
"不可排课的活动事件时段存在已排课程");
/**
* 维度编码(前端传参使用此值)
*/
private final String code;
/**
* 卡片标题(对应页面卡片名称)
*/
private final String title;
/**
* 卡片描述(对应页面卡片下的说明文字)
*/
private final String description;
TimetableConflictDimension(String code, String title, String description) {
this.code = code;
this.title = title;
this.description = description;
}
/**
* 根据编码获取枚举值(前端传 code → 后端反查枚举)。
*
* @param code 维度编码
* @return 对应枚举,找不到返回 null
*/
public static TimetableConflictDimension of(String code) {
if (code == null || code.isEmpty()) {
return null;
}
for (TimetableConflictDimension d : values()) {
if (d.getCode().equalsIgnoreCase(code)) {
return d;
}
}
return null;
}
}
@@ -0,0 +1,101 @@
package com.roomroot.jwgl.vo.courserunning;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 分组教学任务列表展示 VO。
* <p>
* 对应"分组教学任务"页面表格展示的所有字段,
* 通过多表关联查询(教学任务 + 学员队 + 课表 + 教材 + 教员 + 教室 + 实施课程)聚合展示。
* </p>
*
* @author management
*/
@Data
public class GroupTeachingTaskVO {
/**
* 序号,前端展示用的行号。
*/
private Integer xh;
/**
* 课程名称,关联课表的"课名称"。
*/
private String kcmc;
/**
* 教材名称,关联教材信息表的"名称"。
*/
private String jcmc;
/**
* 实施教员姓名,关联教员表的"教员姓名"。
*/
private String jyxm;
/**
* 教学场地(教室名称),关联教室表的"教室名称"。
*/
private String jsmc;
/**
* 可选队别班次名称(学员队名称),多个班次用分号分隔。
* 例:"2025级计算机1班;2025级计算机2班"。
*/
private String xydmc;
/**
* 计划学时,来自实施_课程表的"学时"字段。
*/
private Integer jhxs;
/**
* 运行学时,来自实施_课程表的"运行学时"字段。
*/
private Integer yxxs;
/**
* 学分,来自实施_课程表的"学分"字段。
*/
private Float xf;
/**
* 开课日期,班次的开始教学日期。
*/
private LocalDateTime kkrq;
/**
* 结课日期,班次的结束教学日期。
*/
private LocalDateTime jkrq;
/**
* 报名日期,学员报名截止日期。
*/
private LocalDateTime bmrq;
/**
* 计划人数,来自学员队表的"学员队人数"。
*/
private Integer jhrs;
/**
* 要求说明,来自学员队拼班信息的"要求说明"。
*/
private String yqsm;
// ====== 查询条件字段(仅用于筛选,不回显) ======
/**
* 年度(查询条件)。
*/
private Integer nd;
/**
* 任务状态(查询条件)。
*/
private String zt;
}
@@ -0,0 +1,51 @@
package com.roomroot.jwgl.vo.timetableconflict;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 单张"冲突检查卡片"结果 VO。
* <p>
* 对应页面上方 6 个检查卡片,每个卡片有:标题、描述、检查状态、冲突数量、维度编码。
* 前端根据 conflictCount 的值展示为"0(绿色通过)/ >0(红色警告)"。
* </p>
*
* @author management
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TimetableConflictCardVO {
/**
* 冲突维度编码(见 {@link com.roomroot.jwgl.unit.conflict.TimetableConflictDimension})。
* 例:TEAM_CONFLICT、TEACHER_CONFLICT。
*/
private String dimensionCode;
/**
* 卡片标题。例:"教学班时间冲突检查"。
*/
private String title;
/**
* 卡片描述。例:"同一教学班次在同一时间段被安排多门课程"。
*/
private String description;
/**
* 是否已执行检查。
* true = 已检查(页面去掉"未检查"标签,显示冲突数)
* false = 未检查(页面显示灰色"未检查"标签,冲突数视为 0)
*/
private boolean checked;
/**
* 检测出的冲突总数(= 冲突明细列表的总行数,非聚合数)。
* 例:同一班次在 3 个日期节次有冲突 → conflictCount = 3。
*/
private int conflictCount;
}
@@ -0,0 +1,135 @@
package com.roomroot.jwgl.vo.timetableconflict;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 单条冲突明细 VO。
* <p>
* 对应页面下方"冲突明细"表格的每一行数据。
* 所有维度的冲突统一用此结构返回,前端根据 dimensionCode 做颜色区分或图标区分。
* </p>
*
* @author management
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TimetableConflictDetailVO {
/**
* 冲突维度编码(见枚举 TimetableConflictDimension#code)。
* 例:TEACHER_CONFLICT → 前端用红色标签标注"教员冲突"。
*/
private String dimensionCode;
/**
* 冲突维度标题(中文)。例:"教员时间冲突"。
*/
private String dimensionTitle;
// ==================== 发生冲突的资源 ====================
/**
* 发生冲突的资源编号。
* 根据维度不同,含义不同:
* TEAM_CONFLICT → 教学班次(学员队)编号
* ELECTIVE_REQUIRED_CONFLICT → 教学班次(学员队)编号
* TEACHER_CONFLICT → 教员编号
* CLASSROOM_CONFLICT → 教室编号
* GUARANTEE_CONFLICT → 保障项目编号
* EVENT_CONFLICT → 活动事件编号
*/
private String resourceId;
/**
* 发生冲突的资源中文名(直接展示给用户,无需再次翻译)。
* 例:"张教授"、"101多媒体教室"、"2025级计算机1班"。
*/
private String resourceName;
// ==================== 冲突的日期节次 ====================
/**
* 发生冲突的日期。
* 例:2026-09-10。
*/
private LocalDateTime rq;
/**
* 发生冲突的节次。
* 例:3(表示当日第 3 节有冲突)。
*/
private Integer jc;
// ==================== 冲突涉及的课次/课程信息 ====================
/**
* 同一"资源+日期节次"上发生冲突的课次数量。
* 一般 >= 2(1 个资源同一时间被安排多门课)。
* 用于明细列"占用课次数"展示。
*/
private Integer occupiedLessonCount;
/**
* 冲突涉及的课程名称列表,用";"分隔。
* 例:"高等数学;大学英语"。
*/
private String courseNames;
/**
* 冲突涉及的课次编号列表,用","分隔。
* 对应表【实施_课程表 SSKCB】的主键 编号。
* 用于前端做"点击跳转到具体课次详情"的超链接。
*/
private String conflictSskcbBhs;
// ==================== 备注 ====================
/**
* 冲突原因的中文描述。
* 例:"教学班次【2025级计算机1班】在 2026-09-10 第3节同时被安排《高等数学》和《大学英语》两门课程"。
* 用于"查看详情"弹窗,或表格的备注列。
*/
private String reason;
// ==================== UI 明细表格新增业务字段(对应截图8列:冲突号/班次/教员/场地/责任单位)====================
/**
* 冲突组唯一编号(格式:日期_节次_资源编号,例:20260821_03_XYD001)。
* 由内层聚合出的"日期+节次+资源号"在外层直接拼接,便于前端定位和排查。
*/
private String conflictNo;
/**
* 班次名称(学员队名称)。
* 该冲突涉及的所有教学班次/学员队,多值用 WM_CONCAT(DISTINCT ...) 去重拼接,展示给用户。
* 例:"2025级计算机1班;2025级计算机2班"。
*/
private String xydNames;
/**
* 教员姓名。
* 该冲突涉及的所有主讲/辅讲教员,多个时拼接;可带【主】/【辅】前缀便于区分角色。
* 例:"【主】张教授;【辅】李助教"。
*/
private String teacherNames;
/**
* 场地/教室名称。
* 该冲突涉及的所有教学场地(教室)名称,多值去重拼接。
* 例:"1号教学楼101多媒体教室;2号教学楼实验机房"。
*/
private String classroomNames;
/**
* 责任单位(班次的所属单位 / 教研室名称)。
* 统一从学员队表的「所属单位」列取值(WM_CONCAT 去重)。
*/
private String responsibleDept;
}
@@ -0,0 +1,67 @@
package com.roomroot.jwgl.vo.timetableconflict;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 课表冲突检查 —— 全维度汇总结果 VO。
* <p>
* 对应页面"执行全部检查"按钮:返回 6 张卡片的检查结果数组。
* 页面初次加载时也返回此对象(所有卡片的 checked=false),供前端渲染卡片布局。
* </p>
*
* @author management
*/
@Data
public class TimetableConflictSummaryVO {
/**
* 当前检查所属年度(学期)。
* 例:2026(对应页面顶部 2026年秋季学期)。
*/
private Integer nd;
/**
* 6 张(当前已启用 4 张)冲突检查卡片的结果列表。
* 列表顺序与 UI 卡片顺序保持一致:
* 1.教学班时间冲突 → 2.必修选修冲突 → 3.教员时间冲突 → 4.教室时间冲突 → 5.保障 → 6.活动事件
*/
private List<TimetableConflictCardVO> cards;
/**
* 全部冲突总数(= 所有卡片 conflictCount 之和)。
* 用于页面顶部快速展示"本学期共有多少条冲突待处理"。
*/
private int totalConflictCount;
/**
* 是否已完成完整的全量检查(即所有卡片都执行过一次)。
* true 表示用户至少点过一次"执行全部检查"
* false 表示还未全部检查(可能只单独检查过部分卡片)
*/
private boolean fullyChecked;
public TimetableConflictSummaryVO() {
this.cards = new ArrayList<>();
this.totalConflictCount = 0;
this.fullyChecked = false;
}
/**
* 重计算总冲突数。每次单卡片检查结果更新后,上层汇总时调用此方法。
*/
public void recalculateTotal() {
int total = 0;
boolean all = !cards.isEmpty();
for (TimetableConflictCardVO c : cards) {
total += c.getConflictCount();
if (!c.isChecked()) {
all = false;
}
}
this.totalConflictCount = total;
this.fullyChecked = all;
}
}

Some files were not shown because too many files have changed in this diff Show More