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,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);
}
}
@@ -0,0 +1 @@
restart.include.json=/com.alibaba.fastjson2.*.jar
@@ -0,0 +1,56 @@
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
# 达梦连接串:ip:端口,schema指定默认模式
url: jdbc:dm://10.1.1.17:5236?schema=JIAOWU&useUnicode=true&characterEncoding=utf-8
username: JIAOWU
password: Jiaowu123
driver-class-name: dm.jdbc.driver.DmDriver
# Druid 基础参数
druid:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置连接超时时间
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: roomroot
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
db-type: dm
@@ -0,0 +1,149 @@
# 项目相关配置
roomroot:
# 名称
name: roomroot
# 版本
version: 3.9.2
# 版权年份
copyrightYear: 2026
# 文件路径 示例( Windows配置D:/roomroot/uploadPath,Linux配置 /home/roomroot/uploadPath)
profile: D:/roomroot/uploadPath
# 获取ip地址开关
addressEnabled: false
# 验证码类型 math 数字计算 char 字符验证
captchaType: math
# 开发环境配置
server:
# 服务器的HTTP端口,默认为8080
port: 8080
servlet:
# 应用的访问路径
context-path: /api
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数,默认为100
accept-count: 1000
threads:
# tomcat最大线程数,默认为200
max: 800
# Tomcat启动初始化的线程数,默认值10
min-spare: 100
# 日志配置
logging:
level:
com.roomroot: debug
#org.springframework: warn
# 用户配置
user:
password:
# 密码最大错误次数
maxRetryCount: 5
# 密码锁定时间(默认10分钟)
lockTime: 10
# Spring配置
spring:
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages
profiles:
active: druid
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 10MB
# 设置总上传的文件大小
max-request-size: 20MB
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
# 服务模块
devtools:
restart:
# 热部署开关
enabled: true
data:
# redis 配置
redis:
# 地址
host: localhost
# 端口,默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: abcdefghijklmnopqrstuvwxyz
# 令牌有效期(默认30分钟)
expireTime: 30
# MyBatis-Plus配置
mybatis-plus:
# 搜索指定包别名
typeAliasesPackage: com.roomroot.**.domain
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapperLocations: classpath*:mapper/**/*Mapper.xml,classpath*:com/roomroot/**/mapper/*.xml
# 加载全局的配置文件
configLocation: classpath:mybatis/mybatis-config.xml
# Springdoc配置
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html
tags-sorter: alpha
group-configs:
- group: 'default'
display-name: '测试模块'
paths-to-match: '/**'
packages-to-scan: com.roomroot.web.controller.tool
# 防盗链配置
referer:
# 防盗链开关
enabled: false
# 允许的域名列表
allowed-domains: localhost,127.0.0.1,roomroot.vip,www.roomroot.vip
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# 自定义内嵌Redis开关
custom:
embedded-redis:
# dev环境true自动启动内嵌Redis;prod改为false使用外部Redis
enable: false
port: 6379
max-memory: 128M
@@ -0,0 +1,5 @@
Application Version: ${roomroot.version}
Spring Boot Version: ${spring-boot.version}
////////////////////////////////////////////////////////////////////
// 启动开始 永不宕机 永无BUG //
////////////////////////////////////////////////////////////////////
@@ -0,0 +1,38 @@
#错误消息
not.null=* 必须填写
user.jcaptcha.error=验证码错误
user.jcaptcha.expire=验证码已失效
user.not.exists=用户不存在/密码错误
user.password.not.match=用户不存在/密码错误
user.password.retry.limit.count=密码输入错误{0}次
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
user.password.delete=对不起,您的账号已被删除
user.blocked=用户已封禁,请联系管理员
role.blocked=角色已封禁,请联系管理员
login.blocked=很遗憾,访问IP已被列入系统黑名单
user.logout.success=退出成功
length.not.valid=长度必须在{min}到{max}个字符之间
user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
user.password.not.valid=* 5-50个字符
user.email.not.valid=邮箱格式错误
user.mobile.phone.number.not.valid=手机号格式错误
user.login.success=登录成功
user.register.success=注册成功
user.notfound=请重新登录
user.forcelogout=管理员强制退出,请重新登录
user.unknown.error=未知错误,请重新登录
##文件上传消息
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
upload.filename.exceed.length=上传的文件名最长{0}个字符
##权限
no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径 -->
<property name="log.path" value="/home/roomroot/logs" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-info.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 用户访问日志输出 -->
<appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-user.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滚 daily -->
<fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统模块日志级别控制 -->
<logger name="com.roomroot" level="info" />
<!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" />
<root level="info">
<appender-ref ref="console" />
</root>
<!--系统操作日志-->
<root level="info">
<appender-ref ref="file_info" />
<appender-ref ref="file_error" />
</root>
<!--系统用户操作日志-->
<logger name="sys-user" level="info">
<appender-ref ref="sys-user"/>
</logger>
</configuration>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 全局参数 -->
<settings>
<!-- 使全局的映射器启用或禁用缓存 -->
<setting name="cacheEnabled" value="true" />
<!-- 允许JDBC 支持自动生成主键 -->
<setting name="useGeneratedKeys" value="true" />
<!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
<setting name="defaultExecutorType" value="SIMPLE" />
<!-- 指定 MyBatis 所用日志的具体实现 -->
<setting name="logImpl" value="SLF4J" />
<!-- 使用驼峰命名法转换字段 -->
<!-- <setting name="mapUnderscoreToCamelCase" value="true"/> -->
</settings>
</configuration>
@@ -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.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.roomroot.jwgl.entity.SSKCBFZJY; import com.roomroot.jwgl.entity.SSKCBFZJY;
import com.roomroot.jwgl.vo.timetableconflict.TimetableConflictDetailVO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@@ -13,4 +14,12 @@ public interface SSKCBFZJYMapper extends BaseMapper<SSKCBFZJY> {
List<SSKCBFZJY> selectBySskcbh(@Param("sskcbh") String sskcbh); List<SSKCBFZJY> selectBySskcbh(@Param("sskcbh") String sskcbh);
int deleteBySskcbbh(@Param("sskcbbh") String sskcbbh); 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"> <mapper namespace="com.roomroot.jwgl.mapper.SSKCBFZJYMapper">
<select id="selectBySskcbh" resultType="com.roomroot.jwgl.entity.SSKCBFZJY"> <select id="selectBySskcbh" resultType="com.roomroot.jwgl.entity.SSKCBFZJY">
SELECT * FROM 实施_课程表_辅助教员 WHERE 实施_课程表编号 = #{sskcbh} SELECT * FROM "实施_课程表_辅助教员" WHERE "实施_课程表编号" = #{sskcbh}
</select> </select>
<delete id="deleteBySskcbbh"> <delete id="deleteBySskcbbh">
DELETE FROM 实施_课程表_辅助教员 WHERE 实施_课程表编号 = #{sskcbbh} DELETE FROM "实施_课程表_辅助教员" WHERE "实施_课程表编号" = #{sskcbbh}
</delete> </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> </mapper>
@@ -2,6 +2,7 @@ package com.roomroot.jwgl.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.roomroot.jwgl.entity.SSKCBJS; import com.roomroot.jwgl.entity.SSKCBJS;
import com.roomroot.jwgl.vo.timetableconflict.TimetableConflictDetailVO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@@ -13,4 +14,12 @@ public interface SSKCBJSMapper extends BaseMapper<SSKCBJS> {
List<SSKCBJS> selectBySskcbh(@Param("sskcbh") String sskcbh); List<SSKCBJS> selectBySskcbh(@Param("sskcbh") String sskcbh);
int deleteBySskcbbh(@Param("sskcbbh") String sskcbbh); 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"> <mapper namespace="com.roomroot.jwgl.mapper.SSKCBJSMapper">
<select id="selectBySskcbh" resultType="com.roomroot.jwgl.entity.SSKCBJS"> <select id="selectBySskcbh" resultType="com.roomroot.jwgl.entity.SSKCBJS">
SELECT * FROM 实施_课程表_教室 WHERE 实施_课程表编号 = #{sskcbh} SELECT * FROM "实施_课程表_教室" WHERE "实施_课程表编号" = #{sskcbh}
</select> </select>
<delete id="deleteBySskcbbh"> <delete id="deleteBySskcbbh">
DELETE FROM 实施_课程表_教室 WHERE 实施_课程表编号 = #{sskcbbh} DELETE FROM "实施_课程表_教室" WHERE "实施_课程表编号" = #{sskcbbh}
</delete> </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> </mapper>
@@ -3,6 +3,7 @@ package com.roomroot.jwgl.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.roomroot.jwgl.entity.SSKCBXYD; import com.roomroot.jwgl.entity.SSKCBXYD;
import com.roomroot.jwgl.vo.elective.ClassScheduleVO; 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.Mapper;
import org.apache.ibatis.annotations.Param; 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); 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."课名称" k."课名称"
FROM "实施_课程表" s FROM "实施_课程表" s
JOIN "实施_课程表_学员队" t ON t."实施_课程表编号" = 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} WHERE t."学员队编号" = #{xydbh}
<if test="nd != null"> <if test="nd != null">
AND s."年度" = #{nd} AND s."年度" = #{nd}
@@ -38,4 +44,187 @@
ORDER BY s."日期", s."节次" ORDER BY s."日期", s."节次"
</select> </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> </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) → 按条件 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);
}
}
@@ -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);
}
@@ -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();
}
}
}
@@ -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