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<Long> myClassIds()
|
{
|
CjClass q = new CjClass();
|
q.setManagerUserId(SecurityUtils.getUserId());
|
List<CjClass> myClasses = cjClassMapper.selectCjClassList(q);
|
List<Long> 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<Long> mine = myClassIds();
|
List<Long> 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<CjFillTask> list = cjFillTaskService.selectCjFillTaskList(cjFillTask);
|
return getDataTable(list);
|
}
|
// 教师:任务量少,全量取出后按本班交集过滤(不分页,避免 startPage 计数失真)
|
List<CjFillTask> all = cjFillTaskService.selectCjFillTaskList(cjFillTask);
|
List<CjFillTask> 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<Long> 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<CjClass> allClasses = cjClassMapper.selectCjClassList(new CjClass());
|
Map<Long, CjClass> classMap = new HashMap<>();
|
if (allClasses != null)
|
{
|
for (CjClass cl : allClasses) { classMap.put(cl.getClassId(), cl); }
|
}
|
Map<Long, Integer> rosterMap = countMap(cjFillRecordMapper.classRosterCount(classIds));
|
Map<Long, Integer> 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<Long, Integer> subMap = countMap(cjFillRecordMapper.classRecordCount(taskId, "1"));
|
Map<Long, Integer> draftMap = countMap(cjFillRecordMapper.classRecordCount(taskId, "0"));
|
// 教师视野:分班提交/草稿统计须裁剪到本班,防止「任务范围=部分交集」时带出外班计数
|
if (!isSchoolWide())
|
{
|
java.util.Set<Long> mineSet = new java.util.HashSet<>();
|
for (Long cid : classIds) { mineSet.add(cid); }
|
subMap.keySet().retainAll(mineSet);
|
draftMap.keySet().retainAll(mineSet);
|
}
|
|
// 班级名称映射
|
Map<Long, String> nameMap = new HashMap<>();
|
for (Map.Entry<Long, CjClass> e : classMap.entrySet()) { nameMap.put(e.getKey(), e.getValue().getClassName()); }
|
|
long totalShould = 0, totalSub = 0, totalDraft = 0;
|
List<Map<String, Object>> classes = new ArrayList<>();
|
java.util.Set<Long> 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<String, Object> 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<String, Object> 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<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> 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<Map<String, Object>> fieldMeta = new ArrayList<>();
|
List<Map<String, Object>> fields = new ArrayList<>();
|
for (Object go : schemaGroups(task))
|
{
|
Map<String, Object> g = (Map<String, Object>) go;
|
Object fs = g.get("fields");
|
if (!(fs instanceof List)) { continue; }
|
for (Object fo : (List<?>) fs)
|
{
|
Map<String, Object> f = (Map<String, Object>) fo;
|
fieldMeta.add(f);
|
Map<String, Object> 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<Map<String, Object>> out = new ArrayList<>();
|
for (Map<String, Object> row : rows)
|
{
|
Map<String, Object> values = jsonMap(row.get("form_data"));
|
Map<String, Object> 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<String> cells = new ArrayList<>();
|
for (Map<String, Object> f : fieldMeta)
|
{
|
cells.add(fieldDisplay(f, values.get(strOf(f.get("code")))));
|
}
|
m.put("cells", cells);
|
out.add(m);
|
}
|
|
Map<String, Object> 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<Long> 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<Map<String, Object>> 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<Map<String, Object>> fieldMeta = new ArrayList<>();
|
for (Object go : schemaGroups(task))
|
{
|
Map<String, Object> g = (Map<String, Object>) go;
|
Object fs = g.get("fields");
|
if (fs instanceof List)
|
{
|
for (Object fo : (List<?>) fs)
|
{
|
Map<String, Object> f = (Map<String, Object>) 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<String, Object> f : fieldMeta) { head.createCell(col++).setCellValue(labelOf(f)); }
|
|
int r = 1;
|
for (Map<String, Object> 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<String, Object> values = jsonMap(row.get("form_data"));
|
for (Map<String, Object> 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<String, Object> f)
|
{
|
String label = strOf(f.get("label"));
|
return label == null ? String.valueOf(f.get("code")) : label;
|
}
|
private String fieldDisplay(Map<String, Object> 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<String> 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<String, Object> m = (Map<String, Object>) o;
|
if (String.valueOf(m.get("value")).equals(String.valueOf(v))) { return strOf(m.get("label")); }
|
}
|
}
|
return String.valueOf(v);
|
}
|
private Map<String, Object> jsonMap(Object raw)
|
{
|
Map<String, Object> map = new LinkedHashMap<>();
|
if (raw == null) return map;
|
try { map.putAll(objectMapper.readValue(raw.toString(), new TypeReference<Map<String, Object>>() { })); }
|
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<Object> 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<List<Object>>() { });
|
}
|
}
|
catch (Exception ignore) { }
|
return new ArrayList<>();
|
}
|
|
/** 解析记录 form_data 为值 Map */
|
private Map<String, Object> valuesMap(com.ruoyi.system.domain.CjFillRecord rec)
|
{
|
Map<String, Object> values = new LinkedHashMap<>();
|
if (rec.getFormData() == null || rec.getFormData().trim().isEmpty()) { return values; }
|
try
|
{
|
Map<String, Object> m = objectMapper.readValue(rec.getFormData(), new TypeReference<Map<String, Object>>() { });
|
if (m != null) { values.putAll(m); }
|
}
|
catch (Exception ignore) { }
|
return values;
|
}
|
|
/** mapper 行 {class_id,c} -> Map<classId,count> */
|
private Map<Long, Integer> countMap(List<Map<String, Object>> rows)
|
{
|
Map<Long, Integer> map = new HashMap<>();
|
if (rows != null)
|
{
|
for (Map<String, Object> 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<Map<String, Object>> buildOrgTree(Set<Long> allowClassIds)
|
{
|
List<Map<String, Object>> roots = new ArrayList<>();
|
|
CjCollege qCollege = new CjCollege();
|
qCollege.setStatus("0");
|
List<CjCollege> colleges = cjCollegeMapper.selectCjCollegeList(qCollege);
|
if (colleges == null) { return roots; }
|
|
// 一次性取专业/班级,内存分组,避免 N+1
|
CjMajor qMajor = new CjMajor();
|
qMajor.setStatus("0");
|
List<CjMajor> majors = cjMajorMapper.selectCjMajorList(qMajor);
|
CjClass qClass = new CjClass();
|
qClass.setStatus("0");
|
List<CjClass> classes = cjClassMapper.selectCjClassList(qClass);
|
|
Map<Long, List<Map<String, Object>>> 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<Long, List<Map<String, Object>>> 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<String, Object> collegeNode = node(college.getCollegeId(), college.getCollegeName());
|
List<Map<String, Object>> majorsOfCollege = collegeMajors.get(college.getCollegeId());
|
if (majorsOfCollege != null)
|
{
|
for (Map<String, Object> 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<String, Object> node(Long id, String name)
|
{
|
Map<String, Object> 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());
|
}
|
}
|