Kaynağa Gözat

Merge branch 'master' of http://49.234.186.218:9000/root/ieplus

mingfu 2 hafta önce
ebeveyn
işleme
f9e013ae55

+ 10 - 0
back-ui/src/api/dz/papers.js

@@ -684,4 +684,14 @@ export function sendUnfinishAlarm(data) {
         method: 'post',
         data
     })
+}
+
+export function getClassesBuildStatsDetail(params) {
+    // params: {buildType, batchId, classId, statType}
+    // statType: 'send' | 'total' | 'unexact' | 'unfinish' | 'unsend'
+    return request({
+        url: '/learn/teaching/getClassesBuildStatsDetail',
+        method: 'get',
+        params
+    })
 }

+ 49 - 3
back-ui/src/views/dz/cards/components/CardTable.vue

@@ -68,9 +68,16 @@
         </el-table-column>
         <el-table-column label="定向" prop="directedStudy" align="center" width="140">
           <template #default="scope">
-            <div v-if="getFirstDirectedStudy(scope.row)" class="cursor-pointer text-blue-500 hover:text-blue-700" @click="handleShowDirectedStudy(scope.row)">
+            <div v-if="getFirstDirectedStudyInfo(scope.row)" class="cursor-pointer text-blue-500 hover:text-blue-700" @click="handleShowDirectedStudy(scope.row)">
               <el-tooltip :content="getFirstDirectedStudy(scope.row)" placement="top" :disabled="!getFirstDirectedStudy(scope.row)">
-                <div class="truncate">{{ getFirstDirectedStudy(scope.row) }}</div>
+                <div class="directed-study-cell">
+                  <div v-if="getFirstDirectedStudyInfo(scope.row).universityName" class="directed-study-line">
+                    {{ getFirstDirectedStudyInfo(scope.row).universityName }}
+                  </div>
+                  <div v-if="getFirstDirectedStudyInfo(scope.row).majorName" class="directed-study-line">
+                    {{ getFirstDirectedStudyInfo(scope.row).majorName }}
+                  </div>
+                </div>
               </el-tooltip>
             </div>
             <span v-else>-</span>
@@ -191,7 +198,7 @@ const getAssignExamType = (row) => {
   return row && row.assignExamType ? row.assignExamType : null;
 };
 
