This commit is contained in:
zlwu committed 2026-07-22 14:14:34 +08:00
1 parent f3869785ff
commit 4ada3c6f01
8 files changed
+724 -5

No files matched your search

@@ -1,27 +1,41 @@
package org.dromara.xg.controller; package org.dromara.xg.controller;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*; import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;
import org.dromara.common.idempotent.annotation.RepeatSubmit; import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log; import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController; import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.validate.AddGroup; import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup; import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType; import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.core.DropDownOptions;
import org.dromara.common.excel.core.ExcelResult;
import org.dromara.common.excel.utils.ExcelUtil; import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.xg.domain.vo.XgClassMsgImportVo;
import org.dromara.xg.domain.vo.XgClassMsgVo; import org.dromara.xg.domain.vo.XgClassMsgVo;
import org.dromara.xg.domain.vo.XgSysDeptVo;
import org.dromara.xg.domain.bo.XgClassMsgBo; import org.dromara.xg.domain.bo.XgClassMsgBo;
import org.dromara.xg.domain.bo.XgClassStatusBo; import org.dromara.xg.domain.bo.XgClassStatusBo;
import org.dromara.xg.domain.bo.XgClassStatusBatchBo; import org.dromara.xg.domain.bo.XgClassStatusBatchBo;
import org.dromara.xg.excel.XgClassMsgImportTemplateWriter;
import org.dromara.xg.listener.XgClassMsgImportListener;
import org.dromara.xg.service.IXgClassMsgService; import org.dromara.xg.service.IXgClassMsgService;
import org.dromara.xg.service.IXgSysDeptService;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
@@ -31,6 +45,7 @@ import org.dromara.common.satoken.utils.LoginHelper;
* @author zlwu * @author zlwu
* @date 2025-07-23 * @date 2025-07-23
*/ */
@Slf4j
@Validated @Validated
@RequiredArgsConstructor @RequiredArgsConstructor
@RestController @RestController
@@ -38,6 +53,7 @@ import org.dromara.common.satoken.utils.LoginHelper;
public class XgClassMsgController extends BaseController { public class XgClassMsgController extends BaseController {
private final IXgClassMsgService xgClassMsgService; private final IXgClassMsgService xgClassMsgService;
private final IXgSysDeptService xgSysDeptService;
/** /**
* 查询班级信息-新版列表 * 查询班级信息-新版列表
@@ -146,4 +162,91 @@ public class XgClassMsgController extends BaseController {
List<XgClassMsgVo> list = xgClassMsgService.queryList(bo); List<XgClassMsgVo> list = xgClassMsgService.queryList(bo);
return R.ok(list); return R.ok(list);
} }
/**
* 下载班级信息导入模板(含字典下拉、学院-专业联动下拉)
*/
// @SaCheckPermission("classMsg:classMsg:import")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
try {
List<XgClassMsgImportVo> templateData = new ArrayList<>();
XgClassMsgImportVo example = new XgClassMsgImportVo();
int currentYear = java.time.Year.now().getValue();
example.setClassName("示例:" + currentYear + "计算机1班");
example.setClassType("1");
example.setGrade(String.valueOf(currentYear));
example.setEnrollmentYear((long) currentYear);
example.setSchoolSystem(3L);
// 毕业年份由模板公式自动计算:入学年份+学制
example.setGraduationYear(null);
example.setIsGraduation(0L);
example.setClassLocation("教学楼A101");
example.setClassStatus("1");
example.setClassDescription("示例数据,导入前请删除或修改");
example.setRemark("班主任选填:姓名唯一匹配才写入");
templateData.add(example);
List<DropDownOptions> options = buildImportDropDownOptions();
XgClassMsgImportTemplateWriter.write(templateData, response, options);
} catch (Exception e) {
log.error("生成班级信息导入模板失败", e);
throw new ServiceException("生成模板失败: " + e.getMessage());
}
}
/**
* 导入班级信息
*/
// @SaCheckPermission("classMsg:classMsg:import")
@Log(title = "班级信息-新版", businessType = BusinessType.IMPORT)
@PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<Void> importData(@RequestPart("file") MultipartFile file) throws Exception {
ExcelResult<XgClassMsgImportVo> result = ExcelUtil.importExcel(
file.getInputStream(),
XgClassMsgImportVo.class,
new XgClassMsgImportListener());
return R.ok(result.getAnalysis());
}
/**
* 构建导入模板下拉:学院-专业联动 + 常见学制
*/
private List<DropDownOptions> buildImportDropDownOptions() {
List<XgSysDeptVo> collegeList = xgSysDeptService.selectCollegeList();
List<XgSysDeptVo> majorList = new ArrayList<>();
for (XgSysDeptVo college : collegeList) {
List<XgSysDeptVo> majors = xgSysDeptService.selectMajorListByCollegeId(college.getDeptId());
if (majors != null) {
majorList.addAll(majors);
}
}
int collegeIndex = 0;
int majorIndex = 1;
DropDownOptions collegeToMajor = DropDownOptions.buildLinkedOptions(
collegeList,
collegeIndex,
majorList,
majorIndex,
XgSysDeptVo::getDeptId,
XgSysDeptVo::getParentId,
this::buildDeptOptionValue
);
// 学制常见可选值
DropDownOptions schoolSystemOptions = new DropDownOptions(6, Arrays.asList("2", "3", "4", "5", "6"));
List<DropDownOptions> options = new ArrayList<>();
options.add(collegeToMajor);
options.add(schoolSystemOptions);
return options;
}
/**
* 生成学院/专业下拉选项值:仅展示名称(导入时按名称解析为 ID)
*/
private String buildDeptOptionValue(XgSysDeptVo dept) {
return StringUtils.blankToDefault(dept.getDeptName(), "未命名");
}
} }
@@ -0,0 +1,128 @@
package org.dromara.xg.domain.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.annotation.ExcelNotation;
import org.dromara.common.excel.convert.ExcelDictConvert;
import java.io.Serial;
import java.io.Serializable;
/**
* 班级信息导入VO
*
* @author zlwu
*/
@Data
@ExcelIgnoreUnannotated
public class XgClassMsgImportVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 学院(下拉:学院名称,与专业联动)
*/
@ExcelProperty(value = "学院", index = 0)
@ColumnWidth(24)
@NotBlank(message = "学院不能为空")
private String college;
/**
* 专业(下拉:专业名称,随学院联动)
*/
@ExcelProperty(value = "专业", index = 1)
@ColumnWidth(30)
@NotBlank(message = "专业不能为空")
private String major;
/**
* 班级名称(同时作为班级代码)
*/
@ExcelProperty(value = "班级名称", index = 2)
@NotBlank(message = "班级名称不能为空")
private String className;
/**
* 学历层级(字典下拉:zsjy_education_level)
*/
@ExcelProperty(value = "学历层级", index = 3, converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "zsjy_education_level")
@ColumnWidth(20)
@NotBlank(message = "学历层级不能为空")
private String classType;
/**
* 年级
*/
@ExcelProperty(value = "年级", index = 4)
@NotBlank(message = "年级不能为空")
private String grade;
/**
* 入学年份
*/
@ExcelProperty(value = "入学年份", index = 5)
@NotNull(message = "入学年份不能为空")
private Long enrollmentYear;
/**
* 学制(年)
*/
@ExcelProperty(value = "学制(年)", index = 6)
@NotNull(message = "学制不能为空")
private Long schoolSystem;
/**
* 毕业年份(模板内公式:入学年份+学制;可不填,导入时也会自动计算)
*/
@ExcelProperty(value = "毕业年份", index = 7)
@ExcelNotation(value = "自动计算:入学年份+学制,也可留空由系统计算")
private Long graduationYear;
/**
* 是否已毕业(字典下拉:is_graduation)
*/
@ExcelProperty(value = "是否已毕业", index = 8, converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "is_graduation")
@NotNull(message = "是否已毕业不能为空")
private Long isGraduation;
/**
* 班级教室位置
*/
@ExcelProperty(value = "班级教室位置", index = 9)
private String classLocation;
/**
* 班级状态(字典下拉:xg_class_status)
*/
@ExcelProperty(value = "班级状态", index = 10, converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "xg_class_status")
@NotBlank(message = "班级状态不能为空")
private String classStatus;
/**
* 班主任(选填,填写姓名;仅唯一匹配时写入,不影响班级保存)
*/
@ExcelProperty(value = "班主任", index = 11)
@ExcelNotation(value = "选填,填写姓名。匹配到唯一人员才写入;未匹配或多人同名则不写入,不影响班级导入")
private String classTeacherName;
/**
* 班级描述
*/
@ExcelProperty(value = "班级描述", index = 12)
private String classDescription;
/**
* 备注信息
*/
@ExcelProperty(value = "备注信息", index = 13)
private String remark;
}
@@ -0,0 +1,291 @@
package org.dromara.xg.listener;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.http.HtmlUtil;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.event.AnalysisEventListener;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.excel.core.DropDownOptions;
import org.dromara.common.excel.core.ExcelListener;
import org.dromara.common.excel.core.ExcelResult;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.xg.domain.XgClassMsg;
import org.dromara.xg.domain.XgSysDept;
import org.dromara.xg.domain.XgSysUser;
import org.dromara.xg.domain.bo.XgClassMsgBo;
import org.dromara.xg.domain.bo.XgClassTeacherCounselorRelBo;
import org.dromara.xg.domain.vo.XgClassMsgImportVo;
import org.dromara.xg.domain.vo.XgSysDeptVo;
import org.dromara.xg.mapper.XgClassMsgMapper;
import org.dromara.xg.mapper.XgSysDeptMapper;
import org.dromara.xg.mapper.XgSysUserMapper;
import org.dromara.xg.service.IXgClassMsgService;
import org.dromara.xg.service.IXgClassTeacherCounselorRelService;
import org.dromara.xg.service.IXgSysDeptService;
import java.util.Date;
import java.util.List;
/**
* 班级信息导入监听器
*
* @author zlwu
*/
@Slf4j
public class XgClassMsgImportListener extends AnalysisEventListener<XgClassMsgImportVo>
implements ExcelListener<XgClassMsgImportVo> {
private final IXgClassMsgService classMsgService;
private final IXgSysDeptService deptService;
private final IXgClassTeacherCounselorRelService classTeacherCounselorRelService;
private final XgSysDeptMapper deptMapper;
private final XgClassMsgMapper classMsgMapper;
private final XgSysUserMapper sysUserMapper;
private final Long operUserId;
private int successNum = 0;
private int failureNum = 0;
private final StringBuilder successMsg = new StringBuilder();
private final StringBuilder failureMsg = new StringBuilder();
public XgClassMsgImportListener() {
this.classMsgService = SpringUtils.getBean(IXgClassMsgService.class);
this.deptService = SpringUtils.getBean(IXgSysDeptService.class);
this.classTeacherCounselorRelService = SpringUtils.getBean(IXgClassTeacherCounselorRelService.class);
this.deptMapper = SpringUtils.getBean(XgSysDeptMapper.class);
this.classMsgMapper = SpringUtils.getBean(XgClassMsgMapper.class);
this.sysUserMapper = SpringUtils.getBean(XgSysUserMapper.class);
this.operUserId = LoginHelper.getUserId();
}
@Override
public void invoke(XgClassMsgImportVo importVo, AnalysisContext context) {
Integer rowIndex = context.readRowHolder().getRowIndex() + 1;
try {
if (isBlankRow(importVo)) {
return;
}
// 跳过模板自带的示例行
if (StringUtils.isNotBlank(importVo.getClassName()) && importVo.getClassName().startsWith("示例")) {
return;
}
ValidatorUtils.validate(importVo);
Long collegeId = resolveCollegeId(importVo.getCollege());
XgSysDept college = deptMapper.selectById(collegeId);
if (college == null || !"0".equals(college.getDelFlag())) {
throw new ServiceException("学院不存在或已停用:" + importVo.getCollege());
}
if (college.getParentId() == null || college.getParentId() != 101L) {
throw new ServiceException("所选不是有效学院:" + importVo.getCollege());
}
Long majorId = resolveMajorId(importVo.getMajor(), collegeId);
XgSysDept major = deptMapper.selectById(majorId);
if (major == null || !"0".equals(major.getDelFlag())) {
throw new ServiceException("专业不存在或已停用:" + importVo.getMajor());
}
if (!collegeId.equals(major.getParentId())) {
throw new ServiceException("专业不属于所选学院,请重新从下拉框选择学院和专业");
}
String className = StringUtils.trim(importVo.getClassName());
LambdaQueryWrapper<XgClassMsg> existWrapper = Wrappers.lambdaQuery();
existWrapper.and(w -> w.eq(XgClassMsg::getClassName, className).or().eq(XgClassMsg::getClassCode, className));
Long existCount = classMsgMapper.selectCount(existWrapper);
if (existCount != null && existCount > 0) {
throw new ServiceException("班级名称已存在:" + className);
}
XgClassMsgBo bo = new XgClassMsgBo();
bo.setCollegeId(collegeId);
bo.setCollegeName(college.getDeptName());
bo.setMajorId(majorId);
bo.setMajorName(major.getDeptName());
bo.setClassName(className);
bo.setClassCode(className);
bo.setClassType(importVo.getClassType());
bo.setGrade(StringUtils.trim(importVo.getGrade()));
bo.setEnrollmentYear(importVo.getEnrollmentYear());
bo.setSchoolSystem(importVo.getSchoolSystem());
// 统一按入学年份+学制计算,与模板公式一致
Long graduationYear = importVo.getEnrollmentYear() + importVo.getSchoolSystem();
bo.setGraduationYear(graduationYear);
bo.setIsGraduation(importVo.getIsGraduation());
bo.setClassLocation(importVo.getClassLocation());
bo.setClassStatus(importVo.getClassStatus());
bo.setClassDescription(importVo.getClassDescription());
bo.setRemark(importVo.getRemark());
bo.setCreateBy(operUserId);
classMsgService.insertByBo(bo);
String teacherTip = bindClassTeacherIfUnique(bo.getId(), importVo.getClassTeacherName());
successNum++;
successMsg.append("<br/>").append(successNum).append("、第").append(rowIndex)
.append("行班级 ").append(className).append(" 导入成功");
if (StringUtils.isNotBlank(teacherTip)) {
successMsg.append("(").append(teacherTip).append(")");
}
} catch (Exception e) {
failureNum++;
String msg = e.getMessage();
if (e instanceof ConstraintViolationException cvException) {
msg = StreamUtils.join(cvException.getConstraintViolations(), ConstraintViolation::getMessage, ", ");
}
String className = importVo == null ? "" : StringUtils.blankToDefault(importVo.getClassName(), "");
failureMsg.append("<br/>").append(failureNum).append("、第").append(rowIndex)
.append("行班级 ").append(HtmlUtil.cleanHtmlTag(className)).append(" 导入失败:")
.append(HtmlUtil.cleanHtmlTag(StringUtils.blankToDefault(msg, e.getClass().getSimpleName())));
log.warn("班级信息导入失败,行号: {}, 原因: {}", rowIndex, msg, e);
}
}
/**
* 按姓名匹配班主任:仅唯一命中时写入;空值/未匹配/多人同名均跳过,不影响班级保存。
*
* @return 提示文案,无需提示时返回空
*/
private String bindClassTeacherIfUnique(Long classId, String teacherNameRaw) {
String teacherName = StringUtils.trim(teacherNameRaw);
if (StringUtils.isBlank(teacherName) || classId == null) {
return null;
}
try {
List<XgSysUser> users = sysUserMapper.selectActiveStaffByNickName(teacherName);
if (users == null || users.isEmpty()) {
log.info("班级导入班主任未匹配到人员,classId={}, name={}", classId, teacherName);
return "班主任「" + teacherName + "」未匹配到人员,已跳过";
}
if (users.size() > 1) {
log.info("班级导入班主任姓名匹配到多人,跳过设置,classId={}, name={}, count={}",
classId, teacherName, users.size());
return "班主任「" + teacherName + "」匹配到多人,已跳过";
}
XgSysUser teacher = users.get(0);
XgClassTeacherCounselorRelBo relBo = new XgClassTeacherCounselorRelBo();
relBo.setClassId(classId);
relBo.setTeacherId(teacher.getUserId());
relBo.setTeacherName(teacher.getNickName());
relBo.setTeacherType("class_teacher");
relBo.setStartDate(new Date());
classTeacherCounselorRelService.insertByBo(relBo);
return "已设置班主任「" + teacher.getNickName() + "」";
} catch (Exception e) {
// 班主任设置失败不影响班级导入结果
log.warn("班级导入后设置班主任失败,classId={}, name={}, cause={}", classId, teacherName, e.getMessage(), e);
return "班主任设置失败已跳过";
}
}
private boolean isBlankRow(XgClassMsgImportVo importVo) {
if (importVo == null) {
return true;
}
// 毕业年份、班主任为辅助字段,不参与空行判断
return StringUtils.isAllBlank(
importVo.getCollege(),
importVo.getMajor(),
importVo.getClassName(),
importVo.getClassType(),
importVo.getGrade(),
importVo.getClassStatus(),
importVo.getClassLocation(),
importVo.getClassDescription(),
importVo.getRemark()
) && importVo.getEnrollmentYear() == null
&& importVo.getSchoolSystem() == null
&& importVo.getIsGraduation() == null;
}
private Long resolveCollegeId(String optionValue) {
Long id = parseOptionId(optionValue);
if (id != null) {
return id;
}
String name = StringUtils.trim(optionValue);
return deptService.selectCollegeList().stream()
.filter(item -> name.equals(item.getDeptName()))
.map(XgSysDeptVo::getDeptId)
.findFirst()
.orElseThrow(() -> new ServiceException("未找到学院:" + name + ",请从模板下拉框中选择"));
}
private Long resolveMajorId(String optionValue, Long collegeId) {
Long id = parseOptionId(optionValue);
if (id != null) {
return id;
}
String name = StringUtils.trim(optionValue);
return deptService.selectMajorListByCollegeId(collegeId).stream()
.filter(item -> name.equals(item.getDeptName()))
.map(XgSysDeptVo::getDeptId)
.findFirst()
.orElseThrow(() -> new ServiceException("未找到专业:" + name + ",请从模板下拉框中选择"));
}
/**
* 兼容旧模板「名称_ID」格式;新模板仅名称时返回 null,改走名称匹配。
*/
private Long parseOptionId(String optionValue) {
String value = StringUtils.trim(optionValue);
if (StringUtils.isBlank(value)) {
throw new ServiceException("学院/专业不能为空");
}
// 纯名称(无下划线)直接走名称匹配,避免误把名称解析为 ID
if (!value.contains("_")) {
return null;
}
List<String> parts = DropDownOptions.analyzeOptionValue(value);
if (!parts.isEmpty()) {
String idStr = parts.get(parts.size() - 1);
if (NumberUtil.isLong(idStr)) {
return Long.parseLong(idStr);
}
}
return null;
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// no-op
}
@Override
public ExcelResult<XgClassMsgImportVo> getExcelResult() {
return new ExcelResult<>() {
@Override
public List<XgClassMsgImportVo> getList() {
return null;
}
@Override
public List<String> getErrorList() {
return null;
}
@Override
public String getAnalysis() {
if (failureNum > 0) {
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString());
}
if (successNum == 0) {
throw new ServiceException("未读取到有效数据,请检查是否填写了班级信息");
}
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
return successMsg.toString();
}
};
}
}
@@ -28,4 +28,12 @@ public interface XgSysUserMapper extends BaseMapperPlus<XgSysUser, XgSysUser> {
*/ */
List<XgSysUser> selectActiveUserListByAccountType(String accountType); List<XgSysUser> selectActiveUserListByAccountType(String accountType);
/**
* 按昵称精确匹配有效教职工
*
* @param nickName 用户昵称(姓名)
* @return 匹配用户列表
*/
List<XgSysUser> selectActiveStaffByNickName(String nickName);
} }
@@ -34,4 +34,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
order by user_name asc, create_time desc order by user_name asc, create_time desc
</select> </select>
<!-- 按昵称精确匹配有效教职工(account_type=1) -->
<select id="selectActiveStaffByNickName" parameterType="String" resultMap="XgSysUserResult">
select user_id, user_name, nick_name, phonenumber, account_type
from sys_user
where status = '0' and del_flag = '0' and account_type = '1'
and nick_name = #{nickName}
order by user_id asc
</select>
</mapper> </mapper>
+1 -1
View File
@@ -174,7 +174,7 @@
| [学生信息收集批次管理API](./学生信息收集批次管理API.md) | 学生信息收集批次班级权限管理 | ✅ 完成 | 2025-01-27 | | [学生信息收集批次管理API](./学生信息收集批次管理API.md) | 学生信息收集批次班级权限管理 | ✅ 完成 | 2025-01-27 |
| [学生信息批次收集API](./学生信息批次收集API.md) | 学生信息批次收集管理(新增) | ✅ 完成 | 2025-01-27 | | [学生信息批次收集API](./学生信息批次收集API.md) | 学生信息批次收集管理(新增) | ✅ 完成 | 2025-01-27 |
| [学生学籍异动与换班级API](./学生学籍异动与换班级API.md) | 学籍异动与换班级(含复学+换班级事务接口) | ✅ 完成 | 2026-03-13 | | [学生学籍异动与换班级API](./学生学籍异动与换班级API.md) | 学籍异动与换班级(含复学+换班级事务接口) | ✅ 完成 | 2026-03-13 |
| [班级信息管理API](./班级信息管理API.md) | 班级基础信息增删改查 | ✅ 完成 | 2025-01-27 | | [班级信息管理API](./班级信息管理API.md) | 班级基础信息增删改查、Excel导入(字典下拉+学院专业联动) | ✅ 完成 | 2026-07-22 |
| [二级学院管理API](./二级学院管理API.md) | 二级学院组织信息管理 | ✅ 完成 | 2025-12-26 | | [二级学院管理API](./二级学院管理API.md) | 二级学院组织信息管理 | ✅ 完成 | 2025-12-26 |
| [专业信息管理API](./专业信息管理API.md) | 专业信息管理(树形菜单+列表) | ✅ 完成 | 2025-12-26 | | [专业信息管理API](./专业信息管理API.md) | 专业信息管理(树形菜单+列表) | ✅ 完成 | 2025-12-26 |
| [班级干部信息管理API](./班级干部信息管理API.md) | 班级干部信息管理(含用户角色关联) | ✅ 完成 | 2025-01-27 | | [班级干部信息管理API](./班级干部信息管理API.md) | 班级干部信息管理(含用户角色关联) | ✅ 完成 | 2025-01-27 |
@@ -0,0 +1,73 @@
# 班级信息管理 API
**模块名称**:classMsg / 班级信息
**基础路径**:`/classMsg/classMsg`
**更新时间**:2026-07-22
## 模块功能
班级基础信息的增删改查、状态批量变更,以及 Excel 批量导入(模板含字典下拉、学院-专业联动下拉)。
## 导入相关接口
### 1. 下载导入模板
- **URL**:`POST /classMsg/classMsg/importTemplate`
- **说明**:生成带下拉校验的 Excel 模板
- **模板列**:
| 列名 | 填写方式 | 说明 |
|------|----------|------|
| 学院 | 下拉(联动) | 仅显示学院名称,请从下拉选择;列已加宽 |
| 专业 | 下拉(联动) | 随学院变化,仅显示专业名称;列已加宽 |
| 班级名称 | 文本 | 必填;同时作为班级代码写入系统 |
| 学历层级 | 字典下拉 | `zsjy_education_level` 中文标签;列已加宽 |
| 年级 | 文本 | 如当前年份 |
| 入学年份 | 数字 | 如当前年份 |
| 学制(年) | 下拉 | 常见值 2/3/4/5/6 |
| 毕业年份 | 公式自动计算 | Excel 公式:`入学年份+学制`;导入时同样按此规则计算 |
| 是否已毕业 | 字典下拉 | `is_graduation`(未毕业/已毕业) |
| 班级教室位置 | 文本 | 可选 |
| 班级状态 | 字典下拉 | `xg_class_status`(正常/暂停/毕业/解散) |
| 班主任 | 文本(姓名) | 选填;按教职工姓名精确匹配。唯一命中才写入;未匹配或多人同名则跳过,**不影响班级保存** |
| 班级描述 | 文本 | 可选 |
| 备注信息 | 文本 | 可选 |
- **注意**:
- 模板首行示例数据以「示例」开头,导入时会自动跳过
- 学院、专业、字典项请务必从下拉框选择,避免手填错误
- 班主任匹配失败不会导致该行班级导入失败,可事后在列表中单独设置
### 2. 导入班级数据
- **URL**:`POST /classMsg/classMsg/importData`
- **Content-Type**:`multipart/form-data`
- **参数**:`file`(Excel 文件)
- **成功响应**:`R`,`msg` 为导入成功明细(HTML,含班主任匹配提示)
- **失败响应**:抛出业务异常,`msg` 含失败行号与原因
- **处理逻辑**:
1. 解析学院/专业名称,并校验专业属于所选学院
2. 字典中文标签自动转换为字典值
3. 班级名称作为 `classCode` / `className`,重复则该行失败
4. 毕业年份按 `入学年份 + 学制` 自动计算
5. 调用既有新增逻辑,同步创建对应 `sys_dept` 班级节点
6. 班主任按姓名匹配有效教职工:0 人或多人 → 不写班主任;1 人 → 写入并同步关联/角色
## 其他已有接口(摘要)
| 接口 | 方法 | 路径 |
|------|------|------|
| 分页列表 | GET | `/classMsg/classMsg/list` |
| 详情 | GET | `/classMsg/classMsg/{id}` |
| 新增 | POST | `/classMsg/classMsg` |
| 修改 | PUT | `/classMsg/classMsg` |
| 删除 | DELETE | `/classMsg/classMsg/{ids}` |
| 更新状态 | PUT | `/classMsg/classMsg/status` |
| 批量更新状态 | PUT | `/classMsg/classMsg/status/batch` |
| 导出 | POST | `/classMsg/classMsg/export` |
| 我作为班主任的班级 | GET | `/classMsg/classMsg/myClassListAsTeacher` |
## 前端入口
- 页面:`web/src/views/xg/xsjcxx/classmsg/classMsg/index.vue`
- 操作:工具栏「导入」→ 下载模板 / 上传 Excel
@@ -49,6 +49,17 @@
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['classMsg:classMsg:add']">新增</el-button> <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['classMsg:classMsg:add']">新增</el-button>
</el-col> </el-col>
<el-col :span="1.5">
<el-dropdown v-hasPermi="['classMsg:classMsg:add']" split-button type="info" plain @click="handleImport">
导入
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item>
<el-dropdown-item icon="Upload" @click="handleImport">导入数据</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['classMsg:classMsg:edit']" <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['classMsg:classMsg:edit']"
>修改</el-button >修改</el-button
@@ -194,13 +205,13 @@
<el-input v-model="form.grade" placeholder="请输入年级" /> <el-input v-model="form.grade" placeholder="请输入年级" />
</el-form-item> </el-form-item>
<el-form-item label="入学年份" prop="enrollmentYear"> <el-form-item label="入学年份" prop="enrollmentYear">
<el-input v-model="form.enrollmentYear" placeholder="请输入入学年份" /> <el-input v-model="form.enrollmentYear" placeholder="请输入入学年份" @input="syncGraduationYear" />
</el-form-item> </el-form-item>
<el-form-item label="学制(年)" prop="schoolSystem"> <el-form-item label="学制(年)" prop="schoolSystem">
<el-input v-model="form.schoolSystem" placeholder="请输入学制(年)" /> <el-input v-model="form.schoolSystem" placeholder="请输入学制(年)" @input="syncGraduationYear" />
</el-form-item> </el-form-item>
<el-form-item label="毕业年份" prop="graduationYear"> <el-form-item label="毕业年份" prop="graduationYear">
<el-input v-model="form.graduationYear" placeholder="请输入毕业年份" /> <el-input v-model="form.graduationYear" placeholder="入学年份+学制自动计算,可修改" />
</el-form-item> </el-form-item>
<el-form-item label="辅导员ID" prop="counselorId" v-if="false"> <el-form-item label="辅导员ID" prop="counselorId" v-if="false">
<el-input v-model="form.counselorId" placeholder="请输入辅导员ID" /> <el-input v-model="form.counselorId" placeholder="请输入辅导员ID" />
@@ -386,6 +397,41 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 班级信息导入对话框 -->
<el-dialog v-model="upload.open" :title="upload.title" width="420px" append-to-body>
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:on-error="handleFileError"
:auto-upload="false"
drag
>
<el-icon class="el-icon--upload"><i-ep-upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div>请先下载模板,通过下拉框选择学院、专业及字典项后上传。</div>
<span>仅允许导入 xls、xlsx 格式文件。</span>
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">
下载模板
</el-link>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" :loading="upload.isUploading" @click="submitFileForm">确 定</el-button>
<el-button @click="upload.open = false">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 设置辅导员对话框 --> <!-- 设置辅导员对话框 -->
<el-dialog title="设置辅导员" v-model="counselorDialog.visible" width="500px" append-to-body> <el-dialog title="设置辅导员" v-model="counselorDialog.visible" width="500px" append-to-body>
<el-descriptions title="班级信息" :column="1" border> <el-descriptions title="班级信息" :column="1" border>
@@ -446,7 +492,9 @@ import {
import { listColleges, collegeTree, listMajorsByCollegeId } from '@/api/xg/xsjcxx/classmsg/college'; import { listColleges, collegeTree, listMajorsByCollegeId } from '@/api/xg/xsjcxx/classmsg/college';
import { ClassMsgVO, ClassMsgQuery, ClassMsgForm, SysDeptVo } from '@/api/xg/xsjcxx/classmsg/types'; import { ClassMsgVO, ClassMsgQuery, ClassMsgForm, SysDeptVo } from '@/api/xg/xsjcxx/classmsg/types';
import { ComponentInternalInstance, ref, reactive, toRefs, onMounted, getCurrentInstance, watch } from 'vue'; import { ComponentInternalInstance, ref, reactive, toRefs, onMounted, getCurrentInstance, watch } from 'vue';
import type { FormInstance as ElFormInstance } from 'element-plus'; import type { FormInstance as ElFormInstance, UploadFile, UploadInstance } from 'element-plus';
import { ElMessageBox } from 'element-plus';
import { globalHeaders } from '@/utils/request';
import { getActiveUserList } from '@/api/xg/sysUser'; // 添加用户API导入 import { getActiveUserList } from '@/api/xg/sysUser'; // 添加用户API导入
import { addClassTeacherCounselorRel, updateClassTeacherCounselorRel } from '@/api/xg/classTeacherCounselorRel'; // 添加班级辅导员关联API导入 import { addClassTeacherCounselorRel, updateClassTeacherCounselorRel } from '@/api/xg/classTeacherCounselorRel'; // 添加班级辅导员关联API导入
@@ -526,6 +574,16 @@ const collegeTreeData = ref<SysDeptVo[]>([]); // 学院专业树形数据
const queryFormRef = ref<ElFormInstance>(); const queryFormRef = ref<ElFormInstance>();
const classMsgFormRef = ref<ElFormInstance>(); const classMsgFormRef = ref<ElFormInstance>();
const uploadRef = ref<UploadInstance>();
/** 班级导入参数 */
const upload = reactive({
open: false,
title: '',
isUploading: false,
headers: globalHeaders(),
url: import.meta.env.VITE_APP_BASE_API + '/classMsg/classMsg/importData'
});
const dialog = reactive<DialogOption>({ const dialog = reactive<DialogOption>({
visible: false, visible: false,
@@ -705,6 +763,15 @@ watch(
} }
); );
/** 根据入学年份 + 学制自动计算毕业年份 */
const syncGraduationYear = () => {
const enrollmentYear = Number(form.value.enrollmentYear);
const schoolSystem = Number(form.value.schoolSystem);
if (Number.isFinite(enrollmentYear) && Number.isFinite(schoolSystem) && schoolSystem > 0) {
form.value.graduationYear = enrollmentYear + schoolSystem;
}
};
/** 取消按钮 */ /** 取消按钮 */
const cancel = () => { const cancel = () => {
reset(); reset();
@@ -744,6 +811,46 @@ const handleAdd = () => {
dialog.title = '添加班级信息'; dialog.title = '添加班级信息';
}; };
/** 导入按钮操作 */
const handleImport = () => {
upload.title = '班级信息导入';
upload.open = true;
};
/** 下载导入模板 */
const importTemplate = () => {
proxy?.download('classMsg/classMsg/importTemplate', {}, `班级信息导入模板_${new Date().getTime()}.xlsx`);
};
/** 文件上传中处理 */
const handleFileUploadProgress = () => {
upload.isUploading = true;
};
/** 文件上传成功处理 */
const handleFileSuccess = (response: any, file: UploadFile) => {
upload.open = false;
upload.isUploading = false;
uploadRef.value?.handleRemove(file);
ElMessageBox.alert(
"<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + (response.msg || '导入完成') + '</div>',
'导入结果',
{ dangerouslyUseHTMLString: true }
);
getList();
};
/** 文件上传失败处理 */
const handleFileError = () => {
upload.isUploading = false;
proxy?.$modal.msgError('导入失败,请检查文件格式或网络连接');
};
/** 提交上传文件 */
const submitFileForm = () => {
uploadRef.value?.submit();
};
/** 修改按钮操作 */ /** 修改按钮操作 */
const handleUpdate = async (row?: ClassMsgVO) => { const handleUpdate = async (row?: ClassMsgVO) => {
reset(); reset();