Merge remote-tracking branch 'origin/main'
This commit is contained in:
+167
@@ -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 作为"聚合展示用"JOIN(WM_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."编号" 是 VARBINARY,s."实施_课程编号" 是 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:同维度1,LEFT 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>
|
||||
|
||||
+82
@@ -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) → 按条件 delete(reset 场景清该年度所有)</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);
|
||||
}
|
||||
}
|
||||
+144
@@ -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>:检查结果只存在 ConcurrentHashMap,JVM 重启丢失。
|
||||
* 方法签名不带 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);
|
||||
}
|
||||
|
||||
+438
@@ -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 QueryWrapper:SELECT 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+149
@@ -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;
|
||||
}
|
||||
}
|
||||
+101
@@ -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;
|
||||
}
|
||||
+51
@@ -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;
|
||||
}
|
||||
+135
@@ -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;
|
||||
}
|
||||
+67
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user