1、校历的78、晚上、夜间等直接落库,

2、配当、任务书完善、排课窗完善;
3、任务书批量发布、删除等;
4、班历同步校历
This commit is contained in:
2026-09-18 10:38:39 +08:00
parent 911dfe911b
commit f3ae9fb2fd
62 changed files with 3508 additions and 143 deletions
@@ -2,6 +2,8 @@ package com.roomroot.jwgl.dto.kcb;
import lombok.Data;
import java.util.List;
/**
* 班次课表查询条件。
* <p>未传年度、学期时按当前时间取本学期。</p>
@@ -20,4 +22,19 @@ public class TimetableQuery {
/** 学员队名称(队别班次模糊) */
private String xydmc;
/** 教员编号(主讲/辅讲/责任教员任一命中) */
private String jybh;
/** 教室编号(课次场地或课程默认场地任一命中) */
private String jsbh;
/** 教研室代号(按课程归属教研室过滤) */
private String jysdh;
/** 学员编号(服务端解析为其所在学员队后按队过滤) */
private String xybh;
/** 教员编号集合(教研室等批量范围过滤用) */
private List<String> jybhs;
}
@@ -0,0 +1,20 @@
package com.roomroot.jwgl.dto.scheduling;
import lombok.Data;
/**
* 拖拽移动课次请求(阶段 E4):把课程在 from 格的课次平移到 to 格。
* 目标格沿用 arrange 的硬冲突校验;软提示(配当周次不符)照常移动并返回 warning。
*/
@Data
public class SchedulingMoveRequest {
/** 实施_课程编号 */
private String sskcbh;
/** 源格(已有课次) */
private SchedulingArrangeRequest.Cell from;
/** 目标格 */
private SchedulingArrangeRequest.Cell to;
}
@@ -0,0 +1,30 @@
package com.roomroot.jwgl.dto.taskbook;
import lombok.Data;
import java.util.List;
/**
* 批量填报请求:对所选课程任务行统一应用填报字段(仅写非空字段)。
*/
@Data
public class TaskBookBatchFillRequest {
/** 课程任务编号列表 */
private List<String> bhList;
/** 教研室计划教员编号 */
private String jysjhjybh;
/** 场地编号(useSpecial=true 时忽略,取班次专用教室) */
private String jsbh;
/** 使用班次专用教室 */
private Boolean useSpecial;
/** 排课建议(教研室计划备注) */
private String jysjhbz;
/** 课次序号 */
private Integer kcxh;
}
@@ -0,0 +1,21 @@
package com.roomroot.jwgl.dto.taskbook;
import lombok.Data;
import java.util.List;
/**
* 批量指定场地请求。
*/
@Data
public class TaskBookBatchRoomRequest {
/** 课程任务编号列表 */
private List<String> bhList;
/** 教室编号(useSpecial=false 时必传) */
private String jsbh;
/** true=使用各行班次专用教室(忽略 jsbh,取各班次学期.专用教室编号) */
private Boolean useSpecial;
}
@@ -0,0 +1,26 @@
package com.roomroot.jwgl.dto.taskbook;
import lombok.Data;
import java.util.List;
/**
* 批量指定责任教员请求。
*/
@Data
public class TaskBookBatchTeacherRequest {
/** 课程任务编号列表 */
private List<String> bhList;
/**
* 指定方式:
* unit — 责任单位教员(教员必须属于各行教研室 jysdh);
* academy — 全院教员(任意在职教员);
* plan — 应用各行教研室计划教员(jybh = 各行 jysjhjybh)。
*/
private String mode;
/** 责任教员编号(mode=unit / academy 时必传) */
private String jybh;
}
@@ -19,4 +19,7 @@ public class TaskBookFillRequest {
/** 排课建议(教研室计划备注) */
private String jysjhbz;
/** 课次序号(课序设置;非空时写入) */
private Integer kcxh;
}
@@ -25,6 +25,44 @@
AND cxf."学期第次" = #{cond.xqdc}
)
</if>
<if test="cond != null and cond.jybh != null and cond.jybh != ''">
AND (
c."教员编号" = #{cond.jybh}
OR c."责任教员编号" = #{cond.jybh}
OR EXISTS (
SELECT 1 FROM "实施_课程表_辅助教员" f2
WHERE f2."实施_课程表编号" = s."编号"
AND f2."辅助教员编号" = #{cond.jybh}
)
)
</if>
<if test="cond != null and cond.jybhs != null and cond.jybhs.size() > 0">
AND (
c."教员编号" IN
<foreach collection="cond.jybhs" item="j" open="(" separator="," close=")">#{j}</foreach>
OR c."责任教员编号" IN
<foreach collection="cond.jybhs" item="j" open="(" separator="," close=")">#{j}</foreach>
OR EXISTS (
SELECT 1 FROM "实施_课程表_辅助教员" f3
WHERE f3."实施_课程表编号" = s."编号"
AND f3."辅助教员编号" IN
<foreach collection="cond.jybhs" item="j" open="(" separator="," close=")">#{j}</foreach>
)
)
</if>
<if test="cond != null and cond.jsbh != null and cond.jsbh != ''">
AND (
c."教室编号" = #{cond.jsbh}
OR EXISTS (
SELECT 1 FROM "实施_课程表_教室" j2
WHERE j2."实施_课程表编号" = s."编号"
AND j2."教室编号" = #{cond.jsbh}
)
)
</if>
<if test="cond != null and cond.jysdh != null and cond.jysdh != ''">
AND k."教研室代号" = #{cond.jysdh}
</if>
</sql>
<select id="selectLessonTimetable" resultType="com.roomroot.jwgl.vo.kcb.DailyWeeklyTimetableVO">
@@ -89,6 +127,50 @@
<if test="cond != null and cond.xydmc != null and cond.xydmc != ''">
AND xyd."学员队名称" LIKE CONCAT('%', #{cond.xydmc}, '%')
</if>
<if test="cond != null and cond.jybh != null and cond.jybh != ''">
AND (
c."教员编号" = #{cond.jybh}
OR c."责任教员编号" = #{cond.jybh}
OR EXISTS (
SELECT 1 FROM "实施_课程表" t2
INNER JOIN "实施_课程表_辅助教员" f2 ON f2."实施_课程表编号" = t2."编号"
WHERE LOWER(RAWTOHEX(c."编号")) = LOWER(REPLACE(TRIM(t2."实施_课程编号"), '-', ''))
AND COALESCE(t2."删除状态", 0) = 0
AND f2."辅助教员编号" = #{cond.jybh}
)
)
</if>
<if test="cond != null and cond.jybhs != null and cond.jybhs.size() > 0">
AND (
c."教员编号" IN
<foreach collection="cond.jybhs" item="j" open="(" separator="," close=")">#{j}</foreach>
OR c."责任教员编号" IN
<foreach collection="cond.jybhs" item="j" open="(" separator="," close=")">#{j}</foreach>
OR EXISTS (
SELECT 1 FROM "实施_课程表" t3
INNER JOIN "实施_课程表_辅助教员" f3 ON f3."实施_课程表编号" = t3."编号"
WHERE LOWER(RAWTOHEX(c."编号")) = LOWER(REPLACE(TRIM(t3."实施_课程编号"), '-', ''))
AND COALESCE(t3."删除状态", 0) = 0
AND f3."辅助教员编号" IN
<foreach collection="cond.jybhs" item="j" open="(" separator="," close=")">#{j}</foreach>
)
)
</if>
<if test="cond != null and cond.jsbh != null and cond.jsbh != ''">
AND (
c."教室编号" = #{cond.jsbh}
OR EXISTS (
SELECT 1 FROM "实施_课程表" t4
INNER JOIN "实施_课程表_教室" j2 ON j2."实施_课程表编号" = t4."编号"
WHERE LOWER(RAWTOHEX(c."编号")) = LOWER(REPLACE(TRIM(t4."实施_课程编号"), '-', ''))
AND COALESCE(t4."删除状态", 0) = 0
AND j2."教室编号" = #{cond.jsbh}
)
)
</if>
<if test="cond != null and cond.jysdh != null and cond.jysdh != ''">
AND k."教研室代号" = #{cond.jysdh}
</if>
</sql>
<sql id="ClassTimetableSelect">
@@ -242,6 +242,7 @@
d."学员队名称" AS xydmc,
r."教员编号" AS jybh,
jy."教员姓名" AS jyxm,
jysx."主任" AS zr,
r."教室编号" AS jsbh,
js."教室名称" AS jsmc,
r."课次序号" AS kcxh,
@@ -256,6 +257,7 @@
LEFT JOIN "课表" k ON k."课编号" = r."课编号"
LEFT JOIN "学员队表" d ON d."学员队编号" = r."学员队编号"
LEFT JOIN "教员表" jy ON jy."教员编号" = r."教员编号"
LEFT JOIN "教员属性" jysx ON jysx."教员编号" = r."教员编号"
LEFT JOIN "教员表" jy2 ON jy2."教员编号" = r."教研室计划教员编号"
LEFT JOIN "教室表" js ON js."教室编号" = r."教室编号"
LEFT JOIN "教研室表" jys ON jys."教研室代号" = r."教研室代号"
@@ -28,10 +28,11 @@ public interface ClassEventCalendarService {
int batchSave(ClassCalendarBatchSaveDTO body);
/**
* 重新同步校历:只补校历有而班历缺失的时间格,不覆盖人工改动
* 重新同步校历:默认只补校历有而班历缺失的时间格,不覆盖人工改动
* force=true 时额外把与校历不一致的已有格按校历覆盖。
* confirm=false 仅返回差异预览;confirm=true 实际写入。
*/
ClassCalendarResyncVO resync(String xydxqbh, boolean confirm);
ClassCalendarResyncVO resync(String xydxqbh, boolean confirm, boolean force);
/**
* 把选定或整个班历覆盖到其它班次的对应时间格。不改课程任务。
@@ -16,6 +16,12 @@ public interface ClassPeriodService {
*/
List<String> slotLabels();
/**
* 双节次时间格行(双节次=1),按起始节次排序;表内无数据时返回空。
* 供前端按「节次时间表」渲染日历节次列。
*/
List<JCSJB> slotRows();
/**
* 上午时段的双节次标签,用于「正课」默认判定。
*/
@@ -6,6 +6,7 @@ import com.roomroot.jwgl.unit.PageQuery;
import com.roomroot.jwgl.unit.PageResult;
import com.roomroot.jwgl.vo.kcb.ClassTimetableVO;
import com.roomroot.jwgl.vo.kcb.DailyWeeklyTimetableRangeVO;
import com.roomroot.jwgl.vo.kcb.DailyWeeklyTimetableVO;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
@@ -87,4 +88,10 @@ public interface KCBService {
* 班次课程列表另存 Excel。
*/
void exportClassCourses(TimetableQuery cond, HttpServletResponse response);
/**
* 课次综合查询:多维条件(年度/学期/学员队/教员/教室/教研室/学员)+ 日期范围。
* start/end 缺省时取本周;身份范围在服务端强制(学员本队、教员本人、教研室本室)。
*/
List<DailyWeeklyTimetableVO> listLessons(TimetableQuery cond, String start, String end);
}
@@ -0,0 +1,54 @@
package com.roomroot.jwgl.service;
import com.roomroot.jwgl.vo.kcb.ClassTimetableVO;
import com.roomroot.jwgl.vo.kcb.DailyWeeklyTimetableVO;
import com.roomroot.jwgl.vo.kcb.TimetableGridVO;
import com.roomroot.jwgl.vo.mybusiness.MyCourseTaskVO;
import java.util.List;
import java.util.Map;
/**
* 「我的教学」角色视图服务(阶段 C)。
*
* <p>所有接口由服务端按登录身份解析业务主体(教员/教研室/学员队),
* 不接受调用方传入他人编号,防止越权。</p>
*/
public interface MyBusinessService {
// ==================== 教员 ====================
/** 本教员课程任务(学员队任务表视角,含教学任务归属)。 */
List<MyCourseTaskVO> teacherCourses(Integer nd, Integer xqdc);
/** 本教员实施计划(课次日历列表,默认本周)。 */
List<DailyWeeklyTimetableVO> teacherLessons(String start, String end, Integer nd);
/** 本教员课表网格(dim=teacher)。 */
TimetableGridVO teacherGrid(String mbbh, String start, String end, Integer nd);
// ==================== 教研室 ====================
/** 今日本室计划:本室全部教员(含辅讲)当日课次。 */
List<DailyWeeklyTimetableVO> officeToday(String rq, Integer offset);
/** 本室课程(班次课程列表按教研室过滤)。 */
List<ClassTimetableVO> officeCourses(Integer nd, Integer xqdc);
/** 本室教员清单(jybh/jyxm/zr)。 */
List<Map<String, Object>> officeTeachers();
// ==================== 学员 ====================
/** 本学员队课程列表。 */
List<ClassTimetableVO> studentCourses(Integer nd, Integer xqdc);
/** 本学员队课次(日实施计划,默认本周)。 */
List<DailyWeeklyTimetableVO> studentLessons(String start, String end);
/** 本学员队课表网格(dim=team)。 */
TimetableGridVO studentGrid(String mbbh, String start, String end, Integer nd);
/** 本学员队干部清单(骨干任职非空的同期学员)。 */
List<Map<String, Object>> teamCadres();
}
@@ -3,6 +3,7 @@ package com.roomroot.jwgl.service;
import com.roomroot.jwgl.dto.scheduling.SchedulingArrangeRequest;
import com.roomroot.jwgl.dto.scheduling.SchedulingCellOpRequest;
import com.roomroot.jwgl.dto.scheduling.SchedulingCourseEditRequest;
import com.roomroot.jwgl.dto.scheduling.SchedulingMoveRequest;
import com.roomroot.jwgl.vo.scheduling.SchedulingViewVO;
import java.util.List;
@@ -26,6 +27,14 @@ public interface SchedulingWindowService {
*/
Map<String, Object> arrange(SchedulingArrangeRequest request);
/**
* 拖拽移动课次:把 from 格的课次平移到 to 格(阶段 E4)。
* 目标格走与 arrange 相同的硬冲突校验;软提示照常移动并返回 warning。
*
* @return {moved: 1, warning: 软提示原因(可空)}
*/
Map<String, Object> moveLesson(SchedulingMoveRequest request);
/** 删除所选节次:仅删除该课程在这些格上的课次 */
int deleteCells(SchedulingCellOpRequest request);
@@ -38,6 +47,9 @@ public interface SchedulingWindowService {
/** 排课日志查询:按课程(可叠加操作类型) */
List<Map<String, Object>> logs(String sskcbh, String czlx);
/** 导出排课窗视图 Excel:每周一个 sheet(节次×星期网格,E4) */
byte[] exportView(String xydxqbh);
/** 编辑课程教学任务信息:学时/周课时/课程简称/责任教员/默认场地(手册 12.3.1) */
Map<String, Object> updateCourse(SchedulingCourseEditRequest request);
@@ -0,0 +1,22 @@
package com.roomroot.jwgl.service;
import com.roomroot.jwgl.entity.DBVersion;
/**
* 教务系统参数(DBVersion 单行)。
*
* <p>阶段 E1:校历/编辑器显示配置(显示78节、显示晚上、显示夜间)与
* 教学要点名称等参数的读写。</p>
*/
public interface SystemSettingsService {
/**
* 读取系统参数行;库中无记录时返回默认行(不落库)。
*/
DBVersion get();
/**
* 更新系统参数;只写非空字段。库中无记录时先插入一行。
*/
DBVersion update(DBVersion settings);
}
@@ -1,5 +1,8 @@
package com.roomroot.jwgl.service;
import com.roomroot.jwgl.dto.taskbook.TaskBookBatchFillRequest;
import com.roomroot.jwgl.dto.taskbook.TaskBookBatchRoomRequest;
import com.roomroot.jwgl.dto.taskbook.TaskBookBatchTeacherRequest;
import com.roomroot.jwgl.dto.taskbook.TaskBookBhListRequest;
import com.roomroot.jwgl.dto.taskbook.TaskBookFillRequest;
import com.roomroot.jwgl.dto.taskbook.TaskBookRoomRequest;
@@ -27,9 +30,15 @@ public interface TaskBookFillService {
/**
* 任务书填报列表:某教学任务下全部课程任务行(排序 课程 → 责任教员 → 课次)。
* 教员账号自动过滤为本人相关行;教研室账号过滤为本室行。
*/
List<TaskBookRowVO> list(String jxrwbh);
/**
* 任务书填报列表(可按责任/计划教员编号过滤)。
*/
List<TaskBookRowVO> list(String jxrwbh, String jybh);
/**
* 手动合班:所选行写入同一编组(新组号 = 现有最大编组 + 1)。
* 校验 科目、学时、课类型、成绩分制 相同,任一不同则拒绝并说明。
@@ -70,9 +79,45 @@ public interface TaskBookFillService {
int setRoom(TaskBookRoomRequest request);
/**
* 填报字段:计划教员(jysjhjybh)、场地(jsbh,同合班同步)、排课建议(jysjhbz)。
* 填报字段:计划教员(jysjhjybh)、场地(jsbh,同合班同步)、排课建议(jysjhbz)、课次序号(kcxh
*
* @return 实际更新行数
*/
int fill(TaskBookFillRequest request);
/**
* 批量指定责任教员:对每行执行与 {@link #setTeacher} 相同的校验与合班组同步。
*
* @return 实际更新行数合计
*/
int batchSetTeacher(TaskBookBatchTeacherRequest request);
/**
* 批量指定场地:对每行执行与 {@link #setRoom} 相同的校验与合班组同步。
*
* @return 实际更新行数合计
*/
int batchSetRoom(TaskBookBatchRoomRequest request);
/**
* 批量填报:对所选行统一应用填报字段(计划教员/场地/排课建议/课次序号,仅写非空)。
* 每行执行与 {@link #fill} 相同的校验;整批单事务。
*
* @return 实际更新行数合计
*/
int batchFill(TaskBookBatchFillRequest request);
/**
* 行级删除:所选课程任务行逻辑删除(delFlag=1)。
* 与拆班同口径:仅机关/教研室可做;已发布到运行课表的班次须先撤回。
*
* @return 实际删除行数
*/
int deleteRows(TaskBookBhListRequest request);
/**
* 教研室跨任务汇总:该教研室全部任务书(JYSRWS),附教学任务名称/年度/状态。
* 教研室账号强制本室代号。
*/
List<Map<String, Object>> officeSummary(String jysdh);
}
@@ -49,6 +49,15 @@ public interface TeachingAllocationService {
*/
int setGroup(AllocationGroupRequest request);
/**
* 自动排布:按铺学时建议把「配档起始周」回填到任务(pdqsz 落库)。
* onlyMissing=true(辅助排布)只回填尚未设置起始周的任务,不动人工排布结果;
* bhList 非空时只处理所选任务。
*
* @return 实际回填行数
*/
int autoArrange(String xydxqbh, List<String> bhList, boolean onlyMissing);
/**
* 导出配当 Excel(批量:每行一个任务,含班次名称与学期代号列)。
*/
@@ -64,13 +64,13 @@ public interface TeachingTaskService {
List<JXRW> listAllValid();
/**
* 发布教学任务及教研室任务书
* 根据教学任务编号修改状态为"发布",并批量更新所有关联的教研室任务书状态为"发布"
* 批量发布教学任务及教研室任务书
* 逐个任务置"发布"并同步其教研室任务书;任一任务校验失败(已结束/任务书未生成)整批回滚。
*
* @param bh 教学任务编号
* @return 更新数量
* @param bhList 教学任务编号列表
* @return 更新的教研室任务书行数合计
*/
int batchPublish(String bh);
int batchPublish(List<String> bhList);
/**
* 结束发布。结束后教研室不能再改任务书和课程任务。
@@ -1,6 +1,7 @@
package com.roomroot.jwgl.service;
import com.roomroot.jwgl.dto.kcb.TimetableGridQuery;
import com.roomroot.jwgl.vo.kcb.FreePeriodsVO;
import com.roomroot.jwgl.vo.kcb.TimetableGridVO;
/**
@@ -18,4 +19,10 @@ public interface TimetableGridService {
* 导出网格课表为矩阵 Excel(每周一个 sheet)。
*/
byte[] exportGrid(TimetableGridQuery query);
/**
* 有课/无课时段查询:按日期给出 busy(有课节次)与 free(无课节次)。
* 复用 {@link #grid} 的维度解析与历表不可排判定。
*/
FreePeriodsVO freePeriods(TimetableGridQuery query);
}
@@ -138,54 +138,101 @@ public class ClassEventCalendarServiceImpl implements ClassEventCalendarService
}
/**
* 重新同步校历(口径A:只补缺失格,不覆盖人工改动)
* confirm=false 返回差异预览;confirm=true 实际写入
* 重新同步校历。confirm=false 返回差异预览;confirm=true 实际写入
* 默认只补缺失格,不覆盖人工改动;force=true 时额外把与校历不一致的已有格按校历覆盖
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ClassCalendarResyncVO resync(String xydxqbh, boolean confirm) {
public ClassCalendarResyncVO resync(String xydxqbh, boolean confirm, boolean force) {
XYDNDXQJBXXB semester = requireSemester(xydxqbh);
LocalDate start = semester.getKxrq();
LocalDate end = semester.getJsrq();
if (start == null || end == null) {
throw new ServiceException("班次学期缺少开学或结束日期,不能同步校历", BAD_REQUEST);
}
Set<String> existingKeys = new HashSet<>();
Map<String, JQB> existingByKey = new LinkedHashMap<>();
for (JQB row : listRows(xydxqbh)) {
existingKeys.add(cellKey(row.getJqsj(), row.getCourseClass()));
existingByKey.putIfAbsent(cellKey(row.getJqsj(), row.getCourseClass()), row);
}
List<ClassCalendarEventDTO> missing = new ArrayList<>();
List<ClassCalendarEventDTO> changed = new ArrayList<>();
for (XQXLB school : mergeByDayAndPeriod(loadSchoolRows(semester, start, end), start, end)) {
LocalDate day = parseDay(school.getJqsj());
if (!existingKeys.contains(cellKey(Integer.valueOf(day.format(COMPACT)), school.getCourseClass()))) {
ClassCalendarEventDTO dto = new ClassCalendarEventDTO();
dto.setJqsj(day.format(DAY));
dto.setCourseClass(school.getCourseClass());
dto.setJqmc(school.getJqmc());
dto.setJc(Boolean.TRUE.equals(school.getJc()));
dto.setKpk(Boolean.TRUE.equals(school.getKpk()));
dto.setBzxs(Boolean.TRUE.equals(school.getBzxs()));
dto.setZdpk(Boolean.TRUE.equals(school.getZdpk()));
dto.setZk(Boolean.TRUE.equals(school.getZk()));
dto.setBz(school.getBz());
missing.add(dto);
String key = cellKey(Integer.valueOf(day.format(COMPACT)), school.getCourseClass());
JQB existing = existingByKey.get(key);
if (existing == null) {
missing.add(schoolDto(day, school));
} else if (force && differs(existing, school)) {
changed.add(schoolDto(day, school));
}
}
ClassCalendarResyncVO vo = new ClassCalendarResyncVO();
vo.setItems(missing);
vo.setAddCount(missing.size());
if (confirm && !missing.isEmpty()) {
vo.setUpdateItems(changed);
vo.setUpdateCount(changed.size());
if (confirm && (!missing.isEmpty() || (force && !changed.isEmpty()))) {
teachingTaskWriteGuard.assertCourseTaskWritable(xydxqbh);
for (ClassCalendarEventDTO dto : missing) {
insertCell(semester, parseDay(dto.getJqsj()), dto.getCourseClass(), dto.getJqmc(),
flag(dto.getJc()), flag(dto.getKpk()), flag(dto.getBzxs()),
flag(dto.getZdpk()), flag(dto.getZk()), dto.getBz());
}
if (force) {
for (ClassCalendarEventDTO dto : changed) {
LocalDate day = parseDay(dto.getJqsj());
JQB existing = existingByKey.get(
cellKey(Integer.valueOf(day.format(COMPACT)), dto.getCourseClass()));
if (existing != null) {
applyCell(semester, existing, day, dto);
}
}
}
vo.setExecuted(true);
}
return vo;
}
/**
* 校历格转时间格 DTO(同步预览/写入的统一口径)。
*/
private ClassCalendarEventDTO schoolDto(LocalDate day, XQXLB school) {
ClassCalendarEventDTO dto = new ClassCalendarEventDTO();
dto.setJqsj(day.format(DAY));
dto.setCourseClass(school.getCourseClass());
dto.setJqmc(school.getJqmc());
dto.setJc(Boolean.TRUE.equals(school.getJc()));
dto.setKpk(Boolean.TRUE.equals(school.getKpk()));
dto.setBzxs(Boolean.TRUE.equals(school.getBzxs()));
dto.setZdpk(Boolean.TRUE.equals(school.getZdpk()));
dto.setZk(Boolean.TRUE.equals(school.getZk()));
dto.setBz(school.getBz());
return dto;
}
/**
* 班历已有格与校历口径是否不一致(force 覆盖判定;null 与空串视为相同)。
*/
private boolean differs(JQB row, XQXLB school) {
if (!norm(row.getJqmc()).equals(norm(school.getJqmc()))
|| !norm(row.getBz()).equals(norm(school.getBz()))) {
return true;
}
return flagDiffers(row.getJc(), school.getJc())
|| flagDiffers(row.getKpk(), school.getKpk())
|| flagDiffers(row.getBzxs(), school.getBzxs())
|| flagDiffers(row.getZdpk(), school.getZdpk())
|| flagDiffers(row.getZk(), school.getZk());
}
private boolean flagDiffers(Integer rowFlag, Boolean schoolFlag) {
return Integer.valueOf(1).equals(rowFlag) != Boolean.TRUE.equals(schoolFlag);
}
private String norm(String value) {
return value == null ? "" : value.trim();
}
/**
* 按 编号 / (学员队学期编号+日期+节次) 定位已有格。
*/
@@ -37,6 +37,11 @@ public class ClassPeriodServiceImpl implements ClassPeriodService {
.filter(s -> s != null && !s.isEmpty()).collect(Collectors.toList());
}
@Override
public List<JCSJB> slotRows() {
return loadSlots();
}
@Override
public Set<String> mainSlotLabels() {
List<JCSJB> slots = loadSlots();
@@ -269,7 +269,7 @@ public class ClassSemesterServiceImpl implements ClassSemesterService {
.and(w -> w.lt(com.roomroot.jwgl.entity.JQB::getJqsj, start)
.or().gt(com.roomroot.jwgl.entity.JQB::getJqsj, end)));
// 只补缺失格、不覆盖人工改动(与班历弹窗「同步校历」同一口径)
classEventCalendarService.resync(bh, true);
classEventCalendarService.resync(bh, true, false);
}
return count;
@@ -782,6 +782,14 @@ public class CourseRunningImportServiceImpl implements CourseRunningImportServic
throw new BusinessException(BasicManagementConstants.BAD_REQUEST, "该课次已提交,不允许修改,如需修改请先撤回");
}
// 步骤4.5:教学内容必填;教学方法缺省按「理论讲授」
if (dto.getJxnr() == null || dto.getJxnr().trim().isEmpty()) {
throw new BusinessException(BasicManagementConstants.BAD_REQUEST, "教学内容不能为空");
}
if (dto.getJxff() == null || dto.getJxff().trim().isEmpty()) {
dto.setJxff("理论讲授");
}
// 步骤5:将DTO中的填写内容设置到课次实体
lesson.setJxnr(dto.getJxnr()); // 教学内容
lesson.setJxyd(dto.getJxyd()); // 教学要点
@@ -5,9 +5,13 @@ import com.roomroot.common.utils.StringUtils;
import com.roomroot.common.utils.poi.ExcelUtil;
import com.roomroot.jwgl.dto.kcb.TimetableQuery;
import com.roomroot.jwgl.entity.KCB;
import com.roomroot.jwgl.entity.XYDQB;
import com.roomroot.jwgl.entity.XYXX;
import com.roomroot.jwgl.mapper.KCBMapper;
import com.roomroot.jwgl.mapper.KcbTimetableMapper;
import com.roomroot.jwgl.mapper.SystemInitializationMapper;
import com.roomroot.jwgl.mapper.XYDQBMapper;
import com.roomroot.jwgl.mapper.XYXXMapper;
import com.roomroot.jwgl.service.KCBService;
import com.roomroot.jwgl.unit.BusinessException;
import com.roomroot.jwgl.unit.PageQuery;
@@ -51,6 +55,12 @@ public class KCBServiceImpl implements KCBService {
@Resource
private JwglRoleHelper jwglRoleHelper;
@Resource
private XYXXMapper xyxxMapper;
@Resource
private XYDQBMapper xydqbMapper;
@Override
public void add(KCB kcb) {
if (kcb.getCjsj() == null) {
@@ -139,6 +149,24 @@ public class KCBServiceImpl implements KCBService {
util.exportExcel(response, list, "班次课程列表");
}
@Override
public List<DailyWeeklyTimetableVO> listLessons(TimetableQuery cond, String start, String end) {
TimetableQuery filter = prepareFilter(cond);
LocalDate s = StringUtils.isEmpty(start) ? null : parseViewDate(start);
LocalDate e = StringUtils.isEmpty(end) ? null : parseViewDate(end);
if (s == null && e == null) {
LocalDate monday = LocalDate.now().with(DayOfWeek.MONDAY);
s = monday;
e = monday.plusDays(6);
} else if (s == null) {
s = e;
} else if (e == null) {
e = s;
}
return kcbTimetableMapper.selectLessonTimetable(filter,
s.format(DATE_FORMAT), e.format(DATE_FORMAT));
}
private LocalDate parseViewDate(String rq) {
if (StringUtils.isEmpty(rq)) {
return LocalDate.now();
@@ -169,13 +197,36 @@ public class KCBServiceImpl implements KCBService {
if (jwglRoleHelper.isStudent()) {
filter.setXydbh(jwglRoleHelper.requireStudentTeamId());
filter.setXydmc(null);
filter.setXybh(null);
filter.setJybh(null);
filter.setJybhs(null);
} else {
if (StringUtils.isNotEmpty(filter.getXybh())) {
filter.setXydbh(resolveTeamByStudent(filter.getXybh().trim()));
filter.setXydmc(null);
}
if (jwglRoleHelper.isTeacher()) {
filter.setJybh(jwglRoleHelper.requireTeacherId());
filter.setJybhs(null);
}
if (jwglRoleHelper.isResearchOffice() && StringUtils.isEmpty(filter.getJysdh())) {
filter.setJysdh(jwglRoleHelper.requireResearchOfficeId());
}
if (StringUtils.isNotEmpty(filter.getXydbh())) {
filter.setXydbh(filter.getXydbh().trim());
}
if (StringUtils.isNotEmpty(filter.getXydmc())) {
filter.setXydmc(filter.getXydmc().trim());
}
if (StringUtils.isNotEmpty(filter.getJybh())) {
filter.setJybh(filter.getJybh().trim());
}
if (StringUtils.isNotEmpty(filter.getJsbh())) {
filter.setJsbh(filter.getJsbh().trim());
}
if (StringUtils.isNotEmpty(filter.getJysdh())) {
filter.setJysdh(filter.getJysdh().trim());
}
}
if (filter.getNd() != null) {
return filter;
@@ -212,4 +263,14 @@ public class KCBServiceImpl implements KCBService {
return new String[]{start.format(DATE_FORMAT), end.format(DATE_FORMAT)};
}
/** 学员编号 → 其当前所属学员队编号(查不到返回原值使结果为空而非报错) */
private String resolveTeamByStudent(String xybh) {
XYXX student = xyxxMapper.selectById(xybh);
if (student == null || StringUtils.isEmpty(student.getXydqbh())) {
return xybh;
}
XYDQB period = xydqbMapper.selectById(student.getXydqbh());
return period == null || StringUtils.isEmpty(period.getXydbh()) ? xybh : period.getXydbh();
}
}
@@ -0,0 +1,332 @@
package com.roomroot.jwgl.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.roomroot.common.exception.ServiceException;
import com.roomroot.common.utils.StringUtils;
import com.roomroot.jwgl.dto.kcb.TimetableGridQuery;
import com.roomroot.jwgl.dto.kcb.TimetableQuery;
import com.roomroot.jwgl.entity.JXRW;
import com.roomroot.jwgl.entity.JYB;
import com.roomroot.jwgl.entity.JYSX;
import com.roomroot.jwgl.entity.KB;
import com.roomroot.jwgl.entity.XYDB;
import com.roomroot.jwgl.entity.XYDNDXQJBXXB;
import com.roomroot.jwgl.entity.XYDRWB;
import com.roomroot.jwgl.entity.XYXX;
import com.roomroot.jwgl.entity.JSB;
import com.roomroot.jwgl.mapper.ClassRoomMapper;
import com.roomroot.jwgl.mapper.ClassSemesterMapper;
import com.roomroot.jwgl.mapper.JYBMapper;
import com.roomroot.jwgl.mapper.JYSXMapper;
import com.roomroot.jwgl.mapper.KBMapper;
import com.roomroot.jwgl.mapper.KcbTimetableMapper;
import com.roomroot.jwgl.mapper.StudentTeamTaskMapper;
import com.roomroot.jwgl.mapper.TeachingTaskMapper;
import com.roomroot.jwgl.mapper.XYDBMapper;
import com.roomroot.jwgl.mapper.XYXXMapper;
import com.roomroot.jwgl.service.MyBusinessService;
import com.roomroot.jwgl.service.TimetableGridService;
import com.roomroot.jwgl.utils.JwglRoleHelper;
import com.roomroot.jwgl.vo.kcb.ClassTimetableVO;
import com.roomroot.jwgl.vo.kcb.DailyWeeklyTimetableVO;
import com.roomroot.jwgl.vo.kcb.TimetableGridVO;
import com.roomroot.jwgl.vo.mybusiness.MyCourseTaskVO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST;
import static com.roomroot.jwgl.utils.BasicManagementConstants.FORBIDDEN;
/**
* 「我的教学」角色视图实现(阶段 C)。
*/
@Service
public class MyBusinessServiceImpl implements MyBusinessService {
private static final DateTimeFormatter DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Resource
private JwglRoleHelper roleHelper;
@Resource
private KcbTimetableMapper kcbTimetableMapper;
@Resource
private StudentTeamTaskMapper taskMapper;
@Resource
private ClassSemesterMapper classSemesterMapper;
@Resource
private TeachingTaskMapper jxrwMapper;
@Resource
private KBMapper kbMapper;
@Resource
private XYDBMapper xydbMapper;
@Resource
private JYBMapper jybMapper;
@Resource
private JYSXMapper jysxMapper;
@Resource
private ClassRoomMapper classRoomMapper;
@Resource
private XYXXMapper xyxxMapper;
@Resource
private TimetableGridService timetableGridService;
// ==================== 教员 ====================
@Override
public List<MyCourseTaskVO> teacherCourses(Integer nd, Integer xqdc) {
String self = roleHelper.requireTeacherId();
List<XYDRWB> rows = taskMapper.selectList(new LambdaQueryWrapper<XYDRWB>()
.eq(XYDRWB::getDelFlag, 0)
.and(w -> w.eq(XYDRWB::getJybh, self).or().eq(XYDRWB::getJysjhjybh, self))
.eq(nd != null, XYDRWB::getNd, nd)
.eq(xqdc != null, XYDRWB::getXqdc, xqdc)
.orderByAsc(XYDRWB::getKbh).orderByAsc(XYDRWB::getKcxh));
return decorate(rows);
}
private List<MyCourseTaskVO> decorate(List<XYDRWB> rows) {
Set<String> semBhs = rows.stream().map(XYDRWB::getXydxqbh).filter(Objects::nonNull).collect(Collectors.toSet());
Map<String, XYDNDXQJBXXB> sems = semBhs.isEmpty() ? Map.of()
: classSemesterMapper.selectBatchIds(semBhs).stream()
.collect(Collectors.toMap(XYDNDXQJBXXB::getBh, s -> s, (a, b) -> a));
Set<String> taskBhs = sems.values().stream().map(XYDNDXQJBXXB::getJxrwbh)
.filter(StringUtils::isNotEmpty).collect(Collectors.toSet());
Map<String, String> taskNames = taskBhs.isEmpty() ? Map.of()
: jxrwMapper.selectBatchIds(taskBhs).stream()
.collect(Collectors.toMap(JXRW::getBh, t -> t.getRwmc() == null ? "" : t.getRwmc(), (a, b) -> a));
Set<String> kbhs = rows.stream().map(XYDRWB::getKbh).filter(Objects::nonNull).collect(Collectors.toSet());
Map<String, KB> kbs = kbhs.isEmpty() ? Map.of()
: kbMapper.selectBatchIds(kbhs).stream()
.collect(Collectors.toMap(KB::getKbh, k -> k, (a, b) -> a));
Set<String> teamBhs = rows.stream().map(XYDRWB::getXydbh).filter(Objects::nonNull).collect(Collectors.toSet());
Map<String, String> teamNames = teamBhs.isEmpty() ? Map.of()
: xydbMapper.selectList(new LambdaQueryWrapper<XYDB>().in(XYDB::getXydbh, teamBhs)).stream()
.collect(Collectors.toMap(XYDB::getXydbh, t -> t.getXydmc() == null ? t.getXydbh() : t.getXydmc(), (a, b) -> a));
Set<String> jybhSet = new java.util.HashSet<>();
rows.forEach(r -> { if (r.getJybh() != null) jybhSet.add(r.getJybh()); if (r.getJysjhjybh() != null) jybhSet.add(r.getJysjhjybh()); });
Map<String, String> teacherNames = jybhSet.isEmpty() ? Map.of()
: jybMapper.selectList(new LambdaQueryWrapper<JYB>().in(JYB::getJybh, jybhSet)).stream()
.collect(Collectors.toMap(JYB::getJybh, t -> t.getJyxm() == null ? t.getJybh() : t.getJyxm(), (a, b) -> a));
Map<String, Integer> directorOf = jybhSet.isEmpty() ? Map.of()
: jysxMapper.selectBatchIds(jybhSet).stream()
.collect(Collectors.toMap(JYSX::getJybh, t -> t.getZr() == null ? 0 : t.getZr(), (a, b) -> a));
Set<String> jsbhSet = rows.stream().map(XYDRWB::getJsbh).filter(Objects::nonNull).collect(Collectors.toSet());
Map<String, String> roomNames = jsbhSet.isEmpty() ? Map.of()
: classRoomMapper.selectList(new LambdaQueryWrapper<JSB>()
.in(JSB::getJsbh, jsbhSet).eq(JSB::getDelFlag, 0)).stream()
.collect(Collectors.toMap(JSB::getJsbh, j -> j.getJsmc() == null ? j.getJsbh() : j.getJsmc(), (a, b) -> a));
List<MyCourseTaskVO> out = new ArrayList<>();
for (XYDRWB r : rows) {
MyCourseTaskVO vo = new MyCourseTaskVO();
vo.setBh(r.getBh());
vo.setXydxqbh(r.getXydxqbh());
vo.setNd(r.getNd());
vo.setXqdc(r.getXqdc());
vo.setKbh(r.getKbh());
KB kb = kbs.get(r.getKbh());
vo.setKcmc(StringUtils.isNotEmpty(r.getJc()) ? r.getJc()
: (kb != null && kb.getKmc() != null ? kb.getKmc() : r.getKbh()));
vo.setKlx(r.getKlx());
vo.setXs(r.getXs());
vo.setZks(r.getZks());
vo.setXydbh(r.getXydbh());
vo.setXydmc(teamNames.get(r.getXydbh()));
vo.setJybh(r.getJybh());
vo.setJyxm(teacherNames.get(r.getJybh()));
vo.setZr(directorOf.get(r.getJybh()));
vo.setJysjhjybh(r.getJysjhjybh());
vo.setJysjhjyxm(teacherNames.get(r.getJysjhjybh()));
vo.setJysdh(r.getJysdh());
vo.setJsmc(roomNames.get(r.getJsbh()));
vo.setJsbh(r.getJsbh());
vo.setKcxh(r.getKcxh());
vo.setBz2(r.getBz2());
XYDNDXQJBXXB sem = sems.get(r.getXydxqbh());
if (sem != null) {
vo.setJxrwbh(sem.getJxrwbh());
vo.setRwmc(taskNames.get(sem.getJxrwbh()));
}
out.add(vo);
}
return out;
}
@Override
public List<DailyWeeklyTimetableVO> teacherLessons(String start, String end, Integer nd) {
String self = roleHelper.requireTeacherId();
String[] range = weekRange(start, end);
TimetableQuery cond = new TimetableQuery();
cond.setJybh(self);
cond.setNd(nd);
return kcbTimetableMapper.selectLessonTimetable(cond, range[0], range[1]);
}
@Override
public TimetableGridVO teacherGrid(String mbbh, String start, String end, Integer nd) {
TimetableGridQuery q = new TimetableGridQuery();
q.setDim("teacher");
q.setId(roleHelper.requireTeacherId());
q.setMbbh(mbbh);
q.setStart(start);
q.setEnd(end);
q.setNd(nd);
return timetableGridService.grid(q);
}
// ==================== 教研室 ====================
@Override
public List<DailyWeeklyTimetableVO> officeToday(String rq, Integer offset) {
String office = roleHelper.requireResearchOfficeId();
LocalDate base;
if (StringUtils.isEmpty(rq)) {
base = LocalDate.now();
} else {
try {
base = LocalDate.parse(rq.trim(), DAY);
} catch (Exception e) {
throw new ServiceException("日期格式应为 yyyy-MM-dd", BAD_REQUEST);
}
}
LocalDate day = base.plusDays(offset == null ? 0 : offset);
List<String> teachers = officeTeacherIds(office);
if (teachers.isEmpty()) {
return new ArrayList<>();
}
TimetableQuery cond = new TimetableQuery();
cond.setJybhs(teachers);
String d = day.format(DAY);
return kcbTimetableMapper.selectLessonTimetable(cond, d, d);
}
@Override
public List<ClassTimetableVO> officeCourses(Integer nd, Integer xqdc) {
String office = roleHelper.requireResearchOfficeId();
TimetableQuery cond = new TimetableQuery();
cond.setJysdh(office);
cond.setNd(nd);
cond.setXqdc(xqdc);
return kcbTimetableMapper.selectClassTimetableList(cond);
}
@Override
public List<Map<String, Object>> officeTeachers() {
String office = roleHelper.requireResearchOfficeId();
List<Map<String, Object>> out = new ArrayList<>();
List<JYB> teachers = jybMapper.selectList(new LambdaQueryWrapper<JYB>()
.eq(JYB::getJysdh, office)
.and(w -> w.isNull(JYB::getLzzt).or().ne(JYB::getLzzt, 1)));
Set<String> ids = teachers.stream().map(JYB::getJybh).collect(Collectors.toSet());
Map<String, Integer> directorOf = ids.isEmpty() ? Map.of()
: jysxMapper.selectBatchIds(ids).stream()
.collect(Collectors.toMap(JYSX::getJybh, t -> t.getZr() == null ? 0 : t.getZr(), (a, b) -> a));
for (JYB t : teachers) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("jybh", t.getJybh());
m.put("jyxm", t.getJyxm());
m.put("zr", directorOf.getOrDefault(t.getJybh(), 0));
out.add(m);
}
return out;
}
private List<String> officeTeacherIds(String office) {
return jybMapper.selectList(new LambdaQueryWrapper<JYB>()
.eq(JYB::getJysdh, office)
.and(w -> w.isNull(JYB::getLzzt).or().ne(JYB::getLzzt, 1))).stream()
.map(JYB::getJybh).filter(Objects::nonNull).collect(Collectors.toList());
}
// ==================== 学员 ====================
@Override
public List<ClassTimetableVO> studentCourses(Integer nd, Integer xqdc) {
String team = roleHelper.requireStudentTeamId();
TimetableQuery cond = new TimetableQuery();
cond.setXydbh(team);
cond.setNd(nd);
cond.setXqdc(xqdc);
return kcbTimetableMapper.selectClassTimetableList(cond);
}
@Override
public List<DailyWeeklyTimetableVO> studentLessons(String start, String end) {
String team = roleHelper.requireStudentTeamId();
String[] range = weekRange(start, end);
TimetableQuery cond = new TimetableQuery();
cond.setXydbh(team);
return kcbTimetableMapper.selectLessonTimetable(cond, range[0], range[1]);
}
@Override
public TimetableGridVO studentGrid(String mbbh, String start, String end, Integer nd) {
TimetableGridQuery q = new TimetableGridQuery();
q.setDim("team");
q.setId(roleHelper.requireStudentTeamId());
q.setMbbh(mbbh);
q.setStart(start);
q.setEnd(end);
q.setNd(nd);
return timetableGridService.grid(q);
}
@Override
public List<Map<String, Object>> teamCadres() {
String studentId = roleHelper.requireStudentId();
XYXX me = xyxxMapper.selectById(studentId);
if (me == null || StringUtils.isEmpty(me.getXydqbh())) {
throw new ServiceException("当前学员未分班,无法查看学员队信息", FORBIDDEN);
}
List<XYXX> cadres = xyxxMapper.selectList(new LambdaQueryWrapper<XYXX>()
.eq(XYXX::getXydqbh, me.getXydqbh())
.isNotNull(XYXX::getGgrz)
.ne(XYXX::getGgrz, ""));
List<Map<String, Object>> out = new ArrayList<>();
for (XYXX s : cadres) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("xh", s.getXh());
m.put("xm", s.getXm());
m.put("ggrz", s.getGgrz());
out.add(m);
}
return out;
}
/** start/end 缺省时取本周(周一~周日) */
private String[] weekRange(String start, String end) {
LocalDate s = StringUtils.isEmpty(start) ? null : LocalDate.parse(start.trim(), DAY);
LocalDate e = StringUtils.isEmpty(end) ? null : LocalDate.parse(end.trim(), DAY);
if (s == null && e == null) {
LocalDate monday = LocalDate.now().with(DayOfWeek.MONDAY);
s = monday;
e = monday.plusDays(6);
} else if (s == null) {
s = e;
} else if (e == null) {
e = s;
}
return new String[]{s.format(DAY), e.format(DAY)};
}
}
@@ -6,6 +6,7 @@ import com.roomroot.common.utils.SecurityUtils;
import com.roomroot.jwgl.dto.scheduling.SchedulingArrangeRequest;
import com.roomroot.jwgl.dto.scheduling.SchedulingCellOpRequest;
import com.roomroot.jwgl.dto.scheduling.SchedulingCourseEditRequest;
import com.roomroot.jwgl.dto.scheduling.SchedulingMoveRequest;
import com.roomroot.jwgl.entity.BZLB;
import com.roomroot.jwgl.entity.BZTGMX;
import com.roomroot.jwgl.entity.JCSJB;
@@ -55,9 +56,18 @@ import com.roomroot.jwgl.vo.scheduling.SchedulingViewVO;
import com.roomroot.common.core.domain.entity.SysUser;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.ByteArrayOutputStream;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -712,6 +722,166 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
return result;
}
// ==================== 拖拽移动(E4 ====================
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> moveLesson(SchedulingMoveRequest request) {
if (request == null || request.getSskcbh() == null
|| request.getFrom() == null || request.getFrom().getRq() == null || request.getFrom().getJc() == null
|| request.getTo() == null || request.getTo().getRq() == null || request.getTo().getJc() == null) {
throw new ServiceException("请指定课程与源/目标节次", BAD_REQUEST);
}
SSKC course = requireCourse(request.getSskcbh());
Ctx ctx = loadCtxForCourse(course);
if (!canEdit(course, ctx)) {
throw new ServiceException("没有该课程的排课权限", FORBIDDEN);
}
String fromKey = request.getFrom().getRq() + "#" + request.getFrom().getJc();
String toKey = request.getTo().getRq() + "#" + request.getTo().getJc();
if (fromKey.equals(toKey)) {
Map<String, Object> same = new HashMap<>();
same.put("moved", 0);
return same;
}
// 找源格课次
SSKCB lesson = null;
List<SSKCB> courseLessons = sskcbMapper.selectList(new LambdaQueryWrapper<SSKCB>()
.eq(SSKCB::getSskcbh, course.getBh()));
for (SSKCB l : courseLessons) {
if (l.getRq() == null || l.getJc() == null) continue;
if (fromKey.equals(l.getRq().toLocalDate().toString() + "#" + l.getJc())) {
lesson = l;
break;
}
}
if (lesson == null) {
throw new ServiceException("源节次没有该课程的课次", BAD_REQUEST);
}
if (lesson.getJhsjap() != null && lesson.getJhsjap() == 1) {
throw new ServiceException(request.getFrom().getRq() + "" + request.getFrom().getJc()
+ "节已提交实施计划,请走调课申请", BAD_REQUEST);
}
// 课次关联资源
List<String> lessonRooms = sskcbjsMapper.selectList(new LambdaQueryWrapper<SSKCBJS>()
.eq(SSKCBJS::getSskcbbh, lesson.getBh())).stream()
.map(SSKCBJS::getJsbh).filter(s -> s != null && !s.isEmpty()).distinct().toList();
List<String> lessonTeachers = sskcbfzjyMapper.selectList(new LambdaQueryWrapper<SSKCBFZJY>()
.eq(SSKCBFZJY::getSskcbbh, lesson.getBh())).stream()
.map(SSKCBFZJY::getFzjybh).filter(s -> s != null && !s.isEmpty()).distinct().toList();
List<String> lessonTeams = sskcbxydMapper.selectList(new LambdaQueryWrapper<SSKCBXYD>()
.eq(SSKCBXYD::getSskcbbh, lesson.getBh())).stream()
.map(SSKCBXYD::getXydbh).filter(s -> s != null && !s.isEmpty()).distinct().toList();
// 目标格硬冲突(与 arrange 同口径)
Set<String> schoolHard = calendarHardKeys(ctx, true);
Set<String> classHard = calendarHardKeys(ctx, false);
if (schoolHard.contains(toKey)) {
throw new ServiceException("目标节次校历不可排课", BAD_REQUEST);
}
if (classHard.contains(toKey)) {
throw new ServiceException("目标节次班历不可排课", BAD_REQUEST);
}
Map<String, String> roomBlocked = new HashMap<>();
for (JXCDL row : classRoomCalendarMapper.selectList(new LambdaQueryWrapper<JXCDL>()
.eq(JXCDL::getKpk, false).eq(JXCDL::getDelFlag, 0))) {
if (row.getRq() != null && row.getJsbh() != null) {
roomBlocked.put(row.getRq().toLocalDate().toString() + "#" + row.getJc() + "#" + row.getJsbh(), "场地历不可用");
}
}
for (String room : lessonRooms) {
if (roomBlocked.containsKey(toKey + "#" + room)) {
throw new ServiceException("目标节次场地历不可用(教室 " + resolveRoomName(room) + "", BAD_REQUEST);
}
}
Map<String, String> teacherBlocked = new HashMap<>();
for (JYL row : jylMapper.selectList(new LambdaQueryWrapper<JYL>().eq(JYL::getKpk, 0))) {
if (row.getRq() != null && row.getJybh() != null) {
teacherBlocked.put(row.getRq().toLocalDate().toString() + "#" + row.getJc() + "#" + row.getJybh(), "教员历不可用");
}
}
for (String jybh : lessonTeachers) {
if (teacherBlocked.containsKey(toKey + "#" + jybh)) {
throw new ServiceException("目标节次教员历不可用(" + resolveTeacherName(jybh) + "", BAD_REQUEST);
}
}
// 目标格双占:其它课程的课次与本课次共享学员队/教室/教员
List<SSKCB> semesterLessons = ctx.lessons;
List<String> occupantIds = semesterLessons.stream()
.filter(l -> l.getRq() != null && l.getJc() != null
&& toKey.equals(l.getRq().toLocalDate().toString() + "#" + l.getJc())
&& !course.getBh().equals(l.getSskcbh()))
.map(SSKCB::getBh).toList();
if (!occupantIds.isEmpty()) {
Set<String> occTeams = sskcbxydMapper.selectList(new LambdaQueryWrapper<SSKCBXYD>()
.in(SSKCBXYD::getSskcbbh, occupantIds)).stream()
.map(SSKCBXYD::getXydbh).collect(Collectors.toSet());
Set<String> occRooms = sskcbjsMapper.selectList(new LambdaQueryWrapper<SSKCBJS>()
.in(SSKCBJS::getSskcbbh, occupantIds)).stream()
.map(SSKCBJS::getJsbh).collect(Collectors.toSet());
Set<String> occTeachers = sskcbfzjyMapper.selectList(new LambdaQueryWrapper<SSKCBFZJY>()
.in(SSKCBFZJY::getSskcbbh, occupantIds)).stream()
.map(SSKCBFZJY::getFzjybh).collect(Collectors.toSet());
if (lessonTeams.stream().anyMatch(occTeams::contains)) {
throw new ServiceException("目标节次班次已有其它课程", BAD_REQUEST);
}
if (lessonRooms.stream().anyMatch(occRooms::contains)) {
throw new ServiceException("目标节次教室已被占用", BAD_REQUEST);
}
if (lessonTeachers.stream().anyMatch(occTeachers::contains)) {
throw new ServiceException("目标节次教员已有课", BAD_REQUEST);
}
}
// 软提示:配当周次不符照常移动
String warning = null;
Set<Integer> allocatedWeeks = allocatedWeeks(ctx, course);
LocalDate toDate = LocalDate.parse(request.getTo().getRq());
if (allocatedWeeks != null && !allocatedWeeks.isEmpty()) {
int weekNo = weekNoOf(ctx, toDate);
if (!allocatedWeeks.contains(weekNo)) {
warning = "配当周次不符(第 " + weekNo + " 周不在该课配当范围)";
}
}
// 平移主表 + 子表日期节次
LocalDateTime now = LocalDateTime.now();
lesson.setRq(toDate.atStartOfDay());
lesson.setJc(request.getTo().getJc());
lesson.setBdsj(now);
sskcbMapper.updateById(lesson);
for (SSKCBXYD l : sskcbxydMapper.selectList(new LambdaQueryWrapper<SSKCBXYD>()
.eq(SSKCBXYD::getSskcbbh, lesson.getBh()))) {
l.setRq(lesson.getRq());
l.setJc2(request.getTo().getJc());
sskcbxydMapper.updateById(l);
}
for (SSKCBJS l : sskcbjsMapper.selectList(new LambdaQueryWrapper<SSKCBJS>()
.eq(SSKCBJS::getSskcbbh, lesson.getBh()))) {
l.setRq(lesson.getRq());
l.setJc(request.getTo().getJc());
sskcbjsMapper.updateById(l);
}
for (SSKCBFZJY l : sskcbfzjyMapper.selectList(new LambdaQueryWrapper<SSKCBFZJY>()
.eq(SSKCBFZJY::getSskcbbh, lesson.getBh()))) {
l.setRq(lesson.getRq());
l.setJc(request.getTo().getJc());
sskcbfzjyMapper.updateById(l);
}
for (KCBBZMX l : kcbbzMapper.selectList(new LambdaQueryWrapper<KCBBZMX>()
.eq(KCBBZMX::getKcbbh, lesson.getBh()))) {
l.setRq(lesson.getRq());
l.setJc(request.getTo().getJc());
kcbbzMapper.updateById(l);
}
logOperation(course, "移动", "课次 " + fromKey + "" + toKey, null);
Map<String, Object> result = new HashMap<>();
result.put("moved", 1);
if (warning != null) result.put("warning", warning);
return result;
}
// ==================== 删除 ====================
@Override
@@ -815,6 +985,85 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService {
}).collect(Collectors.toList());
}
// ==================== 导出(E4 ====================
@Override
public byte[] exportView(String xydxqbh) {
SchedulingViewVO vo = view(xydxqbh);
try (Workbook wb = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
CellStyle head = wb.createCellStyle();
Font hf = wb.createFont();
hf.setBold(true);
head.setFont(hf);
head.setAlignment(HorizontalAlignment.CENTER);
head.setVerticalAlignment(VerticalAlignment.CENTER);
CellStyle center = wb.createCellStyle();
center.setAlignment(HorizontalAlignment.CENTER);
center.setVerticalAlignment(VerticalAlignment.CENTER);
center.setWrapText(true);
CellStyle title = wb.createCellStyle();
Font tf = wb.createFont();
tf.setBold(true);
tf.setFontHeightInPoints((short) 14);
title.setFont(tf);
title.setAlignment(HorizontalAlignment.CENTER);
String[] heads = {"节次", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"};
for (SchedulingViewVO.Week week : vo.getWeeks()) {
org.apache.poi.ss.usermodel.Sheet sheet = wb.createSheet("" + week.getWeekNo() + "");
int r = 0;
org.apache.poi.ss.usermodel.Row titleRow = sheet.createRow(r++);
org.apache.poi.ss.usermodel.Cell tc = titleRow.createCell(0);
tc.setCellValue((vo.getXydmc() == null ? "" : vo.getXydmc())
+ " 排课窗口(" + week.getStartDate() + " ~ " + week.getEndDate() + "");
tc.setCellStyle(title);
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 7));
org.apache.poi.ss.usermodel.Row headRow = sheet.createRow(r++);
for (int i = 0; i < heads.length; i++) {
org.apache.poi.ss.usermodel.Cell c = headRow.createCell(i);
c.setCellValue(heads[i]);
c.setCellStyle(head);
}
for (SchedulingViewVO.Period p : vo.getPeriods()) {
org.apache.poi.ss.usermodel.Row row = sheet.createRow(r++);
org.apache.poi.ss.usermodel.Cell pc = row.createCell(0);
pc.setCellValue(p.getLabel() != null ? p.getLabel() : String.valueOf(p.getJc()));
pc.setCellStyle(head);
for (SchedulingViewVO.Day day : week.getDays()) {
org.apache.poi.ss.usermodel.Cell c = row.createCell(day.getWeekday());
SchedulingViewVO.Cell cell = day.getCells().stream()
.filter(x -> Objects.equals(x.getJc(), p.getJc()))
.findFirst().orElse(null);
if (cell != null) {
if (cell.getLessons() != null && !cell.getLessons().isEmpty()) {
c.setCellValue(cell.getLessons().stream()
.map(l -> {
StringBuilder sb = new StringBuilder();
sb.append(l.getKcmc() == null ? "" : l.getKcmc());
if (l.getJyxm() != null) sb.append("\n").append(l.getJyxm());
if (l.getJsmc() != null) sb.append("\n").append(l.getJsmc());
return sb.toString();
}).collect(Collectors.joining("\n----\n")));
} else if (Boolean.TRUE.equals(cell.getUnavailable())) {
c.setCellValue(cell.getReason() == null ? "不可排" : cell.getReason());
}
}
c.setCellStyle(center);
}
}
sheet.setColumnWidth(0, 10 * 256);
for (int i = 1; i <= 7; i++) {
sheet.setColumnWidth(i, 24 * 256);
}
}
wb.write(out);
return out.toByteArray();
} catch (Exception e) {
throw new ServiceException("导出排课窗失败:" + e.getMessage(), BAD_REQUEST);
}
}
/** 操作人显示名:按登录名取用户昵称,取不到则回显登录名 */
private String resolveOperatorName(String username) {
if (username == null || username.isEmpty()) return "-";
@@ -0,0 +1,60 @@
package com.roomroot.jwgl.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.roomroot.jwgl.entity.DBVersion;
import com.roomroot.jwgl.mapper.DBVersionMapper;
import com.roomroot.jwgl.service.SystemSettingsService;
import com.roomroot.jwgl.utils.UuidUtil;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 教务系统参数实现:DBVersion 单行 get-or-create,更新仅写非空字段。
*/
@Service
public class SystemSettingsServiceImpl implements SystemSettingsService {
@Resource
private DBVersionMapper dbVersionMapper;
@Override
public DBVersion get() {
DBVersion row = dbVersionMapper.selectOne(
new LambdaQueryWrapper<DBVersion>().last("LIMIT 1"));
return row == null ? new DBVersion() : row;
}
@Transactional(rollbackFor = Exception.class)
@Override
public DBVersion update(DBVersion settings) {
DBVersion row = dbVersionMapper.selectOne(
new LambdaQueryWrapper<DBVersion>().last("LIMIT 1"));
if (row == null) {
row = new DBVersion();
row.setXYZKQJMBWJBH(UuidUtil.getUUID());
apply(row, settings);
dbVersionMapper.insert(row);
return row;
}
apply(row, settings);
dbVersionMapper.updateById(row);
return row;
}
/** 只写非空字段,避免前端局部提交清空其它参数 */
private void apply(DBVersion row, DBVersion src) {
if (src.getXS78J() != null) {
row.setXS78J(src.getXS78J());
}
if (src.getXSWS() != null) {
row.setXSWS(src.getXSWS());
}
if (src.getXSYJ() != null) {
row.setXSYJ(src.getXSYJ());
}
if (src.getJXYDMC() != null) {
row.setJXYDMC(src.getJXYDMC());
}
}
}
@@ -2,22 +2,30 @@ package com.roomroot.jwgl.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.roomroot.common.exception.ServiceException;
import com.roomroot.jwgl.dto.taskbook.TaskBookBatchFillRequest;
import com.roomroot.jwgl.dto.taskbook.TaskBookBatchRoomRequest;
import com.roomroot.jwgl.dto.taskbook.TaskBookBatchTeacherRequest;
import com.roomroot.jwgl.dto.taskbook.TaskBookBhListRequest;
import com.roomroot.jwgl.dto.taskbook.TaskBookFillRequest;
import com.roomroot.jwgl.dto.taskbook.TaskBookRoomRequest;
import com.roomroot.jwgl.dto.taskbook.TaskBookTeacherRequest;
import com.roomroot.jwgl.entity.JYB;
import com.roomroot.jwgl.entity.JXRW;
import com.roomroot.jwgl.entity.JYSB;
import com.roomroot.jwgl.entity.JYSRWS;
import com.roomroot.jwgl.entity.SSKCXYD;
import com.roomroot.jwgl.entity.XYDNDXQJBXXB;
import com.roomroot.jwgl.entity.XYDRWB;
import com.roomroot.jwgl.mapper.ClassSemesterMapper;
import com.roomroot.jwgl.mapper.JYBMapper;
import com.roomroot.jwgl.mapper.JYSBMapper;
import com.roomroot.jwgl.mapper.OfficeTaskBookMapper;
import com.roomroot.jwgl.mapper.SSKCXYDMapper;
import com.roomroot.jwgl.mapper.StudentTeamTaskMapper;
import com.roomroot.jwgl.mapper.TeachingTaskMapper;
import com.roomroot.jwgl.service.TaskBookFillService;
import com.roomroot.jwgl.service.TeachingTaskWriteGuard;
import com.roomroot.jwgl.utils.JwglRoleHelper;
import com.roomroot.jwgl.utils.SemesterCodeUtil;
import com.roomroot.jwgl.utils.TeachingTaskStatus;
import com.roomroot.jwgl.vo.taskbook.TaskBookRowVO;
@@ -36,6 +44,7 @@ import java.util.Set;
import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST;
import static com.roomroot.jwgl.utils.BasicManagementConstants.CONFLICT;
import static com.roomroot.jwgl.utils.BasicManagementConstants.FORBIDDEN;
import static com.roomroot.jwgl.utils.BasicManagementConstants.NOT_FOUND;
/**
@@ -62,10 +71,24 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
@Resource
private SSKCXYDMapper sskcxydMapper;
@Resource
private OfficeTaskBookMapper officeTaskBookMapper;
@Resource
private JYSBMapper jysbMapper;
@Resource
private JwglRoleHelper roleHelper;
// ==================== 列表 ====================
@Override
public List<TaskBookRowVO> list(String jxrwbh) {
return list(jxrwbh, null);
}
@Override
public List<TaskBookRowVO> list(String jxrwbh, String jybh) {
if (jxrwbh == null || jxrwbh.isEmpty()) {
throw new ServiceException("请指定教学任务", BAD_REQUEST);
}
@@ -74,6 +97,27 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
throw new ServiceException("教学任务不存在:" + jxrwbh, NOT_FOUND);
}
List<TaskBookRowVO> rows = jxrwMapper.selectTaskBookRows(jxrwbh);
// 身份范围:学员无权看任务书;教员只看本人相关行;教研室只看本室行
if (roleHelper.isStudent()) {
throw new ServiceException("学员无权查看教学任务书", FORBIDDEN);
}
if (roleHelper.isTeacher()) {
String self = roleHelper.requireTeacherId();
rows = rows.stream()
.filter(r -> self.equals(r.getJybh()) || self.equals(r.getJysjhjybh()))
.toList();
} else if (roleHelper.isResearchOffice()) {
String office = roleHelper.requireResearchOfficeId();
rows = rows.stream()
.filter(r -> office.equals(r.getJysdh()))
.toList();
}
if (jybh != null && !jybh.isEmpty()) {
String t = jybh.trim();
rows = rows.stream()
.filter(r -> t.equals(r.getJybh()) || t.equals(r.getJysjhjybh()))
.toList();
}
boolean frozen = isFrozen(jxrw);
rows.forEach(r -> r.setFrozen(frozen));
return rows;
@@ -91,6 +135,7 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
public Integer merge(TaskBookBhListRequest request) {
List<XYDRWB> rows = loadRows(request);
assertAllFillable(rows);
assertOfficeEditable(rows);
assertSameXq(rows);
assertMergeCompatible(rows);
int groupNo = nextGroupNo();
@@ -163,6 +208,7 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
public List<Map<String, Object>> mergeByPreset(TaskBookBhListRequest request) {
List<XYDRWB> rows = loadRows(request);
assertAllFillable(rows);
assertOfficeEditable(rows);
assertSameXq(rows);
// 行按 课程科目(kbh) → 预设编班号(ysbbh,空则各班次学期独立) 分组
Map<String, List<XYDRWB>> groups = new LinkedHashMap<>();
@@ -198,6 +244,7 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
public int split(TaskBookBhListRequest request) {
List<XYDRWB> rows = loadRows(request);
assertAllFillable(rows);
assertOfficeEditable(rows);
// 阶段 5 已落地:行所属班次学期若已发布到运行课表,合班组已被固化成运行课程,必须先撤回。
assertNotPublishedToRunning(rows);
LocalDateTime now = LocalDateTime.now();
@@ -221,9 +268,13 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
if (request == null || request.getBh() == null || request.getBh().isEmpty()) {
throw new ServiceException("请指定课程任务", BAD_REQUEST);
}
XYDRWB row = requireRow(request.getBh());
return setTeacherForRow(requireRow(request.getBh()), request.getMode(), request.getJybh());
}
private int setTeacherForRow(XYDRWB row, String reqMode, String reqJybh) {
assertFillable(row);
String mode = request.getMode() == null ? "" : request.getMode();
assertCanEdit(row);
String mode = reqMode == null ? "" : reqMode;
String jybh;
if ("plan".equalsIgnoreCase(mode)) {
// 应用教研室计划教员:jybh ← 该行 jysjhjybh
@@ -232,7 +283,7 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
throw new ServiceException("该行没有教研室计划教员,请先在填报字段中指定", BAD_REQUEST);
}
} else if ("unit".equalsIgnoreCase(mode) || "academy".equalsIgnoreCase(mode)) {
jybh = request.getJybh();
jybh = reqJybh;
if (jybh == null || jybh.isEmpty()) {
throw new ServiceException("请选择教员", BAD_REQUEST);
}
@@ -254,23 +305,40 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
return updateGroupField(row, "jybh", jybh);
}
@Transactional(rollbackFor = Exception.class)
@Override
public int batchSetTeacher(TaskBookBatchTeacherRequest request) {
if (request == null || request.getBhList() == null || request.getBhList().isEmpty()) {
throw new ServiceException("请先选择课程任务行", BAD_REQUEST);
}
int count = 0;
for (String bh : request.getBhList()) {
count += setTeacherForRow(requireRow(bh), request.getMode(), request.getJybh());
}
return count;
}
@Transactional(rollbackFor = Exception.class)
@Override
public int setRoom(TaskBookRoomRequest request) {
if (request == null || request.getBh() == null || request.getBh().isEmpty()) {
throw new ServiceException("请指定课程任务", BAD_REQUEST);
}
XYDRWB row = requireRow(request.getBh());
return setRoomForRow(requireRow(request.getBh()), request.getJsbh(), request.getUseSpecial());
}
private int setRoomForRow(XYDRWB row, String reqJsbh, Boolean useSpecial) {
assertFillable(row);
assertCanEdit(row);
String jsbh;
if (Boolean.TRUE.equals(request.getUseSpecial())) {
if (Boolean.TRUE.equals(useSpecial)) {
XYDNDXQJBXXB semester = requireSemester(row.getXydxqbh());
jsbh = semester.getZyjsbh();
if (jsbh == null || jsbh.isEmpty()) {
throw new ServiceException("该班次学期没有专用教室,请改选其它教室", BAD_REQUEST);
}
} else {
jsbh = request.getJsbh();
jsbh = reqJsbh;
if (jsbh == null || jsbh.isEmpty()) {
throw new ServiceException("请选择教室,或选择使用班次专用教室", BAD_REQUEST);
}
@@ -278,6 +346,98 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
return updateGroupField(row, "jsbh", jsbh);
}
@Transactional(rollbackFor = Exception.class)
@Override
public int batchSetRoom(TaskBookBatchRoomRequest request) {
if (request == null || request.getBhList() == null || request.getBhList().isEmpty()) {
throw new ServiceException("请先选择课程任务行", BAD_REQUEST);
}
int count = 0;
for (String bh : request.getBhList()) {
count += setRoomForRow(requireRow(bh), request.getJsbh(), request.getUseSpecial());
}
return count;
}
@Transactional(rollbackFor = Exception.class)
@Override
public int batchFill(TaskBookBatchFillRequest request) {
if (request == null || request.getBhList() == null || request.getBhList().isEmpty()) {
throw new ServiceException("请先选择课程任务行", BAD_REQUEST);
}
int count = 0;
for (String bh : request.getBhList()) {
XYDRWB row = requireRow(bh);
TaskBookFillRequest item = new TaskBookFillRequest();
item.setBh(bh);
item.setJysjhjybh(request.getJysjhjybh());
item.setJysjhbz(request.getJysjhbz());
item.setKcxh(request.getKcxh());
if (!Boolean.TRUE.equals(request.getUseSpecial())) {
item.setJsbh(request.getJsbh());
}
count += fill(item);
if (Boolean.TRUE.equals(request.getUseSpecial())) {
count += setRoomForRow(row, null, true);
}
}
return count;
}
@Transactional(rollbackFor = Exception.class)
@Override
public int deleteRows(TaskBookBhListRequest request) {
List<XYDRWB> rows = loadRows(request);
assertAllFillable(rows);
assertOfficeEditable(rows);
assertNotPublishedToRunning(rows);
int count = 0;
for (XYDRWB row : rows) {
row.setDelFlag(1);
row.setBdsj(LocalDateTime.now());
taskMapper.updateById(row);
count++;
}
return count;
}
@Override
public List<Map<String, Object>> officeSummary(String jysdh) {
if (roleHelper.isResearchOffice()) {
jysdh = roleHelper.requireResearchOfficeId();
} else if (roleHelper.isStudent() || roleHelper.isTeacher()) {
throw new ServiceException("当前角色无权查看教研室任务书汇总", FORBIDDEN);
}
if (jysdh == null || jysdh.isEmpty()) {
throw new ServiceException("请指定教研室代号", BAD_REQUEST);
}
String office = jysdh;
List<JYSRWS> books = officeTaskBookMapper.selectByJysdh(office);
JYSB jysb = jysbMapper.selectOne(new LambdaQueryWrapper<JYSB>()
.eq(JYSB::getJysdh, office)
.last("LIMIT 1"));
String jysmc = jysb != null && jysb.getJysmc() != null ? jysb.getJysmc() : office;
List<Map<String, Object>> result = new ArrayList<>();
for (JYSRWS book : books) {
JXRW task = book.getJxrwbh() == null ? null : jxrwMapper.selectById(book.getJxrwbh());
if (task == null || Integer.valueOf(1).equals(task.getDelFlag())) {
continue;
}
Map<String, Object> item = new LinkedHashMap<>();
item.put("bh", book.getBh());
item.put("jysdh", office);
item.put("jysmc", jysmc);
item.put("jxrwbh", book.getJxrwbh());
item.put("rwmc", task.getRwmc());
item.put("nd", task.getNd());
item.put("taskZt", task.getZt());
item.put("zt", book.getZt());
item.put("sbsj", book.getSbsj());
result.add(item);
}
return result;
}
@Transactional(rollbackFor = Exception.class)
@Override
public int fill(TaskBookFillRequest request) {
@@ -286,6 +446,7 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
}
XYDRWB row = requireRow(request.getBh());
assertFillable(row);
assertCanEdit(row);
LocalDateTime now = LocalDateTime.now();
int count;
if (request.getJysjhjybh() != null) {
@@ -294,6 +455,9 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
if (request.getJysjhbz() != null) {
row.setJysjhbz(request.getJysjhbz());
}
if (request.getKcxh() != null) {
row.setKcxh(request.getKcxh());
}
row.setBdsj(now);
taskMapper.updateById(row);
count = 1;
@@ -384,6 +548,46 @@ public class TaskBookFillServiceImpl implements TaskBookFillService {
}
}
/**
* 拟制权限(与排课窗口 canEdit 同口径):
* 机关/管理员全部可改;教研室账号可改本室行;教员只能改本人责任/计划行;其余禁止。
*/
private void assertCanEdit(XYDRWB row) {
if (roleHelper.isDepartmentPersonnel()) {
return;
}
if (roleHelper.isResearchOffice()) {
if (roleHelper.requireResearchOfficeId().equals(row.getJysdh())) {
return;
}
throw new ServiceException("只能修改本教研室的课程任务", FORBIDDEN);
}
if (roleHelper.isTeacher()) {
String self = roleHelper.requireTeacherId();
if (self.equals(row.getJybh()) || self.equals(row.getJysjhjybh())) {
return;
}
throw new ServiceException("只能修改本人承担的课程任务", FORBIDDEN);
}
throw new ServiceException("当前角色无任务书拟制权限", FORBIDDEN);
}
/** 合班/拆班属教研室级操作:仅机关/管理员与本室教研室账号可做 */
private void assertOfficeEditable(List<XYDRWB> rows) {
if (roleHelper.isDepartmentPersonnel()) {
return;
}
if (!roleHelper.isResearchOffice()) {
throw new ServiceException("合班/拆班仅教研室账号可执行", FORBIDDEN);
}
String office = roleHelper.requireResearchOfficeId();
for (XYDRWB row : rows) {
if (!office.equals(row.getJysdh())) {
throw new ServiceException("只能合班/拆班本教研室的课程任务", FORBIDDEN);
}
}
}
/**
* 阶段 5 反向门禁:拆班前确认行所在班次学期尚未发布到运行课表。
*
@@ -6,12 +6,18 @@ import com.roomroot.jwgl.dto.allocation.AllocationGroupRequest;
import com.roomroot.jwgl.dto.allocation.AllocationSaveItem;
import com.roomroot.jwgl.entity.JQB;
import com.roomroot.jwgl.entity.KB;
import com.roomroot.jwgl.entity.SSKC;
import com.roomroot.jwgl.entity.SSKCB;
import com.roomroot.jwgl.entity.SSKCXYD;
import com.roomroot.jwgl.entity.XQXLB;
import com.roomroot.jwgl.entity.XYDB;
import com.roomroot.jwgl.entity.XYDNDXQJBXXB;
import com.roomroot.jwgl.entity.XYDRWB;
import com.roomroot.jwgl.mapper.JQBMapper;
import com.roomroot.jwgl.mapper.KBMapper;
import com.roomroot.jwgl.mapper.SSKCBMapper;
import com.roomroot.jwgl.mapper.SSKCMapper;
import com.roomroot.jwgl.mapper.SSKCXYDMapper;
import com.roomroot.jwgl.mapper.SemesterCalendarMapper;
import com.roomroot.jwgl.mapper.StudentTeamTaskMapper;
import com.roomroot.jwgl.mapper.XYDBMapper;
@@ -34,10 +40,12 @@ import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST;
import static com.roomroot.jwgl.utils.BasicManagementConstants.NOT_FOUND;
@@ -89,6 +97,18 @@ public class TeachingAllocationServiceImpl implements TeachingAllocationService
@Resource
private TeachingTaskWriteGuard teachingTaskWriteGuard;
@Resource
private SSKCXYDMapper sskcxydMapper;
@Resource
private SSKCMapper sskcMapper;
@Resource
private SSKCBMapper sskcbMapper;
/** 已排学时口径:每课次 2 学时 + 节次调节(与排课窗 scheduledHours 一致) */
private static final int HOURS_PER_LESSON = 2;
// ==================== 视图 ====================
@Override
@@ -115,10 +135,68 @@ public class TeachingAllocationServiceImpl implements TeachingAllocationService
voMap.put(task.getBh(), toVO(task));
}
allocate(weeks, tasks, voMap);
fillScheduledHours(semester, voMap);
aggregateWeeks(weeks, voMap);
vo.setTasks(new ArrayList<>(voMap.values()));
return vo;
}
/**
* 已排/剩余学时:按 任务.kbh → 运行课程(科目编号) → 课次 折算,
* 课次只取与本班本学期关联(SSKCXYD xydbh+学期代号)且未删除的课程。
*/
private void fillScheduledHours(XYDNDXQJBXXB semester, Map<String, AllocationTaskVO> voMap) {
Integer ndCode = SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc());
List<SSKCXYD> links = sskcxydMapper.selectList(new LambdaQueryWrapper<SSKCXYD>()
.eq(SSKCXYD::getXydbh, semester.getXydbh())
.eq(SSKCXYD::getNd, ndCode));
List<String> courseIds = links.stream().map(SSKCXYD::getSskcbh).distinct().toList();
Map<String, Integer> hoursByKbh = new HashMap<>();
if (!courseIds.isEmpty()) {
Map<String, String> kbhByCourse = new HashMap<>();
for (SSKC course : sskcMapper.selectByBhs(courseIds)) {
if (course.getKmbh() != null) {
kbhByCourse.put(course.getBh(), course.getKmbh());
}
}
if (!kbhByCourse.isEmpty()) {
for (SSKCB lesson : sskcbMapper.selectList(new LambdaQueryWrapper<SSKCB>()
.in(SSKCB::getSskcbh, kbhByCourse.keySet())
.eq(SSKCB::getSczt, 0))) {
String kbh = kbhByCourse.get(lesson.getSskcbh());
if (kbh == null) continue;
int hours = HOURS_PER_LESSON + (lesson.getJcdj() == null ? 0 : lesson.getJcdj());
hoursByKbh.merge(kbh, hours, Integer::sum);
}
}
}
for (AllocationTaskVO vo : voMap.values()) {
int scheduled = hoursByKbh.getOrDefault(vo.getKbh(), 0);
vo.setScheduledHours(scheduled);
int total = vo.getXs() == null ? 0 : vo.getXs();
vo.setRemainingHours(Math.max(0, total - scheduled));
}
}
/** 周轴已铺/剩余:全部任务分布按周合计,剩余 = 可排正课 − 已铺(不为负) */
private void aggregateWeeks(List<AllocationWeekVO> weeks, Map<String, AllocationTaskVO> voMap) {
int[] used = new int[weeks.size()];
for (AllocationTaskVO vo : voMap.values()) {
for (AllocationTaskVO.WeekHours wh : vo.getDistribution()) {
if (wh.getWeek() != null && wh.getWeek() >= 1 && wh.getWeek() <= weeks.size()
&& wh.getHours() != null) {
used[wh.getWeek() - 1] += wh.getHours();
}
}
}
for (int i = 0; i < weeks.size(); i++) {
AllocationWeekVO w = weeks.get(i);
w.setScheduledHours(used[i]);
int avail = w.getAvailableHours() == null ? 0 : w.getAvailableHours();
w.setRemainingHours(Math.max(0, avail - used[i]));
}
}
private boolean isFrozen(String xydxqbh) {
try {
teachingTaskWriteGuard.assertCourseTaskWritable(xydxqbh);
@@ -485,6 +563,42 @@ public class TeachingAllocationServiceImpl implements TeachingAllocationService
return tasks.stream().map(XYDRWB::getBh).toList();
}
/**
* 自动排布:复用铺学时计算结果,把各任务的计算起始周回填到 pdqsz。
* onlyMissing(辅助排布)只补未设起始周的任务,不覆盖人工排布。
*/
@Transactional(rollbackFor = Exception.class)
@Override
public int autoArrange(String xydxqbh, List<String> bhList, boolean onlyMissing) {
teachingTaskWriteGuard.assertCourseTaskWritable(xydxqbh);
AllocationViewVO view = view(xydxqbh);
Set<String> filter = bhList == null || bhList.isEmpty() ? null : new HashSet<>(bhList);
LocalDateTime now = LocalDateTime.now();
int count = 0;
for (AllocationTaskVO vo : view.getTasks()) {
if (filter != null && !filter.contains(vo.getBh())) {
continue;
}
if (vo.getStartWeek() == null) {
continue;
}
if (onlyMissing && vo.getPdqsz() != null && vo.getPdqsz() > 0) {
continue;
}
XYDRWB task = xydrwbMapper.selectById(vo.getBh());
if (task == null || Integer.valueOf(1).equals(task.getDelFlag())) {
continue;
}
if (!Objects.equals(task.getPdqsz(), vo.getStartWeek())) {
task.setPdqsz(vo.getStartWeek());
task.setBdsj(now);
xydrwbMapper.updateById(task);
count++;
}
}
return count;
}
@Override
public List<Map<String, Object>> groups(String kbh) {
if (kbh == null || kbh.isEmpty()) {
@@ -109,7 +109,18 @@ public class TeachingTaskServiceImpl implements TeachingTaskService {
@Transactional(rollbackFor = Exception.class)
@Override
public int batchPublish(String bh) {
public int batchPublish(List<String> bhList) {
if (bhList == null || bhList.isEmpty()) {
return 0;
}
int count = 0;
for (String bh : bhList) {
count += publishOne(bh);
}
return count;
}
private int publishOne(String bh) {
if (bh == null || bh.isEmpty()) {
return 0;
}
@@ -119,7 +130,11 @@ public class TeachingTaskServiceImpl implements TeachingTaskService {
return 0;
}
if (TeachingTaskStatus.isEnded(jxrw.getZt())) {
throw new ServiceException("教学任务已结束,不能再次发布", CONFLICT);
throw new ServiceException("教学任务" + jxrw.getRwmc() + "已结束,不能再次发布", CONFLICT);
}
// 发布前校验:任务书须已生成课程任务行
if (teachingTaskMapper.selectTaskBookRows(bh).isEmpty()) {
throw new ServiceException("教学任务「" + jxrw.getRwmc() + "」尚未生成任务书课程任务,不能发布", BAD_REQUEST);
}
// 更新教学任务状态为"发布"
@@ -33,6 +33,7 @@ import com.roomroot.jwgl.service.ClassPeriodService;
import com.roomroot.jwgl.service.TimetableGridService;
import com.roomroot.jwgl.unit.BusinessException;
import com.roomroot.jwgl.utils.PeriodUtil;
import com.roomroot.jwgl.vo.kcb.FreePeriodsVO;
import com.roomroot.jwgl.vo.kcb.TimetableGridVO;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
@@ -56,6 +57,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
/**
@@ -349,6 +351,48 @@ public class TimetableGridServiceImpl implements TimetableGridService {
return vo;
}
@Override
public FreePeriodsVO freePeriods(TimetableGridQuery query) {
TimetableGridVO grid = grid(query);
FreePeriodsVO vo = new FreePeriodsVO();
vo.setDim(grid.getDim());
vo.setTargetName(grid.getTargetName());
vo.setStartDate(grid.getStartDate());
vo.setEndDate(grid.getEndDate());
vo.setPeriods(grid.getPeriods());
List<Integer> allJcs = grid.getPeriods().stream()
.map(TimetableGridVO.Period::getJc).sorted().collect(Collectors.toList());
for (TimetableGridVO.Week week : grid.getWeeks()) {
for (TimetableGridVO.Day day : week.getDays()) {
FreePeriodsVO.DayFree df = new FreePeriodsVO.DayFree();
df.setDate(day.getDate());
df.setWeekday(day.getWeekday());
df.setBlocked(day.getBlocked());
Set<Integer> busy = new TreeSet<>();
if (!Boolean.TRUE.equals(day.getBlocked())) {
for (Map.Entry<String, List<TimetableGridVO.Cell>> e : day.getCells().entrySet()) {
if (e.getValue() != null && !e.getValue().isEmpty()) {
try {
busy.add(Integer.parseInt(e.getKey()));
} catch (NumberFormatException ignored) {
}
}
}
}
df.setBusy(new ArrayList<>(busy));
if (!Boolean.TRUE.equals(day.getBlocked())) {
for (Integer jc : allJcs) {
if (!busy.contains(jc)) {
df.getFree().add(jc);
}
}
}
vo.getDays().add(df);
}
}
return vo;
}
@Override
public byte[] exportGrid(TimetableGridQuery query) {
TimetableGridVO vo = grid(query);
@@ -49,6 +49,12 @@ public class AllocationTaskVO {
/** 教研室代号 */
private String jysdh;
/** 已排学时(实施课程表已排课次折算,每课次 2 学时 + 节次调节) */
private Integer scheduledHours;
/** 剩余学时(学时 − 已排学时,不为负) */
private Integer remainingHours;
/** 计算起始周(铺学时结果) */
private Integer startWeek;
@@ -26,6 +26,12 @@ public class AllocationWeekVO {
/** 该周可排正课学时 */
private Integer availableHours;
/** 该周已铺学时(全部课程任务分布合计,含超容量部分) */
private Integer scheduledHours;
/** 该周剩余可排学时(可排正课 − 已铺,不为负) */
private Integer remainingHours;
/** 容量来源:class-班历 / school-校历 / default-默认 */
private String source;
}
@@ -21,6 +21,16 @@ public class ClassCalendarResyncVO {
*/
private List<ClassCalendarEventDTO> items;
/**
* 与校历不一致、force 时将被覆盖的时间格数量
*/
private int updateCount;
/**
* 将被覆盖的时间格明细(按校历口径)
*/
private List<ClassCalendarEventDTO> updateItems;
/**
* 是否已实际写入(false = 仅预览)
*/
@@ -0,0 +1,43 @@
package com.roomroot.jwgl.vo.kcb;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 有课/无课时段查询结果。
* <p>按日期展开:每天给出 busy(有课节次)与 free(无课节次),
* 不可排日(blocked)所有节次既不算 busy 也不算 free。</p>
*/
@Data
public class FreePeriodsVO {
/** 维度(semester/team/teacher/room */
private String dim;
/** 维度目标显示名 */
private String targetName;
private String startDate;
private String endDate;
/** 全部节次行定义 */
private List<TimetableGridVO.Period> periods = new ArrayList<>();
/** 每日占用情况 */
private List<DayFree> days = new ArrayList<>();
@Data
public static class DayFree {
private String date;
/** 1=周一 … 7=周日 */
private Integer weekday;
/** 历表标记不可排课 */
private Boolean blocked = false;
/** 有课节次序号(升序) */
private List<Integer> busy = new ArrayList<>();
/** 无课节次序号(升序) */
private List<Integer> free = new ArrayList<>();
}
}
@@ -0,0 +1,82 @@
package com.roomroot.jwgl.vo.mybusiness;
import lombok.Data;
/**
* 「我的课程任务」行(学员队任务表视角,附教学任务归属)。
*/
@Data
public class MyCourseTaskVO {
/** 课程任务编号(学员队任务表.编号) */
private String bh;
/** 学员队学期编号 */
private String xydxqbh;
/** 教学任务编号(经班次学期关联) */
private String jxrwbh;
/** 教学任务名称 */
private String rwmc;
/** 年度 */
private Integer nd;
/** 学期第次 */
private Integer xqdc;
/** 课编号 */
private String kbh;
/** 课程名称(简称优先,缺省取课表课名称) */
private String kcmc;
/** 课类型 */
private String klx;
/** 学时 */
private Integer xs;
/** 周课时 */
private Integer zks;
/** 学员队编号 */
private String xydbh;
/** 学员队名称 */
private String xydmc;
/** 责任教员编号 */
private String jybh;
/** 责任教员姓名 */
private String jyxm;
/** 责任教员是否教研室主任 */
private Integer zr;
/** 教研室计划教员编号 */
private String jysjhjybh;
/** 教研室计划教员姓名 */
private String jysjhjyxm;
/** 教研室代号 */
private String jysdh;
/** 教研室名称 */
private String jysmc;
/** 教室编号 */
private String jsbh;
/** 教室名称 */
private String jsmc;
/** 课次序号 */
private Integer kcxh;
/** 合班分组号 */
private Integer bz2;
}
@@ -47,6 +47,9 @@ public class TaskBookRowVO {
/** 责任教员姓名 */
private String jyxm;
/** 责任教员是否教研室主任(教员属性.主任,1=主任,前端标 *) */
private Integer zr;
/** 场地(教室编号) */
private String jsbh;