package com.ruoyi.system.controller; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletResponse; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.system.domain.CjClass; import com.ruoyi.system.domain.CjCollege; import com.ruoyi.system.domain.CjFillTask; import com.ruoyi.system.domain.CjMajor; import com.ruoyi.system.mapper.CjClassMapper; import com.ruoyi.system.mapper.CjCollegeMapper; import com.ruoyi.system.mapper.CjFillRecordMapper; import com.ruoyi.system.mapper.CjMajorMapper; import com.ruoyi.system.service.ICjFillTaskService; /** * 填表任务Controller(发起填表) * * @author ruoyi * @date 2026-09-09 */ @RestController @RequestMapping("/system/formTask") public class CjFillTaskController extends BaseController { @Autowired private ICjFillTaskService cjFillTaskService; @Autowired private CjCollegeMapper cjCollegeMapper; @Autowired private CjMajorMapper cjMajorMapper; @Autowired private CjClassMapper cjClassMapper; @Autowired private CjFillRecordMapper cjFillRecordMapper; private final ObjectMapper objectMapper = new ObjectMapper(); // ========== 数据范围(教师=仅本班任务;超管/教务处=全校) ========== /** 全校视野:超管(userId=1/admin 角色) 或 教务处(jwc) 角色 */ private boolean isSchoolWide() { Long userId = SecurityUtils.getUserId(); if (userId != null && userId == 1L) { return true; } try { return SecurityUtils.hasRole("admin") || SecurityUtils.hasRole("jwc"); } catch (Exception e) { return false; } } /** 当前用户(教师)管理的班级ID集合 */ private List myClassIds() { CjClass q = new CjClass(); q.setManagerUserId(SecurityUtils.getUserId()); List myClasses = cjClassMapper.selectCjClassList(q); List ids = new ArrayList<>(); if (myClasses != null) { for (CjClass c : myClasses) { ids.add(c.getClassId()); } } return ids; } /** 把任务填报范围收敛到当前视野:全校原样返回;教师取「任务班级 ∩ 本班」,无交集返回空数组 */ private Long[] restrictScope(CjFillTask task) { Long[] scoped = parseClassIds(task); if (isSchoolWide()) { return scoped; } List mine = myClassIds(); List out = new ArrayList<>(); for (Long id : scoped) { if (mine.contains(id)) { out.add(id); } } return out.toArray(new Long[0]); } /** * 查询填表任务列表(教师仅见填报范围与其本班有交集的任务) */ @PreAuthorize("@ss.hasAnyPermi('system:formTask:list,system:fillData:list')") @GetMapping("/list") public TableDataInfo list(CjFillTask cjFillTask) { if (isSchoolWide()) { startPage(); List list = cjFillTaskService.selectCjFillTaskList(cjFillTask); return getDataTable(list); } // 教师:任务量少,全量取出后按本班交集过滤(不分页,避免 startPage 计数失真) List all = cjFillTaskService.selectCjFillTaskList(cjFillTask); List visible = new ArrayList<>(); if (all != null) { for (CjFillTask t : all) { if (restrictScope(t).length > 0) { visible.add(t); } } } return getDataTable(visible); } /** * 学院→专业→班级 组织树(填报范围选择用;仅返回启用的节点) * 教师仅见本班分支(fillData 页筛选也要用此树) */ @PreAuthorize("@ss.hasAnyPermi('system:formTask:query,system:fillData:query')") @GetMapping("/orgTree") public AjaxResult orgTree() { Set allow = isSchoolWide() ? null : new HashSet<>(myClassIds()); return success(buildOrgTree(allow)); } /** * 获取填表任务详细信息(教师仅限与本班有交集的任务) */ @PreAuthorize("@ss.hasAnyPermi('system:formTask:query,system:fillData:query')") @GetMapping(value = "/{taskId}") public AjaxResult getInfo(@PathVariable("taskId") Long taskId) { CjFillTask task = cjFillTaskService.selectCjFillTaskByTaskId(taskId); if (task != null && !isSchoolWide() && restrictScope(task).length == 0) { return error("任务不存在或无权查看"); } return success(task); } /** * 新增填表任务(存为草稿) */ @PreAuthorize("@ss.hasPermi('system:formTask:add')") @Log(title = "填表任务", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@RequestBody CjFillTask cjFillTask) { return toAjax(cjFillTaskService.insertCjFillTask(cjFillTask)); } /** * 修改填表任务(仅草稿) */ @PreAuthorize("@ss.hasPermi('system:formTask:edit')") @Log(title = "填表任务", businessType = BusinessType.UPDATE) @PutMapping public AjaxResult edit(@RequestBody CjFillTask cjFillTask) { return toAjax(cjFillTaskService.updateCjFillTask(cjFillTask)); } /** * 删除填表任务(仅草稿) */ @PreAuthorize("@ss.hasPermi('system:formTask:remove')") @Log(title = "填表任务", businessType = BusinessType.DELETE) @DeleteMapping("/{taskIds}") public AjaxResult remove(@PathVariable Long[] taskIds) { return toAjax(cjFillTaskService.deleteCjFillTaskByTaskIds(taskIds)); } /** * 发布填表任务(草稿→填报中) */ @PreAuthorize("@ss.hasPermi('system:formTask:publish')") @Log(title = "填表任务", businessType = BusinessType.UPDATE) @PostMapping("/publish/{taskId}") public AjaxResult publish(@PathVariable("taskId") Long taskId) { return toAjax(cjFillTaskService.publish(taskId)); } /** * 取消发布(填报中→草稿) */ @PreAuthorize("@ss.hasPermi('system:formTask:publish')") @Log(title = "填表任务", businessType = BusinessType.UPDATE) @PostMapping("/unpublish/{taskId}") public AjaxResult unpublish(@PathVariable("taskId") Long taskId) { return toAjax(cjFillTaskService.unpublish(taskId)); } /** * 截止(→已截止) */ @PreAuthorize("@ss.hasPermi('system:formTask:publish')") @Log(title = "填表任务", businessType = BusinessType.UPDATE) @PostMapping("/close/{taskId}") public AjaxResult close(@PathVariable("taskId") Long taskId) { return toAjax(cjFillTaskService.close(taskId)); } /** * 归档(已截止→已归档) */ @PreAuthorize("@ss.hasPermi('system:formTask:publish')") @Log(title = "填表任务", businessType = BusinessType.UPDATE) @PostMapping("/archive/{taskId}") public AjaxResult archive(@PathVariable("taskId") Long taskId) { return toAjax(cjFillTaskService.archive(taskId)); } // ========== 填报进度 ========== /** * 填报进度总览:应填/实填(已提交)/草稿/未填,含分班统计 */ @PreAuthorize("@ss.hasAnyPermi('system:formTask:list,system:fillData:query')") @GetMapping("/progress/{taskId}") public AjaxResult progress(@PathVariable("taskId") Long taskId) { CjFillTask task = cjFillTaskService.selectCjFillTaskByTaskId(taskId); if (task == null) { throw new ServiceException("填表任务不存在"); } Long[] classIds = restrictScope(task); if (!isSchoolWide() && classIds.length == 0) { throw new ServiceException("任务不存在或无权查看"); } // 应填总数以班级管理的「参考人数」(class_size) 为准: // V2 改为学生扫班级码自助建档,花名册在册数不再是班级实际人数;参考人数为空/0 时回退花名册在册数 List allClasses = cjClassMapper.selectCjClassList(new CjClass()); Map classMap = new HashMap<>(); if (allClasses != null) { for (CjClass cl : allClasses) { classMap.put(cl.getClassId(), cl); } } Map rosterMap = countMap(cjFillRecordMapper.classRosterCount(classIds)); Map shouldMap = new LinkedHashMap<>(); for (Long cid : classIds) { CjClass cl = classMap.get(cid); int size = cl == null || cl.getClassSize() == null ? 0 : cl.getClassSize().intValue(); shouldMap.put(cid, size > 0 ? size : rosterMap.getOrDefault(cid, 0)); } Map subMap = countMap(cjFillRecordMapper.classRecordCount(taskId, "1")); Map draftMap = countMap(cjFillRecordMapper.classRecordCount(taskId, "0")); // 教师视野:分班提交/草稿统计须裁剪到本班,防止「任务范围=部分交集」时带出外班计数 if (!isSchoolWide()) { java.util.Set mineSet = new java.util.HashSet<>(); for (Long cid : classIds) { mineSet.add(cid); } subMap.keySet().retainAll(mineSet); draftMap.keySet().retainAll(mineSet); } // 班级名称映射 Map nameMap = new HashMap<>(); for (Map.Entry e : classMap.entrySet()) { nameMap.put(e.getKey(), e.getValue().getClassName()); } long totalShould = 0, totalSub = 0, totalDraft = 0; List> classes = new ArrayList<>(); java.util.Set ids = new java.util.LinkedHashSet<>(); ids.addAll(shouldMap.keySet()); ids.addAll(subMap.keySet()); ids.addAll(draftMap.keySet()); for (Long cid : ids) { int should = shouldMap.getOrDefault(cid, 0); int sub = subMap.getOrDefault(cid, 0); int draft = draftMap.getOrDefault(cid, 0); totalShould += should; totalSub += sub; totalDraft += draft; Map row = new LinkedHashMap<>(); row.put("classId", cid); row.put("className", nameMap.getOrDefault(cid, "班级" + cid)); row.put("should", should); row.put("submitted", sub); row.put("draft", draft); row.put("unfilled", Math.max(0, should - sub)); classes.add(row); } Map data = new LinkedHashMap<>(); data.put("taskId", taskId); data.put("taskName", task.getTaskName()); data.put("schoolYear", task.getSchoolYear()); data.put("semester", task.getSemester()); data.put("should", totalShould); data.put("submitted", totalSub); data.put("draft", totalDraft); data.put("unfilled", Math.max(0, totalShould - totalSub)); data.put("rate", totalShould == 0 ? 0 : Math.round(totalSub * 100.0 / totalShould)); data.put("classes", classes); return success(data); } /** * 任务范围内学生填报状态列表(分页) * status: 空=全部 / unfilled=未填 / 0=草稿 / 1=已提交 */ @PreAuthorize("@ss.hasAnyPermi('system:formTask:list,system:fillData:list')") @GetMapping("/states") public TableDataInfo states(@RequestParam("taskId") Long taskId, @RequestParam(value = "status", required = false) String status, @RequestParam(value = "name", required = false) String name, @RequestParam(value = "classId", required = false) Long classId, @RequestParam(value = "majorId", required = false) Long majorId, @RequestParam(value = "collegeId", required = false) Long collegeId) { CjFillTask task = cjFillTaskService.selectCjFillTaskByTaskId(taskId); if (task == null) { throw new ServiceException("填表任务不存在"); } Long[] classIds = restrictScope(task); if (!isSchoolWide() && classIds.length == 0) { throw new ServiceException("任务不存在或无权查看"); } startPage(); List> list = cjFillRecordMapper.rosterStates(taskId, classIds, status, name, classId, majorId, collegeId); return getDataTable(list); } /** * 填报记录详情(按任务 schema 还原每个字段的值) */ @PreAuthorize("@ss.hasAnyPermi('system:formTask:list,system:fillData:query')") @GetMapping("/recordDetail/{recordId}") public AjaxResult recordDetail(@PathVariable("recordId") Long recordId) { com.ruoyi.system.domain.CjFillRecord rec = cjFillRecordMapper.selectByRecordId(recordId); if (rec == null) { throw new ServiceException("填报记录不存在"); } if (!isSchoolWide() && !myClassIds().contains(rec.getClassId())) { throw new ServiceException("记录不存在或无权查看"); } CjFillTask task = cjFillTaskService.selectCjFillTaskByTaskId(rec.getTaskId()); if (task == null) { throw new ServiceException("记录所属任务不存在"); } Map data = new LinkedHashMap<>(); data.put("recordId", rec.getRecordId()); data.put("taskId", rec.getTaskId()); data.put("taskName", task.getTaskName()); data.put("status", rec.getStatus()); data.put("submitTime", rec.getSubmitTime()); data.put("studentNo", rec.getStudentNo()); data.put("studentName", rec.getStudentName()); data.put("genderText", rec.getGenderText()); data.put("phone", rec.getPhone()); data.put("className", rec.getClassName()); data.put("majorName", rec.getMajorName()); data.put("collegeName", rec.getCollegeName()); data.put("schemaGroups", schemaGroups(task)); data.put("values", valuesMap(rec)); return success(data); } /** 批量预览的行数上限:超过则截断并回传 truncated,避免前端一次性渲染几十万个单元格卡死 */ private static final int PREVIEW_MAX_ROWS = 3000; /** * 批量预览填报数据(一次拿全,学生×字段的宽表) * 筛选口径与 /export 完全一致,只是返回 JSON 而非 Excel 文件。 */ @PreAuthorize("@ss.hasAnyPermi('system:formTask:list,system:fillData:query')") @GetMapping("/previewData") public AjaxResult previewData(@RequestParam("taskId") Long taskId, @RequestParam(value = "status", required = false) String status, @RequestParam(value = "name", required = false) String name, @RequestParam(value = "classId", required = false) Long classId, @RequestParam(value = "majorId", required = false) Long majorId, @RequestParam(value = "collegeId", required = false) Long collegeId) { CjFillTask task = cjFillTaskService.selectCjFillTaskByTaskId(taskId); if (task == null) { throw new ServiceException("填表任务不存在"); } Long[] classIds = restrictScope(task); if (!isSchoolWide() && classIds.length == 0) { throw new ServiceException("任务不存在或无权查看"); } List> rows = "unfilled".equals(status) ? cjFillRecordMapper.rosterStates(taskId, classIds, "unfilled", name, classId, majorId, collegeId) : cjFillRecordMapper.exportRecords(taskId, classIds, status, name, classId, majorId, collegeId); if (rows == null) { rows = new ArrayList<>(); } // fieldMeta 是完整字段定义(fieldDisplay 要靠它把选项值翻成中文); // fields 只是下发给前端的列描述,两者一一对应。 List> fieldMeta = new ArrayList<>(); List> fields = new ArrayList<>(); for (Object go : schemaGroups(task)) { Map g = (Map) go; Object fs = g.get("fields"); if (!(fs instanceof List)) { continue; } for (Object fo : (List) fs) { Map f = (Map) fo; fieldMeta.add(f); Map col = new LinkedHashMap<>(); col.put("code", strOf(f.get("code"))); col.put("label", labelOf(f)); col.put("groupName", strOf(g.get("groupName"))); col.put("type", strOf(f.get("type"))); fields.add(col); } } int total = rows.size(); boolean truncated = total > PREVIEW_MAX_ROWS; if (truncated) { rows = rows.subList(0, PREVIEW_MAX_ROWS); } List> out = new ArrayList<>(); for (Map row : rows) { Map values = jsonMap(row.get("form_data")); Map m = new LinkedHashMap<>(); m.put("recordId", row.get("record_id")); m.put("collegeName", orEmpty(row.get("college_name"))); m.put("majorName", orEmpty(row.get("major_name"))); m.put("className", orEmpty(row.get("class_name"))); m.put("name", orEmpty(row.get("name"))); m.put("studentNo", orEmpty(row.get("student_no"))); m.put("genderText", genderText(row.get("gender"))); m.put("phone", orEmpty(row.get("phone"))); m.put("statusText", statusLabel(row.get("record_status"))); m.put("submitted", "1".equals(String.valueOf(row.get("record_status")))); m.put("submitTime", fmtExcelTime(row.get("submit_time"))); // 字段值先按导出同款规则转成可读文本,前端直接贴出来,避免两端两套换算逻辑 List cells = new ArrayList<>(); for (Map f : fieldMeta) { cells.add(fieldDisplay(f, values.get(strOf(f.get("code"))))); } m.put("cells", cells); out.add(m); } Map data = new LinkedHashMap<>(); data.put("taskId", taskId); data.put("taskName", task.getTaskName()); data.put("fields", fields); data.put("rows", out); data.put("total", total); data.put("truncated", truncated); data.put("limit", PREVIEW_MAX_ROWS); return success(data); } /** 解析任务范围 JSON 里的 classIds */ private Long[] parseClassIds(CjFillTask task) { String scope = task.getScopeJson(); try { JsonNode root = objectMapper.readTree(scope); JsonNode arr = root.path("classIds"); if (arr.isArray()) { List ids = new ArrayList<>(); for (JsonNode n : arr) { ids.add(n.asLong()); } return ids.toArray(new Long[0]); } } catch (Exception e) { /* fallthrough */ } return new Long[0]; } /** * 导出填报数据 Excel(列 = 任务表单各字段,值 = 学生填写内容) * status: ''=全部(草稿+提交) '1'=已提交 '0'=草稿 'unfilled'=未填写(仅基础列) */ @PreAuthorize("@ss.hasAnyPermi('system:formTask:list,system:fillData:export')") @PostMapping("/export") public void export(HttpServletResponse response, @RequestParam("taskId") Long taskId, @RequestParam(value = "status", required = false) String status, @RequestParam(value = "name", required = false) String name, @RequestParam(value = "classId", required = false) Long classId, @RequestParam(value = "majorId", required = false) Long majorId, @RequestParam(value = "collegeId", required = false) Long collegeId) throws Exception { CjFillTask task = cjFillTaskService.selectCjFillTaskByTaskId(taskId); if (task == null) { throw new ServiceException("填表任务不存在"); } Long[] classIds = restrictScope(task); if (!isSchoolWide() && classIds.length == 0) { throw new ServiceException("任务不存在或无权导出"); } boolean onlyUnfilled = "unfilled".equals(status); List> rows; if (onlyUnfilled) { rows = cjFillRecordMapper.rosterStates(taskId, classIds, "unfilled", name, classId, majorId, collegeId); } else { rows = cjFillRecordMapper.exportRecords(taskId, classIds, status, name, classId, majorId, collegeId); } if (rows == null) { rows = new ArrayList<>(); } // 字段列:按 schema 顺序收集字段元信息 List> fieldMeta = new ArrayList<>(); for (Object go : schemaGroups(task)) { Map g = (Map) go; Object fs = g.get("fields"); if (fs instanceof List) { for (Object fo : (List) fs) { Map f = (Map) fo; fieldMeta.add(f); } } } String[] fixedHead = { "学院", "专业", "班级", "姓名", "学号", "性别", "联系电话", "状态", "提交时间" }; try (XSSFWorkbook wb = new XSSFWorkbook()) { Sheet sheet = wb.createSheet("填报数据"); // 表头 Row head = sheet.createRow(0); int col = 0; for (String h : fixedHead) { head.createCell(col++).setCellValue(h); } for (Map f : fieldMeta) { head.createCell(col++).setCellValue(labelOf(f)); } int r = 1; for (Map row : rows) { Row x = sheet.createRow(r++); int c = 0; x.createCell(c++).setCellValue(orEmpty(row.get("college_name"))); x.createCell(c++).setCellValue(orEmpty(row.get("major_name"))); x.createCell(c++).setCellValue(orEmpty(row.get("class_name"))); x.createCell(c++).setCellValue(orEmpty(row.get("name"))); x.createCell(c++).setCellValue(orEmpty(row.get("student_no"))); x.createCell(c++).setCellValue(genderText(row.get("gender"))); x.createCell(c++).setCellValue(orEmpty(row.get("phone"))); x.createCell(c++).setCellValue(statusLabel(row.get("record_status"))); x.createCell(c++).setCellValue(fmtExcelTime(row.get("submit_time"))); // 各字段值 Map values = jsonMap(row.get("form_data")); for (Map f : fieldMeta) { Object v = values.get(strOf(f.get("code"))); x.createCell(c++).setCellValue(fieldDisplay(f, v)); } } for (int i = 0; i < fixedHead.length + fieldMeta.size(); i++) { sheet.setColumnWidth(i, 5000); } String fileName = URLEncoder.encode((task.getTaskName() == null ? "填报数据" : task.getTaskName()) + ".xlsx", "UTF-8").replaceAll("\\+", "%20"); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); wb.write(response.getOutputStream()); } } /** * 后台退回草稿(管理员把某条已提交退回,学生可改后重提) */ @PreAuthorize("@ss.hasAnyPermi('system:formTask:list,system:fillData:rollback')") @PostMapping("/rollback/{recordId}") public AjaxResult rollback(@PathVariable("recordId") Long recordId) { com.ruoyi.system.domain.CjFillRecord rec = cjFillRecordMapper.selectByRecordId(recordId); if (rec == null) { throw new ServiceException("填报记录不存在"); } if (!isSchoolWide() && !myClassIds().contains(rec.getClassId())) { throw new ServiceException("记录不存在或无权操作"); } if (!"1".equals(rec.getStatus())) { throw new ServiceException("仅“已提交”的记录可退回草稿"); } cjFillRecordMapper.rollbackDraft(recordId); return success("已退回草稿,学生可修改后重新提交"); } // ========== 导出辅助 ========== private String labelOf(Map f) { String label = strOf(f.get("label")); return label == null ? String.valueOf(f.get("code")) : label; } private String fieldDisplay(Map f, Object v) { if (v == null) return ""; // 图片/文件字段:值是路径数组,导出时转成文件名列表,避免出现 "[/profile/upload/..., ...]" String type = strOf(f.get("type")); if (("image".equals(type) || "file".equals(type)) && v instanceof List) { List names = new ArrayList<>(); for (Object o : (List) v) { String s = strOf(o); if (!s.isEmpty()) { names.add(s.substring(s.lastIndexOf('/') + 1)); } } return String.join(";", names); } Object opts = f.get("options"); if (opts instanceof List) { for (Object o : (List) opts) { Map m = (Map) o; if (String.valueOf(m.get("value")).equals(String.valueOf(v))) { return strOf(m.get("label")); } } } return String.valueOf(v); } private Map jsonMap(Object raw) { Map map = new LinkedHashMap<>(); if (raw == null) return map; try { map.putAll(objectMapper.readValue(raw.toString(), new TypeReference>() { })); } catch (Exception ignore) { } return map; } private String statusLabel(Object s) { String v = String.valueOf(s == null ? "" : s); if ("1".equals(v)) return "已提交"; if ("0".equals(v)) return "草稿"; return "未填写"; } private String genderText(Object g) { String v = String.valueOf(g == null ? "" : g); if ("0".equals(v) || "男".equals(v)) return "男"; if ("1".equals(v) || "女".equals(v)) return "女"; return ""; } private String orEmpty(Object o) { return o == null ? "" : o.toString(); } private String strOf(Object o) { return o == null ? null : o.toString(); } private String fmtExcelTime(Object o) { if (o == null) return ""; String s = o.toString(); s = s.replace('T', ' '); return s.length() > 16 ? s.substring(0, 16) : s; } /** 任务 schema 的 groups(纯对象,供前端按字段还原展示) */ private List schemaGroups(CjFillTask task) { if (task.getSchemaJson() == null || task.getSchemaJson().trim().isEmpty()) { return new ArrayList<>(); } try { JsonNode root = objectMapper.readTree(task.getSchemaJson()); JsonNode groups = root.path("groups"); if (groups.isArray()) { return objectMapper.convertValue(groups, new TypeReference>() { }); } } catch (Exception ignore) { } return new ArrayList<>(); } /** 解析记录 form_data 为值 Map */ private Map valuesMap(com.ruoyi.system.domain.CjFillRecord rec) { Map values = new LinkedHashMap<>(); if (rec.getFormData() == null || rec.getFormData().trim().isEmpty()) { return values; } try { Map m = objectMapper.readValue(rec.getFormData(), new TypeReference>() { }); if (m != null) { values.putAll(m); } } catch (Exception ignore) { } return values; } /** mapper 行 {class_id,c} -> Map */ private Map countMap(List> rows) { Map map = new HashMap<>(); if (rows != null) { for (Map r : rows) { Object k = r.get("class_id"); Object v = r.get("c"); if (k == null) { continue; } map.put(((Number) k).longValue(), v == null ? 0 : ((Number) v).intValue()); } } return map; } // ========== 组织树组装 ========== private List> buildOrgTree(Set allowClassIds) { List> roots = new ArrayList<>(); CjCollege qCollege = new CjCollege(); qCollege.setStatus("0"); List colleges = cjCollegeMapper.selectCjCollegeList(qCollege); if (colleges == null) { return roots; } // 一次性取专业/班级,内存分组,避免 N+1 CjMajor qMajor = new CjMajor(); qMajor.setStatus("0"); List majors = cjMajorMapper.selectCjMajorList(qMajor); CjClass qClass = new CjClass(); qClass.setStatus("0"); List classes = cjClassMapper.selectCjClassList(qClass); Map>> collegeMajors = new HashMap<>(); if (majors != null) { for (CjMajor major : majors) { Long cid = major.getCollegeId(); if (cid == null) { continue; } collegeMajors.computeIfAbsent(cid, k -> new ArrayList<>()).add( node(major.getMajorId(), major.getMajorName())); } } Map>> majorClasses = new HashMap<>(); if (classes != null) { for (CjClass clazz : classes) { Long mid = clazz.getMajorId(); if (mid == null) { continue; } // 教师态:班级层按授权集合剪枝(null=全校) if (allowClassIds != null && !allowClassIds.contains(clazz.getClassId())) { continue; } majorClasses.computeIfAbsent(mid, k -> new ArrayList<>()).add( node(clazz.getClassId(), clazz.getClassName())); } } for (CjCollege college : colleges) { Map collegeNode = node(college.getCollegeId(), college.getCollegeName()); List> majorsOfCollege = collegeMajors.get(college.getCollegeId()); if (majorsOfCollege != null) { for (Map majorNode : majorsOfCollege) { Long majorId = toLong(majorNode.get("id")); majorNode.put("children", majorClasses.getOrDefault(majorId, new ArrayList<>())); } } collegeNode.put("children", majorsOfCollege == null ? new ArrayList<>() : majorsOfCollege); roots.add(collegeNode); } return roots; } private Map node(Long id, String name) { Map map = new LinkedHashMap<>(); map.put("id", id); map.put("label", name); map.put("children", new ArrayList<>()); return map; } private Long toLong(Object v) { return v instanceof Number ? ((Number) v).longValue() : Long.valueOf(v.toString()); } }