-// 解析directedStudy JSON并获取第一个的显示文本
+// 解析directedStudy JSON并获取第一个的显示文本(用于tooltip)
 const getFirstDirectedStudy = (row) => {
   if (!row || !row.directedStudy) {
     return null;
@@ -218,6 +225,33 @@ const getFirstDirectedStudy = (row) => {
   return null;
 };
 
+// 解析directedStudy JSON并获取第一个的信息对象
+const getFirstDirectedStudyInfo = (row) => {
+  if (!row || !row.directedStudy) {
+    return null;
+  }
+  try {
+    const directedStudy = typeof row.directedStudy === 'string' 
+      ? JSON.parse(row.directedStudy) 
+      : row.directedStudy;
+    
+    if (Array.isArray(directedStudy) && directedStudy.length > 0) {
+      const first = directedStudy[0];
+      const universityName = first?.universityName || '';
+      const majorName = first?.majorName || '';
+      if (universityName || majorName) {
+        return {
+          universityName: universityName,
+          majorName: majorName
+        };
+      }
+    }
+  } catch (e) {
+    console.error('解析directedStudy失败:', e);
+  }
+  return null;
+};
+
 // 获取完整的directedStudy列表
 const getDirectedStudyList = (row) => {
   if (!row || !row.directedStudy) {
@@ -271,4 +305,16 @@ const handleShowDirectedStudy = (row) => {
   top: 0 !important;
   left: 0 !important;
 }
+
+.directed-study-cell {
+  text-align: center;
+  line-height: 1.5;
+}
+
+.directed-study-line {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-size: 12px;
+}
 </style>

+ 302 - 2
back-ui/src/views/dz/papers/components/paper-exact-intelligent.vue

@@ -10,7 +10,7 @@
             </el-form>
         </el-col>
         <el-col :span="16">
-            <class-statistic-table exact-mode/>
+            <class-statistic-table exact-mode @stat-click="handleStatClick"/>
         </el-col>
     </el-row>
     <el-divider/>
@@ -18,17 +18,104 @@
         <el-button type="primary" size="large" @click="handleSubmit">生成试卷</el-button>
     </div>
     <built-paper-list ref="built" @send="handleSubmit" />
+    
+    <!-- 学生列表弹窗 -->
+    <el-dialog v-model="studentDialogVisible" :title="studentDialogTitle" width="1200px" destroy-on-close>
+        <div class="student-table-container">
+            <el-table 
+                v-loading="studentLoading" 
+                :data="studentList" 
+                stripe
+                height="calc(100vh - 350px)"
+                style="width: 100%"
+            >
+                <!-- <el-table-column label="姓名" prop="studentName" width="120" align="center"/> -->
+                <el-table-column label="姓名-手机" prop="namePhone" min-width="180" align="center"/>
+                <el-table-column label="卡号" prop="cardNo" min-width="120" align="center"/>
+                <el-table-column label="注册学校" prop="registerSchool" min-width="120" align="center" show-overflow-tooltip/>
+                <el-table-column label="学校班级" prop="registerClass" min-width="80" align="center" show-overflow-tooltip/>
+                <el-table-column label="培训校区" prop="trainSchool" min-width="120" align="center" show-overflow-tooltip/>
+                <el-table-column label="校区班级" prop="trainClass" min-width="80" align="center" show-overflow-tooltip/>
+                <el-table-column label="考生类型" prop="examType" min-width="150" align="center">
+                    <template #default="scope">
+                        <template v-if="scope.row && scope.row.examType && exam_type">
+                            <dict-tag :options="exam_type" :value="scope.row.examType" />
+                        </template>
+                        <span v-else>-</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="定向" prop="direct" min-width="150" align="center">
+                    <template #default="{row}">
+                        <div v-if="getFirstDirectedStudyInfo(row.direct)" class="cursor-pointer text-blue-500 hover:text-blue-700" @click.stop="handleShowDirectedStudy(row.direct)">
+                            <el-tooltip :content="getFirstDirectedStudyTooltip(row.direct)" placement="top" :disabled="!getFirstDirectedStudyInfo(row.direct)">
+                                <div class="directed-study-cell">
+                                    <div v-if="getFirstDirectedStudyInfo(row.direct).universityName" class="directed-study-line">
+                                        {{ getFirstDirectedStudyInfo(row.direct).universityName }}
+                                    </div>
+                                    <div v-if="getFirstDirectedStudyInfo(row.direct).majorName" class="directed-study-line">
+                                        {{ getFirstDirectedStudyInfo(row.direct).majorName }}
+                                    </div>
+                                </div>
+                            </el-tooltip>
+                        </div>
+                        <span v-else>-</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="代理商" prop="agent" min-width="120" align="center" show-overflow-tooltip/>
+                <el-table-column label="单招年份" prop="year" min-width="80" align="center"/>
+                <el-table-column label="省份" prop="province" min-width="80" align="center"/>
+                <el-table-column label="机构" prop="institution" min-width="150" align="center" show-overflow-tooltip/>
+            </el-table>
+            
+            <!-- 前端分页 -->
+            <div class="student-pagination">
+                <el-pagination
+                    v-model:current-page="studentPageNum"
+                    v-model:page-size="studentPageSize"
+                    :page-sizes="[10, 20, 50, 100]"
+                    :total="studentTotal"
+                    layout="total, sizes, prev, pager, next, jumper"
+                    @size-change="handleStudentPageSizeChange"
+                    @current-change="handleStudentPageChange"
+                />
+            </div>
+        </div>
+        
+        <!-- 定向信息弹窗 -->
+        <el-dialog v-model="directDetailVisible" title="定向信息" width="900px" destroy-on-close>
+            <el-table :data="directDetailData" class="w-full" style="width: 100%">
+                <el-table-column label="序号" type="index" width="60" align="center"></el-table-column>
+                <el-table-column label="编码" prop="code" min-width="120" align="center"></el-table-column>
+                <el-table-column label="学校" prop="universityName" min-width="200" align="center">
+                    <template #default="scope">
+                        <span>{{ scope.row.universityName }}{{ scope.row.universityId ? `(${scope.row.universityId})` : '' }}</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="专业" prop="majorName" min-width="200" align="center">
+                    <template #default="scope">
+                        <span>{{ scope.row.majorName }}{{ scope.row.majorId ? `(${scope.row.majorId})` : '' }}</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="专业类" prop="majorAncestors" min-width="200" align="center"></el-table-column>
+            </el-table>
+        </el-dialog>
+    </el-dialog>
 </template>
 
 <script setup name="PaperExactIntelligent">
+import { ref, computed, getCurrentInstance } from 'vue'
 import consts from "@/utils/consts.js";
 import {useProvidePaperBatchCondition} from "@/views/dz/papers/hooks/usePaperBatchCondition.js";
 import {useProvidePaperClassStatisticCondition} from "@/views/dz/papers/hooks/usePaperClassStatisticCondition.js";
 import ClassStatisticTable from "@/views/dz/papers/components/plugs/class-statistic-table.vue";
 import {ElMessage} from "element-plus";
-import {buildPaperExactIntelligent} from "@/api/dz/papers.js";
+import {buildPaperExactIntelligent, getClassesBuildStatsDetail} from "@/api/dz/papers.js";
 import {useInjectGlobalLoading} from "@/views/hooks/useGlobalLoading.js";
 import BuiltPaperList from "@/views/dz/papers/components/plugs/built-paper-list.vue";
+import DictTag from '@/components/DictTag/index.vue';
+
+const { proxy } = getCurrentInstance();
+const { exam_type } = proxy.useDict("exam_type");
 
 const type = consts.enums.buildType.ExactIntelligent
 const {batchId, batchList, onBatchReady} = useProvidePaperBatchCondition(type, true)
@@ -37,6 +124,187 @@ const {loading} = useInjectGlobalLoading()
 const built = ref(null)
 const hasBuiltPaper = computed(() => built.value?.hasPaper)
 
+// 统计字段映射
+const statTypeMap = {
+    'send': '组卷已完成',
+    'total': '班级人数',
+    'unexact': '未定向未组卷',
+    'unfinish': '组卷未完成',
+    'unsend': '定向未组卷'
+}
+
+// 学生列表弹窗相关
+const studentDialogVisible = ref(false)
+const studentDialogTitle = ref('')
+const studentList = ref([])
+const studentLoading = ref(false)
+const studentPageNum = ref(1)
+const studentPageSize = ref(10)
+const studentTotal = ref(0)
+const currentStatType = ref('')
+const currentRow = ref(null)
+
+// 定向详情弹窗相关
+const directDetailVisible = ref(false)
+const directDetailData = ref([])
+
+// 解析directedStudy JSON并获取第一个的显示文本(用于tooltip)
+const getFirstDirectedStudyTooltip = (direct) => {
+    if (!direct) {
+        return null;
+    }
+    try {
+        const directedStudy = typeof direct === 'string' 
+            ? JSON.parse(direct) 
+            : direct;
+        
+        if (Array.isArray(directedStudy) && directedStudy.length > 0) {
+            const first = directedStudy[0];
+            const universityName = first?.universityName || '';
+            const majorName = first?.majorName || '';
+            if (universityName || majorName) {
+                const parts = [];
+                if (universityName) parts.push(universityName);
+                if (majorName) parts.push(majorName);
+                return parts.join(' - ');
+            }
+        }
+    } catch (e) {
+        console.error('解析directedStudy失败:', e);
+    }
+    return null;
+};
+
+// 解析directedStudy JSON并获取第一个的信息对象
+const getFirstDirectedStudyInfo = (direct) => {
+    if (!direct) {
+        return null;
+    }
+    try {
+        const directedStudy = typeof direct === 'string' 
+            ? JSON.parse(direct) 
+            : direct;
+        
+        if (Array.isArray(directedStudy) && directedStudy.length > 0) {
+            const first = directedStudy[0];
+            const universityName = first?.universityName || '';
+            const majorName = first?.majorName || '';
+            if (universityName || majorName) {
+                return {
+                    universityName: universityName,
+                    majorName: majorName
+                };
+            }
+        }
+    } catch (e) {
+        console.error('解析directedStudy失败:', e);
+    }
+    return null;
+};
+
+// 获取完整的directedStudy列表
+const getDirectedStudyList = (direct) => {
+    if (!direct) {
+        return [];
+    }
+    try {
+        const directedStudy = typeof direct === 'string' 
+            ? JSON.parse(direct) 
+            : direct;
+        
+        if (Array.isArray(directedStudy)) {
+            return directedStudy.map(item => ({
+                code: item?.code || '-',
+                majorName: item?.majorName || '-',
+                majorId: item?.majorId || null,
+                universityName: item?.universityName || '-',
+                universityId: item?.universityId || null,
+                majorAncestors: item?.majorAncestors || '-'
+            }));
+        }
+    } catch (e) {
+        console.error('解析directedStudy失败:', e);
+    }
+    return [];
+};
+
+// 显示定向信息弹窗
+const handleShowDirectedStudy = (direct) => {
+    directDetailData.value = getDirectedStudyList(direct);
+    directDetailVisible.value = true;
+};
+
+// 处理统计字段点击
+const handleStatClick = async (row, statType) => {
+    console.log('点击统计字段:', { row, statType })
+    if (!row || !statType) {
+        console.error('参数错误:', { row, statType })
+        return
+    }
+    
+    currentRow.value = row
+    currentStatType.value = statType
+    studentDialogTitle.value = `${row.className || ''} - ${statTypeMap[statType] || statType}`
+    studentPageNum.value = 1
+    studentPageSize.value = 10
+    
+    // 先显示弹窗,再加载数据
+    studentDialogVisible.value = true
+    await loadStudentList()
+}
+
+// 加载学生列表
+const loadStudentList = async () => {
+    if (!currentRow.value || !batchId.value) {
+        console.error('参数不完整:', { currentRow: currentRow.value, batchId: batchId.value })
+        return
+    }
+    
+    studentLoading.value = true
+    try {
+        const params = {
+            buildType: type,
+            batchId: batchId.value,
+            classId: currentRow.value.classId,
+            statType: currentStatType.value
+        }
+        console.log('请求参数:', params)
+        
+        const res = await getClassesBuildStatsDetail(params)
+        console.log('接口响应:', res)
+        
+        const allStudents = res.data || []
+        studentTotal.value = allStudents.length
+        
+        console.log('学生总数:', studentTotal.value)
+        
+        // 前端分页
+        const start = (studentPageNum.value - 1) * studentPageSize.value
+        const end = start + studentPageSize.value
+        studentList.value = allStudents.slice(start, end)
+        
+        console.log('当前页学生数据:', studentList.value)
+    } catch (error) {
+        console.error('加载学生列表失败:', error)
+        ElMessage.error('加载学生列表失败: ' + (error.message || '未知错误'))
+    } finally {
+        studentLoading.value = false
+    }
+}
+
+// 分页大小改变
+const handleStudentPageSizeChange = (size) => {
+    studentPageSize.value = size
+    studentPageNum.value = 1
+    loadStudentList()
+}
+
+// 页码改变
+const handleStudentPageChange = (page) => {
+    studentPageNum.value = page
+    loadStudentList()
+}
+
 const handleSubmit = async function () {
     if (!batchId.value) return ElMessage.error('请选择批次')
     const classIds = selectedClasses.value.map(c => c.classId)
@@ -70,5 +338,37 @@ watch(batchId, () => built.value.reset())
 </script>
 
 <style scoped>
+.truncate {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
 
+.student-table-container {
+    display: flex;
+    flex-direction: column;
+    height: calc(100vh - 300px);
+    min-height: 500px;
+    max-height: calc(100vh - 300px);
+}
+
+.student-pagination {
+    margin-top: 20px;
+    text-align: right;
+    flex-shrink: 0;
+    padding: 10px 0;
+    background-color: #fff;
+}
+
+.directed-study-cell {
+    text-align: center;
+    line-height: 1.5;
+}
+
+.directed-study-line {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    font-size: 12px;
+}
 </style>

+ 51 - 4
back-ui/src/views/dz/papers/components/plugs/class-statistic-table.vue

@@ -1,6 +1,32 @@
 <template>
     <Table :data="classList" :columns="columns" selection-mode="multiple" row-key="classId"
-           :selected-rows="selectedClasses" @selection-change="handleSelectionChange"/>
+           :selected-rows="selectedClasses" @selection-change="handleSelectionChange">
+        <template #send="{row}">
+            <el-link type="primary" :underline="false" @click.stop="$emit('stat-click', row, 'send')" style="cursor: pointer;">
+                {{ row.send || 0 }}
+            </el-link>
+        </template>
+        <template #total="{row}">
+            <el-link type="primary" :underline="false" @click.stop="$emit('stat-click', row, 'total')" style="cursor: pointer;">
+                {{ row.total || 0 }}
+            </el-link>
+        </template>
+        <template #unexact="{row}">
+            <el-link type="primary" :underline="false" @click.stop="$emit('stat-click', row, 'unexact')" style="cursor: pointer;">
+                {{ row.unexact || 0 }}
+            </el-link>
+        </template>
+        <template #unfinish="{row}">
+            <el-link type="primary" :underline="false" @click.stop="$emit('stat-click', row, 'unfinish')" style="cursor: pointer;">
+                {{ row.unfinish || 0 }}
+            </el-link>
+        </template>
+        <template #unsend="{row}">
+            <el-link type="primary" :underline="false" @click.stop="$emit('stat-click', row, 'unsend')" style="cursor: pointer;">
+                {{ row.unsend || 0 }}
+            </el-link>
+        </template>
+    </Table>
 </template>
 
 <script setup name="ClassStatisticTable">
@@ -13,6 +39,8 @@ const props = defineProps({
     handMode: Boolean
 })
 
+defineEmits(['stat-click'])
+
 const {classList, selectedClasses} = useInjectPaperClassStatisticCondition()
 
 const ids = ref([]);
@@ -21,9 +49,28 @@ const multiple = ref(true);
 
 const columns = props.exactMode
     ? props.handMode
-        ? [{label: '班级名称', prop: 'className'}, {label: '专业人数', prop: 'total'}, ...consts.config.exactColumns.slice(0, 3)]
-        : [{label: '班级名称', prop: 'className'}, {label: '总人数', prop: 'total'}, ...consts.config.exactColumns]
-    : [{label: '班级名称', prop: 'className'}, {label: '总人数', prop: 'total'}, ...consts.config.fullColumns]
+        ? [
+            {label: '班级名称', prop: 'className'}, 
+            {label: '专业人数', prop: 'total', type: 'slot', slotName: 'total'}, 
+            {label: '组卷已完成', prop: 'send', type: 'slot', slotName: 'send'},
+            {label: '组卷未完成', prop: 'unfinish', type: 'slot', slotName: 'unfinish'},
+            {label: '定向未组卷', prop: 'unsend', type: 'slot', slotName: 'unsend'}
+        ]
+        : [
+            {label: '班级名称', prop: 'className'}, 
+            {label: '总人数', prop: 'total', type: 'slot', slotName: 'total'}, 
+            {label: '组卷已完成', prop: 'send', type: 'slot', slotName: 'send'},
+            {label: '组卷未完成', prop: 'unfinish', type: 'slot', slotName: 'unfinish'},
+            {label: '定向未组卷', prop: 'unsend', type: 'slot', slotName: 'unsend'},
+            {label: '未定向未组卷', prop: 'unexact', type: 'slot', slotName: 'unexact'}
+        ]
+    : [
+        {label: '班级名称', prop: 'className'}, 
+        {label: '总人数', prop: 'total', type: 'slot', slotName: 'total'}, 
+        {label: '组卷已完成', prop: 'send', type: 'slot', slotName: 'send'},
+        {label: '组卷未完成', prop: 'unfinish', type: 'slot', slotName: 'unfinish'},
+        {label: '未组卷', prop: 'unsend', type: 'slot', slotName: 'unsend'}
+    ]
 
 function handleSelectionChange(selection) {
     ids.value = selection.map((item) => item.classId);

+ 16 - 0
ie-admin/src/main/java/com/ruoyi/web/controller/learn/LearnTeacherController.java

@@ -115,6 +115,22 @@ public class LearnTeacherController extends BaseController {
         return AjaxResult.success(learnTeacherService.getClassesBuildStats(req, getTeacherId()));
     }
 
+    @GetMapping(value = "getClassesBuildStatsDetail")
+    @ApiOperation("班级组卷统计详情(学生列表)")
+    public AjaxResult getClassesBuildStatsDetail(
+            @ApiParam("组卷类型") @RequestParam String buildType,
+            @ApiParam("批次ID") @RequestParam(required = false) Integer batchId,
+            @ApiParam("班级ID") @RequestParam(required = false) Long classId,
+            @ApiParam("统计类型:send/total/unexact/unfinish/unsend") @RequestParam String statType)
+    {
+        TestPaperVO.TestPaperBuildReq req = new TestPaperVO.TestPaperBuildReq();
+        req.setBuildType(buildType);
+        req.setBatchId(batchId);
+        req.setClassId(classId);
+        List<JSONObject> list = learnTeacherService.getClassesBuildStatsDetail(req, getTeacherId(), statType);
+        return AjaxResult.success(list);
+    }
+
 
     @PreAuthorize("@ss.hasPermi('learn:test_paper:add')")
     @GetMapping("build/getBuiltPaper")

+ 18 - 0
ie-admin/src/main/java/com/ruoyi/web/service/LearnTeacherService.java

@@ -125,6 +125,24 @@ public class LearnTeacherService {
         return list;
     }
 
+    /**
+     * 获取班级组卷统计详情(学生列表)
+     * @param req 请求参数
+     * @param teacherId 教师ID
+     * @param statType 统计类型:send/total/unexact/unfinish/unsend
+     * @return 学生信息列表
+     */
+    public List<JSONObject> getClassesBuildStatsDetail(TestPaperVO.TestPaperBuildReq req, Long teacherId, String statType) {
+        req.setTeacherId(teacherId);
+        String buildType = req.getBuildType();
+        if(buildType.startsWith("Exact")) {
+            req.setSubjectId(11L);
+        }
+        Map<String, Object> map = req.toMap();
+        map.put("statType", statType);
+        return dzClassesMapper.selectClassesBuildStatsDetail(map);
+    }
+
     public List<LearnTestPaper> getBuiltTestPaper(TestPaperVO.TestPaperBuildReq req) {
         LearnTestPaper cond = new LearnTestPaper();
         BeanUtils.copyProperties(req, cond);

+ 1 - 0
ie-system/src/main/java/com/ruoyi/dz/mapper/DzClassesMapper.java

@@ -19,6 +19,7 @@ public interface DzClassesMapper
     List<JSONObject> selectClassesDirectedBuildStats(Map cond);
     List<JSONObject> selectClassesBuildStats(Map cond);
     List<JSONObject> getPaperStudentRecords(Map cond);
+    List<JSONObject> selectClassesBuildStatsDetail(Map cond);
 
     public List<DzClasses> selectClassesByIds(@Param("ids") Collection<Long> ids);
     public List<DzClasses> selectClassesForTeacher(@Param("teacherId") Long teacherId);

+ 66 - 0
ie-system/src/main/resources/mapper/dz/DzClassesMapper.xml

@@ -100,6 +100,72 @@
         </where>
     </select>
 
+    <select id="selectClassesBuildStatsDetail" resultType="com.alibaba.fastjson2.JSONObject">
+        SELECT 
+            u.`nick_name` AS studentName,
+            CONCAT(u.`nick_name`, '-', IFNULL(u.`phonenumber`, '')) AS namePhone,
+            IFNULL(card.`card_no`, '') AS cardNo,
+            IFNULL(d.`dept_name`, '') AS institution,
+            IFNULL(u.`location`, IFNULL(reg_school.`pro`, '')) AS province,
+            IFNULL(agent.`name`, '') AS agent,
+            IFNULL(u.`end_year`, '') AS year,
+            IFNULL(reg_school.`name`, '') AS registerSchool,
+            IFNULL(reg_class.`name`, '') AS registerClass,
+            IFNULL(train_school.`name`, '') AS trainSchool,
+            IFNULL(train_class.`name`, '') AS trainClass,
+            IFNULL(u.`exam_type`, '') AS examType,
+            u.directed_study AS direct
+        FROM `dz_teacher_class` tc
+        JOIN `learn_student` ls ON tc.`class_id` = ls.`class_id`
+        JOIN `dz_classes` c ON ls.`class_id` = c.`class_id`
+        JOIN `sys_user` u ON ls.`student_id` = u.`user_id`
+        JOIN `learn_test` lt ON lt.`year` = #{year} <if test="batchId != null"> AND lt.`batch_id` = #{batchId} </if>
+        LEFT JOIN `sys_dept` d ON u.`dept_id` = d.`dept_id`
+        LEFT JOIN `dz_cards` card ON u.`card_id` = card.`card_id`
+        LEFT JOIN `dz_school` reg_school ON card.`school_id` = reg_school.`id`
+        LEFT JOIN `dz_classes` reg_class ON card.`class_id` = reg_class.`class_id`
+        LEFT JOIN `dz_school` train_school ON ls.`school_id` = train_school.`id`
+        LEFT JOIN `dz_classes` train_class ON ls.`class_id` = train_class.`class_id`
+        LEFT JOIN `dz_agent` agent ON (card.`agent_id` = agent.`agent_id` OR card.`leaf_agent_id` = agent.`agent_id` OR (u.`invite_code` IS NOT NULL AND u.`invite_code` != '' AND CAST(u.`invite_code` AS UNSIGNED) = agent.`agent_id`))
+        LEFT JOIN `learn_test_student` ts ON ts.`student_id` = ls.`student_id` AND ts.`batch_id` = lt.`batch_id` AND ts.`class_id` = tc.`class_id`
+            <if test="buildType != null"> AND ts.`build_type` = #{buildType} </if>
+            <if test="subjectId != null"> AND ts.`subject_id` = #{subjectId} </if>
+        <where>
+            tc.`teacher_id` = #{teacherId}
+            <if test="classId != null"> AND ls.`class_id` = #{classId}</if>
+            <if test="batchId != null"> AND lt.`batch_id` = #{batchId}</if>
+            <!-- 根据统计类型过滤 -->
+            <choose>
+                <!-- send: 组卷已完成 -->
+                <when test="statType == 'send'">
+                    AND ls.`major_plan_id` IS NOT NULL 
+                    AND ts.`student_id` IS NOT NULL 
+                    AND ts.`status` = 4
+                </when>
+                <!-- total: 班级人数 -->
+                <when test="statType == 'total'">
+                    <!-- 不添加额外条件,返回所有学生 -->
+                </when>
+                <!-- unexact: 未定向未组卷 -->
+                <when test="statType == 'unexact'">
+                    AND ls.`major_plan_id` IS NULL
+                </when>
+                <!-- unfinish: 组卷未完成 -->
+                <when test="statType == 'unfinish'">
+                    AND ls.`major_plan_id` IS NOT NULL 
+                    AND ts.`student_id` IS NOT NULL 
+                    AND ts.`status` != 4
+                </when>
+                <!-- unsend: 定向未组卷 -->
+                <when test="statType == 'unsend'">
+                    AND ls.`major_plan_id` IS NOT NULL 
+                    AND ts.`student_id` IS NULL
+                </when>
+            </choose>
+        </where>
+        ORDER BY u.`user_id` DESC
+    </select>
+
 
     <sql id="selectDzClassesVo">
         select t1.class_id, t1.dept_id, t1.school_id, t1.year, t1.name, t1.online, t1.status, t1.stats, t1.create_time, t1.update_time,t1.is_default,