huminmin
3 天以前 70a0b21595941890a9d26cfde01ff17e3427214a
src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java
@@ -2,27 +2,50 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.domain.SysUserDept;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.mapper.SysUserDeptMapper;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.production.mapper.ProductionAccountMapper;
import com.ruoyi.production.pojo.ProductionAccount;
import com.ruoyi.technology.mapper.TechnologyOperationMapper;
import com.ruoyi.technology.pojo.TechnologyOperation;
import com.ruoyi.staff.excel.StaffSalaryExportData;
import jakarta.servlet.http.HttpServletResponse;
import com.ruoyi.staff.dto.CalculateSalaryDto;
import com.ruoyi.staff.dto.StaffSalaryCalculateDto;
import com.ruoyi.staff.mapper.StaffLeaveMapper;
import com.ruoyi.staff.mapper.StaffSalaryDetailMapper;
import com.ruoyi.staff.pojo.SchemeApplicableStaff;
import com.ruoyi.staff.mapper.StaffSalaryMainMapper;
import com.ruoyi.staff.pojo.StaffLeave;
import com.ruoyi.staff.pojo.StaffSalaryDetail;
import com.ruoyi.staff.pojo.StaffSalaryMain;
import com.ruoyi.staff.mapper.StaffSalaryMainMapper;
import com.ruoyi.staff.service.StaffSalaryDetailService;
import com.ruoyi.staff.service.StaffSalaryMainService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.stream.Collectors;
import java.time.LocalDate;
import java.time.YearMonth;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.IOException;
/**
 * <p>
@@ -33,25 +56,18 @@
 * @since 2026-03-06 01:22:05
 */
