forked from liweijie/education
Merge remote-tracking branch 'origin/main'
This commit is contained in:
+284
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user