BUG和工资
This commit is contained in:
		| @ -21,6 +21,7 @@ import org.dromara.contractor.domain.vo.usersalaryperiod.SubConstructionUserSala | |||||||
| import org.dromara.contractor.service.ISubUserSalaryDetailService; | import org.dromara.contractor.service.ISubUserSalaryDetailService; | ||||||
| import org.springframework.validation.annotation.Validated; | import org.springframework.validation.annotation.Validated; | ||||||
| import org.springframework.web.bind.annotation.*; | import org.springframework.web.bind.annotation.*; | ||||||
|  | import org.springframework.web.multipart.MultipartFile; | ||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| @ -85,4 +86,16 @@ public class SubUserSalaryDetailController extends BaseController { | |||||||
|         subUserSalaryDetailService.export(response, dto); |         subUserSalaryDetailService.export(response, dto); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @PutMapping("/import") | ||||||
|  |     public R<Void> importData(@RequestParam("file") MultipartFile file, | ||||||
|  |                               @RequestParam("month") String month) { | ||||||
|  |         subUserSalaryDetailService.importData(file, month); | ||||||
|  |         return R.ok(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @GetMapping("/detailList") | ||||||
|  |     public R<List<SubUserSalaryDetail>> detailList( SubConstructionUserSalaryDto dto) { | ||||||
|  |         return R.ok(subUserSalaryDetailService.detailList(dto)); | ||||||
|  |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -65,4 +65,13 @@ public class SubUserSalaryDetail extends BaseEntity { | |||||||
|      */ |      */ | ||||||
|     private String remark; |     private String remark; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 工时 | ||||||
|  |      */ | ||||||
|  |     private Double workHour; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 当日总工资 | ||||||
|  |      */ | ||||||
|  |     private BigDecimal totalSalary; | ||||||
| } | } | ||||||
|  | |||||||
| @ -28,4 +28,10 @@ public class SubConstructionUserSalaryDto { | |||||||
|      */ |      */ | ||||||
|     @NotBlank(message = "时间不能为空") |     @NotBlank(message = "时间不能为空") | ||||||
|     private String time; |     private String time; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户 | ||||||
|  |      */ | ||||||
|  |     private Long userId; | ||||||
|  |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -36,6 +36,10 @@ public class SubConstructionUserSalaryVo implements Serializable { | |||||||
|      */ |      */ | ||||||
|     private Long id; |     private Long id; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户id | ||||||
|  |      */ | ||||||
|  |     private Long userId; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 人员姓名 |      * 人员姓名 | ||||||
| @ -83,7 +87,7 @@ public class SubConstructionUserSalaryVo implements Serializable { | |||||||
|     private BigDecimal salary; |     private BigDecimal salary; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 时间 |      * 发放时间 | ||||||
|      */ |      */ | ||||||
|     private String time; |     private String time; | ||||||
|  |  | ||||||
| @ -91,4 +95,9 @@ public class SubConstructionUserSalaryVo implements Serializable { | |||||||
|  |  | ||||||
|     private String blank; |     private String blank; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 上传时间 | ||||||
|  |      */ | ||||||
|  |     private String createTime; | ||||||
|  |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -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<String, Double> dailyAttendance = new LinkedHashMap<>(); | ||||||
|  | } | ||||||
| @ -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<Map<Integer, String>> { | ||||||
|  |     private final List<DynamicSalaryData> 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<DynamicSalaryData> 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<Integer, String> headMap, AnalysisContext context) { | ||||||
|  |         // 仅处理固定行号的表头,且仅初始化1次 | ||||||
|  |         int currentRowIndex = context.readRowHolder().getRowIndex(); | ||||||
|  |         if (currentRowIndex != FIXED_HEAD_ROW_NUMBER || isHeadInitialized) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 解析“合计”列索引 | ||||||
|  |         for (Map.Entry<Integer, String> 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<Integer, String> 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<Integer, String> 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<Integer, String> 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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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<DynamicSalaryData> readAllAttendanceByStream(InputStream inputStream, String month) { | ||||||
|  |         List<DynamicSalaryData> 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<ReadSheet> 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<DynamicSalaryData> readAllAttendance(String excelFilePath, String month) { | ||||||
|  |         List<DynamicSalaryData> 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<ReadSheet> 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<DynamicSalaryData> 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()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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.usersalarydetail.SubUserSalaryDetailVo; | ||||||
| import org.dromara.contractor.domain.vo.usersalaryperiod.SubConstructionUserSalaryVo; | import org.dromara.contractor.domain.vo.usersalaryperiod.SubConstructionUserSalaryVo; | ||||||
| import org.springframework.validation.annotation.Validated; | 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.io.IOException; | ||||||
| import java.time.LocalDate; | import java.time.LocalDate; | ||||||
| @ -95,6 +97,14 @@ public interface ISubUserSalaryDetailService extends IService<SubUserSalaryDetai | |||||||
|      */ |      */ | ||||||
|     void export(HttpServletResponse response, SubConstructionUserSalaryDto dto) throws IOException; |     void export(HttpServletResponse response, SubConstructionUserSalaryDto dto) throws IOException; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 导入 | ||||||
|  |      */ | ||||||
|  |     void importData( MultipartFile file, String month); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 人员详情 | ||||||
|  |      */ | ||||||
|  |     List<SubUserSalaryDetail> detailList( SubConstructionUserSalaryDto dto); | ||||||
|  |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1127,7 +1127,14 @@ public class SubConstructionUserServiceImpl extends ServiceImpl<SubConstructionU | |||||||
|             vo.setGender(wordsResult.getGender() != null ? wordsResult.getGender().getWords() : ""); |             vo.setGender(wordsResult.getGender() != null ? wordsResult.getGender().getWords() : ""); | ||||||
|             vo.setImage(upload); |             vo.setImage(upload); | ||||||
|         } else { |         } else { | ||||||
|             vo.setExpiryDate(wordsResult.getExpiryDate() != null ? wordsResult.getExpiryDate().getWords() : ""); |             if (wordsResult.getExpiryDate() != null ) { | ||||||
|  |                 vo.setExpiryDate(wordsResult.getExpiryDate().getWords()); | ||||||
|  |                 if ("长期".equals(wordsResult.getExpiryDate().getWords())) { | ||||||
|  |                     vo.setExpiryDate("9999-12-31"); | ||||||
|  |                 } | ||||||
|  |             }else { | ||||||
|  |                 vo.setExpiryDate(""); | ||||||
|  |             } | ||||||
|             vo.setIssuingAuthority(wordsResult.getIssuingAuthority() != null ? wordsResult.getIssuingAuthority().getWords() : ""); |             vo.setIssuingAuthority(wordsResult.getIssuingAuthority() != null ? wordsResult.getIssuingAuthority().getWords() : ""); | ||||||
|             vo.setIssuingDate(wordsResult.getIssuingDate() != null ? wordsResult.getIssuingDate().getWords() : ""); |             vo.setIssuingDate(wordsResult.getIssuingDate() != null ? wordsResult.getIssuingDate().getWords() : ""); | ||||||
|             vo.setImage(upload); |             vo.setImage(upload); | ||||||
|  | |||||||
| @ -3,11 +3,15 @@ package org.dromara.contractor.service.impl; | |||||||
| import cn.hutool.core.bean.BeanUtil; | import cn.hutool.core.bean.BeanUtil; | ||||||
| import cn.hutool.core.collection.CollUtil; | import cn.hutool.core.collection.CollUtil; | ||||||
| import cn.hutool.core.collection.CollectionUtil; | import cn.hutool.core.collection.CollectionUtil; | ||||||
|  | import cn.hutool.core.date.DateUtil; | ||||||
| import com.alibaba.excel.EasyExcel; | import com.alibaba.excel.EasyExcel; | ||||||
| import com.alibaba.excel.ExcelWriter; | import com.alibaba.excel.ExcelWriter; | ||||||
| import com.alibaba.excel.write.metadata.WriteSheet; | import com.alibaba.excel.write.metadata.WriteSheet; | ||||||
| import com.alibaba.excel.write.metadata.fill.FillConfig; | import com.alibaba.excel.write.metadata.fill.FillConfig; | ||||||
|  | import com.baomidou.mybatisplus.core.conditions.Wrapper; | ||||||
| import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | ||||||
|  | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | ||||||
|  | import com.baomidou.mybatisplus.core.toolkit.Wrappers; | ||||||
| import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | ||||||
| import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; | ||||||
| import jakarta.annotation.Resource; | import jakarta.annotation.Resource; | ||||||
| @ -19,6 +23,7 @@ import org.apache.poi.ss.usermodel.Sheet; | |||||||
| import org.apache.poi.ss.usermodel.Workbook; | import org.apache.poi.ss.usermodel.Workbook; | ||||||
| import org.apache.poi.ss.usermodel.WorkbookFactory; | import org.apache.poi.ss.usermodel.WorkbookFactory; | ||||||
| import org.apache.poi.xssf.usermodel.XSSFWorkbook; | import org.apache.poi.xssf.usermodel.XSSFWorkbook; | ||||||
|  | import org.dromara.bigscreen.domain.BusConstructionUser; | ||||||
| import org.dromara.common.core.constant.HttpStatus; | import org.dromara.common.core.constant.HttpStatus; | ||||||
| import org.dromara.common.core.exception.ServiceException; | import org.dromara.common.core.exception.ServiceException; | ||||||
| import org.dromara.common.core.utils.ObjectUtils; | import org.dromara.common.core.utils.ObjectUtils; | ||||||
| @ -36,9 +41,11 @@ import org.dromara.contractor.domain.exportvo.BusConstructionUserExportVo; | |||||||
| import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserVo; | import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserVo; | ||||||
| import org.dromara.contractor.domain.vo.usersalarydetail.SubUserSalaryDetailVo; | import org.dromara.contractor.domain.vo.usersalarydetail.SubUserSalaryDetailVo; | ||||||
| import org.dromara.contractor.domain.vo.usersalaryperiod.SubConstructionUserSalaryVo; | import org.dromara.contractor.domain.vo.usersalaryperiod.SubConstructionUserSalaryVo; | ||||||
|  | import org.dromara.contractor.excel.DynamicSalaryData; | ||||||
| import org.dromara.contractor.mapper.SubUserSalaryDetailMapper; | import org.dromara.contractor.mapper.SubUserSalaryDetailMapper; | ||||||
| import org.dromara.contractor.service.ISubConstructionUserService; | import org.dromara.contractor.service.ISubConstructionUserService; | ||||||
| import org.dromara.contractor.service.ISubUserSalaryDetailService; | import org.dromara.contractor.service.ISubUserSalaryDetailService; | ||||||
|  | import org.dromara.gps.domain.vo.ConstructionUser; | ||||||
| import org.dromara.project.domain.BusAttendance; | import org.dromara.project.domain.BusAttendance; | ||||||
| import org.dromara.project.domain.BusProject; | import org.dromara.project.domain.BusProject; | ||||||
| import org.dromara.project.domain.BusProjectTeam; | import org.dromara.project.domain.BusProjectTeam; | ||||||
| @ -51,6 +58,7 @@ import org.springframework.beans.BeanUtils; | |||||||
| import org.springframework.context.annotation.Lazy; | import org.springframework.context.annotation.Lazy; | ||||||
| import org.springframework.scheduling.annotation.Async; | import org.springframework.scheduling.annotation.Async; | ||||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||||
|  | import org.springframework.web.multipart.MultipartFile; | ||||||
| import software.amazon.awssdk.utils.CollectionUtils; | import software.amazon.awssdk.utils.CollectionUtils; | ||||||
|  |  | ||||||
| import java.io.ByteArrayInputStream; | import java.io.ByteArrayInputStream; | ||||||
| @ -65,6 +73,7 @@ import java.time.YearMonth; | |||||||
| import java.time.format.DateTimeFormatter; | import java.time.format.DateTimeFormatter; | ||||||
| import java.util.*; | import java.util.*; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
|  | import org.dromara.contractor.excel.SalaryExcelReader; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 员工每日工资Service业务层处理 |  * 员工每日工资Service业务层处理 | ||||||
| @ -263,76 +272,53 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet | |||||||
|     @Override |     @Override | ||||||
|     public TableDataInfo<SubConstructionUserSalaryVo> salaryPageList(SubConstructionUserSalaryDto dto, PageQuery pageQuery) { |     public TableDataInfo<SubConstructionUserSalaryVo> salaryPageList(SubConstructionUserSalaryDto dto, PageQuery pageQuery) { | ||||||
|  |  | ||||||
|         SubConstructionUserQueryReq req = new SubConstructionUserQueryReq(); |  | ||||||
|         req.setProjectId(dto.getProjectId()); |  | ||||||
|         req.setTeamId(dto.getTeamId()); |  | ||||||
|         req.setUserName(dto.getUserName()); |  | ||||||
|  |  | ||||||
|         TableDataInfo<SubConstructionUserVo> subConstructionUserVoTableDataInfo = constructionUserService.queryPageList(req, pageQuery); |  | ||||||
|         List<SubConstructionUserVo> rows = subConstructionUserVoTableDataInfo.getRows(); |  | ||||||
|         if(CollectionUtil.isEmpty(rows)){ |  | ||||||
|             return TableDataInfo.build(); |  | ||||||
|         } |  | ||||||
|         String time = dto.getTime(); |         String time = dto.getTime(); | ||||||
|         YearMonth parse = YearMonth.parse(time, DateTimeFormatter.ofPattern("yyyy-MM")); |         YearMonth parse = YearMonth.parse(time, DateTimeFormatter.ofPattern("yyyy-MM")); | ||||||
|         LocalDate start = parse.atDay(1); |         LocalDate start = parse.atDay(1); | ||||||
|         LocalDate end = parse.atEndOfMonth(); |         LocalDate end = parse.atEndOfMonth(); | ||||||
|  |  | ||||||
|         List<Long> userIds = rows.stream().map(SubConstructionUserVo::getSysUserId).toList(); |         QueryWrapper<SubUserSalaryDetail> queryWrapper = new QueryWrapper<>(); | ||||||
|         //考勤数据 |         queryWrapper.select("user_id", "SUM(work_hour) as workHour", "SUM(total_salary) as totalSalary","max(create_time) as createTime") | ||||||
|         List<BusAttendance> attendanceList = attendanceService.lambdaQuery() |             .eq("project_id", dto.getProjectId()) | ||||||
|             .eq(BusAttendance::getProjectId, dto.getProjectId()) |             .between("report_date", start, end) | ||||||
|             .in(BusAttendance::getUserId, userIds) |             .eq(dto.getTeamId()!=null,"team_id", dto.getTeamId()) | ||||||
|             .between(BusAttendance::getClockDate, start, end) |             .like(StringUtils.isNotBlank(dto.getUserName()),"user_name", dto.getUserName()) | ||||||
|             .list(); |             .groupBy("user_id"); | ||||||
|         //工资数据 |  | ||||||
|         List<SubUserSalaryDetail> salaryDetailList = this.lambdaQuery() |  | ||||||
|             .eq(SubUserSalaryDetail::getProjectId, dto.getProjectId()) |  | ||||||
|             .in(SubUserSalaryDetail::getUserId, userIds) |  | ||||||
|             .between(SubUserSalaryDetail::getReportDate, start, end) |  | ||||||
|             .list(); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         Page<SubUserSalaryDetail> result = this.page(pageQuery.build(), queryWrapper); | ||||||
|  |         List<SubUserSalaryDetail> records = result.getRecords(); | ||||||
|  |         List<Long> userIds = records.stream().map(SubUserSalaryDetail::getUserId).toList(); | ||||||
|  |         Map<Long, SubConstructionUser> collect = new HashMap<>(); | ||||||
|  |         if(CollectionUtil.isNotEmpty(userIds)){ | ||||||
|  |             List<SubConstructionUser> subConstructionUsers = constructionUserService.list(Wrappers.lambdaQuery(SubConstructionUser.class) | ||||||
|  |                 .in(SubConstructionUser::getSysUserId, userIds)); | ||||||
|  |             collect = subConstructionUsers.stream().collect(Collectors.toMap(SubConstructionUser::getSysUserId, vo -> vo)); | ||||||
|  |         } | ||||||
|         ArrayList<SubConstructionUserSalaryVo> vos = new ArrayList<>(); |         ArrayList<SubConstructionUserSalaryVo> vos = new ArrayList<>(); | ||||||
|         for (SubConstructionUserVo row : rows) { |         for (SubUserSalaryDetail detail : records) { | ||||||
|             SubConstructionUserSalaryVo vo = new SubConstructionUserSalaryVo(); |             SubConstructionUserSalaryVo vo = new SubConstructionUserSalaryVo(); | ||||||
|             BeanUtil.copyProperties(row,vo); |             vo.setId(detail.getId()); | ||||||
| //            vo.setSfzNumber(idCardEncryptorUtil.decrypt(vo.getSfzNumber())); |  | ||||||
|             vo.setTime(dto.getTime()); |             vo.setTime(dto.getTime()); | ||||||
|             // 获取工资 |             vo.setTotalSalary(detail.getTotalSalary()); | ||||||
|             BigDecimal salary = row.getSalary(); |             vo.setCreateTime(DateUtil.format(detail.getCreateTime(), "yyyy-MM-dd HH:mm:ss")); | ||||||
|             if (salary == null || salary.compareTo(BigDecimal.ZERO) == 0) { |             vo.setWorkDay(detail.getWorkHour()); | ||||||
|                 String typeOfWork = row.getTypeOfWork(); |             vo.setProjectId(dto.getProjectId()); | ||||||
|                 String wageMeasureUnit = row.getWageMeasureUnit(); |             vo.setUserId(detail.getUserId()); | ||||||
|                 if (StringUtils.isNotEmpty(typeOfWork) && StringUtils.isNotEmpty(wageMeasureUnit)) { |             SubConstructionUser constructionUser = collect.get(detail.getUserId()); | ||||||
|                     BusWorkWage workWage = workWageService.lambdaQuery() |             if(constructionUser != null){ | ||||||
|                         .eq(BusWorkWage::getProjectId, row.getProjectId()) |                 if(constructionUser.getSfzNumber() != null){ | ||||||
|                         .eq(BusWorkWage::getWorkType, typeOfWork) |                     vo.setSfzNumber(idCardEncryptorUtil.decrypt(constructionUser.getSfzNumber())); | ||||||
|                         .eq(BusWorkWage::getWageMeasureUnit, wageMeasureUnit) |  | ||||||
|                         .one(); |  | ||||||
|                     if (workWage != null) { |  | ||||||
|                         salary = workWage.getWage(); |  | ||||||
|                     } else { |  | ||||||
|                         salary = BigDecimal.ZERO; |  | ||||||
|                 } |                 } | ||||||
|                 } else { |                 vo.setUserName(constructionUser.getUserName()); | ||||||
|                     salary = BigDecimal.ZERO; |                 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<BusAttendance> 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); |             vos.add(vo); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return new TableDataInfo<>(vos,subConstructionUserVoTableDataInfo.getTotal()); |         return new TableDataInfo<>(vos,result.getTotal()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @ -342,27 +328,34 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet | |||||||
|         if (project == null) { |         if (project == null) { | ||||||
|             throw new ServiceException("项目不存在"); |             throw new ServiceException("项目不存在"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         List<SubConstructionUser> 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. 解析年月和查询薪资明细 |         // 2. 解析年月和查询薪资明细 | ||||||
|         YearMonth yearMonth = YearMonth.parse(dto.getTime(), DateTimeFormatter.ofPattern("yyyy-MM")); |         YearMonth yearMonth = YearMonth.parse(dto.getTime(), DateTimeFormatter.ofPattern("yyyy-MM")); | ||||||
|         LocalDate start = yearMonth.atDay(1); |         LocalDate start = yearMonth.atDay(1); | ||||||
|         LocalDate end = yearMonth.atEndOfMonth(); |         LocalDate end = yearMonth.atEndOfMonth(); | ||||||
|  |  | ||||||
|         List<Long> userIds = userList.stream().map(SubConstructionUser::getSysUserId).toList(); |         QueryWrapper<SubUserSalaryDetail> queryWrapper = new QueryWrapper<>(); | ||||||
|         List<SubUserSalaryDetail> salaryDetails = this.lambdaQuery() |         queryWrapper.select("user_id", "SUM(work_hour) as workHour", "SUM(total_salary) as totalSalary","max(create_time) as createTime") | ||||||
|             .eq(SubUserSalaryDetail::getProjectId, dto.getProjectId()) |             .eq("project_id", dto.getProjectId()) | ||||||
|             .in(SubUserSalaryDetail::getUserId, userIds) |             .between("report_date", start, end) | ||||||
|             .between(SubUserSalaryDetail::getReportDate, 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<SubUserSalaryDetail> salaryDetailsList= baseMapper.selectList(queryWrapper); | ||||||
|  |  | ||||||
|  |         if (salaryDetailsList.isEmpty()) { | ||||||
|  |             throw new ServiceException("暂无数据"); | ||||||
|  |         } | ||||||
|  |         Map<Long, SubUserSalaryDetail> map = salaryDetailsList.stream().collect(Collectors.toMap(SubUserSalaryDetail::getUserId, vo -> vo)); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         List<Long> userIds = salaryDetailsList.stream().map(SubUserSalaryDetail::getUserId).toList(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         List<SubConstructionUser> userList = constructionUserService.lambdaQuery() | ||||||
|  |             .in(SubConstructionUser::getSysUserId, userIds) | ||||||
|             .list(); |             .list(); | ||||||
|  |  | ||||||
|         // 3. 设置响应头(下载文件配置) |         // 3. 设置响应头(下载文件配置) | ||||||
| @ -465,7 +458,7 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet | |||||||
|                         .build(); |                         .build(); | ||||||
|  |  | ||||||
|                     // 填充列表数据和汇总数据 |                     // 填充列表数据和汇总数据 | ||||||
|                     excelWriter.fill(getTeamData(userList, salaryDetails, teamId), fillConfig, writeSheet); |                     excelWriter.fill(getTeamData(userList, map, teamId), fillConfig, writeSheet); | ||||||
|                     excelWriter.fill(getTeamSummary(project, teamNameMap.get(teamId), yearMonth), writeSheet); |                     excelWriter.fill(getTeamSummary(project, teamNameMap.get(teamId), yearMonth), writeSheet); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
| @ -474,7 +467,7 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet | |||||||
|                     WriteSheet noTeamSheet = EasyExcel.writerSheet("无班组").build(); |                     WriteSheet noTeamSheet = EasyExcel.writerSheet("无班组").build(); | ||||||
|                     FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build(); |                     FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build(); | ||||||
|  |  | ||||||
|                     excelWriter.fill(getTeamData(userList, salaryDetails, null), fillConfig, noTeamSheet); |                     excelWriter.fill(getTeamData(userList, map, null), fillConfig, noTeamSheet); | ||||||
|                     excelWriter.fill(getTeamSummary(project, null, yearMonth), noTeamSheet); |                     excelWriter.fill(getTeamSummary(project, null, yearMonth), noTeamSheet); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @ -498,7 +491,7 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private List<SubConstructionUserSalaryVo> getTeamData(List<SubConstructionUser> rows, |     private List<SubConstructionUserSalaryVo> getTeamData(List<SubConstructionUser> rows, | ||||||
|                                    List<SubUserSalaryDetail> salaryDetailList,Long teamId){ |                                                           Map<Long, SubUserSalaryDetail> map,Long teamId){ | ||||||
|         List<SubConstructionUser> list1 ; |         List<SubConstructionUser> list1 ; | ||||||
|         if(teamId == null){ |         if(teamId == null){ | ||||||
|             list1 =  rows.stream().filter(row -> row.getTeamId() == null && Arrays.asList("1","2").contains(row.getExitStatus())).toList(); |             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<SubUserSalaryDet | |||||||
|             SubConstructionUserSalaryVo vo = new SubConstructionUserSalaryVo(); |             SubConstructionUserSalaryVo vo = new SubConstructionUserSalaryVo(); | ||||||
|             BeanUtil.copyProperties(row,vo); |             BeanUtil.copyProperties(row,vo); | ||||||
|             vo.setOrder(i); |             vo.setOrder(i); | ||||||
|             vo.setTotalSalary(salaryDetailList.stream().filter(detail -> detail.getUserId().equals(row.getSysUserId())) |             SubUserSalaryDetail detail1 = map.get(row.getSysUserId()); | ||||||
|                 .map(SubUserSalaryDetail::getDailySalary) |             vo.setTotalSalary(detail1.getTotalSalary()); | ||||||
|                 .reduce(BigDecimal.ZERO, BigDecimal::add) |  | ||||||
|             ); |  | ||||||
|             vos.add(vo); |             vos.add(vo); | ||||||
|             i++; |             i++; | ||||||
|         } |         } | ||||||
| @ -532,4 +523,109 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void importData(MultipartFile file, String month) { | ||||||
|  |         // 1. 校验文件合法性 | ||||||
|  |         if (file.isEmpty()) { | ||||||
|  |             throw new IllegalArgumentException("上传的Excel文件不能为空!"); | ||||||
|  |         } | ||||||
|  |         // 校验文件格式(仅允许xlsx/xls) | ||||||
|  |         String originalFilename = file.getOriginalFilename(); | ||||||
|  |         if (originalFilename == null || !(originalFilename.endsWith(".xlsx"))) { | ||||||
|  |             throw new IllegalArgumentException("仅支持上传Excel文件(.xlsx 格式)!"); | ||||||
|  |         } | ||||||
|  |         List<DynamicSalaryData> 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<String, Map<String, Double>> dataMap =  dataList.stream().collect(Collectors.toMap(vo -> idCardEncryptorUtil.encrypt(vo.getIdCard()), DynamicSalaryData::getDailyAttendance)); | ||||||
|  |         Set<String> cards = dataMap.keySet(); | ||||||
|  |  | ||||||
|  |         YearMonth parse = YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyy-MM")); | ||||||
|  |         LocalDate start = parse.atDay(1); | ||||||
|  |         LocalDate end = parse.atEndOfMonth(); | ||||||
|  |  | ||||||
|  |         //人员数据 | ||||||
|  |         List<SubConstructionUser> list = constructionUserService.list(Wrappers.<SubConstructionUser>lambdaQuery() | ||||||
|  |             .in(SubConstructionUser::getSfzNumber, cards)); | ||||||
|  |  | ||||||
|  |         List<Long> userIds = list.stream().map(SubConstructionUser::getSysUserId).toList(); | ||||||
|  |         //考勤数据 | ||||||
|  |         List<BusAttendance> attendanceList = attendanceService | ||||||
|  |             .list(Wrappers.<BusAttendance>lambdaQuery() | ||||||
|  |                 .in(BusAttendance::getUserId, userIds) | ||||||
|  |                 .between(BusAttendance::getClockDate, start, end) | ||||||
|  |             ); | ||||||
|  |         // 将 attendanceList 转换为 Map<String, BigDecimal> 格式 | ||||||
|  |         Map<String, BigDecimal> 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<SubUserSalaryDetail> addList = new ArrayList<>(); | ||||||
|  |         for(SubConstructionUser constructionUser : list){ | ||||||
|  |             Map<String, Double> stringIntegerMap = dataMap.get(constructionUser.getSfzNumber()); | ||||||
|  |             if(stringIntegerMap != null){ | ||||||
|  |                 for(Map.Entry<String, Double> 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.<SubUserSalaryDetail>lambdaQuery() | ||||||
|  |             .in(SubUserSalaryDetail::getUserId, userIds) | ||||||
|  |             .between(SubUserSalaryDetail::getReportDate, start, end) | ||||||
|  |         ); | ||||||
|  |         baseMapper.insertBatch(addList); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public List<SubUserSalaryDetail> detailList(SubConstructionUserSalaryDto dto) { | ||||||
|  |  | ||||||
|  |         LambdaQueryWrapper<SubUserSalaryDetail> 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); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| package org.dromara.job.attendance; | package org.dromara.job.attendance; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | import cn.hutool.core.collection.CollectionUtil; | ||||||
| import cn.hutool.core.date.DateUtil; | import cn.hutool.core.date.DateUtil; | ||||||
| import com.aizuda.snailjob.client.job.core.annotation.JobExecutor; | import com.aizuda.snailjob.client.job.core.annotation.JobExecutor; | ||||||
| import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | 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.domain.SubConstructionUser; | ||||||
| import org.dromara.contractor.service.ISubConstructionUserService; | import org.dromara.contractor.service.ISubConstructionUserService; | ||||||
| import org.dromara.project.constant.BusProjectConstant; | import org.dromara.project.constant.BusProjectConstant; | ||||||
| import org.dromara.project.domain.BusAttendance; | import org.dromara.project.domain.*; | ||||||
| import org.dromara.project.domain.BusAttendanceRule; |  | ||||||
| import org.dromara.project.domain.BusProject; |  | ||||||
| import org.dromara.project.domain.BusUserProjectRelevancy; |  | ||||||
| import org.dromara.project.domain.enums.BusAttendanceClockStatusEnum; | import org.dromara.project.domain.enums.BusAttendanceClockStatusEnum; | ||||||
| import org.dromara.project.domain.enums.BusAttendanceCommuterEnum; | import org.dromara.project.domain.enums.BusAttendanceCommuterEnum; | ||||||
| import org.dromara.project.service.*; | 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.scheduling.annotation.Scheduled; | ||||||
| import org.springframework.stereotype.Component; | import org.springframework.stereotype.Component; | ||||||
|  |  | ||||||
|  | import java.math.BigDecimal; | ||||||
| import java.time.DayOfWeek; | import java.time.DayOfWeek; | ||||||
| import java.time.LocalDate; | import java.time.LocalDate; | ||||||
| import java.time.LocalDateTime; | import java.time.LocalDateTime; | ||||||
| import java.time.LocalTime; | import java.time.LocalTime; | ||||||
| import java.util.*; | import java.util.*; | ||||||
|  | import java.util.stream.Stream; | ||||||
|  |  | ||||||
| @Slf4j | @Slf4j | ||||||
| @Component | @Component | ||||||
| @ -53,6 +53,11 @@ public class AttendanceJob { | |||||||
|     @Resource |     @Resource | ||||||
|     private ISubConstructionUserService constructionUserService; |     private ISubConstructionUserService constructionUserService; | ||||||
|  |  | ||||||
|  |     @Resource | ||||||
|  |     private IBusWorkWageService workWageService; | ||||||
|  |  | ||||||
|  |     //0系统管理员 1普通人员 2项目管理员  3分包    只有1才生成缺卡记录  无需打卡人员 | ||||||
|  |     private static final List<String> noClockUserTypes = Arrays.asList("0","2"); | ||||||
|  |  | ||||||
| //    @Scheduled(cron = "0 0/10 * * * ?") | //    @Scheduled(cron = "0 0/10 * * * ?") | ||||||
|     @JobExecutor(name = "clockInMiss") |     @JobExecutor(name = "clockInMiss") | ||||||
| @ -126,7 +131,7 @@ public class AttendanceJob { | |||||||
|  |  | ||||||
|                 for (BusUserProjectRelevancy relevancy : relevancyList) { |                 for (BusUserProjectRelevancy relevancy : relevancyList) { | ||||||
|  |  | ||||||
|                     if (attendanceUserIds.contains(relevancy.getUserId()) || "0".equals(relevancy.getUserType())) { |                     if (attendanceUserIds.contains(relevancy.getUserId()) || noClockUserTypes.contains(relevancy.getUserType())) { | ||||||
|                         continue; |                         continue; | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
| @ -156,6 +161,7 @@ public class AttendanceJob { | |||||||
|                     }else { |                     }else { | ||||||
|                         busAttendance.setClockStatus(BusAttendanceClockStatusEnum.UNCLOCK.getValue()); |                         busAttendance.setClockStatus(BusAttendanceClockStatusEnum.UNCLOCK.getValue()); | ||||||
|                     } |                     } | ||||||
|  |                     busAttendance.setSalary(computeSalary(constructionUser, null)); | ||||||
|  |  | ||||||
|                     missList.add(busAttendance); |                     missList.add(busAttendance); | ||||||
|                 } |                 } | ||||||
| @ -204,7 +210,9 @@ public class AttendanceJob { | |||||||
|             //管理员关联多个项目,需要记录是否已生成缺卡记录 |             //管理员关联多个项目,需要记录是否已生成缺卡记录 | ||||||
|             HashSet<Long> manageUserIds = new HashSet<>(); |             HashSet<Long> manageUserIds = new HashSet<>(); | ||||||
|  |  | ||||||
|  |  | ||||||
|             List<BusAttendance> missList = new ArrayList<>(); |             List<BusAttendance> missList = new ArrayList<>(); | ||||||
|  |  | ||||||
|             for (BusAttendanceRule rule : list) { |             for (BusAttendanceRule rule : list) { | ||||||
|  |  | ||||||
|                 LocalTime clockOutTime = rule.getClockOutTime(); |                 LocalTime clockOutTime = rule.getClockOutTime(); | ||||||
| @ -235,16 +243,22 @@ public class AttendanceJob { | |||||||
|                 List<BusUserProjectRelevancy> relevancyList = userProjectRelevancyService.list(Wrappers.lambdaQuery(BusUserProjectRelevancy.class) |                 List<BusUserProjectRelevancy> relevancyList = userProjectRelevancyService.list(Wrappers.lambdaQuery(BusUserProjectRelevancy.class) | ||||||
|                     .eq(BusUserProjectRelevancy::getProjectId, rule.getProjectId())); |                     .eq(BusUserProjectRelevancy::getProjectId, rule.getProjectId())); | ||||||
|  |  | ||||||
|                 //查询当天已打下班卡人员 |                 //查询当天打卡人员 | ||||||
|                 List<BusAttendance> attendanceList = attendanceService.list(Wrappers.lambdaQuery(BusAttendance.class) |                 List<BusAttendance> allAttendanceList = attendanceService.list(Wrappers.lambdaQuery(BusAttendance.class) | ||||||
|                     .eq(BusAttendance::getClockDate, date) |                     .eq(BusAttendance::getClockDate, date) | ||||||
|                     .eq(BusAttendance::getClockType, BusAttendanceCommuterEnum.CLOCKOUT.getValue()) |  | ||||||
|                 ); |                 ); | ||||||
|  |  | ||||||
|  |                 List<BusAttendance> inAttendanceList  = allAttendanceList.stream() | ||||||
|  |                     .filter(attendance -> BusAttendanceCommuterEnum.CLOCKIN.getValue().equals(attendance.getClockType())).toList(); | ||||||
|  |  | ||||||
|  |                 List<BusAttendance> attendanceList = allAttendanceList.stream() | ||||||
|  |                     .filter(attendance -> BusAttendanceCommuterEnum.CLOCKOUT.getValue().equals(attendance.getClockType())).toList(); | ||||||
|  |  | ||||||
|                 List<Long> attendanceUserIds = attendanceList.stream().map(BusAttendance::getUserId).toList(); |                 List<Long> attendanceUserIds = attendanceList.stream().map(BusAttendance::getUserId).toList(); | ||||||
|  |  | ||||||
|  |  | ||||||
|                 for (BusUserProjectRelevancy relevancy : relevancyList) { |                 for (BusUserProjectRelevancy relevancy : relevancyList) { | ||||||
|                     if (attendanceUserIds.contains(relevancy.getUserId()) || "0".equals(relevancy.getUserType())) { |                     if (attendanceUserIds.contains(relevancy.getUserId()) || noClockUserTypes.contains(relevancy.getUserType())) { | ||||||
|                         continue; |                         continue; | ||||||
|                     } |                     } | ||||||
|                     BusAttendance busAttendance = new BusAttendance(); |                     BusAttendance busAttendance = new BusAttendance(); | ||||||
| @ -263,8 +277,6 @@ public class AttendanceJob { | |||||||
|                         continue; |                         continue; | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|                     busAttendance.setUserId(relevancy.getUserId()); |                     busAttendance.setUserId(relevancy.getUserId()); | ||||||
|                     busAttendance.setUserName(constructionUser.getUserName()); |                     busAttendance.setUserName(constructionUser.getUserName()); | ||||||
|  |  | ||||||
| @ -276,7 +288,8 @@ public class AttendanceJob { | |||||||
|                     }else { |                     }else { | ||||||
|                         busAttendance.setClockStatus(BusAttendanceClockStatusEnum.UNCLOCK.getValue()); |                         busAttendance.setClockStatus(BusAttendanceClockStatusEnum.UNCLOCK.getValue()); | ||||||
|                     } |                     } | ||||||
|  |                     List<BusAttendance> list1 = inAttendanceList.stream().filter(vo -> relevancy.getUserId().equals(vo.getUserId())).toList(); | ||||||
|  |                     busAttendance.setSalary(computeSalary(constructionUser, list1)); | ||||||
|                     missList.add(busAttendance); |                     missList.add(busAttendance); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
| @ -313,4 +326,28 @@ public class AttendanceJob { | |||||||
|  |  | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 计算工资 | ||||||
|  |      */ | ||||||
|  |     public BigDecimal computeSalary(SubConstructionUser constructionUser, List<BusAttendance> 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(); | ||||||
|  |  | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.*; | |||||||
| import lombok.Data; | import lombok.Data; | ||||||
| import lombok.EqualsAndHashCode; | import lombok.EqualsAndHashCode; | ||||||
|  |  | ||||||
|  | import java.math.BigDecimal; | ||||||
| import java.time.LocalDate; | import java.time.LocalDate; | ||||||
| import java.time.LocalDateTime; | import java.time.LocalDateTime; | ||||||
| import java.time.LocalTime; | import java.time.LocalTime; | ||||||
| @ -102,4 +103,9 @@ public class BusAttendance extends BaseEntity { | |||||||
|      * 处理 0-未处理,1-已处理 |      * 处理 0-未处理,1-已处理 | ||||||
|      */ |      */ | ||||||
|     private String handle; |     private String handle; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 薪水 | ||||||
|  |      */ | ||||||
|  |     private BigDecimal salary; | ||||||
| } | } | ||||||
|  | |||||||
| @ -30,4 +30,9 @@ public class AttendanceUserDataDetailVo { | |||||||
|      * 迟到或早退的分钟 |      * 迟到或早退的分钟 | ||||||
|      */ |      */ | ||||||
|     private Integer minuteCount; |     private Integer minuteCount; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 出勤天数 | ||||||
|  |      */ | ||||||
|  |     private Double workDay; | ||||||
| } | } | ||||||
|  | |||||||
| @ -95,4 +95,10 @@ public interface IBusWorkWageService extends IService<BusWorkWage> { | |||||||
|      * @return 工种薪水分页对象视图 |      * @return 工种薪水分页对象视图 | ||||||
|      */ |      */ | ||||||
|     Page<BusWorkWageVo> getVoPage(Page<BusWorkWage> workWagePage); |     Page<BusWorkWageVo> getVoPage(Page<BusWorkWage> workWagePage); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 根据工种获取薪水设置 | ||||||
|  |      */ | ||||||
|  |     BusWorkWage getWorkWageByWorkType(String workType); | ||||||
| } | } | ||||||
|  | |||||||
| @ -54,6 +54,7 @@ import org.springframework.web.multipart.MultipartFile; | |||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.OutputStream; | import java.io.OutputStream; | ||||||
|  | import java.math.BigDecimal; | ||||||
| import java.time.*; | import java.time.*; | ||||||
| import java.time.format.DateTimeFormatter; | import java.time.format.DateTimeFormatter; | ||||||
| import java.util.*; | import java.util.*; | ||||||
| @ -104,6 +105,8 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|  |  | ||||||
|     private final ISubUserSalaryDetailService userSalaryDetailService; |     private final ISubUserSalaryDetailService userSalaryDetailService; | ||||||
|  |  | ||||||
|  |     private final IBusWorkWageService workWageService; | ||||||
|  |  | ||||||
|     @Resource |     @Resource | ||||||
|     private IdCardEncryptorUtil idCardEncryptorUtil; |     private IdCardEncryptorUtil idCardEncryptorUtil; | ||||||
|  |  | ||||||
| @ -149,7 +152,7 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|         Long userId = req.getUserId(); |         Long userId = req.getUserId(); | ||||||
|         String clockMonth = req.getClockMonth(); |         String clockMonth = req.getClockMonth(); | ||||||
|         SubConstructionUser constructionUser = constructionUserService.getById(userId); |         SubConstructionUser constructionUser = constructionUserService.getById(userId); | ||||||
|         if ( constructionUser == null) { |         if (constructionUser == null) { | ||||||
|             throw new ServiceException("施工人员信息不存在", HttpStatus.NOT_FOUND); |             throw new ServiceException("施工人员信息不存在", HttpStatus.NOT_FOUND); | ||||||
|         } |         } | ||||||
|         // 解析月份 |         // 解析月份 | ||||||
| @ -404,13 +407,11 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|                         log.error("异步发送系统消息失败,用户ID: {}, 消息: {}", userId, "打卡成功", e); |                         log.error("异步发送系统消息失败,用户ID: {}, 消息: {}", userId, "打卡成功", e); | ||||||
|                     } |                     } | ||||||
|                 }); |                 }); | ||||||
|  |                 //计算工资 | ||||||
|  |                 attendance.setSalary(computeSalary(constructionUser, inAttendances)); | ||||||
|  |                 return this.save(attendance); | ||||||
|  |  | ||||||
|                     boolean save = this.save(attendance); |             } else if (clockTypeByTime == 2 || CollectionUtils.isNotEmpty(inAttendances)) { | ||||||
|                     //插入工资 |  | ||||||
|                     userSalaryDetailService.insertByAttendance(userId, attendance.getClockDate()); |  | ||||||
|                     return save; |  | ||||||
|  |  | ||||||
|             } else if (clockTypeByTime == 2 || CollectionUtils.isEmpty(outAttendances)) { |  | ||||||
|  |  | ||||||
|                 if (CollectionUtil.isNotEmpty(outAttendances)) { |                 if (CollectionUtil.isNotEmpty(outAttendances)) { | ||||||
|                     BusAttendance busAttendance = outAttendances.getFirst(); |                     BusAttendance busAttendance = outAttendances.getFirst(); | ||||||
| @ -426,7 +427,8 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|                     } else { |                     } else { | ||||||
|                         busAttendance.setClockStatus(BusAttendanceClockStatusEnum.NORMAL.getValue()); |                         busAttendance.setClockStatus(BusAttendanceClockStatusEnum.NORMAL.getValue()); | ||||||
|                         busAttendance.setMinuteCount(0); |                         busAttendance.setMinuteCount(0); | ||||||
|                     }updateById(busAttendance); |                     } | ||||||
|  |                     updateById(busAttendance); | ||||||
|                 } else { |                 } else { | ||||||
|                     BusAttendance attendance = new BusAttendance(); |                     BusAttendance attendance = new BusAttendance(); | ||||||
|                     // 下班打卡 |                     // 下班打卡 | ||||||
| @ -460,10 +462,9 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|                             log.error("异步发送系统消息失败,用户ID: {}, 消息: {}", userId, "打卡成功", e); |                             log.error("异步发送系统消息失败,用户ID: {}, 消息: {}", userId, "打卡成功", e); | ||||||
|                         } |                         } | ||||||
|                     }); |                     }); | ||||||
|                     boolean save = this.save(attendance); |                     //计算工资 | ||||||
|                     //插入工资 |                     attendance.setSalary(computeSalary(constructionUser, inAttendances)); | ||||||
|                     userSalaryDetailService.insertByAttendance(userId, attendance.getClockDate()); |                     return this.save(attendance); | ||||||
|                     return save; |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @ -503,6 +504,31 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|         return attendedUserIds; |         return attendedUserIds; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 计算工资 | ||||||
|  |      */ | ||||||
|  |     public BigDecimal computeSalary(SubConstructionUser constructionUser, List<BusAttendance> 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<BusAttendanceMapper, B | |||||||
|             .orderByAsc(BusAttendance::getClockDate)); |             .orderByAsc(BusAttendance::getClockDate)); | ||||||
|  |  | ||||||
|         // 处理正常出勤记录(去重日期) |         // 处理正常出勤记录(去重日期) | ||||||
|         List<AttendanceUserDataDetailVo> workList = attendanceList.stream() |  | ||||||
|  |         // 过滤有效考勤记录并按日期分组 | ||||||
|  |         Map<LocalDate, List<BusAttendanceVo>> dateAttendanceMap = attendanceList.stream() | ||||||
|             .filter(a -> validStatusList.contains(a.getClockStatus())) |             .filter(a -> validStatusList.contains(a.getClockStatus())) | ||||||
|             .map(this::convertToDetailVo) |             .collect(Collectors.groupingBy(BusAttendanceVo::getClockDate)); | ||||||
|             .distinct() // 去除重复日期 |         List<AttendanceUserDataDetailVo> workList = new ArrayList<>(); | ||||||
|             .collect(Collectors.toList()); |         for (Map.Entry<LocalDate, List<BusAttendanceVo>> entry : dateAttendanceMap.entrySet()) { | ||||||
|  |             LocalDate key = entry.getKey(); | ||||||
|  |             List<BusAttendanceVo> 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); |         vo.setWork(workList); | ||||||
|  |  | ||||||
|         // 处理迟到记录 |         // 处理迟到记录 | ||||||
| @ -1185,10 +1221,10 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|             List<SubConstructionUser> users = entry.getValue(); |             List<SubConstructionUser> users = entry.getValue(); | ||||||
|             SubConstructionUser constructionUser = users.getFirst(); |             SubConstructionUser constructionUser = users.getFirst(); | ||||||
|             String teamName = constructionUser.getTeamName(); |             String teamName = constructionUser.getTeamName(); | ||||||
|             if(teamId == 0){ |             if (teamId == 0) { | ||||||
|                 teamName = "无班组"; |                 teamName = "无班组"; | ||||||
|             } |             } | ||||||
|             if(StringUtils.isBlank(teamName)){ |             if (StringUtils.isBlank(teamName)) { | ||||||
|                 teamName = teamId.toString(); |                 teamName = teamId.toString(); | ||||||
|             } |             } | ||||||
|             System.out.println("name:" + teamName); |             System.out.println("name:" + teamName); | ||||||
| @ -1208,6 +1244,11 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|             sheet.setColumnWidth(3 + daysInMonth + 1, 10 * 256); // 是否离场 |             sheet.setColumnWidth(3 + daysInMonth + 1, 10 * 256); // 是否离场 | ||||||
|             sheet.setColumnWidth(3 + daysInMonth + 2, 15 * 256); // 签字 |             sheet.setColumnWidth(3 + daysInMonth + 2, 15 * 256); // 签字 | ||||||
|  |  | ||||||
|  |             // ==================== 创建样式 ==================== | ||||||
|  |             CellStyle borderStyle = createBorderStyle(workbook); | ||||||
|  | //            CellStyle numberStyle = createNumberStyle(workbook); | ||||||
|  |  | ||||||
|  |  | ||||||
|             // ==================== 表头部分 ==================== |             // ==================== 表头部分 ==================== | ||||||
|             Row titleRow = sheet.createRow(0); |             Row titleRow = sheet.createRow(0); | ||||||
|             Cell titleCell = titleRow.createCell(0); |             Cell titleCell = titleRow.createCell(0); | ||||||
| @ -1247,13 +1288,75 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|             // ==================== 数据表头 ==================== |             // ==================== 数据表头 ==================== | ||||||
|             Row headerRow = sheet.createRow(2); |             Row headerRow = sheet.createRow(2); | ||||||
|             writeHeaderRow(headerRow, daysInMonth); |             writeHeaderRow(headerRow, daysInMonth); | ||||||
|  |             // 设置表头边框 | ||||||
|  |             for (int i = 0; i < totalColumns; i++) { | ||||||
|  |                 Cell cell = headerRow.getCell(i); | ||||||
|  |                 if (cell != null) { | ||||||
|  |                     cell.setCellStyle(borderStyle); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|             // ==================== 数据行 ==================== |             // ==================== 数据行 ==================== | ||||||
|  |             CellStyle numberBorderStyle = createNumberBorderStyle(workbook); | ||||||
|  |  | ||||||
|             int rowIndex = 3; |             int rowIndex = 3; | ||||||
|             for (SubConstructionUser user : users) { |             for (SubConstructionUser user : users) { | ||||||
|                 Row row = sheet.createRow(rowIndex++); |                 Row row = sheet.createRow(rowIndex++); | ||||||
|                 writeDataRow(row, user, attendanceList, start, end, daysInMonth); |                 writeDataRow(row, user, attendanceList, start, end, daysInMonth,borderStyle,numberBorderStyle); | ||||||
|  |  | ||||||
|  |                 // 设置数据行边框 | ||||||
|  |                 for (int i = 0; i < totalColumns; i++) { | ||||||
|  |                     Cell cell = row.getCell(i); | ||||||
|  |                     if (cell != null) { | ||||||
|  |                         cell.setCellStyle(borderStyle); | ||||||
|                     } |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             // ==================== 设置月份天数列为数字格式 + 边框 ==================== | ||||||
|  |  | ||||||
|  |  | ||||||
|  |             // 表头 | ||||||
|  |             for (int i = 3; i < 3 + daysInMonth; i++) { | ||||||
|  |                 Cell headerCell = headerRow.getCell(i); | ||||||
|  |                 if (headerCell != null) { | ||||||
|  |                     headerCell.setCellStyle(numberBorderStyle); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // 数据行 | ||||||
|  |             for (int i = 3; i < 3 + daysInMonth; i++) { | ||||||
|  |                 for (int j = 3; j < rowIndex; j++) { | ||||||
|  |                     Row dataRow = sheet.getRow(j); | ||||||
|  |                     if (dataRow != null) { | ||||||
|  |                         Cell dataCell = dataRow.getCell(i); | ||||||
|  |                         if (dataCell != null) { | ||||||
|  |                             dataCell.setCellStyle(numberBorderStyle); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             // ==================== 设置合计列公式 ==================== | ||||||
|  | //            for (int i = 4; i < rowIndex; i++) { | ||||||
|  | //                Row dataRow = sheet.getRow(i); | ||||||
|  | //                if (dataRow != null) { | ||||||
|  | //                    Cell sumCell = dataRow.createCell(3 + daysInMonth); | ||||||
|  | // | ||||||
|  | //                    // 构建求和公式:从 D 列到第 (3 + daysInMonth) 列 | ||||||
|  | //                    String startCol = "D"; // 第 3 列 | ||||||
|  | //                    int endColIndex = 3 + daysInMonth; // 最后一列的索引(例如 33) | ||||||
|  | //                    String endCol = getColumnName(endColIndex); // 将列号转为列名(如 AF) | ||||||
|  | // | ||||||
|  | //                    // 拼接公式:=SUM(D4:AF4) | ||||||
|  | //                    StringBuilder formula = new StringBuilder(); | ||||||
|  | //                    formula.append("SUM("); | ||||||
|  | //                    formula.append(startCol).append(i).append(":"); | ||||||
|  | //                    formula.append(endCol).append(i); | ||||||
|  | //                    formula.append(")"); | ||||||
|  | // | ||||||
|  | //                    sumCell.setCellFormula(formula.toString()); | ||||||
|  | //                    sumCell.setCellStyle(borderStyle); | ||||||
|  | //                } | ||||||
|  | //            } | ||||||
|  |  | ||||||
|             // ==================== 表尾部分 ==================== |             // ==================== 表尾部分 ==================== | ||||||
|  |  | ||||||
| @ -1304,6 +1407,42 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private String getColumnName(int columnNum) { | ||||||
|  |         StringBuilder columnName = new StringBuilder(); | ||||||
|  |         while (columnNum > 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) { |     private CellStyle createProjectCellStyle(Workbook workbook) { | ||||||
|         CellStyle style = workbook.createCellStyle(); |         CellStyle style = workbook.createCellStyle(); | ||||||
|         Font font = workbook.createFont(); |         Font font = workbook.createFont(); | ||||||
| @ -1339,20 +1478,29 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|         row.createCell(2 + daysInMonth + 3).setCellValue("签字"); |         row.createCell(2 + daysInMonth + 3).setCellValue("签字"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void writeDataRow(Row row, SubConstructionUser user, List<BusAttendance> attendanceList, LocalDate start, LocalDate end, int daysInMonth) { |     private void writeDataRow(Row row, SubConstructionUser user, List<BusAttendance> attendanceList, LocalDate start, LocalDate end, int daysInMonth,CellStyle borderStyle,CellStyle numberBorderStyle) { | ||||||
|         int index = row.getRowNum(); |         int index = row.getRowNum()-2; | ||||||
|         row.createCell(0).setCellValue(index); |         row.createCell(0).setCellValue(index); | ||||||
|         row.createCell(1).setCellValue(user.getUserName()); |         row.createCell(1).setCellValue(user.getUserName()); | ||||||
|         row.createCell(2).setCellValue(idCardEncryptorUtil.decrypt(user.getSfzNumber())); |         row.createCell(2).setCellValue(idCardEncryptorUtil.decrypt(user.getSfzNumber())); | ||||||
|  |  | ||||||
|         for (int i = 1; i <= daysInMonth; i++) { |         for (int i = 1; i <= daysInMonth; i++) { | ||||||
|             LocalDate date = start.plusDays(i - 1); |             LocalDate date = start.plusDays(i - 1); | ||||||
|             String value = getAttendanceValue(user.getSysUserId(), date, attendanceList); |             Cell cell = row.createCell(2 + i); | ||||||
|             row.createCell(2 + i).setCellValue(value); |             Double value = getAttendanceValue(user.getSysUserId(), date, attendanceList); | ||||||
|  |             cell.setCellValue(value); | ||||||
|  |             // 设置数字格式样式 | ||||||
|  |             cell.setCellStyle(numberBorderStyle); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         double total = countAttendance(user.getSysUserId(), attendanceList, start, end); | //        double total = countAttendance(user.getSysUserId(), attendanceList, start, end); | ||||||
|         row.createCell(2 + daysInMonth + 1).setCellValue(total); |         // ==================== 在这里设置合计列公式 ==================== | ||||||
|  |         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()) ? "未离场" : "已离场"; |         String leaveStatus = "0".equals(user.getExitStatus()) ? "未离场" : "已离场"; | ||||||
|         row.createCell(2 + daysInMonth + 2).setCellValue(leaveStatus); |         row.createCell(2 + daysInMonth + 2).setCellValue(leaveStatus); | ||||||
| @ -1360,14 +1508,14 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|         row.createCell(2 + daysInMonth + 3).setCellValue(""); |         row.createCell(2 + daysInMonth + 3).setCellValue(""); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private String getAttendanceValue(Long userId, LocalDate date, List<BusAttendance> attendanceList) { |     private Double getAttendanceValue(Long userId, LocalDate date, List<BusAttendance> attendanceList) { | ||||||
|         List<BusAttendance> validRecords = attendanceList.stream() |         List<BusAttendance> validRecords = attendanceList.stream() | ||||||
|             .filter(a -> a.getUserId().equals(userId) && a.getClockDate().equals(date)) |             .filter(a -> a.getUserId().equals(userId) && a.getClockDate().equals(date)) | ||||||
|             .filter(a -> !a.getClockStatus().equals("4")) // 排除缺卡记录 |             .filter(a -> !a.getClockStatus().equals("4")) // 排除缺卡记录 | ||||||
|             .collect(Collectors.toList()); |             .collect(Collectors.toList()); | ||||||
|  |  | ||||||
|         if (validRecords.isEmpty()) { |         if (validRecords.isEmpty()) { | ||||||
|             return "0"; // 无有效打卡 |             return 0D; // 无有效打卡 | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // 检查是否有有效出勤状态(1,2,3,5) |         // 检查是否有有效出勤状态(1,2,3,5) | ||||||
| @ -1375,16 +1523,16 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|             .anyMatch(a -> Arrays.asList("1", "2", "3", "5").contains(a.getClockStatus())); |             .anyMatch(a -> Arrays.asList("1", "2", "3", "5").contains(a.getClockStatus())); | ||||||
|  |  | ||||||
|         if (!hasValidStatus) { |         if (!hasValidStatus) { | ||||||
|             return "0"; // 状态无效,如补卡、其他异常 |             return 0D; // 状态无效,如补卡、其他异常 | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // 判断是否为半勤(仅一次有效打卡) |         // 判断是否为半勤(仅一次有效打卡) | ||||||
|         if (validRecords.size() == 1) { |         if (validRecords.size() == 1) { | ||||||
|             return "0.5"; |             return 0.5D; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // 正常出勤(两次有效打卡) |         // 正常出勤(两次有效打卡) | ||||||
|         return "1"; |         return 1D; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private double countAttendance(Long userId, List<BusAttendance> attendanceList, LocalDate start, LocalDate end) { |     private double countAttendance(Long userId, List<BusAttendance> attendanceList, LocalDate start, LocalDate end) { | ||||||
| @ -1392,12 +1540,8 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B | |||||||
|         LocalDate current = start; |         LocalDate current = start; | ||||||
|  |  | ||||||
|         while (!current.isAfter(end)) { |         while (!current.isAfter(end)) { | ||||||
|             String value = getAttendanceValue(userId, current, attendanceList); |             Double value = getAttendanceValue(userId, current, attendanceList); | ||||||
|             if ("1".equals(value)) { |             total+=value; | ||||||
|                 total += 1.0; |  | ||||||
|             } else if ("0.5".equals(value)) { |  | ||||||
|                 total += 0.5; |  | ||||||
|             } |  | ||||||
|             current = current.plusDays(1); |             current = current.plusDays(1); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | |||||||
| @ -248,4 +248,11 @@ public class BusWorkWageServiceImpl extends ServiceImpl<BusWorkWageMapper, BusWo | |||||||
|         workWageVoPage.setRecords(workWageVoList); |         workWageVoPage.setRecords(workWageVoList); | ||||||
|         return workWageVoPage; |         return workWageVoPage; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public BusWorkWage getWorkWageByWorkType(String workType) { | ||||||
|  |  | ||||||
|  |         return  baseMapper.selectOne(new LambdaQueryWrapper<BusWorkWage>().eq(BusWorkWage::getWorkType, workType) | ||||||
|  |             .last("limit 1")); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 zt
					zt