diff --git a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainRegHistoryStatController.java b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainRegHistoryStatController.java new file mode 100644 index 000000000..6bd22d78f --- /dev/null +++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainRegHistoryStatController.java @@ -0,0 +1,110 @@ +package com.ruoyi.web.controller.train; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.train.domain.query.RegHistoryStatQuery; +import com.ruoyi.train.domain.vo.RegHistoryDetailVO; +import com.ruoyi.train.domain.vo.RegHistoryGroupVO; +import com.ruoyi.train.domain.vo.RegHistoryPageVO; +import com.ruoyi.train.service.ITrainRegHistoryStatService; + +/** + * 历史报到统计 Controller(独立模块)。 + * + *
路由前缀 /train/regdata/history-stat,与既有 {@code /train/regdata/history/**} 互不影响; + * 权限沿用既有报到查询/导出权限,不新增权限点,避免影响现有角色授权。
+ * + *口径:报到状态取自 train_enrollment.enrollment_status,报到时间取自 join_time; + * 已报到 = status in (1,4) 且 join_time 非空;不使用 train_class_report。
+ * + * @author ruoyi + */ +@RestController +@RequestMapping("/train/regdata/history-stat") +public class TrainRegHistoryStatController extends BaseController +{ + @Autowired + private ITrainRegHistoryStatService trainRegHistoryStatService; + + /** + * 总览统计。 + * + *GET /train/regdata/history-stat/overview
+ */ + @PreAuthorize("@ss.hasPermi('train:report:list')") + @GetMapping("/overview") + public AjaxResult overview(RegHistoryStatQuery query) + { + return success(trainRegHistoryStatService.selectOverview(query)); + } + + /** + * 分组汇总(batch/dept/major/place/gender/ageBand/dutyLevel/enrollSource/accommodation/firstTime)。 + * + *GET /train/regdata/history-stat/groups?groupBy=dept&pageNum=1&pageSize=100
+ */ + @PreAuthorize("@ss.hasPermi('train:report:list')") + @GetMapping("/groups") + public TableDataInfo groups(RegHistoryStatQuery query) + { + RegHistoryPageVOGET /train/regdata/history-stat/times?granularity=day
+ */ + @PreAuthorize("@ss.hasPermi('train:report:list')") + @GetMapping("/times") + public AjaxResult times(RegHistoryStatQuery query) + { + List> buckets = trainRegHistoryStatService.selectTimeBuckets(query); + return success(buckets); + } + + /** + * 报到明细(分页,支持异常筛选)。 + * + *GET /train/regdata/history-stat/details?exceptionType=all&pageNum=1&pageSize=10
+ */ + @PreAuthorize("@ss.hasPermi('train:report:list')") + @GetMapping("/details") + public TableDataInfo details(RegHistoryStatQuery query) + { + RegHistoryPageVOPOST /train/regdata/history-stat/details/export
+ */ + @PreAuthorize("@ss.hasPermi('train:report:export')") + @Log(title = "历史报到统计明细", businessType = BusinessType.EXPORT) + @PostMapping("/details/export") + public void exportDetails(HttpServletResponse response, RegHistoryStatQuery query) + { + List该对象仅服务于新增的历史报到统计(/train/regdata/history-stat), + * 与既有 {@code /train/regdata/history/*} 报表相互独立,不参与旧统计口径。
+ * + * @author ruoyi + */ +public class RegHistoryStatQuery +{ + /** 培训班ID(可空,空表示全部批次) */ + private String planId; + + /** 送培单位ID(train_student.dept_id) */ + private Long deptId; + + /** 专业代码(train_class_plan.class_major) */ + private Integer classMajor; + + /** 批次开班日期下界 yyyy-MM-dd */ + private String startDate; + + /** 批次开班日期上界 yyyy-MM-dd */ + private String endDate; + + /** 是否纳入未开始批次(默认 false,仅统计进行中/已结束批次) */ + private Boolean includeNotStarted = Boolean.FALSE; + + /** 分组维度:batch/dept/major/place/gender/ageBand/dutyLevel/enrollSource/accommodation/firstTime */ + private String groupBy = "batch"; + + /** 时间分布粒度:day/week/month/hour */ + private String granularity = "day"; + + /** 明细异常筛选:all/inconsistent/lagging/timeConflict/late/overdue/abnormal */ + private String exceptionType = "all"; + + /** 页码(内存分页,从 1 开始) */ + private Integer pageNum = 1; + + /** 每页条数 */ + private Integer pageSize = 10; + + /** 单次取数上限,防止全量拉取失控 */ + private Integer limit = 50000; + + /** 内部时间下界(由 Service 由 startDate 解析,不对外暴露) */ + private Date startTime; + + /** 内部时间上界(由 Service 由 endDate 解析,不对外暴露) */ + private Date endTime; + + public String getPlanId() + { + return planId; + } + + public void setPlanId(String planId) + { + this.planId = planId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + public Integer getClassMajor() + { + return classMajor; + } + + public void setClassMajor(Integer classMajor) + { + this.classMajor = classMajor; + } + + public String getStartDate() + { + return startDate; + } + + public void setStartDate(String startDate) + { + this.startDate = startDate; + } + + public String getEndDate() + { + return endDate; + } + + public void setEndDate(String endDate) + { + this.endDate = endDate; + } + + public Boolean getIncludeNotStarted() + { + return includeNotStarted; + } + + public void setIncludeNotStarted(Boolean includeNotStarted) + { + this.includeNotStarted = includeNotStarted; + } + + public String getGroupBy() + { + return groupBy; + } + + public void setGroupBy(String groupBy) + { + this.groupBy = groupBy; + } + + public String getGranularity() + { + return granularity; + } + + public void setGranularity(String granularity) + { + this.granularity = granularity; + } + + public String getExceptionType() + { + return exceptionType; + } + + public void setExceptionType(String exceptionType) + { + this.exceptionType = exceptionType; + } + + public Integer getPageNum() + { + return pageNum; + } + + public void setPageNum(Integer pageNum) + { + this.pageNum = pageNum; + } + + public Integer getPageSize() + { + return pageSize; + } + + public void setPageSize(Integer pageSize) + { + this.pageSize = pageSize; + } + + public Integer getLimit() + { + return limit; + } + + public void setLimit(Integer limit) + { + this.limit = limit; + } + + public Date getStartTime() + { + return startTime; + } + + public void setStartTime(Date startTime) + { + this.startTime = startTime; + } + + public Date getEndTime() + { + return endTime; + } + + public void setEndTime(Date endTime) + { + this.endTime = endTime; + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryDetailVO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryDetailVO.java new file mode 100644 index 000000000..d167e2f1a --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryDetailVO.java @@ -0,0 +1,396 @@ +package com.ruoyi.train.domain.vo; + +import java.io.Serializable; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; + +/** + * 历史报到明细行。 + * + *不含到训耗时、耗时分位、平均到训时刻;报到时间统一取 join_time。
+ * + * @author ruoyi + */ +public class RegHistoryDetailVO implements Serializable +{ + private static final long serialVersionUID = 1L; + + @Excel(name = "参训档案ID") + private Long enrollmentId; + + @Excel(name = "培训班ID") + private String planId; + + @Excel(name = "班级名称") + private String className; + + @Excel(name = "开班名称") + private String planName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "开班时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "报到开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date reportStartTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "报到截止时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date reportDeadline; + + private String studentId; + + @Excel(name = "学员姓名") + private String studentName; + + @Excel(name = "人员编号") + private String personNo; + + @Excel(name = "性别") + private String gender; + + @Excel(name = "年龄段") + private String ageBand; + + private Long deptId; + + @Excel(name = "送培单位") + private String deptName; + + private Integer enrollmentStatus; + + @Excel(name = "参训状态") + private String statusText; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "报到时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date joinTime; + + @Excel(name = "报到日期") + private String reportDate; + + @Excel(name = "报到时段") + private String reportHour; + + @Excel(name = "报到情况") + private String lateText; + + @Excel(name = "异常标记") + private String exceptionText; + + /** 是否已报到 */ + private Boolean reportedFlag = Boolean.FALSE; + + /** 是否迟到 */ + private Boolean lateFlag = Boolean.FALSE; + + /** 迟到分钟数 */ + private Integer lateMinutes = 0; + + /** 状态时间不一致 */ + private Boolean inconsistentFlag = Boolean.FALSE; + + /** 状态滞后(待报到但有报到时间) */ + private Boolean laggingFlag = Boolean.FALSE; + + /** 时间倒挂或未来时间 */ + private Boolean timeConflictFlag = Boolean.FALSE; + + /** 逾期未报到 */ + private Boolean overdueFlag = Boolean.FALSE; + + /** 是否存在数据质量异常 */ + private Boolean abnormalFlag = Boolean.FALSE; + + public Long getEnrollmentId() + { + return enrollmentId; + } + + public void setEnrollmentId(Long enrollmentId) + { + this.enrollmentId = enrollmentId; + } + + public String getPlanId() + { + return planId; + } + + public void setPlanId(String planId) + { + this.planId = planId; + } + + public String getClassName() + { + return className; + } + + public void setClassName(String className) + { + this.className = className; + } + + public String getPlanName() + { + return planName; + } + + public void setPlanName(String planName) + { + this.planName = planName; + } + + public Date getStartTime() + { + return startTime; + } + + public void setStartTime(Date startTime) + { + this.startTime = startTime; + } + + public Date getReportStartTime() + { + return reportStartTime; + } + + public void setReportStartTime(Date reportStartTime) + { + this.reportStartTime = reportStartTime; + } + + public Date getReportDeadline() + { + return reportDeadline; + } + + public void setReportDeadline(Date reportDeadline) + { + this.reportDeadline = reportDeadline; + } + + public String getStudentId() + { + return studentId; + } + + public void setStudentId(String studentId) + { + this.studentId = studentId; + } + + public String getStudentName() + { + return studentName; + } + + public void setStudentName(String studentName) + { + this.studentName = studentName; + } + + public String getPersonNo() + { + return personNo; + } + + public void setPersonNo(String personNo) + { + this.personNo = personNo; + } + + public String getGender() + { + return gender; + } + + public void setGender(String gender) + { + this.gender = gender; + } + + public String getAgeBand() + { + return ageBand; + } + + public void setAgeBand(String ageBand) + { + this.ageBand = ageBand; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public Integer getEnrollmentStatus() + { + return enrollmentStatus; + } + + public void setEnrollmentStatus(Integer enrollmentStatus) + { + this.enrollmentStatus = enrollmentStatus; + } + + public String getStatusText() + { + return statusText; + } + + public void setStatusText(String statusText) + { + this.statusText = statusText; + } + + public Date getJoinTime() + { + return joinTime; + } + + public void setJoinTime(Date joinTime) + { + this.joinTime = joinTime; + } + + public String getReportDate() + { + return reportDate; + } + + public void setReportDate(String reportDate) + { + this.reportDate = reportDate; + } + + public String getReportHour() + { + return reportHour; + } + + public void setReportHour(String reportHour) + { + this.reportHour = reportHour; + } + + public String getLateText() + { + return lateText; + } + + public void setLateText(String lateText) + { + this.lateText = lateText; + } + + public String getExceptionText() + { + return exceptionText; + } + + public void setExceptionText(String exceptionText) + { + this.exceptionText = exceptionText; + } + + public Boolean getReportedFlag() + { + return reportedFlag; + } + + public void setReportedFlag(Boolean reportedFlag) + { + this.reportedFlag = reportedFlag; + } + + public Boolean getLateFlag() + { + return lateFlag; + } + + public void setLateFlag(Boolean lateFlag) + { + this.lateFlag = lateFlag; + } + + public Integer getLateMinutes() + { + return lateMinutes; + } + + public void setLateMinutes(Integer lateMinutes) + { + this.lateMinutes = lateMinutes; + } + + public Boolean getInconsistentFlag() + { + return inconsistentFlag; + } + + public void setInconsistentFlag(Boolean inconsistentFlag) + { + this.inconsistentFlag = inconsistentFlag; + } + + public Boolean getLaggingFlag() + { + return laggingFlag; + } + + public void setLaggingFlag(Boolean laggingFlag) + { + this.laggingFlag = laggingFlag; + } + + public Boolean getTimeConflictFlag() + { + return timeConflictFlag; + } + + public void setTimeConflictFlag(Boolean timeConflictFlag) + { + this.timeConflictFlag = timeConflictFlag; + } + + public Boolean getOverdueFlag() + { + return overdueFlag; + } + + public void setOverdueFlag(Boolean overdueFlag) + { + this.overdueFlag = overdueFlag; + } + + public Boolean getAbnormalFlag() + { + return abnormalFlag; + } + + public void setAbnormalFlag(Boolean abnormalFlag) + { + this.abnormalFlag = abnormalFlag; + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryFactVO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryFactVO.java new file mode 100644 index 000000000..f5599c23d --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryFactVO.java @@ -0,0 +1,428 @@ +package com.ruoyi.train.domain.vo; + +import java.io.Serializable; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; + +/** + * 历史报到统计事实行(只读投影)。 + * + *数据来源严格限定为 train_enrollment + train_class_plan + train_student + sys_dept, + * 不引用 train_class_report,报到状态取自 enrollment_status,报到时间取自 join_time。
+ * + * @author ruoyi + */ +public class RegHistoryFactVO implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 参训档案ID */ + private Long enrollmentId; + + /** 培训班ID */ + private String planId; + + /** 班级名称(回退开班名称) */ + private String className; + + /** 开班名称 */ + private String planName; + + /** 批次状态 0未开始 1进行中 2已结束 3已取消 */ + private Integer planStatus; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + /** 计划报到开始时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date reportStartTime; + + /** 计划报到截止时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date reportDeadline; + + /** 批次创建时间(用于识别报到时间早于计划创建) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date planCreateTime; + + /** 所属专业 */ + private Integer classMajor; + + /** 培训地点 */ + private String trainAddress; + + /** 班主任 */ + private String classTeacher; + + /** 主办部门ID */ + private Integer hostDeptId; + + /** 负责人 */ + private String manager; + + /** 学员ID */ + private String studentId; + + /** 学员姓名 */ + private String studentName; + + /** 人员编号 */ + private String personNo; + + /** 性别 */ + private String gender; + + /** 出生日期 */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date birth; + + /** 职级 */ + private Integer dutyLevel; + + /** 送培单位ID */ + private Long deptId; + + /** 送培单位名称 */ + private String deptName; + + /** 学员主档状态 */ + private Integer studentStatus; + + /** 学员主档删除标志 */ + private String studentDelFlag; + + /** 参训名单来源 */ + private Integer enrollSource; + + /** 是否需住宿 0否 1是 */ + private Integer accommodationRequired; + + /** 参训状态 0待报到 1已报到 2请假 3放弃 4已完成 */ + private Integer enrollmentStatus; + + /** 入班报到时间(唯一报到时间来源) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date joinTime; + + /** 参训档案创建时间(用于首次参训判定) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date enrollCreateTime; + + /** 参训档案更新时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + public Long getEnrollmentId() + { + return enrollmentId; + } + + public void setEnrollmentId(Long enrollmentId) + { + this.enrollmentId = enrollmentId; + } + + public String getPlanId() + { + return planId; + } + + public void setPlanId(String planId) + { + this.planId = planId; + } + + public String getClassName() + { + return className; + } + + public void setClassName(String className) + { + this.className = className; + } + + public String getPlanName() + { + return planName; + } + + public void setPlanName(String planName) + { + this.planName = planName; + } + + public Integer getPlanStatus() + { + return planStatus; + } + + public void setPlanStatus(Integer planStatus) + { + this.planStatus = planStatus; + } + + public Date getStartTime() + { + return startTime; + } + + public void setStartTime(Date startTime) + { + this.startTime = startTime; + } + + public Date getEndTime() + { + return endTime; + } + + public void setEndTime(Date endTime) + { + this.endTime = endTime; + } + + public Date getReportStartTime() + { + return reportStartTime; + } + + public void setReportStartTime(Date reportStartTime) + { + this.reportStartTime = reportStartTime; + } + + public Date getReportDeadline() + { + return reportDeadline; + } + + public void setReportDeadline(Date reportDeadline) + { + this.reportDeadline = reportDeadline; + } + + public Date getPlanCreateTime() + { + return planCreateTime; + } + + public void setPlanCreateTime(Date planCreateTime) + { + this.planCreateTime = planCreateTime; + } + + public Integer getClassMajor() + { + return classMajor; + } + + public void setClassMajor(Integer classMajor) + { + this.classMajor = classMajor; + } + + public String getTrainAddress() + { + return trainAddress; + } + + public void setTrainAddress(String trainAddress) + { + this.trainAddress = trainAddress; + } + + public String getClassTeacher() + { + return classTeacher; + } + + public void setClassTeacher(String classTeacher) + { + this.classTeacher = classTeacher; + } + + public Integer getHostDeptId() + { + return hostDeptId; + } + + public void setHostDeptId(Integer hostDeptId) + { + this.hostDeptId = hostDeptId; + } + + public String getManager() + { + return manager; + } + + public void setManager(String manager) + { + this.manager = manager; + } + + public String getStudentId() + { + return studentId; + } + + public void setStudentId(String studentId) + { + this.studentId = studentId; + } + + public String getStudentName() + { + return studentName; + } + + public void setStudentName(String studentName) + { + this.studentName = studentName; + } + + public String getPersonNo() + { + return personNo; + } + + public void setPersonNo(String personNo) + { + this.personNo = personNo; + } + + public String getGender() + { + return gender; + } + + public void setGender(String gender) + { + this.gender = gender; + } + + public Date getBirth() + { + return birth; + } + + public void setBirth(Date birth) + { + this.birth = birth; + } + + public Integer getDutyLevel() + { + return dutyLevel; + } + + public void setDutyLevel(Integer dutyLevel) + { + this.dutyLevel = dutyLevel; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public Integer getStudentStatus() + { + return studentStatus; + } + + public void setStudentStatus(Integer studentStatus) + { + this.studentStatus = studentStatus; + } + + public String getStudentDelFlag() + { + return studentDelFlag; + } + + public void setStudentDelFlag(String studentDelFlag) + { + this.studentDelFlag = studentDelFlag; + } + + public Integer getEnrollSource() + { + return enrollSource; + } + + public void setEnrollSource(Integer enrollSource) + { + this.enrollSource = enrollSource; + } + + public Integer getAccommodationRequired() + { + return accommodationRequired; + } + + public void setAccommodationRequired(Integer accommodationRequired) + { + this.accommodationRequired = accommodationRequired; + } + + public Integer getEnrollmentStatus() + { + return enrollmentStatus; + } + + public void setEnrollmentStatus(Integer enrollmentStatus) + { + this.enrollmentStatus = enrollmentStatus; + } + + public Date getJoinTime() + { + return joinTime; + } + + public void setJoinTime(Date joinTime) + { + this.joinTime = joinTime; + } + + public Date getEnrollCreateTime() + { + return enrollCreateTime; + } + + public void setEnrollCreateTime(Date enrollCreateTime) + { + this.enrollCreateTime = enrollCreateTime; + } + + public Date getUpdateTime() + { + return updateTime; + } + + public void setUpdateTime(Date updateTime) + { + this.updateTime = updateTime; + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryGroupVO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryGroupVO.java new file mode 100644 index 000000000..fe24f726c --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryGroupVO.java @@ -0,0 +1,301 @@ +package com.ruoyi.train.domain.vo; + +import java.io.Serializable; + +/** + * 历史报到统计分组汇总行。 + * + *不含到训耗时、耗时分位、平均到训时刻三类指标。
+ * + * @author ruoyi + */ +public class RegHistoryGroupVO implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 分组键(部门ID、批次ID、属性取值等) */ + private String groupKey; + + /** 分组展示名 */ + private String groupLabel; + + /** 覆盖批次数 */ + private Integer batchCount = 0; + + /** 应报到人次 */ + private Integer expectedCount = 0; + + /** 已报到人次 */ + private Integer reportedCount = 0; + + /** 已报到去重人数 */ + private Integer reportedPersonCount = 0; + + /** 报到率(%) */ + private Double reportRate = 0D; + + /** 出勤报到率(%)(请假不计入分母) */ + private Double attendanceRate = 0D; + + /** 待报到人次 */ + private Integer waitingCount = 0; + + /** 请假人次 */ + private Integer leaveCount = 0; + + /** 状态时间不一致人次 */ + private Integer inconsistentCount = 0; + + /** 迟到人次 */ + private Integer lateCount = 0; + + /** 迟到率(%) */ + private Double lateRate = 0D; + + /** 准点率(%) */ + private Double onTimeRate = 0D; + + /** 是否达到目标报到率(仅批次分组有效) */ + private Boolean targetMet; + + /** 批次内首位报到时间(仅批次分组有效) */ + private String firstArriveTime; + + /** 批次内末位报到时间(仅批次分组有效) */ + private String lastArriveTime; + + /** 批次状态(仅批次分组有效) */ + private Integer planStatus; + + /** 与全局报到率的偏差(百分点) */ + private Double gapToGlobal; + + /** 单位离群标记:|偏差| >= 20 个百分点且应报到 >= 10 */ + private Boolean outlierFlag = Boolean.FALSE; + + /** 集中报到标记:单小时报到人数 >= max(10, 已报到 × 50%) */ + private Boolean concentratedFlag = Boolean.FALSE; + + /** 报到率波动标记:与最近 6 期均值偏差 >= 10 个百分点(仅批次分组有效) */ + private Boolean volatilityFlag = Boolean.FALSE; + + public String getGroupKey() + { + return groupKey; + } + + public void setGroupKey(String groupKey) + { + this.groupKey = groupKey; + } + + public String getGroupLabel() + { + return groupLabel; + } + + public void setGroupLabel(String groupLabel) + { + this.groupLabel = groupLabel; + } + + public Integer getBatchCount() + { + return batchCount; + } + + public void setBatchCount(Integer batchCount) + { + this.batchCount = batchCount; + } + + public Integer getExpectedCount() + { + return expectedCount; + } + + public void setExpectedCount(Integer expectedCount) + { + this.expectedCount = expectedCount; + } + + public Integer getReportedCount() + { + return reportedCount; + } + + public void setReportedCount(Integer reportedCount) + { + this.reportedCount = reportedCount; + } + + public Integer getReportedPersonCount() + { + return reportedPersonCount; + } + + public void setReportedPersonCount(Integer reportedPersonCount) + { + this.reportedPersonCount = reportedPersonCount; + } + + public Double getReportRate() + { + return reportRate; + } + + public void setReportRate(Double reportRate) + { + this.reportRate = reportRate; + } + + public Double getAttendanceRate() + { + return attendanceRate; + } + + public void setAttendanceRate(Double attendanceRate) + { + this.attendanceRate = attendanceRate; + } + + public Integer getWaitingCount() + { + return waitingCount; + } + + public void setWaitingCount(Integer waitingCount) + { + this.waitingCount = waitingCount; + } + + public Integer getLeaveCount() + { + return leaveCount; + } + + public void setLeaveCount(Integer leaveCount) + { + this.leaveCount = leaveCount; + } + + public Integer getInconsistentCount() + { + return inconsistentCount; + } + + public void setInconsistentCount(Integer inconsistentCount) + { + this.inconsistentCount = inconsistentCount; + } + + public Integer getLateCount() + { + return lateCount; + } + + public void setLateCount(Integer lateCount) + { + this.lateCount = lateCount; + } + + public Double getLateRate() + { + return lateRate; + } + + public void setLateRate(Double lateRate) + { + this.lateRate = lateRate; + } + + public Double getOnTimeRate() + { + return onTimeRate; + } + + public void setOnTimeRate(Double onTimeRate) + { + this.onTimeRate = onTimeRate; + } + + public Boolean getTargetMet() + { + return targetMet; + } + + public void setTargetMet(Boolean targetMet) + { + this.targetMet = targetMet; + } + + public String getFirstArriveTime() + { + return firstArriveTime; + } + + public void setFirstArriveTime(String firstArriveTime) + { + this.firstArriveTime = firstArriveTime; + } + + public String getLastArriveTime() + { + return lastArriveTime; + } + + public void setLastArriveTime(String lastArriveTime) + { + this.lastArriveTime = lastArriveTime; + } + + public Integer getPlanStatus() + { + return planStatus; + } + + public void setPlanStatus(Integer planStatus) + { + this.planStatus = planStatus; + } + + public Double getGapToGlobal() + { + return gapToGlobal; + } + + public void setGapToGlobal(Double gapToGlobal) + { + this.gapToGlobal = gapToGlobal; + } + + public Boolean getOutlierFlag() + { + return outlierFlag; + } + + public void setOutlierFlag(Boolean outlierFlag) + { + this.outlierFlag = outlierFlag; + } + + public Boolean getConcentratedFlag() + { + return concentratedFlag; + } + + public void setConcentratedFlag(Boolean concentratedFlag) + { + this.concentratedFlag = concentratedFlag; + } + + public Boolean getVolatilityFlag() + { + return volatilityFlag; + } + + public void setVolatilityFlag(Boolean volatilityFlag) + { + this.volatilityFlag = volatilityFlag; + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryOverviewVO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryOverviewVO.java new file mode 100644 index 000000000..75a95fdea --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryOverviewVO.java @@ -0,0 +1,237 @@ +package com.ruoyi.train.domain.vo; + +import java.io.Serializable; + +/** + * 历史报到统计总览(不含到训耗时类指标)。 + * + *口径:已报到 = enrollment_status in (1,4) 且 join_time 非空; + * 应报到 = enrollment_status in (0,1,2,4)。
+ * + * @author ruoyi + */ +public class RegHistoryOverviewVO implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 参与统计的批次数 */ + private Integer batchCount = 0; + + /** 应报到人次 */ + private Integer expectedCount = 0; + + /** 已报到人次 */ + private Integer reportedCount = 0; + + /** 已报到去重人数(按 student_id) */ + private Integer reportedPersonCount = 0; + + /** 报到率(%) = 已报到 / 应报到 */ + private Double reportRate = 0D; + + /** 出勤报到率(%) = 已报到 / (待报到+已报到+已完成),请假不计入分母 */ + private Double attendanceRate = 0D; + + /** 待报到人次 */ + private Integer waitingCount = 0; + + /** 逾期未报到人次(status=0 且已超过报到截止基准) */ + private Integer waitingOverdueCount = 0; + + /** 请假人次 */ + private Integer leaveCount = 0; + + /** 状态与时间不一致人次(status in (1,4) 但 join_time 为空) */ + private Integer inconsistentCount = 0; + + /** 迟到人次 */ + private Integer lateCount = 0; + + /** 迟到率(%) = 迟到 / 已报到 */ + private Double lateRate = 0D; + + /** 准点率(%) = (已报到 - 迟到) / 已报到 */ + private Double onTimeRate = 0D; + + /** 数据质量异常人次(状态时间不一致、状态滞后、时间倒挂、未来时间等) */ + private Integer abnormalCount = 0; + + /** 因同批次同人重复档案被合并的记录数 */ + private Integer duplicateArchiveCount = 0; + + /** 目标报到率(%) */ + private Double targetRate = 0D; + + /** 达标批次数(批次报到率 >= 目标) */ + private Integer targetMetBatchCount = 0; + + public Integer getBatchCount() + { + return batchCount; + } + + public void setBatchCount(Integer batchCount) + { + this.batchCount = batchCount; + } + + public Integer getExpectedCount() + { + return expectedCount; + } + + public void setExpectedCount(Integer expectedCount) + { + this.expectedCount = expectedCount; + } + + public Integer getReportedCount() + { + return reportedCount; + } + + public void setReportedCount(Integer reportedCount) + { + this.reportedCount = reportedCount; + } + + public Integer getReportedPersonCount() + { + return reportedPersonCount; + } + + public void setReportedPersonCount(Integer reportedPersonCount) + { + this.reportedPersonCount = reportedPersonCount; + } + + public Double getReportRate() + { + return reportRate; + } + + public void setReportRate(Double reportRate) + { + this.reportRate = reportRate; + } + + public Double getAttendanceRate() + { + return attendanceRate; + } + + public void setAttendanceRate(Double attendanceRate) + { + this.attendanceRate = attendanceRate; + } + + public Integer getWaitingCount() + { + return waitingCount; + } + + public void setWaitingCount(Integer waitingCount) + { + this.waitingCount = waitingCount; + } + + public Integer getWaitingOverdueCount() + { + return waitingOverdueCount; + } + + public void setWaitingOverdueCount(Integer waitingOverdueCount) + { + this.waitingOverdueCount = waitingOverdueCount; + } + + public Integer getLeaveCount() + { + return leaveCount; + } + + public void setLeaveCount(Integer leaveCount) + { + this.leaveCount = leaveCount; + } + + public Integer getInconsistentCount() + { + return inconsistentCount; + } + + public void setInconsistentCount(Integer inconsistentCount) + { + this.inconsistentCount = inconsistentCount; + } + + public Integer getLateCount() + { + return lateCount; + } + + public void setLateCount(Integer lateCount) + { + this.lateCount = lateCount; + } + + public Double getLateRate() + { + return lateRate; + } + + public void setLateRate(Double lateRate) + { + this.lateRate = lateRate; + } + + public Double getOnTimeRate() + { + return onTimeRate; + } + + public void setOnTimeRate(Double onTimeRate) + { + this.onTimeRate = onTimeRate; + } + + public Integer getAbnormalCount() + { + return abnormalCount; + } + + public void setAbnormalCount(Integer abnormalCount) + { + this.abnormalCount = abnormalCount; + } + + public Integer getDuplicateArchiveCount() + { + return duplicateArchiveCount; + } + + public void setDuplicateArchiveCount(Integer duplicateArchiveCount) + { + this.duplicateArchiveCount = duplicateArchiveCount; + } + + public Double getTargetRate() + { + return targetRate; + } + + public void setTargetRate(Double targetRate) + { + this.targetRate = targetRate; + } + + public Integer getTargetMetBatchCount() + { + return targetMetBatchCount; + } + + public void setTargetMetBatchCount(Integer targetMetBatchCount) + { + this.targetMetBatchCount = targetMetBatchCount; + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryPageVO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryPageVO.java new file mode 100644 index 000000000..083a9a8ce --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/RegHistoryPageVO.java @@ -0,0 +1,54 @@ +package com.ruoyi.train.domain.vo; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 内存分页结果(用于历史报到统计的分组表与明细表)。 + * + *统计口径要求先全量聚合再分页,因此不复用 PageHelper 的 SQL 分页。
+ * + * @param仅查询 train_enrollment / train_class_plan / train_student / sys_dept, + * 不访问 train_class_report,也不写入任何表。
+ * + * @author ruoyi + */ +public interface TrainRegHistoryStatMapper +{ + /** + * 查询历史报到统计事实行(扁平投影,聚合在 Java 侧完成)。 + * + * @param query 过滤条件 + * @return 事实行集合 + */ + List统计口径:已报到 = enrollment_status in (1,4) 且 join_time 非空; + * 不使用 train_class_report;不计算耗时类指标。
+ * + * @author ruoyi + */ +public interface ITrainRegHistoryStatService +{ + /** + * 总览统计。 + * + * @param query 过滤条件 + * @return 总览 + */ + RegHistoryOverviewVO selectOverview(RegHistoryStatQuery query); + + /** + * 分组汇总(内存分页)。 + * + * @param query 过滤条件与分组维度 + * @return 分组行分页结果 + */ + RegHistoryPageVO与既有 {@code TrainRegDataServiceImpl} 完全隔离:独立 Mapper、独立口径、独立接口, + * 不修改任何既有统计方法的返回值。
+ * + * @author ruoyi + */ +@Service +public class TrainRegHistoryStatServiceImpl implements ITrainRegHistoryStatService +{ + /** 单次取数上限 */ + private static final int MAX_LIMIT = 50000; + + /** 单页上限 */ + private static final int MAX_PAGE_SIZE = 200; + + /** 默认页大小 */ + private static final int DEFAULT_PAGE_SIZE = 10; + + @Autowired + private TrainRegHistoryStatMapper trainRegHistoryStatMapper; + + @Override + public RegHistoryOverviewVO selectOverview(RegHistoryStatQuery query) + { + RegHistoryStatQuery normalized = normalize(query); + RegHistoryStatCalculator.Dataset dataset = loadDataset(normalized); + RegHistoryOverviewVO overview = RegHistoryStatCalculator.buildOverview(dataset.getRows(), + RegHistoryStatCalculator.DEFAULT_TARGET_RATE); + overview.setDuplicateArchiveCount(dataset.getDuplicateRemovedCount()); + return overview; + } + + @Override + public RegHistoryPageVO口径(固定)
+ *本类与既有 {@code TrainRegDataServiceImpl} 的统计逻辑完全独立,仅被新增的 + * 历史报到统计服务调用。
+ * + * @author ruoyi + */ +public final class RegHistoryStatCalculator +{ + /** 待报到 */ + public static final int STATUS_WAITING = 0; + + /** 已报到 */ + public static final int STATUS_REPORTED = 1; + + /** 请假 */ + public static final int STATUS_LEAVE = 2; + + /** 放弃(不计入应报到) */ + public static final int STATUS_ABANDONED = 3; + + /** 已完成(视为已报到) */ + public static final int STATUS_COMPLETED = 4; + + /** 默认目标报到率(%) */ + public static final double DEFAULT_TARGET_RATE = 90D; + + public static final String GROUP_BATCH = "batch"; + public static final String GROUP_DEPT = "dept"; + public static final String GROUP_MAJOR = "major"; + public static final String GROUP_PLACE = "place"; + public static final String GROUP_GENDER = "gender"; + public static final String GROUP_AGE = "ageBand"; + public static final String GROUP_ACCOMMODATION = "accommodation"; + public static final String GROUP_FIRST_TIME = "firstTime"; + + public static final String GRANULARITY_DAY = "day"; + public static final String GRANULARITY_WEEK = "week"; + public static final String GRANULARITY_MONTH = "month"; + public static final String GRANULARITY_HOUR = "hour"; + + /** 集中报到判定:单小时最少人数 */ + private static final int CONCENTRATED_MIN_COUNT = 10; + + /** 集中报到判定:单小时占已报到最小比例 */ + private static final double CONCENTRATED_RATIO = 0.5D; + + /** 报到率波动阈值(百分点) */ + private static final double VOLATILITY_PP = 10D; + + /** 单位离群阈值(百分点) */ + private static final double OUTLIER_PP = 20D; + + /** 单位离群最小应报到样本 */ + private static final int OUTLIER_MIN_EXPECTED = 10; + + /** 波动对比回看期数 */ + private static final int VOLATILITY_LOOKBACK = 6; + + private static final Set去重键 = plan_id + student_id(student_id 为空时退化为 enrollment_id); + * 保留优先级:已报到(1/4 且 join_time 非空) > 已完成(4) > 已报到(1) > 其他, + * 同级取 enrollment_id 较大者。
+ */ + public static Dataset prepare(List编码沿用系统字典 {@code sys_user_sex} / {@code sys_user_sex2} 的约定: + * 0 = 男,1 = 女,2 = 未知。历史数据中同时存在 {@code "0"/"1"}、 + * {@code "男"/"女"}、{@code "M"/"F"} 等写法,统一收敛到 0/1/2, + * 避免同一性别因写法不同被拆成多个统计项。
+ * + * @param raw 学员主档 s_gender 原始值 + * @return 归一化编码:0/1/2,或无法识别的原始值 + */ + private static String normalizeGenderKey(String raw) + { + String value = raw == null ? "" : raw.trim(); + if (value.isEmpty()) + { + return "2"; + } + if ("0".equals(value) || "男".equals(value) || "M".equalsIgnoreCase(value) || "male".equalsIgnoreCase(value)) + { + return "0"; + } + if ("1".equals(value) || "女".equals(value) || "F".equalsIgnoreCase(value) || "female".equalsIgnoreCase(value)) + { + return "1"; + } + if ("2".equals(value) || "未知".equals(value) || "U".equalsIgnoreCase(value) + || "unknown".equalsIgnoreCase(value)) + { + return "2"; + } + return value; + } + + /** + * 性别编码转展示文本:0 = 男,1 = 女,2 = 未知;其他编码原样返回(保持编码与含义对应)。 + */ + private static String genderLabel(String key) + { + if ("0".equals(key)) + { + return "男"; + } + if ("1".equals(key)) + { + return "女"; + } + if ("2".equals(key)) + { + return UNKNOWN; + } + return key == null ? UNKNOWN : key; + } + + private static String textOr(String value, String fallback) + { + return value == null || value.trim().isEmpty() ? fallback : value.trim(); + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainRegHistoryStatMapper.xml b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainRegHistoryStatMapper.xml new file mode 100644 index 000000000..6d6daa748 --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainRegHistoryStatMapper.xml @@ -0,0 +1,80 @@ + + +保护三条硬约束:
+ *覆盖:已报到判定、放弃过滤、去重优先级、报到率分母、请假口径、 + * 迟到基准回退、分组计数、时间分桶、明细异常标记、参数归一化。
+ * + * @author ruoyi + */ +public class RegHistoryStatCalculatorTest +{ + @Test + public void reportedStatusRequiresStatus1Or4AndJoinTime() + { + assertTrue(RegHistoryStatCalculator.isReported(1, date("2026-03-01 09:00"))); + assertTrue(RegHistoryStatCalculator.isReported(4, date("2026-03-01 09:00"))); + assertFalse("状态为已报到但报到时间为空,不得计为已报到", + RegHistoryStatCalculator.isReported(1, null)); + assertFalse(RegHistoryStatCalculator.isReported(0, date("2026-03-01 09:00"))); + assertFalse(RegHistoryStatCalculator.isReported(2, date("2026-03-01 09:00"))); + assertFalse(RegHistoryStatCalculator.isExpected(3)); + assertTrue(RegHistoryStatCalculator.isExpected(0)); + assertTrue(RegHistoryStatCalculator.isExpected(1)); + assertTrue(RegHistoryStatCalculator.isExpected(2)); + assertTrue(RegHistoryStatCalculator.isExpected(4)); + } + + @Test + public void prepareFiltersAbandonedAndDeduplicatesByPlanAndStudent() + { + List