Quellcode durchsuchen

fix(dorm): 人文学院宿舍分配班级树以班级表为准合并全校一年级

班级树改为 xg_class_msg 主数据源(无住宿生也可见),统一入学年口径与学院向上解析,避免代理院一年级列表不全或挂错。

Co-authored-by: Cursor <cursoragent@cursor.com>
“wuzhen” vor 2 Wochen
Ursprung
Commit
a0bb39d069

+ 89 - 0
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-");
+    }
+}

+ 21 - 1
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
+                + "」的宿舍分配(一年级由人文学院代理,其它年级仅本学院可操作)");
+        }
+    }
 }

+ 6 - 6
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMsgMapper.java

@@ -103,18 +103,18 @@ public interface XgStudentMsgMapper extends BaseMapperPlus<XgStudentMsg, XgStude
     List<Map<String, Object>> getCollegeStudentStats(@Param("secondaryCollegeId") Long secondaryCollegeId);
 
     /**
-     * 查询二级学院下的学院、专业、班级学生统计信息(按床位分配统计)
+     * 本院全部有效班级树统计(宿舍分配到班级,以 xg_class_msg 为准)
      *
-     * @param secondaryCollegeId 二级学院ID
-     * @return 学生统计信息列表
+     * @param collegeId 二级学院ID(班级 college_id)
+     * @return 班级统计行(含 enrollment_year;无住宿生人数可为 0)
      */
-    List<Map<String, Object>> getCollegeStudentStatsByClass(@Param("secondaryCollegeId") Long secondaryCollegeId);
+    List<Map<String, Object>> getCollegeStudentStatsByClass(@Param("collegeId") Long collegeId);
 
     /**
-     * 按入学年查询全校一年级班级学生统计(宿舍分配到班级:代理学院可见)
+     * 全校一年级班级树统计(宿舍分配到班级:代理学院可见,以班级 enrollment_year 为准)
      *
      * @param enrollmentYear 入学年(当前届)
-     * @return 学生统计信息列表(含 enrollment_year)
+     * @return 班级统计行(含 enrollment_year;无住宿生人数可为 0)
      */
     List<Map<String, Object>> getFreshmanStudentStatsByClass(@Param("enrollmentYear") Integer enrollmentYear);
 

+ 4 - 52
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<XgSysDept> 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<XgSysDept> parentColleges = xgSysDeptMapper.selectList(parentQuery);
-            if (!parentColleges.isEmpty()) {
-                return parentColleges.get(0).getDeptId();
-            }
-
-            // 如果上面的查询没找到,尝试通过dept_category查找
-            LambdaQueryWrapper<XgSysDept> 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<XgSysDept> categoryColleges = xgSysDeptMapper.selectList(categoryQuery);
-            return categoryColleges.isEmpty() ? null : categoryColleges.get(0).getDeptId();
-
-        } catch (Exception e) {
-            log.error("获取当前用户二级学院ID失败", e);
-            return null;
-        }
+        return currentUserCollegeHelper.resolveCurrentUserCollegeId();
     }
 
     /**

+ 9 - 62
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<XgSysDept> 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<XgSysDept> parentColleges = xgSysDeptMapper.selectList(parentQuery);
-            if (!parentColleges.isEmpty()) {
-                return parentColleges.get(0).getDeptId();
-            }
-
-            // 如果上面的查询没找到,尝试通过dept_category查找
-            LambdaQueryWrapper<XgSysDept> 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<XgSysDept> 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());
         }
     }
 

+ 4 - 52
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<XgSysDept> 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<XgSysDept> parentColleges = xgSysDeptMapper.selectList(parentQuery);
-            if (!parentColleges.isEmpty()) {
-                return parentColleges.get(0).getDeptId();
-            }
-
-            // 如果上面的查询没找到,尝试通过dept_category查找
-            LambdaQueryWrapper<XgSysDept> 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<XgSysDept> categoryColleges = xgSysDeptMapper.selectList(categoryQuery);
-            return categoryColleges.isEmpty() ? null : categoryColleges.get(0).getDeptId();
-
-        } catch (Exception e) {
-            log.error("获取当前用户二级学院ID失败", e);
-            return null;
-        }
+        return currentUserCollegeHelper.resolveCurrentUserCollegeId();
     }
 
     /**

+ 14 - 57
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 {
     /**
      * 获取当前用户所在学院的学生信息树(宿舍「分配到班级」专用入口)。
      * <p>
-     * 一年级代理规则(不改班级真实学院归属):
+     * 一年级代理规则(不改班级真实学院归属;班级树以 xg_class_msg 为准):
      * <ul>
      *   <li>代理学院(人文学院):本院全部班级 + 全校一年级班级</li>
      *   <li>其他学院:本院非一年级班级(一年级隐藏,防双头操作)</li>
-     *   <li>代理学院未配置:保持原逻辑,仅本院班级</li>
+     *   <li>代理学院未配置:按非代理处理(本院 − 一年级),并打警告日志</li>
      * </ul>
      */
     @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<Map<String, Object>> collegeStats = baseMapper.getCollegeStudentStatsByClass(collegeId);
