This commit is contained in:
zt
2025-09-23 11:38:29 +08:00
parent 8af5fb52ec
commit 70553dff79
3 changed files with 286 additions and 199 deletions

View File

@ -1,9 +1,13 @@
package org.dromara.contractor.mapper; package org.dromara.contractor.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.dromara.contractor.domain.SubUserSalaryDetail; import org.dromara.contractor.domain.SubUserSalaryDetail;
import org.dromara.contractor.domain.vo.usersalarydetail.SubUserSalaryDetailVo; import org.dromara.contractor.domain.vo.usersalarydetail.SubUserSalaryDetailVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import java.util.List;
/** /**
* 员工每日工资Mapper接口 * 员工每日工资Mapper接口
* *
@ -12,4 +16,15 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
*/ */
public interface SubUserSalaryDetailMapper extends BaseMapperPlus<SubUserSalaryDetail, SubUserSalaryDetailVo> { public interface SubUserSalaryDetailMapper extends BaseMapperPlus<SubUserSalaryDetail, SubUserSalaryDetailVo> {
@Insert({
"<script>",
"INSERT INTO sub_user_salary_detail (project_id, team_id, user_id, user_name, report_date, work_hour, daily_salary, total_salary, create_by, update_by, remark) VALUES ",
"<foreach collection='list' item='item' separator=','>",
"(#{item.projectId}, #{item.teamId}, #{item.userId}, #{item.userName}, #{item.reportDate}, #{item.workHour}, #{item.dailySalary}, #{item.totalSalary}, #{userId}, #{userId}, #{item.remark})",
"</foreach>",
"</script>"
})
Boolean insertAll(@Param("list")List<SubUserSalaryDetail> list, @Param("userId") Long userId);
} }

View File

