7 小时以前 571ccc18671ef45c6403496e8d99efec82168083
1.人员排班同步请假信息
2.人员排班同步调休信息
已添加1个文件
已修改4个文件
297 ■■■■■ 文件已修改
docs/personal_shift_holiday_linkage.md 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/dto/PerformanceShiftMapDto.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/mapper/PersonalShiftMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/PersonalShiftServiceImpl.java 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/PersonalShiftMapper.xml 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/personal_shift_holiday_linkage.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,142 @@
# äººå‘˜æŽ’班请假休息联动
## æ¶‰åŠé¡µé¢
- äººå‘˜æŽ’班管理页面(月度排班查看)
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /personalShift/page | äººå‘˜æŽ’班月度分页查询(已新增请假休息联动) |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| size | Integer | æ˜¯ | åˆ†é¡µå¤§å° |
| current | Integer | æ˜¯ | å½“前页码 |
| time | String | æ˜¯ | æ—¶é—´ï¼ˆæ ¼å¼ï¼šyyyy-MM-dd HH:mm:ss) |
| userName | String | å¦ | å‘˜å·¥å§“名(模糊查询) |
| sysDeptId | Integer | å¦ | éƒ¨é—¨ID |
**响应结构变化:**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "page": {
      "records": [
        {
          "name": "员工姓名",
          "userId": "员工ID",
          "department": "部门",
          "shiftTime": null,
          "monthlyAttendance": {
            "早": 5,
            "中": 3,
            "休息": 2,
            "totalAttendance": 8
          },
          "list": [
            {
              "id": "排班记录ID",
              "shift": "早",
              "time": "2026-06-01 00:00:00",
              "isHoliday": false
            },
            {
              "id": "排班记录ID",
              "shift": "休息",
              "time": "2026-06-05 00:00:00",
              "isHoliday": true
            }
          ],
          "holidayDates": ["2026-06-05", "2026-06-06"]
        }
      ]
    },
    "headerList": [...]
  }
}
```
### æ–°å¢žå­—段说明
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| list[].isHoliday | Boolean | è¯¥æ—¥æœŸæ˜¯å¦ä¸ºå®¡æ ¸é€šè¿‡çš„请假日期 |
| list[].shift | String | å¦‚有请假则显示"休息",否则显示原班次 |
| holidayDates | List<String> | è¯¥å‘˜å·¥å½“月所有审核通过的请假日期列表(格式:yyyy-MM-dd) |
| monthlyAttendance.休息 | Integer | å½“月请假休息天数统计 |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. è¡¨æ ¼æ¸²æŸ“逻辑修改
在渲染排班表格时,需要根据 `isHoliday` å­—段判断是否为请假休息日期,并显示特殊样式。
```html
<el-table-column v-for="day in headerList" :key="day.headerTime" :label="day.headerTime">
  <template #default="{ row }">
    <!-- æ‰¾åˆ°å¯¹åº”日期的排班数据 -->
    <div
      v-for="item in row.list.filter(i => i.time.startsWith(day.headerTime.split(' ')[0]))"
      :key="item.id"
      :class="{ 'holiday-cell': item.isHoliday }"
    >
      {{ item.shift }}
    </div>
  </template>
</el-table-column>
```
### 2. æ ·å¼ä¿®æ”¹
请假休息日期需要特殊样式标记。
```css
.holiday-cell {
  background-color: #fff3cd;
  color: #856404;
  border: 1px solid #ffc107;
  padding: 4px 8px;
  border-radius: 4px;
}
```
### 3. æ•°æ®å¤„理
```js
// åœ¨èŽ·å–æ•°æ®åŽï¼Œå¤„ç†è¯·å‡æ—¥æœŸæ˜¾ç¤º
handleShiftData(data) {
  data.page.records.forEach(row => {
    // row.holidayDates åŒ…含所有请假日期
    // row.list ä¸­ isHoliday=true çš„项表示请假休息
    // row.monthlyAttendance.休息 è¡¨ç¤ºå½“月请假休息天数
  });
}
```
### 4. å¯é€‰ï¼šè¯·å‡è¯¦æƒ…弹窗
如需查看请假详情,可通过 `holidayDates` åˆ—表展示。
```html
<el-tooltip v-if="row.holidayDates.length > 0" placement="top">
  <template #content>
    <div>请假日期:</div>
    <div v-for="date in row.holidayDates" :key="date">{{ date }}</div>
  </template>
  <el-tag type="warning" size="small">{{ row.holidayDates.length }}天请假</el-tag>
