Browse Source

fit:归档调整

zzw 2 tuần trước cách đây
mục cha
commit
7efc05814a

+ 12 - 1
assembly/src/main/resources/application-dev.yml

@@ -197,7 +197,18 @@ third:
       - /server/**
 
 
-
+# 卷宗模板配置
+archive:
+  template:
+    # 案卷封面模板路径
+#    coverPath: D:/fx/1、卷宗封面.docx
+    coverPath: /www/fx/1、卷宗封面.docx
+#     卷内目录模板路径
+#    catalogPath: D:/fx/2、卷内目录.docx
+    catalogPath: /www/fx/2、卷内目录.docx
+#     案卷封底模板路径
+#    backCoverPath: D:/fx/3、案卷封底.docx
+    backCoverPath: /www/fx/3、案卷封底.docx
 assistmg:
   profile: /home/eip/uploadPath
   imgUrl: http://1.71.9.215:9506

+ 262 - 31
assistMg/src/main/java/com/hotent/enterpriseDeclare/controller/material/CostProjectTaskMaterialSummaryController.java

@@ -12,7 +12,9 @@ import com.hotent.project.manager.CostProjectTaskMaterialSummaryManager;
 import com.hotent.project.model.CostProjectTask;
 import com.hotent.project.model.CostProjectTaskMaterialSummary;
 import com.hotent.project.model.CostProjectTaskMaterialSummaryDetail;
+import com.hotent.project.req.ArchiveProofreadReq;
 import com.hotent.project.req.MaterialSummarySortReq;
+import com.hotent.project.resp.ArchiveProofreadResp;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -21,7 +23,9 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
 import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 资料归纳主表 前端控制器
@@ -56,7 +60,7 @@ public class CostProjectTaskMaterialSummaryController extends BaseController<Cos
             @RequestParam(required = true) String taskId) {
 
         List<CostProjectTaskMaterialSummary> summaryList = baseService.listByTaskId(taskId);
-        // 如果为空,自动生成13个基础资料类别
+        // 如果为空,自动生成14个基础资料类别
         if (summaryList == null || summaryList.isEmpty()) {
             // 获取任务信息
             com.hotent.project.model.CostProjectTask mainTask = costProjectTaskManager.getById(taskId);
@@ -67,6 +71,7 @@ public class CostProjectTaskMaterialSummaryController extends BaseController<Cos
                 );
                 // 同步生成资料归纳
                 asyncMaterialSummaryService.generateMaterialSummary(mainTask, childTasks);
+                // 计算总页数
                 asyncMaterialSummaryService.calculatePageCountAsync(mainTask.getId());
                 // 重新查询
                 summaryList = baseService.listByTaskId(taskId);
@@ -256,43 +261,236 @@ public class CostProjectTaskMaterialSummaryController extends BaseController<Cos
     @Autowired
     private com.hotent.project.service.AsyncMaterialSummaryService asyncMaterialSummaryService;
 
+
+
+    // ==================== 卷宗校对功能接口 ====================
+
     /**
-     * 档案校对通过,触发异步生成Word卷宗
+     * 校对列表
      * @param taskId 任务ID
-     * @return
+     * @return 卷宗校对列表
      */
