package com.ruoyi.system.controller; import java.util.ArrayList; import java.util.Collection; 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.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.entity.SysDictData; 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.mapper.CjStudentMapper; import com.ruoyi.system.service.ICjFillTaskService; import com.ruoyi.system.service.ISysConfigService; import com.ruoyi.system.service.ISysDictTypeService; /** * 工作台Controller(登录后首页聚合) * * 数据视野与花名册一致:超管/教务处(admin/jwc)=全校统计;教师=仅本人管理班级。 * 权限门复用 system:student:list(teacher/jwc 均持有,admin 通配),不新增权限串。 * 返回统一信封:{scope, totals, classes, tasks}(教师)/ {scope, totals, taskStats, collegeStats, recentTasks}(全校)。 * * @author ruoyi * @date 2026-09-17 */ @RestController @RequestMapping("/system/workbench") public class CjWorkbenchController extends BaseController { @Autowired private CjClassMapper cjClassMapper; @Autowired private CjMajorMapper cjMajorMapper; @Autowired private CjCollegeMapper cjCollegeMapper; @Autowired private CjStudentMapper cjStudentMapper; @Autowired private CjFillRecordMapper cjFillRecordMapper; @Autowired private ICjFillTaskService cjFillTaskService; @Autowired private ISysConfigService configService; @Autowired private ISysDictTypeService dictTypeService; private final ObjectMapper objectMapper = new ObjectMapper(); /** 全校视野:超管(userId=1/admin 角色) 或 教务处(jwc) 角色(与 CjStudentController 同语义) */ 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; } } /** 当前用户(教师)管理的班级 */ private List myClasses() { CjClass q = new CjClass(); q.setManagerUserId(SecurityUtils.getUserId()); List list = cjClassMapper.selectCjClassList(q); return list == null ? new ArrayList<>() : list; } /** * 工作台总览(教师/全校双视图) */ @PreAuthorize("@ss.hasPermi('system:student:list')") @GetMapping("/overview") public AjaxResult overview() { Map data = isSchoolWide() ? schoolView() : teacherView(); // 注册码打印图文案随信封下发:教师只有 student/class 权限,直接调字典接口会被 403 data.put("titles", posterTitles()); return success(data); } /** 注册码打印图文案(字典 cj_fill_title,缺失时回退内置默认) */ private Map posterTitles() { Map titles = new LinkedHashMap<>(); titles.put("posterTitle", dictText("二维码海报标题", "产教融合实践教学管理平台")); titles.put("posterTip", dictText("二维码海报提示", "扫码进入,按提示注册并设置密码")); return titles; } /** * 取文案字典中某个用途的正文;字典项缺失/被清空时回退默认,海报不会空白。 * 用 selectDictDataByType 而非 DictUtils:前者缓存未命中会回源数据库并补缓存, * 线上跑完 15 号脚本无需重启或手动刷缓存即可生效。 */ private String dictText(String label, String fallback) { try { List datas = dictTypeService.selectDictDataByType("cj_fill_title"); if (datas != null) { for (SysDictData d : datas) { if (label.equals(d.getDictLabel())) { String v = d.getDictValue(); if (v != null && !v.trim().isEmpty()) { return v.trim(); } break; } } } } catch (Exception e) { // 字典不可用时不应阻断工作台,静默回退 } return fallback; } // ========== 教师视图 ========== private Map teacherView() { Map data = new LinkedHashMap<>(); data.put("scope", "teacher"); List classes = myClasses(); Long[] ids = new Long[classes.size()]; for (int i = 0; i < classes.size(); i++) { ids[i] = classes.get(i).getClassId(); } Map> statsMap = toClassIdMap(cjStudentMapper.classStudentStats(ids)); // 专业/学院名称内存映射(selectCjClassList 不带名称,避免 N+1) Map majorMap = new HashMap<>(); for (CjMajor m : orEmptyList(cjMajorMapper.selectCjMajorList(new CjMajor()))) { majorMap.put(m.getMajorId(), m); } Map collegeMap = new HashMap<>(); for (CjCollege c : orEmptyList(cjCollegeMapper.selectCjCollegeList(new CjCollege()))) { collegeMap.put(c.getCollegeId(), c.getCollegeName()); } String h5Base = trimTrailingSlash(configService.selectConfigByKey("cj.student.h5BaseUrl")); List> classRows = new ArrayList<>(); long studentTotal = 0, activatedTotal = 0; for (CjClass cl : classes) { Map st = statsMap.get(cl.getClassId()); int total = st == null ? 0 : num(st.get("total")).intValue(); int activated = st == null ? 0 : num(st.get("activated")).intValue(); studentTotal += total; activatedTotal += activated; CjMajor mj = majorMap.get(cl.getMajorId()); Map row = new LinkedHashMap<>(); row.put("classId", cl.getClassId()); row.put("className", cl.getClassName()); row.put("majorName", mj == null ? "" : mj.getMajorName()); row.put("collegeName", mj == null ? "" : collegeMap.getOrDefault(mj.getCollegeId(), "")); row.put("studentCount", total); row.put("activatedCount", activated); row.put("bindCode", cl.getBindCode()); // 注册链接由后端拼好下发:教师无 class/config 菜单权限也能分享班级码 row.put("registerUrl", cl.getBindCode() == null || cl.getBindCode().isEmpty() || h5Base.isEmpty() ? null : h5Base + "/#/?c=" + cl.getBindCode()); classRows.add(row); } Map totals = new LinkedHashMap<>(); totals.put("classCount", classes.size()); totals.put("studentCount", studentTotal); totals.put("activatedCount", activatedTotal); Set mine = new HashSet<>(); Map sizeMap = new HashMap<>(); for (CjClass cl : classes) { mine.add(cl.getClassId()); sizeMap.put(cl.getClassId(), cl.getClassSize() == null ? 0 : cl.getClassSize().intValue()); } List> tasks = myOpenTasks(mine, sizeMap); totals.put("openTaskCount", tasks.size()); data.put("totals", totals); data.put("classes", classRows); data.put("tasks", tasks); return data; } /** 进行中的任务 × 本班交集的填报进度(分母=班级参考人数,空/0 回退花名册在册数) */ private List> myOpenTasks(Set mine, Map sizeMap) { List> result = new ArrayList<>(); CjFillTask q = new CjFillTask(); q.setStatus("1"); List openTasks = orEmptyList(cjFillTaskService.selectCjFillTaskList(q)); for (CjFillTask t : openTasks) { List inter = new ArrayList<>(); for (Long cid : parseClassIds(t)) { if (mine.contains(cid)) { inter.add(cid); } } if (inter.isEmpty()) { continue; } Long[] ids = inter.toArray(new Long[0]); Map rosterCnt = new HashMap<>(); for (Map r : orEmptyList(cjFillRecordMapper.classRosterCount(ids))) { rosterCnt.put(num(r.get("class_id")).longValue(), num(r.get("c")).intValue()); } long should = 0; for (Long cid : inter) { int size = sizeMap.getOrDefault(cid, 0); should += size > 0 ? size : rosterCnt.getOrDefault(cid, 0); } long submitted = sumCounts(cjFillRecordMapper.classRecordCount(t.getTaskId(), "1"), inter); Map row = new LinkedHashMap<>(); row.put("taskId", t.getTaskId()); row.put("taskName", t.getTaskName()); row.put("fillEndDate", t.getFillEndDate()); row.put("should", should); row.put("submitted", submitted); row.put("unfilled", Math.max(0, should - submitted)); row.put("rate", should == 0 ? 0 : Math.round(submitted * 100.0 / should)); result.add(row); } return result; } // ========== 全校视图(超管/教务处) ========== private Map schoolView() { Map data = new LinkedHashMap<>(); data.put("scope", "school"); List colleges = orEmptyList(cjCollegeMapper.selectCjCollegeList(new CjCollege())); List classes = orEmptyList(cjClassMapper.selectCjClassList(new CjClass())); Map classSizeMap = new HashMap<>(); for (CjClass cl : classes) { classSizeMap.put(cl.getClassId(), cl.getClassSize() == null ? 0 : cl.getClassSize().intValue()); } long studentTotal = 0, activatedTotal = 0; for (Map st : orEmptyList(cjStudentMapper.classStudentStats(null))) { studentTotal += num(st.get("total")).longValue(); activatedTotal += num(st.get("activated")).longValue(); } List tasks = orEmptyList(cjFillTaskService.selectCjFillTaskList(new CjFillTask())); int draft = 0, filling = 0, closed = 0, archived = 0; for (CjFillTask t : tasks) { String s = t.getStatus(); if ("0".equals(s)) { draft++; } else if ("1".equals(s)) { filling++; } else if ("2".equals(s)) { closed++; } else if ("3".equals(s)) { archived++; } } Map totals = new LinkedHashMap<>(); totals.put("collegeCount", colleges.size()); totals.put("classCount", classes.size()); totals.put("studentCount", studentTotal); totals.put("activatedCount", activatedTotal); totals.put("openTaskCount", filling); Map taskStats = new LinkedHashMap<>(); taskStats.put("total", tasks.size()); taskStats.put("draft", draft); taskStats.put("filling", filling); taskStats.put("closed", closed); taskStats.put("archived", archived); List> collegeStats = new ArrayList<>(); for (Map r : orEmptyList(cjStudentMapper.collegeStudentStats())) { Map row = new LinkedHashMap<>(); row.put("collegeName", r.get("college_name")); row.put("classCount", num(r.get("class_count")).intValue()); row.put("studentCount", num(r.get("student_count")).intValue()); row.put("activatedCount", num(r.get("activated_count")).intValue()); collegeStats.add(row); } // 最近 5 个任务 + 全校提交率(分母=任务范围班级参考人数合计,为 0 回退发布快照 scopeCount) List> recentTasks = new ArrayList<>(); int n = 0; for (CjFillTask t : tasks) { if (n++ >= 5) { break; } long submitted = sumCounts(cjFillRecordMapper.classRecordCount(t.getTaskId(), "1"), null); long should = 0; for (Long cid : parseClassIds(t)) { should += classSizeMap.getOrDefault(cid, 0); } if (should == 0 && t.getScopeCount() != null) { should = t.getScopeCount(); } Map row = new LinkedHashMap<>(); row.put("taskId", t.getTaskId()); row.put("taskName", t.getTaskName()); row.put("schoolYear", t.getSchoolYear()); row.put("semester", t.getSemester()); row.put("status", t.getStatus()); row.put("fillStartDate", t.getFillStartDate()); row.put("fillEndDate", t.getFillEndDate()); row.put("scopeCount", should); row.put("submitted", submitted); row.put("rate", should == 0 ? 0 : Math.round(submitted * 100.0 / should)); recentTasks.add(row); } data.put("totals", totals); data.put("taskStats", taskStats); data.put("collegeStats", collegeStats); data.put("recentTasks", recentTasks); return data; } // ========== 通用辅助 ========== /** 行集按 class_id 建索引 */ private Map> toClassIdMap(List> rows) { Map> map = new HashMap<>(); for (Map r : rows) { Object k = r.get("class_id"); if (k != null) { map.put(num(k).longValue(), r); } } return map; } /** 聚合行 {class_id,c} 求和;classFilter 非空时仅统计交集班级 */ private long sumCounts(List> rows, Collection classFilter) { long sum = 0; for (Map r : orEmptyList(rows)) { if (classFilter != null) { Object k = r.get("class_id"); if (k == null || !classFilter.contains(num(k).longValue())) { continue; } } sum += num(r.get("c")).longValue(); } return sum; } /** 数字列安全转换(count/sum 可能返回 Long/BigDecimal) */ private Number num(Object v) { return v instanceof Number ? (Number) v : Long.valueOf(v == null ? "0" : v.toString()); } private List orEmptyList(List list) { return list == null ? new ArrayList<>() : list; } private String trimTrailingSlash(String s) { if (s == null) { return ""; } s = s.trim(); while (s.endsWith("/")) { s = s.substring(0, s.length() - 1); } return s; } /** 解析任务范围 JSON 里的 classIds(与 CjFillTaskController 同逻辑) */ 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) { /* 解析失败按无范围处理 */ } return new Long[0]; } }