Browse Source

评估-报备项目比对处理功能

徐滕 1 week ago
parent
commit
1a03e81f7c

+ 6 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/FinanceInvoiceService.java

@@ -1930,6 +1930,12 @@ public class FinanceInvoiceService extends ServiceImpl<FinanceInvoiceMapper, Fin
      * @param response HTTP响应对象,用于输出文件流
      */
     public void importAndExportLedger(MultipartFile file, HttpServletResponse response) throws Exception {
+        // 校验文件格式
+        String fileName = file.getOriginalFilename();
+        if (fileName == null || (!fileName.toLowerCase().endsWith(".xls") && !fileName.toLowerCase().endsWith(".xlsx"))) {
+            throw new RuntimeException("仅支持 .xls 和 .xlsx 格式的Excel文件");
+        }
+
         // 第1步:解析Excel,识别数据sheet,读取有效行
         List<InvoiceLedgerImportDTO> ledgerRows = parseLedgerExcel(file);
 

+ 9 - 4
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/program/commonIssue/mapper/xml/CommonIssueMapper.xml

@@ -13,9 +13,10 @@
         <result property="resetMonths" column="reset_months" jdbcType="INTEGER"/>
         <result property="sortOrder" column="sort_order" jdbcType="INTEGER"/>
         <result property="status" column="status" jdbcType="CHAR"/>
-        <result property="createById" column="create_by_id" jdbcType="VARCHAR"/>
+        <result property="createBy.id" column="create_by_id" jdbcType="VARCHAR"/>
+        <result property="createByName" column="create_by_name" jdbcType="VARCHAR"/>
         <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
-        <result property="updateById" column="update_by_id" jdbcType="VARCHAR"/>
+        <result property="updateBy.id" column="update_by_id" jdbcType="VARCHAR"/>
         <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
         <result property="delFlag" column="del_flag" jdbcType="INTEGER"/>
         <result property="remarks" column="remarks" jdbcType="VARCHAR"/>
@@ -44,15 +45,19 @@
 
     <select id="findPageList" resultMap="BaseResultMap">
         SELECT
-        <include refid="Base_Column_List"/>
+        <include refid="Base_Column_List"/>,
+        u.name AS create_by_name
         FROM program_common_issue ci
+        LEFT JOIN sys_user u ON ci.create_by_id = u.id AND u.del_flag = 0
         ${ew.customSqlSegment}
     </select>
 
     <select id="queryById" resultMap="BaseResultMap">
         SELECT
-        <include refid="Base_Column_List"/>
+        <include refid="Base_Column_List"/>,
+        u.name AS create_by_name
         FROM program_common_issue ci
+        LEFT JOIN sys_user u ON ci.create_by_id = u.id AND u.del_flag = 0
         WHERE ci.del_flag = 0 AND ci.id = #{id}
     </select>
 

+ 15 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/program/commonIssue/service/dto/CommonIssueDTO.java

@@ -55,4 +55,19 @@ public class CommonIssueDTO extends BaseDTO {
      * 状态(0停用 1启用)
      */
     private String status;
+
+    /**
+     * 备注
+     */
+    private String remarks;
+
+    /**
+     * 租户ID
+     */
+    private String tenantId;
+
+    /**
+     * 创建人姓名(关联查询)
+     */
+    private String createByName;
 }

+ 26 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/program/configuration/projectList/controller/ProjectListController.java

@@ -36,6 +36,7 @@ import java.io.File;
 import java.io.IOException;
 import java.util.*;
 import java.util.stream.Collectors;
+import org.springframework.web.multipart.MultipartFile;
 
 @Api(tags ="项目列表管理")
 @RestController
@@ -883,4 +884,29 @@ public class ProjectListController {
                     .replaceAll("(?i)</span\\s*>", "");
         });
     }
+
+    /**
+     * 省级项目备案数据导入
+     * 读取原始Excel,按报告文号匹配系统和发票数据,末尾追加7列(项目与开票信息)后输出
+     */
+    @ApiLog("省级项目备案数据导入")
+    @PostMapping("importProvincialFiling")
+    @ApiOperation(value = "省级项目备案数据导入")
+    @PreAuthorize("hasAuthority('program:registered:importProvincialFiling')")
+    public void importProvincialFiling(@RequestParam("file") MultipartFile file, HttpServletResponse response) {
+        if (file == null || file.isEmpty()) {
+            throw new RuntimeException("请选择要导入的文件");
+        }
+        String fileName = file.getOriginalFilename();
+        if (fileName == null || (!fileName.toLowerCase().endsWith(".xls") && !fileName.toLowerCase().endsWith(".xlsx"))) {
+            throw new RuntimeException("仅支持 .xls 和 .xlsx 格式的Excel文件");
+        }
+        try {
+            projectListService.importProvincialFiling(file.getInputStream(), fileName, response);
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new RuntimeException("导入处理失败:" + e.getMessage());
+        }
+    }
 }

+ 5 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/program/configuration/projectList/domain/ProgramProjectListInfo.java

@@ -165,6 +165,11 @@ public class ProgramProjectListInfo extends BaseEntity {
     @Query(tableColumn = "a.status")
     private String status;
 
+    /**
+     * 备案是否作废(省级备案导入时从Excel同步)
+     */
+    private String recordCancel;
+
     private String procInsId;
 
     private String processDefinitionId;

+ 130 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/program/configuration/projectList/service/ProjectListService.java

@@ -11,12 +11,18 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.jeeplus.assess.invoice.domain.FinanceInvoice;
+import com.jeeplus.assess.invoice.domain.FinanceInvoiceBase;
+import com.jeeplus.assess.invoice.mapper.FinanceInvoiceBaseMapper;
 import com.jeeplus.assess.invoice.mapper.FinanceInvoiceDetailMapper;
+import com.jeeplus.assess.invoice.mapper.FinanceInvoiceMapper;
 import com.jeeplus.assess.program.configuration.projectList.domain.*;
 import com.jeeplus.assess.program.configuration.projectList.mapper.*;
 import com.jeeplus.assess.program.configuration.projectList.service.dto.*;
 import com.jeeplus.assess.program.configuration.projectList.service.mapstruct.ProgramAuditAssessPeopleWrapper;
 import com.jeeplus.assess.program.configuration.projectList.service.mapstruct.ProgramAuditWrapper;
+import com.jeeplus.assess.program.configuration.projectList.utils.ProvincialFilingExcelUtils;
+import com.jeeplus.assess.program.configuration.projectList.utils.dto.ProvincialFilingRowData;
 import com.jeeplus.assess.program.serviceType.service.ProgramServiceTypeService;
 import com.jeeplus.assess.program.serviceType.service.dto.ProgramServiceTypeDTO;
 import com.jeeplus.assess.proofread.domain.ProofreadDetail;
@@ -48,12 +54,15 @@ import com.jeeplus.sys.service.dto.WorkAttachmentInfoDTO;
 //import com.jeeplus.sys.utils.UserUtils;
 import org.apache.commons.compress.utils.Lists;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.ss.usermodel.Workbook;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import java.io.InputStream;
 import java.text.SimpleDateFormat;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -109,6 +118,12 @@ public class ProjectListService {
 
     @Autowired
     private FinanceInvoiceDetailMapper financeInvoiceDetailMapper;
+
+    @Autowired
+    private FinanceInvoiceMapper financeInvoiceMapper;
+
+    @Autowired
+    private FinanceInvoiceBaseMapper financeInvoiceBaseMapper;
 //    @Autowired
 //    private UserService userService;
 
@@ -2471,4 +2486,119 @@ public class ProjectListService {
 
         programMembersService.save(members);
     }
+
+    /**
+     * 省级项目备案数据导入:读取原始Excel,按报告文号匹配系统和发票数据,末尾追加7列后输出
+     */
+    @SuppressWarnings("unchecked")
+    public void importProvincialFiling(InputStream inputStream, String fileName, HttpServletResponse response) throws Exception {
+        // 1. 使用工具类读取Excel,提取报告文号和是否作废列信息
+        Object[] extractResult = ProvincialFilingExcelUtils.readAndExtractReportNos(inputStream);
+        Workbook workbook = (Workbook) extractResult[0];
+        int lastColIndex = (int) extractResult[2];
+        Map<Integer, String> rowReportNoMap = (Map<Integer, String>) extractResult[3];
+        Map<Integer, String> rowCancelMap = (Map<Integer, String>) extractResult[5];
+
+        // 2. 收集所有报告文号
+        List<String> reportNoList = new ArrayList<>(rowReportNoMap.values());
+
+        // 3. 批量查询:通过finance_invoice_base.report_no关联项目和发票
+        // 3.1 查询所有匹配的invoice_base记录
+        Map<String, List<FinanceInvoiceBase>> reportNoToBasesMap = new HashMap<>();
+        Set<String> allInvoiceIds = new HashSet<>();
+        if (!reportNoList.isEmpty()) {
+            LambdaQueryWrapper<FinanceInvoiceBase> baseWrapper = new LambdaQueryWrapper<>();
+            baseWrapper.in(FinanceInvoiceBase::getReportNo, reportNoList);
+            baseWrapper.eq(FinanceInvoiceBase::getDelFlag, 0);
+            List<FinanceInvoiceBase> allBases = financeInvoiceBaseMapper.selectList(baseWrapper);
+            for (FinanceInvoiceBase base : allBases) {
+                String rn = base.getReportNo();
+                reportNoToBasesMap.computeIfAbsent(rn, k -> new ArrayList<>()).add(base);
+                if (StringUtils.isNotBlank(base.getInvoiceId())) {
+                    allInvoiceIds.add(base.getInvoiceId());
+                }
+            }
+        }
+
+        // 3.2 查询所有相关发票主表
+        Map<String, FinanceInvoice> invoiceMap = new HashMap<>();
+        if (!allInvoiceIds.isEmpty()) {
+            List<FinanceInvoice> invoices = financeInvoiceMapper.selectBatchIds(allInvoiceIds);
+            for (FinanceInvoice inv : invoices) {
+                invoiceMap.put(inv.getId(), inv);
+            }
+        }
+
+        // 3.3 构建 invoiceId → 关联的所有reportNo 映射(用于判断多对多标黄)
+        Map<String, Set<String>> invoiceToReportNosMap = new HashMap<>();
+        for (FinanceInvoiceBase base : reportNoToBasesMap.values().stream()
+                .flatMap(Collection::stream).collect(Collectors.toList())) {
+            if (StringUtils.isNotBlank(base.getInvoiceId()) && StringUtils.isNotBlank(base.getReportNo())) {
+                invoiceToReportNosMap.computeIfAbsent(base.getInvoiceId(), k -> new HashSet<>())
+                        .add(base.getReportNo());
+            }
+        }
+
+        // 3.4 批量查询项目信息(通过program_id)
+        Set<String> allProgramIds = new HashSet<>();
+        for (List<FinanceInvoiceBase> bases : reportNoToBasesMap.values()) {
+            for (FinanceInvoiceBase base : bases) {
+                if (StringUtils.isNotBlank(base.getProgramId())) {
+                    allProgramIds.add(base.getProgramId());
+                }
+            }
+        }
+        Map<String, ProgramProjectListInfo> programMap = new HashMap<>();
+        if (!allProgramIds.isEmpty()) {
+            LambdaQueryWrapper<ProgramProjectListInfo> progWrapper = new LambdaQueryWrapper<>();
+            progWrapper.in(ProgramProjectListInfo::getId, allProgramIds);
+            progWrapper.eq(ProgramProjectListInfo::getDelFlag, 0);
+            List<ProgramProjectListInfo> programs = projectListMapper.selectList(progWrapper);
+            for (ProgramProjectListInfo prog : programs) {
+                programMap.put(prog.getId(), prog);
+            }
+        }
+
+        // 3.5 批量查询归档信息
+        Map<String, ProgramArchive> archiveMap = new HashMap<>();
+        if (!allProgramIds.isEmpty()) {
+            LambdaQueryWrapper<ProgramArchive> archiveWrapper = new LambdaQueryWrapper<>();
+            archiveWrapper.in(ProgramArchive::getProgramId, allProgramIds);
+            archiveWrapper.eq(ProgramArchive::getDelFlag, 0);
+            List<ProgramArchive> archives = programArchiveMapper.selectList(archiveWrapper);
+            for (ProgramArchive archive : archives) {
+                archiveMap.put(archive.getProgramId(), archive);
+            }
+        }
+
+        // 4. 使用工具类构建每行数据
+        Map<String, ProvincialFilingRowData> dataMap =
+                ProvincialFilingExcelUtils.buildDataMap(reportNoList, reportNoToBasesMap, invoiceMap,
+                        invoiceToReportNosMap, programMap, archiveMap);
+
+        // 5. 同步"是否作废"到数据库(通过报告文号找到项目,更新record_cancel字段)
+        for (Map.Entry<Integer, String> entry : rowReportNoMap.entrySet()) {
+            int rowIndex = entry.getKey();
+            String reportNo = entry.getValue();
+            String cancelValue = rowCancelMap.get(rowIndex);
+
+            // 通过reportNo找到关联的项目
+            List<FinanceInvoiceBase> bases = reportNoToBasesMap.get(reportNo);
+            if (bases != null && !bases.isEmpty()) {
+                String programId = bases.get(0).getProgramId();
+                if (StringUtils.isNotBlank(programId)) {
+                    ProgramProjectListInfo prog = programMap.get(programId);
+                    if (prog != null && StringUtils.isNotBlank(cancelValue)) {
+                        // 更新record_cancel字段
+                        prog.setRecordCancel(cancelValue);
+                        projectListMapper.updateById(prog);
+                    }
+                }
+            }
+        }
+
+        // 6. 使用工具类追加7列并输出下载
+        ProvincialFilingExcelUtils.appendColumnsAndDownload(workbook, lastColIndex, rowReportNoMap,
+                dataMap, fileName, response);
+    }
 }

+ 381 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/program/configuration/projectList/utils/ProvincialFilingExcelUtils.java

@@ -0,0 +1,381 @@
+package com.jeeplus.assess.program.configuration.projectList.utils;
+
+import com.jeeplus.assess.invoice.domain.FinanceInvoice;
+import com.jeeplus.assess.invoice.domain.FinanceInvoiceBase;
+import com.jeeplus.assess.program.configuration.projectList.domain.ProgramArchive;
+import com.jeeplus.assess.program.configuration.projectList.domain.ProgramProjectListInfo;
+import com.jeeplus.assess.program.configuration.projectList.utils.dto.ProvincialFilingRowData;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddress;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * 省级项目备案Excel导入工具类
+ * 负责Excel文件的读取、列追加、样式设置和输出下载
+ */
+public class ProvincialFilingExcelUtils {
+
+    /** 追加的8列详细表头 */
+    private static final String[] NEW_HEADERS = {"是否归档", "收费标准", "责任人", "开票编号", "开票金额", "是否开票", "开票状态", "开票时间"};
+    /** 大类表头名称 */
+    private static final String CATEGORY_HEADER = "项目与开票信息";
+    /** 需要匹配的列名 */
+    private static final String REPORT_NO_HEADER = "报告文号";
+    /** 是否作废列名 */
+    private static final String CANCEL_HEADER = "是否作废";
+
+    /**
+     * 读取Excel并提取报告文号列索引、是否作废列索引和所有数据行信息
+     * @param inputStream Excel文件流
+     * @return [0]=Workbook, [1]=报告文号列索引(Integer), [2]=最后列索引(Integer),
+     *         [3]=行号→报告文号映射(Map<Integer,String>), [4]=是否作废列索引(Integer),
+     *         [5]=行号→是否作废值映射(Map<Integer,String>)
+     */
+    public static Object[] readAndExtractReportNos(InputStream inputStream) throws Exception {
+        Workbook workbook = WorkbookFactory.create(inputStream);
+        Sheet sheet = workbook.getSheetAt(0);
+
+        Row headerRow = sheet.getRow(1);
+        if (headerRow == null) {
+            workbook.close();
+            throw new RuntimeException("Excel格式不正确,未找到表头行");
+        }
+
+        int reportNoColIndex = -1;
+        int cancelColIndex = -1;
+        int lastColIndex = 0;
+        for (int i = 0; i <= headerRow.getLastCellNum(); i++) {
+            Cell cell = headerRow.getCell(i);
+            if (cell == null) continue;
+            String headerName = getCellStringValue(cell);
+            if (REPORT_NO_HEADER.equals(headerName)) {
+                reportNoColIndex = i;
+            }
+            if (CANCEL_HEADER.equals(headerName)) {
+                cancelColIndex = i;
+            }
+            if (i > lastColIndex) {
+                lastColIndex = i;
+            }
+        }
+        if (reportNoColIndex == -1) {
+            workbook.close();
+            throw new RuntimeException("未在Excel表头中找到\"" + REPORT_NO_HEADER + "\"列,请确认文件格式");
+        }
+
+        Map<Integer, String> rowReportNoMap = new LinkedHashMap<>();
+        Map<Integer, String> rowCancelMap = new LinkedHashMap<>();
+        for (int i = 2; i <= sheet.getLastRowNum(); i++) {
+            Row row = sheet.getRow(i);
+            if (row == null) continue;
+            Cell cell = row.getCell(reportNoColIndex);
+            String reportNo = getCellStringValue(cell);
+            if (StringUtils.isNotBlank(reportNo)) {
+                rowReportNoMap.put(i, reportNo);
+                // 提取是否作废列的值
+                if (cancelColIndex != -1) {
+                    Cell cancelCell = row.getCell(cancelColIndex);
+                    String cancelValue = getCellStringValue(cancelCell);
+                    rowCancelMap.put(i, StringUtils.defaultString(cancelValue));
+                }
+            }
+        }
+
+        return new Object[]{workbook, reportNoColIndex, lastColIndex, rowReportNoMap, cancelColIndex, rowCancelMap};
+    }
+
+    /**
+     * 将查询到的数据追加到Excel末尾7列,并输出下载
+     */
+    public static void appendColumnsAndDownload(
+            Workbook workbook,
+            int lastColIndex,
+            Map<Integer, String> rowReportNoMap,
+            Map<String, ProvincialFilingRowData> dataMap,
+            String originalFileName,
+            HttpServletResponse response
+    ) throws Exception {
+        Sheet sheet = workbook.getSheetAt(0);
+        Row headerRow = sheet.getRow(1);
+        int newColCount = NEW_HEADERS.length;
+
+        // 检测第0行已有合并区域的最大结束列,避免重叠冲突
+        int maxMergedEndCol = -1;
+        for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
+            CellRangeAddress merged = sheet.getMergedRegion(i);
+            if (merged.getFirstRow() == 0 && merged.getLastRow() == 0) {
+                if (merged.getLastColumn() > maxMergedEndCol) {
+                    maxMergedEndCol = merged.getLastColumn();
+                }
+            }
+        }
+        // 新列起始位置 = max(表头最后列, 已有合并区域结束列+1)
+        int newStartCol = Math.max(lastColIndex, maxMergedEndCol + 1);
+
+        // 创建样式
+        CellStyle lightGreenStyle = createStyle(workbook, IndexedColors.LIGHT_GREEN);
+        CellStyle lightYellowStyle = createStyle(workbook, IndexedColors.LEMON_CHIFFON);
+        CellStyle normalBorderStyle = createBorderStyle(workbook);
+
+        // 写第1行大类表头(合并单元格)
+        Row categoryRow = sheet.getRow(0);
+        if (categoryRow == null) {
+            categoryRow = sheet.createRow(0);
+        }
+        Cell categoryCell = categoryRow.createCell(newStartCol);
+        categoryCell.setCellValue(CATEGORY_HEADER);
+        categoryCell.setCellStyle(lightGreenStyle);
+        if (newColCount > 1) {
+            sheet.addMergedRegion(new CellRangeAddress(0, 0, newStartCol, newStartCol + newColCount - 1));
+        }
+
+        // 写第2行详细表头
+        for (int i = 0; i < newColCount; i++) {
+            Cell cell = headerRow.createCell(newStartCol + i);
+            cell.setCellValue(NEW_HEADERS[i]);
+            cell.setCellStyle(lightGreenStyle);
+        }
+
+        // 遍历数据行,填入新列数据
+        for (Map.Entry<Integer, String> entry : rowReportNoMap.entrySet()) {
+            int rowIndex = entry.getKey();
+            String reportNo = entry.getValue();
+            Row dataRow = sheet.getRow(rowIndex);
+            if (dataRow == null) {
+                dataRow = sheet.createRow(rowIndex);
+            }
+
+            ProvincialFilingRowData rowData = dataMap.get(reportNo);
+            boolean needHighlight = (rowData != null && rowData.isNeedHighlight());
+
+            // 新列整列绿色背景
+            CellStyle colStyle = lightGreenStyle;
+
+            String isArchived = (rowData != null) ? rowData.getIsArchived() : "否";
+            String reportCharges = (rowData != null) ? rowData.getReportCharges() : "";
+            String projectManagerName = (rowData != null) ? rowData.getProjectManagerName() : "";
+            String invoiceNos = (rowData != null && rowData.getInvoiceNos() != null) ? String.join(",", rowData.getInvoiceNos()) : "";
+            Double invoiceAmount = (rowData != null) ? rowData.getInvoiceAmount() : null;
+            String isInvoiced = (rowData != null) ? rowData.getIsInvoiced() : "否";
+            String invoiceStatuses = (rowData != null && rowData.getInvoiceStatuses() != null) ? String.join(",", rowData.getInvoiceStatuses()) : "";
+            String invoiceDates = (rowData != null && rowData.getInvoiceDates() != null) ? String.join(",", rowData.getInvoiceDates()) : "";
+
+            // 是否归档
+            setCell(dataRow, newStartCol, isArchived, colStyle);
+            // 收费标准
+            setCell(dataRow, newStartCol + 1, reportCharges, colStyle);
+            // 责任人
+            setCell(dataRow, newStartCol + 2, projectManagerName, colStyle);
+            // 开票编号
+            setCell(dataRow, newStartCol + 3, invoiceNos, colStyle);
+            // 开票金额
+            Cell amountCell = dataRow.createCell(newStartCol + 4);
+            if (invoiceAmount != null) {
+                amountCell.setCellValue(invoiceAmount);
+            }
+            amountCell.setCellStyle(colStyle);
+            // 是否开票
+            setCell(dataRow, newStartCol + 5, isInvoiced, colStyle);
+            // 开票状态
+            setCell(dataRow, newStartCol + 6, invoiceStatuses, colStyle);
+            // 开票时间
+            setCell(dataRow, newStartCol + 7, invoiceDates, colStyle);
+
+            // 如果需要标黄,将整行所有单元格设为黄色(覆盖绿色)
+            if (needHighlight) {
+                int lastCellNum = dataRow.getLastCellNum();
+                for (int col = 0; col < lastCellNum; col++) {
+                    Cell cell = dataRow.getCell(col);
+                    if (cell != null) {
+                        cell.setCellStyle(lightYellowStyle);
+                    }
+                }
+            }
+        }
+
+        // 输出下载
+        String downloadName = "备案数据_" + originalFileName;
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(downloadName, "UTF-8"));
+        OutputStream out = response.getOutputStream();
+        workbook.write(out);
+        out.flush();
+        out.close();
+        workbook.close();
+    }
+
+    /**
+     * 根据查询结果构建每行的数据
+     */
+    public static Map<String, ProvincialFilingRowData> buildDataMap(
+            List<String> reportNoList,
+            Map<String, List<FinanceInvoiceBase>> reportNoToBasesMap,
+            Map<String, FinanceInvoice> invoiceMap,
+            Map<String, Set<String>> invoiceToReportNosMap,
+            Map<String, ProgramProjectListInfo> programMap,
+            Map<String, ProgramArchive> archiveMap
+    ) {
+        Map<String, ProvincialFilingRowData> dataMap = new HashMap<>();
+
+        for (String reportNo : reportNoList) {
+            ProvincialFilingRowData rowData = new ProvincialFilingRowData();
+            List<FinanceInvoiceBase> bases = reportNoToBasesMap.get(reportNo);
+
+            if (bases != null && !bases.isEmpty()) {
+                // 取第一个base的programId获取项目和归档信息
+                String programId = bases.get(0).getProgramId();
+                if (StringUtils.isNotBlank(programId)) {
+                    ProgramProjectListInfo prog = programMap.get(programId);
+                    if (prog != null) {
+                        rowData.setProjectManagerName(StringUtils.defaultString(prog.getProjectManagerName()));
+                    }
+                    ProgramArchive archive = archiveMap.get(programId);
+                    if (archive != null) {
+                        // 状态5=已归档,7=超期已归档
+                        String archiveStatus = archive.getStatus();
+                        if ("5".equals(archiveStatus) || "7".equals(archiveStatus)) {
+                            rowData.setIsArchived("是");
+                        }
+                        rowData.setReportCharges(StringUtils.defaultString(archive.getReportCharges()));
+                    }
+                }
+
+                // 遍历关联的发票
+                Set<String> processedInvoiceIds = new HashSet<>();
+                for (FinanceInvoiceBase base : bases) {
+                    String invoiceId = base.getInvoiceId();
+                    if (StringUtils.isBlank(invoiceId) || processedInvoiceIds.contains(invoiceId)) {
+                        continue;
+                    }
+                    processedInvoiceIds.add(invoiceId);
+
+                    // 检查该发票是否关联了多个不同的报告文号(多对多标黄)
+                    Set<String> linkedReportNos = invoiceToReportNosMap.get(invoiceId);
+                    if (linkedReportNos != null && linkedReportNos.size() > 1) {
+                        rowData.setNeedHighlight(true);
+                    }
+
+                    FinanceInvoice inv = invoiceMap.get(invoiceId);
+                    if (inv != null) {
+                        if (StringUtils.isNotBlank(inv.getNo())) {
+                            rowData.getInvoiceNos().add(inv.getNo());
+                        }
+                        if (StringUtils.isNotBlank(inv.getAccount())) {
+                            try {
+                                double currentAmount = rowData.getInvoiceAmount() != null ? rowData.getInvoiceAmount() : 0;
+                                rowData.setInvoiceAmount(currentAmount + Double.parseDouble(inv.getAccount()));
+                            } catch (NumberFormatException e) {
+                                // 忽略无法解析的金额
+                            }
+                        }
+                        if (StringUtils.isNotBlank(inv.getStatus())) {
+                            rowData.getInvoiceStatuses().add(convertInvoiceStatus(inv.getStatus()));
+                        }
+                        if (inv.getBillingDate() != null) {
+                            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+                            rowData.getInvoiceDates().add(sdf.format(inv.getBillingDate()));
+                        }
+                    }
+                }
+
+                // 判断是否开票(开票成功为已开票)
+                boolean isInvoiced = rowData.getInvoiceStatuses().stream().anyMatch(s -> "开票成功".equals(s));
+                rowData.setIsInvoiced(isInvoiced ? "是" : "否");
+            }
+
+            dataMap.put(reportNo, rowData);
+        }
+
+        return dataMap;
+    }
+
+    // ==================== 私有工具方法 ====================
+
+    private static CellStyle createStyle(Workbook workbook, IndexedColors color) {
+        CellStyle style = workbook.createCellStyle();
+        style.setFillForegroundColor(color.getIndex());
+        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        style.setBorderBottom(BorderStyle.THIN);
+        style.setBorderTop(BorderStyle.THIN);
+        style.setBorderLeft(BorderStyle.THIN);
+        style.setBorderRight(BorderStyle.THIN);
+        return style;
+    }
+
+    private static CellStyle createBorderStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        style.setBorderBottom(BorderStyle.THIN);
+        style.setBorderTop(BorderStyle.THIN);
+        style.setBorderLeft(BorderStyle.THIN);
+        style.setBorderRight(BorderStyle.THIN);
+        return style;
+    }
+
+    private static void setCell(Row row, int colIndex, String value, CellStyle style) {
+        Cell cell = row.createCell(colIndex);
+        cell.setCellValue(value);
+        cell.setCellStyle(style);
+    }
+
+    /**
+     * 发票状态码转中文
+     */
+    private static String convertInvoiceStatus(String status) {
+        if (StringUtils.isBlank(status)) {
+            return "未知";
+        }
+        switch (status) {
+            case "0": return "暂存";
+            case "1": return "待审核";
+            case "2": return "审核中";
+            case "3": return "已撤回";
+            case "4": return "已驳回";
+            case "5": return "开票成功";
+            case "6": return "审核中";
+            case "7": return "已作废";
+            default: return status;
+        }
+    }
+
+    /**
+     * 安全获取单元格的字符串值
+     */
+    public static String getCellStringValue(Cell cell) {
+        if (cell == null) return "";
+        switch (cell.getCellType()) {
+            case STRING:
+                return cell.getStringCellValue().trim();
+            case NUMERIC:
+                if (DateUtil.isCellDateFormatted(cell)) {
+                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+                    return sdf.format(cell.getDateCellValue());
+                }
+                double numVal = cell.getNumericCellValue();
+                if (numVal == Math.floor(numVal) && !Double.isInfinite(numVal)) {
+                    return String.valueOf((long) numVal);
+                }
+                return String.valueOf(numVal);
+            case BOOLEAN:
+                return String.valueOf(cell.getBooleanCellValue());
+            case FORMULA:
+                try {
+                    return cell.getStringCellValue().trim();
+                } catch (Exception e) {
+                    return String.valueOf(cell.getNumericCellValue());
+                }
+            default:
+                return "";
+        }
+    }
+}

+ 40 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/program/configuration/projectList/utils/dto/ProvincialFilingRowData.java

@@ -0,0 +1,40 @@
+package com.jeeplus.assess.program.configuration.projectList.utils.dto;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 省级项目备案导入 - 每行追加数据的载体
+ */
+@Data
+public class ProvincialFilingRowData {
+
+    /** 是否归档 */
+    private String isArchived = "否";
+
+    /** 收费标准(归档信息中的实际开票金额) */
+    private String reportCharges = "";
+
+    /** 责任人(项目经理) */
+    private String projectManagerName = "";
+
+    /** 开票编号列表 */
+    private List<String> invoiceNos = new ArrayList<>();
+
+    /** 开票金额合计 */
+    private Double invoiceAmount = null;
+
+    /** 是否开票(是/否) */
+    private String isInvoiced = "否";
+
+    /** 开票状态列表 */
+    private List<String> invoiceStatuses = new ArrayList<>();
+
+    /** 开票时间列表 */
+    private List<String> invoiceDates = new ArrayList<>();
+
+    /** 是否需要标黄(多对多发票) */
+    private boolean needHighlight = false;
+}