diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/CurrentUserCollegeHelper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/CurrentUserCollegeHelper.java new file mode 100644 index 00000000..ec03147e --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/CurrentUserCollegeHelper.java @@ -0,0 +1,89 @@ +package org.dromara.xg.helper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.xg.domain.XgSysDept; +import org.dromara.xg.mapper.XgSysDeptMapper; +import org.springframework.stereotype.Component; + +/** + * 解析当前登录用户所属二级学院(沿部门祖先链向上查找)。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class CurrentUserCollegeHelper { + + private static final Long SECONDARY_COLLEGE_PARENT_ID = 101L; + + private final XgSysDeptMapper xgSysDeptMapper; + + /** + * 从当前用户部门向上找二级学院:自身或 ancestors 中 parent_id=101 / dept_category 以 xy- 开头。 + * + * @return 二级学院 dept_id;无法识别时返回 null + */ + public Long resolveCurrentUserCollegeId() { + try { + Long currentDeptId = LoginHelper.getDeptId(); + if (currentDeptId == null) { + log.warn("用户未登录或没有部门信息"); + return null; + } + + XgSysDept currentDept = xgSysDeptMapper.selectById(currentDeptId); + if (currentDept == null || !isActiveDept(currentDept)) { + log.warn("当前用户部门不存在或已停用/删除, deptId={}", currentDeptId); + return null; + } + + if (isSecondaryCollege(currentDept)) { + return currentDept.getDeptId(); + } + + String ancestors = currentDept.getAncestors(); + if (StringUtils.isBlank(ancestors)) { + log.warn("当前用户部门无祖先链, deptId={}", currentDeptId); + return null; + } + + String[] parts = ancestors.split(","); + for (int i = parts.length - 1; i >= 0; i--) { + String part = parts[i] == null ? "" : parts[i].trim(); + if (part.isEmpty()) { + continue; + } + Long ancestorId; + try { + ancestorId = Long.valueOf(part); + } catch (NumberFormatException e) { + continue; + } + XgSysDept ancestor = xgSysDeptMapper.selectById(ancestorId); + if (ancestor != null && isActiveDept(ancestor) && isSecondaryCollege(ancestor)) { + return ancestor.getDeptId(); + } + } + + log.warn("未能从部门祖先链解析二级学院, deptId={}, ancestors={}", currentDeptId, ancestors); + return null; + } catch (Exception e) { + log.error("获取当前用户二级学院ID失败", e); + return null; + } + } + + private boolean isActiveDept(XgSysDept dept) { + return "0".equals(dept.getStatus()) && "0".equals(dept.getDelFlag()); + } + + private boolean isSecondaryCollege(XgSysDept dept) { + if (SECONDARY_COLLEGE_PARENT_ID.equals(dept.getParentId())) { + return true; + } + String category = dept.getDeptCategory(); + return StringUtils.isNotBlank(category) && category.startsWith("xy-"); + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/FreshmanCollegeProxyHelper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/FreshmanCollegeProxyHelper.java index bc6ddc73..a3165dba 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/FreshmanCollegeProxyHelper.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/FreshmanCollegeProxyHelper.java @@ -41,7 +41,7 @@ public class FreshmanCollegeProxyHelper { } /** - * 读取代理学院;未配置时返回 null(宿舍树降级为原逻辑,避免拖垮其它功能)。 + * 读取代理学院;未配置时返回 null(宿舍树按非代理处理:本院排除一年级)。 */ public Long resolveProxyCollegeIdOrNull() { String configValue = configService.getConfigValue( @@ -74,4 +74,24 @@ public class FreshmanCollegeProxyHelper { return FreshmanCollegeProxySupport.canOperateClassForDormAllocation( operatorCollegeId, classCollegeId, enrollmentYear, resolveProxyCollegeIdOrNull()); } + + /** + * 写路径校验:班级入学年必填,且符合一年级代理规则。 + * + * @param className 班级名称(用于异常文案) + */ + public void assertCanOperateClassForDormAllocation(Long operatorCollegeId, + Long classCollegeId, + Long enrollmentYear, + String className) { + String displayName = StringUtils.blankToDefault(className, ""); + if (enrollmentYear == null) { + throw new ServiceException("班级「" + displayName + + "」入学年信息不完整,无法分配/回收宿舍,请先维护班级入学年份"); + } + if (!canOperateClassForDormAllocation(operatorCollegeId, classCollegeId, enrollmentYear)) { + throw new ServiceException("无权操作班级「" + displayName + + "」的宿舍分配(一年级由人文学院代理,其它年级仅本学院可操作)"); + } + } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMsgMapper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMsgMapper.java index 1aa21ee3..94ea940a 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMsgMapper.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMsgMapper.java @@ -103,18 +103,18 @@ public interface XgStudentMsgMapper extends BaseMapperPlus> getCollegeStudentStats(@Param("secondaryCollegeId") Long secondaryCollegeId); /** - * 查询二级学院下的学院、专业、班级学生统计信息(按床位分配统计) + * 本院全部有效班级树统计(宿舍分配到班级,以 xg_class_msg 为准) * - * @param secondaryCollegeId 二级学院ID - * @return 学生统计信息列表 + * @param collegeId 二级学院ID(班级 college_id) + * @return 班级统计行(含 enrollment_year;无住宿生人数可为 0) */ - List> getCollegeStudentStatsByClass(@Param("secondaryCollegeId") Long secondaryCollegeId); + List> getCollegeStudentStatsByClass(@Param("collegeId") Long collegeId); /** - * 按入学年查询全校一年级班级学生统计(宿舍分配到班级:代理学院可见) + * 全校一年级班级树统计(宿舍分配到班级:代理学院可见,以班级 enrollment_year 为准) * * @param enrollmentYear 入学年(当前届) - * @return 学生统计信息列表(含 enrollment_year) + * @return 班级统计行(含 enrollment_year;无住宿生人数可为 0) */ List> getFreshmanStudentStatsByClass(@Param("enrollmentYear") Integer enrollmentYear); diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedServiceImpl.java index 32316a43..88e0f28b 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedServiceImpl.java @@ -16,15 +16,13 @@ import org.dromara.xg.domain.vo.XgDormBedDetailVo; import org.dromara.xg.domain.XgDormBed; import org.dromara.xg.domain.XgDormRoom; import org.dromara.xg.domain.XgDormBedStudent; -import org.dromara.xg.domain.XgSysDept; import org.dromara.xg.domain.vo.XgStudentLeaveVo; import org.dromara.xg.mapper.XgDormBedMapper; import org.dromara.xg.mapper.XgDormRoomMapper; import org.dromara.xg.mapper.XgDormBedStudentMapper; -import org.dromara.xg.mapper.XgSysDeptMapper; import org.dromara.xg.service.IXgDormBedService; import org.dromara.xg.service.IXgStudentLeaveService; -import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.xg.helper.CurrentUserCollegeHelper; import java.util.List; import java.util.Map; @@ -49,8 +47,8 @@ public class XgDormBedServiceImpl implements IXgDormBedService { private final XgDormBedMapper baseMapper; private final XgDormRoomMapper xgDormRoomMapper; private final XgDormBedStudentMapper xgDormBedStudentMapper; - private final XgSysDeptMapper xgSysDeptMapper; private final IXgStudentLeaveService xgStudentLeaveService; + private final CurrentUserCollegeHelper currentUserCollegeHelper; /** * 查询宿舍床位信息 @@ -245,56 +243,10 @@ public class XgDormBedServiceImpl implements IXgDormBedService { } /** - * 获取当前用户的二级学院ID - * 通过当前用户的部门id,向上寻找包括他自己,找到dept_category前几位是xy-的就是二级学院 - * 或者parent_id = 101的部门就是二级学院 + * 获取当前用户的二级学院ID(沿部门祖先链向上解析)。 */ private Long getCurrentUserCollegeId() { - try { - Long currentUserId = LoginHelper.getUserId(); - Long currentDeptId = LoginHelper.getDeptId(); - - if (currentUserId == null || currentDeptId == null) { - log.warn("用户未登录或没有部门信息"); - return null; - } - - // 首先查找parent_id = 101的部门(使用LambdaQueryWrapper) - LambdaQueryWrapper parentQuery = Wrappers.lambdaQuery(); - parentQuery.eq(XgSysDept::getParentId, 101L) - .eq(XgSysDept::getStatus, "0") - .eq(XgSysDept::getDelFlag, "0") - .and(wrapper -> wrapper.eq(XgSysDept::getDeptId, currentDeptId) - .or().like(XgSysDept::getAncestors, "," + currentDeptId + ",") // 中间位置 - .or().likeRight(XgSysDept::getAncestors, currentDeptId + ",") // 开头位置 - .or().likeLeft(XgSysDept::getAncestors, "," + currentDeptId) // 末尾位置 - .or().eq(XgSysDept::getAncestors, currentDeptId.toString())) // 单独一个值 - .last("LIMIT 1"); - - List parentColleges = xgSysDeptMapper.selectList(parentQuery); - if (!parentColleges.isEmpty()) { - return parentColleges.get(0).getDeptId(); - } - - // 如果上面的查询没找到,尝试通过dept_category查找 - LambdaQueryWrapper categoryQuery = Wrappers.lambdaQuery(); - categoryQuery.likeRight(XgSysDept::getDeptCategory, "xy-") - .eq(XgSysDept::getStatus, "0") - .eq(XgSysDept::getDelFlag, "0") - .and(wrapper -> wrapper.eq(XgSysDept::getDeptId, currentDeptId) - .or().like(XgSysDept::getAncestors, "," + currentDeptId + ",") // 中间位置 - .or().likeRight(XgSysDept::getAncestors, currentDeptId + ",") // 开头位置 - .or().likeLeft(XgSysDept::getAncestors, "," + currentDeptId) // 末尾位置 - .or().eq(XgSysDept::getAncestors, currentDeptId.toString())) // 单独一个值 - .last("LIMIT 1"); - - List categoryColleges = xgSysDeptMapper.selectList(categoryQuery); - return categoryColleges.isEmpty() ? null : categoryColleges.get(0).getDeptId(); - - } catch (Exception e) { - log.error("获取当前用户二级学院ID失败", e); - return null; - } + return currentUserCollegeHelper.resolveCurrentUserCollegeId(); } /** diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java index 58763c06..f699fdd8 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java @@ -49,8 +49,6 @@ import org.dromara.xg.domain.XgDormCampus; import org.dromara.xg.domain.XgDormArea; import org.dromara.xg.mapper.XgDormCampusMapper; import org.dromara.xg.mapper.XgDormAreaMapper; -import org.dromara.xg.mapper.XgSysDeptMapper; -import org.dromara.xg.domain.XgSysDept; import java.util.stream.Collectors; import java.util.LinkedHashMap; import org.dromara.xg.domain.bo.XgDormBedBatchAllocateToClassBo; @@ -59,6 +57,7 @@ import org.dromara.xg.domain.XgClassMsg; import org.dromara.xg.domain.vo.XgDormBedDetailVo; import org.dromara.xg.domain.bo.XgDormSelectiveAllocationBo; import org.dromara.xg.helper.FreshmanCollegeProxyHelper; +import org.dromara.xg.helper.CurrentUserCollegeHelper; /** * 床位学生分配关系Service业务层处理 @@ -78,10 +77,10 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { private final XgStudentMsgMapper xgStudentMsgMapper; private final XgDormCampusMapper xgDormCampusMapper; private final XgDormAreaMapper xgDormAreaMapper; - private final XgSysDeptMapper xgSysDeptMapper; private final XgClassMsgMapper xgClassMsgMapper; private final IXgStudentLeaveService xgStudentLeaveService; private final FreshmanCollegeProxyHelper freshmanCollegeProxyHelper; + private final CurrentUserCollegeHelper currentUserCollegeHelper; /** * 查询床位学生分配关系 @@ -697,13 +696,10 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { throw new ServiceException("部分班级信息不存在,请检查班级ID"); } - // 6.1 安全:一年级仅代理学院可分;非一年级仅本学院可分 + // 6.1 安全:入学年必填;一年级仅代理学院可分;非一年级仅本学院可分 for (XgClassMsg clazz : classes) { - if (!freshmanCollegeProxyHelper.canOperateClassForDormAllocation( - operatorCollegeId, clazz.getCollegeId(), clazz.getEnrollmentYear())) { - throw new ServiceException("无权将床位分配给班级「" + clazz.getClassName() - + "」(一年级由人文学院代理,其它年级仅本学院可操作)"); - } + freshmanCollegeProxyHelper.assertCanOperateClassForDormAllocation( + operatorCollegeId, clazz.getCollegeId(), clazz.getEnrollmentYear(), clazz.getClassName()); } // 建立班级ID到班级信息的映射 @@ -1091,56 +1087,10 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { } /** - * 获取当前用户的二级学院ID - * 通过当前用户的部门id,向上寻找包括他自己,找到dept_category前几位是xy-的就是二级学院 - * 或者parent_id = 101的部门就是二级学院 + * 获取当前用户的二级学院ID(沿部门祖先链向上解析)。 */ private Long getCurrentUserCollegeId() { - try { - Long currentUserId = LoginHelper.getUserId(); - Long currentDeptId = LoginHelper.getDeptId(); - - if (currentUserId == null || currentDeptId == null) { - log.warn("用户未登录或没有部门信息"); - return null; - } - - // 首先查找parent_id = 101的部门(使用LambdaQueryWrapper) - LambdaQueryWrapper parentQuery = Wrappers.lambdaQuery(); - parentQuery.eq(XgSysDept::getParentId, 101L) - .eq(XgSysDept::getStatus, "0") - .eq(XgSysDept::getDelFlag, "0") - .and(wrapper -> wrapper.eq(XgSysDept::getDeptId, currentDeptId) - .or().like(XgSysDept::getAncestors, "," + currentDeptId + ",") // 中间位置 - .or().likeRight(XgSysDept::getAncestors, currentDeptId + ",") // 开头位置 - .or().likeLeft(XgSysDept::getAncestors, "," + currentDeptId) // 末尾位置 - .or().eq(XgSysDept::getAncestors, currentDeptId.toString())) // 单独一个值 - .last("LIMIT 1"); - - List parentColleges = xgSysDeptMapper.selectList(parentQuery); - if (!parentColleges.isEmpty()) { - return parentColleges.get(0).getDeptId(); - } - - // 如果上面的查询没找到,尝试通过dept_category查找 - LambdaQueryWrapper categoryQuery = Wrappers.lambdaQuery(); - categoryQuery.likeRight(XgSysDept::getDeptCategory, "xy-") - .eq(XgSysDept::getStatus, "0") - .eq(XgSysDept::getDelFlag, "0") - .and(wrapper -> wrapper.eq(XgSysDept::getDeptId, currentDeptId) - .or().like(XgSysDept::getAncestors, "," + currentDeptId + ",") // 中间位置 - .or().likeRight(XgSysDept::getAncestors, currentDeptId + ",") // 开头位置 - .or().likeLeft(XgSysDept::getAncestors, "," + currentDeptId) // 末尾位置 - .or().eq(XgSysDept::getAncestors, currentDeptId.toString())) // 单独一个值 - .last("LIMIT 1"); - - List categoryColleges = xgSysDeptMapper.selectList(categoryQuery); - return categoryColleges.isEmpty() ? null : categoryColleges.get(0).getDeptId(); - - } catch (Exception e) { - log.error("获取当前用户二级学院ID失败", e); - return null; - } + return currentUserCollegeHelper.resolveCurrentUserCollegeId(); } /** @@ -1169,11 +1119,8 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { if (clazz == null) { throw new ServiceException("床位关联班级不存在,禁止回收"); } - if (!freshmanCollegeProxyHelper.canOperateClassForDormAllocation( - operatorCollegeId, clazz.getCollegeId(), clazz.getEnrollmentYear())) { - throw new ServiceException("无权回收班级「" + clazz.getClassName() - + "」的宿舍分配(一年级由人文学院代理)"); - } + freshmanCollegeProxyHelper.assertCanOperateClassForDormAllocation( + operatorCollegeId, clazz.getCollegeId(), clazz.getEnrollmentYear(), clazz.getClassName()); } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRoomServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRoomServiceImpl.java index 19f70a56..df4e1374 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRoomServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRoomServiceImpl.java @@ -14,12 +14,10 @@ import org.dromara.xg.domain.bo.XgDormRoomBo; import org.dromara.xg.domain.vo.XgDormRoomVo; import org.dromara.xg.domain.XgDormRoom; import org.dromara.xg.domain.XgDormBedStudent; -import org.dromara.xg.domain.XgSysDept; import org.dromara.xg.mapper.XgDormRoomMapper; import org.dromara.xg.mapper.XgDormBedStudentMapper; -import org.dromara.xg.mapper.XgSysDeptMapper; import org.dromara.xg.service.IXgDormRoomService; -import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.xg.helper.CurrentUserCollegeHelper; import java.util.List; import java.util.Map; @@ -58,8 +56,8 @@ public class XgDormRoomServiceImpl implements IXgDormRoomService { private final XgDormRoomMapper baseMapper; private final XgDormBedStudentMapper xgDormBedStudentMapper; - private final XgSysDeptMapper xgSysDeptMapper; private final XgDormBedMapper xgDormBedMapper; + private final CurrentUserCollegeHelper currentUserCollegeHelper; /** * 查询宿舍房间信息 @@ -234,56 +232,10 @@ public class XgDormRoomServiceImpl implements IXgDormRoomService { } /** - * 获取当前用户的二级学院ID - * 通过当前用户的部门id,向上寻找包括他自己,找到dept_category前几位是xy-的就是二级学院 - * 或者parent_id = 101的部门就是二级学院 + * 获取当前用户的二级学院ID(沿部门祖先链向上解析)。 */ private Long getCurrentUserCollegeId() { - try { - Long currentUserId = LoginHelper.getUserId(); - Long currentDeptId = LoginHelper.getDeptId(); - - if (currentUserId == null || currentDeptId == null) { - log.warn("用户未登录或没有部门信息"); - return null; - } - - // 首先查找parent_id = 101的部门(使用LambdaQueryWrapper) - LambdaQueryWrapper parentQuery = Wrappers.lambdaQuery(); - parentQuery.eq(XgSysDept::getParentId, 101L) - .eq(XgSysDept::getStatus, "0") - .eq(XgSysDept::getDelFlag, "0") - .and(wrapper -> wrapper.eq(XgSysDept::getDeptId, currentDeptId) - .or().like(XgSysDept::getAncestors, "," + currentDeptId + ",") // 中间位置 - .or().likeRight(XgSysDept::getAncestors, currentDeptId + ",") // 开头位置 - .or().likeLeft(XgSysDept::getAncestors, "," + currentDeptId) // 末尾位置 - .or().eq(XgSysDept::getAncestors, currentDeptId.toString())) // 单独一个值 - .last("LIMIT 1"); - - List parentColleges = xgSysDeptMapper.selectList(parentQuery); - if (!parentColleges.isEmpty()) { - return parentColleges.get(0).getDeptId(); - } - - // 如果上面的查询没找到,尝试通过dept_category查找 - LambdaQueryWrapper categoryQuery = Wrappers.lambdaQuery(); - categoryQuery.likeRight(XgSysDept::getDeptCategory, "xy-") - .eq(XgSysDept::getStatus, "0") - .eq(XgSysDept::getDelFlag, "0") - .and(wrapper -> wrapper.eq(XgSysDept::getDeptId, currentDeptId) - .or().like(XgSysDept::getAncestors, "," + currentDeptId + ",") // 中间位置 - .or().likeRight(XgSysDept::getAncestors, currentDeptId + ",") // 开头位置 - .or().likeLeft(XgSysDept::getAncestors, "," + currentDeptId) // 末尾位置 - .or().eq(XgSysDept::getAncestors, currentDeptId.toString())) // 单独一个值 - .last("LIMIT 1"); - - List categoryColleges = xgSysDeptMapper.selectList(categoryQuery); - return categoryColleges.isEmpty() ? null : categoryColleges.get(0).getDeptId(); - - } catch (Exception e) { - log.error("获取当前用户二级学院ID失败", e); - return null; - } + return currentUserCollegeHelper.resolveCurrentUserCollegeId(); } /** diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java index 1f114d6b..268d7de1 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java @@ -38,14 +38,13 @@ import org.dromara.xg.mapper.XgStudentManagerSourceMapper; import org.dromara.xg.mapper.XgClassTeacherCounselorRelMapper; import org.dromara.xg.mapper.XgClassMsgMapper; import org.dromara.xg.mapper.XgDormBedStudentMapper; -import org.dromara.xg.mapper.XgSysDeptMapper; import org.dromara.xg.domain.XgStudentManager; import org.dromara.xg.domain.XgStudentManagerSource; import org.dromara.xg.domain.XgClassTeacherCounselorRel; import org.dromara.xg.domain.XgClassMsg; import org.dromara.xg.domain.XgDormBedStudent; -import org.dromara.xg.domain.XgSysDept; import org.dromara.xg.helper.FreshmanCollegeProxyHelper; +import org.dromara.xg.helper.CurrentUserCollegeHelper; import java.util.stream.Collectors; import java.util.List; @@ -80,8 +79,8 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { private final XgClassTeacherCounselorRelMapper xgClassTeacherCounselorRelMapper; private final XgClassMsgMapper xgClassMsgMapper; private final XgDormBedStudentMapper xgDormBedStudentMapper; - private final XgSysDeptMapper xgSysDeptMapper; private final FreshmanCollegeProxyHelper freshmanCollegeProxyHelper; + private final CurrentUserCollegeHelper currentUserCollegeHelper; /** * 查询学生信息 @@ -1344,11 +1343,11 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { /** * 获取当前用户所在学院的学生信息树(宿舍「分配到班级」专用入口)。 *

- * 一年级代理规则(不改班级真实学院归属): + * 一年级代理规则(不改班级真实学院归属;班级树以 xg_class_msg 为准): *

    *
  • 代理学院(人文学院):本院全部班级 + 全校一年级班级
  • *
  • 其他学院:本院非一年级班级(一年级隐藏,防双头操作)
  • - *
  • 代理学院未配置:保持原逻辑,仅本院班级
  • + *
  • 代理学院未配置:按非代理处理(本院 − 一年级),并打警告日志
  • *
*/ @Override @@ -1362,14 +1361,18 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { Long proxyCollegeId = freshmanCollegeProxyHelper.resolveProxyCollegeIdOrNull(); int cohortYear = freshmanCollegeProxyHelper.currentCohortYear(); - log.info("宿舍班级树:userCollegeId={}, proxyCollegeId={}, cohortYear={}", - collegeId, proxyCollegeId, cohortYear); + List> collegeStats = baseMapper.getCollegeStudentStatsByClass(collegeId); + log.info("宿舍班级树:userCollegeId={}, proxyCollegeId={}, cohortYear={}, ownClassRows={}", + collegeId, proxyCollegeId, cohortYear, collegeStats.size()); if (proxyCollegeId == null) { - return getStudentTreeByClass(collegeId); + log.warn("一年级宿舍代理学院未配置,宿舍班级树按非代理处理(本院排除一年级)"); + List> nonFreshmanStats = collegeStats.stream() + .filter(item -> !isFreshmanStatRow(item, cohortYear)) + .collect(Collectors.toList()); + return buildStudentTreeFromClassStats(nonFreshmanStats); } - List> collegeStats = baseMapper.getCollegeStudentStatsByClass(collegeId); if (proxyCollegeId.equals(collegeId)) { List> freshmanStats = baseMapper.getFreshmanStudentStatsByClass(cohortYear); @@ -1549,55 +1552,9 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { } /** - * 获取当前用户的二级学院ID - * 通过当前用户的部门id,向上寻找包括他自己,找到dept_category前几位是xy-的就是二级学院 - * 或者parent_id = 101的部门就是二级学院 + * 获取当前用户的二级学院ID(沿部门祖先链向上解析)。 */ private Long getCurrentUserCollegeId() { - try { - Long currentUserId = LoginHelper.getUserId(); - Long currentDeptId = LoginHelper.getDeptId(); - - if (currentUserId == null || currentDeptId == null) { - log.warn("用户未登录或没有部门信息"); - return null; - } - - // 首先查找parent_id = 101的部门(使用LambdaQueryWrapper) - LambdaQueryWrapper parentQuery = Wrappers.lambdaQuery(); - parentQuery.eq(XgSysDept::getParentId, 101L) - .eq(XgSysDept::getStatus, "0") - .eq(XgSysDept::getDelFlag, "0") - .and(wrapper -> wrapper.eq(XgSysDept::getDeptId, currentDeptId) - .or().like(XgSysDept::getAncestors, "," + currentDeptId + ",") // 中间位置 - .or().likeRight(XgSysDept::getAncestors, currentDeptId + ",") // 开头位置 - .or().likeLeft(XgSysDept::getAncestors, "," + currentDeptId) // 末尾位置 - .or().eq(XgSysDept::getAncestors, currentDeptId.toString())) // 单独一个值 - .last("LIMIT 1"); - - List parentColleges = xgSysDeptMapper.selectList(parentQuery); - if (!parentColleges.isEmpty()) { - return parentColleges.get(0).getDeptId(); - } - - // 如果上面的查询没找到,尝试通过dept_category查找 - LambdaQueryWrapper categoryQuery = Wrappers.lambdaQuery(); - categoryQuery.likeRight(XgSysDept::getDeptCategory, "xy-") - .eq(XgSysDept::getStatus, "0") - .eq(XgSysDept::getDelFlag, "0") - .and(wrapper -> wrapper.eq(XgSysDept::getDeptId, currentDeptId) - .or().like(XgSysDept::getAncestors, "," + currentDeptId + ",") // 中间位置 - .or().likeRight(XgSysDept::getAncestors, currentDeptId + ",") // 开头位置 - .or().likeLeft(XgSysDept::getAncestors, "," + currentDeptId) // 末尾位置 - .or().eq(XgSysDept::getAncestors, currentDeptId.toString())) // 单独一个值 - .last("LIMIT 1"); - - List categoryColleges = xgSysDeptMapper.selectList(categoryQuery); - return categoryColleges.isEmpty() ? null : categoryColleges.get(0).getDeptId(); - - } catch (Exception e) { - log.error("获取当前用户二级学院ID失败", e); - return null; - } + return currentUserCollegeHelper.resolveCurrentUserCollegeId(); } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsg/XgStudentMsgMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsg/XgStudentMsgMapper.xml index 74db559a..16c563a9 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsg/XgStudentMsgMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsg/XgStudentMsgMapper.xml @@ -218,107 +218,85 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ORDER BY c.grade DESC, sm.college_name, sm.major_name, sm.class_name, MAX(sm.create_time) DESC - + + + c.grade, + c.enrollment_year AS enrollment_year, + c.college_id, + COALESCE(dc.dept_name, c.college_name) AS college_name, + c.major_id, + COALESCE(dm.dept_name, c.major_name) AS major_name, + c.id AS class_id, + c.class_name, + COALESCE(SUM(CASE WHEN sm.id IS NOT NULL AND sm.gender = '0' AND sm.is_boarder = 1 THEN 1 ELSE 0 END), 0) AS male_total, + COALESCE(SUM(CASE WHEN sm.id IS NOT NULL AND sm.gender = '1' AND sm.is_boarder = 1 THEN 1 ELSE 0 END), 0) AS female_total, + COALESCE(( + SELECT COUNT(DISTINCT dbs.bed_id) + FROM xg_dorm_bed_student dbs + LEFT JOIN xg_dorm_bed db ON dbs.bed_id = db.id + LEFT JOIN xg_dorm_room dr ON db.room_id = dr.id + LEFT JOIN xg_dorm_building dbuild ON dr.building_id = dbuild.id + WHERE dbs.class_id = c.id + AND dbs.deleted = '0' + AND dbs.status = '0' + AND db.deleted = '0' + AND dbuild.building_type = '1' + ), 0) AS male_assigned, + COALESCE(( + SELECT COUNT(DISTINCT dbs.bed_id) + FROM xg_dorm_bed_student dbs + LEFT JOIN xg_dorm_bed db ON dbs.bed_id = db.id + LEFT JOIN xg_dorm_room dr ON db.room_id = dr.id + LEFT JOIN xg_dorm_building dbuild ON dr.building_id = dbuild.id + WHERE dbs.class_id = c.id + AND dbs.deleted = '0' + AND dbs.status = '0' + AND db.deleted = '0' + AND dbuild.building_type = '2' + ), 0) AS female_assigned + + + + FROM xg_class_msg c + LEFT JOIN sys_dept dc ON c.college_id = dc.dept_id + LEFT JOIN sys_dept dm ON c.major_id = dm.dept_id + LEFT JOIN xg_student_msg sm ON sm.class_id = c.id + AND sm.deleted = 0 + AND sm.student_status = '1' + + + + GROUP BY c.grade, c.enrollment_year, c.college_id, COALESCE(dc.dept_name, c.college_name), + c.major_id, COALESCE(dm.dept_name, c.major_name), c.id, c.class_name + + + - + diff --git a/backend/script/sql/update/宿舍一年级代理班级树核对.sql b/backend/script/sql/update/宿舍一年级代理班级树核对.sql new file mode 100644 index 00000000..9b40aabf --- /dev/null +++ b/backend/script/sql/update/宿舍一年级代理班级树核对.sql @@ -0,0 +1,45 @@ +-- ============================================================================= +-- 宿舍「分配到班级」一年级代理:上线前只读核对 +-- 日期:2026-10-14 +-- ============================================================================= + +-- 1) 代理配置 vs 人文学院 dept_id +SELECT config_key, config_value, remark +FROM sys_config +WHERE config_key = 'xg.studentLeave.freshmanProxyCollegeId'; + +SELECT dept_id, dept_name, parent_id, dept_category, status, del_flag +FROM sys_dept +WHERE dept_name = '人文学院' + AND del_flag = '0'; + +-- 2) 当前届一年级班级数量(9 月 1 日切届) +SET @cohort_year = YEAR(CURDATE()) - IF(MONTH(CURDATE()) >= 9, 0, 1); +SELECT @cohort_year AS cohort_year, + COUNT(*) AS freshman_class_cnt +FROM xg_class_msg +WHERE deleted = 0 + AND enrollment_year = @cohort_year; + +-- 3) 入学年缺失 +SELECT id, class_name, grade, enrollment_year, college_id, college_name +FROM xg_class_msg +WHERE deleted = 0 + AND enrollment_year IS NULL +LIMIT 200; + +-- 4) 学生学院与班级学院漂移(可选治理) +SELECT sm.id, + sm.student_no, + sm.name, + sm.college_id AS stu_college, + c.college_id AS class_college, + c.class_name +FROM xg_student_msg sm +JOIN xg_class_msg c ON sm.class_id = c.id AND c.deleted = 0 +WHERE sm.deleted = 0 + AND sm.student_status = '1' + AND sm.college_id IS NOT NULL + AND c.college_id IS NOT NULL + AND CAST(sm.college_id AS CHAR) <> CAST(c.college_id AS CHAR) +LIMIT 200; diff --git a/docs/业务模块/宿舍管理/一年级宿舍分配到班级代理学院说明.md b/docs/业务模块/宿舍管理/一年级宿舍分配到班级代理学院说明.md index 0cd5e135..445780a2 100644 --- a/docs/业务模块/宿舍管理/一年级宿舍分配到班级代理学院说明.md +++ b/docs/业务模块/宿舍管理/一年级宿舍分配到班级代理学院说明.md @@ -6,26 +6,80 @@ - **不修改**班级/学生真实 `college_id`。 - 一年级床位须先在「分配到学院」分给人文学院,人文学院才能看到楼/房/床。 - 其他学院在「分配到班级」中**看不到也不可操作**一年级班级(防双头)。 -- 一年级判定:`enrollment_year`,每年 **9 月 1 日**切届(与请假一致)。 +- 一年级判定:班级表 `xg_class_msg.enrollment_year`,每年 **9 月 1 日**切届(与请假一致)。 - 配置键(复用请假):`xg.studentLeave.freshmanProxyCollegeId` ## 实现要点 | 能力 | 说明 | |------|------| -| 班级树 | `getCurrentUserStudentTree`:代理院=本院∪全校一年级;其它院=本院−一年级 | -| 写校验 | `batchAllocateBedsToClass` / `batchRecallClassAllocation`:床位须属操作人学院;班级权限按一年级代理规则 | +| 班级树主数据 | 以 `xg_class_msg` 为准;无住宿生的班级也出节点(需求人数可为 0) | +| 班级树 | `getCurrentUserStudentTree`:代理院=本院全部 ∪ 全校一年级;其它院=本院−一年级;配置缺失时按非代理处理并打 warn | +| 展示归属 | 使用班级 `college_id` / 专业维度,不用学生冗余 `sm.college_*` | +| 写校验 | `batchAllocateBedsToClass` / `batchRecallClassAllocation`:床位须属操作人学院;班级入学年必填;权限按一年级代理规则 | +| 学院解析 | `CurrentUserCollegeHelper`:沿用户部门 ancestors **向上**找 `parent_id=101` 或 `xy-` | | 公共逻辑 | `FreshmanCollegeProxyHelper` / `FreshmanCollegeProxySupport` | +## 上线前数据核对(只读) + +```sql +-- 1) 代理配置 = 现网人文学院 dept_id +SELECT config_key, config_value +FROM sys_config +WHERE config_key = 'xg.studentLeave.freshmanProxyCollegeId'; + +SELECT dept_id, dept_name, parent_id, dept_category +FROM sys_dept +WHERE dept_name = '人文学院' AND del_flag = '0'; + +-- 2) 当前届一年级班级数量(2026-09-01 后 enrollment_year = 当年) +SELECT COUNT(*) AS freshman_class_cnt +FROM xg_class_msg +WHERE deleted = 0 + AND enrollment_year = YEAR(CURDATE()) - IF(MONTH(CURDATE()) >= 9, 0, 1); + +-- 3) 入学年缺失或疑似冲突 +SELECT id, class_name, grade, enrollment_year, college_id, college_name +FROM xg_class_msg +WHERE deleted = 0 + AND (enrollment_year IS NULL + OR (grade IS NOT NULL AND grade NOT LIKE CONCAT('%', enrollment_year, '%'))) +LIMIT 200; + +-- 4) 学生学院与班级学院漂移(树已不再依赖学生学院挂载,仍建议治理) +SELECT sm.id, sm.student_no, sm.name, sm.college_id AS stu_college, + c.college_id AS class_college, c.class_name +FROM xg_student_msg sm +JOIN xg_class_msg c ON sm.class_id = c.id AND c.deleted = 0 +WHERE sm.deleted = 0 + AND sm.student_status = '1' + AND sm.college_id IS NOT NULL + AND c.college_id IS NOT NULL + AND CAST(sm.college_id AS CHAR) <> CAST(c.college_id AS CHAR) +LIMIT 200; +``` + +核对后:若配置值与人文 `dept_id` 不一致,更新 `sys_config` 并刷新参数缓存 / 重启后端。 + ## 运营注意 -1. 系统参数中确认人文学院 `dept_id` 已配置。 +1. 系统参数中确认人文学院 `dept_id` 已配置且与账号解析出的学院 ID 一致。 2. 一年级床位在 `xsssfpejxy` 分给人文学院。 3. 切届后:树自动切回本学院;若床位 `college_id` 仍挂人文,需另行移交(本期未自动迁移)。 +4. 班级 `enrollment_year` 为空时:不计入一年级树;分配/回收会报错,需先维护入学年。 + +## 验收用例 + +1. **人文账号**:看见本院全部班级 + 全校当前届一年级班级;可对一年级分配/回收。 +2. **其它学院账号**:只见本院非一年级;操作一年级被拒。 +3. **人文下级科室账号**:解析学院仍为人文学院,代理分支生效(可见全校一年级)。 +4. **无住宿生的一年级空班**:树中可见,男女需求可为 0,仍可分配床位。 ## 相关代码 +- `backend/ruoyi-modules/ruoyi-xg/.../helper/CurrentUserCollegeHelper.java` - `backend/ruoyi-modules/ruoyi-xg/.../helper/FreshmanCollegeProxyHelper.java` - `backend/ruoyi-modules/ruoyi-xg/.../service/impl/XgStudentMsgServiceImpl.java` - `backend/ruoyi-modules/ruoyi-xg/.../service/impl/XgDormBedStudentServiceImpl.java` -- `web/src/views/xg/xgss/ejxyfp/`(前端仍调原 API,无强制改动) +- `backend/ruoyi-modules/ruoyi-xg/.../resources/mapper/studentMsg/XgStudentMsgMapper.xml` +- `web/src/views/xg/xgss/ejxyfp/`(前端仍调原 API)