@ -18,6 +18,7 @@ import jakarta.annotation.Resource;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.io.IOUtils;
import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.Workbook;
@ -30,6 +31,7 @@ import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.utils.IdCardEncryptorUtil; import org.dromara.common.utils.IdCardEncryptorUtil;
import org.dromara.contractor.domain.SubConstructionUser; import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.domain.SubUserSalaryDetail; import org.dromara.contractor.domain.SubUserSalaryDetail;
@ -58,6 +60,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.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.utils.CollectionUtils; import software.amazon.awssdk.utils.CollectionUtils;
@ -72,6 +75,7 @@ import java.time.LocalDate;
import java.time.YearMonth; import java.time.YearMonth;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.dromara.contractor.excel.SalaryExcelReader; import org.dromara.contractor.excel.SalaryExcelReader;
@ -83,6 +87,7 @@ import org.dromara.contractor.excel.SalaryExcelReader;
*/ */
@RequiredArgsConstructor @RequiredArgsConstructor
@Service @Service
@Slf4j
public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDetailMapper, SubUserSalaryDetail> public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDetailMapper, SubUserSalaryDetail>
implements ISubUserSalaryDetailService { implements ISubUserSalaryDetailService {
@ -525,19 +530,21 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet
@Override @Override
@Transactional(rollbackFor = Exception.class)
public void importData(MultipartFile file, String month) { public void importData(MultipartFile file, String month) {
long startTime = System.currentTimeMillis();
// 1. 校验文件合法性 // 1. 校验文件合法性
if (file.isEmpty()) { if (file.isEmpty()) {
throw new IllegalArgumentException("上传的Excel文件不能为空"); throw new IllegalArgumentException("上传的Excel文件不能为空");
} }
// 校验文件格式仅允许xlsx/xls
String originalFilename = file.getOriginalFilename(); String originalFilename = file.getOriginalFilename();
if (originalFilename == null || !(originalFilename.endsWith(".xlsx"))) { if (originalFilename == null || !(originalFilename.endsWith(".xlsx"))) {
throw new IllegalArgumentException("仅支持上传Excel文件.xlsx 格式)!"); throw new IllegalArgumentException("仅支持上传Excel文件.xlsx 格式)!");
} }
List<DynamicSalaryData> dataList; List<DynamicSalaryData> dataList;
try { try {
// 2. 将 MultipartFile 转为 InputStream传给工具类解析
dataList = SalaryExcelReader.readAllAttendanceByStream(file.getInputStream(), month); dataList = SalaryExcelReader.readAllAttendanceByStream(file.getInputStream(), month);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Excel流解析失败" + e.getMessage(), e); throw new RuntimeException("Excel流解析失败" + e.getMessage(), e);
@ -546,42 +553,79 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet
throw new ServiceException("未读取到数据"); throw new ServiceException("未读取到数据");
} }
Map<String, Map<String, Double>> dataMap = dataList.stream().collect(Collectors.toMap(vo -> idCardEncryptorUtil.encrypt(vo.getIdCard()), DynamicSalaryData::getDailyAttendance)); // 2. 数据预处理 - 使用并行流提高处理速度
Set<String> cards = dataMap.keySet(); Map<String, Map<String, Double>> dataMap = dataList.parallelStream()
.collect(Collectors.toMap(
vo -> idCardEncryptorUtil.encrypt(vo.getIdCard()),
DynamicSalaryData::getDailyAttendance
));
Set<String> cards = dataMap.keySet();
month = dataList.getFirst().getMonth(); month = dataList.getFirst().getMonth();
YearMonth parse = YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyy-MM")); YearMonth parse = YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyy-MM"));
LocalDate start = parse.atDay(1); LocalDate start = parse.atDay(1);
LocalDate end = parse.atEndOfMonth(); LocalDate end = parse.atEndOfMonth();
//人员数据 // 3. 分批处理人员数据 - 避免一次性加载大量数据
List<SubConstructionUser> list = constructionUserService.list(Wrappers.<SubConstructionUser>lambdaQuery() List<SubConstructionUser> allUsers = new ArrayList<>();
.in(SubConstructionUser::getSfzNumber, cards)); int batchSize = 1000; // 批量查询大小
List<Long> userIds = list.stream().map(SubConstructionUser::getSysUserId).toList(); // 分批查询用户数据
//考勤数据 List<List<String>> cardBatches = new ArrayList<>();
List<BusAttendance> attendanceList = attendanceService List<String> cardList = new ArrayList<>(cards);
.list(Wrappers.<BusAttendance>lambdaQuery() for (int i = 0; i < cardList.size(); i += batchSize) {
.in(BusAttendance::getUserId, userIds) int endIndex = Math.min(i + batchSize, cardList.size());
cardBatches.add(cardList.subList(i, endIndex));
}
for (List<String> batchCards : cardBatches) {
List<SubConstructionUser> batchUsers = constructionUserService.list(
Wrappers.<SubConstructionUser>lambdaQuery()
.in(SubConstructionUser::getSfzNumber, batchCards)
);
allUsers.addAll(batchUsers);
}
if (CollUtil.isEmpty(allUsers)) {
throw new ServiceException("未找到匹配的人员信息");
}
// 4. 分批处理考勤数据 - 避免内存溢出
List<Long> userIds = allUsers.stream()
.map(SubConstructionUser::getSysUserId)
.distinct()
.collect(Collectors.toList());
Map<String, BigDecimal> attendanceSalaryMap = new ConcurrentHashMap<>();
// 分批查询考勤数据
for (int i = 0; i < userIds.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, userIds.size());
List<Long> batchUserIds = userIds.subList(i, endIndex);
List<BusAttendance> batchAttendanceList = attendanceService.list(
Wrappers.<BusAttendance>lambdaQuery()
.in(BusAttendance::getUserId, batchUserIds)
.between(BusAttendance::getClockDate, start, end) .between(BusAttendance::getClockDate, start, end)
); );
// 将 attendanceList 转换为 Map<String, BigDecimal> 格式
Map<String, BigDecimal> attendanceSalaryMap = new HashMap<>();
// 按 userId+日期 分组只保留每组的第一条记录的salary // 并行处理考勤数据
attendanceList.stream() batchAttendanceList.parallelStream()
.collect(Collectors.groupingBy( .collect(Collectors.groupingBy(
attendance -> attendance.getUserId() + "_" + attendance.getClockDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), attendance -> attendance.getUserId() + "_" + attendance.getClockDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
Collectors.toList() Collectors.toList()
)) ))
.forEach((key, dateList) -> attendanceSalaryMap.put(key, dateList.getFirst().getSalary())); .forEach((key, dateList) -> attendanceSalaryMap.put(key, dateList.getFirst().getSalary()));
}
// 5. 批量构建数据 - 使用并行处理
List<SubUserSalaryDetail> allAddList = Collections.synchronizedList(new ArrayList<>());
List<SubUserSalaryDetail> addList = new ArrayList<>(); // 并行处理用户数据构建
for(SubConstructionUser constructionUser : list){ allUsers.parallelStream().forEach(constructionUser -> {
Map<String, Double> stringIntegerMap = dataMap.get(constructionUser.getSfzNumber()); Map<String, Double> userAttendanceMap = dataMap.get(constructionUser.getSfzNumber());
if(stringIntegerMap != null){ if(userAttendanceMap != null){
for(Map.Entry<String, Double> entry : stringIntegerMap.entrySet()){ userAttendanceMap.entrySet().parallelStream().forEach(entry -> {
String key = entry.getKey(); String key = entry.getKey();
Double value = entry.getValue(); Double value = entry.getValue();
@ -600,16 +644,35 @@ public class SubUserSalaryDetailServiceImpl extends ServiceImpl<SubUserSalaryDet
} }
subUserSalaryDetail.setDailySalary(bigDecimal); subUserSalaryDetail.setDailySalary(bigDecimal);
subUserSalaryDetail.setTotalSalary(bigDecimal.multiply(new BigDecimal(value.toString()))); subUserSalaryDetail.setTotalSalary(bigDecimal.multiply(new BigDecimal(value.toString())));
addList.add(subUserSalaryDetail);
allAddList.add(subUserSalaryDetail);
});
}
});
// 6. 批量删除和插入 - 分批处理避免数据库压力
if (CollUtil.isNotEmpty(allAddList)) {
// 批量删除现有数据
for (int i = 0; i < userIds.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, userIds.size());
List<Long> deleteUserIds = userIds.subList(i, endIndex);
baseMapper.delete(Wrappers.<SubUserSalaryDetail>lambdaQuery()
.in(SubUserSalaryDetail::getUserId, deleteUserIds)
.between(SubUserSalaryDetail::getReportDate, start, end)
);
}
// 批量插入新数据 - 使用自定义批量插入方法
for (int i = 0; i < allAddList.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, allAddList.size());
List<SubUserSalaryDetail> insertBatch = allAddList.subList(i, endIndex);
baseMapper.insertAll(insertBatch,LoginHelper.getUserId());
} }
} }
} long endTime = System.currentTimeMillis();
baseMapper.delete(Wrappers.<SubUserSalaryDetail>lambdaQuery() log.info("工资数据导入完成,耗时: {} ms处理数据: {} 条", (endTime - startTime), allAddList.size());
.in(SubUserSalaryDetail::getUserId, userIds)
.between(SubUserSalaryDetail::getReportDate, start, end)
);
baseMapper.insertBatch(addList);
} }

