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<CjClass> myClasses()
|
{
|
CjClass q = new CjClass();
|
q.setManagerUserId(SecurityUtils.getUserId());
|
List<CjClass> list = cjClassMapper.selectCjClassList(q);
|
return list == null ? new ArrayList<>() : list;
|
}
|
|
/**
|
* 工作台总览(教师/全校双视图)
|
*/
|
@PreAuthorize("@ss.hasPermi('system:student:list')")
|
@GetMapping("/overview")
|
public AjaxResult overview()
|
{
|
Map<String, Object> data = isSchoolWide() ? schoolView() : teacherView();
|
// 注册码打印图文案随信封下发:教师只有 student/class 权限,直接调字典接口会被 403
|
data.put("titles", posterTitles());
|
return success(data);
|
}
|
|
/** 注册码打印图文案(字典 cj_fill_title,缺失时回退内置默认) */
|
private Map<String, Object> posterTitles()
|
{
|
Map<String, Object> 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<SysDictData> 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<String, Object> teacherView()
|
{
|
Map<String, Object> data = new LinkedHashMap<>();
|
data.put("scope", "teacher");
|
|
List<CjClass> classes = myClasses();
|
Long[] ids = new Long[classes.size()];
|
for (int i = 0; i < classes.size(); i++)
|
{
|
ids[i] = classes.get(i).getClassId();
|
}
|
Map<Long, Map<String, Object>> statsMap = toClassIdMap(cjStudentMapper.classStudentStats(ids));
|
|
// 专业/学院名称内存映射(selectCjClassList 不带名称,避免 N+1)
|
Map<Long, CjMajor> majorMap = new HashMap<>();
|
for (CjMajor m : orEmptyList(cjMajorMapper.selectCjMajorList(new CjMajor())))
|
{
|
majorMap.put(m.getMajorId(), m);
|
}
|
Map<Long, String> 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<Map<String, Object>> classRows = new ArrayList<>();
|
long studentTotal = 0, activatedTotal = 0;
|
for (CjClass cl : classes)
|
{
|
Map<String, Object> 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<String, Object> 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<String, Object> totals = new LinkedHashMap<>();
|
totals.put("classCount", classes.size());
|
totals.put("studentCount", studentTotal);
|
totals.put("activatedCount", activatedTotal);
|
Set<Long> mine = new HashSet<>();
|
Map<Long, Integer> sizeMap = new HashMap<>();
|
for (CjClass cl : classes)
|
{
|
mine.add(cl.getClassId());
|
sizeMap.put(cl.getClassId(), cl.getClassSize() == null ? 0 : cl.getClassSize().intValue());
|
}
|
List<Map<String, Object>> 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<Map<String, Object>> myOpenTasks(Set<Long> mine, Map<Long, Integer> sizeMap)
|
{
|
List<Map<String, Object>> result = new ArrayList<>();
|
CjFillTask q = new CjFillTask();
|
q.setStatus("1");
|
List<CjFillTask> openTasks = orEmptyList(cjFillTaskService.selectCjFillTaskList(q));
|
for (CjFillTask t : openTasks)
|
{
|
List<Long> 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<Long, Integer> rosterCnt = new HashMap<>();
|
for (Map<String, Object> 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<String, Object> 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<String, Object> schoolView()
|
{
|
Map<String, Object> data = new LinkedHashMap<>();
|
data.put("scope", "school");
|
|
List<CjCollege> colleges = orEmptyList(cjCollegeMapper.selectCjCollegeList(new CjCollege()));
|
List<CjClass> classes = orEmptyList(cjClassMapper.selectCjClassList(new CjClass()));
|
Map<Long, Integer> 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<String, Object> st : orEmptyList(cjStudentMapper.classStudentStats(null)))
|
{
|
studentTotal += num(st.get("total")).longValue();
|
activatedTotal += num(st.get("activated")).longValue();
|
}
|
|
List<CjFillTask> 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<String, Object> 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<String, Object> 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<Map<String, Object>> collegeStats = new ArrayList<>();
|
for (Map<String, Object> r : orEmptyList(cjStudentMapper.collegeStudentStats()))
|
{
|
Map<String, Object> 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<Map<String, Object>> 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<String, Object> 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<Long, Map<String, Object>> toClassIdMap(List<Map<String, Object>> rows)
|
{
|
Map<Long, Map<String, Object>> map = new HashMap<>();
|
for (Map<String, Object> 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<Map<String, Object>> rows, Collection<Long> classFilter)
|
{
|
long sum = 0;
|
for (Map<String, Object> 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 <T> List<T> orEmptyList(List<T> 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<Long> ids = new ArrayList<>();
|
for (JsonNode n : arr) { ids.add(n.asLong()); }
|
return ids.toArray(new Long[0]);
|
}
|
}
|
catch (Exception e) { /* 解析失败按无范围处理 */ }
|
return new Long[0];
|
}
|
}
|