Explorar el Código

开发了考勤明细相关的接口

jinshihui hace 3 horas
padre
commit
c7e7bce74f

+ 28 - 8
nightFragrance-massage/src/main/java/com/ylx/massage/controller/MerchantDailyAttendanceController.java

@@ -1,11 +1,9 @@
 package com.ylx.massage.controller;
 
-
-
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ylx.common.core.domain.R;
 import com.ylx.massage.domain.MerchantDailyAttendance;
+import com.ylx.massage.domain.dto.MerchantDailyAttendanceQueryDTO;
 import com.ylx.massage.service.MerchantDailyAttendanceService;
 import org.springframework.web.bind.annotation.*;
 
@@ -32,12 +30,34 @@ public class MerchantDailyAttendanceController  {
      * 分页查询所有数据
      *
      * @param page 分页对象
-     * @param merchantDailyAttendance 查询实体
-     * @return 所有数据
+     * @param queryDTO 查询参数
+     * @return R 所有数据
+     */
+    @GetMapping("/queryDailyAttendance")
+    public R selectAll(Page<MerchantDailyAttendance> page, MerchantDailyAttendanceQueryDTO queryDTO) {
+        try {
+            return R.ok(this.merchantDailyAttendanceService.queryDailyAttendance(page, queryDTO));
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 查询商户指定日期的工作时间明细
+     *
+     * @param merchantId 商户ID
+     * @param attendanceDate 考勤日期,格式:yyyy-MM-dd
+     * @return R 工作时间明细
      */
-    @GetMapping
-    public R selectAll(Page<MerchantDailyAttendance> page, MerchantDailyAttendance merchantDailyAttendance) {
-        return R.ok(this.merchantDailyAttendanceService.page(page, new QueryWrapper<>(merchantDailyAttendance)));
+    @GetMapping("/queryDailyWorkTime")
+    public R queryDailyWorkTime(@RequestParam("merchantId") Integer merchantId, @RequestParam("attendanceDate") String attendanceDate) {
+        try {
+            return R.ok(this.merchantDailyAttendanceService.queryDailyWorkTime(merchantId, attendanceDate));
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new RuntimeException(e);
+        }
     }
 
     /**

+ 63 - 18
nightFragrance-massage/src/main/java/com/ylx/massage/domain/MerchantDailyAttendance.java

@@ -4,7 +4,6 @@ import java.time.LocalDateTime;
 import java.util.Date;
 
 import com.baomidou.mybatisplus.annotation.*;
-import com.baomidou.mybatisplus.extension.activerecord.Model;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -23,60 +22,106 @@ import java.io.Serializable;
 @Accessors(chain = true)
 @TableName(value = "merchant_daily_attendance", autoResultMap = true)
 public class MerchantDailyAttendance implements Serializable {
-//主键ID
+
     /**
-     * 主键
+     * 主键ID。
      */
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
-    //关联商户ID
+
+    /**
+     * 关联商户ID。
+     */
     @TableField("merchant_id")
     private Integer merchantId;
-    //关联商户姓名
+
+    /**
+     * 关联商户姓名。
+     */
     @TableField("merchant_name")
     private String merchantName;
-    //考勤日期 (对应UI中的"日期")
+
+    /**
+     * 考勤日期,对应页面中的日期。
+     */
     @TableField("attendance_date")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
     private Date attendanceDate;
-    //当日首次打卡/上线时间
+
+    /**
+     * 当日首次打卡或上线时间。
+     */
     @TableField("attendance_start_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date attendanceStartTime;
-    //当日末次打卡/下线时间
+
+    /**
+     * 当日末次打卡或下线时间。
+     */
     @TableField("attendance_end_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date attendanceEndTime;
-    //日累计工作时长(分钟),方便精确计算
+
+    /**
+     * 日累计工作时长,单位:分钟。
+     */
     @TableField("total_work_minutes")
     private Integer totalWorkMinutes;
-    //考勤状态: 1-正常, 2-异常
+
+    /**
+     * 考勤状态,1表示正常,2表示异常。
+     */
     @TableField("attendance_status")
     private Integer attendanceStatus;
-    //扣款金额
+
+    /**
+     * 扣款金额。
+     */
     @TableField("deduction_amount")
     private Double deductionAmount;
-    //备注
+
+    /**
+     * 备注。
+     */
     @TableField("remark")
     private String remark;
-    //创建人
+
+    /**
+     * 创建人。
+     */
     @TableField("create_by")
     private String createBy;
+
     /**
-     * 创建时间
+     * 创建时间
      */
     @TableField(value = "create_time", fill = FieldFill.INSERT)
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
     /**
-     * 更新时间
+     * 更新时间
      */
     @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
-    //修改人
+
+    /**
+     * 修改人。
+     */
     @TableField("update_by")
     private String updateBy;
-    //是否删除(0否1是)
+
+    /**
+     * 逻辑删除标识,0表示否,1表示是。
+     */
     @TableField("is_delete")
+    @TableLogic
     private Integer isDelete;
-}
 
+    /**
+     * 页面展示日累计工作时长文本,不对应数据库字段。
+     */
+    @TableField(exist = false)
+    private String totalWorkDurationText;
+}

+ 41 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/MerchantDailyAttendanceQueryDTO.java

@@ -0,0 +1,41 @@
+package com.ylx.massage.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 商户每日考勤明细查询参数。
+ */
+@Data
+@ApiModel("商户每日考勤明细查询参数")
+public class MerchantDailyAttendanceQueryDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 商户姓名。
+     */
+    @ApiModelProperty("商户姓名")
+    private String merchantName;
+
+    /**
+     * 查询开始日期,格式:yyyy-MM-dd。
+     */
+    @ApiModelProperty("查询开始日期")
+    private String beginDate;
+
+    /**
+     * 查询结束日期,格式:yyyy-MM-dd。
+     */
+    @ApiModelProperty("查询结束日期")
+    private String endDate;
+
+    /**
+     * 考勤状态,1表示正常,2表示异常。
+     */
+    @ApiModelProperty("考勤状态:1-正常 2-异常")
+    private Integer attendanceStatus;
+}

+ 26 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/MerchantDailyAttendanceService.java

@@ -1,9 +1,13 @@
 package com.ylx.massage.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ylx.massage.domain.MerchantDailyAttendance;
+import com.ylx.massage.domain.dto.MerchantDailyAttendanceQueryDTO;
 import org.springframework.stereotype.Service;
 
+import java.util.List;
+
 
 /**
  * 商户每日考勤统计表(MerchantDailyAttendance)表服务接口
@@ -14,5 +18,27 @@ import org.springframework.stereotype.Service;
 @Service
 public interface MerchantDailyAttendanceService extends IService<MerchantDailyAttendance> {
 
+    /**
+     * 分页查询考勤明细列表。
+     *
+     * @param page 分页参数
+     * @param query 查询条件
+     * @return 考勤明细分页数据
+     */
+    Page<MerchantDailyAttendance> queryDailyAttendance(Page<MerchantDailyAttendance> page, MerchantDailyAttendanceQueryDTO query);
+
+    /**
+     * 查询商户指定日期的工作时间明细。
+     *
+     * @param merchantId 商户ID
+     * @param attendanceDate 考勤日期,格式:yyyy-MM-dd
+     * @return 工作时间记录
+     */
+    List<MerchantDailyAttendance> queryDailyWorkTime(Integer merchantId, String attendanceDate);
+
+    /**
+     * 刷新昨日考勤扣款金额。
+     */
+    void refreshYesterdayDeductionAmount();
 }
 

+ 316 - 1
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/MerchantDailyAttendanceServiceImpl.java

@@ -1,10 +1,38 @@
 package com.ylx.massage.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ylx.attendanceconfig.domain.AttendanceDeductionRule;
+import com.ylx.attendanceconfig.domain.AttendanceRule;
+import com.ylx.attendanceconfig.mapper.AttendanceDeductionRuleMapper;
+import com.ylx.attendanceconfig.mapper.AttendanceRuleMapper;
+import com.ylx.common.exception.ServiceException;
+import com.ylx.common.utils.DateUtils;
+import com.ylx.common.utils.StringUtils;
 import com.ylx.massage.domain.MerchantDailyAttendance;
+import com.ylx.massage.domain.dto.MerchantDailyAttendanceQueryDTO;
 import com.ylx.massage.mapper.MerchantDailyAttendanceMapper;
 import com.ylx.massage.service.MerchantDailyAttendanceService;
+import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
 
 /**
  * 商户每日考勤统计表(MerchantDailyAttendance)表服务实现类
@@ -15,5 +43,292 @@ import org.springframework.stereotype.Service;
 @Service("merchantDailyAttendanceService")
 public class MerchantDailyAttendanceServiceImpl extends ServiceImpl<MerchantDailyAttendanceMapper, MerchantDailyAttendance> implements MerchantDailyAttendanceService {
 
-}
+    private static final int NOT_DELETE = 0;
+
+    /**
+     * 正常考勤状态
+     */
+    private static final int ATTENDANCE_NORMAL = 1;
+
+    /**
+     * 异常考勤状态
+     */
+    private static final int ATTENDANCE_ABNORMAL = 2;
+    private static final int ENABLED = 1;
+    private static final int EARLY_LEAVE_DEDUCTION = 1;
+
+    @Resource
+    private AttendanceRuleMapper attendanceRuleMapper;
+
+    @Resource
+    private AttendanceDeductionRuleMapper attendanceDeductionRuleMapper;
+
+    @Override
+    public Page<MerchantDailyAttendance> queryDailyAttendance(Page<MerchantDailyAttendance> page, MerchantDailyAttendanceQueryDTO query) {
+        MerchantDailyAttendanceQueryDTO condition = query == null ? new MerchantDailyAttendanceQueryDTO() : query;
+        List<MerchantDailyAttendance> attendanceList = list(buildBaseQuery(condition));
+        AttendanceRule enabledRule = getEnabledAttendanceRule();
+        List<AttendanceDeductionRule> deductionRules = getDeductionRules(enabledRule);
+
+        /**
+         * 构建每日考勤统计列表
+         */
+        List<MerchantDailyAttendance> summaryList = buildDailySummaryList(attendanceList, enabledRule, deductionRules);
+        if (condition.getAttendanceStatus() != null) {
+            summaryList = summaryList.stream()
+                    .filter(item -> condition.getAttendanceStatus().equals(item.getAttendanceStatus()))
+                    .collect(Collectors.toList());
+        }
+
+        long current = page.getCurrent() <= 0 ? 1 : page.getCurrent();
+        long size = page.getSize() <= 0 ? 10 : page.getSize();
+        int fromIndex = (int) Math.min((current - 1) * size, summaryList.size());
+        int toIndex = (int) Math.min(fromIndex + size, summaryList.size());
+
+        Page<MerchantDailyAttendance> resultPage = new Page<>(current, size);
+        resultPage.setTotal(summaryList.size());
+        resultPage.setRecords(summaryList.subList(fromIndex, toIndex));
+        return resultPage;
+    }
+
+    @Override
+    public List<MerchantDailyAttendance> queryDailyWorkTime(Integer merchantId, String attendanceDate) {
+        if (merchantId == null) {
+            throw new ServiceException("商户ID不能为空");
+        }
+        Date dayStart = parseDateStart(attendanceDate, "考勤日期不能为空");
+        Date nextDayStart = addDays(dayStart, 1);
+
+        List<MerchantDailyAttendance> records = list(new LambdaQueryWrapper<MerchantDailyAttendance>()
+                .eq(MerchantDailyAttendance::getMerchantId, merchantId)
+                .ge(MerchantDailyAttendance::getAttendanceDate, dayStart)
+                .lt(MerchantDailyAttendance::getAttendanceDate, nextDayStart)
+                .orderByAsc(MerchantDailyAttendance::getAttendanceStartTime));
+
+        records.forEach(item -> item.setTotalWorkDurationText(formatDuration(defaultMinutes(item.getTotalWorkMinutes()))));
+        return records;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void refreshYesterdayDeductionAmount() {
+        Date yesterday = addDays(DateUtils.getNowDate(), -1);
+        refreshDeductionAmountByDate(yesterday);
+    }
+
+    /**
+     * 构建基础查询条件。
+     * @param query 查询实体
+     * @return LambdaQueryWrapper<MerchantDailyAttendance> 基础查询条件
+     */
+    private LambdaQueryWrapper<MerchantDailyAttendance> buildBaseQuery(MerchantDailyAttendanceQueryDTO query) {
+        LambdaQueryWrapper<MerchantDailyAttendance> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StringUtils.isNotBlank(query.getMerchantName()), MerchantDailyAttendance::getMerchantName, query.getMerchantName())
+                .orderByDesc(MerchantDailyAttendance::getAttendanceDate);
+        //考勤状态
+        /*if (query.getAttendanceStatus() != null) {
+            wrapper.eq(MerchantDailyAttendance::getAttendanceStatus, query.getAttendanceStatus());
+        }*/
+        //开始日期
+        if (StringUtils.isNotBlank(query.getBeginDate())) {
+            wrapper.ge(MerchantDailyAttendance::getAttendanceDate, parseDateStart(query.getBeginDate(), "开始日期格式不正确"));
+        }
+        //结束日期
+        if (StringUtils.isNotBlank(query.getEndDate())) {
+            wrapper.lt(MerchantDailyAttendance::getAttendanceDate, addDays(parseDateStart(query.getEndDate(), "结束日期格式不正确"), 1));
+        }
+        return wrapper;
+    }
+
+    /**
+     * 构建每日考勤统计列表
+     * @param attendanceList 考勤记录列表
+     * @param enabledRule 有效考勤规则
+     * @param deductionRules 早退扣款规则列表
+     * @return List<MerchantDailyAttendance>
+     */
+    private List<MerchantDailyAttendance> buildDailySummaryList(List<MerchantDailyAttendance> attendanceList, AttendanceRule enabledRule, List<AttendanceDeductionRule> deductionRules) {
+        Map<String, List<MerchantDailyAttendance>> groupMap = attendanceList.stream()
+                .filter(item -> item.getMerchantId() != null && item.getAttendanceDate() != null)
+                .collect(Collectors.groupingBy(this::buildDailyGroupKey, LinkedHashMap::new, Collectors.toList()));
 
+        List<MerchantDailyAttendance> summaryList = new ArrayList<>();
+        for (List<MerchantDailyAttendance> groupRecords : groupMap.values()) {
+            MerchantDailyAttendance first = groupRecords.get(0);
+            int totalMinutes = groupRecords.stream()
+                    .map(MerchantDailyAttendance::getTotalWorkMinutes)
+                    .filter(Objects::nonNull)
+                    .mapToInt(Integer::intValue)
+                    .sum();
+
+            MerchantDailyAttendance summary = new MerchantDailyAttendance();
+            BeanUtils.copyProperties(first, summary);
+            summary.setTotalWorkMinutes(totalMinutes);
+            summary.setTotalWorkDurationText(formatDuration(totalMinutes));
+            summary.setAttendanceStatus(calculateAttendanceStatus(totalMinutes, enabledRule, first.getAttendanceStatus()));
+            summary.setDeductionAmount(calculateDeductionAmount(totalMinutes, enabledRule, deductionRules).doubleValue());
+            summaryList.add(summary);
+        }
+
+        summaryList.sort(Comparator
+                .comparing(MerchantDailyAttendance::getAttendanceDate, Comparator.nullsLast(Date::compareTo)).reversed()
+                .thenComparing(MerchantDailyAttendance::getMerchantId, Comparator.nullsLast(Integer::compareTo)));
+        return summaryList;
+    }
+
+    private void refreshDeductionAmountByDate(Date attendanceDate) {
+        Date dayStart = startOfDay(attendanceDate);
+        Date nextDayStart = addDays(dayStart, 1);
+        List<MerchantDailyAttendance> records = list(new LambdaQueryWrapper<MerchantDailyAttendance>()
+                .ge(MerchantDailyAttendance::getAttendanceDate, dayStart)
+                .lt(MerchantDailyAttendance::getAttendanceDate, nextDayStart)
+                .eq(MerchantDailyAttendance::getIsDelete, NOT_DELETE));
+        if (records.isEmpty()) {
+            return;
+        }
+
+        AttendanceRule enabledRule = getEnabledAttendanceRule();
+        List<AttendanceDeductionRule> deductionRules = getDeductionRules(enabledRule);
+        List<MerchantDailyAttendance> summaryList = buildDailySummaryList(records, enabledRule, deductionRules);
+        for (MerchantDailyAttendance summary : summaryList) {
+            update(null, new LambdaUpdateWrapper<MerchantDailyAttendance>()
+                    .eq(MerchantDailyAttendance::getMerchantId, summary.getMerchantId())
+                    .ge(MerchantDailyAttendance::getAttendanceDate, startOfDay(summary.getAttendanceDate()))
+                    .lt(MerchantDailyAttendance::getAttendanceDate, addDays(startOfDay(summary.getAttendanceDate()), 1))
+                    .eq(MerchantDailyAttendance::getIsDelete, NOT_DELETE)
+                    .set(MerchantDailyAttendance::getAttendanceStatus, summary.getAttendanceStatus())
+                    .set(MerchantDailyAttendance::getDeductionAmount, summary.getDeductionAmount())
+                    .set(MerchantDailyAttendance::getUpdateTime, java.time.LocalDateTime.now()));
+        }
+    }
+
+    /**
+     * 获取当前生效的考勤规则
+     * @return AttendanceRule
+     */
+    private AttendanceRule getEnabledAttendanceRule() {
+        return attendanceRuleMapper.selectOne(new LambdaQueryWrapper<AttendanceRule>()
+                .eq(AttendanceRule::getStatus, ENABLED)
+                .eq(AttendanceRule::getWorkDurationRuleEnabled, ENABLED)
+                .orderByDesc(AttendanceRule::getCreateTime)
+                .last("LIMIT 1"));
+    }
+
+    /**
+     * 获取早退扣款规则
+     * @param rule
+     * @return List<AttendanceDeductionRule>
+     */
+    private List<AttendanceDeductionRule> getDeductionRules(AttendanceRule rule) {
+        if (rule == null || rule.getId() == null) {
+            return new ArrayList<>();
+        }
+        return attendanceDeductionRuleMapper.selectList(new LambdaQueryWrapper<AttendanceDeductionRule>()
+                .eq(AttendanceDeductionRule::getRuleId, rule.getId())
+                .eq(AttendanceDeductionRule::getRuleType, EARLY_LEAVE_DEDUCTION)
+                .orderByAsc(AttendanceDeductionRule::getCreateTime));
+    }
+
+    /**
+     * 计算考勤状态
+     * @param totalMinutes 工作时长,单位:分钟
+     * @param rule 考勤规则
+     * @param fallbackStatus 回退状态
+     * @return 考勤状态
+     */
+    private Integer calculateAttendanceStatus(int totalMinutes, AttendanceRule rule, Integer fallbackStatus) {
+        if (rule == null || rule.getBasicWorkHours() == null) {
+            return fallbackStatus == null ? ATTENDANCE_NORMAL : fallbackStatus;
+        }
+        return totalMinutes >= getBasicWorkMinutes(rule) ? ATTENDANCE_NORMAL : ATTENDANCE_ABNORMAL;
+    }
+
+    /**
+     * 计算扣款金额
+     * @param totalMinutes
+     * @param rule
+     * @param deductionRules
+     * @return BigDecimal 扣款金额,单位:元
+     */
+    private BigDecimal calculateDeductionAmount(int totalMinutes, AttendanceRule rule, List<AttendanceDeductionRule> deductionRules) {
+        if (rule == null || rule.getBasicWorkHours() == null || deductionRules == null || deductionRules.isEmpty()) {
+            return BigDecimal.ZERO;
+        }
+        int lackMinutes = getBasicWorkMinutes(rule) - totalMinutes;
+        if (lackMinutes <= 0) {
+            return BigDecimal.ZERO;
+        }
+        return deductionRules.stream()
+                .filter(ruleItem -> ruleItem.getStartMinutes() != null && ruleItem.getEndMinutes() != null)
+                .filter(ruleItem -> lackMinutes >= ruleItem.getStartMinutes() && lackMinutes <= ruleItem.getEndMinutes())
+                .map(AttendanceDeductionRule::getDeductAmount)
+                .filter(Objects::nonNull)
+                .findFirst()
+                .orElse(BigDecimal.ZERO);
+    }
+
+    /**
+     * 获取基本工作时长
+     *
+     * @param rule 考勤规则
+     * @return 基本工作时长,单位:分钟
+     */
+    private int getBasicWorkMinutes(AttendanceRule rule) {
+        return rule.getBasicWorkHours()
+                .multiply(BigDecimal.valueOf(60))
+                .setScale(0, RoundingMode.HALF_UP)
+                .intValue();
+    }
+
+    /**
+     * 构建每日考勤统计分组键
+     * @param attendance 考勤记录
+     * @return String 分组键
+     */
+    private String buildDailyGroupKey(MerchantDailyAttendance attendance) {
+        return attendance.getMerchantId() + "_" + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, attendance.getAttendanceDate());
+    }
+
+    /**
+     * 格式化工作时长
+     * @param minutes 工作时长,单位:分钟
+     * @return 格式化后的字符串时长
+     */
+    private String formatDuration(Integer minutes) {
+        int totalMinutes = defaultMinutes(minutes);
+        return totalMinutes / 60 + "小时" + String.format("%02d", totalMinutes % 60) + "分钟";
+    }
+
+    private int defaultMinutes(Integer minutes) {
+        return minutes == null ? 0 : minutes;
+    }
+
+    /**
+     * 解析日期字符串为日期对象
+     * @param date
+     * @param errorMessage
+     * @return
+     */
+    private Date parseDateStart(String date, String errorMessage) {
+        if (StringUtils.isBlank(date)) {
+            throw new ServiceException(errorMessage);
+        }
+        Date parsedDate = DateUtils.parseDate(date.trim());
+        if (parsedDate == null) {
+            throw new ServiceException(errorMessage);
+        }
+        return startOfDay(parsedDate);
+    }
+
+    private Date startOfDay(Date date) {
+        LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
+    }
+
+    private Date addDays(Date date, int amount) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.add(Calendar.DAY_OF_MONTH, amount);
+        return calendar.getTime();
+    }
+}

+ 29 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/task/MerchantDailyAttendanceTask.java

@@ -0,0 +1,29 @@
+package com.ylx.massage.task;
+
+import com.ylx.massage.service.MerchantDailyAttendanceService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 商户考勤定时任务。
+ */
+@Slf4j
+@Component("merchantDailyAttendanceTask")
+public class MerchantDailyAttendanceTask {
+
+    @Resource
+    private MerchantDailyAttendanceService merchantDailyAttendanceService;
+
+    /**
+     * 刷新昨日考勤扣款金额。
+     * <p>
+     * Quartz配置建议:0 0 0 * * ?
+     */
+    public void refreshYesterdayDeductionAmount() {
+        log.info("开始刷新昨日商户考勤扣款金额");
+        merchantDailyAttendanceService.refreshYesterdayDeductionAmount();
+        log.info("刷新昨日商户考勤扣款金额完成");
+    }
+}