+            log.info("宿舍班级树:userCollegeId={}, proxyCollegeId={}, cohortYear={}, ownClassRows={}",
+                collegeId, proxyCollegeId, cohortYear, collegeStats.size());
 
             if (proxyCollegeId == null) {
-                return getStudentTreeByClass(collegeId);
+                log.warn("一年级宿舍代理学院未配置,宿舍班级树按非代理处理(本院排除一年级)");
+                List<Map<String, Object>> nonFreshmanStats = collegeStats.stream()
+                    .filter(item -> !isFreshmanStatRow(item, cohortYear))
+                    .collect(Collectors.toList());
+                return buildStudentTreeFromClassStats(nonFreshmanStats);
             }
 
-            List<Map<String, Object>> collegeStats = baseMapper.getCollegeStudentStatsByClass(collegeId);
             if (proxyCollegeId.equals(collegeId)) {
                 List<Map<String, Object>> 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<XgSysDept> 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<XgSysDept> parentColleges = xgSysDeptMapper.selectList(parentQuery);
-            if (!parentColleges.isEmpty()) {
-                return parentColleges.get(0).getDeptId();
-            }
-
-            // 如果上面的查询没找到,尝试通过dept_category查找
-            LambdaQueryWrapper<XgSysDept> 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<XgSysDept> categoryColleges = xgSysDeptMapper.selectList(categoryQuery);
-            return categoryColleges.isEmpty() ? null : categoryColleges.get(0).getDeptId();
-
-        } catch (Exception e) {
-            log.error("获取当前用户二级学院ID失败", e);
-            return null;
-        }
+        return currentUserCollegeHelper.resolveCurrentUserCollegeId();
     }
 }

+ 52 - 74
backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsg/XgStudentMsgMapper.xml

@@ -218,26 +218,28 @@ 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
     </select>
 
-    <!-- 查询二级学院下的学院、专业、班级学生统计信息(按床位分配统计) -->
-    <select id="getCollegeStudentStatsByClass" resultType="java.util.Map">
-        SELECT
+    <!--
+      宿舍「分配到班级」班级树:以 xg_class_msg 为主数据源(无住宿生也出班节点)。
+      住宿生人数 / 已分配床位仅 LEFT JOIN 统计。
+    -->
+    <sql id="dormClassTreeSelectColumns">
             c.grade,
-            COALESCE(c.enrollment_year, sm.enrollment_year) AS enrollment_year,
-            sm.college_id,
-            sm.college_name,
-            sm.major_id,
-            sm.major_name,
-            sm.class_id,
-            sm.class_name,
-            COALESCE(SUM(CASE WHEN sm.gender = '0' AND sm.is_boarder = 1 THEN 1 ELSE 0 END), 0) AS male_total,
-            COALESCE(SUM(CASE WHEN sm.gender = '1' AND sm.is_boarder = 1 THEN 1 ELSE 0 END), 0) AS female_total,
+            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 = sm.class_id
+                WHERE dbs.class_id = c.id
                   AND dbs.deleted = '0'
                   AND dbs.status = '0'
                   AND db.deleted = '0'
