diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/KSController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/KSController.java index 2a766fb44..d3e2afe1b 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/KSController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/KSController.java @@ -195,6 +195,17 @@ public class KSController { return Result.success(ksService.pageHourStatistics(new PageQuery(pageNum, pageSize), cond)); } + /** + * 课时核算:按指定自然年的已排课次聚合生成课时统计表(覆盖该年数据)。 + * 仅教务人员可执行。 + * + * @param year 自然年(可选,默认当前年) + */ + @PostMapping("/hour-stat/recalculate") + public Result recalcHourStatistics(@RequestParam(required = false) Integer year) { + return Result.success(ksService.recalcHourStatistics(year)); + } + /** * 导出课时统计 Excel。 */ diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/KSService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/KSService.java index 9840f91a6..56bfe0dd2 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/KSService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/KSService.java @@ -32,4 +32,13 @@ public interface KSService { * 导出课时费统计。 */ void exportFeeStatistics(HourStatisticsQuery cond, HttpServletResponse response); + + /** + * 课时核算:按已排课次(实施_课程表)聚合指定自然年各教员上下半年课时, + * 覆盖写入课时统计表。仅教务人员可执行。 + * + * @param year 自然年(如 2026);为空取当前年 + * @return 写入统计表的教员行数 + */ + int recalcHourStatistics(Integer year); } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/KSServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/KSServiceImpl.java index c6f1b4b6e..897e1a086 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/KSServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/KSServiceImpl.java @@ -6,9 +6,18 @@ import com.roomroot.common.exception.ServiceException; import com.roomroot.common.utils.StringUtils; import com.roomroot.common.utils.poi.ExcelUtil; import com.roomroot.jwgl.dto.ks.HourStatisticsQuery; +import com.roomroot.jwgl.entity.JYB; import com.roomroot.jwgl.entity.KSFBZ; +import com.roomroot.jwgl.entity.KSTJB; +import com.roomroot.jwgl.entity.SSKC; +import com.roomroot.jwgl.entity.SSKCB; +import com.roomroot.jwgl.entity.SSKCBFZJY; +import com.roomroot.jwgl.mapper.JYBMapper; import com.roomroot.jwgl.mapper.KSFBZMapper; import com.roomroot.jwgl.mapper.KSTJBMapper; +import com.roomroot.jwgl.mapper.SSKCBFZJYMapper; +import com.roomroot.jwgl.mapper.SSKCBMapper; +import com.roomroot.jwgl.mapper.SSKCMapper; import com.roomroot.jwgl.mapper.SystemInitializationMapper; import com.roomroot.jwgl.service.KSService; import com.roomroot.jwgl.unit.PageQuery; @@ -18,14 +27,22 @@ import com.roomroot.jwgl.vo.ks.HourStatisticsVO; import com.roomroot.jwgl.vo.systeminitialization.SemesterInitializationVO; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import jakarta.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +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; import static com.roomroot.jwgl.utils.BasicManagementConstants.NOT_FOUND; /** @@ -49,6 +66,18 @@ public class KSServiceImpl implements KSService { @Resource private JwglRoleHelper jwglRoleHelper; + @Resource + private SSKCBMapper sskcbMapper; + + @Resource + private SSKCBFZJYMapper sskcbfzjyMapper; + + @Resource + private SSKCMapper sskcMapper; + + @Resource + private JYBMapper jybMapper; + @Override public PageResult pageHourStatistics(PageQuery query, HourStatisticsQuery cond) { Page page = queryHours(query, cond); @@ -136,6 +165,66 @@ public class KSServiceImpl implements KSService { return systemInitializationMapper.selectCurrentSemester(); } + /** + * 课时核算:把指定自然年已排课次(每课次 2 + 节次调节 学时)按教员聚合到课时统计表。 + * 归属:课次主讲教员(实施_课程表_辅助教员 zjy=1),无主讲记录回退课程责任教员。 + * 半年口径:1-6 月上半年、7-12 月下半年。该年原有统计行先整体清掉再重写。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int recalcHourStatistics(Integer year) { + if (!jwglRoleHelper.isDepartmentPersonnel()) { + throw new ServiceException("只有教务人员可以执行课时核算", FORBIDDEN); + } + int y = year != null ? year : LocalDate.now().getYear(); + List yearLessons = sskcbMapper.selectList(new LambdaQueryWrapper() + .eq(SSKCB::getSczt, 0).isNotNull(SSKCB::getRq)).stream() + .filter(l -> l.getRq().getYear() == y).collect(Collectors.toList()); + if (yearLessons.isEmpty()) { + kstjbMapper.delete(new LambdaQueryWrapper().eq(KSTJB::getNf, y)); + return 0; + } + // 课次主讲教员(zjy=1,每课次取一条) + List lessonIds = yearLessons.stream().map(SSKCB::getBh).collect(Collectors.toList()); + Map mainTeacher = new HashMap<>(); + for (SSKCBFZJY f : sskcbfzjyMapper.selectList(new LambdaQueryWrapper() + .in(SSKCBFZJY::getSskcbbh, lessonIds).eq(SSKCBFZJY::getZjy, 1))) { + if (f.getFzjybh() != null && !f.getFzjybh().isEmpty()) { + mainTeacher.putIfAbsent(f.getSskcbbh(), f.getFzjybh()); + } + } + // 课程责任教员兜底 + Set courseIds = yearLessons.stream().map(SSKCB::getSskcbh).collect(Collectors.toSet()); + Map courseTeacher = new HashMap<>(); + if (!courseIds.isEmpty()) { + for (SSKC c : sskcMapper.selectByBhs(new ArrayList<>(courseIds))) { + courseTeacher.put(c.getBh(), c.getJybh()); + } + } + // 按教员累计上/下半年课时 + Map hours = new HashMap<>(); + for (SSKCB l : yearLessons) { + String jybh = mainTeacher.getOrDefault(l.getBh(), courseTeacher.get(l.getSskcbh())); + if (jybh == null || jybh.isEmpty()) continue; + double h = 2D + (l.getJcdj() == null ? 0 : l.getJcdj()); + int half = l.getRq().getMonthValue() <= 6 ? 0 : 1; + hours.computeIfAbsent(jybh, k -> new double[2])[half] += h; + } + kstjbMapper.delete(new LambdaQueryWrapper().eq(KSTJB::getNf, y)); + for (Map.Entry e : hours.entrySet()) { + KSTJB row = new KSTJB(); + row.setNf(y); + row.setJybh(e.getKey()); + row.setSbn(e.getValue()[0]); + row.setXbn(e.getValue()[1]); + JYB teacher = jybMapper.selectById(e.getKey()); + // 职称列为 NOT NULL 且达梦空串等同 NULL,无职称须给兜底值 + row.setZc(teacher != null && StringUtils.isNotEmpty(teacher.getZc()) ? teacher.getZc() : "未定级"); + kstjbMapper.insert(row); + } + return hours.size(); + } + private KSFBZ resolveFeeStandard(HourStatisticsQuery cond) { if (cond != null && StringUtils.isNotEmpty(cond.getKsfbzbh())) { KSFBZ specified = ksfbzMapper.selectById(cond.getKsfbzbh()); diff --git a/frontend/src/api/classHour/hourStat.js b/frontend/src/api/classHour/hourStat.js index 3075223c1..05ce3cb27 100644 --- a/frontend/src/api/classHour/hourStat.js +++ b/frontend/src/api/classHour/hourStat.js @@ -12,6 +12,15 @@ export function listHourStatistics(query) { }) } +// 课时核算:按自然年聚合已排课次生成课时统计表(覆盖该年,仅教务) +export function recalcHourStatistics(year) { + return request({ + url: '/ks/hour-stat/recalculate', + method: 'post', + params: year != null ? { year } : {} + }) +} + // 导出课时统计 Excel(返回 Blob,文件名 课时统计.xlsx) export function exportHourStatistics(query) { return request({ diff --git a/frontend/src/views/classHour/subsidy/index.vue b/frontend/src/views/classHour/subsidy/index.vue index 3ce0b7464..81847f370 100644 --- a/frontend/src/views/classHour/subsidy/index.vue +++ b/frontend/src/views/classHour/subsidy/index.vue @@ -31,6 +31,7 @@
查询 + 课时核算 导出 Excel
@@ -130,6 +131,7 @@ import { saveAs } from 'file-saver' import { listHourStatistics, exportHourStatistics, + recalcHourStatistics, listFeeStatistics, exportFeeStatistics } from '@/api/classHour/hourStat' @@ -230,6 +232,21 @@ export default { this.fetchList() }, + /** 课时核算:按学期年度(缺省当前年)聚合已排课次,覆盖该年统计 */ + handleRecalc() { + const nd = (this.hourForm.nd || '').trim() + const year = nd ? Number(nd) : null + const tip = year + ? `将按 ${year} 年已排课次重新核算课时统计,该年现有统计将被覆盖,是否继续?` + : '将按当前年已排课次重新核算课时统计,该年现有统计将被覆盖,是否继续?' + this.$confirm(tip, '课时核算', { type: 'warning' }).then(() => { + recalcHourStatistics(year).then(res => { + this.$message.success(`核算完成,共生成 ${(res && res.data) || 0} 名教员的课时统计`) + this.fetchList() + }).catch(() => {}) + }).catch(() => {}) + }, + // ==================== 导出 ==================== handleExport() { const clean = this.buildParams(this.currentForm)