Commit e2fa61b7e59152e89532c0c461a926e8c1368833

Authored by YunaiV
1 parent caade90f

feat(hrm): consolidate human resources management

Showing 663 changed files with 74608 additions and 30 deletions

Too many changes.

To preserve performance only 100 of 663 files are displayed.

... ... @@ -16,7 +16,7 @@
16 16 <module>yudao-module-system</module>
17 17 <module>yudao-module-infra</module>
18 18 <!-- <module>yudao-module-member</module>-->
19   -<!-- <module>yudao-module-bpm</module>-->
  19 + <module>yudao-module-bpm</module>
20 20 <!-- <module>yudao-module-report</module>-->
21 21 <!-- <module>yudao-module-mp</module>-->
22 22 <!-- <module>yudao-module-pay</module>-->
... ... @@ -26,7 +26,9 @@
26 26 <!-- <module>yudao-module-iot</module>-->
27 27 <!-- <module>yudao-module-mes</module>-->
28 28 <!-- <module>yudao-module-wms</module>-->
29   -<!-- <module>yudao-module-im</module>-->
  29 + <module>yudao-module-hrm</module>
  30 + <module>yudao-module-fms</module>
  31 + <!-- <module>yudao-module-im</module>-->
30 32 <!-- 请参考 https://doc.iocoder.cn/ai/build/ 文档,完成 AI 模块的启动!!! -->
31 33 <!-- <module>yudao-module-ai</module>-->
32 34 </modules>
... ... @@ -36,7 +38,7 @@
36 38 <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
37 39  
38 40 <properties>
39   - <revision>2026.06-SNAPSHOT</revision>
  41 + <revision>2026.07-SNAPSHOT</revision>
40 42 <!-- Maven 相关 -->
41 43 <java.version>17</java.version>
42 44 <maven.compiler.source>${java.version}</maven.compiler.source>
... ...
yudao-dependencies/pom.xml
... ... @@ -57,6 +57,7 @@
57 57 <hutool-5.version>5.8.46</hutool-5.version>
58 58 <hutool-6.version>6.0.0-M22</hutool-6.version>
59 59 <fastexcel.version>1.3.0</fastexcel.version>
  60 + <aviator.version>5.3.3</aviator.version>
60 61 <velocity.version>2.4.1</velocity.version>
61 62 <fastjson.version>1.2.83</fastjson.version>
62 63 <fastjson2.version>2.0.62</fastjson2.version>
... ... @@ -488,6 +489,12 @@
488 489 </dependency>
489 490  
490 491 <dependency>
  492 + <groupId>com.googlecode.aviator</groupId>
  493 + <artifactId>aviator</artifactId>
  494 + <version>${aviator.version}</version>
  495 + </dependency>
  496 +
  497 + <dependency>
491 498 <groupId>org.apache.tika</groupId>
492 499 <artifactId>tika-core</artifactId> <!-- 文件类型的识别 -->
493 500 <version>${tika-core.version}</version>
... ...
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
... ... @@ -4,8 +4,10 @@ import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.collection.CollectionUtil;
5 5 import cn.hutool.core.util.ArrayUtil;
6 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
7 8 import com.google.common.collect.ImmutableMap;
8 9  
  10 +import java.math.BigDecimal;
9 11 import java.util.*;
10 12 import java.util.function.*;
11 13 import java.util.stream.Collectors;
... ... @@ -33,6 +35,34 @@ public class CollectionUtils {
33 35 return from.stream().anyMatch(predicate);
34 36 }
35 37  
  38 + public static <T> long sum(Collection<T> from, ToLongFunction<T> valueFunc) {
  39 + if (CollUtil.isEmpty(from)) {
  40 + return 0L;
  41 + }
  42 + return from.stream().mapToLong(valueFunc).sum();
  43 + }
  44 +
  45 + public static <T> BigDecimal sumBigDecimal(Collection<T> from, Function<T, BigDecimal> valueFunc) {
  46 + if (CollUtil.isEmpty(from)) {
  47 + return BigDecimal.ZERO;
  48 + }
  49 + return from.stream().map(valueFunc).map(NumberUtils::zeroIfNull).reduce(BigDecimal.ZERO, BigDecimal::add);
  50 + }
  51 +
  52 + public static <T> long count(Collection<T> from, Predicate<T> predicate) {
  53 + if (CollUtil.isEmpty(from)) {
  54 + return 0L;
  55 + }
  56 + return from.stream().filter(predicate).count();
  57 + }
  58 +
  59 + public static <T, R> long distinctCount(Collection<T> from, Function<T, R> keyMapper) {
  60 + if (CollUtil.isEmpty(from)) {
  61 + return 0L;
  62 + }
  63 + return from.stream().map(keyMapper).filter(Objects::nonNull).distinct().count();
  64 + }
  65 +
36 66 public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) {
37 67 if (CollUtil.isEmpty(from)) {
38 68 return new ArrayList<>();
... ... @@ -124,6 +154,15 @@ public class CollectionUtils {
124 154 return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
125 155 }
126 156  
  157 + public static <T, U, S extends Set<U>> S convertSetBySupplier(
  158 + Collection<T> from, Function<T, U> func, Supplier<S> supplier) {
  159 + if (CollUtil.isEmpty(from)) {
  160 + return supplier.get();
  161 + }
  162 + return from.stream().map(func).filter(Objects::nonNull)
  163 + .collect(Collectors.toCollection(supplier));
  164 + }
  165 +
127 166 public static <T, U> Set<U> convertLinkedSet(Collection<T> from, Function<T, U> func) {
128 167 if (CollUtil.isEmpty(from)) {
129 168 return new LinkedHashSet<>();
... ... @@ -398,4 +437,4 @@ public class CollectionUtils {
398 437 return list;
399 438 }
400 439  
401   -}
402 440 \ No newline at end of file
  441 +}
... ...
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java
... ... @@ -6,6 +6,9 @@ import cn.hutool.core.date.LocalDateTimeUtil;
6 6 import cn.hutool.core.lang.Assert;
7 7 import cn.hutool.core.util.StrUtil;
8 8 import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum;
  9 +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
  10 +import lombok.AllArgsConstructor;
  11 +import lombok.Getter;
9 12  
10 13 import java.sql.Timestamp;
11 14 import java.time.*;
... ... @@ -14,13 +17,16 @@ import java.time.format.DateTimeParseException;
14 17 import java.time.temporal.ChronoUnit;
15 18 import java.time.temporal.TemporalAdjusters;
16 19 import java.util.ArrayList;
  20 +import java.util.Collections;
  21 +import java.util.Comparator;
17 22 import java.util.List;
18 23 import java.util.TimeZone;
  24 +import java.util.stream.Collectors;
19 25  
20 26 import static cn.hutool.core.date.DatePattern.*;
21 27  
22 28 /**
23   - * 时间工具类,用于 {@link LocalDateTime}
  29 + * 时间工具类,用于 {@link LocalDate}、{@link LocalDateTime}
24 30 *
25 31 * @author 芋道源码
26 32 */
... ... @@ -54,6 +60,16 @@ public class LocalDateTimeUtils {
54 60 }
55 61 }
56 62  
  63 + /**
  64 + * 解析年月字符串为 {@link YearMonth},格式为 yyyy-MM
  65 + *
  66 + * @param month 年月字符串
  67 + * @return 年月
  68 + */
  69 + public static YearMonth parseYearMonth(String month) {
  70 + return YearMonth.parse(month, NORM_MONTH_FORMATTER);
  71 + }
  72 +
57 73 public static LocalDateTime addTime(Duration duration) {
58 74 return LocalDateTime.now().plus(duration);
59 75 }
... ... @@ -70,6 +86,319 @@ public class LocalDateTimeUtils {
70 86 return date.isAfter(LocalDateTime.now());
71 87 }
72 88  
  89 + public static boolean beforeOrEqualNow(LocalDateTime time) {
  90 + LocalDateTime now = LocalDateTime.now();
  91 + return time.isBefore(now) || time.isEqual(now);
  92 + }
  93 +
  94 + public static boolean beforeNow(LocalDate date) {
  95 + return date.isBefore(LocalDate.now());
  96 + }
  97 +
  98 + public static boolean beforeOrEqualNow(LocalDate date) {
  99 + LocalDate today = LocalDate.now();
  100 + return date.isBefore(today) || date.isEqual(today);
  101 + }
  102 +
  103 + public static boolean afterNow(LocalDate date) {
  104 + return date.isAfter(LocalDate.now());
  105 + }
  106 +
  107 + /**
  108 + * 判断时间是否处于一月
  109 + *
  110 + * @param time 时间
  111 + * @return 是否处于一月
  112 + */
  113 + public static boolean isJanuary(LocalDateTime time) {
  114 + return time.getMonthValue() == 1;
  115 + }
  116 +
  117 + /**
  118 + * 判断第一个日期是否早于或等于第二个日期
  119 + *
  120 + * @param firstDate 第一个日期
  121 + * @param secondDate 第二个日期
  122 + * @return 是否早于或等于
  123 + */
  124 + public static boolean isBeforeOrEqual(LocalDate firstDate, LocalDate secondDate) {
  125 + return firstDate.isBefore(secondDate) || firstDate.isEqual(secondDate);
  126 + }
  127 +
  128 + /**
  129 + * 判断第一个日期是否晚于或等于第二个日期
  130 + *
  131 + * @param firstDate 第一个日期
  132 + * @param secondDate 第二个日期
  133 + * @return 是否晚于或等于
  134 + */
  135 + public static boolean isAfterOrEqual(LocalDate firstDate, LocalDate secondDate) {
  136 + return firstDate.isAfter(secondDate) || firstDate.isEqual(secondDate);
  137 + }
  138 +
  139 + /**
  140 + * 判断第一个时间是否早于或等于第二个时间
  141 + *
  142 + * @param firstTime 第一个时间
  143 + * @param secondTime 第二个时间
  144 + * @return 是否早于或等于
  145 + */
  146 + public static boolean isBeforeOrEqual(LocalDateTime firstTime, LocalDateTime secondTime) {
  147 + return firstTime.isBefore(secondTime) || firstTime.isEqual(secondTime);
  148 + }
  149 +
  150 + /**
  151 + * 判断第一个时间是否晚于或等于第二个时间
  152 + *
  153 + * @param firstTime 第一个时间
  154 + * @param secondTime 第二个时间
  155 + * @return 是否晚于或等于
  156 + */
  157 + public static boolean isAfterOrEqual(LocalDateTime firstTime, LocalDateTime secondTime) {
  158 + return firstTime.isAfter(secondTime) || firstTime.isEqual(secondTime);
  159 + }
  160 +
  161 + /**
  162 + * 判断时间范围是否有效
  163 + *
  164 + * @param beginTime 开始时间
  165 + * @param endTime 结束时间
  166 + * @return 开始、结束时间非空,且结束时间晚于开始时间时返回 {@code true}
  167 + */
  168 + public static boolean isTimeRangeValid(LocalDateTime beginTime, LocalDateTime endTime) {
  169 + return beginTime != null && endTime != null && endTime.isAfter(beginTime);
  170 + }
  171 +
  172 + /**
  173 + * 判断时间范围是否存在有效边界
  174 + *
  175 + * @param times 时间范围
  176 + * @return 任一边界非空时返回 {@code true}
  177 + */
  178 + public static boolean isTimeRangePresent(LocalDateTime[] times) {
  179 + return ArrayUtils.get(times, 0) != null || ArrayUtils.get(times, 1) != null;
  180 + }
  181 +
  182 + /**
  183 + * 判断指定日期是否位于闭区间内
  184 + *
  185 + * @param beginDate 开始日期
  186 + * @param endDate 结束日期
  187 + * @param date 指定日期
  188 + * @return 是否位于闭区间内
  189 + */
  190 + public static boolean isBetween(LocalDate beginDate, LocalDate endDate, LocalDate date) {
  191 + return beginDate != null && endDate != null && date != null
  192 + && isAfterOrEqual(date, beginDate) && isBeforeOrEqual(date, endDate);
  193 + }
  194 +
  195 + /**
  196 + * 判断指定日期是否不在闭区间内
  197 + *
  198 + * @param beginDate 开始日期
  199 + * @param endDate 结束日期
  200 + * @param date 指定日期
  201 + * @return 是否不在闭区间内;任一参数为空时返回 {@code true}
  202 + */
  203 + public static boolean isNotBetween(LocalDate beginDate, LocalDate endDate, LocalDate date) {
  204 + return !isBetween(beginDate, endDate, date);
  205 + }
  206 +
  207 + /**
  208 + * 判断指定时间是否位于闭区间内
  209 + *
  210 + * @param beginTime 开始时间
  211 + * @param endTime 结束时间
  212 + * @param time 指定时间
  213 + * @return 是否位于闭区间内
  214 + */
  215 + public static boolean isBetween(LocalDateTime beginTime, LocalDateTime endTime, LocalDateTime time) {
  216 + return beginTime != null && endTime != null && time != null
  217 + && isAfterOrEqual(time, beginTime) && isBeforeOrEqual(time, endTime);
  218 + }
  219 +
  220 + /**
  221 + * 判断指定时间是否不在闭区间内
  222 + *
  223 + * @param beginTime 开始时间
  224 + * @param endTime 结束时间
  225 + * @param time 指定时间
  226 + * @return 是否不在闭区间内;任一参数为空时返回 {@code true}
  227 + */
  228 + public static boolean isNotBetween(LocalDateTime beginTime, LocalDateTime endTime, LocalDateTime time) {
  229 + return !isBetween(beginTime, endTime, time);
  230 + }
  231 +
  232 + /**
  233 + * 计算两个日期之间经过的完整年数
  234 + *
  235 + * @param beginDate 开始日期
  236 + * @param endDate 结束日期
  237 + * @return 完整年数;日期为空或开始日期晚于结束日期时,返回 0
  238 + */
  239 + public static int getYearsBetween(LocalDate beginDate, LocalDate endDate) {
  240 + if (beginDate == null || endDate == null || beginDate.isAfter(endDate)) {
  241 + return 0;
  242 + }
  243 + return Period.between(beginDate, endDate).getYears();
  244 + }
  245 +
  246 + /**
  247 + * 从时间范围列表中扣除指定时间范围
  248 + *
  249 + * @param sourceRanges 原时间范围列表
  250 + * @param excludedRange 需要扣除的时间范围
  251 + * @return 扣除后的时间范围列表
  252 + */
  253 + public static List<TimeRange> subtractTimeRanges(List<TimeRange> sourceRanges, TimeRange excludedRange) {
  254 + List<TimeRange> result = new ArrayList<>();
  255 + for (TimeRange sourceRange : sourceRanges) {
  256 + TimeRange overlap = intersectTimeRange(sourceRange, excludedRange);
  257 + if (overlap == null) {
  258 + result.add(sourceRange);
  259 + continue;
  260 + }
  261 + if (sourceRange.getStartTime().isBefore(overlap.getStartTime())) {
  262 + result.add(new TimeRange(sourceRange.getStartTime(), overlap.getStartTime()));
  263 + }
  264 + if (overlap.getEndTime().isBefore(sourceRange.getEndTime())) {
  265 + result.add(new TimeRange(overlap.getEndTime(), sourceRange.getEndTime()));
  266 + }
  267 + }
  268 + return result;
  269 + }
  270 +
  271 + /**
  272 + * 合并重叠或首尾相接的时间范围
  273 + *
  274 + * @param timeRanges 时间范围列表
  275 + * @return 合并后的时间范围列表
  276 + */
  277 + public static List<TimeRange> mergeTimeRanges(List<TimeRange> timeRanges) {
  278 + if (CollUtil.isEmpty(timeRanges)) {
  279 + return Collections.emptyList();
  280 + }
  281 + List<TimeRange> sortedRanges = timeRanges.stream()
  282 + .sorted(Comparator.comparing(TimeRange::getStartTime))
  283 + .collect(Collectors.toList());
  284 + List<TimeRange> result = new ArrayList<>();
  285 + TimeRange currentRange = CollUtil.getFirst(sortedRanges);
  286 + for (int i = 1; i < sortedRanges.size(); i++) {
  287 + TimeRange nextRange = sortedRanges.get(i);
  288 + if (nextRange.getStartTime().isAfter(currentRange.getEndTime())) {
  289 + result.add(currentRange);
  290 + currentRange = nextRange;
  291 + continue;
  292 + }
  293 + LocalDateTime endTime = currentRange.getEndTime().isAfter(nextRange.getEndTime())
  294 + ? currentRange.getEndTime() : nextRange.getEndTime();
  295 + currentRange = new TimeRange(currentRange.getStartTime(), endTime);
  296 + }
  297 + result.add(currentRange);
  298 + return result;
  299 + }
  300 +
  301 + /**
  302 + * 计算两个时间范围列表的交集
  303 + *
  304 + * @param firstRanges 第一组时间范围
  305 + * @param secondRanges 第二组时间范围
  306 + * @return 合并后的交集时间范围列表
  307 + */
  308 + public static List<TimeRange> intersectTimeRanges(
  309 + List<TimeRange> firstRanges, List<TimeRange> secondRanges) {
  310 + List<TimeRange> result = new ArrayList<>();
  311 + for (TimeRange firstRange : firstRanges) {
  312 + for (TimeRange secondRange : secondRanges) {
  313 + TimeRange overlap = intersectTimeRange(firstRange, secondRange);
  314 + if (overlap != null) {
  315 + result.add(overlap);
  316 + }
  317 + }
  318 + }
  319 + return mergeTimeRanges(result);
  320 + }
  321 +
  322 + /**
  323 + * 计算两个时间范围的交集
  324 + *
  325 + * @param firstRange 第一个时间范围
  326 + * @param secondRange 第二个时间范围
  327 + * @return 交集时间范围;没有交集时返回 {@code null}
  328 + */
  329 + public static TimeRange intersectTimeRange(TimeRange firstRange, TimeRange secondRange) {
  330 + LocalDateTime startTime = firstRange.getStartTime().isAfter(secondRange.getStartTime())
  331 + ? firstRange.getStartTime() : secondRange.getStartTime();
  332 + LocalDateTime endTime = firstRange.getEndTime().isBefore(secondRange.getEndTime())
  333 + ? firstRange.getEndTime() : secondRange.getEndTime();
  334 + return endTime.isAfter(startTime) ? new TimeRange(startTime, endTime) : null;
  335 + }
  336 +
  337 + /**
  338 + * 计算时间范围的总分钟数
  339 + *
  340 + * @param timeRanges 时间范围列表
  341 + * @return 总分钟数
  342 + */
  343 + public static int calculateDurationMinutes(List<TimeRange> timeRanges) {
  344 + return timeRanges.stream()
  345 + .mapToInt(range -> Math.toIntExact(Duration.between(
  346 + range.getStartTime(), range.getEndTime()).toMinutes()))
  347 + .sum();
  348 + }
  349 +
  350 + /**
  351 + * 构建日期范围内每天对应的时间范围
  352 + *
  353 + * 当结束时刻早于或等于开始时刻时,按跨天时间范围处理。
  354 + *
  355 + * @param beginDate 开始日期
  356 + * @param endDate 结束日期
  357 + * @param beginTime 每日开始时刻
  358 + * @param endTime 每日结束时刻
  359 + * @return 每日时间范围列表
  360 + */
  361 + public static List<TimeRange> buildDailyTimeRanges(
  362 + LocalDate beginDate, LocalDate endDate, LocalTime beginTime, LocalTime endTime) {
  363 + if (beginDate == null || endDate == null || beginTime == null || endTime == null
  364 + || beginDate.isAfter(endDate)) {
  365 + return Collections.emptyList();
  366 + }
  367 + List<TimeRange> result = new ArrayList<>();
  368 + for (LocalDate date = beginDate; isBeforeOrEqual(date, endDate); date = date.plusDays(1)) {
  369 + LocalDateTime rangeBeginTime = date.atTime(beginTime);
  370 + LocalDateTime rangeEndTime = date.atTime(endTime);
  371 + if (rangeEndTime.isBefore(rangeBeginTime) || rangeEndTime.isEqual(rangeBeginTime)) {
  372 + rangeEndTime = rangeEndTime.plusDays(1);
  373 + }
  374 + result.add(new TimeRange(rangeBeginTime, rangeEndTime));
  375 + }
  376 + return result;
  377 + }
  378 +
  379 + /**
  380 + * 查找包含指定时刻的每日时间范围
  381 + *
  382 + * 每日时间范围允许跨天,因此会同时检查基准日期的前一天、当天和后一天。
  383 + * 范围匹配使用左右闭区间。
  384 + *
  385 + * @param date 基准日期
  386 + * @param beginTime 每日开始时刻
  387 + * @param endTime 每日结束时刻
  388 + * @param time 指定时刻
  389 + * @return 包含指定时刻的时间范围;不存在时返回 {@code null}
  390 + */
  391 + public static TimeRange findDailyTimeRange(
  392 + LocalDate date, LocalTime beginTime, LocalTime endTime, LocalDateTime time) {
  393 + if (date == null || time == null) {
  394 + return null;
  395 + }
  396 + List<TimeRange> timeRanges = buildDailyTimeRanges(
  397 + date.minusDays(1), date.plusDays(1), beginTime, endTime);
  398 + return CollUtil.findOne(timeRanges, timeRange -> isBetween(
  399 + timeRange.getStartTime(), timeRange.getEndTime(), time));
  400 + }
  401 +
73 402 /**
74 403 * 将 Unix 秒时间戳转换为默认时区的本地时间
75 404 *
... ... @@ -185,6 +514,62 @@ public class LocalDateTimeUtils {
185 514 }
186 515  
187 516 /**
  517 + * 获得指定日期的开始时间
  518 + *
  519 + * @param date 日期
  520 + * @return 当日开始时间;日期为空时返回 {@code null}
  521 + */
  522 + public static LocalDateTime getDayBeginTime(LocalDate date) {
  523 + return date == null ? null : date.atStartOfDay();
  524 + }
  525 +
  526 + /**
  527 + * 获得指定时间所在日期的开始时间
  528 + *
  529 + * @param time 时间
  530 + * @return 当日开始时间;时间为空时返回 {@code null}
  531 + */
  532 + public static LocalDateTime getDayBeginTime(LocalDateTime time) {
  533 + return time == null ? null : getDayBeginTime(time.toLocalDate());
  534 + }
  535 +
  536 + /**
  537 + * 获得指定日期的结束时间
  538 + *
  539 + * @param date 日期
  540 + * @return 当日结束时间;日期为空时返回 {@code null}
  541 + */
  542 + public static LocalDateTime getDayEndTime(LocalDate date) {
  543 + return date == null ? null : date.atTime(LocalTime.MAX);
  544 + }
  545 +
  546 + /**
  547 + * 获得自然日期范围对应的日期时间闭区间
  548 + *
  549 + * @param beginDate 开始日期
  550 + * @param endDate 结束日期
  551 + * @return 从开始日期零点到结束日期最后时刻的闭区间;两个日期都为空时返回 {@code null}
  552 + */
  553 + public static LocalDateTime[] getDateTimeRange(LocalDate beginDate, LocalDate endDate) {
  554 + if (beginDate == null && endDate == null) {
  555 + return null;
  556 + }
  557 + return new LocalDateTime[]{getDayBeginTime(beginDate), getDayEndTime(endDate)};
  558 + }
  559 +
  560 + /**
  561 + * 获得指定月份的日期时间闭区间
  562 + *
  563 + * @param year 年份
  564 + * @param month 月份
  565 + * @return 从当月月初零点到月末最后时刻的闭区间
  566 + */
  567 + public static LocalDateTime[] getMonthDateTimeRange(int year, int month) {
  568 + LocalDate monthBeginDate = LocalDate.of(year, month, 1);
  569 + return getDateTimeRange(monthBeginDate, monthBeginDate.with(TemporalAdjusters.lastDayOfMonth()));
  570 + }
  571 +
  572 + /**
188 573 * 获取指定日期所在的月份的开始时间
189 574 * 例如:2023-09-30 00:00:00,000
190 575 *
... ... @@ -196,6 +581,16 @@ public class LocalDateTimeUtils {
196 581 }
197 582  
198 583 /**
  584 + * 获取指定日期的开始时间
  585 + *
  586 + * @param date 日期
  587 + * @return 当日开始时间
  588 + */
  589 + public static LocalDateTime beginOfDay(LocalDateTime date) {
  590 + return date.with(LocalTime.MIN);
  591 + }
  592 +
  593 + /**
199 594 * 获取指定日期所在的月份的最后时间
200 595 * 例如:2023-09-30 23:59:59,999
201 596 *
... ... @@ -207,6 +602,82 @@ public class LocalDateTimeUtils {
207 602 }
208 603  
209 604 /**
  605 + * 获得指定年月的月初时间
  606 + *
  607 + * @param year 年份
  608 + * @param month 月份
  609 + * @return 月初时间
  610 + */
  611 + public static LocalDateTime getMonthBeginTime(int year, int month) {
  612 + return LocalDateTime.of(year, month, 1, 0, 0);
  613 + }
  614 +
  615 + /**
  616 + * 获得指定年月的月初时间
  617 + *
  618 + * @param month 年月
  619 + * @return 月初时间
  620 + */
  621 + public static LocalDateTime getMonthBeginTime(YearMonth month) {
  622 + return getMonthBeginTime(month.getYear(), month.getMonthValue());
  623 + }
  624 +
  625 + /**
  626 + * 获得指定年月的下月月初时间
  627 + *
  628 + * @param year 年份
  629 + * @param month 月份
  630 + * @return 下月月初时间
  631 + */
  632 + public static LocalDateTime getNextMonthBeginTime(int year, int month) {
  633 + return getMonthBeginTime(year, month).plusMonths(1);
  634 + }
  635 +
  636 + /**
  637 + * 获得指定年月的下月月初时间
  638 + *
  639 + * @param month 年月
  640 + * @return 下月月初时间
  641 + */
  642 + public static LocalDateTime getNextMonthBeginTime(YearMonth month) {
  643 + return getMonthBeginTime(month).plusMonths(1);
  644 + }
  645 +
  646 + /**
  647 + * 判断两个时间段是否存在正时长的重叠
  648 + *
  649 + * @param beginTime1 第一个范围的开始时间
  650 + * @param endTime1 第一个范围的结束时间
  651 + * @param beginTime2 第二个范围的开始时间
  652 + * @param endTime2 第二个范围的结束时间
  653 + * @return 是否存在正时长的重叠
  654 + */
  655 + public static boolean isOverlap(LocalDateTime beginTime1, LocalDateTime endTime1,
  656 + LocalDateTime beginTime2, LocalDateTime endTime2) {
  657 + if (beginTime1 == null || endTime1 == null || beginTime2 == null || endTime2 == null) {
  658 + return false;
  659 + }
  660 + return beginTime1.isBefore(endTime2) && endTime1.isAfter(beginTime2);
  661 + }
  662 +
  663 + /**
  664 + * 判断两个左右闭合的时间范围是否重叠
  665 + *
  666 + * @param beginTime1 第一个范围的开始时间
  667 + * @param endTime1 第一个范围的结束时间
  668 + * @param beginTime2 第二个范围的开始时间
  669 + * @param endTime2 第二个范围的结束时间
  670 + * @return 是否重叠
  671 + */
  672 + public static boolean isClosedRangeOverlap(LocalDateTime beginTime1, LocalDateTime endTime1,
  673 + LocalDateTime beginTime2, LocalDateTime endTime2) {
  674 + if (beginTime1 == null || endTime1 == null || beginTime2 == null || endTime2 == null) {
  675 + return false;
  676 + }
  677 + return isBeforeOrEqual(beginTime1, endTime2) && isAfterOrEqual(endTime1, beginTime2);
  678 + }
  679 +
  680 + /**
210 681 * 获得指定日期所在季度
211 682 *
212 683 * @param date 日期
... ... @@ -353,7 +824,11 @@ public class LocalDateTimeUtils {
353 824 * @return 日期列表,包含开始日期
354 825 */
355 826 public static List<LocalDate> getDateList(LocalDate startDate, int days) {
356   - return startDate.datesUntil(startDate.plusDays(days)).toList();
  827 + List<LocalDate> dateList = new ArrayList<>(days);
  828 + for (int i = 0; i < days; i++) {
  829 + dateList.add(startDate.plusDays(i));
  830 + }
  831 + return dateList;
357 832 }
358 833  
359 834 /**
... ... @@ -433,4 +908,23 @@ public class LocalDateTimeUtils {
433 908 return sourceDateTime.atZone(zoneId).toEpochSecond();
434 909 }
435 910  
  911 + /**
  912 + * 时间范围
  913 + */
  914 + @Getter
  915 + @AllArgsConstructor
  916 + @SuppressWarnings("ClassCanBeRecord")
  917 + public static class TimeRange {
  918 +
  919 + /**
  920 + * 开始时间
  921 + */
  922 + private final LocalDateTime startTime;
  923 + /**
  924 + * 结束时间
  925 + */
  926 + private final LocalDateTime endTime;
  927 +
  928 + }
  929 +
436 930 }
... ...
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java
... ... @@ -97,6 +97,39 @@ public class MoneyUtils {
97 97 }
98 98  
99 99 /**
  100 + * 格式化金额,默认进行四舍五入
  101 + *
  102 + * 位数:{@link #PRICE_SCALE}。金额为空时,按 0 处理。
  103 + *
  104 + * @param price 金额
  105 + * @return 格式化后的金额
  106 + */
  107 + public static BigDecimal priceScale(BigDecimal price) {
  108 + return (price == null ? BigDecimal.ZERO : price).setScale(PRICE_SCALE, RoundingMode.HALF_UP);
  109 + }
  110 +
  111 + /**
  112 + * 金额相加,默认进行四舍五入
  113 + *
  114 + * 位数:{@link #PRICE_SCALE}。忽略空金额,没有有效金额时返回 0。
  115 + *
  116 + * @param prices 金额数组
  117 + * @return 金额相加结果
  118 + */
  119 + public static BigDecimal priceAdd(BigDecimal... prices) {
  120 + BigDecimal result = BigDecimal.ZERO;
  121 + if (prices == null) {
  122 + return priceScale(result);
  123 + }
  124 + for (BigDecimal price : prices) {
  125 + if (price != null) {
  126 + result = result.add(price);
  127 + }
  128 + }
  129 + return priceScale(result);
  130 + }
  131 +
  132 + /**
100 133 * 金额相乘,默认进行四舍五入
101 134 *
102 135 * 位数:{@link #PRICE_SCALE}
... ... @@ -109,7 +142,7 @@ public class MoneyUtils {
109 142 if (price == null || count == null) {
110 143 return null;
111 144 }
112   - return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP);
  145 + return priceScale(price.multiply(count));
113 146 }
114 147  
115 148 /**
... ...
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java
... ... @@ -2,10 +2,12 @@ package cn.iocoder.yudao.framework.common.util.number;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.NumberUtil;
  5 +import cn.hutool.core.util.ReUtil;
5 6 import cn.hutool.core.util.StrUtil;
6 7  
7 8 import java.math.BigDecimal;
8 9 import java.util.List;
  10 +import java.util.regex.Pattern;
9 11  
10 12 /**
11 13 * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能
... ... @@ -14,6 +16,13 @@ import java.util.List;
14 16 */
15 17 public class NumberUtils {
16 18  
  19 + private static final Pattern FIRST_BIG_DECIMAL_PATTERN = Pattern.compile("-?\\d+(?:\\.\\d+)?");
  20 +
  21 + /**
  22 + * 一百,用于百分数和比例计算
  23 + */
  24 + public static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
  25 +
17 26 public static Long parseLong(String str) {
18 27 return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null;
19 28 }
... ... @@ -22,6 +31,30 @@ public class NumberUtils {
22 31 return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null;
23 32 }
24 33  
  34 + /**
  35 + * 获得数字,为 null 时返回 {@link BigDecimal#ZERO}
  36 + *
  37 + * @param value 数字
  38 + * @return 数字
  39 + */
  40 + public static BigDecimal zeroIfNull(BigDecimal value) {
  41 + return value == null ? BigDecimal.ZERO : value;
  42 + }
  43 +
  44 + /**
  45 + * 从文本中解析首个数字
  46 + *
  47 + * @param text 文本
  48 + * @return 数字;不存在时返回 {@link BigDecimal#ZERO}
  49 + */
  50 + public static BigDecimal parseFirstBigDecimal(String text) {
  51 + if (StrUtil.isEmpty(text)) {
  52 + return BigDecimal.ZERO;
  53 + }
  54 + String number = ReUtil.getGroup0(FIRST_BIG_DECIMAL_PATTERN, text.replace(",", ""));
  55 + return NumberUtil.toBigDecimal(number);
  56 + }
  57 +
25 58 public static boolean isAllNumber(List<String> values) {
26 59 if (CollUtil.isEmpty(values)) {
27 60 return false;
... ...
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java
... ... @@ -5,10 +5,13 @@ import cn.hutool.core.lang.func.Func1;
5 5 import cn.hutool.core.lang.func.LambdaUtil;
6 6 import cn.hutool.core.util.ArrayUtil;
7 7 import cn.iocoder.yudao.framework.common.pojo.PageParam;
  8 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
8 9 import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
9 10 import cn.iocoder.yudao.framework.common.pojo.SortingField;
10 11 import org.springframework.util.Assert;
11 12  
  13 +import java.util.List;
  14 +
12 15 import static java.util.Collections.singletonList;
13 16  
14 17 /**
... ... @@ -25,6 +28,27 @@ public class PageUtils {
25 28 }
26 29  
27 30 /**
  31 + * 对内存列表进行分页
  32 + *
  33 + * @param list 数据列表
  34 + * @param pageParam 分页参数
  35 + * @param <T> 数据类型
  36 + * @return 分页结果
  37 + */
  38 + public static <T> PageResult<T> buildPageResult(List<T> list, PageParam pageParam) {
  39 + if (CollUtil.isEmpty(list)) {
  40 + return PageResult.empty();
  41 + }
  42 + long total = list.size();
  43 + if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
  44 + return new PageResult<>(list, total);
  45 + }
  46 + int fromIndex = Math.toIntExact(Math.min(getStart(pageParam), total));
  47 + int toIndex = Math.toIntExact(Math.min((long) fromIndex + pageParam.getPageSize(), total));
  48 + return new PageResult<>(list.subList(fromIndex, toIndex), total);
  49 + }
  50 +
  51 + /**
28 52 * 构建排序字段(默认倒序)
29 53 *
30 54 * @param func 排序字段的 Lambda 表达式
... ...
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.common.util.servlet;
2 2  
3 3 import cn.hutool.core.util.StrUtil;
4 4 import cn.hutool.extra.servlet.JakartaServletUtil;
  5 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
5 6 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6 7 import jakarta.servlet.ServletRequest;
7 8 import jakarta.servlet.http.HttpServletRequest;
... ... @@ -11,6 +12,7 @@ import org.springframework.web.context.request.RequestAttributes;
11 12 import org.springframework.web.context.request.RequestContextHolder;
12 13 import org.springframework.web.context.request.ServletRequestAttributes;
13 14  
  15 +import java.io.IOException;
14 16 import java.util.Map;
15 17  
16 18 /**
... ... @@ -102,4 +104,11 @@ public class ServletUtils {
102 104 return JakartaServletUtil.getHeaderMap(request);
103 105 }
104 106  
  107 + public static void writeAttachment(HttpServletResponse response, String filename, byte[] content)
  108 + throws IOException {
  109 + response.getOutputStream().write(content);
  110 + response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
  111 + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
  112 + }
  113 +
105 114 }
... ...
yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtilsTest.java
... ... @@ -6,7 +6,9 @@ import org.junit.jupiter.api.Test;
6 6  
7 7 import java.util.Arrays;
8 8 import java.util.Collection;
  9 +import java.util.Collections;
9 10 import java.util.List;
  11 +import java.util.TreeSet;
10 12 import java.util.function.BiFunction;
11 13  
12 14 import static org.junit.jupiter.api.Assertions.assertEquals;
... ... @@ -27,6 +29,38 @@ public class CollectionUtilsTest {
27 29 }
28 30  
29 31 @Test
  32 + public void testSum() {
  33 + assertEquals(6L, CollectionUtils.sum(Arrays.asList(1L, 2L, 3L), Long::longValue));
  34 + assertEquals(0L, CollectionUtils.sum(Collections.<Long>emptyList(), Long::longValue));
  35 + }
  36 +
  37 + @Test
  38 + public void testCount() {
  39 + assertEquals(2L, CollectionUtils.count(Arrays.asList(1, 2, 3, 4), value -> value % 2 == 0));
  40 + assertEquals(0L, CollectionUtils.count(Collections.emptyList(), value -> true));
  41 + }
  42 +
  43 + @Test
  44 + public void testDistinctCount() {
  45 + assertEquals(3L, CollectionUtils.distinctCount(Arrays.asList("a", "b", "a", "c"), value -> value));
  46 + assertEquals(0L, CollectionUtils.distinctCount(Collections.emptyList(), value -> value));
  47 + }
  48 +
  49 + @Test
  50 + public void testConvertSet_withSupplier() {
  51 + // 调用,并断言:按指定 Set 类型完成转换、去重和排序
  52 + TreeSet<Integer> result = CollectionUtils.convertSetBySupplier(
  53 + Arrays.asList("3", "1", null, "3", "2"),
  54 + value -> value == null ? null : Integer.valueOf(value), TreeSet::new);
  55 + assertEquals(Arrays.asList(1, 2, 3), Arrays.asList(result.toArray(new Integer[0])));
  56 +
  57 + // 调用,并断言:空集合仍返回指定 Set 类型
  58 + result = CollectionUtils.convertSetBySupplier(Collections.<String>emptyList(),
  59 + Integer::valueOf, TreeSet::new);
  60 + assertEquals(TreeSet.class, result.getClass());
  61 + }
  62 +
  63 + @Test
30 64 public void testDiffList() {
31 65 // 准备参数
32 66 Collection<Dog> oldList = Arrays.asList(
... ...
yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.common.util.date;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.TimeRange;
  5 +import org.junit.jupiter.api.Test;
  6 +
  7 +import java.time.LocalDate;
  8 +import java.time.LocalDateTime;
  9 +import java.time.LocalTime;
  10 +import java.time.YearMonth;
  11 +import java.util.Arrays;
  12 +import java.util.Collections;
  13 +import java.util.List;
  14 +
  15 +import static org.junit.jupiter.api.Assertions.assertEquals;
  16 +import static org.junit.jupiter.api.Assertions.assertFalse;
  17 +import static org.junit.jupiter.api.Assertions.assertNotNull;
  18 +import static org.junit.jupiter.api.Assertions.assertNull;
  19 +import static org.junit.jupiter.api.Assertions.assertTrue;
  20 +
  21 +/**
  22 + * {@link LocalDateTimeUtils} 的单元测试
  23 + *
  24 + * @author 芋道源码
  25 + */
  26 +public class LocalDateTimeUtilsTest {
  27 +
  28 + @Test
  29 + public void testDateCompare() {
  30 + // 准备参数
  31 + LocalDate firstDate = LocalDate.of(2026, 7, 23);
  32 + LocalDate secondDate = LocalDate.of(2026, 7, 24);
  33 +
  34 + // 调用,并断言
  35 + assertTrue(LocalDateTimeUtils.isBeforeOrEqual(firstDate, secondDate));
  36 + assertTrue(LocalDateTimeUtils.isBeforeOrEqual(secondDate, secondDate));
  37 + assertFalse(LocalDateTimeUtils.isBeforeOrEqual(secondDate, firstDate));
  38 + assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondDate, firstDate));
  39 + assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondDate, secondDate));
  40 + assertFalse(LocalDateTimeUtils.isAfterOrEqual(firstDate, secondDate));
  41 + }
  42 +
  43 + @Test
  44 + public void testDateTimeCompare() {
  45 + // 准备参数
  46 + LocalDateTime firstTime = LocalDateTime.of(2026, 7, 24, 9, 0);
  47 + LocalDateTime secondTime = LocalDateTime.of(2026, 7, 24, 18, 0);
  48 +
  49 + // 调用,并断言
  50 + assertTrue(LocalDateTimeUtils.isBeforeOrEqual(firstTime, secondTime));
  51 + assertTrue(LocalDateTimeUtils.isBeforeOrEqual(secondTime, secondTime));
  52 + assertFalse(LocalDateTimeUtils.isBeforeOrEqual(secondTime, firstTime));
  53 + assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondTime, firstTime));
  54 + assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondTime, secondTime));
  55 + assertFalse(LocalDateTimeUtils.isAfterOrEqual(firstTime, secondTime));
  56 + }
  57 +
  58 + @Test
  59 + public void testIsTimeRangeValid() {
  60 + // 准备参数
  61 + LocalDateTime beginTime = LocalDateTime.of(2026, 7, 24, 9, 0);
  62 + LocalDateTime endTime = LocalDateTime.of(2026, 7, 24, 18, 0);
  63 +
  64 + // 调用,并断言
  65 + assertTrue(LocalDateTimeUtils.isTimeRangeValid(beginTime, endTime));
  66 + assertFalse(LocalDateTimeUtils.isTimeRangeValid(beginTime, beginTime));
  67 + assertFalse(LocalDateTimeUtils.isTimeRangeValid(endTime, beginTime));
  68 + assertFalse(LocalDateTimeUtils.isTimeRangeValid(null, endTime));
  69 + assertFalse(LocalDateTimeUtils.isTimeRangeValid(beginTime, null));
  70 + }
  71 +
  72 + @Test
  73 + public void testIsTimeRangePresent() {
  74 + // 准备参数
  75 + LocalDateTime time = LocalDateTime.of(2026, 7, 24, 9, 0);
  76 +
  77 + // 调用,并断言:空数组和双空边界都不是有效范围
  78 + assertFalse(LocalDateTimeUtils.isTimeRangePresent(null));
  79 + assertFalse(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[0]));
  80 + assertFalse(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{null, null}));
  81 +
  82 + // 调用,并断言:单边或双边存在即为有效范围
  83 + assertTrue(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{time}));
  84 + assertTrue(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{null, time}));
  85 + assertTrue(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{time, time.plusHours(1)}));
  86 + }
  87 +
  88 + @Test
  89 + public void testGetDayTime() {
  90 + // 准备参数
  91 + LocalDate date = LocalDate.of(2026, 7, 24);
  92 + LocalDateTime time = date.atTime(15, 30);
  93 +
  94 + // 调用,并断言
  95 + assertEquals(LocalDateTime.of(2026, 7, 24, 0, 0),
  96 + LocalDateTimeUtils.getDayBeginTime(date));
  97 + assertEquals(LocalDateTime.of(2026, 7, 24, 0, 0),
  98 + LocalDateTimeUtils.getDayBeginTime(time));
  99 + assertNull(LocalDateTimeUtils.getDayBeginTime((LocalDateTime) null));
  100 + assertEquals(LocalDateTime.of(date, LocalTime.MAX),
  101 + LocalDateTimeUtils.getDayEndTime(date));
  102 + }
  103 +
  104 + @Test
  105 + public void testGetDateTimeRange() {
  106 + // 准备参数
  107 + LocalDate beginDate = LocalDate.of(2026, 7, 1);
  108 + LocalDate endDate = LocalDate.of(2026, 7, 31);
  109 +
  110 + // 调用,并断言:左右两端均为闭区间边界
  111 + LocalDateTime[] result = LocalDateTimeUtils.getDateTimeRange(beginDate, endDate);
  112 + assertEquals(beginDate.atStartOfDay(), result[0]);
  113 + assertEquals(endDate.atTime(LocalTime.MAX), result[1]);
  114 +
  115 + // 调用,并断言:支持单边范围
  116 + result = LocalDateTimeUtils.getDateTimeRange(beginDate, null);
  117 + assertEquals(beginDate.atStartOfDay(), result[0]);
  118 + assertNull(result[1]);
  119 + result = LocalDateTimeUtils.getDateTimeRange(null, endDate);
  120 + assertNull(result[0]);
  121 + assertEquals(endDate.atTime(LocalTime.MAX), result[1]);
  122 +
  123 + // 调用,并断言:两个边界都为空时不生成范围
  124 + assertNull(LocalDateTimeUtils.getDateTimeRange(null, null));
  125 + }
  126 +
  127 + @Test
  128 + public void testGetMonthDateTimeRange() {
  129 + // 调用:闰年二月
  130 + LocalDateTime[] result = LocalDateTimeUtils.getMonthDateTimeRange(2024, 2);
  131 +
  132 + // 断言:完整包含闰年二月的首尾时刻
  133 + assertEquals(LocalDateTime.of(2024, 2, 1, 0, 0), result[0]);
  134 + assertEquals(LocalDate.of(2024, 2, 29).atTime(LocalTime.MAX), result[1]);
  135 + }
  136 +
  137 + @Test
  138 + public void testGetMonthBeginTime() {
  139 + // 准备参数
  140 + YearMonth month = YearMonth.of(2026, 12);
  141 +
  142 + // 调用,并断言
  143 + assertEquals(LocalDateTime.of(2026, 12, 1, 0, 0),
  144 + LocalDateTimeUtils.getMonthBeginTime(month));
  145 + assertEquals(LocalDateTime.of(2027, 1, 1, 0, 0),
  146 + LocalDateTimeUtils.getNextMonthBeginTime(month));
  147 + }
  148 +
  149 + @Test
  150 + public void testIsBetween() {
  151 + // 准备参数
  152 + LocalDate beginDate = LocalDate.of(2026, 7, 23);
  153 + LocalDate endDate = LocalDate.of(2026, 7, 25);
  154 + LocalDateTime beginTime = beginDate.atTime(9, 0);
  155 + LocalDateTime endTime = beginDate.atTime(18, 0);
  156 +
  157 + // 调用,并断言:日期闭区间
  158 + assertTrue(LocalDateTimeUtils.isBetween(beginDate, endDate, beginDate));
  159 + assertTrue(LocalDateTimeUtils.isBetween(beginDate, endDate, LocalDate.of(2026, 7, 24)));
  160 + assertTrue(LocalDateTimeUtils.isBetween(beginDate, endDate, endDate));
  161 + assertFalse(LocalDateTimeUtils.isBetween(beginDate, endDate, LocalDate.of(2026, 7, 26)));
  162 + assertFalse(LocalDateTimeUtils.isBetween(beginDate, endDate, null));
  163 + assertFalse(LocalDateTimeUtils.isNotBetween(beginDate, endDate, beginDate));
  164 + assertTrue(LocalDateTimeUtils.isNotBetween(beginDate, endDate, LocalDate.of(2026, 7, 26)));
  165 + assertTrue(LocalDateTimeUtils.isNotBetween(beginDate, endDate, null));
  166 +
  167 + // 调用,并断言:时间闭区间
  168 + assertTrue(LocalDateTimeUtils.isBetween(beginTime, endTime, beginTime));
  169 + assertTrue(LocalDateTimeUtils.isBetween(beginTime, endTime, beginDate.atTime(12, 0)));
  170 + assertTrue(LocalDateTimeUtils.isBetween(beginTime, endTime, endTime));
  171 + assertFalse(LocalDateTimeUtils.isBetween(beginTime, endTime, beginDate.atTime(18, 1)));
  172 + assertFalse(LocalDateTimeUtils.isBetween(beginTime, endTime, (LocalDateTime) null));
  173 + assertFalse(LocalDateTimeUtils.isNotBetween(beginTime, endTime, endTime));
  174 + assertTrue(LocalDateTimeUtils.isNotBetween(beginTime, endTime, beginDate.atTime(18, 1)));
  175 + assertTrue(LocalDateTimeUtils.isNotBetween(beginTime, endTime, null));
  176 + }
  177 +
  178 + @Test
  179 + public void testIsClosedRangeOverlap() {
  180 + // 准备参数
  181 + LocalDateTime firstBeginTime = LocalDateTime.of(2026, 7, 24, 9, 0);
  182 + LocalDateTime firstEndTime = LocalDateTime.of(2026, 7, 24, 12, 0);
  183 +
  184 + // 调用,并断言:首尾相接属于闭区间重叠
  185 + assertTrue(LocalDateTimeUtils.isClosedRangeOverlap(
  186 + firstBeginTime, firstEndTime, firstEndTime, firstEndTime.plusHours(1)));
  187 + assertTrue(LocalDateTimeUtils.isClosedRangeOverlap(
  188 + firstBeginTime, firstEndTime, firstBeginTime.minusHours(1), firstBeginTime));
  189 + assertFalse(LocalDateTimeUtils.isClosedRangeOverlap(
  190 + firstBeginTime, firstEndTime, firstEndTime.plusNanos(1), firstEndTime.plusHours(1)));
  191 + assertFalse(LocalDateTimeUtils.isClosedRangeOverlap(
  192 + firstBeginTime, firstEndTime, null, firstEndTime));
  193 + }
  194 +
  195 + @Test
  196 + public void testSubtractTimeRanges() {
  197 + // 准备参数
  198 + LocalDate date = LocalDate.of(2026, 7, 24);
  199 + TimeRange sourceRange = new TimeRange(date.atTime(9, 0), date.atTime(18, 0));
  200 +
  201 + // 调用:扣除原时段中间的一段
  202 + List<TimeRange> result = LocalDateTimeUtils.subtractTimeRanges(
  203 + Collections.singletonList(sourceRange),
  204 + new TimeRange(date.atTime(12, 0), date.atTime(13, 0)));
  205 +
  206 + // 断言:原时段被拆分为两段
  207 + assertEquals(2, result.size());
  208 + assertTimeRange(CollUtil.getFirst(result), date.atTime(9, 0), date.atTime(12, 0));
  209 + assertTimeRange(result.get(1), date.atTime(13, 0), date.atTime(18, 0));
  210 +
  211 + // 调用,并断言:扣除时段完整覆盖原时段
  212 + result = LocalDateTimeUtils.subtractTimeRanges(
  213 + Collections.singletonList(sourceRange),
  214 + new TimeRange(date.atTime(8, 0), date.atTime(19, 0)));
  215 + assertTrue(result.isEmpty());
  216 +
  217 + // 调用:扣除时段与原时段首尾相接
  218 + result = LocalDateTimeUtils.subtractTimeRanges(
  219 + Collections.singletonList(sourceRange),
  220 + new TimeRange(date.atTime(18, 0), date.atTime(19, 0)));
  221 + // 断言:两个时段仅首尾相接,没有正时长交集,原时段保持不变
  222 + assertEquals(1, result.size());
  223 + assertTimeRange(CollUtil.getFirst(result), date.atTime(9, 0), date.atTime(18, 0));
  224 + }
  225 +
  226 + @Test
  227 + public void testMergeTimeRanges() {
  228 + // 准备参数
  229 + LocalDate date = LocalDate.of(2026, 7, 24);
  230 + List<TimeRange> timeRanges = Arrays.asList(
  231 + new TimeRange(date.atTime(13, 0), date.atTime(14, 0)),
  232 + new TimeRange(date.atTime(9, 0), date.atTime(11, 0)),
  233 + new TimeRange(date.atTime(10, 0), date.atTime(13, 0)),
  234 + new TimeRange(date.atTime(16, 0), date.atTime(17, 0)));
  235 +
  236 + // 调用
  237 + List<TimeRange> result = LocalDateTimeUtils.mergeTimeRanges(timeRanges);
  238 +
  239 + // 断言:重叠或首尾相接的时段被合并,独立时段保持不变
  240 + assertEquals(2, result.size());
  241 + assertTimeRange(CollUtil.getFirst(result), date.atTime(9, 0), date.atTime(14, 0));
  242 + assertTimeRange(result.get(1), date.atTime(16, 0), date.atTime(17, 0));
  243 + // 调用,并断言:空列表
  244 + assertTrue(LocalDateTimeUtils.mergeTimeRanges(Collections.emptyList()).isEmpty());
  245 + }
  246 +
  247 + @Test
  248 + public void testIntersectTimeRanges() {
  249 + // 准备参数
  250 + LocalDate date = LocalDate.of(2026, 7, 24);
  251 + List<TimeRange> firstRanges = Arrays.asList(
  252 + new TimeRange(date.atTime(9, 0), date.atTime(12, 0)),
  253 + new TimeRange(date.atTime(13, 0), date.atTime(18, 0)));
  254 + List<TimeRange> secondRanges = Arrays.asList(
  255 + new TimeRange(date.atTime(10, 0), date.atTime(14, 0)),
  256 + new TimeRange(date.atTime(17, 0), date.atTime(19, 0)));
  257 +
  258 + // 调用
  259 + List<TimeRange> result = LocalDateTimeUtils.intersectTimeRanges(firstRanges, secondRanges);
  260 +
  261 + // 断言
  262 + assertEquals(3, result.size());
  263 + assertTimeRange(CollUtil.getFirst(result), date.atTime(10, 0), date.atTime(12, 0));
  264 + assertTimeRange(result.get(1), date.atTime(13, 0), date.atTime(14, 0));
  265 + assertTimeRange(result.get(2), date.atTime(17, 0), date.atTime(18, 0));
  266 + // 调用,并断言:两个时段仅首尾相接时不生成零时长交集
  267 + assertNull(LocalDateTimeUtils.intersectTimeRange(
  268 + new TimeRange(date.atTime(9, 0), date.atTime(10, 0)),
  269 + new TimeRange(date.atTime(10, 0), date.atTime(11, 0))));
  270 + }
  271 +
  272 + @Test
  273 + public void testCalculateDurationMinutes() {
  274 + // 准备参数
  275 + LocalDate date = LocalDate.of(2026, 7, 24);
  276 + List<TimeRange> timeRanges = Arrays.asList(
  277 + new TimeRange(date.atTime(9, 0), date.atTime(10, 0)),
  278 + new TimeRange(date.atTime(13, 0), date.atTime(14, 30)));
  279 +
  280 + // 调用,并断言
  281 + assertEquals(150, LocalDateTimeUtils.calculateDurationMinutes(timeRanges));
  282 + // 调用,并断言:空列表
  283 + assertEquals(0, LocalDateTimeUtils.calculateDurationMinutes(Collections.emptyList()));
  284 + }
  285 +
  286 + @Test
  287 + public void testBuildDailyTimeRanges() {
  288 + // 准备参数
  289 + LocalDate beginDate = LocalDate.of(2026, 7, 24);
  290 + LocalDate endDate = LocalDate.of(2026, 7, 25);
  291 +
  292 + // 调用:构建跨天时间范围
  293 + List<TimeRange> result = LocalDateTimeUtils.buildDailyTimeRanges(
  294 + beginDate, endDate, LocalTime.of(22, 0), LocalTime.of(6, 0));
  295 +
  296 + // 断言
  297 + assertEquals(2, result.size());
  298 + assertTimeRange(CollUtil.getFirst(result), beginDate.atTime(22, 0), beginDate.plusDays(1).atTime(6, 0));
  299 + assertTimeRange(result.get(1), endDate.atTime(22, 0), endDate.plusDays(1).atTime(6, 0));
  300 +
  301 + // 调用:每日开始和结束时刻相同
  302 + result = LocalDateTimeUtils.buildDailyTimeRanges(
  303 + beginDate, beginDate, LocalTime.of(9, 0), LocalTime.of(9, 0));
  304 + // 断言:按完整一天处理
  305 + assertEquals(1, result.size());
  306 + assertTimeRange(CollUtil.getFirst(result), beginDate.atTime(9, 0), beginDate.plusDays(1).atTime(9, 0));
  307 +
  308 + // 调用,并断言:开始日期晚于结束日期
  309 + assertTrue(LocalDateTimeUtils.buildDailyTimeRanges(
  310 + endDate, beginDate, LocalTime.of(9, 0), LocalTime.of(18, 0)).isEmpty());
  311 + }
  312 +
  313 + @Test
  314 + public void testFindDailyTimeRange() {
  315 + // 准备参数
  316 + LocalDate date = LocalDate.of(2026, 7, 8);
  317 +
  318 + // 调用,并断言普通时间范围的左右边界
  319 + TimeRange result = LocalDateTimeUtils.findDailyTimeRange(
  320 + date, LocalTime.of(8, 0), LocalTime.of(10, 0), date.atTime(8, 0));
  321 + assertEquals(date.atTime(8, 0), result.getStartTime());
  322 + assertEquals(date.atTime(10, 0), result.getEndTime());
  323 + assertNotNull(LocalDateTimeUtils.findDailyTimeRange(
  324 + date, LocalTime.of(8, 0), LocalTime.of(10, 0), date.atTime(10, 0)));
  325 +
  326 + // 调用,并断言跨日时间范围
  327 + result = LocalDateTimeUtils.findDailyTimeRange(
  328 + date, LocalTime.of(20, 0), LocalTime.of(6, 0), date.plusDays(1).atTime(5, 0));
  329 + assertEquals(date.atTime(20, 0), result.getStartTime());
  330 + assertEquals(date.plusDays(1).atTime(6, 0), result.getEndTime());
  331 + assertNull(LocalDateTimeUtils.findDailyTimeRange(
  332 + date, LocalTime.of(8, 0), LocalTime.of(10, 0), date.atTime(12, 0)));
  333 + }
  334 +
  335 + private static void assertTimeRange(TimeRange timeRange,
  336 + LocalDateTime expectedStartTime,
  337 + LocalDateTime expectedEndTime) {
  338 + assertEquals(expectedStartTime, timeRange.getStartTime());
  339 + assertEquals(expectedEndTime, timeRange.getEndTime());
  340 + }
  341 +
  342 +}
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/StringListConvert.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.excel.core.convert;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.idev.excel.converters.Converter;
  5 +import cn.idev.excel.metadata.GlobalConfiguration;
  6 +import cn.idev.excel.metadata.data.WriteCellData;
  7 +import cn.idev.excel.metadata.property.ExcelContentProperty;
  8 +
  9 +import java.util.List;
  10 +
  11 +/**
  12 + * Excel 字符串列表转换器
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +public class StringListConvert implements Converter<List<String>> {
  17 +
  18 + @Override
  19 + public WriteCellData<String> convertToExcelData(List<String> value,
  20 + ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
  21 + return new WriteCellData<>(CollUtil.isEmpty(value) ? "" : String.join("/", value));
  22 + }
  23 +
  24 +}
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java
1 1 package cn.iocoder.yudao.framework.excel.core.util;
2 2  
3 3 import cn.idev.excel.FastExcelFactory;
  4 +import cn.idev.excel.context.AnalysisContext;
4 5 import cn.idev.excel.converters.longconverter.LongStringConverter;
  6 +import cn.idev.excel.read.listener.ReadListener;
5 7 import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
6 8 import cn.iocoder.yudao.framework.excel.core.handler.ColumnWidthMatchStyleStrategy;
7 9 import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler;
... ... @@ -10,7 +12,10 @@ import org.springframework.web.multipart.MultipartFile;
10 12  
11 13 import java.io.IOException;
12 14 import java.io.InputStream;
  15 +import java.util.ArrayList;
  16 +import java.util.Collections;
13 17 import java.util.List;
  18 +import java.util.Map;
14 19  
15 20 /**
16 21 * Excel 工具类
... ... @@ -44,7 +49,33 @@ public class ExcelUtils {
44 49 response.setContentType("application/vnd.ms-excel;charset=UTF-8");
45 50 }
46 51  
  52 + /**
  53 + * 将动态表头和数据以 Excel 响应给前端
  54 + *
  55 + * @param response 响应
  56 + * @param filename 文件名
  57 + * @param sheetName Excel sheet 名
  58 + * @param head Excel head 头
  59 + * @param data 数据列表
  60 + * @throws IOException 写入失败的情况
  61 + */
  62 + public static void write(HttpServletResponse response, String filename, String sheetName,
  63 + List<List<String>> head, List<List<Object>> data) throws IOException {
  64 + // 输出 Excel
  65 + FastExcelFactory.write(response.getOutputStream())
  66 + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
  67 + .registerWriteHandler(new ColumnWidthMatchStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
  68 + .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
  69 + .head(head).sheet(sheetName).doWrite(data);
  70 + // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
  71 + response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
  72 + response.setContentType("application/vnd.ms-excel;charset=UTF-8");
  73 + }
  74 +
47 75 public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
  76 + if (file == null || file.isEmpty()) {
  77 + return Collections.emptyList();
  78 + }
48 79 // 参考 https://t.zsxq.com/zM77F 帖子,增加 try 处理,兼容 windows 场景
49 80 try (InputStream inputStream = file.getInputStream()) {
50 81 return FastExcelFactory.read(inputStream, head, null)
... ... @@ -53,4 +84,64 @@ public class ExcelUtils {
53 84 }
54 85 }
55 86  
  87 + /**
  88 + * 读取指定数量以内的 Excel 数据,达到上限后停止继续解析。
  89 + *
  90 + * @param file Excel 文件,可为空
  91 + * @param head Excel 表头类型
  92 + * @param maxRowCount 最大读取行数
  93 + * @return Excel 数据
  94 + */
  95 + public static <T> List<T> read(MultipartFile file, Class<T> head, int maxRowCount) throws IOException {
  96 + if (file == null || file.isEmpty()) {
  97 + return Collections.emptyList();
  98 + }
  99 + if (maxRowCount <= 0) {
  100 + throw new IllegalArgumentException("maxRowCount must be greater than 0");
  101 + }
  102 + List<T> rows = new ArrayList<>(Math.min(maxRowCount, 1024));
  103 + try (InputStream inputStream = file.getInputStream()) {
  104 + FastExcelFactory.read(inputStream, head, new ReadListener<T>() {
  105 +
  106 + @Override
  107 + public void invoke(T data, AnalysisContext context) {
  108 + rows.add(data);
  109 + }
  110 +
  111 + @Override
  112 + public void doAfterAllAnalysed(AnalysisContext context) {
  113 + // 无需处理
  114 + }
  115 +
  116 + @Override
  117 + public boolean hasNext(AnalysisContext context) {
  118 + return rows.size() < maxRowCount;
  119 + }
  120 +
  121 + }).autoCloseStream(false).doReadAll();
  122 + }
  123 + return rows;
  124 + }
  125 +
  126 + /**
  127 + * 读取动态表头 Excel 数据
  128 + *
  129 + * @param file Excel 文件,可为空
  130 + * @return 按列下标存储的行数据
  131 + * @throws IOException 读取失败
  132 + */
  133 + public static List<Map<Integer, String>> read(MultipartFile file) throws IOException {
  134 + if (file == null || file.isEmpty()) {
  135 + return Collections.emptyList();
  136 + }
  137 + // 参考 https://t.zsxq.com/zM77F 帖子,增加 try 处理,兼容 windows 场景
  138 + try (InputStream inputStream = file.getInputStream()) {
  139 + return FastExcelFactory.read(inputStream)
  140 + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
  141 + .headRowNumber(1)
  142 + .sheet()
  143 + .doReadSync();
  144 + }
  145 + }
  146 +
56 147 }
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/PoiExcelUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.excel.core.util;
  2 +
  3 +import cn.hutool.core.util.NumberUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import lombok.experimental.UtilityClass;
  6 +import org.apache.poi.ss.usermodel.BorderStyle;
  7 +import org.apache.poi.ss.usermodel.Cell;
  8 +import org.apache.poi.ss.usermodel.CellStyle;
  9 +import org.apache.poi.ss.usermodel.ClientAnchor;
  10 +import org.apache.poi.ss.usermodel.Comment;
  11 +import org.apache.poi.ss.usermodel.CreationHelper;
  12 +import org.apache.poi.ss.usermodel.DataFormatter;
  13 +import org.apache.poi.ss.usermodel.FillPatternType;
  14 +import org.apache.poi.ss.usermodel.Font;
  15 +import org.apache.poi.ss.usermodel.FormulaEvaluator;
  16 +import org.apache.poi.ss.usermodel.HorizontalAlignment;
  17 +import org.apache.poi.ss.usermodel.IndexedColors;
  18 +import org.apache.poi.ss.usermodel.Row;
  19 +import org.apache.poi.ss.usermodel.Sheet;
  20 +import org.apache.poi.ss.usermodel.VerticalAlignment;
  21 +import org.apache.poi.ss.usermodel.Workbook;
  22 +import org.apache.poi.ss.util.CellRangeAddress;
  23 +
  24 +import java.math.BigDecimal;
  25 +import java.util.Collection;
  26 +
  27 +/**
  28 + * 原生 POI 的 Excel 工具类
  29 + *
  30 + * 全局 {@link ExcelUtils}(FastExcel)无法表达单元格锁定、公式、动态列等复杂模板时,各模块基于本类实现自定义 Excel 读写
  31 + *
  32 + * @author 芋道源码
  33 + */
  34 +@UtilityClass
  35 +public class PoiExcelUtils {
  36 +
  37 + /**
  38 + * 读取公式计算值必须 DataFormatter + FormulaEvaluator,hutool 没有更简的等价写法
  39 + */
  40 + private static final DataFormatter DATA_FORMATTER = new DataFormatter();
  41 +
  42 + // ==================== 读取 ====================
  43 +
  44 + /**
  45 + * 读取单元格文本,公式单元格取公式计算值
  46 + *
  47 + * @param row 行
  48 + * @param column 列
  49 + * @param evaluator 公式计算器,不需要求值时传 null
  50 + * @return 文本,空单元格返回空字符串
  51 + */
  52 + public static String getCellText(Row row, Integer column, FormulaEvaluator evaluator) {
  53 + if (row == null || column == null || row.getCell(column) == null) {
  54 + return "";
  55 + }
  56 + return evaluator == null
  57 + ? DATA_FORMATTER.formatCellValue(row.getCell(column))
  58 + : DATA_FORMATTER.formatCellValue(row.getCell(column), evaluator);
  59 + }
  60 +
  61 + /**
  62 + * 读取单元格数字,兼容千分位文本
  63 + *
  64 + * @param row 行
  65 + * @param column 列
  66 + * @param evaluator 公式计算器,不需要求值时传 null
  67 + * @return 数字,空单元格返回 null;格式不正确时抛出 {@link NumberFormatException}
  68 + */
  69 + public static BigDecimal getDecimal(Row row, Integer column, FormulaEvaluator evaluator) {
  70 + String value = getCellText(row, column, evaluator).replace(",", "");
  71 + return StrUtil.isBlank(value) ? null : NumberUtil.toBigDecimal(value);
  72 + }
  73 +
  74 + /**
  75 + * 判断指定列是否全部为空
  76 + *
  77 + * @param row 行
  78 + * @param evaluator 公式计算器,不需要求值时传 null
  79 + * @param columns 列数组
  80 + * @return 是否全部为空
  81 + */
  82 + public static boolean isEmptyRow(Row row, FormulaEvaluator evaluator, Collection<Integer> columns) {
  83 + if (row == null) {
  84 + return true;
  85 + }
  86 + for (Integer column : columns) {
  87 + if (StrUtil.isNotBlank(getCellText(row, column, evaluator))) {
  88 + return false;
  89 + }
  90 + }
  91 + return true;
  92 + }
  93 +
  94 + // ==================== 写入 ====================
  95 +
  96 + /**
  97 + * 写入文本单元格,null 按空字符串处理
  98 + */
  99 + public static void writeText(Row row, int column, String value, CellStyle style) {
  100 + Cell cell = row.createCell(column);
  101 + cell.setCellValue(StrUtil.nullToEmpty(value));
  102 + cell.setCellStyle(style);
  103 + }
  104 +
  105 + /**
  106 + * 写入数字单元格,null 时留空
  107 + */
  108 + public static void writeNumber(Row row, int column, BigDecimal value, CellStyle style) {
  109 + Cell cell = row.createCell(column);
  110 + if (value != null) {
  111 + cell.setCellValue(value.doubleValue());
  112 + }
  113 + cell.setCellStyle(style);
  114 + }
  115 +
  116 + /**
  117 + * 写入公式单元格
  118 + */
  119 + public static void writeFormula(Row row, int column, String formula, CellStyle style) {
  120 + Cell cell = row.createCell(column);
  121 + cell.setCellFormula(formula);
  122 + cell.setCellStyle(style);
  123 + }
  124 +
  125 + /**
  126 + * 写入跨两行的合并表头
  127 + */
  128 + public static void mergeHeader(Sheet sheet, Row firstRow, Row secondRow,
  129 + int beginColumn, int endColumn, String title, CellStyle style) {
  130 + sheet.addMergedRegion(new CellRangeAddress(firstRow.getRowNum(), secondRow.getRowNum(),
  131 + beginColumn, endColumn));
  132 + Cell cell = firstRow.createCell(beginColumn);
  133 + cell.setCellValue(title);
  134 + cell.setCellStyle(style);
  135 + secondRow.createCell(beginColumn).setCellStyle(style);
  136 + }
  137 +
  138 + /**
  139 + * 添加单元格批注
  140 + *
  141 + * @param cell 单元格
  142 + * @param text 批注内容
  143 + */
  144 + public static void addComment(Cell cell, String text) {
  145 + CreationHelper helper = cell.getSheet().getWorkbook().getCreationHelper();
  146 + ClientAnchor anchor = helper.createClientAnchor();
  147 + anchor.setCol1(cell.getColumnIndex());
  148 + anchor.setCol2(cell.getColumnIndex() + 3);
  149 + anchor.setRow1(cell.getRowIndex());
  150 + anchor.setRow2(cell.getRowIndex() + 3);
  151 + Comment comment = cell.getSheet().createDrawingPatriarch().createCellComment(anchor);
  152 + comment.setString(helper.createRichTextString(text));
  153 + cell.setCellComment(comment);
  154 + }
  155 +
  156 + // ==================== 样式 ====================
  157 +
  158 + /**
  159 + * 创建细边框样式
  160 + */
  161 + public static CellStyle createBorderStyle(Workbook workbook) {
  162 + CellStyle style = workbook.createCellStyle();
  163 + style.setBorderTop(BorderStyle.THIN);
  164 + style.setBorderRight(BorderStyle.THIN);
  165 + style.setBorderBottom(BorderStyle.THIN);
  166 + style.setBorderLeft(BorderStyle.THIN);
  167 + return style;
  168 + }
  169 +
  170 + /**
  171 + * 创建数据单元格样式:细边框、垂直居中,可指定锁定和填充色
  172 + *
  173 + * @param workbook 工作簿
  174 + * @param locked 是否锁定,配合 protectSheet 使用
  175 + * @param fillColor 填充色,传 null 时不填充
  176 + */
  177 + public static CellStyle createDataStyle(Workbook workbook, boolean locked, IndexedColors fillColor) {
  178 + CellStyle style = createBorderStyle(workbook);
  179 + style.setLocked(locked);
  180 + if (fillColor != null) {
  181 + style.setFillForegroundColor(fillColor.getIndex());
  182 + style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
  183 + }
  184 + style.setVerticalAlignment(VerticalAlignment.CENTER);
  185 + return style;
  186 + }
  187 +
  188 + /**
  189 + * 基于已有样式创建数字格式样式
  190 + */
  191 + public static CellStyle createNumberStyle(Workbook workbook, CellStyle base, String format) {
  192 + CellStyle style = workbook.createCellStyle();
  193 + style.cloneStyleFrom(base);
  194 + style.setDataFormat(workbook.createDataFormat().getFormat(format));
  195 + return style;
  196 + }
  197 +
  198 + /**
  199 + * 创建表头样式:细边框、指定填充色、加粗居中
  200 + */
  201 + public static CellStyle createHeaderStyle(Workbook workbook, IndexedColors fillColor) {
  202 + CellStyle style = createBorderStyle(workbook);
  203 + style.setFillForegroundColor(fillColor.getIndex());
  204 + style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
  205 + style.setAlignment(HorizontalAlignment.CENTER);
  206 + style.setVerticalAlignment(VerticalAlignment.CENTER);
  207 + Font font = workbook.createFont();
  208 + font.setBold(true);
  209 + style.setFont(font);
  210 + return style;
  211 + }
  212 +
  213 + /**
  214 + * 创建标题样式:大号加粗居中
  215 + */
  216 + public static CellStyle createTitleStyle(Workbook workbook) {
  217 + CellStyle style = workbook.createCellStyle();
  218 + style.setAlignment(HorizontalAlignment.CENTER);
  219 + Font font = workbook.createFont();
  220 + font.setBold(true);
  221 + font.setFontHeightInPoints((short) 20);
  222 + style.setFont(font);
  223 + return style;
  224 + }
  225 +
  226 +}
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/test/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.excel.core.util;
  2 +
  3 +import cn.idev.excel.FastExcelFactory;
  4 +import cn.idev.excel.annotation.ExcelProperty;
  5 +import org.junit.jupiter.api.Test;
  6 +import org.springframework.mock.web.MockMultipartFile;
  7 +
  8 +import java.io.ByteArrayOutputStream;
  9 +import java.util.Arrays;
  10 +import java.util.List;
  11 +
  12 +import static org.junit.jupiter.api.Assertions.assertEquals;
  13 +
  14 +/**
  15 + * {@link ExcelUtils} 的单元测试
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +public class ExcelUtilsTest {
  20 +
  21 + @Test
  22 + public void testRead_maxRowCount() throws Exception {
  23 + // mock 数据
  24 + ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
  25 + FastExcelFactory.write(outputStream, TestRow.class).sheet().doWrite(Arrays.asList(
  26 + new TestRow("员工一"), new TestRow("员工二"), new TestRow("员工三")));
  27 + MockMultipartFile file = new MockMultipartFile(
  28 + "file", "employees.xlsx", null, outputStream.toByteArray());
  29 +
  30 + // 调用
  31 + List<TestRow> rows = ExcelUtils.read(file, TestRow.class, 2);
  32 +
  33 + // 断言
  34 + assertEquals(2, rows.size());
  35 + assertEquals("员工一", rows.get(0).getName());
  36 + assertEquals("员工二", rows.get(1).getName());
  37 + }
  38 +
  39 + public static class TestRow {
  40 +
  41 + @ExcelProperty("姓名")
  42 + private String name;
  43 +
  44 + public TestRow() {
  45 + }
  46 +
  47 + public TestRow(String name) {
  48 + this.name = name;
  49 + }
  50 +
  51 + public String getName() {
  52 + return name;
  53 + }
  54 +
  55 + public void setName(String name) {
  56 + this.name = name;
  57 + }
  58 +
  59 + }
  60 +
  61 +}
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
... ... @@ -145,6 +145,18 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; {
145 145 }
146 146  
147 147 /**
  148 + * 获得满足条件的第 1 条记录,并使用 FOR UPDATE 锁定。
  149 + *
  150 + * 注意:需要在事务中调用,否则锁会立即释放。
  151 + *
  152 + * @param queryWrapper 查询条件
  153 + * @return 实体
  154 + */
  155 + default T selectFirstOneForUpdate(LambdaQueryWrapper<T> queryWrapper) {
  156 + return selectOne(queryWrapper.last("LIMIT 1 FOR UPDATE"));
  157 + }
  158 +
  159 + /**
148 160 * 获取满足条件的第 1 条记录
149 161 *
150 162 * 目的:解决并发场景下,插入多条记录后,使用 selectOne 会报错的问题
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperX.java
... ... @@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
7 7 import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
8 8 import org.springframework.util.StringUtils;
9 9  
  10 +import java.time.LocalDateTime;
10 11 import java.util.Collection;
11 12  
12 13 /**
... ... @@ -107,6 +108,24 @@ public class LambdaQueryWrapperX&lt;T&gt; extends LambdaQueryWrapper&lt;T&gt; {
107 108 return betweenIfPresent(column, val1, val2);
108 109 }
109 110  
  111 + /**
  112 + * 拼接两个闭区间的重叠条件
  113 + *
  114 + * <p>记录区间与查询区间重叠的条件为:记录开始值小于等于查询结束值,
  115 + * 且记录结束值大于等于查询开始值。不能使用单字段 {@code BETWEEN} 代替,
  116 + * 否则会漏掉完整覆盖查询区间的记录。</p>
  117 + *
  118 + * @param beginColumn 记录开始值字段
  119 + * @param endColumn 记录结束值字段
  120 + * @param times 查询闭区间
  121 + * @return 查询条件
  122 + */
  123 + public LambdaQueryWrapperX<T> betweenIfPresent(
  124 + SFunction<T, ?> beginColumn, SFunction<T, ?> endColumn, LocalDateTime[] times) {
  125 + return leIfPresent(beginColumn, ArrayUtils.get(times, 1))
  126 + .geIfPresent(endColumn, ArrayUtils.get(times, 0));
  127 + }
  128 +
110 129 // ========== 重写父类方法,方便链式调用 ==========
111 130  
112 131 @Override
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java
... ... @@ -102,16 +102,9 @@ public class QueryWrapperX&lt;T&gt; extends QueryWrapper&lt;T&gt; {
102 102 }
103 103  
104 104 public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) {
105   - if (values != null && values.length != 0 && values[0] != null && values[1] != null) {
106   - return (QueryWrapperX<T>) super.between(column, values[0], values[1]);
107   - }
108   - if (values != null && values.length != 0 && values[0] != null) {
109   - return (QueryWrapperX<T>) ge(column, values[0]);
110   - }
111   - if (values != null && values.length != 0 && values[1] != null) {
112   - return (QueryWrapperX<T>) le(column, values[1]);
113   - }
114   - return this;
  105 + Object val1 = values != null && values.length > 0 ? values[0] : null;
  106 + Object val2 = values != null && values.length > 1 ? values[1] : null;
  107 + return betweenIfPresent(column, val1, val2);
115 108 }
116 109  
117 110 // ========== 重写父类方法,方便链式调用 ==========
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java
... ... @@ -24,6 +24,8 @@ import java.util.ArrayList;
24 24 import java.util.Collection;
25 25 import java.util.List;
26 26 import java.util.regex.Pattern;
  27 +import java.util.stream.Collectors;
  28 +import java.util.stream.IntStream;
27 29  
28 30 /**
29 31 * MyBatis 工具类
... ... @@ -179,6 +181,28 @@ public class MyBatisUtils {
179 181 return findInSet(columnName, paramIndex);
180 182 }
181 183  
  184 + /**
  185 + * 跨数据库的 find_in_set 实现,适用于同一字段匹配多个参数的场景
  186 + *
  187 + * 每个参数生成一个 find_in_set 条件,并使用 OR 连接。
  188 + *
  189 + * @param columnName 字段名称
  190 + * @param values 参数集合
  191 + * @return sql
  192 + */
  193 + public static String findInSet(String columnName, Collection<?> values) {
  194 + return findInSet(JdbcUtils.getDbType(), columnName, values);
  195 + }
  196 +
  197 + static String findInSet(DbType dbType, String columnName, Collection<?> values) {
  198 + if (CollUtil.isEmpty(values)) {
  199 + throw new IllegalArgumentException("Values cannot be empty");
  200 + }
  201 + return IntStream.range(0, values.size())
  202 + .mapToObj(index -> findInSet(dbType, columnName, index))
  203 + .collect(Collectors.joining(" OR "));
  204 + }
  205 +
182 206 private static String findInSet(String columnName, int paramIndex) {
183 207 DbType dbType = JdbcUtils.getDbType();
184 208 return findInSet(dbType, columnName, paramIndex);
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperXTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.mybatis.core.query;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.TableName;
  4 +import com.baomidou.mybatisplus.core.MybatisConfiguration;
  5 +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
  6 +import lombok.Data;
  7 +import org.apache.ibatis.builder.MapperBuilderAssistant;
  8 +import org.junit.jupiter.api.BeforeAll;
  9 +import org.junit.jupiter.api.Test;
  10 +
  11 +import java.time.LocalDateTime;
  12 +
  13 +import static org.junit.jupiter.api.Assertions.assertEquals;
  14 +import static org.junit.jupiter.api.Assertions.assertTrue;
  15 +
  16 +/**
  17 + * {@link LambdaQueryWrapperX} 的单元测试
  18 + *
  19 + * @author 芋道源码
  20 + */
  21 +public class LambdaQueryWrapperXTest {
  22 +
  23 + @BeforeAll
  24 + public static void initTableInfo() {
  25 + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TestDO.class);
  26 + }
  27 +
  28 + @Test
  29 + public void testBetweenIfPresent_overlap() {
  30 + // 调用,并断言:空范围不追加条件
  31 + assertEquals("", new LambdaQueryWrapperX<TestDO>()
  32 + .betweenIfPresent(TestDO::getStartTime, TestDO::getEndTime, null)
  33 + .getSqlSegment());
  34 +
  35 + // 调用,并断言:双边范围使用闭区间重叠条件
  36 + LocalDateTime beginTime = LocalDateTime.of(2026, 8, 1, 0, 0);
  37 + LocalDateTime endTime = LocalDateTime.of(2026, 8, 31, 23, 59);
  38 + String sqlSegment = new LambdaQueryWrapperX<TestDO>()
  39 + .betweenIfPresent(TestDO::getStartTime, TestDO::getEndTime,
  40 + new LocalDateTime[]{beginTime, endTime})
  41 + .getSqlSegment();
  42 + assertTrue(sqlSegment.contains("start_time <="));
  43 + assertTrue(sqlSegment.contains("end_time >="));
  44 +
  45 + // 调用,并断言:单边范围只追加对应的重叠边界
  46 + sqlSegment = new LambdaQueryWrapperX<TestDO>()
  47 + .betweenIfPresent(TestDO::getStartTime, TestDO::getEndTime,
  48 + new LocalDateTime[]{beginTime})
  49 + .getSqlSegment();
  50 + assertTrue(sqlSegment.contains("end_time >="));
  51 + sqlSegment = new LambdaQueryWrapperX<TestDO>()
  52 + .betweenIfPresent(TestDO::getStartTime, TestDO::getEndTime,
  53 + new LocalDateTime[]{null, endTime})
  54 + .getSqlSegment();
  55 + assertTrue(sqlSegment.contains("start_time <="));
  56 + }
  57 +
  58 + @Data
  59 + @TableName("test_data")
  60 + private static class TestDO {
  61 +
  62 + private LocalDateTime startTime;
  63 + private LocalDateTime endTime;
  64 +
  65 + }
  66 +
  67 +}
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperXTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.mybatis.core.query;
  2 +
  3 +import org.junit.jupiter.api.Test;
  4 +
  5 +import static org.junit.jupiter.api.Assertions.assertEquals;
  6 +import static org.junit.jupiter.api.Assertions.assertTrue;
  7 +
  8 +/**
  9 + * {@link QueryWrapperX} 的单元测试
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +public class QueryWrapperXTest {
  14 +
  15 + @Test
  16 + public void testBetweenIfPresent_array() {
  17 + // 调用,并断言:空范围不追加条件
  18 + assertEquals("", new QueryWrapperX<>().betweenIfPresent("create_time", (Object[]) null).getSqlSegment());
  19 + assertEquals("", new QueryWrapperX<>().betweenIfPresent("create_time", new Object[0]).getSqlSegment());
  20 +
  21 + // 调用,并断言:单边范围安全降级为大于等于或小于等于
  22 + assertTrue(new QueryWrapperX<>().betweenIfPresent("create_time", new Object[]{1})
  23 + .getSqlSegment().contains("create_time >="));
  24 + assertTrue(new QueryWrapperX<>().betweenIfPresent("create_time", new Object[]{null, 2})
  25 + .getSqlSegment().contains("create_time <="));
  26 +
  27 + // 调用,并断言:双边范围使用左右闭合的 BETWEEN
  28 + assertTrue(new QueryWrapperX<>().betweenIfPresent("create_time", new Object[]{1, 2})
  29 + .getSqlSegment().contains("create_time BETWEEN"));
  30 + }
  31 +
  32 +}
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtilsTest.java
... ... @@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
10 10 import org.junit.jupiter.api.Test;
11 11  
12 12 import java.util.Arrays;
  13 +import java.util.Collections;
13 14 import java.util.List;
14 15  
15 16 import static org.junit.jupiter.api.Assertions.assertEquals;
... ... @@ -129,6 +130,14 @@ public class MyBatisUtilsTest {
129 130 }
130 131  
131 132 @Test
  133 + public void testFindInSet_multipleValues() {
  134 + assertEquals("FIND_IN_SET({0}, tag_ids) <> 0 OR FIND_IN_SET({1}, tag_ids) <> 0",
  135 + MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", Arrays.asList(1L, 2L)));
  136 + assertThrows(IllegalArgumentException.class,
  137 + () -> MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", Collections.emptyList()));
  138 + }
  139 +
  140 + @Test
132 141 public void testFindInSet_applyBindsValue() {
133 142 // 准备参数
134 143 QueryWrapper<Object> query = new QueryWrapper<>();
... ...
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java
... ... @@ -19,6 +19,13 @@ public interface BpmProcessInstanceApi {
19 19 */
20 20 String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO);
21 21  
22   -
  22 + /**
  23 + * 发起人取消运行中的流程实例(提供给内部)
  24 + *
  25 + * @param userId 发起人用户编号
  26 + * @param processInstanceId 流程实例编号
  27 + * @param reason 取消原因
  28 + */
  29 + void cancelProcessInstanceByStartUser(Long userId, String processInstanceId, String reason);
23 30  
24 31 }
... ...
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java
1 1 package cn.iocoder.yudao.module.bpm.api.task;
2 2  
3 3 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
  4 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO;
4 5 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
5 6 import jakarta.annotation.Resource;
6 7 import jakarta.validation.Valid;
... ... @@ -25,4 +26,10 @@ public class BpmProcessInstanceApiImpl implements BpmProcessInstanceApi {
25 26 return processInstanceService.createProcessInstance(userId, reqDTO);
26 27 }
27 28  
  29 + @Override
  30 + public void cancelProcessInstanceByStartUser(Long userId, String processInstanceId, String reason) {
  31 + processInstanceService.cancelProcessInstanceByStartUser(userId,
  32 + new BpmProcessInstanceCancelReqVO().setId(processInstanceId).setReason(reason));
  33 + }
  34 +
28 35 }
... ...
yudao-module-hrm/pom.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <parent>
  6 + <groupId>cn.iocoder.boot</groupId>
  7 + <artifactId>yudao</artifactId>
  8 + <version>${revision}</version>
  9 + </parent>
  10 + <modelVersion>4.0.0</modelVersion>
  11 + <artifactId>yudao-module-hrm</artifactId>
  12 + <packaging>jar</packaging>
  13 +
  14 + <name>${project.artifactId}</name>
  15 + <description>
  16 + hrm 包下,人力资源管理(Human Resource Management)
  17 + 例如说:员工、招聘、考勤、薪资、社保、绩效等等
  18 + </description>
  19 +
  20 + <dependencies>
  21 + <dependency>
  22 + <groupId>cn.iocoder.boot</groupId>
  23 + <artifactId>yudao-module-system</artifactId>
  24 + <version>${revision}</version>
  25 + </dependency>
  26 + <dependency>
  27 + <groupId>cn.iocoder.boot</groupId>
  28 + <artifactId>yudao-module-bpm</artifactId>
  29 + <version>${revision}</version>
  30 + </dependency>
  31 +
  32 + <!-- Web 相关 -->
  33 + <dependency>
  34 + <groupId>cn.iocoder.boot</groupId>
  35 + <artifactId>yudao-spring-boot-starter-web</artifactId>
  36 + </dependency>
  37 +
  38 + <dependency>
  39 + <groupId>cn.iocoder.boot</groupId>
  40 + <artifactId>yudao-spring-boot-starter-security</artifactId>
  41 + </dependency>
  42 +
  43 + <!-- DB 相关 -->
  44 + <dependency>
  45 + <groupId>cn.iocoder.boot</groupId>
  46 + <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
  47 + </dependency>
  48 +
  49 + <!-- 工具类相关 -->
  50 + <dependency>
  51 + <groupId>cn.iocoder.boot</groupId>
  52 + <artifactId>yudao-spring-boot-starter-excel</artifactId>
  53 + </dependency>
  54 +
  55 + <!-- 定时任务相关 -->
  56 + <dependency>
  57 + <groupId>cn.iocoder.boot</groupId>
  58 + <artifactId>yudao-spring-boot-starter-job</artifactId>
  59 + </dependency>
  60 +
  61 + <!-- Test 测试相关 -->
  62 + <dependency>
  63 + <groupId>cn.iocoder.boot</groupId>
  64 + <artifactId>yudao-spring-boot-starter-test</artifactId>
  65 + <scope>test</scope>
  66 + </dependency>
  67 + </dependencies>
  68 +
  69 +</project>
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceClockController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  8 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  9 +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
  10 +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.TimeRange;
  11 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  12 +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
  13 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockPageReqVO;
  14 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockRespVO;
  15 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockSaveReqVO;
  16 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockShiftReqVO;
  17 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockShiftRespVO;
  18 +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.record.HrmAttendanceClockDO;
  19 +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.config.HrmAttendanceGroupDO;
  20 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO;
  21 +import cn.iocoder.yudao.module.hrm.service.attendance.record.HrmAttendanceClockService;
  22 +import cn.iocoder.yudao.module.hrm.service.attendance.config.HrmAttendanceGroupService;
  23 +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService;
  24 +import cn.iocoder.yudao.module.system.api.dept.DeptApi;
  25 +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
  26 +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum;
  27 +import io.swagger.v3.oas.annotations.Operation;
  28 +import io.swagger.v3.oas.annotations.Parameter;
  29 +import io.swagger.v3.oas.annotations.tags.Tag;
  30 +import jakarta.annotation.Resource;
  31 +import jakarta.servlet.http.HttpServletResponse;
  32 +import jakarta.validation.Valid;
  33 +import jakarta.validation.constraints.NotEmpty;
  34 +import org.springframework.security.access.prepost.PreAuthorize;
  35 +import org.springframework.validation.annotation.Validated;
  36 +import org.springframework.web.bind.annotation.DeleteMapping;
  37 +import org.springframework.web.bind.annotation.GetMapping;
  38 +import org.springframework.web.bind.annotation.PostMapping;
  39 +import org.springframework.web.bind.annotation.PutMapping;
  40 +import org.springframework.web.bind.annotation.RequestBody;
  41 +import org.springframework.web.bind.annotation.RequestMapping;
  42 +import org.springframework.web.bind.annotation.RequestParam;
  43 +import org.springframework.web.bind.annotation.RestController;
  44 +
  45 +import java.io.IOException;
  46 +import java.time.LocalDate;
  47 +import java.util.Collections;
  48 +import java.util.List;
  49 +import java.util.Map;
  50 +import java.util.Set;
  51 +
  52 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  53 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  54 +
  55 +@Tag(name = "管理后台 - HRM 打卡记录")
  56 +@RestController
  57 +@RequestMapping("/hrm/attendance/clock")
  58 +@Validated
  59 +public class HrmAttendanceClockController {
  60 +
  61 + @Resource
  62 + private HrmAttendanceClockService attendanceClockService;
  63 + @Resource
  64 + private HrmAttendanceGroupService attendanceGroupService;
  65 + @Resource
  66 + private HrmEmployeeService employeeService;
  67 + @Resource
  68 + private DeptApi deptApi;
  69 +
  70 + @PostMapping("/create")
  71 + @Operation(summary = "创建打卡记录")
  72 + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:create')")
  73 + public CommonResult<Long> createAttendanceClock(@Valid @RequestBody HrmAttendanceClockSaveReqVO createReqVO) {
  74 + return success(attendanceClockService.createAttendanceClock(createReqVO));
  75 + }
  76 +
  77 + @PutMapping("/update")
  78 + @Operation(summary = "修改打卡记录")
  79 + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:update')")
  80 + public CommonResult<Boolean> updateAttendanceClock(@Valid @RequestBody HrmAttendanceClockSaveReqVO updateReqVO) {
  81 + attendanceClockService.updateAttendanceClock(updateReqVO);
  82 + return success(true);
  83 + }
  84 +
  85 + @DeleteMapping("/delete")
  86 + @Operation(summary = "删除打卡记录")
  87 + @Parameter(name = "id", description = "打卡记录编号", required = true, example = "1024")
  88 + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:delete')")
  89 + public CommonResult<Boolean> deleteAttendanceClock(@RequestParam("id") Long id) {
  90 + attendanceClockService.deleteAttendanceClock(id);
  91 + return success(true);
  92 + }
  93 +
  94 + @DeleteMapping("/delete-list")
  95 + @Operation(summary = "批量删除打卡记录")
  96 + @Parameter(name = "ids", description = "打卡记录编号列表", required = true, example = "1024,1025")
  97 + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:delete')")
  98 + public CommonResult<Boolean> deleteAttendanceClockList(
  99 + @RequestParam("ids") @NotEmpty(message = "打卡记录不能为空") List<Long> ids) {
  100 + attendanceClockService.deleteAttendanceClockList(ids);
  101 + return success(true);
  102 + }
  103 +
  104 + @GetMapping("/get")
  105 + @Operation(summary = "获得打卡记录")
  106 + @Parameter(name = "id", description = "打卡记录编号", required = true, example = "1024")
  107 + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:query')")
  108 + public CommonResult<HrmAttendanceClockRespVO> getAttendanceClock(@RequestParam("id") Long id) {
  109 + HrmAttendanceClockDO attendanceClock = attendanceClockService.getAttendanceClock(id);
  110 + return success(attendanceClock == null ? null
  111 + : CollUtil.getFirst(buildAttendanceClockRespVOList(Collections.singletonList(attendanceClock))));
  112 + }
  113 +
  114 + @GetMapping("/get-shift")
  115 + @Operation(summary = "获得员工实际班次和允许打卡时间")
  116 + @PreAuthorize("@ss.hasAnyPermissions('hrm:attendance:clock:create', 'hrm:attendance:clock:update')")
  117 + public CommonResult<HrmAttendanceClockShiftRespVO> getAttendanceClockShift(
  118 + @Valid HrmAttendanceClockShiftReqVO reqVO) {
  119 + HrmAttendanceGroupDO.Shift shift = attendanceGroupService.getEmployeeAttendanceShift(
  120 + reqVO.getEmployeeId(), reqVO.getAttendanceTime());
  121 + return success(shift == null ? null : buildAttendanceClockShiftRespVO(reqVO, shift));
  122 + }
  123 +
  124 + @GetMapping("/page")
  125 + @Operation(summary = "获得打卡记录分页")
  126 + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:query')")
  127 + public CommonResult<PageResult<HrmAttendanceClockRespVO>> getAttendanceClockPage(
  128 + @Validated HrmAttendanceClockPageReqVO pageReqVO) {
  129 + PageResult<HrmAttendanceClockDO> pageResult = attendanceClockService.getAttendanceClockPage(pageReqVO);
  130 + return success(new PageResult<>(buildAttendanceClockRespVOList(pageResult.getList()), pageResult.getTotal()));
  131 + }
  132 +
  133 + @GetMapping("/export-excel")
  134 + @Operation(summary = "导出打卡记录")
  135 + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:export')")
  136 + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT)
  137 + public void exportAttendanceClock(@Validated HrmAttendanceClockPageReqVO exportReqVO,
  138 + HttpServletResponse response) throws IOException {
  139 + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
  140 + List<HrmAttendanceClockRespVO> list = buildAttendanceClockRespVOList(
  141 + attendanceClockService.getAttendanceClockList(exportReqVO));
  142 + ExcelUtils.write(response, "打卡记录.xls", "数据", HrmAttendanceClockRespVO.class, list);
  143 + }
  144 +
  145 + // ==================== 拼接 VO ====================
  146 +
  147 + private HrmAttendanceClockShiftRespVO buildAttendanceClockShiftRespVO(
  148 + HrmAttendanceClockShiftReqVO reqVO, HrmAttendanceGroupDO.Shift shift) {
  149 + if (shift.getStartTime() == null || shift.getEndTime() == null
  150 + || shift.getClockInStartTime() == null || shift.getClockInEndTime() == null
  151 + || shift.getClockOutStartTime() == null || shift.getClockOutEndTime() == null) {
  152 + return null;
  153 + }
  154 + LocalDate attendanceDate = reqVO.getAttendanceTime().toLocalDate();
  155 + TimeRange shiftTimeRange = CollUtil.getFirst(LocalDateTimeUtils.buildDailyTimeRanges(
  156 + attendanceDate, attendanceDate, shift.getStartTime(), shift.getEndTime()));
  157 + TimeRange clockInTimeRange = LocalDateTimeUtils.findDailyTimeRange(
  158 + attendanceDate, shift.getClockInStartTime(),
  159 + shift.getClockInEndTime(), shiftTimeRange.getStartTime());
  160 + TimeRange clockOutTimeRange = LocalDateTimeUtils.findDailyTimeRange(
  161 + attendanceDate, shift.getClockOutStartTime(),
  162 + shift.getClockOutEndTime(), shiftTimeRange.getEndTime());
  163 + if (clockInTimeRange == null || clockOutTimeRange == null) {
  164 + return null;
  165 + }
  166 + return new HrmAttendanceClockShiftRespVO()
  167 + .setStartTime(shiftTimeRange.getStartTime()).setEndTime(shiftTimeRange.getEndTime())
  168 + .setClockInStartTime(clockInTimeRange.getStartTime()).setClockInEndTime(clockInTimeRange.getEndTime())
  169 + .setClockOutStartTime(clockOutTimeRange.getStartTime()).setClockOutEndTime(clockOutTimeRange.getEndTime());
  170 + }
  171 +
  172 + private List<HrmAttendanceClockRespVO> buildAttendanceClockRespVOList(List<HrmAttendanceClockDO> clocks) {
  173 + if (CollUtil.isEmpty(clocks)) {
  174 + return Collections.emptyList();
  175 + }
  176 + // 1. 获得员工和部门信息
  177 + Map<Long, HrmEmployeeDO> employeeMap = employeeService.getEmployeeMap(
  178 + convertSet(clocks, HrmAttendanceClockDO::getEmployeeId));
  179 + Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(employeeMap.values(), HrmEmployeeDO::getDeptId));
  180 +
  181 + // 2. 拼接响应
  182 + return BeanUtils.toBean(clocks, HrmAttendanceClockRespVO.class, respVO -> {
  183 + MapUtils.findAndThen(employeeMap, respVO.getEmployeeId(), employee -> {
  184 + respVO.setEmployeeName(employee.getName()).setJobNumber(employee.getJobNumber())
  185 + .setDeptId(employee.getDeptId()).setPostName(employee.getPostName());
  186 + });
  187 + MapUtils.findAndThen(deptMap, respVO.getDeptId(), dept -> respVO.setDeptName(dept.getName()));
  188 + });
  189 + }
  190 +
  191 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceGroupController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.group.HrmAttendanceGroupPageReqVO;
  7 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.group.HrmAttendanceGroupRespVO;
  8 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.group.HrmAttendanceGroupSaveReqVO;
  9 +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.config.HrmAttendanceGroupDO;
  10 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO;
  11 +import cn.iocoder.yudao.module.hrm.service.attendance.config.HrmAttendanceGroupService;
  12 +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService;
  13 +import cn.iocoder.yudao.module.system.api.dept.DeptApi;
  14 +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
  15 +import io.swagger.v3.oas.annotations.Operation;
  16 +import io.swagger.v3.oas.annotations.Parameter;
  17 +import io.swagger.v3.oas.annotations.tags.Tag;
  18 +import jakarta.annotation.Resource;
  19 +import jakarta.validation.Valid;
  20 +import org.springframework.security.access.prepost.PreAuthorize;
  21 +import org.springframework.validation.annotation.Validated;
  22 +import org.springframework.web.bind.annotation.DeleteMapping;
  23 +import org.springframework.web.bind.annotation.GetMapping;
  24 +import org.springframework.web.bind.annotation.PostMapping;
  25 +import org.springframework.web.bind.annotation.PutMapping;
  26 +import org.springframework.web.bind.annotation.RequestBody;
  27 +import org.springframework.web.bind.annotation.RequestMapping;
  28 +import org.springframework.web.bind.annotation.RequestParam;
  29 +import org.springframework.web.bind.annotation.RestController;
  30 +
  31 +import java.util.Collection;
  32 +import java.util.Collections;
  33 +import java.util.List;
  34 +import java.util.Map;
  35 +import java.util.Set;
  36 +
  37 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  38 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  39 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
  40 +
  41 +@Tag(name = "管理后台 - HRM 考勤组")
  42 +@RestController
  43 +@RequestMapping("/hrm/attendance/group")
  44 +@Validated
  45 +public class HrmAttendanceGroupController {
  46 +
  47 + @Resource
  48 + private HrmAttendanceGroupService attendanceGroupService;
  49 + @Resource
  50 + private HrmEmployeeService employeeService;
  51 + @Resource
  52 + private DeptApi deptApi;
  53 +
  54 + @PostMapping("/create")
  55 + @Operation(summary = "创建考勤组")
  56 + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:create')")
  57 + public CommonResult<Long> createAttendanceGroup(@Valid @RequestBody HrmAttendanceGroupSaveReqVO createReqVO) {
  58 + return success(attendanceGroupService.createAttendanceGroup(createReqVO));
  59 + }
  60 +
  61 + @PutMapping("/update")
  62 + @Operation(summary = "修改考勤组")
  63 + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:update')")
  64 + public CommonResult<Boolean> updateAttendanceGroup(@Valid @RequestBody HrmAttendanceGroupSaveReqVO updateReqVO) {
  65 + attendanceGroupService.updateAttendanceGroup(updateReqVO);
  66 + return success(true);
  67 + }
  68 +
  69 + @DeleteMapping("/delete")
  70 + @Operation(summary = "删除考勤组")
  71 + @Parameter(name = "id", description = "考勤组编号", required = true, example = "1024")
  72 + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:delete')")
  73 + public CommonResult<Boolean> deleteAttendanceGroup(@RequestParam("id") Long id) {
  74 + attendanceGroupService.deleteAttendanceGroup(id);
  75 + return success(true);
  76 + }
  77 +
  78 + @GetMapping("/get")
  79 + @Operation(summary = "获得考勤组")
  80 + @Parameter(name = "id", description = "考勤组编号", required = true, example = "1024")
  81 + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:query')")
  82 + public CommonResult<HrmAttendanceGroupRespVO> getAttendanceGroup(@RequestParam("id") Long id) {
  83 + return success(buildAttendanceGroupRespVO(attendanceGroupService.getAttendanceGroup(id)));
  84 + }
  85 +
  86 + @GetMapping("/my")
  87 + @Operation(summary = "获得员工所在考勤组")
  88 + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024")
  89 + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:query')")
  90 + public CommonResult<HrmAttendanceGroupRespVO> getMyAttendanceGroup(@RequestParam("employeeId") Long employeeId) {
  91 + return success(buildAttendanceGroupRespVO(attendanceGroupService.getMyAttendanceGroup(employeeId)));
  92 + }
  93 +
  94 + @GetMapping("/page")
  95 + @Operation(summary = "获得考勤组分页")
  96 + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:query')")
  97 + public CommonResult<PageResult<HrmAttendanceGroupRespVO>> getAttendanceGroupPage(
  98 + @Validated HrmAttendanceGroupPageReqVO pageReqVO) {
  99 + PageResult<HrmAttendanceGroupDO> pageResult = attendanceGroupService.getAttendanceGroupPage(pageReqVO);
  100 + return success(new PageResult<>(
  101 + buildAttendanceGroupRespVOList(pageResult.getList()), pageResult.getTotal()));
  102 + }
  103 +
  104 + @GetMapping("/simple-list")
  105 + @Operation(summary = "获得考勤组精简列表")
  106 + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:query')")
  107 + public CommonResult<List<HrmAttendanceGroupRespVO>> getAttendanceGroupSimpleList() {
  108 + return success(convertList(attendanceGroupService.getAttendanceGroupList(),
  109 + group -> new HrmAttendanceGroupRespVO().setId(group.getId()).setName(group.getName())));
  110 + }
  111 +
  112 + // ==================== 拼接 VO ====================
  113 +
  114 + private HrmAttendanceGroupRespVO buildAttendanceGroupRespVO(HrmAttendanceGroupDO group) {
  115 + if (group == null) {
  116 + return null;
  117 + }
  118 + return buildAttendanceGroupRespVOList(Collections.singletonList(group)).get(0);
  119 + }
  120 +
  121 + private List<HrmAttendanceGroupRespVO> buildAttendanceGroupRespVOList(List<HrmAttendanceGroupDO> list) {
  122 + Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
  123 + convertSetByFlatMap(list, HrmAttendanceGroupDO::getDeptIds, Collection::stream));
  124 + Map<Long, HrmEmployeeDO> employeeMap = employeeService.getEmployeeMap(
  125 + convertSetByFlatMap(list, HrmAttendanceGroupDO::getEmployeeIds, Collection::stream));
  126 + return convertList(list, group -> BeanUtils.toBean(group, HrmAttendanceGroupRespVO.class)
  127 + .setDeptNames(convertList(group.getDeptIds(),
  128 + deptId -> deptMap.containsKey(deptId) ? deptMap.get(deptId).getName() : null))
  129 + .setEmployeeNames(convertList(group.getEmployeeIds(),
  130 + employeeId -> employeeMap.containsKey(employeeId)
  131 + ? employeeMap.get(employeeId).getName() : null)));
  132 + }
  133 +
  134 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceHolidayController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.holiday.HrmAttendanceHolidayPageReqVO;
  7 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.holiday.HrmAttendanceHolidayRespVO;
  8 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.holiday.HrmAttendanceHolidaySaveReqVO;
  9 +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.config.HrmAttendanceHolidayDO;
  10 +import cn.iocoder.yudao.module.hrm.service.attendance.config.HrmAttendanceHolidayService;
  11 +import io.swagger.v3.oas.annotations.Operation;
  12 +import io.swagger.v3.oas.annotations.Parameter;
  13 +import io.swagger.v3.oas.annotations.tags.Tag;
  14 +import jakarta.annotation.Resource;
  15 +import jakarta.validation.Valid;
  16 +import org.springframework.format.annotation.DateTimeFormat;
  17 +import org.springframework.security.access.prepost.PreAuthorize;
  18 +import org.springframework.validation.annotation.Validated;
  19 +import org.springframework.web.bind.annotation.DeleteMapping;
  20 +import org.springframework.web.bind.annotation.GetMapping;
  21 +import org.springframework.web.bind.annotation.PostMapping;
  22 +import org.springframework.web.bind.annotation.PutMapping;
  23 +import org.springframework.web.bind.annotation.RequestBody;
  24 +import org.springframework.web.bind.annotation.RequestMapping;
  25 +import org.springframework.web.bind.annotation.RequestParam;
  26 +import org.springframework.web.bind.annotation.RestController;
  27 +
  28 +import java.time.LocalDateTime;
  29 +
  30 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  31 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  32 +
  33 +@Tag(name = "管理后台 - HRM 考勤节假日")
  34 +@RestController
  35 +@RequestMapping("/hrm/attendance/holiday")
  36 +@Validated
  37 +public class HrmAttendanceHolidayController {
  38 +
  39 + @Resource
  40 + private HrmAttendanceHolidayService attendanceHolidayService;
  41 +
  42 + @PostMapping("/create")
  43 + @Operation(summary = "创建考勤节假日")
  44 + @PreAuthorize("@ss.hasPermission('hrm:attendance:holiday:create')")
  45 + public CommonResult<Long> createAttendanceHoliday(@Valid @RequestBody HrmAttendanceHolidaySaveReqVO createReqVO) {
  46 + return success(attendanceHolidayService.createAttendanceHoliday(createReqVO));
  47 + }
  48 +
  49 + @PutMapping("/update")
  50 + @Operation(summary = "修改考勤节假日")
  51 + @PreAuthorize("@ss.hasPermission('hrm:attendance:holiday:update')")
  52 + public CommonResult<Boolean> updateAttendanceHoliday(@Valid @RequestBody HrmAttendanceHolidaySaveReqVO updateReqVO) {
  53 + attendanceHolidayService.updateAttendanceHoliday(updateReqVO);
  54 + return success(true);
  55 + }
  56 +
  57 + @DeleteMapping("/delete")
  58 + @Operation(summary = "删除考勤节假日")
  59 + @Parameter(name = "id", description = "考勤节假日编号", required = true, example = "1024")
  60 + @PreAuthorize("@ss.hasPermission('hrm:attendance:holiday:delete')")
  61 + public CommonResult<Boolean> deleteAttendanceHoliday(@RequestParam("id") Long id) {
  62 + attendanceHolidayService.deleteAttendanceHoliday(id);
  63 + return success(true);
  64 + }
  65 +
  66 + @GetMapping("/get")
  67 + @Operation(summary = "获得考勤节假日")
  68 + @Parameter(name = "id", description = "考勤节假日编号", required = true, example = "1024")
  69 + @PreAuthorize("@ss.hasPermission('hrm:attendance:holiday:query')")
  70 + public CommonResult<HrmAttendanceHolidayRespVO> getAttendanceHoliday(@RequestParam("id") Long id) {
  71 + HrmAttendanceHolidayDO holiday = attendanceHolidayService.getAttendanceHoliday(id);
  72 + return success(BeanUtils.toBean(holiday, HrmAttendanceHolidayRespVO.class));
  73 + }
  74 +
  75 + @GetMapping("/check")
  76 + @Operation(summary = "检查日期是否为考勤节假日")
  77 + @Parameter(name = "day", description = "日期", required = true)
  78 + @PreAuthorize("@ss.hasPermission('hrm:attendance:holiday:query')")
  79 + public CommonResult<HrmAttendanceHolidayRespVO> checkAttendanceHoliday(
  80 + @RequestParam("day")
  81 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) LocalDateTime day) {
  82 + HrmAttendanceHolidayDO holiday = attendanceHolidayService.checkAttendanceHoliday(day);
  83 + return success(BeanUtils.toBean(holiday, HrmAttendanceHolidayRespVO.class));
  84 + }
  85 +
  86 + @GetMapping("/page")
  87 + @Operation(summary = "获得考勤节假日分页")
  88 + @PreAuthorize("@ss.hasPermission('hrm:attendance:holiday:query')")
  89 + public CommonResult<PageResult<HrmAttendanceHolidayRespVO>> getAttendanceHolidayPage(
  90 + @Validated HrmAttendanceHolidayPageReqVO pageReqVO) {
  91 + PageResult<HrmAttendanceHolidayDO> pageResult = attendanceHolidayService.getAttendanceHolidayPage(pageReqVO);
  92 + return success(BeanUtils.toBean(pageResult, HrmAttendanceHolidayRespVO.class));
  93 + }
  94 +
  95 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceLeaveController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance;
  2 +
  3 +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  8 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  9 +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
  10 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave.HrmAttendanceLeavePageReqVO;
  11 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave.HrmAttendanceLeaveRespVO;
  12 +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.record.HrmAttendanceLeaveDO;
  13 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO;
  14 +import cn.iocoder.yudao.module.hrm.service.attendance.record.HrmAttendanceLeaveService;
  15 +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService;
  16 +import cn.iocoder.yudao.module.system.api.dept.DeptApi;
  17 +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
  18 +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum;
  19 +import io.swagger.v3.oas.annotations.Operation;
  20 +import io.swagger.v3.oas.annotations.Parameter;
  21 +import io.swagger.v3.oas.annotations.tags.Tag;
  22 +import jakarta.annotation.Resource;
  23 +import jakarta.servlet.http.HttpServletResponse;
  24 +import org.springframework.security.access.prepost.PreAuthorize;
  25 +import org.springframework.validation.annotation.Validated;
  26 +import org.springframework.web.bind.annotation.GetMapping;
  27 +import org.springframework.web.bind.annotation.RequestMapping;
  28 +import org.springframework.web.bind.annotation.RequestParam;
  29 +import org.springframework.web.bind.annotation.RestController;
  30 +
  31 +import java.io.IOException;
  32 +import java.util.Collections;
  33 +import java.util.List;
  34 +import java.util.Map;
  35 +
  36 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  37 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  38 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  39 +
  40 +@Tag(name = "管理后台 - HRM 请假")
  41 +@RestController
  42 +@RequestMapping("/hrm/attendance/leave")
  43 +@Validated
  44 +public class HrmAttendanceLeaveController {
  45 +
  46 + @Resource
  47 + private HrmAttendanceLeaveService attendanceLeaveService;
  48 + @Resource
  49 + private HrmEmployeeService employeeService;
  50 +
  51 + @Resource
  52 + private DeptApi deptApi;
  53 +
  54 + @GetMapping("/get")
  55 + @Operation(summary = "获得请假")
  56 + @Parameter(name = "id", description = "请假记录编号", required = true, example = "1024")
  57 + @PreAuthorize("@ss.hasAnyPermissions('hrm:attendance:leave:query', "
  58 + + "'bpm:process-instance:query', 'bpm:task:query')")
  59 + public CommonResult<HrmAttendanceLeaveRespVO> getAttendanceLeave(@RequestParam("id") Long id) {
  60 + HrmAttendanceLeaveDO list = attendanceLeaveService.getAttendanceLeave(id);
  61 + return success(buildAttendanceLeaveRespVO(list));
  62 + }
  63 +
  64 + @GetMapping("/page")
  65 + @Operation(summary = "获得请假分页")
  66 + @PreAuthorize("@ss.hasPermission('hrm:attendance:leave:query')")
  67 + public CommonResult<PageResult<HrmAttendanceLeaveRespVO>> getAttendanceLeavePage(
  68 + @Validated HrmAttendanceLeavePageReqVO pageReqVO) {
  69 + PageResult<HrmAttendanceLeaveDO> pageResult = attendanceLeaveService.getAttendanceLeavePage(pageReqVO);
  70 + return success(new PageResult<>(buildAttendanceLeaveRespVOList(pageResult.getList()), pageResult.getTotal()));
  71 + }
  72 +
  73 + @GetMapping("/export-excel")
  74 + @Operation(summary = "导出请假")
  75 + @PreAuthorize("@ss.hasPermission('hrm:attendance:leave:export')")
  76 + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT)
  77 + public void exportAttendanceLeave(@Validated HrmAttendanceLeavePageReqVO exportReqVO,
  78 + HttpServletResponse response) throws IOException {
  79 + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
  80 + List<HrmAttendanceLeaveRespVO> list = buildAttendanceLeaveRespVOList(
  81 + attendanceLeaveService.getAttendanceLeaveList(exportReqVO));
  82 + ExcelUtils.write(response, "请假记录.xls", "数据", HrmAttendanceLeaveRespVO.class, list);
  83 + }
  84 +
  85 + // ==================== 拼接 VO ====================
  86 +
  87 + private HrmAttendanceLeaveRespVO buildAttendanceLeaveRespVO(HrmAttendanceLeaveDO attendanceLeave) {
  88 + if (attendanceLeave == null) {
  89 + return null;
  90 + }
  91 + return buildAttendanceLeaveRespVOList(Collections.singletonList(attendanceLeave)).get(0);
  92 + }
  93 +
  94 + private List<HrmAttendanceLeaveRespVO> buildAttendanceLeaveRespVOList(List<HrmAttendanceLeaveDO> list) {
  95 + Map<Long, HrmEmployeeDO> employeeMap = employeeService.getEmployeeMap(
  96 + convertSet(list, HrmAttendanceLeaveDO::getEmployeeId));
  97 + Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(employeeMap.values(), HrmEmployeeDO::getDeptId));
  98 + return convertList(list, attendanceLeave -> {
  99 + HrmAttendanceLeaveRespVO respVO = BeanUtils.toBean(attendanceLeave, HrmAttendanceLeaveRespVO.class);
  100 + HrmEmployeeDO employee = employeeMap.get(attendanceLeave.getEmployeeId());
  101 + if (employee == null) {
  102 + return respVO;
  103 + }
  104 + respVO.setEmployeeName(employee.getName()).setJobNumber(employee.getJobNumber())
  105 + .setDeptId(employee.getDeptId()).setPostName(employee.getPostName());
  106 + MapUtils.findAndThen(deptMap, employee.getDeptId(), dept -> respVO.setDeptName(dept.getName()));
  107 + return respVO;
  108 + });
  109 + }
  110 +
  111 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceStatisticsController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
  7 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  8 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  9 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  10 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  11 +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
  12 +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
  13 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockRespVO;
  14 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceDailyDetailReqVO;
  15 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceDailyDetailRespVO;
  16 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceDailyOverviewRespVO;
  17 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceMonthDailyOverviewRespVO;
  18 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceMonthDetailRespVO;
  19 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceMonthRecordPageReqVO;
  20 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceMonthRecordRespVO;
  21 +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants;
  22 +import cn.iocoder.yudao.module.hrm.service.attendance.statistics.HrmAttendanceStatisticsService;
  23 +import cn.iocoder.yudao.module.system.api.dept.DeptApi;
  24 +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
  25 +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum;
  26 +import io.swagger.v3.oas.annotations.Operation;
  27 +import io.swagger.v3.oas.annotations.Parameter;
  28 +import io.swagger.v3.oas.annotations.Parameters;
  29 +import io.swagger.v3.oas.annotations.tags.Tag;
  30 +import jakarta.annotation.Resource;
  31 +import jakarta.servlet.http.HttpServletResponse;
  32 +import jakarta.validation.Valid;
  33 +import org.springframework.security.access.prepost.PreAuthorize;
  34 +import org.springframework.validation.annotation.Validated;
  35 +import org.springframework.web.bind.annotation.GetMapping;
  36 +import org.springframework.web.bind.annotation.RequestMapping;
  37 +import org.springframework.web.bind.annotation.RequestParam;
  38 +import org.springframework.web.bind.annotation.RestController;
  39 +
  40 +import java.io.IOException;
  41 +import java.time.YearMonth;
  42 +import java.util.ArrayList;
  43 +import java.util.Arrays;
  44 +import java.util.Collections;
  45 +import java.util.Comparator;
  46 +import java.util.List;
  47 +import java.util.Map;
  48 +import java.util.stream.Collectors;
  49 +
  50 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  51 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  52 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  53 +
  54 +@Tag(name = "管理后台 - HRM 考勤统计")
  55 +@RestController
  56 +@RequestMapping("/hrm/attendance/statistics")
  57 +@Validated
  58 +public class HrmAttendanceStatisticsController {
  59 +
  60 + @Resource
  61 + private HrmAttendanceStatisticsService attendanceStatisticsService;
  62 +
  63 + @Resource
  64 + private DeptApi deptApi;
  65 +
  66 + @GetMapping("/daily-detail")
  67 + @Operation(summary = "查询员工每日打卡明细")
  68 + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:query')")
  69 + public CommonResult<HrmAttendanceDailyDetailRespVO> getAttendanceDailyDetail(
  70 + @Valid HrmAttendanceDailyDetailReqVO reqVO) {
  71 + return success(attendanceStatisticsService.getAttendanceDailyDetail(reqVO));
  72 + }
  73 +
  74 + @GetMapping("/month-record-page")
  75 + @Operation(summary = "查询员工月度考勤汇总")
  76 + @PreAuthorize("@ss.hasPermission('hrm:attendance:statistics:query')")
  77 + public CommonResult<PageResult<HrmAttendanceMonthRecordRespVO>> getAttendanceMonthRecordPage(
  78 + @Valid HrmAttendanceMonthRecordPageReqVO reqVO) {
  79 + PageResult<HrmAttendanceMonthRecordRespVO> pageResult =
  80 + attendanceStatisticsService.getAttendanceMonthRecordPage(reqVO);
  81 + fillAttendanceMonthRecordDeptName(pageResult.getList());
  82 + return success(pageResult);
  83 + }
  84 +
  85 + @GetMapping("/month-daily-page")
  86 + @Operation(summary = "查询员工月度打卡概况")
  87 + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:query')")
  88 + public CommonResult<PageResult<HrmAttendanceMonthDailyOverviewRespVO>> getAttendanceMonthDailyOverviewPage(
  89 + @Valid HrmAttendanceMonthRecordPageReqVO reqVO) {
  90 + PageResult<HrmAttendanceMonthDailyOverviewRespVO> pageResult =
  91 + attendanceStatisticsService.getAttendanceMonthDailyOverviewPage(reqVO);
  92 + fillAttendanceMonthDailyOverview(pageResult.getList());
  93 + return success(pageResult);
  94 + }
  95 +
  96 + @GetMapping("/month-record-export-excel")
  97 + @Operation(summary = "导出员工月度考勤汇总")
  98 + @PreAuthorize("@ss.hasPermission('hrm:attendance:statistics:export')")
  99 + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT)
  100 + public void exportAttendanceMonthRecord(@Validated HrmAttendanceMonthRecordPageReqVO exportReqVO,
  101 + HttpServletResponse response) throws IOException {
  102 + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
  103 + List<HrmAttendanceMonthRecordRespVO> list =
  104 + attendanceStatisticsService.getAttendanceMonthRecordList(exportReqVO);
  105 + fillAttendanceMonthRecordDeptName(list);
  106 + ExcelUtils.write(response, "员工月度考勤汇总.xls", "数据", HrmAttendanceMonthRecordRespVO.class, list);
  107 + }
  108 +
  109 + @GetMapping("/month-daily-export-excel")
  110 + @Operation(summary = "导出员工月度打卡概况")
  111 + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:export')")
  112 + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT)
  113 + public void exportAttendanceMonthDailyOverview(@Validated HrmAttendanceMonthRecordPageReqVO exportReqVO,
  114 + HttpServletResponse response) throws IOException {
  115 + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
  116 + List<HrmAttendanceMonthDailyOverviewRespVO> list =
  117 + attendanceStatisticsService.getAttendanceMonthDailyOverviewList(exportReqVO);
  118 + fillAttendanceMonthDailyOverview(list);
  119 + writeAttendanceMonthDailyOverview(response,
  120 + YearMonth.of(exportReqVO.getYear(), exportReqVO.getMonth()), list);
  121 + }
  122 +
  123 + @GetMapping("/month-detail")
  124 + @Operation(summary = "查询员工月度考勤详情")
  125 + @Parameters({
  126 + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024"),
  127 + @Parameter(name = "year", description = "年份", required = true, example = "2026"),
  128 + @Parameter(name = "month", description = "月份", required = true, example = "8")
  129 + })
  130 + @PreAuthorize("@ss.hasPermission('hrm:attendance:statistics:query')")
  131 + public CommonResult<HrmAttendanceMonthDetailRespVO> getAttendanceMonthDetail(
  132 + @RequestParam("employeeId") Long employeeId,
  133 + @RequestParam("year") Integer year,
  134 + @RequestParam("month") Integer month) {
  135 + HrmAttendanceMonthDetailRespVO detail =
  136 + attendanceStatisticsService.getAttendanceMonthDetail(employeeId, year, month);
  137 + fillAttendanceMonthRecordDeptName(Collections.singletonList(detail.getSummary()));
  138 + return success(detail);
  139 + }
  140 +
  141 + private void writeAttendanceMonthDailyOverview(HttpServletResponse response, YearMonth month,
  142 + List<HrmAttendanceMonthDailyOverviewRespVO> list)
  143 + throws IOException {
  144 + List<List<String>> head = convertList(
  145 + Arrays.asList("员工编号", "员工姓名", "工号", "部门", "岗位"), Collections::singletonList);
  146 + for (int day = 1; day <= month.lengthOfMonth(); day++) {
  147 + head.add(Collections.singletonList(month.atDay(day).toString()));
  148 + }
  149 + ExcelUtils.write(response, "员工月度打卡概况.xls", "打卡概况", head,
  150 + buildAttendanceMonthDailyOverviewData(month, list));
  151 + }
  152 +
  153 + private List<List<Object>> buildAttendanceMonthDailyOverviewData(
  154 + YearMonth month, List<HrmAttendanceMonthDailyOverviewRespVO> list) {
  155 + List<List<Object>> result = new ArrayList<>(list.size());
  156 + for (HrmAttendanceMonthDailyOverviewRespVO row : list) {
  157 + List<Object> data = new ArrayList<>(Arrays.asList(row.getEmployeeId(), row.getEmployeeName(),
  158 + row.getJobNumber(), row.getDeptName(), row.getPostName()));
  159 + for (int day = 1; day <= month.lengthOfMonth(); day++) {
  160 + data.add(formatDailyClockOverview(row.getDailyClockMap().get(month.atDay(day))));
  161 + }
  162 + result.add(data);
  163 + }
  164 + return result;
  165 + }
  166 +
  167 + private List<HrmAttendanceDailyOverviewRespVO.OverviewItem> buildDailyClockOverviewList(
  168 + HrmAttendanceDailyOverviewRespVO dailyOverview) {
  169 + if (CollUtil.isEmpty(dailyOverview.getClocks())) {
  170 + return Collections.singletonList(new HrmAttendanceDailyOverviewRespVO.OverviewItem()
  171 + .setText(StrUtil.emptyIfNull(dailyOverview.getAttendanceResult())));
  172 + }
  173 + // 按打卡时间升序展示每日打卡概况
  174 + dailyOverview.getClocks().sort(Comparator.comparing(HrmAttendanceClockRespVO::getClockTime));
  175 + return convertList(dailyOverview.getClocks(),
  176 + clock -> new HrmAttendanceDailyOverviewRespVO.OverviewItem()
  177 + .setType(StrUtil.removeSuffix(StrUtil.emptyIfNull(DictFrameworkUtils.parseDictDataLabel(
  178 + DictTypeConstants.HRM_ATTENDANCE_CLOCK_TYPE, clock.getType())), "打卡"))
  179 + .setTime(clock.getClockTime() == null ? "-" : clock.getClockTime().toLocalTime().toString())
  180 + .setStatus(StrUtil.emptyIfNull(DictFrameworkUtils.parseDictDataLabel(
  181 + DictTypeConstants.HRM_ATTENDANCE_CLOCK_STATUS, clock.getStatus()))));
  182 + }
  183 +
  184 + private String formatDailyClockOverview(HrmAttendanceDailyOverviewRespVO dailyOverview) {
  185 + if (dailyOverview == null) {
  186 + return "";
  187 + }
  188 + return dailyOverview.getOverviews().stream()
  189 + .map(item -> StrUtil.isNotEmpty(item.getText()) ? item.getText()
  190 + : String.format("%s %s %s", item.getType(), item.getTime(), item.getStatus()))
  191 + .collect(Collectors.joining(";"));
  192 + }
  193 +
  194 + // ==================== 拼接 VO ====================
  195 +
  196 + private void fillAttendanceMonthDailyOverview(List<HrmAttendanceMonthDailyOverviewRespVO> list) {
  197 + Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
  198 + convertSet(list, HrmAttendanceMonthDailyOverviewRespVO::getDeptId));
  199 + list.forEach(vo -> {
  200 + MapUtils.findAndThen(deptMap, vo.getDeptId(), dept -> vo.setDeptName(dept.getName()));
  201 + MapUtil.emptyIfNull(vo.getDailyClockMap()).values().forEach(
  202 + overview -> overview.setOverviews(buildDailyClockOverviewList(overview)));
  203 + });
  204 + }
  205 +
  206 + private void fillAttendanceMonthRecordDeptName(List<HrmAttendanceMonthRecordRespVO> list) {
  207 + Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
  208 + convertSet(list, HrmAttendanceMonthRecordRespVO::getDeptId));
  209 + list.forEach(vo -> MapUtils.findAndThen(deptMap, vo.getDeptId(), dept -> vo.setDeptName(dept.getName())));
  210 + }
  211 +
  212 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  5 +import cn.iocoder.yudao.module.hrm.enums.attendance.record.HrmAttendanceClockSourceEnum;
  6 +import cn.iocoder.yudao.module.hrm.enums.attendance.record.HrmAttendanceClockStatusEnum;
  7 +import cn.iocoder.yudao.module.hrm.enums.attendance.record.HrmAttendanceClockTypeEnum;
  8 +import io.swagger.v3.oas.annotations.media.Schema;
  9 +import lombok.Data;
  10 +import lombok.EqualsAndHashCode;
  11 +import lombok.ToString;
  12 +import org.springframework.format.annotation.DateTimeFormat;
  13 +
  14 +import java.time.LocalDateTime;
  15 +import java.util.Collection;
  16 +import java.util.List;
  17 +
  18 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  19 +
  20 +@Schema(description = "管理后台 - HRM 打卡记录分页 Request VO")
  21 +@Data
  22 +@EqualsAndHashCode(callSuper = true)
  23 +@ToString(callSuper = true)
  24 +public class HrmAttendanceClockPageReqVO extends PageParam {
  25 +
  26 + @Schema(description = "员工姓名或工号,模糊匹配", example = "张三")
  27 + private String search;
  28 +
  29 + @Schema(description = "员工编号", example = "1024")
  30 + private Long employeeId;
  31 +
  32 + @Schema(description = "部门编号数组", example = "100,101")
  33 + private List<Long> deptIds;
  34 +
  35 + @Schema(hidden = true)
  36 + private Collection<Long> employeeIds;
  37 +
  38 + @Schema(description = "考勤打卡类型", example = "1")
  39 + @InEnum(value = HrmAttendanceClockTypeEnum.class, message = "打卡类型必须是 {value}")
  40 + private Integer type;
  41 +
  42 + @Schema(description = "考勤打卡状态", example = "1")
  43 + @InEnum(value = HrmAttendanceClockStatusEnum.class, message = "打卡状态必须是 {value}")
  44 + private Integer status;
  45 +
  46 + @Schema(description = "来源类型数组", example = "[1, 2]")
  47 + @InEnum(value = HrmAttendanceClockSourceEnum.class, message = "打卡来源必须是 {value}")
  48 + private List<Integer> sourceTypes;
  49 +
  50 + @Schema(description = "打卡地点,模糊匹配", example = "文三路")
  51 + private String address;
  52 +
  53 + @Schema(description = "上班日期范围")
  54 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  55 + private LocalDateTime[] attendanceTime;
  56 +
  57 + @Schema(description = "实际打卡时间范围")
  58 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  59 + private LocalDateTime[] clockTime;
  60 +
  61 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock;
  2 +
  3 +import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
  4 +import cn.idev.excel.annotation.ExcelProperty;
  5 +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
  6 +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
  7 +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants;
  8 +import io.swagger.v3.oas.annotations.media.Schema;
  9 +import lombok.Data;
  10 +
  11 +import java.math.BigDecimal;
  12 +import java.time.LocalDateTime;
  13 +
  14 +@Schema(description = "管理后台 - HRM 打卡记录 Response VO")
  15 +@Data
  16 +@ExcelIgnoreUnannotated
  17 +public class HrmAttendanceClockRespVO {
  18 +
  19 + @Schema(description = "打卡记录编号", example = "1024")
  20 + @ExcelProperty(value = "记录编号", index = 0)
  21 + private Long id;
  22 +
  23 + @Schema(description = "打卡员工编号", example = "1001")
  24 + @ExcelProperty(value = "员工编号", index = 1)
  25 + private Long employeeId;
  26 +
  27 + @Schema(description = "员工姓名", example = "芋道")
  28 + @ExcelProperty(value = "员工姓名", index = 2)
  29 + private String employeeName;
  30 +
  31 + @Schema(description = "工号", example = "HRM001")
  32 + @ExcelProperty(value = "工号", index = 3)
  33 + private String jobNumber;
  34 +
  35 + @Schema(description = "部门编号", example = "100")
  36 + private Long deptId;
  37 +
  38 + @Schema(description = "部门名称", example = "研发部")
  39 + @ExcelProperty(value = "部门", index = 4)
  40 + private String deptName;
  41 +
  42 + @Schema(description = "职位名称", example = "Java 工程师")
  43 + @ExcelProperty(value = "岗位", index = 5)
  44 + private String postName;
  45 +
  46 + @Schema(description = "打卡时间")
  47 + @ExcelProperty(value = "打卡时间", index = 8)
  48 + private LocalDateTime clockTime;
  49 +
  50 + @Schema(description = "打卡类型", example = "1")
  51 + @ExcelProperty(value = "打卡类型", index = 6, converter = DictConvert.class)
  52 + @DictFormat(DictTypeConstants.HRM_ATTENDANCE_CLOCK_TYPE)
  53 + private Integer type;
  54 +
  55 + @Schema(description = "应打卡时间")
  56 + @ExcelProperty(value = "应打卡时间", index = 7)
  57 + private LocalDateTime attendanceTime;
  58 +
  59 + @Schema(description = "打卡来源")
  60 + @ExcelProperty(value = "来源", index = 10, converter = DictConvert.class)
  61 + @DictFormat(DictTypeConstants.HRM_ATTENDANCE_CLOCK_SOURCE)
  62 + private Integer sourceType;
  63 +
  64 + @Schema(description = "打卡状态")
  65 + @ExcelProperty(value = "打卡状态", index = 9, converter = DictConvert.class)
  66 + @DictFormat(DictTypeConstants.HRM_ATTENDANCE_CLOCK_STATUS)
  67 + private Integer status;
  68 +
  69 + @Schema(description = "打卡阶段")
  70 + private Integer stage;
  71 +
  72 + @Schema(description = "打卡地址", example = "总部大楼")
  73 + @ExcelProperty(value = "地点", index = 11)
  74 + private String address;
  75 +
  76 + @Schema(description = "经度", example = "121.473701")
  77 + private BigDecimal longitude;
  78 +
  79 + @Schema(description = "纬度", example = "31.230416")
  80 + private BigDecimal latitude;
  81 +
  82 + @Schema(description = "WiFi 名称", example = "office_wifi")
  83 + private String ssid;
  84 +
  85 + @Schema(description = "WiFi MAC 地址", example = "00:11:22:33:44:55")
  86 + private String mac;
  87 +
  88 + @Schema(description = "备注", example = "手工补卡")
  89 + @ExcelProperty(value = "备注", index = 12)
  90 + private String remark;
  91 +
  92 + @Schema(description = "创建时间")
  93 + private LocalDateTime createTime;
  94 +
  95 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.hrm.enums.attendance.record.HrmAttendanceClockTypeEnum;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import jakarta.validation.constraints.DecimalMax;
  7 +import jakarta.validation.constraints.DecimalMin;
  8 +import jakarta.validation.constraints.NotNull;
  9 +import jakarta.validation.constraints.Size;
  10 +import lombok.Data;
  11 +
  12 +import java.math.BigDecimal;
  13 +import java.time.LocalDateTime;
  14 +
  15 +@Schema(description = "管理后台 - HRM 打卡记录保存 Request VO")
  16 +@Data
  17 +public class HrmAttendanceClockSaveReqVO {
  18 +
  19 + @Schema(description = "打卡记录编号", example = "1024")
  20 + private Long id;
  21 +
  22 + @Schema(description = "打卡员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
  23 + @NotNull(message = "打卡员工不能为空")
  24 + private Long employeeId;
  25 +
  26 + @Schema(description = "打卡时间", requiredMode = Schema.RequiredMode.REQUIRED)
  27 + @NotNull(message = "打卡时间不能为空")
  28 + private LocalDateTime clockTime;
  29 +
  30 + @Schema(description = "打卡类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  31 + @NotNull(message = "打卡类型不能为空")
  32 + @InEnum(value = HrmAttendanceClockTypeEnum.class, message = "打卡类型必须是 {value}")
  33 + private Integer type;
  34 +
  35 + @Schema(description = "应打卡时间", requiredMode = Schema.RequiredMode.REQUIRED)
  36 + @NotNull(message = "应打卡时间不能为空")
  37 + private LocalDateTime attendanceTime;
  38 +
  39 + @Schema(description = "打卡地址", example = "总部大楼")
  40 + @Size(max = 255, message = "打卡地址不能超过 255 个字符")
  41 + private String address;
  42 +
  43 + @Schema(description = "经度", example = "121.473701")
  44 + @DecimalMin(value = "-180", message = "经度必须在 -180 到 180 之间")
  45 + @DecimalMax(value = "180", message = "经度必须在 -180 到 180 之间")
  46 + private BigDecimal longitude;
  47 +
  48 + @Schema(description = "纬度", example = "31.230416")
  49 + @DecimalMin(value = "-90", message = "纬度必须在 -90 到 90 之间")
  50 + @DecimalMax(value = "90", message = "纬度必须在 -90 到 90 之间")
  51 + private BigDecimal latitude;
  52 +
  53 + @Schema(description = "WiFi 名称", example = "office_wifi")
  54 + @Size(max = 50, message = "WiFi 名称不能超过 50 个字符")
  55 + private String ssid;
  56 +
  57 + @Schema(description = "WiFi MAC 地址", example = "00:11:22:33:44:55")
  58 + @Size(max = 50, message = "WiFi MAC 地址不能超过 50 个字符")
  59 + private String mac;
  60 +
  61 + @Schema(description = "备注", example = "手工补卡")
  62 + @Size(max = 255, message = "备注不能超过 255 个字符")
  63 + private String remark;
  64 +
  65 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockShiftReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +import org.springframework.format.annotation.DateTimeFormat;
  7 +
  8 +import java.time.LocalDateTime;
  9 +
  10 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  11 +
  12 +@Schema(description = "管理后台 - HRM 员工实际班次 Request VO")
  13 +@Data
  14 +public class HrmAttendanceClockShiftReqVO {
  15 +
  16 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  17 + @NotNull(message = "员工不能为空")
  18 + private Long employeeId;
  19 +
  20 + @Schema(description = "考勤时间", requiredMode = Schema.RequiredMode.REQUIRED)
  21 + @NotNull(message = "考勤时间不能为空")
  22 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  23 + private LocalDateTime attendanceTime;
  24 +
  25 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockShiftRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - HRM 员工实际班次 Response VO")
  9 +@Data
  10 +public class HrmAttendanceClockShiftRespVO {
  11 +
  12 + @Schema(description = "上班时间")
  13 + private LocalDateTime startTime;
  14 +
  15 + @Schema(description = "下班时间")
  16 + private LocalDateTime endTime;
  17 +
  18 + @Schema(description = "上班打卡开始时间")
  19 + private LocalDateTime clockInStartTime;
  20 +
  21 + @Schema(description = "上班打卡结束时间")
  22 + private LocalDateTime clockInEndTime;
  23 +
  24 + @Schema(description = "下班打卡开始时间")
  25 + private LocalDateTime clockOutStartTime;
  26 +
  27 + @Schema(description = "下班打卡结束时间")
  28 + private LocalDateTime clockOutEndTime;
  29 +
  30 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/group/HrmAttendanceGroupPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.group;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +@Schema(description = "管理后台 - HRM 考勤组分页 Request VO")
  10 +@Data
  11 +@EqualsAndHashCode(callSuper = true)
  12 +@ToString(callSuper = true)
  13 +public class HrmAttendanceGroupPageReqVO extends PageParam {
  14 +
  15 + @Schema(description = "考勤组名称", example = "默认考勤组")
  16 + private String name;
  17 +
  18 + @Schema(description = "是否默认考勤组", example = "true")
  19 + private Boolean defaultStatus;
  20 +
  21 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/group/HrmAttendanceGroupRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.group;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonFormat;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +
  7 +import java.math.BigDecimal;
  8 +import java.time.LocalDateTime;
  9 +import java.time.LocalTime;
  10 +import java.util.List;
  11 +
  12 +@Schema(description = "管理后台 - HRM 考勤组 Response VO")
  13 +@Data
  14 +public class HrmAttendanceGroupRespVO {
  15 +
  16 + @Schema(description = "考勤组编号", example = "1024")
  17 + private Long id;
  18 +
  19 + @Schema(description = "名称", example = "总部考勤组")
  20 + private String name;
  21 +
  22 + @Schema(description = "是否启用 WiFi 打卡")
  23 + private Boolean openWifiCard;
  24 +
  25 + @Schema(description = "是否启用定位打卡")
  26 + private Boolean openPointCard;
  27 +
  28 + @Schema(description = "是否法定节假日休息")
  29 + private Boolean rest;
  30 +
  31 + @Schema(description = "是否默认配置")
  32 + private Boolean defaultStatus;
  33 +
  34 + @Schema(description = "特殊日期设置数组")
  35 + private List<SpecialDate> specialDates;
  36 +
  37 + @Schema(description = "部门范围")
  38 + private List<Long> deptIds;
  39 +
  40 + @Schema(description = "适用部门名称")
  41 + private List<String> deptNames;
  42 +
  43 + @Schema(description = "员工范围")
  44 + private List<Long> employeeIds;
  45 +
  46 + @Schema(description = "适用员工名称")
  47 + private List<String> employeeNames;
  48 +
  49 + @Schema(description = "班次配置")
  50 + private List<Shift> shifts;
  51 +
  52 + @Schema(description = "打卡地点")
  53 + private List<Point> points;
  54 +
  55 + @Schema(description = "打卡 WiFi")
  56 + private List<Wifi> wifis;
  57 +
  58 + @Schema(description = "扣款规则")
  59 + private DeductRule deductRule;
  60 +
  61 + @Schema(description = "创建时间")
  62 + private LocalDateTime createTime;
  63 +
  64 + @Schema(description = "管理后台 - HRM 考勤组特殊日期 Response VO")
  65 + @Data
  66 + public static class SpecialDate {
  67 +
  68 + @Schema(description = "日期类型", example = "1")
  69 + private Integer type;
  70 +
  71 + @Schema(description = "特殊日期")
  72 + private LocalDateTime date;
  73 + }
  74 +
  75 + @Schema(description = "管理后台 - HRM 考勤组班次配置 Response VO")
  76 + @Data
  77 + public static class Shift {
  78 +
  79 + @Schema(description = "工作日数组", example = "[1, 2, 3, 4, 5]")
  80 + private List<Integer> weeks;
  81 +
  82 + @Schema(description = "上班时间", example = "09:00")
  83 + @JsonFormat(pattern = "HH:mm")
  84 + private LocalTime startTime;
  85 +
  86 + @Schema(description = "下班时间", example = "18:00")
  87 + @JsonFormat(pattern = "HH:mm")
  88 + private LocalTime endTime;
  89 +
  90 + @Schema(description = "上班打卡开始时间", example = "08:00")
  91 + @JsonFormat(pattern = "HH:mm")
  92 + private LocalTime clockInStartTime;
  93 +
  94 + @Schema(description = "上班打卡结束时间", example = "10:00")
  95 + @JsonFormat(pattern = "HH:mm")
  96 + private LocalTime clockInEndTime;
  97 +
  98 + @Schema(description = "下班打卡开始时间", example = "17:00")
  99 + @JsonFormat(pattern = "HH:mm")
  100 + private LocalTime clockOutStartTime;
  101 +
  102 + @Schema(description = "下班打卡结束时间", example = "20:00")
  103 + @JsonFormat(pattern = "HH:mm")
  104 + private LocalTime clockOutEndTime;
  105 +
  106 + @Schema(description = "休息开始时间", example = "12:00")
  107 + @JsonFormat(pattern = "HH:mm")
  108 + private LocalTime restStartTime;
  109 +
  110 + @Schema(description = "休息结束时间", example = "13:00")
  111 + @JsonFormat(pattern = "HH:mm")
  112 + private LocalTime restEndTime;
  113 +
  114 + @Schema(description = "休息时间是否不计入工作时长")
  115 + private Boolean excludeRestTime;
  116 + }
  117 +
  118 + @Schema(description = "管理后台 - HRM 考勤组打卡地点 Response VO")
  119 + @Data
  120 + public static class Point {
  121 +
  122 + @Schema(description = "地点名称", example = "总部办公区")
  123 + private String name;
  124 +
  125 + @Schema(description = "定位地址", example = "北京市海淀区中关村大街")
  126 + private String address;
  127 +
  128 + @Schema(description = "纬度", example = "39.983424")
  129 + private BigDecimal latitude;
  130 +
  131 + @Schema(description = "经度", example = "116.322987")
  132 + private BigDecimal longitude;
  133 +
  134 + @Schema(description = "有效打卡半径,单位:米", example = "300")
  135 + private Integer radius;
  136 + }
  137 +
  138 + @Schema(description = "管理后台 - HRM 考勤组 WiFi 配置 Response VO")
  139 + @Data
  140 + public static class Wifi {
  141 +
  142 + @Schema(description = "WiFi 名称", example = "Office-WiFi")
  143 + private String ssid;
  144 +
  145 + @Schema(description = "MAC 地址", example = "00:11:22:33:44:55")
  146 + private String mac;
  147 + }
  148 +
  149 + @Schema(description = "管理后台 - HRM 考勤组扣款规则 Response VO")
  150 + @Data
  151 + public static class DeductRule {
  152 +
  153 + @Schema(description = "迟到扣款方式", example = "1")
  154 + private Integer lateMethod;
  155 +
  156 + @Schema(description = "迟到扣款金额", example = "10.00")
  157 + private BigDecimal lateDeductMoney;
  158 +
  159 + @Schema(description = "早退扣款方式", example = "1")
  160 + private Integer earlyMethod;
  161 +
  162 + @Schema(description = "早退扣款金额", example = "10.00")
  163 + private BigDecimal earlyDeductMoney;
  164 +
  165 + @Schema(description = "旷工扣款方式", example = "1")
  166 + private Integer absenteeismMethod;
  167 +
  168 + @Schema(description = "旷工扣款金额", example = "100.00")
  169 + private BigDecimal absenteeismDeductMoney;
  170 +
  171 + @Schema(description = "缺卡扣款方式", example = "1")
  172 + private Integer misscardMethod;
  173 +
  174 + @Schema(description = "缺卡扣款金额", example = "20.00")
  175 + private BigDecimal misscardDeductMoney;
  176 + }
  177 +
  178 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/group/HrmAttendanceGroupSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.group;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
  5 +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.TimeRange;
  6 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  7 +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceAbsenteeismDeductMethodEnum;
  8 +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceHolidayTypeEnum;
  9 +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceLateEarlyDeductMethodEnum;
  10 +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceMisscardDeductMethodEnum;
  11 +import com.fasterxml.jackson.annotation.JsonFormat;
  12 +import com.fasterxml.jackson.annotation.JsonIgnore;
  13 +import com.mzt.logapi.starter.annotation.DiffLogField;
  14 +import io.swagger.v3.oas.annotations.media.Schema;
  15 +import jakarta.validation.Valid;
  16 +import jakarta.validation.constraints.AssertTrue;
  17 +import jakarta.validation.constraints.DecimalMax;
  18 +import jakarta.validation.constraints.DecimalMin;
  19 +import jakarta.validation.constraints.Digits;
  20 +import jakarta.validation.constraints.Max;
  21 +import jakarta.validation.constraints.Min;
  22 +import jakarta.validation.constraints.NotBlank;
  23 +import jakarta.validation.constraints.NotEmpty;
  24 +import jakarta.validation.constraints.NotNull;
  25 +import jakarta.validation.constraints.Pattern;
  26 +import jakarta.validation.constraints.Positive;
  27 +import jakarta.validation.constraints.Size;
  28 +import lombok.Data;
  29 +
  30 +import java.math.BigDecimal;
  31 +import java.time.LocalDate;
  32 +import java.time.LocalDateTime;
  33 +import java.time.LocalTime;
  34 +import java.util.Arrays;
  35 +import java.util.HashSet;
  36 +import java.util.List;
  37 +import java.util.Set;
  38 +
  39 +@Schema(description = "管理后台 - HRM 考勤组保存 Request VO")
  40 +@Data
  41 +public class HrmAttendanceGroupSaveReqVO {
  42 +
  43 + private static final Set<Integer> POINT_RADIUS_VALUES = new HashSet<>(
  44 + Arrays.asList(100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 3000));
  45 +
  46 + @Schema(description = "考勤组编号", example = "1024")
  47 + private Long id;
  48 +
  49 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "总部考勤组")
  50 + @NotBlank(message = "考勤组名称不能为空")
  51 + @Size(max = 50, message = "考勤组名称不能超过 50 个字符")
  52 + @DiffLogField(name = "考勤组名称")
  53 + private String name;
  54 +
  55 + @Schema(description = "是否启用 WiFi 打卡", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  56 + @NotNull(message = "是否启用 WiFi 打卡不能为空")
  57 + @DiffLogField(name = "是否启用 WiFi 打卡")
  58 + private Boolean openWifiCard;
  59 +
  60 + @Schema(description = "是否启用定位打卡", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  61 + @NotNull(message = "是否启用定位打卡不能为空")
  62 + @DiffLogField(name = "是否启用定位打卡")
  63 + private Boolean openPointCard;
  64 +
  65 + @Schema(description = "是否法定节假日休息", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  66 + @NotNull(message = "是否法定节假日休息不能为空")
  67 + @DiffLogField(name = "是否法定节假日休息")
  68 + private Boolean rest;
  69 +
  70 + @Schema(description = "特殊日期设置数组", requiredMode = Schema.RequiredMode.REQUIRED)
  71 + @Valid
  72 + @NotNull(message = "特殊日期设置不能为空")
  73 + @DiffLogField(name = "特殊日期设置")
  74 + private List<SpecialDate> specialDates;
  75 +
  76 + @Schema(description = "部门范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1024, 2048]")
  77 + @NotNull(message = "部门范围不能为空")
  78 + @DiffLogField(name = "适用部门")
  79 + private List<@NotNull(message = "部门编号不能为空") Long> deptIds;
  80 +
  81 + @Schema(description = "员工范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1024, 2048]")
  82 + @NotNull(message = "员工范围不能为空")
  83 + @DiffLogField(name = "适用员工")
  84 + private List<@NotNull(message = "员工编号不能为空") Long> employeeIds;
  85 +
  86 + @Schema(description = "班次配置", requiredMode = Schema.RequiredMode.REQUIRED)
  87 + @Valid
  88 + @NotEmpty(message = "班次配置不能为空")
  89 + @DiffLogField(name = "班次配置")
  90 + private List<Shift> shifts;
  91 +
  92 + @Schema(description = "打卡地点", requiredMode = Schema.RequiredMode.REQUIRED)
  93 + @Valid
  94 + @NotNull(message = "打卡地点不能为空")
  95 + @DiffLogField(name = "打卡地点")
  96 + private List<Point> points;
  97 +
  98 + @Schema(description = "打卡 WiFi", requiredMode = Schema.RequiredMode.REQUIRED)
  99 + @Valid
  100 + @NotNull(message = "打卡 WiFi 不能为空")
  101 + @DiffLogField(name = "打卡 WiFi")
  102 + private List<Wifi> wifis;
  103 +
  104 + @Schema(description = "扣款规则", requiredMode = Schema.RequiredMode.REQUIRED)
  105 + @Valid
  106 + @NotNull(message = "扣款规则不能为空")
  107 + @DiffLogField(name = "扣款规则")
  108 + private DeductRule deductRule;
  109 +
  110 + @Schema(description = "管理后台 - HRM 考勤组特殊日期")
  111 + @Data
  112 + public static class SpecialDate {
  113 +
  114 + @Schema(description = "日期类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  115 + @NotNull(message = "特殊日期类型不能为空")
  116 + @InEnum(value = HrmAttendanceHolidayTypeEnum.class, message = "特殊日期类型必须是 {value}")
  117 + private Integer type;
  118 +
  119 + @Schema(description = "特殊日期", requiredMode = Schema.RequiredMode.REQUIRED)
  120 + @NotNull(message = "特殊日期不能为空")
  121 + private LocalDateTime date;
  122 + }
  123 +
  124 + @Schema(description = "管理后台 - HRM 考勤组班次配置")
  125 + @Data
  126 + public static class Shift {
  127 +
  128 + @Schema(description = "工作日数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2, 3, 4, 5]")
  129 + @NotEmpty(message = "班次工作日不能为空")
  130 + private List<@NotNull(message = "班次工作日不能为空")
  131 + @Min(value = 1, message = "班次工作日必须在 1 到 7 之间")
  132 + @Max(value = 7, message = "班次工作日必须在 1 到 7 之间") Integer> weeks;
  133 +
  134 + @Schema(description = "上班时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "09:00")
  135 + @JsonFormat(pattern = "HH:mm")
  136 + @NotNull(message = "上班时间不能为空")
  137 + private LocalTime startTime;
  138 +
  139 + @Schema(description = "下班时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "18:00")
  140 + @JsonFormat(pattern = "HH:mm")
  141 + @NotNull(message = "下班时间不能为空")
  142 + private LocalTime endTime;
  143 +
  144 + @Schema(description = "上班打卡开始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "08:00")
  145 + @JsonFormat(pattern = "HH:mm")
  146 + @NotNull(message = "上班打卡开始时间不能为空")
  147 + private LocalTime clockInStartTime;
  148 +
  149 + @Schema(description = "上班打卡结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "10:00")
  150 + @JsonFormat(pattern = "HH:mm")
  151 + @NotNull(message = "上班打卡结束时间不能为空")
  152 + private LocalTime clockInEndTime;
  153 +
  154 + @Schema(description = "下班打卡开始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "17:00")
  155 + @JsonFormat(pattern = "HH:mm")
  156 + @NotNull(message = "下班打卡开始时间不能为空")
  157 + private LocalTime clockOutStartTime;
  158 +
  159 + @Schema(description = "下班打卡结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "20:00")
  160 + @JsonFormat(pattern = "HH:mm")
  161 + @NotNull(message = "下班打卡结束时间不能为空")
  162 + private LocalTime clockOutEndTime;
  163 +
  164 + @Schema(description = "休息开始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "12:00")
  165 + @JsonFormat(pattern = "HH:mm")
  166 + @NotNull(message = "休息开始时间不能为空")
  167 + private LocalTime restStartTime;
  168 +
  169 + @Schema(description = "休息结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "13:00")
  170 + @JsonFormat(pattern = "HH:mm")
  171 + @NotNull(message = "休息结束时间不能为空")
  172 + private LocalTime restEndTime;
  173 +
  174 + @Schema(description = "休息时间是否不计入工作时长", requiredMode = Schema.RequiredMode.REQUIRED)
  175 + @NotNull(message = "休息时间计算方式不能为空")
  176 + private Boolean excludeRestTime;
  177 + }
  178 +
  179 + @Schema(description = "管理后台 - HRM 考勤组打卡地点")
  180 + @Data
  181 + public static class Point {
  182 +
  183 + @Schema(description = "地点名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "总部办公区")
  184 + @NotBlank(message = "地点名称不能为空")
  185 + @Size(max = 50, message = "地点名称不能超过 50 个字符")
  186 + private String name;
  187 +
  188 + @Schema(description = "定位地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "北京市海淀区中关村大街")
  189 + @NotBlank(message = "定位地址不能为空")
  190 + @Size(max = 255, message = "定位地址不能超过 255 个字符")
  191 + private String address;
  192 +
  193 + @Schema(description = "纬度", requiredMode = Schema.RequiredMode.REQUIRED, example = "39.983424")
  194 + @NotNull(message = "纬度不能为空")
  195 + @DecimalMin(value = "-90", message = "纬度必须在 -90 到 90 之间")
  196 + @DecimalMax(value = "90", message = "纬度必须在 -90 到 90 之间")
  197 + private BigDecimal latitude;
  198 +
  199 + @Schema(description = "经度", requiredMode = Schema.RequiredMode.REQUIRED, example = "116.322987")
  200 + @NotNull(message = "经度不能为空")
  201 + @DecimalMin(value = "-180", message = "经度必须在 -180 到 180 之间")
  202 + @DecimalMax(value = "180", message = "经度必须在 -180 到 180 之间")
  203 + private BigDecimal longitude;
  204 +
  205 + @Schema(description = "有效打卡半径,单位:米", requiredMode = Schema.RequiredMode.REQUIRED, example = "300")
  206 + @NotNull(message = "有效打卡半径不能为空")
  207 + @Positive(message = "有效打卡半径必须大于 0")
  208 + private Integer radius;
  209 + }
  210 +
  211 + @Schema(description = "管理后台 - HRM 考勤组 WiFi 配置")
  212 + @Data
  213 + public static class Wifi {
  214 +
  215 + @Schema(description = "WiFi 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "Office-WiFi")
  216 + @NotBlank(message = "WiFi 名称不能为空")
  217 + @Size(max = 50, message = "WiFi 名称不能超过 50 个字符")
  218 + private String ssid;
  219 +
  220 + @Schema(description = "MAC 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "00:11:22:33:44:55")
  221 + @NotBlank(message = "MAC 地址不能为空")
  222 + @Pattern(regexp = "^((([0-9a-fA-F]{2}:){5})|(([0-9a-fA-F]{2}-){5}))[0-9a-fA-F]{2}$",
  223 + message = "MAC 地址格式不正确")
  224 + private String mac;
  225 + }
  226 +
  227 + @Schema(description = "管理后台 - HRM 考勤组扣款规则")
  228 + @Data
  229 + public static class DeductRule {
  230 +
  231 + @Schema(description = "迟到扣款方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  232 + @NotNull(message = "迟到扣款方式不能为空")
  233 + @InEnum(value = HrmAttendanceLateEarlyDeductMethodEnum.class, message = "迟到扣款方式必须是 {value}")
  234 + private Integer lateMethod;
  235 +
  236 + @Schema(description = "迟到扣款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.00")
  237 + @NotNull(message = "迟到扣款金额不能为空")
  238 + @DecimalMin(value = "0", message = "迟到扣款金额不能小于 0")
  239 + @Digits(integer = 8, fraction = 2, message = "迟到扣款金额最多 8 位整数和 2 位小数")
  240 + private BigDecimal lateDeductMoney;
  241 +
  242 + @Schema(description = "早退扣款方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  243 + @NotNull(message = "早退扣款方式不能为空")
  244 + @InEnum(value = HrmAttendanceLateEarlyDeductMethodEnum.class, message = "早退扣款方式必须是 {value}")
  245 + private Integer earlyMethod;
  246 +
  247 + @Schema(description = "早退扣款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.00")
  248 + @NotNull(message = "早退扣款金额不能为空")
  249 + @DecimalMin(value = "0", message = "早退扣款金额不能小于 0")
  250 + @Digits(integer = 8, fraction = 2, message = "早退扣款金额最多 8 位整数和 2 位小数")
  251 + private BigDecimal earlyDeductMoney;
  252 +
  253 + @Schema(description = "旷工扣款方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  254 + @NotNull(message = "旷工扣款方式不能为空")
  255 + @InEnum(value = HrmAttendanceAbsenteeismDeductMethodEnum.class, message = "旷工扣款方式必须是 {value}")
  256 + private Integer absenteeismMethod;
  257 +
  258 + @Schema(description = "旷工扣款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
  259 + @NotNull(message = "旷工扣款金额不能为空")
  260 + @DecimalMin(value = "0", message = "旷工扣款金额不能小于 0")
  261 + @Digits(integer = 8, fraction = 2, message = "旷工扣款金额最多 8 位整数和 2 位小数")
  262 + private BigDecimal absenteeismDeductMoney;
  263 +
  264 + @Schema(description = "缺卡扣款方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  265 + @NotNull(message = "缺卡扣款方式不能为空")
  266 + @InEnum(value = HrmAttendanceMisscardDeductMethodEnum.class, message = "缺卡扣款方式必须是 {value}")
  267 + private Integer misscardMethod;
  268 +
  269 + @Schema(description = "缺卡扣款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "20.00")
  270 + @NotNull(message = "缺卡扣款金额不能为空")
  271 + @DecimalMin(value = "0", message = "缺卡扣款金额不能小于 0")
  272 + @Digits(integer = 8, fraction = 2, message = "缺卡扣款金额最多 8 位整数和 2 位小数")
  273 + private BigDecimal misscardDeductMoney;
  274 + }
  275 +
  276 + @AssertTrue(message = "不同班次不能配置重复工作日")
  277 + @JsonIgnore
  278 + public boolean isShiftWeeksUnique() {
  279 + if (CollUtil.isEmpty(shifts)) {
  280 + return true;
  281 + }
  282 + Set<Integer> weeks = new HashSet<>();
  283 + return shifts.stream().allMatch(shift -> shift != null && CollUtil.isNotEmpty(shift.getWeeks())
  284 + && shift.getWeeks().stream().allMatch(weeks::add));
  285 + }
  286 +
  287 + @AssertTrue(message = "班次上下班、打卡窗口或休息时间配置不正确")
  288 + @JsonIgnore
  289 + public boolean isShiftTimeValid() {
  290 + if (CollUtil.isEmpty(shifts)) {
  291 + return true;
  292 + }
  293 + return shifts.stream().allMatch(this::isShiftTimeValid);
  294 + }
  295 +
  296 + private boolean isShiftTimeValid(Shift shift) {
  297 + if (shift == null || shift.getStartTime() == null || shift.getEndTime() == null
  298 + || shift.getClockInStartTime() == null || shift.getClockInEndTime() == null
  299 + || shift.getClockOutStartTime() == null || shift.getClockOutEndTime() == null
  300 + || shift.getRestStartTime() == null || shift.getRestEndTime() == null
  301 + || shift.getExcludeRestTime() == null) {
  302 + // 交由内嵌字段的 @NotNull 输出具体校验错误
  303 + return true;
  304 + }
  305 + if (shift.getStartTime().equals(shift.getEndTime())
  306 + || shift.getClockInStartTime().equals(shift.getClockInEndTime())
  307 + || shift.getClockOutStartTime().equals(shift.getClockOutEndTime())) {
  308 + return false;
  309 + }
  310 + LocalDate attendanceDate = LocalDate.of(2000, 1, 3);
  311 + TimeRange shiftTimeRange = CollUtil.getFirst(LocalDateTimeUtils.buildDailyTimeRanges(
  312 + attendanceDate, attendanceDate, shift.getStartTime(), shift.getEndTime()));
  313 + return LocalDateTimeUtils.findDailyTimeRange(
  314 + attendanceDate, shift.getClockInStartTime(), shift.getClockInEndTime(),
  315 + shiftTimeRange.getStartTime()) != null
  316 + && LocalDateTimeUtils.findDailyTimeRange(
  317 + attendanceDate, shift.getClockOutStartTime(), shift.getClockOutEndTime(),
  318 + shiftTimeRange.getEndTime()) != null
  319 + && isRestTimeValid(attendanceDate, shift, shiftTimeRange);
  320 + }
  321 +
  322 + private boolean isRestTimeValid(LocalDate attendanceDate, Shift shift,
  323 + TimeRange shiftTimeRange) {
  324 + if (Boolean.FALSE.equals(shift.getExcludeRestTime())) {
  325 + return true;
  326 + }
  327 + if (shift.getRestStartTime().equals(shift.getRestEndTime())) {
  328 + return false;
  329 + }
  330 + List<TimeRange> restTimeRanges = LocalDateTimeUtils.buildDailyTimeRanges(
  331 + attendanceDate.minusDays(1), attendanceDate.plusDays(1),
  332 + shift.getRestStartTime(), shift.getRestEndTime());
  333 + return CollUtil.findOne(restTimeRanges, restTimeRange ->
  334 + LocalDateTimeUtils.isAfterOrEqual(restTimeRange.getStartTime(), shiftTimeRange.getStartTime())
  335 + && LocalDateTimeUtils.isBeforeOrEqual(
  336 + restTimeRange.getEndTime(), shiftTimeRange.getEndTime())) != null;
  337 + }
  338 +
  339 + @AssertTrue(message = "特殊日期不能重复")
  340 + @JsonIgnore
  341 + public boolean isSpecialDatesUnique() {
  342 + if (CollUtil.isEmpty(specialDates)) {
  343 + return true;
  344 + }
  345 + Set<LocalDate> dates = new HashSet<>();
  346 + return specialDates.stream().allMatch(specialDate -> specialDate != null && specialDate.getDate() != null
  347 + && dates.add(specialDate.getDate().toLocalDate()));
  348 + }
  349 +
  350 + @AssertTrue(message = "考勤组至少需要选择一个适用部门或员工")
  351 + @JsonIgnore
  352 + public boolean isScopeValid() {
  353 + if (deptIds == null || employeeIds == null) {
  354 + return true;
  355 + }
  356 + return CollUtil.isNotEmpty(deptIds) || CollUtil.isNotEmpty(employeeIds);
  357 + }
  358 +
  359 + @AssertTrue(message = "考勤组至少需要启用定位打卡或 WiFi 打卡")
  360 + @JsonIgnore
  361 + public boolean isClockMethodValid() {
  362 + if (openPointCard == null || openWifiCard == null) {
  363 + return true;
  364 + }
  365 + return openPointCard || openWifiCard;
  366 + }
  367 +
  368 + @AssertTrue(message = "启用定位打卡时,打卡地点不能为空")
  369 + @JsonIgnore
  370 + public boolean isPointSettingValid() {
  371 + if (Boolean.TRUE.equals(openPointCard)) {
  372 + return CollUtil.isNotEmpty(points);
  373 + }
  374 + return true;
  375 + }
  376 +
  377 + @AssertTrue(message = "启用 WiFi 打卡时,打卡 WiFi 不能为空")
  378 + @JsonIgnore
  379 + public boolean isWifiSettingValid() {
  380 + if (Boolean.TRUE.equals(openWifiCard)) {
  381 + return CollUtil.isNotEmpty(wifis);
  382 + }
  383 + return true;
  384 + }
  385 +
  386 + @AssertTrue(message = "有效打卡半径必须是 100 至 1000 米(间隔 100 米),或 2000、3000 米")
  387 + @JsonIgnore
  388 + public boolean isPointRadiusValid() {
  389 + if (CollUtil.isEmpty(points)) {
  390 + return true;
  391 + }
  392 + return points.stream().allMatch(point -> point != null
  393 + && point.getRadius() != null && POINT_RADIUS_VALUES.contains(point.getRadius()));
  394 + }
  395 +
  396 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/holiday/HrmAttendanceHolidayPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.holiday;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  5 +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceHolidayTypeEnum;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import lombok.Data;
  8 +import lombok.EqualsAndHashCode;
  9 +import lombok.ToString;
  10 +
  11 +import org.springframework.format.annotation.DateTimeFormat;
  12 +
  13 +import java.time.LocalDateTime;
  14 +
  15 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  16 +
  17 +@Schema(description = "管理后台 - HRM 考勤节假日分页 Request VO")
  18 +@Data
  19 +@EqualsAndHashCode(callSuper = true)
  20 +@ToString(callSuper = true)
  21 +public class HrmAttendanceHolidayPageReqVO extends PageParam {
  22 +
  23 + @Schema(description = "日期范围")
  24 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  25 + private LocalDateTime[] date;
  26 +
  27 + @Schema(description = "节假日类型", example = "1")
  28 + @InEnum(value = HrmAttendanceHolidayTypeEnum.class, message = "节假日类型必须是 {value}")
  29 + private Integer type;
  30 +
  31 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/holiday/HrmAttendanceHolidayRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.holiday;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - HRM 考勤节假日 Response VO")
  9 +@Data
  10 +public class HrmAttendanceHolidayRespVO {
  11 +
  12 + @Schema(description = "编号")
  13 + private Long id;
  14 +
  15 + @Schema(description = "日期")
  16 + private LocalDateTime date;
  17 +
  18 + @Schema(description = "考勤假期类型")
  19 + private Integer type;
  20 +
  21 + @Schema(description = "创建时间")
  22 + private LocalDateTime createTime;
  23 +
  24 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/holiday/HrmAttendanceHolidaySaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.holiday;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceHolidayTypeEnum;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import jakarta.validation.constraints.NotNull;
  7 +import lombok.Data;
  8 +
  9 +import java.time.LocalDateTime;
  10 +
  11 +@Schema(description = "管理后台 - HRM 考勤节假日保存 Request VO")
  12 +@Data
  13 +public class HrmAttendanceHolidaySaveReqVO {
  14 +
  15 + @Schema(description = "节假日编号", example = "1024")
  16 + private Long id;
  17 +
  18 + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED)
  19 + @NotNull(message = "日期不能为空")
  20 + private LocalDateTime date;
  21 +
  22 + @Schema(description = "节假日类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  23 + @NotNull(message = "类型不能为空")
  24 + @InEnum(value = HrmAttendanceHolidayTypeEnum.class, message = "节假日类型必须是 {value}")
  25 + private Integer type;
  26 +
  27 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeaveCancelReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotBlank;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import jakarta.validation.constraints.Size;
  7 +import lombok.Data;
  8 +
  9 +@Schema(description = "管理后台 - HRM 员工请假申请取消 Request VO")
  10 +@Data
  11 +public class HrmAttendanceLeaveCancelReqVO {
  12 +
  13 + @Schema(description = "请假记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  14 + @NotNull(message = "请假记录编号不能为空")
  15 + private Long id;
  16 +
  17 + @Schema(description = "取消原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "行程取消")
  18 + @NotBlank(message = "取消原因不能为空")
  19 + @Size(max = 500, message = "取消原因不能超过 500 个字符")
  20 + private String reason;
  21 +
  22 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeaveCreateReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonIgnore;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.constraints.AssertTrue;
  6 +import jakarta.validation.constraints.DecimalMin;
  7 +import jakarta.validation.constraints.Digits;
  8 +import jakarta.validation.constraints.NotBlank;
  9 +import jakarta.validation.constraints.NotNull;
  10 +import jakarta.validation.constraints.Size;
  11 +import lombok.Data;
  12 +
  13 +import java.math.BigDecimal;
  14 +import java.time.LocalDateTime;
  15 +
  16 +@Schema(description = "管理后台 - HRM 员工请假申请创建 Request VO")
  17 +@Data
  18 +public class HrmAttendanceLeaveCreateReqVO {
  19 +
  20 + @Schema(description = "请假类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "事假")
  21 + @NotBlank(message = "请假类型不能为空")
  22 + @Size(max = 64, message = "请假类型不能超过 64 个字符")
  23 + private String type;
  24 +
  25 + @Schema(description = "请假开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
  26 + @NotNull(message = "请假开始时间不能为空")
  27 + private LocalDateTime startTime;
  28 +
  29 + @Schema(description = "请假结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
  30 + @NotNull(message = "请假结束时间不能为空")
  31 + private LocalDateTime endTime;
  32 +
  33 + @Schema(description = "请假天数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.00")
  34 + @NotNull(message = "请假天数不能为空")
  35 + @DecimalMin(value = "0.01", message = "请假天数必须大于 0")
  36 + @Digits(integer = 8, fraction = 2, message = "请假天数最多保留两位小数")
  37 + private BigDecimal day;
  38 +
  39 + @Schema(description = "请假事由", requiredMode = Schema.RequiredMode.REQUIRED, example = "个人事务")
  40 + @NotBlank(message = "请假事由不能为空")
  41 + @Size(max = 300, message = "请假事由不能超过 300 个字符")
  42 + private String reason;
  43 +
  44 + @Schema(description = "备注", example = "请审批")
  45 + @Size(max = 500, message = "备注不能超过 500 个字符")
  46 + private String remark;
  47 +
  48 + @Schema(hidden = true)
  49 + @AssertTrue(message = "请假结束时间必须晚于请假开始时间")
  50 + @JsonIgnore
  51 + public boolean isTimeRangeValid() {
  52 + return startTime == null || endTime == null || endTime.isAfter(startTime);
  53 + }
  54 +
  55 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeavePageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +import org.springframework.format.annotation.DateTimeFormat;
  9 +
  10 +import java.time.LocalDateTime;
  11 +import java.util.Collection;
  12 +import java.util.List;
  13 +
  14 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  15 +
  16 +@Schema(description = "管理后台 - HRM 请假记录分页 Request VO")
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +public class HrmAttendanceLeavePageReqVO extends SortablePageParam {
  21 +
  22 + @Schema(description = "员工编号", example = "1024")
  23 + private Long employeeId;
  24 +
  25 + @Schema(description = "部门编号数组", example = "[100, 101]")
  26 + private List<Long> deptIds;
  27 +
  28 + @Schema(description = "员工姓名或工号", example = "张三")
  29 + private String employeeKeyword;
  30 +
  31 + @Schema(description = "请假类型数组", example = "[事假, 病假]")
  32 + private List<String> types;
  33 +
  34 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  35 + @Schema(description = "开始时间")
  36 + private LocalDateTime[] startTime;
  37 +
  38 + @Schema(description = "审批状态", example = "2")
  39 + private Integer approvalStatus;
  40 +
  41 + @Schema(hidden = true)
  42 + private Collection<Long> employeeIds;
  43 +
  44 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeaveRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave;
  2 +
  3 +import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
  4 +import cn.idev.excel.annotation.ExcelProperty;
  5 +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
  6 +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
  7 +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants;
  8 +import io.swagger.v3.oas.annotations.media.Schema;
  9 +import lombok.Data;
  10 +
  11 +import java.math.BigDecimal;
  12 +import java.time.LocalDateTime;
  13 +
  14 +@Schema(description = "管理后台 - HRM 请假记录 Response VO")
  15 +@Data
  16 +@ExcelIgnoreUnannotated
  17 +public class HrmAttendanceLeaveRespVO {
  18 +
  19 + @Schema(description = "请假记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  20 + @ExcelProperty(value = "记录编号", index = 0)
  21 + private Long id;
  22 +
  23 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
  24 + @ExcelProperty(value = "员工编号", index = 1)
  25 + private Long employeeId;
  26 +
  27 + @Schema(description = "员工姓名", example = "张三")
  28 + @ExcelProperty(value = "员工姓名", index = 2)
  29 + private String employeeName;
  30 +
  31 + @Schema(description = "工号", example = "HRM001")
  32 + @ExcelProperty(value = "工号", index = 3)
  33 + private String jobNumber;
  34 +
  35 + @Schema(description = "部门编号", example = "100")
  36 + private Long deptId;
  37 +
  38 + @Schema(description = "部门名称", example = "研发部")
  39 + @ExcelProperty(value = "部门", index = 4)
  40 + private String deptName;
  41 +
  42 + @Schema(description = "职位名称", example = "Java 工程师")
  43 + @ExcelProperty(value = "岗位", index = 5)
  44 + private String postName;
  45 +
  46 + @Schema(description = "请假类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "事假")
  47 + @ExcelProperty(value = "请假类型", index = 6, converter = DictConvert.class)
  48 + @DictFormat(DictTypeConstants.HRM_ATTENDANCE_LEAVE_TYPE)
  49 + private String type;
  50 +
  51 + @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
  52 + @ExcelProperty(value = "开始时间", index = 7)
  53 + private LocalDateTime startTime;
  54 +
  55 + @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
  56 + @ExcelProperty(value = "结束时间", index = 8)
  57 + private LocalDateTime endTime;
  58 +
  59 + @Schema(description = "请假天数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
  60 + @ExcelProperty(value = "请假天数", index = 9)
  61 + private BigDecimal day;
  62 +
  63 + @Schema(description = "请假事由", example = "个人事务")
  64 + @ExcelProperty(value = "请假事由", index = 10)
  65 + private String reason;
  66 +
  67 + @Schema(description = "备注")
  68 + @ExcelProperty(value = "备注", index = 11)
  69 + private String remark;
  70 +
  71 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  72 + @ExcelProperty(value = "创建时间", index = 12)
  73 + private LocalDateTime createTime;
  74 +
  75 + @Schema(description = "审批状态", example = "1")
  76 + private Integer approvalStatus;
  77 +
  78 + @Schema(description = "流程实例编号")
  79 + private String processInstanceId;
  80 +
  81 + @Schema(description = "审批时间")
  82 + private LocalDateTime approvalTime;
  83 +
  84 + @Schema(description = "审批意见")
  85 + private String approvalReason;
  86 +
  87 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceDailyDetailReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +
  9 +import java.time.LocalDateTime;
  10 +
  11 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  12 +
  13 +@Schema(description = "管理后台 - HRM 员工每日考勤明细 Request VO")
  14 +@Data
  15 +public class HrmAttendanceDailyDetailReqVO {
  16 +
  17 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  18 + @NotNull(message = "员工不能为空")
  19 + private Long employeeId;
  20 +
  21 + @Schema(description = "考勤日期", requiredMode = Schema.RequiredMode.REQUIRED)
  22 + @NotNull(message = "考勤日期不能为空")
  23 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  24 + private LocalDateTime attendanceTime;
  25 +
  26 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceDailyDetailRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics;
  2 +
  3 +import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
  4 +import cn.idev.excel.annotation.ExcelProperty;
  5 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockRespVO;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import lombok.Data;
  8 +
  9 +import java.math.BigDecimal;
  10 +import java.time.LocalDateTime;
  11 +import java.util.List;
  12 +
  13 +@Schema(description = "管理后台 - HRM 员工每日考勤明细 Response VO")
  14 +@Data
  15 +@ExcelIgnoreUnannotated
  16 +public class HrmAttendanceDailyDetailRespVO {
  17 +
  18 + @Schema(description = "员工编号")
  19 + private Long employeeId;
  20 +
  21 + @Schema(description = "员工姓名")
  22 + private String employeeName;
  23 +
  24 + @Schema(description = "工号")
  25 + private String jobNumber;
  26 +
  27 + @Schema(description = "部门编号")
  28 + private Long deptId;
  29 +
  30 + @Schema(description = "部门名称")
  31 + private String deptName;
  32 +
  33 + @Schema(description = "职位名称")
  34 + private String postName;
  35 +
  36 + @ExcelProperty(value = "日期", index = 0)
  37 + @Schema(description = "日期")
  38 + private LocalDateTime attendanceTime;
  39 +
  40 + @ExcelProperty(value = "班次", index = 1)
  41 + @Schema(description = "班次")
  42 + private String shiftName;
  43 +
  44 + @Schema(description = "是否排班")
  45 + private Boolean scheduled;
  46 +
  47 + @Schema(description = "应打卡次数")
  48 + private Integer requiredClockCount;
  49 +
  50 + @Schema(description = "排班分钟数")
  51 + private Integer scheduledMinutes;
  52 +
  53 + @ExcelProperty(value = "缺卡次数", index = 8)
  54 + @Schema(description = "缺卡次数")
  55 + private Integer misscardCount;
  56 +
  57 + @Schema(description = "是否旷工")
  58 + private Boolean absenteeism;
  59 +
  60 + @Schema(description = "旷工分钟数")
  61 + private Integer absenteeismMinutes;
  62 +
  63 + @ExcelProperty(value = "旷工天数", index = 9)
  64 + @Schema(description = "旷工天数")
  65 + private BigDecimal absenteeismDays;
  66 +
  67 + @Schema(description = "是否请假")
  68 + private Boolean leaveStatus;
  69 +
  70 + @Schema(description = "请假分钟数")
  71 + private Integer leaveMinutes;
  72 +
  73 + @ExcelProperty(value = "请假天数", index = 10)
  74 + @Schema(description = "请假天数")
  75 + private BigDecimal leaveDays;
  76 +
  77 + @ExcelProperty(value = "考勤结果", index = 2)
  78 + @Schema(description = "考勤结果")
  79 + private String attendanceResult;
  80 +
  81 + @ExcelProperty(value = "打卡次数", index = 3)
  82 + @Schema(description = "打卡次数")
  83 + private Integer clockCount;
  84 +
  85 + @ExcelProperty(value = "迟到次数", index = 4)
  86 + @Schema(description = "迟到次数")
  87 + private Integer lateCount;
  88 +
  89 + @ExcelProperty(value = "迟到分钟", index = 5)
  90 + @Schema(description = "迟到分钟")
  91 + private Integer lateMinutes;
  92 +
  93 + @ExcelProperty(value = "早退次数", index = 6)
  94 + @Schema(description = "早退次数")
  95 + private Integer earlyCount;
  96 +
  97 + @ExcelProperty(value = "早退分钟", index = 7)
  98 + @Schema(description = "早退分钟")
  99 + private Integer earlyMinutes;
  100 +
  101 + @Schema(description = "打卡列表")
  102 + private List<HrmAttendanceClockRespVO> clockList;
  103 +
  104 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceDailyOverviewRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics;
  2 +
  3 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockRespVO;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +
  7 +import java.util.List;
  8 +
  9 +@Schema(description = "管理后台 - HRM 员工每日打卡概况 Response VO")
  10 +@Data
  11 +public class HrmAttendanceDailyOverviewRespVO {
  12 +
  13 + @Schema(description = "打卡记录")
  14 + private List<HrmAttendanceClockRespVO> clocks;
  15 +
  16 + @Schema(description = "考勤结果")
  17 + private String attendanceResult;
  18 +
  19 + @Schema(description = "打卡概况展示项")
  20 + private List<OverviewItem> overviews;
  21 +
  22 + @Schema(description = "管理后台 - HRM 员工每日打卡概况展示项 Response VO")
  23 + @Data
  24 + public static class OverviewItem {
  25 +
  26 + @Schema(description = "打卡类型")
  27 + private String type;
  28 +
  29 + @Schema(description = "打卡时间")
  30 + private String time;
  31 +
  32 + @Schema(description = "打卡状态")
  33 + private String status;
  34 +
  35 + @Schema(description = "考勤结果")
  36 + private String text;
  37 +
  38 + }
  39 +
  40 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthDailyOverviewRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDate;
  7 +import java.util.Map;
  8 +
  9 +@Schema(description = "管理后台 - HRM 员工月度打卡概况 Response VO")
  10 +@Data
  11 +public class HrmAttendanceMonthDailyOverviewRespVO {
  12 +
  13 + @Schema(description = "员工编号")
  14 + private Long employeeId;
  15 +
  16 + @Schema(description = "员工姓名")
  17 + private String employeeName;
  18 +
  19 + @Schema(description = "工号")
  20 + private String jobNumber;
  21 +
  22 + @Schema(description = "部门编号")
  23 + private Long deptId;
  24 +
  25 + @Schema(description = "部门名称")
  26 + private String deptName;
  27 +
  28 + @Schema(description = "职位名称")
  29 + private String postName;
  30 +
  31 + @Schema(description = "年份")
  32 + private Integer year;
  33 +
  34 + @Schema(description = "月份")
  35 + private Integer month;
  36 +
  37 + @Schema(description = "每日打卡概况,Key 为日期")
  38 + private Map<LocalDate, HrmAttendanceDailyOverviewRespVO> dailyClockMap;
  39 +
  40 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthDetailRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics;
  2 +
  3 +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave.HrmAttendanceLeaveRespVO;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +
  7 +import java.util.List;
  8 +
  9 +@Schema(description = "管理后台 - HRM 员工月度考勤详情 Response VO")
  10 +@Data
  11 +public class HrmAttendanceMonthDetailRespVO {
  12 +
  13 + @Schema(description = "月度汇总")
  14 + private HrmAttendanceMonthRecordRespVO summary;
  15 +
  16 + @Schema(description = "每日打卡明细")
  17 + private List<HrmAttendanceDailyDetailRespVO> dailyDetails;
  18 +
  19 + @Schema(description = "请假记录")
  20 + private List<HrmAttendanceLeaveRespVO> leaves;
  21 +
  22 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthRecordPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.constraints.Max;
  6 +import jakarta.validation.constraints.Min;
  7 +import jakarta.validation.constraints.NotNull;
  8 +import lombok.Data;
  9 +import lombok.EqualsAndHashCode;
  10 +import lombok.ToString;
  11 +
  12 +import java.util.List;
  13 +
  14 +@Schema(description = "管理后台 - HRM 员工月度考勤汇总分页 Request VO")
  15 +@Data
  16 +@EqualsAndHashCode(callSuper = true)
  17 +@ToString(callSuper = true)
  18 +public class HrmAttendanceMonthRecordPageReqVO extends PageParam {
  19 +
  20 + @Schema(description = "员工姓名或工号,模糊匹配", example = "张三")
  21 + private String search;
  22 +
  23 + @Schema(description = "员工编号", example = "1024")
  24 + private Long employeeId;
  25 +
  26 + @Schema(description = "部门编号数组", example = "100,101")
  27 + private List<Long> deptIds;
  28 +
  29 + @Schema(description = "年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026")
  30 + @NotNull(message = "年份不能为空")
  31 + private Integer year;
  32 +
  33 + @Schema(description = "月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "7")
  34 + @NotNull(message = "月份不能为空")
  35 + @Min(value = 1, message = "月份必须在 1 到 12 之间")
  36 + @Max(value = 12, message = "月份必须在 1 到 12 之间")
  37 + private Integer month;
  38 +
  39 + @Schema(description = "是否全勤", example = "true")
  40 + private Boolean fullAttendance;
  41 +
  42 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthRecordRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics;
  2 +
  3 +import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
  4 +import cn.idev.excel.annotation.ExcelProperty;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import lombok.Data;
  7 +
  8 +import java.math.BigDecimal;
  9 +import java.time.LocalDateTime;
  10 +
  11 +@Schema(description = "管理后台 - HRM 员工月度考勤汇总 Response VO")
  12 +@Data
  13 +@ExcelIgnoreUnannotated
  14 +public class HrmAttendanceMonthRecordRespVO {
  15 +
  16 + @ExcelProperty(value = "员工编号", index = 0)
  17 + @Schema(description = "员工编号")
  18 + private Long employeeId;
  19 +
  20 + @ExcelProperty(value = "员工姓名", index = 1)
  21 + @Schema(description = "员工姓名")
  22 + private String employeeName;
  23 +
  24 + @ExcelProperty(value = "工号", index = 2)
  25 + @Schema(description = "工号")
  26 + private String jobNumber;
  27 +
  28 + @Schema(description = "部门编号")
  29 + private Long deptId;
  30 +
  31 + @ExcelProperty(value = "部门", index = 3)
  32 + @Schema(description = "部门名称")
  33 + private String deptName;
  34 +
  35 + @Schema(description = "职位名称")
  36 + @ExcelProperty(value = "岗位", index = 4)
  37 + private String postName;
  38 +
  39 + @Schema(description = "考勤组名称")
  40 + private String attendanceGroupName;
  41 +
  42 + @Schema(description = "入职时间")
  43 + private LocalDateTime entryTime;
  44 +
  45 + @Schema(description = "员工状态")
  46 + private Integer employeeStatus;
  47 +
  48 + @Schema(description = "工作城市")
  49 + private String workCity;
  50 +
  51 + @ExcelProperty(value = "年份", index = 5)
  52 + @Schema(description = "年份")
  53 + private Integer year;
  54 +
  55 + @ExcelProperty(value = "月份", index = 6)
  56 + @Schema(description = "月份")
  57 + private Integer month;
  58 +
  59 + @ExcelProperty(value = "应出勤天数", index = 7)
  60 + @Schema(description = "应出勤天数")
  61 + private Integer attendDays;
  62 +
  63 + @ExcelProperty(value = "实际出勤天数", index = 8)
  64 + @Schema(description = "实际出勤天数")
  65 + private BigDecimal actualDays;
  66 +
  67 + @ExcelProperty(value = "迟到时长(分钟)", index = 9)
  68 + @Schema(description = "迟到时长(分钟)")
  69 + private Integer lateMinute;
  70 +
  71 + @ExcelProperty(value = "迟到次数", index = 10)
  72 + @Schema(description = "迟到次数")
  73 + private Integer lateCount;
  74 +
  75 + @ExcelProperty(value = "早退时长(分钟)", index = 11)
  76 + @Schema(description = "早退时长(分钟)")
  77 + private Integer earlyMinute;
  78 +
  79 + @ExcelProperty(value = "早退次数", index = 12)
  80 + @Schema(description = "早退次数")
  81 + private Integer earlyCount;
  82 +
  83 + @ExcelProperty(value = "缺卡次数", index = 13)
  84 + @Schema(description = "缺卡次数")
  85 + private Integer misscardCount;
  86 +
  87 + @ExcelProperty(value = "旷工天数", index = 14)
  88 + @Schema(description = "旷工天数")
  89 + private BigDecimal absenteeismDays;
  90 +
  91 + @ExcelProperty(value = "旷工时长(分钟)", index = 15)
  92 + @Schema(description = "旷工时长(分钟)")
  93 + private Integer absenteeismMinutes;
  94 +
  95 + @ExcelProperty(value = "请假天数", index = 16)
  96 + @Schema(description = "请假天数")
  97 + private BigDecimal leaveDays;
  98 +
  99 + @ExcelProperty(value = "请假时长(分钟)", index = 17)
  100 + @Schema(description = "请假时长(分钟)")
  101 + private Integer leaveMinutes;
  102 +
  103 + @ExcelProperty(value = "迟到扣款", index = 18)
  104 + @Schema(description = "迟到扣款")
  105 + private BigDecimal lateDeductAmount;
  106 +
  107 + @ExcelProperty(value = "早退扣款", index = 19)
  108 + @Schema(description = "早退扣款")
  109 + private BigDecimal earlyDeductAmount;
  110 +
  111 + @ExcelProperty(value = "缺卡扣款", index = 20)
  112 + @Schema(description = "缺卡扣款")
  113 + private BigDecimal misscardDeductAmount;
  114 +
  115 + @ExcelProperty(value = "旷工扣款", index = 21)
  116 + @Schema(description = "旷工扣款")
  117 + private BigDecimal absenteeismDeductAmount;
  118 +
  119 + @ExcelProperty(value = "考勤扣款合计", index = 22)
  120 + @Schema(description = "考勤扣款合计")
  121 + private BigDecimal attendanceDeductAmount;
  122 +
  123 + @Schema(description = "是否全勤")
  124 + private Boolean fullAttendance;
  125 +
  126 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeCertificateController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.certificate.HrmEmployeeCertificateRespVO;
  6 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.certificate.HrmEmployeeCertificateSaveReqVO;
  7 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience.HrmEmployeeCertificateDO;
  8 +import cn.iocoder.yudao.module.hrm.service.employee.experience.HrmEmployeeCertificateService;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +import io.swagger.v3.oas.annotations.Parameter;
  11 +import io.swagger.v3.oas.annotations.tags.Tag;
  12 +import jakarta.annotation.Resource;
  13 +import jakarta.validation.Valid;
  14 +import org.springframework.security.access.prepost.PreAuthorize;
  15 +import org.springframework.validation.annotation.Validated;
  16 +import org.springframework.web.bind.annotation.DeleteMapping;
  17 +import org.springframework.web.bind.annotation.GetMapping;
  18 +import org.springframework.web.bind.annotation.PostMapping;
  19 +import org.springframework.web.bind.annotation.PutMapping;
  20 +import org.springframework.web.bind.annotation.RequestBody;
  21 +import org.springframework.web.bind.annotation.RequestMapping;
  22 +import org.springframework.web.bind.annotation.RequestParam;
  23 +import org.springframework.web.bind.annotation.RestController;
  24 +
  25 +import java.util.List;
  26 +
  27 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  28 +
  29 +@Tag(name = "管理后台 - HRM 员工证书")
  30 +@RestController
  31 +@RequestMapping("/hrm/employee/certificate")
  32 +@Validated
  33 +public class HrmEmployeeCertificateController {
  34 +
  35 + @Resource
  36 + private HrmEmployeeCertificateService certificateService;
  37 +
  38 + @GetMapping("/list")
  39 + @Operation(summary = "获得员工证书列表")
  40 + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024")
  41 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  42 + public CommonResult<List<HrmEmployeeCertificateRespVO>> getCertificateList(
  43 + @RequestParam("employeeId") Long employeeId) {
  44 + List<HrmEmployeeCertificateDO> certificates = certificateService.getCertificateListByEmployeeId(employeeId);
  45 + return success(BeanUtils.toBean(certificates, HrmEmployeeCertificateRespVO.class));
  46 + }
  47 +
  48 + @PostMapping("/create")
  49 + @Operation(summary = "创建员工证书")
  50 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  51 + public CommonResult<Long> createCertificate(
  52 + @Valid @RequestBody HrmEmployeeCertificateSaveReqVO reqVO) {
  53 + return success(certificateService.createCertificate(reqVO));
  54 + }
  55 +
  56 + @PutMapping("/update")
  57 + @Operation(summary = "更新员工证书")
  58 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  59 + public CommonResult<Boolean> updateCertificate(
  60 + @Valid @RequestBody HrmEmployeeCertificateSaveReqVO reqVO) {
  61 + certificateService.updateCertificate(reqVO);
  62 + return success(true);
  63 + }
  64 +
  65 + @DeleteMapping("/delete")
  66 + @Operation(summary = "删除员工证书")
  67 + @Parameter(name = "id", description = "证书编号", required = true, example = "1024")
  68 + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')")
  69 + public CommonResult<Boolean> deleteCertificate(@RequestParam("id") Long id) {
  70 + certificateService.deleteCertificate(id);
  71 + return success(true);
  72 + }
  73 +
  74 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeChangeRecordController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord.HrmEmployeeChangeRecordRespVO;
  7 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeChangeRecordDO;
  8 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO;
  9 +import cn.iocoder.yudao.module.hrm.service.employee.employment.HrmEmployeeChangeRecordService;
  10 +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService;
  11 +import cn.iocoder.yudao.module.system.api.dept.DeptApi;
  12 +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
  13 +import io.swagger.v3.oas.annotations.Operation;
  14 +import io.swagger.v3.oas.annotations.Parameter;
  15 +import io.swagger.v3.oas.annotations.tags.Tag;
  16 +import jakarta.annotation.Resource;
  17 +import org.springframework.security.access.prepost.PreAuthorize;
  18 +import org.springframework.validation.annotation.Validated;
  19 +import org.springframework.web.bind.annotation.GetMapping;
  20 +import org.springframework.web.bind.annotation.RequestMapping;
  21 +import org.springframework.web.bind.annotation.RequestParam;
  22 +import org.springframework.web.bind.annotation.RestController;
  23 +
  24 +import java.util.List;
  25 +import java.util.Map;
  26 +import java.util.Set;
  27 +import java.util.stream.Stream;
  28 +
  29 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  30 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
  31 +
  32 +@Tag(name = "管理后台 - HRM 员工异动记录")
  33 +@RestController
  34 +@RequestMapping("/hrm/employee/change-record")
  35 +@Validated
  36 +public class HrmEmployeeChangeRecordController {
  37 +
  38 + @Resource
  39 + private HrmEmployeeChangeRecordService changeRecordService;
  40 + @Resource
  41 + private HrmEmployeeService employeeService;
  42 + @Resource
  43 + private DeptApi deptApi;
  44 +
  45 + @GetMapping("/list")
  46 + @Operation(summary = "获得员工异动记录列表")
  47 + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024")
  48 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  49 + public CommonResult<List<HrmEmployeeChangeRecordRespVO>> getEmployeeChangeRecordList(
  50 + @RequestParam("employeeId") Long employeeId) {
  51 + return success(buildEmployeeChangeRecordRespVOList(
  52 + changeRecordService.getEmployeeChangeRecordListByEmployeeId(employeeId)));
  53 + }
  54 +
  55 + // ==================== 拼接 VO ====================
  56 +
  57 + private List<HrmEmployeeChangeRecordRespVO> buildEmployeeChangeRecordRespVOList(
  58 + List<HrmEmployeeChangeRecordDO> changeRecords) {
  59 + Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
  60 + convertSetByFlatMap(changeRecords, changeRecord -> Stream.of(changeRecord.getOldDeptId(), changeRecord.getNewDeptId())));
  61 + Map<Long, HrmEmployeeDO> leaderEmployeeMap = employeeService.getEmployeeMap(
  62 + convertSetByFlatMap(changeRecords, changeRecord -> Stream.of(changeRecord.getOldLeaderEmployeeId(), changeRecord.getNewLeaderEmployeeId())));
  63 + return BeanUtils.toBean(changeRecords, HrmEmployeeChangeRecordRespVO.class, changeRecord -> {
  64 + MapUtils.findAndThen(deptMap, changeRecord.getOldDeptId(),
  65 + dept -> changeRecord.setOldDeptName(dept.getName()));
  66 + MapUtils.findAndThen(deptMap, changeRecord.getNewDeptId(),
  67 + dept -> changeRecord.setNewDeptName(dept.getName()));
  68 + MapUtils.findAndThen(leaderEmployeeMap, changeRecord.getOldLeaderEmployeeId(),
  69 + leader -> changeRecord.setOldLeaderEmployeeName(leader.getName()));
  70 + MapUtils.findAndThen(leaderEmployeeMap, changeRecord.getNewLeaderEmployeeId(),
  71 + leader -> changeRecord.setNewLeaderEmployeeName(leader.getName()));
  72 + });
  73 + }
  74 +
  75 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContactController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contact.HrmEmployeeContactRespVO;
  6 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contact.HrmEmployeeContactSaveReqVO;
  7 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeContactDO;
  8 +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeContactService;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +import io.swagger.v3.oas.annotations.Parameter;
  11 +import io.swagger.v3.oas.annotations.tags.Tag;
  12 +import jakarta.annotation.Resource;
  13 +import jakarta.validation.Valid;
  14 +import org.springframework.security.access.prepost.PreAuthorize;
  15 +import org.springframework.validation.annotation.Validated;
  16 +import org.springframework.web.bind.annotation.DeleteMapping;
  17 +import org.springframework.web.bind.annotation.GetMapping;
  18 +import org.springframework.web.bind.annotation.PostMapping;
  19 +import org.springframework.web.bind.annotation.PutMapping;
  20 +import org.springframework.web.bind.annotation.RequestBody;
  21 +import org.springframework.web.bind.annotation.RequestMapping;
  22 +import org.springframework.web.bind.annotation.RequestParam;
  23 +import org.springframework.web.bind.annotation.RestController;
  24 +
  25 +import java.util.List;
  26 +
  27 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  28 +
  29 +@Tag(name = "管理后台 - HRM 员工联系人")
  30 +@RestController
  31 +@RequestMapping("/hrm/employee/contact")
  32 +@Validated
  33 +public class HrmEmployeeContactController {
  34 +
  35 + @Resource
  36 + private HrmEmployeeContactService contactService;
  37 +
  38 + @GetMapping("/list")
  39 + @Operation(summary = "获得员工联系人列表")
  40 + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024")
  41 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  42 + public CommonResult<List<HrmEmployeeContactRespVO>> getContactList(
  43 + @RequestParam("employeeId") Long employeeId) {
  44 + List<HrmEmployeeContactDO> contacts = contactService.getContactListByEmployeeId(employeeId);
  45 + return success(BeanUtils.toBean(contacts, HrmEmployeeContactRespVO.class));
  46 + }
  47 +
  48 + @PostMapping("/create")
  49 + @Operation(summary = "创建员工联系人")
  50 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  51 + public CommonResult<Long> createContact(@Valid @RequestBody HrmEmployeeContactSaveReqVO reqVO) {
  52 + return success(contactService.createContact(reqVO));
  53 + }
  54 +
  55 + @PutMapping("/update")
  56 + @Operation(summary = "更新员工联系人")
  57 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  58 + public CommonResult<Boolean> updateContact(@Valid @RequestBody HrmEmployeeContactSaveReqVO reqVO) {
  59 + contactService.updateContact(reqVO);
  60 + return success(true);
  61 + }
  62 +
  63 + @DeleteMapping("/delete")
  64 + @Operation(summary = "删除员工联系人")
  65 + @Parameter(name = "id", description = "联系人编号", required = true, example = "1024")
  66 + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')")
  67 + public CommonResult<Boolean> deleteContact(@RequestParam("id") Long id) {
  68 + contactService.deleteContact(id);
  69 + return success(true);
  70 + }
  71 +
  72 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contract.HrmEmployeeContractRespVO;
  6 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contract.HrmEmployeeContractSaveReqVO;
  7 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeContractDO;
  8 +import cn.iocoder.yudao.module.hrm.service.employee.employment.HrmEmployeeContractService;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +import io.swagger.v3.oas.annotations.Parameter;
  11 +import io.swagger.v3.oas.annotations.tags.Tag;
  12 +import jakarta.annotation.Resource;
  13 +import jakarta.validation.Valid;
  14 +import org.springframework.security.access.prepost.PreAuthorize;
  15 +import org.springframework.validation.annotation.Validated;
  16 +import org.springframework.web.bind.annotation.DeleteMapping;
  17 +import org.springframework.web.bind.annotation.GetMapping;
  18 +import org.springframework.web.bind.annotation.PostMapping;
  19 +import org.springframework.web.bind.annotation.PutMapping;
  20 +import org.springframework.web.bind.annotation.RequestBody;
  21 +import org.springframework.web.bind.annotation.RequestMapping;
  22 +import org.springframework.web.bind.annotation.RequestParam;
  23 +import org.springframework.web.bind.annotation.RestController;
  24 +
  25 +import java.util.List;
  26 +
  27 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  28 +
  29 +@Tag(name = "管理后台 - HRM 员工合同")
  30 +@RestController
  31 +@RequestMapping("/hrm/employee/contract")
  32 +@Validated
  33 +public class HrmEmployeeContractController {
  34 +
  35 + @Resource
  36 + private HrmEmployeeContractService contractService;
  37 +
  38 + @GetMapping("/list")
  39 + @Operation(summary = "获得员工合同列表")
  40 + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024")
  41 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  42 + public CommonResult<List<HrmEmployeeContractRespVO>> getContractList(
  43 + @RequestParam("employeeId") Long employeeId) {
  44 + List<HrmEmployeeContractDO> contracts = contractService.getContractListByEmployeeId(employeeId);
  45 + return success(BeanUtils.toBean(contracts, HrmEmployeeContractRespVO.class));
  46 + }
  47 +
  48 + @PostMapping("/create")
  49 + @Operation(summary = "创建员工合同")
  50 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  51 + public CommonResult<Long> createContract(@Valid @RequestBody HrmEmployeeContractSaveReqVO reqVO) {
  52 + return success(contractService.createContract(reqVO));
  53 + }
  54 +
  55 + @PutMapping("/update")
  56 + @Operation(summary = "更新员工合同")
  57 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  58 + public CommonResult<Boolean> updateContract(@Valid @RequestBody HrmEmployeeContractSaveReqVO reqVO) {
  59 + contractService.updateContract(reqVO);
  60 + return success(true);
  61 + }
  62 +
  63 + @DeleteMapping("/delete")
  64 + @Operation(summary = "删除员工合同")
  65 + @Parameter(name = "id", description = "合同编号", required = true, example = "1024")
  66 + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')")
  67 + public CommonResult<Boolean> deleteContract(@RequestParam("id") Long id) {
  68 + contractService.deleteContract(id);
  69 + return success(true);
  70 + }
  71 +
  72 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  8 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  9 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  10 +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
  11 +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
  12 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord.HrmEmployeeConvertToFullTimeReqVO;
  13 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord.HrmEmployeeDemoteReqVO;
  14 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord.HrmEmployeePromoteReqVO;
  15 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord.HrmEmployeeRegularReqVO;
  16 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord.HrmEmployeeTransferReqVO;
  17 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeCancelQuitReqVO;
  18 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeConfirmEntryReqVO;
  19 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeCreateFromUserReqVO;
  20 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeDeptStatisticsRespVO;
  21 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeImportExcelVO;
  22 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeImportRespVO;
  23 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeListReqVO;
  24 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeePageReqVO;
  25 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeNotifyRespVO;
  26 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeQuitReqVO;
  27 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeRehireReqVO;
  28 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeRespVO;
  29 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeSaveReqVO;
  30 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeStatusCountRespVO;
  31 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO;
  32 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeSalaryCardDO;
  33 +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.employee.HrmInsuranceEmployeeInfoDO;
  34 +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.config.HrmRecruitChannelDO;
  35 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeEntryStatusEnum;
  36 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum;
  37 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusTabEnum;
  38 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTypeEnum;
  39 +import cn.iocoder.yudao.module.hrm.service.employee.employment.HrmEmployeeSalaryCardService;
  40 +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService;
  41 +import cn.iocoder.yudao.module.hrm.service.insurance.employee.HrmInsuranceEmployeeInfoService;
  42 +import cn.iocoder.yudao.module.hrm.service.recruit.config.HrmRecruitChannelService;
  43 +import cn.iocoder.yudao.module.system.api.dept.DeptApi;
  44 +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
  45 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  46 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
  47 +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum;
  48 +import io.swagger.v3.oas.annotations.Operation;
  49 +import io.swagger.v3.oas.annotations.Parameter;
  50 +import io.swagger.v3.oas.annotations.Parameters;
  51 +import io.swagger.v3.oas.annotations.tags.Tag;
  52 +import jakarta.annotation.Resource;
  53 +import jakarta.servlet.http.HttpServletResponse;
  54 +import jakarta.validation.Valid;
  55 +import jakarta.validation.constraints.NotEmpty;
  56 +import jakarta.validation.constraints.Size;
  57 +import org.springframework.security.access.prepost.PreAuthorize;
  58 +import org.springframework.validation.annotation.Validated;
  59 +import org.springframework.web.bind.annotation.DeleteMapping;
  60 +import org.springframework.web.bind.annotation.GetMapping;
  61 +import org.springframework.web.bind.annotation.PostMapping;
  62 +import org.springframework.web.bind.annotation.PutMapping;
  63 +import org.springframework.web.bind.annotation.RequestBody;
  64 +import org.springframework.web.bind.annotation.RequestMapping;
  65 +import org.springframework.web.bind.annotation.RequestParam;
  66 +import org.springframework.web.bind.annotation.RestController;
  67 +import org.springframework.web.multipart.MultipartFile;
  68 +
  69 +import java.io.IOException;
  70 +import java.time.LocalDate;
  71 +import java.time.LocalDateTime;
  72 +import java.util.Arrays;
  73 +import java.util.Collections;
  74 +import java.util.List;
  75 +import java.util.Map;
  76 +import java.util.Set;
  77 +
  78 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  79 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  80 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  81 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getFirst;
  82 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.sum;
  83 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.getYearsBetween;
  84 +
  85 +@Tag(name = "管理后台 - HRM 员工档案")
  86 +@RestController
  87 +@RequestMapping("/hrm/employee")
  88 +@Validated
  89 +public class HrmEmployeeController {
  90 +
  91 + @Resource
  92 + private HrmEmployeeService employeeService;
  93 + @Resource
  94 + private HrmRecruitChannelService recruitChannelService;
  95 + @Resource
  96 + private HrmInsuranceEmployeeInfoService insuranceEmployeeInfoService;
  97 + @Resource
  98 + private HrmEmployeeSalaryCardService employeeSalaryCardService;
  99 +
  100 + @Resource
  101 + private AdminUserApi adminUserApi;
  102 + @Resource
  103 + private DeptApi deptApi;
  104 +
  105 + @PostMapping("/create")
  106 + @Operation(summary = "创建员工档案")
  107 + @PreAuthorize("@ss.hasPermission('hrm:employee:create')")
  108 + public CommonResult<Long> createEmployee(@Valid @RequestBody HrmEmployeeSaveReqVO createReqVO) {
  109 + return success(employeeService.createEmployee(createReqVO));
  110 + }
  111 +
  112 + @PostMapping("/create-list")
  113 + @Operation(summary = "从未建档后台用户批量创建员工档案")
  114 + @PreAuthorize("@ss.hasPermission('hrm:employee:create')")
  115 + public CommonResult<List<Long>> createEmployeeList(
  116 + @RequestBody @NotEmpty(message = "待创建员工列表不能为空")
  117 + @Size(max = 100, message = "单次最多创建 100 名员工")
  118 + List<@Valid HrmEmployeeCreateFromUserReqVO> createReqVOList) {
  119 + return success(employeeService.createEmployeeList(createReqVOList));
  120 + }
  121 +
  122 + @GetMapping("/bound-user-id-list")
  123 + @Operation(summary = "获得已经建立员工档案的后台用户编号列表")
  124 + @PreAuthorize("@ss.hasPermission('hrm:employee:create')")
  125 + public CommonResult<List<Long>> getBoundUserIdList() {
  126 + return success(employeeService.getBoundUserIdList());
  127 + }
  128 +
  129 + @PostMapping("/send-profile-fill-message")
  130 + @Operation(summary = "发送填写员工档案通知")
  131 + @Parameter(name = "ids", description = "员工编号数组", required = true, example = "1024,1025")
  132 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  133 + public CommonResult<HrmEmployeeNotifyRespVO> sendEmployeeProfileFillMessage(
  134 + @RequestParam("ids") @NotEmpty(message = "员工不能为空")
  135 + @Size(max = 100, message = "单次最多通知 100 名员工") List<Long> ids) {
  136 + return success(employeeService.sendEmployeeProfileFillMessage(ids));
  137 + }
  138 +
  139 + @PutMapping("/update")
  140 + @Operation(summary = "更新员工档案")
  141 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  142 + public CommonResult<Boolean> updateEmployee(@Valid @RequestBody HrmEmployeeSaveReqVO updateReqVO) {
  143 + employeeService.updateEmployee(updateReqVO);
  144 + return success(true);
  145 + }
  146 +
  147 + @DeleteMapping("/delete")
  148 + @Operation(summary = "删除员工档案")
  149 + @Parameter(name = "id", description = "员工档案编号", required = true, example = "1024")
  150 + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')")
  151 + public CommonResult<Boolean> deleteEmployee(@RequestParam("id") Long id) {
  152 + employeeService.deleteEmployee(id);
  153 + return success(true);
  154 + }
  155 +
  156 + @DeleteMapping("/delete-list")
  157 + @Operation(summary = "批量删除员工档案")
  158 + @Parameter(name = "ids", description = "员工档案编号数组", required = true, example = "1024,1025")
  159 + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')")
  160 + public CommonResult<Boolean> deleteEmployeeList(@RequestParam("ids") @NotEmpty List<Long> ids) {
  161 + employeeService.deleteEmployeeList(ids);
  162 + return success(true);
  163 + }
  164 +
  165 + @GetMapping("/get")
  166 + @Operation(summary = "获得员工档案")
  167 + @Parameter(name = "id", description = "员工档案编号", required = true, example = "1024")
  168 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  169 + public CommonResult<HrmEmployeeRespVO> getEmployee(@RequestParam("id") Long id) {
  170 + return success(buildEmployeeRespVO(employeeService.getEmployee(id)));
  171 + }
  172 +
  173 + @PutMapping("/confirm-entry")
  174 + @Operation(summary = "确认员工入职")
  175 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  176 + public CommonResult<Boolean> confirmEmployeeEntry(@Valid @RequestBody HrmEmployeeConfirmEntryReqVO reqVO) {
  177 + employeeService.confirmEmployeeEntry(reqVO);
  178 + return success(true);
  179 + }
  180 +
  181 + @PostMapping("/rehire")
  182 + @Operation(summary = "办理离职员工再入职")
  183 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  184 + public CommonResult<Boolean> rehireEmployee(@Valid @RequestBody HrmEmployeeRehireReqVO reqVO) {
  185 + employeeService.rehireEmployee(reqVO);
  186 + return success(true);
  187 + }
  188 +
  189 + @PostMapping("/regular")
  190 + @Operation(summary = "办理员工转正")
  191 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  192 + public CommonResult<Boolean> regularEmployee(@Valid @RequestBody HrmEmployeeRegularReqVO reqVO) {
  193 + employeeService.regularEmployee(reqVO);
  194 + return success(true);
  195 + }
  196 +
  197 + @PostMapping("/transfer")
  198 + @Operation(summary = "办理员工调岗")
  199 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  200 + public CommonResult<Boolean> transferEmployee(@Valid @RequestBody HrmEmployeeTransferReqVO reqVO) {
  201 + employeeService.transferEmployee(reqVO);
  202 + return success(true);
  203 + }
  204 +
  205 + @PostMapping("/promote")
  206 + @Operation(summary = "办理员工晋升")
  207 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  208 + public CommonResult<Boolean> promoteEmployee(@Valid @RequestBody HrmEmployeePromoteReqVO reqVO) {
  209 + employeeService.promoteEmployee(reqVO);
  210 + return success(true);
  211 + }
  212 +
  213 + @PostMapping("/demote")
  214 + @Operation(summary = "办理员工降级")
  215 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  216 + public CommonResult<Boolean> demoteEmployee(@Valid @RequestBody HrmEmployeeDemoteReqVO reqVO) {
  217 + employeeService.demoteEmployee(reqVO);
  218 + return success(true);
  219 + }
  220 +
  221 + @PostMapping("/convert-to-full-time")
  222 + @Operation(summary = "办理员工转为全职")
  223 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  224 + public CommonResult<Boolean> convertEmployeeToFullTime(
  225 + @Valid @RequestBody HrmEmployeeConvertToFullTimeReqVO reqVO) {
  226 + employeeService.convertEmployeeToFullTime(reqVO);
  227 + return success(true);
  228 + }
  229 +
  230 + @PostMapping("/quit")
  231 + @Operation(summary = "办理员工离职")
  232 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  233 + public CommonResult<Boolean> quitEmployee(@Valid @RequestBody HrmEmployeeQuitReqVO reqVO) {
  234 + employeeService.quitEmployee(reqVO);
  235 + return success(true);
  236 + }
  237 +
  238 + @PutMapping("/cancel-quit")
  239 + @Operation(summary = "取消员工离职")
  240 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  241 + public CommonResult<Boolean> cancelEmployeeQuit(@Valid @RequestBody HrmEmployeeCancelQuitReqVO reqVO) {
  242 + employeeService.cancelEmployeeQuit(reqVO);
  243 + return success(true);
  244 + }
  245 +
  246 + @GetMapping("/page")
  247 + @Operation(summary = "获得员工档案分页")
  248 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  249 + public CommonResult<PageResult<HrmEmployeeRespVO>> getEmployeePage(@Validated HrmEmployeePageReqVO pageReqVO) {
  250 + PageResult<HrmEmployeeDO> pageResult = employeeService.getEmployeePage(pageReqVO);
  251 + return success(new PageResult<>(buildEmployeeRespVOList(pageResult.getList()), pageResult.getTotal()));
  252 + }
  253 +
  254 + @GetMapping("/list")
  255 + @Operation(summary = "获得员工列表", description = "用于表单回显已选员工")
  256 + @Parameter(name = "ids", description = "员工编号列表", required = true, example = "[1024]")
  257 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  258 + public CommonResult<List<HrmEmployeeRespVO>> getEmployeeList(@RequestParam("ids") @NotEmpty List<Long> ids) {
  259 + return success(buildEmployeeRespVOList(employeeService.getEmployeeListByIds(ids)));
  260 + }
  261 +
  262 + @GetMapping("/simple-page")
  263 + @Operation(summary = "获得员工精简分页", description = "只返回员工选择器需要的非敏感字段")
  264 + public CommonResult<PageResult<HrmEmployeeRespVO>> getEmployeeSimplePage(
  265 + @Validated HrmEmployeePageReqVO pageReqVO) {
  266 + PageResult<HrmEmployeeDO> pageResult = employeeService.getEmployeePage(pageReqVO);
  267 + return success(new PageResult<>(buildEmployeeSimpleRespVOList(pageResult.getList()), pageResult.getTotal()));
  268 + }
  269 +
  270 + @GetMapping("/simple-list")
  271 + @Operation(summary = "获得员工精简列表", description = "用于员工选择器回显已选员工")
  272 + @Parameter(name = "ids", description = "员工编号列表", required = true, example = "[1024]")
  273 + public CommonResult<List<HrmEmployeeRespVO>> getEmployeeSimpleList(
  274 + @RequestParam("ids") @NotEmpty List<Long> ids) {
  275 + return success(buildEmployeeSimpleRespVOList(employeeService.getEmployeeListByIds(ids)));
  276 + }
  277 +
  278 + @GetMapping("/export-excel")
  279 + @Operation(summary = "导出员工档案")
  280 + @PreAuthorize("@ss.hasPermission('hrm:employee:export')")
  281 + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT)
  282 + public void exportEmployee(@Validated HrmEmployeePageReqVO exportReqVO,
  283 + HttpServletResponse response) throws IOException {
  284 + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
  285 + List<HrmEmployeeRespVO> list = buildEmployeeExportRespVOList(employeeService.getEmployeeList(
  286 + BeanUtils.toBean(exportReqVO, HrmEmployeeListReqVO.class)));
  287 + ExcelUtils.write(response, "员工档案.xlsx", "数据", HrmEmployeeRespVO.class, list);
  288 + }
  289 +
  290 + @GetMapping("/get-import-template")
  291 + @Operation(summary = "获得员工档案导入模板")
  292 + @PreAuthorize("@ss.hasPermission('hrm:employee:import')")
  293 + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT)
  294 + public void importTemplate(HttpServletResponse response) throws IOException {
  295 + List<HrmEmployeeImportExcelVO> list = Collections.singletonList(
  296 + HrmEmployeeImportExcelVO.builder()
  297 + .name("张三").jobNumber("HRM001").mobile("15601691300")
  298 + .country("中国").nation("汉族").idType(5).idNumber("OTHER-19900101")
  299 + .sex(1).email("hrm@example.com").nativePlace("浙江杭州")
  300 + .birthday(LocalDateTime.of(1990, 1, 1, 0, 0))
  301 + .address("杭州市西湖区").highestEducation(8)
  302 + .deptId(1L).leaderJobNumber("HRM000")
  303 + .postName("Java 工程师").postLevel("P6")
  304 + .entryStatus(HrmEmployeeEntryStatusEnum.ACTIVE.getStatus())
  305 + .status(HrmEmployeeStatusEnum.REGULAR.getStatus())
  306 + .type(HrmEmployeeTypeEnum.FORMAL.getType()).probation(3)
  307 + .entryTime(LocalDateTime.of(2026, 7, 9, 9, 0))
  308 + .regularTime(LocalDateTime.of(2026, 10, 9, 9, 0))
  309 + .workCity("杭州").workAddress("西湖区").workDetailAddress("文三路")
  310 + .companyAgeStartTime(LocalDateTime.of(2026, 7, 9, 9, 0))
  311 + .userMobile("15601691301")
  312 + .bankCardNumber("622202600001").bankAreaId(330100).bankName("招商银行")
  313 + .bankBranchName("杭州高新支行").firstSocialSecurity(false)
  314 + .firstAccumulationFund(false).socialSecurityNumber("SB20260001")
  315 + .accumulationFundNumber("GJJ20260001")
  316 + .socialSecurityStartMonth(LocalDateTime.of(2026, 7, 1, 0, 0))
  317 + .schemeName("杭州标准参保方案")
  318 + .remark("示例员工").build()
  319 + );
  320 + ExcelUtils.write(response, "员工档案导入模板.xlsx", "员工列表", HrmEmployeeImportExcelVO.class, list);
  321 + }
  322 +
  323 + @PostMapping("/import")
  324 + @Operation(summary = "导入员工档案")
  325 + @Parameters({
  326 + @Parameter(name = "file", description = "Excel 文件", required = true),
  327 + @Parameter(name = "duplicateStrategy", description = "重复员工处理策略:1 跳过,2 覆盖,3 判失败", example = "3")
  328 + })
  329 + @PreAuthorize("@ss.hasPermission('hrm:employee:import')")
  330 + @ApiAccessLog(operateType = OperateTypeEnum.IMPORT)
  331 + public CommonResult<HrmEmployeeImportRespVO> importExcel(
  332 + @RequestParam("file") MultipartFile file,
  333 + @RequestParam(value = "duplicateStrategy", required = false, defaultValue = "3") Integer duplicateStrategy)
  334 + throws Exception {
  335 + List<HrmEmployeeImportExcelVO> list = ExcelUtils.read(file, HrmEmployeeImportExcelVO.class);
  336 + return success(employeeService.importEmployeeList(list, duplicateStrategy));
  337 + }
  338 +
  339 + @GetMapping("/status-count")
  340 + @Operation(summary = "获得员工状态统计")
  341 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  342 + public CommonResult<List<HrmEmployeeStatusCountRespVO>> getEmployeeStatusCount(
  343 + @Validated HrmEmployeePageReqVO pageReqVO) {
  344 + Map<Integer, Long> countMap = employeeService.getEmployeeStatusCount(pageReqVO);
  345 + return success(convertList(Arrays.asList(HrmEmployeeStatusTabEnum.values()),
  346 + status -> new HrmEmployeeStatusCountRespVO(status.getStatus(),
  347 + countMap.getOrDefault(status.getStatus(), 0L))));
  348 + }
  349 +
  350 + @GetMapping("/dept-statistics")
  351 + @Operation(summary = "获得员工部门统计")
  352 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  353 + public CommonResult<List<HrmEmployeeDeptStatisticsRespVO>> getEmployeeDeptStatistics() {
  354 + Map<Long, Map<Integer, Long>> countMap = employeeService.getEmployeeCountMapByDeptAndType();
  355 + return success(convertList(countMap.entrySet(), entry -> {
  356 + Map<Integer, Long> typeCountMap = entry.getValue();
  357 + long activeCount = sum(typeCountMap.values(), Long::longValue);
  358 + return new HrmEmployeeDeptStatisticsRespVO(entry.getKey(), activeCount,
  359 + typeCountMap.getOrDefault(HrmEmployeeTypeEnum.FORMAL.getType(), 0L),
  360 + typeCountMap.getOrDefault(HrmEmployeeTypeEnum.INFORMAL.getType(), 0L));
  361 + }));
  362 + }
  363 +
  364 + // ==================== 拼接 VO ====================
  365 +
  366 + private HrmEmployeeRespVO buildEmployeeRespVO(HrmEmployeeDO employee) {
  367 + if (employee == null) {
  368 + return null;
  369 + }
  370 + return getFirst(buildEmployeeRespVOList(Collections.singletonList(employee)));
  371 + }
  372 +
  373 + private List<HrmEmployeeRespVO> buildEmployeeRespVOList(List<HrmEmployeeDO> list) {
  374 + if (CollUtil.isEmpty(list)) {
  375 + return Collections.emptyList();
  376 + }
  377 + // 1.1 后台用户信息
  378 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
  379 + convertSet(list, HrmEmployeeDO::getUserId));
  380 + // 1.2 部门信息
  381 + Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(list, HrmEmployeeDO::getDeptId));
  382 + // 1.3 直属上级员工信息
  383 + Map<Long, HrmEmployeeDO> leaderEmployeeMap = employeeService.getEmployeeMap(
  384 + convertSet(list, HrmEmployeeDO::getLeaderEmployeeId));
  385 + // 1.4 招聘渠道信息
  386 + Map<Long, HrmRecruitChannelDO> channelMap = recruitChannelService.getRecruitChannelMap(
  387 + convertSet(list, HrmEmployeeDO::getChannelId));
  388 + // 1.5 工资卡和社保资料
  389 + Set<Long> employeeIds = convertSet(list, HrmEmployeeDO::getId);
  390 + Map<Long, HrmEmployeeSalaryCardDO> salaryCardMap =
  391 + employeeSalaryCardService.getSalaryCardMap(employeeIds);
  392 + Map<Long, HrmInsuranceEmployeeInfoDO> insuranceEmployeeInfoMap =
  393 + insuranceEmployeeInfoService.getInsuranceEmployeeInfoMap(employeeIds);
  394 +
  395 + // 2. 拼接响应
  396 + LocalDate today = LocalDate.now();
  397 + return BeanUtils.toBean(list, HrmEmployeeRespVO.class, vo -> {
  398 + MapUtils.findAndThen(userMap, vo.getUserId(), user -> vo.setUserNickname(user.getNickname()));
  399 + MapUtils.findAndThen(deptMap, vo.getDeptId(), dept -> vo.setDeptName(dept.getName()));
  400 + MapUtils.findAndThen(leaderEmployeeMap, vo.getLeaderEmployeeId(),
  401 + leader -> vo.setLeaderEmployeeName(leader.getName()));
  402 + MapUtils.findAndThen(channelMap, vo.getChannelId(),
  403 + channel -> vo.setChannelName(channel.getName()));
  404 + MapUtils.findAndThen(salaryCardMap, vo.getId(), salaryCard -> vo
  405 + .setSalaryCardNumber(salaryCard.getBankCardNumber())
  406 + .setSalaryCardAreaId(salaryCard.getBankAreaId())
  407 + .setSalaryCardAreaName(AreaUtils.format(salaryCard.getBankAreaId()))
  408 + .setSalaryCardBankName(salaryCard.getBankName())
  409 + .setSalaryCardBankBranchName(salaryCard.getBankBranchName()));
  410 + MapUtils.findAndThen(insuranceEmployeeInfoMap, vo.getId(), socialSecurity -> vo
  411 + .setSocialSecurityNumber(socialSecurity.getSocialSecurityNumber())
  412 + .setAccumulationFundNumber(socialSecurity.getAccumulationFundNumber()));
  413 + if (vo.getBirthday() != null) {
  414 + vo.setAge(getYearsBetween(vo.getBirthday().toLocalDate(), today));
  415 + }
  416 + if (vo.getCompanyAgeStartTime() != null) {
  417 + vo.setCompanyAge(getYearsBetween(vo.getCompanyAgeStartTime().toLocalDate(), today));
  418 + }
  419 + });
  420 + }
  421 +
  422 + private List<HrmEmployeeRespVO> buildEmployeeSimpleRespVOList(List<HrmEmployeeDO> list) {
  423 + if (CollUtil.isEmpty(list)) {
  424 + return Collections.emptyList();
  425 + }
  426 + // 1. 查询部门和直属上级信息
  427 + Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(list, HrmEmployeeDO::getDeptId));
  428 + Map<Long, HrmEmployeeDO> leaderEmployeeMap = employeeService.getEmployeeMap(
  429 + convertSet(list, HrmEmployeeDO::getLeaderEmployeeId));
  430 +
  431 + // 2. 拼接精简响应
  432 + return convertList(list, employee -> {
  433 + HrmEmployeeRespVO vo = new HrmEmployeeRespVO()
  434 + .setId(employee.getId()).setName(employee.getName())
  435 + .setJobNumber(employee.getJobNumber()).setMobile(employee.getMobile())
  436 + .setDeptId(employee.getDeptId()).setLeaderEmployeeId(employee.getLeaderEmployeeId())
  437 + .setPostName(employee.getPostName()).setPostLevel(employee.getPostLevel())
  438 + .setEntryStatus(employee.getEntryStatus()).setStatus(employee.getStatus());
  439 + MapUtils.findAndThen(deptMap, employee.getDeptId(), dept -> vo.setDeptName(dept.getName()));
  440 + MapUtils.findAndThen(leaderEmployeeMap, employee.getLeaderEmployeeId(),
  441 + leader -> vo.setLeaderEmployeeName(leader.getName()));
  442 + return vo;
  443 + });
  444 + }
  445 +
  446 + private List<HrmEmployeeRespVO> buildEmployeeExportRespVOList(List<HrmEmployeeDO> list) {
  447 + if (CollUtil.isEmpty(list)) {
  448 + return Collections.emptyList();
  449 + }
  450 + // 1. 查询导出展示信息,不加载工资卡和社保等敏感聚合
  451 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSet(list, HrmEmployeeDO::getUserId));
  452 + Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(list, HrmEmployeeDO::getDeptId));
  453 + Map<Long, HrmEmployeeDO> leaderEmployeeMap = employeeService.getEmployeeMap(
  454 + convertSet(list, HrmEmployeeDO::getLeaderEmployeeId));
  455 + Map<Long, HrmRecruitChannelDO> channelMap = recruitChannelService.getRecruitChannelMap(
  456 + convertSet(list, HrmEmployeeDO::getChannelId));
  457 +
  458 + // 2. 拼接安全导出字段
  459 + return convertList(list, employee -> {
  460 + HrmEmployeeRespVO vo = new HrmEmployeeRespVO()
  461 + .setId(employee.getId()).setName(employee.getName())
  462 + .setJobNumber(employee.getJobNumber()).setMobile(employee.getMobile())
  463 + .setPostName(employee.getPostName()).setPostLevel(employee.getPostLevel())
  464 + .setEntryStatus(employee.getEntryStatus()).setStatus(employee.getStatus()).setType(employee.getType())
  465 + .setEntryTime(employee.getEntryTime()).setRegularTime(employee.getRegularTime())
  466 + .setLeaveTime(employee.getLeaveTime())
  467 + .setWorkCity(employee.getWorkCity()).setWorkAddress(employee.getWorkAddress());
  468 + MapUtils.findAndThen(userMap, employee.getUserId(), user -> vo.setUserNickname(user.getNickname()));
  469 + MapUtils.findAndThen(deptMap, employee.getDeptId(), dept -> vo.setDeptName(dept.getName()));
  470 + MapUtils.findAndThen(leaderEmployeeMap, employee.getLeaderEmployeeId(),
  471 + leader -> vo.setLeaderEmployeeName(leader.getName()));
  472 + MapUtils.findAndThen(channelMap, employee.getChannelId(), channel -> vo.setChannelName(channel.getName()));
  473 + return vo;
  474 + });
  475 + }
  476 +
  477 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeEducationExperienceController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.educationexperience.HrmEmployeeEducationExperienceRespVO;
  6 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.educationexperience.HrmEmployeeEducationExperienceSaveReqVO;
  7 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience.HrmEmployeeEducationExperienceDO;
  8 +import cn.iocoder.yudao.module.hrm.service.employee.experience.HrmEmployeeEducationExperienceService;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +import io.swagger.v3.oas.annotations.Parameter;
  11 +import io.swagger.v3.oas.annotations.tags.Tag;
  12 +import jakarta.annotation.Resource;
  13 +import jakarta.validation.Valid;
  14 +import org.springframework.security.access.prepost.PreAuthorize;
  15 +import org.springframework.validation.annotation.Validated;
  16 +import org.springframework.web.bind.annotation.DeleteMapping;
  17 +import org.springframework.web.bind.annotation.GetMapping;
  18 +import org.springframework.web.bind.annotation.PostMapping;
  19 +import org.springframework.web.bind.annotation.PutMapping;
  20 +import org.springframework.web.bind.annotation.RequestBody;
  21 +import org.springframework.web.bind.annotation.RequestMapping;
  22 +import org.springframework.web.bind.annotation.RequestParam;
  23 +import org.springframework.web.bind.annotation.RestController;
  24 +
  25 +import java.util.List;
  26 +
  27 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  28 +
  29 +@Tag(name = "管理后台 - HRM 员工教育经历")
  30 +@RestController
  31 +@RequestMapping("/hrm/employee/education-experience")
  32 +@Validated
  33 +public class HrmEmployeeEducationExperienceController {
  34 +
  35 + @Resource
  36 + private HrmEmployeeEducationExperienceService educationExperienceService;
  37 +
  38 + @GetMapping("/list")
  39 + @Operation(summary = "获得员工教育经历列表")
  40 + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024")
  41 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  42 + public CommonResult<List<HrmEmployeeEducationExperienceRespVO>> getEducationExperienceList(
  43 + @RequestParam("employeeId") Long employeeId) {
  44 + List<HrmEmployeeEducationExperienceDO> educationExperiences =
  45 + educationExperienceService.getEducationExperienceListByEmployeeId(employeeId);
  46 + return success(BeanUtils.toBean(educationExperiences, HrmEmployeeEducationExperienceRespVO.class));
  47 + }
  48 +
  49 + @PostMapping("/create")
  50 + @Operation(summary = "创建员工教育经历")
  51 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  52 + public CommonResult<Long> createEducationExperience(
  53 + @Valid @RequestBody HrmEmployeeEducationExperienceSaveReqVO reqVO) {
  54 + return success(educationExperienceService.createEducationExperience(reqVO));
  55 + }
  56 +
  57 + @PutMapping("/update")
  58 + @Operation(summary = "更新员工教育经历")
  59 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  60 + public CommonResult<Boolean> updateEducationExperience(
  61 + @Valid @RequestBody HrmEmployeeEducationExperienceSaveReqVO reqVO) {
  62 + educationExperienceService.updateEducationExperience(reqVO);
  63 + return success(true);
  64 + }
  65 +
  66 + @DeleteMapping("/delete")
  67 + @Operation(summary = "删除员工教育经历")
  68 + @Parameter(name = "id", description = "教育经历编号", required = true, example = "1024")
  69 + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')")
  70 + public CommonResult<Boolean> deleteEducationExperience(@RequestParam("id") Long id) {
  71 + educationExperienceService.deleteEducationExperience(id);
  72 + return success(true);
  73 + }
  74 +
  75 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeFieldConfigController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config.HrmEmployeeArchiveFieldConfigSaveReqVO;
  5 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config.HrmEmployeeCreateFieldConfigSaveReqVO;
  6 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config.HrmEmployeeFieldConfigRespVO;
  7 +import cn.iocoder.yudao.module.hrm.service.employee.config.HrmEmployeeFieldConfigService;
  8 +import io.swagger.v3.oas.annotations.Operation;
  9 +import io.swagger.v3.oas.annotations.Parameter;
  10 +import io.swagger.v3.oas.annotations.tags.Tag;
  11 +import jakarta.annotation.Resource;
  12 +import jakarta.validation.Valid;
  13 +import org.springframework.security.access.prepost.PreAuthorize;
  14 +import org.springframework.validation.annotation.Validated;
  15 +import org.springframework.web.bind.annotation.GetMapping;
  16 +import org.springframework.web.bind.annotation.PutMapping;
  17 +import org.springframework.web.bind.annotation.RequestBody;
  18 +import org.springframework.web.bind.annotation.RequestMapping;
  19 +import org.springframework.web.bind.annotation.RequestParam;
  20 +import org.springframework.web.bind.annotation.RestController;
  21 +
  22 +import java.util.List;
  23 +
  24 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  25 +
  26 +@Tag(name = "管理后台 - HRM 员工字段配置")
  27 +@RestController
  28 +@RequestMapping("/hrm/employee/config")
  29 +@Validated
  30 +public class HrmEmployeeFieldConfigController {
  31 +
  32 + @Resource
  33 + private HrmEmployeeFieldConfigService employeeFieldConfigService;
  34 +
  35 + @GetMapping("/create-field/list")
  36 + @Operation(summary = "获得新建员工字段配置")
  37 + @Parameter(name = "entryStatus", description = "入职状态:1 在职,2 待入职", required = true, example = "1")
  38 + @PreAuthorize("@ss.hasPermission('hrm:employee:config:query') or @ss.hasPermission('hrm:employee:create')")
  39 + public CommonResult<List<HrmEmployeeFieldConfigRespVO>> getEmployeeCreateFieldConfigList(
  40 + @RequestParam("entryStatus") Integer entryStatus) {
  41 + return success(employeeFieldConfigService.getEmployeeCreateFieldConfigList(entryStatus));
  42 + }
  43 +
  44 + @PutMapping("/create-field/save")
  45 + @Operation(summary = "保存新建员工字段配置")
  46 + @PreAuthorize("@ss.hasPermission('hrm:employee:config:update')")
  47 + public CommonResult<Boolean> saveEmployeeCreateFieldConfig(
  48 + @Valid @RequestBody HrmEmployeeCreateFieldConfigSaveReqVO reqVO) {
  49 + employeeFieldConfigService.saveEmployeeCreateFieldConfig(reqVO);
  50 + return success(true);
  51 + }
  52 +
  53 + @GetMapping("/archive-field/list")
  54 + @Operation(summary = "获得员工档案字段配置")
  55 + @PreAuthorize("@ss.hasPermission('hrm:employee:config:query')")
  56 + public CommonResult<List<HrmEmployeeFieldConfigRespVO>> getEmployeeArchiveFieldConfigList() {
  57 + return success(employeeFieldConfigService.getEmployeeArchiveFieldConfigList());
  58 + }
  59 +
  60 + @PutMapping("/archive-field/save")
  61 + @Operation(summary = "保存员工档案字段配置")
  62 + @PreAuthorize("@ss.hasPermission('hrm:employee:config:update')")
  63 + public CommonResult<Boolean> saveEmployeeArchiveFieldConfig(
  64 + @Valid @RequestBody HrmEmployeeArchiveFieldConfigSaveReqVO reqVO) {
  65 + employeeFieldConfigService.saveEmployeeArchiveFieldConfig(reqVO);
  66 + return success(true);
  67 + }
  68 +
  69 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeFileController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.file.HrmEmployeeFileRespVO;
  6 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.file.HrmEmployeeFileSaveReqVO;
  7 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeFileDO;
  8 +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeFileService;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +import io.swagger.v3.oas.annotations.Parameter;
  11 +import io.swagger.v3.oas.annotations.tags.Tag;
  12 +import jakarta.annotation.Resource;
  13 +import jakarta.validation.Valid;
  14 +import org.springframework.security.access.prepost.PreAuthorize;
  15 +import org.springframework.validation.annotation.Validated;
  16 +import org.springframework.web.bind.annotation.GetMapping;
  17 +import org.springframework.web.bind.annotation.PutMapping;
  18 +import org.springframework.web.bind.annotation.RequestBody;
  19 +import org.springframework.web.bind.annotation.RequestMapping;
  20 +import org.springframework.web.bind.annotation.RequestParam;
  21 +import org.springframework.web.bind.annotation.RestController;
  22 +
  23 +import java.util.List;
  24 +
  25 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  26 +
  27 +@Tag(name = "管理后台 - HRM 员工材料附件")
  28 +@RestController
  29 +@RequestMapping("/hrm/employee/file")
  30 +@Validated
  31 +public class HrmEmployeeFileController {
  32 +
  33 + @Resource
  34 + private HrmEmployeeFileService employeeFileService;
  35 +
  36 + @GetMapping("/list")
  37 + @Operation(summary = "获得员工材料附件列表")
  38 + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024")
  39 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  40 + public CommonResult<List<HrmEmployeeFileRespVO>> getEmployeeFileList(
  41 + @RequestParam("employeeId") Long employeeId) {
  42 + List<HrmEmployeeFileDO> employeeFiles = employeeFileService.getEmployeeFileList(employeeId);
  43 + return success(BeanUtils.toBean(employeeFiles, HrmEmployeeFileRespVO.class));
  44 + }
  45 +
  46 + @PutMapping("/save")
  47 + @Operation(summary = "保存员工材料附件")
  48 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  49 + public CommonResult<Boolean> saveEmployeeFiles(@Valid @RequestBody HrmEmployeeFileSaveReqVO reqVO) {
  50 + employeeFileService.saveEmployeeFiles(reqVO);
  51 + return success(true);
  52 + }
  53 +
  54 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeePersonalNoteController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.personalnote.HrmEmployeePersonalNoteCreateReqVO;
  5 +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeePersonalNoteService;
  6 +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService;
  7 +import io.swagger.v3.oas.annotations.Operation;
  8 +import io.swagger.v3.oas.annotations.Parameter;
  9 +import io.swagger.v3.oas.annotations.tags.Tag;
  10 +import jakarta.annotation.Resource;
  11 +import jakarta.validation.Valid;
  12 +import org.springframework.security.access.prepost.PreAuthorize;
  13 +import org.springframework.validation.annotation.Validated;
  14 +import org.springframework.web.bind.annotation.DeleteMapping;
  15 +import org.springframework.web.bind.annotation.PostMapping;
  16 +import org.springframework.web.bind.annotation.RequestBody;
  17 +import org.springframework.web.bind.annotation.RequestMapping;
  18 +import org.springframework.web.bind.annotation.RequestParam;
  19 +import org.springframework.web.bind.annotation.RestController;
  20 +
  21 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  22 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  23 +
  24 +@Tag(name = "管理后台 - HRM 员工个人备忘")
  25 +@RestController
  26 +@RequestMapping("/hrm/employee/personal-note")
  27 +@Validated
  28 +public class HrmEmployeePersonalNoteController {
  29 +
  30 + @Resource
  31 + private HrmEmployeePersonalNoteService personalNoteService;
  32 + @Resource
  33 + private HrmEmployeeService employeeService;
  34 +
  35 + @PostMapping("/create")
  36 + @Operation(summary = "创建员工个人备忘")
  37 + @PreAuthorize("@ss.hasPermission('hrm:employee:personal-note:create')")
  38 + public CommonResult<Long> createPersonalNote(
  39 + @Valid @RequestBody HrmEmployeePersonalNoteCreateReqVO reqVO) {
  40 + Long employeeId = employeeService.validateEmployeeBySelf(getLoginUserId()).getId();
  41 + return success(personalNoteService.createPersonalNote(employeeId, reqVO));
  42 + }
  43 +
  44 + @DeleteMapping("/delete")
  45 + @Operation(summary = "删除员工个人备忘")
  46 + @Parameter(name = "id", description = "员工个人备忘编号", required = true, example = "1024")
  47 + @PreAuthorize("@ss.hasPermission('hrm:employee:personal-note:delete')")
  48 + public CommonResult<Boolean> deletePersonalNote(@RequestParam("id") Long id) {
  49 + Long employeeId = employeeService.validateEmployeeBySelf(getLoginUserId()).getId();
  50 + personalNoteService.deletePersonalNote(employeeId, id);
  51 + return success(true);
  52 + }
  53 +
  54 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeQuitInfoController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.quitinfo.HrmEmployeeQuitInfoRespVO;
  6 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeQuitInfoDO;
  7 +import cn.iocoder.yudao.module.hrm.service.employee.employment.HrmEmployeeQuitInfoService;
  8 +import io.swagger.v3.oas.annotations.Operation;
  9 +import io.swagger.v3.oas.annotations.Parameter;
  10 +import io.swagger.v3.oas.annotations.tags.Tag;
  11 +import jakarta.annotation.Resource;
  12 +import org.springframework.security.access.prepost.PreAuthorize;
  13 +import org.springframework.validation.annotation.Validated;
  14 +import org.springframework.web.bind.annotation.GetMapping;
  15 +import org.springframework.web.bind.annotation.RequestMapping;
  16 +import org.springframework.web.bind.annotation.RequestParam;
  17 +import org.springframework.web.bind.annotation.RestController;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  20 +
  21 +@Tag(name = "管理后台 - HRM 员工离职信息")
  22 +@RestController
  23 +@RequestMapping("/hrm/employee/quit-info")
  24 +@Validated
  25 +public class HrmEmployeeQuitInfoController {
  26 +
  27 + @Resource
  28 + private HrmEmployeeQuitInfoService quitInfoService;
  29 +
  30 + @GetMapping("/get")
  31 + @Operation(summary = "获得员工离职信息")
  32 + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024")
  33 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  34 + public CommonResult<HrmEmployeeQuitInfoRespVO> getQuitInfo(
  35 + @RequestParam("employeeId") Long employeeId) {
  36 + HrmEmployeeQuitInfoDO quitInfo = quitInfoService.getQuitInfoByEmployeeId(employeeId);
  37 + return success(BeanUtils.toBean(quitInfo, HrmEmployeeQuitInfoRespVO.class));
  38 + }
  39 +
  40 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeSalaryCardController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
  6 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.salarycard.HrmEmployeeSalaryCardRespVO;
  7 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.salarycard.HrmEmployeeSalaryCardSaveReqVO;
  8 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeSalaryCardDO;
  9 +import cn.iocoder.yudao.module.hrm.service.employee.employment.HrmEmployeeSalaryCardService;
  10 +import io.swagger.v3.oas.annotations.Operation;
  11 +import io.swagger.v3.oas.annotations.Parameter;
  12 +import io.swagger.v3.oas.annotations.tags.Tag;
  13 +import jakarta.annotation.Resource;
  14 +import jakarta.validation.Valid;
  15 +import org.springframework.security.access.prepost.PreAuthorize;
  16 +import org.springframework.validation.annotation.Validated;
  17 +import org.springframework.web.bind.annotation.DeleteMapping;
  18 +import org.springframework.web.bind.annotation.GetMapping;
  19 +import org.springframework.web.bind.annotation.PutMapping;
  20 +import org.springframework.web.bind.annotation.RequestBody;
  21 +import org.springframework.web.bind.annotation.RequestMapping;
  22 +import org.springframework.web.bind.annotation.RequestParam;
  23 +import org.springframework.web.bind.annotation.RestController;
  24 +
  25 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  26 +
  27 +@Tag(name = "管理后台 - HRM 员工工资卡")
  28 +@RestController
  29 +@RequestMapping("/hrm/employee/salary-card")
  30 +@Validated
  31 +public class HrmEmployeeSalaryCardController {
  32 +
  33 + @Resource
  34 + private HrmEmployeeSalaryCardService salaryCardService;
  35 +
  36 + @GetMapping("/get")
  37 + @Operation(summary = "获得员工工资卡")
  38 + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024")
  39 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  40 + public CommonResult<HrmEmployeeSalaryCardRespVO> getEmployeeSalaryCard(
  41 + @RequestParam("employeeId") Long employeeId) {
  42 + HrmEmployeeSalaryCardDO salaryCard = salaryCardService.getSalaryCardByEmployeeId(employeeId);
  43 + if (salaryCard == null) {
  44 + return success(null);
  45 + }
  46 + HrmEmployeeSalaryCardRespVO respVO = BeanUtils.toBean(salaryCard, HrmEmployeeSalaryCardRespVO.class)
  47 + .setBankAreaName(AreaUtils.format(salaryCard.getBankAreaId()));
  48 + return success(respVO);
  49 + }
  50 +
  51 + @PutMapping("/save")
  52 + @Operation(summary = "保存员工工资卡")
  53 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  54 + public CommonResult<Long> saveEmployeeSalaryCard(
  55 + @Valid @RequestBody HrmEmployeeSalaryCardSaveReqVO reqVO) {
  56 + return success(salaryCardService.saveSalaryCard(reqVO));
  57 + }
  58 +
  59 + @DeleteMapping("/delete")
  60 + @Operation(summary = "删除员工工资卡")
  61 + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024")
  62 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  63 + public CommonResult<Boolean> deleteEmployeeSalaryCard(@RequestParam("employeeId") Long employeeId) {
  64 + salaryCardService.deleteSalaryCardByEmployeeId(employeeId);
  65 + return success(true);
  66 + }
  67 +
  68 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeTrainingExperienceController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.trainingexperience.HrmEmployeeTrainingExperienceRespVO;
  6 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.trainingexperience.HrmEmployeeTrainingExperienceSaveReqVO;
  7 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience.HrmEmployeeTrainingExperienceDO;
  8 +import cn.iocoder.yudao.module.hrm.service.employee.experience.HrmEmployeeTrainingExperienceService;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +import io.swagger.v3.oas.annotations.Parameter;
  11 +import io.swagger.v3.oas.annotations.tags.Tag;
  12 +import jakarta.annotation.Resource;
  13 +import jakarta.validation.Valid;
  14 +import org.springframework.security.access.prepost.PreAuthorize;
  15 +import org.springframework.validation.annotation.Validated;
  16 +import org.springframework.web.bind.annotation.DeleteMapping;
  17 +import org.springframework.web.bind.annotation.GetMapping;
  18 +import org.springframework.web.bind.annotation.PostMapping;
  19 +import org.springframework.web.bind.annotation.PutMapping;
  20 +import org.springframework.web.bind.annotation.RequestBody;
  21 +import org.springframework.web.bind.annotation.RequestMapping;
  22 +import org.springframework.web.bind.annotation.RequestParam;
  23 +import org.springframework.web.bind.annotation.RestController;
  24 +
  25 +import java.util.List;
  26 +
  27 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  28 +
  29 +@Tag(name = "管理后台 - HRM 员工培训经历")
  30 +@RestController
  31 +@RequestMapping("/hrm/employee/training-experience")
  32 +@Validated
  33 +public class HrmEmployeeTrainingExperienceController {
  34 +
  35 + @Resource
  36 + private HrmEmployeeTrainingExperienceService trainingExperienceService;
  37 +
  38 + @GetMapping("/list")
  39 + @Operation(summary = "获得员工培训经历列表")
  40 + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024")
  41 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  42 + public CommonResult<List<HrmEmployeeTrainingExperienceRespVO>> getTrainingExperienceList(
  43 + @RequestParam("employeeId") Long employeeId) {
  44 + List<HrmEmployeeTrainingExperienceDO> trainingExperiences =
  45 + trainingExperienceService.getTrainingExperienceListByEmployeeId(employeeId);
  46 + return success(BeanUtils.toBean(trainingExperiences, HrmEmployeeTrainingExperienceRespVO.class));
  47 + }
  48 +
  49 + @PostMapping("/create")
  50 + @Operation(summary = "创建员工培训经历")
  51 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  52 + public CommonResult<Long> createTrainingExperience(
  53 + @Valid @RequestBody HrmEmployeeTrainingExperienceSaveReqVO reqVO) {
  54 + return success(trainingExperienceService.createTrainingExperience(reqVO));
  55 + }
  56 +
  57 + @PutMapping("/update")
  58 + @Operation(summary = "更新员工培训经历")
  59 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  60 + public CommonResult<Boolean> updateTrainingExperience(
  61 + @Valid @RequestBody HrmEmployeeTrainingExperienceSaveReqVO reqVO) {
  62 + trainingExperienceService.updateTrainingExperience(reqVO);
  63 + return success(true);
  64 + }
  65 +
  66 + @DeleteMapping("/delete")
  67 + @Operation(summary = "删除员工培训经历")
  68 + @Parameter(name = "id", description = "培训经历编号", required = true, example = "1024")
  69 + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')")
  70 + public CommonResult<Boolean> deleteTrainingExperience(@RequestParam("id") Long id) {
  71 + trainingExperienceService.deleteTrainingExperience(id);
  72 + return success(true);
  73 + }
  74 +
  75 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeWorkExperienceController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.workexperience.HrmEmployeeWorkExperienceRespVO;
  6 +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.workexperience.HrmEmployeeWorkExperienceSaveReqVO;
  7 +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience.HrmEmployeeWorkExperienceDO;
  8 +import cn.iocoder.yudao.module.hrm.service.employee.experience.HrmEmployeeWorkExperienceService;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +import io.swagger.v3.oas.annotations.Parameter;
  11 +import io.swagger.v3.oas.annotations.tags.Tag;
  12 +import jakarta.annotation.Resource;
  13 +import jakarta.validation.Valid;
  14 +import org.springframework.security.access.prepost.PreAuthorize;
  15 +import org.springframework.validation.annotation.Validated;
  16 +import org.springframework.web.bind.annotation.DeleteMapping;
  17 +import org.springframework.web.bind.annotation.GetMapping;
  18 +import org.springframework.web.bind.annotation.PostMapping;
  19 +import org.springframework.web.bind.annotation.PutMapping;
  20 +import org.springframework.web.bind.annotation.RequestBody;
  21 +import org.springframework.web.bind.annotation.RequestMapping;
  22 +import org.springframework.web.bind.annotation.RequestParam;
  23 +import org.springframework.web.bind.annotation.RestController;
  24 +
  25 +import java.util.List;
  26 +
  27 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  28 +
  29 +@Tag(name = "管理后台 - HRM 员工工作经历")
  30 +@RestController
  31 +@RequestMapping("/hrm/employee/work-experience")
  32 +@Validated
  33 +public class HrmEmployeeWorkExperienceController {
  34 +
  35 + @Resource
  36 + private HrmEmployeeWorkExperienceService workExperienceService;
  37 +
  38 + @GetMapping("/list")
  39 + @Operation(summary = "获得员工工作经历列表")
  40 + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024")
  41 + @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
  42 + public CommonResult<List<HrmEmployeeWorkExperienceRespVO>> getWorkExperienceList(
  43 + @RequestParam("employeeId") Long employeeId) {
  44 + List<HrmEmployeeWorkExperienceDO> workExperiences =
  45 + workExperienceService.getWorkExperienceListByEmployeeId(employeeId);
  46 + return success(BeanUtils.toBean(workExperiences, HrmEmployeeWorkExperienceRespVO.class));
  47 + }
  48 +
  49 + @PostMapping("/create")
  50 + @Operation(summary = "创建员工工作经历")
  51 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  52 + public CommonResult<Long> createWorkExperience(
  53 + @Valid @RequestBody HrmEmployeeWorkExperienceSaveReqVO reqVO) {
  54 + return success(workExperienceService.createWorkExperience(reqVO));
  55 + }
  56 +
  57 + @PutMapping("/update")
  58 + @Operation(summary = "更新员工工作经历")
  59 + @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
  60 + public CommonResult<Boolean> updateWorkExperience(
  61 + @Valid @RequestBody HrmEmployeeWorkExperienceSaveReqVO reqVO) {
  62 + workExperienceService.updateWorkExperience(reqVO);
  63 + return success(true);
  64 + }
  65 +
  66 + @DeleteMapping("/delete")
  67 + @Operation(summary = "删除员工工作经历")
  68 + @Parameter(name = "id", description = "工作经历编号", required = true, example = "1024")
  69 + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')")
  70 + public CommonResult<Boolean> deleteWorkExperience(@RequestParam("id") Long id) {
  71 + workExperienceService.deleteWorkExperience(id);
  72 + return success(true);
  73 + }
  74 +
  75 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/certificate/HrmEmployeeCertificateRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.certificate;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - HRM 员工证书 Response VO")
  9 +@Data
  10 +public class HrmEmployeeCertificateRespVO {
  11 +
  12 + @Schema(description = "证书编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  16 + private Long employeeId;
  17 +
  18 + @Schema(description = "证书名称", example = "高级工程师")
  19 + private String name;
  20 +
  21 + @Schema(description = "证书级别", example = "高级")
  22 + private String level;
  23 +
  24 + @Schema(description = "证书编号", example = "CERT-001")
  25 + private String no;
  26 +
  27 + @Schema(description = "有效起始日期")
  28 + private LocalDateTime startTime;
  29 +
  30 + @Schema(description = "有效结束日期")
  31 + private LocalDateTime endTime;
  32 +
  33 + @Schema(description = "发证机构", example = "人社局")
  34 + private String issuingAuthority;
  35 +
  36 + @Schema(description = "发证日期")
  37 + private LocalDateTime issuingTime;
  38 +
  39 + @Schema(description = "备注")
  40 + private String remark;
  41 +
  42 + @Schema(description = "排序", example = "1")
  43 + private Integer sort;
  44 +
  45 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  46 + private LocalDateTime createTime;
  47 +
  48 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/certificate/HrmEmployeeCertificateSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.certificate;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonIgnore;
  4 +import com.mzt.logapi.starter.annotation.DiffLogField;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import jakarta.validation.constraints.AssertTrue;
  7 +import jakarta.validation.constraints.NotBlank;
  8 +import jakarta.validation.constraints.NotNull;
  9 +import jakarta.validation.constraints.Size;
  10 +import lombok.Data;
  11 +
  12 +import java.time.LocalDateTime;
  13 +
  14 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isAfterOrEqual;
  15 +
  16 +@Schema(description = "管理后台 - HRM 员工证书保存 Request VO")
  17 +@Data
  18 +public class HrmEmployeeCertificateSaveReqVO {
  19 +
  20 + @Schema(description = "证书编号", example = "1024")
  21 + private Long id;
  22 +
  23 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  24 + @NotNull(message = "员工编号不能为空")
  25 + private Long employeeId;
  26 +
  27 + @Schema(description = "证书名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "高级工程师")
  28 + @NotBlank(message = "证书名称不能为空")
  29 + @Size(max = 255, message = "证书名称长度不能超过 255 个字符")
  30 + @DiffLogField(name = "证书名称")
  31 + private String name;
  32 +
  33 + @Schema(description = "证书级别", example = "高级")
  34 + @Size(max = 255, message = "证书级别长度不能超过 255 个字符")
  35 + @DiffLogField(name = "证书级别")
  36 + private String level;
  37 +
  38 + @Schema(description = "证书编号", example = "CERT-001")
  39 + @Size(max = 255, message = "证书编号长度不能超过 255 个字符")
  40 + @DiffLogField(name = "证书编号")
  41 + private String no;
  42 +
  43 + @Schema(description = "有效起始日期")
  44 + @DiffLogField(name = "有效起始日期")
  45 + private LocalDateTime startTime;
  46 +
  47 + @Schema(description = "有效结束日期")
  48 + @DiffLogField(name = "有效结束日期")
  49 + private LocalDateTime endTime;
  50 +
  51 + @Schema(description = "发证机构", example = "人社局")
  52 + @Size(max = 255, message = "发证机构长度不能超过 255 个字符")
  53 + @DiffLogField(name = "发证机构")
  54 + private String issuingAuthority;
  55 +
  56 + @Schema(description = "发证日期")
  57 + @DiffLogField(name = "发证日期")
  58 + private LocalDateTime issuingTime;
  59 +
  60 + @Schema(description = "备注", example = "专业技术资格证书")
  61 + @Size(max = 500, message = "备注长度不能超过 500 个字符")
  62 + @DiffLogField(name = "备注")
  63 + private String remark;
  64 +
  65 + @Schema(description = "排序", example = "1")
  66 + @DiffLogField(name = "排序")
  67 + private Integer sort;
  68 +
  69 + @AssertTrue(message = "证书有效期的结束日期不能早于开始日期")
  70 + @JsonIgnore
  71 + public boolean isTimeRangeValid() {
  72 + return startTime == null || endTime == null || isAfterOrEqual(endTime, startTime);
  73 + }
  74 +
  75 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeChangeRecordCreateReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeReasonEnum;
  5 +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeTypeEnum;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import jakarta.validation.constraints.Max;
  8 +import jakarta.validation.constraints.Min;
  9 +import jakarta.validation.constraints.NotNull;
  10 +import jakarta.validation.constraints.Size;
  11 +import lombok.Data;
  12 +
  13 +import java.time.LocalDateTime;
  14 +
  15 +@Schema(description = "管理后台 - HRM 员工异动记录创建 Request VO")
  16 +@Data
  17 +public class HrmEmployeeChangeRecordCreateReqVO {
  18 +
  19 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  20 + @NotNull(message = "员工编号不能为空")
  21 + private Long employeeId;
  22 +
  23 + @Schema(description = "异动类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
  24 + @NotNull(message = "异动类型不能为空")
  25 + @InEnum(value = HrmEmployeeChangeTypeEnum.class, message = "异动类型必须是 {value}")
  26 + private Integer type;
  27 +
  28 + @Schema(description = "异动原因", example = "1")
  29 + @InEnum(value = HrmEmployeeChangeReasonEnum.class, message = "异动原因必须是 {value}")
  30 + private Integer reason;
  31 +
  32 + @Schema(description = "原部门编号", example = "100")
  33 + private Long oldDeptId;
  34 +
  35 + @Schema(description = "原岗位名称", example = "Java 工程师")
  36 + @Size(max = 255, message = "原岗位名称不能超过 255 个字符")
  37 + private String oldPostName;
  38 +
  39 + @Schema(description = "原职级", example = "P5")
  40 + @Size(max = 255, message = "原职级不能超过 255 个字符")
  41 + private String oldPostLevel;
  42 +
  43 + @Schema(description = "原工作地点", example = "杭州")
  44 + @Size(max = 255, message = "原工作地点不能超过 255 个字符")
  45 + private String oldWorkAddress;
  46 +
  47 + @Schema(description = "原直属上级员工编号", example = "2")
  48 + private Long oldLeaderEmployeeId;
  49 +
  50 + @Schema(description = "新部门编号", example = "200")
  51 + private Long newDeptId;
  52 +
  53 + @Schema(description = "新岗位名称", example = "高级 Java 工程师")
  54 + @Size(max = 255, message = "新岗位名称不能超过 255 个字符")
  55 + private String newPostName;
  56 +
  57 + @Schema(description = "新职级", example = "P6")
  58 + @Size(max = 255, message = "新职级不能超过 255 个字符")
  59 + private String newPostLevel;
  60 +
  61 + @Schema(description = "新工作地点", example = "上海")
  62 + @Size(max = 255, message = "新工作地点不能超过 255 个字符")
  63 + private String newWorkAddress;
  64 +
  65 + @Schema(description = "新直属上级员工编号", example = "3")
  66 + private Long newLeaderEmployeeId;
  67 +
  68 + @Schema(description = "试用期,单位月", example = "3")
  69 + @Min(value = 0, message = "试用期不能小于 0")
  70 + @Max(value = 6, message = "试用期不能超过 6 个月")
  71 + private Integer probation;
  72 +
  73 + @Schema(description = "生效时间", requiredMode = Schema.RequiredMode.REQUIRED)
  74 + @NotNull(message = "生效时间不能为空")
  75 + private LocalDateTime effectTime;
  76 +
  77 + @Schema(description = "备注", example = "年度晋升")
  78 + @Size(max = 500, message = "备注不能超过 500 个字符")
  79 + private String remark;
  80 +
  81 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeChangeRecordRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - HRM 员工异动记录 Response VO")
  9 +@Data
  10 +public class HrmEmployeeChangeRecordRespVO {
  11 +
  12 + @Schema(description = "异动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  16 + private Long employeeId;
  17 +
  18 + @Schema(description = "变动类型", example = "5")
  19 + private Integer type;
  20 +
  21 + @Schema(description = "异动原因", example = "1")
  22 + private Integer reason;
  23 +
  24 + @Schema(description = "原部门编号", example = "100")
  25 + private Long oldDeptId;
  26 +
  27 + @Schema(description = "原部门名称", example = "研发部")
  28 + private String oldDeptName;
  29 +
  30 + @Schema(description = "新部门编号", example = "200")
  31 + private Long newDeptId;
  32 +
  33 + @Schema(description = "新部门名称", example = "产品部")
  34 + private String newDeptName;
  35 +
  36 + @Schema(description = "原岗位名称")
  37 + private String oldPostName;
  38 +
  39 + @Schema(description = "新岗位名称")
  40 + private String newPostName;
  41 +
  42 + @Schema(description = "原职级")
  43 + private String oldPostLevel;
  44 +
  45 + @Schema(description = "新职级")
  46 + private String newPostLevel;
  47 +
  48 + @Schema(description = "原工作地点")
  49 + private String oldWorkAddress;
  50 +
  51 + @Schema(description = "新工作地点")
  52 + private String newWorkAddress;
  53 +
  54 + @Schema(description = "原直属上级员工编号")
  55 + private Long oldLeaderEmployeeId;
  56 +
  57 + @Schema(description = "原直属上级员工姓名")
  58 + private String oldLeaderEmployeeName;
  59 +
  60 + @Schema(description = "新直属上级员工编号")
  61 + private Long newLeaderEmployeeId;
  62 +
  63 + @Schema(description = "新直属上级员工姓名")
  64 + private String newLeaderEmployeeName;
  65 +
  66 + @Schema(description = "试用期,单位月")
  67 + private Integer probation;
  68 +
  69 + @Schema(description = "生效日期")
  70 + private LocalDateTime effectTime;
  71 +
  72 + @Schema(description = "备注")
  73 + private String remark;
  74 +
  75 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  76 + private LocalDateTime createTime;
  77 +
  78 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeConvertToFullTimeReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeReasonEnum;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import jakarta.validation.constraints.Max;
  7 +import jakarta.validation.constraints.Min;
  8 +import jakarta.validation.constraints.NotNull;
  9 +import jakarta.validation.constraints.Size;
  10 +import lombok.Data;
  11 +
  12 +import java.time.LocalDateTime;
  13 +
  14 +@Schema(description = "管理后台 - HRM 员工转为全职 Request VO")
  15 +@Data
  16 +public class HrmEmployeeConvertToFullTimeReqVO {
  17 +
  18 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  19 + @NotNull(message = "员工编号不能为空")
  20 + private Long employeeId;
  21 +
  22 + @Schema(description = "异动原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  23 + @NotNull(message = "异动原因不能为空")
  24 + @InEnum(value = HrmEmployeeChangeReasonEnum.class, message = "异动原因必须是 {value}")
  25 + private Integer reason;
  26 +
  27 + @Schema(description = "试用期,单位:月;0 表示无试用期", requiredMode = Schema.RequiredMode.REQUIRED,
  28 + example = "3")
  29 + @NotNull(message = "试用期不能为空")
  30 + @Min(value = 0, message = "试用期不能小于 0")
  31 + @Max(value = 6, message = "试用期不能超过 6 个月")
  32 + private Integer probation;
  33 +
  34 + @Schema(description = "新部门编号;未填写表示不变", example = "200")
  35 + private Long newDeptId;
  36 +
  37 + @Schema(description = "新岗位名称;未填写表示不变", example = "Java 工程师")
  38 + @Size(max = 255, message = "新岗位名称不能超过 255 个字符")
  39 + private String newPostName;
  40 +
  41 + @Schema(description = "新职级;未填写表示不变", example = "P6")
  42 + @Size(max = 255, message = "新职级不能超过 255 个字符")
  43 + private String newPostLevel;
  44 +
  45 + @Schema(description = "新工作地点;未填写表示不变", example = "上海")
  46 + @Size(max = 255, message = "新工作地点不能超过 255 个字符")
  47 + private String newWorkAddress;
  48 +
  49 + @Schema(description = "新直属上级员工编号;未填写表示不变", example = "3")
  50 + private Long newLeaderEmployeeId;
  51 +
  52 + @Schema(description = "生效日期", requiredMode = Schema.RequiredMode.REQUIRED)
  53 + @NotNull(message = "生效日期不能为空")
  54 + private LocalDateTime effectTime;
  55 +
  56 + @Schema(description = "备注", example = "转为全职员工")
  57 + @Size(max = 500, message = "备注不能超过 500 个字符")
  58 + private String remark;
  59 +
  60 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeDemoteReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeReasonEnum;
  5 +import com.fasterxml.jackson.annotation.JsonIgnore;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import jakarta.validation.constraints.AssertTrue;
  8 +import jakarta.validation.constraints.NotNull;
  9 +import jakarta.validation.constraints.Size;
  10 +import lombok.Data;
  11 +
  12 +import java.time.LocalDateTime;
  13 +
  14 +@Schema(description = "管理后台 - HRM 员工降级 Request VO")
  15 +@Data
  16 +public class HrmEmployeeDemoteReqVO {
  17 +
  18 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  19 + @NotNull(message = "员工编号不能为空")
  20 + private Long employeeId;
  21 +
  22 + @Schema(description = "异动原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  23 + @NotNull(message = "异动原因不能为空")
  24 + @InEnum(value = HrmEmployeeChangeReasonEnum.class, message = "异动原因必须是 {value}")
  25 + private Integer reason;
  26 +
  27 + @Schema(description = "新部门编号", example = "200")
  28 + private Long newDeptId;
  29 +
  30 + @Schema(description = "新岗位名称", example = "Java 工程师")
  31 + @Size(max = 255, message = "新岗位名称不能超过 255 个字符")
  32 + private String newPostName;
  33 +
  34 + @Schema(description = "新职级", example = "P5")
  35 + @Size(max = 255, message = "新职级不能超过 255 个字符")
  36 + private String newPostLevel;
  37 +
  38 + @Schema(description = "新工作地点", example = "杭州")
  39 + @Size(max = 255, message = "新工作地点不能超过 255 个字符")
  40 + private String newWorkAddress;
  41 +
  42 + @Schema(description = "新直属上级员工编号", example = "3")
  43 + private Long newLeaderEmployeeId;
  44 +
  45 + @Schema(description = "生效日期", requiredMode = Schema.RequiredMode.REQUIRED)
  46 + @NotNull(message = "生效日期不能为空")
  47 + private LocalDateTime effectTime;
  48 +
  49 + @Schema(description = "备注", example = "岗位职级调整")
  50 + @Size(max = 500, message = "备注不能超过 500 个字符")
  51 + private String remark;
  52 +
  53 + @AssertTrue(message = "降级原因不合法")
  54 + @JsonIgnore
  55 + public boolean isReasonValid() {
  56 + return reason == null || HrmEmployeeChangeReasonEnum.DEMOTION_REASONS.contains(reason);
  57 + }
  58 +
  59 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeePromoteReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeReasonEnum;
  5 +import com.fasterxml.jackson.annotation.JsonIgnore;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import jakarta.validation.constraints.AssertTrue;
  8 +import jakarta.validation.constraints.NotNull;
  9 +import jakarta.validation.constraints.Size;
  10 +import lombok.Data;
  11 +
  12 +import java.time.LocalDateTime;
  13 +
  14 +@Schema(description = "管理后台 - HRM 员工晋升 Request VO")
  15 +@Data
  16 +public class HrmEmployeePromoteReqVO {
  17 +
  18 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  19 + @NotNull(message = "员工编号不能为空")
  20 + private Long employeeId;
  21 +
  22 + @Schema(description = "异动原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  23 + @NotNull(message = "异动原因不能为空")
  24 + @InEnum(value = HrmEmployeeChangeReasonEnum.class, message = "异动原因必须是 {value}")
  25 + private Integer reason;
  26 +
  27 + @Schema(description = "新部门编号", example = "200")
  28 + private Long newDeptId;
  29 +
  30 + @Schema(description = "新岗位名称", example = "高级 Java 工程师")
  31 + @Size(max = 255, message = "新岗位名称不能超过 255 个字符")
  32 + private String newPostName;
  33 +
  34 + @Schema(description = "新职级", example = "P6")
  35 + @Size(max = 255, message = "新职级不能超过 255 个字符")
  36 + private String newPostLevel;
  37 +
  38 + @Schema(description = "新工作地点", example = "上海")
  39 + @Size(max = 255, message = "新工作地点不能超过 255 个字符")
  40 + private String newWorkAddress;
  41 +
  42 + @Schema(description = "新直属上级员工编号", example = "3")
  43 + private Long newLeaderEmployeeId;
  44 +
  45 + @Schema(description = "生效日期", requiredMode = Schema.RequiredMode.REQUIRED)
  46 + @NotNull(message = "生效日期不能为空")
  47 + private LocalDateTime effectTime;
  48 +
  49 + @Schema(description = "备注", example = "年度晋升")
  50 + @Size(max = 500, message = "备注不能超过 500 个字符")
  51 + private String remark;
  52 +
  53 + @AssertTrue(message = "晋升原因不合法")
  54 + @JsonIgnore
  55 + public boolean isReasonValid() {
  56 + return reason == null || HrmEmployeeChangeReasonEnum.TRANSFER_AND_PROMOTION_REASONS.contains(reason);
  57 + }
  58 +
  59 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeRegularReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeReasonEnum;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import jakarta.validation.constraints.NotNull;
  7 +import jakarta.validation.constraints.Size;
  8 +import lombok.Data;
  9 +
  10 +import java.time.LocalDateTime;
  11 +
  12 +@Schema(description = "管理后台 - HRM 员工转正 Request VO")
  13 +@Data
  14 +public class HrmEmployeeRegularReqVO {
  15 +
  16 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  17 + @NotNull(message = "员工编号不能为空")
  18 + private Long employeeId;
  19 +
  20 + @Schema(description = "异动原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  21 + @NotNull(message = "异动原因不能为空")
  22 + @InEnum(value = HrmEmployeeChangeReasonEnum.class, message = "异动原因必须是 {value}")
  23 + private Integer reason;
  24 +
  25 + @Schema(description = "新部门编号;未填写表示不变", example = "200")
  26 + private Long newDeptId;
  27 +
  28 + @Schema(description = "新岗位名称;未填写表示不变", example = "Java 工程师")
  29 + @Size(max = 255, message = "新岗位名称不能超过 255 个字符")
  30 + private String newPostName;
  31 +
  32 + @Schema(description = "新职级;未填写表示不变", example = "P6")
  33 + @Size(max = 255, message = "新职级不能超过 255 个字符")
  34 + private String newPostLevel;
  35 +
  36 + @Schema(description = "新工作地点;未填写表示不变", example = "上海")
  37 + @Size(max = 255, message = "新工作地点不能超过 255 个字符")
  38 + private String newWorkAddress;
  39 +
  40 + @Schema(description = "新直属上级员工编号;未填写表示不变", example = "3")
  41 + private Long newLeaderEmployeeId;
  42 +
  43 + @Schema(description = "生效日期", requiredMode = Schema.RequiredMode.REQUIRED)
  44 + @NotNull(message = "生效日期不能为空")
  45 + private LocalDateTime effectTime;
  46 +
  47 + @Schema(description = "备注", example = "试用期考核通过")
  48 + @Size(max = 500, message = "备注不能超过 500 个字符")
  49 + private String remark;
  50 +
  51 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeTransferReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeReasonEnum;
  5 +import com.fasterxml.jackson.annotation.JsonIgnore;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import jakarta.validation.constraints.AssertTrue;
  8 +import jakarta.validation.constraints.NotNull;
  9 +import jakarta.validation.constraints.Size;
  10 +import lombok.Data;
  11 +
  12 +import java.time.LocalDateTime;
  13 +
  14 +@Schema(description = "管理后台 - HRM 员工调岗 Request VO")
  15 +@Data
  16 +public class HrmEmployeeTransferReqVO {
  17 +
  18 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  19 + @NotNull(message = "员工编号不能为空")
  20 + private Long employeeId;
  21 +
  22 + @Schema(description = "异动原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  23 + @NotNull(message = "异动原因不能为空")
  24 + @InEnum(value = HrmEmployeeChangeReasonEnum.class, message = "异动原因必须是 {value}")
  25 + private Integer reason;
  26 +
  27 + @Schema(description = "新部门编号", example = "200")
  28 + private Long newDeptId;
  29 +
  30 + @Schema(description = "新岗位名称", example = "产品经理")
  31 + @Size(max = 255, message = "新岗位名称不能超过 255 个字符")
  32 + private String newPostName;
  33 +
  34 + @Schema(description = "新职级", example = "P6")
  35 + @Size(max = 255, message = "新职级不能超过 255 个字符")
  36 + private String newPostLevel;
  37 +
  38 + @Schema(description = "新工作地点", example = "上海")
  39 + @Size(max = 255, message = "新工作地点不能超过 255 个字符")
  40 + private String newWorkAddress;
  41 +
  42 + @Schema(description = "新直属上级员工编号", example = "3")
  43 + private Long newLeaderEmployeeId;
  44 +
  45 + @Schema(description = "生效日期", requiredMode = Schema.RequiredMode.REQUIRED)
  46 + @NotNull(message = "生效日期不能为空")
  47 + private LocalDateTime effectTime;
  48 +
  49 + @Schema(description = "备注", example = "业务调整")
  50 + @Size(max = 500, message = "备注不能超过 500 个字符")
  51 + private String remark;
  52 +
  53 + @AssertTrue(message = "调岗原因不合法")
  54 + @JsonIgnore
  55 + public boolean isReasonValid() {
  56 + return reason == null || HrmEmployeeChangeReasonEnum.TRANSFER_AND_PROMOTION_REASONS.contains(reason);
  57 + }
  58 +
  59 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeArchiveFieldConfigSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config;
  2 +
  3 +import com.mzt.logapi.starter.annotation.DiffLogField;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.Valid;
  6 +import jakarta.validation.constraints.NotEmpty;
  7 +import lombok.Data;
  8 +
  9 +import java.util.List;
  10 +
  11 +@Schema(description = "管理后台 - HRM 员工档案字段配置保存 Request VO")
  12 +@Data
  13 +public class HrmEmployeeArchiveFieldConfigSaveReqVO {
  14 +
  15 + @Schema(description = "字段配置列表", requiredMode = Schema.RequiredMode.REQUIRED)
  16 + @Valid
  17 + @NotEmpty(message = "字段配置列表不能为空")
  18 + @DiffLogField(name = "字段配置")
  19 + private List<HrmEmployeeFieldConfigSaveReqVO> fields;
  20 +
  21 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeCreateFieldConfigSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeEntryStatusEnum;
  5 +import com.mzt.logapi.starter.annotation.DiffLogField;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import jakarta.validation.Valid;
  8 +import jakarta.validation.constraints.NotEmpty;
  9 +import jakarta.validation.constraints.NotNull;
  10 +import lombok.Data;
  11 +import org.hibernate.validator.constraints.Range;
  12 +
  13 +import java.util.List;
  14 +
  15 +@Schema(description = "管理后台 - HRM 新建员工字段配置保存 Request VO")
  16 +@Data
  17 +public class HrmEmployeeCreateFieldConfigSaveReqVO {
  18 +
  19 + @Schema(description = "入职状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  20 + @NotNull(message = "入职状态不能为空")
  21 + @InEnum(value = HrmEmployeeEntryStatusEnum.class, message = "入职状态必须是 {value}")
  22 + @Range(min = 1, max = 2, message = "入职状态必须是 1 或 2")
  23 + @DiffLogField(name = "入职状态")
  24 + private Integer entryStatus;
  25 +
  26 + @Schema(description = "字段配置列表", requiredMode = Schema.RequiredMode.REQUIRED)
  27 + @Valid
  28 + @NotEmpty(message = "字段配置列表不能为空")
  29 + @DiffLogField(name = "字段配置")
  30 + private List<HrmEmployeeFieldConfigSaveReqVO> fields;
  31 +
  32 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeFieldConfigRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +@Schema(description = "管理后台 - HRM 员工字段配置 Response VO")
  7 +@Data
  8 +public class HrmEmployeeFieldConfigRespVO {
  9 +
  10 + @Schema(description = "字段名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "name")
  11 + private String name;
  12 +
  13 + @Schema(description = "字段标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "员工姓名")
  14 + private String title;
  15 +
  16 + @Schema(description = "字段分组名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "基本信息")
  17 + private String groupName;
  18 +
  19 + @Schema(description = "是否显示", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  20 + private Boolean visible;
  21 +
  22 + @Schema(description = "是否允许员工编辑", example = "true")
  23 + private Boolean editable;
  24 +
  25 + @Schema(description = "是否锁定显示", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
  26 + private Boolean visibleLocked;
  27 +
  28 + @Schema(description = "是否锁定编辑", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
  29 + private Boolean editableLocked;
  30 +
  31 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeFieldConfigSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config;
  2 +
  3 +import com.mzt.logapi.starter.annotation.DiffLogField;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.constraints.NotBlank;
  6 +import jakarta.validation.constraints.NotNull;
  7 +import jakarta.validation.constraints.Size;
  8 +import lombok.Data;
  9 +
  10 +@Schema(description = "管理后台 - HRM 员工字段配置保存 Request VO")
  11 +@Data
  12 +public class HrmEmployeeFieldConfigSaveReqVO {
  13 +
  14 + @Schema(description = "字段名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "name")
  15 + @NotBlank(message = "字段名称不能为空")
  16 + @Size(max = 40, message = "字段名称不能超过 40 个字符")
  17 + @DiffLogField(name = "字段名称")
  18 + private String name;
  19 +
  20 + @Schema(description = "是否显示", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  21 + @NotNull(message = "是否显示不能为空")
  22 + @DiffLogField(name = "是否显示")
  23 + private Boolean visible;
  24 +
  25 + @Schema(description = "是否允许员工编辑", example = "true")
  26 + @DiffLogField(name = "是否允许员工编辑")
  27 + private Boolean editable;
  28 +
  29 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeFieldConfigValueVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +@Schema(description = "管理后台 - HRM 员工字段配置值 VO")
  9 +@Data
  10 +@NoArgsConstructor
  11 +@AllArgsConstructor
  12 +public class HrmEmployeeFieldConfigValueVO {
  13 +
  14 + @Schema(description = "字段名称", example = "mobile")
  15 + private String name;
  16 +
  17 + @Schema(description = "是否显示", example = "true")
  18 + private Boolean visible;
  19 +
  20 + @Schema(description = "是否允许员工编辑", example = "true")
  21 + private Boolean editable;
  22 +
  23 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contact/HrmEmployeeContactRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contact;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - HRM 员工联系人 Response VO")
  9 +@Data
  10 +public class HrmEmployeeContactRespVO {
  11 +
  12 + @Schema(description = "联系人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  16 + private Long employeeId;
  17 +
  18 + @Schema(description = "联系人名称", example = "李四")
  19 + private String name;
  20 +
  21 + @Schema(description = "关系", example = "配偶")
  22 + private String relation;
  23 +
  24 + @Schema(description = "联系人电话", example = "15601691301")
  25 + private String phone;
  26 +
  27 + @Schema(description = "联系人工作单位")
  28 + private String workUnit;
  29 +
  30 + @Schema(description = "联系人职务")
  31 + private String postName;
  32 +
  33 + @Schema(description = "联系人地址")
  34 + private String address;
  35 +
  36 + @Schema(description = "排序", example = "1")
  37 + private Integer sort;
  38 +
  39 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  40 + private LocalDateTime createTime;
  41 +
  42 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contact/HrmEmployeeContactSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contact;
  2 +
  3 +import com.mzt.logapi.starter.annotation.DiffLogField;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.constraints.NotBlank;
  6 +import jakarta.validation.constraints.NotNull;
  7 +import jakarta.validation.constraints.Size;
  8 +import lombok.Data;
  9 +
  10 +@Schema(description = "管理后台 - HRM 员工联系人保存 Request VO")
  11 +@Data
  12 +public class HrmEmployeeContactSaveReqVO {
  13 +
  14 + @Schema(description = "联系人编号", example = "1024")
  15 + private Long id;
  16 +
  17 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  18 + @NotNull(message = "员工编号不能为空")
  19 + private Long employeeId;
  20 +
  21 + @Schema(description = "联系人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
  22 + @NotBlank(message = "联系人名称不能为空")
  23 + @Size(max = 64, message = "联系人名称长度不能超过 64 个字符")
  24 + @DiffLogField(name = "联系人名称")
  25 + private String name;
  26 +
  27 + @Schema(description = "关系", example = "配偶")
  28 + @Size(max = 64, message = "关系长度不能超过 64 个字符")
  29 + @DiffLogField(name = "关系")
  30 + private String relation;
  31 +
  32 + @Schema(description = "联系人电话", example = "15601691301")
  33 + @Size(max = 40, message = "联系人电话长度不能超过 40 个字符")
  34 + @DiffLogField(name = "联系人电话")
  35 + private String phone;
  36 +
  37 + @Schema(description = "联系人工作单位", example = "示例科技")
  38 + @Size(max = 128, message = "联系人工作单位长度不能超过 128 个字符")
  39 + @DiffLogField(name = "联系人工作单位")
  40 + private String workUnit;
  41 +
  42 + @Schema(description = "联系人职务", example = "产品经理")
  43 + @Size(max = 128, message = "联系人职务长度不能超过 128 个字符")
  44 + @DiffLogField(name = "联系人职务")
  45 + private String postName;
  46 +
  47 + @Schema(description = "联系人地址", example = "杭州市西湖区")
  48 + @Size(max = 255, message = "联系人地址长度不能超过 255 个字符")
  49 + @DiffLogField(name = "联系人地址")
  50 + private String address;
  51 +
  52 + @Schema(description = "排序", example = "1")
  53 + @DiffLogField(name = "排序")
  54 + private Integer sort;
  55 +
  56 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contract/HrmEmployeeContractRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contract;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +import java.util.List;
  8 +
  9 +@Schema(description = "管理后台 - HRM 员工合同 Response VO")
  10 +@Data
  11 +public class HrmEmployeeContractRespVO {
  12 +
  13 + @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  14 + private Long id;
  15 +
  16 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  17 + private Long employeeId;
  18 +
  19 + @Schema(description = "合同编号", example = "HT-2026-001")
  20 + private String no;
  21 +
  22 + @Schema(description = "合同类型", example = "1")
  23 + private Integer type;
  24 +
  25 + @Schema(description = "合同开始日期")
  26 + private LocalDateTime startTime;
  27 +
  28 + @Schema(description = "合同结束日期")
  29 + private LocalDateTime endTime;
  30 +
  31 + @Schema(description = "期限", example = "36")
  32 + private Integer term;
  33 +
  34 + @Schema(description = "合同状态", example = "1")
  35 + private Integer status;
  36 +
  37 + @Schema(description = "签约公司")
  38 + private String signCompany;
  39 +
  40 + @Schema(description = "合同签订日期")
  41 + private LocalDateTime signTime;
  42 +
  43 + @Schema(description = "备注")
  44 + private String remark;
  45 +
  46 + @Schema(description = "是否到期提醒", example = "true")
  47 + private Boolean expireRemind;
  48 +
  49 + @Schema(description = "附件地址数组")
  50 + private List<String> fileUrls;
  51 +
  52 + @Schema(description = "排序", example = "1")
  53 + private Integer sort;
  54 +
  55 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  56 + private LocalDateTime createTime;
  57 +
  58 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contract/HrmEmployeeContractSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contract;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeContractStatusEnum;
  5 +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeContractTypeEnum;
  6 +import com.fasterxml.jackson.annotation.JsonIgnore;
  7 +import com.mzt.logapi.starter.annotation.DiffLogField;
  8 +import io.swagger.v3.oas.annotations.media.Schema;
  9 +import jakarta.validation.constraints.AssertTrue;
  10 +import jakarta.validation.constraints.Max;
  11 +import jakarta.validation.constraints.Min;
  12 +import jakarta.validation.constraints.NotNull;
  13 +import jakarta.validation.constraints.Size;
  14 +import lombok.Data;
  15 +
  16 +import java.time.LocalDateTime;
  17 +import java.util.List;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isAfterOrEqual;
  20 +
  21 +@Schema(description = "管理后台 - HRM 员工合同保存 Request VO")
  22 +@Data
  23 +public class HrmEmployeeContractSaveReqVO {
  24 +
  25 + @Schema(description = "合同编号", example = "1024")
  26 + private Long id;
  27 +
  28 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  29 + @NotNull(message = "员工编号不能为空")
  30 + private Long employeeId;
  31 +
  32 + @Schema(description = "合同编号", example = "HT-2026-001")
  33 + @Size(max = 128, message = "合同编号长度不能超过 128 个字符")
  34 + @DiffLogField(name = "合同编号")
  35 + private String no;
  36 +
  37 + @Schema(description = "合同类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  38 + @NotNull(message = "合同类型不能为空")
  39 + @InEnum(value = HrmEmployeeContractTypeEnum.class, message = "合同类型必须是 {value}")
  40 + @DiffLogField(name = "合同类型")
  41 + private Integer type;
  42 +
  43 + @Schema(description = "合同开始日期", requiredMode = Schema.RequiredMode.REQUIRED)
  44 + @NotNull(message = "合同开始日期不能为空")
  45 + @DiffLogField(name = "合同开始日期")
  46 + private LocalDateTime startTime;
  47 +
  48 + @Schema(description = "合同结束日期", requiredMode = Schema.RequiredMode.REQUIRED)
  49 + @NotNull(message = "合同结束日期不能为空")
  50 + @DiffLogField(name = "合同结束日期")
  51 + private LocalDateTime endTime;
  52 +
  53 + @Schema(description = "期限,单位:年", example = "3")
  54 + @Min(value = 1, message = "合同期限不能小于 1 年")
  55 + @Max(value = 10, message = "合同期限不能超过 10 年")
  56 + @DiffLogField(name = "合同期限")
  57 + private Integer term;
  58 +
  59 + @Schema(description = "合同状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  60 + @NotNull(message = "合同状态不能为空")
  61 + @InEnum(value = HrmEmployeeContractStatusEnum.class, message = "合同状态必须是 {value}")
  62 + @DiffLogField(name = "合同状态")
  63 + private Integer status;
  64 +
  65 + @Schema(description = "签约公司", example = "示例科技有限公司")
  66 + @Size(max = 255, message = "签约公司长度不能超过 255 个字符")
  67 + @DiffLogField(name = "签约公司")
  68 + private String signCompany;
  69 +
  70 + @Schema(description = "合同签订日期", requiredMode = Schema.RequiredMode.REQUIRED)
  71 + @NotNull(message = "合同签订日期不能为空")
  72 + @DiffLogField(name = "合同签订日期")
  73 + private LocalDateTime signTime;
  74 +
  75 + @Schema(description = "备注", example = "首次签订劳动合同")
  76 + @Size(max = 500, message = "备注长度不能超过 500 个字符")
  77 + @DiffLogField(name = "备注")
  78 + private String remark;
  79 +
  80 + @Schema(description = "是否到期提醒", example = "true")
  81 + @DiffLogField(name = "是否到期提醒")
  82 + private Boolean expireRemind;
  83 +
  84 + @Schema(description = "附件地址数组", example = "[https://example.com/contract.pdf]")
  85 + @DiffLogField(name = "附件")
  86 + private List<String> fileUrls;
  87 +
  88 + @Schema(description = "排序", example = "1")
  89 + @DiffLogField(name = "排序")
  90 + private Integer sort;
  91 +
  92 + @AssertTrue(message = "合同结束日期不能早于开始日期")
  93 + @JsonIgnore
  94 + public boolean isTimeRangeValid() {
  95 + return startTime == null || endTime == null || isAfterOrEqual(endTime, startTime);
  96 + }
  97 +
  98 + @AssertTrue(message = "合同期限不能为空")
  99 + @JsonIgnore
  100 + public boolean isTermValid() {
  101 + return type == null
  102 + || HrmEmployeeContractTypeEnum.NON_FIXED_TERM_LABOR_CONTRACT.getType().equals(type)
  103 + || term != null;
  104 + }
  105 +
  106 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/educationexperience/HrmEmployeeEducationExperienceRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.educationexperience;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - HRM 员工教育经历 Response VO")
  9 +@Data
  10 +public class HrmEmployeeEducationExperienceRespVO {
  11 +
  12 + @Schema(description = "教育经历编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  16 + private Long employeeId;
  17 +
  18 + @Schema(description = "学历", example = "8")
  19 + private Integer education;
  20 +
  21 + @Schema(description = "毕业院校", example = "浙江大学")
  22 + private String graduateSchool;
  23 +
  24 + @Schema(description = "专业", example = "软件工程")
  25 + private String major;
  26 +
  27 + @Schema(description = "入学日期")
  28 + private LocalDateTime admissionTime;
  29 +
  30 + @Schema(description = "毕业日期")
  31 + private LocalDateTime graduationTime;
  32 +
  33 + @Schema(description = "教学方式", example = "1")
  34 + private Integer teachingMethods;
  35 +
  36 + @Schema(description = "是否第一学历", example = "true")
  37 + private Boolean firstDegree;
  38 +
  39 + @Schema(description = "排序", example = "1")
  40 + private Integer sort;
  41 +
  42 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  43 + private LocalDateTime createTime;
  44 +
  45 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/educationexperience/HrmEmployeeEducationExperienceSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.educationexperience;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonIgnore;
  4 +import com.mzt.logapi.starter.annotation.DiffLogField;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import jakarta.validation.constraints.AssertTrue;
  7 +import jakarta.validation.constraints.NotBlank;
  8 +import jakarta.validation.constraints.NotNull;
  9 +import jakarta.validation.constraints.Size;
  10 +import lombok.Data;
  11 +
  12 +import java.time.LocalDateTime;
  13 +
  14 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isAfterOrEqual;
  15 +
  16 +@Schema(description = "管理后台 - HRM 员工教育经历保存 Request VO")
  17 +@Data
  18 +public class HrmEmployeeEducationExperienceSaveReqVO {
  19 +
  20 + @Schema(description = "教育经历编号", example = "1024")
  21 + private Long id;
  22 +
  23 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  24 + @NotNull(message = "员工编号不能为空")
  25 + private Long employeeId;
  26 +
  27 + @Schema(description = "学历", requiredMode = Schema.RequiredMode.REQUIRED, example = "8")
  28 + @NotNull(message = "学历不能为空")
  29 + @DiffLogField(name = "学历")
  30 + private Integer education;
  31 +
  32 + @Schema(description = "毕业院校", requiredMode = Schema.RequiredMode.REQUIRED, example = "浙江大学")
  33 + @NotBlank(message = "毕业院校不能为空")
  34 + @Size(max = 255, message = "毕业院校长度不能超过 255 个字符")
  35 + @DiffLogField(name = "毕业院校")
  36 + private String graduateSchool;
  37 +
  38 + @Schema(description = "专业", requiredMode = Schema.RequiredMode.REQUIRED, example = "软件工程")
  39 + @NotBlank(message = "专业不能为空")
  40 + @Size(max = 255, message = "专业长度不能超过 255 个字符")
  41 + @DiffLogField(name = "专业")
  42 + private String major;
  43 +
  44 + @Schema(description = "入学日期")
  45 + @DiffLogField(name = "入学日期")
  46 + private LocalDateTime admissionTime;
  47 +
  48 + @Schema(description = "毕业日期")
  49 + @DiffLogField(name = "毕业日期")
  50 + private LocalDateTime graduationTime;
  51 +
  52 + @Schema(description = "教学方式", example = "1")
  53 + @DiffLogField(name = "教学方式")
  54 + private Integer teachingMethods;
  55 +
  56 + @Schema(description = "是否第一学历", example = "true")
  57 + @DiffLogField(name = "是否第一学历")
  58 + private Boolean firstDegree;
  59 +
  60 + @Schema(description = "排序", example = "1")
  61 + @DiffLogField(name = "排序")
  62 + private Integer sort;
  63 +
  64 + @AssertTrue(message = "毕业日期不能早于入学日期")
  65 + @JsonIgnore
  66 + public boolean isTimeRangeValid() {
  67 + return admissionTime == null || graduationTime == null
  68 + || isAfterOrEqual(graduationTime, admissionTime);
  69 + }
  70 +
  71 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeCancelQuitReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotBlank;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import jakarta.validation.constraints.Size;
  7 +import lombok.Data;
  8 +
  9 +@Schema(description = "管理后台 - HRM 员工取消离职 Request VO")
  10 +@Data
  11 +public class HrmEmployeeCancelQuitReqVO {
  12 +
  13 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  14 + @NotNull(message = "员工编号不能为空")
  15 + private Long employeeId;
  16 +
  17 + @Schema(description = "取消原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "员工撤回离职申请")
  18 + @NotBlank(message = "取消原因不能为空")
  19 + @Size(max = 500, message = "取消原因不能超过 500 个字符")
  20 + private String reason;
  21 +
  22 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeConfirmEntryReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonIgnore;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.constraints.AssertTrue;
  6 +import lombok.Data;
  7 +import lombok.EqualsAndHashCode;
  8 +
  9 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.beforeOrEqualNow;
  10 +
  11 +@Schema(description = "管理后台 - HRM 员工确认入职 Request VO")
  12 +@Data
  13 +@EqualsAndHashCode(callSuper = true)
  14 +public class HrmEmployeeConfirmEntryReqVO extends HrmEmployeeSaveReqVO {
  15 +
  16 + @AssertTrue(message = "入职时间不能晚于当前时间")
  17 + @JsonIgnore
  18 + public boolean isEntryTimeValid() {
  19 + return getEntryTime() == null || beforeOrEqualNow(getEntryTime());
  20 + }
  21 +
  22 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeCreateFromUserReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import cn.hutool.core.util.ObjUtil;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  5 +import cn.iocoder.yudao.framework.common.validation.Mobile;
  6 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum;
  7 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTypeEnum;
  8 +import com.fasterxml.jackson.annotation.JsonIgnore;
  9 +import io.swagger.v3.oas.annotations.media.Schema;
  10 +import jakarta.validation.constraints.AssertTrue;
  11 +import jakarta.validation.constraints.Max;
  12 +import jakarta.validation.constraints.Min;
  13 +import jakarta.validation.constraints.NotBlank;
  14 +import jakarta.validation.constraints.NotNull;
  15 +import jakarta.validation.constraints.Size;
  16 +import lombok.Data;
  17 +
  18 +import java.time.LocalDateTime;
  19 +
  20 +@Schema(description = "管理后台 - HRM 从后台用户创建员工 Request VO")
  21 +@Data
  22 +public class HrmEmployeeCreateFromUserReqVO {
  23 +
  24 + @Schema(description = "后台用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  25 + @NotNull(message = "后台用户编号不能为空")
  26 + private Long userId;
  27 +
  28 + @Schema(description = "工号", requiredMode = Schema.RequiredMode.REQUIRED, example = "HRM001")
  29 + @NotBlank(message = "工号不能为空")
  30 + @Size(max = 64, message = "工号不能超过 64 个字符")
  31 + private String jobNumber;
  32 +
  33 + @Schema(description = "员工手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300")
  34 + @NotBlank(message = "员工手机号不能为空")
  35 + @Mobile
  36 + private String mobile;
  37 +
  38 + @Schema(description = "部门编号", example = "100")
  39 + private Long deptId;
  40 +
  41 + @Schema(description = "直属上级员工编号", example = "1")
  42 + private Long leaderEmployeeId;
  43 +
  44 + @Schema(description = "聘用形式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  45 + @NotNull(message = "聘用形式不能为空")
  46 + @InEnum(value = HrmEmployeeTypeEnum.class, message = "聘用形式必须是 {value}")
  47 + private Integer type;
  48 +
  49 + @Schema(description = "员工状态;非正式员工必填", example = "3")
  50 + @InEnum(value = HrmEmployeeStatusEnum.class, message = "员工状态必须是 {value}")
  51 + private Integer status;
  52 +
  53 + @Schema(description = "入职时间", requiredMode = Schema.RequiredMode.REQUIRED)
  54 + @NotNull(message = "入职时间不能为空")
  55 + private LocalDateTime entryTime;
  56 +
  57 + @Schema(description = "试用期,单位:月;正式员工必填", example = "3")
  58 + @Min(value = 0, message = "试用期不能小于 0")
  59 + @Max(value = 6, message = "试用期不能超过 6 个月")
  60 + private Integer probation;
  61 +
  62 + @Schema(description = "职位名称", example = "Java 工程师")
  63 + @Size(max = 255, message = "职位名称不能超过 255 个字符")
  64 + private String postName;
  65 +
  66 + @Schema(description = "岗位职级", example = "P6")
  67 + @Size(max = 255, message = "岗位职级不能超过 255 个字符")
  68 + private String postLevel;
  69 +
  70 + @Schema(description = "工作城市", example = "杭州")
  71 + @Size(max = 64, message = "工作城市不能超过 64 个字符")
  72 + private String workCity;
  73 +
  74 + @Schema(description = "工作地点", example = "西湖区")
  75 + @Size(max = 255, message = "工作地点不能超过 255 个字符")
  76 + private String workAddress;
  77 +
  78 + @Schema(description = "备注", example = "从后台用户批量建档")
  79 + @Size(max = 500, message = "备注不能超过 500 个字符")
  80 + private String remark;
  81 +
  82 + @AssertTrue(message = "正式员工试用期不能为空")
  83 + @JsonIgnore
  84 + public boolean isProbationValid() {
  85 + return ObjUtil.notEqual(HrmEmployeeTypeEnum.FORMAL.getType(), type) || probation != null;
  86 + }
  87 +
  88 + @AssertTrue(message = "非正式员工状态不能为空")
  89 + @JsonIgnore
  90 + public boolean isStatusRequired() {
  91 + return ObjUtil.notEqual(HrmEmployeeTypeEnum.INFORMAL.getType(), type) || status != null;
  92 + }
  93 +
  94 + @AssertTrue(message = "员工状态与聘用形式不匹配")
  95 + @JsonIgnore
  96 + public boolean isStatusValid() {
  97 + return ObjUtil.notEqual(HrmEmployeeTypeEnum.INFORMAL.getType(), type)
  98 + || status == null || HrmEmployeeStatusEnum.INFORMAL_STATUSES.contains(status);
  99 + }
  100 +
  101 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeDeptStatisticsRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - HRM 员工部门统计 Response VO")
  8 +@Data
  9 +@AllArgsConstructor
  10 +public class HrmEmployeeDeptStatisticsRespVO {
  11 +
  12 + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
  13 + private Long deptId;
  14 +
  15 + @Schema(description = "在职员工人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
  16 + private Long activeCount;
  17 +
  18 + @Schema(description = "全职员工人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "8")
  19 + private Long fullTimeCount;
  20 +
  21 + @Schema(description = "非全职员工人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  22 + private Long nonFullTimeCount;
  23 +
  24 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeImportExcelVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
  4 +import cn.idev.excel.annotation.ExcelProperty;
  5 +import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect;
  6 +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
  7 +import cn.iocoder.yudao.framework.excel.core.convert.AreaConvert;
  8 +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
  9 +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants;
  10 +import cn.iocoder.yudao.module.hrm.framework.excel.core.HrmEmployeeProbationExcelColumnSelectFunction;
  11 +import cn.iocoder.yudao.module.hrm.framework.excel.core.HrmRecruitChannelExcelColumnSelectFunction;
  12 +import io.swagger.v3.oas.annotations.media.Schema;
  13 +import lombok.AllArgsConstructor;
  14 +import lombok.Builder;
  15 +import lombok.Data;
  16 +import lombok.NoArgsConstructor;
  17 +
  18 +import java.time.LocalDateTime;
  19 +
  20 +@Schema(description = "管理后台 - HRM 员工档案 Excel 导入 VO")
  21 +@Data
  22 +@Builder
  23 +@AllArgsConstructor
  24 +@NoArgsConstructor
  25 +@ExcelIgnoreUnannotated
  26 +public class HrmEmployeeImportExcelVO {
  27 +
  28 + @ExcelProperty("员工姓名")
  29 + @Schema(description = "员工姓名")
  30 + private String name;
  31 +
  32 + @ExcelProperty("工号")
  33 + @Schema(description = "工号")
  34 + private String jobNumber;
  35 +
  36 + @ExcelProperty("手机号")
  37 + @Schema(description = "手机号")
  38 + private String mobile;
  39 +
  40 + @ExcelProperty("国家或地区")
  41 + @Schema(description = "国家或地区")
  42 + private String country;
  43 +
  44 + @ExcelProperty("民族")
  45 + @Schema(description = "民族")
  46 + private String nation;
  47 +
  48 + @ExcelProperty(value = "证件类型", converter = DictConvert.class)
  49 + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_ID_TYPE)
  50 + @ExcelColumnSelect(dictType = DictTypeConstants.HRM_EMPLOYEE_ID_TYPE)
  51 + @Schema(description = "证件类型")
  52 + private Integer idType;
  53 +
  54 + @ExcelProperty("证件号码")
  55 + @Schema(description = "证件号码")
  56 + private String idNumber;
  57 +
  58 + @ExcelProperty(value = "性别", converter = DictConvert.class)
  59 + @DictFormat(cn.iocoder.yudao.module.system.enums.DictTypeConstants.USER_SEX)
  60 + @ExcelColumnSelect(dictType = cn.iocoder.yudao.module.system.enums.DictTypeConstants.USER_SEX)
  61 + @Schema(description = "性别")
  62 + private Integer sex;
  63 +
  64 + @ExcelProperty("邮箱")
  65 + @Schema(description = "邮箱")
  66 + private String email;
  67 +
  68 + @ExcelProperty("籍贯")
  69 + @Schema(description = "籍贯")
  70 + private String nativePlace;
  71 +
  72 + @ExcelProperty("出生日期")
  73 + @Schema(description = "出生日期")
  74 + private LocalDateTime birthday;
  75 +
  76 + @ExcelProperty("户籍地址")
  77 + @Schema(description = "户籍地址")
  78 + private String address;
  79 +
  80 + @ExcelProperty(value = "最高学历", converter = DictConvert.class)
  81 + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_EDUCATION)
  82 + @ExcelColumnSelect(dictType = DictTypeConstants.HRM_EMPLOYEE_EDUCATION)
  83 + @Schema(description = "最高学历")
  84 + private Integer highestEducation;
  85 +
  86 + @ExcelProperty("部门编号")
  87 + @Schema(description = "部门编号")
  88 + private Long deptId;
  89 +
  90 + @ExcelProperty("直属上级工号")
  91 + @Schema(description = "直属上级工号")
  92 + private String leaderJobNumber;
  93 +
  94 + @ExcelProperty("职位名称")
  95 + @Schema(description = "职位名称")
  96 + private String postName;
  97 +
  98 + @ExcelProperty("岗位职级")
  99 + @Schema(description = "岗位职级")
  100 + private String postLevel;
  101 +
  102 + @ExcelProperty(value = "入职状态", converter = DictConvert.class)
  103 + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_ENTRY_STATUS)
  104 + @ExcelColumnSelect(dictType = DictTypeConstants.HRM_EMPLOYEE_ENTRY_STATUS)
  105 + @Schema(description = "入职状态")
  106 + private Integer entryStatus;
  107 +
  108 + @ExcelProperty(value = "员工状态", converter = DictConvert.class)
  109 + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_STATUS)
  110 + @ExcelColumnSelect(dictType = DictTypeConstants.HRM_EMPLOYEE_STATUS)
  111 + @Schema(description = "员工状态")
  112 + private Integer status;
  113 +
  114 + @ExcelProperty(value = "聘用形式", converter = DictConvert.class)
  115 + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_TYPE)
  116 + @ExcelColumnSelect(dictType = DictTypeConstants.HRM_EMPLOYEE_TYPE)
  117 + @Schema(description = "聘用形式")
  118 + private Integer type;
  119 +
  120 + @ExcelProperty("入职时间")
  121 + @Schema(description = "入职时间")
  122 + private LocalDateTime entryTime;
  123 +
  124 + @ExcelProperty("试用期(月)")
  125 + @ExcelColumnSelect(functionName = HrmEmployeeProbationExcelColumnSelectFunction.NAME)
  126 + @Schema(description = "试用期(月)")
  127 + private Integer probation;
  128 +
  129 + @ExcelProperty("转正时间")
  130 + @Schema(description = "转正时间")
  131 + private LocalDateTime regularTime;
  132 +
  133 + @ExcelProperty("离职时间")
  134 + @Schema(description = "离职时间")
  135 + private LocalDateTime leaveTime;
  136 +
  137 + @ExcelProperty("工作城市")
  138 + @Schema(description = "工作城市")
  139 + private String workCity;
  140 +
  141 + @ExcelProperty("工作地点")
  142 + @Schema(description = "工作地点")
  143 + private String workAddress;
  144 +
  145 + @ExcelProperty("工作详细地址")
  146 + @Schema(description = "工作详细地址")
  147 + private String workDetailAddress;
  148 +
  149 + @ExcelProperty("招聘渠道")
  150 + @ExcelColumnSelect(functionName = HrmRecruitChannelExcelColumnSelectFunction.NAME)
  151 + @Schema(description = "招聘渠道选项,格式为名称(ID)")
  152 + private String channelName;
  153 +
  154 + @ExcelProperty("司龄起算时间")
  155 + @Schema(description = "司龄起算时间")
  156 + private LocalDateTime companyAgeStartTime;
  157 +
  158 + @ExcelProperty("绑定用户手机号")
  159 + @Schema(description = "绑定后台用户的手机号")
  160 + private String userMobile;
  161 +
  162 + @ExcelProperty("银行卡号")
  163 + @Schema(description = "银行卡号")
  164 + private String bankCardNumber;
  165 +
  166 + @ExcelProperty(value = "开户地区", converter = AreaConvert.class)
  167 + @Schema(description = "开户地区")
  168 + private Integer bankAreaId;
  169 +
  170 + @ExcelProperty("银行名称")
  171 + @Schema(description = "银行名称")
  172 + private String bankName;
  173 +
  174 + @ExcelProperty("开户支行名称")
  175 + @Schema(description = "开户支行名称")
  176 + private String bankBranchName;
  177 +
  178 + @ExcelProperty(value = "是否首次参保", converter = DictConvert.class)
  179 + @DictFormat(DictTypeConstants.BOOLEAN_STRING)
  180 + @ExcelColumnSelect(dictType = DictTypeConstants.BOOLEAN_STRING)
  181 + @Schema(description = "是否首次参保")
  182 + private Boolean firstSocialSecurity;
  183 +
  184 + @ExcelProperty(value = "是否首次缴纳公积金", converter = DictConvert.class)
  185 + @DictFormat(DictTypeConstants.BOOLEAN_STRING)
  186 + @ExcelColumnSelect(dictType = DictTypeConstants.BOOLEAN_STRING)
  187 + @Schema(description = "是否首次缴纳公积金")
  188 + private Boolean firstAccumulationFund;
  189 +
  190 + @ExcelProperty("个人社保账号")
  191 + @Schema(description = "个人社保账号")
  192 + private String socialSecurityNumber;
  193 +
  194 + @ExcelProperty("个人公积金账号")
  195 + @Schema(description = "个人公积金账号")
  196 + private String accumulationFundNumber;
  197 +
  198 + @ExcelProperty("社保起缴月份")
  199 + @Schema(description = "社保起缴月份")
  200 + private LocalDateTime socialSecurityStartMonth;
  201 +
  202 + @ExcelProperty("参保方案名称")
  203 + @Schema(description = "参保方案名称")
  204 + private String schemeName;
  205 +
  206 + @ExcelProperty("备注")
  207 + @Schema(description = "备注")
  208 + private String remark;
  209 +
  210 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeImportRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Builder;
  5 +import lombok.Data;
  6 +
  7 +import java.util.List;
  8 +import java.util.Map;
  9 +
  10 +@Schema(description = "管理后台 - HRM 员工档案导入 Response VO")
  11 +@Data
  12 +@Builder
  13 +public class HrmEmployeeImportRespVO {
  14 +
  15 + @Schema(description = "创建成功的工号数组", requiredMode = Schema.RequiredMode.REQUIRED)
  16 + private List<String> createJobNumbers;
  17 +
  18 + @Schema(description = "更新成功的工号数组", requiredMode = Schema.RequiredMode.REQUIRED)
  19 + private List<String> updateJobNumbers;
  20 +
  21 + @Schema(description = "因重复而跳过的工号数组", requiredMode = Schema.RequiredMode.REQUIRED)
  22 + private List<String> skipJobNumbers;
  23 +
  24 + @Schema(description = "导入失败的员工集合,key 为原始行号和员工标识,value 为失败原因",
  25 + requiredMode = Schema.RequiredMode.REQUIRED)
  26 + private Map<String, String> failureJobNumbers;
  27 +
  28 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeListReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeEntryStatusEnum;
  5 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum;
  6 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusTabEnum;
  7 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeSurveyTypeEnum;
  8 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTypeEnum;
  9 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTodoTypeEnum;
  10 +import io.swagger.v3.oas.annotations.media.Schema;
  11 +import lombok.Data;
  12 +import org.springframework.format.annotation.DateTimeFormat;
  13 +
  14 +import java.time.LocalDateTime;
  15 +import java.util.List;
  16 +
  17 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  18 +
  19 +@Schema(description = "管理后台 - HRM 员工档案列表 Request VO")
  20 +@Data
  21 +public class HrmEmployeeListReqVO {
  22 +
  23 + @Schema(description = "员工编号列表", example = "1,2")
  24 + private List<Long> ids;
  25 +
  26 + @Schema(hidden = true)
  27 + private List<Long> excludeIds;
  28 +
  29 + @Schema(description = "员工姓名或工号,模糊匹配", example = "张三")
  30 + private String search;
  31 +
  32 + @Schema(description = "员工姓名,模糊匹配", example = "张")
  33 + private String name;
  34 +
  35 + @Schema(description = "工号,模糊匹配", example = "HRM")
  36 + private String jobNumber;
  37 +
  38 + @Schema(description = "手机号,模糊匹配", example = "156")
  39 + private String mobile;
  40 +
  41 + @Schema(description = "部门编号", example = "100")
  42 + private Long deptId;
  43 +
  44 + @Schema(description = "部门编号数组", example = "100,101")
  45 + private List<Long> deptIds;
  46 +
  47 + @Schema(description = "直属上级员工编号", example = "1")
  48 + private Long leaderEmployeeId;
  49 +
  50 + @Schema(description = "性别", example = "1")
  51 + private Integer sex;
  52 +
  53 + @Schema(description = "入职时间范围")
  54 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  55 + private LocalDateTime[] entryTime;
  56 +
  57 + @Schema(description = "职位名称,模糊匹配", example = "Java")
  58 + private String postName;
  59 +
  60 + @Schema(description = "转正时间范围")
  61 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  62 + private LocalDateTime[] regularTime;
  63 +
  64 + @Schema(description = "工作地点,模糊匹配", example = "西湖区")
  65 + private String workAddress;
  66 +
  67 + @Schema(description = "招聘渠道编号", example = "10")
  68 + private Long channelId;
  69 +
  70 + @Schema(description = "聘用形式", example = "1")
  71 + @InEnum(value = HrmEmployeeTypeEnum.class, message = "聘用形式必须是 {value}")
  72 + private Integer type;
  73 +
  74 + @Schema(description = "入职状态", example = "1")
  75 + @InEnum(value = HrmEmployeeEntryStatusEnum.class, message = "入职状态必须是 {value}")
  76 + private Integer entryStatus;
  77 +
  78 + @Schema(description = "员工状态", example = "1")
  79 + @InEnum(value = HrmEmployeeStatusEnum.class, message = "员工状态必须是 {value}")
  80 + private Integer status;
  81 +
  82 + @Schema(description = "员工状态分类", example = "12")
  83 + @InEnum(value = HrmEmployeeStatusTabEnum.class, message = "员工状态分类必须是 {value}")
  84 + private Integer statusCategory;
  85 +
  86 + @Schema(description = "首页人事概况筛选类型", example = "1")
  87 + @InEnum(value = HrmEmployeeSurveyTypeEnum.class, message = "首页人事概况筛选类型必须是 {value}")
  88 + private Integer surveyType;
  89 +
  90 + @Schema(description = "首页待办筛选类型", example = "5")
  91 + @InEnum(value = HrmEmployeeTodoTypeEnum.class, message = "首页待办筛选类型必须是 {value}")
  92 + private Integer todoType;
  93 +
  94 + @Schema(hidden = true)
  95 + private LocalDateTime[] activeTime;
  96 +
  97 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeNotifyRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +import lombok.experimental.Accessors;
  6 +
  7 +@Schema(description = "管理后台 - HRM 员工通知发送结果 Response VO")
  8 +@Data
  9 +@Accessors(chain = true)
  10 +public class HrmEmployeeNotifyRespVO {
  11 +
  12 + @Schema(description = "发送成功数量", example = "10")
  13 + private Integer successCount;
  14 +
  15 + @Schema(description = "无后台账号跳过数量", example = "2")
  16 + private Integer skippedCount;
  17 +
  18 + @Schema(description = "发送失败数量", example = "1")
  19 + private Integer failureCount;
  20 +
  21 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeePageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  5 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeEntryStatusEnum;
  6 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum;
  7 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusTabEnum;
  8 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeSurveyTypeEnum;
  9 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTypeEnum;
  10 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTodoTypeEnum;
  11 +import io.swagger.v3.oas.annotations.media.Schema;
  12 +import lombok.Data;
  13 +import lombok.EqualsAndHashCode;
  14 +import org.springframework.format.annotation.DateTimeFormat;
  15 +
  16 +import java.time.LocalDateTime;
  17 +import java.util.List;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  20 +
  21 +@Schema(description = "管理后台 - HRM 员工档案分页 Request VO")
  22 +@Data
  23 +@EqualsAndHashCode(callSuper = true)
  24 +public class HrmEmployeePageReqVO extends SortablePageParam {
  25 +
  26 + @Schema(description = "员工编号列表", example = "1,2")
  27 + private List<Long> ids;
  28 +
  29 + @Schema(hidden = true)
  30 + private List<Long> excludeIds;
  31 +
  32 + @Schema(description = "员工姓名、工号或手机号,模糊匹配", example = "张三")
  33 + private String search;
  34 +
  35 + @Schema(description = "员工姓名,模糊匹配", example = "张")
  36 + private String name;
  37 +
  38 + @Schema(description = "工号,模糊匹配", example = "HRM")
  39 + private String jobNumber;
  40 +
  41 + @Schema(description = "手机号,模糊匹配", example = "156")
  42 + private String mobile;
  43 +
  44 + @Schema(description = "部门编号", example = "100")
  45 + private Long deptId;
  46 +
  47 + @Schema(description = "部门编号数组", example = "100,101")
  48 + private List<Long> deptIds;
  49 +
  50 + @Schema(description = "直属上级员工编号", example = "1")
  51 + private Long leaderEmployeeId;
  52 +
  53 + @Schema(description = "性别", example = "1")
  54 + private Integer sex;
  55 +
  56 + @Schema(description = "入职时间范围")
  57 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  58 + private LocalDateTime[] entryTime;
  59 +
  60 + @Schema(description = "职位名称,模糊匹配", example = "Java")
  61 + private String postName;
  62 +
  63 + @Schema(description = "转正时间范围")
  64 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  65 + private LocalDateTime[] regularTime;
  66 +
  67 + @Schema(description = "工作地点,模糊匹配", example = "西湖区")
  68 + private String workAddress;
  69 +
  70 + @Schema(description = "招聘渠道编号", example = "10")
  71 + private Long channelId;
  72 +
  73 + @Schema(description = "聘用形式", example = "1")
  74 + @InEnum(value = HrmEmployeeTypeEnum.class, message = "聘用形式必须是 {value}")
  75 + private Integer type;
  76 +
  77 + @Schema(description = "入职状态", example = "1")
  78 + @InEnum(value = HrmEmployeeEntryStatusEnum.class, message = "入职状态必须是 {value}")
  79 + private Integer entryStatus;
  80 +
  81 + @Schema(description = "员工状态", example = "1")
  82 + @InEnum(value = HrmEmployeeStatusEnum.class, message = "员工状态必须是 {value}")
  83 + private Integer status;
  84 +
  85 + @Schema(description = "员工状态分类", example = "12")
  86 + @InEnum(value = HrmEmployeeStatusTabEnum.class, message = "员工状态分类必须是 {value}")
  87 + private Integer statusCategory;
  88 +
  89 + @Schema(description = "首页人事概况筛选类型", example = "1")
  90 + @InEnum(value = HrmEmployeeSurveyTypeEnum.class, message = "首页人事概况筛选类型必须是 {value}")
  91 + private Integer surveyType;
  92 +
  93 + @Schema(description = "首页待办筛选类型", example = "5")
  94 + @InEnum(value = HrmEmployeeTodoTypeEnum.class, message = "首页待办筛选类型必须是 {value}")
  95 + private Integer todoType;
  96 +
  97 + @Schema(hidden = true)
  98 + private LocalDateTime[] activeTime;
  99 +
  100 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeQuitReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeQuitReasonEnum;
  5 +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeQuitTypeEnum;
  6 +import com.fasterxml.jackson.annotation.JsonIgnore;
  7 +import io.swagger.v3.oas.annotations.media.Schema;
  8 +import jakarta.validation.constraints.AssertTrue;
  9 +import jakarta.validation.constraints.NotNull;
  10 +import jakarta.validation.constraints.Size;
  11 +import lombok.Data;
  12 +
  13 +import java.time.LocalDateTime;
  14 +
  15 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isAfterOrEqual;
  16 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isBeforeOrEqual;
  17 +
  18 +@Schema(description = "管理后台 - HRM 员工离职 Request VO")
  19 +@Data
  20 +public class HrmEmployeeQuitReqVO {
  21 +
  22 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  23 + @NotNull(message = "员工编号不能为空")
  24 + private Long employeeId;
  25 +
  26 + @Schema(description = "计划离职时间", requiredMode = Schema.RequiredMode.REQUIRED)
  27 + @NotNull(message = "计划离职时间不能为空")
  28 + private LocalDateTime planQuitTime;
  29 +
  30 + @Schema(description = "申请离职时间", requiredMode = Schema.RequiredMode.REQUIRED)
  31 + @NotNull(message = "申请离职时间不能为空")
  32 + private LocalDateTime applyQuitTime;
  33 +
  34 + @Schema(description = "薪资结算时间", requiredMode = Schema.RequiredMode.REQUIRED)
  35 + @NotNull(message = "薪资结算时间不能为空")
  36 + private LocalDateTime salarySettlementTime;
  37 +
  38 + @Schema(description = "离职类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  39 + @NotNull(message = "离职类型不能为空")
  40 + @InEnum(value = HrmEmployeeQuitTypeEnum.class, message = "离职类型必须是 {value}")
  41 + private Integer type;
  42 +
  43 + @Schema(description = "离职原因", example = "1")
  44 + @InEnum(value = HrmEmployeeQuitReasonEnum.class, message = "离职原因必须是 {value}")
  45 + private Integer reason;
  46 +
  47 + @Schema(description = "备注", example = "个人原因离职")
  48 + @Size(max = 500, message = "备注不能超过 500 个字符")
  49 + private String remark;
  50 +
  51 + @AssertTrue(message = "申请离职时间不能晚于计划离职时间")
  52 + @JsonIgnore
  53 + public boolean isApplyQuitTimeValid() {
  54 + return applyQuitTime == null || planQuitTime == null
  55 + || isBeforeOrEqual(applyQuitTime.toLocalDate(), planQuitTime.toLocalDate());
  56 + }
  57 +
  58 + @AssertTrue(message = "薪资结算时间不能早于计划离职时间")
  59 + @JsonIgnore
  60 + public boolean isSalarySettlementTimeValid() {
  61 + return salarySettlementTime == null || planQuitTime == null
  62 + || isAfterOrEqual(salarySettlementTime.toLocalDate(), planQuitTime.toLocalDate());
  63 + }
  64 +
  65 + @AssertTrue(message = "离职原因与离职类型不匹配")
  66 + @JsonIgnore
  67 + public boolean isReasonValid() {
  68 + if (type == null || HrmEmployeeQuitTypeEnum.RETIREMENT.getType().equals(type)) {
  69 + return true;
  70 + }
  71 + HrmEmployeeQuitReasonEnum quitReason = HrmEmployeeQuitReasonEnum.valueOf(reason);
  72 + return quitReason != null && quitReason.getQuitType().equals(type);
  73 + }
  74 +
  75 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeRehireReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonIgnore;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.constraints.AssertTrue;
  6 +import jakarta.validation.constraints.NotNull;
  7 +import lombok.Data;
  8 +import lombok.EqualsAndHashCode;
  9 +
  10 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.beforeOrEqualNow;
  11 +
  12 +@Schema(description = "管理后台 - HRM 员工再入职 Request VO")
  13 +@Data
  14 +@EqualsAndHashCode(callSuper = true)
  15 +public class HrmEmployeeRehireReqVO extends HrmEmployeeSaveReqVO {
  16 +
  17 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  18 + @NotNull(message = "员工编号不能为空")
  19 + private Long employeeId;
  20 +
  21 + @AssertTrue(message = "入职时间不能晚于当前时间")
  22 + @JsonIgnore
  23 + public boolean isEntryTimeValid() {
  24 + return getEntryTime() == null || beforeOrEqualNow(getEntryTime());
  25 + }
  26 +
  27 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
  4 +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
  5 +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants;
  6 +import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
  7 +import cn.idev.excel.annotation.ExcelProperty;
  8 +import io.swagger.v3.oas.annotations.media.Schema;
  9 +import lombok.Data;
  10 +import lombok.experimental.Accessors;
  11 +
  12 +import java.time.LocalDateTime;
  13 +
  14 +@Schema(description = "管理后台 - HRM 员工档案 Response VO")
  15 +@Data
  16 +@Accessors(chain = true)
  17 +@ExcelIgnoreUnannotated
  18 +public class HrmEmployeeRespVO {
  19 +
  20 + @ExcelProperty(value = "员工编号", index = 0)
  21 + @Schema(description = "员工档案编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  22 + private Long id;
  23 +
  24 + @ExcelProperty(value = "员工姓名", index = 1)
  25 + @Schema(description = "员工姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
  26 + private String name;
  27 +
  28 + @ExcelProperty(value = "工号", index = 2)
  29 + @Schema(description = "工号", example = "HRM001")
  30 + private String jobNumber;
  31 +
  32 + @Schema(description = "系统用户编号", example = "1024")
  33 + private Long userId;
  34 +
  35 + @ExcelProperty(value = "绑定用户", index = 17)
  36 + @Schema(description = "后台用户昵称", example = "芋道")
  37 + private String userNickname;
  38 +
  39 + @ExcelProperty(value = "手机号", index = 3)
  40 + @Schema(description = "手机号", example = "15601691300")
  41 + private String mobile;
  42 +
  43 + @Schema(description = "国家地区", example = "中国")
  44 + private String country;
  45 +
  46 + @Schema(description = "民族", example = "汉族")
  47 + private String nation;
  48 +
  49 + @Schema(description = "证件类型", example = "1")
  50 + private Integer idType;
  51 +
  52 + @Schema(description = "证件号码", example = "310101199001011234")
  53 + private String idNumber;
  54 +
  55 + @Schema(description = "性别", example = "1")
  56 + private Integer sex;
  57 +
  58 + @Schema(description = "邮箱", example = "hrm@example.com")
  59 + private String email;
  60 +
  61 + @Schema(description = "籍贯", example = "浙江杭州")
  62 + private String nativePlace;
  63 +
  64 + @Schema(description = "出生时间")
  65 + private LocalDateTime birthday;
  66 +
  67 + @Schema(description = "年龄", example = "28")
  68 + private Integer age;
  69 +
  70 + @Schema(description = "户籍地址", example = "杭州市西湖区")
  71 + private String address;
  72 +
  73 + @Schema(description = "最高学历", example = "8")
  74 + private Integer highestEducation;
  75 +
  76 + @Schema(description = "部门编号", example = "100")
  77 + private Long deptId;
  78 +
  79 + @ExcelProperty(value = "部门", index = 4)
  80 + @Schema(description = "部门名称", example = "研发部")
  81 + private String deptName;
  82 +
  83 + @Schema(description = "直属上级员工编号", example = "1")
  84 + private Long leaderEmployeeId;
  85 +
  86 + @ExcelProperty(value = "直属上级", index = 7)
  87 + @Schema(description = "直属上级员工姓名", example = "李四")
  88 + private String leaderEmployeeName;
  89 +
  90 + @ExcelProperty(value = "入职状态", index = 8, converter = DictConvert.class)
  91 + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_ENTRY_STATUS)
  92 + @Schema(description = "入职状态", example = "1")
  93 + private Integer entryStatus;
  94 +
  95 + @ExcelProperty(value = "员工状态", index = 9, converter = DictConvert.class)
  96 + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_STATUS)
  97 + @Schema(description = "员工状态", example = "1")
  98 + private Integer status;
  99 +
  100 + @ExcelProperty(value = "聘用形式", index = 10, converter = DictConvert.class)
  101 + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_TYPE)
  102 + @Schema(description = "聘用形式", example = "1")
  103 + private Integer type;
  104 +
  105 + @ExcelProperty(value = "入职时间", index = 11)
  106 + @Schema(description = "入职时间")
  107 + private LocalDateTime entryTime;
  108 +
  109 + @Schema(description = "试用期,单位月;0 表示无试用期", example = "3")
  110 + private Integer probation;
  111 +
  112 + @ExcelProperty(value = "转正时间", index = 12)
  113 + @Schema(description = "转正时间")
  114 + private LocalDateTime regularTime;
  115 +
  116 + @ExcelProperty(value = "离职时间", index = 13)
  117 + @Schema(description = "离职时间")
  118 + private LocalDateTime leaveTime;
  119 +
  120 + @ExcelProperty(value = "岗位", index = 5)
  121 + @Schema(description = "职位名称", example = "Java 工程师")
  122 + private String postName;
  123 +
  124 + @ExcelProperty(value = "岗位职级", index = 6)
  125 + @Schema(description = "岗位职级", example = "P6")
  126 + private String postLevel;
  127 +
  128 + @ExcelProperty(value = "工作城市", index = 14)
  129 + @Schema(description = "工作城市", example = "上海")
  130 + private String workCity;
  131 +
  132 + @ExcelProperty(value = "工作地点", index = 15)
  133 + @Schema(description = "工作地址", example = "浦东新区")
  134 + private String workAddress;
  135 +
  136 + @Schema(description = "工作详细地址", example = "张江高科 1 号楼")
  137 + private String workDetailAddress;
  138 +
  139 + @Schema(description = "招聘渠道编号", example = "10")
  140 + private Long channelId;
  141 +
  142 + @ExcelProperty(value = "招聘渠道", index = 16)
  143 + @Schema(description = "招聘渠道名称", example = "BOSS直聘")
  144 + private String channelName;
  145 +
  146 + @Schema(description = "司龄开始时间")
  147 + private LocalDateTime companyAgeStartTime;
  148 +
  149 + @Schema(description = "司龄,单位年", example = "2")
  150 + private Integer companyAge;
  151 +
  152 + @Schema(description = "关联招聘候选人编号", example = "200")
  153 + private Long candidateId;
  154 +
  155 + // ==================== 工资卡信息 ====================
  156 +
  157 + @Schema(description = "银行卡号", example = "622202600001")
  158 + private String salaryCardNumber;
  159 +
  160 + @Schema(description = "开户地区编号", example = "440300")
  161 + private Integer salaryCardAreaId;
  162 +
  163 + @Schema(description = "开户地区名称", example = "广东省 深圳市")
  164 + private String salaryCardAreaName;
  165 +
  166 + @Schema(description = "银行名称", example = "招商银行")
  167 + private String salaryCardBankName;
  168 +
  169 + @Schema(description = "开户支行名称", example = "科技园支行")
  170 + private String salaryCardBankBranchName;
  171 +
  172 + // ==================== 社保信息 ====================
  173 +
  174 + @Schema(description = "个人社保账号", example = "SB20260001")
  175 + private String socialSecurityNumber;
  176 +
  177 + @Schema(description = "个人公积金账号", example = "GJJ20260001")
  178 + private String accumulationFundNumber;
  179 +
  180 + @Schema(description = "备注", example = "首批迁移员工")
  181 + private String remark;
  182 +
  183 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  184 + private LocalDateTime createTime;
  185 +
  186 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import cn.hutool.core.util.IdcardUtil;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  7 +import cn.iocoder.yudao.framework.common.validation.Mobile;
  8 +import cn.iocoder.yudao.module.hrm.enums.employee.experience.HrmEmployeeEducationEnum;
  9 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeEntryStatusEnum;
  10 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeIdTypeEnum;
  11 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum;
  12 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTypeEnum;
  13 +import com.fasterxml.jackson.annotation.JsonIgnore;
  14 +import io.swagger.v3.oas.annotations.media.Schema;
  15 +import jakarta.validation.constraints.AssertTrue;
  16 +import jakarta.validation.constraints.Email;
  17 +import jakarta.validation.constraints.Max;
  18 +import jakarta.validation.constraints.Min;
  19 +import jakarta.validation.constraints.NotBlank;
  20 +import jakarta.validation.constraints.NotNull;
  21 +import jakarta.validation.constraints.PastOrPresent;
  22 +import jakarta.validation.constraints.Size;
  23 +import lombok.Data;
  24 +
  25 +import java.time.LocalDateTime;
  26 +
  27 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isBeforeOrEqual;
  28 +
  29 +@Schema(description = "管理后台 - HRM 员工档案新增/修改 Request VO")
  30 +@Data
  31 +public class HrmEmployeeSaveReqVO {
  32 +
  33 + @Schema(description = "员工档案编号", example = "1024")
  34 + private Long id;
  35 +
  36 + @Schema(description = "员工姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
  37 + @NotBlank(message = "员工姓名不能为空")
  38 + @Size(max = 255, message = "员工姓名不能超过 255 个字符")
  39 + private String name;
  40 +
  41 + @Schema(description = "工号", example = "HRM001")
  42 + @Size(max = 64, message = "工号不能超过 64 个字符")
  43 + private String jobNumber;
  44 +
  45 + @Schema(description = "系统用户编号", example = "1024")
  46 + private Long userId;
  47 +
  48 + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300")
  49 + @NotBlank(message = "手机号不能为空")
  50 + @Mobile
  51 + private String mobile;
  52 +
  53 + @Schema(description = "国家地区", example = "中国")
  54 + @Size(max = 64, message = "国家地区不能超过 64 个字符")
  55 + private String country;
  56 +
  57 + @Schema(description = "民族", example = "汉族")
  58 + @Size(max = 64, message = "民族不能超过 64 个字符")
  59 + private String nation;
  60 +
  61 + @Schema(description = "证件类型", example = "1")
  62 + @InEnum(value = HrmEmployeeIdTypeEnum.class, message = "证件类型必须是 {value}")
  63 + private Integer idType;
  64 +
  65 + @Schema(description = "证件号码", example = "310101199001011234")
  66 + @Size(max = 255, message = "证件号码不能超过 255 个字符")
  67 + private String idNumber;
  68 +
  69 + @Schema(description = "性别", example = "1")
  70 + private Integer sex;
  71 +
  72 + @Schema(description = "邮箱", example = "hrm@example.com")
  73 + @Email(message = "邮箱格式不正确")
  74 + @Size(max = 255, message = "邮箱不能超过 255 个字符")
  75 + private String email;
  76 +
  77 + @Schema(description = "籍贯", example = "浙江杭州")
  78 + @Size(max = 128, message = "籍贯不能超过 128 个字符")
  79 + private String nativePlace;
  80 +
  81 + @Schema(description = "出生时间")
  82 + @PastOrPresent(message = "出生时间不能晚于当前时间")
  83 + private LocalDateTime birthday;
  84 +
  85 + @Schema(description = "年龄", example = "28")
  86 + private Integer age;
  87 +
  88 + @Schema(description = "户籍地址", example = "杭州市西湖区")
  89 + @Size(max = 255, message = "户籍地址不能超过 255 个字符")
  90 + private String address;
  91 +
  92 + @Schema(description = "最高学历", example = "8")
  93 + @InEnum(value = HrmEmployeeEducationEnum.class, message = "最高学历必须是 {value}")
  94 + private Integer highestEducation;
  95 +
  96 + @Schema(description = "部门编号", example = "100")
  97 + private Long deptId;
  98 +
  99 + @Schema(description = "直属上级员工编号", example = "1")
  100 + private Long leaderEmployeeId;
  101 +
  102 + @Schema(description = "入职状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  103 + @NotNull(message = "入职状态不能为空")
  104 + @InEnum(value = HrmEmployeeEntryStatusEnum.class, message = "入职状态必须是 {value}")
  105 + private Integer entryStatus;
  106 +
  107 + @Schema(description = "员工状态", example = "1")
  108 + @InEnum(value = HrmEmployeeStatusEnum.class, message = "员工状态必须是 {value}")
  109 + private Integer status;
  110 +
  111 + @Schema(description = "聘用形式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  112 + @NotNull(message = "聘用形式不能为空")
  113 + @InEnum(value = HrmEmployeeTypeEnum.class, message = "聘用形式必须是 {value}")
  114 + private Integer type;
  115 +
  116 + @Schema(description = "入职时间", requiredMode = Schema.RequiredMode.REQUIRED)
  117 + @NotNull(message = "入职时间不能为空")
  118 + private LocalDateTime entryTime;
  119 +
  120 + @Schema(description = "试用期,单位月;0 表示无试用期", example = "3")
  121 + @Min(value = 0, message = "试用期不能小于 0 个月")
  122 + @Max(value = 6, message = "试用期不能超过 6 个月")
  123 + private Integer probation;
  124 +
  125 + @Schema(description = "转正时间")
  126 + private LocalDateTime regularTime;
  127 +
  128 + @Schema(description = "离职时间")
  129 + private LocalDateTime leaveTime;
  130 +
  131 + @Schema(description = "职位名称", example = "Java 工程师")
  132 + @Size(max = 255, message = "职位名称不能超过 255 个字符")
  133 + private String postName;
  134 +
  135 + @Schema(description = "岗位职级", example = "P6")
  136 + @Size(max = 255, message = "岗位职级不能超过 255 个字符")
  137 + private String postLevel;
  138 +
  139 + @Schema(description = "工作城市", example = "上海")
  140 + @Size(max = 64, message = "工作城市不能超过 64 个字符")
  141 + private String workCity;
  142 +
  143 + @Schema(description = "工作地址", example = "浦东新区")
  144 + @Size(max = 255, message = "工作地址不能超过 255 个字符")
  145 + private String workAddress;
  146 +
  147 + @Schema(description = "工作详细地址", example = "张江高科 1 号楼")
  148 + @Size(max = 255, message = "工作详细地址不能超过 255 个字符")
  149 + private String workDetailAddress;
  150 +
  151 + @Schema(description = "招聘渠道编号", example = "10")
  152 + private Long channelId;
  153 +
  154 + @Schema(description = "司龄开始时间")
  155 + private LocalDateTime companyAgeStartTime;
  156 +
  157 + @Schema(description = "司龄,单位年", example = "2")
  158 + private Integer companyAge;
  159 +
  160 + @Schema(description = "关联招聘候选人编号", example = "200")
  161 + private Long candidateId;
  162 +
  163 + @Schema(description = "备注", example = "首批迁移员工")
  164 + @Size(max = 500, message = "备注不能超过 500 个字符")
  165 + private String remark;
  166 +
  167 + @AssertTrue(message = "身份证号码格式不正确")
  168 + @JsonIgnore
  169 + public boolean isIdNumberValid() {
  170 + return ObjUtil.notEqual(HrmEmployeeIdTypeEnum.ID_CARD.getType(), idType)
  171 + || StrUtil.isBlank(idNumber) || IdcardUtil.isValidCard(idNumber);
  172 + }
  173 +
  174 + @AssertTrue(message = "员工状态与聘用形式不匹配")
  175 + @JsonIgnore
  176 + public boolean isStatusValid() {
  177 + return ObjUtil.notEqual(HrmEmployeeTypeEnum.INFORMAL.getType(), type)
  178 + || status == null || HrmEmployeeStatusEnum.INFORMAL_STATUSES.contains(status);
  179 + }
  180 +
  181 + @AssertTrue(message = "非正式员工状态不能为空")
  182 + @JsonIgnore
  183 + public boolean isStatusRequired() {
  184 + return ObjUtil.notEqual(HrmEmployeeTypeEnum.INFORMAL.getType(), type) || status != null;
  185 + }
  186 +
  187 + @AssertTrue(message = "在职员工工号不能为空")
  188 + @JsonIgnore
  189 + public boolean isJobNumberValid() {
  190 + return ObjUtil.notEqual(HrmEmployeeEntryStatusEnum.ACTIVE.getStatus(), entryStatus)
  191 + || StrUtil.isNotBlank(jobNumber);
  192 + }
  193 +
  194 + @AssertTrue(message = "正式员工试用期不能为空")
  195 + @JsonIgnore
  196 + public boolean isProbationValid() {
  197 + return ObjUtil.notEqual(HrmEmployeeTypeEnum.FORMAL.getType(), type) || probation != null;
  198 + }
  199 +
  200 + @AssertTrue(message = "司龄起算时间不能晚于入职时间")
  201 + @JsonIgnore
  202 + public boolean isCompanyAgeStartTimeValid() {
  203 + return entryTime == null || companyAgeStartTime == null
  204 + || isBeforeOrEqual(companyAgeStartTime, entryTime);
  205 + }
  206 +
  207 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeStatusCountRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - HRM 员工状态数量 Response VO")
  8 +@Data
  9 +@AllArgsConstructor
  10 +public class HrmEmployeeStatusCountRespVO {
  11 +
  12 + @Schema(description = "状态页签", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
  13 + private Integer status;
  14 +
  15 + @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
  16 + private Long count;
  17 +
  18 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/file/HrmEmployeeFileRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.file;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - HRM 员工材料附件 Response VO")
  9 +@Data
  10 +public class HrmEmployeeFileRespVO {
  11 +
  12 + @Schema(description = "附件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  16 + private Long employeeId;
  17 +
  18 + @Schema(description = "附件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
  19 + private Integer type;
  20 +
  21 + @Schema(description = "附件地址", requiredMode = Schema.RequiredMode.REQUIRED)
  22 + private String url;
  23 +
  24 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  25 + private LocalDateTime createTime;
  26 +
  27 +}
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/file/HrmEmployeeFileSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.file;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeFileTypeEnum;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import jakarta.validation.constraints.NotBlank;
  7 +import jakarta.validation.constraints.NotNull;
  8 +import jakarta.validation.constraints.Size;
  9 +import lombok.Data;
  10 +
  11 +import java.util.List;
  12 +
  13 +@Schema(description = "管理后台 - HRM 员工材料附件保存 Request VO")
  14 +@Data
  15 +public class HrmEmployeeFileSaveReqVO {
  16 +
  17 + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  18 + @NotNull(message = "员工编号不能为空")
  19 + private Long employeeId;
  20 +
  21 + @Schema(description = "附件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
  22 + @NotNull(message = "附件类型不能为空")
  23 + @InEnum(value = HrmEmployeeFileTypeEnum.class, message = "附件类型必须是 {value}")
  24 + private Integer type;
  25 +
  26 + @Schema(description = "附件地址数组", requiredMode = Schema.RequiredMode.REQUIRED,
  27 + example = "[https://example.com/archive.pdf]")
  28 + @NotNull(message = "附件地址数组不能为空")
  29 + @Size(max = 20, message = "同类附件不能超过 20 个")
  30 + private List<@NotBlank(message = "附件地址不能为空")
  31 + @Size(max = 512, message = "附件地址长度不能超过 512 个字符") String> fileUrls;
  32 +
  33 +}
... ...