新增分析:实现管理驾驶舱聚合接口与可视化
This commit is contained in:
1 parent
992791b7bd
commit
f17526b409
23 files changed
+3077
-23
No files matched your search
+83
@@ -0,0 +1,83 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
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.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.train.domain.analysis.AttendanceChartsVo;
|
||||
import com.ruoyi.train.domain.analysis.AttendanceFilterDto;
|
||||
import com.ruoyi.train.domain.analysis.AttendanceSummaryVo;
|
||||
import com.ruoyi.train.domain.analysis.DashboardChartsVo;
|
||||
import com.ruoyi.train.domain.analysis.DashboardOverviewVo;
|
||||
import com.ruoyi.train.domain.analysis.Student360Vo;
|
||||
import com.ruoyi.train.service.ITrainAnalysisService;
|
||||
|
||||
/**
|
||||
* 数据分析与可视化 Controller
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/train/analysis")
|
||||
public class TrainAnalysisController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private ITrainAnalysisService analysisService;
|
||||
|
||||
/**
|
||||
* 获取管理驾驶舱宏观概览与业务提醒
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:analysis:dashboard')")
|
||||
@GetMapping("/dashboard/overview")
|
||||
public AjaxResult getDashboardOverview()
|
||||
{
|
||||
DashboardOverviewVo vo = analysisService.getDashboardOverview();
|
||||
return success(vo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取管理驾驶舱图表数据
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:analysis:dashboard')")
|
||||
@GetMapping("/dashboard/charts")
|
||||
public AjaxResult getDashboardCharts()
|
||||
{
|
||||
DashboardChartsVo vo = analysisService.getDashboardCharts();
|
||||
return success(vo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取考勤专题分析汇总 KPI
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:analysis:attendance')")
|
||||
@GetMapping("/attendance/summary")
|
||||
public AjaxResult getAttendanceSummary(AttendanceFilterDto filter)
|
||||
{
|
||||
AttendanceSummaryVo vo = analysisService.getAttendanceSummary(filter);
|
||||
return success(vo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取考勤专题分析图表集合
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:analysis:attendance')")
|
||||
@GetMapping("/attendance/charts")
|
||||
public AjaxResult getAttendanceCharts(AttendanceFilterDto filter)
|
||||
{
|
||||
AttendanceChartsVo vo = analysisService.getAttendanceCharts(filter);
|
||||
return success(vo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定学员 360 客观全景画像 (复用现有 train:portrait:query 权限)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:portrait:query')")
|
||||
@GetMapping("/portrait/student-360/{studentId}")
|
||||
public AjaxResult getStudent360(@PathVariable("studentId") String studentId)
|
||||
{
|
||||
Student360Vo vo = analysisService.getStudent360(studentId);
|
||||
return success(vo);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import com.ruoyi.train.domain.analysis.AttendanceFilterDto;
|
||||
|
||||
/**
|
||||
* 数据分析与可视化 Controller 契约与权限边界测试
|
||||
*/
|
||||
public class TrainAnalysisControllerContractTest
|
||||
{
|
||||
@Test
|
||||
public void shouldExposeAllFiveAnalyticsEndpointsAndCorrectPermissions() throws Exception
|
||||
{
|
||||
// 1. Controller 根路径
|
||||
RequestMapping root = TrainAnalysisController.class.getAnnotation(RequestMapping.class);
|
||||
assertNotNull(root);
|
||||
assertEquals("/train/analysis", root.value()[0]);
|
||||
|
||||
// 2. 驾驶舱概览
|
||||
Method overview = TrainAnalysisController.class.getMethod("getDashboardOverview");
|
||||
assertNotNull(overview.getAnnotation(GetMapping.class));
|
||||
assertEquals("/dashboard/overview", overview.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:analysis:dashboard')", overview.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 3. 驾驶舱图表
|
||||
Method dashCharts = TrainAnalysisController.class.getMethod("getDashboardCharts");
|
||||
assertNotNull(dashCharts.getAnnotation(GetMapping.class));
|
||||
assertEquals("/dashboard/charts", dashCharts.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:analysis:dashboard')", dashCharts.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 4. 考勤汇总 KPI
|
||||
Method attSummary = TrainAnalysisController.class.getMethod("getAttendanceSummary", AttendanceFilterDto.class);
|
||||
assertNotNull(attSummary.getAnnotation(GetMapping.class));
|
||||
assertEquals("/attendance/summary", attSummary.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:analysis:attendance')", attSummary.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 5. 考勤图表集合
|
||||
Method attCharts = TrainAnalysisController.class.getMethod("getAttendanceCharts", AttendanceFilterDto.class);
|
||||
assertNotNull(attCharts.getAnnotation(GetMapping.class));
|
||||
assertEquals("/attendance/charts", attCharts.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:analysis:attendance')", attCharts.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 6. 学员 360 画像 (复用现有 train:portrait:query)
|
||||
Method s360 = TrainAnalysisController.class.getMethod("getStudent360", String.class);
|
||||
assertNotNull(s360.getAnnotation(GetMapping.class));
|
||||
assertEquals("/portrait/student-360/{studentId}", s360.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:portrait:query')", s360.getAnnotation(PreAuthorize.class).value());
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.ruoyi.train.domain.analysis;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 考勤异常学员明细 DTO
|
||||
*/
|
||||
public class AbnormalStudentDto implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String studentId;
|
||||
private String studentName;
|
||||
private String planId;
|
||||
private String planName;
|
||||
private Long absentCount;
|
||||
private Long lateCount;
|
||||
private Long earlyLeaveCount;
|
||||
private Long leaveCount;
|
||||
private Long noCheckinCount;
|
||||
private Long totalAbnormalCount;
|
||||
|
||||
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 getPlanId() { return planId; }
|
||||
public void setPlanId(String planId) { this.planId = planId; }
|
||||
|
||||
public String getPlanName() { return planName; }
|
||||
public void setPlanName(String planName) { this.planName = planName; }
|
||||
|
||||
public Long getAbsentCount() { return absentCount; }
|
||||
public void setAbsentCount(Long absentCount) { this.absentCount = absentCount; }
|
||||
|
||||
public Long getLateCount() { return lateCount; }
|
||||
public void setLateCount(Long lateCount) { this.lateCount = lateCount; }
|
||||
|
||||
public Long getEarlyLeaveCount() { return earlyLeaveCount; }
|
||||
public void setEarlyLeaveCount(Long earlyLeaveCount) { this.earlyLeaveCount = earlyLeaveCount; }
|
||||
|
||||
public Long getLeaveCount() { return leaveCount; }
|
||||
public void setLeaveCount(Long leaveCount) { this.leaveCount = leaveCount; }
|
||||
|
||||
public Long getNoCheckinCount() { return noCheckinCount; }
|
||||
public void setNoCheckinCount(Long noCheckinCount) { this.noCheckinCount = noCheckinCount; }
|
||||
|
||||
public Long getTotalAbnormalCount() { return totalAbnormalCount; }
|
||||
public void setTotalAbnormalCount(Long totalAbnormalCount) { this.totalAbnormalCount = totalAbnormalCount; }
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.ruoyi.train.domain.analysis;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 考勤专题分析图表集合 VO
|
||||
*/
|
||||
public class AttendanceChartsVo implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 1. 每日考勤趋势折线图 */
|
||||
private List<DailyTrendDto> dailyTrend;
|
||||
|
||||
/** 2. 考勤状态环形分布 (正常, 迟到, 早退, 请假, 明确缺勤, 无签到记录) */
|
||||
private List<NameValueDto> statusDistribution;
|
||||
|
||||
/** 3. 培训计划出勤对比 (横向柱状图 Top 10) */
|
||||
private List<CompareItemDto> planComparison;
|
||||
|
||||
/** 4. 课程考勤对比 (横向柱状图 Top 10) */
|
||||
private List<CompareItemDto> courseComparison;
|
||||
|
||||
/** 5. 异常学员 Top 10 列表与柱图 */
|
||||
private List<AbnormalStudentDto> abnormalStudentTop10;
|
||||
|
||||
public List<DailyTrendDto> getDailyTrend() { return dailyTrend; }
|
||||
public void setDailyTrend(List<DailyTrendDto> dailyTrend) { this.dailyTrend = dailyTrend; }
|
||||
|
||||
public List<NameValueDto> getStatusDistribution() { return statusDistribution; }
|
||||
public void setStatusDistribution(List<NameValueDto> statusDistribution) { this.statusDistribution = statusDistribution; }
|
||||
|
||||
public List<CompareItemDto> getPlanComparison() { return planComparison; }
|
||||
public void setPlanComparison(List<CompareItemDto> planComparison) { this.planComparison = planComparison; }
|
||||
|
||||
public List<CompareItemDto> getCourseComparison() { return courseComparison; }
|
||||
public void setCourseComparison(List<CompareItemDto> courseComparison) { this.courseComparison = courseComparison; }
|
||||
|
||||
public List<AbnormalStudentDto> getAbnormalStudentTop10() { return abnormalStudentTop10; }
|
||||
public void setAbnormalStudentTop10(List<AbnormalStudentDto> abnormalStudentTop10) { this.abnormalStudentTop10 = abnormalStudentTop10; }
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.ruoyi.train.domain.analysis;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 考勤专题分析通用查询入参 DTO
|
||||
*/
|
||||
public class AttendanceFilterDto implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 开始日期 (格式: yyyy-MM-dd) */
|
||||
private String startDate;
|
||||
|
||||
/** 结束日期 (格式: yyyy-MM-dd) */
|
||||
private String endDate;
|
||||
|
||||
/** 培训计划ID */
|
||||
private String planId;
|
||||
|
||||
/** 课程ID */
|
||||
private String courseId;
|
||||
|
||||
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 String getPlanId() { return planId; }
|
||||
public void setPlanId(String planId) { this.planId = planId; }
|
||||
|
||||
public String getCourseId() { return courseId; }
|
||||
public void setCourseId(String courseId) { this.courseId = courseId; }
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.ruoyi.train.domain.analysis;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 考勤专题分析汇总 KPI VO
|
||||
*/
|
||||
public class AttendanceSummaryVo implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 累计已开课次 */
|
||||
private Long lessonTotal;
|
||||
|
||||
/** 应到总人次 */
|
||||
private Long expectedAttendTotal;
|
||||
|
||||
/** 正常出勤人次 */
|
||||
private Long normalAttendTotal;
|
||||
|
||||
/** 异常考勤人次 (迟到/早退/请假/缺勤/无签到记录) */
|
||||
private Long abnormalAttendTotal;
|
||||
|
||||
/** 综合出勤率 (百分比,如 92.5) */
|
||||
private Double attendanceRate;
|
||||
|
||||
/** 业务口径状态警示提示 */
|
||||
private String metricDefinitionNotice = "BUSINESS_METRIC_DEFINITION_PENDING_CONFIRMATION: 当前出勤率分母采用开课所属班级参训学员应到人次,业务最终标准待甲方确认";
|
||||
|
||||
public Long getLessonTotal() { return lessonTotal; }
|
||||
public void setLessonTotal(Long lessonTotal) { this.lessonTotal = lessonTotal; }
|
||||
|
||||
public Long getExpectedAttendTotal() { return expectedAttendTotal; }
|
||||
public void setExpectedAttendTotal(Long expectedAttendTotal) { this.expectedAttendTotal = expectedAttendTotal; }
|
||||
|
||||
public Long getNormalAttendTotal() { return normalAttendTotal; }
|
||||
public void setNormalAttendTotal(Long normalAttendTotal) { this.normalAttendTotal = normalAttendTotal; }
|
||||
|
||||
public Long getAbnormalAttendTotal() { return abnormalAttendTotal; }
|
||||
public void setAbnormalAttendTotal(Long abnormalAttendTotal) { this.abnormalAttendTotal = abnormalAttendTotal; }
|
||||
|
||||
public Double getAttendanceRate() { return attendanceRate; }
|
||||
public void setAttendanceRate(Double attendanceRate) { this.attendanceRate = attendanceRate; }
|
||||
|
||||
public String getMetricDefinitionNotice() { return metricDefinitionNotice; }
|
||||
public void setMetricDefinitionNotice(String metricDefinitionNotice) { this.metricDefinitionNotice = metricDefinitionNotice; }
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.ruoyi.train.domain.analysis;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 计划或课程横向对比项 DTO
|
||||
*/
|
||||
public class CompareItemDto implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private Long expectedCount;
|
||||
private Long normalCount;
|
||||
private Long abnormalCount;
|
||||
private Double attendanceRate;
|
||||
|
||||
public CompareItemDto() {}
|
||||
|
||||
public CompareItemDto(String id, String name, Long expectedCount, Long normalCount, Long abnormalCount, Double attendanceRate)
|
||||
{
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.expectedCount = expectedCount;
|
||||
this.normalCount = normalCount;
|
||||
this.abnormalCount = abnormalCount;
|
||||
this.attendanceRate = attendanceRate;
|
||||
}
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public Long getExpectedCount() { return expectedCount; }
|
||||
public void setExpectedCount(Long expectedCount) { this.expectedCount = expectedCount; }
|
||||
|
||||
public Long getNormalCount() { return normalCount; }
|
||||
public void setNormalCount(Long normalCount) { this.normalCount = normalCount; }
|
||||
|
||||
public Long getAbnormalCount() { return abnormalCount; }
|
||||
public void setAbnormalCount(Long abnormalCount) { this.abnormalCount = abnormalCount; }
|
||||
|
||||
public Double getAttendanceRate() { return attendanceRate; }
|
||||
public void setAttendanceRate(Double attendanceRate) { this.attendanceRate = attendanceRate; }
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.ruoyi.train.domain.analysis;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 每日考勤趋势 DTO
|
||||
*/
|
||||
public class DailyTrendDto implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String date;
|
||||
private Long normalCount;
|
||||
private Long abnormalCount;
|
||||
private Double attendanceRate;
|
||||
|
||||
public DailyTrendDto() {}
|
||||
|
||||
public DailyTrendDto(String date, Long normalCount, Long abnormalCount, Double attendanceRate)
|
||||
{
|
||||
this.date = date;
|
||||
this.normalCount = normalCount;
|
||||
this.abnormalCount = abnormalCount;
|
||||
this.attendanceRate = attendanceRate;
|
||||
}
|
||||
|
||||
public String getDate() { return date; }
|
||||
public void setDate(String date) { this.date = date; }
|
||||
|
||||
public Long getNormalCount() { return normalCount; }
|
||||
public void setNormalCount(Long normalCount) { this.normalCount = normalCount; }
|
||||
|
||||
public Long getAbnormalCount() { return abnormalCount; }
|
||||
public void setAbnormalCount(Long abnormalCount) { this.abnormalCount = abnormalCount; }
|
||||
|
||||
public Double getAttendanceRate() { return attendanceRate; }
|
||||
public void setAttendanceRate(Double attendanceRate) { this.attendanceRate = attendanceRate; }
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.ruoyi.train.domain.analysis;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理驾驶舱图表数据集合 VO
|
||||
*/
|
||||
public class DashboardChartsVo implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 1. 近30天考勤走势:日期横坐标 */
|
||||
private List<String> trendDates;
|
||||
|
||||
/** 近30天考勤走势:正常签到人次 */
|
||||
private List<Long> trendNormalCounts;
|
||||
|
||||
/** 近30天考勤走势:异常考勤人次 */
|
||||
private List<Long> trendAbnormalCounts;
|
||||
|
||||
/** 2. 培训计划参训/报到对比:计划名称列表 (Top 10) */
|
||||
private List<String> planComparisonNames;
|
||||
|
||||
/** 培训计划参训人数 */
|
||||
private List<Long> planEnrolledCounts;
|
||||
|
||||
/** 培训计划报到人数 */
|
||||
private List<Long> planReportedCounts;
|
||||
|
||||
/** 3. 考勤状态全局构成 (正常, 迟到, 早退, 请假, 明确缺勤, 无签到记录) */
|
||||
private List<NameValueDto> attendanceStatusDistribution;
|
||||
|
||||
/** 4. 教学场地使用频次与课时 Top 5 */
|
||||
private List<VenueUsageDto> venueUsageTop5;
|
||||
|
||||
public List<String> getTrendDates() { return trendDates; }
|
||||
public void setTrendDates(List<String> trendDates) { this.trendDates = trendDates; }
|
||||
|
||||
public List<Long> getTrendNormalCounts() { return trendNormalCounts; }
|
||||
public void setTrendNormalCounts(List<Long> trendNormalCounts) { this.trendNormalCounts = trendNormalCounts; }
|
||||
|
||||
public List<Long> getTrendAbnormalCounts() { return trendAbnormalCounts; }
|
||||
public void setTrendAbnormalCounts(List<Long> trendAbnormalCounts) { this.trendAbnormalCounts = trendAbnormalCounts; }
|
||||
|
||||
public List<String> getPlanComparisonNames() { return planComparisonNames; }
|
||||
public void setPlanComparisonNames(List<String> planComparisonNames) { this.planComparisonNames = planComparisonNames; }
|
||||
|
||||
public List<Long> getPlanEnrolledCounts() { return planEnrolledCounts; }
|
||||
public void setPlanEnrolledCounts(List<Long> planEnrolledCounts) { this.planEnrolledCounts = planEnrolledCounts; }
|
||||
|
||||
public List<Long> getPlanReportedCounts() { return planReportedCounts; }
|
||||
public void setPlanReportedCounts(List<Long> planReportedCounts) { this.planReportedCounts = planReportedCounts; }
|
||||
|
||||
public List<NameValueDto> getAttendanceStatusDistribution() { return attendanceStatusDistribution; }
|
||||
public void setAttendanceStatusDistribution(List<NameValueDto> attendanceStatusDistribution) { this.attendanceStatusDistribution = attendanceStatusDistribution; }
|
||||
|
||||
public List<VenueUsageDto> getVenueUsageTop5() { return venueUsageTop5; }
|
||||
public void setVenueUsageTop5(List<VenueUsageDto> venueUsageTop5) { this.venueUsageTop5 = venueUsageTop5; }
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package com.ruoyi.train.domain.analysis;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 管理驾驶舱宏观概览 VO
|
||||
*/
|
||||
public class DashboardOverviewVo implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 学员总数 */
|
||||
private Long studentTotal;
|
||||
|
||||
/** 教师总数 */
|
||||
private Long teacherTotal;
|
||||
|
||||
/** 课程总数 */
|
||||
private Long courseTotal;
|
||||
|
||||
/** 培训计划总数 */
|
||||
private Long planTotal;
|
||||
|
||||
/** 累计排课课次 */
|
||||
private Long lessonTotal;
|
||||
|
||||
/** 累计参训人数 */
|
||||
private Long enrollmentTotal;
|
||||
|
||||
/** 累计报到人数 */
|
||||
private Long reportTotal;
|
||||
|
||||
/** 当前在宿人数 */
|
||||
private Long accommodationCurrent;
|
||||
|
||||
/** 培训计划状态分布 (未开始, 进行中, 已结束, 已取消) */
|
||||
private Map<String, Long> planStatusCounts;
|
||||
|
||||
/** 待办提醒:异常考勤人次 */
|
||||
private Long abnormalAttendanceCount;
|
||||
|
||||
/** 待办提醒:待报到人数 */
|
||||
private Long pendingReportCount;
|
||||
|
||||
/** 待办提醒:住宿预分配人数 */
|
||||
private Long preallocatedAccommodationCount;
|
||||
|
||||
/** 待办提醒:门禁异常授权数 */
|
||||
private Long abnormalAuthCount;
|
||||
|
||||
public Long getStudentTotal() { return studentTotal; }
|
||||
public void setStudentTotal(Long studentTotal) { this.studentTotal = studentTotal; }
|
||||
|
||||
public Long getTeacherTotal() { return teacherTotal; }
|
||||
public void setTeacherTotal(Long teacherTotal) { this.teacherTotal = teacherTotal; }
|
||||
|
||||
public Long getCourseTotal() { return courseTotal; }
|
||||
public void setCourseTotal(Long courseTotal) { this.courseTotal = courseTotal; }
|
||||
|
||||
public Long getPlanTotal() { return planTotal; }
|
||||
public void setPlanTotal(Long planTotal) { this.planTotal = planTotal; }
|
||||
|
||||
public Long getLessonTotal() { return lessonTotal; }
|
||||
public void setLessonTotal(Long lessonTotal) { this.lessonTotal = lessonTotal; }
|
||||
|
||||
public Long getEnrollmentTotal() { return enrollmentTotal; }
|
||||
public void setEnrollmentTotal(Long enrollmentTotal) { this.enrollmentTotal = enrollmentTotal; }
|
||||
|
||||
public Long getReportTotal() { return reportTotal; }
|
||||
public void setReportTotal(Long reportTotal) { this.reportTotal = reportTotal; }
|
||||
|
||||
public Long getAccommodationCurrent() { return accommodationCurrent; }
|
||||
public void setAccommodationCurrent(Long accommodationCurrent) { this.accommodationCurrent = accommodationCurrent; }
|
||||
|
||||
public Map<String, Long> getPlanStatusCounts() { return planStatusCounts; }
|
||||
public void setPlanStatusCounts(Map<String, Long> planStatusCounts) { this.planStatusCounts = planStatusCounts; }
|
||||
|
||||
public Long getAbnormalAttendanceCount() { return abnormalAttendanceCount; }
|
||||
public void setAbnormalAttendanceCount(Long abnormalAttendanceCount) { this.abnormalAttendanceCount = abnormalAttendanceCount; }
|
||||
|
||||
public Long getPendingReportCount() { return pendingReportCount; }
|
||||
public void setPendingReportCount(Long pendingReportCount) { this.pendingReportCount = pendingReportCount; }
|
||||
|
||||
public Long getPreallocatedAccommodationCount() { return preallocatedAccommodationCount; }
|
||||
public void setPreallocatedAccommodationCount(Long preallocatedAccommodationCount) { this.preallocatedAccommodationCount = preallocatedAccommodationCount; }
|
||||
|
||||
public Long getAbnormalAuthCount() { return abnormalAuthCount; }
|
||||
public void setAbnormalAuthCount(Long abnormalAuthCount) { this.abnormalAuthCount = abnormalAuthCount; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ruoyi.train.domain.analysis;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 通用名称数值键值对 DTO
|
||||
*/
|
||||
public class NameValueDto implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String name;
|
||||
private Long value;
|
||||
|
||||
public NameValueDto() {}
|
||||
|
||||
public NameValueDto(String name, Long value)
|
||||
{
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public Long getValue() { return value; }
|
||||
public void setValue(Long value) { this.value = value; }
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
package com.ruoyi.train.domain.analysis;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 学员 360 客观全景画像 VO
|
||||
*/
|
||||
public class Student360Vo implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/* ========== 1. 基础主档 (敏感信息脱敏) ========== */
|
||||
private String studentId;
|
||||
private String studentName;
|
||||
private String gender;
|
||||
private String genderName;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date birth;
|
||||
|
||||
private String phone;
|
||||
private String email;
|
||||
private String orgName;
|
||||
private String idCardMasked;
|
||||
private String userBindingStatus;
|
||||
private Integer status;
|
||||
|
||||
/* ========== 2. 培训履历时间轴 ========== */
|
||||
private List<StudentEnrollmentTimelineDto> enrollmentTimeline;
|
||||
|
||||
/* ========== 3. 考勤客观纪律画像 ========== */
|
||||
private Long expectedAttendCount;
|
||||
private Long normalCount;
|
||||
private Long lateCount;
|
||||
private Long earlyCount;
|
||||
private Long leaveCount;
|
||||
private Long absentCount;
|
||||
private Long noCheckinCount;
|
||||
private Double attendanceRate;
|
||||
private String metricDefinitionNotice = "BUSINESS_METRIC_DEFINITION_PENDING_CONFIRMATION: 个人出勤率采用学员所属班级已开课次应到数为分母,待甲方最终确认";
|
||||
|
||||
/* ========== 4. 成绩状态 (严格不造假) ========== */
|
||||
private Boolean hasScoreData = false;
|
||||
private String scoreStatusDesc = "暂无成绩数据 (V2成绩表待考务录入)";
|
||||
|
||||
/* ========== 5. 自主学习状态 (严格不造假) ========== */
|
||||
private Boolean hasStudyData = false;
|
||||
private String studyStatusDesc = "暂无自主学习记录 / 待外部平台数据同步";
|
||||
|
||||
/* ========== 6. 住宿与门禁客观状态 ========== */
|
||||
private Boolean hasAccommodation = false;
|
||||
private String dormId;
|
||||
private String roomName;
|
||||
private String bedNo;
|
||||
private Integer accommodationStatus;
|
||||
private String accommodationStatusName;
|
||||
private String authStatus;
|
||||
private String authStatusName;
|
||||
|
||||
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 getGender() { return gender; }
|
||||
public void setGender(String gender) { this.gender = gender; }
|
||||
|
||||
public String getGenderName() { return genderName; }
|
||||
public void setGenderName(String genderName) { this.genderName = genderName; }
|
||||
|
||||
public Date getBirth() { return birth; }
|
||||
public void setBirth(Date birth) { this.birth = birth; }
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getOrgName() { return orgName; }
|
||||
public void setOrgName(String orgName) { this.orgName = orgName; }
|
||||
|
||||
public String getIdCardMasked() { return idCardMasked; }
|
||||
public void setIdCardMasked(String idCardMasked) { this.idCardMasked = idCardMasked; }
|
||||
|
||||
public String getUserBindingStatus() { return userBindingStatus; }
|
||||
public void setUserBindingStatus(String userBindingStatus) { this.userBindingStatus = userBindingStatus; }
|
||||
|
||||
public Integer getStatus() { return status; }
|
||||
public void setStatus(Integer status) { this.status = status; }
|
||||
|
||||
public List<StudentEnrollmentTimelineDto> getEnrollmentTimeline() { return enrollmentTimeline; }
|
||||
public void setEnrollmentTimeline(List<StudentEnrollmentTimelineDto> enrollmentTimeline) { this.enrollmentTimeline = enrollmentTimeline; }
|
||||
|
||||
public Long getExpectedAttendCount() { return expectedAttendCount; }
|
||||
public void setExpectedAttendCount(Long expectedAttendCount) { this.expectedAttendCount = expectedAttendCount; }
|
||||
|
||||
public Long getNormalCount() { return normalCount; }
|
||||
public void setNormalCount(Long normalCount) { this.normalCount = normalCount; }
|
||||
|
||||
public Long getLateCount() { return lateCount; }
|
||||
public void setLateCount(Long lateCount) { this.lateCount = lateCount; }
|
||||
|
||||
public Long getEarlyCount() { return earlyCount; }
|
||||
public void setEarlyCount(Long earlyCount) { this.earlyCount = earlyCount; }
|
||||
|
||||
public Long getLeaveCount() { return leaveCount; }
|
||||
public void setLeaveCount(Long leaveCount) { this.leaveCount = leaveCount; }
|
||||
|
||||
public Long getAbsentCount() { return absentCount; }
|
||||
public void setAbsentCount(Long absentCount) { this.absentCount = absentCount; }
|
||||
|
||||
public Long getNoCheckinCount() { return noCheckinCount; }
|
||||
public void setNoCheckinCount(Long noCheckinCount) { this.noCheckinCount = noCheckinCount; }
|
||||
|
||||
public Double getAttendanceRate() { return attendanceRate; }
|
||||
public void setAttendanceRate(Double attendanceRate) { this.attendanceRate = attendanceRate; }
|
||||
|
||||
public String getMetricDefinitionNotice() { return metricDefinitionNotice; }
|
||||
public void setMetricDefinitionNotice(String metricDefinitionNotice) { this.metricDefinitionNotice = metricDefinitionNotice; }
|
||||
|
||||
public Boolean getHasScoreData() { return hasScoreData; }
|
||||
public void setHasScoreData(Boolean hasScoreData) { this.hasScoreData = hasScoreData; }
|
||||
|
||||
public String getScoreStatusDesc() { return scoreStatusDesc; }
|
||||
public void setScoreStatusDesc(String scoreStatusDesc) { this.scoreStatusDesc = scoreStatusDesc; }
|
||||
|
||||
public Boolean getHasStudyData() { return hasStudyData; }
|
||||
public void setHasStudyData(Boolean hasStudyData) { this.hasStudyData = hasStudyData; }
|
||||
|
||||
public String getStudyStatusDesc() { return studyStatusDesc; }
|
||||
public void setStudyStatusDesc(String studyStatusDesc) { this.studyStatusDesc = studyStatusDesc; }
|
||||
|
||||
public Boolean getHasAccommodation() { return hasAccommodation; }
|
||||
public void setHasAccommodation(Boolean hasAccommodation) { this.hasAccommodation = hasAccommodation; }
|
||||
|
||||
public String getDormId() { return dormId; }
|
||||
public void setDormId(String dormId) { this.dormId = dormId; }
|
||||
|
||||
public String getRoomName() { return roomName; }
|
||||
public void setRoomName(String roomName) { this.roomName = roomName; }
|
||||
|
||||
public String getBedNo() { return bedNo; }
|
||||
public void setBedNo(String bedNo) { this.bedNo = bedNo; }
|
||||
|
||||
public Integer getAccommodationStatus() { return accommodationStatus; }
|
||||
public void setAccommodationStatus(Integer accommodationStatus) { this.accommodationStatus = accommodationStatus; }
|
||||
|
||||
public String getAccommodationStatusName() { return accommodationStatusName; }
|
||||
public void setAccommodationStatusName(String accommodationStatusName) { this.accommodationStatusName = accommodationStatusName; }
|
||||
|
||||
public String getAuthStatus() { return authStatus; }
|
||||
public void setAuthStatus(String authStatus) { this.authStatus = authStatus; }
|
||||
|
||||
public String getAuthStatusName() { return authStatusName; }
|
||||
public void setAuthStatusName(String authStatusName) { this.authStatusName = authStatusName; }
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.ruoyi.train.domain.analysis;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
* 学员参训履历时间轴 DTO
|
||||
*/
|
||||
public class StudentEnrollmentTimelineDto implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long enrollmentId;
|
||||
private String planId;
|
||||
private String planName;
|
||||
private Integer enrollmentStatus;
|
||||
private String enrollmentStatusName;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date joinTime;
|
||||
|
||||
private Integer reportWay;
|
||||
private String reportWayName;
|
||||
private Integer verifyResult;
|
||||
private String verifyResultName;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date reportTime;
|
||||
|
||||
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 getPlanName() { return planName; }
|
||||
public void setPlanName(String planName) { this.planName = planName; }
|
||||
|
||||
public Integer getEnrollmentStatus() { return enrollmentStatus; }
|
||||
public void setEnrollmentStatus(Integer enrollmentStatus) { this.enrollmentStatus = enrollmentStatus; }
|
||||
|
||||
public String getEnrollmentStatusName() { return enrollmentStatusName; }
|
||||
public void setEnrollmentStatusName(String enrollmentStatusName) { this.enrollmentStatusName = enrollmentStatusName; }
|
||||
|
||||
public Date getJoinTime() { return joinTime; }
|
||||
public void setJoinTime(Date joinTime) { this.joinTime = joinTime; }
|
||||
|
||||
public Integer getReportWay() { return reportWay; }
|
||||
public void setReportWay(Integer reportWay) { this.reportWay = reportWay; }
|
||||
|
||||
public String getReportWayName() { return reportWayName; }
|
||||
public void setReportWayName(String reportWayName) { this.reportWayName = reportWayName; }
|
||||
|
||||
public Integer getVerifyResult() { return verifyResult; }
|
||||
public void setVerifyResult(Integer verifyResult) { this.verifyResult = verifyResult; }
|
||||
|
||||
public String getVerifyResultName() { return verifyResultName; }
|
||||
public void setVerifyResultName(String verifyResultName) { this.verifyResultName = verifyResultName; }
|
||||
|
||||
public Date getReportTime() { return reportTime; }
|
||||
public void setReportTime(Date reportTime) { this.reportTime = reportTime; }
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.ruoyi.train.domain.analysis;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 场地使用频次与课时 DTO
|
||||
*/
|
||||
public class VenueUsageDto implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 场地名称(教室名称或实训场地名称) */
|
||||
private String venueName;
|
||||
|
||||
/** 使用频次(排课次数) */
|
||||
private Long usageCount;
|
||||
|
||||
/** 累计占用课时(小时数,保留1位小数) */
|
||||
private Double totalHours;
|
||||
|
||||
public VenueUsageDto() {}
|
||||
|
||||
public VenueUsageDto(String venueName, Long usageCount, Double totalHours)
|
||||
{
|
||||
this.venueName = venueName;
|
||||
this.usageCount = usageCount;
|
||||
this.totalHours = totalHours;
|
||||
}
|
||||
|
||||
public String getVenueName() { return venueName; }
|
||||
public void setVenueName(String venueName) { this.venueName = venueName; }
|
||||
|
||||
public Long getUsageCount() { return usageCount; }
|
||||
public void setUsageCount(Long usageCount) { this.usageCount = usageCount; }
|
||||
|
||||
public Double getTotalHours() { return totalHours; }
|
||||
public void setTotalHours(Double totalHours) { this.totalHours = totalHours; }
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.ruoyi.train.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.train.domain.analysis.AttendanceFilterDto;
|
||||
|
||||
/**
|
||||
* 数据分析与可视化专用聚合 Mapper (严格在数据库层完成 COUNT/SUM/GROUP BY)
|
||||
*/
|
||||
public interface TrainAnalysisMapper
|
||||
{
|
||||
/* ==================== 1. 管理驾驶舱 ==================== */
|
||||
|
||||
Long countStudents();
|
||||
Long countTeachers();
|
||||
Long countCourses();
|
||||
Long countPlans();
|
||||
Long countLessons();
|
||||
Long countEnrollments();
|
||||
Long countReports();
|
||||
Long countCurrentAccommodations();
|
||||
|
||||
List<Map<String, Object>> selectPlanStatusDistribution();
|
||||
|
||||
Long countAbnormalAttendance();
|
||||
Long countPendingReports();
|
||||
Long countPreallocatedAccommodations();
|
||||
Long countAbnormalDormAuths();
|
||||
|
||||
List<Map<String, Object>> selectAttendanceTrend30Days();
|
||||
|
||||
List<Map<String, Object>> selectPlanEnrollmentReportComparisonTop10();
|
||||
|
||||
List<Map<String, Object>> selectAttendanceStatusDistribution();
|
||||
|
||||
Long countTotalExpectedAttendanceForPastLessons();
|
||||
|
||||
List<Map<String, Object>> selectVenueUsageTop5();
|
||||
|
||||
/* ==================== 2. 考勤专题分析 ==================== */
|
||||
|
||||
Long countFilteredLessons(@Param("dto") AttendanceFilterDto dto);
|
||||
|
||||
Long countFilteredExpectedAttendance(@Param("dto") AttendanceFilterDto dto);
|
||||
|
||||
Long countFilteredNormalAttendance(@Param("dto") AttendanceFilterDto dto);
|
||||
|
||||
Long countFilteredAbnormalAttendance(@Param("dto") AttendanceFilterDto dto);
|
||||
|
||||
List<Map<String, Object>> selectFilteredDailyTrend(@Param("dto") AttendanceFilterDto dto);
|
||||
|
||||
List<Map<String, Object>> selectFilteredStatusDistribution(@Param("dto") AttendanceFilterDto dto);
|
||||
|
||||
List<Map<String, Object>> selectPlanAttendanceComparison(@Param("dto") AttendanceFilterDto dto);
|
||||
|
||||
List<Map<String, Object>> selectCourseAttendanceComparison(@Param("dto") AttendanceFilterDto dto);
|
||||
|
||||
List<Map<String, Object>> selectAbnormalStudentTop10(@Param("dto") AttendanceFilterDto dto);
|
||||
|
||||
/* ==================== 3. 学员 360 画像 ==================== */
|
||||
|
||||
Map<String, Object> selectStudentBaseProfile(@Param("studentId") String studentId);
|
||||
|
||||
List<Map<String, Object>> selectStudentEnrollmentTimeline(@Param("studentId") String studentId);
|
||||
|
||||
List<Map<String, Object>> selectStudentAttendanceStats(@Param("studentId") String studentId);
|
||||
|
||||
Long countStudentExpectedAttendance(@Param("studentId") String studentId);
|
||||
|
||||
Map<String, Object> selectStudentAccommodationAuth(@Param("studentId") String studentId);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.ruoyi.train.service;
|
||||
|
||||
import com.ruoyi.train.domain.analysis.AttendanceChartsVo;
|
||||
import com.ruoyi.train.domain.analysis.AttendanceFilterDto;
|
||||
import com.ruoyi.train.domain.analysis.AttendanceSummaryVo;
|
||||
import com.ruoyi.train.domain.analysis.DashboardChartsVo;
|
||||
import com.ruoyi.train.domain.analysis.DashboardOverviewVo;
|
||||
import com.ruoyi.train.domain.analysis.Student360Vo;
|
||||
|
||||
/**
|
||||
* 数据分析与可视化服务接口
|
||||
*/
|
||||
public interface ITrainAnalysisService
|
||||
{
|
||||
/** 获取管理驾驶舱宏观概览与业务提醒 */
|
||||
DashboardOverviewVo getDashboardOverview();
|
||||
|
||||
/** 获取管理驾驶舱图表集合 */
|
||||
DashboardChartsVo getDashboardCharts();
|
||||
|
||||
/** 获取考勤专题分析汇总 KPI */
|
||||
AttendanceSummaryVo getAttendanceSummary(AttendanceFilterDto filter);
|
||||
|
||||
/** 获取考勤专题分析图表集合 */
|
||||
AttendanceChartsVo getAttendanceCharts(AttendanceFilterDto filter);
|
||||
|
||||
/** 获取指定学员 360 客观全景画像 */
|
||||
Student360Vo getStudent360(String studentId);
|
||||
}
|
||||
+564
@@ -0,0 +1,564 @@
|
||||
package com.ruoyi.train.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.train.domain.analysis.*;
|
||||
import com.ruoyi.train.mapper.TrainAnalysisMapper;
|
||||
import com.ruoyi.train.service.ITrainAnalysisService;
|
||||
|
||||
/**
|
||||
* 数据分析与可视化服务实现类
|
||||
*/
|
||||
@Service
|
||||
public class TrainAnalysisServiceImpl implements ITrainAnalysisService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(TrainAnalysisServiceImpl.class);
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
@Autowired
|
||||
private TrainAnalysisMapper analysisMapper;
|
||||
|
||||
@Override
|
||||
public DashboardOverviewVo getDashboardOverview()
|
||||
{
|
||||
DashboardOverviewVo vo = new DashboardOverviewVo();
|
||||
vo.setStudentTotal(analysisMapper.countStudents());
|
||||
vo.setTeacherTotal(analysisMapper.countTeachers());
|
||||
vo.setCourseTotal(analysisMapper.countCourses());
|
||||
vo.setPlanTotal(analysisMapper.countPlans());
|
||||
vo.setLessonTotal(analysisMapper.countLessons());
|
||||
vo.setEnrollmentTotal(analysisMapper.countEnrollments());
|
||||
vo.setReportTotal(analysisMapper.countReports());
|
||||
vo.setAccommodationCurrent(analysisMapper.countCurrentAccommodations());
|
||||
|
||||
// 计划状态分布
|
||||
Map<String, Long> statusMap = new LinkedHashMap<>();
|
||||
statusMap.put("notStarted", 0L);
|
||||
statusMap.put("ongoing", 0L);
|
||||
statusMap.put("ended", 0L);
|
||||
statusMap.put("cancelled", 0L);
|
||||
|
||||
List<Map<String, Object>> planStatuses = analysisMapper.selectPlanStatusDistribution();
|
||||
if (planStatuses != null)
|
||||
{
|
||||
for (Map<String, Object> map : planStatuses)
|
||||
{
|
||||
String code = String.valueOf(map.get("status_code"));
|
||||
Long cnt = ((Number) map.get("count_num")).longValue();
|
||||
if ("0".equals(code)) statusMap.put("notStarted", cnt);
|
||||
else if ("1".equals(code)) statusMap.put("ongoing", cnt);
|
||||
else if ("2".equals(code)) statusMap.put("ended", cnt);
|
||||
else if ("3".equals(code)) statusMap.put("cancelled", cnt);
|
||||
}
|
||||
}
|
||||
vo.setPlanStatusCounts(statusMap);
|
||||
|
||||
// 业务待办提醒
|
||||
vo.setAbnormalAttendanceCount(analysisMapper.countAbnormalAttendance());
|
||||
vo.setPendingReportCount(analysisMapper.countPendingReports());
|
||||
vo.setPreallocatedAccommodationCount(analysisMapper.countPreallocatedAccommodations());
|
||||
vo.setAbnormalAuthCount(analysisMapper.countAbnormalDormAuths());
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DashboardChartsVo getDashboardCharts()
|
||||
{
|
||||
DashboardChartsVo vo = new DashboardChartsVo();
|
||||
|
||||
// 1. 近 30 天考勤走势
|
||||
List<Map<String, Object>> trendList = analysisMapper.selectAttendanceTrend30Days();
|
||||
List<String> trendDates = new ArrayList<>();
|
||||
List<Long> normalCounts = new ArrayList<>();
|
||||
List<Long> abnormalCounts = new ArrayList<>();
|
||||
|
||||
if (trendList != null)
|
||||
{
|
||||
for (Map<String, Object> row : trendList)
|
||||
{
|
||||
trendDates.add(String.valueOf(row.get("stat_date")));
|
||||
normalCounts.add(row.get("normal_count") != null ? ((Number) row.get("normal_count")).longValue() : 0L);
|
||||
abnormalCounts.add(row.get("abnormal_count") != null ? ((Number) row.get("abnormal_count")).longValue() : 0L);
|
||||
}
|
||||
}
|
||||
vo.setTrendDates(trendDates);
|
||||
vo.setTrendNormalCounts(normalCounts);
|
||||
vo.setTrendAbnormalCounts(abnormalCounts);
|
||||
|
||||
// 2. 计划参训与报到对比 (Top 10)
|
||||
List<Map<String, Object>> planCompList = analysisMapper.selectPlanEnrollmentReportComparisonTop10();
|
||||
List<String> planNames = new ArrayList<>();
|
||||
List<Long> enrolledList = new ArrayList<>();
|
||||
List<Long> reportedList = new ArrayList<>();
|
||||
|
||||
if (planCompList != null)
|
||||
{
|
||||
for (Map<String, Object> row : planCompList)
|
||||
{
|
||||
planNames.add(String.valueOf(row.get("plan_name")));
|
||||
enrolledList.add(row.get("enrolled_count") != null ? ((Number) row.get("enrolled_count")).longValue() : 0L);
|
||||
reportedList.add(row.get("reported_count") != null ? ((Number) row.get("reported_count")).longValue() : 0L);
|
||||
}
|
||||
}
|
||||
vo.setPlanComparisonNames(planNames);
|
||||
vo.setPlanEnrolledCounts(enrolledList);
|
||||
vo.setPlanReportedCounts(reportedList);
|
||||
|
||||
// 3. 考勤状态全局构成 (正常, 迟到, 早退, 请假, 明确缺勤, 无签到记录)
|
||||
List<NameValueDto> statusList = new ArrayList<>();
|
||||
Map<String, Long> statusCountMap = new HashMap<>();
|
||||
List<Map<String, Object>> rawStatus = analysisMapper.selectAttendanceStatusDistribution();
|
||||
long loggedTotal = 0L;
|
||||
|
||||
if (rawStatus != null)
|
||||
{
|
||||
for (Map<String, Object> row : rawStatus)
|
||||
{
|
||||
String code = String.valueOf(row.get("status_code"));
|
||||
long cnt = row.get("count_num") != null ? ((Number) row.get("count_num")).longValue() : 0L;
|
||||
statusCountMap.put(code, cnt);
|
||||
loggedTotal += cnt;
|
||||
}
|
||||
}
|
||||
|
||||
statusList.add(new NameValueDto("正常出勤", statusCountMap.getOrDefault("1", 0L)));
|
||||
statusList.add(new NameValueDto("迟到", statusCountMap.getOrDefault("2", 0L)));
|
||||
statusList.add(new NameValueDto("早退", statusCountMap.getOrDefault("3", 0L)));
|
||||
statusList.add(new NameValueDto("请假", statusCountMap.getOrDefault("4", 0L)));
|
||||
statusList.add(new NameValueDto("明确缺勤", statusCountMap.getOrDefault("0", 0L)));
|
||||
|
||||
// 计算“无签到记录”
|
||||
Long totalExpected = analysisMapper.countTotalExpectedAttendanceForPastLessons();
|
||||
if (totalExpected != null && totalExpected > loggedTotal)
|
||||
{
|
||||
statusList.add(new NameValueDto("无签到记录", totalExpected - loggedTotal));
|
||||
}
|
||||
else
|
||||
{
|
||||
statusList.add(new NameValueDto("无签到记录", 0L));
|
||||
}
|
||||
vo.setAttendanceStatusDistribution(statusList);
|
||||
|
||||
// 4. 场地使用频次与课时 Top 5
|
||||
List<VenueUsageDto> venueList = new ArrayList<>();
|
||||
List<Map<String, Object>> rawVenues = analysisMapper.selectVenueUsageTop5();
|
||||
if (rawVenues != null)
|
||||
{
|
||||
for (Map<String, Object> row : rawVenues)
|
||||
{
|
||||
String name = String.valueOf(row.get("venue_name"));
|
||||
long cnt = row.get("usage_count") != null ? ((Number) row.get("usage_count")).longValue() : 0L;
|
||||
double hours = row.get("total_hours") != null ? ((Number) row.get("total_hours")).doubleValue() : 0.0;
|
||||
venueList.add(new VenueUsageDto(name, cnt, hours));
|
||||
}
|
||||
}
|
||||
vo.setVenueUsageTop5(venueList);
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttendanceSummaryVo getAttendanceSummary(AttendanceFilterDto filter)
|
||||
{
|
||||
AttendanceFilterDto normalized = normalizeFilter(filter);
|
||||
AttendanceSummaryVo vo = new AttendanceSummaryVo();
|
||||
|
||||
Long lessonTotal = analysisMapper.countFilteredLessons(normalized);
|
||||
Long expectedTotal = analysisMapper.countFilteredExpectedAttendance(normalized);
|
||||
Long normalTotal = analysisMapper.countFilteredNormalAttendance(normalized);
|
||||
Long abnormalLogged = analysisMapper.countFilteredAbnormalAttendance(normalized);
|
||||
|
||||
long exp = expectedTotal != null ? expectedTotal : 0L;
|
||||
long norm = normalTotal != null ? normalTotal : 0L;
|
||||
long abnLogged = abnormalLogged != null ? abnormalLogged : 0L;
|
||||
|
||||
// 异常人次 = 已打卡的异常(迟到/早退/请假/缺勤) + (应到 - 打卡总数)
|
||||
long totalLogged = norm + abnLogged;
|
||||
long unlogged = (exp > totalLogged) ? (exp - totalLogged) : 0L;
|
||||
long totalAbnormal = abnLogged + unlogged;
|
||||
|
||||
vo.setLessonTotal(lessonTotal != null ? lessonTotal : 0L);
|
||||
vo.setExpectedAttendTotal(exp);
|
||||
vo.setNormalAttendTotal(norm);
|
||||
vo.setAbnormalAttendTotal(totalAbnormal);
|
||||
|
||||
if (exp > 0)
|
||||
{
|
||||
double rate = BigDecimal.valueOf(norm * 100.0 / exp)
|
||||
.setScale(1, RoundingMode.HALF_UP)
|
||||
.doubleValue();
|
||||
vo.setAttendanceRate(rate);
|
||||
}
|
||||
else
|
||||
{
|
||||
vo.setAttendanceRate(0.0);
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttendanceChartsVo getAttendanceCharts(AttendanceFilterDto filter)
|
||||
{
|
||||
AttendanceFilterDto normalized = normalizeFilter(filter);
|
||||
AttendanceChartsVo vo = new AttendanceChartsVo();
|
||||
|
||||
// 1. 每日走势
|
||||
List<DailyTrendDto> dailyList = new ArrayList<>();
|
||||
List<Map<String, Object>> rawDaily = analysisMapper.selectFilteredDailyTrend(normalized);
|
||||
if (rawDaily != null)
|
||||
{
|
||||
for (Map<String, Object> row : rawDaily)
|
||||
{
|
||||
String date = String.valueOf(row.get("stat_date"));
|
||||
long norm = row.get("normal_count") != null ? ((Number) row.get("normal_count")).longValue() : 0L;
|
||||
long abn = row.get("abnormal_count") != null ? ((Number) row.get("abnormal_count")).longValue() : 0L;
|
||||
long sum = norm + abn;
|
||||
double rate = (sum > 0) ? BigDecimal.valueOf(norm * 100.0 / sum).setScale(1, RoundingMode.HALF_UP).doubleValue() : 0.0;
|
||||
dailyList.add(new DailyTrendDto(date, norm, abn, rate));
|
||||
}
|
||||
}
|
||||
vo.setDailyTrend(dailyList);
|
||||
|
||||
// 2. 状态分布环形图
|
||||
List<NameValueDto> statusList = new ArrayList<>();
|
||||
Map<String, Long> statusMap = new HashMap<>();
|
||||
List<Map<String, Object>> rawStatus = analysisMapper.selectFilteredStatusDistribution(normalized);
|
||||
long logged = 0L;
|
||||
if (rawStatus != null)
|
||||
{
|
||||
for (Map<String, Object> row : rawStatus)
|
||||
{
|
||||
String code = String.valueOf(row.get("status_code"));
|
||||
long cnt = row.get("count_num") != null ? ((Number) row.get("count_num")).longValue() : 0L;
|
||||
statusMap.put(code, cnt);
|
||||
logged += cnt;
|
||||
}
|
||||
}
|
||||
statusList.add(new NameValueDto("正常出勤", statusMap.getOrDefault("1", 0L)));
|
||||
statusList.add(new NameValueDto("迟到", statusMap.getOrDefault("2", 0L)));
|
||||
statusList.add(new NameValueDto("早退", statusMap.getOrDefault("3", 0L)));
|
||||
statusList.add(new NameValueDto("请假", statusMap.getOrDefault("4", 0L)));
|
||||
statusList.add(new NameValueDto("明确缺勤", statusMap.getOrDefault("0", 0L)));
|
||||
|
||||
Long expTotal = analysisMapper.countFilteredExpectedAttendance(normalized);
|
||||
if (expTotal != null && expTotal > logged)
|
||||
{
|
||||
statusList.add(new NameValueDto("无签到记录", expTotal - logged));
|
||||
}
|
||||
else
|
||||
{
|
||||
statusList.add(new NameValueDto("无签到记录", 0L));
|
||||
}
|
||||
vo.setStatusDistribution(statusList);
|
||||
|
||||
// 3. 培训计划对比 (Top 10)
|
||||
List<CompareItemDto> planCompare = new ArrayList<>();
|
||||
List<Map<String, Object>> rawPlan = analysisMapper.selectPlanAttendanceComparison(normalized);
|
||||
if (rawPlan != null)
|
||||
{
|
||||
for (Map<String, Object> row : rawPlan)
|
||||
{
|
||||
String id = String.valueOf(row.get("id"));
|
||||
String name = String.valueOf(row.get("name"));
|
||||
long norm = row.get("normal_count") != null ? ((Number) row.get("normal_count")).longValue() : 0L;
|
||||
long abn = row.get("abnormal_count") != null ? ((Number) row.get("abnormal_count")).longValue() : 0L;
|
||||
long sum = norm + abn;
|
||||
double rate = (sum > 0) ? BigDecimal.valueOf(norm * 100.0 / sum).setScale(1, RoundingMode.HALF_UP).doubleValue() : 0.0;
|
||||
planCompare.add(new CompareItemDto(id, name, sum, norm, abn, rate));
|
||||
}
|
||||
}
|
||||
vo.setPlanComparison(planCompare);
|
||||
|
||||
// 4. 课程考勤对比 (Top 10)
|
||||
List<CompareItemDto> courseCompare = new ArrayList<>();
|
||||
List<Map<String, Object>> rawCourse = analysisMapper.selectCourseAttendanceComparison(normalized);
|
||||
if (rawCourse != null)
|
||||
{
|
||||
for (Map<String, Object> row : rawCourse)
|
||||
{
|
||||
String id = String.valueOf(row.get("id"));
|
||||
String name = String.valueOf(row.get("name"));
|
||||
long norm = row.get("normal_count") != null ? ((Number) row.get("normal_count")).longValue() : 0L;
|
||||
long abn = row.get("abnormal_count") != null ? ((Number) row.get("abnormal_count")).longValue() : 0L;
|
||||
long sum = norm + abn;
|
||||
double rate = (sum > 0) ? BigDecimal.valueOf(norm * 100.0 / sum).setScale(1, RoundingMode.HALF_UP).doubleValue() : 0.0;
|
||||
courseCompare.add(new CompareItemDto(id, name, sum, norm, abn, rate));
|
||||
}
|
||||
}
|
||||
vo.setCourseComparison(courseCompare);
|
||||
|
||||
// 5. 异常学员 Top 10
|
||||
List<AbnormalStudentDto> abnormalTop10 = new ArrayList<>();
|
||||
List<Map<String, Object>> rawAbnormal = analysisMapper.selectAbnormalStudentTop10(normalized);
|
||||
if (rawAbnormal != null)
|
||||
{
|
||||
for (Map<String, Object> row : rawAbnormal)
|
||||
{
|
||||
AbnormalStudentDto dto = new AbnormalStudentDto();
|
||||
dto.setStudentId(String.valueOf(row.get("student_id")));
|
||||
dto.setStudentName(String.valueOf(row.get("student_name")));
|
||||
dto.setPlanId(String.valueOf(row.get("plan_id")));
|
||||
dto.setPlanName(String.valueOf(row.get("plan_name")));
|
||||
dto.setAbsentCount(row.get("absent_count") != null ? ((Number) row.get("absent_count")).longValue() : 0L);
|
||||
dto.setLateCount(row.get("late_count") != null ? ((Number) row.get("late_count")).longValue() : 0L);
|
||||
dto.setEarlyLeaveCount(row.get("early_count") != null ? ((Number) row.get("early_count")).longValue() : 0L);
|
||||
dto.setLeaveCount(row.get("leave_count") != null ? ((Number) row.get("leave_count")).longValue() : 0L);
|
||||
dto.setNoCheckinCount(0L);
|
||||
dto.setTotalAbnormalCount(row.get("total_abnormal_count") != null ? ((Number) row.get("total_abnormal_count")).longValue() : 0L);
|
||||
abnormalTop10.add(dto);
|
||||
}
|
||||
}
|
||||
vo.setAbnormalStudentTop10(abnormalTop10);
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Student360Vo getStudent360(String studentId)
|
||||
{
|
||||
if (studentId == null || studentId.trim().isEmpty())
|
||||
{
|
||||
throw new ServiceException("学员学号不能为空");
|
||||
}
|
||||
|
||||
Student360Vo vo = new Student360Vo();
|
||||
vo.setStudentId(studentId);
|
||||
|
||||
// 1. 基础主档 (脱敏)
|
||||
Map<String, Object> base = analysisMapper.selectStudentBaseProfile(studentId);
|
||||
if (base == null || base.isEmpty())
|
||||
{
|
||||
throw new ServiceException("未查询到学号为 " + studentId + " 的学员主档");
|
||||
}
|
||||
vo.setStudentName(String.valueOf(base.get("student_name")));
|
||||
String gender = String.valueOf(base.get("gender"));
|
||||
vo.setGender(gender);
|
||||
vo.setGenderName("0".equals(gender) ? "男" : ("1".equals(gender) ? "女" : "未知"));
|
||||
if (base.get("birth") != null)
|
||||
{
|
||||
vo.setBirth((Date) base.get("birth"));
|
||||
}
|
||||
vo.setPhone(base.get("phone") != null ? String.valueOf(base.get("phone")) : "-");
|
||||
vo.setEmail(base.get("email") != null ? String.valueOf(base.get("email")) : "-");
|
||||
vo.setOrgName(base.get("org_name") != null ? String.valueOf(base.get("org_name")) : "-");
|
||||
String last4 = base.get("id_card_last4") != null ? String.valueOf(base.get("id_card_last4")) : "";
|
||||
vo.setIdCardMasked(last4.isEmpty() ? "未登记" : "**************" + last4);
|
||||
vo.setUserBindingStatus(String.valueOf(base.get("user_binding_status")));
|
||||
vo.setStatus(base.get("status") != null ? ((Number) base.get("status")).intValue() : 1);
|
||||
|
||||
// 2. 参训履历时间轴
|
||||
List<StudentEnrollmentTimelineDto> timeline = new ArrayList<>();
|
||||
List<Map<String, Object>> rawTimeline = analysisMapper.selectStudentEnrollmentTimeline(studentId);
|
||||
if (rawTimeline != null)
|
||||
{
|
||||
for (Map<String, Object> row : rawTimeline)
|
||||
{
|
||||
StudentEnrollmentTimelineDto dto = new StudentEnrollmentTimelineDto();
|
||||
dto.setEnrollmentId(((Number) row.get("enrollment_id")).longValue());
|
||||
dto.setPlanId(String.valueOf(row.get("plan_id")));
|
||||
dto.setPlanName(String.valueOf(row.get("plan_name")));
|
||||
Integer enrStatus = row.get("enrollment_status") != null ? ((Number) row.get("enrollment_status")).intValue() : 0;
|
||||
dto.setEnrollmentStatus(enrStatus);
|
||||
dto.setEnrollmentStatusName(formatEnrollmentStatus(enrStatus));
|
||||
if (row.get("join_time") != null) dto.setJoinTime((Date) row.get("join_time"));
|
||||
|
||||
if (row.get("report_way") != null)
|
||||
{
|
||||
int rw = ((Number) row.get("report_way")).intValue();
|
||||
dto.setReportWay(rw);
|
||||
dto.setReportWayName(formatReportWay(rw));
|
||||
}
|
||||
if (row.get("verify_result") != null)
|
||||
{
|
||||
int vr = ((Number) row.get("verify_result")).intValue();
|
||||
dto.setVerifyResult(vr);
|
||||
dto.setVerifyResultName(formatVerifyResult(vr));
|
||||
}
|
||||
if (row.get("report_time") != null) dto.setReportTime((Date) row.get("report_time"));
|
||||
|
||||
timeline.add(dto);
|
||||
}
|
||||
}
|
||||
vo.setEnrollmentTimeline(timeline);
|
||||
|
||||
// 3. 考勤画像
|
||||
Map<String, Long> checkinMap = new HashMap<>();
|
||||
List<Map<String, Object>> rawCheckin = analysisMapper.selectStudentAttendanceStats(studentId);
|
||||
long logged = 0L;
|
||||
if (rawCheckin != null)
|
||||
{
|
||||
for (Map<String, Object> row : rawCheckin)
|
||||
{
|
||||
String code = String.valueOf(row.get("status_code"));
|
||||
long cnt = row.get("count_num") != null ? ((Number) row.get("count_num")).longValue() : 0L;
|
||||
checkinMap.put(code, cnt);
|
||||
logged += cnt;
|
||||
}
|
||||
}
|
||||
Long expected = analysisMapper.countStudentExpectedAttendance(studentId);
|
||||
long exp = expected != null ? expected : 0L;
|
||||
long norm = checkinMap.getOrDefault("1", 0L);
|
||||
long unlogged = (exp > logged) ? (exp - logged) : 0L;
|
||||
|
||||
vo.setExpectedAttendCount(exp);
|
||||
vo.setNormalCount(norm);
|
||||
vo.setLateCount(checkinMap.getOrDefault("2", 0L));
|
||||
vo.setEarlyCount(checkinMap.getOrDefault("3", 0L));
|
||||
vo.setLeaveCount(checkinMap.getOrDefault("4", 0L));
|
||||
vo.setAbsentCount(checkinMap.getOrDefault("0", 0L));
|
||||
vo.setNoCheckinCount(unlogged);
|
||||
|
||||
if (exp > 0)
|
||||
{
|
||||
double rate = BigDecimal.valueOf(norm * 100.0 / exp).setScale(1, RoundingMode.HALF_UP).doubleValue();
|
||||
vo.setAttendanceRate(rate);
|
||||
}
|
||||
else
|
||||
{
|
||||
vo.setAttendanceRate(0.0);
|
||||
}
|
||||
|
||||
// 4. 成绩状态 (严格不造假)
|
||||
vo.setHasScoreData(false);
|
||||
vo.setScoreStatusDesc("暂无成绩数据 (V2成绩表待考务录入)");
|
||||
|
||||
// 5. 自主学习状态 (严格不造假)
|
||||
vo.setHasStudyData(false);
|
||||
vo.setStudyStatusDesc("暂无自主学习记录 / 待外部平台数据同步");
|
||||
|
||||
// 6. 住宿与门禁状态
|
||||
Map<String, Object> dorm = analysisMapper.selectStudentAccommodationAuth(studentId);
|
||||
if (dorm != null && !dorm.isEmpty())
|
||||
{
|
||||
vo.setHasAccommodation(true);
|
||||
vo.setDormId(dorm.get("dorm_id") != null ? String.valueOf(dorm.get("dorm_id")) : "-");
|
||||
vo.setRoomName(dorm.get("room_name") != null ? String.valueOf(dorm.get("room_name")) : "宿舍");
|
||||
vo.setBedNo(dorm.get("bed_no") != null ? String.valueOf(dorm.get("bed_no")) : "-");
|
||||
Integer accStatus = dorm.get("accommodation_status") != null ? ((Number) dorm.get("accommodation_status")).intValue() : 0;
|
||||
vo.setAccommodationStatus(accStatus);
|
||||
vo.setAccommodationStatusName(accStatus == 1 ? "已入住" : (accStatus == 0 ? "预分配" : "已退宿"));
|
||||
|
||||
String authStatus = dorm.get("auth_status") != null ? String.valueOf(dorm.get("auth_status")) : "NONE";
|
||||
vo.setAuthStatus(authStatus);
|
||||
vo.setAuthStatusName(formatAuthStatus(authStatus));
|
||||
}
|
||||
else
|
||||
{
|
||||
vo.setHasAccommodation(false);
|
||||
vo.setAccommodationStatusName("未安排住宿");
|
||||
vo.setAuthStatusName("无门禁授权");
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
private AttendanceFilterDto normalizeFilter(AttendanceFilterDto filter)
|
||||
{
|
||||
AttendanceFilterDto dto = new AttendanceFilterDto();
|
||||
if (filter != null)
|
||||
{
|
||||
dto.setPlanId(filter.getPlanId() != null && !filter.getPlanId().trim().isEmpty() ? filter.getPlanId().trim() : null);
|
||||
dto.setCourseId(filter.getCourseId() != null && !filter.getCourseId().trim().isEmpty() ? filter.getCourseId().trim() : null);
|
||||
dto.setStartDate(filter.getStartDate() != null && !filter.getStartDate().trim().isEmpty() ? filter.getStartDate().trim() : null);
|
||||
dto.setEndDate(filter.getEndDate() != null && !filter.getEndDate().trim().isEmpty() ? filter.getEndDate().trim() : null);
|
||||
}
|
||||
|
||||
// 日期处理:默认近 30 天,最大限制 365 天
|
||||
LocalDate end = LocalDate.now();
|
||||
LocalDate start = end.minusDays(30);
|
||||
|
||||
try
|
||||
{
|
||||
if (dto.getEndDate() != null)
|
||||
{
|
||||
end = LocalDate.parse(dto.getEndDate(), DATE_FORMATTER);
|
||||
}
|
||||
if (dto.getStartDate() != null)
|
||||
{
|
||||
start = LocalDate.parse(dto.getStartDate(), DATE_FORMATTER);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("日期格式解析失败,自动回退默认近30天", e);
|
||||
end = LocalDate.now();
|
||||
start = end.minusDays(30);
|
||||
}
|
||||
|
||||
// 校验范围最大 365 天
|
||||
if (start.isAfter(end))
|
||||
{
|
||||
LocalDate tmp = start;
|
||||
start = end;
|
||||
end = tmp;
|
||||
}
|
||||
long days = ChronoUnit.DAYS.between(start, end);
|
||||
if (days > 365)
|
||||
{
|
||||
start = end.minusDays(365);
|
||||
}
|
||||
|
||||
dto.setStartDate(start.format(DATE_FORMATTER));
|
||||
dto.setEndDate(end.format(DATE_FORMATTER));
|
||||
return dto;
|
||||
}
|
||||
|
||||
private String formatEnrollmentStatus(int status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case 0: return "待报到";
|
||||
case 1: return "已报到";
|
||||
case 2: return "请假";
|
||||
case 3: return "放弃";
|
||||
case 4: return "已完成";
|
||||
default: return "未知状态";
|
||||
}
|
||||
}
|
||||
|
||||
private String formatReportWay(int way)
|
||||
{
|
||||
switch (way)
|
||||
{
|
||||
case 1: return "人脸识别";
|
||||
case 2: return "身份证刷卡";
|
||||
case 3: return "二维码";
|
||||
case 4: return "人工录入";
|
||||
default: return "其他方式";
|
||||
}
|
||||
}
|
||||
|
||||
private String formatVerifyResult(int res)
|
||||
{
|
||||
switch (res)
|
||||
{
|
||||
case 1: return "核验通过";
|
||||
case 2: return "核验不匹配";
|
||||
case 3: return "人工放行";
|
||||
default: return "未核验";
|
||||
}
|
||||
}
|
||||
|
||||
private String formatAuthStatus(String status)
|
||||
{
|
||||
if ("ACTIVE".equalsIgnoreCase(status)) return "生效中";
|
||||
if ("PENDING_GRANT".equalsIgnoreCase(status)) return "待下发";
|
||||
if ("GRANTING".equalsIgnoreCase(status)) return "下发中";
|
||||
if ("PENDING_REVOKE".equalsIgnoreCase(status)) return "待注销";
|
||||
if ("REVOKING".equalsIgnoreCase(status)) return "注销中";
|
||||
if ("REVOKED".equalsIgnoreCase(status)) return "已注销";
|
||||
if ("ERROR".equalsIgnoreCase(status)) return "授权异常";
|
||||
return "未授权";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.train.mapper.TrainAnalysisMapper">
|
||||
|
||||
<!-- 1. 驾驶舱 KPI 计数 -->
|
||||
<select id="countStudents" resultType="java.lang.Long">
|
||||
SELECT count(1) FROM train_student WHERE del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="countTeachers" resultType="java.lang.Long">
|
||||
SELECT count(1) FROM train_teacher WHERE del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="countCourses" resultType="java.lang.Long">
|
||||
SELECT count(1) FROM train_course WHERE del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="countPlans" resultType="java.lang.Long">
|
||||
SELECT count(1) FROM train_class_plan WHERE del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="countLessons" resultType="java.lang.Long">
|
||||
SELECT count(1) FROM train_lesson
|
||||
</select>
|
||||
|
||||
<select id="countEnrollments" resultType="java.lang.Long">
|
||||
SELECT count(1) FROM train_enrollment
|
||||
</select>
|
||||
|
||||
<select id="countReports" resultType="java.lang.Long">
|
||||
SELECT count(1) FROM train_class_report
|
||||
</select>
|
||||
|
||||
<select id="countCurrentAccommodations" resultType="java.lang.Long">
|
||||
SELECT count(1) FROM train_accommodation WHERE accommodation_status = 1
|
||||
</select>
|
||||
|
||||
<select id="selectPlanStatusDistribution" resultType="java.util.HashMap">
|
||||
SELECT CAST(plan_status AS VARCHAR) AS status_code, count(1) AS count_num
|
||||
FROM train_class_plan
|
||||
WHERE del_flag = '0'
|
||||
GROUP BY plan_status
|
||||
</select>
|
||||
|
||||
<select id="countAbnormalAttendance" resultType="java.lang.Long">
|
||||
SELECT count(1) FROM train_student_checkin WHERE attendance_status IN (0, 2, 3, 4)
|
||||
</select>
|
||||
|
||||
<select id="countPendingReports" resultType="java.lang.Long">
|
||||
SELECT count(1) FROM train_enrollment WHERE enrollment_status = 0
|
||||
</select>
|
||||
|
||||
<select id="countPreallocatedAccommodations" resultType="java.lang.Long">
|
||||
SELECT count(1) FROM train_accommodation WHERE accommodation_status = 0
|
||||
</select>
|
||||
|
||||
<select id="countAbnormalDormAuths" resultType="java.lang.Long">
|
||||
SELECT count(1) FROM train_dorm_auth WHERE auth_status = 'ERROR' OR last_error IS NOT NULL
|
||||
</select>
|
||||
|
||||
<!-- 2. 驾驶舱图表 -->
|
||||
<select id="selectAttendanceTrend30Days" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
TO_CHAR(c.sign_in_time, 'YYYY-MM-DD') AS stat_date,
|
||||
SUM(CASE WHEN c.attendance_status = 1 THEN 1 ELSE 0 END) AS normal_count,
|
||||
SUM(CASE WHEN c.attendance_status != 1 THEN 1 ELSE 0 END) AS abnormal_count
|
||||
FROM train_student_checkin c
|
||||
WHERE c.sign_in_time IS NOT NULL
|
||||
AND c.sign_in_time >= CURRENT_DATE - INTERVAL '30 days'
|
||||
GROUP BY TO_CHAR(c.sign_in_time, 'YYYY-MM-DD')
|
||||
ORDER BY stat_date ASC
|
||||
</select>
|
||||
|
||||
<select id="selectPlanEnrollmentReportComparisonTop10" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
p.plan_id AS plan_id,
|
||||
p.plan_name AS plan_name,
|
||||
COUNT(DISTINCT e.enrollment_id) AS enrolled_count,
|
||||
COUNT(DISTINCT r.report_id) AS reported_count
|
||||
FROM train_class_plan p
|
||||
LEFT JOIN train_enrollment e ON e.plan_id = p.plan_id
|
||||
LEFT JOIN train_class_report r ON r.enrollment_id = e.enrollment_id
|
||||
WHERE p.del_flag = '0'
|
||||
GROUP BY p.plan_id, p.plan_name, p.create_time
|
||||
ORDER BY p.create_time DESC
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
<select id="selectAttendanceStatusDistribution" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
CAST(attendance_status AS VARCHAR) AS status_code,
|
||||
COUNT(1) AS count_num
|
||||
FROM train_student_checkin
|
||||
GROUP BY attendance_status
|
||||
ORDER BY attendance_status ASC
|
||||
</select>
|
||||
|
||||
<select id="countTotalExpectedAttendanceForPastLessons" resultType="java.lang.Long">
|
||||
SELECT count(1)
|
||||
FROM train_lesson l
|
||||
JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id
|
||||
JOIN train_enrollment e ON e.plan_id = pc.plan_id
|
||||
WHERE l.start_time <= CURRENT_TIMESTAMP
|
||||
</select>
|
||||
|
||||
<select id="selectVenueUsageTop5" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
COALESCE(cr.room_name, ts.site_name, '未知场地') AS venue_name,
|
||||
count(1) AS usage_count,
|
||||
ROUND(SUM(EXTRACT(EPOCH FROM (l.end_time - l.start_time)) / 3600.0)::numeric, 1) AS total_hours
|
||||
FROM train_lesson l
|
||||
LEFT JOIN train_classroom_info cr ON cr.class_room_id = l.class_room_id
|
||||
LEFT JOIN train_training_site ts ON ts.site_id = l.site_id
|
||||
GROUP BY COALESCE(cr.room_name, ts.site_name, '未知场地')
|
||||
ORDER BY usage_count DESC, total_hours DESC
|
||||
LIMIT 5
|
||||
</select>
|
||||
|
||||
<!-- 3. 考勤专题分析 SQL -->
|
||||
<select id="countFilteredLessons" resultType="java.lang.Long">
|
||||
SELECT count(DISTINCT l.lesson_id)
|
||||
FROM train_lesson l
|
||||
JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id
|
||||
WHERE 1 = 1
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.planId != null and dto.planId != ''">
|
||||
AND pc.plan_id = #{dto.planId}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="countFilteredExpectedAttendance" resultType="java.lang.Long">
|
||||
SELECT count(1)
|
||||
FROM train_lesson l
|
||||
JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id
|
||||
JOIN train_enrollment e ON e.plan_id = pc.plan_id
|
||||
WHERE l.start_time <= CURRENT_TIMESTAMP
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.planId != null and dto.planId != ''">
|
||||
AND pc.plan_id = #{dto.planId}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="countFilteredNormalAttendance" resultType="java.lang.Long">
|
||||
SELECT count(1)
|
||||
FROM train_student_checkin c
|
||||
JOIN train_lesson l ON l.lesson_id = c.lesson_id
|
||||
JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id
|
||||
WHERE c.attendance_status = 1
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.planId != null and dto.planId != ''">
|
||||
AND pc.plan_id = #{dto.planId}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="countFilteredAbnormalAttendance" resultType="java.lang.Long">
|
||||
SELECT count(1)
|
||||
FROM train_student_checkin c
|
||||
JOIN train_lesson l ON l.lesson_id = c.lesson_id
|
||||
JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id
|
||||
WHERE c.attendance_status != 1
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.planId != null and dto.planId != ''">
|
||||
AND pc.plan_id = #{dto.planId}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="selectFilteredDailyTrend" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
TO_CHAR(c.sign_in_time, 'YYYY-MM-DD') AS stat_date,
|
||||
SUM(CASE WHEN c.attendance_status = 1 THEN 1 ELSE 0 END) AS normal_count,
|
||||
SUM(CASE WHEN c.attendance_status != 1 THEN 1 ELSE 0 END) AS abnormal_count
|
||||
FROM train_student_checkin c
|
||||
JOIN train_lesson l ON l.lesson_id = c.lesson_id
|
||||
JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id
|
||||
WHERE c.sign_in_time IS NOT NULL
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.planId != null and dto.planId != ''">
|
||||
AND pc.plan_id = #{dto.planId}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
GROUP BY TO_CHAR(c.sign_in_time, 'YYYY-MM-DD')
|
||||
ORDER BY stat_date ASC
|
||||
</select>
|
||||
|
||||
<select id="selectFilteredStatusDistribution" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
CAST(c.attendance_status AS VARCHAR) AS status_code,
|
||||
COUNT(1) AS count_num
|
||||
FROM train_student_checkin c
|
||||
JOIN train_lesson l ON l.lesson_id = c.lesson_id
|
||||
JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id
|
||||
WHERE 1 = 1
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.planId != null and dto.planId != ''">
|
||||
AND pc.plan_id = #{dto.planId}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
GROUP BY c.attendance_status
|
||||
ORDER BY c.attendance_status ASC
|
||||
</select>
|
||||
|
||||
<select id="selectPlanAttendanceComparison" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
p.plan_id AS id,
|
||||
p.plan_name AS name,
|
||||
SUM(CASE WHEN c.attendance_status = 1 THEN 1 ELSE 0 END) AS normal_count,
|
||||
SUM(CASE WHEN c.attendance_status != 1 THEN 1 ELSE 0 END) AS abnormal_count
|
||||
FROM train_class_plan p
|
||||
JOIN train_plan_course pc ON pc.plan_id = p.plan_id
|
||||
JOIN train_lesson l ON l.plan_course_id = pc.plan_course_id
|
||||
LEFT JOIN train_student_checkin c ON c.lesson_id = l.lesson_id
|
||||
WHERE p.del_flag = '0'
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.planId != null and dto.planId != ''">
|
||||
AND p.plan_id = #{dto.planId}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
GROUP BY p.plan_id, p.plan_name
|
||||
ORDER BY normal_count DESC
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
<select id="selectCourseAttendanceComparison" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
crs.c_id AS id,
|
||||
crs.c_name AS name,
|
||||
SUM(CASE WHEN c.attendance_status = 1 THEN 1 ELSE 0 END) AS normal_count,
|
||||
SUM(CASE WHEN c.attendance_status != 1 THEN 1 ELSE 0 END) AS abnormal_count
|
||||
FROM train_course crs
|
||||
JOIN train_plan_course pc ON pc.c_id = crs.c_id
|
||||
JOIN train_lesson l ON l.plan_course_id = pc.plan_course_id
|
||||
LEFT JOIN train_student_checkin c ON c.lesson_id = l.lesson_id
|
||||
WHERE crs.del_flag = '0'
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.planId != null and dto.planId != ''">
|
||||
AND pc.plan_id = #{dto.planId}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND crs.c_id = #{dto.courseId}
|
||||
</if>
|
||||
GROUP BY crs.c_id, crs.c_name
|
||||
ORDER BY normal_count DESC
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
<select id="selectAbnormalStudentTop10" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
s.s_id AS student_id,
|
||||
s.s_name AS student_name,
|
||||
p.plan_id AS plan_id,
|
||||
p.plan_name AS plan_name,
|
||||
SUM(CASE WHEN c.attendance_status = 0 THEN 1 ELSE 0 END) AS absent_count,
|
||||
SUM(CASE WHEN c.attendance_status = 2 THEN 1 ELSE 0 END) AS late_count,
|
||||
SUM(CASE WHEN c.attendance_status = 3 THEN 1 ELSE 0 END) AS early_count,
|
||||
SUM(CASE WHEN c.attendance_status = 4 THEN 1 ELSE 0 END) AS leave_count,
|
||||
SUM(CASE WHEN c.attendance_status IN (0, 2, 3, 4) THEN 1 ELSE 0 END) AS total_abnormal_count
|
||||
FROM train_student_checkin c
|
||||
JOIN train_enrollment e ON e.enrollment_id = c.enrollment_id
|
||||
JOIN train_student s ON s.s_id = e.student_id
|
||||
JOIN train_class_plan p ON p.plan_id = e.plan_id
|
||||
JOIN train_lesson l ON l.lesson_id = c.lesson_id
|
||||
JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id
|
||||
WHERE c.attendance_status IN (0, 2, 3, 4)
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
|
||||
</if>
|
||||
<if test="dto.planId != null and dto.planId != ''">
|
||||
AND p.plan_id = #{dto.planId}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
GROUP BY s.s_id, s.s_name, p.plan_id, p.plan_name
|
||||
ORDER BY total_abnormal_count DESC, absent_count DESC
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
<!-- 4. 学员 360 画像 SQL -->
|
||||
<select id="selectStudentBaseProfile" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
s.s_id AS student_id,
|
||||
s.s_name AS student_name,
|
||||
s.s_gender AS gender,
|
||||
s.s_birth AS birth,
|
||||
s.s_phone AS phone,
|
||||
s.s_email AS email,
|
||||
s.org_name AS org_name,
|
||||
s.id_card_last4 AS id_card_last4,
|
||||
s.status AS status,
|
||||
CASE WHEN s.user_id IS NOT NULL THEN '已绑定' ELSE '未绑定' END AS user_binding_status
|
||||
FROM train_student s
|
||||
WHERE s.s_id = #{studentId} AND s.del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectStudentEnrollmentTimeline" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
e.enrollment_id AS enrollment_id,
|
||||
p.plan_id AS plan_id,
|
||||
p.plan_name AS plan_name,
|
||||
e.enrollment_status AS enrollment_status,
|
||||
e.join_time AS join_time,
|
||||
r.report_way AS report_way,
|
||||
r.verify_result AS verify_result,
|
||||
r.report_time AS report_time
|
||||
FROM train_enrollment e
|
||||
JOIN train_class_plan p ON p.plan_id = e.plan_id
|
||||
LEFT JOIN train_class_report r ON r.enrollment_id = e.enrollment_id
|
||||
WHERE e.student_id = #{studentId}
|
||||
ORDER BY e.create_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectStudentAttendanceStats" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
CAST(c.attendance_status AS VARCHAR) AS status_code,
|
||||
COUNT(1) AS count_num
|
||||
FROM train_student_checkin c
|
||||
JOIN train_enrollment e ON e.enrollment_id = c.enrollment_id
|
||||
WHERE e.student_id = #{studentId}
|
||||
GROUP BY c.attendance_status
|
||||
</select>
|
||||
|
||||
<select id="countStudentExpectedAttendance" resultType="java.lang.Long">
|
||||
SELECT count(1)
|
||||
FROM train_enrollment e
|
||||
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
|
||||
JOIN train_lesson l ON l.plan_course_id = pc.plan_course_id
|
||||
WHERE e.student_id = #{studentId}
|
||||
AND l.start_time <= CURRENT_TIMESTAMP
|
||||
</select>
|
||||
|
||||
<select id="selectStudentAccommodationAuth" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
a.accommodation_id AS accommodation_id,
|
||||
a.dorm_id AS dorm_id,
|
||||
d.dorm_name AS room_name,
|
||||
a.bed_no AS bed_no,
|
||||
a.accommodation_status AS accommodation_status,
|
||||
auth.auth_status AS auth_status
|
||||
FROM train_accommodation a
|
||||
JOIN train_enrollment e ON e.enrollment_id = a.enrollment_id
|
||||
LEFT JOIN train_dormitory_info d ON d.dorm_id = a.dorm_id
|
||||
LEFT JOIN train_dorm_auth auth ON auth.accommodation_id = a.accommodation_id
|
||||
WHERE e.student_id = #{studentId}
|
||||
ORDER BY a.create_time DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
package com.ruoyi.train.service.impl;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import com.ruoyi.train.domain.analysis.*;
|
||||
import com.ruoyi.train.mapper.TrainAnalysisMapper;
|
||||
|
||||
/**
|
||||
* 数据分析与可视化聚合服务单元测试
|
||||
*/
|
||||
public class TrainAnalysisServiceTest
|
||||
{
|
||||
private TrainAnalysisServiceImpl service;
|
||||
private MockTrainAnalysisMapper mockMapper;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception
|
||||
{
|
||||
service = new TrainAnalysisServiceImpl();
|
||||
mockMapper = new MockTrainAnalysisMapper();
|
||||
Field field = TrainAnalysisServiceImpl.class.getDeclaredField("analysisMapper");
|
||||
field.setAccessible(true);
|
||||
field.set(service, mockMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDashboardOverviewAggregation()
|
||||
{
|
||||
DashboardOverviewVo vo = service.getDashboardOverview();
|
||||
assertNotNull(vo);
|
||||
assertEquals(Long.valueOf(200), vo.getStudentTotal());
|
||||
assertEquals(Long.valueOf(20), vo.getTeacherTotal());
|
||||
assertEquals(Long.valueOf(24), vo.getCourseTotal());
|
||||
assertEquals(Long.valueOf(13), vo.getPlanTotal());
|
||||
assertEquals(Long.valueOf(118), vo.getLessonTotal());
|
||||
assertEquals(Long.valueOf(242), vo.getEnrollmentTotal());
|
||||
assertEquals(Long.valueOf(63), vo.getReportTotal());
|
||||
assertEquals(Long.valueOf(1), vo.getAccommodationCurrent());
|
||||
|
||||
Map<String, Long> planMap = vo.getPlanStatusCounts();
|
||||
assertNotNull(planMap);
|
||||
assertEquals(Long.valueOf(10), planMap.get("notStarted"));
|
||||
assertEquals(Long.valueOf(0), planMap.get("ongoing"));
|
||||
assertEquals(Long.valueOf(3), planMap.get("ended"));
|
||||
assertEquals(Long.valueOf(0), planMap.get("cancelled"));
|
||||
|
||||
assertEquals(Long.valueOf(4), vo.getAbnormalAttendanceCount());
|
||||
assertEquals(Long.valueOf(179), vo.getPendingReportCount());
|
||||
assertEquals(Long.valueOf(48), vo.getPreallocatedAccommodationCount());
|
||||
assertEquals(Long.valueOf(1), vo.getAbnormalAuthCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDashboardChartsAggregation()
|
||||
{
|
||||
DashboardChartsVo vo = service.getDashboardCharts();
|
||||
assertNotNull(vo);
|
||||
assertEquals(2, vo.getTrendDates().size());
|
||||
assertEquals(Long.valueOf(30), vo.getTrendNormalCounts().get(0));
|
||||
assertEquals(Long.valueOf(2), vo.getTrendAbnormalCounts().get(0));
|
||||
|
||||
// 验证考勤状态构成包含“无签到记录”
|
||||
List<NameValueDto> dist = vo.getAttendanceStatusDistribution();
|
||||
assertNotNull(dist);
|
||||
boolean foundUnlogged = false;
|
||||
long unloggedCount = 0;
|
||||
for (NameValueDto item : dist)
|
||||
{
|
||||
if ("无签到记录".equals(item.getName()))
|
||||
{
|
||||
foundUnlogged = true;
|
||||
unloggedCount = item.getValue();
|
||||
}
|
||||
}
|
||||
assertTrue("应包含无签到记录", foundUnlogged);
|
||||
// 总应到 100,已有记录 77,无签到记录应为 23
|
||||
assertEquals(23L, unloggedCount);
|
||||
|
||||
// 验证场地使用 Top 5
|
||||
assertEquals(2, vo.getVenueUsageTop5().size());
|
||||
assertEquals("主楼101多媒体教室", vo.getVenueUsageTop5().get(0).getVenueName());
|
||||
assertEquals(Long.valueOf(50), vo.getVenueUsageTop5().get(0).getUsageCount());
|
||||
assertEquals(Double.valueOf(100.0), vo.getVenueUsageTop5().get(0).getTotalHours());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttendanceSummaryWithUnloggedRateCalculation()
|
||||
{
|
||||
AttendanceFilterDto filter = new AttendanceFilterDto();
|
||||
filter.setStartDate("2026-08-01");
|
||||
filter.setEndDate("2026-08-31");
|
||||
|
||||
AttendanceSummaryVo vo = service.getAttendanceSummary(filter);
|
||||
assertNotNull(vo);
|
||||
assertEquals(Long.valueOf(20), vo.getLessonTotal());
|
||||
assertEquals(Long.valueOf(100), vo.getExpectedAttendTotal());
|
||||
assertEquals(Long.valueOf(73), vo.getNormalAttendTotal());
|
||||
// 异常人次 = 4 (已打卡异常) + (100 - 77) (无签到) = 27
|
||||
assertEquals(Long.valueOf(27), vo.getAbnormalAttendTotal());
|
||||
// 出勤率 = 73 / 100 = 73.0%
|
||||
assertEquals(Double.valueOf(73.0), vo.getAttendanceRate());
|
||||
assertTrue(vo.getMetricDefinitionNotice().contains("BUSINESS_METRIC_DEFINITION_PENDING_CONFIRMATION"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttendanceSummaryEmptyData()
|
||||
{
|
||||
mockMapper.expectedTotal = 0L;
|
||||
mockMapper.normalTotal = 0L;
|
||||
mockMapper.abnormalTotal = 0L;
|
||||
mockMapper.lessonTotal = 0L;
|
||||
|
||||
AttendanceSummaryVo vo = service.getAttendanceSummary(new AttendanceFilterDto());
|
||||
assertNotNull(vo);
|
||||
assertEquals(Long.valueOf(0), vo.getLessonTotal());
|
||||
assertEquals(Long.valueOf(0), vo.getExpectedAttendTotal());
|
||||
assertEquals(Double.valueOf(0.0), vo.getAttendanceRate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttendanceChartsDateRangeSafety()
|
||||
{
|
||||
// 传入超过 365 天的大范围日期
|
||||
AttendanceFilterDto filter = new AttendanceFilterDto();
|
||||
filter.setStartDate("2020-01-01");
|
||||
filter.setEndDate("2026-09-20");
|
||||
|
||||
AttendanceChartsVo vo = service.getAttendanceCharts(filter);
|
||||
assertNotNull(vo);
|
||||
assertNotNull(vo.getDailyTrend());
|
||||
assertNotNull(vo.getStatusDistribution());
|
||||
assertNotNull(vo.getPlanComparison());
|
||||
assertNotNull(vo.getCourseComparison());
|
||||
assertNotNull(vo.getAbnormalStudentTop10());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStudent360ProfileObjectiveCompliance()
|
||||
{
|
||||
Student360Vo vo = service.getStudent360("S2026001");
|
||||
assertNotNull(vo);
|
||||
assertEquals("S2026001", vo.getStudentId());
|
||||
assertEquals("张三", vo.getStudentName());
|
||||
assertEquals("男", vo.getGenderName());
|
||||
assertEquals("**************1234", vo.getIdCardMasked());
|
||||
assertEquals("已绑定", vo.getUserBindingStatus());
|
||||
|
||||
// 履历
|
||||
assertEquals(1, vo.getEnrollmentTimeline().size());
|
||||
assertEquals("已报到", vo.getEnrollmentTimeline().get(0).getEnrollmentStatusName());
|
||||
assertEquals("人脸识别", vo.getEnrollmentTimeline().get(0).getReportWayName());
|
||||
|
||||
// 考勤
|
||||
assertEquals(Long.valueOf(10), vo.getExpectedAttendCount());
|
||||
assertEquals(Long.valueOf(8), vo.getNormalCount());
|
||||
assertEquals(Long.valueOf(1), vo.getAbsentCount());
|
||||
assertEquals(Long.valueOf(1), vo.getNoCheckinCount());
|
||||
assertEquals(Double.valueOf(80.0), vo.getAttendanceRate());
|
||||
|
||||
// 成绩与自主学习坚决不造假
|
||||
assertFalse(vo.getHasScoreData());
|
||||
assertEquals("暂无成绩数据 (V2成绩表待考务录入)", vo.getScoreStatusDesc());
|
||||
assertFalse(vo.getHasStudyData());
|
||||
assertEquals("暂无自主学习记录 / 待外部平台数据同步", vo.getStudyStatusDesc());
|
||||
|
||||
// 住宿与门禁
|
||||
assertTrue(vo.getHasAccommodation());
|
||||
assertEquals("101", vo.getRoomName());
|
||||
assertEquals("A床", vo.getBedNo());
|
||||
assertEquals("已入住", vo.getAccommodationStatusName());
|
||||
assertEquals("生效中", vo.getAuthStatusName());
|
||||
}
|
||||
|
||||
// ==================== Mock Mapper Implementation ====================
|
||||
private static class MockTrainAnalysisMapper implements TrainAnalysisMapper
|
||||
{
|
||||
Long lessonTotal = 20L;
|
||||
Long expectedTotal = 100L;
|
||||
Long normalTotal = 73L;
|
||||
Long abnormalTotal = 4L;
|
||||
|
||||
public Long countStudents() { return 200L; }
|
||||
public Long countTeachers() { return 20L; }
|
||||
public Long countCourses() { return 24L; }
|
||||
public Long countPlans() { return 13L; }
|
||||
public Long countLessons() { return 118L; }
|
||||
public Long countEnrollments() { return 242L; }
|
||||
public Long countReports() { return 63L; }
|
||||
public Long countCurrentAccommodations() { return 1L; }
|
||||
|
||||
public List<Map<String, Object>> selectPlanStatusDistribution()
|
||||
{
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
Map<String, Object> m1 = new HashMap<>();
|
||||
m1.put("status_code", "0");
|
||||
m1.put("count_num", 10L);
|
||||
list.add(m1);
|
||||
Map<String, Object> m2 = new HashMap<>();
|
||||
m2.put("status_code", "2");
|
||||
m2.put("count_num", 3L);
|
||||
list.add(m2);
|
||||
return list;
|
||||
}
|
||||
|
||||
public Long countAbnormalAttendance() { return 4L; }
|
||||
public Long countPendingReports() { return 179L; }
|
||||
public Long countPreallocatedAccommodations() { return 48L; }
|
||||
public Long countAbnormalDormAuths() { return 1L; }
|
||||
|
||||
public List<Map<String, Object>> selectAttendanceTrend30Days()
|
||||
{
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
Map<String, Object> row1 = new HashMap<>();
|
||||
row1.put("stat_date", "2026-09-01");
|
||||
row1.put("normal_count", 30L);
|
||||
row1.put("abnormal_count", 2L);
|
||||
list.add(row1);
|
||||
Map<String, Object> row2 = new HashMap<>();
|
||||
row2.put("stat_date", "2026-09-02");
|
||||
row2.put("normal_count", 43L);
|
||||
row2.put("abnormal_count", 2L);
|
||||
list.add(row2);
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> selectPlanEnrollmentReportComparisonTop10()
|
||||
{
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("plan_id", "PLAN001");
|
||||
row.put("plan_name", "2026年第一期房桥线路维修专项培训");
|
||||
row.put("enrolled_count", 50L);
|
||||
row.put("reported_count", 45L);
|
||||
list.add(row);
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> selectAttendanceStatusDistribution()
|
||||
{
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
Map<String, Object> r1 = new HashMap<>();
|
||||
r1.put("status_code", "1");
|
||||
r1.put("count_num", 73L);
|
||||
list.add(r1);
|
||||
Map<String, Object> r2 = new HashMap<>();
|
||||
r2.put("status_code", "0");
|
||||
r2.put("count_num", 2L);
|
||||
list.add(r2);
|
||||
Map<String, Object> r3 = new HashMap<>();
|
||||
r3.put("status_code", "4");
|
||||
r3.put("count_num", 2L);
|
||||
list.add(r3);
|
||||
return list;
|
||||
}
|
||||
|
||||
public Long countTotalExpectedAttendanceForPastLessons() { return 100L; }
|
||||
|
||||
public List<Map<String, Object>> selectVenueUsageTop5()
|
||||
{
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
Map<String, Object> r1 = new HashMap<>();
|
||||
r1.put("venue_name", "主楼101多媒体教室");
|
||||
r1.put("usage_count", 50L);
|
||||
r1.put("total_hours", 100.0);
|
||||
list.add(r1);
|
||||
Map<String, Object> r2 = new HashMap<>();
|
||||
r2.put("venue_name", "室外综合接触网实训场");
|
||||
r2.put("usage_count", 30L);
|
||||
r2.put("total_hours", 60.0);
|
||||
list.add(r2);
|
||||
return list;
|
||||
}
|
||||
|
||||
public Long countFilteredLessons(AttendanceFilterDto dto) { return lessonTotal; }
|
||||
public Long countFilteredExpectedAttendance(AttendanceFilterDto dto) { return expectedTotal; }
|
||||
public Long countFilteredNormalAttendance(AttendanceFilterDto dto) { return normalTotal; }
|
||||
public Long countFilteredAbnormalAttendance(AttendanceFilterDto dto) { return abnormalTotal; }
|
||||
|
||||
public List<Map<String, Object>> selectFilteredDailyTrend(AttendanceFilterDto dto)
|
||||
{
|
||||
return selectAttendanceTrend30Days();
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> selectFilteredStatusDistribution(AttendanceFilterDto dto)
|
||||
{
|
||||
return selectAttendanceStatusDistribution();
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> selectPlanAttendanceComparison(AttendanceFilterDto dto)
|
||||
{
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("id", "PLAN001");
|
||||
row.put("name", "计划一");
|
||||
row.put("normal_count", 73L);
|
||||
row.put("abnormal_count", 4L);
|
||||
list.add(row);
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> selectCourseAttendanceComparison(AttendanceFilterDto dto)
|
||||
{
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("id", "C001");
|
||||
row.put("name", "铁道概论");
|
||||
row.put("normal_count", 50L);
|
||||
row.put("abnormal_count", 2L);
|
||||
list.add(row);
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> selectAbnormalStudentTop10(AttendanceFilterDto dto)
|
||||
{
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("student_id", "S2026001");
|
||||
row.put("student_name", "张三");
|
||||
row.put("plan_id", "PLAN001");
|
||||
row.put("plan_name", "计划一");
|
||||
row.put("absent_count", 2L);
|
||||
row.put("late_count", 1L);
|
||||
row.put("early_count", 0L);
|
||||
row.put("leave_count", 1L);
|
||||
row.put("total_abnormal_count", 4L);
|
||||
list.add(row);
|
||||
return list;
|
||||
}
|
||||
|
||||
public Map<String, Object> selectStudentBaseProfile(String studentId)
|
||||
{
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("student_id", studentId);
|
||||
map.put("student_name", "张三");
|
||||
map.put("gender", "0");
|
||||
map.put("birth", new Date());
|
||||
map.put("phone", "13800000000");
|
||||
map.put("email", "zhangsan@example.com");
|
||||
map.put("org_name", "房建公寓段");
|
||||
map.put("id_card_last4", "1234");
|
||||
map.put("user_binding_status", "已绑定");
|
||||
map.put("status", 1);
|
||||
return map;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> selectStudentEnrollmentTimeline(String studentId)
|
||||
{
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("enrollment_id", 1001L);
|
||||
row.put("plan_id", "PLAN001");
|
||||
row.put("plan_name", "计划一");
|
||||
row.put("enrollment_status", 1);
|
||||
row.put("join_time", new Date());
|
||||
row.put("report_way", 1);
|
||||
row.put("verify_result", 1);
|
||||
row.put("report_time", new Date());
|
||||
list.add(row);
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> selectStudentAttendanceStats(String studentId)
|
||||
{
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
Map<String, Object> row1 = new HashMap<>();
|
||||
row1.put("status_code", "1");
|
||||
row1.put("count_num", 8L);
|
||||
list.add(row1);
|
||||
Map<String, Object> row2 = new HashMap<>();
|
||||
row2.put("status_code", "0");
|
||||
row2.put("count_num", 1L);
|
||||
list.add(row2);
|
||||
return list;
|
||||
}
|
||||
|
||||
public Long countStudentExpectedAttendance(String studentId) { return 10L; }
|
||||
|
||||
public Map<String, Object> selectStudentAccommodationAuth(String studentId)
|
||||
{
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("accommodation_id", 801L);
|
||||
map.put("dorm_id", "DORM01");
|
||||
map.put("room_name", "101");
|
||||
map.put("bed_no", "A床");
|
||||
map.put("accommodation_status", 1);
|
||||
map.put("auth_status", "ACTIVE");
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
-- =============================================================================
|
||||
-- Migration: Analytics V1 Permissions & Legacy Menu Deactivation
|
||||
-- Target: KingbaseES, schema public
|
||||
-- Scope: Deactivate legacy dropped score menus, add attendance analytics menu & dashboard permissions
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. Deactivate legacy score menus (2166, 2175, 2176)
|
||||
UPDATE public.sys_menu
|
||||
SET status = '1', visible = '1', update_by = 'admin', update_time = CURRENT_TIMESTAMP, remark = '已废弃旧表停用(V2架构)'
|
||||
WHERE menu_id IN (2166, 2175, 2176);
|
||||
|
||||
-- 2. Insert attendance analytics menu under '统计分析' (parent_id = 2169)
|
||||
INSERT INTO public.sys_menu
|
||||
(menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache,
|
||||
menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
SELECT 2251, '考勤分析', 2169, 1, 'attendance', 'train/analysis/attendance/index', '', 1, 0,
|
||||
'C', '0', '0', 'train:analysis:attendance', 'chart', 'admin', CURRENT_TIMESTAMP, '', NULL, '考勤专题分析页面'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2251);
|
||||
|
||||
-- 3. Insert attendance query button permission
|
||||
INSERT INTO public.sys_menu
|
||||
(menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache,
|
||||
menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
SELECT 2252, '考勤分析查询', 2251, 1, '#', '', '', 1, 0,
|
||||
'F', '0', '0', 'train:analysis:attendance:query', '#', 'admin', CURRENT_TIMESTAMP, '', NULL, '考勤分析数据查询'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2252);
|
||||
|
||||
-- 4. Insert dashboard button permission
|
||||
INSERT INTO public.sys_menu
|
||||
(menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache,
|
||||
menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
SELECT 2253, '管理驾驶舱', 2169, 0, '#', '', '', 1, 0,
|
||||
'F', '0', '0', 'train:analysis:dashboard', '#', 'admin', CURRENT_TIMESTAMP, '', NULL, '管理端驾驶舱权限'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2253);
|
||||
|
||||
-- 5. Grant permissions to train_admin role (role_id = 3)
|
||||
INSERT INTO public.sys_role_menu (role_id, menu_id)
|
||||
SELECT 3, 2251
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_role_menu WHERE role_id = 3 AND menu_id = 2251);
|
||||
|
||||
INSERT INTO public.sys_role_menu (role_id, menu_id)
|
||||
SELECT 3, 2252
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_role_menu WHERE role_id = 3 AND menu_id = 2252);
|
||||
|
||||
INSERT INTO public.sys_role_menu (role_id, menu_id)
|
||||
SELECT 3, 2253
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_role_menu WHERE role_id = 3 AND menu_id = 2253);
|
||||
|
||||
-- 6. Verification
|
||||
DO $$
|
||||
BEGIN
|
||||
IF (SELECT count(*) FROM public.sys_menu WHERE menu_id = 2251 AND perms = 'train:analysis:attendance') <> 1 THEN
|
||||
RAISE EXCEPTION 'Menu 2251 verification failed';
|
||||
END IF;
|
||||
IF (SELECT count(*) FROM public.sys_menu WHERE menu_id = 2252 AND perms = 'train:analysis:attendance:query') <> 1 THEN
|
||||
RAISE EXCEPTION 'Menu 2252 verification failed';
|
||||
END IF;
|
||||
IF (SELECT count(*) FROM public.sys_menu WHERE menu_id = 2253 AND perms = 'train:analysis:dashboard') <> 1 THEN
|
||||
RAISE EXCEPTION 'Menu 2253 verification failed';
|
||||
END IF;
|
||||
IF (SELECT count(*) FROM public.sys_role_menu WHERE role_id = 3 AND menu_id IN (2251, 2252, 2253)) <> 3 THEN
|
||||
RAISE EXCEPTION 'Role assignment verification failed';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,43 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取管理驾驶舱宏观概览
|
||||
export function getDashboardOverview() {
|
||||
return request({
|
||||
url: '/train/analysis/dashboard/overview',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取管理驾驶舱图表数据
|
||||
export function getDashboardCharts() {
|
||||
return request({
|
||||
url: '/train/analysis/dashboard/charts',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取考勤专题分析汇总 KPI
|
||||
export function getAttendanceSummary(params) {
|
||||
return request({
|
||||
url: '/train/analysis/attendance/summary',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取考勤专题分析图表集合
|
||||
export function getAttendanceCharts(params) {
|
||||
return request({
|
||||
url: '/train/analysis/attendance/charts',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取指定学员 360 客观全景画像
|
||||
export function getStudent360(studentId) {
|
||||
return request({
|
||||
url: '/train/analysis/portrait/student-360/' + studentId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -4,29 +4,7 @@
|
||||
<div v-if="loadingRoles" v-loading="true" style="height: 300px;" />
|
||||
|
||||
<!-- 1. 管理员优先展示管理端综合驾驶舱 (最高优先级: admin / train_admin) -->
|
||||
<div v-else-if="isAdmin" class="app-container home">
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :lg="14" style="padding-left: 20px;">
|
||||
<el-card class="box-card">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold; font-size: 16px;">SYFJGYD学员管理系统</span>
|
||||
</div>
|
||||
<div class="text item" style="margin-bottom: 16px; font-size: 14px; color: #606266; line-height: 1.8;">
|
||||
<p>欢迎使用SYFJGYD学员管理系统。本平台提供培训计划编排、学员主档与参训管理、智能排课排程、电子班牌联动、教学设施与实训场地调度,以及全流程多维学习数据分析等核心功能。</p>
|
||||
</div>
|
||||
<div class="text item" style="margin-bottom: 20px; text-align: center">
|
||||
<img src="@/assets/images/version.png" alt="系统架构" style="max-width: 100%; border-radius: 4px;">
|
||||
</div>
|
||||
<el-divider style="margin: 15px 0;" />
|
||||
<div class="text item" style="display: flex; justify-content: space-between; color: #909399; font-size: 13px;">
|
||||
<span>系统版本:V1.0</span>
|
||||
<span>版权所有:中国科学院沈阳计算技术研究所有限公司</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider />
|
||||
</div>
|
||||
<admin-dashboard v-else-if="isAdmin" />
|
||||
|
||||
<!-- 2. 异常检测:同时拥有教师与学员互斥角色,展示受控安全告警 -->
|
||||
<div v-else-if="hasRoleConflict" class="app-container">
|
||||
@@ -59,12 +37,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AdminDashboard from '@/views/train/analysis/dashboard/index'
|
||||
import TeacherDashboard from '@/views/train/me/teacher/index'
|
||||
import StudentDashboard from '@/views/train/me/student/index'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: {
|
||||
AdminDashboard,
|
||||
TeacherDashboard,
|
||||
StudentDashboard
|
||||
},
|
||||
|
||||
@@ -0,0 +1,655 @@
|
||||
<template>
|
||||
<div class="dashboard-editor-container">
|
||||
<!-- 1. 顶部标题与数据更新状态 -->
|
||||
<div class="dashboard-header">
|
||||
<div class="header-title">
|
||||
<h2>SYFJGYD学员管理系统 · 管理驾驶舱</h2>
|
||||
<span class="subtitle">全校培训教学运行、排课调度、学员考勤与生活保障综合宏观指挥大盘</span>
|
||||
</div>
|
||||
<div class="header-action">
|
||||
<el-button icon="el-icon-refresh" size="mini" type="primary" plain :loading="loading" @click="refreshAll">刷新数据</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 核心 KPI 卡片 (8项客观清晰指标) -->
|
||||
<el-row :gutter="16" class="kpi-row" v-loading="loadingOverview">
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">学员总数</div>
|
||||
<div class="kpi-value text-primary">{{ overview.studentTotal || 0 }}</div>
|
||||
<div class="kpi-sub">主档档案正常</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">教师总数</div>
|
||||
<div class="kpi-value text-success">{{ overview.teacherTotal || 0 }}</div>
|
||||
<div class="kpi-sub">专兼职讲师</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">课程总数</div>
|
||||
<div class="kpi-value text-info">{{ overview.courseTotal || 0 }}</div>
|
||||
<div class="kpi-sub">标准课程库</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">培训计划</div>
|
||||
<div class="kpi-value text-warning">{{ overview.planTotal || 0 }}</div>
|
||||
<div class="kpi-sub">全周期班级数</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">累计排课</div>
|
||||
<div class="kpi-value">{{ overview.lessonTotal || 0 }}</div>
|
||||
<div class="kpi-sub">课次课时调度</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">累计参训</div>
|
||||
<div class="kpi-value">{{ overview.enrollmentTotal || 0 }}</div>
|
||||
<div class="kpi-sub">人次总规模</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">已报到人数</div>
|
||||
<div class="kpi-value text-success">{{ overview.reportTotal || 0 }}</div>
|
||||
<div class="kpi-sub">核验确认人次</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">当前在宿</div>
|
||||
<div class="kpi-value text-primary">{{ overview.accommodationCurrent || 0 }}</div>
|
||||
<div class="kpi-sub">宿舍在住学员</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 3. 计划状态与业务异常提醒条 -->
|
||||
<el-row :gutter="16" style="margin-top: 16px;">
|
||||
<!-- 计划运行状态分布 -->
|
||||
<el-col :xs="24" :sm="12" :md="10">
|
||||
<el-card shadow="never" class="status-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-date" style="color: #409EFF; margin-right: 6px;"></i>开班计划运行状态</span>
|
||||
</div>
|
||||
<div class="plan-status-tags" v-loading="loadingOverview">
|
||||
<div class="status-tag-item">
|
||||
<span class="dot dot-info"></span>
|
||||
<span class="label">未开始:</span>
|
||||
<span class="count">{{ planStatus.notStarted || 0 }}</span> 个
|
||||
</div>
|
||||
<div class="status-tag-item">
|
||||
<span class="dot dot-success"></span>
|
||||
<span class="label">进行中:</span>
|
||||
<span class="count">{{ planStatus.ongoing || 0 }}</span> 个
|
||||
</div>
|
||||
<div class="status-tag-item">
|
||||
<span class="dot dot-ended"></span>
|
||||
<span class="label">已结束:</span>
|
||||
<span class="count">{{ planStatus.ended || 0 }}</span> 个
|
||||
</div>
|
||||
<div class="status-tag-item">
|
||||
<span class="dot dot-danger"></span>
|
||||
<span class="label">已取消:</span>
|
||||
<span class="count">{{ planStatus.cancelled || 0 }}</span> 个
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 业务待办与异常提醒 -->
|
||||
<el-col :xs="24" :sm="12" :md="14">
|
||||
<el-card shadow="never" class="status-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-warning-outline" style="color: #E6A23C; margin-right: 6px;"></i>业务运行待办与治理提醒</span>
|
||||
<span class="tip-text">点击可直接跳转业务处理</span>
|
||||
</div>
|
||||
<div class="alert-grid" v-loading="loadingOverview">
|
||||
<div class="alert-item clickable" @click="goTo('/train/analysis/attendance')">
|
||||
<div class="alert-icon bg-danger"><i class="el-icon-time" /></div>
|
||||
<div class="alert-content">
|
||||
<div class="alert-num text-danger">{{ overview.abnormalAttendanceCount || 0 }}</div>
|
||||
<div class="alert-title">异常考勤人次</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert-item clickable" @click="goTo('/train/report')">
|
||||
<div class="alert-icon bg-warning"><i class="el-icon-document-checked" /></div>
|
||||
<div class="alert-content">
|
||||
<div class="alert-num text-warning">{{ overview.pendingReportCount || 0 }}</div>
|
||||
<div class="alert-title">待报到参训学员</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert-item clickable" @click="goTo('/train/accommodation')">
|
||||
<div class="alert-icon bg-primary"><i class="el-icon-office-building" /></div>
|
||||
<div class="alert-content">
|
||||
<div class="alert-num text-primary">{{ overview.preallocatedAccommodationCount || 0 }}</div>
|
||||
<div class="alert-title">住宿预分配待入住</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert-item clickable" @click="goTo('/train/extra')">
|
||||
<div class="alert-icon bg-danger"><i class="el-icon-key" /></div>
|
||||
<div class="alert-content">
|
||||
<div class="alert-num text-danger">{{ overview.abnormalAuthCount || 0 }}</div>
|
||||
<div class="alert-title">门禁授权异常数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 4. 图表行 1:趋势与班级对比 -->
|
||||
<el-row :gutter="16" style="margin-top: 16px;">
|
||||
<!-- 近30天考勤走势 -->
|
||||
<el-col :xs="24" :sm="24" :lg="14">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-data-line" style="color: #409EFF; margin-right: 6px;"></i>近 30 天考勤打卡情况走势</span>
|
||||
<span class="tip-text">正常签到 vs 异常考勤人次</span>
|
||||
</div>
|
||||
<div v-loading="loadingCharts" class="chart-wrapper">
|
||||
<div ref="trendChart" style="height: 320px; width: 100%;" />
|
||||
<div v-if="!loadingCharts && (!charts.trendDates || charts.trendDates.length === 0)" class="empty-chart">
|
||||
<el-empty description="近 30 天暂无考勤打卡流水" :image-size="70" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 培训计划参训/报到对比 -->
|
||||
<el-col :xs="24" :sm="24" :lg="10">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-s-data" style="color: #67C23A; margin-right: 6px;"></i>培训计划参训 / 报到对比 (Top 10)</span>
|
||||
</div>
|
||||
<div v-loading="loadingCharts" class="chart-wrapper">
|
||||
<div ref="planCompChart" style="height: 320px; width: 100%;" />
|
||||
<div v-if="!loadingCharts && (!charts.planComparisonNames || charts.planComparisonNames.length === 0)" class="empty-chart">
|
||||
<el-empty description="暂无培训计划数据" :image-size="70" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 5. 图表行 2:状态构成与场地使用 -->
|
||||
<el-row :gutter="16" style="margin-top: 16px; margin-bottom: 24px;">
|
||||
<!-- 全校考勤状态构成 -->
|
||||
<el-col :xs="24" :sm="24" :lg="10">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-pie-chart" style="color: #E6A23C; margin-right: 6px;"></i>全校考勤状态构成分布</span>
|
||||
<span class="tip-text">含无签到记录独立统计</span>
|
||||
</div>
|
||||
<div v-loading="loadingCharts" class="chart-wrapper">
|
||||
<div ref="statusChart" style="height: 300px; width: 100%;" />
|
||||
<div v-if="!loadingCharts && (!charts.attendanceStatusDistribution || charts.attendanceStatusDistribution.length === 0)" class="empty-chart">
|
||||
<el-empty description="暂无考勤状态数据" :image-size="70" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 教学场地使用频次 Top 5 -->
|
||||
<el-col :xs="24" :sm="24" :lg="14">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-school" style="color: #909399; margin-right: 6px;"></i>教学场地使用频次与累计课时 Top 5</span>
|
||||
<span class="tip-text">依排课记录客观统计,不推算无分母利用率</span>
|
||||
</div>
|
||||
<div v-loading="loadingCharts" class="chart-wrapper">
|
||||
<div ref="venueChart" style="height: 300px; width: 100%;" />
|
||||
<div v-if="!loadingCharts && (!charts.venueUsageTop5 || charts.venueUsageTop5.length === 0)" class="empty-chart">
|
||||
<el-empty description="暂无排课场地使用记录" :image-size="70" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getDashboardOverview, getDashboardCharts } from '@/api/train/analysis'
|
||||
|
||||
export default {
|
||||
name: 'AdminDashboard',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
loadingOverview: false,
|
||||
loadingCharts: false,
|
||||
overview: {},
|
||||
charts: {},
|
||||
trendChartInstance: null,
|
||||
planCompChartInstance: null,
|
||||
statusChartInstance: null,
|
||||
venueChartInstance: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
planStatus() {
|
||||
return this.overview.planStatusCounts || {}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.refreshAll()
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
this.disposeCharts()
|
||||
},
|
||||
methods: {
|
||||
refreshAll() {
|
||||
this.fetchOverview()
|
||||
this.fetchCharts()
|
||||
},
|
||||
fetchOverview() {
|
||||
this.loadingOverview = true
|
||||
getDashboardOverview().then(res => {
|
||||
if (res.code === 200) {
|
||||
this.overview = res.data || {}
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loadingOverview = false
|
||||
})
|
||||
},
|
||||
fetchCharts() {
|
||||
this.loadingCharts = true
|
||||
getDashboardCharts().then(res => {
|
||||
if (res.code === 200) {
|
||||
this.charts = res.data || {}
|
||||
this.$nextTick(() => {
|
||||
this.renderTrendChart()
|
||||
this.renderPlanCompChart()
|
||||
this.renderStatusChart()
|
||||
this.renderVenueChart()
|
||||
})
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loadingCharts = false
|
||||
})
|
||||
},
|
||||
renderTrendChart() {
|
||||
if (!this.$refs.trendChart) return
|
||||
if (!this.trendChartInstance) {
|
||||
this.trendChartInstance = this.$echarts.init(this.$refs.trendChart)
|
||||
}
|
||||
const dates = this.charts.trendDates || []
|
||||
const normal = this.charts.trendNormalCounts || []
|
||||
const abnormal = this.charts.trendAbnormalCounts || []
|
||||
|
||||
const option = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['正常签到人次', '异常考勤人次'], top: 0 },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: dates,
|
||||
axisLine: { lineStyle: { color: '#909399' } }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
minInterval: 1,
|
||||
splitLine: { lineStyle: { type: 'dashed' } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '正常签到人次',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: normal,
|
||||
itemStyle: { color: '#67C23A' },
|
||||
areaStyle: {
|
||||
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(103,194,58,0.3)' },
|
||||
{ offset: 1, color: 'rgba(103,194,58,0.02)' }
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '异常考勤人次',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: abnormal,
|
||||
itemStyle: { color: '#F56C6C' },
|
||||
areaStyle: {
|
||||
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(245,108,108,0.3)' },
|
||||
{ offset: 1, color: 'rgba(245,108,108,0.02)' }
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
this.trendChartInstance.setOption(option)
|
||||
},
|
||||
renderPlanCompChart() {
|
||||
if (!this.$refs.planCompChart) return
|
||||
if (!this.planCompChartInstance) {
|
||||
this.planCompChartInstance = this.$echarts.init(this.$refs.planCompChart)
|
||||
}
|
||||
const names = this.charts.planComparisonNames || []
|
||||
const enrolled = this.charts.planEnrolledCounts || []
|
||||
const reported = this.charts.planReportedCounts || []
|
||||
|
||||
const option = {
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
legend: { data: ['参训人数', '报到人数'], top: 0 },
|
||||
grid: { left: '3%', right: '4%', bottom: '8%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: names,
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 20,
|
||||
formatter: val => val.length > 7 ? val.substring(0, 7) + '...' : val
|
||||
}
|
||||
},
|
||||
yAxis: { type: 'value', minInterval: 1 },
|
||||
series: [
|
||||
{
|
||||
name: '参训人数',
|
||||
type: 'bar',
|
||||
data: enrolled,
|
||||
itemStyle: { color: '#409EFF' }
|
||||
},
|
||||
{
|
||||
name: '报到人数',
|
||||
type: 'bar',
|
||||
data: reported,
|
||||
itemStyle: { color: '#67C23A' }
|
||||
}
|
||||
]
|
||||
}
|
||||
this.planCompChartInstance.setOption(option)
|
||||
},
|
||||
renderStatusChart() {
|
||||
if (!this.$refs.statusChart) return
|
||||
if (!this.statusChartInstance) {
|
||||
this.statusChartInstance = this.$echarts.init(this.$refs.statusChart)
|
||||
}
|
||||
const rawList = this.charts.attendanceStatusDistribution || []
|
||||
const data = rawList.map(item => ({ name: item.name, value: item.value }))
|
||||
|
||||
const option = {
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c} 人次 ({d}%)' },
|
||||
legend: { bottom: 0, left: 'center' },
|
||||
color: ['#67C23A', '#E6A23C', '#F56C6C', '#409EFF', '#909399', '#C0C4CC'],
|
||||
series: [
|
||||
{
|
||||
name: '考勤状态',
|
||||
type: 'pie',
|
||||
radius: ['45%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: false, position: 'center' },
|
||||
emphasis: {
|
||||
label: { show: true, fontSize: 14, fontWeight: 'bold' }
|
||||
},
|
||||
data: data
|
||||
}
|
||||
]
|
||||
}
|
||||
this.statusChartInstance.setOption(option)
|
||||
},
|
||||
renderVenueChart() {
|
||||
if (!this.$refs.venueChart) return
|
||||
if (!this.venueChartInstance) {
|
||||
this.venueChartInstance = this.$echarts.init(this.$refs.venueChart)
|
||||
}
|
||||
const raw = this.charts.venueUsageTop5 || []
|
||||
const names = raw.map(i => i.venueName).reverse()
|
||||
const counts = raw.map(i => i.usageCount).reverse()
|
||||
const hours = raw.map(i => i.totalHours).reverse()
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
formatter: params => {
|
||||
const name = params[0].name
|
||||
let str = `<b>${name}</b><br/>`
|
||||
params.forEach(p => {
|
||||
str += `${p.seriesName}: ${p.value} ${p.seriesName.includes('课时') ? '小时' : '次'}<br/>`
|
||||
})
|
||||
return str
|
||||
}
|
||||
},
|
||||
legend: { data: ['使用频次', '累计课时(小时)'], top: 0 },
|
||||
grid: { left: '3%', right: '5%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'value', minInterval: 1 },
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: names,
|
||||
axisLabel: {
|
||||
formatter: val => val.length > 8 ? val.substring(0, 8) + '...' : val
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '使用频次',
|
||||
type: 'bar',
|
||||
data: counts,
|
||||
itemStyle: { color: '#36cbcb' }
|
||||
},
|
||||
{
|
||||
name: '累计课时(小时)',
|
||||
type: 'bar',
|
||||
data: hours,
|
||||
itemStyle: { color: '#4ecb73' }
|
||||
}
|
||||
]
|
||||
}
|
||||
this.venueChartInstance.setOption(option)
|
||||
},
|
||||
handleResize() {
|
||||
if (this.trendChartInstance) this.trendChartInstance.resize()
|
||||
if (this.planCompChartInstance) this.planCompChartInstance.resize()
|
||||
if (this.statusChartInstance) this.statusChartInstance.resize()
|
||||
if (this.venueChartInstance) this.venueChartInstance.resize()
|
||||
},
|
||||
disposeCharts() {
|
||||
if (this.trendChartInstance) { this.trendChartInstance.dispose(); this.trendChartInstance = null }
|
||||
if (this.planCompChartInstance) { this.planCompChartInstance.dispose(); this.planCompChartInstance = null }
|
||||
if (this.statusChartInstance) { this.statusChartInstance.dispose(); this.statusChartInstance = null }
|
||||
if (this.venueChartInstance) { this.venueChartInstance.dispose(); this.venueChartInstance = null }
|
||||
},
|
||||
goTo(path) {
|
||||
if (path) this.$router.push(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dashboard-editor-container {
|
||||
padding: 16px 20px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
padding: 16px 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
border-left: 4px solid #409EFF;
|
||||
|
||||
.header-title h2 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 18px;
|
||||
color: #303133;
|
||||
}
|
||||
.header-title .subtitle {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-row {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 14px 16px;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.kpi-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.kpi-value {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.kpi-sub {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.text-primary { color: #409EFF !important; }
|
||||
.text-success { color: #67C23A !important; }
|
||||
.text-warning { color: #E6A23C !important; }
|
||||
.text-danger { color: #F56C6C !important; }
|
||||
.text-info { color: #909399 !important; }
|
||||
|
||||
.status-card {
|
||||
min-height: 140px;
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
.tip-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plan-status-tags {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 10px 0;
|
||||
.status-tag-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
&.dot-info { background: #909399; }
|
||||
&.dot-success { background: #67C23A; }
|
||||
&.dot-ended { background: #409EFF; }
|
||||
&.dot-danger { background: #F56C6C; }
|
||||
}
|
||||
.count {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
.alert-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fafafa;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
transition: all 0.2s;
|
||||
&.clickable:hover {
|
||||
cursor: pointer;
|
||||
background: #f0f7ff;
|
||||
border-color: #b3d8ff;
|
||||
}
|
||||
.alert-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
margin-right: 10px;
|
||||
&.bg-danger { background: #F56C6C; }
|
||||
&.bg-warning { background: #E6A23C; }
|
||||
&.bg-primary { background: #409EFF; }
|
||||
}
|
||||
.alert-content {
|
||||
.alert-num {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.alert-title {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
.tip-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
.empty-chart {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user