diff --git a/backend/ruoyi-modules/ruoyi-xg/pom.xml b/backend/ruoyi-modules/ruoyi-xg/pom.xml index 3b47f0fb..cc54a015 100644 --- a/backend/ruoyi-modules/ruoyi-xg/pom.xml +++ b/backend/ruoyi-modules/ruoyi-xg/pom.xml @@ -113,6 +113,12 @@ ${revision} + + org.springframework.boot + spring-boot-starter-test + test + + 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 new file mode 100644 index 00000000..bc6ddc73 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/FreshmanCollegeProxyHelper.java @@ -0,0 +1,77 @@ +package org.dromara.xg.helper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.core.service.ConfigService; +import org.dromara.common.core.utils.StringUtils; +import org.springframework.stereotype.Component; + +/** + * 一年级代理学院(人文学院)解析与宿舍分配权限判定。 + *

+ * 配置键复用请假:{@link FreshmanCollegeProxySupport#FRESHMAN_PROXY_COLLEGE_CONFIG_KEY}。 + * 不修改班级真实 college_id,仅运行时判定操作权。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class FreshmanCollegeProxyHelper { + + private final ConfigService configService; + + public int currentCohortYear() { + return FreshmanCollegeProxySupport.currentCohortYear(); + } + + public boolean isFreshmanByEnrollmentYear(Long enrollmentYear) { + return FreshmanCollegeProxySupport.isFreshmanByEnrollmentYear(enrollmentYear); + } + + /** + * 读取代理学院;未配置或非法时抛业务异常(请假等强制场景)。 + */ + public Long requireProxyCollegeId() { + Long proxyCollegeId = resolveProxyCollegeIdOrNull(); + if (proxyCollegeId == null) { + throw new ServiceException("一年级请假代理学院未配置或无效,请在系统参数中配置 " + + FreshmanCollegeProxySupport.FRESHMAN_PROXY_COLLEGE_CONFIG_KEY); + } + return proxyCollegeId; + } + + /** + * 读取代理学院;未配置时返回 null(宿舍树降级为原逻辑,避免拖垮其它功能)。 + */ + public Long resolveProxyCollegeIdOrNull() { + String configValue = configService.getConfigValue( + FreshmanCollegeProxySupport.FRESHMAN_PROXY_COLLEGE_CONFIG_KEY); + if (StringUtils.isBlank(configValue)) { + log.warn("系统参数 {} 未配置,一年级宿舍代理不生效", + FreshmanCollegeProxySupport.FRESHMAN_PROXY_COLLEGE_CONFIG_KEY); + return null; + } + try { + return Long.valueOf(configValue.trim()); + } catch (NumberFormatException e) { + log.warn("系统参数 {} 配置无效: {}", + FreshmanCollegeProxySupport.FRESHMAN_PROXY_COLLEGE_CONFIG_KEY, configValue); + return null; + } + } + + public boolean isProxyCollege(Long collegeId) { + Long proxyCollegeId = resolveProxyCollegeIdOrNull(); + return proxyCollegeId != null && proxyCollegeId.equals(collegeId); + } + + /** + * 当前学院用户是否可对指定班级做宿舍「分配到班级 / 回收班级分配」。 + */ + public boolean canOperateClassForDormAllocation(Long operatorCollegeId, + Long classCollegeId, + Long enrollmentYear) { + return FreshmanCollegeProxySupport.canOperateClassForDormAllocation( + operatorCollegeId, classCollegeId, enrollmentYear, resolveProxyCollegeIdOrNull()); + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/FreshmanCollegeProxySupport.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/FreshmanCollegeProxySupport.java new file mode 100644 index 00000000..c9b1ec75 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/helper/FreshmanCollegeProxySupport.java @@ -0,0 +1,70 @@ +package org.dromara.xg.helper; + +import java.time.LocalDate; +import java.time.ZoneId; + +/** + * 一年级代理学院纯计算逻辑(无 Spring 依赖,便于单测)。 + *

+ * 学年切换日:每年 9 月 1 日(Asia/Shanghai)。与请假模块口径一致。 + */ +public final class FreshmanCollegeProxySupport { + + public static final ZoneId SHANGHAI_ZONE = ZoneId.of("Asia/Shanghai"); + + /** 与请假共用的系统参数键 */ + public static final String FRESHMAN_PROXY_COLLEGE_CONFIG_KEY = "xg.studentLeave.freshmanProxyCollegeId"; + + private FreshmanCollegeProxySupport() { + } + + /** + * 当前入学届别年份(9 月 1 日起算新一届)。 + */ + public static int currentCohortYear(LocalDate today) { + LocalDate date = today != null ? today : LocalDate.now(SHANGHAI_ZONE); + return date.getMonthValue() >= 9 ? date.getYear() : date.getYear() - 1; + } + + /** + * 当前入学届别年份(按上海时区当天)。 + */ + public static int currentCohortYear() { + return currentCohortYear(LocalDate.now(SHANGHAI_ZONE)); + } + + /** + * 是否一年级(入学年等于当前届)。入学年为空视为非一年级,避免误代理。 + */ + public static boolean isFreshmanByEnrollmentYear(Long enrollmentYear, LocalDate today) { + if (enrollmentYear == null) { + return false; + } + return enrollmentYear.intValue() == currentCohortYear(today); + } + + public static boolean isFreshmanByEnrollmentYear(Long enrollmentYear) { + return isFreshmanByEnrollmentYear(enrollmentYear, LocalDate.now(SHANGHAI_ZONE)); + } + + /** + * 宿舍「分配到班级」权限判定。 + * + * @param operatorCollegeId 操作人所属二级学院 + * @param classCollegeId 班级真实所属学院 + * @param enrollmentYear 班级入学年 + * @param proxyCollegeId 代理学院(未配置则为 null) + */ + public static boolean canOperateClassForDormAllocation(Long operatorCollegeId, + Long classCollegeId, + Long enrollmentYear, + Long proxyCollegeId) { + if (operatorCollegeId == null) { + return false; + } + if (isFreshmanByEnrollmentYear(enrollmentYear)) { + return proxyCollegeId != null && proxyCollegeId.equals(operatorCollegeId); + } + return classCollegeId != null && operatorCollegeId.equals(classCollegeId); + } +} 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 2ce3da84..1aa21ee3 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 @@ -110,6 +110,14 @@ public interface XgStudentMsgMapper extends BaseMapperPlus> getCollegeStudentStatsByClass(@Param("secondaryCollegeId") Long secondaryCollegeId); + /** + * 按入学年查询全校一年级班级学生统计(宿舍分配到班级:代理学院可见) + * + * @param enrollmentYear 入学年(当前届) + * @return 学生统计信息列表(含 enrollment_year) + */ + List> getFreshmanStudentStatsByClass(@Param("enrollmentYear") Integer enrollmentYear); + /** * 查询二级学院下的学院床位分配统计信息(基于college_id统计) * 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 ec004ac0..58763c06 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 @@ -1,5 +1,6 @@ package org.dromara.xg.service.impl; +import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.mybatis.core.page.TableDataInfo; @@ -57,6 +58,7 @@ import org.dromara.xg.mapper.XgClassMsgMapper; 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; /** * 床位学生分配关系Service业务层处理 @@ -79,6 +81,7 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { private final XgSysDeptMapper xgSysDeptMapper; private final XgClassMsgMapper xgClassMsgMapper; private final IXgStudentLeaveService xgStudentLeaveService; + private final FreshmanCollegeProxyHelper freshmanCollegeProxyHelper; /** * 查询床位学生分配关系 @@ -526,6 +529,13 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { public Boolean batchRecallClassAllocation(XgDormBedBatchRecallBo bo) { Date now = new Date(); Long loginUserId = LoginHelper.getUserId(); + Long operatorCollegeId = getCurrentUserCollegeId(); + if (operatorCollegeId == null) { + throw new ServiceException("无法识别当前用户所属学院,禁止回收班级分配"); + } + if (bo.getBedIds() == null || bo.getBedIds().isEmpty()) { + throw new ServiceException("床位列表不能为空"); + } // 1) 查出这些床位当前有效的分配关系(status=0, deleted=0,并且已分配给班级) LambdaQueryWrapper activeAssignQuery = Wrappers.lambdaQuery(); @@ -539,11 +549,20 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { return true; // 没有需要处理的数据 } - // 2) 更新分配关系:清空班级相关字段,但保留学院分配信息 + // 1.1) 安全校验:仅可回收本学院床位;一年级班级仅代理学院可回收 + assertOperatorCanManageBedAssignments(operatorCollegeId, activeAssignments); + + Set allowedBedIds = activeAssignments.stream() + .map(XgDormBedStudent::getBedId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + // 2) 更新分配关系:清空班级相关字段,但保留学院分配信息(仅允许的床位) LambdaUpdateWrapper updateAssign = Wrappers.lambdaUpdate(); - updateAssign.in(XgDormBedStudent::getBedId, bo.getBedIds()) + updateAssign.in(XgDormBedStudent::getBedId, allowedBedIds) .eq(XgDormBedStudent::getStatus, "0") .eq(XgDormBedStudent::getDeleted, "0") + .eq(XgDormBedStudent::getCollegeId, operatorCollegeId) .isNotNull(XgDormBedStudent::getClassId) .set(XgDormBedStudent::getMajorId, null) .set(XgDormBedStudent::getClassId, null) @@ -588,6 +607,17 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { @Transactional(rollbackFor = Exception.class) public Boolean batchAllocateBedsToClass(XgDormBedBatchAllocateToClassBo bo) { try { + Long operatorCollegeId = getCurrentUserCollegeId(); + if (operatorCollegeId == null) { + throw new ServiceException("无法识别当前用户所属学院,禁止分配床位到班级"); + } + if (bo.getBedIds() == null || bo.getBedIds().isEmpty()) { + throw new ServiceException("床位列表不能为空"); + } + if (bo.getClassIds() == null || bo.getClassIds().isEmpty()) { + throw new ServiceException("班级列表不能为空"); + } + // 1. 获取床位信息 List beds = xgDormBedMapper.selectBatchIds(bo.getBedIds()); @@ -610,7 +640,7 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { .map(XgDormBed::getBedName) .collect(java.util.stream.Collectors.toList()); - throw new RuntimeException("以下床位已分配给班级,无法重复分配:" + String.join("、", allocatedBedNames)); + throw new ServiceException("以下床位已分配给班级,无法重复分配:" + String.join("、", allocatedBedNames)); } // 3. 检查床位是否已分配给学院(必须先分配给学院才能分配给班级) @@ -618,12 +648,20 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { LambdaQueryWrapper collegeCheckWrapper = new LambdaQueryWrapper<>(); collegeCheckWrapper.in(XgDormBedStudent::getBedId, bo.getBedIds()) .isNotNull(XgDormBedStudent::getCollegeId) // 已分配给学院 - .eq(XgDormBedStudent::getStatus, "0"); // 正常状态,未退宿 + .eq(XgDormBedStudent::getStatus, "0") // 正常状态,未退宿 + .eq(XgDormBedStudent::getDeleted, "0"); List existingCollegeAllocations = baseMapper.selectList(collegeCheckWrapper); if (existingCollegeAllocations.isEmpty()) { - throw new RuntimeException("选择的床位尚未分配给学院,请先分配给学院后再分配给班级"); + throw new ServiceException("选择的床位尚未分配给学院,请先分配给学院后再分配给班级"); + } + + // 3.1 安全:只能操作已归属本学院的床位(防跨学院越权) + boolean hasForeignCollegeBed = existingCollegeAllocations.stream() + .anyMatch(item -> !operatorCollegeId.equals(item.getCollegeId())); + if (hasForeignCollegeBed) { + throw new ServiceException("只能将本学院已分配的床位再分配到班级"); } // 4. 验证所有床位都已分配给学院 @@ -641,22 +679,31 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { .map(XgDormBed::getBedName) .collect(java.util.stream.Collectors.toList()); - throw new RuntimeException("以下床位尚未分配给学院,无法分配给班级:" + String.join("、", unallocatedBedNames)); + throw new ServiceException("以下床位尚未分配给学院,无法分配给班级:" + String.join("、", unallocatedBedNames)); } - log.info("床位预检验证通过:75个床位中,{}个已分配给学院,{}个未分配给学院", - collegeAllocatedBedIds.size(), unallocatedBedIds.size()); + log.info("床位预检验证通过:请求{}个床位,{}个已分配给本学院", + bo.getBedIds().size(), collegeAllocatedBedIds.size()); // 5. 获取楼栋信息,用于获取校区ID和宿舍区ID XgDormBuilding building = xgDormBuildingMapper.selectById(bo.getBuildingId()); if (building == null) { - throw new RuntimeException("楼栋信息不存在,buildingId: " + bo.getBuildingId()); + throw new ServiceException("楼栋信息不存在,buildingId: " + bo.getBuildingId()); } // 6. 查询班级信息,获取专业ID和学院ID List classes = xgClassMsgMapper.selectBatchIds(bo.getClassIds()); if (classes.size() != bo.getClassIds().size()) { - throw new RuntimeException("部分班级信息不存在,请检查班级ID"); + throw new ServiceException("部分班级信息不存在,请检查班级ID"); + } + + // 6.1 安全:一年级仅代理学院可分;非一年级仅本学院可分 + for (XgClassMsg clazz : classes) { + if (!freshmanCollegeProxyHelper.canOperateClassForDormAllocation( + operatorCollegeId, clazz.getCollegeId(), clazz.getEnrollmentYear())) { + throw new ServiceException("无权将床位分配给班级「" + clazz.getClassName() + + "」(一年级由人文学院代理,其它年级仅本学院可操作)"); + } } // 建立班级ID到班级信息的映射 @@ -1096,6 +1143,40 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { } } + /** + * 校验操作人是否可管理这些床位的班级分配(防跨学院 / 防非代理操作一年级)。 + */ + private void assertOperatorCanManageBedAssignments(Long operatorCollegeId, + List assignments) { + Set classIds = assignments.stream() + .map(XgDormBedStudent::getClassId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Map classMap = classIds.isEmpty() + ? Map.of() + : xgClassMsgMapper.selectBatchIds(classIds).stream() + .collect(Collectors.toMap(XgClassMsg::getId, c -> c, (a, b) -> a)); + + for (XgDormBedStudent assignment : assignments) { + if (!operatorCollegeId.equals(assignment.getCollegeId())) { + throw new ServiceException("只能回收本学院床位的班级分配"); + } + Long classId = assignment.getClassId(); + if (classId == null) { + continue; + } + XgClassMsg clazz = classMap.get(classId); + if (clazz == null) { + throw new ServiceException("床位关联班级不存在,禁止回收"); + } + if (!freshmanCollegeProxyHelper.canOperateClassForDormAllocation( + operatorCollegeId, clazz.getCollegeId(), clazz.getEnrollmentYear())) { + throw new ServiceException("无权回收班级「" + clazz.getClassName() + + "」的宿舍分配(一年级由人文学院代理)"); + } + } + } + /** * 获取分配给指定学院的宿舍楼ID列表 */ 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 00dbdb61..1f114d6b 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 @@ -1,7 +1,6 @@ package org.dromara.xg.service.impl; import org.dromara.common.core.exception.ServiceException; -import org.dromara.common.core.service.ConfigService; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.mybatis.core.page.TableDataInfo; @@ -46,7 +45,7 @@ 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.common.satoken.utils.LoginHelper; +import org.dromara.xg.helper.FreshmanCollegeProxyHelper; import java.util.stream.Collectors; import java.util.List; @@ -57,8 +56,7 @@ import java.util.Calendar; import java.util.Date; import java.util.Set; import java.util.HashSet; -import java.time.LocalDate; -import java.time.ZoneId; +import java.util.LinkedHashMap; /** * 学生信息Service业务层处理 @@ -71,14 +69,9 @@ import java.time.ZoneId; @Service public class XgStudentMsgServiceImpl implements IXgStudentMsgService { - /** 一年级请假代理学院(人文学院)dept_id,配置于 sys_config */ - private static final String FRESHMAN_PROXY_COLLEGE_CONFIG_KEY = "xg.studentLeave.freshmanProxyCollegeId"; - /** 请假二级学院审批角色 */ private static final String LEAVE_COLLEGE_APPROVER_ROLE_KEY = "xsqj-ejxyspr"; - private static final ZoneId SHANGHAI_ZONE = ZoneId.of("Asia/Shanghai"); - private final XgStudentMsgMapper baseMapper; private final ISysUserService sysUserService; private final SysDeptMapper sysDeptMapper; @@ -88,7 +81,7 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { private final XgClassMsgMapper xgClassMsgMapper; private final XgDormBedStudentMapper xgDormBedStudentMapper; private final XgSysDeptMapper xgSysDeptMapper; - private final ConfigService configService; + private final FreshmanCollegeProxyHelper freshmanCollegeProxyHelper; /** * 查询学生信息 @@ -1149,44 +1142,18 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { throw new ServiceException("未找到该学生所在学院信息,无法提交请假"); } - if (!isFreshmanByEnrollmentYear(enrollmentYear)) { + if (!freshmanCollegeProxyHelper.isFreshmanByEnrollmentYear(enrollmentYear)) { log.info("非一年级请假,使用学生本学院 - studentId: {}, enrollmentYear: {}, collegeId: {}", studentId, enrollmentYear, ownCollegeId); return ownCollegeId; } - Long proxyCollegeId = resolveFreshmanProxyCollegeId(); + Long proxyCollegeId = freshmanCollegeProxyHelper.requireProxyCollegeId(); log.info("一年级请假,使用代理学院 - studentId: {}, enrollmentYear: {}, proxyCollegeId: {}, ownCollegeId: {}", studentId, enrollmentYear, proxyCollegeId, ownCollegeId); return proxyCollegeId; } - /** - * 按入学年判断是否一年级。学年切换日:每年 9 月 1 日(Asia/Shanghai)。 - */ - private boolean isFreshmanByEnrollmentYear(Long enrollmentYear) { - LocalDate today = LocalDate.now(SHANGHAI_ZONE); - int currentCohortYear = today.getMonthValue() >= 9 ? today.getYear() : today.getYear() - 1; - return enrollmentYear.intValue() == currentCohortYear; - } - - /** - * 从系统参数读取一年级请假代理学院 dept_id。 - */ - private Long resolveFreshmanProxyCollegeId() { - String configValue = configService.getConfigValue(FRESHMAN_PROXY_COLLEGE_CONFIG_KEY); - if (StringUtils.isBlank(configValue)) { - throw new ServiceException("一年级请假代理学院未配置,请在系统参数中配置 " - + FRESHMAN_PROXY_COLLEGE_CONFIG_KEY); - } - try { - return Long.valueOf(configValue.trim()); - } catch (NumberFormatException e) { - throw new ServiceException("一年级请假代理学院配置无效(应为部门ID数字),请检查参数 " - + FRESHMAN_PROXY_COLLEGE_CONFIG_KEY); - } - } - private Long parseCollegeId(String collegeIdStr, Long studentId) { if (StringUtils.isBlank(collegeIdStr)) { return null; @@ -1365,165 +1332,60 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { */ public List getStudentTreeByClass(Long secondaryCollegeId) { log.info("开始构建学生信息树(按床位分配统计),二级学院ID: {}", secondaryCollegeId); - - List result = new ArrayList<>(); - try { - // 1. 查询二级学院下的所有学院信息,并统计男女生人数(使用新的按班级床位分配统计方法) List> collegeStats = baseMapper.getCollegeStudentStatsByClass(secondaryCollegeId); - log.info("查询到 {} 个学院", collegeStats.size()); - - // 按学院分组 - Map>> collegeMap = collegeStats.stream() - .collect(Collectors.groupingBy(item -> { - Object collegeIdObj = item.get("college_id"); - Long collegeId = collegeIdObj instanceof Long ? (Long) collegeIdObj - : Long.valueOf(collegeIdObj.toString()); - String collegeName = (String) item.get("college_name"); - return collegeId + "_" + collegeName; - })); - - // 2. 构建学院节点 - for (Map.Entry>> collegeEntry : collegeMap.entrySet()) { - String collegeKey = collegeEntry.getKey(); - List> collegeMajors = collegeEntry.getValue(); - - String[] keyParts = collegeKey.split("_"); - Long collegeId = Long.valueOf(keyParts[0]); - String collegeName = keyParts[1]; - - // 计算学院级别的统计信息 - int collegeMaleTotal = 0; - int collegeFemaleTotal = 0; - int collegeMaleAssigned = 0; - int collegeFemaleAssigned = 0; - - for (Map item : collegeMajors) { - collegeMaleTotal += ((Number) item.get("male_total")).intValue(); - collegeFemaleTotal += ((Number) item.get("female_total")).intValue(); - collegeMaleAssigned += ((Number) item.get("male_assigned")).intValue(); - collegeFemaleAssigned += ((Number) item.get("female_assigned")).intValue(); - } - - XgStudentTreeVo collegeNode = new XgStudentTreeVo(); - collegeNode.setId(collegeId.toString()); - collegeNode.setLabel(collegeName); - collegeNode.setType("college"); - collegeNode.setCollegeId(collegeId); - collegeNode.setCollegeName(collegeName); - collegeNode.setMaleTotal(collegeMaleTotal); - collegeNode.setFemaleTotal(collegeFemaleTotal); - collegeNode.setMaleAssigned(collegeMaleAssigned); - collegeNode.setFemaleAssigned(collegeFemaleAssigned); - collegeNode.setChildren(new ArrayList<>()); - - // 按专业分组 - Map>> majorMap = collegeMajors.stream() - .collect(Collectors.groupingBy(item -> { - Object majorIdObj = item.get("major_id"); - Long majorId = majorIdObj instanceof Long ? (Long) majorIdObj - : Long.valueOf(majorIdObj.toString()); - String majorName = (String) item.get("major_name"); - return majorId + "_" + majorName; - })); - - // 3. 构建专业节点 - for (Map.Entry>> majorEntry : majorMap.entrySet()) { - String majorKey = majorEntry.getKey(); - List> majorClasses = majorEntry.getValue(); - - String[] majorKeyParts = majorKey.split("_"); - Long majorId = Long.valueOf(majorKeyParts[0]); - String majorName = majorKeyParts[1]; - - // 计算专业级别的统计信息 - int majorMaleTotal = 0; - int majorFemaleTotal = 0; - int majorMaleAssigned = 0; - int majorFemaleAssigned = 0; - - for (Map item : majorClasses) { - majorMaleTotal += ((Number) item.get("male_total")).intValue(); - majorFemaleTotal += ((Number) item.get("female_total")).intValue(); - majorMaleAssigned += ((Number) item.get("male_assigned")).intValue(); - majorFemaleAssigned += ((Number) item.get("female_assigned")).intValue(); - } - - XgStudentTreeVo majorNode = new XgStudentTreeVo(); - majorNode.setId(majorId.toString()); - majorNode.setLabel(majorName); - majorNode.setType("major"); - majorNode.setMajorId(majorId); - majorNode.setMajorName(majorName); - majorNode.setMaleTotal(majorMaleTotal); - majorNode.setFemaleTotal(majorFemaleTotal); - majorNode.setMaleAssigned(majorMaleAssigned); - majorNode.setFemaleAssigned(majorFemaleAssigned); - majorNode.setChildren(new ArrayList<>()); - - // 4. 构建班级节点 - for (Map classData : majorClasses) { - Object classIdObj = classData.get("class_id"); - Long classId = classIdObj instanceof Long ? (Long) classIdObj - : Long.valueOf(classIdObj.toString()); - String className = (String) classData.get("class_name"); - String grade = (String) classData.get("grade"); - - int classMaleTotal = ((Number) classData.get("male_total")).intValue(); - int classFemaleTotal = ((Number) classData.get("female_total")).intValue(); - int classMaleAssigned = ((Number) classData.get("male_assigned")).intValue(); - int classFemaleAssigned = ((Number) classData.get("female_assigned")).intValue(); - - XgStudentTreeVo classNode = new XgStudentTreeVo(); - classNode.setId(classId.toString()); - classNode.setLabel(className); - classNode.setType("class"); - classNode.setGrade(grade); - classNode.setClassId(classId); - classNode.setClassName(className); - classNode.setMaleTotal(classMaleTotal); - classNode.setFemaleTotal(classFemaleTotal); - classNode.setMaleAssigned(classMaleAssigned); - classNode.setFemaleAssigned(classFemaleAssigned); - classNode.setChildren(new ArrayList<>()); - - majorNode.getChildren().add(classNode); - } - - collegeNode.getChildren().add(majorNode); - } - - result.add(collegeNode); - } - - log.info("成功构建学生信息树,共 {} 个学院", result.size()); - + return buildStudentTreeFromClassStats(collegeStats); } catch (Exception e) { log.error("构建学生信息树失败", e); + return new ArrayList<>(); } - - return result; } /** - * 获取当前用户所在学院的学生信息树 - * - * @return 学生信息树 + * 获取当前用户所在学院的学生信息树(宿舍「分配到班级」专用入口)。 + *

+ * 一年级代理规则(不改班级真实学院归属): + *

*/ @Override public List getCurrentUserStudentTree() { try { - // 1. 获取当前用户的二级学院ID Long collegeId = getCurrentUserCollegeId(); if (collegeId == null) { log.warn("无法获取当前用户的二级学院信息"); return new ArrayList<>(); } - log.info("获取到当前用户的学院ID: {}", collegeId); + Long proxyCollegeId = freshmanCollegeProxyHelper.resolveProxyCollegeIdOrNull(); + int cohortYear = freshmanCollegeProxyHelper.currentCohortYear(); + log.info("宿舍班级树:userCollegeId={}, proxyCollegeId={}, cohortYear={}", + collegeId, proxyCollegeId, cohortYear); - // 2. 调用按床位分配统计的方法 - return getStudentTreeByClass(collegeId); + if (proxyCollegeId == null) { + return getStudentTreeByClass(collegeId); + } + + List> collegeStats = baseMapper.getCollegeStudentStatsByClass(collegeId); + if (proxyCollegeId.equals(collegeId)) { + List> freshmanStats = + baseMapper.getFreshmanStudentStatsByClass(cohortYear); + List> merged = + mergeOwnCollegeWithFreshmanStats(collegeStats, freshmanStats); + log.info("代理学院班级树:本院行={}, 一年级行={}, 合并后={}", + collegeStats.size(), freshmanStats.size(), merged.size()); + return buildStudentTreeFromClassStats(merged); + } + + List> nonFreshmanStats = collegeStats.stream() + .filter(item -> !isFreshmanStatRow(item, cohortYear)) + .collect(Collectors.toList()); + log.info("非代理学院班级树:本院行={}, 排除一年级后={}", + collegeStats.size(), nonFreshmanStats.size()); + return buildStudentTreeFromClassStats(nonFreshmanStats); } catch (Exception e) { log.error("获取当前用户学院的学生信息树失败", e); @@ -1531,6 +1393,161 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { } } + /** + * 本院统计 ∪ 全校一年级统计(按 class_id 去重,本院优先)。 + */ + private List> mergeOwnCollegeWithFreshmanStats( + List> ownCollegeStats, + List> freshmanStats) { + Map> byClassId = new LinkedHashMap<>(); + for (Map row : ownCollegeStats) { + String classKey = stringKey(row.get("class_id")); + if (classKey != null) { + byClassId.put(classKey, row); + } + } + for (Map row : freshmanStats) { + String classKey = stringKey(row.get("class_id")); + if (classKey != null) { + byClassId.putIfAbsent(classKey, row); + } + } + return new ArrayList<>(byClassId.values()); + } + + private boolean isFreshmanStatRow(Map row, int cohortYear) { + Object enrollmentYearObj = row.get("enrollment_year"); + if (enrollmentYearObj == null) { + return false; + } + long enrollmentYear = enrollmentYearObj instanceof Number + ? ((Number) enrollmentYearObj).longValue() + : Long.parseLong(enrollmentYearObj.toString()); + return enrollmentYear == cohortYear; + } + + private String stringKey(Object value) { + return value == null ? null : String.valueOf(value); + } + + /** + * 将班级统计行构建为学院→专业→班级树(与历史 getStudentTreeByClass 结构一致)。 + */ + private List buildStudentTreeFromClassStats(List> collegeStats) { + List result = new ArrayList<>(); + if (collegeStats == null || collegeStats.isEmpty()) { + return result; + } + + Map>> collegeMap = collegeStats.stream() + .collect(Collectors.groupingBy(item -> { + Object collegeIdObj = item.get("college_id"); + Long collegeId = collegeIdObj instanceof Long ? (Long) collegeIdObj + : Long.valueOf(collegeIdObj.toString()); + String collegeName = (String) item.get("college_name"); + return collegeId + "_" + collegeName; + }, LinkedHashMap::new, Collectors.toList())); + + for (Map.Entry>> collegeEntry : collegeMap.entrySet()) { + String collegeKey = collegeEntry.getKey(); + List> collegeMajors = collegeEntry.getValue(); + + int underscore = collegeKey.indexOf('_'); + Long collegeId = Long.valueOf(collegeKey.substring(0, underscore)); + String collegeName = collegeKey.substring(underscore + 1); + + int collegeMaleTotal = 0; + int collegeFemaleTotal = 0; + int collegeMaleAssigned = 0; + int collegeFemaleAssigned = 0; + for (Map item : collegeMajors) { + collegeMaleTotal += ((Number) item.get("male_total")).intValue(); + collegeFemaleTotal += ((Number) item.get("female_total")).intValue(); + collegeMaleAssigned += ((Number) item.get("male_assigned")).intValue(); + collegeFemaleAssigned += ((Number) item.get("female_assigned")).intValue(); + } + + XgStudentTreeVo collegeNode = new XgStudentTreeVo(); + collegeNode.setId(collegeId.toString()); + collegeNode.setLabel(collegeName); + collegeNode.setType("college"); + collegeNode.setCollegeId(collegeId); + collegeNode.setCollegeName(collegeName); + collegeNode.setMaleTotal(collegeMaleTotal); + collegeNode.setFemaleTotal(collegeFemaleTotal); + collegeNode.setMaleAssigned(collegeMaleAssigned); + collegeNode.setFemaleAssigned(collegeFemaleAssigned); + collegeNode.setChildren(new ArrayList<>()); + + Map>> majorMap = collegeMajors.stream() + .collect(Collectors.groupingBy(item -> { + Object majorIdObj = item.get("major_id"); + Long majorId = majorIdObj instanceof Long ? (Long) majorIdObj + : Long.valueOf(majorIdObj.toString()); + String majorName = (String) item.get("major_name"); + return majorId + "_" + majorName; + }, LinkedHashMap::new, Collectors.toList())); + + for (Map.Entry>> majorEntry : majorMap.entrySet()) { + String majorKey = majorEntry.getKey(); + List> majorClasses = majorEntry.getValue(); + int majorUnderscore = majorKey.indexOf('_'); + Long majorId = Long.valueOf(majorKey.substring(0, majorUnderscore)); + String majorName = majorKey.substring(majorUnderscore + 1); + + int majorMaleTotal = 0; + int majorFemaleTotal = 0; + int majorMaleAssigned = 0; + int majorFemaleAssigned = 0; + for (Map item : majorClasses) { + majorMaleTotal += ((Number) item.get("male_total")).intValue(); + majorFemaleTotal += ((Number) item.get("female_total")).intValue(); + majorMaleAssigned += ((Number) item.get("male_assigned")).intValue(); + majorFemaleAssigned += ((Number) item.get("female_assigned")).intValue(); + } + + XgStudentTreeVo majorNode = new XgStudentTreeVo(); + majorNode.setId(majorId.toString()); + majorNode.setLabel(majorName); + majorNode.setType("major"); + majorNode.setMajorId(majorId); + majorNode.setMajorName(majorName); + majorNode.setMaleTotal(majorMaleTotal); + majorNode.setFemaleTotal(majorFemaleTotal); + majorNode.setMaleAssigned(majorMaleAssigned); + majorNode.setFemaleAssigned(majorFemaleAssigned); + majorNode.setChildren(new ArrayList<>()); + + for (Map classData : majorClasses) { + Object classIdObj = classData.get("class_id"); + Long classId = classIdObj instanceof Long ? (Long) classIdObj + : Long.valueOf(classIdObj.toString()); + String className = (String) classData.get("class_name"); + String grade = (String) classData.get("grade"); + + XgStudentTreeVo classNode = new XgStudentTreeVo(); + classNode.setId(classId.toString()); + classNode.setLabel(className); + classNode.setType("class"); + classNode.setGrade(grade); + classNode.setClassId(classId); + classNode.setClassName(className); + classNode.setMaleTotal(((Number) classData.get("male_total")).intValue()); + classNode.setFemaleTotal(((Number) classData.get("female_total")).intValue()); + classNode.setMaleAssigned(((Number) classData.get("male_assigned")).intValue()); + classNode.setFemaleAssigned(((Number) classData.get("female_assigned")).intValue()); + classNode.setChildren(new ArrayList<>()); + majorNode.getChildren().add(classNode); + } + collegeNode.getChildren().add(majorNode); + } + result.add(collegeNode); + } + + log.info("成功构建学生信息树,共 {} 个学院", result.size()); + return result; + } + /** * 获取当前用户的二级学院ID * 通过当前用户的部门id,向上寻找包括他自己,找到dept_category前几位是xy-的就是二级学院 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 e5ed6db7..74db559a 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 @@ -222,6 +222,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + +