fix(dorm): 假期留宿按时刻判定,点名可同时显示请假与留宿

结束时刻过后不再整天标留宿;请假与假期留宿可同时展示,标签仅显示状态名。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
“wuzhen”andCursor committed 2026-10-26 09:36:38 +08:00
1 parent df4c21bd0d
commit 845c7fd942
8 files changed
+149 -77

No files matched your search

@@ -119,7 +119,7 @@ public interface IXgHolidayStayService {
Boolean revoke(XgHolidayStayAuditBo auditBo);
/**
* 按检查时刻查询学生当前有效的假期留宿(已通过,且日期落在留宿起止日内,结束日当天整天有效)
* 按检查时刻查询学生当前有效的假期留宿(已通过,且检查时刻落在留宿起止 datetime 内,含边界)
*
* @param studentIds 学生ID列表
* @param checkDate 检查时刻
@@ -2579,9 +2579,9 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
leaveInfoMap.put("leaveEndTime", leaveInfo.getEndTime());
studentInfo.put("leaveInfo", leaveInfoMap);
}
// 请假优先;无请假时再挂假期留宿
// 请假与假期留宿可同时生效,一并返回供点名双状态展示
XgHolidayStayVo holidayStay = holidayStayMap.get(studentIdValue);
if (leaveInfo == null && holidayStay != null) {
if (holidayStay != null) {
Map<String, Object> holidayStayInfoMap = new LinkedHashMap<>();
holidayStayInfoMap.put("holidayStayId", holidayStay.getId());
holidayStayInfoMap.put("holidayType", holidayStay.getHolidayType());
@@ -343,15 +343,21 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService {
for (XgDormRollCallDetail detail : allStudentDetails) {
String status = detail.getRollCallStatus();
if (!"1".equals(status) && !"2".equals(status) && !"4".equals(status)) {
boolean isLeave = "1".equals(status) || detail.getLeaveId() != null;
boolean isHolidayStay = "4".equals(status) || detail.getHolidayStayId() != null;
boolean isAbsent = "2".equals(status);
if (!isLeave && !isHolidayStay && !isAbsent) {
continue;
}
DormRollCallTodayDetailItemVo item = toTodayDetailItem(detail, rollCallMap.get(detail.getRollCallId()));
if ("1".equals(status)) {
// 请假与假期留宿可同时存在,分别计入两类明细
if (isLeave) {
leaveDetails.add(item);
} else if ("4".equals(status)) {
}
if (isHolidayStay) {
holidayStayDetails.add(item);
} else {
}
if (isAbsent && !isLeave && !isHolidayStay) {
absentDetails.add(item);
}
}
@@ -39,10 +39,7 @@ import java.util.HashMap;
import java.util.Calendar;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import org.springframework.beans.BeanUtils;
/**
@@ -1006,7 +1003,8 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService {
/**
* 按检查时刻查询学生当前有效的假期留宿。
* 条件:已通过;检查日落在 [applyStartDate, applyEndDate](按日期比较,结束日当天整天有效)。
* 条件:已通过;检查时刻落在 [applyStartDate, applyEndDate](按 datetime 比较,与请假判定一致)。
* 例:结束为 10-25 08:00,则 25 日 21 点点名不再显示假期留宿。
*/
@Override
public Map<Long, XgHolidayStayVo> queryStudentHolidayStaysByCheckDate(List<Long> studentIds, LocalDateTime checkDate) {
@@ -1014,18 +1012,13 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService {
return Map.of();
}
ZoneId zone = ZoneId.systemDefault();
LocalDate checkDay = checkDate.toLocalDate();
Date dayStart = Date.from(checkDay.atStartOfDay(zone).toInstant());
Date dayEnd = Date.from(LocalDateTime.of(checkDay, LocalTime.MAX).atZone(zone).toInstant());
LambdaQueryWrapper<XgHolidayStay> lqw = Wrappers.lambdaQuery();
lqw.in(XgHolidayStay::getStudentId, studentIds);
// 仅已通过
lqw.eq(XgHolidayStay::getApplyStatus, "2");
// 开始日 <= 检查日末,结束日 >= 检查日初(结束日当天整天有效)
lqw.le(XgHolidayStay::getApplyStartDate, dayEnd);
lqw.ge(XgHolidayStay::getApplyEndDate, dayStart);
// 与请假一致:按时刻落在起止区间内(含边界)
lqw.le(XgHolidayStay::getApplyStartDate, checkDate);
lqw.ge(XgHolidayStay::getApplyEndDate, checkDate);
lqw.isNotNull(XgHolidayStay::getApplyStartDate);
lqw.isNotNull(XgHolidayStay::getApplyEndDate);
lqw.orderByDesc(XgHolidayStay::getCreateTime);
@@ -7,16 +7,24 @@
## 有效留宿判定(后端统一)
接口:`getRoomStudents` 在返回学生信息时附加 `holidayStayInfo`(有有效请假时不返回,请假优先)。
接口:`getRoomStudents` 在返回学生信息时附加 `leaveInfo` / `holidayStayInfo`(**可同时返回**)。
条件:
条件(假期留宿):
1. `apply_status = '2'`(已通过)
2. 检查日落在 `[apply_start_date, apply_end_date]`(按日期比较,**结束日当天整天有效**)
2. 检查**时刻**落在 `[apply_start_date, apply_end_date]`(按 datetime 比较,与请假一致,含边界)
3. 同一学生多条记录时取最新一条
例:留宿结束为 `2026-10-25 08:00:00`,则 25 日 21 点宿舍点名**不再**显示「假期留宿」,恢复默认「在寝」。
若同一学生当前既有有效请假又有有效假期留宿:点名页**同时展示两个标签**;落库主状态仍为请假(`1`),并同时写入 `leave_*` 与 `holiday_*` 字段;今日汇总两类明细均可计入该生。
假期结束后,接口不再返回 `holidayStayInfo`,前端恢复默认「在寝」可切换。
> **变更说明(2026-10-26)**:
> 1. 原先按「结束日当天整天有效」判定 → 已改为按时刻判定。
> 2. 原先请假优先时不返回假期留宿 → 已改为两者可同时展示。
## 点名状态字典 `xg_dorm_roll_call_status`
| 值 | 含义 |
@@ -18,14 +18,23 @@
<view class="student-no">{{ bed.studentNo }}</view>
</view>
<view v-else class="empty-bed">空床位</view>
<view v-if="bed.isLeaveAuto" class="leave-tag">
<u-tag text="请假" type="warning" size="mini"></u-tag>
<view v-if="bed.isLeaveAuto || bed.isHolidayStayAuto" class="leave-tag">
<!-- 只展示状态名,不展示请假类型(如「其他假」) -->
<u-tag v-if="bed.isLeaveAuto" text="请假" type="warning" size="mini"></u-tag>
<u-tag
v-if="bed.isHolidayStayAuto"
text="假期留宿"
type="primary"
size="mini"
:custom-style="{ marginLeft: bed.isLeaveAuto ? '8rpx' : '0' }"
></u-tag>
</view>
<view v-else-if="bed.isHolidayStayAuto" class="leave-tag">
<u-tag text="假期留宿" type="primary" size="mini"></u-tag>
</view>
<view class="status-label" :class="getStatusColorClass(bed.rollCallStatus)">
{{ getStatusLabel(bed.rollCallStatus) }}
<view
v-if="!bed.isLeaveAuto && !bed.isHolidayStayAuto"
class="status-label"
:class="getStatusColorClass(bed)"
>
{{ getStatusLabel(bed) }}
</view>
</view>
</view>
@@ -112,11 +121,21 @@ export default {
let present = 0, leave = 0, holidayStay = 0, absent = 0, empty = 0;
this.bedDetails.forEach((bed) => {
const status = bed.rollCallStatus;
if (status === '0') present++;
else if (status === '1') leave++;
else if (status === '4') holidayStay++;
else if (status === '2') absent++;
else if (status === '3') empty++;
if (status === '3') {
empty++;
return;
}
if (status === '2') {
absent++;
return;
}
if (status === '0') {
present++;
return;
}
// 请假/留宿可同时计入(同一人可同时出现在两类统计)
if (bed.isLeaveAuto || status === '1') leave++;
if (bed.isHolidayStayAuto || status === '4') holidayStay++;
});
return { present, leave, holidayStay, absent, empty };
},
@@ -215,22 +234,28 @@ export default {
if (!bed.studentId) {
bed.rollCallStatus = '3';
} else if (item.leaveInfo) {
// 请假优先
bed.rollCallStatus = '1';
} else {
if (item.leaveInfo) {
bed.isLeaveAuto = true;
bed.leaveId = item.leaveInfo.leaveId;
bed.leaveTypeName = item.leaveInfo.leaveTypeName;
bed.leaveStartTime = item.leaveInfo.leaveStartTime;
bed.leaveEndTime = item.leaveInfo.leaveEndTime;
} else if (item.holidayStayInfo) {
bed.rollCallStatus = '4';
}
if (item.holidayStayInfo) {
bed.isHolidayStayAuto = true;
bed.holidayStayId = item.holidayStayInfo.holidayStayId;
bed.holidayType = item.holidayStayInfo.holidayType;
bed.holidayStartDate = item.holidayStayInfo.applyStartDate;
bed.holidayEndDate = item.holidayStayInfo.applyEndDate;
}
// 主状态:请假优先落库;仅留宿时为 4;两者都有时仍为 1,但双标签都展示
if (bed.isLeaveAuto) {
bed.rollCallStatus = '1';
} else if (bed.isHolidayStayAuto) {
bed.rollCallStatus = '4';
}
}
return bed;
});
@@ -250,6 +275,7 @@ export default {
},
getBedCardClass(bed) {
if (bed.isLeaveAuto && bed.isHolidayStayAuto) return 'bed-leave bed-holiday-stay-both';
if (bed.rollCallStatus === '0') return 'bed-present';
if (bed.rollCallStatus === '1') return 'bed-leave';
if (bed.rollCallStatus === '4') return 'bed-holiday-stay';
@@ -258,7 +284,9 @@ export default {
return '';
},
getStatusLabel(status) {
getStatusLabel(bed) {
if (bed && bed.isLeaveAuto && bed.isHolidayStayAuto) return '请假+假期留宿';
const status = typeof bed === 'string' ? bed : bed && bed.rollCallStatus;
if (status === '0') return '在寝';
if (status === '1') return '请假';
if (status === '4') return '假期留宿';
@@ -267,7 +295,9 @@ export default {
return '';
},
getStatusColorClass(status) {
getStatusColorClass(bed) {
if (bed && bed.isLeaveAuto && bed.isHolidayStayAuto) return 'color-leave';
const status = typeof bed === 'string' ? bed : bed && bed.rollCallStatus;
if (status === '0') return 'color-present';
if (status === '1') return 'color-leave';
if (status === '4') return 'color-holiday-stay';
@@ -277,12 +307,14 @@ export default {
},
handleBedClick(bed) {
if (bed.isLeaveAuto) {
uni.showToast({ title: '该学生处于请假状态,不可修改', icon: 'none' });
return;
}
if (bed.isHolidayStayAuto) {
uni.showToast({ title: '该学生处于假期留宿状态,不可修改', icon: 'none' });
if (bed.isLeaveAuto || bed.isHolidayStayAuto) {
const tip =
bed.isLeaveAuto && bed.isHolidayStayAuto
? '该学生同时处于请假与假期留宿,不可修改'
: bed.isLeaveAuto
? '该学生处于请假状态,不可修改'
: '该学生处于假期留宿状态,不可修改';
uni.showToast({ title: tip, icon: 'none' });
return;
}
if (!bed.studentId) {
@@ -22,19 +22,31 @@
<el-table-column label="床位" prop="bedName" width="70" />
<el-table-column label="姓名" prop="studentName" width="80" />
<el-table-column label="学号" prop="studentNo" width="120" />
<el-table-column label="状态" prop="rollCallStatus" width="90">
<el-table-column label="状态" prop="rollCallStatus" width="160">
<template #default="scope">
<el-tag v-if="scope.row.rollCallStatus === '0'" type="success" size="small">在寝</el-tag>
<el-tag v-else-if="scope.row.rollCallStatus === '1'" type="warning" size="small">请假</el-tag>
<el-tag v-else-if="scope.row.rollCallStatus === '4'" type="primary" size="small">假期留宿</el-tag>
<template v-if="scope.row.leaveId || scope.row.rollCallStatus === '1' || scope.row.holidayStayId || scope.row.rollCallStatus === '4'">
<el-tag v-if="scope.row.leaveId || scope.row.rollCallStatus === '1'" type="warning" size="small">请假</el-tag>
<el-tag
v-if="scope.row.holidayStayId || scope.row.rollCallStatus === '4'"
type="primary"
size="small"
style="margin-left: 4px"
>假期留宿</el-tag>
</template>
<el-tag v-else-if="scope.row.rollCallStatus === '0'" type="success" size="small">在寝</el-tag>
<el-tag v-else-if="scope.row.rollCallStatus === '2'" type="danger" size="small">不在</el-tag>
<el-tag v-else type="info" size="small">空床位</el-tag>
</template>
</el-table-column>
<el-table-column label="备注信息" min-width="120">
<el-table-column label="备注信息" min-width="140">
<template #default="scope">
<span v-if="scope.row.leaveTypeName">{{ scope.row.leaveTypeName }}</span>
<span v-else-if="scope.row.holidayType || scope.row.holidayStayId">假期留宿{{ scope.row.holidayType ? `(${scope.row.holidayType})` : '' }}</span>
<span v-if="scope.row.leaveTypeName || scope.row.holidayType || scope.row.holidayStayId">
<template v-if="scope.row.leaveTypeName">{{ scope.row.leaveTypeName }}</template>
<template v-if="scope.row.leaveTypeName && (scope.row.holidayType || scope.row.holidayStayId)"> / </template>
<template v-if="scope.row.holidayType || scope.row.holidayStayId">
假期留宿{{ scope.row.holidayType ? `(${scope.row.holidayType})` : '' }}
</template>
</span>
<span v-else>-</span>
</template>
</el-table-column>
@@ -15,14 +15,13 @@
<div class="student-no">{{ bed.studentNo }}</div>
</div>
<div v-else class="empty-bed">空床位</div>
<div v-if="bed.leaveId" class="leave-info">
<el-tag type="warning" size="small">{{ bed.leaveTypeName || '请假' }}</el-tag>
<div v-if="bed.leaveId || bed.holidayStayId" class="leave-info">
<!-- 只展示状态名,不展示请假类型(如「其他假」) -->
<el-tag v-if="bed.leaveId" type="warning" size="small">请假</el-tag>
<el-tag v-if="bed.holidayStayId" type="primary" size="small" style="margin-left: 4px">假期留宿</el-tag>
</div>
<div v-else-if="bed.holidayStayId" class="leave-info">
<el-tag type="primary" size="small">假期留宿</el-tag>
</div>
<div class="status-label">
{{ getStatusLabel(bed.rollCallStatus) }}
<div v-if="!bed.leaveId && !bed.holidayStayId" class="status-label">
{{ getStatusLabel(bed) }}
</div>
</div>
</div>
@@ -88,16 +87,26 @@ const summary = computed(() => {
let present = 0, leave = 0, holidayStay = 0, absent = 0, empty = 0;
bedDetails.value.forEach(bed => {
const status = bed.rollCallStatus;
if (status === '0') present++;
else if (status === '1') leave++;
else if (status === '4') holidayStay++;
else if (status === '2') absent++;
else if (status === '3') empty++;
if (status === '3') {
empty++;
return;
}
if (status === '2') {
absent++;
return;
}
if (status === '0') {
present++;
return;
}
if (bed.isLeaveAuto || status === '1') leave++;
if (bed.isHolidayStayAuto || status === '4') holidayStay++;
});
return { present, leave, holidayStay, absent, empty };
});
const getBedCardClass = (bed: BedItem) => {
if (bed.isLeaveAuto && bed.isHolidayStayAuto) return 'bed-leave';
const status = bed.rollCallStatus;
if (status === '0') return 'bed-present';
if (status === '1') return 'bed-leave';
@@ -107,7 +116,11 @@ const getBedCardClass = (bed: BedItem) => {
return '';
};
const getStatusLabel = (status?: string) => {
const getStatusLabel = (bed: BedItem | string) => {
if (typeof bed !== 'string' && bed.isLeaveAuto && bed.isHolidayStayAuto) {
return '请假+假期留宿';
}
const status = typeof bed === 'string' ? bed : bed.rollCallStatus;
if (status === '0') return '在寝';
if (status === '1') return '请假';
if (status === '4') return '假期留宿';
@@ -117,12 +130,14 @@ const getStatusLabel = (status?: string) => {
};
const handleBedClick = (bed: BedItem) => {
if (bed.isLeaveAuto) {
ElMessage.info('该学生处于请假状态,不可修改');
return;
}
if (bed.isHolidayStayAuto) {
ElMessage.info('该学生处于假期留宿状态,不可修改');
if (bed.isLeaveAuto || bed.isHolidayStayAuto) {
ElMessage.info(
bed.isLeaveAuto && bed.isHolidayStayAuto
? '该学生同时处于请假与假期留宿,不可修改'
: bed.isLeaveAuto
? '该学生处于请假状态,不可修改'
: '该学生处于假期留宿状态,不可修改'
);
return;
}
if (!bed.studentId) {
@@ -206,21 +221,27 @@ const loadBedDetails = async () => {
if (!bed.studentId) {
bed.rollCallStatus = '3';
} else if (item.leaveInfo) {
bed.rollCallStatus = '1';
} else {
if (item.leaveInfo) {
bed.isLeaveAuto = true;
bed.leaveId = item.leaveInfo.leaveId;
bed.leaveTypeName = item.leaveInfo.leaveTypeName;
bed.leaveStartTime = item.leaveInfo.leaveStartTime;
bed.leaveEndTime = item.leaveInfo.leaveEndTime;
} else if (item.holidayStayInfo) {
bed.rollCallStatus = '4';
}
if (item.holidayStayInfo) {
bed.isHolidayStayAuto = true;
bed.holidayStayId = item.holidayStayInfo.holidayStayId;
bed.holidayType = item.holidayStayInfo.holidayType;
bed.holidayStartDate = item.holidayStayInfo.applyStartDate;
bed.holidayEndDate = item.holidayStayInfo.applyEndDate;
}
if (bed.isLeaveAuto) {
bed.rollCallStatus = '1';
} else if (bed.isHolidayStayAuto) {
bed.rollCallStatus = '4';
}
}
bed.originalStatus = bed.rollCallStatus;
return bed;