</el-tooltip>
```
## æ³¨æ„äº‹é¡¹
- è¯·å‡æ•°æ®æ¥æºäºŽ `HolidayApplication` è¡¨ï¼Œåªæœ‰çŠ¶æ€ä¸º `APPROVED`(审核通过)的请假记录才会被同步
- è¯·å‡æ—¥æœŸèŒƒå›´å†…,原班次会被替换为"休息"显示
- æœˆåº¦ç»Ÿè®¡ä¸­çš„"休息"天数会单独统计请假休息天数
- å‡ºå‹¤å¤©æ•°ç»Ÿè®¡ï¼ˆæ—©/中/晚/夜)不会统计请假休息日期
src/main/java/com/ruoyi/staff/dto/PerformanceShiftMapDto.java
@@ -23,4 +23,9 @@
    private List<Map<String, Object>> list = new ArrayList<>();
    private List<Map<Object, Object>> headerList = new ArrayList<>();
    /**
     * è¯·å‡æ—¥æœŸåˆ—表(审核通过的请假日期,格式:yyyy-MM-dd)
     */
    private List<String> holidayDates = new ArrayList<>();
}
src/main/java/com/ruoyi/staff/mapper/PersonalShiftMapper.java
@@ -31,4 +31,11 @@
    List<Map<String, Object>> performanceShiftYearList(@Param("time") String time, @Param("userName") String userName, @Param("sysDeptId") Integer sysDeptId);
    List<PerformanceShiftMapDto> performanceShiftList(@Param("time") String time, @Param("userName") String userName, @Param("sysDeptId") Integer sysDeptId);
    /**
     * æŸ¥è¯¢å‘˜å·¥çš„请假信息(从approval_instance的form_config解析)
     * @param staffIds å‘˜å·¥ID列表
     * @return Map: staff_id, form_config
     */
    List<Map<String, Object>> selectStaffHolidayDates(@Param("staffIds") List<Long> staffIds);
}
src/main/java/com/ruoyi/staff/service/impl/PersonalShiftServiceImpl.java
@@ -29,6 +29,7 @@
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
import java.util.stream.Collectors;
/**
@@ -116,13 +117,89 @@
        IPage<PerformanceShiftMapDto> mapIPage = baseMapper.performanceShiftPage(page, time, userName, sysDeptId);
        //查询所有班次(打卡规则)
        List<PersonalAttendanceLocationConfig> personalAttendanceLocationConfigs = personalAttendanceLocationConfigMapper.selectList(null);
        // èŽ·å–å½“æœˆæ—¶é—´èŒƒå›´
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        DateTimeFormatter formatters = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime localDateTime = LocalDateTime.parse(time, formatters);
        LocalDate firstDayOfMonth = localDateTime.toLocalDate().withDayOfMonth(1);
        LocalDate lastDayOfMonth = localDateTime.toLocalDate().with(TemporalAdjusters.lastDayOfMonth());
        // æ”¶é›†æ‰€æœ‰å‘˜å·¥ID
        List<Long> staffIds = mapIPage.getRecords().stream()
                .filter(dto -> dto.getUserId() != null)
                .map(dto -> Long.valueOf(dto.getUserId()))
                .distinct()
                .collect(Collectors.toList());
        // æŸ¥è¯¢å½“月审核通过的请假记录(从approval_instance的form_config解析)
        Map<Long, List<String>> staffHolidayMap = new HashMap<>();
        if (!staffIds.isEmpty()) {
            // é€šè¿‡å…³è”查询获取员工请假信息
            List<Map<String, Object>> holidayRecords = baseMapper.selectStaffHolidayDates(staffIds);
            // æž„建员工-请假日期映射(从form_config JSON解析)
            for (Map<String, Object> record : holidayRecords) {
                Long staffId = ((Number) record.get("staff_id")).longValue();
                String formConfig = (String) record.get("form_config");
                if (formConfig != null && !formConfig.isEmpty()) {
                    try {
                        // è§£æžform_config JSON
                        Map<String, Object> formMap = JackSonUtil.unmarshal(formConfig, Map.class);
                        Object formPayloadObj = formMap.get("formPayload");
                        if (formPayloadObj instanceof Map) {
                            Map<String, Object> formPayload = (Map<String, Object>) formPayloadObj;
                            Object dateRangeObj = formPayload.get("00"); // è¯·å‡æ—¥æœŸå­—段key为"00"
                            if (dateRangeObj instanceof List) {
                                List<String> dateRange = (List<String>) dateRangeObj;
                                if (dateRange.size() >= 2) {
                                    String startDateTime = dateRange.get(0);
                                    String endDateTime = dateRange.get(1);
                                    // è§£æžæ—¥æœŸï¼ˆæ ¼å¼ï¼šyyyy-MM-dd HH:mm:ss)
                                    LocalDate startDate = LocalDate.parse(startDateTime.substring(0, 10));
                                    LocalDate endDate = LocalDate.parse(endDateTime.substring(0, 10));
                                    // è¿‡æ»¤å½“月范围内的请假日期
                                    List<String> dates = getDatesBetween(startDate, endDate).stream()
                                            .filter(date -> {
                                                LocalDate d = LocalDate.parse(date);
                                                return !d.isBefore(firstDayOfMonth) && !d.isAfter(lastDayOfMonth);
                                            })
                                            .collect(Collectors.toList());
                                    if (!dates.isEmpty()) {
                                        staffHolidayMap.computeIfAbsent(staffId, k -> new ArrayList<>()).addAll(dates);
                                    }
                                }
                            }
                        }
                    } catch (Exception e) {
                        // JSON解析失败,跳过此记录
                    }
                }
            }
        }
        for (PerformanceShiftMapDto i : mapIPage.getRecords()) {
            // è®¾ç½®è¯·å‡æ—¥æœŸåˆ—表
            if (i.getUserId() != null) {
                Long staffId = Long.valueOf(i.getUserId());
                List<String> holidayDates = staffHolidayMap.getOrDefault(staffId, new ArrayList<>());
                i.setHolidayDates(holidayDates);
            }
            List<String> shiftTimes = StrUtil.split(i.getShiftTime(), ";");
            if(CollUtil.isEmpty(shiftTimes)){
                continue;
            }
            double totalAttendance = 0;//总出勤天数
            List<Map<String, Object>> map = new ArrayList<>();
            // èŽ·å–è¯¥å‘˜å·¥çš„è¯·å‡æ—¥æœŸé›†åˆ
            Set<String> holidayDateSet = new HashSet<>(i.getHolidayDates());
            // åˆ†å‰²æ—¥æœŸ
            for (String shiftTime : shiftTimes) {
                i.setShiftTime(null);
@@ -135,9 +212,31 @@
                hashMap.put("id", shiftTimeAndShift.get(2));
                hashMap.put("shift", shiftTimeAndShift.get(1));
                hashMap.put("time", shiftTimeAndShift.get(0));
                // æå–日期部分(格式:yyyy-MM-dd HH:mm:ss -> yyyy-MM-dd)
                String workTimeStr = shiftTimeAndShift.get(0);
                String workDate = workTimeStr.length() >= 10 ? workTimeStr.substring(0, 10) : workTimeStr;
                // å¦‚果该日期有审核通过的请假记录,设置isHoliday为true,并将班次设为休息
                if (holidayDateSet.contains(workDate)) {
                    hashMap.put("isHoliday", true);
                    hashMap.put("shift", "休息");
                } else {
                    hashMap.put("isHoliday", false);
                }
                map.add(hashMap);
                i.setList(map);
                //汇总的各班次统计数据
                // ç»Ÿè®¡ç­æ¬¡æ•°æ®
                if (holidayDateSet.contains(workDate)) {
                    // å¦‚果是请假日期,统计为休息(只统计一次)
                    if (!i.getMonthlyAttendance().containsKey("休息")){
                        i.getMonthlyAttendance().put("休息", 0);
                    }
                    i.getMonthlyAttendance().put("休息", new BigDecimal(i.getMonthlyAttendance().get("休息").toString()).add(new BigDecimal("1")));
                } else {
                    // ä¸æ˜¯è¯·å‡æ—¥æœŸï¼Œç»Ÿè®¡åŽŸç­æ¬¡
                for (PersonalAttendanceLocationConfig personalAttendanceLocationConfig : personalAttendanceLocationConfigs) {
                    if (!i.getMonthlyAttendance().containsKey(personalAttendanceLocationConfig.getShift())){
                        i.getMonthlyAttendance().put(personalAttendanceLocationConfig.getShift(), 0);
@@ -147,7 +246,7 @@
                        i.getMonthlyAttendance().put(personalAttendanceLocationConfig.getShift(), bigDecimal.add(new BigDecimal("1")));
                    }
                }
                //统计总出勤天数(早/中/晚/夜)都算出勤,其余都是休息
                    // ç»Ÿè®¡æ€»å‡ºå‹¤å¤©æ•°(早/中/晚/夜)都算出勤
                if (shiftTimeAndShift.get(1).contains("早") ||
                        shiftTimeAndShift.get(1).contains("中") ||
                        shiftTimeAndShift.get(1).contains("晚") ||
@@ -156,13 +255,8 @@
                }
            }
        }
        }
        // èŽ·å–header时间
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        DateTimeFormatter formatters = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        // å°†å­—符串时间转换为 LocalDateTime ç±»åž‹æ—¶é—´
        LocalDateTime localDateTime = LocalDateTime.parse(time, formatters);
        LocalDate firstDayOfMonth = localDateTime.toLocalDate().withDayOfMonth(1);
        LocalDate lastDayOfMonth = localDateTime.toLocalDate().with(TemporalAdjusters.lastDayOfMonth());
        List<LocalDateTime> localDateTimesBetween = getLocalDateTimesBetween(firstDayOfMonth.atStartOfDay(), lastDayOfMonth.atStartOfDay());
        List<Object> list1 = new ArrayList<>();
        for (LocalDateTime dateTime : localDateTimesBetween) {
@@ -178,6 +272,21 @@
        return resultMap;
    }
    /**
     * èŽ·å–ä¸¤ä¸ªæ—¥æœŸä¹‹é—´çš„æ‰€æœ‰æ—¥æœŸï¼ˆæ ¼å¼ï¼šyyyy-MM-dd)
     * æ³¨æ„ï¼šç»“束日期不包含在内(请假日期范围是开始日期到结束日期,不含结束日期)
     */
    private List<String> getDatesBetween(LocalDate startDate, LocalDate endDate) {
        List<String> dates = new ArrayList<>();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        LocalDate current = startDate;
        while (current.isBefore(endDate)) {
            dates.add(current.format(formatter));
            current = current.plusDays(1);
        }
        return dates;
    }
    @Override
    public void performanceShiftUpdate(PersonalShift personalShift) {
        baseMapper.update(new PersonalShift(), Wrappers.<PersonalShift>lambdaUpdate()
src/main/resources/mapper/staff/PersonalShiftMapper.xml
@@ -103,4 +103,22 @@
        order by s.create_time
    </select>
    <!-- æŸ¥è¯¢å‘˜å·¥çš„请假信息(从approval_instance的form_config解析) -->
    <!-- å…³è”路径: staff_on_job.staff_no = sys_user.user_name, sys_user.user_id = approval_instance.applicant_id -->
    <select id="selectStaffHolidayDates" resultType="java.util.Map">
        SELECT
            soj.id AS staff_id,
            ai.form_config
        FROM staff_on_job soj
        INNER JOIN sys_user su ON su.user_name = soj.staff_no
        INNER JOIN approval_instance ai ON ai.applicant_id = su.user_id
        WHERE soj.id IN
        <foreach collection="staffIds" item="staffId" open="(" separator="," close=")">
            #{staffId}
        </foreach>
        AND ai.business_type IN (14, 19)
        AND ai.status = 'APPROVED'
        AND ai.deleted = 0
    </select>
</mapper>