feat: 提交课表冲突检查相关功能模块

This commit is contained in:
dengxiaoyue
2026-08-21 14:39:03 +08:00
parent 179b12e3bf
commit f190c29e05
857 changed files with 10838 additions and 5 deletions
@@ -0,0 +1,284 @@
package com.roomroot.web.controller.jwgl;
import com.roomroot.jwgl.service.TimetableConflictService;
import com.roomroot.jwgl.unit.PageResult;
import com.roomroot.jwgl.unit.Result;
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 org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 课表冲突检查控制器。
* <p>
* 【涉及数据库表总览】(每类冲突用到的具体表详见各方法内部注释):
* <pre>
* 核心表(必用):
* 实施_课程表 SSKCB —— 日期、节次、年度、删除状态、课程科目编号
* 实施_课程表_学员队 SSKCBXYD —— 课次与教学班次(学员队)的多对多关联
* 实施_课程表_辅助教员 SSKCBFZJY —— 课次与教员(主讲/辅讲)的多对多关联
* 实施_课程表_教室 SSKCBJS —— 课次与教室的多对多关联
* 翻译表(可选,用于中文名展示;即使不存在也不会阻断聚合结果):
* 学员队表 XYDB —— 班次中文名
* 教员表 / 教研室人员 JYB / JYSX —— 教员姓名
* 教室表 JSB —— 教室名称
* 课表 KB —— 课程名称、课程类型(必修/选修)
* </pre>
* </p>
*
* @author management
*/
@RestController
@RequestMapping("/timetable-conflict")
public class TimetableConflictController {
/**
* 课表冲突检查业务服务。
* 内部维护 ConcurrentHashMap 按「年度_维度编码」为 key 缓存每次检查结果明细,
* 避免重复执行 GROUP BY+HAVING COUNT 聚合 SQL。
*/
@Resource
private TimetableConflictService timetableConflictService;
// =====================================================================
// 接口 1:单维度冲突检查(原接口2,移除/summary后接口序号前移)
// =====================================================================
/**
* 执行指定维度的冲突检查。
* <p>
* 对应页面每张卡片右下角「检查」按钮。根据前端传入的 dimensionCode,
* 分发到 Service → Mapper,执行对应的 GROUP BY + HAVING COUNT(DISTINCT) 聚合 SQL。
* 执行完成后:① 结果写入缓存 ② 可选写落库表 ③ 返回该卡片的最新状态。
* </p>
* <p>
* 不同 dimensionCode 用到的数据库表:
* <ul>
* <li>TEAM_CONFLICT(教学班时间冲突):
* 实施_课程表 SSKCB + 实施_课程表_学员队 SSKCBXYD + 学员队表 XYDB + 课表 KB</li>
* <li>ELECTIVE_REQUIRED_CONFLICT(必修选修冲突):
* 实施_课程表 SSKCB + 实施_课程表_学员队 SSKCBXYD + 学员队表 XYDB + 课表 KB(课程类型字段)</li>
* <li>TEACHER_CONFLICT(教员时间冲突):
* 实施_课程表 SSKCB + 实施_课程表_辅助教员 SSKCBFZJY + 教员表 JYB/教研室人员 + 课表 KB</li>
* <li>CLASSROOM_CONFLICT(教室时间冲突):
* 实施_课程表 SSKCB + 实施_课程表_教室 SSKCBJS + 教室表 JSB + 课表 KB</li>
* <li>GUARANTEE_CONFLICT(保障资源冲突,预留):
* 课程保障明细表 KCBBZMX + 保障类别 BZLB,当前返回空不报错</li>
* <li>EVENT_CONFLICT(活动事件冲突,预留):
* 学期校历 XQ + 学员队期班历 XYJXQJB,当前返回空不报错</li>
* </ul>
* </p>
*
* @param body 请求体 JSON,结构:
* {
* "nd": 2026, // 年度,必需
* "dimensionCode": "TEAM_CONFLICT", // 维度编码(见枚举 TimetableConflictDimension.code)
* "writeToDb": false, // 【可选】是否同时写入"课表_冲突检查结果"表,默认 false
* "jcpch": null // 【可选】批次号(writeToDb=true 时,不传则自动生成一个)
* }
* @return 单卡片检查结果 VO
*/
@PostMapping("/check")
public Result<TimetableConflictCardVO> checkDimension(@RequestBody Map<String, Object> body) {
// 1. 提取并校验参数
Object ndObj = body.get("nd");
if (ndObj == null) {
return Result.badRequest("年度(nd)不能为空");
}
Integer nd = Integer.parseInt(String.valueOf(ndObj));
String code = (String) body.get("dimensionCode");
TimetableConflictDimension dim = TimetableConflictDimension.of(code);
if (dim == null) {
return Result.badRequest("无效的冲突检查维度编码:dimensionCode=" + code);
}
// 2. 新参数:是否落库 / 指定批次号
boolean writeToDb = Boolean.TRUE.equals(body.get("writeToDb"));
String jcpch = (String) body.get("jcpch");
// 3. 执行业务层单维度检查(内存 + 可选落库)
TimetableConflictCardVO card = timetableConflictService.checkDimension(nd, dim, jcpch, writeToDb);
// 4. 组装提示信息(落库模式下把批次号带给前端,便于后续 /details 用 useDb 查询)
String msg;
if (writeToDb) {
// 单维度 check:jcpch 传了就用传的,没传就是 service 内部生成的一个——这里拿不到内部生成的,
// 只能提示"已落库";如果批次号对前端重要,建议前端调 /check-all 统一拿批次号。
msg = "【" + card.getTitle() + "】检查完成(已落库),发现 "
+ card.getConflictCount() + " 条冲突";
} else {
msg = "【" + card.getTitle() + "】检查完成,发现 "
+ card.getConflictCount() + " 条冲突";
}
return Result.success(msg, card);
}
// =====================================================================
// 接口 2:执行全部维度检查
// =====================================================================
/**
* 执行所有维度的课表冲突检查。
* <p>
* 对应页面右上角「执行全部检查」按钮。内部依次对 6 个维度调用 {@link #checkDimension}:
* 教学班 → 必修选修 → 教员 → 教室 → 保障资源(占位) → 活动事件(占位)
* 所有维度 SQL 执行完后汇总返回。fullyChecked 会被标记为 true。
* </p>
* <p>
* 方案 A 落库模式:当 writeToDb=true 时,内部自动生成一个统一批次号(jcpch,格式 {@code 年度_时间戳毫秒}),
* 6 个维度的所有冲突明细都会带上这个批次号一起写进【课表_冲突检查结果】表;
* 返回的 Result.message 会把本次批次号拼在提示文字里,前端可以保存并在后续
* {@code GET /details?useDb=true&jcpch=xxx} 中拉取该次检查的历史明细。
* </p>
*
* @param body 请求体 JSON,结构:
* {
* "nd": 2026, // 年度,必需
* "writeToDb": false // 【可选】true=同时写入"课表_冲突检查结果"表(生成统一批次号),默认 false
* }
* @return 完整汇总 VO(6 张卡片 + 总冲突数 + fullyChecked=true)。
* 落库模式下 message 形如:"全维度检查完成(批次号:2026_20260821-153000-123),共发现 N 条冲突"
*/
@PostMapping("/check-all")
public Result<TimetableConflictSummaryVO> checkAll(@RequestBody Map<String, Object> body) {
Object ndObj = body.get("nd");
if (ndObj == null) {
return Result.badRequest("年度(nd)不能为空");
}
Integer nd = Integer.parseInt(String.valueOf(ndObj));
// 新参数:是否落库(=true 时 service 内部生成一个统一批次号,6 个维度共用)
boolean writeToDb = Boolean.TRUE.equals(body.get("writeToDb"));
TimetableConflictSummaryVO result = timetableConflictService.checkAll(nd, writeToDb);
// 组装返回 message,落库模式带上批次号(前端保存后可在 /details 用 useDb=true 查询)
String msg;
if (writeToDb) {
String batchNo = timetableConflictService.getLastBatchNo();
msg = "全维度检查完成(批次号:" + batchNo + "),共发现 "
+ result.getTotalConflictCount() + " 条冲突";
} else {
msg = "全维度检查完成,共发现 "
+ result.getTotalConflictCount() + " 条冲突";
}
return Result.success(msg, result);
}
// =====================================================================
// 接口 3:重置检查结果(清空缓存)
// =====================================================================
/**
* 重置当前学期的课表冲突检查结果。
* <p>
* 对应页面「重置」按钮。默认行为:
* 清除 Service 层 ConcurrentHashMap 中所有以 "{nd}_" 开头的 key 的缓存记录,
* 所有卡片恢复 checked=false / conflictCount=0 的初始状态。
* </p>
* <p>
* 可选行为(clearDb=true):同步 DELETE 【课表_冲突检查结果】表中该年度的全部批次记录,
* 用于"彻底清空历史检查痕迹"的场景(默认 false,保持历史,避免误删)。
* </p>
*
* @param body 请求体 JSON,结构:
* {
* "nd": 2026, // 年度,必需
* "clearDb": false // 【可选】true=同时清空落库表中该年度所有批次,默认 false
* }
* @return 重置后的汇总 VO(所有卡片均为未检查状态)
*/
@PostMapping("/reset")
public Result<TimetableConflictSummaryVO> reset(@RequestBody Map<String, Object> body) {
Object ndObj = body.get("nd");
if (ndObj == null) {
return Result.badRequest("年度(nd)不能为空");
}
Integer nd = Integer.parseInt(String.valueOf(ndObj));
// 新参数:是否同时清落库表(默认只清内存,保留历史)
boolean clearDb = Boolean.TRUE.equals(body.get("clearDb"));
TimetableConflictSummaryVO result = timetableConflictService.reset(nd, clearDb);
String msg = clearDb
? "检查结果已重置(已同步清空落库表中该年度的所有历史批次)"
: "检查结果已重置(仅清内存缓存,落库历史记录保留)";
return Result.success(msg, result);
}
// =====================================================================
// 接口 4:分页查询冲突明细
// =====================================================================
/**
* 分页查询冲突明细(页面下方"冲突明细"表格使用)。
* <p>
* <b>两种数据源(useDb 参数切换):</b>
* <ul>
* <li>useDb=false(默认,兼容旧前端):从 Service 内存缓存(ConcurrentHashMap)读。
* <b>必须先点击单卡片"检查"按钮或"执行全部检查"后,才能查到对应明细。</b>
* 若尚未检查 → 返回 records=[](前端显示"请先点击上方卡片「检查」按钮"的空状态)。</li>
* <li>useDb=true(方案 A 落库模式):从【课表_冲突检查结果】表读。
* 好处:JVM 重启不丢、可回看任意一次历史批次。
* 此时 jcpch 参数有意义:传具体批次号 → 只查那一次;不传 → 自动取该年度的最新批次。</li>
* </ul>
* </p>
*
* @param nd 年度(学期过滤),必需
* @param dimensionCode 维度编码(可选)。传 null / "ALL" 表示合并展示所有维度的冲突;
* 也可传单一维度编码(如 "TEACHER_CONFLICT")只看教员冲突。
* @param pageNum 当前页码(从 1 开始,默认 1)
* @param pageSize 每页大小(默认 20 条)
* @param useDb 【可选】true=从落库表查;false=从内存缓存查(默认)
* @param jcpch 【可选,useDb=true 时生效】查哪一个检查批次号。
* 不传则自动取该年度在 DB 中的"最新批次"(MAX 创建时间)。
* @return 分页结果。每条明细包含:冲突号、课程、日期、节次、班次、教员、场地、责任单位
*/
@GetMapping("/details")
public Result<PageResult<TimetableConflictDetailVO>> getDetails(
@RequestParam(value = "nd") Integer nd,
@RequestParam(value = "dimensionCode", required = false) String dimensionCode,
@RequestParam(value = "pageNum", required = false) Integer pageNum,
@RequestParam(value = "pageSize", required = false) Integer pageSize,
@RequestParam(value = "useDb", required = false, defaultValue = "false") boolean useDb,
@RequestParam(value = "jcpch", required = false) String jcpch) {
PageResult<TimetableConflictDetailVO> page
= timetableConflictService.getDetailsPage(nd, dimensionCode, pageNum, pageSize, useDb, jcpch);
String msg = useDb
? "查询成功(数据源:落库表" + (jcpch != null && !jcpch.isEmpty() ? ",批次号=" + jcpch : ",最新批次") + ")"
: "查询成功(数据源:内存缓存)";
return Result.success(msg, page);
}
// =====================================================================
// 接口 5:查询指定年度的检查批次号列表(落库模式辅助接口)
// =====================================================================
/**
* 查询指定年度的全部检查批次号(按创建时间倒序,最新排第一)。
* <p>
* 前端用途:"查看历史检查"下拉框,用户选择某次批次号后,
* 再调用 {@code GET /details?useDb=true&jcpch=xxx} 拉取那次的明细。
* </p>
*
* @param nd 年度,必需
* @return 批次号字符串列表(空表示无历史)
*/
@GetMapping("/batch-nos")
public Result<java.util.List<String>> listBatchNos(
@RequestParam(value = "nd") Integer nd) {
java.util.List<String> list = timetableConflictService.listBatchNos(nd);
return Result.success("查询成功,共 " + list.size() + " 个批次", list);
}
}