zlwu il y a 1 an
Parent
commit
9c3741562d

+ 2 - 1
.gitignore

@@ -326,4 +326,5 @@ docs/变更管理/
 查找界面中涉及到的权限字符.md
 docs/部署文档/nginx配置/hzdyjsweb.camasort.com.conf
 首页完善的提示词.md
-scripts/
+scripts/
+学生宿舍卫生检查、学生违纪记录统计.md

+ 82 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentStatisticsController.java

@@ -0,0 +1,82 @@
+package org.dromara.xg.controller;
+
+import java.util.List;
+
+import lombok.RequiredArgsConstructor;
+import jakarta.validation.constraints.*;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.validation.annotation.Validated;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.xg.domain.bo.XgStudentStatisticsBo;
+import org.dromara.xg.domain.vo.XgStudentStatisticsVo;
+import org.dromara.xg.domain.vo.XgCascadeOptionVo;
+import org.dromara.xg.service.IXgStudentStatisticsService;
+
+/**
+ * 学生宿舍卫生检查、违纪记录统计
+ *
+ * @author zlwu
+ * @date 2025-01-14
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/xg/studentStatistics")
+public class XgStudentStatisticsController extends BaseController {
+
+    private final IXgStudentStatisticsService studentStatisticsService;
+
+    /**
+     * 查询学生宿舍卫生检查、违纪记录统计数据
+     */
+    @SaCheckPermission("xg:studentStatistics:query")
+    @PostMapping("/statistics")
+    public R<XgStudentStatisticsVo> getStatistics(@Validated @RequestBody XgStudentStatisticsBo bo) {
+        return R.ok(studentStatisticsService.getStatistics(bo));
+    }
+
+    /**
+     * 导出学生宿舍卫生检查、违纪记录统计数据
+     */
+    @SaCheckPermission("xg:studentStatistics:export")
+    @Log(title = "学生统计数据", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void exportStatistics(@Validated @RequestBody XgStudentStatisticsBo bo,
+            jakarta.servlet.http.HttpServletResponse response) {
+        studentStatisticsService.exportStatistics(bo, response);
+    }
+
+    /**
+     * 获取二级学院列表
+     */
+    @SaCheckPermission("xg:studentStatistics:query")
+    @GetMapping("/colleges")
+    public R<List<XgCascadeOptionVo>> getColleges() {
+        return R.ok(studentStatisticsService.getColleges());
+    }
+
+    /**
+     * 根据学院ID获取专业列表
+     */
+    @SaCheckPermission("xg:studentStatistics:query")
+    @GetMapping("/majors/{collegeId}")
+    public R<List<XgCascadeOptionVo>> getMajorsByCollegeId(
+            @NotNull(message = "学院ID不能为空") @PathVariable Long collegeId) {
+        return R.ok(studentStatisticsService.getMajorsByCollegeId(collegeId));
+    }
+
+    /**
+     * 根据专业ID获取班级列表
+     */
+    @SaCheckPermission("xg:studentStatistics:query")
+    @GetMapping("/classes/{majorId}")
+    public R<List<XgCascadeOptionVo>> getClassesByMajorId(
+            @NotNull(message = "专业ID不能为空") @PathVariable Long majorId) {
+        return R.ok(studentStatisticsService.getClassesByMajorId(majorId));
+    }
+
+}

+ 48 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgStudentStatisticsBo.java

@@ -0,0 +1,48 @@
+package org.dromara.xg.domain.bo;
+
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import jakarta.validation.constraints.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import java.time.LocalDateTime;
+
+/**
+ * 学生宿舍卫生检查、违纪记录统计查询业务对象
+ *
+ * @author zlwu
+ * @date 2025-01-14
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class XgStudentStatisticsBo extends BaseEntity {
+
+    /**
+     * 统计开始时间
+     */
+    @NotNull(message = "统计开始时间不能为空")
+    private LocalDateTime startTime;
+
+    /**
+     * 统计结束时间
+     */
+    @NotNull(message = "统计结束时间不能为空")
+    private LocalDateTime endTime;
+
+    /**
+     * 二级学院ID
+     */
+    private Long collegeId;
+
+    /**
+     * 专业ID
+     */
+    private Long majorId;
+
+    /**
+     * 班级ID
+     */
+    private Long classId;
+
+}

+ 34 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgCascadeOptionVo.java

@@ -0,0 +1,34 @@
+package org.dromara.xg.domain.vo;
+
+import lombok.Data;
+
+/**
+ * 级联选择器选项视图对象
+ *
+ * @author zlwu
+ * @date 2025-01-14
+ */
+@Data
+public class XgCascadeOptionVo {
+
+    /**
+     * ID
+     */
+    private Long id;
+
+    /**
+     * 显示名称
+     */
+    private String label;
+
+    /**
+     * 值
+     */
+    private String value;
+
+    /**
+     * 排序
+     */
+    private Integer sort;
+
+}

+ 117 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgStudentStatisticsVo.java

@@ -0,0 +1,117 @@
+package org.dromara.xg.domain.vo;
+
+import lombok.Data;
+import java.util.List;
+
+/**
+ * 学生宿舍卫生检查、违纪记录统计视图对象
+ *
+ * @author zlwu
+ * @date 2025-01-14
+ */
+@Data
+public class XgStudentStatisticsVo {
+
+    /**
+     * 统计维度 (college-学院, major-专业, class-班级, classDetail-班级详情)
+     */
+    private String dimension;
+
+    /**
+     * 统计图表数据
+     */
+    private List<StatisticsChartData> chartData;
+
+    /**
+     * 明细数据
+     */
+    private List<StatisticsDetailData> detailData;
+
+    /**
+     * 图表数据内部类
+     */
+    @Data
+    public static class StatisticsChartData {
+        /**
+         * 名称 (学院名、专业名、班级名)
+         */
+        private String name;
+
+        /**
+         * 宿舍扣分总数
+         */
+        private Integer dormDeductionCount;
+
+        /**
+         * 宿舍违纪记录总数
+         */
+        private Integer dormViolationCount;
+
+        /**
+         * 宿舍违纪记录扣分总数
+         */
+        private Double dormViolationDeduction;
+
+        /**
+         * 教学区违纪记录总数
+         */
+        private Integer teachingViolationCount;
+
+        /**
+         * 教学区违纪记录扣分总数
+         */
+        private Double teachingViolationDeduction;
+    }
+
+    /**
+     * 明细数据内部类
+     */
+    @Data
+    public static class StatisticsDetailData {
+        /**
+         * ID
+         */
+        private Long id;
+
+        /**
+         * 名称
+         */
+        private String name;
+
+        /**
+         * 上级名称(如学院下的专业名称显示学院名)
+         */
+        private String parentName;
+
+        /**
+         * 宿舍扣分总数
+         */
+        private Integer dormDeductionCount;
+
+        /**
+         * 宿舍违纪记录总数
+         */
+        private Integer dormViolationCount;
+
+        /**
+         * 宿舍违纪记录扣分总数
+         */
+        private Double dormViolationDeduction;
+
+        /**
+         * 教学区违纪记录总数
+         */
+        private Integer teachingViolationCount;
+
+        /**
+         * 教学区违纪记录扣分总数
+         */
+        private Double teachingViolationDeduction;
+
+        /**
+         * 合计扣分
+         */
+        private Double totalDeduction;
+    }
+
+}