-    @PostMapping(value = "/approveArchive")
-    @ApiOperation(value = "档案校对通过", httpMethod = "POST", notes = "档案校对通过后,异步生成Word卷宗")
-    public CommonResult<String> approveArchive(
+    @GetMapping(value = "/archiveProofreadList")
+    @ApiOperation(value = "获取卷宗校对列表", httpMethod = "GET", notes = "获取卷宗校对列表(从封面到封底,按页码排序)")
+    public CommonResult<List<ArchiveProofreadResp>> archiveProofreadList(
             @ApiParam(name = "taskId", value = "任务ID", required = true)
-            @RequestParam(required = true) String taskId) {
+            @RequestParam(required = true) String taskId)
+    {
+        // 获取所有资料归纳数据(包含封面、目录、封底)
+        List<CostProjectTaskMaterialSummary> summaryList = baseService.listAllByTaskId(taskId);
 
-        // 校验任务是否存在资料归纳数据
-        List<CostProjectTaskMaterialSummary> summaryList = baseService.listByTaskId(taskId);
-        if (summaryList == null || summaryList.isEmpty()) {
-            return CommonResult.<String>error().message("该任务没有资料归纳数据,无法校对");
-        }
+        // 分离封面、目录、封底和普通资料
+        CostProjectTaskMaterialSummary coverSummary = null;    // -1
+        CostProjectTaskMaterialSummary catalogSummary = null;  // -2
+        CostProjectTaskMaterialSummary backCoverSummary = null; // -3
+        List<CostProjectTaskMaterialSummary> materialList = new ArrayList<>();
 
-        // 校验所有资料是否都已上传附件
-        boolean allHasAttachment = true;
         for (CostProjectTaskMaterialSummary summary : summaryList) {
-            if (summary.getDetailList() != null) {
-                for (com.hotent.project.model.CostProjectTaskMaterialSummaryDetail detail : summary.getDetailList()) {
-                    if (StringUtil.isEmpty(detail.getAttachmentUrl())) {
-                        allHasAttachment = false;
-                        break;
-                    }
+            Integer orderNum = summary.getMaterialOrderNum();
+            if (orderNum != null && orderNum == -1) {
+                coverSummary = summary;
+            } else if (orderNum != null && orderNum == -2) {
+                catalogSummary = summary;
+            } else if (orderNum != null && orderNum == -3) {
+                backCoverSummary = summary;
+            } else {
+                materialList.add(summary);
+            }
+        }
+
+        List<ArchiveProofreadResp> resultList = new ArrayList<>();
+        int currentPage = 1;
+        int displayOrderNum = 1;
+
+        // 1. 案卷封面(不计入页码)
+        ArchiveProofreadResp coverResp = new ArchiveProofreadResp();
+        coverResp.setOrderNum(displayOrderNum++);
+        coverResp.setMaterialName("卷宗封面");
+        coverResp.setDocumentType(1);
+        coverResp.setCanGenerate("1");
+        coverResp.setCanEdit("1");
+        coverResp.setPageRange("—");
+        if (coverSummary != null && coverSummary.getDetailList() != null && !coverSummary.getDetailList().isEmpty()) {
+            CostProjectTaskMaterialSummaryDetail coverDetail = coverSummary.getDetailList().get(0);
+            if (StringUtil.isNotEmpty(coverDetail.getAttachmentUrl())) {
+                coverResp.setRelatedId(coverSummary.getId());
+                coverResp.setAttachmentUrl(coverDetail.getAttachmentUrl());
+                coverResp.setGenerateStatus("1");
+            } else {
+                coverResp.setRelatedId(coverSummary.getId());
+                coverResp.setGenerateStatus("0");
+            }
+        } else {
+            coverResp.setGenerateStatus("0");
+        }
+        resultList.add(coverResp);
+
+        // 2. 卷内目录(不计入页码)
+        ArchiveProofreadResp catalogResp = new ArchiveProofreadResp();
+        catalogResp.setOrderNum(displayOrderNum++);
+        catalogResp.setMaterialName("卷内目录");
+        catalogResp.setDocumentType(2);
+        catalogResp.setCanGenerate("1");
+        catalogResp.setCanEdit("1");
+        catalogResp.setPageRange("—");
+        if (catalogSummary != null && catalogSummary.getDetailList() != null && !catalogSummary.getDetailList().isEmpty()) {
+            CostProjectTaskMaterialSummaryDetail catalogDetail = catalogSummary.getDetailList().get(0);
+            if (StringUtil.isNotEmpty(catalogDetail.getAttachmentUrl())) {
+                catalogResp.setRelatedId(catalogSummary.getId());
+                catalogResp.setAttachmentUrl(catalogDetail.getAttachmentUrl());
+                catalogResp.setGenerateStatus("1");
+            } else {
+                catalogResp.setRelatedId(catalogSummary.getId());
+                catalogResp.setGenerateStatus("0");
+            }
+        } else {
+            catalogResp.setGenerateStatus("0");
+        }
+        resultList.add(catalogResp);
+
+        // 3. 14个资料归纳
+        for (CostProjectTaskMaterialSummary summary : materialList) {
+            ArchiveProofreadResp resp = new ArchiveProofreadResp();
+            resp.setOrderNum(displayOrderNum++);
+            resp.setMaterialName(summary.getMaterialName());
+            resp.setDocumentType(0);
+            resp.setCanGenerate("0");
+            resp.setCanEdit("0");
+            resp.setRelatedId(summary.getId());
+            resp.setGenerateStatus("1");
+
+            int pageCount = 0;
+            if (summary.getTotalPageCount() != null) {
+                try {
+                    pageCount = Integer.parseInt(summary.getTotalPageCount());
+                } catch (NumberFormatException e) {
+                    pageCount = 0;
                 }
             }
-            if (!allHasAttachment) {
-                break;
+            resp.setPageCount(pageCount);
+            if (pageCount > 0) {
+                resp.setPageRange(currentPage + "-" + (currentPage + pageCount - 1));
+                currentPage += pageCount;
+            } else {
+                resp.setPageRange("—");
             }
+            resultList.add(resp);
         }
 
-        if (!allHasAttachment) {
-            return CommonResult.<String>error().message("部分资料未上传附件,请完善后再进行校对");
+        // 4. 案卷封底(不计入页码)
+        ArchiveProofreadResp backCoverResp = new ArchiveProofreadResp();
+        backCoverResp.setOrderNum(displayOrderNum);
+        backCoverResp.setMaterialName("案卷封底(成本监审卷宗备考表)");
+        backCoverResp.setDocumentType(3);
+        backCoverResp.setCanGenerate("1");
+        backCoverResp.setCanEdit("1");
+        backCoverResp.setPageRange("—");
+        if (backCoverSummary != null && backCoverSummary.getDetailList() != null && !backCoverSummary.getDetailList().isEmpty()) {
+            CostProjectTaskMaterialSummaryDetail backCoverDetail = backCoverSummary.getDetailList().get(0);
+            if (StringUtil.isNotEmpty(backCoverDetail.getAttachmentUrl())) {
+                backCoverResp.setRelatedId(backCoverSummary.getId());
+                backCoverResp.setAttachmentUrl(backCoverDetail.getAttachmentUrl());
+                backCoverResp.setGenerateStatus("1");
+            } else {
+                backCoverResp.setRelatedId(backCoverSummary.getId());
+                backCoverResp.setGenerateStatus("0");
+            }
+        } else {
+            backCoverResp.setGenerateStatus("0");
         }
+        resultList.add(backCoverResp);
 
+        return CommonResult.<List<ArchiveProofreadResp>>ok().value(resultList);
+    }
+
+    /**
+     * 保存卷宗文书(封面/目录/封底)
+     * @param req 保存请求
+     * @return 生成的文书URL
+     */
+    @PostMapping(value = "/saveArchiveDocument")
+    @ApiOperation(value = "保存卷宗文书", httpMethod = "POST", notes = "保存案卷封面/卷内目录/案卷封底文书(新增和编辑统一接口)")
+    @Transactional
+    public CommonResult<String> saveArchiveDocument(
+            @ApiParam(name = "req", value = "保存请求", required = true)
+            @RequestBody ArchiveProofreadReq req)
+    {
+
+        if (StringUtil.isEmpty(req.getTaskId())) {
+            return CommonResult.<String>error().message("任务ID不能为空");
+        }
+        // id为空是新增,必须传documentType;id不为空是编辑
+        if (StringUtil.isEmpty(req.getId())) {
+            if (req.getDocumentType() == null || req.getDocumentType() < 1 || req.getDocumentType() > 3) {
+                return CommonResult.<String>error().message("新增时文书类型不能为空,请选择1-案卷封面、2-卷内目录、3-案卷封底");
+            }
+        }
+
+        try {
+            String documentUrl = asyncMaterialSummaryService.generateArchiveDocument(req);
+            return CommonResult.<String>ok().value(documentUrl).message("保存成功");
+        } catch (Exception e) {
+            e.printStackTrace();
+            return CommonResult.<String>error().message("保存失败:" + e.getMessage());
+        }
+    }
+
+
+
+    /**
+     *  生成Word卷宗
+     * @param req 请求参数(包含taskId)
+     * @return
+     */
+    @PostMapping(value = "/approveArchive")
+    @ApiOperation(value = "生成Word卷宗", httpMethod = "POST", notes = "档案校对通过后,异步生成Word卷宗")
+    public CommonResult<String> approveArchive(
+            @ApiParam(name = "req", value = "请求参数", required = true)
+            @RequestBody Map<String, String> req)
+    {
+        String taskId = req.get("taskId");
+        if (StringUtil.isEmpty(taskId)) {
+            return CommonResult.<String>error().message("任务ID不能为空");
+        }
+        // 校验任务是否存在资料归纳数据(包含封面、目录、封底)
+        List<CostProjectTaskMaterialSummary> summaryList = baseService.listAllByTaskId(taskId);
+        if (summaryList == null || summaryList.isEmpty()) {
+            return CommonResult.<String>error().message("该任务没有资料归纳数据,无法校对");
+        }
+
+        // 校验所有资料,收集未完成的资料名称
+//        List<String> missingList = new ArrayList<>();
+//        for (CostProjectTaskMaterialSummary summary : summaryList) {
+//            Integer orderNum = summary.getMaterialOrderNum();
+//            if (orderNum != null && orderNum < 0) {
+//                if (StringUtil.isEmpty(summary.getTotalPageCount())) {
+//                    missingList.add(summary.getMaterialName());
+//                }
+//            } else {
+//                // 普通资料:检查明细附件
+//                if (summary.getDetailList() != null) {
+//                    for (com.hotent.project.model.CostProjectTaskMaterialSummaryDetail detail : summary.getDetailList()) {
+//                        if (StringUtil.isEmpty(detail.getAttachmentUrl())) {
+//                            String detailName = StringUtil.isNotEmpty(detail.getDocumentName())
+//                                    ? detail.getDocumentName()
+//                                    : summary.getMaterialName();
+//                            missingList.add(detailName);
+//                        }
+//                    }
+//                }
+//            }
+//        }
+//
+//        if (!missingList.isEmpty()) {
+//            String missingNames = String.join("、", missingList);
+//            return CommonResult.<String>error().message("以下资料未完成:" + missingNames);
+//        }
+        // 更新状态为"生成中"
+        CostProjectTask task = costProjectTaskManager.getById(taskId);
+        if (task != null) {
+            task.setArchiveStatus("1");
+            costProjectTaskManager.updateById(task);
+        }
         // 异步生成Word卷宗
         asyncMaterialSummaryService.generateWordArchiveAsync(taskId);
 
@@ -302,16 +500,49 @@ public class CostProjectTaskMaterialSummaryController extends BaseController<Cos
     /**
      * 查询Word卷宗生成状态
      * @param taskId 任务ID
-     * @return
+     * @return 返回任务信息(包含archiveStatus、archiveUrl、archiveTime)
      */
     @GetMapping(value = "/getArchiveStatus")
-    @ApiOperation(value = "查询Word卷宗生成状态", httpMethod = "GET", notes = "查询Word卷宗生成状态")
-    public CommonResult<String> getArchiveStatus(
+    @ApiOperation(value = "查询Word卷宗生成状态", httpMethod = "GET", notes = "查询Word卷宗生成状态:0-未生成 1-生成中 2-已生成 3-生成失败")
+    public CommonResult<CostProjectTask> getArchiveStatus(
             @ApiParam(name = "taskId", value = "任务ID", required = true)
-            @RequestParam(required = true) String taskId) {
-        // TODO: 实现查询卷宗生成状态的逻辑
-        // 可以在数据库中添加一个状态字段来记录生成进度
-        return CommonResult.<String>ok().value("生成中").message("查询成功");
+            @RequestParam(required = true) String taskId)
+    {
+        CostProjectTask task = costProjectTaskManager.getById(taskId);
+        if (task == null) {
+            return CommonResult.<CostProjectTask>error().message("任务不存在");
+        }
+        return CommonResult.<CostProjectTask>ok().value(task).message("查询成功");
+    }
+
+    /**
+     * 确定归档
+     * @param req 请求参数(包含taskId)
+     * @return
+     */
+    @PostMapping(value = "/confirmArchive")
+    @ApiOperation(value = "确定归档", httpMethod = "POST", notes = "确定归档,更新任务归档状态")
+    @Transactional
+    public CommonResult<String> confirmArchive(
+            @ApiParam(name = "req", value = "请求参数", required = true)
+            @RequestBody Map<String, String> req)
+    {
+        String taskId = req.get("taskId");
+        if (StringUtil.isEmpty(taskId)) {
+            return CommonResult.<String>error().message("任务ID不能为空");
+        }
+        CostProjectTask task = costProjectTaskManager.getById(taskId);
+        if (task == null) {
+            return CommonResult.<String>error().message("任务不存在");
+        }
+        if (StringUtil.isEmpty(task.getArchiveUrl())) {
+            return CommonResult.<String>error().message("请先生成卷宗文件");
+        }
+        task.setIsGd("1");
+        task.setArchiveStatus("2");
+        task.setArchiveTime(java.time.LocalDateTime.now());
+        costProjectTaskManager.updateById(task);
+        return CommonResult.<String>ok().message("归档成功");
     }
 }
 

+ 132 - 127
assistMg/src/main/java/com/hotent/enterpriseDeclare/controller/material/CostProjectTaskSurveyGenericController.java

@@ -630,7 +630,7 @@ public class CostProjectTaskSurveyGenericController {
                         surveyTemplateId, currentVersion.getId());
 
                 Workbook workbook = new XSSFWorkbook();
-                String sheetName = System.currentTimeMillis() + "_成本调查表";
+                String sheetName = "成本调查表";
                 Sheet sheet = workbook.createSheet(sheetName);
 
                 // 创建样式
@@ -787,7 +787,7 @@ public class CostProjectTaskSurveyGenericController {
 
                 // 5.返回excel
                 Workbook workbook = new XSSFWorkbook();
-                String sheetName = System.currentTimeMillis() + "_财务数据表";
+                String sheetName = "财务数据表";
                 Sheet sheet = workbook.createSheet(sheetName);
 
                 // 创建样式
@@ -930,7 +930,7 @@ public class CostProjectTaskSurveyGenericController {
                         costVerifyTemplateItemsDao.selectByVerifyTemplateId(surveyTemplateId, null);
 
                 Workbook workbook = new XSSFWorkbook();
-                String sheetName = System.currentTimeMillis() + "_核定表";
+                String sheetName = "核定表";
                 Sheet sheet = workbook.createSheet(sheetName);
 
                 // 创建样式
@@ -1151,14 +1151,14 @@ public class CostProjectTaskSurveyGenericController {
                         }
                     }
 
-//                    // 校验表头是否与导出时一致
-//                    List<String> headerValidationErrors = validateExcelHeaders(
-//                        headerRow, headersList, taskId, templateType, type,
-//                        rowIdColumnIndex, parentIdColumnIndex, auditPeriodColumnMap, remarkColumnIndex);
-//                    if (!headerValidationErrors.isEmpty()) {
-//                        return CommonResult.<String>error().message(
-//                                "导入失败,表头校验不通过:<br>" + String.join("<br>", headerValidationErrors));
-//                    }
+                    // 校验表头是否与导出时一致
+                    List<String> headerValidationErrors = validateExcelHeaders(
+                        headerRow, headersList, taskId, templateType, type,
+                        rowIdColumnIndex, parentIdColumnIndex, auditPeriodColumnMap, remarkColumnIndex);
+                    if (!headerValidationErrors.isEmpty()) {
+                        return CommonResult.<String>error().message(
+                                "导入失败,表头校验不通过:" + String.join("<br>", headerValidationErrors));
+                    }
 
                     List<CostSurveyTemplateUploadData> dataList = new ArrayList<>();
                     Map<String, Integer> rowIdToExcelRowMap = new HashMap<>();
@@ -1387,13 +1387,13 @@ public class CostProjectTaskSurveyGenericController {
                     }
 
                     // 校验表头是否与导出时一致
-//                    List<String> headerValidationErrors = validateExcelHeadersFd(
-//                        headerRow, headersList, taskId, templateType, type,
-//                        rowIdColumnIndex, parentIdColumnIndex, auditPeriodColumnMap, remarkColumnIndex);
-//                    if (!headerValidationErrors.isEmpty()) {
-//                        return CommonResult.<String>error().message(
-//                                "导入失败,表头校验不通过:<br>" + String.join("<br>", headerValidationErrors));
-//                    }
+                    List<String> headerValidationErrors = validateExcelHeadersFd(
+                        headerRow, headersList, taskId, templateType, type,
+                        rowIdColumnIndex, parentIdColumnIndex, auditPeriodColumnMap, remarkColumnIndex);
+                    if (!headerValidationErrors.isEmpty()) {
+                        return CommonResult.<String>error().message(
+                                "导入失败,表头校验不通过:" + String.join("<br>", headerValidationErrors));
+                    }
 
                     List<CostSurveyTemplateUploadData> dataList = new ArrayList<>();
                     Map<String, Integer> rowIdToExcelRowMap = new HashMap<>();
@@ -1683,7 +1683,14 @@ public class CostProjectTaskSurveyGenericController {
                         }
                     }
 
-                    // 保存数据(核定表不需要复杂的字段校验)
+                    List<String> errors = verifyImportData(dataList, type, surveyTemplateId, rowIdToExcelRowMap);
+                    if (!errors.isEmpty()) {
+                        CommonResult<String> result = CommonResult.<String>error().message("导入失败,发现以下问题:<br>" + String.join("<br>", errors));
+                        result.setCode(250);
+                        return result;
+                    }
+
+                    // 保存数据
                     costSurveyTemplateUploadDataManager.saveData(dataList);
 
                     return CommonResult.<String>ok().message("导入成功,共导入 " + importRowCount + " 行数据");
@@ -1794,8 +1801,9 @@ public class CostProjectTaskSurveyGenericController {
                 // 对于年限列,需要构建 cellCode_年限 -> 值 的映射(如 A1_2024 -> "1")
                 Map<String, String> globalCellCodeMap = new HashMap<>();
 
-                // 先构建 rowid -> cellCode 的映射
+                // 先构建 rowid -> cellCode 的映射,以及反向映射 cellCode -> rowid
                 Map<String, String> rowidToCellCodeMap = new HashMap<>();
+                Map<String, String> cellCodeToRowIdMap = new HashMap<>();
                 for (Map.Entry<String, List<? extends Object>> entry : itemsByRowId.entrySet()) {
                     String rowid = entry.getKey();
                     List<? extends Object> items = entry.getValue();
@@ -1803,6 +1811,7 @@ public class CostProjectTaskSurveyGenericController {
                         String cellCode = getItemCellCode(items.get(0));
                         if (StringUtil.isNotEmpty(cellCode)) {
                             rowidToCellCodeMap.put(rowid, cellCode);
+                            cellCodeToRowIdMap.put(cellCode, rowid); // 反向映射
                         }
                     }
                 }
@@ -1861,7 +1870,7 @@ public class CostProjectTaskSurveyGenericController {
 
                     // 针对每个年限分别校验(收集错误)
                     for (String period : periods) {
-                        List<String> formulaErrors = validateRowFormulasForPeriod(rowid, rowData, rowItems, globalCellCodeMap, period, type, rowIdToExcelRowMap);
+                        List<String> formulaErrors = validateRowFormulasForPeriod(rowid, rowData, rowItems, globalCellCodeMap, period, type, rowIdToExcelRowMap, cellCodeToRowIdMap);
                         errors.addAll(formulaErrors);
                     }
                 }
@@ -2155,13 +2164,13 @@ public class CostProjectTaskSurveyGenericController {
                     try {
                         Long.parseLong(value);
 
-                        // 校验整数位数(必须精确匹配)
+                        // 校验整数位数(最多N位,而不是精确匹配)
                         if (fieldTypelen != null && fieldTypelen > 0) {
-                            String absValue = value.replace("-", "").replace("+", ""); // 去除正负号
-                            if (absValue.length() != fieldTypelen) {
+                            String absValue = value.replace("-", "").replace("+", "");
+                            if (absValue.length() > fieldTypelen) {
                                 throw new IllegalArgumentException(
-                                    String.format("%s [%s]必须是%d位整数,实际值:%s",
-                                        rowDisplay, fieldName, fieldTypelen, value));
+                                    String.format("%s [%s]整数位数超限,最多%d位,实际:%d位",
+                                        rowDisplay, fieldName, fieldTypelen, absValue.length()));
                             }
                         }
                     } catch (NumberFormatException e) {
@@ -2183,22 +2192,17 @@ public class CostProjectTaskSurveyGenericController {
                                     rowDisplay, fieldName, (fieldTypelen - fieldTypenointlen), intPartLen));
                         }
 
-                        // 小数部分长度(必须精确匹配)
+                        // 小数部分长度(最多N位,而不是精确匹配)
                         if (fieldTypenointlen != null && fieldTypenointlen > 0) {
-                            if (parts.length == 1) {
-                                // 没有小数部分,但要求有小数位
-                                throw new IllegalArgumentException(
-                                    String.format("%s [%s]必须包含%d位小数,实际值:%s",
-                                        rowDisplay, fieldName, fieldTypenointlen, value));
-                            } else {
+                            if (parts.length > 1) {
                                 int decimalPartLen = parts[1].length();
-                                // 改为精确匹配,而不是"不超过"
-                                if (decimalPartLen != fieldTypenointlen) {
+                                if (decimalPartLen > fieldTypenointlen) {
                                     throw new IllegalArgumentException(
-                                        String.format("%s [%s]必须是%d位小数,实际:%d位",
+                                        String.format("%s [%s]小数位数超限,最多%d位,实际:%d位",
                                             rowDisplay, fieldName, fieldTypenointlen, decimalPartLen));
                                 }
                             }
+                            // 如果没有小数部分(parts.length == 1),不报错,因为整数也是合法的double
                         }
                     } catch (NumberFormatException e) {
                         throw new IllegalArgumentException(String.format("%s [%s]应为数字,实际值:%s", rowDisplay, fieldName, value));
@@ -2461,7 +2465,8 @@ public class CostProjectTaskSurveyGenericController {
      */
     private List<String> validateRowFormulasForPeriod(String rowid, Map<String, String> rowData,
                                               List<? extends Object> rowItems, Map<String, String> globalCellCodeMap,
-                                              String period, String type, Map<String, Integer> rowIdToExcelRowMap) {
+                                              String period, String type, Map<String, Integer> rowIdToExcelRowMap,
+                                              Map<String, String> cellCodeToRowIdMap) {
         List<String> errors = new ArrayList<>();
         // 找到该行的计算公式(同一行的所有模板项共享同一个公式)
         String calculationFormula = null;
@@ -2486,7 +2491,6 @@ public class CostProjectTaskSurveyGenericController {
         java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("[A-Z]+\\d+");
         java.util.regex.Matcher matcher = pattern.matcher(calculationFormula);
         List<String> referencedCellsDebug = new ArrayList<>();
-        Map<String, String> cellCodeToItemNameMap = new HashMap<>(); // 存储cellCode到项目名称的映射
 
         while (matcher.find()) {
             String referencedCell = matcher.group();
@@ -2494,10 +2498,6 @@ public class CostProjectTaskSurveyGenericController {
             String value = globalCellCodeMap.get(mapKey);
             String displayValue = StringUtil.isNotEmpty(value) ? value : "0";
 
-            // 获取该cellCode对应的项目名称
-            String itemName = getCellCodeItemName(referencedCell, type);
-            cellCodeToItemNameMap.put(referencedCell, itemName);
-
             referencedCellsDebug.add(referencedCell + "=" + displayValue);
             if (StringUtil.isNotEmpty(value)) {
                 hasAnyReferencedValue = true;
@@ -2547,11 +2547,12 @@ public class CostProjectTaskSurveyGenericController {
                 Integer excelRow = rowIdToExcelRowMap.get(rowid);
                 String rowDisplay = excelRow != null ? "第" + excelRow + "行" : "行[" + rowid + "]";
 
-                // 构建详细的计算过程(包含项目名称)
-                String calculationProcess = buildCalculationProcessWithNames(calculationFormula, referencedCellsDebug, cellCodeToItemNameMap);
+                // 构建详细的计算过程(包含行号、列和值,保留原始公式结构)
+                String calculationProcess = buildCalculationProcessWithNames(calculationFormula, referencedCellsDebug,
+                        cellCodeToRowIdMap, rowIdToExcelRowMap, period);
 
-                errors.add(String.format("%s %s年列数据错误:<br>计算公式:%s<br>计算过程:%s<br>计算结果:%.2f<br>实际填写:%.2f",
-                        rowDisplay, period, calculationFormula, calculationProcess, calculatedValue, inputValue));
+                errors.add(String.format("%s %s年列数据错误:计算过程:%s,计算结果:%.2f,实际填写:%.2f",
+                        rowDisplay, period, calculationProcess, calculatedValue, inputValue));
             } else {
                 System.out.println("校验结果: ✓ 通过");
                 System.out.println("========================================");
@@ -3370,23 +3371,38 @@ public class CostProjectTaskSurveyGenericController {
 
         String fieldEname = null;
         String fieldName = null;
+        String isDict = null;
+        String dictCode = null;
 
         if (header instanceof CostSurveyTemplateHeaders) {
             CostSurveyTemplateHeaders h = (CostSurveyTemplateHeaders) header;
             fieldEname = h.getFieldEname();
             fieldName = h.getFieldName();
+            isDict = h.getIsDict();
+            dictCode = h.getDictCode();
         } else if (header instanceof CostSurveyFdTemplateHeaders) {
             CostSurveyFdTemplateHeaders h = (CostSurveyFdTemplateHeaders) header;
             fieldEname = h.getFieldEname();
             fieldName = h.getFieldName();
+            isDict = h.getIsDict();
+            dictCode = h.getDictCode();
         } else if (header instanceof CostVerifyTemplateHeaders) {
             CostVerifyTemplateHeaders h = (CostVerifyTemplateHeaders) header;
             fieldEname = h.getFieldEname();
             fieldName = h.getFieldName();
+            isDict = h.getIsDict();
+            dictCode = h.getDictCode();
         }
 
         uploadData.setRkey(fieldName);
-        uploadData.setRvalue(value);
+
+        // 如果是字典字段,将value转换为key
+        String finalValue = value;
+        if (("1".equals(isDict) || "true".equalsIgnoreCase(isDict)) && StringUtil.isNotEmpty(dictCode)) {
+            finalValue = convertDictValueToKey(value, dictCode);
+        }
+
+        uploadData.setRvalue(finalValue);
         if (StringUtil.isNotEmpty(periodRecordId)) {
             uploadData.setPeriodRecordId(periodRecordId);
         }
@@ -3395,6 +3411,41 @@ public class CostProjectTaskSurveyGenericController {
     }
 
     /**
+     * 将字典value转换为key
+     */
+    private String convertDictValueToKey(String value, String dictCode) {
+        if (StringUtil.isEmpty(value) || StringUtil.isEmpty(dictCode)) {
+            return value;
+        }
+
+        try {
+            SysType TYPE = sysTypeManager.getOne(
+                    new QueryWrapper<SysType>()
+                            .eq("TYPE_KEY_", dictCode)
+            );
+            if (TYPE == null) {
+                return value;
+            }
+
+            QueryWrapper<DataDict> wrapper = new QueryWrapper<>();
+            wrapper.eq("TYPE_ID_", TYPE.getId());
+            List<DataDict> dictDataList = dataDictManager.list(wrapper);
+
+            if (dictDataList == null || dictDataList.isEmpty()) {
+                return value;
+            }
+
+            return dictDataList.stream()
+                    .filter(d -> value.equals(d.getName()))
+                    .map(DataDict::getKey)
+                    .findFirst()
+                    .orElse(value);
+        } catch (Exception e) {
+            return value;
+        }
+    }
+
+    /**
      * 校验Excel表头是否与导出时一致(成本调查表)
      */
     private List<String> validateExcelHeaders(Row headerRow, List<CostSurveyTemplateHeaders> headersList,
@@ -3529,100 +3580,54 @@ public class CostProjectTaskSurveyGenericController {
     }
 
     /**
-     * 根据cellCode获取对应的项目名称
-     * @param cellCode 单元格编码,如 "A1"
-     * @param type 类型:1-成本调查表 2-财务数据表 3-核定表
-     * @return 项目名称,如 "基本工资"
-     */
-    private String getCellCodeItemName(String cellCode, String type) {
-        try {
-            List<? extends Object> allItems = null;
-
-            // 根据type获取所有模板项
-            switch (type) {
-                case "1": {
-                    // 成本调查表 - 需要获取所有模板的items
-                    List<CostSurveyTemplate> templates = costSurveyTemplateManager.list();
-                    for (CostSurveyTemplate template : templates) {
-                        CostSurveyTemplateVersion version = costSurveyTemplateVersionManager.selectCurrentVersion(template.getSurveyTemplateId());
-                        if (version != null) {
-                            List<CostSurveyTemplateItems> items = costSurveyTemplateItemsDao.selectBySurveyTemplateIdAndVersion(
-                                    template.getSurveyTemplateId(), version.getId());
-                            for (CostSurveyTemplateItems item : items) {
-                                if (cellCode.equals(item.getCellCode())) {
-                                    return item.getRvalue(); // 返回项目名称
-                                }
-                            }
-                        }
-                    }
-                    break;
-                }
-                case "2": {
-                    // 财务数据表
-                    List<CostSurveyFdTemplate> templates = costSurveyFdTemplateManager.list();
-                    for (CostSurveyFdTemplate template : templates) {
-                        CostSurveyFdTemplateVersion version = costSurveyFdTemplateVersionManager.selectCurrentVersion(template.getSurveyTemplateId());
-                        if (version != null) {
-                            List<CostSurveyFdTemplateItems> items = costSurveyFdTemplateItemsDao.selectBySurveyTemplateIdAndVersion(
-                                    template.getSurveyTemplateId(), version.getId());
-                            for (CostSurveyFdTemplateItems item : items) {
-                                if (cellCode.equals(item.getCellCode())) {
-                                    return item.getRvalue(); // 返回项目名称
-                                }
-                            }
-                        }
-                    }
-                    break;
-                }
-                case "3": {
-                    // 核定表
-                    List<CostVerifyTemplate> templates = costVerifyTemplateManager.list();
-                    for (CostVerifyTemplate template : templates) {
-                        List<CostVerifyTemplateItems> items = costVerifyTemplateItemsDao.selectByVerifyTemplateId(
-                                template.getSurveyTemplateId(), null);
-                        for (CostVerifyTemplateItems item : items) {
-                            if (cellCode.equals(item.getCellCode())) {
-                                return item.getRvalue(); // 返回项目名称
-                            }
-                        }
-                    }
-                    break;
-                }
-            }
-        } catch (Exception e) {
-            System.err.println("获取cellCode[" + cellCode + "]对应的项目名称失败: " + e.getMessage());
-        }
-
-        return cellCode; // 如果找不到,返回cellCode本身
-    }
-
-    /**
-     * 构建带项目名称的计算过程字符串
-     * @param formula 原始公式,如 "(A1+A2+A3)"
-     * @param referencedCellsDebug 引用单元格的值列表,如 ["A1=10", "A2=20", "A3=30"]
-     * @param cellCodeToItemNameMap cellCode到项目名称的映射
-     * @return 计算过程字符串,如 "基本工资(10) + 津贴(20) + 奖金(30)"
+     * 构建带行号和列信息的计算过程字符串(保留原始公式结构)
+     * @param formula 原始公式,如 "A1*(A4-A3)" 或 "(Q3/A1)"
+     * @param referencedCellsDebug 引用单元格的值列表,如 ["A1=10", "A4=20", "A3=5"]
+     * @param cellCodeToRowIdMap cellCode到rowid的映射
+     * @param rowIdToExcelRowMap rowid到Excel行号的映射
+     * @param period 当前校验的年限
+     * @return 计算过程字符串,如 "第3行2024年列(值=10)*(第6行2024年列(值=20)-第5行2024年列(值=5))"
      */
     private String buildCalculationProcessWithNames(String formula, List<String> referencedCellsDebug,
-                                                     Map<String, String> cellCodeToItemNameMap) {
+                                                     Map<String, String> cellCodeToRowIdMap,
+                                                     Map<String, Integer> rowIdToExcelRowMap,
+                                                     String period) {
         String process = formula;
 
-        // 将引用单元格替换为 "项目名称(值)" 的格式
+        // 构建 cellCode -> 替换字符串 的映射
+        Map<String, String> replacementMap = new HashMap<>();
         for (String cellDebug : referencedCellsDebug) {
             String[] parts = cellDebug.split("=");
             if (parts.length == 2) {
                 String cellCode = parts[0];
                 String value = parts[1];
-                String itemName = cellCodeToItemNameMap.getOrDefault(cellCode, cellCode);
 
-                // 构建替换字符串:基本工资(10)
-                String replacement = itemName + "(" + value + ")";
+                // 获取该cellCode对应的Excel行号
+                String rowId = cellCodeToRowIdMap.get(cellCode);
+                String rowDisplay;
+                if (rowId != null && rowIdToExcelRowMap.get(rowId) != null) {
+                    rowDisplay = "第" + rowIdToExcelRowMap.get(rowId) + "行";
+                } else {
+                    rowDisplay = "行[" + cellCode + "]";
+                }
 
-                // 使用正则表达式替换,确保只替换完整的单元格引用
-                process = process.replaceAll("\\b" + cellCode + "\\b", replacement);
+                // 构建替换字符串:第3行2024年列(值=10)
+                String replacement = String.format("%s%s年列(值=%s)", rowDisplay, period, value);
+                replacementMap.put(cellCode, replacement);
             }
         }
 
+        // 按cellCode长度降序排序,避免A1被A10的替换影响
+        List<String> sortedCellCodes = new ArrayList<>(replacementMap.keySet());
+        sortedCellCodes.sort((a, b) -> b.length() - a.length());
+
+        // 依次替换公式中的cellCode
+        for (String cellCode : sortedCellCodes) {
+            String replacement = replacementMap.get(cellCode);
+            // 使用正则表达式确保只替换完整的cellCode(避免A1匹配到A10中的A1)
+            process = process.replaceAll("\\b" + cellCode + "\\b", replacement);
+        }
+
         return process;
     }
 

+ 17 - 1
assistMg/src/main/java/com/hotent/project/manager/CostProjectTaskMaterialSummaryManager.java

@@ -15,13 +15,20 @@ import java.util.List;
 public interface CostProjectTaskMaterialSummaryManager extends BaseManager<CostProjectTaskMaterialSummary> {
 
     /**
-     * 根据taskId查询资料归纳列表(包含明细)
+     * 根据taskId查询资料归纳列表(包含明细,不含封面/目录/封底
      * @param taskId 任务ID
      * @return 资料归纳列表
      */
     List<CostProjectTaskMaterialSummary> listByTaskId(String taskId);
 
     /**
+     * 根据taskId查询全部资料归纳列表(包含封面/目录/封底)
+     * @param taskId 任务ID
+     * @return 全部资料归纳列表
+     */
+    List<CostProjectTaskMaterialSummary> listAllByTaskId(String taskId);
+
+    /**
      * 根据id查询资料归纳详情(包含明细)
      * @param id 主表ID
      * @return 资料归纳详情
@@ -52,5 +59,14 @@ public interface CostProjectTaskMaterialSummaryManager extends BaseManager<CostP
      * @param direction 排序方向:up-上移,down-下移
      */
     void sort(String id, String direction);
+
+    /**
+     * 获取卷宗文书(封面/目录/封底)
+     * @param taskId 任务ID
+     * @param documentType 文书类型:1-案卷封面 2-卷内目录 3-案卷封底
+     * @return 卷宗文书记录
+     */
+    CostProjectTaskMaterialSummary getArchiveDocument(String taskId, Integer documentType);
+
 }
 

+ 3 - 0
assistMg/src/main/java/com/hotent/project/manager/impl/CostProjectTaskManagerImpl.java

@@ -279,6 +279,9 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         if (StringUtil.isNotEmpty(req.getYear())) {
             queryWrapper.eq(CostProjectTask::getYear, req.getYear());
         }
+        if (StringUtil.isNotEmpty(req.getIsGd())) {
+            queryWrapper.eq(CostProjectTask::getIsGd, req.getIsGd());
+        }
         queryWrapper.eq(req.getCurrentNode() != null, CostProjectTask::getCurrentNode, req.getCurrentNode());
         queryWrapper.ne(req.getNCurrentNode() != null, CostProjectTask::getCurrentNode, req.getNCurrentNode());
         queryWrapper.ne(req.getNStatus() != null, CostProjectTask::getStatus, req.getNStatus());

+ 2 - 0
assistMg/src/main/java/com/hotent/project/manager/impl/CostProjectTaskMaterialManagerImpl.java

@@ -73,6 +73,8 @@ public class CostProjectTaskMaterialManagerImpl extends BaseManagerImpl<CostProj
 					} else if (isExcelFile(fileExtension)) {
 						// Excel文件设置为2
 						costProjectTaskMaterial.setInformationType("2");
+					}else{
+						costProjectTaskMaterial.setInformationType("1");
 					}
 				}
 			}

+ 40 - 1
assistMg/src/main/java/com/hotent/project/manager/impl/CostProjectTaskMaterialSummaryManagerImpl.java

@@ -34,7 +34,29 @@ public class CostProjectTaskMaterialSummaryManagerImpl extends BaseManagerImpl<C
             return java.util.Collections.emptyList();
         }
 
-        // 查询主表列表
+        // 查询主表列表,过滤掉封面(-1)、目录(-2)、封底(-3)
+        LambdaQueryWrapper<CostProjectTaskMaterialSummary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(CostProjectTaskMaterialSummary::getTaskId, taskId)
+                .and(w -> w.isNull(CostProjectTaskMaterialSummary::getMaterialOrderNum)
+                        .or()
+                        .ge(CostProjectTaskMaterialSummary::getMaterialOrderNum, 0))
+                .orderByAsc(CostProjectTaskMaterialSummary::getMaterialOrderNum);
+
+        List<CostProjectTaskMaterialSummary> summaryList = this.list(queryWrapper);
+
+        // 填充明细列表
+        fillDetailList(summaryList);
+
+        return summaryList;
+    }
+
+    @Override
+    public List<CostProjectTaskMaterialSummary> listAllByTaskId(String taskId) {
+        if (StringUtil.isEmpty(taskId)) {
+            return java.util.Collections.emptyList();
+        }
+
+        // 查询全部主表列表(包含封面/目录/封底)
         LambdaQueryWrapper<CostProjectTaskMaterialSummary> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(CostProjectTaskMaterialSummary::getTaskId, taskId)
                 .orderByAsc(CostProjectTaskMaterialSummary::getMaterialOrderNum);
@@ -262,5 +284,22 @@ public class CostProjectTaskMaterialSummaryManagerImpl extends BaseManagerImpl<C
             this.updateById(current);
         }
     }
+
+    @Override
+    public CostProjectTaskMaterialSummary getArchiveDocument(String taskId, Integer documentType) {
+        if (StringUtil.isEmpty(taskId) || documentType == null) {
+            return null;
+        }
+
+        // 使用负数序号区分卷宗文书:-1=封面,-2=目录,-3=封底
+        int orderNum = -documentType;
+
+        LambdaQueryWrapper<CostProjectTaskMaterialSummary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(CostProjectTaskMaterialSummary::getTaskId, taskId)
+                .eq(CostProjectTaskMaterialSummary::getMaterialOrderNum, orderNum);
+
+        return this.getOne(queryWrapper);
+    }
+
 }
 

+ 14 - 1
assistMg/src/main/java/com/hotent/project/model/CostProjectTask.java

@@ -150,6 +150,19 @@ public class CostProjectTask extends BaseModel<CostProjectTask> {
     @JsonProperty("warningStatus")
     private String warningStatus;
 
-
+    @ApiModelProperty(value = "卷宗生成状态:0-未生成 1-生成中 2-已生成 3-生成失败")
+    @TableField("archive_status")
+    @JsonProperty("archiveStatus")
+    private String archiveStatus;
+
+    @ApiModelProperty(value = "最终卷宗URL")
+    @TableField("archive_url")
+    @JsonProperty("archiveUrl")
+    private String archiveUrl;
+
+    @ApiModelProperty(value = "卷宗生成时间")
+    @TableField("archive_time")
+    @JsonProperty("archiveTime")
+    private LocalDateTime archiveTime;
 
 }

+ 62 - 0
assistMg/src/main/java/com/hotent/project/req/ArchiveProofreadReq.java

@@ -0,0 +1,62 @@
+package com.hotent.project.req;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 卷宗校对请求类
+ *
+ * 自动获取的字段(无需传递):
+ * - 年份(从任务表year字段获取)
+ * - 监审项目名称(从任务表projectName字段获取)
+ * - 被监审单位(从任务表auditedUnitName字段获取)
+ * - 监审开始/结束年度(从任务表auditPeriod字段解析)
+ * - 监审组负责人及人员(从立项审批表auditGroup字段获取)
+ * - 监审办结时间(从任务表updateTime字段获取,状态为办结时)
+ *
+ * @company 山西清众科技股份有限公司
+ * @author 超级管理员
+ * @since 2025-12-08
+ */
+@ApiModel(value = "ArchiveProofreadReq", description = "卷宗校对请求")
+@Data
+public class ArchiveProofreadReq implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "任务ID", required = true)
+    @JsonProperty("taskId")
+    private String taskId;
+
+    @ApiModelProperty(value = "文书ID(为空新增,不为空编辑)")
+    @JsonProperty("id")
+    private String id;
+
+    @ApiModelProperty(value = "文书类型:1-案卷封面 2-卷内目录 3-案卷封底(成本监审卷宗备考表),新增时必填")
+    @JsonProperty("documentType")
+    private Integer documentType;
+
+    // ========== 需要用户传递的字段 ==========
+
+    @ApiModelProperty(value = "价格主管部门或成本监审机构")
+    @JsonProperty("priceAuthority")
+    private String priceAuthority;
+
+    @ApiModelProperty(value = "卷宗号")
+    @JsonProperty("archiveNo")
+    private String archiveNo;
+
+    @ApiModelProperty(value = "保管期间周期")
+    @JsonProperty("retentionPeriod")
+    private String retentionPeriod;
+
+
+
+    @ApiModelProperty(value = "备注")
+    @JsonProperty("remark")
+    private String remark;
+}

+ 62 - 0
assistMg/src/main/java/com/hotent/project/resp/ArchiveProofreadResp.java

@@ -0,0 +1,62 @@
+package com.hotent.project.resp;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 卷宗校对响应类
+ *
+ * @company 山西清众科技股份有限公司
+ * @author 超级管理员
+ * @since 2025-12-08
+ */
+@ApiModel(value = " ", description = "卷宗校对响应")
+@Data
+public class ArchiveProofreadResp implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "序号")
+    @JsonProperty("orderNum")
+    private Integer orderNum;
+
+    @ApiModelProperty(value = "资料名称")
+    @JsonProperty("materialName")
+    private String materialName;
+
+    @ApiModelProperty(value = "资料页数")
+    @JsonProperty("pageCount")
+    private Integer pageCount;
+
+    @ApiModelProperty(value = "起止页码")
+    @JsonProperty("pageRange")
+    private String pageRange;
+
+    @ApiModelProperty(value = "文书类型:0-普通资料 1-案卷封面 2-卷内目录 3-案卷封底")
+    @JsonProperty("documentType")
+    private Integer documentType;
+
+    @ApiModelProperty(value = "是否可生成:0-否 1-是")
+    @JsonProperty("canGenerate")
+    private String canGenerate;
+
+    @ApiModelProperty(value = "是否可编辑:0-否 1-是")
+    @JsonProperty("canEdit")
+    private String canEdit;
+
+    @ApiModelProperty(value = "关联ID(资料归纳主表ID或卷宗文书ID)")
+    @JsonProperty("relatedId")
+    private String relatedId;
+
+    @ApiModelProperty(value = "附件URL")
+    @JsonProperty("attachmentUrl")
+    private String attachmentUrl;
+
+    @ApiModelProperty(value = "文书生成状态:0-未生成 1-已生成")
+    @JsonProperty("generateStatus")
+    private String generateStatus;
+}

+ 696 - 116
assistMg/src/main/java/com/hotent/project/service/AsyncMaterialSummaryService.java

@@ -1,22 +1,31 @@
 package com.hotent.project.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.hotent.base.util.StringUtil;
+import com.hotent.config.EipConfig;
 import com.hotent.project.manager.*;
 import com.hotent.project.model.*;
 import com.hotent.surveyinfo.manager.CostSurveyTemplateUploadManager;
 import com.hotent.surveyinfo.model.CostSurveyTemplateUpload;
-import org.apache.poi.xwpf.usermodel.XWPFDocument;
-import org.apache.poi.xwpf.usermodel.XWPFParagraph;
-import org.apache.poi.xwpf.usermodel.XWPFRun;
+import com.hotent.uc.manager.OrgManager;
+import com.hotent.uc.manager.UserManager;
+import com.hotent.uc.model.Org;
+import com.hotent.uc.model.User;
+import com.hotent.util.FileUploadUtil;
+import com.hotent.util.wordexcelutils.CompleteTemplateProcessor;
+import com.hotent.util.wordexcelutils.SmartTemplateWriter;
+import org.apache.poi.xwpf.usermodel.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import java.io.FileOutputStream;
+import java.time.LocalDateTime;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 /**
@@ -29,6 +38,8 @@ import java.util.stream.Collectors;
 @Service
 public class AsyncMaterialSummaryService {
 
+    private static final Logger logger = LoggerFactory.getLogger(AsyncMaterialSummaryService.class);
+
     @Autowired
     private CostProjectTaskMaterialSummaryManager costProjectTaskMaterialSummaryManager;
 
@@ -44,7 +55,6 @@ public class AsyncMaterialSummaryService {
     @Autowired
     private CostSurveyTemplateUploadManager costSurveyTemplateUploadManager;
 
-
     @Autowired
     private CostProjectTaskMaterialManager costProjectTaskMaterialManager;
 
@@ -54,25 +64,42 @@ public class AsyncMaterialSummaryService {
     @Autowired
     private CostProjectTaskAdjustmentRecordManager adjustmentRecordManager;
 
-
     @Autowired
     private CostProjectTaskEvidenceManager costProjectTaskEvidenceManager;
 
     @Autowired
     private CostProjectTaskPreliminaryOpinionManager preliminaryOpinionManager;
 
+    @Autowired
+    private CostProjectApprovalManager costProjectApprovalManager;
+
+
+    @Autowired
+    private OrgManager orgManager;
+
+    @Autowired
+    private UserManager userManager;
+
+    // 从配置文件读取模板路径
+    @org.springframework.beans.factory.annotation.Value("${archive.template.coverPath:}")
+    private String coverTemplatePath;
 
+    @org.springframework.beans.factory.annotation.Value("${archive.template.catalogPath:}")
+    private String catalogTemplatePath;
+
+    @org.springframework.beans.factory.annotation.Value("${archive.template.backCoverPath:}")
+    private String backCoverTemplatePath;
 
 
     /**
-     * 生成资料归纳主表(13个基础资料类别)- 核心逻辑
+     * 生成资料归纳主表(14个基础资料类别)- 核心逻辑
      *
      * @param mainTask 主任务
      * @param childTasks 子任务列表
      */
     public void generateMaterialSummary(CostProjectTask mainTask, List<CostProjectTask> childTasks) {
         try {
-            // 定义13个基础资料类别
+            // 定义14个基础资料类别
             String[] materialNames = {
                     "成本监审报告(含成本监审报告签发稿、送达回证)",
                     "被监审单位申请定(调)价报告(复印件)",
@@ -86,10 +113,10 @@ public class AsyncMaterialSummaryService {
                     "成本监审集体审议记录",
                     "成本监审工作底稿",
                     "成本监审提取资料登记表",
-                    "提取的成本资料和会计凭证等复印件"
+                    "中止定价成本监审料通知书(含送达回证)"
             };
 
-            // 为主任务生成13个资料归纳主表记录
+            // 为主任务生成14个资料归纳主表记录
             for (int i = 0; i < materialNames.length; i++) {
                 CostProjectTaskMaterialSummary summary = new CostProjectTaskMaterialSummary();
                 summary.setTaskId(mainTask.getId());
@@ -103,7 +130,7 @@ public class AsyncMaterialSummaryService {
             }
         } catch (Exception e) {
             // 记录异常日志,但不影响主流程
-            e.printStackTrace();
+            logger.error("生成资料归纳失败", e);
         }
     }
 
@@ -179,17 +206,21 @@ public class AsyncMaterialSummaryService {
                     // 提取的成本资料和会计凭证等复印件
                     generateType13Details(summary, mainTask, childTasks, documents);
                     break;
+                case 14:
+                    // 中止定价成本监审料通知书(含送达回证)
+                    generateType14Details(summary, mainTask, childTasks, documents);
                 default:
                     throw new RuntimeException("未知的资料类型");
             }
         } catch (Exception e) {
-            System.err.println("生成资料类型 " + materialType + " 的明细时出错:" + e.getMessage());
-            e.printStackTrace();
+            logger.error("生成资料类型 {} 的明细时出错:{}", materialType, e.getMessage(), e);
             // 继续执行,不中断整个流程
         }
     }
 
     // ==================== 类型1:成本监审报告(含成本监审报告签发稿、送达回证) ====================
+    // A.成本监审报告(含成本监审报告签发稿)
+    //  包括成本监审报告、报告签发稿等文书。
     private void generateType1Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectDocument> documents) {
         int orderNum = 1;
         // 获取"成本监审报告"、"成本监审报告签发稿"、"送达回证"
@@ -215,6 +246,8 @@ public class AsyncMaterialSummaryService {
     }
 
     // ==================== 类型2:被监审单位申请定(调)价报告(复印件) ====================
+    // 被监审单位申请定(调)价报告等。
+    //来源:从“监审立项”附件获取。
     private void generateType2Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectTask> childTasks, List<CostProjectDocument> documents) {
         int orderNum = 1;
         // 获取"申请定(调)价报告"
@@ -237,15 +270,32 @@ public class AsyncMaterialSummaryService {
             detail.setIsDeleted("0");
             costProjectTaskMaterialSummaryDetailManager.save(detail);
         }
+        CostProjectApproval approval = costProjectApprovalManager.getById(mainTask.getProjectId());
+        if (approval != null && StringUtil.isNotEmpty(approval.getAccordingFileUrl())) {
+            CostProjectTaskMaterialSummaryDetail detail = new CostProjectTaskMaterialSummaryDetail();
+            detail.setMasterId(summary.getId());
+            detail.setTaskId(mainTask.getId());
+            detail.setDocumentName("成本项目定(调)价依据");
+            detail.setDocumentNumber("");
+            detail.setAuditedUnitName("");
+            detail.setFileSource("系统生成电子文书");
+            detail.setPageCount(0);
+            detail.setAttachmentUrl(approval.getAccordingFileUrl());
+            detail.setOrderNum(orderNum++);
+            detail.setIsDeleted("0");
+            costProjectTaskMaterialSummaryDetailManager.save(detail);
+        }
     }
 
     // ==================== 类型3:成本监审通知书(含送达回证) ====================
+    // 包括成本监审通知书、送达回证等文书。
+    //来源:从“监审文书”中获取。
     private void generateType3Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectTask> childTasks, List<CostProjectDocument> documents) {
         int orderNum = 1;
         // "成本监审通知书"和"送达回证"
         List<CostProjectDocument> matchedDocuments = documents.stream()
                 .filter(doc -> "成本监审通知书".equals(doc.getDocumentName())
-                        || "送达回证".equals(doc.getDocumentName()))
+                        || "成本监审通知书-送达回证".equals(doc.getDocumentName()))
                 .sorted(Comparator.comparing(doc -> doc.getOrderNum() != null ? doc.getOrderNum() : 0))
                 .collect(Collectors.toList());
 
@@ -265,7 +315,9 @@ public class AsyncMaterialSummaryService {
         }
     }
 
-    // ==================== 类型4:经营者需提供成本资料清单 ====================
+    // ==================== 类型4:经营者需提供成本资料清单(提取资料) ====================
+    // 成本监审任务中的资料报送要求。
+    //来源:从“监审任务部署”中获取。
     private void generateType4Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectTask> childTasks, List<CostProjectDocument> documents) {
         int orderNum = 1;
         // 获取"成本资料清单"
@@ -288,9 +340,34 @@ public class AsyncMaterialSummaryService {
             detail.setIsDeleted("0");
             costProjectTaskMaterialSummaryDetailManager.save(detail);
         }
+        for (CostProjectTask childTask : childTasks) {
+            List<CostProjectTaskEvidence> list = costProjectTaskEvidenceManager.list(
+                    new LambdaQueryWrapper<CostProjectTaskEvidence>().eq(CostProjectTaskEvidence::getTaskId, childTask.getId())
+            );
+            for (CostProjectTaskEvidence evidence : list) {
+                // 只处理有附件的资料登记
+                if (StringUtil.isNotEmpty(evidence.getAttachmentUrl())) {
+                    CostProjectTaskMaterialSummaryDetail detail = new CostProjectTaskMaterialSummaryDetail();
+                    detail.setMasterId(summary.getId());
+                    detail.setTaskId(mainTask.getId());
+                    detail.setDocumentName(evidence.getMaterialName() != null ? evidence.getMaterialName() : "提取资料登记");
+                    detail.setDocumentNumber("");
+                    detail.setAuditedUnitName(childTask.getAuditedUnitName() != null ? childTask.getAuditedUnitName() : "");
+                    detail.setFileSource("系统生成电子文书");
+                    detail.setPageCount(evidence.getPageCount() != null ? evidence.getPageCount() : 0);
+                    detail.setAttachmentUrl(evidence.getAttachmentUrl());
+                    detail.setOrderNum(orderNum++);
+                    detail.setIsDeleted("0");
+                    costProjectTaskMaterialSummaryDetailManager.save(detail);
+                }
+            }
+
+        }
     }
 
     // ==================== 类型5:《政府定价成本监审调查表》 ====================
+    // 各被监审单位的“成本监审调查表”文档。
+    //来源:从“监审任务”中获取各被监审单位填报的成本监审调查表。
     private void generateType5Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectTask> childTasks, List<CostProjectDocument> documents) {
         int orderNum = 1;
         for (CostProjectTask childTask : childTasks) {
@@ -312,9 +389,12 @@ public class AsyncMaterialSummaryService {
                 }
             }
         }
+        // 成本调查表(kkkk)
     }
 
     // ==================== 类型6:成本监审补充资料通知书(含送达回证) ====================
+    // 监审主体发送给各被监审单位的补充资料通知书、送达回证等文书。
+    //来源:从“监审文书”中获取。
     private void generateType6Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectTask> childTasks, List<CostProjectDocument> documents) {
         int orderNum = 1;
         List<CostProjectDocument> matchedDocuments = documents.stream()
@@ -362,12 +442,14 @@ public class AsyncMaterialSummaryService {
     }
 
     // ==================== 类型7:成本审核初步意见告知书(含送达回证) ====================
+    // 监审主体发送给各被监审单位的初步意见告知书、送达回证等文书。
+    // 来源:从“监审文书”中获取。
     private void generateType7Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectTask> childTasks, List<CostProjectDocument> documents) {
         int orderNum = 1;
         // 获取"成本审核初步意见告知书"和"送达回证"
         List<CostProjectDocument> matchedDocuments = documents.stream()
                 .filter(doc -> "成本审核初步意见告知书".equals(doc.getDocumentName())
-                        || "送达回证".equals(doc.getDocumentName()))
+                        || "成本审核初步意见告知书-送达回证".equals(doc.getDocumentName()))
                 .sorted(Comparator.comparing(doc -> doc.getOrderNum() != null ? doc.getOrderNum() : 0))
                 .collect(Collectors.toList());
 
@@ -388,6 +470,8 @@ public class AsyncMaterialSummaryService {
     }
 
     // ==================== 类型8:经营者书面反馈的材料 ====================
+    // 各被监审单位对初步意见告知书的意见反馈资料。
+    //来源:从“监审任务”中获取反馈意见。
     private void generateType8Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectTask> childTasks, List<CostProjectDocument> documents) {
         int orderNum = 1;
         // 获取"书面反馈材料"(来源:监审单位反馈文件)
@@ -437,6 +521,7 @@ public class AsyncMaterialSummaryService {
     }
 
     // ==================== 类型9:成本审核初步意见表(集体审议用) ====================
+    // 来源:从“监审文书”中获取
     private void generateType9Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectTask> childTasks, List<CostProjectDocument> documents) {
         int orderNum = 1;
         // 获取"成本审核初步意见表"
@@ -462,6 +547,7 @@ public class AsyncMaterialSummaryService {
     }
 
     // ==================== 类型10:成本监审集体审议记录 ====================
+    // 来源:从“监审文书”中获取。
     private void generateType10Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectDocument> documents) {
         int orderNum = 1;
         // 获取"成本监审集体审议记录"
@@ -508,6 +594,7 @@ public class AsyncMaterialSummaryService {
     }
 
     // ==================== 类型11:成本监审工作底稿 ====================
+    // 来源:从“监审文书”中获取。
     private void generateType11Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectTask> childTasks, List<CostProjectDocument> documents) {
         int orderNum = 1;
         // 获取"成本监审工作底稿"
@@ -554,6 +641,7 @@ public class AsyncMaterialSummaryService {
     }
 
     // ==================== 类型12:成本监审提取资料登记表 ====================
+    // 来源:从“监审文书”中获取。
     private void generateType12Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectTask> childTasks, List<CostProjectDocument> documents) {
         int orderNum = 1;
         // 获取"成本监审提取资料登记表"
@@ -602,6 +690,7 @@ public class AsyncMaterialSummaryService {
     }
 
     // ==================== 类型13:提取的成本资料和会计凭证等复印件 ====================
+    // 来源:各被监审单位首次及补充材料时提交的资料,包括综合性材料、财会资料等。
     private void generateType13Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectTask> childTasks, List<CostProjectDocument> documents) {
         int orderNum = 1;
         // 获取"成本资料和会计凭证"(来源:被监审单位提交的资料)
@@ -626,109 +715,176 @@ public class AsyncMaterialSummaryService {
         }
     }
 
