|
@@ -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();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|