View File

@ -17,6 +17,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.dromara.common.core.constant.DateConstant; import org.dromara.common.core.constant.DateConstant;
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;
@ -1041,7 +1042,7 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
AttendanceUserDataDetailVo detailVo = new AttendanceUserDataDetailVo(); AttendanceUserDataDetailVo detailVo = new AttendanceUserDataDetailVo();
detailVo.setClockDate(key); detailVo.setClockDate(key);
detailVo.setWeek(key.getDayOfWeek().getValue()); detailVo.setWeek(key.getDayOfWeek().getValue());
detailVo.setWorkDay(value.size()*0.5); detailVo.setWorkDay(value.size() * 0.5);
workList.add(detailVo); workList.add(detailVo);
} }
vo.setWork(workList); vo.setWork(workList);
@ -1178,15 +1179,25 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
@Override @Override
public void getExportList(AttendanceExportDto dto, HttpServletResponse response) { public void getExportList(AttendanceExportDto dto, HttpServletResponse response) {
try (OutputStream outputStream = response.getOutputStream()) {
BusProject project = projectService.getById(dto.getProjectId()); BusProject project = projectService.getById(dto.getProjectId());
if (project == null) { if (project == null) {
throw new ServiceException("项目不存在"); throw new ServiceException("项目不存在");
} }
String clockDate = dto.getClockDate();
YearMonth yearMonth = YearMonth.parse(clockDate, DateTimeFormatter.ofPattern("yyyy-MM"));
LocalDate start = yearMonth.atDay(1);
LocalDate end = yearMonth.atEndOfMonth();
LocalDateTime startTime = LocalDateTime.of(yearMonth.atDay(1), LocalTime.MIN); // 00:00:00
LocalDateTime endTime = LocalDateTime.of(yearMonth.atEndOfMonth(), LocalTime.MAX);
List<SubConstructionUser> list = constructionUserService.list(Wrappers.lambdaQuery(SubConstructionUser.class) List<SubConstructionUser> list = constructionUserService.list(Wrappers.lambdaQuery(SubConstructionUser.class)
.eq(SubConstructionUser::getProjectId, dto.getProjectId()) .eq(SubConstructionUser::getProjectId, dto.getProjectId())
.isNotNull(SubConstructionUser::getEntryDate)
.and(wrapper -> wrapper.between(SubConstructionUser::getLeaveDate, startTime, endTime).or()
.isNull(SubConstructionUser::getLeaveDate))
.eq(dto.getTeamId() != null, SubConstructionUser::getTeamId, dto.getTeamId()) .eq(dto.getTeamId() != null, SubConstructionUser::getTeamId, dto.getTeamId())
.eq(StrUtil.isNotBlank(dto.getTypeOfWork()), SubConstructionUser::getTypeOfWork, dto.getTypeOfWork()) .eq(StrUtil.isNotBlank(dto.getTypeOfWork()), SubConstructionUser::getTypeOfWork, dto.getTypeOfWork())
.like(StringUtils.isNotBlank(dto.getUserName()), SubConstructionUser::getUserName, dto.getUserName()) .like(StringUtils.isNotBlank(dto.getUserName()), SubConstructionUser::getUserName, dto.getUserName())
@ -1197,11 +1208,6 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
} }
List<Long> userIds = list.stream().map(SubConstructionUser::getSysUserId).toList(); List<Long> userIds = list.stream().map(SubConstructionUser::getSysUserId).toList();
String clockDate = dto.getClockDate();
YearMonth yearMonth = YearMonth.parse(clockDate, DateTimeFormatter.ofPattern("yyyy-MM"));
LocalDate start = yearMonth.atDay(1);
LocalDate end = yearMonth.atEndOfMonth();
int daysInMonth = yearMonth.lengthOfMonth(); // 动态获取天数 int daysInMonth = yearMonth.lengthOfMonth(); // 动态获取天数
@ -1213,7 +1219,8 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
Map<Long, List<SubConstructionUser>> teamUserMap = list.stream() Map<Long, List<SubConstructionUser>> teamUserMap = list.stream()
.collect(Collectors.groupingBy(user -> .collect(Collectors.groupingBy(user ->
user.getTeamId() != null ? user.getTeamId() : 0L)); user.getTeamId() != null ? user.getTeamId() : 0L));
Workbook workbook = new HSSFWorkbook();
Workbook workbook = new XSSFWorkbook();
for (Map.Entry<Long, List<SubConstructionUser>> entry : teamUserMap.entrySet()) { for (Map.Entry<Long, List<SubConstructionUser>> entry : teamUserMap.entrySet()) {
Long teamId = entry.getKey(); Long teamId = entry.getKey();
@ -1302,7 +1309,7 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
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,borderStyle,numberBorderStyle); writeDataRow(row, user, attendanceList, start, end, daysInMonth, borderStyle, numberBorderStyle);
// 设置数据行边框 // 设置数据行边框
for (int i = 0; i < totalColumns; i++) { for (int i = 0; i < totalColumns; i++) {
@ -1398,7 +1405,7 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
} }
try (OutputStream outputStream = response.getOutputStream()) {
workbook.write(outputStream); workbook.write(outputStream);
workbook.close(); workbook.close();
} catch (IOException e) { } catch (IOException e) {
@ -1417,6 +1424,7 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
} }
return columnName.toString(); return columnName.toString();
} }
private CellStyle createBorderStyle(Workbook workbook) { private CellStyle createBorderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle(); CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN); style.setBorderTop(BorderStyle.THIN);
@ -1425,10 +1433,11 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
style.setBorderRight(BorderStyle.THIN); style.setBorderRight(BorderStyle.THIN);
return style; return style;
} }
private CellStyle createNumberBorderStyle(Workbook workbook) { private CellStyle createNumberBorderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle(); CellStyle style = workbook.createCellStyle();
DataFormat format = workbook.createDataFormat(); DataFormat format = workbook.createDataFormat();
style.setDataFormat(format.getFormat("0")); // 强制显示 0 style.setDataFormat(format.getFormat("0.0")); // 显示一位小数
style.setBorderTop(BorderStyle.THIN); style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN); style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN); style.setBorderLeft(BorderStyle.THIN);
@ -1478,8 +1487,8 @@ 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,CellStyle borderStyle,CellStyle numberBorderStyle) { 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; 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()));
@ -1502,7 +1511,7 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
sumCell.setCellFormula(formula); sumCell.setCellFormula(formula);
sumCell.setCellStyle(borderStyle); sumCell.setCellStyle(borderStyle);
String leaveStatus = "0".equals(user.getExitStatus()) ? "离场" : "离场"; String leaveStatus = user.getLeaveDate() != null ? "离场" : "离场";
row.createCell(2 + daysInMonth + 2).setCellValue(leaveStatus); row.createCell(2 + daysInMonth + 2).setCellValue(leaveStatus);
row.createCell(2 + daysInMonth + 3).setCellValue(""); row.createCell(2 + daysInMonth + 3).setCellValue("");
@ -1541,7 +1550,7 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
while (!current.isAfter(end)) { while (!current.isAfter(end)) {
Double value = getAttendanceValue(userId, current, attendanceList); Double value = getAttendanceValue(userId, current, attendanceList);
total+=value; total += value;
current = current.plusDays(1); current = current.plusDays(1);
} }