jinxia.mo 1 месяц назад
Родитель
Сommit
cababa1f48

+ 9 - 0
back-ui/src/api/learn/student.js

@@ -42,3 +42,12 @@ export function delStudent(studentId) {
     method: 'delete'
   })
 }
+
+// 统计学习记录
+export function statisticStudyRecord(query) {
+  return request({
+    url: '/learn/student/statisticStudyRecord',
+    method: 'get',
+    params: query
+  })
+}

+ 22 - 5
back-ui/src/views/dz/agent/index.vue

@@ -16,6 +16,15 @@
               <pane size="100">
                   <el-col>
                       <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+                          <el-form-item label="平台机构" prop="deptId">
+                              <IeInstitutionSelect 
+                                  v-model="queryParams.deptId" 
+                                  placeholder="请选择平台机构" 
+                                  clearable 
+                                  style="width: 240px"
+                                  @change="handleDeptChange"
+                              />
+                          </el-form-item>
                           <el-form-item label="代理商" prop="agentId">
                               <el-select
                                       v-model="queryParams.agentId"
@@ -99,7 +108,7 @@
                           <!--      <el-table-column label="上级代理商ID" align="center" prop="parentId" />-->
 <!--                          <el-table-column label="学校/校区" align="center" prop="schools" />-->
 <!--                          <el-table-column label="学校/校区" align="center" prop="schoolName" />-->
-                          <el-table-column label="备注" align="center" prop="remark" />
+                          <!-- <el-table-column label="备注" align="center" prop="remark" /> -->
                           <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
                               <template #default="scope">
                                   <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['dz:agent:edit']">修改</el-button>
@@ -126,9 +135,9 @@
               <el-tree-select v-model="form.deptId" :data="enabledDeptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择归属机构" clearable check-strictly />
           </el-form-item>
 
-        <el-form-item label="联系电话" prop="phonenumber">
+        <!-- <el-form-item label="联系电话" prop="phonenumber">
           <el-input v-model="form.phonenumber" placeholder="请输入联系电话" />
-        </el-form-item>
+        </el-form-item> -->
         <el-form-item label="上级代理" prop="parentId">
           <el-tree-select
             v-model="form.parentId"
@@ -153,9 +162,9 @@
 <!--                  />-->
 <!--              </el-select>-->
 <!--          </el-form-item>-->
-        <el-form-item label="备注" prop="remark">
+        <!-- <el-form-item label="备注" prop="remark">
           <el-input v-model="form.remark" placeholder="请输入备注" />
-        </el-form-item>
+        </el-form-item> -->
       </el-form>
       <template #footer>
         <div class="dialog-footer">
@@ -174,6 +183,7 @@ import useAppStore from '@/store/modules/app'
 import { Splitpanes, Pane } from "splitpanes"
 import "splitpanes/dist/splitpanes.css"
 import { listAllSchool } from "@/api/dz/school"
+import IeInstitutionSelect from '@/components/IeInstitutionSelect/index.vue'
 
 const { proxy } = getCurrentInstance()
 const appStore = useAppStore()
@@ -297,6 +307,13 @@ function reset() {
   proxy.resetForm("agentRef")
 }
 