+    // ==================== 类型14:中止定价成本监审料通知书(含送达回证) ====================
+    // 来源:从“监审文书”中获取。
+    private void generateType14Details(CostProjectTaskMaterialSummary summary, CostProjectTask mainTask, List<CostProjectTask> childTasks, List<CostProjectDocument> documents) {
+        int orderNum = 1;
+        // 获取"成本资料和会计凭证"(来源:被监审单位提交的资料)
+        List<CostProjectDocument> matchedDocuments = documents.stream()
+                .filter(doc -> "中止定价成本监审通知书".equals(doc.getDocumentName())
+                        || "中止定价成本监审通知书-送达回证".equals(doc.getDocumentName()))
+                .sorted(Comparator.comparing(doc -> doc.getOrderNum() != null ? doc.getOrderNum() : 0))
+                .collect(Collectors.toList());
+
+        for (CostProjectDocument document : matchedDocuments) {
+            CostProjectTaskMaterialSummaryDetail detail = new CostProjectTaskMaterialSummaryDetail();
+            detail.setMasterId(summary.getId());
+            detail.setTaskId(mainTask.getId());
+            detail.setDocumentName(document.getDocumentName());
+            detail.setDocumentNumber(document.getDocumentNumber() != null ? document.getDocumentNumber() : "");
+            detail.setAuditedUnitName("");
+            detail.setFileSource("监审单位反馈文件");
+            detail.setPageCount(0);
+            detail.setAttachmentUrl(document.getFeedbackDocumentUrl() != null ? document.getFeedbackDocumentUrl() : "");
+            detail.setOrderNum(orderNum++);
+            detail.setIsDeleted("0");
+            costProjectTaskMaterialSummaryDetailManager.save(detail);
+        }
+    }
+
     /**
      * 档案校对通过后,异步生成Word卷宗
+     * 顺序:封面(-1) -> 目录(-2) -> 14个资料(1-13) -> 封底(-3)
      *
      * @param taskId 任务ID
      */
     @Async
     public void generateWordArchiveAsync(String taskId) {
+        CostProjectTask task = costProjectTaskManager.getById(taskId);
         try {
-            // 1. 查询任务的所有资料归纳主表(按序号排序)
-            // 2. 遍历每个主表,获取其明细列表(按序号排序)
-            // 3. 按顺序读取每个明细对应的文件
-            // 4. 将所有文件合并为一个完整的Word文档
-            // 5. 保存生成的Word卷宗文件
+            logger.info("开始生成Word卷宗,任务ID:{}", taskId);
 
-            System.out.println("开始生成Word卷宗,任务ID:" + taskId);
+            // 查询任务的所有资料归纳主表(包含封面、目录、封底)
+            List<CostProjectTaskMaterialSummary> allSummaryList =
+                costProjectTaskMaterialSummaryManager.listAllByTaskId(taskId);
 
-            // 示例代码框架(需要根据实际情况实现)
-             List<CostProjectTaskMaterialSummary> summaryList =
-                 costProjectTaskMaterialSummaryManager.listByTaskId(taskId);
+            // 创建Word文档
+            XWPFDocument document = new XWPFDocument();
+            boolean isFirstDocument = true;
+
+            // 按顺序合并:封面(-1) -> 目录(-2) -> 14个资料(1-13) -> 封底(-3)
+            // 排序:-1, -2, 1, 2, ..., 13, -3
+            allSummaryList.sort((a, b) -> {
+                int orderA = a.getMaterialOrderNum() != null ? a.getMaterialOrderNum() : 0;
+                int orderB = b.getMaterialOrderNum() != null ? b.getMaterialOrderNum() : 0;
+                // -1(封面) < -2(目录) < 1-13(资料) < -3(封底)
+                int sortA = orderA == -1 ? -100 : (orderA == -2 ? -99 : (orderA == -3 ? 100 : orderA));
+                int sortB = orderB == -1 ? -100 : (orderB == -2 ? -99 : (orderB == -3 ? 100 : orderB));
+                return Integer.compare(sortA, sortB);
+            });
+
+            // 遍历所有主表,从明细表获取附件URL进行合并
+            for (CostProjectTaskMaterialSummary summary : allSummaryList) {
+                logger.info("合并资料:{}", summary.getMaterialName());
+                List<CostProjectTaskMaterialSummaryDetail> detailList = summary.getDetailList();
+                if (detailList != null && !detailList.isEmpty()) {
+                    detailList.sort(Comparator.comparing(d -> d.getOrderNum() != null ? d.getOrderNum() : 0));
+                    for (CostProjectTaskMaterialSummaryDetail detail : detailList) {
+                        if (StringUtil.isNotEmpty(detail.getAttachmentUrl())) {
+                            isFirstDocument = mergeDocumentFile(document, detail.getAttachmentUrl(), isFirstDocument);
+                        }
+                    }
+                }
+            }
 
-            // 按序号排序
-             summaryList.sort(Comparator.comparing(CostProjectTaskMaterialSummary::getMaterialOrderNum));
+            // 生成输出文件路径
+            String fileName = FileUploadUtil.generateFileName("卷宗_" + taskId + ".docx");
+            String outputPath = FileUploadUtil.generateSavePath(
+                EipConfig.getUploadPath(), fileName, "docx");
 
-            // 创建Word文档
-             XWPFDocument document = new XWPFDocument();
-
-             for (CostProjectTaskMaterialSummary summary : summaryList) {
-                 // 获取明细列表
-                 List<CostProjectTaskMaterialSummaryDetail> detailList = summary.getDetailList();
-                 if (detailList != null) {
-                     detailList.sort(Comparator.comparing(CostProjectTaskMaterialSummaryDetail::getOrderNum));
-
-                     for (CostProjectTaskMaterialSummaryDetail detail : detailList) {
-                         // 读取文件内容并添加到Word文档
-                         if (StringUtil.isNotEmpty(detail.getAttachmentUrl())) {
-                             try {
-
-                                 // 读取Word文件
-                                 java.io.FileInputStream fis = new java.io.FileInputStream(detail.getAttachmentUrl());
-                                 XWPFDocument sourceDoc = new XWPFDocument(fis);
-
-                                 // 合并段落
-                                 for (XWPFParagraph srcPara : sourceDoc.getParagraphs()) {
-                                     XWPFParagraph destPara = document.createParagraph();
-                                     // 复制段落样式
-                                     destPara.setAlignment(srcPara.getAlignment());
-                                     destPara.setIndentationLeft(srcPara.getIndentationLeft());
-                                     destPara.setIndentationRight(srcPara.getIndentationRight());
-                                     destPara.setSpacingBefore(srcPara.getSpacingBefore());
-                                     destPara.setSpacingAfter(srcPara.getSpacingAfter());
-                                     for (XWPFRun srcRun : srcPara.getRuns()) {
-                                         XWPFRun destRun = destPara.createRun();
-                                         destRun.setText(srcRun.getText(0));
-                                         destRun.setBold(srcRun.isBold());
-                                         destRun.setItalic(srcRun.isItalic());
-                                         destRun.setUnderline(srcRun.getUnderline());
-                                         destRun.setStrikeThrough(srcRun.isStrikeThrough());
-                                         destRun.setFontSize(srcRun.getFontSize());
-                                         if (srcRun.getFontFamily() != null) {
-                                             destRun.setFontFamily(srcRun.getFontFamily());
-                                         }
-                                         if (srcRun.getColor() != null) {
-                                             destRun.setColor(srcRun.getColor());
-                                         }
-                                     }
-                                 }
-
-                                 // 添加分页符(每个文档之间分页)
-                                 XWPFParagraph pageBreakPara = document.createParagraph();
-                                 XWPFRun pageBreakRun = pageBreakPara.createRun();
-                                 pageBreakRun.addBreak(org.apache.poi.xwpf.usermodel.BreakType.PAGE);
-
-                                 sourceDoc.close();
-                                 fis.close();
-                             } catch (Exception e) {
-                                 System.err.println("合并文件失败:" + detail.getDocumentName() + ",错误:" + e.getMessage());
-                                 e.printStackTrace();
-                             }
-                         }
-                     }
-                 }
-             }
-
-             //保存Word文档
-             String outputPath = "卷宗_" + taskId + ".docx";
-             FileOutputStream out = new FileOutputStream(outputPath);
-             document.write(out);
-             out.close();
-             document.close();
-
-            System.out.println("Word卷宗生成完成,任务ID:" + taskId);
-
-            // 更新任务的归档状态
-            CostProjectTask task = costProjectTaskManager.getById(taskId);
+            // 确保目录存在
+            java.io.File outputFile = new java.io.File(outputPath);
+            if (!outputFile.getParentFile().exists()) {
+                outputFile.getParentFile().mkdirs();
+            }
+
+            // 保存Word文档
+            FileOutputStream out = new FileOutputStream(outputPath);
+            document.write(out);
+            out.close();
+            document.close();
+
+            // 生成访问URL
+            String archiveUrl = FileUploadUtil.getPathFileName(outputPath, fileName);
+
+            logger.info("Word卷宗生成完成,任务ID:{},URL:{}", taskId, archiveUrl);
+
+            // 更新任务的卷宗URL
+            if (task != null) {
+                task.setArchiveUrl(archiveUrl);
+                task.setArchiveStatus("2");
+                task.setArchiveTime(LocalDateTime.now());
+                costProjectTaskManager.updateById(task);
+                logger.info("卷宗URL已更新,任务ID:{}", taskId);
+            }
+
+        } catch (Exception e) {
             if (task != null) {
-                task.setIsGd("1");
+                task.setArchiveStatus("3");
                 costProjectTaskManager.updateById(task);
-                System.out.println("任务归档状态已更新,任务ID:" + taskId);
+            }
+            logger.error("生成Word卷宗失败,任务ID:,错误:{}", taskId, e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 合并单个文档文件到主文档(保持原始样式)
+     *
+     * @param document 主文档
+     * @param fileUrl 文件URL(如:/profile/upload/20251116/xxx.docx)
+     * @param isFirstDocument 是否是第一个文档
+     * @return 是否是第一个文档(用于下次调用)
+     */
+    private boolean mergeDocumentFile(XWPFDocument document, String fileUrl, boolean isFirstDocument) {
+        try {
+            String filePath = fileUrl;
+            // 处理路径:/profile 开头的路径需要替换为实际路径
+            if (filePath.startsWith("/profile")) {
+                filePath = EipConfig.getProfile() + filePath.substring(8);
+            }
+
+            java.io.File file = new java.io.File(filePath);
+            if (!file.exists()) {
+                logger.warn("文件不存在:{}(原始路径:{})", filePath, fileUrl);
+                return isFirstDocument;
             }
 
+            // 添加分页符(非第一个文档)
+            if (!isFirstDocument) {
+                XWPFParagraph pageBreakPara = document.createParagraph();
+                XWPFRun pageBreakRun = pageBreakPara.createRun();
+                pageBreakRun.addBreak(BreakType.PAGE);
+            }
+
+            // 读取Word文件
+            java.io.FileInputStream fis = new java.io.FileInputStream(filePath);
+            XWPFDocument sourceDoc = new XWPFDocument(fis);
+
+            // 使用XML级别复制,保持原始样式
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody srcBody = sourceDoc.getDocument().getBody();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody destBody = document.getDocument().getBody();
+
+            // 复制所有元素(段落、表格等)
+            for (org.apache.xmlbeans.XmlObject obj : srcBody.selectPath("./*")) {
+                if (obj instanceof org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP) {
+                    // 复制段落
+                    org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP newP = destBody.addNewP();
+                    newP.set(obj.copy());
+                } else if (obj instanceof org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl) {
+                    // 复制表格
+                    org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl newTbl = destBody.addNewTbl();
+                    newTbl.set(obj.copy());
+                }
+            }
+
+            sourceDoc.close();
+            fis.close();
+
+            return false;
+
         } catch (Exception e) {
-            e.printStackTrace();
-            System.err.println("生成Word卷宗失败,任务ID:" + taskId + ",错误:" + e.getMessage());
+            logger.error("合并文件失败:{},错误:{}", fileUrl, e.getMessage(), e);
+            return isFirstDocument;
         }
     }
 
@@ -741,7 +897,7 @@ public class AsyncMaterialSummaryService {
         try {
             CostProjectTaskMaterialSummary summary = costProjectTaskMaterialSummaryManager.getById(masterId);
             if (summary == null) {
-                System.err.println("资料归纳主表不存在,ID:" + masterId);
+                logger.warn("资料归纳主表不存在,ID:{}", masterId);
                 return;
             }
 
@@ -765,22 +921,21 @@ public class AsyncMaterialSummaryService {
             summary.setTotalPageCount(String.valueOf(totalPageCount));
             costProjectTaskMaterialSummaryManager.updateById(summary);
 
-            System.out.println("资料归纳【" + summary.getMaterialName() + "】总页数已更新:" + totalPageCount);
+            logger.info("资料归纳【{}】总页数已更新:{}", summary.getMaterialName(), totalPageCount);
 
         } catch (Exception e) {
-            e.printStackTrace();
-            System.err.println("更新主表总页数失败,主表ID:" + masterId + ",错误:" + e.getMessage());
+            logger.error("更新主表总页数失败,主表ID:{},错误:{}", masterId, e.getMessage(), e);
         }
     }
 
     /**
-     * 异步统计每个资料归纳主表的总页数(所有明细的页数之和)
+     * 统计每个资料归纳主表的总页数(所有明细的页数之和)
      *
      * @param taskId 任务ID
      */
     public void calculatePageCountAsync(String taskId) {
         try {
-            System.out.println("开始异步统计页数,任务ID:" + taskId);
+            logger.info("开始异步统计页数,任务ID:{}", taskId);
 
             // 查询任务的所有资料归纳主表
             List<CostProjectTaskMaterialSummary> summaryList = costProjectTaskMaterialSummaryManager.listByTaskId(taskId);
@@ -806,14 +961,13 @@ public class AsyncMaterialSummaryService {
                 summary.setTotalPageCount(String.valueOf(totalPageCount));
                 costProjectTaskMaterialSummaryManager.updateById(summary);
 
-                System.out.println("资料归纳【" + summary.getMaterialName() + "】统计完成,总页数:" + totalPageCount);
+                logger.info("资料归纳【{}】统计完成,总页数:{}", summary.getMaterialName(), totalPageCount);
             }
 
-            System.out.println("页数统计完成,任务ID:" + taskId);
+            logger.info("页数统计完成,任务ID:{}", taskId);
 
         } catch (Exception e) {
-            e.printStackTrace();
-            System.err.println("统计页数失败,任务ID:" + taskId + ",错误:" + e.getMessage());
+            logger.error("统计页数失败,任务ID:{},错误:{}", taskId, e.getMessage(), e);
         }
     }
 
@@ -844,7 +998,7 @@ public class AsyncMaterialSummaryService {
             }
 
         } catch (Exception e) {
-            System.err.println("计算文件页数失败,文件:" + fileUrl + ",错误:" + e.getMessage());
+            logger.error("计算文件页数失败,文件:{},错误:{}", fileUrl, e.getMessage(), e);
             return 0;
         }
     }
@@ -862,7 +1016,7 @@ public class AsyncMaterialSummaryService {
         try {
             String filePath = fileUrl;
             if (filePath.startsWith("/profile")) {
-                filePath = com.hotent.config.EipConfig.getProfile() + filePath.substring(8);
+                filePath = EipConfig.getProfile() + filePath.substring(8);
             }
 
             fis = new java.io.FileInputStream(filePath);
@@ -874,7 +1028,7 @@ public class AsyncMaterialSummaryService {
             return pageCount > 0 ? pageCount : 1;
 
         } catch (Exception e) {
-            System.err.println("读取Word页数失败,文件:" + fileUrl + ",错误:" + e.getMessage());
+            logger.error("读取Word页数失败,文件:{},错误:{}", fileUrl, e.getMessage(), e);
             // 读取失败默认返回1页
             return 1;
         } finally {
@@ -887,9 +1041,435 @@ public class AsyncMaterialSummaryService {
                     fis.close();
                 }
             } catch (Exception e) {
-                System.err.println("关闭文件流失败:" + e.getMessage());
+                logger.warn("关闭文件流失败:{}", e.getMessage());
+            }
+        }
+    }
+
+    // ==================== 卷宗校对文书生成功能 ====================
+
+
+    /**
+     * 生成卷宗文书(封面/目录/封底)
+     * id为空是新增,id不为空是编辑
+     *
+     * @param req 生成请求
+     * @return 生成的文书URL
+     */
+    public String generateArchiveDocument(com.hotent.project.req.ArchiveProofreadReq req) throws Exception {
+        Integer documentType = req.getDocumentType();
+        String existingId = req.getId();
+
+        // 如果id不为空,从数据库获取已有记录的documentType(id是主表ID)
+        if (StringUtil.isNotEmpty(existingId)) {
+            CostProjectTaskMaterialSummary existing = costProjectTaskMaterialSummaryManager.getById(existingId);
+            if (existing == null) {
+                throw new RuntimeException("主表记录不存在:" + existingId);
+            }
+            Integer orderNum = existing.getMaterialOrderNum();
+            if (orderNum != null && orderNum == -1) {
+                documentType = 1;
+            } else if (orderNum != null && orderNum == -2) {
+                documentType = 2;
+            } else if (orderNum != null && orderNum == -3) {
+                documentType = 3;
+            } else {
+                throw new RuntimeException("无效的文书记录");
             }
         }
+
+        String templatePath;
+        String documentName;
+
+        switch (documentType) {
+            case 1:
+                templatePath = coverTemplatePath;
+                documentName = "案卷封面";
+                if (StringUtil.isEmpty(templatePath)) {
+                    throw new RuntimeException("案卷封面模板路径未配置");
+                }
+                break;
+            case 2:
+                templatePath = catalogTemplatePath;
+                documentName = "卷内目录";
+                if (StringUtil.isEmpty(templatePath)) {
+                    throw new RuntimeException("卷内目录模板路径未配置");
+                }
+                break;
+            case 3:
+                templatePath = backCoverTemplatePath;
+                documentName = "案卷封底";
+                if (StringUtil.isEmpty(templatePath)) {
+                    throw new RuntimeException("案卷封底模板路径未配置");
+                }
+                break;
+            default:
+                throw new RuntimeException("不支持的文书类型:" + documentType);
+        }
+
+        // 检查模板文件是否存在
+        java.io.File templateFile = new java.io.File(templatePath);
+        if (!templateFile.exists()) {
+            throw new RuntimeException("模板文件不存在:" + templatePath);
+        }
+
+        // 生成文书
+        String outputUrl = generateDocumentFromTemplate(templatePath, documentName, req);
+
+        // 保存到数据库(主表+明细表)
+        int orderNum = -documentType; // -1封面、-2目录、-3封底
+        String masterId;
+
+        if (StringUtil.isNotEmpty(existingId)) {
+            // 编辑:existingId是主表ID,查找对应的明细记录
+            masterId = existingId;
+            CostProjectTaskMaterialSummaryDetail existingDetail = costProjectTaskMaterialSummaryDetailManager.getOne(
+                new LambdaQueryWrapper<CostProjectTaskMaterialSummaryDetail>()
+                    .eq(CostProjectTaskMaterialSummaryDetail::getMasterId, masterId)
+                    .eq(CostProjectTaskMaterialSummaryDetail::getIsDeleted, "0")
+            );
+            if (existingDetail != null) {
+                // 更新已有明细
+                existingDetail.setAttachmentUrl(outputUrl);
+                existingDetail.setGenerateTime(java.time.LocalDateTime.now());
+                costProjectTaskMaterialSummaryDetailManager.updateById(existingDetail);
+            } else {
+                // 创建新明细
+                CostProjectTaskMaterialSummaryDetail detail = new CostProjectTaskMaterialSummaryDetail();
+                detail.setMasterId(masterId);
+                detail.setTaskId(req.getTaskId());
+                detail.setDocumentName(documentName);
+                detail.setFileSource("系统生成电子文书");
+                detail.setAttachmentUrl(outputUrl);
+                detail.setOrderNum(1);
+                detail.setGenerateTime(java.time.LocalDateTime.now());
+                detail.setIsDeleted("0");
+                costProjectTaskMaterialSummaryDetailManager.save(detail);
+            }
+        } else {
+            // 新增:创建主表记录
+            CostProjectTaskMaterialSummary summary = new CostProjectTaskMaterialSummary();
+            summary.setTaskId(req.getTaskId());
+            summary.setMaterialOrderNum(orderNum);
+            summary.setMaterialName(documentName);
+            summary.setIsDeleted("0");
+            costProjectTaskMaterialSummaryManager.save(summary);
+            masterId = summary.getId();
+
+            // 创建明细记录
+            CostProjectTaskMaterialSummaryDetail detail = new CostProjectTaskMaterialSummaryDetail();
+            detail.setMasterId(masterId);
+            detail.setTaskId(req.getTaskId());
+            detail.setDocumentName(documentName);
+            detail.setFileSource("系统生成电子文书");
+            detail.setAttachmentUrl(outputUrl);
+            detail.setOrderNum(1);
+            detail.setGenerateTime(java.time.LocalDateTime.now());
+            detail.setIsDeleted("0");
+            costProjectTaskMaterialSummaryDetailManager.save(detail);
+        }
+
+        return outputUrl;
+    }
+
+
+    /**
+     * 根据模板生成文书
+     *
+     * @param templatePath 模板路径
+     * @param documentName 文书名称
+     * @param req 请求参数
+     * @return 生成的文书URL
+     */
+    private String generateDocumentFromTemplate(String templatePath, String documentName,
+                                                 com.hotent.project.req.ArchiveProofreadReq req) throws Exception {
+        java.io.FileInputStream fis = null;
+        XWPFDocument document = null;
+        java.io.FileOutputStream fos = null;
+
+        try {
+            // 读取模板
+            fis = new java.io.FileInputStream(templatePath);
+            document = new XWPFDocument(fis);
+
+            // 构建替换映射
+            Map<String, String> replacements = buildReplacementMap(req);
+
+            // 根据文书类型进行不同处理
+            switch (req.getDocumentType()) {
+                case 1:
+                    // 案卷封面
+                    generateCoverDocument(document, replacements);
+                    break;
+                case 2:
+                    // 卷内目录
+                    generateCatalogDocument(document, replacements, req.getTaskId());
+                    break;
+                case 3:
+                    // 案卷封底
+                    generateBackCoverDocument(document, replacements);
+                    break;
+            }
+
+            // 生成输出文件路径
+            String fileName = FileUploadUtil.generateFileName(documentName + ".docx");
+            String outputPath = FileUploadUtil.generateSavePath(
+                    EipConfig.getUploadPath(), fileName, "docx");
+
+            // 确保目录存在
+            java.io.File outputFile = new java.io.File(outputPath);
+            if (!outputFile.getParentFile().exists()) {
+                outputFile.getParentFile().mkdirs();
+            }
+
+            // 写入文件
+            fos = new java.io.FileOutputStream(outputPath);
+            document.write(fos);
+
+            // 返回访问URL
+            return FileUploadUtil.getPathFileName(outputPath, fileName);
+
+        } finally {
+            // 关闭资源
+            try {
+                if (fos != null) fos.close();
+                if (document != null) document.close();
+                if (fis != null) fis.close();
+            } catch (Exception e) {
+                logger.warn("关闭文件流失败:{}", e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * 构建替换映射
+     * 自动从数据库获取:年份、监审项目名称、被监审单位、监审期间(开始-结束年度)、监审人员、办结时间
+     * 需要用户传递:价格主管部门、卷宗号、保管期间周期、立卷人、立卷日期、检查人、检查日期、备注
+     */
+    private Map<String, String> buildReplacementMap(com.hotent.project.req.ArchiveProofreadReq req) {
+        Map<String, String> map = new java.util.HashMap<>();
+
+        // 从数据库获取任务信息
+        CostProjectTask task = costProjectTaskManager.getById(req.getTaskId());
+
+        // 从数据库获取立项审批信息(包含监审组人员)
+        CostProjectApproval approval = costProjectApprovalManager.getOne(
+            new LambdaQueryWrapper<CostProjectApproval>()
+                .eq(CostProjectApproval::getProjectId, task.getProjectId())
+        );
+
+        // ========== 自动获取的字段 ==========
+        Org org = orgManager.getById(approval.getOrgId());
+        map.put("{价格主管部门或成本监审机构}", org.getName());
+        map.put("{年份}", task.getYear());
+        map.put("{监审项目名称}", task.getProjectName());
+        map.put("{被监审单位}", task.getAuditedUnitName());
+
+        // 监审期间(解析开始-结束年度,格式如:2024,2025,2023 逗号分隔)
+        String startYear = "";
+        String endYear = "";
+        String auditPeriod = task.getAuditPeriod();
+        if (StringUtil.isNotEmpty(auditPeriod)) {
+            String[] years = auditPeriod.split(",");
+            int minYear = Integer.MAX_VALUE;
+            int maxYear = Integer.MIN_VALUE;
+            for (String year : years) {
+                try {
+                    int y = Integer.parseInt(year.trim());
+                    if (y < minYear) minYear = y;
+                    if (y > maxYear) maxYear = y;
+                } catch (NumberFormatException e) {
+                }
+            }
+            if (minYear != Integer.MAX_VALUE) {
+                startYear = String.valueOf(minYear);
+                endYear = String.valueOf(maxYear);
+            }
+        }
+        map.put("{监审开始年度}", startYear);
+        map.put("{监审结束年度}", endYear);
+
+        // 监审组负责人及人员(将ID转换为姓名)
+        String auditGroupNames = "";
+        if (approval != null && StringUtil.isNotEmpty(approval.getAuditGroup())) {
+            String[] userIds = approval.getAuditGroup().split(",");
+            StringBuilder names = new StringBuilder();
+            for (String userId : userIds) {
+                try {
+                    User user = userManager.get(userId.trim());
+                    if (user != null) {
+                        if (names.length() > 0) {
+                            names.append(",");
+                        }
+                        names.append(user.getFullname());
+                    }
+                } catch (Exception e) {
+                }
+            }
+            auditGroupNames = names.toString();
+        }
+        map.put("{监审组负责人及人员}", auditGroupNames);
+
+        // 监审办结时间(任务状态为办结时,取更新时间,格式:xxxx年xx月)
+        String auditEndTime = "";
+        if ("400".equals(task.getStatus()) && task.getUpdateTime() != null) {
+            auditEndTime = task.getUpdateTime().getYear() + "年" + task.getUpdateTime().getMonthValue() + "月";
+        }
+        map.put("{监审办结时间}", auditEndTime);
+
+        // ========== 需要用户传递的字段 ==========
+        map.put("{卷宗号}", req.getArchiveNo() != null ? req.getArchiveNo() : "");
+        map.put("{保管期间周期}", req.getRetentionPeriod() != null ? req.getRetentionPeriod() : "");
+
+        // ========== 卷宗统计信息 ==========
+        // 获取14个资料归纳的总页数
+        List<CostProjectTaskMaterialSummary> summaryList = costProjectTaskMaterialSummaryManager.listByTaskId(req.getTaskId());
+        summaryList = summaryList.stream()
+                .filter(s -> s.getMaterialOrderNum() != null && s.getMaterialOrderNum() > 0)
+                .collect(Collectors.toList());
+        int totalPageCount = 0;
+        for (CostProjectTaskMaterialSummary summary : summaryList) {
+            if (summary.getTotalPageCount() != null) {
+                try {
+                    totalPageCount += Integer.parseInt(summary.getTotalPageCount());
+                } catch (NumberFormatException e) {
+                }
+            }
+        }
+        map.put("{卷宗件数}", "1"); // 默认1件
+        map.put("{卷宗页数}", String.valueOf(totalPageCount));
+        map.put("{第几件}", "1"); // 默认第1件
+
+        return map;
+    }
+
+    /**
+     * 生成案卷封面
+     */
+    private void generateCoverDocument(XWPFDocument document, Map<String, String> replacements) {
+        SmartTemplateWriter.writeToTemplate(document, replacements);
+    }
+
+    /**
+     * 生成卷内目录(只填充表格,不替换占位符)
+     */
+    private void generateCatalogDocument(XWPFDocument document, Map<String, String> replacements, String taskId) {
+        // 获取资料归纳列表,生成目录表格
+        List<CostProjectTaskMaterialSummary> summaryList = costProjectTaskMaterialSummaryManager.listByTaskId(taskId);
+
+        // 过滤掉封面、目录、封底(负数序号)
+        summaryList = summaryList.stream()
+                .filter(s -> s.getMaterialOrderNum() != null && s.getMaterialOrderNum() > 0)
+                .sorted(Comparator.comparing(CostProjectTaskMaterialSummary::getMaterialOrderNum))
+                .collect(Collectors.toList());
+
+        // 构建目录数据
+        List<CompleteTemplateProcessor.TableRowData> tableDataList = new java.util.ArrayList<>();
+        int currentPage = 1;
+
+        for (CostProjectTaskMaterialSummary summary : summaryList) {
+            CompleteTemplateProcessor.TableRowData rowData =
+                    new CompleteTemplateProcessor.TableRowData();
+            rowData.setDocumentName(summary.getMaterialName());
+
+            int pageCount = 0;
+            if (summary.getTotalPageCount() != null) {
+                try {
+                    pageCount = Integer.parseInt(summary.getTotalPageCount());
+                } catch (NumberFormatException e) {
+                    pageCount = 0;
+                }
+            }
+            rowData.setPageCount(pageCount);
+
+            // 计算页码范围作为备注
+            if (pageCount > 0) {
+                rowData.setRemark(currentPage + "-" + (currentPage + pageCount - 1));
+                currentPage += pageCount;
+            } else {
+                rowData.setRemark("—");
+            }
+
+            tableDataList.add(rowData);
+        }
+
+        // 只填充表格数据,
+        if (!tableDataList.isEmpty()) {
+            processTableData(document, tableDataList);
+        }
+    }
+
+    /**
+     * 生成案卷封底
+     */
+    private void generateBackCoverDocument(XWPFDocument document, Map<String, String> replacements) {
+        SmartTemplateWriter.writeToTemplate(document, replacements);
+    }
+
+    /**
+     * 处理目录表格数据
+     */
+    private void processTableData(XWPFDocument document, List<CompleteTemplateProcessor.TableRowData> dataList) {
+        List<XWPFTable> tables = document.getTables();
+        if (tables.isEmpty()) {
+            logger.warn("未找到表格");
+            return;
+        }
+
+        XWPFTable table = tables.get(0);
+
+        // 找到模板行(第二行,第一行是表头)
+        int templateRowIndex = 1;
+        if (table.getRows().size() <= templateRowIndex) {
+            logger.warn("表格行数不足");
+            return;
+        }
+
+        // 获取模板行的单元格数量
+        int cellCount = table.getRow(templateRowIndex).getTableCells().size();
+
+        // 删除模板行
+        table.removeRow(templateRowIndex);
+
+        // 从模板行位置开始插入数据行
+        for (int i = 0; i < dataList.size(); i++) {
+            CompleteTemplateProcessor.TableRowData data = dataList.get(i);
+            XWPFTableRow newRow = table.insertNewTableRow(templateRowIndex + i);
+            // 创建单元格
+            for (int j = 0; j < cellCount; j++) {
+                newRow.addNewTableCell();
+            }
+            fillTableRow(newRow, i + 1, data);
+        }
+    }
+
+    /**
+     * 填充表格行数据
+     */
+    private void fillTableRow(XWPFTableRow row, int sequence,
+                               CompleteTemplateProcessor.TableRowData data) {
+        List<XWPFTableCell> cells = row.getTableCells();
+        if (cells.size() >= 4) {
+            setCellText(cells.get(0), String.valueOf(sequence));
+            setCellText(cells.get(1), data.getDocumentName());
+            setCellText(cells.get(2), data.getPageCount() > 0 ? String.valueOf(data.getPageCount()) : "");
+            setCellText(cells.get(3), data.getRemark() != null ? data.getRemark() : "");
+        }
+    }
+
+    /**
+     * 设置单元格文本
+     */
+    private void setCellText(XWPFTableCell cell, String text) {
+        // 清除现有内容
+        for (int i = cell.getParagraphs().size() - 1; i >= 0; i--) {
+            cell.removeParagraph(i);
+        }
+        // 添加新内容
+        XWPFParagraph paragraph = cell.addParagraph();
+        XWPFRun run = paragraph.createRun();
+        run.setText(text);
+        paragraph.setAlignment(ParagraphAlignment.CENTER);
     }
 
  }

+ 260 - 0
assistMg/src/main/java/com/hotent/util/wordexcelutils/ArchiveDocumentGeneratorTest.java

@@ -0,0 +1,260 @@
+package com.hotent.util.wordexcelutils;
+
+import org.apache.poi.xwpf.usermodel.*;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.util.*;
+
+/**
+ * 卷宗文书生成测试工具类
+ *
+ * @company 山西清众科技股份有限公司
+ * @author 超级管理员
+ * @since 2025-12-08
+ */
+public class ArchiveDocumentGeneratorTest {
+
+    // 固定模板路径
+    private static final String COVER_TEMPLATE_PATH = "D:\\fx\\1、卷宗封面.docx";      // 案卷封面模板
+    private static final String CATALOG_TEMPLATE_PATH = "D:\\fx\\2、卷内目录.docx";    // 卷内目录模板
+    private static final String BACK_COVER_TEMPLATE_PATH = "D:\\fx\\3.docx"; // 案卷封底模板
+
+    // 输出路径
+    private static final String OUTPUT_DIR = "D:\\fx\\output\\";
+
+    public static void main(String[] args) {
+        try {
+            System.out.println("========== 卷宗文书生成测试 ==========");
+
+            // 测试生成案卷封面
+            System.out.println("\n1. 测试生成案卷封面...");
+            generateCoverTest();
+
+            // 测试生成卷内目录
+            System.out.println("\n2. 测试生成卷内目录...");
+            generateCatalogTest();
+
+            // 测试生成案卷封底
+//            System.out.println("\n3. 测试生成案卷封底...");
+//            generateBackCoverTest();
+
+            System.out.println("\n========== 测试完成 ==========");
+            System.out.println("输出目录: " + OUTPUT_DIR);
+
+        } catch (Exception e) {
+            System.err.println("测试失败: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 测试生成案卷封面
+     */
+    private static void generateCoverTest() throws Exception {
+        Map<String, String> replacements = new HashMap<>();
+        replacements.put("{价格主管部门或成本监审机构}", "山西省发展和改革委员会");
+        replacements.put("{年份}", "2025");
+        replacements.put("{卷宗号}", "001");
+        replacements.put("{监审项目名称}", "某某供水公司成本监审项目");
+        replacements.put("{被监审单位}", "某某供水有限公司");
+        replacements.put("{监审开始年度}", "2022");
+        replacements.put("{监审结束年度}", "2024");
+        replacements.put("{监审组负责人及人员}", "张三、李四、王五");
+        replacements.put("{监审办结时间}", "2025年12月");
+        replacements.put("{保管期间周期}", "永久");
+
+        String outputPath = OUTPUT_DIR + "案卷封面_测试.docx";
+        generateDocument(COVER_TEMPLATE_PATH, outputPath, replacements, 1, null);
+        System.out.println("   生成成功: " + outputPath);
+    }
+
+    /**
+     * 测试生成卷内目录
+     * 注意:卷内目录不需要替换占位符,只需要填充表格数据
+     */
+    private static void generateCatalogTest() throws Exception {
+        // 卷内目录不需要替换文档中的占位符,表头的{序号}{资料名称}等是列标题,保持不变
+        Map<String, String> replacements = new HashMap<>();
+
+        // 模拟资料列表数据
+        List<TableRowData> dataList = new ArrayList<>();
+        dataList.add(new TableRowData("成本监审报告(含成本监审报告签发稿、送达回证)", 30, "1-30"));
+        dataList.add(new TableRowData("被监审单位申请定(调)价报告(复印件)", 5, "31-35"));
+        dataList.add(new TableRowData("成本监审通知书(含送达回证)", 13, "36-48"));
+        dataList.add(new TableRowData("经营者需提供成本资料清单", 5, "49-54"));
+        dataList.add(new TableRowData("《政府定价成本监审调查表》", 5, "55-60"));
+        dataList.add(new TableRowData("成本监审补充资料通知书(含送达回证)", 5, "61-65"));
+        dataList.add(new TableRowData("成本审核初步意见告知书(含送达回证)", 5, "66-70"));
+        dataList.add(new TableRowData("经营者书面反馈的材料", 1, "71"));
+        dataList.add(new TableRowData("成本审核初步意见表(集体审议用)", 1, "72"));
+        dataList.add(new TableRowData("成本监审集体审议记录", 2, "73-74"));
+        dataList.add(new TableRowData("成本监审工作底稿", 10, "75-84"));
+        dataList.add(new TableRowData("成本监审提取资料登记表", 3, "85-87"));
+        dataList.add(new TableRowData("提取的成本资料和会计凭证等复印件", 13, "88-100"));
+
+        String outputPath = OUTPUT_DIR + "卷内目录_测试.docx";
+        generateDocument(CATALOG_TEMPLATE_PATH, outputPath, replacements, 2, dataList);
+        System.out.println("   生成成功: " + outputPath);
+    }
+
+    /**
+     * 测试生成案卷封底
+     */
+    private static void generateBackCoverTest() throws Exception {
+        Map<String, String> replacements = new HashMap<>();
+        replacements.put("{价格主管部门或成本监审机构}", "山西省发展和改革委员会");
+        replacements.put("{立卷人}", "张三");
+        replacements.put("{立卷日期}", "2025年12月8日");
+        replacements.put("{检查人}", "李四");
+        replacements.put("{检查日期}", "2025年12月9日");
+        replacements.put("{备注}", "无");
+        replacements.put("{材料件数}", "13");
+        replacements.put("{材料页数}", "100");
+
+        String outputPath = OUTPUT_DIR + "案卷封底_测试.docx";
+        generateDocument(BACK_COVER_TEMPLATE_PATH, outputPath, replacements, 3, null);
+        System.out.println("   生成成功: " + outputPath);
+    }
+
+    /**
+     * 生成文书
+     *
+     * @param templatePath 模板路径
+     * @param outputPath   输出路径
+     * @param replacements 替换映射
+     * @param documentType 文书类型:1-封面 2-目录 3-封底
+     * @param dataList     表格数据(仅目录使用)
+     */
+    private static void generateDocument(String templatePath, String outputPath,
+                                          Map<String, String> replacements, int documentType,
+                                          List<TableRowData> dataList) throws Exception {
+        FileInputStream fis = null;
+        XWPFDocument document = null;
+        FileOutputStream fos = null;
+
+        try {
+            // 检查模板文件是否存在
+            java.io.File templateFile = new java.io.File(templatePath);
+            if (!templateFile.exists()) {
+                System.err.println("   警告: 模板文件不存在 - " + templatePath);
+                System.out.println("   跳过此测试,请确保模板文件存在");
+                return;
+            }
+
+            // 读取模板
+            fis = new FileInputStream(templatePath);
+            document = new XWPFDocument(fis);
+
+            // 替换占位符
+            SmartTemplateWriter.writeToTemplate(document, replacements);
+
+            // 如果是目录,处理表格数据
+            if (documentType == 2 && dataList != null && !dataList.isEmpty()) {
+                processTableData(document, dataList);
+            }
+
+            // 确保输出目录存在
+            java.io.File outputFile = new java.io.File(outputPath);
+            if (!outputFile.getParentFile().exists()) {
+                outputFile.getParentFile().mkdirs();
+            }
+
+            // 写入文件
+            fos = new FileOutputStream(outputPath);
+            document.write(fos);
+
+        } finally {
+            // 关闭资源
+            try {
+                if (fos != null) fos.close();
+                if (document != null) document.close();
+                if (fis != null) fis.close();
+            } catch (Exception e) {
+                System.err.println("关闭文件流失败: " + e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * 处理目录表格数据
+     * 表格结构:第1行表头({序号}{资料名称}等列标题,保持不变),后面是空行需要填充数据
+     */
+    private static void processTableData(XWPFDocument document, List<TableRowData> dataList) {
+        List<XWPFTable> tables = document.getTables();
+        if (tables.isEmpty()) {
+            System.err.println("   警告: 未找到表格");
+            return;
+        }
+
+        XWPFTable table = tables.get(0);
+        System.out.println("   原始表格行数: " + table.getRows().size());
+
+        // 第1行是表头(索引0),保持不变
+        // 从第2行开始是数据行(索引1)
+        int headerRowIndex = 0;
+        int dataStartRowIndex = 1;
+
+        XWPFTableRow headerRow = table.getRow(headerRowIndex);
+        int cellCount = headerRow.getTableCells().size();
+        System.out.println("   表头单元格数: " + cellCount);
+
+        // 删除表头之后的所有行(只保留表头)
+        while (table.getRows().size() > 1) {
+            table.removeRow(1);
+        }
+
+        // 从表头后面开始添加数据行
+        for (int i = 0; i < dataList.size(); i++) {
+            TableRowData data = dataList.get(i);
+            // 创建新行
+            XWPFTableRow newRow = table.createRow();
+            fillTableRow(newRow, i + 1, data);
+        }
+
+        System.out.println("   填充完成,最终行数: " + table.getRows().size());
+    }
+
+    /**
+     * 填充表格行数据
+     */
+    private static void fillTableRow(XWPFTableRow row, int sequence, TableRowData data) {
+        List<XWPFTableCell> cells = row.getTableCells();
+        if (cells.size() >= 4) {
+            setCellText(cells.get(0), String.valueOf(sequence));
+            setCellText(cells.get(1), data.documentName);
+            setCellText(cells.get(2), data.pageCount > 0 ? String.valueOf(data.pageCount) : "");
+            setCellText(cells.get(3), data.pageRange != null ? data.pageRange : "");
+        }
+    }
+
+    /**
+     * 设置单元格文本
+     */
+    private static void setCellText(XWPFTableCell cell, String text) {
+        // 清除现有内容
+        for (int i = cell.getParagraphs().size() - 1; i >= 0; i--) {
+            cell.removeParagraph(i);
+        }
+        // 添加新内容
+        XWPFParagraph paragraph = cell.addParagraph();
+        XWPFRun run = paragraph.createRun();
+        run.setText(text);
+        paragraph.setAlignment(ParagraphAlignment.CENTER);
+    }
+
+    /**
+     * 表格行数据类
+     */
+    static class TableRowData {
+        String documentName;
+        int pageCount;
+        String pageRange;
+
+        TableRowData(String documentName, int pageCount, String pageRange) {
+            this.documentName = documentName;
+            this.pageCount = pageCount;
+            this.pageRange = pageRange;
+        }
+    }
+}