+ 63 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentStatisticsMapper.java

@@ -0,0 +1,63 @@
+package org.dromara.xg.mapper;
+
+import org.apache.ibatis.annotations.Param;
+import org.dromara.xg.domain.bo.XgStudentStatisticsBo;
+import org.dromara.xg.domain.vo.XgStudentStatisticsVo;
+import org.dromara.xg.domain.vo.XgCascadeOptionVo;
+
+import java.util.List;
+
+/**
+ * 学生宿舍卫生检查、违纪记录统计Mapper接口
+ *
+ * @author zlwu
+ * @date 2025-01-14
+ */
+public interface XgStudentStatisticsMapper {
+
+    /**
+     * 按学院维度统计图表数据
+     */
+    List<XgStudentStatisticsVo.StatisticsChartData> getCollegeStatistics(@Param("bo") XgStudentStatisticsBo bo);
+
+    /**
+     * 按学院维度统计明细数据
+     */
+    List<XgStudentStatisticsVo.StatisticsDetailData> getCollegeDetailStatistics(@Param("bo") XgStudentStatisticsBo bo);
+
+    /**
+     * 按专业维度统计图表数据
+     */
+    List<XgStudentStatisticsVo.StatisticsChartData> getMajorStatistics(@Param("bo") XgStudentStatisticsBo bo);
+
+    /**
+     * 按专业维度统计明细数据
+     */
+    List<XgStudentStatisticsVo.StatisticsDetailData> getMajorDetailStatistics(@Param("bo") XgStudentStatisticsBo bo);
+
+    /**
+     * 按班级维度统计图表数据
+     */
+    List<XgStudentStatisticsVo.StatisticsChartData> getClassStatistics(@Param("bo") XgStudentStatisticsBo bo);
+
+    /**
+     * 按班级维度统计明细数据
+     */
+    List<XgStudentStatisticsVo.StatisticsDetailData> getClassDetailStatistics(@Param("bo") XgStudentStatisticsBo bo);
+
+    /**
+     * 获取二级学院列表
+     */
+    List<XgCascadeOptionVo> getColleges();
+
+    /**
+     * 根据学院ID获取专业列表
+     */
+    List<XgCascadeOptionVo> getMajorsByCollegeId(@Param("collegeId") Long collegeId);
+
+    /**
+     * 根据专业ID获取班级列表
+     */
+    List<XgCascadeOptionVo> getClassesByMajorId(@Param("majorId") Long majorId);
+
+}

+ 56 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentStatisticsService.java

@@ -0,0 +1,56 @@
+package org.dromara.xg.service;
+
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.dromara.xg.domain.bo.XgStudentStatisticsBo;
+import org.dromara.xg.domain.vo.XgStudentStatisticsVo;
+import org.dromara.xg.domain.vo.XgCascadeOptionVo;
+
+/**
+ * 学生宿舍卫生检查、违纪记录统计Service接口
+ *
+ * @author zlwu
+ * @date 2025-01-14
+ */
+public interface IXgStudentStatisticsService {
+
+    /**
+     * 获取统计数据
+     *
+     * @param bo 查询条件
+     * @return 统计结果
+     */
+    XgStudentStatisticsVo getStatistics(XgStudentStatisticsBo bo);
+
+    /**
+     * 导出统计数据
+     *
+     * @param bo       查询条件
+     * @param response HTTP响应
+     */
+    void exportStatistics(XgStudentStatisticsBo bo, HttpServletResponse response);
+
+    /**
+     * 获取二级学院列表
+     *
+     * @return 学院列表
+     */
+    List<XgCascadeOptionVo> getColleges();
+
+    /**
+     * 根据学院ID获取专业列表
+     *
+     * @param collegeId 学院ID
+     * @return 专业列表
+     */
+    List<XgCascadeOptionVo> getMajorsByCollegeId(Long collegeId);
+
+    /**
+     * 根据专业ID获取班级列表
+     *
+     * @param majorId 专业ID
+     * @return 班级列表
+     */
+    List<XgCascadeOptionVo> getClassesByMajorId(Long majorId);
+
+}

+ 117 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentStatisticsServiceImpl.java

@@ -0,0 +1,117 @@
+package org.dromara.xg.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import jakarta.servlet.http.HttpServletResponse;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.xg.service.IXgStudentStatisticsService;
+import org.dromara.xg.domain.bo.XgStudentStatisticsBo;
+import org.dromara.xg.domain.vo.XgStudentStatisticsVo;
+import org.dromara.xg.domain.vo.XgCascadeOptionVo;
+import org.dromara.xg.mapper.XgStudentStatisticsMapper;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 学生宿舍卫生检查、违纪记录统计Service业务层处理
+ *
+ * @author zlwu
+ * @date 2025-01-14
+ */
+@RequiredArgsConstructor
+@Service
+public class XgStudentStatisticsServiceImpl implements IXgStudentStatisticsService {
+
+    private final XgStudentStatisticsMapper statisticsMapper;
+
+    @Override
+    public XgStudentStatisticsVo getStatistics(XgStudentStatisticsBo bo) {
+        XgStudentStatisticsVo result = new XgStudentStatisticsVo();
+
+        // 确定统计维度
+        String dimension = determineDimension(bo);
+        result.setDimension(dimension);
+
+        // 获取图表数据和明细数据
+        switch (dimension) {
+            case "college":
+                result.setChartData(statisticsMapper.getCollegeStatistics(bo));
+                result.setDetailData(statisticsMapper.getCollegeDetailStatistics(bo));
+                break;
+            case "major":
+                result.setChartData(statisticsMapper.getMajorStatistics(bo));
+                result.setDetailData(statisticsMapper.getMajorDetailStatistics(bo));
+                break;
+            case "class":
+                result.setChartData(statisticsMapper.getClassStatistics(bo));
+                result.setDetailData(statisticsMapper.getClassDetailStatistics(bo));
+                break;
+            case "classDetail":
+                // 班级详情统计:图表数据和明细数据都只显示指定班级
+                List<XgStudentStatisticsVo.StatisticsDetailData> detailData = statisticsMapper
+                        .getClassDetailStatistics(bo);
+                result.setDetailData(detailData);
+                // 将明细数据转换为图表数据格式
+                result.setChartData(convertDetailToChartData(detailData));
+                break;
+        }
+
+        return result;
+    }
+
+    @Override
+    public void exportStatistics(XgStudentStatisticsBo bo, HttpServletResponse response) {
+        XgStudentStatisticsVo statistics = getStatistics(bo);
+        ExcelUtil.exportExcel(statistics.getDetailData(), "学生统计数据", XgStudentStatisticsVo.StatisticsDetailData.class,
+                response);
+    }
+
+    @Override
+    public List<XgCascadeOptionVo> getColleges() {
+        return statisticsMapper.getColleges();
+    }
+
+    @Override
+    public List<XgCascadeOptionVo> getMajorsByCollegeId(Long collegeId) {
+        return statisticsMapper.getMajorsByCollegeId(collegeId);
+    }
+
+    @Override
+    public List<XgCascadeOptionVo> getClassesByMajorId(Long majorId) {
+        return statisticsMapper.getClassesByMajorId(majorId);
+    }
+
+    /**
+     * 确定统计维度
+     */
+    private String determineDimension(XgStudentStatisticsBo bo) {
+        if (bo.getClassId() != null) {
+            return "classDetail";
+        } else if (bo.getMajorId() != null) {
+            return "class";
+        } else if (bo.getCollegeId() != null) {
+            return "major";
+        } else {
+            return "college";
+        }
+    }
+
+    /**
+     * 将明细数据转换为图表数据格式
+     */
+    private List<XgStudentStatisticsVo.StatisticsChartData> convertDetailToChartData(
+            List<XgStudentStatisticsVo.StatisticsDetailData> detailData) {
+        return detailData.stream().map(detail -> {
+            XgStudentStatisticsVo.StatisticsChartData chartData = new XgStudentStatisticsVo.StatisticsChartData();
+            chartData.setName(detail.getName());
+            chartData.setDormDeductionCount(detail.getDormDeductionCount());
+            chartData.setDormViolationCount(detail.getDormViolationCount());
+            chartData.setDormViolationDeduction(detail.getDormViolationDeduction());
+            chartData.setTeachingViolationCount(detail.getTeachingViolationCount());
+            chartData.setTeachingViolationDeduction(detail.getTeachingViolationDeduction());
+            return chartData;
+        }).collect(java.util.stream.Collectors.toList());
+    }
+
+}

