فهرست منبع

fix:流程明细

zzw 1 روز پیش
والد
کامیت
5ecc88a6bd
20فایلهای تغییر یافته به همراه2159 افزوده شده و 215 حذف شده
  1. 6 0
      assistMg/src/main/java/com/hotent/enterpriseDeclare/controller/material/CostProjectTaskDraftController.java
  2. 6 0
      assistMg/src/main/java/com/hotent/enterpriseDeclare/controller/material/CostProjectTaskEvidenceController.java
  3. 31 53
      assistMg/src/main/java/com/hotent/enterpriseDeclare/controller/material/CostProjectTaskSurveyGenericController.java
  4. 10 1
      assistMg/src/main/java/com/hotent/enterpriseDeclare/controller/opinion/CostProjectTaskPreliminaryOpinionController.java
  5. 347 25
      assistMg/src/main/java/com/hotent/enterpriseDeclare/service/CostSurveyExcelAsyncService.java
  6. 18 0
      assistMg/src/main/java/com/hotent/project/controller/CostProjectConclusionController.java
  7. 6 0
      assistMg/src/main/java/com/hotent/project/controller/CostProjectDeliberateController.java
  8. 19 0
      assistMg/src/main/java/com/hotent/project/controller/CostProjectDocumentController.java
  9. 1 1
      assistMg/src/main/java/com/hotent/project/controller/CostProjectTaskMaterialController.java
  10. 15 0
      assistMg/src/main/java/com/hotent/project/dao/CostProjectProccessNodeDetailDao.java
  11. 37 0
      assistMg/src/main/java/com/hotent/project/manager/CostProjectProccessNodeDetailManager.java
  12. 13 2
      assistMg/src/main/java/com/hotent/project/manager/impl/CostProjectProccessManagerImpl.java
  13. 191 0
      assistMg/src/main/java/com/hotent/project/manager/impl/CostProjectProccessNodeDetailManagerImpl.java
  14. 121 43
      assistMg/src/main/java/com/hotent/project/manager/impl/CostProjectTaskManagerImpl.java
  15. 31 5
      assistMg/src/main/java/com/hotent/project/manager/impl/CostProjectTaskMaterialManagerImpl.java
  16. 149 0
      assistMg/src/main/java/com/hotent/project/model/CostProjectProccessNodeDetail.java
  17. 5 1
      assistMg/src/main/java/com/hotent/project/resp/CostProjectProccessNodeResp.java
  18. 580 47
      assistMg/src/main/java/com/hotent/project/service/ArchiveTest.java
  19. 522 37
      assistMg/src/main/java/com/hotent/project/service/AsyncMaterialSummaryService.java
  20. 51 0
      assistMg/src/main/resources/mapper/CostProjectProccessNodeDetailMapper.xml

+ 6 - 0
assistMg/src/main/java/com/hotent/enterpriseDeclare/controller/material/CostProjectTaskDraftController.java

@@ -4,6 +4,7 @@ import com.hotent.base.annotation.ApiGroup;
 import com.hotent.base.constants.ApiGroupConsts;
 import com.hotent.base.model.CommonResult;
 import com.hotent.constant.BaseConstant;
+import com.hotent.project.manager.CostProjectProccessNodeDetailManager;
 import com.hotent.project.manager.CostProjectTaskAdjustmentRecordManager;
 import com.hotent.project.manager.CostProjectTaskDraftManager;
 import com.hotent.project.model.CostProjectTaskAdjustmentRecord;
