From 9772f1a024d39ea568c58de57c4abc61949c90f5 Mon Sep 17 00:00:00 2001 From: zt Date: Mon, 22 Sep 2025 21:09:36 +0800 Subject: [PATCH] =?UTF-8?q?BUG=E5=92=8C=E5=B7=A5=E8=B5=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SubUserSalaryDetailController.java | 13 + .../domain/SubUserSalaryDetail.java | 9 + .../SubConstructionUserSalaryDto.java | 6 + .../SubConstructionUserSalaryVo.java | 11 +- .../contractor/excel/DynamicSalaryData.java | 23 ++ .../excel/DynamicSalaryListener.java | 176 +++++++++++ .../contractor/excel/SalaryExcelReader.java | 111 +++++++ .../service/ISubUserSalaryDetailService.java | 10 + .../impl/SubConstructionUserServiceImpl.java | 9 +- .../impl/SubUserSalaryDetailServiceImpl.java | 250 +++++++++++----- .../dromara/job/attendance/AttendanceJob.java | 61 +++- .../dromara/project/domain/BusAttendance.java | 6 + .../AttendanceUserDataDetailVo.java | 5 + .../project/service/IBusWorkWageService.java | 6 + .../impl/BusAttendanceServiceImpl.java | 278 +++++++++++++----- .../service/impl/BusWorkWageServiceImpl.java | 7 + 16 files changed, 823 insertions(+), 158 deletions(-) create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/excel/DynamicSalaryData.java create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/excel/DynamicSalaryListener.java create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/excel/SalaryExcelReader.java diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/controller/SubUserSalaryDetailController.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/controller/SubUserSalaryDetailController.java index 5cba9f45..8ee8e3c7 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/controller/SubUserSalaryDetailController.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/controller/SubUserSalaryDetailController.java @@ -21,6 +21,7 @@ import org.dromara.contractor.domain.vo.usersalaryperiod.SubConstructionUserSala import org.dromara.contractor.service.ISubUserSalaryDetailService; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.List; @@ -85,4 +86,16 @@ public class SubUserSalaryDetailController extends BaseController { subUserSalaryDetailService.export(response, dto); } + @PutMapping("/import") + public R importData(@RequestParam("file") MultipartFile file, + @RequestParam("month") String month) { + subUserSalaryDetailService.importData(file, month); + return R.ok(); + } + + @GetMapping("/detailList") + public R> detailList( SubConstructionUserSalaryDto dto) { + return R.ok(subUserSalaryDetailService.detailList(dto)); + } + } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/domain/SubUserSalaryDetail.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/domain/SubUserSalaryDetail.java index 605919cf..41919387 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/domain/SubUserSalaryDetail.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/domain/SubUserSalaryDetail.java @@ -65,4 +65,13 @@ public class SubUserSalaryDetail extends BaseEntity { */ private String remark; + /** + * 工时 + */ + private Double workHour; + + /** + * 当日总工资 + */ + private BigDecimal totalSalary; } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/domain/dto/usersalaryperiod/SubConstructionUserSalaryDto.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/domain/dto/usersalaryperiod/SubConstructionUserSalaryDto.java index af4afefd..c73aaff0 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/domain/dto/usersalaryperiod/SubConstructionUserSalaryDto.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/domain/dto/usersalaryperiod/SubConstructionUserSalaryDto.java @@ -28,4 +28,10 @@ public class SubConstructionUserSalaryDto { */ @NotBlank(message = "时间不能为空") private String time; + + /** + * 用户 + */ + private Long userId; + } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/domain/vo/usersalaryperiod/SubConstructionUserSalaryVo.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/domain/vo/usersalaryperiod/SubConstructionUserSalaryVo.java index 72eb2bb6..c83cc072 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/domain/vo/usersalaryperiod/SubConstructionUserSalaryVo.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/domain/vo/usersalaryperiod/SubConstructionUserSalaryVo.java @@ -36,6 +36,10 @@ public class SubConstructionUserSalaryVo implements Serializable { */ private Long id; + /** + * 用户id + */ + private Long userId; /** * 人员姓名 @@ -83,7 +87,7 @@ public class SubConstructionUserSalaryVo implements Serializable { private BigDecimal salary; /** - * 时间 + * 发放时间 */ private String time; @@ -91,4 +95,9 @@ public class SubConstructionUserSalaryVo implements Serializable { private String blank; + /** + * 上传时间 + */ + private String createTime; + } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/excel/DynamicSalaryData.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/excel/DynamicSalaryData.java new file mode 100644 index 00000000..744d6d49 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/excel/DynamicSalaryData.java @@ -0,0 +1,23 @@ +package org.dromara.contractor.excel; + +import lombok.Data; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 动态日期考勤数据实体类(无班组字段) + */ +@Data +public class DynamicSalaryData { + // 基础固定字段 + private Integer serialNumber; // 序号 + private String name; // 姓名 + private String idCard; // 身份证号 + private Double total; // 合计出勤天数 + private String isLeave; // 是否离场(未离场/已离场) + private String signature; // 签字(可选) + + // 动态日期考勤:key=完整日期(如“2025-09-01”),value=考勤状态(0=未出勤,1=出勤) + private Map dailyAttendance = new LinkedHashMap<>(); +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/excel/DynamicSalaryListener.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/excel/DynamicSalaryListener.java new file mode 100644 index 00000000..a8f2e54c --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/excel/DynamicSalaryListener.java @@ -0,0 +1,176 @@ +package org.dromara.contractor.excel; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.utils.IdCardEncryptorUtil; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; + +@Slf4j +public class DynamicSalaryListener extends AnalysisEventListener> { + private final List allAttendanceList; + private final String month; + private static final int DATE_COL_START_INDEX = 3; // 日期列起始索引(第4列) + private int dateColEndIndex; + private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private boolean isHeadInitialized = false; + // 写死表头行号(0-based,对应Excel第3行) + private static final int FIXED_HEAD_ROW_NUMBER = 2; + + public DynamicSalaryListener(List allAttendanceList, String month) { + this.allAttendanceList = allAttendanceList; + this.month = month; + // 月份格式校验 + try { + LocalDate.parse(month + "-01", dateFormatter); + } catch (Exception e) { + throw new IllegalArgumentException("月份格式错误!需传入yyyy-MM格式(如2025-09)"); + } + } + + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + // 仅处理固定行号的表头,且仅初始化1次 + int currentRowIndex = context.readRowHolder().getRowIndex(); + if (currentRowIndex != FIXED_HEAD_ROW_NUMBER || isHeadInitialized) { + return; + } + + // 解析“合计”列索引 + for (Map.Entry entry : headMap.entrySet()) { + String headValue = entry.getValue().trim(); + if ("合计".equals(headValue)) { + this.dateColEndIndex = entry.getKey() - 1; + break; + } + } + + // 校验表头有效性 + if (dateColEndIndex < DATE_COL_START_INDEX) { + throw new RuntimeException( + String.format("行号%d(Excel第%d行)未找到“合计”列,Excel格式不符合要求", + FIXED_HEAD_ROW_NUMBER, FIXED_HEAD_ROW_NUMBER + 1) + ); + } + + isHeadInitialized = true; + log.info("Sheet【{}】- 月份:{},日期列范围=索引{}~{}(共{}天)", + context.readSheetHolder().getSheetName(), // 打印当前Sheet名 + month, DATE_COL_START_INDEX, dateColEndIndex, + dateColEndIndex - DATE_COL_START_INDEX + 1); + } + + + @Override + public void invoke(Map rowData, AnalysisContext context) { + // 【核心优化:强化无效行过滤】 + // 1. 获取第1列(序号)和第2列(姓名)的值,用于判断是否为有效数据行 + String serialNumberStr = getCellValue(rowData, 0); + String name = getCellValue(rowData, 1); + + // 2. 过滤规则: + // - 序号为空或非数字(有效数据行序号是1、2、3...) + // - 姓名为空或包含“姓名/日期”“制表人”“注:”“签字”“项目负责人”等关键词 + // - 整行无有效数据(所有列均为空) + if (serialNumberStr == null || serialNumberStr.isEmpty() + || !serialNumberStr.matches("\\d+") // 序号必须是数字 + || name == null || name.isEmpty() + || name.contains("姓名/日期") + || name.contains("制表人:") + || name.contains("注:") + || name.contains("签字") + || name.contains("项目负责人") + || isAllColumnsEmpty(rowData)) { // 过滤空行 + log.debug("Sheet【{}】- 跳过无效行:序号={},姓名={}", + context.readSheetHolder().getSheetName(), serialNumberStr, name); + return; + } + + // 3. 封装有效考勤数据(原有逻辑不变) + DynamicSalaryData attendance = new DynamicSalaryData(); + attendance.setSerialNumber(parseInt(serialNumberStr)); + attendance.setName(name); + attendance.setIdCard(getCellValue(rowData, 2)); + attendance.setTotal(parseDouble(getCellValue(rowData, dateColEndIndex + 1))); + attendance.setIsLeave(getCellValue(rowData, dateColEndIndex + 2)); + attendance.setSignature(getCellValue(rowData, dateColEndIndex + 3)); + + // 4. 解析动态日期列 + for (int colIndex = DATE_COL_START_INDEX; colIndex <= dateColEndIndex; colIndex++) { + int day = colIndex - DATE_COL_START_INDEX + 1; + String fullDate = month + "-" + String.format("%02d", day); + Double attendStatus = parseDouble(getCellValue(rowData, colIndex)); + attendance.getDailyAttendance().put(fullDate, attendStatus == null ? 0 : attendStatus); + } + + allAttendanceList.add(attendance); + } + + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + log.info("Sheet【{}】读取完成!累计汇总【{}】条有效考勤记录", + context.readSheetHolder().getSheetName(), allAttendanceList.size()); + } + + + // ------------------- 新增工具方法:判断整行是否为空 ------------------- + /** + * 判断行数据是否所有列均为空(过滤空行) + */ + private boolean isAllColumnsEmpty(Map rowData) { + if (rowData == null || rowData.isEmpty()) { + return true; + } + for (String value : rowData.values()) { + if (value != null && !value.trim().isEmpty()) { + return false; // 存在非空列,不是空行 + } + } + return true; // 所有列均为空,是空行 + } + + + // 原有工具方法(不变) + private String getCellValue(Map rowData, int colIndex) { + String value = rowData.get(colIndex); + return value == null ? null : value.trim(); + } + + private Integer parseInt(String value) { + if (value == null || value.isEmpty()) { + return null; + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + log.warn("非数字值:{},已忽略", value); + return null; + } + } + + private Double parseDouble(String value) { + if (value == null || value.isEmpty()) { + return null; + } + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + log.warn("非数字值:{},已忽略", value); + return null; + } + } + + // ------------------- 重置表头初始化状态(用于多Sheet遍历) ------------------- + /** + * 切换Sheet时调用,重置表头初始化状态(确保新Sheet重新解析表头) + */ + public void resetHeadInitialized() { + this.isHeadInitialized = false; + } +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/excel/SalaryExcelReader.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/excel/SalaryExcelReader.java new file mode 100644 index 00000000..e735c237 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/excel/SalaryExcelReader.java @@ -0,0 +1,111 @@ +package org.dromara.contractor.excel; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelReader; +import com.alibaba.excel.read.metadata.ReadSheet; +import com.alibaba.excel.support.ExcelTypeEnum; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.utils.IdCardEncryptorUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + + +@Slf4j +public class SalaryExcelReader { + + private static final int GLOBAL_HEAD_ROW_NUMBER = 3; + + /** + * 【新增】通过输入流解析Excel(支持前端上传的流) + * @param inputStream Excel文件输入流(如MultipartFile.getInputStream()) + * @param month 月份(格式:yyyy-MM) + * @return 汇总的考勤数据列表 + */ + public static List readAllAttendanceByStream(InputStream inputStream, String month) { + List allAttendanceList = new ArrayList<>(); + DynamicSalaryListener listener = new DynamicSalaryListener(allAttendanceList, month); + + try ( + // 1. 构建Excel读取器:读取源为 InputStream,全局设置表头行号 + ExcelReader excelReader = EasyExcel.read(inputStream) + .excelType(ExcelTypeEnum.XLSX) // 自动识别xlsx/xls格式(兼容前端上传的两种格式) + .headRowNumber(GLOBAL_HEAD_ROW_NUMBER) // 强制表头行号=2 + .registerReadListener(listener) // 注册监听器 + .build() + ) { + // 2. 获取所有Sheet并逐个解析 + List readSheetList = excelReader.excelExecutor().sheetList(); + log.info("从流中发现Excel共【{}】个Sheet,开始逐个解析", readSheetList.size()); + + for (ReadSheet readSheet : readSheetList) { + log.info("开始解析Sheet【{}】(索引:{})", + readSheet.getSheetName(), readSheet.getSheetNo()); + // 切换Sheet前重置表头初始化状态 + listener.resetHeadInitialized(); + // 读取当前Sheet(流解析,无本地文件) + excelReader.read(readSheet); + } + } catch (Exception e) { + log.error("Excel流解析失败!月份:{}", month, e); + throw new RuntimeException("Excel流解析异常:" + e.getMessage(), e); + } + + log.info("所有Sheet流解析完成!共汇总【{}】条有效考勤记录", allAttendanceList.size()); + return allAttendanceList; + } + + public static List readAllAttendance(String excelFilePath, String month) { + List allAttendanceList = new ArrayList<>(); + DynamicSalaryListener listener = new DynamicSalaryListener(allAttendanceList, month); + + try ( + // 【核心修改1:在读取器初始化时就全局设置表头行号】 + ExcelReader excelReader = EasyExcel.read(excelFilePath) + .excelType(ExcelTypeEnum.XLSX) + .headRowNumber(GLOBAL_HEAD_ROW_NUMBER) // 全局强制设置表头行号=2 + .registerReadListener(listener) // 注册监听器 + .build() + ) { + // 【核心修改2:获取所有Sheet后,直接读取(无需重新构建ReadSheet)】 + List readSheetList = excelReader.excelExecutor().sheetList(); + log.info("发现Excel共【{}】个Sheet,全局表头行号配置:{}", + readSheetList.size(), GLOBAL_HEAD_ROW_NUMBER); + + for (ReadSheet readSheet : readSheetList) { + log.info("开始解析Sheet【{}】(索引:{})", + readSheet.getSheetName(), readSheet.getSheetNo()); + // 切换Sheet前重置表头初始化状态 + listener.resetHeadInitialized(); + // 直接读取Sheet(此时Sheet已继承全局的headRowNumber=2配置) + excelReader.read(readSheet); + } + } catch (Exception e) { + log.error("Excel读取失败!文件路径:{},全局表头行号:{}", + excelFilePath, GLOBAL_HEAD_ROW_NUMBER, e); + throw new RuntimeException("Excel读取异常:" + e.getMessage()); + } + + log.info("所有Sheet解析完成!共汇总【{}】条有效考勤记录", allAttendanceList.size()); + return allAttendanceList; + } + + + // ------------------- 测试示例 ------------------- + public static void main(String[] args) { + String excelPath = "C:\\Users\\YuanJie\\Desktop\\test.xlsx"; + String inputMonth = "2025-09"; + List attendanceList = readAllAttendance(excelPath, inputMonth); + + // 打印部分数据验证 + System.out.println("=== 解析结果预览 ==="); + for (int i = 0; i < Math.min(3, attendanceList.size()); i++) { + DynamicSalaryData data = attendanceList.get(i); + System.out.printf("序号:%d,姓名:%s,身份证号:%s,合计出勤:%d天%n", + data.getSerialNumber(), data.getName(), data.getIdCard(), data.getTotal()); + } + } +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/service/ISubUserSalaryDetailService.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/service/ISubUserSalaryDetailService.java index 83d2d2d9..9293312b 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/service/ISubUserSalaryDetailService.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/service/ISubUserSalaryDetailService.java @@ -12,6 +12,8 @@ import org.dromara.contractor.domain.dto.usersalaryperiod.SubConstructionUserSal import org.dromara.contractor.domain.vo.usersalarydetail.SubUserSalaryDetailVo; import org.dromara.contractor.domain.vo.usersalaryperiod.SubConstructionUserSalaryVo; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.time.LocalDate; @@ -95,6 +97,14 @@ public interface ISubUserSalaryDetailService extends IService detailList( SubConstructionUserSalaryDto dto); } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/service/impl/SubConstructionUserServiceImpl.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/service/impl/SubConstructionUserServiceImpl.java index 1d1a4adc..4f3cb032 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/service/impl/SubConstructionUserServiceImpl.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/contractor/service/impl/SubConstructionUserServiceImpl.java @@ -1127,7 +1127,14 @@ public class SubConstructionUserServiceImpl extends ServiceImpl salaryPageList(SubConstructionUserSalaryDto dto, PageQuery pageQuery) { - SubConstructionUserQueryReq req = new SubConstructionUserQueryReq(); - req.setProjectId(dto.getProjectId()); - req.setTeamId(dto.getTeamId()); - req.setUserName(dto.getUserName()); - TableDataInfo subConstructionUserVoTableDataInfo = constructionUserService.queryPageList(req, pageQuery); - List rows = subConstructionUserVoTableDataInfo.getRows(); - if(CollectionUtil.isEmpty(rows)){ - return TableDataInfo.build(); - } String time = dto.getTime(); YearMonth parse = YearMonth.parse(time, DateTimeFormatter.ofPattern("yyyy-MM")); LocalDate start = parse.atDay(1); LocalDate end = parse.atEndOfMonth(); - List userIds = rows.stream().map(SubConstructionUserVo::getSysUserId).toList(); - //考勤数据 - List attendanceList = attendanceService.lambdaQuery() - .eq(BusAttendance::getProjectId, dto.getProjectId()) - .in(BusAttendance::getUserId, userIds) - .between(BusAttendance::getClockDate, start, end) - .list(); - //工资数据 - List salaryDetailList = this.lambdaQuery() - .eq(SubUserSalaryDetail::getProjectId, dto.getProjectId()) - .in(SubUserSalaryDetail::getUserId, userIds) - .between(SubUserSalaryDetail::getReportDate, start, end) - .list(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.select("user_id", "SUM(work_hour) as workHour", "SUM(total_salary) as totalSalary","max(create_time) as createTime") + .eq("project_id", dto.getProjectId()) + .between("report_date", start, end) + .eq(dto.getTeamId()!=null,"team_id", dto.getTeamId()) + .like(StringUtils.isNotBlank(dto.getUserName()),"user_name", dto.getUserName()) + .groupBy("user_id"); + Page result = this.page(pageQuery.build(), queryWrapper); + List records = result.getRecords(); + List userIds = records.stream().map(SubUserSalaryDetail::getUserId).toList(); + Map collect = new HashMap<>(); + if(CollectionUtil.isNotEmpty(userIds)){ + List subConstructionUsers = constructionUserService.list(Wrappers.lambdaQuery(SubConstructionUser.class) + .in(SubConstructionUser::getSysUserId, userIds)); + collect = subConstructionUsers.stream().collect(Collectors.toMap(SubConstructionUser::getSysUserId, vo -> vo)); + } ArrayList vos = new ArrayList<>(); - for (SubConstructionUserVo row : rows) { + for (SubUserSalaryDetail detail : records) { SubConstructionUserSalaryVo vo = new SubConstructionUserSalaryVo(); - BeanUtil.copyProperties(row,vo); -// vo.setSfzNumber(idCardEncryptorUtil.decrypt(vo.getSfzNumber())); + vo.setId(detail.getId()); vo.setTime(dto.getTime()); - // 获取工资 - BigDecimal salary = row.getSalary(); - if (salary == null || salary.compareTo(BigDecimal.ZERO) == 0) { - String typeOfWork = row.getTypeOfWork(); - String wageMeasureUnit = row.getWageMeasureUnit(); - if (StringUtils.isNotEmpty(typeOfWork) && StringUtils.isNotEmpty(wageMeasureUnit)) { - BusWorkWage workWage = workWageService.lambdaQuery() - .eq(BusWorkWage::getProjectId, row.getProjectId()) - .eq(BusWorkWage::getWorkType, typeOfWork) - .eq(BusWorkWage::getWageMeasureUnit, wageMeasureUnit) - .one(); - if (workWage != null) { - salary = workWage.getWage(); - } else { - salary = BigDecimal.ZERO; - } - } else { - salary = BigDecimal.ZERO; + vo.setTotalSalary(detail.getTotalSalary()); + vo.setCreateTime(DateUtil.format(detail.getCreateTime(), "yyyy-MM-dd HH:mm:ss")); + vo.setWorkDay(detail.getWorkHour()); + vo.setProjectId(dto.getProjectId()); + vo.setUserId(detail.getUserId()); + SubConstructionUser constructionUser = collect.get(detail.getUserId()); + if(constructionUser != null){ + if(constructionUser.getSfzNumber() != null){ + vo.setSfzNumber(idCardEncryptorUtil.decrypt(constructionUser.getSfzNumber())); } + vo.setUserName(constructionUser.getUserName()); + vo.setYhkNumber(constructionUser.getYhkNumber()); + vo.setYhkOpeningBank(constructionUser.getYhkOpeningBank()); } - vo.setSalary(salary); - vo.setTotalSalary(salaryDetailList.stream().filter(detail -> detail.getUserId().equals(row.getSysUserId())) - .map(SubUserSalaryDetail::getDailySalary) - .reduce(BigDecimal.ZERO, BigDecimal::add) - ); - List list = attendanceList.stream().filter(attendance -> attendance.getUserId().equals(row.getSysUserId()) - && Arrays.asList("1", "2", "3", "5").contains(attendance.getClockStatus()) - ).toList(); - vo.setWorkDay(list.size()*0.5); - vos.add(vo); } - return new TableDataInfo<>(vos,subConstructionUserVoTableDataInfo.getTotal()); + return new TableDataInfo<>(vos,result.getTotal()); } @Override @@ -342,27 +328,34 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl userList = constructionUserService.lambdaQuery() - .eq(SubConstructionUser::getProjectId, dto.getProjectId()) - .eq(dto.getTeamId() != null, SubConstructionUser::getTeamId, dto.getTeamId()) - .like(StringUtils.isNotBlank(dto.getUserName()), SubConstructionUser::getUserName, dto.getUserName()) - .list(); - - if (userList.isEmpty()) { - throw new ServiceException("暂无数据"); - } - // 2. 解析年月和查询薪资明细 YearMonth yearMonth = YearMonth.parse(dto.getTime(), DateTimeFormatter.ofPattern("yyyy-MM")); LocalDate start = yearMonth.atDay(1); LocalDate end = yearMonth.atEndOfMonth(); - List userIds = userList.stream().map(SubConstructionUser::getSysUserId).toList(); - List salaryDetails = this.lambdaQuery() - .eq(SubUserSalaryDetail::getProjectId, dto.getProjectId()) - .in(SubUserSalaryDetail::getUserId, userIds) - .between(SubUserSalaryDetail::getReportDate, start, end) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.select("user_id", "SUM(work_hour) as workHour", "SUM(total_salary) as totalSalary","max(create_time) as createTime") + .eq("project_id", dto.getProjectId()) + .between("report_date", start, end) + .eq(dto.getUserId()!=null,"user_id", dto.getUserId()) + .eq(dto.getTeamId()!=null,"team_id", dto.getTeamId()) + .like(StringUtils.isNotBlank(dto.getUserName()),"user_name", dto.getUserName()) + .groupBy("user_id"); + + List salaryDetailsList= baseMapper.selectList(queryWrapper); + + if (salaryDetailsList.isEmpty()) { + throw new ServiceException("暂无数据"); + } + Map map = salaryDetailsList.stream().collect(Collectors.toMap(SubUserSalaryDetail::getUserId, vo -> vo)); + + + List userIds = salaryDetailsList.stream().map(SubUserSalaryDetail::getUserId).toList(); + + + + List userList = constructionUserService.lambdaQuery() + .in(SubConstructionUser::getSysUserId, userIds) .list(); // 3. 设置响应头(下载文件配置) @@ -465,7 +458,7 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl getTeamData(List rows, - List salaryDetailList,Long teamId){ + Map map,Long teamId){ List list1 ; if(teamId == null){ list1 = rows.stream().filter(row -> row.getTeamId() == null && Arrays.asList("1","2").contains(row.getExitStatus())).toList(); @@ -511,10 +504,8 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl detail.getUserId().equals(row.getSysUserId())) - .map(SubUserSalaryDetail::getDailySalary) - .reduce(BigDecimal.ZERO, BigDecimal::add) - ); + SubUserSalaryDetail detail1 = map.get(row.getSysUserId()); + vo.setTotalSalary(detail1.getTotalSalary()); vos.add(vo); i++; } @@ -532,4 +523,109 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl dataList; + try { + // 2. 将 MultipartFile 转为 InputStream,传给工具类解析 + dataList = SalaryExcelReader.readAllAttendanceByStream(file.getInputStream(), month); + } catch (Exception e) { + throw new RuntimeException("Excel流解析失败:" + e.getMessage(), e); + } + if(CollUtil.isEmpty(dataList)){ + throw new ServiceException("未读取到数据"); + } + + Map> dataMap = dataList.stream().collect(Collectors.toMap(vo -> idCardEncryptorUtil.encrypt(vo.getIdCard()), DynamicSalaryData::getDailyAttendance)); + Set cards = dataMap.keySet(); + + YearMonth parse = YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyy-MM")); + LocalDate start = parse.atDay(1); + LocalDate end = parse.atEndOfMonth(); + + //人员数据 + List list = constructionUserService.list(Wrappers.lambdaQuery() + .in(SubConstructionUser::getSfzNumber, cards)); + + List userIds = list.stream().map(SubConstructionUser::getSysUserId).toList(); + //考勤数据 + List attendanceList = attendanceService + .list(Wrappers.lambdaQuery() + .in(BusAttendance::getUserId, userIds) + .between(BusAttendance::getClockDate, start, end) + ); + // 将 attendanceList 转换为 Map 格式 + Map attendanceSalaryMap = new HashMap<>(); + + // 按 userId+日期 分组,只保留每组的第一条记录的salary + attendanceList.stream() + .collect(Collectors.groupingBy( + attendance -> attendance.getUserId() + "_" + attendance.getClockDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), + Collectors.toList() + )) + .forEach((key, dateList) -> attendanceSalaryMap.put(key, dateList.getFirst().getSalary())); + + + List addList = new ArrayList<>(); + for(SubConstructionUser constructionUser : list){ + Map stringIntegerMap = dataMap.get(constructionUser.getSfzNumber()); + if(stringIntegerMap != null){ + for(Map.Entry entry : stringIntegerMap.entrySet()){ + String key = entry.getKey(); + Double value = entry.getValue(); + + SubUserSalaryDetail subUserSalaryDetail = new SubUserSalaryDetail(); + subUserSalaryDetail.setProjectId(constructionUser.getProjectId()); + subUserSalaryDetail.setTeamId(constructionUser.getTeamId()); + subUserSalaryDetail.setUserId(constructionUser.getSysUserId()); + subUserSalaryDetail.setUserName(constructionUser.getUserName()); + subUserSalaryDetail.setReportDate(LocalDate.parse(key, DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + subUserSalaryDetail.setWorkHour(value); + + String attendanceKey = constructionUser.getSysUserId()+"_"+key; + BigDecimal bigDecimal = attendanceSalaryMap.get(attendanceKey); + if(bigDecimal == null){ + bigDecimal = BigDecimal.ZERO; + } + subUserSalaryDetail.setDailySalary(bigDecimal); + subUserSalaryDetail.setTotalSalary(bigDecimal.multiply(new BigDecimal(value.toString()))); + addList.add(subUserSalaryDetail); + } + } + + } + baseMapper.delete(Wrappers.lambdaQuery() + .in(SubUserSalaryDetail::getUserId, userIds) + .between(SubUserSalaryDetail::getReportDate, start, end) + ); + baseMapper.insertBatch(addList); + } + + + @Override + public List detailList(SubConstructionUserSalaryDto dto) { + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + YearMonth parse = YearMonth.parse(dto.getTime(), DateTimeFormatter.ofPattern("yyyy-MM")); + LocalDate start = parse.atDay(1); + LocalDate end = parse.atEndOfMonth(); + + wrapper.eq(SubUserSalaryDetail::getProjectId, dto.getProjectId()); + wrapper.eq( dto.getTeamId()!=null,SubUserSalaryDetail::getTeamId, dto.getTeamId()); + wrapper.eq(dto.getUserId()!=null,SubUserSalaryDetail::getUserId, dto.getUserId()); + wrapper.between(SubUserSalaryDetail::getReportDate,start, end); + + return this.list(wrapper); + } } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/job/attendance/AttendanceJob.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/job/attendance/AttendanceJob.java index 25a984d6..4b19fbc9 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/job/attendance/AttendanceJob.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/job/attendance/AttendanceJob.java @@ -1,6 +1,7 @@ package org.dromara.job.attendance; +import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.date.DateUtil; import com.aizuda.snailjob.client.job.core.annotation.JobExecutor; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; @@ -12,10 +13,7 @@ import org.dromara.common.core.utils.DateUtils; import org.dromara.contractor.domain.SubConstructionUser; import org.dromara.contractor.service.ISubConstructionUserService; import org.dromara.project.constant.BusProjectConstant; -import org.dromara.project.domain.BusAttendance; -import org.dromara.project.domain.BusAttendanceRule; -import org.dromara.project.domain.BusProject; -import org.dromara.project.domain.BusUserProjectRelevancy; +import org.dromara.project.domain.*; import org.dromara.project.domain.enums.BusAttendanceClockStatusEnum; import org.dromara.project.domain.enums.BusAttendanceCommuterEnum; import org.dromara.project.service.*; @@ -25,11 +23,13 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import java.math.BigDecimal; import java.time.DayOfWeek; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.*; +import java.util.stream.Stream; @Slf4j @Component @@ -53,6 +53,11 @@ public class AttendanceJob { @Resource private ISubConstructionUserService constructionUserService; + @Resource + private IBusWorkWageService workWageService; + + //0系统管理员 1普通人员 2项目管理员 3分包 只有1才生成缺卡记录 无需打卡人员 + private static final List noClockUserTypes = Arrays.asList("0","2"); // @Scheduled(cron = "0 0/10 * * * ?") @JobExecutor(name = "clockInMiss") @@ -126,7 +131,7 @@ public class AttendanceJob { for (BusUserProjectRelevancy relevancy : relevancyList) { - if (attendanceUserIds.contains(relevancy.getUserId()) || "0".equals(relevancy.getUserType())) { + if (attendanceUserIds.contains(relevancy.getUserId()) || noClockUserTypes.contains(relevancy.getUserType())) { continue; } @@ -156,6 +161,7 @@ public class AttendanceJob { }else { busAttendance.setClockStatus(BusAttendanceClockStatusEnum.UNCLOCK.getValue()); } + busAttendance.setSalary(computeSalary(constructionUser, null)); missList.add(busAttendance); } @@ -204,7 +210,9 @@ public class AttendanceJob { //管理员关联多个项目,需要记录是否已生成缺卡记录 HashSet manageUserIds = new HashSet<>(); + List missList = new ArrayList<>(); + for (BusAttendanceRule rule : list) { LocalTime clockOutTime = rule.getClockOutTime(); @@ -235,16 +243,22 @@ public class AttendanceJob { List relevancyList = userProjectRelevancyService.list(Wrappers.lambdaQuery(BusUserProjectRelevancy.class) .eq(BusUserProjectRelevancy::getProjectId, rule.getProjectId())); - //查询当天已打下班卡人员 - List attendanceList = attendanceService.list(Wrappers.lambdaQuery(BusAttendance.class) + //查询当天打卡人员 + List allAttendanceList = attendanceService.list(Wrappers.lambdaQuery(BusAttendance.class) .eq(BusAttendance::getClockDate, date) - .eq(BusAttendance::getClockType, BusAttendanceCommuterEnum.CLOCKOUT.getValue()) ); + + List inAttendanceList = allAttendanceList.stream() + .filter(attendance -> BusAttendanceCommuterEnum.CLOCKIN.getValue().equals(attendance.getClockType())).toList(); + + List attendanceList = allAttendanceList.stream() + .filter(attendance -> BusAttendanceCommuterEnum.CLOCKOUT.getValue().equals(attendance.getClockType())).toList(); + List attendanceUserIds = attendanceList.stream().map(BusAttendance::getUserId).toList(); for (BusUserProjectRelevancy relevancy : relevancyList) { - if (attendanceUserIds.contains(relevancy.getUserId()) || "0".equals(relevancy.getUserType())) { + if (attendanceUserIds.contains(relevancy.getUserId()) || noClockUserTypes.contains(relevancy.getUserType())) { continue; } BusAttendance busAttendance = new BusAttendance(); @@ -263,8 +277,6 @@ public class AttendanceJob { continue; } - - busAttendance.setUserId(relevancy.getUserId()); busAttendance.setUserName(constructionUser.getUserName()); @@ -276,7 +288,8 @@ public class AttendanceJob { }else { busAttendance.setClockStatus(BusAttendanceClockStatusEnum.UNCLOCK.getValue()); } - + List list1 = inAttendanceList.stream().filter(vo -> relevancy.getUserId().equals(vo.getUserId())).toList(); + busAttendance.setSalary(computeSalary(constructionUser, list1)); missList.add(busAttendance); } @@ -313,4 +326,28 @@ public class AttendanceJob { return false; } + + /** + * 计算工资 + */ + public BigDecimal computeSalary(SubConstructionUser constructionUser, List inAttendances) { + if (CollectionUtil.isNotEmpty(inAttendances)) { + BusAttendance first = inAttendances.getFirst(); + if (first.getSalary().compareTo(BigDecimal.ZERO) > 0) { + return first.getSalary(); + } + } + + if (constructionUser.getSalary() != null && constructionUser.getSalary().compareTo(BigDecimal.ZERO) > 0) { + return constructionUser.getSalary(); + } + + String typeOfWork = constructionUser.getTypeOfWork(); + BusWorkWage workWageByWorkType = workWageService.getWorkWageByWorkType(typeOfWork); + if (workWageByWorkType == null || workWageByWorkType.getWage() == null) { + return BigDecimal.ZERO; + } + return workWageByWorkType.getWage(); + + } } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/domain/BusAttendance.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/domain/BusAttendance.java index a07a65c7..f090ea73 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/domain/BusAttendance.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/domain/BusAttendance.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import lombok.EqualsAndHashCode; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -102,4 +103,9 @@ public class BusAttendance extends BaseEntity { * 处理 0-未处理,1-已处理 */ private String handle; + + /** + * 薪水 + */ + private BigDecimal salary; } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/domain/vo/attendance/AttendanceUserDataDetailVo.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/domain/vo/attendance/AttendanceUserDataDetailVo.java index 902cb637..d6cef961 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/domain/vo/attendance/AttendanceUserDataDetailVo.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/domain/vo/attendance/AttendanceUserDataDetailVo.java @@ -30,4 +30,9 @@ public class AttendanceUserDataDetailVo { * 迟到或早退的分钟 */ private Integer minuteCount; + + /** + * 出勤天数 + */ + private Double workDay; } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/service/IBusWorkWageService.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/service/IBusWorkWageService.java index e3ac966d..7356c221 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/service/IBusWorkWageService.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/service/IBusWorkWageService.java @@ -95,4 +95,10 @@ public interface IBusWorkWageService extends IService { * @return 工种薪水分页对象视图 */ Page getVoPage(Page workWagePage); + + + /** + * 根据工种获取薪水设置 + */ + BusWorkWage getWorkWageByWorkType(String workType); } diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/service/impl/BusAttendanceServiceImpl.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/service/impl/BusAttendanceServiceImpl.java index 65b25d4e..5c3f5b9e 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/service/impl/BusAttendanceServiceImpl.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/project/service/impl/BusAttendanceServiceImpl.java @@ -54,6 +54,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.io.OutputStream; +import java.math.BigDecimal; import java.time.*; import java.time.format.DateTimeFormatter; import java.util.*; @@ -104,6 +105,8 @@ public class BusAttendanceServiceImpl extends ServiceImpl { + try { + chatServerHandler.sendSystemMessageToUser(userId, "打卡成功", "1"); + } catch (Exception e) { + log.error("异步发送系统消息失败,用户ID: {}, 消息: {}", userId, "打卡成功", e); } - // 填充信息 - attendance.setUserId(userId); - attendance.setProjectId(req.getProjectId()); - attendance.setClockDate(localDate); - attendance.setClockTime(now); - attendance.setUserName(constructionUser.getUserName()); - // 记录打卡坐标 - attendance.setLat(req.getLat()); - attendance.setLng(req.getLng()); - attendance.setClockLocation(JSTUtil.getLocationName(req.getLat(), req.getLng())); - // 上传人脸照 - SysOssVo upload = ossService.upload(file); - attendance.setFacePic(upload.getOssId().toString()); - CompletableFuture.runAsync(() -> { - try { - chatServerHandler.sendSystemMessageToUser(userId, "打卡成功", "1"); - } catch (Exception e) { - log.error("异步发送系统消息失败,用户ID: {}, 消息: {}", userId, "打卡成功", e); - } - }); + }); + //计算工资 + attendance.setSalary(computeSalary(constructionUser, inAttendances)); + return this.save(attendance); - boolean save = this.save(attendance); - //插入工资 - userSalaryDetailService.insertByAttendance(userId, attendance.getClockDate()); - return save; - - } else if (clockTypeByTime == 2 || CollectionUtils.isEmpty(outAttendances)) { + } else if (clockTypeByTime == 2 || CollectionUtils.isNotEmpty(inAttendances)) { if (CollectionUtil.isNotEmpty(outAttendances)) { BusAttendance busAttendance = outAttendances.getFirst(); @@ -426,7 +427,8 @@ public class BusAttendanceServiceImpl extends ServiceImpl inAttendances) { + if (CollectionUtil.isNotEmpty(inAttendances)) { + BusAttendance first = inAttendances.getFirst(); + if (first.getSalary().compareTo(BigDecimal.ZERO) > 0) { + return first.getSalary(); + } + } + + if (constructionUser.getSalary() != null && constructionUser.getSalary().compareTo(BigDecimal.ZERO) > 0) { + return constructionUser.getSalary(); + } + + String typeOfWork = constructionUser.getTypeOfWork(); + BusWorkWage workWageByWorkType = workWageService.getWorkWageByWorkType(typeOfWork); + if (workWageByWorkType == null || workWageByWorkType.getWage() == null) { + return BigDecimal.ZERO; + } + return workWageByWorkType.getWage(); + + } + /** * 计算实际打卡时间与规定时间的分钟差 * @@ -1003,11 +1029,21 @@ public class BusAttendanceServiceImpl extends ServiceImpl workList = attendanceList.stream() + + // 过滤有效考勤记录并按日期分组 + Map> dateAttendanceMap = attendanceList.stream() .filter(a -> validStatusList.contains(a.getClockStatus())) - .map(this::convertToDetailVo) - .distinct() // 去除重复日期 - .collect(Collectors.toList()); + .collect(Collectors.groupingBy(BusAttendanceVo::getClockDate)); + List workList = new ArrayList<>(); + for (Map.Entry> entry : dateAttendanceMap.entrySet()) { + LocalDate key = entry.getKey(); + List value = entry.getValue(); + AttendanceUserDataDetailVo detailVo = new AttendanceUserDataDetailVo(); + detailVo.setClockDate(key); + detailVo.setWeek(key.getDayOfWeek().getValue()); + detailVo.setWorkDay(value.size()*0.5); + workList.add(detailVo); + } vo.setWork(workList); // 处理迟到记录 @@ -1185,10 +1221,10 @@ public class BusAttendanceServiceImpl extends ServiceImpl users = entry.getValue(); SubConstructionUser constructionUser = users.getFirst(); String teamName = constructionUser.getTeamName(); - if(teamId == 0){ + if (teamId == 0) { teamName = "无班组"; } - if(StringUtils.isBlank(teamName)){ + if (StringUtils.isBlank(teamName)) { teamName = teamId.toString(); } System.out.println("name:" + teamName); @@ -1208,6 +1244,11 @@ public class BusAttendanceServiceImpl extends ServiceImpl 0) { + columnNum--; + char c = (char) ('A' + (columnNum % 26)); + columnName.insert(0, c); + columnNum /= 26; + } + return columnName.toString(); + } + private CellStyle createBorderStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setBorderTop(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + return style; + } + private CellStyle createNumberBorderStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + DataFormat format = workbook.createDataFormat(); + style.setDataFormat(format.getFormat("0")); // 强制显示 0 + style.setBorderTop(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + return style; + } + + private CellStyle createNumberStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + DataFormat format = workbook.createDataFormat(); + style.setDataFormat(format.getFormat("#")); + return style; + } + private CellStyle createProjectCellStyle(Workbook workbook) { CellStyle style = workbook.createCellStyle(); Font font = workbook.createFont(); @@ -1339,20 +1478,29 @@ public class BusAttendanceServiceImpl extends ServiceImpl attendanceList, LocalDate start, LocalDate end, int daysInMonth) { - int index = row.getRowNum(); + private void writeDataRow(Row row, SubConstructionUser user, List attendanceList, LocalDate start, LocalDate end, int daysInMonth,CellStyle borderStyle,CellStyle numberBorderStyle) { + int index = row.getRowNum()-2; row.createCell(0).setCellValue(index); row.createCell(1).setCellValue(user.getUserName()); row.createCell(2).setCellValue(idCardEncryptorUtil.decrypt(user.getSfzNumber())); for (int i = 1; i <= daysInMonth; i++) { LocalDate date = start.plusDays(i - 1); - String value = getAttendanceValue(user.getSysUserId(), date, attendanceList); - row.createCell(2 + i).setCellValue(value); + Cell cell = row.createCell(2 + i); + Double value = getAttendanceValue(user.getSysUserId(), date, attendanceList); + cell.setCellValue(value); + // 设置数字格式样式 + cell.setCellStyle(numberBorderStyle); } - double total = countAttendance(user.getSysUserId(), attendanceList, start, end); - row.createCell(2 + daysInMonth + 1).setCellValue(total); +// double total = countAttendance(user.getSysUserId(), attendanceList, start, end); + // ==================== 在这里设置合计列公式 ==================== + Cell sumCell = row.createCell(2 + daysInMonth + 1);// 合计列 + String startCol = "D"; // 第3列 + String endCol = getColumnName(3 + daysInMonth); // 如 AF + String formula = "SUM(" + startCol + (row.getRowNum() + 1) + ":" + endCol + (row.getRowNum() + 1) + ")"; + sumCell.setCellFormula(formula); + sumCell.setCellStyle(borderStyle); String leaveStatus = "0".equals(user.getExitStatus()) ? "未离场" : "已离场"; row.createCell(2 + daysInMonth + 2).setCellValue(leaveStatus); @@ -1360,14 +1508,14 @@ public class BusAttendanceServiceImpl extends ServiceImpl attendanceList) { + private Double getAttendanceValue(Long userId, LocalDate date, List attendanceList) { List validRecords = attendanceList.stream() .filter(a -> a.getUserId().equals(userId) && a.getClockDate().equals(date)) .filter(a -> !a.getClockStatus().equals("4")) // 排除缺卡记录 .collect(Collectors.toList()); if (validRecords.isEmpty()) { - return "0"; // 无有效打卡 + return 0D; // 无有效打卡 } // 检查是否有有效出勤状态(1,2,3,5) @@ -1375,16 +1523,16 @@ public class BusAttendanceServiceImpl extends ServiceImpl Arrays.asList("1", "2", "3", "5").contains(a.getClockStatus())); if (!hasValidStatus) { - return "0"; // 状态无效,如补卡、其他异常 + return 0D; // 状态无效,如补卡、其他异常 } // 判断是否为半勤(仅一次有效打卡) if (validRecords.size() == 1) { - return "0.5"; + return 0.5D; } // 正常出勤(两次有效打卡) - return "1"; + return 1D; } private double countAttendance(Long userId, List attendanceList, LocalDate start, LocalDate end) { @@ -1392,12 +1540,8 @@ public class BusAttendanceServiceImpl extends ServiceImpl().eq(BusWorkWage::getWorkType, workType) + .last("limit 1")); + } }