+ 504 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgStudentStatisticsMapper.xml

@@ -0,0 +1,504 @@
+<?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="org.dromara.xg.mapper.XgStudentStatisticsMapper">
+
+    <!-- 按学院维度统计图表数据 -->
+    <select id="getCollegeStatistics" resultType="org.dromara.xg.domain.vo.XgStudentStatisticsVo$StatisticsChartData">
+        SELECT
+            d.dept_name as name,
+            COALESCE(dorm_stats.dormDeductionCount, 0) as dormDeductionCount,
+            COALESCE(dorm_violation_stats.dormViolationCount, 0) as dormViolationCount,
+            COALESCE(dorm_violation_stats.dormViolationDeduction, 0) as dormViolationDeduction,
+            COALESCE(teaching_violation_stats.teachingViolationCount, 0) as teachingViolationCount,
+            COALESCE(teaching_violation_stats.teachingViolationDeduction, 0) as teachingViolationDeduction
+        FROM sys_dept d
+        LEFT JOIN (
+            -- 宿舍卫生检查扣分统计
+            SELECT
+                c.college_id,
+                COUNT(h.id) as dormDeductionCount
+            FROM xg_dorm_hygiene_check h
+            INNER JOIN xg_class_msg c ON h.room_id IN (
+                SELECT DISTINCT bs.room_id
+                FROM xg_dorm_bed_student bs
+                INNER JOIN xg_student_msg s ON bs.student_id = s.id
+                WHERE s.class_id = c.id AND s.deleted = 0
+            )
+            WHERE h.deleted = 0
+              AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+              AND h.total_score > 0
+            GROUP BY c.college_id
+        ) dorm_stats ON d.dept_id = dorm_stats.college_id
+        LEFT JOIN (
+            -- 宿舍区违纪记录统计
+            SELECT
+                dr.college_id,
+                COUNT(dr.id) as dormViolationCount,
+                COALESCE(SUM(dr.score_deduction), 0) as dormViolationDeduction
+            FROM xskh_discipline_record dr
+            WHERE dr.deleted = 0
+              AND dr.violation_source = '1'
+              AND dr.status = '1'
+              AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+            GROUP BY dr.college_id
+        ) dorm_violation_stats ON d.dept_id = dorm_violation_stats.college_id
+        LEFT JOIN (
+            -- 教学区违纪记录统计
+            SELECT
+                dr.college_id,
+                COUNT(dr.id) as teachingViolationCount,
+                COALESCE(SUM(dr.score_deduction), 0) as teachingViolationDeduction
+            FROM xskh_discipline_record dr
+            WHERE dr.deleted = 0
+              AND dr.violation_source = '2'
+              AND dr.status = '1'
+              AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+            GROUP BY dr.college_id
+        ) teaching_violation_stats ON d.dept_id = teaching_violation_stats.college_id
+        WHERE d.parent_id = 101 AND d.del_flag = '0'
+        ORDER BY d.order_num ASC, d.dept_name ASC
+    </select>
+
+    <!-- 按学院维度统计明细数据 -->
+    <select id="getCollegeDetailStatistics" resultType="org.dromara.xg.domain.vo.XgStudentStatisticsVo$StatisticsDetailData">
+        SELECT
+            d.dept_id as id,
+            d.dept_name as name,
+            '' as parentName,
+            COALESCE(dorm_stats.dormDeductionCount, 0) as dormDeductionCount,
+            COALESCE(dorm_violation_stats.dormViolationCount, 0) as dormViolationCount,
+            COALESCE(dorm_violation_stats.dormViolationDeduction, 0) as dormViolationDeduction,
+            COALESCE(teaching_violation_stats.teachingViolationCount, 0) as teachingViolationCount,
+            COALESCE(teaching_violation_stats.teachingViolationDeduction, 0) as teachingViolationDeduction,
+            COALESCE(dorm_violation_stats.dormViolationDeduction, 0) + COALESCE(teaching_violation_stats.teachingViolationDeduction, 0) as totalDeduction
+        FROM sys_dept d
+        LEFT JOIN (
+            -- 宿舍卫生检查扣分统计
+            SELECT
+                c.college_id,
+                COUNT(h.id) as dormDeductionCount
+            FROM xg_dorm_hygiene_check h
+            INNER JOIN xg_class_msg c ON h.room_id IN (
+                SELECT DISTINCT bs.room_id
+                FROM xg_dorm_bed_student bs
+                INNER JOIN xg_student_msg s ON bs.student_id = s.id
+                WHERE s.class_id = c.id AND s.deleted = 0
+            )
+            WHERE h.deleted = 0
+              AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+              AND h.total_score > 0
+            GROUP BY c.college_id
+        ) dorm_stats ON d.dept_id = dorm_stats.college_id
+        LEFT JOIN (
+            -- 宿舍区违纪记录统计
+            SELECT
+                dr.college_id,
+                COUNT(dr.id) as dormViolationCount,
+                COALESCE(SUM(dr.score_deduction), 0) as dormViolationDeduction
+            FROM xskh_discipline_record dr
+            WHERE dr.deleted = 0
+              AND dr.violation_source = '1'
+              AND dr.status = '1'
+              AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+            GROUP BY dr.college_id
+        ) dorm_violation_stats ON d.dept_id = dorm_violation_stats.college_id
+        LEFT JOIN (
+            -- 教学区违纪记录统计
+            SELECT
+                dr.college_id,
+                COUNT(dr.id) as teachingViolationCount,
+                COALESCE(SUM(dr.score_deduction), 0) as teachingViolationDeduction
+            FROM xskh_discipline_record dr
+            WHERE dr.deleted = 0
+              AND dr.violation_source = '2'
+              AND dr.status = '1'
+              AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+            GROUP BY dr.college_id
+        ) teaching_violation_stats ON d.dept_id = teaching_violation_stats.college_id
+        WHERE d.parent_id = 101 AND d.del_flag = '0'
+        ORDER BY d.order_num ASC, d.dept_name ASC
+    </select>
+
+    <!-- 按专业维度统计图表数据 -->
+    <select id="getMajorStatistics" resultType="org.dromara.xg.domain.vo.XgStudentStatisticsVo$StatisticsChartData">
+        SELECT
+            d.dept_name as name,
+            COALESCE(dorm_stats.dormDeductionCount, 0) as dormDeductionCount,
+            COALESCE(dorm_violation_stats.dormViolationCount, 0) as dormViolationCount,
+            COALESCE(dorm_violation_stats.dormViolationDeduction, 0) as dormViolationDeduction,
+            COALESCE(teaching_violation_stats.teachingViolationCount, 0) as teachingViolationCount,
+            COALESCE(teaching_violation_stats.teachingViolationDeduction, 0) as teachingViolationDeduction
+        FROM sys_dept d
+        LEFT JOIN (
+            -- 宿舍卫生检查扣分统计
+            SELECT
+                c.major_id,
+                COUNT(h.id) as dormDeductionCount
+            FROM xg_dorm_hygiene_check h
+            INNER JOIN xg_class_msg c ON h.room_id IN (
+                SELECT DISTINCT bs.room_id
+                FROM xg_dorm_bed_student bs
+                INNER JOIN xg_student_msg s ON bs.student_id = s.id
+                WHERE s.class_id = c.id AND s.deleted = 0
+            )
+            WHERE h.deleted = 0
+              AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+              AND h.total_score > 0
+              AND c.college_id = #{bo.collegeId}
+            GROUP BY c.major_id
+        ) dorm_stats ON d.dept_id = dorm_stats.major_id
+        LEFT JOIN (
+            -- 宿舍区违纪记录统计
+            SELECT
+                dr.major_id,
+                COUNT(dr.id) as dormViolationCount,
+                COALESCE(SUM(dr.score_deduction), 0) as dormViolationDeduction
+            FROM xskh_discipline_record dr
+            WHERE dr.deleted = 0
+              AND dr.violation_source = '1'
+              AND dr.status = '1'
+              AND dr.college_id = #{bo.collegeId}
+              AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+            GROUP BY dr.major_id
+        ) dorm_violation_stats ON d.dept_id = dorm_violation_stats.major_id
+        LEFT JOIN (
+            -- 教学区违纪记录统计
+            SELECT
+                dr.major_id,
+                COUNT(dr.id) as teachingViolationCount,
+                COALESCE(SUM(dr.score_deduction), 0) as teachingViolationDeduction
+            FROM xskh_discipline_record dr
+            WHERE dr.deleted = 0
+              AND dr.violation_source = '2'
+              AND dr.status = '1'
+              AND dr.college_id = #{bo.collegeId}
+              AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+            GROUP BY dr.major_id
+        ) teaching_violation_stats ON d.dept_id = teaching_violation_stats.major_id
+        WHERE d.parent_id = #{bo.collegeId} AND d.dept_category LIKE 'zy-%' AND d.del_flag = '0'
+        ORDER BY d.order_num ASC, d.dept_name ASC
+    </select>
+
+    <!-- 按专业维度统计明细数据 -->
+    <select id="getMajorDetailStatistics" resultType="org.dromara.xg.domain.vo.XgStudentStatisticsVo$StatisticsDetailData">
+        SELECT
+            d.dept_id as id,
+            d.dept_name as name,
+            p.dept_name as parentName,
+            COALESCE(dorm_stats.dormDeductionCount, 0) as dormDeductionCount,
+            COALESCE(dorm_violation_stats.dormViolationCount, 0) as dormViolationCount,
+            COALESCE(dorm_violation_stats.dormViolationDeduction, 0) as dormViolationDeduction,
+            COALESCE(teaching_violation_stats.teachingViolationCount, 0) as teachingViolationCount,
+            COALESCE(teaching_violation_stats.teachingViolationDeduction, 0) as teachingViolationDeduction,
+            COALESCE(dorm_violation_stats.dormViolationDeduction, 0) + COALESCE(teaching_violation_stats.teachingViolationDeduction, 0) as totalDeduction
+        FROM sys_dept d
+        INNER JOIN sys_dept p ON d.parent_id = p.dept_id
+        LEFT JOIN (
+            -- 宿舍卫生检查扣分统计
+            SELECT
+                c.major_id,
+                COUNT(h.id) as dormDeductionCount
+            FROM xg_dorm_hygiene_check h
+            INNER JOIN xg_class_msg c ON h.room_id IN (
+                SELECT DISTINCT bs.room_id
+                FROM xg_dorm_bed_student bs
+                INNER JOIN xg_student_msg s ON bs.student_id = s.id
+                WHERE s.class_id = c.id AND s.deleted = 0
+            )
+            WHERE h.deleted = 0
+              AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+              AND h.total_score > 0
+              AND c.college_id = #{bo.collegeId}
+            GROUP BY c.major_id
+        ) dorm_stats ON d.dept_id = dorm_stats.major_id
+        LEFT JOIN (
+            -- 宿舍区违纪记录统计
+            SELECT
+                dr.major_id,
+                COUNT(dr.id) as dormViolationCount,
+                COALESCE(SUM(dr.score_deduction), 0) as dormViolationDeduction
+            FROM xskh_discipline_record dr
+            WHERE dr.deleted = 0
+              AND dr.violation_source = '1'
+              AND dr.status = '1'
+              AND dr.college_id = #{bo.collegeId}
+              AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+            GROUP BY dr.major_id
+        ) dorm_violation_stats ON d.dept_id = dorm_violation_stats.major_id
+        LEFT JOIN (
+            -- 教学区违纪记录统计
+            SELECT
+                dr.major_id,
+                COUNT(dr.id) as teachingViolationCount,
+                COALESCE(SUM(dr.score_deduction), 0) as teachingViolationDeduction
+            FROM xskh_discipline_record dr
+            WHERE dr.deleted = 0
+              AND dr.violation_source = '2'
+              AND dr.status = '1'
+              AND dr.college_id = #{bo.collegeId}
+              AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+            GROUP BY dr.major_id
+        ) teaching_violation_stats ON d.dept_id = teaching_violation_stats.major_id
+        WHERE d.parent_id = #{bo.collegeId} AND d.dept_category LIKE 'zy-%' AND d.del_flag = '0'
+        ORDER BY d.order_num ASC, d.dept_name ASC
+    </select>
+
+    <!-- 按班级维度统计图表数据 -->
+    <select id="getClassStatistics" resultType="org.dromara.xg.domain.vo.XgStudentStatisticsVo$StatisticsChartData">
+        SELECT
+            c.class_name as name,
+            (SELECT COUNT(h.id)
+             FROM xg_dorm_hygiene_check h
+             WHERE h.deleted = 0
+               AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+               AND h.total_score > 0
+               AND h.room_id IN (
+                   SELECT DISTINCT bs.room_id
+                   FROM xg_dorm_bed_student bs
+                   INNER JOIN xg_student_msg s ON bs.student_id = s.id
+                   WHERE s.class_id = c.id AND s.deleted = 0
+               )
+            ) as dormDeductionCount,
+            (SELECT COUNT(dr.id)
+             FROM xskh_discipline_record dr
+             WHERE dr.deleted = 0
+               AND dr.violation_source = '1'
+               AND dr.status = '1'
+               AND dr.class_id = c.id
+               AND dr.college_id = #{bo.collegeId}
+               AND dr.major_id = #{bo.majorId}
+               AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+            ) as dormViolationCount,
+            (SELECT COALESCE(SUM(dr.score_deduction), 0)
+             FROM xskh_discipline_record dr
+             WHERE dr.deleted = 0
+               AND dr.violation_source = '1'
+               AND dr.status = '1'
+               AND dr.class_id = c.id
+               AND dr.college_id = #{bo.collegeId}
+               AND dr.major_id = #{bo.majorId}
+               AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+            ) as dormViolationDeduction,
+            (SELECT COUNT(dr.id)
+             FROM xskh_discipline_record dr
+             WHERE dr.deleted = 0
+               AND dr.violation_source = '2'
+               AND dr.status = '1'
+               AND dr.class_id = c.id
+               AND dr.college_id = #{bo.collegeId}
+               AND dr.major_id = #{bo.majorId}
+               AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+            ) as teachingViolationCount,
+            (SELECT COALESCE(SUM(dr.score_deduction), 0)
+             FROM xskh_discipline_record dr
+             WHERE dr.deleted = 0
+               AND dr.violation_source = '2'
+               AND dr.status = '1'
+               AND dr.class_id = c.id
+               AND dr.college_id = #{bo.collegeId}
+               AND dr.major_id = #{bo.majorId}
+               AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+            ) as teachingViolationDeduction
+        FROM xg_class_msg c
+        WHERE c.college_id = #{bo.collegeId} AND c.major_id = #{bo.majorId} AND c.deleted = 0
+        ORDER BY c.class_name ASC
+    </select>
+
+    <!-- 按班级维度统计明细数据 -->
+    <select id="getClassDetailStatistics" resultType="org.dromara.xg.domain.vo.XgStudentStatisticsVo$StatisticsDetailData">
+        <choose>
+            <when test="bo.classId != null">
+                <!-- 班级详情统计 -->
+                SELECT
+                    c.id as id,
+                    c.class_name as name,
+                    CONCAT(c.college_name, '-', c.major_name) as parentName,
+                    (SELECT COUNT(h.id)
+                     FROM xg_dorm_hygiene_check h
+                     WHERE h.deleted = 0
+                       AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                       AND h.total_score > 0
+                       AND h.room_id IN (
+                           SELECT DISTINCT bs.room_id
+                           FROM xg_dorm_bed_student bs
+                           INNER JOIN xg_student_msg s ON bs.student_id = s.id
+                           WHERE s.class_id = c.id AND s.deleted = 0
+                       )
+                    ) as dormDeductionCount,
+                    (SELECT COUNT(dr.id)
+                     FROM xskh_discipline_record dr
+                     WHERE dr.deleted = 0
+                       AND dr.violation_source = '1'
+                       AND dr.status = '1'
+                       AND dr.class_id = #{bo.classId}
+                       AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                    ) as dormViolationCount,
+                    (SELECT COALESCE(SUM(dr.score_deduction), 0)
+                     FROM xskh_discipline_record dr
+                     WHERE dr.deleted = 0
+                       AND dr.violation_source = '1'
+                       AND dr.status = '1'
+                       AND dr.class_id = #{bo.classId}
+                       AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                    ) as dormViolationDeduction,
+                    (SELECT COUNT(dr.id)
+                     FROM xskh_discipline_record dr
+                     WHERE dr.deleted = 0
+                       AND dr.violation_source = '2'
+                       AND dr.status = '1'
+                       AND dr.class_id = #{bo.classId}
+                       AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                    ) as teachingViolationCount,
+                    (SELECT COALESCE(SUM(dr.score_deduction), 0)
+                     FROM xskh_discipline_record dr
+                     WHERE dr.deleted = 0
+                       AND dr.violation_source = '2'
+                       AND dr.status = '1'
+                       AND dr.class_id = #{bo.classId}
+                       AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                    ) as teachingViolationDeduction,
+                    ((SELECT COALESCE(SUM(dr.score_deduction), 0)
+                      FROM xskh_discipline_record dr
+                      WHERE dr.deleted = 0
+                        AND dr.violation_source = '1'
+                        AND dr.status = '1'
+                        AND dr.class_id = #{bo.classId}
+                        AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                     ) +
+                     (SELECT COALESCE(SUM(dr.score_deduction), 0)
+                      FROM xskh_discipline_record dr
+                      WHERE dr.deleted = 0
+                        AND dr.violation_source = '2'
+                        AND dr.status = '1'
+                        AND dr.class_id = #{bo.classId}
+                        AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                     )) as totalDeduction
+                FROM xg_class_msg c
+                WHERE c.id = #{bo.classId} AND c.deleted = 0
+            </when>
+            <otherwise>
+                <!-- 班级列表统计 -->
+                SELECT
+                    c.id as id,
+                    c.class_name as name,
+                    c.major_name as parentName,
+                    (SELECT COUNT(h.id)
+                     FROM xg_dorm_hygiene_check h
+                     WHERE h.deleted = 0
+                       AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                       AND h.total_score > 0
+                       AND h.room_id IN (
+                           SELECT DISTINCT bs.room_id
+                           FROM xg_dorm_bed_student bs
+                           INNER JOIN xg_student_msg s ON bs.student_id = s.id
+                           WHERE s.class_id = c.id AND s.deleted = 0
+                       )
+                    ) as dormDeductionCount,
+                    (SELECT COUNT(dr.id)
+                     FROM xskh_discipline_record dr
+                     WHERE dr.deleted = 0
+                       AND dr.violation_source = '1'
+                       AND dr.status = '1'
+                       AND dr.class_id = c.id
+                       AND dr.college_id = #{bo.collegeId}
+                       AND dr.major_id = #{bo.majorId}
+                       AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                    ) as dormViolationCount,
+                    (SELECT COALESCE(SUM(dr.score_deduction), 0)
+                     FROM xskh_discipline_record dr
+                     WHERE dr.deleted = 0
+                       AND dr.violation_source = '1'
+                       AND dr.status = '1'
+                       AND dr.class_id = c.id
+                       AND dr.college_id = #{bo.collegeId}
+                       AND dr.major_id = #{bo.majorId}
+                       AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                    ) as dormViolationDeduction,
+                    (SELECT COUNT(dr.id)
+                     FROM xskh_discipline_record dr
+                     WHERE dr.deleted = 0
+                       AND dr.violation_source = '2'
+                       AND dr.status = '1'
+                       AND dr.class_id = c.id
+                       AND dr.college_id = #{bo.collegeId}
+                       AND dr.major_id = #{bo.majorId}
+                       AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                    ) as teachingViolationCount,
+                    (SELECT COALESCE(SUM(dr.score_deduction), 0)
+                     FROM xskh_discipline_record dr
+                     WHERE dr.deleted = 0
+                       AND dr.violation_source = '2'
+                       AND dr.status = '1'
+                       AND dr.class_id = c.id
+                       AND dr.college_id = #{bo.collegeId}
+                       AND dr.major_id = #{bo.majorId}
+                       AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                    ) as teachingViolationDeduction,
+                    ((SELECT COALESCE(SUM(dr.score_deduction), 0)
+                      FROM xskh_discipline_record dr
+                      WHERE dr.deleted = 0
+                        AND dr.violation_source = '1'
+                        AND dr.status = '1'
+                        AND dr.class_id = c.id
+                        AND dr.college_id = #{bo.collegeId}
+                        AND dr.major_id = #{bo.majorId}
+                        AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                     ) +
+                     (SELECT COALESCE(SUM(dr.score_deduction), 0)
+                      FROM xskh_discipline_record dr
+                      WHERE dr.deleted = 0
+                        AND dr.violation_source = '2'
+                        AND dr.status = '1'
+                        AND dr.class_id = c.id
+                        AND dr.college_id = #{bo.collegeId}
+                        AND dr.major_id = #{bo.majorId}
+                        AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
+                     )) as totalDeduction
+                FROM xg_class_msg c
+                WHERE c.college_id = #{bo.collegeId} AND c.major_id = #{bo.majorId} AND c.deleted = 0
+                ORDER BY c.class_name ASC
+            </otherwise>
+        </choose>
+    </select>
+
+    <!-- 获取二级学院列表 -->
+    <select id="getColleges" resultType="org.dromara.xg.domain.vo.XgCascadeOptionVo">
+        SELECT
+            dept_id as id,
+            dept_name as label,
+            CAST(dept_id AS CHAR) as value,
+            order_num as sort
+        FROM sys_dept
+        WHERE parent_id = 101 AND del_flag = '0'
+        ORDER BY order_num ASC, dept_name ASC
+    </select>
+
+    <!-- 根据学院ID获取专业列表 -->
+    <select id="getMajorsByCollegeId" resultType="org.dromara.xg.domain.vo.XgCascadeOptionVo">
+        SELECT
+            dept_id as id,
+            dept_name as label,
+            CAST(dept_id AS CHAR) as value,
+            order_num as sort
+        FROM sys_dept
+        WHERE parent_id = #{collegeId}
+          AND dept_category LIKE 'zy-%'
+          AND del_flag = '0'
+        ORDER BY order_num ASC, dept_name ASC
+    </select>
+
+    <!-- 根据专业ID获取班级列表 -->
+    <select id="getClassesByMajorId" resultType="org.dromara.xg.domain.vo.XgCascadeOptionVo">
+        SELECT
+            id,
+            class_name as label,
+            CAST(id AS CHAR) as value,
+            0 as sort
+        FROM xg_class_msg
+        WHERE major_id = #{majorId} AND deleted = 0
+        ORDER BY class_name ASC
+    </select>
+
+</mapper>

