package com.ruoyi.system.controller; import java.util.List; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; 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.enums.BusinessType; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.system.domain.CjStudent; import com.ruoyi.system.domain.CjClass; import com.ruoyi.system.domain.CjCollege; import com.ruoyi.system.domain.CjMajor; import com.ruoyi.system.mapper.CjClassMapper; import com.ruoyi.system.mapper.CjCollegeMapper; import com.ruoyi.system.mapper.CjMajorMapper; import com.ruoyi.system.mapper.CjStudentBindMapper; import com.ruoyi.system.mapper.CjStudentMapper; import com.ruoyi.system.service.ICjStudentService; import com.ruoyi.system.service.MiniappTokenService; import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.core.page.TableDataInfo; /** * 学生花名册Controller * * 约定:学号全校不唯一、仅班级内唯一,学生唯一性 = 所属班级 + 学号; * Excel 导入/导出用“学院名称/专业名称/班级名称”引用班级,无需数字班级ID。 * * @author ruoyi * @date 2026-09-03 */ @RestController @RequestMapping("/system/student") public class CjStudentController extends BaseController { @Autowired private ICjStudentService cjStudentService; @Autowired private CjClassMapper cjClassMapper; @Autowired private CjCollegeMapper cjCollegeMapper; @Autowired private CjMajorMapper cjMajorMapper; @Autowired private CjStudentBindMapper cjStudentBindMapper; @Autowired private CjStudentMapper cjStudentMapper; @Autowired private MiniappTokenService miniappTokenService; @Autowired private RedisCache redisCache; /** * 全校视野:超管(约定 admin 的 userId 恒为 1,或 admin 角色)或「教务处」角色。 * 教务处账号(jwc01)不是 userId=1、也没有 manager_user_id 班级, * 若仍按 userId 判定会看到空花名册,故按角色放行。 */ 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集合;超管返回 null 表示不限 */ 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 boolean canManage(CjStudent stu) { if (stu == null || stu.getClassId() == null) { return false; } if (isSchoolWide()) { return true; } return myClassIds().contains(stu.getClassId()); } /** 班级+学号 查已有学生(判重依据:班级内学号唯一) */ private CjStudent findByClassAndNo(Long classId, String studentNo) { if (classId == null || studentNo == null) { return null; } CjStudent q = new CjStudent(); q.setClassId(classId); q.setStudentNo(studentNo); List list = cjStudentService.selectCjStudentList(q); return list.isEmpty() ? null : list.get(0); } /** 性别码转文本(导出展示列) */ private String genderToText(String gender) { if (gender == null) { return ""; } if ("0".equals(gender)) { return "男"; } if ("1".equals(gender)) { return "女"; } if ("2".equals(gender)) { return "未知"; } return ""; } /** 非法性别标记 */ private static final String GENDER_INVALID = "GENDER_INVALID"; /** 解析导入的性别列(兼容 男/女/未知 与 0/1/2);空白返回 null;非法返回 GENDER_INVALID */ private String parseGender(String text) { if (text == null) { return null; } String t = text.trim(); if (t.isEmpty()) { return null; } if ("男".equals(t) || "0".equals(t)) { return "0"; } if ("女".equals(t) || "1".equals(t)) { return "1"; } if ("未知".equals(t) || "2".equals(t)) { return "2"; } return GENDER_INVALID; } /** * 导出/展示前回填 学院/专业/班级名称 与 性别中文(transient Excel 列) */ private void fillStudentForExcel(List list) { if (list == null || list.isEmpty()) { return; } Map classMap = new HashMap<>(); List classes = cjClassMapper.selectCjClassList(new CjClass()); if (classes != null) { for (CjClass c : classes) { classMap.put(c.getClassId(), c); } } Map majorMap = new HashMap<>(); List majors = cjMajorMapper.selectCjMajorList(new CjMajor()); if (majors != null) { for (CjMajor m : majors) { majorMap.put(m.getMajorId(), m); } } Map collegeMap = new HashMap<>(); List colleges = cjCollegeMapper.selectCjCollegeList(new CjCollege()); if (colleges != null) { for (CjCollege c : colleges) { collegeMap.put(c.getCollegeId(), c.getCollegeName()); } } for (CjStudent s : list) { s.setGenderText(genderToText(s.getGender())); if (s.getClassId() == null) { continue; } CjClass cl = classMap.get(s.getClassId()); if (cl == null) { continue; } s.setClassName(cl.getClassName()); if (cl.getMajorId() != null) { CjMajor m = majorMap.get(cl.getMajorId()); if (m != null) { s.setMajorName(m.getMajorName()); if (m.getCollegeId() != null) { s.setCollegeName(collegeMap.get(m.getCollegeId())); } } } } } /** * 查询学生花名册列表(数据权限:教师仅见本班) */ @PreAuthorize("@ss.hasPermi('system:student:list')") @GetMapping("/list") public TableDataInfo list(CjStudent cjStudent) { // 非超管(admin=1)时,仅返回其管理班级的学生 if (!isSchoolWide()) { List classIds = myClassIds(); if (classIds.isEmpty()) { return getDataTable(new ArrayList<>()); } cjStudent.setClassIds(classIds); } startPage(); List list = cjStudentService.selectCjStudentList(cjStudent); // 手机号脱敏(列表展示 138****1234) list.forEach(s -> { if (s.getPhone() != null && s.getPhone().length() == 11) { s.setPhone(s.getPhone().substring(0, 3) + "****" + s.getPhone().substring(7)); } }); return getDataTable(list); } /** * 查询当前教师可管理的班级(新增/查询“我的班”下拉源;超管返回空,走全校级联) */ @PreAuthorize("@ss.hasPermi('system:student:list')") @GetMapping("/myClasses") public AjaxResult myClasses() { if (isSchoolWide()) { return success(new ArrayList<>()); } return success(myClassIdsForSelect()); } private List myClassIdsForSelect() { CjClass q = new CjClass(); q.setManagerUserId(SecurityUtils.getUserId()); return cjClassMapper.selectCjClassList(q); } /** * 导出学生花名册列表(教师仅导出本班;导出列含学院/专业/班级名称与性别文本) */ @PreAuthorize("@ss.hasPermi('system:student:export')") @Log(title = "学生花名册", businessType = BusinessType.EXPORT) @PostMapping("/export") public void export(HttpServletResponse response, CjStudent cjStudent) { List list; if (!isSchoolWide()) { List classIds = myClassIds(); if (classIds.isEmpty()) { ExcelUtil emptyUtil = new ExcelUtil(CjStudent.class); emptyUtil.exportExcel(response, new ArrayList<>(), "学生花名册数据"); return; } cjStudent.setClassIds(classIds); } list = cjStudentService.selectCjStudentList(cjStudent); fillStudentForExcel(list); ExcelUtil util = new ExcelUtil(CjStudent.class); util.exportExcel(response, list, "学生花名册数据"); } /** * 获取学生花名册详细信息 */ @PreAuthorize("@ss.hasPermi('system:student:query')") @GetMapping(value = "/{studentId}") public AjaxResult getInfo(@PathVariable("studentId") Long studentId) { CjStudent stu = cjStudentService.selectCjStudentByStudentId(studentId); if (!isSchoolWide() && !canManage(stu)) { return error("无权访问其他班级学生数据"); } return success(stu); } /** * 新增学生花名册(教师仅可在本班新增;同班学号不得重复) */ @PreAuthorize("@ss.hasPermi('system:student:add')") @Log(title = "学生花名册", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@RequestBody CjStudent cjStudent) { if (!isSchoolWide() && !myClassIds().contains(cjStudent.getClassId())) { return error("只能在本班新增学生"); } if (findByClassAndNo(cjStudent.getClassId(), cjStudent.getStudentNo()) != null) { return error("该班级已存在相同学号的学生(学号仅班级内唯一),请核对"); } return toAjax(cjStudentService.insertCjStudent(cjStudent)); } /** * 修改学生花名册(教师仅可维护本班学生,且不能改到他人班级;班级+学号不可重复) */ @PreAuthorize("@ss.hasPermi('system:student:edit')") @Log(title = "学生花名册", businessType = BusinessType.UPDATE) @PutMapping public AjaxResult edit(@RequestBody CjStudent cjStudent) { if (!isSchoolWide()) { CjStudent old = cjStudentService.selectCjStudentByStudentId(cjStudent.getStudentId()); if (!canManage(old)) { return error("无权操作其他班级学生"); } if (cjStudent.getClassId() != null && !myClassIds().contains(cjStudent.getClassId())) { return error("只能将学生保留在本人管理的班级"); } } CjStudent same = findByClassAndNo(cjStudent.getClassId(), cjStudent.getStudentNo()); if (same != null && !same.getStudentId().equals(cjStudent.getStudentId())) { return error("该班级已存在相同学号的学生(学号仅班级内唯一),请核对"); } int rows = cjStudentService.updateCjStudent(cjStudent); // 修正学号/姓名/手机号后清掉自助通道的失败锁,避免学生被历史尝试锁在门外 if (rows > 0 && cjStudent.getStudentNo() != null && cjStudent.getName() != null) { redisCache.deleteObject(MiniappTokenService.pwdErrKey(cjStudent.getStudentNo(), cjStudent.getName())); redisCache.deleteObject(MiniappTokenService.resetErrKey(cjStudent.getStudentNo(), cjStudent.getName())); } return toAjax(rows); } /** * 删除学生花名册(引用保护:已微信绑定的学生禁止删除,请改用停用) */ @PreAuthorize("@ss.hasPermi('system:student:remove')") @Log(title = "学生花名册", businessType = BusinessType.DELETE) @DeleteMapping("/{studentIds}") public AjaxResult remove(@PathVariable Long[] studentIds) { if (studentIds == null || studentIds.length == 0) { return toAjax(0); } if (!isSchoolWide()) { for (Long id : studentIds) { CjStudent s = cjStudentService.selectCjStudentByStudentId(id); if (!canManage(s)) { return error("无权操作其他班级学生"); } } } if (cjStudentBindMapper.countByStudentIds(studentIds) > 0) { return error("所选学生中已存在微信绑定记录,禁止删除;请改用“停用”"); } return toAjax(cjStudentService.deleteCjStudentByStudentIds(studentIds)); } /** * 学生停用/启用(0正常 1停用) */ @PreAuthorize("@ss.hasPermi('system:student:status')") @Log(title = "学生花名册", businessType = BusinessType.UPDATE) @PutMapping("/changeStatus") public AjaxResult changeStatus(@RequestBody CjStudent cjStudent) { if (cjStudent.getStudentId() == null) { return error("缺少学生ID"); } if (!isSchoolWide()) { CjStudent old = cjStudentService.selectCjStudentByStudentId(cjStudent.getStudentId()); if (!canManage(old)) { return error("无权操作其他班级学生"); } } CjStudent update = new CjStudent(); update.setStudentId(cjStudent.getStudentId()); update.setStatus(cjStudent.getStatus()); update.setUpdateBy(getUsername()); return toAjax(cjStudentService.updateCjStudent(update)); } /** * 重置学生登录密码为初始密码(AA+学号),并置“下次登录强制改密”; * 同时作废该生全部在线会话与登录失败计数。教师仅限本班学生。 */ @PreAuthorize("@ss.hasPermi('system:student:resetPwd')") @Log(title = "学生账号", businessType = BusinessType.UPDATE) @PutMapping("/resetPwd/{studentId}") public AjaxResult resetPwd(@PathVariable("studentId") Long studentId) { CjStudent stu = cjStudentService.selectCjStudentByStudentId(studentId); if (stu == null) { return error("学生不存在"); } if (!canManage(stu)) { return error("无权操作其他班级学生"); } String initPwd = initialPassword(stu.getStudentNo()); // 从未激活过的学生被重置:视为激活,补 register_time,避免"已激活但激活时间为空" boolean firstActivate = cjStudentMapper.selectPasswordByStudentId(studentId) == null; cjStudentMapper.updateStudentPassword(studentId, SecurityUtils.encryptPassword(initPwd), "1", firstActivate ? new java.util.Date() : null); miniappTokenService.invalidateAll(studentId); // 解除该生的登录失败锁与自助重置锁,否则学生拿到初始密码也可能被锁在门外 redisCache.deleteObject(MiniappTokenService.pwdErrKey(stu.getStudentNo(), stu.getName())); redisCache.deleteObject(MiniappTokenService.resetErrKey(stu.getStudentNo(), stu.getName())); return success("密码已重置为初始密码(AA+学号),学生下次登录须修改密码"); } /** * 强制下线:作废该学生全部小程序/H5 登录会话(下次请求即需重新登录)。教师仅限本班学生。 */ @PreAuthorize("@ss.hasPermi('system:student:forceLogout')") @Log(title = "学生账号", businessType = BusinessType.UPDATE) @DeleteMapping("/forceLogout/{studentId}") public AjaxResult forceLogout(@PathVariable("studentId") Long studentId) { CjStudent stu = cjStudentService.selectCjStudentByStudentId(studentId); if (stu == null) { return error("学生不存在"); } if (!canManage(stu)) { return error("无权操作其他班级学生"); } miniappTokenService.invalidateAll(studentId); return success("已强制下线"); } /** 初始密码规则:AA + 完整学号(满足“8-20位且含字母和数字”的密码策略) */ private String initialPassword(String studentNo) { return "AA" + (studentNo == null ? "" : studentNo.trim()); } /** * 导入学生花名册 * 模板列:所属学院 / 所属专业 / 班级名称 / 学号 / 姓名 / 性别(男/女) / 联系电话 * 判重键:所属班级 + 学号(班级内学号唯一);班级通过 学院→专业→班级名称 三级解析。 * @param file 上传的 Excel * @param updateSupport 是否覆盖更新已存在数据(true=覆盖) * @return {新增X 更新Y 失败Z + 失败名单} */ @PreAuthorize("@ss.hasPermi('system:student:import')") @Log(title = "学生花名册", businessType = BusinessType.IMPORT) @PostMapping("/importData") public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception { ExcelUtil util = new ExcelUtil(CjStudent.class); List studentList = util.importExcel(file.getInputStream()); String message; int insertCount = 0, updateCount = 0, failCount = 0; StringBuilder failMsg = new StringBuilder(); String operName = getUsername(); for (int i = 0; i < studentList.size(); i++) { CjStudent stu = studentList.get(i); int rowNo = i + 1; try { String collegeName = trim(stu.getCollegeName()); String majorName = trim(stu.getMajorName()); String className = trim(stu.getClassName()); String studentNo = trim(stu.getStudentNo()); String stuName = trim(stu.getName()); if (collegeName == null) { failCount++; failMsg.append("第" + rowNo + "条: 所属学院不能为空; "); continue; } if (majorName == null) { failCount++; failMsg.append("第" + rowNo + "条: 所属专业不能为空; "); continue; } if (className == null) { failCount++; failMsg.append("第" + rowNo + "条: 班级名称不能为空; "); continue; } if (studentNo == null) { failCount++; failMsg.append("第" + rowNo + "条: 学号不能为空; "); continue; } if (stuName == null) { failCount++; failMsg.append("第" + rowNo + "条: 姓名不能为空; "); continue; } // 三级名称解析到班级(父级须启用中;班级须已存在且启用) List colleges = cjCollegeMapper.selectEnabledByCollegeName(collegeName); if (colleges.isEmpty()) { failCount++; failMsg.append("第" + rowNo + "条: 所属学院“" + collegeName + "”不存在或已停用; "); continue; } if (colleges.size() > 1) { failCount++; failMsg.append("第" + rowNo + "条: 所属学院“" + collegeName + "”在系统中重复,请先清理; "); continue; } Long collegeId = colleges.get(0).getCollegeId(); List majors = cjMajorMapper.selectEnabledByCollegeIdAndName(collegeId, majorName); if (majors.isEmpty()) { failCount++; failMsg.append("第" + rowNo + "条: 所属专业“" + majorName + "”在学院“" + collegeName + "”下不存在或已停用; "); continue; } if (majors.size() > 1) { failCount++; failMsg.append("第" + rowNo + "条: 专业“" + majorName + "”在该学院下重复,请先清理; "); continue; } Long majorId = majors.get(0).getMajorId(); List classes = cjClassMapper.selectByMajorIdAndName(majorId, className); if (classes.isEmpty()) { failCount++; failMsg.append("第" + rowNo + "条: 班级“" + className + "”在专业“" + majorName + "”下不存在,请先在班级管理中建立; "); continue; } if (classes.size() > 1) { failCount++; failMsg.append("第" + rowNo + "条: 班级“" + className + "”在该专业下重复,无法定位; "); continue; } CjClass cl = classes.get(0); if (cl.getStatus() != null && !"0".equals(cl.getStatus())) { failCount++; failMsg.append("第" + rowNo + "条: 班级“" + className + "”已停用,无法导入学生; "); continue; } // 性别兼容 男/女/未知 与 0/1/2 String gender = parseGender(stu.getGenderText()); if (GENDER_INVALID.equals(gender)) { failCount++; failMsg.append("第" + rowNo + "条: 学号" + studentNo + " 性别“" + stu.getGenderText() + "”无法识别(请填男/女/未知); "); continue; } stu.setCollegeName(collegeName); stu.setMajorName(majorName); stu.setClassName(className); stu.setClassId(cl.getClassId()); stu.setStudentNo(studentNo); stu.setName(stuName); stu.setGender(gender); if (stu.getPhone() != null) { stu.setPhone(stu.getPhone().trim()); } // 判重:班级 + 学号 CjStudent exists = findByClassAndNo(cl.getClassId(), studentNo); if (exists == null) { if (stu.getStatus() == null) { stu.setStatus("0"); } stu.setCreateBy(operName); cjStudentService.insertCjStudent(stu); insertCount++; } else if (updateSupport) { // 覆盖导入时电话列为空 = 保留原登记号码(写成空串会静默废掉学生的三要素自助找回密码) if (stu.getPhone() != null && stu.getPhone().trim().isEmpty()) { stu.setPhone(null); } stu.setStudentId(exists.getStudentId()); stu.setUpdateBy(operName); cjStudentService.updateCjStudent(stu); updateCount++; } else { failCount++; failMsg.append("第" + rowNo + "条: 学号" + studentNo + "在班级“" + className + "”中已存在; "); } } catch (Exception e) { failCount++; failMsg.append("第" + rowNo + "条" + (stu.getStudentNo() != null ? "(学号" + stu.getStudentNo() + ")" : "") + "(导入失败:" + e.getMessage() + "); "); } } if (updateSupport) message = String.format("导入完成!新增 %d 条,更新 %d 条", insertCount, updateCount); else message = String.format("导入完成!新增 %d 条,由于数据重复导致导入失败 %d 条", insertCount, failCount); Map data = new HashMap<>(); data.put("message", message); data.put("insertCount", insertCount); data.put("updateCount", updateCount); data.put("failCount", failCount); data.put("failMsg", failMsg.toString()); return AjaxResult.success(data); } /** 字符串去首尾空白;null/空返回 null */ private String trim(String s) { return (s == null || s.trim().isEmpty()) ? null : s.trim(); } /** * 下载学生花名册导入模板 */ @PreAuthorize("@ss.hasPermi('system:student:import')") @PostMapping("/importTemplate") public void importTemplate(HttpServletResponse response) throws Exception { ExcelUtil util = new ExcelUtil(CjStudent.class); util.importTemplateExcel(response, "学生花名册导入模板"); } }