@@ -34,6 +35,9 @@ public class CostProjectTaskDraftController {
     @Autowired
     private CostProjectTaskAdjustmentRecordManager adjustmentRecordManager;
 
+    @Autowired
+    private CostProjectProccessNodeDetailManager costProjectProccessNodeDetailManager;
+
     /**
      * 根据任务ID获取工作底稿
      */
@@ -52,6 +56,7 @@ public class CostProjectTaskDraftController {
     @ApiOperation(value = "新增或更新工作底稿", httpMethod = "POST", notes = "新增或更新工作底稿")
     public CommonResult<String> save(@RequestBody CostProjectTaskDraft draft) {
         costProjectTaskDraftManager.createOrUpdate(draft);
+        costProjectProccessNodeDetailManager.recordByTaskId(draft.getTaskId(), "uploadMaterial", "保存工作底稿", null);
         return CommonResult.<String>ok().value("操作成功");
     }
 
@@ -86,6 +91,7 @@ public class CostProjectTaskDraftController {
     @ApiOperation(value = "新增核增核减记录", httpMethod = "POST", notes = "新增核增核减记录")
     public CommonResult<String> addRecord(@RequestBody CostProjectTaskAdjustmentRecord record) {
         adjustmentRecordManager.createOrUpdate(record);
+        costProjectProccessNodeDetailManager.recordByTaskId(record.getTaskId(), "uploadMaterial", "保存核减记录", null);
         return CommonResult.<String>ok().value("新增成功");
     }
 

+ 6 - 0
assistMg/src/main/java/com/hotent/enterpriseDeclare/controller/material/CostProjectTaskEvidenceController.java

@@ -9,6 +9,7 @@ import com.hotent.base.model.CommonResult;
 import com.hotent.base.util.StringUtil;
 import com.hotent.constant.BaseConstant;
 import com.hotent.enterpriseDeclare.req.CostTaskEvidencePageReq;
+import com.hotent.project.manager.CostProjectProccessNodeDetailManager;
 import com.hotent.project.manager.CostProjectTaskEvidenceManager;
 import com.hotent.project.model.CostProjectTaskEvidence;
 import io.swagger.annotations.Api;
@@ -26,6 +27,8 @@ public class CostProjectTaskEvidenceController {
 
     @Autowired
     private CostProjectTaskEvidenceManager costProjectTaskEvidenceManager;
+    @Autowired
+    private CostProjectProccessNodeDetailManager costProjectProccessNodeDetailManager;
 
 
     /**
@@ -51,6 +54,7 @@ public class CostProjectTaskEvidenceController {
     @ApiOperation(value = "新增资料登记", httpMethod = "POST", notes = "新增资料登记")
     public CommonResult<String> add(@RequestBody CostProjectTaskEvidence evidence) {
         costProjectTaskEvidenceManager.createOrUpdate(evidence);
+        costProjectProccessNodeDetailManager.recordByTaskId(evidence.getTaskId(), "uploadMaterial", "新增资料登记", null);
         return CommonResult.<String>ok().value("操作成功");
     }
 
@@ -61,6 +65,7 @@ public class CostProjectTaskEvidenceController {
     @ApiOperation(value = "更新资料登记", httpMethod = "POST", notes = "更新资料登记")
     public CommonResult<String> update(@RequestBody CostProjectTaskEvidence evidence) {
         costProjectTaskEvidenceManager.createOrUpdate(evidence);
+        costProjectProccessNodeDetailManager.recordByTaskId(evidence.getTaskId(), "uploadMaterial", "更新资料登记", null);
         return CommonResult.<String>ok().value("更新成功");
     }
 
@@ -71,6 +76,7 @@ public class CostProjectTaskEvidenceController {
     @ApiOperation(value = "删除资料登记", httpMethod = "POST", notes = "删除资料登记")
     public CommonResult<String> delete(@RequestBody CostProjectTaskEvidence evidence) {
         costProjectTaskEvidenceManager.removeById(evidence.getId());
+        costProjectProccessNodeDetailManager.recordByTaskId(evidence.getTaskId(), "uploadMaterial", "删除资料登记", null);
         return CommonResult.<String>ok().value("删除成功");
     }
 }

+ 31 - 53
assistMg/src/main/java/com/hotent/enterpriseDeclare/controller/material/CostProjectTaskSurveyGenericController.java

@@ -16,7 +16,9 @@ import com.hotent.enterpriseDeclare.manager.CostSurveyTemplateUploadDataManager;
 import com.hotent.enterpriseDeclare.model.CostAuditPeriodRecord;
 import com.hotent.enterpriseDeclare.model.CostSurveyTemplateUploadData;
 import com.hotent.enterpriseDeclare.service.CellDataQueryService;
+import com.hotent.enterpriseDeclare.service.CostSurveyExcelAsyncService;
 import com.hotent.project.manager.CostProjectApprovalManager;
+import com.hotent.project.manager.CostProjectProccessNodeDetailManager;
 import com.hotent.project.manager.CostProjectTaskManager;
 import com.hotent.project.manager.CostProjectTaskMaterialManager;
 import com.hotent.project.model.CostProjectApproval;
@@ -133,7 +135,10 @@ public class CostProjectTaskSurveyGenericController {
     private CellDataQueryService cellDataQueryService;
 
     @Autowired
-    private com.hotent.enterpriseDeclare.service.CostSurveyExcelAsyncService costSurveyExcelAsyncService;
+    private CostSurveyExcelAsyncService costSurveyExcelAsyncService;
+
+    @Autowired
+    private CostProjectProccessNodeDetailManager costProjectProccessNodeDetailManager;
 
 
     /**
@@ -226,7 +231,7 @@ public class CostProjectTaskSurveyGenericController {
      * 企业报送-调查表-列表
      *
      * @param taskId 任务ID
-     * @param type   类型:1-成本调查表 2-财务数据表
+     * @param type   类型:1-成本调查表 2-财务数据表 3-核定表
      * @return 上传记录列表
      * @throws Exception
      */
@@ -496,9 +501,12 @@ public class CostProjectTaskSurveyGenericController {
                     upload.setAuditedStatus("0");
                 }
                 costSurveyTemplateUploadManager.updateById(upload);
-
                 // 异步生成Excel文件并保存
                 costSurveyExcelAsyncService.asyncGenerateAndSaveExcel("1", surveyTemplateId, taskId, refId);
+                // 记录流程明细
+                CostSurveyTemplate template = costSurveyTemplateManager.getDetail(surveyTemplateId);
+                String actionName = StringUtil.isNotEmpty(template.getSurveyTemplateName()) ? "保存调查表:" + template.getSurveyTemplateName() : "保存调查表";
+                costProjectProccessNodeDetailManager.recordByTaskId(taskId, "uploadMaterial", actionName, null);
             }
         }
         // 如果是财务数据表,更新材料上传状态
@@ -511,6 +519,10 @@ public class CostProjectTaskSurveyGenericController {
 
                 // 异步生成Excel文件并保存
                 costSurveyExcelAsyncService.asyncGenerateAndSaveExcel("2", surveyTemplateId, taskId, refId);
+                // 记录流程明细
+                CostSurveyFdTemplate template = costSurveyFdTemplateManager.getDetail(surveyTemplateId);
+                String actionName = StringUtil.isNotEmpty(template.getSurveyTemplateName()) ? "保存财务数据表:" + template.getSurveyTemplateName() : "保存财务数据表";
+                costProjectProccessNodeDetailManager.recordByTaskId(taskId, "uploadMaterial", actionName, null);
             }
         }
         return CommonResult.<String>ok().message("保存数据成功");
@@ -831,16 +843,6 @@ public class CostProjectTaskSurveyGenericController {
                 titleCell.setCellValue(templateName);
                 titleCell.setCellStyle(titleStyle);
 
-                // 从itemsList中获取unit值,按headersId分组取第一个非空的unit
-                Map<String, String> headersIdToUnitMap = new HashMap<>();
-                if (itemsList != null) {
-                    for (CostSurveyTemplateItems item : itemsList) {
-                        if (StringUtil.isNotEmpty(item.getHeadersId()) && StringUtil.isNotEmpty(item.getUnit())) {
-                            headersIdToUnitMap.putIfAbsent(item.getHeadersId(), item.getUnit());
-                        }
-                    }
-                }
-
                 // 计算总列数(包括隐藏列)
                 // 对于isAuditPeriod=true的字段,需要按年限展开成多列
                 int totalColumns = 0;
@@ -869,25 +871,19 @@ public class CostProjectTaskSurveyGenericController {
                 for (int i = 0; i < headersList.size(); i++) {
                     CostSurveyTemplateHeaders header = headersList.get(i);
                     String fieldName = header.getFieldName();
-                    String unit = headersIdToUnitMap.get(header.getId());
 
                     if ("true".equals(header.getIsAuditPeriod()) && auditPeriods != null && auditPeriods.length > 0) {
                         // 绑定监审期间的字段,按年限展开成多列:年限+字段名
                         for (String period : auditPeriods) {
                             Cell headerCell = headerRow.createCell(colIndex++);
                             String headerValue = period.trim() + fieldName;
-                            // 如果有单位,拼接到后面
-                            if (StringUtil.isNotEmpty(unit)) {
-                                headerValue = headerValue + "(" + unit + ")";
-                            }
                             headerCell.setCellValue(headerValue);
                             headerCell.setCellStyle(headerStyle);
                         }
                     } else {
                         // 普通字段,正常显示一列
                         Cell headerCell = headerRow.createCell(colIndex++);
-                        String headerValue = StringUtil.isNotEmpty(unit) ? fieldName + "(" + unit + ")" : fieldName;
-                        headerCell.setCellValue(headerValue);
+                        headerCell.setCellValue(fieldName);
                         headerCell.setCellStyle(headerStyle);
                     }
                 }
@@ -1059,16 +1055,6 @@ public class CostProjectTaskSurveyGenericController {
                 titleCell.setCellValue(templateName);
                 titleCell.setCellStyle(titleStyle);
 
-                // 从itemsList中获取unit值,按headersId分组取第一个非空的unit
-                Map<String, String> headersIdToUnitMap = new HashMap<>();
-                if (itemsList != null) {
-                    for (CostSurveyFdTemplateItems item : itemsList) {
-                        if (StringUtil.isNotEmpty(item.getHeadersId()) && StringUtil.isNotEmpty(item.getUnit())) {
-                            headersIdToUnitMap.putIfAbsent(item.getHeadersId(), item.getUnit());
-                        }
-                    }
-                }
-
                 // 计算总列数(包括隐藏列)
                 // 对于isAuditPeriod=1的字段,需要按年限展开成多列
                 int totalColumns = 0;
@@ -1097,25 +1083,19 @@ public class CostProjectTaskSurveyGenericController {
                 for (int i = 0; i < headersList.size(); i++) {
                     CostSurveyFdTemplateHeaders header = headersList.get(i);
                     String fieldName = header.getFieldName();
-                    String unit = headersIdToUnitMap.get(header.getId());
 
                     if ("true".equals(header.getIsAuditPeriod()) && auditPeriods != null && auditPeriods.length > 0) {
                         // 绑定监审期间的字段,按年限展开成多列:年限+字段名
                         for (String period : auditPeriods) {
                             Cell headerCell = headerRow.createCell(colIndex++);
                             String headerValue = period.trim() + fieldName;
-                            // 如果有单位,拼接到后面
-                            if (StringUtil.isNotEmpty(unit)) {
-                                headerValue = headerValue + "(" + unit + ")";
-                            }
                             headerCell.setCellValue(headerValue);
                             headerCell.setCellStyle(headerStyle);
                         }
                     } else {
                         // 普通字段,正常显示一列
                         Cell headerCell = headerRow.createCell(colIndex++);
-                        String headerValue = StringUtil.isNotEmpty(unit) ? fieldName + "(" + unit + ")" : fieldName;
-                        headerCell.setCellValue(headerValue);
+                        headerCell.setCellValue(fieldName);
                         headerCell.setCellStyle(headerStyle);
                     }
                 }
@@ -1264,24 +1244,11 @@ public class CostProjectTaskSurveyGenericController {
                 Row headerRow = sheet.createRow(1);
                 int colIndex = 0;
 
-                // 从itemsList中获取unit值,按headersId分组取第一个非空的unit
-                Map<String, String> headersIdToUnitMap = new HashMap<>();
-                if (itemsList != null) {
-                    for (CostVerifyTemplateItems item : itemsList) {
-                        if (StringUtil.isNotEmpty(item.getHeadersId()) && StringUtil.isNotEmpty(item.getUnit())) {
-                            headersIdToUnitMap.putIfAbsent(item.getHeadersId(), item.getUnit());
-                        }
-                    }
-                }
-
-                // 添加原有表头(字段名+单位)
+                // 添加原有表头
                 for (int i = 0; i < headersList.size(); i++) {
                     Cell headerCell = headerRow.createCell(colIndex++);
                     String fieldName = headersList.get(i).getFieldName();
-                    String unit = headersIdToUnitMap.get(headersList.get(i).getId());
-                    // 如果有单位,拼接到字段名后面
-                    String headerValue = StringUtil.isNotEmpty(unit) ? fieldName + "(" + unit + ")" : fieldName;
-                    headerCell.setCellValue(headerValue);
+                    headerCell.setCellValue(fieldName);
                     headerCell.setCellStyle(headerStyle);
                 }
 
@@ -1645,6 +1612,9 @@ public class CostProjectTaskSurveyGenericController {
 
                     // 异步生成Excel文件并保存
                     costSurveyExcelAsyncService.asyncGenerateAndSaveExcel("1", surveyTemplateId, taskId, refId);
+                    // 记录流程明细
+                    String actionName = StringUtil.isNotEmpty(template.getSurveyTemplateName()) ? "保存调查表:" + template.getSurveyTemplateName() : "保存调查表";
+                    costProjectProccessNodeDetailManager.recordByTaskId(taskId, "uploadMaterial", actionName, null);
 
                     return CommonResult.<String>ok().message("导入成功,共导入 " + dataRowCount + " 行数据");
                 } catch (Exception e) {
@@ -1903,7 +1873,9 @@ public class CostProjectTaskSurveyGenericController {
 
                     // 异步生成Excel文件并保存
                     costSurveyExcelAsyncService.asyncGenerateAndSaveExcel("2", surveyTemplateId, taskId, materialId);
-
+                    // 记录流程明细
+                    String actionName = StringUtil.isNotEmpty(template.getSurveyTemplateName()) ? "保存财务数据表:" + template.getSurveyTemplateName() : "保存财务数据表";
+                    costProjectProccessNodeDetailManager.recordByTaskId(taskId, "uploadMaterial", actionName, null);
                     return CommonResult.<String>ok().message("导入成功,共导入 " + dataRowCount + " 行数据");
                 } catch (Exception e) {
                     return CommonResult.<String>error().message("导入失败:" + e.getMessage());
@@ -2070,6 +2042,12 @@ public class CostProjectTaskSurveyGenericController {
                     // 保存数据
                     costSurveyTemplateUploadDataManager.saveData(dataList);
 
+                    costSurveyExcelAsyncService.asyncGenerateAndSaveExcel("3", surveyTemplateId, taskId, refId);
+
+                    // 记录流程明细
+                    String actionName = StringUtil.isNotEmpty(template.getSurveyTemplateName()) ? "保存核定数据表:" + template.getSurveyTemplateName() : "保存核定数据表";
+                    costProjectProccessNodeDetailManager.recordByTaskId(taskId, "uploadMaterial", actionName, null);
+
                     return CommonResult.<String>ok().message("导入成功,共导入 " + importRowCount + " 行数据");
 
                 } catch (Exception e) {

+ 10 - 1
assistMg/src/main/java/com/hotent/enterpriseDeclare/controller/opinion/CostProjectTaskPreliminaryOpinionController.java

@@ -4,6 +4,7 @@ import com.hotent.base.annotation.ApiGroup;
 import com.hotent.base.constants.ApiGroupConsts;
 import com.hotent.base.model.CommonResult;
 import com.hotent.constant.BaseConstant;
+import com.hotent.project.manager.CostProjectProccessNodeDetailManager;
 import com.hotent.project.manager.CostProjectTaskPreliminaryOpinionManager;
 import com.hotent.project.model.CostProjectTaskPreliminaryOpinion;
 import io.swagger.annotations.Api;
@@ -26,7 +27,8 @@ public class CostProjectTaskPreliminaryOpinionController {
 
     @Autowired
     private CostProjectTaskPreliminaryOpinionManager opinionManager;
-
+    @Autowired
+    private CostProjectProccessNodeDetailManager costProjectProccessNodeDetailManager;
     /**
      * 根据任务ID获取审核意见
      */
@@ -43,7 +45,14 @@ public class CostProjectTaskPreliminaryOpinionController {
     @PostMapping(value = "/save")
     @ApiOperation(value = "新增或更新审核意见", httpMethod = "POST", notes = "新增或更新审核意见(包含初步意见、反馈意见、结论意见)")
     public CommonResult<String> save(@RequestBody CostProjectTaskPreliminaryOpinion opinion) {
+        String actionName = "";
+        if (opinion.getId()==null){
+            actionName = "保存成本审核初步意见";
+        }else{
+            actionName = "保存成本审核结论意见";
+        }
         opinionManager.createOrUpdate(opinion);
+        costProjectProccessNodeDetailManager.recordByTaskId(opinion.getTaskId(), "uploadMaterial", actionName, null);
         return CommonResult.<String>ok().value("保存成功");
     }
 

+ 347 - 25
assistMg/src/main/java/com/hotent/enterpriseDeclare/service/CostSurveyExcelAsyncService.java

@@ -15,6 +15,8 @@ import com.hotent.project.model.CostProjectTask;
 import com.hotent.project.model.CostProjectTaskMaterial;
 import com.hotent.surveyinfo.dao.CostSurveyFdTemplateItemsDao;
 import com.hotent.surveyinfo.dao.CostSurveyTemplateItemsDao;
+import com.hotent.surveyinfo.dao.CostVerifyTemplateHeadersDao;
+import com.hotent.surveyinfo.dao.CostVerifyTemplateItemsDao;
 import com.hotent.surveyinfo.manager.*;
 import com.hotent.surveyinfo.model.*;
 import com.hotent.util.ExcelStyleUtil;
@@ -79,9 +81,18 @@ public class CostSurveyExcelAsyncService {
     @Autowired
     private CellDataQueryService cellDataQueryService;
 
+    @Autowired
+    private CostVerifyTemplateManager costVerifyTemplateManager;
+
+    @Autowired
+    private CostVerifyTemplateHeadersDao costVerifyTemplateHeadersDao;
+
+    @Autowired
+    private CostVerifyTemplateItemsDao costVerifyTemplateItemsDao;
+
     /**
      * 异步生成Excel文件并保存到文件服务器
-     * @param type 类型:1-成本调查表 2-财务数据表
+     * @param type 类型:1-成本调查表 2-财务数据表 3-核定表
      * @param surveyTemplateId 模板ID
      * @param taskId 任务ID
      * @param refId 关联ID(成本调查表的uploadId或财务数据表的materialId)
@@ -118,6 +129,8 @@ public class CostSurveyExcelAsyncService {
             fileName = generateCostSurveyExcel(workbook, surveyTemplateId, taskId, refId, auditPeriods);
         } else if ("2".equals(type)) {
             fileName = generateFinancialDataExcel(workbook, surveyTemplateId, taskId, refId, auditPeriods);
+        } else if ("3".equals(type)) {
+            fileName = generateVerifyExcel(workbook, surveyTemplateId, taskId);
         } else {
             workbook.close();
             return;
@@ -223,10 +236,6 @@ public class CostSurveyExcelAsyncService {
                 totalColumns += 1;
             }
         }
-        boolean needExtraColumns = "2".equals(templateType) || "3".equals(templateType);
-        if (needExtraColumns) {
-            totalColumns += 2;
-        }
 
         Row titleRow = sheet.createRow(0);
         Cell titleCell = titleRow.createCell(0);
@@ -284,15 +293,18 @@ public class CostSurveyExcelAsyncService {
                     idx += 1;
                 }
             }
-            fillExcelData(sheet, itemsList, headerIndexMap, headersList, auditPeriods, templateType, rowIdColIndex, parentIdColIndex, dataStyle, taskId);
+            fillExcelData(sheet, itemsList, headerIndexMap, headersList, auditPeriods, templateType, rowIdColIndex, parentIdColIndex, dataStyle, taskId, uploadDataList);
         }
 
         Row headerRowForWidth = sheet.getRow(1);
         int totalColsForWidth = headerRowForWidth != null ? headerRowForWidth.getLastCellNum() : headersList.size();
         for (int i = 0; i < totalColsForWidth; i++) {
-            sheet.autoSizeColumn(i);
-            int autoWidth = sheet.getColumnWidth(i);
-            sheet.setColumnWidth(i, Math.max(autoWidth + 800, 3500));
+            Cell cell = headerRowForWidth.getCell(i);
+            String headerText = cell != null ? cell.getStringCellValue() : "";
+            int charWidth = ExcelStyleUtil.calculateCharWidth(headerText);
+            // 每个字符约256单位,再加一些边距
+            int columnWidth = (charWidth + 4) * 256;
+            sheet.setColumnWidth(i, Math.max(columnWidth, 3500));
         }
 
         if (rowIdColIndex >= 0) sheet.setColumnHidden(rowIdColIndex, true);
@@ -351,10 +363,6 @@ public class CostSurveyExcelAsyncService {
                 totalColumns += 1;
             }
         }
-        boolean needExtraColumns = "2".equals(templateType) || "3".equals(templateType);
-        if (needExtraColumns) {
-            totalColumns += 2;
-        }
 
         Row titleRow = sheet.createRow(0);
         Cell titleCell = titleRow.createCell(0);
@@ -412,15 +420,18 @@ public class CostSurveyExcelAsyncService {
                     idx += 1;
                 }
             }
-            fillExcelDataFd(sheet, itemsList, headerIndexMap, headersList, auditPeriods, templateType, rowIdColIndex, parentIdColIndex, dataStyle, taskId);
+            fillExcelDataFd(sheet, itemsList, headerIndexMap, headersList, auditPeriods, templateType, rowIdColIndex, parentIdColIndex, dataStyle, taskId, uploadDataList);
         }
 
         Row headerRowForWidth = sheet.getRow(1);
         int totalColsForWidth = headerRowForWidth != null ? headerRowForWidth.getLastCellNum() : headersList.size();
         for (int i = 0; i < totalColsForWidth; i++) {
-            sheet.autoSizeColumn(i);
-            int autoWidth = sheet.getColumnWidth(i);
-            sheet.setColumnWidth(i, Math.max(autoWidth + 800, 3500));
+            Cell cell = headerRowForWidth.getCell(i);
+            String headerText = cell != null ? cell.getStringCellValue() : "";
+            int charWidth = ExcelStyleUtil.calculateCharWidth(headerText);
+            // 每个字符约256单位,再加一些边距
+            int columnWidth = (charWidth + 4) * 256;
+            sheet.setColumnWidth(i, Math.max(columnWidth, 3500));
         }
 
         if (rowIdColIndex >= 0) sheet.setColumnHidden(rowIdColIndex, true);
@@ -431,18 +442,22 @@ public class CostSurveyExcelAsyncService {
 
     /**
      * 合并上传数据到成本调查表模板项
+     * uploadData 的 rkey 是字段名(如"项目名称"或"2025年(绑定年限)")
+     * item 的 rkey 也是字段名,需要用 rowid + rkey 来匹配
      */
     private void mergeUploadDataToItems(List<CostSurveyTemplateItems> itemsList, List<CostSurveyTemplateUploadData> uploadDataList) {
         if (itemsList == null || uploadDataList == null || uploadDataList.isEmpty()) return;
 
+        // 构建 rowid + "_" + rkey -> rvalue 的映射
         Map<String, String> uploadDataMap = new HashMap<>();
         for (CostSurveyTemplateUploadData data : uploadDataList) {
             String key = data.getRowid() + "_" + data.getRkey();
             uploadDataMap.put(key, data.getRvalue());
         }
 
+        // 用 item 的 rowid + rkey 来匹配
         for (CostSurveyTemplateItems item : itemsList) {
-            String key = item.getRowid() + "_" + item.getHeadersId();
+            String key = item.getRowid() + "_" + item.getRkey();
             if (uploadDataMap.containsKey(key)) {
                 item.setRvalue(uploadDataMap.get(key));
             }
@@ -451,18 +466,22 @@ public class CostSurveyExcelAsyncService {
 
     /**
      * 合并上传数据到财务数据表模板项
+     * uploadData 的 rkey 是字段名(如"项目名称"或"2025年(绑定年限)")
+     * item 的 rkey 也是字段名,需要用 rowid + rkey 来匹配
      */
     private void mergeUploadDataToFdItems(List<CostSurveyFdTemplateItems> itemsList, List<CostSurveyTemplateUploadData> uploadDataList) {
         if (itemsList == null || uploadDataList == null || uploadDataList.isEmpty()) return;
 
+        // 构建 rowid + "_" + rkey -> rvalue 的映射
         Map<String, String> uploadDataMap = new HashMap<>();
         for (CostSurveyTemplateUploadData data : uploadDataList) {
             String key = data.getRowid() + "_" + data.getRkey();
             uploadDataMap.put(key, data.getRvalue());
         }
 
+        // 用 item 的 rowid + rkey 来匹配
         for (CostSurveyFdTemplateItems item : itemsList) {
-            String key = item.getRowid() + "_" + item.getHeadersId();
+            String key = item.getRowid() + "_" + item.getRkey();
             if (uploadDataMap.containsKey(key)) {
                 item.setRvalue(uploadDataMap.get(key));
             }
@@ -472,10 +491,20 @@ public class CostSurveyExcelAsyncService {
     private void fillExcelData(Sheet sheet, List<CostSurveyTemplateItems> itemsList,
                                Map<String, Integer> headerIndexMap, List<CostSurveyTemplateHeaders> headersList,
                                String[] auditPeriods, String templateType,
-                               int rowIdColIndex, int parentIdColIndex, CellStyle dataStyle, String taskId) {
+                               int rowIdColIndex, int parentIdColIndex, CellStyle dataStyle, String taskId,
+                               List<CostSurveyTemplateUploadData> uploadDataList) {
         Map<String, CostSurveyTemplateHeaders> headersMap = headersList.stream()
                 .collect(Collectors.toMap(CostSurveyTemplateHeaders::getId, h -> h));
 
+        // 构建 rowid + "_" + rkey -> rvalue 的映射,用于获取年限字段的数据
+        Map<String, String> uploadDataMap = new HashMap<>();
+        if (uploadDataList != null) {
+            for (CostSurveyTemplateUploadData data : uploadDataList) {
+                String key = data.getRowid() + "_" + data.getRkey();
+                uploadDataMap.put(key, data.getRvalue());
+            }
+        }
+
         if ("1".equals(templateType)) {
             Row dataRow = sheet.createRow(2);
             for (CostSurveyTemplateItems item : itemsList) {
@@ -483,9 +512,15 @@ public class CostSurveyExcelAsyncService {
                 if (colIndex != null) {
                     CostSurveyTemplateHeaders header = headersMap.get(item.getHeadersId());
                     if (header != null && "true".equals(header.getIsAuditPeriod()) && auditPeriods != null && auditPeriods.length > 0) {
+                        // 绑定监审年限的字段,从 uploadDataMap 中获取数据
+                        String fieldName = header.getFieldName();
                         for (int i = 0; i < auditPeriods.length; i++) {
                             Cell cell = dataRow.createCell(colIndex + i);
-                            cell.setCellValue("");
+                            // rkey 格式:年限+字段名,如 "2025年(绑定年限)"
+                            String rkey = auditPeriods[i].trim() + fieldName;
+                            String key = item.getRowid() + "_" + rkey;
+                            String value = uploadDataMap.get(key);
+                            cell.setCellValue(StringUtil.isNotEmpty(value) ? value : "");
                             cell.setCellStyle(dataStyle);
                         }
                     } else {
@@ -521,9 +556,15 @@ public class CostSurveyExcelAsyncService {
                         if (colIndex != null) {
                             CostSurveyTemplateHeaders header = headersMap.get(item.getHeadersId());
                             if (header != null && "true".equals(header.getIsAuditPeriod()) && auditPeriods != null && auditPeriods.length > 0) {
+                                // 绑定监审年限的字段,从 uploadDataMap 中获取数据
+                                String fieldName = header.getFieldName();
                                 for (int i = 0; i < auditPeriods.length; i++) {
                                     Cell cell = dataRow.createCell(colIndex + i);
-                                    cell.setCellValue("");
+                                    // rkey 格式:年限+字段名,如 "2025年(绑定年限)"
+                                    String rkey = auditPeriods[i].trim() + fieldName;
+                                    String key = item.getRowid() + "_" + rkey;
+                                    String value = uploadDataMap.get(key);
+                                    cell.setCellValue(StringUtil.isNotEmpty(value) ? value : "");
                                     cell.setCellStyle(dataStyle);
                                 }
                             } else {
@@ -629,10 +670,20 @@ public class CostSurveyExcelAsyncService {
     private void fillExcelDataFd(Sheet sheet, List<CostSurveyFdTemplateItems> itemsList,
                                  Map<String, Integer> headerIndexMap, List<CostSurveyFdTemplateHeaders> headersList,
                                  String[] auditPeriods, String templateType,
-                                 int rowIdColIndex, int parentIdColIndex, CellStyle dataStyle, String taskId) {
+                                 int rowIdColIndex, int parentIdColIndex, CellStyle dataStyle, String taskId,
+                                 List<CostSurveyTemplateUploadData> uploadDataList) {
         Map<String, CostSurveyFdTemplateHeaders> headersMap = headersList.stream()
                 .collect(Collectors.toMap(CostSurveyFdTemplateHeaders::getId, h -> h));
 
+        // 构建 rowid + "_" + rkey -> rvalue 的映射,用于获取年限字段的数据
+        Map<String, String> uploadDataMap = new HashMap<>();
+        if (uploadDataList != null) {
+            for (CostSurveyTemplateUploadData data : uploadDataList) {
+                String key = data.getRowid() + "_" + data.getRkey();
+                uploadDataMap.put(key, data.getRvalue());
+            }
+        }
+
         if ("1".equals(templateType)) {
             Row dataRow = sheet.createRow(2);
             for (CostSurveyFdTemplateItems item : itemsList) {
@@ -640,9 +691,15 @@ public class CostSurveyExcelAsyncService {
                 if (colIndex != null) {
                     CostSurveyFdTemplateHeaders header = headersMap.get(item.getHeadersId());
                     if (header != null && "true".equals(header.getIsAuditPeriod()) && auditPeriods != null && auditPeriods.length > 0) {
+                        // 绑定监审年限的字段,从 uploadDataMap 中获取数据
+                        String fieldName = header.getFieldName();
                         for (int i = 0; i < auditPeriods.length; i++) {
                             Cell cell = dataRow.createCell(colIndex + i);
-                            cell.setCellValue("");
+                            // rkey 格式:年限+字段名,如 "2025年(绑定年限)"
+                            String rkey = auditPeriods[i].trim() + fieldName;
+                            String key = item.getRowid() + "_" + rkey;
+                            String value = uploadDataMap.get(key);
+                            cell.setCellValue(StringUtil.isNotEmpty(value) ? value : "");
                             cell.setCellStyle(dataStyle);
                         }
                     } else {
@@ -678,9 +735,15 @@ public class CostSurveyExcelAsyncService {
                         if (colIndex != null) {
                             CostSurveyFdTemplateHeaders header = headersMap.get(item.getHeadersId());
                             if (header != null && "true".equals(header.getIsAuditPeriod()) && auditPeriods != null && auditPeriods.length > 0) {
+                                // 绑定监审年限的字段,从 uploadDataMap 中获取数据
+                                String fieldName = header.getFieldName();
                                 for (int i = 0; i < auditPeriods.length; i++) {
                                     Cell cell = dataRow.createCell(colIndex + i);
-                                    cell.setCellValue("");
+                                    // rkey 格式:年限+字段名,如 "2025年(绑定年限)"
+                                    String rkey = auditPeriods[i].trim() + fieldName;
+                                    String key = item.getRowid() + "_" + rkey;
+                                    String value = uploadDataMap.get(key);
+                                    cell.setCellValue(StringUtil.isNotEmpty(value) ? value : "");
                                     cell.setCellStyle(dataStyle);
                                 }
                             } else {
@@ -763,4 +826,263 @@ public class CostSurveyExcelAsyncService {
         }
     }
 
+    /**
+     * 生成核定表Excel
+     */
+    private String generateVerifyExcel(Workbook workbook, String surveyTemplateId, String taskId) {
+        CostVerifyTemplate template = costVerifyTemplateManager.get(surveyTemplateId);
+        if (template == null) return "核定表";
+
+        String templateName = template.getSurveyTemplateName();
+
+        // 获取表头列表
+        QueryWrapper<CostVerifyTemplateHeaders> headersWrapper = new QueryWrapper<>();
+        headersWrapper.eq("survey_template_id", surveyTemplateId);
+        List<CostVerifyTemplateHeaders> headersList = costVerifyTemplateHeadersDao.selectList(headersWrapper)
+                .stream()
+                .filter(h -> StringUtil.isEmpty(h.getShowVisible()) || "1".equals(h.getShowVisible()))
+                .sorted(Comparator.comparing(h -> {
+                    String orderNum = h.getOrderNum();
+                    if (StringUtil.isEmpty(orderNum)) {
+                        return Integer.MAX_VALUE;
+                    }
+                    try {
+                        return Integer.parseInt(orderNum.trim());
+                    } catch (NumberFormatException e) {
+                        return Integer.MAX_VALUE;
+                    }
+                }))
+                .collect(Collectors.toList());
+
+        if (headersList.isEmpty()) return templateName;
+
+        // 获取模板项列表
+        List<CostVerifyTemplateItems> itemsList = costVerifyTemplateItemsDao.selectByVerifyTemplateId(surveyTemplateId, null);
+
+        // 获取上传数据
+        QueryWrapper<CostSurveyTemplateUploadData> wrapper = new QueryWrapper<>();
+        wrapper.eq("task_id", taskId).eq("type", "3").eq("is_deleted", "0");
+        List<CostSurveyTemplateUploadData> uploadDataList = costSurveyTemplateUploadDataManager.list(wrapper);
+
+        // 合并上传数据到模板项
+        mergeUploadDataToVerifyItems(itemsList, uploadDataList);
+
+        Sheet sheet = workbook.createSheet(templateName);
+        CellStyle titleStyle = ExcelStyleUtil.createTitleStyle(workbook);
+        CellStyle headerStyle = ExcelStyleUtil.createHeaderStyle(workbook);
+        CellStyle dataStyle = ExcelStyleUtil.createDataStyle(workbook);
+
+        // 从itemsList中获取unit值,按headersId分组取第一个非空的unit
+        Map<String, String> headersIdToUnitMap = new HashMap<>();
+        if (itemsList != null) {
+            for (CostVerifyTemplateItems item : itemsList) {
+                if (StringUtil.isNotEmpty(item.getHeadersId()) && StringUtil.isNotEmpty(item.getUnit())) {
+                    headersIdToUnitMap.putIfAbsent(item.getHeadersId(), item.getUnit());
+                }
+            }
+        }
+
+        int totalColumns = headersList.size();
+
+        // 第一行:大标题
+        Row titleRow = sheet.createRow(0);
+        Cell titleCell = titleRow.createCell(0);
+        titleCell.setCellValue(templateName);
+        titleCell.setCellStyle(titleStyle);
+        if (totalColumns > 1) {
+            sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(0, 0, 0, totalColumns - 1));
+        }
+
+        // 第二行:列表头
+        Row headerRow = sheet.createRow(1);
+        int colIndex = 0;
+        for (CostVerifyTemplateHeaders header : headersList) {
+            Cell headerCell = headerRow.createCell(colIndex++);
+            String fieldName = header.getFieldName();
+            String unit = headersIdToUnitMap.get(header.getId());
+            String headerValue = StringUtil.isNotEmpty(unit) ? fieldName + "(" + unit + ")" : fieldName;
+            headerCell.setCellValue(headerValue);
+            headerCell.setCellStyle(headerStyle);
+        }
+
+        // 填充数据
+        if (itemsList != null && !itemsList.isEmpty()) {
+            Map<String, Integer> headerIndexMap = new HashMap<>();
+            for (int i = 0; i < headersList.size(); i++) {
+                headerIndexMap.put(headersList.get(i).getId(), i);
+            }
+            fillExcelDataVerify(sheet, itemsList, headerIndexMap, dataStyle, uploadDataList);
+        }
+
+        // 设置列宽
+        Row headerRowForWidth = sheet.getRow(1);
+        int totalColsForWidth = headerRowForWidth != null ? headerRowForWidth.getLastCellNum() : headersList.size();
+        for (int i = 0; i < totalColsForWidth; i++) {
+            Cell cell = headerRowForWidth.getCell(i);
+            String headerText = cell != null ? cell.getStringCellValue() : "";
+            int charWidth = ExcelStyleUtil.calculateCharWidth(headerText);
+            int columnWidth = (charWidth + 4) * 256;
+            sheet.setColumnWidth(i, Math.max(columnWidth, 3500));
+        }
+
+        return templateName;
+    }
+
+    /**
+     * 合并上传数据到核定表模板项
+     */
+    private void mergeUploadDataToVerifyItems(List<CostVerifyTemplateItems> itemsList, List<CostSurveyTemplateUploadData> uploadDataList) {
+        if (itemsList == null || uploadDataList == null || uploadDataList.isEmpty()) return;
+
+        // 构建 rowid + "_" + rkey -> rvalue 的映射
+        Map<String, String> uploadDataMap = new HashMap<>();
+        for (CostSurveyTemplateUploadData data : uploadDataList) {
+            String key = data.getRowid() + "_" + data.getRkey();
+            uploadDataMap.put(key, data.getRvalue());
+        }
+
+        // 用 item 的 rowid + rkey 来匹配
+        for (CostVerifyTemplateItems item : itemsList) {
+            String key = item.getRowid() + "_" + item.getRkey();
+            if (uploadDataMap.containsKey(key)) {
+                item.setRvalue(uploadDataMap.get(key));
+            }
+        }
+    }
+
+    /**
+     * 填充核定表Excel数据
+     */
+    private void fillExcelDataVerify(Sheet sheet, List<CostVerifyTemplateItems> itemsList,
+                                     Map<String, Integer> headerIndexMap, CellStyle dataStyle,
+                                     List<CostSurveyTemplateUploadData> uploadDataList) {
+        if (itemsList == null || itemsList.isEmpty()) {
+            return;
+        }
+
+        // 构建 rowid + "_" + rkey -> rvalue 的映射
+        Map<String, String> uploadDataMap = new HashMap<>();
+        if (uploadDataList != null) {
+            for (CostSurveyTemplateUploadData data : uploadDataList) {
+                String key = data.getRowid() + "_" + data.getRkey();
+                uploadDataMap.put(key, data.getRvalue());
+            }
+        }
+
+        // 按 rowid 分组
+        Map<String, List<CostVerifyTemplateItems>> itemsByRowId = itemsList.stream()
+                .filter(item -> StringUtil.isNotEmpty(item.getRowid()))
+                .collect(Collectors.groupingBy(CostVerifyTemplateItems::getRowid));
+
+        // 按父子关系排序
+        List<String> sortedRowIds = sortRowIdsByParentChildVerify(itemsByRowId);
+
+        // 获取表头行,计算总列数
+        Row headerRow = sheet.getRow(1);
+        int totalColumns = headerRow != null ? headerRow.getLastCellNum() : 0;
+
+        int rowNum = 2;
+        for (String rowId : sortedRowIds) {
+            List<CostVerifyTemplateItems> rowItems = itemsByRowId.get(rowId);
+            if (rowItems != null && !rowItems.isEmpty()) {
+                Row dataRow = sheet.createRow(rowNum++);
+
+                // 填充数据列
+                for (CostVerifyTemplateItems item : rowItems) {
+                    Integer colIndex = headerIndexMap.get(item.getHeadersId());
+                    if (colIndex != null) {
+                        Cell cell = dataRow.createCell(colIndex);
+                        String value = item.getRvalue();
+                        cell.setCellValue(StringUtil.isNotEmpty(value) ? value : "");
+                        cell.setCellStyle(dataStyle);
+                    }
+                }
+
+                // 为所有剩余列创建空单元格并应用样式
+                for (int colIdx = 0; colIdx < totalColumns; colIdx++) {
+                    if (dataRow.getCell(colIdx) == null) {
+                        Cell cell = dataRow.createCell(colIdx);
+                        cell.setCellValue("");
+                        cell.setCellStyle(dataStyle);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 按父子关系排序rowId(核定表)
+     */
+    private List<String> sortRowIdsByParentChildVerify(Map<String, List<CostVerifyTemplateItems>> itemsByRowId) {
+        List<String> result = new ArrayList<>();
+
+        // 构建rowId到parentid的映射
+        Map<String, String> rowIdToParentId = new HashMap<>();
+        for (Map.Entry<String, List<CostVerifyTemplateItems>> entry : itemsByRowId.entrySet()) {
+            String rowId = entry.getKey();
+            List<CostVerifyTemplateItems> items = entry.getValue();
+            if (!items.isEmpty()) {
+                String parentId = items.get(0).getParentid();
+                rowIdToParentId.put(rowId, parentId);
+            }
+        }
+
+        // 找出所有根节点(parentid为空或不存在的),按 orderNum 排序
+        Set<String> allRowIds = new HashSet<>(itemsByRowId.keySet());
+        List<String> rootRowIds = allRowIds.stream()
+                .filter(rowId -> {
+                    String parentId = rowIdToParentId.get(rowId);
+                    return StringUtil.isEmpty(parentId) || !allRowIds.contains(parentId);
+                })
+                .sorted((id1, id2) -> {
+                    List<CostVerifyTemplateItems> items1 = itemsByRowId.get(id1);
+                    List<CostVerifyTemplateItems> items2 = itemsByRowId.get(id2);
+                    if (items1 == null || items1.isEmpty()) return 1;
+                    if (items2 == null || items2.isEmpty()) return -1;
+                    Integer order1 = items1.get(0).getOrderNum();
+                    Integer order2 = items2.get(0).getOrderNum();
+                    if (order1 == null) return 1;
+                    if (order2 == null) return -1;
+                    return order1.compareTo(order2);
+                })
+                .collect(Collectors.toList());
+
+        // 递归添加节点及其子节点
+        for (String rootRowId : rootRowIds) {
+            addRowIdWithChildrenVerify(rootRowId, rowIdToParentId, allRowIds, result, itemsByRowId);
+        }
+
+        return result;
+    }
+
+    /**
+     * 递归添加rowId及其所有子节点(核定表)
+     */
+    private void addRowIdWithChildrenVerify(String rowId, Map<String, String> rowIdToParentId,
+                                            Set<String> allRowIds, List<String> result,
+                                            Map<String, List<CostVerifyTemplateItems>> itemsByRowId) {
+        result.add(rowId);
+
+        // 找出所有子节点,按 orderNum 排序
+        List<String> children = allRowIds.stream()
+                .filter(id -> rowId.equals(rowIdToParentId.get(id)))
+                .sorted((id1, id2) -> {
+                    List<CostVerifyTemplateItems> items1 = itemsByRowId.get(id1);
+                    List<CostVerifyTemplateItems> items2 = itemsByRowId.get(id2);
+                    if (items1 == null || items1.isEmpty()) return 1;
+                    if (items2 == null || items2.isEmpty()) return -1;
+                    Integer order1 = items1.get(0).getOrderNum();
+                    Integer order2 = items2.get(0).getOrderNum();
+                    if (order1 == null) return 1;
+                    if (order2 == null) return -1;
+                    return order1.compareTo(order2);
+                })
+                .collect(Collectors.toList());
+
+        // 递归处理子节点
+        for (String childRowId : children) {
+            addRowIdWithChildrenVerify(childRowId, rowIdToParentId, allRowIds, result, itemsByRowId);
+        }
+    }
+
 }

+ 18 - 0
assistMg/src/main/java/com/hotent/project/controller/CostProjectConclusionController.java

@@ -5,9 +5,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.hotent.base.annotation.ApiGroup;
 import com.hotent.base.constants.ApiGroupConsts;
+import com.hotent.base.enums.ResponseErrorEnums;
+import com.hotent.project.manager.CostProjectProccessNodeDetailManager;
 import com.hotent.project.model.CostProjectDeliberate;
 import com.hotent.project.req.CostProjectBasePageReq;
 import io.swagger.annotations.Api;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -35,6 +38,21 @@ import com.hotent.project.manager.CostProjectConclusionManager;
 @ApiGroup(group = {ApiGroupConsts.GROUP_COST})
 public class CostProjectConclusionController extends BaseController<CostProjectConclusionManager, CostProjectConclusion> {
 
+	@Autowired
+	private CostProjectProccessNodeDetailManager costProjectProccessNodeDetailManager;
+
+
+	@PostMapping("/")
+	@ApiOperation("添加实体的接口")
+	public CommonResult<String> create(@ApiParam(name="model", value="实体信息") @RequestBody CostProjectConclusion costProjectConclusion) {
+		boolean result = baseService.save(costProjectConclusion);
+		if(!result) {
+			return new CommonResult<>(ResponseErrorEnums.FAIL_OPTION, null);
+		}
+		costProjectProccessNodeDetailManager.recordByTaskId(costProjectConclusion.getTaskId(), "uploadMaterial", "新增项目结论", null);
+		return new CommonResult<>();
+	}
+
 	/**
 	 * 根据id获取监审项目结论表数据详情
 	 * @param projectId 项目id

+ 6 - 0
assistMg/src/main/java/com/hotent/project/controller/CostProjectDeliberateController.java

@@ -7,6 +7,7 @@ import com.hotent.base.constants.ApiGroupConsts;
 import com.hotent.base.exception.BaseException;
 import com.hotent.base.util.UniqueIdUtil;
 import com.hotent.project.manager.CostProjectApprovalManager;
+import com.hotent.project.manager.CostProjectProccessNodeDetailManager;
 import com.hotent.project.manager.CostProjectTaskManager;
 import com.hotent.project.model.CostProjectApproval;
 import com.hotent.project.model.CostProjectTask;
@@ -50,6 +51,10 @@ public class CostProjectDeliberateController extends BaseController<CostProjectD
 
 	@Autowired
 	private CostProjectTaskManager costProjectTaskManager;
+
+	@Autowired
+	private CostProjectProccessNodeDetailManager costProjectProccessNodeDetailManager;
+
 	/**
 	 * 根据id获取监审项目集体审议表数据详情
 	 * @param id
@@ -85,6 +90,7 @@ public class CostProjectDeliberateController extends BaseController<CostProjectD
 		req.setCreateTime(LocalDateTime.now());
 		req.setId(UniqueIdUtil.getSuid());
 		baseService.create(req);
+		costProjectProccessNodeDetailManager.recordByTaskId(task.getId(), "uploadMaterial", "新增集体审议记录", null);
 		return CommonResult.<String>ok().message("保存成功");
 	}
 

+ 19 - 0
assistMg/src/main/java/com/hotent/project/controller/CostProjectDocumentController.java

@@ -2,6 +2,7 @@ package com.hotent.project.controller;
 
 
 import com.aliyun.tea.utils.StringUtils;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.hotent.base.annotation.ApiGroup;
 import com.hotent.base.constants.ApiGroupConsts;
@@ -10,8 +11,11 @@ import com.hotent.common.CrudService;
 import com.hotent.config.EipConfig;
 import com.hotent.constant.BaseConstant;
 import com.hotent.project.manager.CostProjectDocumentFileManager;
+import com.hotent.project.manager.CostProjectProccessNodeDetailManager;
+import com.hotent.project.manager.CostProjectTaskManager;
 import com.hotent.project.model.CostProjectApproval;
 import com.hotent.project.model.CostProjectDocumentFile;
+import com.hotent.project.model.CostProjectTask;
 import com.hotent.project.req.CostProjectApprovalPageReq;
 import com.hotent.project.req.CostProjectBasePageReq;
 import com.hotent.project.req.CostProjectDocumentPageReq;
@@ -59,6 +63,13 @@ public class CostProjectDocumentController extends BaseController<CostProjectDoc
 
 	@Autowired
 	private CostProjectDocumentFileManager costProjectDocumentFileManager;
+
+	@Autowired
+	private CostProjectProccessNodeDetailManager costProjectProccessNodeDetailManager;
+
+	@Autowired
+	private CostProjectTaskManager costProjectTaskManager;
+
 	/**
 	 * 根据id获取监审项目文书表数据详情
 	 * @param id
@@ -90,6 +101,14 @@ public class CostProjectDocumentController extends BaseController<CostProjectDoc
 	@ApiOperation(value="新增save",httpMethod = "POST",notes = "新增save")
 	public CommonResult<String> save(@RequestBody CostProjectDocumentReq req) throws Exception {
 		String id= costProjectDocumentManager.create(req);
+		CostProjectTask task = costProjectTaskManager.getOne(
+				new LambdaQueryWrapper<CostProjectTask>()
+						.eq(CostProjectTask::getProjectId, req.getProjectId())
+						.eq(CostProjectTask::getPid, "0")
+		);
+		if (task != null){
+			costProjectProccessNodeDetailManager.recordByTaskId(task.getId(), "uploadMaterial", "生成文书", null);
+		}
 		return CommonResult.<String>ok().value(id).message("保存成功");
 
 	}

+ 1 - 1
assistMg/src/main/java/com/hotent/project/controller/CostProjectTaskMaterialController.java

@@ -49,7 +49,7 @@ public class CostProjectTaskMaterialController extends BaseController<CostProjec
     }
 
     /**
-     * 新增,更新任务定制-报送资料要求
+     * 新增,更新任务定制-报送资料要求 1、企业填报资料
      *
      * @param costProjectTaskMaterial
      * @return

+ 15 - 0
assistMg/src/main/java/com/hotent/project/dao/CostProjectProccessNodeDetailDao.java

@@ -0,0 +1,15 @@
+package com.hotent.project.dao;
+
+import com.hotent.project.model.CostProjectProccessNodeDetail;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+ * 流程环节明细表 Mapper 接口
+ *
+ * @company 山西清众科技股份有限公司
+ * @author 超级管理员
+ * @since 2025-12-23
+ */
+public interface CostProjectProccessNodeDetailDao extends BaseMapper<CostProjectProccessNodeDetail> {
+
+}

+ 37 - 0
assistMg/src/main/java/com/hotent/project/manager/CostProjectProccessNodeDetailManager.java

@@ -0,0 +1,37 @@
+package com.hotent.project.manager;
+
+import com.hotent.project.model.CostProjectProccessNodeDetail;
+import com.hotent.base.manager.BaseManager;
+
+import java.util.List;
+
+/**
+ * 流程环节明细表 服务类
+ *
+ * @company 山西清众科技股份有限公司
+ * @author 超级管理员
+ * @since 2025-12-23
+ */
+public interface CostProjectProccessNodeDetailManager extends BaseManager<CostProjectProccessNodeDetail> {
+
+    /**
+     * 根据主键获取详情
+     *
+     * @param id 主键ID
+     * @return 明细对象
+     */
+    CostProjectProccessNodeDetail getDetail(String id);
+
+
+
+
+    /**
+     * 【最简】根据任务ID记录环节明细(自动获取流程、节点、用户、来源类型等所有信息)
+     *
+     * @param taskId     任务ID
+     * @param actionType 操作类型
+     * @param actionName 操作名称
+     * @param remark     备注
+     */
+    void recordByTaskId(String taskId, String actionType, String actionName, String remark);
+}

+ 13 - 2
assistMg/src/main/java/com/hotent/project/manager/impl/CostProjectProccessManagerImpl.java

@@ -11,6 +11,7 @@ import com.hotent.project.model.CostProjectProccess;
 import com.hotent.project.dao.CostProjectProccessDao;
 import com.hotent.base.manager.impl.BaseManagerImpl;
 import com.hotent.project.model.CostProjectProccessNode;
+import com.hotent.project.model.CostProjectProccessNodeDetail;
 import com.hotent.project.model.CostProjectProccessNodeTemplete;
 import com.hotent.project.req.CostProjectProcessNodeReq;
 import com.hotent.project.req.CostProjectProcessReq;
@@ -65,6 +66,9 @@ public class CostProjectProccessManagerImpl extends BaseManagerImpl<CostProjectP
     @Autowired
     private UserManager userManager;
 
+    @Autowired
+    private CostProjectProccessNodeDetailManager costProjectProccessNodeDetailManager;
+
 
     @Override
     public CostProjectProccessResp getDetail(String id) {
@@ -199,8 +203,15 @@ public class CostProjectProccessManagerImpl extends BaseManagerImpl<CostProjectP
             nodeResp.setActRemarks(node.getActRemarks());
             nodeResp.setActUserNames(node.getActUserNames());
 
-
-
+            // 填充环节明细列表
+            List<CostProjectProccessNodeDetail> detailList = costProjectProccessNodeDetailManager.list(
+                    new QueryWrapper<CostProjectProccessNodeDetail>()
+                            .eq("process_node_id", node.getId())
+            );
+            for (CostProjectProccessNodeDetail detail : detailList) {
+                detail.setSourceTypeName("1".equals(detail.getSourceType()) ? "外部" : "内部");
+            }
+            nodeResp.setDetailList(detailList);
 
             return nodeResp;
         }).collect(Collectors.toList()));

+ 191 - 0
assistMg/src/main/java/com/hotent/project/manager/impl/CostProjectProccessNodeDetailManagerImpl.java

@@ -0,0 +1,191 @@
+package com.hotent.project.manager.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.hotent.base.manager.impl.BaseManagerImpl;
+import com.hotent.base.util.AuthenticationUtil;
+import com.hotent.base.util.StringUtil;
+import com.hotent.project.dao.CostProjectProccessNodeDetailDao;
+import com.hotent.project.manager.CostProjectProccessNodeDetailManager;
+import com.hotent.project.manager.CostProjectProccessNodeManager;
+import com.hotent.project.manager.CostProjectProccessManager;
+import com.hotent.project.manager.CostProjectTaskManager;
+import com.hotent.project.manager.CostProjectApprovalManager;
+import com.hotent.project.model.CostProjectProccessNodeDetail;
+import com.hotent.project.model.CostProjectProccessNode;
+import com.hotent.project.model.CostProjectProccess;
+import com.hotent.project.model.CostProjectTask;
+import com.hotent.project.model.CostProjectApproval;
+import com.hotent.uc.api.model.IUser;
+import com.hotent.uc.manager.OrgManager;
+import com.hotent.uc.model.Org;
+import com.hotent.uc.util.ContextUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 流程环节明细表 服务实现类
+ *
+ * @company 山西清众科技股份有限公司
+ * @author 超级管理员
+ * @since 2025-12-23
+ */
+@Service
+public class CostProjectProccessNodeDetailManagerImpl
+        extends BaseManagerImpl<CostProjectProccessNodeDetailDao, CostProjectProccessNodeDetail>
+        implements CostProjectProccessNodeDetailManager {
+
+    @Autowired
+    private CostProjectProccessNodeManager costProjectProccessNodeManager;
+
+    @Autowired
+    private CostProjectProccessManager costProjectProccessManager;
+
+    @Autowired
+    private CostProjectTaskManager costProjectTaskManager;
+
+    @Autowired
+    private CostProjectApprovalManager costProjectApprovalManager;
+
+    @Autowired
+    private OrgManager orgManager;
+
+    @Override
+    public CostProjectProccessNodeDetail getDetail(String id) {
+        CostProjectProccessNodeDetail detail = this.getById(id);
+        if (detail != null) {
+            // 设置来源类型名称
+            detail.setSourceTypeName("1".equals(detail.getSourceType()) ? "外部" : "内部");
+        }
+        return detail;
+    }
+
+
+
+
+
+    @Override
+    @Transactional
+    public void recordByTaskId(String taskId, String actionType, String actionName, String remark) {
+        try {
+            // 1. 获取任务信息
+            CostProjectTask task = costProjectTaskManager.getById(taskId);
+            if (task == null) {
+                return;
+            }
+
+            // 2. 获取流程信息
+            CostProjectProccess proccess = costProjectProccessManager.getOne(
+                    new LambdaQueryWrapper<CostProjectProccess>()
+                            .eq(CostProjectProccess::getProjectId, task.getProjectId())
+            );
+            if (proccess == null) {
+                return;
+            }
+
+            // 3. 获取流程节点信息
+            String processNodeId = "";
+            CostProjectProccessNode node = costProjectProccessNodeManager.getOne(
+                    new LambdaQueryWrapper<CostProjectProccessNode>()
+                            .eq(CostProjectProccessNode::getProcessId, proccess.getProcessId())
+                            .eq(CostProjectProccessNode::getProcessNodeKey, task.getCurrentNode())
+            );
+            if (node != null) {
+                processNodeId = node.getId();
+            }
+
+            // 4. 判断来源类型(内部/外部)
+            String sourceType = determineSourceType(task.getProjectId());
+
+            // 5. 记录明细
+            // 获取当前用户信息
+            IUser currentUser = ContextUtil.getCurrentUser();
+            String executorId = currentUser != null ? currentUser.getUserId() : "";
+            String executorName = currentUser != null ? currentUser.getFullname() : "";
+
+            // 获取用户部门
+            String executorDept = "";
+            try {
+                String orgId = AuthenticationUtil.getCurrentUserMainOrgId();
+                if (StringUtil.isNotEmpty(orgId)) {
+                    Org org = orgManager.getById(orgId);
+                    if (org != null) {
+                        executorDept = org.getName();
+                    }
+                }
+            } catch (Exception e) {
+                // 忽略获取部门异常
+            }
+
+            CostProjectProccessNodeDetail detail = new CostProjectProccessNodeDetail();
+            detail.setProcessId(proccess.getProcessId());
+            detail.setProcessNodeId(processNodeId);
+            detail.setProcessNodeKey(task.getCurrentNode());
+            detail.setTaskId(taskId);
+            detail.setSourceType(sourceType);
+            detail.setExecutorId(executorId);
+            detail.setExecutorName(executorName);
+            detail.setExecutorDept(executorDept);
+            detail.setActionType(actionType);
+            detail.setActionName(actionName);
+            detail.setExecuteTime(LocalDateTime.now());
+            detail.setRemark(remark);
+            detail.setAuditedUnitId(task.getAuditedUnitId());
+            detail.setAuditedUnitName(task.getAuditedUnitName());
+            detail.setIsDeleted("0");
+
+            this.save(detail);
+        } catch (Exception e) {
+            // 记录明细失败不影响主流程
+        }
+    }
+
+    /**
+     * 判断当前用户的来源类型
+     * 如果是监审任务负责人或项目组成员,则为内部(2),否则为外部(1)
+     */
+    private String determineSourceType(String projectId) {
+        try {
+            IUser currentUser = ContextUtil.getCurrentUser();
+            if (currentUser == null) {
+                return "1";
+            }
+            String currentUserId = currentUser.getUserId();
+
+            CostProjectApproval approval = costProjectApprovalManager.getById(projectId);
+            if (approval == null) {
+                return "1";
+            }
+
+            // 检查是否是监审任务负责人
+            if (StringUtil.isNotEmpty(approval.getLeaderId()) && approval.getLeaderId().equals(currentUserId)) {
+                return "2";
+            }
+
+            // 检查是否是项目组成员
+            if (StringUtil.isNotEmpty(approval.getProjectMembers())) {
+                for (String memberId : approval.getProjectMembers().split(",")) {
+                    if (memberId.trim().equals(currentUserId)) {
+                        return "2";
+                    }
+                }
+            }
+
+            // 检查是否是监审组人员
+            if (StringUtil.isNotEmpty(approval.getAuditGroup())) {
+                for (String memberId : approval.getAuditGroup().split(",")) {
+                    if (memberId.trim().equals(currentUserId)) {
+                        return "2";
+                    }
+                }
+            }
+
+            return "1";
+        } catch (Exception e) {
+            return "1";
+        }
+    }
+}

+ 121 - 43
assistMg/src/main/java/com/hotent/project/manager/impl/CostProjectTaskManagerImpl.java

@@ -113,6 +113,9 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
     @Autowired
     private TaskWarningStatusComponent taskWarningStatusComponent;
 
+    @Autowired
+    private CostProjectProccessNodeDetailManager costProjectProccessNodeDetailManager;
+
 
     @Override
     public List<CostProjectTask> getTaskList(CostTaskSearchReq req) throws Exception {
@@ -123,11 +126,13 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         // 1. 先查询当前用户有权限的立项(负责人或项目组成员)
         LambdaQueryWrapper<CostProjectApproval> approvalQueryWrapper = new LambdaQueryWrapper<>();
         approvalQueryWrapper.eq(CostProjectApproval::getIsDeleted, "0");
-        if (StringUtil.isNotEmpty(req.getProjectName())) {
-            approvalQueryWrapper.like(CostProjectApproval::getProjectName,req.getProjectName());
-        }
-        if (StringUtil.isNotEmpty(req.getYear())) {
-            approvalQueryWrapper.eq(CostProjectApproval::getProjectYear,req.getYear());
+        if(req!=null) {
+            if (StringUtil.isNotEmpty(req.getProjectName())) {
+                approvalQueryWrapper.like(CostProjectApproval::getProjectName, req.getProjectName());
+            }
+            if (StringUtil.isNotEmpty(req.getYear())) {
+                approvalQueryWrapper.eq(CostProjectApproval::getProjectYear, req.getYear());
+            }
         }
         approvalQueryWrapper.and(wrapper -> wrapper
                 .eq(CostProjectApproval::getLeaderId, currentUserId)
@@ -449,6 +454,9 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         String sendTarget = task.getCreateBy() == null ? "" : task.getCreateBy();
         costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, sendTarget);
 
+        // 记录环节明细
+        recordNodeDetail(task, "submit", title, null);
+
         return "任务提交成功";
     }
 
@@ -471,6 +479,10 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         String noticeSource = (org.getName()) + " " + AuthenticationUtil.getCurrentUserFullname();
         String sendTarget = task.getCreateBy() == null ? "" : task.getCreateBy();
         costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, sendTarget);
+
+        // 记录环节明细
+        recordNodeDetail(task, "feedback", title, null);
+
         return "意见反馈完成";
 
     }
@@ -581,6 +593,10 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         String noticeSource = (org.getName()) + " " + AuthenticationUtil.getCurrentUserFullname();
         String sendTarget = task.getCreateBy() == null ? "" : task.getCreateBy();
         costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, sendTarget);
+
+        // 记录环节明细
+        recordNodeDetail(task, "pass", title, null);
+
         return title;
     }
 
@@ -608,59 +624,79 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         task.setStatus(TaskStatusConstant.NOT_PASSED.getStatusCode());
         costProjectTaskManager.updateById(task);
 
+        // 记录环节明细
+        recordNodeDetail(task, "reject", title, req.getContent());
+
         return title;
     }
 
     /**
-     * 中止/恢复,如果是主任务中止,所有子任务同步中止;如果是主任务恢复,所有子任务同步恢复
+     * 中止/恢复,支持主任务和子任务操作
+     * 主任务中止:所有子任务同步中止
+     * 主任务恢复:所有子任务同步恢复
+     * 子任务中止/恢复:仅操作该子任务本身
      */
     private String suspendTask(CostProjectTask task, CostTaskPageReq req) {
-        // 恢复操作只支持主任务
-        if (!"0".equals(task.getPid())) {
-            throw new RuntimeException("仅支持主任务进行此操作");
-        }
-
         // 判断是中止还是恢复
         String status = req.getStatus() == null || TaskStatusConstant.SUSPENDED.getStatusCode().equals(req.getStatus()) ? TaskStatusConstant.SUSPENDED.getStatusCode() : TaskStatusConstant.AUDITING.getStatusCode();
 
-        // 中止操作:保存中止前的状态
-        if (TaskStatusConstant.SUSPENDED.getStatusCode().equals(status)) {
-            task.setPreSuspendStatus(task.getStatus());
-            List<CostProjectTask> children = costProjectTaskManager.list(
-                    new LambdaQueryWrapper<CostProjectTask>()
-                            .eq(CostProjectTask::getPid, task.getId())
-                            .eq(CostProjectTask::getIsDeleted, "0")
-            );
-            for (CostProjectTask child : children) {
-                child.setPreSuspendStatus(child.getStatus());
-                child.setStatus(status);
-                costProjectTaskManager.updateById(child);
+        // 判断是主任务还是子任务
+        boolean isMainTask = "0".equals(task.getPid());
+
+        if (isMainTask) {
+            // 主任务操作:同步所有子任务
+            if (TaskStatusConstant.SUSPENDED.getStatusCode().equals(status)) {
+                // 中止操作:保存中止前的状态
+                task.setPreSuspendStatus(task.getStatus());
+                List<CostProjectTask> children = costProjectTaskManager.list(
+                        new LambdaQueryWrapper<CostProjectTask>()
+                                .eq(CostProjectTask::getPid, task.getId())
+                                .eq(CostProjectTask::getIsDeleted, "0")
+                );
+                for (CostProjectTask child : children) {
+                    child.setPreSuspendStatus(child.getStatus());
+                    child.setStatus(status);
+                    costProjectTaskManager.updateById(child);
+                }
+            } else {
+                // 恢复操作:恢复到中止前的状态
+                String preStatus = task.getPreSuspendStatus();
+                task.setStatus(StringUtil.isNotEmpty(preStatus) ? preStatus : TaskStatusConstant.AUDITING.getStatusCode());
+                task.setPreSuspendStatus(null);
+
+                List<CostProjectTask> children = costProjectTaskManager.list(
+                        new LambdaQueryWrapper<CostProjectTask>()
+                                .eq(CostProjectTask::getPid, task.getId())
+                                .eq(CostProjectTask::getIsDeleted, "0")
+                );
+                for (CostProjectTask child : children) {
+                    String childPreStatus = child.getPreSuspendStatus();
+                    child.setStatus(StringUtil.isNotEmpty(childPreStatus) ? childPreStatus : TaskStatusConstant.AUDITING.getStatusCode());
+                    child.setPreSuspendStatus(null);
+                    costProjectTaskManager.updateById(child);
+                }
             }
+            task.setStatus(status);
+            costProjectTaskManager.updateById(task);
         } else {
-            // 恢复操作:恢复到中止前的状态
-            String preStatus = task.getPreSuspendStatus();
-            task.setStatus(StringUtil.isNotEmpty(preStatus) ? preStatus : TaskStatusConstant.AUDITING.getStatusCode());
-            task.setPreSuspendStatus(null);
-
-            List<CostProjectTask> children = costProjectTaskManager.list(
-                    new LambdaQueryWrapper<CostProjectTask>()
-                            .eq(CostProjectTask::getPid, task.getId())
-                            .eq(CostProjectTask::getIsDeleted, "0")
-            );
-            for (CostProjectTask child : children) {
-                String childPreStatus = child.getPreSuspendStatus();
-                child.setStatus(StringUtil.isNotEmpty(childPreStatus) ? childPreStatus : TaskStatusConstant.AUDITING.getStatusCode());
-                child.setPreSuspendStatus(null);
-                costProjectTaskManager.updateById(child);
+            // 子任务操作:仅操作该子任务本身
+            if (TaskStatusConstant.SUSPENDED.getStatusCode().equals(status)) {
+                // 中止操作:保存中止前的状态
+                task.setPreSuspendStatus(task.getStatus());
+                task.setStatus(status);
+            } else {
+                // 恢复操作:恢复到中止前的状态
+                String preStatus = task.getPreSuspendStatus();
+                task.setStatus(StringUtil.isNotEmpty(preStatus) ? preStatus : TaskStatusConstant.AUDITING.getStatusCode());
+                task.setPreSuspendStatus(null);
             }
+            costProjectTaskManager.updateById(task);
         }
 
-        task.setStatus(status);
-        costProjectTaskManager.updateById(task);
-
         // 通知内容组装
-        String title = NodeConstant.getNodeValueByKey(task.getCurrentNode()) + TaskStatusConstant.getStatusNameByCode(status);
-        String content = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]" + AuthenticationUtil.getCurrentUserFullname() + "已" + TaskStatusConstant.getStatusNameByCode(status) + task.getAuditedUnitName() + "的项目(" + task.getProjectName() + ")";
+        String taskTypeDesc = isMainTask ? "" : "子任务";
+        String title = NodeConstant.getNodeValueByKey(task.getCurrentNode()) + taskTypeDesc + TaskStatusConstant.getStatusNameByCode(status);
+        String content = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]" + AuthenticationUtil.getCurrentUserFullname() + "已" + TaskStatusConstant.getStatusNameByCode(status) + task.getAuditedUnitName() + "的" + (isMainTask ? "项目" : "子任务") + "(" + task.getProjectName() + ")";
         if (StringUtil.isNotEmpty(req.getContent())) {
             content += ",原因:" + req.getContent();
         }
@@ -670,6 +706,11 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         String noticeSource = (org.getName()) + " " + AuthenticationUtil.getCurrentUserFullname();
         String sendTarget = task.getCreateBy() == null ? "" : task.getCreateBy();
         costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, sendTarget);
+
+        // 记录环节明细
+        String actionType = TaskStatusConstant.SUSPENDED.getStatusCode().equals(status) ? "suspend" : "resume";
+        recordNodeDetail(task, actionType, title, req.getContent());
+
         return title;
     }
 
@@ -717,6 +758,10 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
                     }
                     // 返回结果,包含所有单位名称
                     String unitNamesStr = unitNames.isEmpty() ? "" : String.join("、", unitNames);
+
+                    // 记录环节明细
+                    recordNodeDetail(task, "supplement", title + "(单位:" + unitNamesStr + ")", null);
+
                     return title + "(单位:" + unitNamesStr + ")";
                 }
             default:
@@ -730,6 +775,10 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
                 String enterpriseId = task.getAuditedUnitId() == null ? "" : task.getAuditedUnitId();
                 String sendTarget = task.getCreateBy() == null ? "" : task.getCreateBy();
                 costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, sendTarget);
+
+                // 记录环节明细
+                recordNodeDetail(task, "supplement", title, null);
+
                 return title;
         }
     }
@@ -855,6 +904,10 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
             content += ",原因:" + req.getContent();
         }
         costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, sendTarget);
+
+        // 记录环节明细
+        recordNodeDetail(nTask, "backPre", title, req.getContent());
+
         return title;
     }
 
@@ -1001,6 +1054,10 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
             }
             costProjectApprovalManager.updateById(costProjectApproval);
         }
+
+        // 记录环节明细
+        recordNodeDetail(nTask, "toNext", title, req.getContent());
+
         return title;
     }
 
@@ -1336,6 +1393,10 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
             // 使用传入的用户ID作为发送目标
             costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, userId);
         }
+
+        // 记录环节明细
+        recordNodeDetail(task, "remind", title, req.getContent());
+
         return title;
     }
 
@@ -1369,6 +1430,10 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
             }
         }
         costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, sendTarget);
+
+        // 记录环节明细
+        recordNodeDetail(task, "remindUnit", title, req.getContent());
+
         return title;
     }
 
@@ -1480,6 +1545,9 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         }
         costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, sendTarget);
 
+        // 记录环节明细
+        recordNodeDetail(nTask, "review", title, req.getContent());
+
         return title;
     }
 
@@ -1532,7 +1600,17 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         String sendTarget = task.getCreateBy() == null ? "" : task.getCreateBy();
         costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, sendTarget);
 
+        // 记录环节明细
+        recordNodeDetail(task, "archive", title, req.getContent());
+
         return title;
     }
 
+    /**
+     * 记录环节操作明细(简化调用)
+     */
+    private void recordNodeDetail(CostProjectTask task, String actionType, String actionName, String remark) {
+        costProjectProccessNodeDetailManager.recordByTaskId(task.getId(), actionType, actionName, remark);
+    }
+
 }

+ 31 - 5
assistMg/src/main/java/com/hotent/project/manager/impl/CostProjectTaskMaterialManagerImpl.java

@@ -4,9 +4,10 @@ import java.util.Arrays;
 import java.util.List;
 
 import com.hotent.project.manager.CostProjectTaskManager;
+import com.hotent.project.manager.CostProjectProccessNodeDetailManager;
 import com.hotent.project.model.CostProjectTask;
+import com.hotent.base.util.StringUtil;
 import com.hotent.surveyinfo.manager.CostSurveyFdTemplateManager;
-import com.hotent.surveyinfo.manager.CostSurveyTemplateManager;
 import com.hotent.surveyinfo.model.CostSurveyFdTemplate;
 import com.hotent.sys.persistence.manager.DataDictManager;
 import com.hotent.sys.persistence.model.DataDict;
@@ -14,9 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.hotent.base.manager.impl.BaseManagerImpl;
-import com.hotent.base.query.QueryFilter;
 import com.hotent.base.util.BeanUtils;
 import com.hotent.project.dao.CostProjectTaskMaterialDao;
 import com.hotent.project.manager.CostProjectTaskMaterialManager;
@@ -43,6 +42,9 @@ public class CostProjectTaskMaterialManagerImpl extends BaseManagerImpl<CostProj
 	@Autowired
 	private CostProjectTaskManager costProjectTaskManager;
 
+	@Autowired
+	private CostProjectProccessNodeDetailManager costProjectProccessNodeDetailManager;
+
 	@Override
 	public CostProjectTaskMaterial getDetail(String id) {
     	CostProjectTaskMaterial costProjectTaskMaterial = this.get(id);
@@ -55,9 +57,21 @@ public class CostProjectTaskMaterialManagerImpl extends BaseManagerImpl<CostProj
 	@Override
 	@Transactional
 	public void createOrUpdate(CostProjectTaskMaterial costProjectTaskMaterial) {
+		// 判断是否是新上传文件(用于记录明细)
+		boolean isNewUpload = false;
+		String oldFileUrl = null;
+		if (StringUtil.isNotEmpty(costProjectTaskMaterial.getId())) {
+			CostProjectTaskMaterial oldMaterial = this.getById(costProjectTaskMaterial.getId());
+			if (oldMaterial != null) {
+				oldFileUrl = oldMaterial.getFileUrl();
+			}
+		}
+
 		// 根据taskId获取任务节点
-		if (costProjectTaskMaterial.getTaskId() != null) {
-			CostProjectTask task = costProjectTaskManager.getById(costProjectTaskMaterial.getTaskId());
+		String taskId = costProjectTaskMaterial.getTaskId();
+		CostProjectTask task = null;
+		if (taskId != null) {
+			task = costProjectTaskManager.getById(taskId);
 			if (task != null) {
 				String currentNode = task.getCurrentNode();
 				costProjectTaskMaterial.setTaskNode(currentNode);
@@ -76,12 +90,24 @@ public class CostProjectTaskMaterialManagerImpl extends BaseManagerImpl<CostProj
 					}else{
 						costProjectTaskMaterial.setFormatRequired("1");
 					}
+
+					// 判断是否是新上传(之前没有文件或文件URL变了)
+					if (StringUtil.isEmpty(oldFileUrl) || !fileUrl.equals(oldFileUrl)) {
+						isNewUpload = true;
+					}
 				}
 			}
 		}
 
 		//新建或更新
 		this.saveOrUpdate(costProjectTaskMaterial);
+
+		// 记录环节明细(只有新上传文件时才记录)
+		if (isNewUpload && taskId != null) {
+			String materialName = costProjectTaskMaterial.getInformationName();
+			String actionName = StringUtil.isNotEmpty(materialName) ? "上传材料:" + materialName : "上传材料";
+			costProjectProccessNodeDetailManager.recordByTaskId(taskId, "uploadMaterial", actionName, null);
+		}
 	}
 
 	/**

+ 149 - 0
assistMg/src/main/java/com/hotent/project/model/CostProjectProccessNodeDetail.java

@@ -0,0 +1,149 @@
+package com.hotent.project.model;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.hotent.base.entity.BaseModel;
+
+import java.time.LocalDateTime;
+import java.io.Serializable;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+/**
+ * 流程环节明细表
+ * 记录每个流程节点的详细操作记录
+ *
+ * @company 山西清众科技股份有限公司
+ * @author 超级管理员
+ * @since 2025-12-23
+ */
+@ApiModel(value = "CostProjectProccessNodeDetail对象", description = "流程环节明细表")
+@TableName(value = "cost_project_proccess_node_detail")
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+public class CostProjectProccessNodeDetail extends BaseModel<CostProjectProccessNodeDetail> {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @JsonProperty("id")
+    private String id;
+
+    @ApiModelProperty(value = "流程ID")
+    @TableField("process_id")
+    @JsonProperty("processId")
+    private String processId;
+
+    @ApiModelProperty(value = "流程节点ID")
+    @TableField("process_node_id")
+    @JsonProperty("processNodeId")
+    private String processNodeId;
+
+    @ApiModelProperty(value = "流程节点Key(冗余字段,便于查询)")
+    @TableField("process_node_key")
+    @JsonProperty("processNodeKey")
+    private String processNodeKey;
+
+    @ApiModelProperty(value = "任务ID(关联主任务或子任务)")
+    @TableField("task_id")
+    @JsonProperty("taskId")
+    private String taskId;
+
+    @ApiModelProperty(value = "执行来源:1-外部(企业),2-内部(审核人员)")
+    @TableField("source_type")
+    @JsonProperty("sourceType")
+    private String sourceType;
+
+    @ApiModelProperty(value = "执行人ID")
+    @TableField("executor_id")
+    @JsonProperty("executorId")
+    private String executorId;
+
+    @ApiModelProperty(value = "执行人姓名")
+    @TableField("executor_name")
+    @JsonProperty("executorName")
+    private String executorName;
+
+    @ApiModelProperty(value = "执行人部门/单位")
+    @TableField("executor_dept")
+    @JsonProperty("executorDept")
+    private String executorDept;
+
+    @ApiModelProperty(value = "执行事项类型:submit-提交材料,pass-通过审核,reject-退回,supplement-补充材料,suspend-中止,resume-恢复,remind-催办,archive-归档,review-复核,feedback-意见反馈")
+    @TableField("action_type")
+    @JsonProperty("actionType")
+    private String actionType;
+
+    @ApiModelProperty(value = "执行事项显示名称")
+    @TableField("action_name")
+    @JsonProperty("actionName")
+    private String actionName;
+
+    @ApiModelProperty(value = "执行时间")
+    @TableField("execute_time")
+    @JsonProperty("executeTime")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm",timezone="GMT+8")
+    private LocalDateTime executeTime;
+
+    @ApiModelProperty(value = "备注/原因说明")
+    @TableField("remark")
+    @JsonProperty("remark")
+    private String remark;
+
+    @ApiModelProperty(value = "被审核单位ID(针对多企业情况)")
+    @TableField("audited_unit_id")
+    @JsonProperty("auditedUnitId")
+    private String auditedUnitId;
+
+    @ApiModelProperty(value = "被审核单位名称")
+    @TableField("audited_unit_name")
+    @JsonProperty("auditedUnitName")
+    private String auditedUnitName;
+
+    @ApiModelProperty(value = "逻辑删除:0-正常,1-删除")
+    @TableField("is_deleted")
+    @JsonProperty("isDeleted")
+    private String isDeleted;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    @JsonProperty("createTime")
+    private LocalDateTime createTime;
+
+    @ApiModelProperty(value = "创建人")
+    @TableField(value = "create_by", fill = FieldFill.INSERT)
+    @JsonProperty("createBy")
+    private String createBy;
+
+    @ApiModelProperty(value = "更新时间")
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonProperty("updateTime")
+    private LocalDateTime updateTime;
+
+    @ApiModelProperty(value = "更新人")
+    @TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
+    @JsonProperty("updateBy")
+    private String updateBy;
+
+    // ========== 非数据库字段 ==========
+
+    @ApiModelProperty(value = "执行来源名称")
+    @TableField(exist = false)
+    private String sourceTypeName;
+
+    @Override
+    protected Serializable pkVal() {
+        return this.id;
+    }
+}

+ 5 - 1
assistMg/src/main/java/com/hotent/project/resp/CostProjectProccessNodeResp.java

@@ -2,7 +2,7 @@ package com.hotent.project.resp;
 
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.hotent.project.model.CostProjectProccessPersonnel;
+import com.hotent.project.model.CostProjectProccessNodeDetail;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Getter;
@@ -87,4 +87,8 @@ public class CostProjectProccessNodeResp {
     private String actRemarks;
     private LocalDateTime actEndTime;
     private LocalDateTime actStartTime;
+
+    @ApiModelProperty(value = "环节明细列表")
+    @JsonProperty("detailList")
+    private List<CostProjectProccessNodeDetail> detailList;
 }

+ 580 - 47
assistMg/src/main/java/com/hotent/project/service/ArchiveTest.java

@@ -1,10 +1,8 @@
 package com.hotent.project.service;
 
 import org.apache.pdfbox.pdmodel.PDDocument;
-import org.apache.poi.xwpf.usermodel.XWPFDocument;
-import org.apache.poi.xwpf.usermodel.XWPFParagraph;
-import org.apache.poi.xwpf.usermodel.XWPFRun;
-import org.apache.poi.xwpf.usermodel.BreakType;
+import org.apache.poi.xwpf.model.XWPFHeaderFooterPolicy;
+import org.apache.poi.xwpf.usermodel.*;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -14,10 +12,13 @@ import java.util.List;
 
 /**
  * 卷宗生成测试类(不依赖Spring)
+ * 测试封面、目录、正文(带页码)、封底的生成效果
  */
 public class ArchiveTest {
 
     public static void main(String[] args) throws Exception {
+        System.out.println("========== 卷宗生成测试(含页码) ==========\n");
+
         System.out.println("========== 文件名称提取 ==========");
         List<String> fileUrls = extractFileNames();
 
@@ -34,11 +35,10 @@ public class ArchiveTest {
     private static List<String> extractFileNames() {
         String[] fileUrls = {
                 "D:\\fx\\cc\\7778、成本监审集体审议记录20251216101702575.docx",
-                "D:\\fx\\cc\\成本审核模板_2025_12_16_14_34_56.xlsx",
+                "D:\\fx\\cc\\被跨固定表_17665464557132025122411205576.xlsx",
                 "D:\\fx\\cc\\文书文件20251216141325913.pdf"
         };
 
-
         List<String> validFiles = new ArrayList<>();
         for (String url : fileUrls) {
             String fileName = extractFileName(url);
@@ -60,7 +60,7 @@ public class ArchiveTest {
         if (url == null || url.isEmpty()) {
             return "";
         }
-        int lastSlash = url.lastIndexOf('/');
+        int lastSlash = Math.max(url.lastIndexOf('/'), url.lastIndexOf('\\'));
         return lastSlash >= 0 ? url.substring(lastSlash + 1) : url;
     }
 
@@ -136,19 +136,31 @@ public class ArchiveTest {
 
     /**
      * 生成卷宗
+     * 结构:封面 -> 目录 -> 正文(带页码) -> 封底
      */
     private static void generateArchive(List<String> fileUrls) throws Exception {
         XWPFDocument document = new XWPFDocument();
-        boolean isFirst = true;
 
+        // 1. 生成封面
+        System.out.println("1. 生成封面...");
+        createCoverPage(document);
+
+        // 2. 添加分页符,生成目录
+        System.out.println("2. 生成目录...");
+        addPageBreak(document);
+        createCatalogPage(document);
+
+        // 3. 添加分页符并设置页码,合并正文文件
+        System.out.println("3. 合并正文文件(带页码)...");
+        addPageBreakWithPageNumber(document);
+
+        boolean isFirst = true;
         for (String filePath : fileUrls) {
             String fileName = extractFileName(filePath);
             String lowerCase = filePath.toLowerCase();
 
             if (!isFirst) {
-                XWPFParagraph pageBreak = document.createParagraph();
-                XWPFRun run = pageBreak.createRun();
-                run.addBreak(BreakType.PAGE);
+                addPageBreak(document);
             }
 
             if (lowerCase.endsWith(".docx") || lowerCase.endsWith(".doc")) {
@@ -160,17 +172,274 @@ public class ArchiveTest {
             }
 
             isFirst = false;
-            System.out.println("✓ 已添加: " + fileName);
+            System.out.println("   ✓ 已添加: " + fileName);
         }
 
+        // 4. 添加分页符,生成封底
+        System.out.println("4. 生成封底...");
+        addPageBreak(document);
+        createBackCoverPage(document);
+
+        // 保存文档
         String outputPath = "D:\\fx\\cc\\卷宗_" + System.currentTimeMillis() + ".docx";
         FileOutputStream fos = new FileOutputStream(outputPath);
         document.write(fos);
         fos.close();
         document.close();
 
-        System.out.println("\n✓ 卷宗生成成功!");
+        System.out.println("\n========== 生成完成 ==========");
         System.out.println("输出路径: " + outputPath);
+        System.out.println("\n页码说明:");
+        System.out.println("- 页码格式:第 X 页 / 共 Y 页");
+        System.out.println("- 页码位置:页脚居中");
+        System.out.println("- 注意:由于POI限制,页码会显示在所有页面(包括封面、目录、封底)");
+    }
+
+    /**
+     * 创建封面页
+     */
+    private static void createCoverPage(XWPFDocument document) {
+        // 添加空行
+        for (int i = 0; i < 10; i++) {
+            document.createParagraph();
+        }
+
+        // 标题
+        XWPFParagraph titlePara = document.createParagraph();
+        titlePara.setAlignment(ParagraphAlignment.CENTER);
+        XWPFRun titleRun = titlePara.createRun();
+        titleRun.setText("成本监审项目卷宗");
+        titleRun.setBold(true);
+        titleRun.setFontSize(36);
+        titleRun.setFontFamily("宋体");
+
+        // 空行
+        for (int i = 0; i < 5; i++) {
+            document.createParagraph();
+        }
+
+        // 项目信息
+        String[] infos = {
+                "项目名称:XXXX成本监审项目",
+                "委托单位:XXXX发展和改革委员会",
+                "承办单位:XXXX会计师事务所",
+                "编制日期:2024年12月"
+        };
+
+        for (String info : infos) {
+            XWPFParagraph para = document.createParagraph();
+            para.setAlignment(ParagraphAlignment.CENTER);
+            XWPFRun run = para.createRun();
+            run.setText(info);
+            run.setFontSize(16);
+            run.setFontFamily("宋体");
+        }
+
+        System.out.println("   ✓ 封面生成完成");
+    }
+
+    /**
+     * 创建目录页
+     */
+    private static void createCatalogPage(XWPFDocument document) {
+        // 标题
+        XWPFParagraph titlePara = document.createParagraph();
+        titlePara.setAlignment(ParagraphAlignment.CENTER);
+        XWPFRun titleRun = titlePara.createRun();
+        titleRun.setText("目    录");
+        titleRun.setBold(true);
+        titleRun.setFontSize(24);
+        titleRun.setFontFamily("宋体");
+
+        document.createParagraph();
+
+        // 目录内容
+        String[] items = {
+                "一、成本监审报告书",
+                "二、成本监审工作底稿",
+                "三、成本监审调查表",
+                "四、成本监审证据资料",
+                "五、成本监审审核意见",
+                "六、成本监审会议纪要",
+                "七、其他相关资料"
+        };
+
+        for (int i = 0; i < items.length; i++) {
+            XWPFParagraph para = document.createParagraph();
+            XWPFRun run = para.createRun();
+            run.setText(items[i] + " ........................ " + (i + 1));
+            run.setFontSize(14);
+            run.setFontFamily("宋体");
+        }
+
+        System.out.println("   ✓ 目录生成完成");
+    }
+
+    /**
+     * 创建封底页
+     */
+    private static void createBackCoverPage(XWPFDocument document) {
+        // 添加空行
+        for (int i = 0; i < 15; i++) {
+            document.createParagraph();
+        }
+
+        // 封底内容
+        XWPFParagraph para1 = document.createParagraph();
+        para1.setAlignment(ParagraphAlignment.CENTER);
+        XWPFRun run1 = para1.createRun();
+        run1.setText("(封底)");
+        run1.setFontSize(24);
+        run1.setFontFamily("宋体");
+
+        document.createParagraph();
+
+        XWPFParagraph para2 = document.createParagraph();
+        para2.setAlignment(ParagraphAlignment.CENTER);
+        XWPFRun run2 = para2.createRun();
+        run2.setText("XXXX会计师事务所");
+        run2.setFontSize(16);
+        run2.setFontFamily("宋体");
+
+        XWPFParagraph para3 = document.createParagraph();
+        para3.setAlignment(ParagraphAlignment.CENTER);
+        XWPFRun run3 = para3.createRun();
+        run3.setText("地址:XXXX市XXXX区XXXX路XXX号");
+        run3.setFontSize(12);
+        run3.setFontFamily("宋体");
+
+        XWPFParagraph para4 = document.createParagraph();
+        para4.setAlignment(ParagraphAlignment.CENTER);
+        XWPFRun run4 = para4.createRun();
+        run4.setText("电话:0XXX-XXXXXXXX");
+        run4.setFontSize(12);
+        run4.setFontFamily("宋体");
+
+        System.out.println("   ✓ 封底生成完成");
+    }
+
+    /**
+     * 添加分页符
+     */
+    private static void addPageBreak(XWPFDocument document) {
+        XWPFParagraph paragraph = document.createParagraph();
+        XWPFRun run = paragraph.createRun();
+        run.addBreak(BreakType.PAGE);
+    }
+
+    /**
+     * 添加分页符并设置页码
+     * 页码格式:第 X 页 / 共 Y 页(居中显示)
+     */
+    private static void addPageBreakWithPageNumber(XWPFDocument document) {
+        try {
+            // 添加分页符
+            XWPFParagraph paragraph = document.createParagraph();
+            XWPFRun run = paragraph.createRun();
+            run.addBreak(BreakType.PAGE);
+
+            // 创建页脚并添加页码
+            XWPFHeaderFooterPolicy policy = document.getHeaderFooterPolicy();
+            if (policy == null) {
+                policy = document.createHeaderFooterPolicy();
+            }
+            XWPFFooter footer = policy.createFooter(XWPFHeaderFooterPolicy.DEFAULT);
+
+            // 创建页脚段落
+            XWPFParagraph footerPara = footer.createParagraph();
+            footerPara.setAlignment(ParagraphAlignment.CENTER);
+
+            // 使用复杂字段方式添加页码
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP ctp = footerPara.getCTP();
+
+            // 添加 "第 " 文本
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR r1 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr rPr1 = r1.addNewRPr();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFonts fonts1 = rPr1.addNewRFonts();
+            fonts1.setAscii("宋体");
+            fonts1.setEastAsia("宋体");
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHpsMeasure sz1 = rPr1.addNewSz();
+            sz1.setVal(java.math.BigInteger.valueOf(20)); // 10号字 = 20半磅
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText t1 = r1.addNewT();
+            t1.setStringValue("第 ");
+
+            // 添加 PAGE 字段开始
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rBegin = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar fldCharBegin = rBegin.addNewFldChar();
+            fldCharBegin.setFldCharType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType.BEGIN);
+
+            // 添加 PAGE 字段指令
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rInstr = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText instrText = rInstr.addNewInstrText();
+            instrText.setStringValue(" PAGE ");
+
+            // 添加字段分隔符
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rSep = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar fldCharSep = rSep.addNewFldChar();
+            fldCharSep.setFldCharType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType.SEPARATE);
+
+            // 添加字段默认值
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rDefault = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText tDefault = rDefault.addNewT();
+            tDefault.setStringValue("1");
+
+            // 添加字段结束
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rEnd = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar fldCharEnd = rEnd.addNewFldChar();
+            fldCharEnd.setFldCharType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType.END);
+
+            // 添加 " 页 / 共 " 文本
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR r2 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr rPr2 = r2.addNewRPr();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFonts fonts2 = rPr2.addNewRFonts();
+            fonts2.setAscii("宋体");
+            fonts2.setEastAsia("宋体");
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHpsMeasure sz2 = rPr2.addNewSz();
+            sz2.setVal(java.math.BigInteger.valueOf(20));
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText t2 = r2.addNewT();
+            t2.setStringValue(" 页 / 共 ");
+
+            // 添加 NUMPAGES 字段开始
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rBegin2 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar fldCharBegin2 = rBegin2.addNewFldChar();
+            fldCharBegin2.setFldCharType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType.BEGIN);
+
+            // 添加 NUMPAGES 字段指令
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rInstr2 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText instrText2 = rInstr2.addNewInstrText();
+            instrText2.setStringValue(" NUMPAGES ");
+
+            // 添加字段分隔符
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rSep2 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar fldCharSep2 = rSep2.addNewFldChar();
+            fldCharSep2.setFldCharType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType.SEPARATE);
+
+            // 添加字段默认值
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rDefault2 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText tDefault2 = rDefault2.addNewT();
+            tDefault2.setStringValue("1");
+
+            // 添加字段结束
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rEnd2 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar fldCharEnd2 = rEnd2.addNewFldChar();
+            fldCharEnd2.setFldCharType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType.END);
+
+            // 添加 " 页" 文本
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR r3 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr rPr3 = r3.addNewRPr();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFonts fonts3 = rPr3.addNewRFonts();
+            fonts3.setAscii("宋体");
+            fonts3.setEastAsia("宋体");
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHpsMeasure sz3 = rPr3.addNewSz();
+            sz3.setVal(java.math.BigInteger.valueOf(20));
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText t3 = r3.addNewT();
+            t3.setStringValue(" 页");
+
+            System.out.println("   ✓ 页码设置完成");
+        } catch (Exception e) {
+            System.out.println("   ✗ 页码设置失败: " + e.getMessage());
+            e.printStackTrace();
+        }
     }
 
     /**
@@ -198,7 +467,8 @@ public class ArchiveTest {
     }
 
     /**
-     * 合并PDF文档
+     * 合并PDF文档(优化版)
+     * 根据PDF实际尺寸自适应,保持宽高比
      */
     private static void mergePdfDocument(XWPFDocument document, String filePath) throws Exception {
         try {
@@ -207,24 +477,53 @@ public class ArchiveTest {
             int pageCount = pdfDoc.getNumberOfPages();
             System.out.println("    PDF页数: " + pageCount);
 
+            // Word页面可用宽度(A4纸,左右边距各2.5cm,约15cm可用)
+            double wordPageWidth = 450; // 点(约15.9cm)
+            double wordPageHeight = 650; // 点(约22.9cm)
+
             for (int i = 0; i < pageCount; i++) {
-                java.awt.image.BufferedImage image = renderer.renderImageWithDPI(i, 200);
                 System.out.println("    正在转换第 " + (i + 1) + " 页...");
 
+                // 获取PDF页面尺寸
+                org.apache.pdfbox.pdmodel.PDPage pdfPage = pdfDoc.getPage(i);
+                org.apache.pdfbox.pdmodel.common.PDRectangle mediaBox = pdfPage.getMediaBox();
+                float pdfWidth = mediaBox.getWidth();
+                float pdfHeight = mediaBox.getHeight();
+
+                // 计算缩放比例,使图片适应Word页面
+                double scaleX = wordPageWidth / pdfWidth;
+                double scaleY = wordPageHeight / pdfHeight;
+                double scale = Math.min(scaleX, scaleY); // 取较小值,保证不超出页面
+
+                // 计算最终图片尺寸(保持宽高比)
+                int imgWidthPt = (int) (pdfWidth * scale);
+                int imgHeightPt = (int) (pdfHeight * scale);
+
+                // 使用较高DPI渲染以获得清晰图片
+                int dpi = 150; // 平衡清晰度和文件大小
+                java.awt.image.BufferedImage image = renderer.renderImageWithDPI(i, dpi);
+
+                // 转为PNG
                 java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
                 javax.imageio.ImageIO.write(image, "png", baos);
                 byte[] imageBytes = baos.toByteArray();
                 baos.close();
 
+                // 创建段落并居中
                 XWPFParagraph para = document.createParagraph();
+                para.setAlignment(ParagraphAlignment.CENTER);
+                para.setSpacingBefore(0);
+                para.setSpacingAfter(0);
+
                 XWPFRun run = para.createRun();
                 java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(imageBytes);
-                run.addPicture(bais, org.apache.poi.xwpf.usermodel.Document.PICTURE_TYPE_PNG,
-                        "pdf_page_" + i + ".png", org.apache.poi.util.Units.toEMU(410), org.apache.poi.util.Units.toEMU(600));
+                run.addPicture(bais, Document.PICTURE_TYPE_PNG,
+                        "pdf_page_" + i + ".png",
+                        org.apache.poi.util.Units.toEMU(imgWidthPt),
+                        org.apache.poi.util.Units.toEMU(imgHeightPt));
                 bais.close();
 
-                para.createRun().addBreak();
-
+                // 每页PDF后添加分页符(最后一页除外)
                 if (i < pageCount - 1) {
                     XWPFParagraph pageBreak = document.createParagraph();
                     XWPFRun pageBreakRun = pageBreak.createRun();
@@ -244,7 +543,7 @@ public class ArchiveTest {
     }
 
     /**
-     * 合并Excel文档
+     * 合并Excel文档(转图片方式,保持原有格式)
      */
     private static void mergeExcelDocument(XWPFDocument document, String filePath) throws Exception {
         try {
@@ -255,33 +554,27 @@ public class ArchiveTest {
 
                 if (sheet.getPhysicalNumberOfRows() == 0) continue;
 
-                int rows = sheet.getPhysicalNumberOfRows();
-                int cols = 0;
-                for (int i = 0; i < rows; i++) {
-                    org.apache.poi.ss.usermodel.Row row = sheet.getRow(i);
-                    if (row != null && row.getPhysicalNumberOfCells() > cols) {
-                        cols = row.getPhysicalNumberOfCells();
-                    }
-                }
-
-                if (cols == 0) cols = 1;
-
-                org.apache.poi.xwpf.usermodel.XWPFTable table = document.createTable(rows, cols);
-                table.setStyleID("TableGrid");
-
-                for (int i = 0; i < rows; i++) {
-                    org.apache.poi.ss.usermodel.Row excelRow = sheet.getRow(i);
-                    org.apache.poi.xwpf.usermodel.XWPFTableRow wordRow = table.getRow(i);
-
-                    if (excelRow != null) {
-                        for (int j = 0; j < cols; j++) {
-                            org.apache.poi.ss.usermodel.Cell cell = excelRow.getCell(j);
-                            String cellValue = cell != null ? cell.toString() : "";
-
-                            org.apache.poi.xwpf.usermodel.XWPFTableCell wordCell = wordRow.getCell(j);
-                            wordCell.setText(cellValue);
-                        }
-                    }
+                System.out.println("    正在转换Sheet: " + sheet.getSheetName());
+
+                // 将Excel Sheet转为图片
+                byte[] imageBytes = excelSheetToImage(workbook, sheetIndex);
+
+                if (imageBytes != null && imageBytes.length > 0) {
+                    XWPFParagraph para = document.createParagraph();
+                    para.setAlignment(ParagraphAlignment.CENTER);
+                    XWPFRun run = para.createRun();
+
+                    java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(imageBytes);
+                    // 设置图片宽度为页面宽度(约16cm = 453pt),高度自适应
+                    run.addPicture(bais, Document.PICTURE_TYPE_PNG,
+                            "excel_sheet_" + sheetIndex + ".png",
+                            org.apache.poi.util.Units.toEMU(450),
+                            org.apache.poi.util.Units.toEMU(600));
+                    bais.close();
+                } else {
+                    // 图片转换失败,使用表格方式
+                    System.out.println("    图片转换失败,使用表格方式");
+                    mergeExcelAsTable(document, sheet);
                 }
 
                 if (sheetIndex < workbook.getNumberOfSheets() - 1) {
@@ -292,11 +585,251 @@ public class ArchiveTest {
             }
 
             workbook.close();
+            System.out.println("    Excel转换完成");
         } catch (Exception e) {
             System.out.println("  [警告] Excel合并失败: " + e.getMessage());
+            e.printStackTrace();
             XWPFParagraph para = document.createParagraph();
             XWPFRun run = para.createRun();
             run.setText("[Excel文件: " + new File(filePath).getName() + " - 无法嵌入]");
         }
     }
+
+    /**
+     * 将Excel Sheet转为PNG图片(优化版)
+     */
+    private static byte[] excelSheetToImage(org.apache.poi.ss.usermodel.Workbook workbook, int sheetIndex) {
+        try {
+            org.apache.poi.ss.usermodel.Sheet sheet = workbook.getSheetAt(sheetIndex);
+
+            // 计算表格区域
+            int lastRowNum = sheet.getLastRowNum();
+            int maxColNum = 0;
+            for (int i = 0; i <= lastRowNum; i++) {
+                org.apache.poi.ss.usermodel.Row row = sheet.getRow(i);
+                if (row != null && row.getLastCellNum() > maxColNum) {
+                    maxColNum = row.getLastCellNum();
+                }
+            }
+
+            if (lastRowNum < 0 || maxColNum <= 0) {
+                return null;
+            }
+
+            // 计算每列的最佳宽度(根据内容)
+            int[] colWidths = new int[maxColNum];
+            java.awt.Font measureFont = new java.awt.Font("宋体", java.awt.Font.PLAIN, 14);
+            java.awt.image.BufferedImage tempImg = new java.awt.image.BufferedImage(1, 1, java.awt.image.BufferedImage.TYPE_INT_RGB);
+            java.awt.Graphics2D tempG = tempImg.createGraphics();
+            tempG.setFont(measureFont);
+            java.awt.FontMetrics fm = tempG.getFontMetrics();
+
+            for (int colIdx = 0; colIdx < maxColNum; colIdx++) {
+                int maxWidth = 60; // 最小宽度
+                for (int rowIdx = 0; rowIdx <= lastRowNum; rowIdx++) {
+                    org.apache.poi.ss.usermodel.Row row = sheet.getRow(rowIdx);
+                    if (row != null) {
+                        org.apache.poi.ss.usermodel.Cell cell = row.getCell(colIdx);
+                        if (cell != null) {
+                            String value = getCellValueAsString(cell);
+                            int width = fm.stringWidth(value) + 30; // 加边距
+                            maxWidth = Math.max(maxWidth, Math.min(width, 200)); // 限制最大宽度
+                        }
+                    }
+                }
+                colWidths[colIdx] = maxWidth;
+            }
+            tempG.dispose();
+
+            // 计算图片尺寸
+            int cellHeight = 35;  // 行高
+            int titleHeight = 45; // 标题行高
+            int padding = 40;     // 边距
+
+            int totalWidth = padding * 2;
+            for (int w : colWidths) {
+                totalWidth += w;
+            }
+
+            int imgHeight = padding + titleHeight + (lastRowNum + 1) * cellHeight + padding;
+
+            // 限制最大尺寸
+            totalWidth = Math.min(totalWidth, 1800);
+            imgHeight = Math.min(imgHeight, 2500);
+
+            // 创建图片
+            java.awt.image.BufferedImage image = new java.awt.image.BufferedImage(
+                    totalWidth, imgHeight, java.awt.image.BufferedImage.TYPE_INT_RGB);
+            java.awt.Graphics2D g2d = image.createGraphics();
+
+            // 设置抗锯齿
+            g2d.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING,
+                    java.awt.RenderingHints.VALUE_ANTIALIAS_ON);
+            g2d.setRenderingHint(java.awt.RenderingHints.KEY_TEXT_ANTIALIASING,
+                    java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+            // 白色背景
+            g2d.setColor(java.awt.Color.WHITE);
+            g2d.fillRect(0, 0, totalWidth, imgHeight);
+
+            // 字体
+            java.awt.Font titleFont = new java.awt.Font("宋体", java.awt.Font.BOLD, 16);
+            java.awt.Font headerFont = new java.awt.Font("宋体", java.awt.Font.BOLD, 14);
+            java.awt.Font contentFont = new java.awt.Font("宋体", java.awt.Font.PLAIN, 13);
+
+            int startX = padding;
+            int startY = padding;
+
+            // 绘制表格
+            for (int rowIdx = 0; rowIdx <= lastRowNum; rowIdx++) {
+                org.apache.poi.ss.usermodel.Row row = sheet.getRow(rowIdx);
+                int currentHeight = (rowIdx == 0) ? titleHeight : cellHeight;
+                int y = startY + (rowIdx == 0 ? 0 : titleHeight + (rowIdx - 1) * cellHeight);
+
+                int x = startX;
+                for (int colIdx = 0; colIdx < maxColNum; colIdx++) {
+                    int cellWidth = colWidths[colIdx];
+
+                    // 获取单元格值
+                    String cellValue = "";
+                    if (row != null) {
+                        org.apache.poi.ss.usermodel.Cell cell = row.getCell(colIdx);
+                        if (cell != null) {
+                            cellValue = getCellValueAsString(cell);
+                        }
+                    }
+
+                    // 绘制单元格背景
+                    if (rowIdx == 0) {
+                        // 标题行 - 深色背景
+                        g2d.setColor(new java.awt.Color(64, 64, 64));
+                        g2d.fillRect(x, y, cellWidth, currentHeight);
+                        g2d.setFont(titleFont);
+                        g2d.setColor(java.awt.Color.WHITE);
+                    } else if (rowIdx == 1) {
+                        // 表头行 - 浅灰背景
+                        g2d.setColor(new java.awt.Color(220, 220, 220));
+                        g2d.fillRect(x, y, cellWidth, currentHeight);
+                        g2d.setFont(headerFont);
+                        g2d.setColor(java.awt.Color.BLACK);
+                    } else {
+                        // 数据行 - 交替颜色
+                        if (rowIdx % 2 == 0) {
+                            g2d.setColor(new java.awt.Color(245, 245, 245));
+                        } else {
+                            g2d.setColor(java.awt.Color.WHITE);
+                        }
+                        g2d.fillRect(x, y, cellWidth, currentHeight);
+                        g2d.setFont(contentFont);
+                        g2d.setColor(java.awt.Color.BLACK);
+                    }
+
+                    // 绘制边框
+                    g2d.setColor(new java.awt.Color(180, 180, 180));
+                    g2d.drawRect(x, y, cellWidth, currentHeight);
+
+                    // 绘制文字(居中)
+                    if (rowIdx == 0) {
+                        g2d.setColor(java.awt.Color.WHITE);
+                    } else {
+                        g2d.setColor(java.awt.Color.BLACK);
+                    }
+                    java.awt.FontMetrics metrics = g2d.getFontMetrics();
+                    int textX = x + (cellWidth - metrics.stringWidth(cellValue)) / 2;
+                    int textY = y + (currentHeight + metrics.getAscent() - metrics.getDescent()) / 2;
+                    g2d.drawString(cellValue, Math.max(textX, x + 5), textY);
+
+                    x += cellWidth;
+                }
+            }
+
+            // 绘制外边框(加粗)
+            g2d.setColor(new java.awt.Color(100, 100, 100));
+            g2d.setStroke(new java.awt.BasicStroke(2));
+            int tableWidth = 0;
+            for (int w : colWidths) tableWidth += w;
+            int tableHeight = titleHeight + lastRowNum * cellHeight;
+            g2d.drawRect(startX, startY, tableWidth, tableHeight);
+
+            g2d.dispose();
+
+            // 转为PNG字节数组
+            java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+            javax.imageio.ImageIO.write(image, "png", baos);
+            return baos.toByteArray();
+
+        } catch (Exception e) {
+            System.out.println("    Excel转图片失败: " + e.getMessage());
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 获取单元格值(字符串形式)
+     */
+    private static String getCellValueAsString(org.apache.poi.ss.usermodel.Cell cell) {
+        if (cell == null) return "";
+
+        switch (cell.getCellType()) {
+            case STRING:
+                return cell.getStringCellValue();
+            case NUMERIC:
+                if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) {
+                    return new java.text.SimpleDateFormat("yyyy-MM-dd").format(cell.getDateCellValue());
+                }
+                double numValue = cell.getNumericCellValue();
+                if (numValue == Math.floor(numValue)) {
+                    return String.valueOf((long) numValue);
+                }
+                return String.valueOf(numValue);
+            case BOOLEAN:
+                return String.valueOf(cell.getBooleanCellValue());
+            case FORMULA:
+                try {
+                    return String.valueOf(cell.getNumericCellValue());
+                } catch (Exception e) {
+                    try {
+                        return cell.getStringCellValue();
+                    } catch (Exception e2) {
+                        return "";
+                    }
+                }
+            default:
+                return "";
+        }
+    }
+
+    /**
+     * 以表格方式合并Excel(备用方案)
+     */
+    private static void mergeExcelAsTable(XWPFDocument document, org.apache.poi.ss.usermodel.Sheet sheet) {
+        int rows = sheet.getPhysicalNumberOfRows();
+        int cols = 0;
+        for (int i = 0; i < rows; i++) {
+            org.apache.poi.ss.usermodel.Row row = sheet.getRow(i);
+            if (row != null && row.getPhysicalNumberOfCells() > cols) {
+                cols = row.getPhysicalNumberOfCells();
+            }
+        }
+
+        if (cols == 0) cols = 1;
+
+        XWPFTable table = document.createTable(rows, cols);
+
+        for (int i = 0; i < rows; i++) {
+            org.apache.poi.ss.usermodel.Row excelRow = sheet.getRow(i);
+            XWPFTableRow wordRow = table.getRow(i);
+
+            if (excelRow != null) {
+                for (int j = 0; j < cols; j++) {
+                    org.apache.poi.ss.usermodel.Cell cell = excelRow.getCell(j);
+                    String cellValue = cell != null ? getCellValueAsString(cell) : "";
+
+                    XWPFTableCell wordCell = wordRow.getCell(j);
+                    wordCell.setText(cellValue);
+                }
+            }
+        }
+    }
 }

+ 522 - 37
assistMg/src/main/java/com/hotent/project/service/AsyncMaterialSummaryService.java

@@ -17,6 +17,7 @@ import com.hotent.util.FileUploadUtil;
 import com.hotent.util.wordexcelutils.CompleteTemplateProcessor;
 import com.hotent.util.wordexcelutils.SmartTemplateWriter;
 import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.poi.xwpf.model.XWPFHeaderFooterPolicy;
 import org.apache.poi.xwpf.usermodel.*;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -825,6 +826,7 @@ public class AsyncMaterialSummaryService {
     /**
      * 档案校对通过后,异步生成Word卷宗
      * 顺序:封面(-1) -> 目录(-2) -> 14个资料(1-13) -> 封底(-3)
+     * 页码只在正文部分(14个资料)显示,封面、目录、封底不显示页码
      *
      * @param taskId 任务ID
      */
@@ -852,9 +854,29 @@ public class AsyncMaterialSummaryService {
                 return Integer.compare(sortA, sortB);
             });
 
-            // 遍历所有主表,从明细表获取附件URL进行合并
+            // 分类:封面、目录、正文、封底
+            List<CostProjectTaskMaterialSummary> coverList = new java.util.ArrayList<>();    // 封面 -1
+            List<CostProjectTaskMaterialSummary> catalogList = new java.util.ArrayList<>();  // 目录 -2
+            List<CostProjectTaskMaterialSummary> contentList = new java.util.ArrayList<>();  // 正文 1-14
+            List<CostProjectTaskMaterialSummary> backCoverList = new java.util.ArrayList<>();// 封底 -3
+
             for (CostProjectTaskMaterialSummary summary : allSummaryList) {
-                logger.info("合并资料:{}", summary.getMaterialName());
+                Integer orderNum = summary.getMaterialOrderNum();
+                if (orderNum == null) continue;
+                if (orderNum == -1) {
+                    coverList.add(summary);
+                } else if (orderNum == -2) {
+                    catalogList.add(summary);
+                } else if (orderNum == -3) {
+                    backCoverList.add(summary);
+                } else if (orderNum > 0) {
+                    contentList.add(summary);
+                }
+            }
+
+            // 1. 合并封面(不显示页码)
+            for (CostProjectTaskMaterialSummary summary : coverList) {
+                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));
@@ -866,6 +888,58 @@ public class AsyncMaterialSummaryService {
                 }
             }
 
+            // 2. 添加分节符后合并目录(不显示页码)
+            if (!catalogList.isEmpty()) {
+                addSectionBreak(document);
+                for (CostProjectTaskMaterialSummary summary : catalogList) {
+                    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);
+                            }
+                        }
+                    }
+                }
+            }
+
+            // 3. 添加分节符后合并正文(显示页码,从1开始)
+            if (!contentList.isEmpty()) {
+                addSectionBreakWithPageNumber(document);
+                contentList.sort(Comparator.comparing(s -> s.getMaterialOrderNum() != null ? s.getMaterialOrderNum() : 0));
+                for (CostProjectTaskMaterialSummary summary : contentList) {
+                    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);
+                            }
+                        }
+                    }
+                }
+            }
+
+            // 4. 添加分节符后合并封底(不显示页码)
+            if (!backCoverList.isEmpty()) {
+                addSectionBreakWithoutPageNumber(document);
+                for (CostProjectTaskMaterialSummary summary : backCoverList) {
+                    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);
+                            }
+                        }
+                    }
+                }
+            }
+
             // 生成输出文件路径
             String fileName = FileUploadUtil.generateFileName("卷宗_" + taskId + ".docx");
             String outputPath = FileUploadUtil.generateSavePath(
@@ -1031,7 +1105,7 @@ public class AsyncMaterialSummaryService {
     }
 
     /**
-     * 合并PDF文档(转换为图片后插入
+     * 合并PDF文档(优化版:根据PDF实际尺寸自适应,保持宽高比
      */
     private void mergePdfDocument(XWPFDocument document, String filePath) {
         PDDocument pdfDoc = null;
@@ -1040,23 +1114,51 @@ public class AsyncMaterialSummaryService {
             org.apache.pdfbox.rendering.PDFRenderer renderer = new org.apache.pdfbox.rendering.PDFRenderer(pdfDoc);
             int pageCount = pdfDoc.getNumberOfPages();
 
-            for (int i = 0; i < pageCount; i++) {
-                java.awt.image.BufferedImage image = renderer.renderImageWithDPI(i, 200);
+            // Word页面可用宽度(A4纸,左右边距各2.5cm,约15cm可用)
+            double wordPageWidth = 450; // 点(约15.9cm)
+            double wordPageHeight = 650; // 点(约22.9cm)
 
+            for (int i = 0; i < pageCount; i++) {
+                // 获取PDF页面尺寸
+                org.apache.pdfbox.pdmodel.PDPage pdfPage = pdfDoc.getPage(i);
+                org.apache.pdfbox.pdmodel.common.PDRectangle mediaBox = pdfPage.getMediaBox();
+                float pdfWidth = mediaBox.getWidth();
+                float pdfHeight = mediaBox.getHeight();
+
+                // 计算缩放比例,使图片适应Word页面
+                double scaleX = wordPageWidth / pdfWidth;
+                double scaleY = wordPageHeight / pdfHeight;
+                double scale = Math.min(scaleX, scaleY); // 取较小值,保证不超出页面
+
+                // 计算最终图片尺寸(保持宽高比)
+                int imgWidthPt = (int) (pdfWidth * scale);
+                int imgHeightPt = (int) (pdfHeight * scale);
+
+                // 使用较高DPI渲染以获得清晰图片
+                int dpi = 150; // 平衡清晰度和文件大小
+                java.awt.image.BufferedImage image = renderer.renderImageWithDPI(i, dpi);
+
+                // 转为PNG
                 java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
                 javax.imageio.ImageIO.write(image, "png", baos);
                 byte[] imageBytes = baos.toByteArray();
                 baos.close();
 
+                // 创建段落并居中
                 XWPFParagraph para = document.createParagraph();
+                para.setAlignment(ParagraphAlignment.CENTER);
+                para.setSpacingBefore(0);
+                para.setSpacingAfter(0);
+
                 XWPFRun run = para.createRun();
                 java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(imageBytes);
                 run.addPicture(bais, org.apache.poi.xwpf.usermodel.Document.PICTURE_TYPE_PNG,
-                        "pdf_page_" + i + ".png", org.apache.poi.util.Units.toEMU(410), org.apache.poi.util.Units.toEMU(600));
+                        "pdf_page_" + i + ".png",
+                        org.apache.poi.util.Units.toEMU(imgWidthPt),
+                        org.apache.poi.util.Units.toEMU(imgHeightPt));
                 bais.close();
 
-                para.createRun().addBreak();
-
+                // 每页PDF后添加分页符(最后一页除外)
                 if (i < pageCount - 1) {
                     XWPFParagraph pageBreak = document.createParagraph();
                     XWPFRun pageBreakRun = pageBreak.createRun();
@@ -1065,7 +1167,7 @@ public class AsyncMaterialSummaryService {
             }
             logger.info("PDF文件已合并:{}", filePath);
         } catch (Exception e) {
-            logger.error("合并PDF失败:{},错误:{}", filePath, e.getMessage());
+            logger.error("合并PDF失败:{},错误:", filePath, e.getMessage());
             XWPFParagraph para = document.createParagraph();
             XWPFRun run = para.createRun();
             run.setText("[PDF文件:" + new java.io.File(filePath).getName() + " - 无法嵌入]");
@@ -1079,7 +1181,7 @@ public class AsyncMaterialSummaryService {
     }
 
     /**
-     * 合并Excel文档(转换为表格后插入
+     * 合并Excel文档(优化版:转图片方式,保持原有格式
      */
     private void mergeExcelDocument(XWPFDocument document, String filePath) {
         org.apache.poi.ss.usermodel.Workbook workbook = null;
@@ -1091,33 +1193,27 @@ public class AsyncMaterialSummaryService {
 
                 if (sheet.getPhysicalNumberOfRows() == 0) continue;
 
-                int rows = sheet.getPhysicalNumberOfRows();
-                int cols = 0;
-                for (int i = 0; i < rows; i++) {
-                    org.apache.poi.ss.usermodel.Row row = sheet.getRow(i);
-                    if (row != null && row.getPhysicalNumberOfCells() > cols) {
-                        cols = row.getPhysicalNumberOfCells();
-                    }
-                }
-
-                if (cols == 0) cols = 1;
-
-                XWPFTable table = document.createTable(rows, cols);
-                table.setStyleID("TableGrid");
-
-                for (int i = 0; i < rows; i++) {
-                    org.apache.poi.ss.usermodel.Row excelRow = sheet.getRow(i);
-                    XWPFTableRow wordRow = table.getRow(i);
-
-                    if (excelRow != null) {
-                        for (int j = 0; j < cols; j++) {
-                            org.apache.poi.ss.usermodel.Cell cell = excelRow.getCell(j);
-                            String cellValue = cell != null ? cell.toString() : "";
-
-                            XWPFTableCell wordCell = wordRow.getCell(j);
-                            wordCell.setText(cellValue);
-                        }
-                    }
+                logger.info("正在转换Sheet: {}", sheet.getSheetName());
+
+                // 将Excel Sheet转为图片
+                byte[] imageBytes = excelSheetToImage(workbook, sheetIndex);
+
+                if (imageBytes != null && imageBytes.length > 0) {
+                    XWPFParagraph para = document.createParagraph();
+                    para.setAlignment(ParagraphAlignment.CENTER);
+                    XWPFRun run = para.createRun();
+
+                    java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(imageBytes);
+                    // 设置图片宽度为页面宽度(约16cm = 453pt),高度自适应
+                    run.addPicture(bais, org.apache.poi.xwpf.usermodel.Document.PICTURE_TYPE_PNG,
+                            "excel_sheet_" + sheetIndex + ".png",
+                            org.apache.poi.util.Units.toEMU(450),
+                            org.apache.poi.util.Units.toEMU(600));
+                    bais.close();
+                } else {
+                    // 图片转换失败,使用表格方式
+                    logger.warn("Excel转图片失败,使用表格方式");
+                    mergeExcelAsTable(document, sheet);
                 }
 
                 if (sheetIndex < workbook.getNumberOfSheets() - 1) {
@@ -1142,6 +1238,243 @@ public class AsyncMaterialSummaryService {
     }
 
     /**
+     * 将Excel Sheet转为PNG图片(优化版)
+     */
+    private byte[] excelSheetToImage(org.apache.poi.ss.usermodel.Workbook workbook, int sheetIndex) {
+        try {
+            org.apache.poi.ss.usermodel.Sheet sheet = workbook.getSheetAt(sheetIndex);
+
+            // 计算表格区域
+            int lastRowNum = sheet.getLastRowNum();
+            int maxColNum = 0;
+            for (int i = 0; i <= lastRowNum; i++) {
+                org.apache.poi.ss.usermodel.Row row = sheet.getRow(i);
+                if (row != null && row.getLastCellNum() > maxColNum) {
+                    maxColNum = row.getLastCellNum();
+                }
+            }
+
+            if (lastRowNum < 0 || maxColNum <= 0) {
+                return null;
+            }
+
+            // 计算每列的最佳宽度(根据内容)
+            int[] colWidths = new int[maxColNum];
+            java.awt.Font measureFont = new java.awt.Font("宋体", java.awt.Font.PLAIN, 14);
+            java.awt.image.BufferedImage tempImg = new java.awt.image.BufferedImage(1, 1, java.awt.image.BufferedImage.TYPE_INT_RGB);
+            java.awt.Graphics2D tempG = tempImg.createGraphics();
+            tempG.setFont(measureFont);
+            java.awt.FontMetrics fm = tempG.getFontMetrics();
+
+            for (int colIdx = 0; colIdx < maxColNum; colIdx++) {
+                int maxWidth = 60; // 最小宽度
+                for (int rowIdx = 0; rowIdx <= lastRowNum; rowIdx++) {
+                    org.apache.poi.ss.usermodel.Row row = sheet.getRow(rowIdx);
+                    if (row != null) {
+                        org.apache.poi.ss.usermodel.Cell cell = row.getCell(colIdx);
+                        if (cell != null) {
+                            String value = getCellValueAsString(cell);
+                            int width = fm.stringWidth(value) + 30; // 加边距
+                            maxWidth = Math.max(maxWidth, Math.min(width, 200)); // 限制最大宽度
+                        }
+                    }
+                }
+                colWidths[colIdx] = maxWidth;
+            }
+            tempG.dispose();
+
+            // 计算图片尺寸
+            int cellHeight = 35;  // 行高
+            int titleHeight = 45; // 标题行高
+            int padding = 40;     // 边距
+
+            int totalWidth = padding * 2;
+            for (int w : colWidths) {
+                totalWidth += w;
+            }
+
+            int imgHeight = padding + titleHeight + (lastRowNum + 1) * cellHeight + padding;
+
+            // 限制最大尺寸
+            totalWidth = Math.min(totalWidth, 1800);
+            imgHeight = Math.min(imgHeight, 2500);
+
+            // 创建图片
+            java.awt.image.BufferedImage image = new java.awt.image.BufferedImage(
+                    totalWidth, imgHeight, java.awt.image.BufferedImage.TYPE_INT_RGB);
+            java.awt.Graphics2D g2d = image.createGraphics();
+
+            // 设置抗锯齿
+            g2d.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING,
+                    java.awt.RenderingHints.VALUE_ANTIALIAS_ON);
+            g2d.setRenderingHint(java.awt.RenderingHints.KEY_TEXT_ANTIALIASING,
+                    java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+            // 白色背景
+            g2d.setColor(java.awt.Color.WHITE);
+            g2d.fillRect(0, 0, totalWidth, imgHeight);
+
+            // 字体
+            java.awt.Font titleFont = new java.awt.Font("宋体", java.awt.Font.BOLD, 16);
+            java.awt.Font headerFont = new java.awt.Font("宋体", java.awt.Font.BOLD, 14);
+            java.awt.Font contentFont = new java.awt.Font("宋体", java.awt.Font.PLAIN, 13);
+
+            int startX = padding;
+            int startY = padding;
+
+            // 绘制表格
+            for (int rowIdx = 0; rowIdx <= lastRowNum; rowIdx++) {
+                org.apache.poi.ss.usermodel.Row row = sheet.getRow(rowIdx);
+                int currentHeight = (rowIdx == 0) ? titleHeight : cellHeight;
+                int y = startY + (rowIdx == 0 ? 0 : titleHeight + (rowIdx - 1) * cellHeight);
+
+                int x = startX;
+                for (int colIdx = 0; colIdx < maxColNum; colIdx++) {
+                    int cellWidth = colWidths[colIdx];
+
+                    // 获取单元格值
+                    String cellValue = "";
+                    if (row != null) {
+                        org.apache.poi.ss.usermodel.Cell cell = row.getCell(colIdx);
+                        if (cell != null) {
+                            cellValue = getCellValueAsString(cell);
+                        }
+                    }
+
+                    // 绘制单元格背景
+                    if (rowIdx == 0) {
+                        // 标题行 - 深色背景
+                        g2d.setColor(new java.awt.Color(64, 64, 64));
+                        g2d.fillRect(x, y, cellWidth, currentHeight);
+                        g2d.setFont(titleFont);
+                        g2d.setColor(java.awt.Color.WHITE);
+                    } else if (rowIdx == 1) {
+                        // 表头行 - 浅灰背景
+                        g2d.setColor(new java.awt.Color(220, 220, 220));
+                        g2d.fillRect(x, y, cellWidth, currentHeight);
+                        g2d.setFont(headerFont);
+                        g2d.setColor(java.awt.Color.BLACK);
+                    } else {
+                        // 数据行 - 交替颜色
+                        if (rowIdx % 2 == 0) {
+                            g2d.setColor(new java.awt.Color(245, 245, 245));
+                        } else {
+                            g2d.setColor(java.awt.Color.WHITE);
+                        }
+                        g2d.fillRect(x, y, cellWidth, currentHeight);
+                        g2d.setFont(contentFont);
+                        g2d.setColor(java.awt.Color.BLACK);
+                    }
+
+                    // 绘制边框
+                    g2d.setColor(new java.awt.Color(180, 180, 180));
+                    g2d.drawRect(x, y, cellWidth, currentHeight);
+
+                    // 绘制文字(居中)
+                    if (rowIdx == 0) {
+                        g2d.setColor(java.awt.Color.WHITE);
+                    } else {
+                        g2d.setColor(java.awt.Color.BLACK);
+                    }
+                    java.awt.FontMetrics metrics = g2d.getFontMetrics();
+                    int textX = x + (cellWidth - metrics.stringWidth(cellValue)) / 2;
+                    int textY = y + (currentHeight + metrics.getAscent() - metrics.getDescent()) / 2;
+                    g2d.drawString(cellValue, Math.max(textX, x + 5), textY);
+
+                    x += cellWidth;
+                }
+            }
+
+            // 绘制外边框(加粗)
+            g2d.setColor(new java.awt.Color(100, 100, 100));
+            g2d.setStroke(new java.awt.BasicStroke(2));
+            int tableWidth = 0;
+            for (int w : colWidths) tableWidth += w;
+            int tableHeight = titleHeight + lastRowNum * cellHeight;
+            g2d.drawRect(startX, startY, tableWidth, tableHeight);
+
+            g2d.dispose();
+
+            // 转为PNG字节数组
+            java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+            javax.imageio.ImageIO.write(image, "png", baos);
+            return baos.toByteArray();
+
+        } catch (Exception e) {
+            logger.error("Excel转图片失败: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 获取单元格值(字符串形式)
+     */
+    private String getCellValueAsString(org.apache.poi.ss.usermodel.Cell cell) {
+        if (cell == null) return "";
+
+        switch (cell.getCellType()) {
+            case STRING:
+                return cell.getStringCellValue();
+            case NUMERIC:
+                if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) {
+                    return new java.text.SimpleDateFormat("yyyy-MM-dd").format(cell.getDateCellValue());
+                }
+                double numValue = cell.getNumericCellValue();
+                if (numValue == Math.floor(numValue)) {
+                    return String.valueOf((long) numValue);
+                }
+                return String.valueOf(numValue);
+            case BOOLEAN:
+                return String.valueOf(cell.getBooleanCellValue());
+            case FORMULA:
+                try {
+                    return String.valueOf(cell.getNumericCellValue());
+                } catch (Exception e) {
+                    try {
+                        return cell.getStringCellValue();
+                    } catch (Exception e2) {
+                        return "";
+                    }
+                }
+            default:
+                return "";
+        }
+    }
+
+    /**
+     * 以表格方式合并Excel(备用方案)
+     */
+    private void mergeExcelAsTable(XWPFDocument document, org.apache.poi.ss.usermodel.Sheet sheet) {
+        int rows = sheet.getPhysicalNumberOfRows();
+        int cols = 0;
+        for (int i = 0; i < rows; i++) {
+            org.apache.poi.ss.usermodel.Row row = sheet.getRow(i);
+            if (row != null && row.getPhysicalNumberOfCells() > cols) {
+                cols = row.getPhysicalNumberOfCells();
+            }
+        }
+
+        if (cols == 0) cols = 1;
+
+        XWPFTable table = document.createTable(rows, cols);
+
+        for (int i = 0; i < rows; i++) {
+            org.apache.poi.ss.usermodel.Row excelRow = sheet.getRow(i);
+            XWPFTableRow wordRow = table.getRow(i);
+
+            if (excelRow != null) {
+                for (int j = 0; j < cols; j++) {
+                    org.apache.poi.ss.usermodel.Cell cell = excelRow.getCell(j);
+                    String cellValue = cell != null ? getCellValueAsString(cell) : "";
+
+                    XWPFTableCell wordCell = wordRow.getCell(j);
+                    wordCell.setText(cellValue);
+                }
+            }
+        }
+    }
+
+    /**
      * 更新单个资料归纳主表的总页数
      *
      * @param masterId 主表ID
@@ -1759,4 +2092,156 @@ public class AsyncMaterialSummaryService {
         paragraph.setAlignment(ParagraphAlignment.CENTER);
     }
 
+    /**
+     * 添加普通分节符(下一页)
+     * 用于封面和目录之间的分隔,不设置页码
+     *
+     * @param document Word文档
+     */
+    private void addSectionBreak(XWPFDocument document) {
+        try {
+            // 添加分页符
+            XWPFParagraph paragraph = document.createParagraph();
+            XWPFRun run = paragraph.createRun();
+            run.addBreak(BreakType.PAGE);
+            logger.info("添加分页符(封面/目录分隔)");
+        } catch (Exception e) {
+            logger.error("添加分页符失败:{}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 添加分节符并设置页码(正文部分使用)
+     * 页码格式:第 X 页 / 共 Y 页(居中显示)
+     *
+     * @param document Word文档
+     */
+    private void addSectionBreakWithPageNumber(XWPFDocument document) {
+        try {
+            // 添加分页符
+            XWPFParagraph paragraph = document.createParagraph();
+            XWPFRun run = paragraph.createRun();
+            run.addBreak(BreakType.PAGE);
+
+            // 创建页脚并添加页码
+            XWPFHeaderFooterPolicy policy = document.getHeaderFooterPolicy();
+            if (policy == null) {
+                policy = document.createHeaderFooterPolicy();
+            }
+            XWPFFooter footer = policy.createFooter(XWPFHeaderFooterPolicy.DEFAULT);
+
+            // 创建页脚段落
+            XWPFParagraph footerPara = footer.createParagraph();
+            footerPara.setAlignment(ParagraphAlignment.CENTER);
+
+            // 使用复杂字段方式添加页码
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP ctp = footerPara.getCTP();
+
+            // 添加 "第 " 文本
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR r1 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr rPr1 = r1.addNewRPr();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFonts fonts1 = rPr1.addNewRFonts();
+            fonts1.setAscii("宋体");
+            fonts1.setEastAsia("宋体");
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHpsMeasure sz1 = rPr1.addNewSz();
+            sz1.setVal(java.math.BigInteger.valueOf(20)); // 10号字 = 20半磅
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText t1 = r1.addNewT();
+            t1.setStringValue("第 ");
+
+            // 添加 PAGE 字段开始
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rBegin = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar fldCharBegin = rBegin.addNewFldChar();
+            fldCharBegin.setFldCharType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType.BEGIN);
+
+            // 添加 PAGE 字段指令
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rInstr = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText instrText = rInstr.addNewInstrText();
+            instrText.setStringValue(" PAGE ");
+
+            // 添加字段分隔符
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rSep = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar fldCharSep = rSep.addNewFldChar();
+            fldCharSep.setFldCharType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType.SEPARATE);
+
+            // 添加字段默认值
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rDefault = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText tDefault = rDefault.addNewT();
+            tDefault.setStringValue("1");
+
+            // 添加字段结束
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rEnd = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar fldCharEnd = rEnd.addNewFldChar();
+            fldCharEnd.setFldCharType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType.END);
+
+            // 添加 " 页 / 共 " 文本
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR r2 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr rPr2 = r2.addNewRPr();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFonts fonts2 = rPr2.addNewRFonts();
+            fonts2.setAscii("宋体");
+            fonts2.setEastAsia("宋体");
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHpsMeasure sz2 = rPr2.addNewSz();
+            sz2.setVal(java.math.BigInteger.valueOf(20));
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText t2 = r2.addNewT();
+            t2.setStringValue(" 页 / 共 ");
+
+            // 添加 NUMPAGES 字段开始
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rBegin2 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar fldCharBegin2 = rBegin2.addNewFldChar();
+            fldCharBegin2.setFldCharType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType.BEGIN);
+
+            // 添加 NUMPAGES 字段指令
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rInstr2 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText instrText2 = rInstr2.addNewInstrText();
+            instrText2.setStringValue(" NUMPAGES ");
+
+            // 添加字段分隔符
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rSep2 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar fldCharSep2 = rSep2.addNewFldChar();
+            fldCharSep2.setFldCharType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType.SEPARATE);
+
+            // 添加字段默认值
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rDefault2 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText tDefault2 = rDefault2.addNewT();
+            tDefault2.setStringValue("1");
+
+            // 添加字段结束
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR rEnd2 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar fldCharEnd2 = rEnd2.addNewFldChar();
+            fldCharEnd2.setFldCharType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType.END);
+
+            // 添加 " 页" 文本
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR r3 = ctp.addNewR();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr rPr3 = r3.addNewRPr();
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFonts fonts3 = rPr3.addNewRFonts();
+            fonts3.setAscii("宋体");
+            fonts3.setEastAsia("宋体");
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHpsMeasure sz3 = rPr3.addNewSz();
+            sz3.setVal(java.math.BigInteger.valueOf(20));
+            org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText t3 = r3.addNewT();
+            t3.setStringValue(" 页");
+
+            logger.info("添加分页符并设置页码");
+        } catch (Exception e) {
+            logger.error("添加带页码的分页符失败:{}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 添加分节符(封底部分使用)
+     * 不显示页码
+     *
+     * @param document Word文档
+     */
+    private void addSectionBreakWithoutPageNumber(XWPFDocument document) {
+        try {
+            // 添加分页符
+            XWPFParagraph paragraph = document.createParagraph();
+            XWPFRun run = paragraph.createRun();
+            run.addBreak(BreakType.PAGE);
+            logger.info("添加分页符(封底分隔)");
+        } catch (Exception e) {
+            logger.error("添加分页符失败:{}", e.getMessage(), e);
+        }
+    }
+
 }

+ 51 - 0
assistMg/src/main/resources/mapper/CostProjectProccessNodeDetailMapper.xml

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.hotent.project.dao.CostProjectProccessNodeDetailDao">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.hotent.project.model.CostProjectProccessNodeDetail">
+        <id column="id" property="id"/>
+        <result column="process_id" property="processId"/>
+        <result column="process_node_id" property="processNodeId"/>
+        <result column="process_node_key" property="processNodeKey"/>
+        <result column="task_id" property="taskId"/>
+        <result column="source_type" property="sourceType"/>
+        <result column="executor_id" property="executorId"/>
+        <result column="executor_name" property="executorName"/>
+        <result column="executor_dept" property="executorDept"/>
+        <result column="action_type" property="actionType"/>
+        <result column="action_name" property="actionName"/>
+        <result column="execute_time" property="executeTime"/>
+        <result column="remark" property="remark"/>
+        <result column="audited_unit_id" property="auditedUnitId"/>
+        <result column="audited_unit_name" property="auditedUnitName"/>
+        <result column="is_deleted" property="isDeleted"/>
+        <result column="create_time" property="createTime"/>
+        <result column="create_by" property="createBy"/>
+        <result column="update_time" property="updateTime"/>
+        <result column="update_by" property="updateBy"/>
+    </resultMap>
+
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+        id, process_id, process_node_id, process_node_key, task_id, source_type,
+        executor_id, executor_name, executor_dept, action_type, action_name,
+        execute_time, remark, audited_unit_id, audited_unit_name, is_deleted,
+        create_time, create_by, update_time, update_by
+    </sql>
+
+    <select id="selectPage" resultMap="BaseResultMap">
+        select
+        <include refid="Base_Column_List"/>
+        from cost_project_proccess_node_detail
+        ${ew.customSqlSegment}
+    </select>
+
+    <select id="selectList" resultMap="BaseResultMap">
+        select
+        <include refid="Base_Column_List"/>
+        from cost_project_proccess_node_detail
+        ${ew.customSqlSegment}
+    </select>
+
+</mapper>