+ 1 - 1
web/package.json

@@ -34,7 +34,7 @@
     "await-to-js": "3.0.0",
     "axios": "1.8.4",
     "crypto-js": "4.2.0",
-    "echarts": "5.6.0",
+    "echarts": "^5.6.0",
     "element-plus": "2.9.8",
     "file-saver": "2.0.5",
     "highlight.js": "11.9.0",

+ 86 - 0
web/src/api/xg/studentStatistics.ts

@@ -0,0 +1,86 @@
+import request from '@/utils/request'
+
+export interface StudentStatisticsBo {
+  startTime: string
+  endTime: string
+  collegeId?: number
+  majorId?: number
+  classId?: number
+}
+
+export interface StatisticsChartData {
+  name: string
+  dormDeductionCount: number
+  dormViolationCount: number
+  dormViolationDeduction: number
+  teachingViolationCount: number
+  teachingViolationDeduction: number
+}
+
+export interface StatisticsDetailData {
+  id: number
+  name: string
+  parentName: string
+  dormDeductionCount: number
+  dormViolationCount: number
+  dormViolationDeduction: number
+  teachingViolationCount: number
+  teachingViolationDeduction: number
+  totalDeduction: number
+}
+
+export interface StudentStatisticsVo {
+  dimension: string
+  chartData: StatisticsChartData[]
+  detailData: StatisticsDetailData[]
+}
+
+export interface CascadeOptionVo {
+  id: number
+  label: string
+  value: string
+  sort: number
+}
+
+// 获取统计数据
+export function getStatistics(data: StudentStatisticsBo) {
+  return request({
+    url: '/xg/studentStatistics/statistics',
+    method: 'post',
+    data: data
+  })
+}
+
+// 导出统计数据
+export function exportStatistics(data: StudentStatisticsBo) {
+  return request({
+    url: '/xg/studentStatistics/export',
+    method: 'post',
+    data: data,
+    responseType: 'blob'
+  })
+}
+
+// 获取二级学院列表
+export function getColleges() {
+  return request({
+    url: '/xg/studentStatistics/colleges',
+    method: 'get'
+  })
+}
+
+// 根据学院ID获取专业列表
+export function getMajorsByCollegeId(collegeId: number) {
+  return request({
+    url: `/xg/studentStatistics/majors/${collegeId}`,
+    method: 'get'
+  })
+}
+
+// 根据专业ID获取班级列表
+export function getClassesByMajorId(majorId: number) {
+  return request({
+    url: `/xg/studentStatistics/classes/${majorId}`,
+    method: 'get'
+  })
+}

