feat(dorm): 一年级宿舍分配到班级由人文学院代理操作
复用请假代理学院配置,班级树对人文学院合并全校一年级、对他院隐藏一年级;分配/回收增加本学院床位与一年级代理写校验,不改班级真实归属。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
103a53f6f3
commit
04d12bbce1
10 files changed
+597
-189
No files matched your search
@@ -113,6 +113,12 @@
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
+77
@@ -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;
|
||||
|
||||
/**
|
||||
* 一年级代理学院(人文学院)解析与宿舍分配权限判定。
|
||||
* <p>
|
||||
* 配置键复用请假:{@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());
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package org.dromara.xg.helper;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/**
|
||||
* 一年级代理学院纯计算逻辑(无 Spring 依赖,便于单测)。
|
||||
* <p>
|
||||
* 学年切换日:每年 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);
|
||||
}
|
||||
}
|
||||
+8
@@ -110,6 +110,14 @@ public interface XgStudentMsgMapper extends BaseMapperPlus<XgStudentMsg, XgStude
|
||||
*/
|
||||
List<Map<String, Object>> getCollegeStudentStatsByClass(@Param("secondaryCollegeId") Long secondaryCollegeId);
|
||||
|
||||
/**
|
||||
* 按入学年查询全校一年级班级学生统计(宿舍分配到班级:代理学院可见)
|
||||
*
|
||||
* @param enrollmentYear 入学年(当前届)
|
||||
* @return 学生统计信息列表(含 enrollment_year)
|
||||
*/
|
||||
List<Map<String, Object>> getFreshmanStudentStatsByClass(@Param("enrollmentYear") Integer enrollmentYear);
|
||||
|
||||
/**
|
||||
* 查询二级学院下的学院床位分配统计信息(基于college_id统计)
|
||||
*
|
||||
|
||||
+91
-10
@@ -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<XgDormBedStudent> activeAssignQuery = Wrappers.lambdaQuery();
|
||||
@@ -539,11 +549,20 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
|
||||
return true; // 没有需要处理的数据
|
||||
}
|
||||
|
||||
// 2) 更新分配关系:清空班级相关字段,但保留学院分配信息
|
||||
// 1.1) 安全校验:仅可回收本学院床位;一年级班级仅代理学院可回收
|
||||
assertOperatorCanManageBedAssignments(operatorCollegeId, activeAssignments);
|
||||
|
||||
Set<Long> allowedBedIds = activeAssignments.stream()
|
||||
.map(XgDormBedStudent::getBedId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 2) 更新分配关系:清空班级相关字段,但保留学院分配信息(仅允许的床位)
|
||||
LambdaUpdateWrapper<XgDormBedStudent> 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<XgDormBed> 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<XgDormBedStudent> 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<XgDormBedStudent> 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<XgClassMsg> 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<XgDormBedStudent> assignments) {
|
||||
Set<Long> classIds = assignments.stream()
|
||||
.map(XgDormBedStudent::getClassId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
Map<Long, XgClassMsg> 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列表
|
||||
*/
|
||||
|
||||
+195
-178
@@ -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<XgStudentTreeVo> getStudentTreeByClass(Long secondaryCollegeId) {
|
||||
log.info("开始构建学生信息树(按床位分配统计),二级学院ID: {}", secondaryCollegeId);
|
||||
|
||||
List<XgStudentTreeVo> result = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 1. 查询二级学院下的所有学院信息,并统计男女生人数(使用新的按班级床位分配统计方法)
|
||||
List<Map<String, Object>> collegeStats = baseMapper.getCollegeStudentStatsByClass(secondaryCollegeId);
|
||||
log.info("查询到 {} 个学院", collegeStats.size());
|
||||
|
||||
// 按学院分组
|
||||
Map<String, List<Map<String, Object>>> 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<String, List<Map<String, Object>>> collegeEntry : collegeMap.entrySet()) {
|
||||
String collegeKey = collegeEntry.getKey();
|
||||
List<Map<String, Object>> 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<String, Object> 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<String, List<Map<String, Object>>> 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<String, List<Map<String, Object>>> majorEntry : majorMap.entrySet()) {
|
||||
String majorKey = majorEntry.getKey();
|
||||
List<Map<String, Object>> 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<String, Object> 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<String, Object> 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 学生信息树
|
||||
* 获取当前用户所在学院的学生信息树(宿舍「分配到班级」专用入口)。
|
||||
* <p>
|
||||
* 一年级代理规则(不改班级真实学院归属):
|
||||
* <ul>
|
||||
* <li>代理学院(人文学院):本院全部班级 + 全校一年级班级</li>
|
||||
* <li>其他学院:本院非一年级班级(一年级隐藏,防双头操作)</li>
|
||||
* <li>代理学院未配置:保持原逻辑,仅本院班级</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
public List<XgStudentTreeVo> 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<Map<String, Object>> collegeStats = baseMapper.getCollegeStudentStatsByClass(collegeId);
|
||||
if (proxyCollegeId.equals(collegeId)) {
|
||||
List<Map<String, Object>> freshmanStats =
|
||||
baseMapper.getFreshmanStudentStatsByClass(cohortYear);
|
||||
List<Map<String, Object>> merged =
|
||||
mergeOwnCollegeWithFreshmanStats(collegeStats, freshmanStats);
|
||||
log.info("代理学院班级树:本院行={}, 一年级行={}, 合并后={}",
|
||||
collegeStats.size(), freshmanStats.size(), merged.size());
|
||||
return buildStudentTreeFromClassStats(merged);
|
||||
}
|
||||
|
||||
List<Map<String, Object>> 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<Map<String, Object>> mergeOwnCollegeWithFreshmanStats(
|
||||
List<Map<String, Object>> ownCollegeStats,
|
||||
List<Map<String, Object>> freshmanStats) {
|
||||
Map<String, Map<String, Object>> byClassId = new LinkedHashMap<>();
|
||||
for (Map<String, Object> row : ownCollegeStats) {
|
||||
String classKey = stringKey(row.get("class_id"));
|
||||
if (classKey != null) {
|
||||
byClassId.put(classKey, row);
|
||||
}
|
||||
}
|
||||
for (Map<String, Object> 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<String, Object> 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<XgStudentTreeVo> buildStudentTreeFromClassStats(List<Map<String, Object>> collegeStats) {
|
||||
List<XgStudentTreeVo> result = new ArrayList<>();
|
||||
if (collegeStats == null || collegeStats.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<String, List<Map<String, Object>>> 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<String, List<Map<String, Object>>> collegeEntry : collegeMap.entrySet()) {
|
||||
String collegeKey = collegeEntry.getKey();
|
||||
List<Map<String, Object>> 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<String, Object> 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<String, List<Map<String, Object>>> 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<String, List<Map<String, Object>>> majorEntry : majorMap.entrySet()) {
|
||||
String majorKey = majorEntry.getKey();
|
||||
List<Map<String, Object>> 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<String, Object> 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<String, Object> 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-的就是二级学院
|
||||
|
||||
+54
-1
@@ -222,6 +222,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<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,
|
||||
@@ -264,10 +265,62 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
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, sm.college_id, sm.college_name, sm.major_id, sm.major_name, sm.class_id, sm.class_name
|
||||
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
|
||||
</select>
|
||||
|
||||
<!-- 全校一年级班级统计(宿舍分配到班级:人文学院代理可见) -->
|
||||
<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
|
||||
</select>
|
||||
|
||||
<!-- 查询二级学院下的学院床位分配统计信息(基于学生表统计需求,基于床位分配表统计已分配) -->
|
||||
<select id="getCollegeBedAssignmentStats" resultType="java.util.Map">
|
||||
SELECT
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package org.dromara.xg.helper;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 一年级代理学院纯逻辑单测(无 Spring / DB)。
|
||||
*/
|
||||
class FreshmanCollegeProxySupportTest {
|
||||
|
||||
@Test
|
||||
void currentCohortYear_beforeSeptember_usesPreviousYear() {
|
||||
assertEquals(2025, FreshmanCollegeProxySupport.currentCohortYear(LocalDate.of(2026, 8, 31)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentCohortYear_fromSeptember_usesCurrentYear() {
|
||||
assertEquals(2026, FreshmanCollegeProxySupport.currentCohortYear(LocalDate.of(2026, 9, 1)));
|
||||
assertEquals(2026, FreshmanCollegeProxySupport.currentCohortYear(LocalDate.of(2026, 10, 12)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isFreshman_matchesCohortYearOnly() {
|
||||
LocalDate today = LocalDate.of(2026, 10, 12);
|
||||
assertTrue(FreshmanCollegeProxySupport.isFreshmanByEnrollmentYear(2026L, today));
|
||||
assertFalse(FreshmanCollegeProxySupport.isFreshmanByEnrollmentYear(2025L, today));
|
||||
assertFalse(FreshmanCollegeProxySupport.isFreshmanByEnrollmentYear(null, today));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canOperate_freshman_onlyProxyCollege() {
|
||||
Long proxy = 202512231307170003L;
|
||||
Long other = 100L;
|
||||
assertTrue(FreshmanCollegeProxySupport.canOperateClassForDormAllocation(
|
||||
proxy, other, 2026L, proxy));
|
||||
assertFalse(FreshmanCollegeProxySupport.canOperateClassForDormAllocation(
|
||||
other, other, 2026L, proxy));
|
||||
assertFalse(FreshmanCollegeProxySupport.canOperateClassForDormAllocation(
|
||||
other, other, 2026L, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canOperate_nonFreshman_onlyOwnCollege() {
|
||||
Long proxy = 202512231307170003L;
|
||||
Long own = 100L;
|
||||
assertTrue(FreshmanCollegeProxySupport.canOperateClassForDormAllocation(
|
||||
own, own, 2025L, proxy));
|
||||
assertFalse(FreshmanCollegeProxySupport.canOperateClassForDormAllocation(
|
||||
proxy, own, 2025L, proxy));
|
||||
assertFalse(FreshmanCollegeProxySupport.canOperateClassForDormAllocation(
|
||||
own, null, 2025L, proxy));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# 一年级宿舍「分配到班级」代理学院说明
|
||||
|
||||
## 业务规则
|
||||
|
||||
- 一年级班级的宿舍「分配到班级」由**人文学院**操作,即使班级真实归属其他学院。
|
||||
- **不修改**班级/学生真实 `college_id`。
|
||||
- 一年级床位须先在「分配到学院」分给人文学院,人文学院才能看到楼/房/床。
|
||||
- 其他学院在「分配到班级」中**看不到也不可操作**一年级班级(防双头)。
|
||||
- 一年级判定:`enrollment_year`,每年 **9 月 1 日**切届(与请假一致)。
|
||||
- 配置键(复用请假):`xg.studentLeave.freshmanProxyCollegeId`
|
||||
|
||||
## 实现要点
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| 班级树 | `getCurrentUserStudentTree`:代理院=本院∪全校一年级;其它院=本院−一年级 |
|
||||
| 写校验 | `batchAllocateBedsToClass` / `batchRecallClassAllocation`:床位须属操作人学院;班级权限按一年级代理规则 |
|
||||
| 公共逻辑 | `FreshmanCollegeProxyHelper` / `FreshmanCollegeProxySupport` |
|
||||
|
||||
## 运营注意
|
||||
|
||||
1. 系统参数中确认人文学院 `dept_id` 已配置。
|
||||
2. 一年级床位在 `xsssfpejxy` 分给人文学院。
|
||||
3. 切届后:树自动切回本学院;若床位 `college_id` 仍挂人文,需另行移交(本期未自动迁移)。
|
||||
|
||||
## 相关代码
|
||||
|
||||
- `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,无强制改动)
|
||||
@@ -56,6 +56,13 @@
|
||||
<!-- 学生树 -->
|
||||
<el-card shadow="hover" class="tree-card mt-4">
|
||||
<div class="card-header-title">班级床位需求信息(已分配/总需求)</div>
|
||||
<el-alert
|
||||
class="mt-2"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="一年级班级由人文学院统一分配到班级;其它学院仅显示非一年级班级。一年级床位请先在「分配到学院」分给人文学院。"
|
||||
/>
|
||||
<el-input
|
||||
v-model="studentSearchKey"
|
||||
prefix-icon="Search"
|
||||
|
||||
Reference in new issue
Block a user