Przeglądaj źródła

在线组卷:全量智能、定向手动、全量手动

jinxia.mo 2 tygodni temu
rodzic
commit
fde08acc18

+ 85 - 44
back-ui/src/views/dz/papers/components/paper-exact-hand.vue

@@ -1,48 +1,53 @@
 <template>
-    <el-row :gutter="20">
-        <el-col :span="8">
-            <el-form label-width="68px">
-                <el-form-item label="批次">
-                    <el-select v-model="batchId" clearable style="width: 227px">
-                        <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
-                    </el-select>
-                </el-form-item>
-                <el-form-item label="考生类型">
-                    <el-select v-model="examType" clearable style="width: 227px">
-                        <el-option v-for="e in examTypes" :label="e.dictLabel" :value="e.dictValue"/>
-                    </el-select>
-                </el-form-item>
-                <el-form-item label="院校">
-                    <el-select v-model="universityId" clearable style="width: 227px">
-                        <el-option v-for="u in universities" :label="u.name" :value="u.id"/>
-                    </el-select>
-                </el-form-item>
-                <el-form-item label="专业组">
-                    <el-select v-model="majorGroup" clearable style="width: 227px">
-                        <el-option v-for="g in majorGroups" :label="g" :value="g"/>
-                    </el-select>
-                </el-form-item>
-                <el-form-item label="专业">
-                    <el-select v-model="majorPlanId" clearable style="width: 227px">
-                        <el-option v-for="m in majors" :label="m.majorName" :value="m.id"/>
-                    </el-select>
-                </el-form-item>
-            </el-form>
-        </el-col>
-        <el-col :span="16">
-            <class-statistic-table exact-mode hand-mode/>
-        </el-col>
-    </el-row>
-    <el-divider/>
-    <el-row v-if="!hasBuiltPaper" :gutter="20">
-        <el-col :span="6">
-            <knowledge-tree/>
-        </el-col>
-        <el-col :span="18">
-            <question-hand exact-mode @submit="handleSubmit"/>
-        </el-col>
-    </el-row>
-    <built-paper-list ref="built" @send="handleSubmit" />
+    <div v-loading="questionLoading" element-loading-text="加载试题中...">
+        <el-row :gutter="20">
+            <el-col :span="8">
+                <el-form label-width="68px">
+                    <el-form-item label="批次">
+                        <el-select v-model="batchId" clearable style="width: 227px">
+                            <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item label="考生类型">
+                        <el-select v-model="examType" clearable style="width: 227px">
+                            <el-option v-for="e in examTypes" :label="e.dictLabel" :value="e.dictValue"/>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item label="院校">
+                        <el-select v-model="universityId" clearable style="width: 227px">
+                            <el-option v-for="u in universities" :label="u.name" :value="u.id"/>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item label="专业组">
+                        <el-select v-model="majorGroup" clearable style="width: 227px">
+                            <el-option v-for="g in majorGroups" :label="g" :value="g"/>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item label="专业">
+                        <el-select v-model="majorPlanId" clearable style="width: 227px">
+                            <el-option v-for="m in majors" :label="m.majorName" :value="m.id"/>
+                        </el-select>
+                    </el-form-item>
+                </el-form>
+            </el-col>
+            <el-col :span="16">
+                <class-statistic-table exact-mode hand-mode @stat-click="handleStatClick"/>
+            </el-col>
+        </el-row>
+        <el-divider/>
+        <el-row v-if="!hasBuiltPaper" :gutter="20">
+            <el-col :span="6">
+                <knowledge-tree/>
+            </el-col>
+            <el-col :span="18">
+                <question-hand exact-mode @submit="handleSubmit"/>
+            </el-col>
+        </el-row>
+        <built-paper-list ref="built" @send="handleSubmit" />
+        
+        <!-- 学生列表弹窗 -->
+        <student-list-dialog ref="studentDialogRef" :stat-type-map="statTypeMap"/>
+    </div>
 </template>
 
 <script setup name="PaperExactHand">
@@ -58,6 +63,8 @@ import {usePaperStorage} from "@/views/dz/papers/hooks/usePaperStorage.js";
 import {ElMessage} from "element-plus";
 import router from "@/router/index.js";
 import BuiltPaperList from "@/views/dz/papers/components/plugs/built-paper-list.vue";
+import StudentListDialog from "@/views/dz/papers/components/plugs/student-list-dialog.vue";
+import {useInjectGlobalLoading} from "@/views/hooks/useGlobalLoading.js";
 
 const type = consts.enums.buildType.ExactHand
 const {
@@ -70,9 +77,43 @@ const {
 } = useProvidePaperExactCondition(type)
 const {selectedClasses, classList, loadClassStatistic} = useProvidePaperClassStatisticCondition()
 const {knowledgeNode, knowledgeCheckNodes, loadKnowledge} = useProvidePaperKnowledgeCondition()
+const {loading: globalLoading} = useInjectGlobalLoading()
+const questionLoading = computed(() => globalLoading.value)
 const built = ref(null)
 const hasBuiltPaper = computed(() => built.value?.hasPaper)
 
+// 统计字段映射
+const statTypeMap = {
+    'send': '组卷已完成',
+    'total': '专业人数',
+    'unexact': '未定向未组卷',
+    'unfinish': '组卷未完成',
+    'unsend': '定向未组卷'
+}
+
+// 学生列表弹窗引用
+const studentDialogRef = ref(null)
+
+// 处理统计字段点击
+const handleStatClick = async (row, statType) => {
+    if (!row || !statType || !batchId.value) {
+        console.error('参数错误:', { row, statType, batchId: batchId.value })
+        return
+    }
+    
+    // 构建查询条件参数
+    const queryParams = {}
+    if (conditionArgs.value) {
+        if (conditionArgs.value.examType) queryParams.examType = conditionArgs.value.examType
+        if (conditionArgs.value.universityId) queryParams.universityId = conditionArgs.value.universityId
+        if (conditionArgs.value.majorGroup) queryParams.majorGroup = conditionArgs.value.majorGroup
+        if (conditionArgs.value.majorPlanId) queryParams.majorPlanId = conditionArgs.value.majorPlanId
+    }
+    
+    // 打开学生列表弹窗
+    studentDialogRef.value?.open(row, statType, type, batchId.value, queryParams)
+}
+
 const handleSubmit = async (questions) => {
     // validation
     if (!batchId.value) return ElMessage.error('请选择批次')

+ 17 - 281
back-ui/src/views/dz/papers/components/paper-exact-intelligent.vue

@@ -20,102 +20,20 @@
     <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>
+    <student-list-dialog ref="studentDialogRef" :stat-type-map="statTypeMap"/>
 </template>
 
 <script setup name="PaperExactIntelligent">
-import { ref, computed, getCurrentInstance } from 'vue'
+import { ref, computed } 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, getClassesBuildStatsDetail} from "@/api/dz/papers.js";
+import {buildPaperExactIntelligent} 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");
+import StudentListDialog from "@/views/dz/papers/components/plugs/student-list-dialog.vue";
 
 const type = consts.enums.buildType.ExactIntelligent
 const {batchId, batchList, onBatchReady} = useProvidePaperBatchCondition(type, true)
@@ -133,176 +51,27 @@ const statTypeMap = {
     '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 studentDialogRef = ref(null)
 
 // 处理统计字段点击
 const handleStatClick = async (row, statType) => {
-    console.log('点击统计字段:', { row, statType })
-    if (!row || !statType) {
-        console.error('参数错误:', { row, statType })
+    if (!row || !statType || !batchId.value) {
+        console.error('参数错误:', { row, statType, batchId: batchId.value })
         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
+    // 构建查询条件参数
+    const queryParams = {}
+    if (statArgs) {
+        if (statArgs.examType) queryParams.examType = statArgs.examType
+        if (statArgs.universityId) queryParams.universityId = statArgs.universityId
+        if (statArgs.majorGroup) queryParams.majorGroup = statArgs.majorGroup
+        if (statArgs.majorPlanId) queryParams.majorPlanId = statArgs.majorPlanId
     }
     
-    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()
+    // 打开学生列表弹窗
+    studentDialogRef.value?.open(row, statType, type, batchId.value, queryParams)
 }
 
 const handleSubmit = async function () {
@@ -338,37 +107,4 @@ 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>

+ 74 - 36
back-ui/src/views/dz/papers/components/paper-full-hand.vue

@@ -1,40 +1,45 @@
 <template>
-    <el-row :gutter="20">
-        <el-col :span="8">
-            <el-form label-width="68px">
-                <el-form-item label="批次">
-                    <el-select v-model="batchId" clearable style="width: 227px">
-                        <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
-                    </el-select>
-                </el-form-item>
-                <el-form-item label="考生类型">
-                    <el-select v-model="examType" clearable style="width: 227px">
-                        <el-option v-for="e in examTypes" :label="e.dictLabel" :value="e.dictValue"/>
-                    </el-select>
-                </el-form-item>
-                <el-form-item label="科目">
-                    <el-select v-model="subjectId" clearable style="width: 227px">
-                        <el-option-group v-for="g in groupedSubjects" :label="g.label">
-                            <el-option v-for="s in g.items" :label="s.subjectName" :value="s.subjectId"/>
-                        </el-option-group>
-                    </el-select>
-                </el-form-item>
-            </el-form>
-        </el-col>
-        <el-col :span="16">
-            <class-statistic-table/>
-        </el-col>
-    </el-row>
-    <el-divider/>
-    <el-row v-if="!hasBuiltPaper" :gutter="20">
-        <el-col :span="6">
-            <knowledge-tree/>
-        </el-col>
-        <el-col :span="18">
-            <question-hand @submit="handleSubmit"/>
-        </el-col>
-    </el-row>
-    <built-paper-list ref="built" @send="handleSubmit" />
+    <div v-loading="questionLoading" element-loading-text="加载试题中...">
+        <el-row :gutter="20">
+            <el-col :span="8">
+                <el-form label-width="68px">
+                    <el-form-item label="批次">
+                        <el-select v-model="batchId" clearable style="width: 227px">
+                            <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item label="考生类型">
+                        <el-select v-model="examType" clearable style="width: 227px">
+                            <el-option v-for="e in examTypes" :label="e.dictLabel" :value="e.dictValue"/>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item label="科目">
+                        <el-select v-model="subjectId" clearable style="width: 227px">
+                            <el-option-group v-for="g in groupedSubjects" :label="g.label">
+                                <el-option v-for="s in g.items" :label="s.subjectName" :value="s.subjectId"/>
+                            </el-option-group>
+                        </el-select>
+                    </el-form-item>
+                </el-form>
+            </el-col>
+            <el-col :span="16">
+                <class-statistic-table @stat-click="handleStatClick"/>
+            </el-col>
+        </el-row>
+        <el-divider/>
+        <el-row v-if="!hasBuiltPaper" :gutter="20">
+            <el-col :span="6">
+                <knowledge-tree/>
+            </el-col>
+            <el-col :span="18">
+                <question-hand @submit="handleSubmit"/>
+            </el-col>
+        </el-row>
+        <built-paper-list ref="built" @send="handleSubmit" />
+        
+        <!-- 学生列表弹窗 -->
+        <student-list-dialog ref="studentDialogRef" :stat-type-map="statTypeMap"/>
+    </div>
 </template>
 
 <script setup name="PaperFullHand">
@@ -49,6 +54,8 @@ import ClassStatisticTable from "@/views/dz/papers/components/plugs/class-statis
 import KnowledgeTree from "@/views/dz/papers/components/plugs/knowledge-tree.vue";
 import QuestionHand from "@/views/dz/papers/components/plugs/question-hand.vue";
 import BuiltPaperList from "@/views/dz/papers/components/plugs/built-paper-list.vue";
+import StudentListDialog from "@/views/dz/papers/components/plugs/student-list-dialog.vue";
+import {useInjectGlobalLoading} from "@/views/hooks/useGlobalLoading.js";
 
 const type = consts.enums.buildType.FullHand
 const {
@@ -63,9 +70,40 @@ const {
 } = useProvidePaperFullCondition(type)
 const {selectedClasses, classList, loadClassStatistic} = useProvidePaperClassStatisticCondition()
 const {knowledgeNode, knowledgeCheckNodes, loadKnowledge} = useProvidePaperKnowledgeCondition()
+const {loading: globalLoading} = useInjectGlobalLoading()
+const questionLoading = computed(() => globalLoading.value)
 const built = ref(null)
 const hasBuiltPaper = computed(() => built.value?.hasPaper)
 
+// 统计字段映射
+const statTypeMap = {
+    'send': '组卷已完成',
+    'total': '总人数',
+    'unfinish': '组卷未完成',
+    'unsend': '未组卷'
+}
+
+// 学生列表弹窗引用
+const studentDialogRef = ref(null)
+
+// 处理统计字段点击
+const handleStatClick = async (row, statType) => {
+    if (!row || !statType || !batchId.value) {
+        console.error('参数错误:', { row, statType, batchId: batchId.value })
+        return
+    }
+    
+    // 构建查询条件参数
+    const queryParams = {}
+    if (conditionArgs.value) {
+        if (conditionArgs.value.examType) queryParams.examType = conditionArgs.value.examType
+        if (conditionArgs.value.subjectId) queryParams.subjectId = conditionArgs.value.subjectId
+    }
+    
+    // 打开学生列表弹窗
+    studentDialogRef.value?.open(row, statType, type, batchId.value, queryParams)
+}
+
 const handleSubmit = async (questions) => {
     // validation
     if (!batchId.value) return ElMessage.error('请选择批次')

+ 34 - 1
back-ui/src/views/dz/papers/components/paper-full-intelligent.vue

@@ -22,7 +22,7 @@
             </el-form>
         </el-col>
         <el-col :span="16">
-            <class-statistic-table />
+            <class-statistic-table @stat-click="handleStatClick"/>
         </el-col>
     </el-row>
     <el-divider />
@@ -35,6 +35,9 @@
         </el-col>
     </el-row>
     <built-paper-list ref="built" @send="handleSubmit" />
+    
+    <!-- 学生列表弹窗 -->
+    <student-list-dialog ref="studentDialogRef" :stat-type-map="statTypeMap"/>
 </template>
 
 <script setup name="PaperFullIntelligent">
@@ -49,6 +52,7 @@ import QuestionIntelligent from "@/views/dz/papers/components/plugs/question-int
 import {ElMessage} from "element-plus";
 import {buildPaperFullIntelligent} from "@/api/dz/papers.js";
 import BuiltPaperList from "@/views/dz/papers/components/plugs/built-paper-list.vue";
+import StudentListDialog from "@/views/dz/papers/components/plugs/student-list-dialog.vue";
 
 const type = consts.enums.buildType.FullIntelligent
 const {
@@ -66,6 +70,35 @@ const {knowledgeNode, knowledgeCheckNodes, loadKnowledge} = useProvidePaperKnowl
 const built = ref(null)
 const hasBuiltPaper = computed(() => built.value?.hasPaper)
 
+// 统计字段映射
+const statTypeMap = {
+    'send': '组卷已完成',
+    'total': '总人数',
+    'unfinish': '组卷未完成',
+    'unsend': '未组卷'
+}
+
+// 学生列表弹窗引用
+const studentDialogRef = ref(null)
+
+// 处理统计字段点击
+const handleStatClick = async (row, statType) => {
+    if (!row || !statType || !batchId.value) {
+        console.error('参数错误:', { row, statType, batchId: batchId.value })
+        return
+    }
+    
+    // 构建查询条件参数
+    const queryParams = {}
+    if (conditionArgs.value) {
+        if (conditionArgs.value.examType) queryParams.examType = conditionArgs.value.examType
+        if (conditionArgs.value.subjectId) queryParams.subjectId = conditionArgs.value.subjectId
+    }
+    
+    // 打开学生列表弹窗
+    studentDialogRef.value?.open(row, statType, type, batchId.value, queryParams)
+}
+
 const handleSubmit = async (qTypes) => {
     // validation
     if (!batchId.value) return ElMessage.error('请选择批次')

+ 18 - 11
back-ui/src/views/dz/papers/components/plugs/question-hand.vue

@@ -32,18 +32,20 @@
         </el-popover>
     </div>
     <el-divider/>
-    <el-empty v-if="total==0"/>
-    <div v-else class="flex flex-col gap-5">
-        <question-content v-for="q in questionList" :question="q" @parse="showParseQuestion=q,showParse=true">
-            <el-button v-if="!hasQuestion(q)" type="primary" icon="plus" class="ml-auto" @click="addQuestion(q)">
-                加入试题篮
-            </el-button>
-            <el-button v-else type="danger" plain icon="delete" class="ml-auto" @click="removeQuestion(q)">移出试题篮
-            </el-button>
-        </question-content>
+    <div v-loading="questionLoading" element-loading-text="加载试题中...">
+        <el-empty v-if="total==0"/>
+        <div v-else class="flex flex-col gap-5">
+            <question-content v-for="q in questionList" :question="q" @parse="showParseQuestion=q,showParse=true">
+                <el-button v-if="!hasQuestion(q)" type="primary" icon="plus" class="ml-auto" @click="addQuestion(q)">
+                    加入试题篮
+                </el-button>
+                <el-button v-else type="danger" plain icon="delete" class="ml-auto" @click="removeQuestion(q)">移出试题篮
+                </el-button>
+            </question-content>
+        </div>
+        <pagination v-show="total>0" :total="total" v-model:page="pageNum" v-model:limit="pageSize"
+                    @pagination="getQuestionList"/>
     </div>
-    <pagination v-show="total>0" :total="total" v-model:page="pageNum" v-model:limit="pageSize"
-                @pagination="getQuestionList"/>
 
     <el-dialog v-model="showParse" append-to-body show-close @close="showParse=false">
         <template #title>ID:{{ showParseQuestion.id }} 试题解析</template>
@@ -61,6 +63,7 @@ import {ElMessage} from "element-plus";
 import {useProvidePaperQuestionCondition} from "@/views/dz/papers/hooks/usePaperQuestionCondition.js";
 import {useInjectPaperBatchCondition} from "@/views/dz/papers/hooks/usePaperBatchCondition.js";
 import {useInjectPaperKnowledgeCondition} from "@/views/dz/papers/hooks/usePaperKnowledgeCondition.js";
+import {useInjectGlobalLoading} from "@/views/hooks/useGlobalLoading.js";
 import QuestionContent from "@/views/components/question-content.vue";
 
 const props = defineProps({
@@ -74,6 +77,7 @@ const showParseQuestion = ref(null)
 
 const {batchId} = useInjectPaperBatchCondition()
 const {knowledgeNode} = useInjectPaperKnowledgeCondition()
+const {loading: globalLoading} = useInjectGlobalLoading()
 const {
     keyword,
     qtpye,
@@ -94,6 +98,9 @@ const {
     clearCart
 } = useProvidePaperQuestionCondition(props.exactMode, true)
 
+// 试题列表加载状态
+const questionLoading = computed(() => globalLoading.value)
+
 const keywordLocal = ref('')
 const confirmKeyword = (val) => keyword.value = keywordLocal.value
 

+ 321 - 0
back-ui/src/views/dz/papers/components/plugs/student-list-dialog.vue

@@ -0,0 +1,321 @@
+<template>
+    <!-- 学生列表弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="1200px" destroy-on-close>
+        <div class="student-table-container">
+            <el-table 
+                v-loading="loading" 
+                :data="studentList" 
+                stripe
+                height="calc(100vh - 350px)"
+                style="width: 100%"
+            >
+                <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="pageNum"
+                    v-model:page-size="pageSize"
+                    :page-sizes="[10, 20, 50, 100]"
+                    :total="total"
+                    layout="total, sizes, prev, pager, next, jumper"
+                    @size-change="handlePageSizeChange"
+                    @current-change="handlePageChange"
+                />
+            </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="StudentListDialog">
+import { ref, getCurrentInstance } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getClassesBuildStatsDetail } from '@/api/dz/papers.js'
+import DictTag from '@/components/DictTag/index.vue'
+
+const { proxy } = getCurrentInstance()
+const { exam_type } = proxy.useDict("exam_type")
+
+// Props
+const props = defineProps({
+    // 统计类型映射,用于显示标题
+    statTypeMap: {
+        type: Object,
+        default: () => ({
+            'send': '组卷已完成',
+            'total': '班级人数',
+            'unexact': '未定向未组卷',
+            'unfinish': '组卷未完成',
+            'unsend': '定向未组卷'
+        })
+    }
+})
+
+// 弹窗状态
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const loading = ref(false)
+
+// 学生列表数据
+const studentList = ref([])
+const pageNum = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+
+// 当前查询参数
+const currentParams = 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 loadStudentList = async () => {
+    if (!currentParams.value) {
+        console.error('参数不完整')
+        return
+    }
+    
+    loading.value = true
+    try {
+        const res = await getClassesBuildStatsDetail(currentParams.value)
+        
+        const allStudents = res.data || []
+        total.value = allStudents.length
+        
+        // 前端分页
+        const start = (pageNum.value - 1) * pageSize.value
+        const end = start + pageSize.value
+        studentList.value = allStudents.slice(start, end)
+    } catch (error) {
+        console.error('加载学生列表失败:', error)
+        ElMessage.error('加载学生列表失败: ' + (error.message || '未知错误'))
+    } finally {
+        loading.value = false
+    }
+}
+
+// 分页大小改变
+const handlePageSizeChange = (size) => {
+    pageSize.value = size
+    pageNum.value = 1
+    loadStudentList()
+}
+
+// 页码改变
+const handlePageChange = (page) => {
+    pageNum.value = page
+    loadStudentList()
+}
+
+// 打开弹窗
+const open = (row, statType, buildType, batchId, queryParams = {}) => {
+    if (!row || !statType || !buildType || !batchId) {
+        console.error('参数错误:', { row, statType, buildType, batchId })
+        return
+    }
+    
+    // 构建查询参数
+    const params = {
+        buildType,
+        batchId,
+        classId: row.classId,
+        statType
+    }
+    
+    // 添加额外的查询条件
+    if (queryParams.examType) params.examType = queryParams.examType
+    if (queryParams.universityId) params.universityId = queryParams.universityId
+    if (queryParams.majorGroup) params.majorGroup = queryParams.majorGroup
+    if (queryParams.majorPlanId) params.majorPlanId = queryParams.majorPlanId
+    
+    currentParams.value = params
+    
+    // 设置标题
+    dialogTitle.value = `${row.className || ''} - ${props.statTypeMap[statType] || statType}`
+    
+    // 重置分页
+    pageNum.value = 1
+    pageSize.value = 10
+    
+    // 显示弹窗并加载数据
+    dialogVisible.value = true
+    loadStudentList()
+}
+
+// 暴露方法给父组件
+defineExpose({
+    open
+})
+</script>
+
+<style scoped>
+.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>
+

+ 36 - 9
back-ui/src/views/dz/papers/hooks/usePaperQuestionCondition.js

@@ -19,7 +19,7 @@ export const useProvidePaperQuestionCondition = function (exactMode, handMode) {
     const questionList = ref([])
 
     const {conditionArgs, conditionData} = exactMode ? useInjectPaperExactCondition() : useInjectPaperFullCondition()
-    const {knowledgeId, knowledgeIds} = useInjectPaperKnowledgeCondition()
+    const {knowledgeId, knowledgeIds, knowledgeNode, knowledgeCheckNodes} = useInjectPaperKnowledgeCondition()
     const loading = useInjectGlobalLoading()
 
     // question cart
@@ -75,18 +75,45 @@ export const useProvidePaperQuestionCondition = function (exactMode, handMode) {
     }
     const clearCart = () => cart.value = []
 
-    // hooks
-    watch([() => conditionArgs.value.subjectId, knowledgeId, () => knowledgeIds.value.toString()], async ([subjectId, knowledgeId, knowledgeIds]) => {
+    // hooks - 监听知识点变化,重新获取题型数据
+    // 监听单个知识点节点和多选知识点节点的变化
+    watch([() => conditionArgs.value.subjectId, knowledgeNode, knowledgeCheckNodes], async ([subjectId, knowledgeNodeVal, knowledgeCheckNodesVal]) => {
         // clean
         qtpye.value = ''
         qTypes.value = []
 
-        if (!subjectId && conditionData.value.subjectList?.length) return
-        if (!knowledgeId && !knowledgeIds) return
-        const query = {subjectId, knowledgeIds: knowledgeId || knowledgeIds}
-        const res = await getPaperQuestionTypes(query)
-        qTypes.value = res.data
-    })
+        // 获取知识点ID:优先使用单个知识点,否则使用多选知识点
+        const currentKnowledgeId = knowledgeNodeVal?.id
+        const currentKnowledgeIds = knowledgeCheckNodesVal?.map(k => k.id) || []
+        
+        // 如果没有知识点,直接返回
+        if (!currentKnowledgeId && currentKnowledgeIds.length === 0) return
+        
+        // 定向模式下,如果subjectId为空,使用默认值11L(与后端逻辑一致)
+        // 全量模式下,必须有subjectId才能继续
+        let finalSubjectId = subjectId
+        if (!finalSubjectId) {
+            if (exactMode) {
+                // 定向模式下,后端会自动设置subjectId为11L
+                finalSubjectId = 11
+            } else {
+                // 全量模式下,必须有subjectId
+                if (conditionData.value.subjectList?.length) return
+            }
+        }
+        
+        // 构建查询参数
+        const knowledgeIdsToUse = currentKnowledgeId ? [currentKnowledgeId] : currentKnowledgeIds
+        const query = {subjectId: finalSubjectId, knowledgeIds: knowledgeIdsToUse}
+        
+        try {
+            const res = await getPaperQuestionTypes(query)
+            qTypes.value = res.data || []
+        } catch (error) {
+            console.error('获取题型列表失败:', error)
+            qTypes.value = []
+        }
+    }, { deep: true, immediate: false })
 
     const questionQuery = computed(() => ({
         pageNum: pageNum.value,

+ 2 - 2
back-ui/src/views/dz/papers/index.vue

@@ -2,7 +2,7 @@
     <div class="app-container" v-loading="loading">
         <el-tabs v-model="currentTab">
             <el-tab-pane v-for="t in tabs" :label="t.label" :name="t.name">
-                <component :is="t.page" v-if="t.visited"/>
+                <component :is="t.page" v-if="t.visited" :key="t.name"/>
             </el-tab-pane>
         </el-tabs>
     </div>
@@ -41,7 +41,7 @@ const tabs = ref([{
 const currentTab = ref('ExactIntelligent')
 
 watch(currentTab, tabName => {
-    // 通过visited=true 延迟渲染
+    // 通过visited=true 延迟渲染,组件只创建一次,切换时不会重新创建
     const tab = tabs.value.find(t => t.name == tabName)
     if (tab) tab.visited = true
 })

+ 2 - 1
ie-admin/src/main/java/com/ruoyi/web/controller/learn/LearnQuestionsController.java

@@ -5,6 +5,7 @@ import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletResponse;
 
 import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.enums.CardAction;
 import com.ruoyi.enums.QuestionType;
 import io.swagger.annotations.Api;
@@ -24,7 +25,7 @@ import com.ruoyi.common.core.page.TableDataInfo;
 
 /**
  * 试题Controller
- * 
+ *
  * @author ruoyi
  * @date 2025-09-18
  */

+ 9 - 1
ie-admin/src/main/java/com/ruoyi/web/controller/learn/LearnTeacherController.java

@@ -121,12 +121,20 @@ public class LearnTeacherController extends BaseController {
             @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)
+            @ApiParam("统计类型:send/total/unexact/unfinish/unsend") @RequestParam String statType,
+            @ApiParam("考生类型") @RequestParam(required = false) String examType,
+            @ApiParam("院校ID") @RequestParam(required = false) Long universityId,
+            @ApiParam("专业组") @RequestParam(required = false) String majorGroup,
+            @ApiParam("专业计划ID") @RequestParam(required = false) Long majorPlanId)
     {
         TestPaperVO.TestPaperBuildReq req = new TestPaperVO.TestPaperBuildReq();
         req.setBuildType(buildType);
         req.setBatchId(batchId);
         req.setClassId(classId);
+        req.setExamType(examType);
+        req.setUniversityId(universityId);
+        req.setMajorGroup(majorGroup);
+        req.setMajorPlanId(majorPlanId);
         List<JSONObject> list = learnTeacherService.getClassesBuildStatsDetail(req, getTeacherId(), statType);
         return AjaxResult.success(list);
     }

+ 2 - 6
ie-admin/src/main/java/com/ruoyi/web/service/LearnTeacherService.java

@@ -91,9 +91,7 @@ public class LearnTeacherService {
         if(buildType.startsWith("Exact")) {
             req.setSubjectId(11L);
         }
-        if("ExactIntelligent".equals(buildType)){
-            return dzClassesMapper.selectClassesDirectedBuildStats(req.toMap());
-        } else if("ExactHand".equals(buildType)){
+        if("ExactIntelligent".equals(buildType)||"ExactHand".equals(buildType)){
             return dzClassesMapper.selectClassesDirectedBuildStats(req.toMap());
         }
         return dzClassesMapper.selectClassesBuildStats(req.toMap());
@@ -105,9 +103,7 @@ public class LearnTeacherService {
         if(buildType.startsWith("Exact")) {
             req.setSubjectId(11L);
         }
-        if("ExactIntelligent".equals(buildType)){
-            return dzClassesMapper.selectClassesDirectedBuildStats(req.toMap());
-        } else if("ExactHand".equals(buildType)){
+        if("ExactIntelligent".equals(buildType)||"ExactHand".equals(buildType)){
             return dzClassesMapper.selectClassesDirectedBuildStats(req.toMap());
         }
         return dzClassesMapper.selectClassesBuildStats(req.toMap());

+ 31 - 0
ie-system/src/main/java/com/ruoyi/enums/QuestionType.java

@@ -1,5 +1,7 @@
 package com.ruoyi.enums;
 
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
 import com.google.common.collect.Maps;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
@@ -17,6 +19,34 @@ public enum QuestionType {
     private final String title;
 
     private static final Map<String, QuestionType> valMap = Maps.newHashMap();
+    private static final Map<Integer, QuestionType> intValMap = Maps.newHashMap();
+
+    @JsonValue
+    public Integer getVal() {
+        return val;
+    }
+
+    @JsonCreator
+    public static QuestionType fromValue(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Integer) {
+            return intValMap.get((Integer) value);
+        }
+        if (value instanceof String) {
+            String str = (String) value;
+            // 尝试作为数字字符串解析
+            try {
+                Integer intVal = Integer.parseInt(str);
+                return intValMap.get(intVal);
+            } catch (NumberFormatException e) {
+                // 如果不是数字,尝试作为名称或标题查找
+                return valMap.get(str);
+            }
+        }
+        return null;
+    }
 
     public static QuestionType of(Integer vol) {
         return valMap.get(vol.toString());
@@ -32,6 +62,7 @@ public enum QuestionType {
             valMap.put(t.val.toString(), t);
             valMap.put(t.title, t);
             valMap.put(t.name(), t);
+            intValMap.put(t.val, t);
         });
     }
 }

+ 1 - 1
ie-system/src/main/java/com/ruoyi/learn/domain/TestPaperVO.java

@@ -74,7 +74,7 @@ public class TestPaperVO {
             if(CollectionUtils.isNotEmpty(types)) {
                 List<TestPaperVO.TypeDef> typeDefList = Lists.newArrayList();
                 for(TypeDef2 typeDef2 : types) {
-                    total += typeDef2.getCount();
+                    total += null==typeDef2.getCount()?0:typeDef2.getCount();
                     TestPaperVO.TypeDef typeDef = new TestPaperVO.TypeDef();
                     BeanUtils.copyProperties(typeDef2, typeDef);
                     typeDef.setType(typeDef.getTitle());

+ 17 - 0
ie-system/src/main/java/com/ruoyi/learn/service/impl/LearnQuestionsServiceImpl.java

@@ -6,6 +6,7 @@ import java.util.stream.Collectors;
 import com.ruoyi.common.utils.DateUtils;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.system.service.ISysConfigService;
+import com.ruoyi.enums.QuestionType;
 import org.apache.commons.collections4.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -52,6 +53,22 @@ public class LearnQuestionsServiceImpl implements ILearnQuestionsService
     @Override
     public List<LearnQuestions> selectLearnQuestionsList(LearnQuestions learnQuestions)
     {
+        // 判断qtpye是否为数字,如果是则使用枚举转换
+        if (StringUtils.isNotEmpty(learnQuestions.getQtpye())){
+            String qtpye = learnQuestions.getQtpye();
+            // 判断是否为数字
+            if (StringUtils.isNumeric(qtpye)) {
+                try {
+                    QuestionType questionType = QuestionType.of(qtpye);
+                    if (questionType != null) {
+                        // 设置qtpye为枚举的title
+                        learnQuestions.setQtpye(questionType.getTitle());
+                    }
+                } catch (Exception e) {
+                    // 如果转换失败,保持原值
+                }
+            }
+        }
         List<LearnQuestions> questions = learnQuestionsMapper.selectLearnQuestionsList(learnQuestions);
         fillQuestionInfo(questions,false);
         return questions;

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

@@ -134,6 +134,9 @@
             tc.`teacher_id` = #{teacherId}
             <if test="classId != null"> AND ls.`class_id` = #{classId}</if>
             <if test="batchId != null"> AND lt.`batch_id` = #{batchId}</if>
+            <if test="universityId != null"> AND ls.`university_id` = #{universityId}</if>
+            <if test="majorPlanId != null"> AND ls.`major_plan_id` = #{majorPlanId}</if>
+            <if test="examType != null and examType != ''"> AND u.`exam_type` = #{examType}</if>
             <!-- 根据统计类型过滤 -->
             <choose>
                 <!-- send: 组卷已完成 -->

+ 58 - 1
ie-system/src/main/resources/mapper/learn/LearnQuestionsMapper.xml

@@ -73,7 +73,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="qtpye != null  and qtpye != ''"> and qtpye = #{qtpye}</if>
             <if test="subjectId != null "> and subjectId = #{subjectId}</if>
             <if test="paperId != null "> and paperId = #{paperId}</if>
-            <if test="knowledgeId != null "> and knowledgeId = #{knowledgeId}</if>
+            <if test="knowledgeId != null ">
+                and EXISTS (
+                    SELECT 1
+                    FROM learn_knowledge_question lkq
+                    WHERE lkq.question_id = learn_questions.id
+                      AND (
+                          lkq.knowledge_id = #{knowledgeId}
+                          OR EXISTS (
+                              SELECT 1
+                              FROM learn_knowledge_tree lkt
+                              WHERE lkt.id = lkq.knowledge_id
+                                AND (lkt.pid = #{knowledgeId} OR FIND_IN_SET(#{knowledgeId}, lkt.ancestors) > 0)
+                          )
+                      )
+                )
+            </if>
             <if test="diff != null "> and diff = #{diff}</if>
             <if test="similarity != null "> and similarity = #{similarity}</if>
             <if test="parse != null  and parse != ''"> and parse = #{parse}</if>
@@ -361,11 +376,53 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         SELECT q.`qtpye`, COUNT(*) number FROM `learn_questions` q
         <where>
             <if test="null != subjectId"> AND q.`subjectId` = #{subjectId}</if>
+            <!--
             <if test="null != knowledgeIds"> AND q.`knowledgeId` IN <foreach item="id" collection="knowledgeIds" open="(" separator="," close=")">#{id}</foreach></if>
+            -->
+            <if test="knowledgeIds != null and knowledgeIds != '' ">
+                AND EXISTS (
+                SELECT 1
+                FROM learn_knowledge_question lkq
+                WHERE lkq.question_id = q.id
+                AND (
+                lkq.knowledge_id = #{knowledgeId}
+                OR EXISTS (
+                SELECT 1
+                FROM learn_knowledge_tree lkt
+                WHERE lkt.id = lkq.knowledge_id
+                AND (lkt.pid = #{knowledgeId} OR FIND_IN_SET(#{knowledgeId}, lkt.ancestors) > 0)
+                )
+                )
+                )
+            </if>
         </where>
          GROUP BY q.`qtpye`
     </select>
 
+    <select id="getQtypes" resultMap="LearnQuestionsResult">
+        SELECT q.`qtpye`, COUNT(*) number FROM `learn_questions` q
+        <where>
+            <if test="subjectId != null"> AND q.`subjectId` = #{subjectId}</if>
+            <if test="knowledgeIds != null and knowledgeIds != '' ">
+                AND EXISTS (
+                SELECT 1
+                FROM learn_knowledge_question lkq
+                WHERE lkq.question_id = q.id
+                AND (
+                lkq.knowledge_id = #{knowledgeId}
+                OR EXISTS (
+                SELECT 1
+                FROM learn_knowledge_tree lkt
+                WHERE lkt.id = lkq.knowledge_id
+                AND (lkt.pid = #{knowledgeId} OR FIND_IN_SET(#{knowledgeId}, lkt.ancestors) > 0)
+                )
+                )
+                )
+            </if>
+        </where>
+        GROUP BY q.`qtpye`
+    </select>
+
 
 
 </mapper>

+ 319 - 0
sql/add_indexes_for_learn_dz.sql

@@ -0,0 +1,319 @@
+-- ============================================
+-- 索引优化 SQL 脚本
+-- 基于 learn 和 dz 目录下的 Mapper XML 分析生成
+-- ============================================
+
+-- ============================================
+-- learn 目录相关表的索引
+-- ============================================
+
+-- learn_answer 表索引
+CREATE INDEX idx_learn_answer_examinee_id ON learn_answer(examinee_id);
+CREATE INDEX idx_learn_answer_student_id ON learn_answer(student_id);
+CREATE INDEX idx_learn_answer_question_id ON learn_answer(question_id);
+CREATE INDEX idx_learn_answer_knowledge_id ON learn_answer(knowledge_id);
+CREATE INDEX idx_learn_answer_state ON learn_answer(state);
+CREATE INDEX idx_learn_answer_create_time ON learn_answer(create_time);
+CREATE INDEX idx_learn_answer_student_state ON learn_answer(student_id, state);
+CREATE INDEX idx_learn_answer_examinee_state ON learn_answer(examinee_id, state);
+CREATE INDEX idx_learn_answer_student_knowledge ON learn_answer(student_id, knowledge_id, state);
+CREATE INDEX idx_learn_answer_student_time ON learn_answer(student_id, create_time);
+
+-- learn_examinee 表索引
+CREATE INDEX idx_learn_examinee_student_id ON learn_examinee(student_id);
+CREATE INDEX idx_learn_examinee_paper_type ON learn_examinee(paper_type);
+CREATE INDEX idx_learn_examinee_paper_id ON learn_examinee(paper_id);
+CREATE INDEX idx_learn_examinee_state ON learn_examinee(state);
+CREATE INDEX idx_learn_examinee_end_time ON learn_examinee(end_time);
+CREATE INDEX idx_learn_examinee_student_paper_type ON learn_examinee(student_id, paper_type);
+CREATE INDEX idx_learn_examinee_student_state ON learn_examinee(student_id, state);
+CREATE INDEX idx_learn_examinee_paper_type_state ON learn_examinee(paper_type, state);
+CREATE INDEX idx_learn_examinee_paper_ids ON learn_examinee(paper_type, paper_id);
+
+-- learn_paper 表索引
+CREATE INDEX idx_learn_paper_subject_id ON learn_paper(subjectId);
+CREATE INDEX idx_learn_paper_paper_type ON learn_paper(paperType);
+CREATE INDEX idx_learn_paper_direct_key ON learn_paper(direct_key);
+CREATE INDEX idx_learn_paper_year ON learn_paper(year);
+CREATE INDEX idx_learn_paper_status ON learn_paper(status);
+CREATE INDEX idx_learn_paper_subject_type ON learn_paper(subjectId, paperType);
+CREATE INDEX idx_learn_paper_direct_key_prefix ON learn_paper(direct_key(50));
+
+-- learn_paper_question 表索引
+CREATE INDEX idx_learn_paper_question_paper_id ON learn_paper_question(paper_id);
+CREATE INDEX idx_learn_paper_question_question_id ON learn_paper_question(question_id);
+CREATE INDEX idx_learn_paper_question_knowledge_id ON learn_paper_question(knowledge_id);
+CREATE INDEX idx_learn_paper_question_seq ON learn_paper_question(paper_id, seq);
+CREATE INDEX idx_learn_paper_question_paper_seq ON learn_paper_question(paper_id, question_id, seq);
+
+-- learn_paper_real 表索引
+CREATE INDEX idx_learn_paper_real_subject_id ON learn_paper_real(subjectId);
+CREATE INDEX idx_learn_paper_real_paper_type ON learn_paper_real(paperType);
+CREATE INDEX idx_learn_paper_real_year ON learn_paper_real(year);
+CREATE INDEX idx_learn_paper_real_online ON learn_paper_real(online);
+
+-- learn_questions 表索引
+CREATE INDEX idx_learn_questions_subject_id ON learn_questions(subjectId);
+CREATE INDEX idx_learn_questions_knowledge_id ON learn_questions(knowledgeId);
+CREATE INDEX idx_learn_questions_qtpye ON learn_questions(qtpye);
+CREATE INDEX idx_learn_questions_paper_id ON learn_questions(paperId);
+CREATE INDEX idx_learn_questions_subject_knowledge ON learn_questions(subjectId, knowledgeId);
+CREATE INDEX idx_learn_questions_knowledge_type ON learn_questions(knowledgeId, qtpye);
+CREATE INDEX idx_learn_questions_md5 ON learn_questions(md5);
+CREATE INDEX idx_learn_questions_md52 ON learn_questions(md52);
+
+-- learn_knowledge_question 表索引
+CREATE INDEX idx_learn_knowledge_question_knowledge_id ON learn_knowledge_question(knowledge_id);
+CREATE INDEX idx_learn_knowledge_question_question_id ON learn_knowledge_question(question_id);
+CREATE INDEX idx_learn_knowledge_question_type ON learn_knowledge_question(type);
+CREATE INDEX idx_learn_knowledge_question_knowledge_type ON learn_knowledge_question(knowledge_id, type);
+
+-- learn_knowledge_tree 表索引
+CREATE INDEX idx_learn_knowledge_tree_pid ON learn_knowledge_tree(pid);
+CREATE INDEX idx_learn_knowledge_tree_subject_id ON learn_knowledge_tree(subjectId);
+CREATE INDEX idx_learn_knowledge_tree_ancestors ON learn_knowledge_tree(ancestors(100));
+CREATE INDEX idx_learn_knowledge_tree_subject_pid ON learn_knowledge_tree(subjectId, pid);
+
+-- learn_knowledge_course 表索引
+CREATE INDEX idx_learn_knowledge_course_pid ON learn_knowledge_course(pid);
+CREATE INDEX idx_learn_knowledge_course_ancestors ON learn_knowledge_course(ancestors(100));
+
+-- learn_student 表索引
+CREATE INDEX idx_learn_student_student_id ON learn_student(student_id);
+CREATE INDEX idx_learn_student_class_id ON learn_student(class_id);
+CREATE INDEX idx_learn_student_university_id ON learn_student(university_id);
+CREATE INDEX idx_learn_student_major_plan_id ON learn_student(major_plan_id);
+CREATE INDEX idx_learn_student_direct_key ON learn_student(direct_key);
+CREATE INDEX idx_learn_student_school_id ON learn_student(school_id);
+CREATE INDEX idx_learn_student_campus_id ON learn_student(campus_id);
+CREATE INDEX idx_learn_student_major_group ON learn_student(major_group);
+CREATE INDEX idx_learn_student_class_university ON learn_student(class_id, university_id);
+
+-- learn_test 表索引
+CREATE INDEX idx_learn_test_year ON learn_test(year);
+CREATE INDEX idx_learn_test_creator_id ON learn_test(creator_id);
+CREATE INDEX idx_learn_test_year_creator ON learn_test(year, creator_id);
+
+-- learn_test_paper 表索引
+CREATE INDEX idx_learn_test_paper_batch_id ON learn_test_paper(batch_id);
+CREATE INDEX idx_learn_test_paper_build_type ON learn_test_paper(build_type);
+CREATE INDEX idx_learn_test_paper_subject_id ON learn_test_paper(subject_id);
+CREATE INDEX idx_learn_test_paper_teacher_id ON learn_test_paper(teacher_id);
+CREATE INDEX idx_learn_test_paper_university_id ON learn_test_paper(university_id);
+CREATE INDEX idx_learn_test_paper_direct_key ON learn_test_paper(direct_key);
+CREATE INDEX idx_learn_test_paper_batch_build_subject ON learn_test_paper(batch_id, build_type, subject_id);
+CREATE INDEX idx_learn_test_paper_batch_university ON learn_test_paper(batch_id, university_id);
+
+-- learn_test_student 表索引
+CREATE INDEX idx_learn_test_student_batch_id ON learn_test_student(batch_id);
+CREATE INDEX idx_learn_test_student_student_id ON learn_test_student(student_id);
+CREATE INDEX idx_learn_test_student_build_type ON learn_test_student(build_type);
+CREATE INDEX idx_learn_test_student_subject_id ON learn_test_student(subject_id);
+CREATE INDEX idx_learn_test_student_paper_id ON learn_test_student(paper_id);
+CREATE INDEX idx_learn_test_student_status ON learn_test_student(status);
+CREATE INDEX idx_learn_test_student_examinee_id ON learn_test_student(examinee_id);
+CREATE INDEX idx_learn_test_student_class_id ON learn_test_student(class_id);
+CREATE INDEX idx_learn_test_student_batch_student ON learn_test_student(batch_id, student_id);
+CREATE INDEX idx_learn_test_student_batch_build_subject ON learn_test_student(batch_id, build_type, subject_id);
+CREATE INDEX idx_learn_test_student_student_batch ON learn_test_student(student_id, batch_id);
+CREATE INDEX idx_learn_test_student_student_status ON learn_test_student(student_id, status);
+
+-- learn_plan 表索引
+CREATE INDEX idx_learn_plan_student_id ON learn_plan(studentId);
+CREATE INDEX idx_learn_plan_status ON learn_plan(status);
+
+-- learn_plan_study 表索引
+CREATE INDEX idx_learn_plan_study_student_id ON learn_plan_study(student_id);
+CREATE INDEX idx_learn_plan_study_plan_id ON learn_plan_study(plan_id);
+CREATE INDEX idx_learn_plan_study_report_date ON learn_plan_study(report_date);
+CREATE INDEX idx_learn_plan_study_month_seq ON learn_plan_study(month_seq);
+CREATE INDEX idx_learn_plan_study_student_plan ON learn_plan_study(student_id, plan_id);
+CREATE INDEX idx_learn_plan_study_student_date ON learn_plan_study(student_id, report_date);
+CREATE INDEX idx_learn_plan_study_plan_month ON learn_plan_study(plan_id, month_seq);
+
+-- learn_wrong_book 表索引
+CREATE INDEX idx_learn_wrong_book_student_id ON learn_wrong_book(student_id);
+CREATE INDEX idx_learn_wrong_book_question_id ON learn_wrong_book(question_id);
+CREATE INDEX idx_learn_wrong_book_subject_id ON learn_wrong_book(subject_id);
+CREATE INDEX idx_learn_wrong_book_paper_id ON learn_wrong_book(paper_id);
+CREATE INDEX idx_learn_wrong_book_knownledge_id ON learn_wrong_book(knownledge_id);
+CREATE INDEX idx_learn_wrong_book_created_time ON learn_wrong_book(created_time);
+CREATE INDEX idx_learn_wrong_book_student_question ON learn_wrong_book(student_id, question_id);
+CREATE INDEX idx_learn_wrong_book_student_subject ON learn_wrong_book(student_id, subject_id);
+CREATE INDEX idx_learn_wrong_book_student_time ON learn_wrong_book(student_id, created_time);
+
+-- learn_wrong_detail 表索引
+CREATE INDEX idx_learn_wrong_detail_wrong_id ON learn_wrong_detail(wrong_id);
+CREATE INDEX idx_learn_wrong_detail_student_id ON learn_wrong_detail(student_id);
+CREATE INDEX idx_learn_wrong_detail_examinee_id ON learn_wrong_detail(examinee_id);
+CREATE INDEX idx_learn_wrong_detail_paper_id ON learn_wrong_detail(paper_id);
+
+-- learn_question_correct 表索引
+CREATE INDEX idx_learn_question_correct_question_id ON learn_question_correct(question_id);
+CREATE INDEX idx_learn_question_correct_user_id ON learn_question_correct(user_id);
+CREATE INDEX idx_learn_question_correct_state ON learn_question_correct(state);
+CREATE INDEX idx_learn_question_correct_question_user ON learn_question_correct(question_id, user_id);
+
+-- learn_culture_knowledge 表索引
+CREATE INDEX idx_learn_culture_knowledge_year ON learn_culture_knowledge(year);
+CREATE INDEX idx_learn_culture_knowledge_university_id ON learn_culture_knowledge(university_id);
+CREATE INDEX idx_learn_culture_knowledge_year_university ON learn_culture_knowledge(year, university_id);
+
+-- learn_directed_knowledge 表索引
+CREATE INDEX idx_learn_directed_knowledge_year ON learn_directed_knowledge(year);
+CREATE INDEX idx_learn_directed_knowledge_university_id ON learn_directed_knowledge(university_id);
+CREATE INDEX idx_learn_directed_knowledge_direct_key ON learn_directed_knowledge(direct_key);
+CREATE INDEX idx_learn_directed_knowledge_year_university ON learn_directed_knowledge(year, university_id);
+
+-- ============================================
+-- dz 目录相关表的索引
+-- ============================================
+
+-- dz_agent 表索引
+CREATE INDEX idx_dz_agent_agent_id ON dz_agent(agent_id);
+CREATE INDEX idx_dz_agent_user_id ON dz_agent(user_id);
+CREATE INDEX idx_dz_agent_dept_id ON dz_agent(dept_id);
+CREATE INDEX idx_dz_agent_parent_id ON dz_agent(parent_id);
+CREATE INDEX idx_dz_agent_parent_dept ON dz_agent(parent_id, dept_id);
+
+-- dz_cards 表索引
+CREATE INDEX idx_dz_cards_card_no ON dz_cards(card_no);
+CREATE INDEX idx_dz_cards_card_id ON dz_cards(card_id);
+CREATE INDEX idx_dz_cards_agent_id ON dz_cards(agent_id);
+CREATE INDEX idx_dz_cards_leaf_agent_id ON dz_cards(leaf_agent_id);
+CREATE INDEX idx_dz_cards_dept_id ON dz_cards(dept_id);
+CREATE INDEX idx_dz_cards_school_id ON dz_cards(school_id);
+CREATE INDEX idx_dz_cards_class_id ON dz_cards(class_id);
+CREATE INDEX idx_dz_cards_campus_id ON dz_cards(campus_id);
+CREATE INDEX idx_dz_cards_campus_class_id ON dz_cards(campus_class_id);
+CREATE INDEX idx_dz_cards_distribute_status ON dz_cards(distribute_status);
+CREATE INDEX idx_dz_cards_pay_status ON dz_cards(pay_status);
+CREATE INDEX idx_dz_cards_type ON dz_cards(type);
+CREATE INDEX idx_dz_cards_status ON dz_cards(status);
+CREATE INDEX idx_dz_cards_distribute_time ON dz_cards(distribute_time);
+CREATE INDEX idx_dz_cards_open_time ON dz_cards(open_time);
+CREATE INDEX idx_dz_cards_agent_leaf ON dz_cards(agent_id, leaf_agent_id);
+CREATE INDEX idx_dz_cards_dept_type ON dz_cards(dept_id, type);
+CREATE INDEX idx_dz_cards_type_status ON dz_cards(type, distribute_status);
+CREATE INDEX idx_dz_cards_type_pay_status ON dz_cards(type, pay_status);
+CREATE INDEX idx_dz_cards_school_class ON dz_cards(school_id, class_id);
+CREATE INDEX idx_dz_cards_campus_class ON dz_cards(campus_id, campus_class_id);
+CREATE INDEX idx_dz_cards_distribute_time_date ON dz_cards(DATE(distribute_time));
+
+-- dz_cards_open 表索引
+CREATE INDEX idx_dz_cards_open_agent_id ON dz_cards_open(agent_id);
+CREATE INDEX idx_dz_cards_open_dept_id ON dz_cards_open(dept_id);
+CREATE INDEX idx_dz_cards_open_school_id ON dz_cards_open(school_id);
+CREATE INDEX idx_dz_cards_open_status ON dz_cards_open(status);
+CREATE INDEX idx_dz_cards_open_card_type ON dz_cards_open(card_type);
+CREATE INDEX idx_dz_cards_open_start_end ON dz_cards_open(start_no, end_no);
+
+-- dz_classes 表索引
+CREATE INDEX idx_dz_classes_class_id ON dz_classes(class_id);
+CREATE INDEX idx_dz_classes_school_id ON dz_classes(school_id);
+CREATE INDEX idx_dz_classes_dept_id ON dz_classes(dept_id);
+CREATE INDEX idx_dz_classes_year ON dz_classes(year);
+CREATE INDEX idx_dz_classes_school_year ON dz_classes(school_id, year);
+CREATE INDEX idx_dz_classes_school_name ON dz_classes(school_id, name);
+CREATE INDEX idx_dz_classes_dept_school ON dz_classes(dept_id, school_id);
+
+-- dz_school 表索引
+CREATE INDEX idx_dz_school_id ON dz_school(id);
+CREATE INDEX idx_dz_school_dept_id ON dz_school(dept_id);
+CREATE INDEX idx_dz_school_location ON dz_school(location);
+CREATE INDEX idx_dz_school_exam_types ON dz_school(exam_types(50));
+CREATE INDEX idx_dz_school_dept_location ON dz_school(dept_id, location);
+
+-- dz_teacher 表索引
+CREATE INDEX idx_dz_teacher_teacher_id ON dz_teacher(teacher_id);
+CREATE INDEX idx_dz_teacher_user_id ON dz_teacher(user_id);
+CREATE INDEX idx_dz_teacher_dept_id ON dz_teacher(dept_id);
+CREATE INDEX idx_dz_teacher_agent_id ON dz_teacher(agent_id);
+CREATE INDEX idx_dz_teacher_school_id ON dz_teacher(school_id);
+CREATE INDEX idx_dz_teacher_campus_id ON dz_teacher(campus_id);
+CREATE INDEX idx_dz_teacher_agent_school ON dz_teacher(agent_id, school_id);
+CREATE INDEX idx_dz_teacher_dept_agent ON dz_teacher(dept_id, agent_id);
+
+-- dz_teacher_class 表索引
+CREATE INDEX idx_dz_teacher_class_teacher_id ON dz_teacher_class(teacher_id);
+CREATE INDEX idx_dz_teacher_class_class_id ON dz_teacher_class(class_id);
+CREATE INDEX idx_dz_teacher_class_school_id ON dz_teacher_class(school_id);
+CREATE INDEX idx_dz_teacher_class_out_date ON dz_teacher_class(out_date);
+CREATE INDEX idx_dz_teacher_class_teacher_class ON dz_teacher_class(teacher_id, class_id);
+CREATE INDEX idx_dz_teacher_class_teacher_out_date ON dz_teacher_class(teacher_id, out_date);
+CREATE INDEX idx_dz_teacher_class_class_out_date ON dz_teacher_class(class_id, out_date);
+
+-- dz_subject 表索引
+CREATE INDEX idx_dz_subject_subject_id ON dz_subject(subject_id);
+CREATE INDEX idx_dz_subject_sort ON dz_subject(sort);
+CREATE INDEX idx_dz_subject_locations ON dz_subject(locations(50));
+CREATE INDEX idx_dz_subject_exam_types ON dz_subject(exam_types(50));
+CREATE INDEX idx_dz_subject_sort_subject ON dz_subject(sort, subject_id);
+
+-- dz_control 表索引
+CREATE INDEX idx_dz_control_location ON dz_control(location);
+CREATE INDEX idx_dz_control_is_valid ON dz_control(is_valid);
+CREATE INDEX idx_dz_control_location_valid ON dz_control(location, is_valid);
+
+-- dz_payment_orders 表索引
+CREATE INDEX idx_dz_payment_orders_card_id ON dz_payment_orders(cardId);
+CREATE INDEX idx_dz_payment_orders_card_no ON dz_payment_orders(cardNo);
+CREATE INDEX idx_dz_payment_orders_code ON dz_payment_orders(code);
+CREATE INDEX idx_dz_payment_orders_out_trade_no ON dz_payment_orders(outTradeNo);
+CREATE INDEX idx_dz_payment_orders_status ON dz_payment_orders(status);
+CREATE INDEX idx_dz_payment_orders_create_time ON dz_payment_orders(createTime);
+CREATE INDEX idx_dz_payment_orders_pay_time ON dz_payment_orders(payTime);
+CREATE INDEX idx_dz_payment_orders_customer_code ON dz_payment_orders(customerCode);
+
+-- dz_select_subject 表索引
+CREATE INDEX idx_dz_select_subject_group_id ON dz_select_subject(group_id);
+CREATE INDEX idx_dz_select_subject_rank ON dz_select_subject(rank);
+
+-- ============================================
+-- 关联表和外键相关索引
+-- ============================================
+
+-- sys_user 表相关索引(用于 JOIN 查询)
+-- 注意:如果 sys_user 表不在当前数据库,请根据实际情况调整
+-- CREATE INDEX idx_sys_user_card_id ON sys_user(card_id);
+-- CREATE INDEX idx_sys_user_exam_type ON sys_user(exam_type);
+-- CREATE INDEX idx_sys_user_user_type ON sys_user(user_type);
+-- CREATE INDEX idx_sys_user_user_type_id ON sys_user(user_type_id);
+
+-- b_customer_video_watches 表相关索引(用于 JOIN 查询)
+-- 注意:如果该表不在当前数据库,请根据实际情况调整
+-- CREATE INDEX idx_b_customer_video_watches_customer_code ON b_customer_video_watches(customerCode);
+-- CREATE INDEX idx_b_customer_video_watches_time ON b_customer_video_watches(time);
+-- CREATE INDEX idx_b_customer_video_watches_customer_time ON b_customer_video_watches(customerCode, time);
+
+-- mxjb_question_collection 表相关索引(用于 JOIN 查询)
+-- 注意:如果该表不在当前数据库,请根据实际情况调整
+-- CREATE INDEX idx_mxjb_question_collection_user_id ON mxjb_question_collection(user_id);
+-- CREATE INDEX idx_mxjb_question_collection_question_id ON mxjb_question_collection(question_id);
+-- CREATE INDEX idx_mxjb_question_collection_user_question ON mxjb_question_collection(user_id, question_id);
+
+-- ============================================
+-- 复合索引说明
+-- ============================================
+-- 1. 复合索引的顺序很重要,应该将选择性高的字段放在前面
+-- 2. 对于经常一起使用的 WHERE 条件,创建复合索引可以提高查询效率
+-- 3. 对于 JOIN 操作,在关联字段上创建索引可以显著提高性能
+-- 4. 对于 ORDER BY 和 GROUP BY 操作,相应的字段索引也有帮助
+-- 5. 对于日期范围查询,在日期字段上创建索引很重要
+-- 6. 对于 LIKE 查询,如果使用前缀匹配(如 'prefix%'),索引仍然有效
+-- 7. 对于字符串字段的部分索引(如 direct_key(50)),可以减少索引大小
+
+-- ============================================
+-- 注意事项
+-- ============================================
+-- 1. 在生产环境执行前,请先在测试环境验证
+-- 2. 创建索引会占用存储空间,并可能影响 INSERT/UPDATE/DELETE 性能
+-- 3. 建议在业务低峰期执行索引创建
+-- 4. 对于大表,索引创建可能需要较长时间
+-- 5. 可以使用 ALTER TABLE ... ADD INDEX 语法,如果索引已存在会报错
+-- 6. 建议使用 SHOW INDEX FROM table_name 检查现有索引,避免重复创建
+
+
+
+
+

+ 18 - 0
sql/add_study_time_indexes.sql

@@ -0,0 +1,18 @@
+-- 为学习相关表的时间字段添加索引
+-- 用于优化卡管理页面的学习时间筛选查询
+
+-- 为 learn_answer 表的 create_time 字段添加索引
+CREATE INDEX IF NOT EXISTS idx_learn_answer_create_time ON learn_answer(create_time);
+
+-- 为 learn_examinee 表的 end_time 字段添加索引
+CREATE INDEX IF NOT EXISTS idx_learn_examinee_end_time ON learn_examinee(end_time);
+
+-- 为 b_customer_video_watches 表的 time 字段添加索引
+CREATE INDEX IF NOT EXISTS idx_customer_video_watches_time ON b_customer_video_watches(time);
+
+
+
+
+
+
+

+ 187 - 0
sql/update_learn_knowledge_tree_ancestors.sql

@@ -0,0 +1,187 @@
+-- ============================================
+-- 填充 learn_knowledge_tree 表的 ancestors 字段
+-- ============================================
+-- 说明:ancestors 字段存储从根节点到当前节点的所有祖先ID,用逗号分隔
+-- 例如:如果节点层级是 1 -> 2 -> 3,那么节点3的ancestors应该是 "1,2"
+-- ============================================
+
+-- 方法1:使用存储过程(推荐,兼容MySQL 5.7+)
+-- 这个方法会递归查找所有节点的祖先并更新ancestors字段
+
+-- ============================================
+-- 方法2:使用递归CTE(MySQL 8.0+推荐,性能更好)
+-- ============================================
+
+-- 第一步:先清空所有ancestors字段(如果需要重新生成)
+-- UPDATE learn_knowledge_tree SET ancestors = '';
+
+-- 第二步:使用递归CTE更新ancestors字段
+WITH RECURSIVE knowledge_tree_path AS (
+    -- 基础查询:根节点(pid为0或NULL的节点)
+    SELECT 
+        id,
+        pid,
+        CAST('' AS CHAR(1000)) AS ancestors_path,
+        0 AS depth
+    FROM learn_knowledge_tree
+    WHERE pid IS NULL OR pid = 0
+    
+    UNION ALL
+    
+    -- 递归查询:子节点
+    SELECT 
+        t.id,
+        t.pid,
+        CASE 
+            WHEN tp.ancestors_path = '' THEN CAST(tp.id AS CHAR(1000))
+            ELSE CONCAT(tp.ancestors_path, ',', tp.id)
+        END AS ancestors_path,
+        tp.depth + 1
+    FROM learn_knowledge_tree t
+    INNER JOIN knowledge_tree_path tp ON t.pid = tp.id
+    WHERE tp.depth < 20  -- 防止无限递归,假设最多20层
+)
+UPDATE learn_knowledge_tree lkt
+INNER JOIN knowledge_tree_path ktp ON lkt.id = ktp.id
+SET lkt.ancestors = ktp.ancestors_path;
+
+-- ============================================
+-- 方法3:简单循环更新(适合小数据量,MySQL 5.7及以下)
+-- ============================================
+
+DELIMITER $$
+
+DROP PROCEDURE IF EXISTS update_knowledge_tree_ancestors$$
+
+CREATE PROCEDURE update_knowledge_tree_ancestors()
+BEGIN
+    DECLARE done INT DEFAULT FALSE;
+    DECLARE v_id BIGINT;
+    DECLARE v_pid BIGINT;
+    DECLARE v_ancestors VARCHAR(1000);
+    DECLARE v_parent_ancestors VARCHAR(1000);
+    
+    -- 声明游标
+    DECLARE cur CURSOR FOR 
+        SELECT id, pid FROM learn_knowledge_tree ORDER BY level, id;
+    
+    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
+    
+    -- 先清空所有ancestors
+    UPDATE learn_knowledge_tree SET ancestors = '';
+    
+    -- 打开游标
+    OPEN cur;
+    
+    read_loop: LOOP
+        FETCH cur INTO v_id, v_pid;
+        
+        IF done THEN
+            LEAVE read_loop;
+        END IF;
+        
+        -- 如果是根节点,ancestors为空
+        IF v_pid IS NULL OR v_pid = 0 THEN
+            SET v_ancestors = '';
+        ELSE
+            -- 获取父节点的ancestors
+            SELECT ancestors INTO v_parent_ancestors 
+            FROM learn_knowledge_tree 
+            WHERE id = v_pid;
+            
+            -- 构建当前节点的ancestors
+            IF v_parent_ancestors IS NULL OR v_parent_ancestors = '' THEN
+                SET v_ancestors = CAST(v_pid AS CHAR);
+            ELSE
+                SET v_ancestors = CONCAT(v_parent_ancestors, ',', v_pid);
+            END IF;
+        END IF;
+        
+        -- 更新当前节点的ancestors
+        UPDATE learn_knowledge_tree 
+        SET ancestors = v_ancestors 
+        WHERE id = v_id;
+        
+    END LOOP;
+    
+    CLOSE cur;
+END$$
+
+DELIMITER ;
+
+-- 执行存储过程
+CALL update_knowledge_tree_ancestors();
+
+-- 删除存储过程(可选)
+-- DROP PROCEDURE IF EXISTS update_knowledge_tree_ancestors;
+
+-- ============================================
+-- 方法3:简单循环更新(适合小数据量)
+-- ============================================
+
+-- 第一步:更新根节点(ancestors为空)
+UPDATE learn_knowledge_tree 
+SET ancestors = '' 
+WHERE pid IS NULL OR pid = 0;
+
+-- 第二步:循环更新子节点(需要多次执行,直到没有更多更新)
+-- 执行多次,直到受影响的行数为0
+UPDATE learn_knowledge_tree t1
+INNER JOIN learn_knowledge_tree t2 ON t1.pid = t2.id
+SET t1.ancestors = CASE 
+    WHEN t2.ancestors IS NULL OR t2.ancestors = '' THEN CAST(t2.id AS CHAR)
+    ELSE CONCAT(t2.ancestors, ',', t2.id)
+END
+WHERE (t1.ancestors IS NULL OR t1.ancestors = '')
+  AND t1.pid IS NOT NULL AND t1.pid != 0;
+
+-- 重复执行上面的UPDATE语句,直到受影响的行数为0
+-- 通常需要执行的次数等于树的最大深度
+
+-- ============================================
+-- 验证查询:检查ancestors是否正确填充
+-- ============================================
+
+UPDATE learn_knowledge_tree set LEVEL=1 where pid is null;
+
+
+SELECT ct1.* from learn_knowledge_tree ct1
+join learn_knowledge_tree pt2 on ct1.pid=pt2.id
+ where ct1.pid is not null and pt2.pid is null
+ 
+ 
+UPDATE learn_knowledge_tree ct1
+join learn_knowledge_tree pt2 on ct1.pid=pt2.id
+ set ct1.LEVEL=2 where ct1.pid is not null and pt2.pid is null
+ 
+ 
+ 
+
+-- 查看所有节点的ancestors
+SELECT 
+    id,
+    name,
+    pid,
+    ancestors,
+    level,
+    (SELECT COUNT(*) FROM learn_knowledge_tree WHERE FIND_IN_SET(t.id, ancestors) > 0) AS children_count
+FROM learn_knowledge_tree t
+ORDER BY level, id
+LIMIT 100;
+
+-- 查找ancestors为空的非根节点(应该没有)
+SELECT id, name, pid, ancestors, level
+FROM learn_knowledge_tree
+WHERE (ancestors IS NULL OR ancestors = '')
+  AND (pid IS NOT NULL AND pid != 0);
+
+-- ============================================
+-- 注意事项:
+-- 1. 如果数据量很大,建议在低峰期执行
+-- 2. 执行前建议备份数据
+-- 3. 如果树结构有循环引用,可能导致无限递归,需要先检查数据
+-- 4. 方法1(递归CTE)需要MySQL 8.0+
+-- 5. 方法2(存储过程)兼容MySQL 5.7及以下版本
+-- 6. 方法3(循环更新)适合小数据量,需要手动多次执行
+-- ============================================
+