BUG和工资

This commit is contained in:
zt
2025-09-22 21:09:36 +08:00
parent 5b6d3bf758
commit 9772f1a024
16 changed files with 823 additions and 158 deletions

View File

@ -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<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));
}
}

View File

@ -65,4 +65,13 @@ public class SubUserSalaryDetail extends BaseEntity {
*/
private String remark;
/**
* 工时
*/
private Double workHour;
/**
* 当日总工资
*/
private BigDecimal totalSalary;
}

View File

@ -28,4 +28,10 @@ public class SubConstructionUserSalaryDto {
*/
@NotBlank(message = "时间不能为空")
private String time;
/**
* 用户
*/
private Long userId;
}

View File

@ -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;
}

View File

@ -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<>();
}

View File

@ -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("行号%dExcel第%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;
}
}

View File

@ -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());
}
}
}

View File

@ -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<SubUserSalaryDetai
*/
void export(HttpServletResponse response, SubConstructionUserSalaryDto dto) throws IOException;
/**
* 导入
*/
void importData( MultipartFile file, String month);
/**
* 人员详情
*/
List<SubUserSalaryDetail> detailList( SubConstructionUserSalaryDto dto);
}

View File

@ -1127,7 +1127,14 @@ public class SubConstructionUserServiceImpl extends ServiceImpl<SubConstructionU
vo.setGender(wordsResult.getGender() != null ? wordsResult.getGender().getWords() : "");
vo.setImage(upload);
} 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.setIssuingDate(wordsResult.getIssuingDate() != null ? wordsResult.getIssuingDate().getWords() : "");
vo.setImage(upload);

View File

@ -3,11 +3,15 @@ package org.dromara.contractor.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
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.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
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.WorkbookFactory;
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.exception.ServiceException;
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.usersalarydetail.SubUserSalaryDetailVo;
import org.dromara.contractor.domain.vo.usersalaryperiod.SubConstructionUserSalaryVo;
import org.dromara.contractor.excel.DynamicSalaryData;
import org.dromara.contractor.mapper.SubUserSalaryDetailMapper;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.dromara.contractor.service.ISubUserSalaryDetailService;
import org.dromara.gps.domain.vo.ConstructionUser;
import org.dromara.project.domain.BusAttendance;
import org.dromara.project.domain.BusProject;
import org.dromara.project.domain.BusProjectTeam;
@ -51,6 +58,7 @@ import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.utils.CollectionUtils;
import java.io.ByteArrayInputStream;
@ -65,6 +73,7 @@ import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import org.dromara.contractor.excel.SalaryExcelReader;
/**
* 员工每日工资Service业务层处理
@ -263,76 +272,53 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet
@Override
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();
YearMonth parse = YearMonth.parse(time, DateTimeFormatter.ofPattern("yyyy-MM"));
LocalDate start = parse.atDay(1);
LocalDate end = parse.atEndOfMonth();
List<Long> userIds = rows.stream().map(SubConstructionUserVo::getSysUserId).toList();
//考勤数据
List<BusAttendance> attendanceList = attendanceService.lambdaQuery()
.eq(BusAttendance::getProjectId, dto.getProjectId())
.in(BusAttendance::getUserId, userIds)
.between(BusAttendance::getClockDate, start, end)
.list();
//工资数据
List<SubUserSalaryDetail> salaryDetailList = this.lambdaQuery()
.eq(SubUserSalaryDetail::getProjectId, dto.getProjectId())
.in(SubUserSalaryDetail::getUserId, userIds)
.between(SubUserSalaryDetail::getReportDate, start, end)
.list();
QueryWrapper<SubUserSalaryDetail> 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<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<>();
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<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);
}
return new TableDataInfo<>(vos,subConstructionUserVoTableDataInfo.getTotal());
return new TableDataInfo<>(vos,result.getTotal());
}
@Override
@ -342,27 +328,34 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet
if (project == null) {
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. 解析年月和查询薪资明细
YearMonth yearMonth = YearMonth.parse(dto.getTime(), DateTimeFormatter.ofPattern("yyyy-MM"));
LocalDate start = yearMonth.atDay(1);
LocalDate end = yearMonth.atEndOfMonth();
List<Long> userIds = userList.stream().map(SubConstructionUser::getSysUserId).toList();
List<SubUserSalaryDetail> salaryDetails = this.lambdaQuery()
.eq(SubUserSalaryDetail::getProjectId, dto.getProjectId())
.in(SubUserSalaryDetail::getUserId, userIds)
.between(SubUserSalaryDetail::getReportDate, start, end)
QueryWrapper<SubUserSalaryDetail> 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<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();
// 3. 设置响应头(下载文件配置)
@ -465,7 +458,7 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet
.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);
}
@ -474,7 +467,7 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet
WriteSheet noTeamSheet = EasyExcel.writerSheet("无班组").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);
}
}
@ -498,7 +491,7 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet
}
private List<SubConstructionUserSalaryVo> getTeamData(List<SubConstructionUser> rows,
List<SubUserSalaryDetail> salaryDetailList,Long teamId){
Map<Long, SubUserSalaryDetail> map,Long teamId){
List<SubConstructionUser> 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<SubUserSalaryDet
SubConstructionUserSalaryVo vo = new SubConstructionUserSalaryVo();
BeanUtil.copyProperties(row,vo);
vo.setOrder(i);
vo.setTotalSalary(salaryDetailList.stream().filter(detail -> 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<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);
}
}

View File

@ -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<String> 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<Long> manageUserIds = new HashSet<>();
List<BusAttendance> missList = new ArrayList<>();
for (BusAttendanceRule rule : list) {
LocalTime clockOutTime = rule.getClockOutTime();
@ -235,16 +243,22 @@ public class AttendanceJob {
List<BusUserProjectRelevancy> relevancyList = userProjectRelevancyService.list(Wrappers.lambdaQuery(BusUserProjectRelevancy.class)
.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::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();
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<BusAttendance> 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<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();
}
}

View File

@ -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;
}

View File

@ -30,4 +30,9 @@ public class AttendanceUserDataDetailVo {
* 迟到或早退的分钟
*/
private Integer minuteCount;
/**
* 出勤天数
*/
private Double workDay;
}