@Service
@RequiredArgsConstructor
public class StaffSalaryMainServiceImpl extends ServiceImpl<StaffSalaryMainMapper, StaffSalaryMain> implements StaffSalaryMainService {
    @Autowired
    private StaffSalaryMainMapper staffSalaryMainMapper;
    @Autowired
    private StaffSalaryDetailService staffSalaryDetailService;
    @Autowired
    private StaffSalaryDetailMapper staffSalaryDetailMapper;
    @Autowired
    private SchemeApplicableStaffServiceImpl schemeApplicableStaffService;
    @Autowired
    private SysUserDeptMapper sysUserDeptMapper;
    @Autowired
    private SysUserMapper sysUserMapper;
    private final StaffSalaryMainMapper staffSalaryMainMapper;
    private final StaffSalaryDetailService staffSalaryDetailService;
    private final StaffSalaryDetailMapper staffSalaryDetailMapper;
    private final SchemeApplicableStaffServiceImpl schemeApplicableStaffService;
    private final SysUserDeptMapper sysUserDeptMapper;
    private final StaffLeaveMapper staffLeaveMapper;
    private final SysDeptMapper sysDeptMapper;
    private final ProductionAccountMapper productionAccountMapper;
    private final TechnologyOperationMapper technologyOperationMapper;
    @Override
    public AjaxResult listPage(Page page, StaffSalaryMain staffSalaryMain) {
@@ -67,6 +83,7 @@
                staffSalaryMainLambdaQueryWrapper.eq(StaffSalaryMain::getStatus, staffSalaryMain.getStatus());
            }
        }
        staffSalaryMainLambdaQueryWrapper.orderByDesc(StaffSalaryMain::getId);
        Page<StaffSalaryMain> page1 = staffSalaryMainMapper.selectPage(page, staffSalaryMainLambdaQueryWrapper);
        page1.getRecords().forEach(main -> {
            List<StaffSalaryDetail> staffSalaryDetailList = staffSalaryDetailMapper.selectList(new LambdaQueryWrapper<StaffSalaryDetail>().eq(StaffSalaryDetail::getMainId, main.getId()));
@@ -77,6 +94,26 @@
    @Override
    public AjaxResult add(StaffSalaryMain staffSalaryMain) {
        if (staffSalaryMain == null || StringUtils.isEmpty(staffSalaryMain.getSalaryMonth())) {
            return AjaxResult.error("参数错误");
        }
        Integer status = staffSalaryMain.getStatus();
        if (status != null && status == 5) {
            String salaryMonth = staffSalaryMain.getSalaryMonth();
            String deptIds = staffSalaryMain.getDeptIds();
            List<StaffSalaryMain> issuedList = staffSalaryMainMapper.selectList(new LambdaQueryWrapper<StaffSalaryMain>()
                    .eq(StaffSalaryMain::getSalaryMonth, salaryMonth)
                    .eq(StaffSalaryMain::getStatus, 5));
            for (StaffSalaryMain issued : issuedList) {
                if (hasOverlappingDept(deptIds, issued.getDeptIds())) {
                    return AjaxResult.error("同一月份同一部门工资不可以发放多次");
                }
            }
        }
        staffSalaryMainMapper.insert(staffSalaryMain);
        staffSalaryMain.getStaffSalaryDetailList().forEach(detail -> {
            detail.setMainId(staffSalaryMain.getId());
@@ -94,16 +131,42 @@
        if(staffSalaryMain1 == null){
            return AjaxResult.error("参数错误");
        }
        // 待审核不可编辑
        if(staffSalaryMain1.getStatus() > 2){
            return AjaxResult.error("待审核不可编辑");
        Integer newStatus = staffSalaryMain.getStatus();
        if (newStatus != null && newStatus == 5) {
            String salaryMonth = staffSalaryMain.getSalaryMonth();
            if (StringUtils.isEmpty(salaryMonth)) {
                salaryMonth = staffSalaryMain1.getSalaryMonth();
            }
            String deptIds = staffSalaryMain.getDeptIds();
            if (StringUtils.isEmpty(deptIds)) {
                deptIds = staffSalaryMain1.getDeptIds();
            }
            List<StaffSalaryMain> issuedList = staffSalaryMainMapper.selectList(new LambdaQueryWrapper<StaffSalaryMain>()
                    .eq(StaffSalaryMain::getSalaryMonth, salaryMonth)
                    .eq(StaffSalaryMain::getStatus, 5)
                    .ne(StaffSalaryMain::getId, staffSalaryMain.getId()));
            for (StaffSalaryMain issued : issuedList) {
                if (hasOverlappingDept(deptIds, issued.getDeptIds())) {
                    return AjaxResult.error("同一月份同一部门工资不可以发放多次");
                }
            }
        }
        // 待审核不可编辑
//        if(staffSalaryMain1.getStatus() > 3){
//            return AjaxResult.error("待审核不可编辑");
//        }
        staffSalaryMainMapper.updateById(staffSalaryMain);
        staffSalaryDetailMapper.delete(new LambdaQueryWrapper<StaffSalaryDetail>().eq(StaffSalaryDetail::getMainId, staffSalaryMain.getId()));
        staffSalaryMain.getStaffSalaryDetailList().forEach(detail -> {
            detail.setMainId(staffSalaryMain.getId());
        });
        staffSalaryDetailService.saveBatch(staffSalaryMain.getStaffSalaryDetailList());
        if(org.apache.commons.collections4.CollectionUtils.isNotEmpty(staffSalaryMain.getStaffSalaryDetailList())){
            staffSalaryDetailMapper.delete(new LambdaQueryWrapper<StaffSalaryDetail>().eq(StaffSalaryDetail::getMainId, staffSalaryMain.getId()));
            staffSalaryMain.getStaffSalaryDetailList().forEach(detail -> {
                detail.setMainId(staffSalaryMain.getId());
            });
            staffSalaryDetailService.saveBatch(staffSalaryMain.getStaffSalaryDetailList());
        }
        return AjaxResult.success("修改成功");
    }
@@ -118,18 +181,281 @@
    }
    @Override
    public AjaxResult calculateSalary(List<Long> ids) {
        if(CollectionUtils.isEmpty(ids)){
    public AjaxResult calculateSalary(CalculateSalaryDto calculateSalaryDto) {
        if(CollectionUtils.isEmpty(calculateSalaryDto.getIds())){
            return AjaxResult.error("参数错误");
        }
        List<Map<String, Object>> longs = setSchemeApplicableStaffUserInfo(ids); // 通过部门ids获取用户信息
        java.math.BigDecimal deductAmount = calculateSalaryDto.getDeductAmount() == null
                ? java.math.BigDecimal.ZERO
                : calculateSalaryDto.getDeductAmount();
        List<Map<String, Object>> longs = setSchemeApplicableStaffUserInfo(calculateSalaryDto.getIds()); // 通过部门ids获取用户信息
        if(CollectionUtils.isEmpty(longs)){
            return AjaxResult.error("无员工");
        }
        List<Map<String, Object>> mapList = new ArrayList<>();
        for (Map<String, Object> id : longs) {
            schemeApplicableStaffService.calculateByEmployeeId((Integer) id.get("id"),id);
            Long staffOnJobId = id.get("id") == null ? null : ((Number) id.get("id")).longValue();
            // 判断员工是否为当月离职
            if((Integer) id.get("staffState") == 0){
                StaffLeave id1 = staffLeaveMapper.selectOne(new LambdaQueryWrapper<StaffLeave>()
                        .eq(StaffLeave::getStaffOnJobId, staffOnJobId)
                        .like(StaffLeave::getLeaveDate, calculateSalaryDto.getDate()));
                if(id1 == null){
                    continue;
                }
            }
            id.put("otherDeduct", deductAmount);
            Long sysUserId = schemeApplicableStaffService.getUidByStaffId(staffOnJobId);
            BigDecimal monthTimeTotal = resolveMonthTimeTotal(sysUserId, calculateSalaryDto.getDate());
            id.put("monthTimeTotal", monthTimeTotal);
            id.put("convertedWorkHours", monthTimeTotal);
            schemeApplicableStaffService.calculateByEmployeeId(staffOnJobId == null ? null : staffOnJobId.intValue(), id, calculateSalaryDto.getDate());
            mapList.add(id);
        }
        return AjaxResult.success(longs);
        return AjaxResult.success(mapList);
    }
    @Override
    public AjaxResult calculateSalaryByEmployee(StaffSalaryCalculateDto staffSalaryCalculateDto) {
        if (staffSalaryCalculateDto == null
                || staffSalaryCalculateDto.getStaffOnJobId() == null
                || StringUtils.isEmpty(staffSalaryCalculateDto.getSalaryMonth())) {
            return AjaxResult.error("参数错误");
        }
        Map<String, Object> map = new HashMap<>();
        map.put("id", staffSalaryCalculateDto.getStaffOnJobId());
        map.put("staffOnJobId", staffSalaryCalculateDto.getStaffOnJobId());
        map.put("salaryMonth", staffSalaryCalculateDto.getSalaryMonth());
        map.put("staffName", staffSalaryCalculateDto.getStaffName());
        map.put("postName", staffSalaryCalculateDto.getPostName());
        map.put("deptName", staffSalaryCalculateDto.getDeptName());
        if (staffSalaryCalculateDto.getBasicSalary() != null) {
            map.put("basicSalary", staffSalaryCalculateDto.getBasicSalary());
        }
        map.put("attendanceBonus", staffSalaryCalculateDto.getAttendanceBonus());
        map.put("pieceSalary", staffSalaryCalculateDto.getPieceSalary());
        map.put("hourlySalary", staffSalaryCalculateDto.getHourlySalary());
        map.put("monthWorkDays", staffSalaryCalculateDto.getMonthWorkDays());
        map.put("standardHours", staffSalaryCalculateDto.getStandardHours());
        map.put("monthTimeTotal", staffSalaryCalculateDto.getMonthTimeTotal());
        map.put("conversionRatio", staffSalaryCalculateDto.getConversionRatio());
        map.put("convertedWorkHours", staffSalaryCalculateDto.getConvertedWorkHours());
        map.put("actualOutput", staffSalaryCalculateDto.getActualOutput());
        map.put("standardQuantity", staffSalaryCalculateDto.getStandardQuantity());
        map.put("conversionFactor", staffSalaryCalculateDto.getConversionFactor());
        map.put("otherIncome", staffSalaryCalculateDto.getOtherIncome());
        map.put("socialPersonal", staffSalaryCalculateDto.getSocialPersonal());
        map.put("fundPersonal", staffSalaryCalculateDto.getFundPersonal());
        map.put("otherDeduct", staffSalaryCalculateDto.getOtherDeduct());
        map.put("socialSupplementAmount", staffSalaryCalculateDto.getSocialSupplementAmount());
        map.put("salaryTax", staffSalaryCalculateDto.getSalaryTax());
        map.put("grossSalary", staffSalaryCalculateDto.getGrossSalary());
        map.put("deductSalary", staffSalaryCalculateDto.getDeductSalary());
        map.put("netSalary", staffSalaryCalculateDto.getNetSalary());
        map.put("remark", staffSalaryCalculateDto.getRemark());
        schemeApplicableStaffService.calculateByEmployeeId(
                staffSalaryCalculateDto.getStaffOnJobId().intValue(),
                map,
                staffSalaryCalculateDto.getSalaryMonth()
        );
        return AjaxResult.success(map);
    }
    @Override
    public void exportSalaryDetail(HttpServletResponse response, String salaryMonth) {
        LambdaQueryWrapper<StaffSalaryMain> mainWrapper = new LambdaQueryWrapper<>();
        if (StringUtils.isNotEmpty(salaryMonth)) {
            mainWrapper.eq(StaffSalaryMain::getSalaryMonth, salaryMonth);
        }
        List<StaffSalaryMain> mainList = staffSalaryMainMapper.selectList(mainWrapper);
        List<StaffSalaryExportData> exportDataList = new ArrayList<>();
        for (StaffSalaryMain main : mainList) {
            List<StaffSalaryDetail> detailList = staffSalaryDetailMapper.selectList(
                    new LambdaQueryWrapper<StaffSalaryDetail>().eq(StaffSalaryDetail::getMainId, main.getId())
            );
            for (StaffSalaryDetail detail : detailList) {
                StaffSalaryExportData exportData = new StaffSalaryExportData();
                exportData.setSalaryMonth(main.getSalaryMonth());
                exportData.setSalaryTitle(main.getSalaryTitle());
                exportData.setStaffName(detail.getStaffName());
                exportData.setDeptName(detail.getDeptName());
                exportData.setBasicSalary(detail.getBasicSalary());
                exportData.setAttendanceBonus(detail.getAttendanceBonus());
                exportData.setPieceSalary(detail.getPieceSalary());
                exportData.setHourlySalary(detail.getHourlySalary());
                exportData.setMonthWorkDays(detail.getMonthWorkDays());
                exportData.setStandardHours(detail.getStandardHours());
                exportData.setMonthTimeTotal(detail.getMonthTimeTotal());
                exportData.setConversionRatio(detail.getConversionRatio());
                exportData.setConvertedWorkHours(detail.getConvertedWorkHours());
                exportData.setOtherIncome(detail.getOtherIncome());
                exportData.setSocialPersonal(detail.getSocialPersonal());
                exportData.setFundPersonal(detail.getFundPersonal());
                exportData.setOtherDeduct(detail.getOtherDeduct());
                exportData.setSocialSupplementAmount(detail.getSocialSupplementAmount());
                exportData.setSalaryTax(detail.getSalaryTax());
                exportData.setGrossSalary(detail.getGrossSalary());
                exportData.setDeductSalary(detail.getDeductSalary());
                exportData.setNetSalary(detail.getNetSalary());
                exportData.setRemark(detail.getRemark());
                exportDataList.add(exportData);
            }
        }
        ExcelUtil<StaffSalaryExportData> util = new ExcelUtil<>(StaffSalaryExportData.class);
        util.exportExcel(response, exportDataList, "员工薪资明细");
    }
    /**
     * 汇总员工当月月时间合计。
     * 计时工序直接取工时,计件工序按产量折算后再叠加补修理工时。
     */
    private java.math.BigDecimal resolveMonthTimeTotal(Long sysUserId, String date) {
        if (sysUserId == null || sysUserId <= 0 || StringUtils.isEmpty(date)) {
            return java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP);
        }
        java.math.BigDecimal total = java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP);
        java.util.List<java.util.Map<String, Object>> items = productionAccountMapper.selectMonthSalaryCalcItems(sysUserId, date);
        for (java.util.Map<String, Object> item : items) {
            Integer operationType = item.get("operationType") == null ? null : ((Number) item.get("operationType")).intValue();
            java.math.BigDecimal finishedNum = toDecimal(item.get("finishedNum"));
            java.math.BigDecimal workHour = toDecimal(item.get("workHour"));
            java.math.BigDecimal repairWorkHour = toDecimal(item.get("repairWorkHour"));
            java.math.BigDecimal standardQuantity = toDecimal(item.get("standardQuantity"));
            java.math.BigDecimal conversionFactor = toDecimal(item.get("conversionFactor"));
            java.math.BigDecimal rowTotal;
            if (Integer.valueOf(0).equals(operationType)) {
                rowTotal = workHour.add(repairWorkHour);
            } else {
                if (standardQuantity.compareTo(java.math.BigDecimal.ZERO) <= 0) {
                    rowTotal = java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP);
                } else {
                    rowTotal = finishedNum
                            .divide(standardQuantity, 8, java.math.RoundingMode.HALF_UP)
                            .multiply(new java.math.BigDecimal("8"))
                            .multiply(conversionFactor)
                            .add(repairWorkHour)
                            .setScale(2, java.math.RoundingMode.HALF_UP);
                }
            }
            total = total.add(rowTotal).setScale(2, java.math.RoundingMode.HALF_UP);
        }
        return total;
    }
    private java.math.BigDecimal toDecimal(Object value) {
        if (value == null) {
            return java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP);
        }
        if (value instanceof java.math.BigDecimal) {
            return ((java.math.BigDecimal) value).setScale(2, java.math.RoundingMode.HALF_UP);
        }
        if (value instanceof Number) {
            return new java.math.BigDecimal(String.valueOf(value)).setScale(2, java.math.RoundingMode.HALF_UP);
        }
        String text = String.valueOf(value).trim();
        if (text.isEmpty()) {
            return java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP);
        }
        return new java.math.BigDecimal(text).setScale(2, java.math.RoundingMode.HALF_UP);
    }
    @Override
    public void exportWorkHours(HttpServletResponse response, String salaryMonth) {
        List<ProductionAccount> accountList = queryProductionAccounts(salaryMonth);
        List<String> operationNames = accountList.stream()
                .map(ProductionAccount::getTechnologyOperationName)
                .filter(StringUtils::isNotEmpty)
                .distinct()
                .sorted()
                .collect(Collectors.toList());
        Workbook workbook = new XSSFWorkbook();
        CellStyle titleStyle = createStyle(workbook, true, HorizontalAlignment.CENTER, 14, false);
        CellStyle headerStyle = createStyle(workbook, false, HorizontalAlignment.CENTER, 11, false);
        CellStyle labelStyle = createStyle(workbook, false, HorizontalAlignment.CENTER, 10, false);
        CellStyle numStyle = createStyle(workbook, false, HorizontalAlignment.CENTER, 10, false);
        DataFormat dataFormat = workbook.createDataFormat();
        numStyle.setDataFormat(dataFormat.getFormat("0.00"));
        if (CollectionUtils.isEmpty(operationNames)) {
            operationNames = new ArrayList<>();
        }
        int sheetIndex = 1;
        for (String operationName : operationNames) {
            TechnologyOperation operation = technologyOperationMapper.selectOne(
                    new LambdaQueryWrapper<TechnologyOperation>().eq(TechnologyOperation::getName, operationName).last("limit 1"));
            String sheetName = operationName;
            if (sheetName.length() > 31) {
                sheetName = sheetName.substring(0, 31);
            }
            Sheet sheet = workbook.createSheet(sheetName);
            int rowIdx = 0;
            Row titleRow = sheet.createRow(rowIdx++);
            titleRow.setHeightInPoints(52.5f);
            titleRow.createCell(0).setCellValue(operationName + "工时统计");
            titleRow.getCell(0).setCellStyle(titleStyle);
            sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 3));
            Row workDayRow = sheet.createRow(rowIdx++);
            workDayRow.createCell(0).setCellValue("工作日");
            workDayRow.getCell(0).setCellStyle(headerStyle);
            Integer monthWorkDays = calculateWorkDays(salaryMonth);
            createNumberCell(workDayRow, 2, monthWorkDays == null ? null : new java.math.BigDecimal(monthWorkDays), headerStyle);
            workDayRow.createCell(3).setCellValue("天");
            workDayRow.getCell(3).setCellStyle(headerStyle);
            createNumberCell(workDayRow, 4, monthWorkDays == null ? null : new java.math.BigDecimal(monthWorkDays * 8), headerStyle);
            workDayRow.createCell(5).setCellValue("小时");
            workDayRow.getCell(5).setCellStyle(headerStyle);
            Row headRow = sheet.createRow(rowIdx++);
            headRow.createCell(0).setCellValue("人员");
            headRow.getCell(0).setCellStyle(headerStyle);
            headRow.createCell(1).setCellValue("月产量");
            headRow.getCell(1).setCellStyle(headerStyle);
            headRow.createCell(2).setCellValue("折算时长");
            headRow.getCell(2).setCellStyle(headerStyle);
            headRow.createCell(3).setCellValue("工资");
            headRow.getCell(3).setCellStyle(headerStyle);
            Map<String, java.math.BigDecimal> standardMap = buildStandardQuantityMap(List.of(operationName));
            Map<String, java.math.BigDecimal> opQtyMap = buildOperationQuantityMap(accountList, operationName);
            Set<String> employeeNames = accountList.stream()
                    .map(ProductionAccount::getSchedulingUserName)
                    .filter(StringUtils::isNotEmpty)
                    .collect(Collectors.toCollection(LinkedHashSet::new));
            for (String staffName : employeeNames) {
                Row row = sheet.createRow(rowIdx++);
                row.createCell(0).setCellValue(staffName);
                row.getCell(0).setCellStyle(labelStyle);
                java.math.BigDecimal qty = opQtyMap.getOrDefault(staffName, java.math.BigDecimal.ZERO);
                createNumberCell(row, 1, qty, numStyle);
                java.math.BigDecimal standardQty = standardMap.getOrDefault(operationName, java.math.BigDecimal.ONE);
                createNumberCell(row, 2, standardQty.compareTo(java.math.BigDecimal.ZERO) == 0 ? java.math.BigDecimal.ZERO : qty.divide(standardQty, 2, java.math.RoundingMode.HALF_UP).multiply(new java.math.BigDecimal("8")), numStyle);
                java.math.BigDecimal wage = operation == null || operation.getSalaryQuota() == null
                        ? java.math.BigDecimal.ZERO
                        : (Integer.valueOf(1).equals(operation.getType()) ? operation.getSalaryQuota().multiply(qty) : operation.getSalaryQuota());
                createNumberCell(row, 3, wage, numStyle);
            }
            for (int i = 0; i <= 3; i++) {
                sheet.autoSizeColumn(i);
            }
            sheetIndex++;
        }
        try {
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("UTF-8");
            String fileName = java.net.URLEncoder.encode("工时表.xlsx", java.nio.charset.StandardCharsets.UTF_8);
            response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName);
            workbook.write(response.getOutputStream());
            workbook.close();
        } catch (IOException e) {
            throw new RuntimeException("导出工时表失败", e);
        }
    }
    public List<Map<String, Object>> setSchemeApplicableStaffUserInfo(List<Long> ids) {
@@ -139,4 +465,177 @@
        // 通过部门获取人员id
        return sysUserDeptMapper.setSchemeApplicableStaffUserInfo(ids);
    }
    @Override
    public AjaxResult getAvailableDepts(String salaryMonth) {
        if (StringUtils.isEmpty(salaryMonth)) {
            return AjaxResult.error("工资月份不能为空");
        }
        List<String> issuedDeptIdStrings = staffSalaryMainMapper.selectIssuedDeptIds(salaryMonth);
        Set<Long> issuedDeptIds = new HashSet<>();
        for (String deptIds : issuedDeptIdStrings) {
            if (StringUtils.isNotEmpty(deptIds)) {
                String[] ids = deptIds.split(",");
                for (String id : ids) {
                    issuedDeptIds.add(Long.parseLong(id.trim()));
                }
            }
        }
        List<SysDept> allDepts = sysDeptMapper.selectList(new LambdaQueryWrapper<SysDept>()
                .eq(SysDept::getStatus, "0")
                .eq(SysDept::getDelFlag, "0"));
        List<SysDept> availableDepts = new ArrayList<>();
        for (SysDept dept : allDepts) {
            if (!issuedDeptIds.contains(dept.getDeptId())) {
                availableDepts.add(dept);
            }
        }
        return AjaxResult.success(availableDepts);
    }
    private boolean hasOverlappingDept(String deptIds1, String deptIds2) {
        if (StringUtils.isEmpty(deptIds1) || StringUtils.isEmpty(deptIds2)) {
            return false;
        }
        Set<String> deptSet1 = new HashSet<>(Arrays.asList(deptIds1.split(",")));
        String[] deptArray2 = deptIds2.split(",");
        for (String deptId : deptArray2) {
            if (deptSet1.contains(deptId.trim())) {
                return true;
            }
        }
        return false;
    }
    private String buildWorkHoursTitle(StaffSalaryMain main) {
        String month = StringUtils.isNotEmpty(main.getSalaryMonth()) ? main.getSalaryMonth() : "未知月份";
        return month + "工时统计";
    }
    private Integer calculateWorkDays(String salaryMonth) {
        if (StringUtils.isEmpty(salaryMonth)) {
            return 0;
        }
        java.time.YearMonth yearMonth = java.time.YearMonth.parse(salaryMonth);
        int workDays = 0;
        for (int day = 1; day <= yearMonth.lengthOfMonth(); day++) {
            java.time.DayOfWeek dayOfWeek = yearMonth.atDay(day).getDayOfWeek();
            if (dayOfWeek != java.time.DayOfWeek.SATURDAY && dayOfWeek != java.time.DayOfWeek.SUNDAY) {
                workDays++;
            }
        }
        return workDays;
    }
    private CellStyle createStyle(Workbook workbook, boolean bold, HorizontalAlignment alignment, int fontSize, boolean fill) {
        Font font = workbook.createFont();
        font.setFontName("Microsoft YaHei");
        font.setFontHeightInPoints((short) fontSize);
        font.setBold(bold);
        CellStyle style = workbook.createCellStyle();
        style.setFont(font);
        style.setAlignment(alignment);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        if (fill) {
            style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
            style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        }
        return style;
    }
    private void createTextCell(Row row, int colIndex, String value, CellStyle style) {
        Cell cell = row.createCell(colIndex);
        cell.setCellValue(value == null ? "" : value);
        cell.setCellStyle(style);
    }
    private void createNumberCell(Row row, int colIndex, java.math.BigDecimal value, CellStyle style) {
        Cell cell = row.createCell(colIndex);
        if (value != null) {
            cell.setCellValue(value.doubleValue());
        }
        cell.setCellStyle(style);
    }
    private void createFormulaCell(Row row, int colIndex, String formula, CellStyle style) {
        Cell cell = row.createCell(colIndex);
        cell.setCellFormula(formula.startsWith("=") ? formula.substring(1) : formula);
        cell.setCellStyle(style);
    }
    private String toCellRef(int colIndex, int rowNum1Based) {
        return cellRef(colIndex) + rowNum1Based;
    }
    private String cellRef(int colIndex) {
        StringBuilder sb = new StringBuilder();
        int col = colIndex;
        do {
            int rem = col % 26;
            sb.insert(0, (char) ('A' + rem));
            col = col / 26 - 1;
        } while (col >= 0);
        return sb.toString();
    }
    private List<ProductionAccount> queryProductionAccounts(String salaryMonth) {
        if (StringUtils.isEmpty(salaryMonth)) {
            return new ArrayList<>();
        }
        YearMonth ym = YearMonth.parse(salaryMonth);
        LocalDate start = ym.atDay(1);
        LocalDate end = ym.atEndOfMonth();
        return productionAccountMapper.selectList(new LambdaQueryWrapper<ProductionAccount>()
                .ge(ProductionAccount::getSchedulingDate, start.atStartOfDay())
                .le(ProductionAccount::getSchedulingDate, end.atTime(23, 59, 59)));
    }
    private Map<String, java.math.BigDecimal> buildOperationQuantityMap(List<ProductionAccount> accountList, String operationName) {
        Map<String, java.math.BigDecimal> map = new LinkedHashMap<>();
        if (CollectionUtils.isEmpty(accountList) || StringUtils.isEmpty(operationName)) {
            return map;
        }
        accountList.stream()
                .filter(item -> item != null && operationName.equals(item.getTechnologyOperationName()))
                .filter(item -> StringUtils.isNotEmpty(item.getSchedulingUserName()))
                .forEach(item -> map.merge(item.getSchedulingUserName(),
                        item.getFinishedNum() == null ? java.math.BigDecimal.ZERO : item.getFinishedNum(),
                        java.math.BigDecimal::add));
        return map;
    }
    private Map<String, java.math.BigDecimal> buildStandardQuantityMap(List<String> operationNames) {
        Map<String, java.math.BigDecimal> map = new LinkedHashMap<>();
        if (CollectionUtils.isEmpty(operationNames)) {
            return map;
        }
        List<TechnologyOperation> operations = technologyOperationMapper.selectList(
                new LambdaQueryWrapper<TechnologyOperation>()
                        .in(TechnologyOperation::getName, operationNames)
        );
        Map<String, java.math.BigDecimal> opMap = operations.stream()
                .filter(item -> item != null && StringUtils.isNotEmpty(item.getName()))
                .collect(Collectors.toMap(
                        TechnologyOperation::getName,
                        item -> item.getStandardQuantity() == null ? java.math.BigDecimal.ZERO : item.getStandardQuantity(),
                        (left, right) -> left,
                        LinkedHashMap::new
                ));
        for (String operationName : operationNames) {
            map.put(operationName, opMap.getOrDefault(operationName, java.math.BigDecimal.ZERO));
        }
        return map;
    }
}