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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
“wuzhen”andCursor committed 2026-10-14 09:46:04 +08:00
1 parent 04d12bbce1
commit a0bb39d069
10 files changed
+324 -330

No files matched your search

@@ -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-");
}
}
@@ -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
+ "」的宿舍分配(一年级由人文学院代理,其它年级仅本学院可操作)");
}
}
}
@@ -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);
@@ -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();
}
/**
@@ -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());
}
}
@@ -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();
}
/**
@@ -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();
}
}
@@ -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
</select>
<!-- 查询二级学院下的学院、专业、班级学生统计信息(按床位分配统计) -->
<!--
宿舍「分配到班级」班级树:以 xg_class_msg 为主数据源(无住宿生也出班节点)。
住宿生人数 / 已分配床位仅 LEFT JOIN 统计。
-->
<sql id="dormClassTreeSelectColumns">
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
</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
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
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
<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>
<!-- 查询二级学院下的学院床位分配统计信息(基于学生表统计需求,基于床位分配表统计已分配) -->
@@ -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;
@@ -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)