+ 426 - 0
web/src/views/xg/studentStatistics/index.vue

@@ -0,0 +1,426 @@
+<template>
+  <div class="p-2 page-container">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" label-width="120px">
+            <el-row :gutter="20">
+              <el-col :span="6">
+                <el-form-item label="统计开始时间" prop="startTime" required>
+                  <el-date-picker
+                    v-model="queryParams.startTime"
+                    type="datetime"
+                    placeholder="选择开始时间"
+                    value-format="YYYY-MM-DD HH:mm:ss"
+                    style="width: 100%"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="统计结束时间" prop="endTime" required>
+                  <el-date-picker
+                    v-model="queryParams.endTime"
+                    type="datetime"
+                    placeholder="选择结束时间"
+                    value-format="YYYY-MM-DD HH:mm:ss"
+                    style="width: 100%"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="二级学院" prop="collegeId">
+                  <el-select v-model="queryParams.collegeId" placeholder="请选择学院" clearable style="width: 100%" @change="onCollegeChange">
+                    <el-option v-for="item in collegeOptions" :key="item.id" :label="item.label" :value="item.id" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="专业" prop="majorId">
+                  <el-select
+                    v-model="queryParams.majorId"
+                    placeholder="请选择专业"
+                    clearable
+                    style="width: 100%"
+                    :disabled="!queryParams.collegeId"
+                    @change="onMajorChange"
+                  >
+                    <el-option v-for="item in majorOptions" :key="item.id" :label="item.label" :value="item.id" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="6">
+                <el-form-item label="班级" prop="classId">
+                  <el-select v-model="queryParams.classId" placeholder="请选择班级" clearable style="width: 100%" :disabled="!queryParams.majorId">
+                    <el-option v-for="item in classOptions" :key="item.id" :label="item.label" :value="item.id" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="18">
+                <el-form-item class="search-btn-container">
+                  <el-button type="primary" icon="Search" @click="handleQuery" :loading="loading">搜索</el-button>
+                  <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+                  <!-- <el-button type="success" icon="Download" @click="handleExport" :loading="exportLoading" v-if="statisticsData">导出</el-button> -->
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <!-- 无数据提示 -->
+    <el-card shadow="never" v-if="!statisticsData && !loading">
+      <el-empty description="暂无数据" style="padding: 60px 0">
+        <template #description>
+          <p style="color: #909399; font-size: 14px">请设置统计时间范围和筛选条件,然后点击【搜索】按钮查看统计数据</p>
+        </template>
+      </el-empty>
+    </el-card>
+
+    <!-- 统计图表 -->
+    <el-card shadow="never" v-if="statisticsData">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <div class="card-header">
+              <span>{{ getChartTitle() }}</span>
+            </div>
+          </el-col>
+          <right-toolbar :showSearch="showSearch" @update:showSearch="showSearch = $event" @queryTable="handleQuery"></right-toolbar>
+        </el-row>
+      </template>
+      <div ref="chartRef" style="height: 400px"></div>
+    </el-card>
+
+    <!-- 统计明细 -->
+    <el-card shadow="never" v-if="statisticsData">
+      <template #header>
+        <div class="card-header">
+          <span>明细数据</span>
+        </div>
+      </template>
+      <el-table :data="statisticsData.detailData" style="width: 100%" border>
+        <el-table-column prop="name" label="名称" width="200" />
+        <el-table-column prop="parentName" label="上级" width="150" v-if="statisticsData.dimension !== 'college'" />
+        <el-table-column prop="dormDeductionCount" label="宿舍扣分总数" min-width="120" align="center">
+          <template #default="scope">
+            {{ scope.row.dormDeductionCount && scope.row.dormDeductionCount !== 0 ? `-${scope.row.dormDeductionCount}` : '0' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="dormViolationCount" label="宿舍违纪记录总数" min-width="140" align="center" />
+        <el-table-column prop="dormViolationDeduction" label="宿舍违纪记录扣分总数" min-width="160" align="center">
+          <template #default="scope">
+            {{
+              scope.row.dormViolationDeduction && scope.row.dormViolationDeduction !== 0 ? `-${scope.row.dormViolationDeduction.toFixed(2)}` : '0.00'
+            }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="teachingViolationCount" label="教学区违纪记录总数" min-width="160" align="center" />
+        <el-table-column prop="teachingViolationDeduction" label="教学区违纪记录扣分总数" min-width="180" align="center">
+          <template #default="scope">
+            {{
+              scope.row.teachingViolationDeduction && scope.row.teachingViolationDeduction !== 0
+                ? `-${scope.row.teachingViolationDeduction.toFixed(2)}`
+                : '0.00'
+            }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="totalDeduction" label="违纪合计扣分" min-width="140" align="center">
+          <template #default="scope">
+            {{ scope.row.totalDeduction && scope.row.totalDeduction !== 0 ? `-${scope.row.totalDeduction.toFixed(2)}` : '0.00' }}
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, nextTick, getCurrentInstance, ComponentInternalInstance } from 'vue';
+import { ElMessage } from 'element-plus';
+import * as echarts from 'echarts';
+import {
+  getStatistics,
+  exportStatistics,
+  getColleges,
+  getMajorsByCollegeId,
+  getClassesByMajorId,
+  type StudentStatisticsBo,
+  type StudentStatisticsVo,
+  type CascadeOptionVo
+} from '@/api/xg/studentStatistics';
+
+defineOptions({
+  name: 'StudentStatistics'
+});
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const queryFormRef = ref();
+const chartRef = ref<HTMLDivElement>();
+const loading = ref(false);
+const exportLoading = ref(false);
+const showSearch = ref(true);
+
+// 查询参数
+const queryParams = reactive<StudentStatisticsBo>({
+  startTime: '',
+  endTime: '',
+  collegeId: undefined,
+  majorId: undefined,
+  classId: undefined
+});
+
+// 级联选择器选项
+const collegeOptions = ref<CascadeOptionVo[]>([]);
+const majorOptions = ref<CascadeOptionVo[]>([]);
+const classOptions = ref<CascadeOptionVo[]>([]);
+
+// 统计数据
+const statisticsData = ref<StudentStatisticsVo>();
+
+// 图表实例
+let chart: echarts.ECharts | null = null;
+
+/** 设置默认时间范围(当前月第一天到最后一天) */
+const setDefaultTimeRange = () => {
+  const now = new Date();
+  const year = now.getFullYear();
+  const month = now.getMonth();
+
+  // 当前月第一天 00:00:00
+  const firstDay = new Date(year, month, 1);
+  const startTime = `${firstDay.getFullYear()}-${String(firstDay.getMonth() + 1).padStart(2, '0')}-${String(firstDay.getDate()).padStart(2, '0')} 00:00:00`;
+
+  // 当前月最后一天 23:59:59
+  const lastDay = new Date(year, month + 1, 0);
+  const endTime = `${lastDay.getFullYear()}-${String(lastDay.getMonth() + 1).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')} 23:59:59`;
+
+  queryParams.startTime = startTime;
+  queryParams.endTime = endTime;
+};
+
+/** 查询学院列表 */
+const getCollegeList = async () => {
+  try {
+    const res = await getColleges();
+    collegeOptions.value = res.data || [];
+  } catch (error) {
+    console.error('获取学院列表失败:', error);
+  }
+};
+
+/** 学院变化事件 */
+const onCollegeChange = async (collegeId?: number) => {
+  queryParams.majorId = undefined;
+  queryParams.classId = undefined;
+  majorOptions.value = [];
+  classOptions.value = [];
+
+  if (collegeId) {
+    try {
+      const res = await getMajorsByCollegeId(collegeId);
+      majorOptions.value = res.data || [];
+    } catch (error) {
+      console.error('获取专业列表失败:', error);
+    }
+  }
+};
+
+/** 专业变化事件 */
+const onMajorChange = async (majorId?: number) => {
+  queryParams.classId = undefined;
+  classOptions.value = [];
+
+  if (majorId) {
+    try {
+      const res = await getClassesByMajorId(majorId);
+      classOptions.value = res.data || [];
+    } catch (error) {
+      console.error('获取班级列表失败:', error);
+    }
+  }
+};
+
+/** 搜索按钮操作 */
+const handleQuery = async () => {
+  if (!queryParams.startTime || !queryParams.endTime) {
+    ElMessage.warning('请填写统计开始时间和结束时间');
+    return;
+  }
+
+  loading.value = true;
+  try {
+    const res = await getStatistics(queryParams);
+    statisticsData.value = res.data;
+
+    // 渲染图表
+    await nextTick();
+    renderChart();
+  } catch (error) {
+    console.error('获取统计数据失败:', error);
+    ElMessage.error('获取统计数据失败');
+  } finally {
+    loading.value = false;
+  }
+};
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  // 重置为默认时间范围
+  setDefaultTimeRange();
+  queryParams.collegeId = undefined;
+  queryParams.majorId = undefined;
+  queryParams.classId = undefined;
+  majorOptions.value = [];
+  classOptions.value = [];
+  statisticsData.value = undefined;
+
+  if (chart) {
+    chart.clear();
+  }
+};
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  exportLoading.value = true;
+  try {
+    await exportStatistics(queryParams);
+    ElMessage.success('导出成功');
+  } catch (error) {
+    console.error('导出失败:', error);
+    ElMessage.error('导出失败');
+  } finally {
+    exportLoading.value = false;
+  }
+};
+
+/** 获取图表标题 */
+const getChartTitle = () => {
+  if (!statisticsData.value) return '';
+
+  const dimension = statisticsData.value.dimension;
+  switch (dimension) {
+    case 'college':
+      return '二级学院统计';
+    case 'major':
+      return '专业统计';
+    case 'class':
+      return '班级统计';
+    case 'classDetail':
+      return '班级详情统计';
+    default:
+      return '统计图表';
+  }
+};
+
+/** 渲染图表 */
+const renderChart = () => {
+  if (!chartRef.value || !statisticsData.value) return;
+
+  // 销毁现有图表
+  if (chart) {
+    chart.dispose();
+  }
+
+  // 创建新图表
+  chart = echarts.init(chartRef.value);
+
+  const chartData = statisticsData.value.chartData || [];
+  const names = chartData.map((item) => item.name);
+
+  const option = {
+    title: {
+      text: getChartTitle(),
+      left: 'center'
+    },
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    legend: {
+      data: ['宿舍扣分总数', '宿舍违纪记录总数', '宿舍违纪记录扣分总数', '教学区违纪记录总数', '教学区违纪记录扣分总数'],
+      top: 30
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      top: '15%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: names,
+      axisTick: {
+        alignWithLabel: true
+      }
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: [
+      {
+        name: '宿舍扣分总数',
+        type: 'bar',
+        data: chartData.map((item) => item.dormDeductionCount)
+      },
+      {
+        name: '宿舍违纪记录总数',
+        type: 'bar',
+        data: chartData.map((item) => item.dormViolationCount)
+      },
+      {
+        name: '宿舍违纪记录扣分总数',
+        type: 'bar',
+        data: chartData.map((item) => item.dormViolationDeduction)
+      },
+      {
+        name: '教学区违纪记录总数',
+        type: 'bar',
+        data: chartData.map((item) => item.teachingViolationCount)
+      },
+      {
+        name: '教学区违纪记录扣分总数',
+        type: 'bar',
+        data: chartData.map((item) => item.teachingViolationDeduction)
+      }
+    ]
+  };
+
+  chart.setOption(option);
+
+  // 监听窗口大小变化
+  window.addEventListener('resize', () => {
+    chart?.resize();
+  });
+};
+
+onMounted(() => {
+  // 设置默认时间范围
+  setDefaultTimeRange();
+  // 获取学院列表
+  getCollegeList();
+});
+</script>
+
+<style scoped>
+.page-container {
+  background-color: #f5f5f5;
+}
+
+.search-btn-container {
+  text-align: left;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

+ 38 - 7
web/src/views/xskh/disciplineRecord/DisciplineRecordForm.vue

@@ -51,6 +51,12 @@
               <el-option v-for="type in disciplineTypeOptions" :key="type.id" :label="type.typeName" :value="type.id" />
             </el-select>
           </el-form-item>
+          <!-- 违纪处分类型说明显示 -->
+          <el-form-item label="类型说明" v-if="selectedDisciplineType && selectedDisciplineType.remark">
+            <div class="discipline-type-remark">
+              <el-text type="info" size="small">{{ selectedDisciplineType.remark }}</el-text>
+            </div>
+          </el-form-item>
           <el-form-item label="违纪处分日期" prop="disciplineDate">
             <el-date-picker
               v-model="form.disciplineDate"
@@ -253,7 +259,7 @@
 <script setup name="DisciplineRecordForm" lang="ts">
 import { ref, reactive, computed, toRefs, getCurrentInstance } from 'vue';
 import { UploadFilled, Document, Loading, Download, Delete } from '@element-plus/icons-vue';
-import { ElMessage, ElMessageBox, ElLoading, FormInstance } from 'element-plus';
+import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
 import { getToken } from '@/utils/auth';
 import { globalHeaders } from '@/utils/request';
 import ImagePreview from '@/components/ImagePreview/index.vue';
@@ -271,7 +277,7 @@ const emit = defineEmits(['success']);
 const { proxy } = getCurrentInstance() as any;
 const { student_discipline_status } = toRefs<any>(proxy?.useDict('student_discipline_status'));
 
-const disciplineRecordFormRef = ref<FormInstance>();
+const disciplineRecordFormRef = ref<any>();
 const buttonLoading = ref(false);
 
 // 学生选择器相关状态
@@ -281,6 +287,9 @@ const studentLoading = ref(false);
 // 违纪处分类型选项
 const disciplineTypeOptions = ref<DisciplineTypeVO[]>([]);
 
+// 选中的违纪处分类型信息
+const selectedDisciplineType = ref<DisciplineTypeVO | null>(null);
+
 // 附件上传相关状态
 const attachmentList = ref<any[]>([]);
 
@@ -307,16 +316,12 @@ const initFormData: DisciplineRecordFormType = {
   disciplineContent: undefined,
   scoreDeduction: undefined,
   status: '1', // 默认生效
-  approvalStatus: undefined,
-  approvalBy: undefined,
-  approvalTime: undefined,
-  approvalRemark: undefined,
   attachmentOssIds: undefined
 };
 
 const form = ref<DisciplineRecordFormType>({ ...initFormData });
 
-const rules = reactive({
+const rules = reactive<any>({
   studentId: [{ required: true, message: '学生不能为空', trigger: 'change' }],
   disciplineTypeId: [{ required: true, message: '违纪处分类型不能为空', trigger: 'change' }],
   disciplineDate: [{ required: true, message: '违纪处分日期不能为空', trigger: 'blur' }],
@@ -383,6 +388,14 @@ const openDialog = async (id?: string | number, title?: string) => {
 
     // 处理附件数据
     await handleAttachmentData();
+
+    // 设置选中的违纪处分类型(用于显示类型说明)
+    if (form.value.disciplineTypeId) {
+      const selectedType = disciplineTypeOptions.value.find((type) => type.id === form.value.disciplineTypeId);
+      if (selectedType) {
+        selectedDisciplineType.value = selectedType;
+      }
+    }
   }
 };
 
@@ -390,6 +403,7 @@ const openDialog = async (id?: string | number, title?: string) => {
 const resetForm = () => {
   form.value = { ...initFormData };
   attachmentList.value = [];
+  selectedDisciplineType.value = null;
   disciplineRecordFormRef.value?.resetFields();
 };
 
@@ -478,12 +492,18 @@ const loadDisciplineTypes = async () => {
 const handleDisciplineTypeChange = (typeId: string | number) => {
   if (!typeId) {
     form.value.disciplineTypeName = undefined;
+    selectedDisciplineType.value = null;
     return;
   }
 
   const selectedType = disciplineTypeOptions.value.find((type) => type.id === typeId);
   if (selectedType) {
     form.value.disciplineTypeName = selectedType.typeName;
+    selectedDisciplineType.value = selectedType;
+    // 自动填充最低扣分(如果有的话)
+    if (selectedType.minScore !== undefined && selectedType.minScore !== null) {
+      form.value.scoreDeduction = Number(selectedType.minScore);
+    }
   }
 };
 
@@ -972,6 +992,17 @@ defineExpose({
   box-sizing: border-box;
 }
 
+/* 违纪处分类型说明显示 */
+.discipline-type-remark {
+  padding: 8px 12px;
+  background: #f0f9ff;
+  border: 1px solid #bfdbfe;
+  border-radius: 4px;
+  font-size: 13px;
+  line-height: 1.5;
+  color: #1e40af;
+}
+
 .student-name-text {
   font-weight: 500;
   color: #303133;