Browse Source

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
“wuzhen” 4 ngày trước cách đây
mục cha
commit
845c7fd942

+ 1 - 1
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgHolidayStayService.java

@@ -119,7 +119,7 @@ public interface IXgHolidayStayService {
     Boolean revoke(XgHolidayStayAuditBo auditBo);
 
     /**
-     * 按检查时刻查询学生当前有效的假期留宿(已通过,且日期落在留宿起止日内,结束日当天整天有效)
+     * 按检查时刻查询学生当前有效的假期留宿(已通过,且检查时刻落在留宿起止 datetime 内,含边界)
      *
      * @param studentIds 学生ID列表
      * @param checkDate  检查时刻

+ 2 - 2
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java

@@ -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());

+ 10 - 4
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRollCallServiceImpl.java

@@ -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);
             }
         }

+ 5 - 12
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHolidayStayServiceImpl.java

@@ -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);

+ 11 - 3
docs/开发文档/API/宿舍点名假期留宿标识说明.md

@@ -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`
 
 | 值 | 含义 |

+ 67 - 35
mobile/src/pages/xgss/ssdm/components/RoomRollCallPopup.vue

@@ -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,21 +234,27 @@ export default {
 
                     if (!bed.studentId) {
                         bed.rollCallStatus = '3';
-                    } else if (item.leaveInfo) {
-                        // 请假优先
-                        bed.rollCallStatus = '1';
-                        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';
-                        bed.isHolidayStayAuto = true;
-                        bed.holidayStayId = item.holidayStayInfo.holidayStayId;
-                        bed.holidayType = item.holidayStayInfo.holidayType;
-                        bed.holidayStartDate = item.holidayStayInfo.applyStartDate;
-                        bed.holidayEndDate = item.holidayStayInfo.applyEndDate;
+                    } 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;
+                        }
+                        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) {

+ 19 - 7
web/src/views/xg/xgss/dormRollCall/dormRollCall/RollCallDetailDrawer.vue

@@ -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>

+ 54 - 33
web/src/views/xg/xgss/dormRollCall/dormRollCall/RoomRollCallPopup.vue

@@ -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,20 +221,26 @@ const loadBedDetails = async () => {
 
       if (!bed.studentId) {
         bed.rollCallStatus = '3';
-      } else if (item.leaveInfo) {
-        bed.rollCallStatus = '1';
-        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';
-        bed.isHolidayStayAuto = true;
-        bed.holidayStayId = item.holidayStayInfo.holidayStayId;
-        bed.holidayType = item.holidayStayInfo.holidayType;
-        bed.holidayStartDate = item.holidayStayInfo.applyStartDate;
-        bed.holidayEndDate = item.holidayStayInfo.applyEndDate;
+      } 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;
+        }
+        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;