@@ -249,76 +251,52 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 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 = sm.class_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_student_msg sm
-        LEFT JOIN xg_class_msg c ON sm.class_id = c.id
-        LEFT JOIN sys_dept d ON sm.college_id = d.dept_id
-        WHERE sm.deleted = 0
-          AND sm.student_status = '1'
-          AND sm.is_boarder = 1
-          AND (d.parent_id = #{secondaryCollegeId} OR d.dept_id = #{secondaryCollegeId} OR FIND_IN_SET(#{secondaryCollegeId}, d.ancestors))
-          AND sm.college_id IS NOT NULL
-          AND sm.major_id IS NOT NULL
-          AND sm.class_id IS NOT NULL
-        GROUP BY c.grade, COALESCE(c.enrollment_year, sm.enrollment_year),
-                 sm.college_id, sm.college_name, sm.major_id, sm.major_name, sm.class_id, sm.class_name
-        ORDER BY c.grade DESC, sm.college_name, sm.major_name, sm.class_name, MAX(sm.create_time) DESC
+    </sql>
+
+    <sql id="dormClassTreeFromJoin">
+        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'
+    </sql>
+
+    <sql id="dormClassTreeGroupBy">
+        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
+    </sql>
+
+    <!-- 本院全部有效班级(宿舍分配到班级) -->
+    <select id="getCollegeStudentStatsByClass" resultType="java.util.Map">
+        SELECT
+        <include refid="dormClassTreeSelectColumns"/>
+        <include refid="dormClassTreeFromJoin"/>
+        WHERE c.deleted = 0
+          AND c.college_id = #{collegeId}
+          AND c.college_id IS NOT NULL
+          AND c.major_id IS NOT NULL
+        <include refid="dormClassTreeGroupBy"/>
+        ORDER BY c.grade DESC, college_name, major_name, c.class_name
     </select>
 
-    <!-- 全校一年级班级统计(宿舍分配到班级:人文学院代理可见) -->
+    <!-- 全校一年级班级(宿舍分配到班级:人文学院代理可见;以班级 enrollment_year 为准) -->
     <select id="getFreshmanStudentStatsByClass" resultType="java.util.Map">
         SELECT
-            c.grade,
-            COALESCE(c.enrollment_year, sm.enrollment_year) AS enrollment_year,
-            sm.college_id,
-            sm.college_name,
-            sm.major_id,
-            sm.major_name,
-            sm.class_id,
-            sm.class_name,
-            COALESCE(SUM(CASE WHEN sm.gender = '0' AND sm.is_boarder = 1 THEN 1 ELSE 0 END), 0) AS male_total,
-            COALESCE(SUM(CASE WHEN 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 = sm.class_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 = sm.class_id
-                  AND dbs.deleted = '0'
-                  AND dbs.status = '0'
-                  AND db.deleted = '0'
-                  AND dbuild.building_type = '2'
-            ), 0) AS female_assigned
-        FROM xg_student_msg sm
-        LEFT JOIN xg_class_msg c ON sm.class_id = c.id
-        WHERE sm.deleted = 0
-          AND sm.student_status = '1'
-          AND sm.is_boarder = 1
-          AND sm.college_id IS NOT NULL
-          AND sm.major_id IS NOT NULL
-          AND sm.class_id IS NOT NULL
-          AND COALESCE(c.enrollment_year, sm.enrollment_year) = #{enrollmentYear}
-        GROUP BY c.grade, COALESCE(c.enrollment_year, sm.enrollment_year),
-                 sm.college_id, sm.college_name, sm.major_id, sm.major_name, sm.class_id, sm.class_name
-        ORDER BY sm.college_name, sm.major_name, sm.class_name, MAX(sm.create_time) DESC
+        <include refid="dormClassTreeSelectColumns"/>
+        <include refid="dormClassTreeFromJoin"/>
+        WHERE c.deleted = 0
+          AND c.college_id IS NOT NULL
+          AND c.major_id IS NOT NULL
+          AND c.enrollment_year = #{enrollmentYear}
+        <include refid="dormClassTreeGroupBy"/>
+        ORDER BY college_name, major_name, c.class_name
     </select>
 
     <!-- 查询二级学院下的学院床位分配统计信息(基于学生表统计需求,基于床位分配表统计已分配) -->

+ 45 - 0
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;

+ 59 - 5
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)