+/** 平台机构变化处理 */
+function handleDeptChange() {
+  // 清空代理商选择,因为平台机构变化后代理商列表会变化
+  queryParams.value.agentId = null
+  handleQuery()
+}
+
 /** 搜索按钮操作 */
 function handleQuery() {
   getList()

+ 373 - 0
back-ui/src/views/learn/student/statisticStudyRecord.vue

@@ -0,0 +1,373 @@
+<template>
+  <div class="app-container">
+    <el-row :gutter="20">
+      <splitpanes :horizontal="appStore.device === 'mobile'" class="default-theme">
+        <pane size="100">
+          <el-col>
+            <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="100px">
+              <el-form-item label="注册学校" prop="schoolId">
+                <IeSelect 
+                  v-model="selectedSchool" 
+                  :options="schoolList" 
+                  label-key="name" 
+                  value-key="id" 
+                  filterable
+                  clearable
+                  placeholder="请选择注册学校"
+                  style="width: 240px"
+                  @change="handleSchoolChange"
+                />
+              </el-form-item>
+              <el-form-item label="学校班级" prop="classId">
+                <IeSelect 
+                  v-model="selectedClass" 
+                  :options="classList" 
+                  label-key="name" 
+                  value-key="classId" 
+                  filterable
+                  clearable
+                  placeholder="请选择学校班级"
+                  style="width: 240px"
+                />
+              </el-form-item>
+              <el-form-item label="培训校区" prop="campusId">
+                <IeSelect 
+                  v-model="selectedCampus" 
+                  :options="campusList" 
+                  label-key="name" 
+                  value-key="id" 
+                  filterable
+                  clearable
+                  placeholder="请选择培训校区"
+                  style="width: 240px"
+                  @change="handleCampusChange"
+                />
+              </el-form-item>
+              <el-form-item label="校区班级" prop="campusClassId">
+                <IeSelect 
+                  v-model="selectedCampusClass" 
+                  :options="campusClassList" 
+                  label-key="name" 
+                  value-key="classId"
+                  filterable
+                  clearable
+                  placeholder="请选择校区班级"
+                  style="width: 240px"
+                />
+              </el-form-item>
+              <el-form-item label="代理商" prop="agentId">
+                <IeAgentSelect 
+                  v-model="queryParams.agentId" 
+                  placeholder="请选择代理商" 
+                  clearable 
+                  filterable 
+                  style="width: 240px" 
+                />
+              </el-form-item>
+              <el-form-item label="姓名" prop="nickName">
+                <el-input
+                  v-model="queryParams.nickName"
+                  placeholder="请输入姓名"
+                  clearable
+                  @keyup.enter="handleQuery"
+                  style="width: 240px"
+                />
+              </el-form-item>
+              <el-form-item label="卡号" prop="cardNo">
+                <el-input
+                  v-model="queryParams.cardNo"
+                  placeholder="请输入卡号"
+                  clearable
+                  @keyup.enter="handleQuery"
+                  style="width: 240px"
+                />
+              </el-form-item>
+              <el-form-item>
+                <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+              </el-form-item>
+            </el-form>
+
+            <el-row :gutter="10" class="mb8">
+              <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+            </el-row>
+
+            <el-table
+              v-loading="loading"
+              :data="statisticsList"
+            >
+              <el-table-column label="注册学校" align="center" prop="schoolName" min-width="120">
+                <template #default="scope">
+                  <span>{{ scope.row.schoolName || '-' }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="学校班级" align="center" prop="schoolClassName" min-width="120">
+                <template #default="scope">
+                  <span>{{ scope.row.schoolClassName || '-' }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="培训校区" align="center" prop="campusName" min-width="120">
+                <template #default="scope">
+                  <span>{{ scope.row.campusName || '-' }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="校区班级" align="center" prop="campusClassName" min-width="120">
+                <template #default="scope">
+                  <span>{{ scope.row.campusClassName || '-' }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="代理商" align="center" prop="agentId" min-width="120">
+                <template #default="scope">
+                  <span>{{ getAgentName(scope.row.agentId) }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="姓名" align="center" prop="nickName" min-width="100" />
+              <el-table-column label="卡号" align="center" prop="cardNo" min-width="120" />
+              <el-table-column label="刷题总数" align="center" prop="num" min-width="100" />
+              <el-table-column label="刷题正确率" align="center" prop="rate" min-width="120">
+                <template #default="scope">
+                  <span>{{ formatRate(scope.row.rate) }}%</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="视频总数" align="center" prop="videoTotalTitles" min-width="100" />
+              <el-table-column label="累计观看时长" align="center" prop="videoDurationValue" min-width="140">
+                <template #default="scope">
+                  <span>{{ formatDuration(scope.row.videoDurationValue) }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="试卷套数" align="center" prop="paperCount" min-width="100" />
+              <el-table-column label="平均正确率" align="center" prop="rate" min-width="120">
+                <template #default="scope">
+                  <span>{{ formatRate(scope.row.rate) }}%</span>
+                </template>
+              </el-table-column>
+            </el-table>
+
+            <pagination
+              v-show="total > 0"
+              :total="total"
+              v-model:page="queryParams.pageNum"
+              v-model:limit="queryParams.pageSize"
+              @pagination="getList"
+            />
+          </el-col>
+        </pane>
+      </splitpanes>
+    </el-row>
+  </div>
+</template>
+
+<script setup name="StudyRecordStatistics">
+import { statisticStudyRecord } from "@/api/learn/student"
+import IeSelect from '@/components/IeSelect/index.vue'
+import IeAgentSelect from '@/components/IeAgentSelect/index.vue'
+import useAppStore from '@/store/modules/app'
+import useSchool from '@/hooks/useSchool'
+import { Splitpanes, Pane } from "splitpanes"
+import "splitpanes/dist/splitpanes.css"
+import Pagination from '@/components/Pagination/index.vue'
+import { getAgentList } from '@/api/dz/cards'
+import { getCurrentInstance, onMounted, ref, reactive, nextTick } from 'vue'
+
+const { proxy } = getCurrentInstance()
+const appStore = useAppStore()
+
+const {
+  reset,
+  schoolList,
+  selectedSchool,
+  classList,
+  selectedClass,
+  campusList,
+  selectedCampus,
+  campusClassList,
+  selectedCampusClass,
+} = useSchool({ loadCampus: true, loadClass: true })
+
+const statisticsList = ref([])
+const loading = ref(false)
+const showSearch = ref(true)
+const total = ref(0)
+const agentList = ref([])
+
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  schoolId: null,
+  classId: null,
+  campusId: null,
+  campusClassId: null,
+  agentId: null,
+  nickName: null,
+  cardNo: null
+})
+
+/** 查询统计列表 */
+function getList() {
+  loading.value = true
+  const params = {
+    pageNum: queryParams.pageNum,
+    pageSize: queryParams.pageSize
+  }
+  
+  if (selectedSchool.value) {
+    params.schoolId = selectedSchool.value
+  }
+  if (selectedClass.value) {
+    params.classId = selectedClass.value
+  }
+  if (selectedCampus.value) {
+    params.campusId = selectedCampus.value
+  }
+  if (selectedCampusClass.value) {
+    params.campusClassId = selectedCampusClass.value
+  }
+  if (queryParams.agentId) {
+    params.agentId = queryParams.agentId
+  }
+  if (queryParams.nickName) {
+    params.nickName = queryParams.nickName
+  }
+  if (queryParams.cardNo) {
+    params.cardNo = queryParams.cardNo
+  }
+  
+  statisticStudyRecord(params).then(response => {
+    console.log('统计接口响应:', response)
+    statisticsList.value = response.rows || []
+    total.value = response.total || 0
+    loading.value = false
+  }).catch(error => {
+    console.error('获取统计数据失败:', error)
+    proxy.$modal.msgError('获取统计数据失败')
+    loading.value = false
+  })
+}
+
+/** 搜索按钮操作 */
+function handleQuery() {
+  queryParams.pageNum = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+function resetQuery() {
+  proxy.resetForm("queryRef")
+  reset()
+  queryParams.agentId = null
+  queryParams.nickName = null
+  queryParams.cardNo = null
+  handleQuery()
+}
+
+/** 学校变化处理 */
+function handleSchoolChange() {
+  selectedClass.value = null
+  getList()
+}
+
+/** 校区变化处理 */
+function handleCampusChange() {
+  selectedCampusClass.value = null
+  getList()
+}
+
+/** 获取学校名称 */
+function getSchoolName(schoolId) {
+  if (!schoolId || !schoolList.value || schoolList.value.length === 0) {
+    return '-'
+  }
+  const school = schoolList.value.find(s => s.id === schoolId)
+  return school ? school.name : '-'
+}
+
+/** 获取班级名称 */
+function getClassName(classId) {
+  if (!classId || !classList.value || classList.value.length === 0) {
+    return '-'
+  }
+  const cls = classList.value.find(c => c.classId === classId)
+  return cls ? cls.name : '-'
+}
+
+/** 获取校区名称 */
+function getCampusName(campusId) {
+  if (!campusId || !campusList.value || campusList.value.length === 0) {
+    return '-'
+  }
+  const campus = campusList.value.find(c => c.id === campusId)
+  return campus ? campus.name : '-'
+}
+
+/** 获取校区班级名称 */
+function getCampusClassName(classId) {
+  if (!classId || !campusClassList.value || campusClassList.value.length === 0) {
+    return '-'
+  }
+  const cls = campusClassList.value.find(c => c.classId === classId)
+  return cls ? cls.name : '-'
+}
+
+/** 获取代理商名称 */
+function getAgentName(agentId) {
+  if (!agentId || !agentList.value || agentList.value.length === 0) {
+    return '-'
+  }
+  const agent = agentList.value.find(a => a.agentId === agentId)
+  return agent ? agent.name : '-'
+}
+
+/** 格式化正确率 */
+function formatRate(rate) {
+  if (rate == null || rate === undefined) {
+    return '0.0'
+  }
+  return rate.toFixed(1)
+}
+
+/** 格式化时长(秒转小时分钟) */
+function formatDuration(seconds) {
+  if (!seconds || seconds === 0) {
+    return '0分钟'
+  }
+  const hours = Math.floor(seconds / 3600)
+  const minutes = Math.floor((seconds % 3600) / 60)
+  if (hours > 0) {
+    return `${hours}小时${minutes}分钟`
+  }
+  return `${minutes}分钟`
+}
+
+/** 加载代理商列表 */
+function loadAgentList() {
+  getAgentList({}).then(response => {
+    agentList.value = response.data || []
+  })
+}
+
+onMounted(() => {
+  console.log('页面初始化,开始加载数据')
+  loadAgentList()
+  // 直接调用,onMounted 已经确保组件挂载完成
+  getList()
+})
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+
+/* 确保 splitpanes 的 pane 可以滚动 */
+:deep(.splitpanes__pane) {
+  overflow: auto !important;
+  height: 100%;
+}
+
+/* 确保 el-col 内容正常显示,不被遮挡 */
+:deep(.splitpanes__pane .el-col) {
+  padding: 0;
+  overflow: visible;
+}
+</style>
+

+ 83 - 2
ie-admin/src/main/java/com/ruoyi/web/controller/learn/LearnStudentController.java

@@ -1,9 +1,17 @@
 package com.ruoyi.web.controller.learn;
 
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletResponse;
 
+import com.ruoyi.dz.domain.DzSchool;
+import com.ruoyi.dz.service.IDzClassesService;
+import com.ruoyi.dz.service.IDzSchoolService;
 import io.swagger.annotations.Api;
+import org.apache.commons.collections.CollectionUtils;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -19,13 +27,14 @@ import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.enums.BusinessType;
 import com.ruoyi.learn.domain.LearnStudent;
+import com.ruoyi.learn.dto.StudyRecordStatisticsDTO;
 import com.ruoyi.learn.service.ILearnStudentService;
 import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.common.core.page.TableDataInfo;
 
 /**
  * 学生Controller
- * 
+ *
  * @author ruoyi
  * @date 2025-09-18
  */
@@ -37,6 +46,11 @@ public class LearnStudentController extends BaseController
     @Autowired
     private ILearnStudentService learnStudentService;
 
+    @Autowired
+    private IDzSchoolService schoolService;
+
+    @Autowired
+    private IDzClassesService classesService;
     /**
      * 查询学生列表
      */
@@ -99,9 +113,76 @@ public class LearnStudentController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('learn:student:remove')")
     @Log(title = "学生", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{studentIds}")
+    @DeleteMapping("/{studentIds}")
     public AjaxResult remove(@PathVariable Long[] studentIds)
     {
         return toAjax(learnStudentService.deleteLearnStudentByStudentIds(studentIds));
     }
+
+    /**
+     * 统计学习记录
+     */
+    @PreAuthorize("@ss.hasPermi('learn:student:list')")
+    @GetMapping("/statisticStudyRecord")
+    public TableDataInfo statisticStudyRecord(StudyRecordStatisticsDTO dto)
+    {
+        startPage();
+        List<StudyRecordStatisticsDTO> list = learnStudentService.statisticStudyRecord(dto);
+        // 处理数据,设置学校名称和班级名称
+        list = processData(list);
+        return getDataTable(list);
+    }
+
+    public List<StudyRecordStatisticsDTO> processData(List<StudyRecordStatisticsDTO> list) {
+        //将里面的campusId与schoolId汇总为一个list后,通过schoolService查询所有学校的信息List<DzSchool>
+        List<Long> schoolIds = list.stream().map(StudyRecordStatisticsDTO::getSchoolId).filter(schoolId -> schoolId != null).collect(Collectors.toList());
+        List<Long> campusIds = list.stream().map(StudyRecordStatisticsDTO::getCampusId).filter(campusId -> campusId != null).collect(Collectors.toList());
+        // 合并schoolId和campusId,并去重
+        List<Long> allIds = new ArrayList<>(schoolIds);
+        allIds.addAll(campusIds);
+        List<Long> distinctSchoolIds = allIds.stream().distinct().collect(Collectors.toList());
+        Map<Long, String> schoolMap = new HashMap<>();
+        if (CollectionUtils.isNotEmpty(distinctSchoolIds)) {
+            List<DzSchool> schoolList = schoolService.selectDzSchoolListByIds(distinctSchoolIds);
+            if (CollectionUtils.isNotEmpty(schoolList)) {
+                schoolMap = schoolList.stream()
+                        .collect(Collectors.toMap(dzSchool -> dzSchool.getId(), dzSchool -> dzSchool.getName()));
+            }
+        }
+
+        //将里面的classId与campusClassId汇总为一个list后,通过classesService查询所有班级的信息List<DzClasses>
+        List<Long> classIds = list.stream().map(StudyRecordStatisticsDTO::getClassId).filter(classId -> classId != null).collect(Collectors.toList());
+        List<Long> campusClassIds = list.stream().map(StudyRecordStatisticsDTO::getCampusClassId).filter(campusClassId -> campusClassId != null).collect(Collectors.toList());
+        // 合并classId和campusClassId,并去重
+        List<Long> allClassIds = new java.util.ArrayList<>(classIds);
+        allClassIds.addAll(campusClassIds);
+        List<Long> distinctClassIds = allClassIds.stream().distinct().collect(Collectors.toList());
+        Map<Long, String> classMap = new HashMap<>();
+        if (CollectionUtils.isNotEmpty(distinctClassIds)) {
+            List<com.ruoyi.dz.domain.DzClasses> classList = classesService.selectDzClassesListByIds(distinctClassIds);
+            if (CollectionUtils.isNotEmpty(classList)) {
+                classMap = classList.stream()
+                        .collect(Collectors.toMap(dzClass -> dzClass.getClassId(), dzClass -> dzClass.getName()));
+            }
+        }
+        for (StudyRecordStatisticsDTO dto : list) {
+            // 设置注册学校名称(schoolId对应的学校名称)
+            if (dto.getSchoolId() != null) {
+                dto.setSchoolName(schoolMap.get(dto.getSchoolId()));
+            }
+            // 设置学校班级名称(classId对应的班级名称)
+            if (dto.getClassId() != null) {
+                dto.setSchoolClassName(classMap.get(dto.getClassId()));
+            }
+            // 设置培训校区名称(campusId对应的学校名称)
+            if (dto.getCampusId() != null) {
+                dto.setCampusName(schoolMap.get(dto.getCampusId()));
+            }
+            // 设置校区班级名称(campusClassId对应的班级名称)
+            if (dto.getCampusClassId() != null) {
+                dto.setCampusClassName(classMap.get(dto.getCampusClassId()));
+            }
+        }
+        return list;
+    }
 }

+ 1 - 0
ie-system/src/main/java/com/ruoyi/dz/dto/CardStatisticsDTO.java

@@ -131,3 +131,4 @@ public class CardStatisticsDTO implements Serializable
     }
 }
 
+

+ 1 - 0
ie-system/src/main/java/com/ruoyi/enums/StatisticsType.java

@@ -19,3 +19,4 @@ public enum StatisticsType {
     private final String title;
 }
 
+

+ 288 - 0
ie-system/src/main/java/com/ruoyi/learn/dto/StudyRecordStatisticsDTO.java

@@ -0,0 +1,288 @@
+package com.ruoyi.learn.dto;
+
+import java.io.Serializable;
+
+/**
+ * 学习记录统计结果DTO
+ * 
+ * @author ruoyi
+ */
+public class StudyRecordStatisticsDTO implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 学校ID */
+    private Long schoolId;
+
+    /** 班级ID */
+    private Long classId;
+
+    /** 校区ID */
+    private Long campusId;
+
+    /** 校区班级ID */
+    private Long campusClassId;
+
+    /** 代理商ID */
+    private Long agentId;
+
+    /** 末级代理商ID */
+    private Long leafAgentId;
+
+    /** 学生昵称 */
+    private String nickName;
+
+    /** 卡号 */
+    private String cardNo;
+
+    /** 学生ID */
+    private Long studentId;
+
+    /** 做题总数 */
+    private Integer num;
+
+    /** 正确数 */
+    private Integer correct;
+
+    /** 正确率 */
+    private Double rate;
+
+    /** 试卷数量 */
+    private Integer paperCount;
+
+    /** 视频观看标题总数 */
+    private Integer videoTotalTitles;
+
+    /** 视频观看时长(秒) */
+    private Long videoDurationValue;
+
+    /** 注册学校名称 */
+    private String schoolName;
+
+    /** 学校班级名称 */
+    private String schoolClassName;
+
+    /** 培训校区名称 */
+    private String campusName;
+
+    /** 校区班级名称 */
+    private String campusClassName;
+
+    public Long getSchoolId()
+    {
+        return schoolId;
+    }
+
+    public void setSchoolId(Long schoolId)
+    {
+        this.schoolId = schoolId;
+    }
+
+    public Long getClassId()
+    {
+        return classId;
+    }
+
+    public void setClassId(Long classId)
+    {
+        this.classId = classId;
+    }
+
+    public Long getCampusId()
+    {
+        return campusId;
+    }
+
+    public void setCampusId(Long campusId)
+    {
+        this.campusId = campusId;
+    }
+
+    public Long getCampusClassId()
+    {
+        return campusClassId;
+    }
+
+    public void setCampusClassId(Long campusClassId)
+    {
+        this.campusClassId = campusClassId;
+    }
+
+    public Long getAgentId()
+    {
+        return agentId;
+    }
+
+    public void setAgentId(Long agentId)
+    {
+        this.agentId = agentId;
+    }
+
+    public Long getLeafAgentId()
+    {
+        return leafAgentId;
+    }
+
+    public void setLeafAgentId(Long leafAgentId)
+    {
+        this.leafAgentId = leafAgentId;
+    }
+
+    public String getNickName()
+    {
+        return nickName;
+    }
+
+    public void setNickName(String nickName)
+    {
+        this.nickName = nickName;
+    }
+
+    public String getCardNo()
+    {
+        return cardNo;
+    }
+
+    public void setCardNo(String cardNo)
+    {
+        this.cardNo = cardNo;
+    }
+
+    public Long getStudentId()
+    {
+        return studentId;
+    }
+
+    public void setStudentId(Long studentId)
+    {
+        this.studentId = studentId;
+    }
+
+    public Integer getNum()
+    {
+        return num;
+    }
+
+    public void setNum(Integer num)
+    {
+        this.num = num;
+    }
+
+    public Integer getCorrect()
+    {
+        return correct;
+    }
+
+    public void setCorrect(Integer correct)
+    {
+        this.correct = correct;
+    }
+
+    public Double getRate()
+    {
+        return rate;
+    }
+
+    public void setRate(Double rate)
+    {
+        this.rate = rate;
+    }
+
+    public Integer getPaperCount()
+    {
+        return paperCount;
+    }
+
+    public void setPaperCount(Integer paperCount)
+    {
+        this.paperCount = paperCount;
+    }
+
+    public Integer getVideoTotalTitles()
+    {
+        return videoTotalTitles;
+    }
+
+    public void setVideoTotalTitles(Integer videoTotalTitles)
+    {
+        this.videoTotalTitles = videoTotalTitles;
+    }
+
+    public Long getVideoDurationValue()
+    {
+        return videoDurationValue;
+    }
+
+    public void setVideoDurationValue(Long videoDurationValue)
+    {
+        this.videoDurationValue = videoDurationValue;
+    }
+
+    public String getSchoolName()
+    {
+        return schoolName;
+    }
+
+    public void setSchoolName(String schoolName)
+    {
+        this.schoolName = schoolName;
+    }
+
+    public String getSchoolClassName()
+    {
+        return schoolClassName;
+    }
+
+    public void setSchoolClassName(String schoolClassName)
+    {
+        this.schoolClassName = schoolClassName;
+    }
+
+    public String getCampusName()
+    {
+        return campusName;
+    }
+
+    public void setCampusName(String campusName)
+    {
+        this.campusName = campusName;
+    }
+
+    public String getCampusClassName()
+    {
+        return campusClassName;
+    }
+
+    public void setCampusClassName(String campusClassName)
+    {
+        this.campusClassName = campusClassName;
+    }
+
+    @Override
+    public String toString()
+    {
+        return "StudyRecordStatisticsDTO{" +
+                "schoolId=" + schoolId +
+                ", classId=" + classId +
+                ", campusId=" + campusId +
+                ", campusClassId=" + campusClassId +
+                ", agentId=" + agentId +
+                ", leafAgentId=" + leafAgentId +
+                ", nickName='" + nickName + '\'' +
+                ", cardNo='" + cardNo + '\'' +
+                ", studentId=" + studentId +
+                ", num=" + num +
+                ", correct=" + correct +
+                ", rate=" + rate +
+                ", paperCount=" + paperCount +
+                ", videoTotalTitles=" + videoTotalTitles +
+                ", videoDurationValue=" + videoDurationValue +
+                ", schoolName='" + schoolName + '\'' +
+                ", schoolClassName='" + schoolClassName + '\'' +
+                ", campusName='" + campusName + '\'' +
+                ", campusClassName='" + campusClassName + '\'' +
+                '}';
+    }
+}
+
+

+ 10 - 2
ie-system/src/main/java/com/ruoyi/learn/mapper/LearnStudentMapper.java

@@ -1,9 +1,9 @@
 package com.ruoyi.learn.mapper;
 
 import java.util.List;
-import java.util.Map;
 
 import com.ruoyi.learn.domain.LearnStudent;
+import com.ruoyi.learn.dto.StudyRecordStatisticsDTO;
 import org.apache.ibatis.annotations.Param;
 
 /**
@@ -14,10 +14,18 @@ import org.apache.ibatis.annotations.Param;
  */
 public interface LearnStudentMapper 
 {
-    public List<LearnStudent> selectLearnStudentsByMap(Map cond);
+    public List<LearnStudent> selectLearnStudentsByMap(java.util.Map<String, Object> cond);
 
     public List<LearnStudent> selectClassStudents(@Param("batchId") Long batchId, @Param("classIds") Long[] classIds);
 
+    /**
+     * 统计学习记录
+     * 
+     * @param dto 查询参数
+     * @return 学习记录统计结果集合
+     */
+    public List<StudyRecordStatisticsDTO> statisticStudyRecord(StudyRecordStatisticsDTO dto);
+
     /**
      * 查询学生
      * 

+ 8 - 0
ie-system/src/main/java/com/ruoyi/learn/service/ILearnStudentService.java

@@ -2,6 +2,7 @@ package com.ruoyi.learn.service;
 
 import java.util.List;
 import com.ruoyi.learn.domain.LearnStudent;
+import com.ruoyi.learn.dto.StudyRecordStatisticsDTO;
 
 /**
  * 学生Service接口
@@ -11,6 +12,13 @@ import com.ruoyi.learn.domain.LearnStudent;
  */
 public interface ILearnStudentService 
 {
+    /**
+     * 统计学习记录
+     * 
+     * @param dto 查询参数
+     * @return 学习记录统计结果集合
+     */
+    public List<StudyRecordStatisticsDTO> statisticStudyRecord(StudyRecordStatisticsDTO dto);
     /**
      * 查询学生
      * 

+ 13 - 0
ie-system/src/main/java/com/ruoyi/learn/service/impl/LearnStudentServiceImpl.java

@@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.ruoyi.learn.mapper.LearnStudentMapper;
 import com.ruoyi.learn.domain.LearnStudent;
+import com.ruoyi.learn.dto.StudyRecordStatisticsDTO;
 import com.ruoyi.learn.service.ILearnStudentService;
 
 /**
@@ -90,4 +91,16 @@ public class LearnStudentServiceImpl implements ILearnStudentService
     {
         return learnStudentMapper.deleteLearnStudentByStudentId(studentId);
     }
+
+    /**
+     * 统计学习记录
+     * 
+     * @param dto 查询参数
+     * @return 学习记录统计结果集合
+     */
+    @Override
+    public List<StudyRecordStatisticsDTO> statisticStudyRecord(StudyRecordStatisticsDTO dto)
+    {
+        return learnStudentMapper.statisticStudyRecord(dto);
+    }
 }

+ 98 - 0
ie-system/src/main/resources/mapper/learn/LearnStudentMapper.xml

@@ -107,4 +107,102 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{studentId}
         </foreach>
     </delete>
+
+    <!-- 统计学习记录 -->
+    <select id="statisticStudyRecord" parameterType="com.ruoyi.learn.dto.StudyRecordStatisticsDTO" resultType="com.ruoyi.learn.dto.StudyRecordStatisticsDTO">
+        WITH T1 AS (
+            <!-- 定义查询 1 的聚合结果(做题统计)-->
+            SELECT 
+                a.student_id,
+                COUNT(*) AS num, 
+                SUM(IF(a.state = 1, 1, 0)) AS correct, 
+                ROUND(SUM(IF(a.state = 1, 1, 0)) * 100 / COUNT(*), 1) AS rate, 
+                COUNT(DISTINCT le.paper_id) AS paperCount
+            FROM `sys_user` u 
+            JOIN `dz_cards` dc ON u.card_id = dc.card_id 
+            JOIN `learn_examinee` le ON le.student_id = u.user_id AND le.state >= 4 
+            JOIN `learn_answer` a ON u.user_id = a.student_id AND a.state > 0 
+            WHERE 
+                dc.school_id != 0 AND dc.class_id != 0 AND dc.campus_id != 0 AND dc.campus_class_id != 0 
+                AND (dc.agent_id != 0 OR dc.leaf_agent_id != 0) AND u.nick_name != '' AND dc.card_no != '' 
+            GROUP BY a.student_id
+        ),
+        T2 AS (
+            <!-- 定义查询 2 的聚合结果(视频观看统计)-->
+            SELECT 
+                ls.student_id, 
+                COUNT(DISTINCT vw.title) AS total, 
+                ROUND(SUM(vw.duration * vw.percent / 100.0)) AS value
+            FROM `b_customer_video_watches` vw
+            JOIN `learn_student` ls ON vw.customerCode = ls.student_id
+            GROUP BY ls.student_id
+        ),
+        AllStudents AS (
+            <!-- 提取所有涉及的唯一 Student ID-->
+            SELECT student_id FROM T1
+            UNION
+            SELECT student_id FROM T2
+        ),
+        StudentBaseInfo AS (
+            <!-- 获取所有涉及学生的基础信息-->
+            SELECT DISTINCT
+                asl.student_id,
+                COALESCE(dc.school_id, NULL) AS school_id, 
+                COALESCE(dc.class_id, NULL) AS class_id, 
+                COALESCE(dc.campus_id, NULL) AS campus_id, 
+                COALESCE(dc.campus_class_id, NULL) AS campus_class_id, 
+                COALESCE(dc.agent_id, NULL) AS agent_id, 
+                COALESCE(dc.leaf_agent_id, NULL) AS leaf_agent_id, 
+                u.nick_name AS nick_name, 
+                COALESCE(dc.card_no, NULL) AS card_no
+            FROM AllStudents asl
+            <!-- 基础信息主要从 sys_user 和 dz_cards 获取-->
+            LEFT JOIN `sys_user` u ON asl.student_id = u.user_id
+            LEFT JOIN `dz_cards` dc ON u.card_id = dc.card_id
+        )
+        <!-- 主查询:将统计数据 LEFT JOIN 到基础信息上-->
+        SELECT
+            <!-- 基础信息字段(已补齐,始终输出)-->
+            SBI.school_id as schoolId, 
+            SBI.class_id as classId, 
+            SBI.campus_id as campusId, 
+            SBI.campus_class_id as campusClassId, 
+            SBI.agent_id as agentId, 
+            SBI.leaf_agent_id as leafAgentId, 
+            SBI.nick_name as nickName, 
+            SBI.card_no as cardNo, 
+            SBI.student_id as studentId,
+            
+            <!-- T1 统计结果-->
+            COALESCE(T1.num, 0) AS num,
+            COALESCE(T1.correct, 0) AS correct,
+            <!-- RATE 字段处理:如果 T1.num 为 0 则 Rate 为 0-->
+            CASE 
+                WHEN COALESCE(T1.num, 0) = 0 THEN 0
+                ELSE T1.rate
+            END AS rate,
+            COALESCE(T1.paperCount, 0) AS paperCount,
+            
+            <!-- T2 统计结果-->
+            COALESCE(T2.total, 0) AS videoTotalTitles,
+            COALESCE(T2.value, 0) AS videoDurationValue
+
+        FROM 
+            StudentBaseInfo AS SBI
+        LEFT JOIN 
+            T1 ON SBI.student_id = T1.student_id
+        LEFT JOIN 
+            T2 ON SBI.student_id = T2.student_id
+        <where>
+            <if test="schoolId != null"> AND SBI.school_id = #{schoolId}</if>
+            <if test="classId != null"> AND SBI.class_id = #{classId}</if>
+            <if test="campusId != null"> AND SBI.campus_id = #{campusId}</if>
+            <if test="campusClassId != null"> AND SBI.campus_class_id = #{campusClassId}</if>
+            <if test="agentId != null"> AND (SBI.agent_id = #{agentId} OR SBI.leaf_agent_id = #{agentId})</if>
+            <if test="nickName != null and nickName != ''"> AND SBI.nick_name LIKE CONCAT('%', #{nickName}, '%')</if>
+            <if test="cardNo != null and cardNo != ''"> AND SBI.card_no LIKE CONCAT('%', #{cardNo}, '%')</if>
+        </where>
+        ORDER BY 
+            SBI.student_id
+    </select>
 </mapper>