View File

@ -95,4 +95,10 @@ public interface IBusWorkWageService extends IService<BusWorkWage> {
* @return 工种薪水分页对象视图
*/
Page<BusWorkWageVo> getVoPage(Page<BusWorkWage> workWagePage);
/**
* 根据工种获取薪水设置
*/
BusWorkWage getWorkWageByWorkType(String workType);
}

View File

@ -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<BusAttendanceMapper, B
private final ISubUserSalaryDetailService userSalaryDetailService;
private final IBusWorkWageService workWageService;
@Resource
private IdCardEncryptorUtil idCardEncryptorUtil;
@ -149,7 +152,7 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
Long userId = req.getUserId();
String clockMonth = req.getClockMonth();
SubConstructionUser constructionUser = constructionUserService.getById(userId);
if ( constructionUser == null) {
if (constructionUser == null) {
throw new ServiceException("施工人员信息不存在", HttpStatus.NOT_FOUND);
}
// 解析月份
@ -372,45 +375,43 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
BusAttendanceCommuterEnum.CLOCKOUT.getValue().equals(attendance.getClockType())).toList();
if (clockTypeByTime == 1 && CollectionUtils.isEmpty(inAttendances)) {
BusAttendance attendance = new BusAttendance();
// 上班打卡
attendance.setClockType(BusAttendanceCommuterEnum.CLOCKIN.getValue());
//打卡时间
attendance.setRuleTime(busAttendanceRuleVo.getClockInTime());
// 判断是否为迟到
if (isLate(now, busAttendanceRuleVo)) {
attendance.setClockStatus(BusAttendanceClockStatusEnum.LATE.getValue());
attendance.setMinuteCount(getMinutesDifference(now, busAttendanceRuleVo.getClockInTime()));
} else {
attendance.setClockStatus(BusAttendanceClockStatusEnum.NORMAL.getValue());
BusAttendance attendance = new BusAttendance();
// 上班打卡
attendance.setClockType(BusAttendanceCommuterEnum.CLOCKIN.getValue());
//打卡时间
attendance.setRuleTime(busAttendanceRuleVo.getClockInTime());
// 判断是否为迟到
if (isLate(now, busAttendanceRuleVo)) {
attendance.setClockStatus(BusAttendanceClockStatusEnum.LATE.getValue());
attendance.setMinuteCount(getMinutesDifference(now, busAttendanceRuleVo.getClockInTime()));
} else {
attendance.setClockStatus(BusAttendanceClockStatusEnum.NORMAL.getValue());
}
// 填充信息
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.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<BusAttendanceMapper, B
} else {
busAttendance.setClockStatus(BusAttendanceClockStatusEnum.NORMAL.getValue());
busAttendance.setMinuteCount(0);
}updateById(busAttendance);
}
updateById(busAttendance);
} else {
BusAttendance attendance = new BusAttendance();
// 下班打卡
@ -460,10 +462,9 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
log.error("异步发送系统消息失败用户ID: {}, 消息: {}", userId, "打卡成功", e);
}
});
boolean save = this.save(attendance);
//插入工资
userSalaryDetailService.insertByAttendance(userId, attendance.getClockDate());
return save;
//计算工资
attendance.setSalary(computeSalary(constructionUser, inAttendances));
return this.save(attendance);
}
}
}
@ -503,6 +504,31 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
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));
// 处理正常出勤记录(去重日期)
List<AttendanceUserDataDetailVo> workList = attendanceList.stream()
// 过滤有效考勤记录并按日期分组
Map<LocalDate, List<BusAttendanceVo>> dateAttendanceMap = attendanceList.stream()
.filter(a -> validStatusList.contains(a.getClockStatus()))
.map(this::convertToDetailVo)
.distinct() // 去除重复日期
.collect(Collectors.toList());
.collect(Collectors.groupingBy(BusAttendanceVo::getClockDate));
List<AttendanceUserDataDetailVo> workList = new ArrayList<>();
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);
// 处理迟到记录
@ -1185,10 +1221,10 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
List<SubConstructionUser> 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<BusAttendanceMapper, B
sheet.setColumnWidth(3 + daysInMonth + 1, 10 * 256); // 是否离场
sheet.setColumnWidth(3 + daysInMonth + 2, 15 * 256); // 签字
// ==================== 创建样式 ====================
CellStyle borderStyle = createBorderStyle(workbook);
// CellStyle numberStyle = createNumberStyle(workbook);
// ==================== 表头部分 ====================
Row titleRow = sheet.createRow(0);
Cell titleCell = titleRow.createCell(0);
@ -1247,13 +1288,75 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
// ==================== 数据表头 ====================
Row headerRow = sheet.createRow(2);
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;
for (SubConstructionUser user : users) {
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) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
@ -1339,20 +1478,29 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
row.createCell(2 + daysInMonth + 3).setCellValue("签字");
}
private void writeDataRow(Row row, SubConstructionUser user, List<BusAttendance> attendanceList, LocalDate start, LocalDate end, int daysInMonth) {
int index = row.getRowNum();
private void writeDataRow(Row row, SubConstructionUser user, List<BusAttendance> 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<BusAttendanceMapper, B
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()
.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<BusAttendanceMapper, B
.anyMatch(a -> 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<BusAttendance> attendanceList, LocalDate start, LocalDate end) {
@ -1392,12 +1540,8 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
LocalDate current = start;
while (!current.isAfter(end)) {
String value = getAttendanceValue(userId, current, attendanceList);
if ("1".equals(value)) {
total += 1.0;
} else if ("0.5".equals(value)) {
total += 0.5;
}
Double value = getAttendanceValue(userId, current, attendanceList);
total+=value;
current = current.plusDays(1);
}

View File

@ -248,4 +248,11 @@ public class BusWorkWageServiceImpl extends ServiceImpl<BusWorkWageMapper, BusWo
workWageVoPage.setRecords(workWageVoList);
return workWageVoPage;
}
@Override
public BusWorkWage getWorkWageByWorkType(String workType) {
return baseMapper.selectOne(new LambdaQueryWrapper<BusWorkWage>().eq(BusWorkWage::getWorkType, workType)
.last("limit 1"));
}
}