浏览代码

fix:是否展示去除校验

zzw 1 月之前
父节点
当前提交
647d2e3123

+ 1 - 1
assistMg/src/main/java/com/hotent/baseInfo/manager/impl/CostDocumentWhManagerImpl.java

@@ -240,7 +240,7 @@ public class CostDocumentWhManagerImpl extends BaseManagerImpl<CostDocumentWhDao
                 LocalDate date = LocalDate.now(); // 获取当前日期
                 // 获取当前年份
                 int currentYear = date.getYear();
-                if (costDocumentWh.getLastGenerateDate().getYear() == currentYear) {
+                if (costDocumentWh.getLastGenerateDate() !=null && costDocumentWh.getLastGenerateDate().getYear() == currentYear) {
                     costDocumentWh.setCurrentValue(costDocumentWh.getCurrentValue()+1);
                     costDocumentWh.setLastGenerateDate(LocalDate.now());
                 }

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

@@ -360,7 +360,7 @@ public class CostProjectTaskMaterialSummaryController extends BaseController<Cos
         }
         resultList.add(catalogResp);
 
-        // 3. 14个资料归纳
+        // 3. 14个资料归纳(每个类别独立显示页码:1-N)
         for (CostProjectTaskMaterialSummary summary : materialList) {
             ArchiveProofreadResp resp = new ArchiveProofreadResp();
             resp.setOrderNum(displayOrderNum++);
@@ -380,9 +380,9 @@ public class CostProjectTaskMaterialSummaryController extends BaseController<Cos
                 }
             }
             resp.setPageCount(pageCount);
+            // 每个资料类别独立显示页码:1-N(不累加)
             if (pageCount > 0) {
-                resp.setPageRange(currentPage + "-" + (currentPage + pageCount - 1));
-                currentPage += pageCount;
+                resp.setPageRange("1-" + pageCount);
             } else {
                 resp.setPageRange("—");
             }

+ 125 - 34
assistMg/src/main/java/com/hotent/project/manager/impl/CostProjectTaskManagerImpl.java

@@ -363,7 +363,20 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
                     .filter(c -> rootTask.getAuditType() != null &&
                             rootTask.getAuditType().equals(String.valueOf(c.getKey())))
                     .findAny().ifPresent(costDictData -> rootTask.setAuditTypeName(costDictData.getName()));
-            // 根任务状态
+            // 获取项目立项信息
+            CostProjectApproval approval = costProjectApprovalManager.getOne(
+                    new LambdaQueryWrapper<CostProjectApproval>()
+                            .eq(CostProjectApproval::getProjectId, rootTask.getProjectId())
+                            .eq(CostProjectApproval::getIsDeleted, "0")
+            );
+            String projectMembers = approval.getProjectMembers();
+            String leaderIds = approval.getLeaderIds();
+            if (leaderIds!=null){
+                rootTask.setCanLeaderOperate(Arrays.asList(leaderIds.split(",")).contains(currentUserId));
+            }else{
+                rootTask.setCanLeaderOperate(false);
+            }
+            rootTask.setCanMemberOperate(false);
             rootTask.setStatusName(TaskStatusConstant.getStatusNameByCode(rootTask.getStatus()));
             rootTask.setCurrentNodeName(NodeConstant.getNodeValueByKey(rootTask.getCurrentNode()));
             rootTask.setWarningStatus(taskWarningStatusComponent.calculateWarningStatus(rootTask));
@@ -390,6 +403,8 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
             List<CostProjectTask> childTasks =
                     childGroup.getOrDefault(rootTask.getId(), Collections.emptyList());
             for (CostProjectTask childTask : childTasks) {
+                childTask.setCanLeaderOperate(false);
+                childTask.setCanMemberOperate(Arrays.asList(projectMembers.split(",")).contains(currentUserId));
                 childTask.setStatusName(TaskStatusConstant.getStatusNameByCode(childTask.getStatus()));
                 CostProjectApproval detail = approvalMap.get(childTask.getProjectId());
                 if (detail != null) {
@@ -452,9 +467,10 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         this.updateById(task);
 
         // 发送通知
-        String title = NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "材料已提交";
+        String nodeName = NodeConstant.getNodeValueByKey(task.getCurrentNode());
+        String title = "【" + nodeName + "】材料已提交";
         // 操作人
-        String content = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]" + AuthenticationUtil.getCurrentUserFullname() + "材料已提交" + task.getAuditedUnitName() + "的项目(" + task.getProjectName() + ")";
+        String content = "【" + nodeName + "】" + AuthenticationUtil.getCurrentUserFullname() + "已提交" + nodeName + "所需材料。(" + task.getProjectName() + ")";
         String enterpriseId = task.getAuditedUnitId() == null ? "" : task.getAuditedUnitId();
         String orgId = getCurrentUserMainOrgId();
         Org org = orgManager.getById(orgId);
@@ -478,9 +494,10 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         task.setStatus(TaskStatusConstant.FEEDBACKED.getStatusCode());
         this.updateById(task);
         // 发送通知
-        String title = NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "意见已反馈";
+        String nodeName = NodeConstant.getNodeValueByKey(task.getCurrentNode());
+        String title = "【" + nodeName + "】意见已反馈";
         // 操作人
-        String content = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]" + AuthenticationUtil.getCurrentUserFullname() + "意见已反馈" + task.getAuditedUnitName() + "的项目(" + task.getProjectName() + ")";
+        String content = "【" + nodeName + "】" + AuthenticationUtil.getCurrentUserFullname() + "已提交意见反馈所需资料。(" + task.getProjectName() + ")";
         String enterpriseId = task.getAuditedUnitId() == null ? "" : task.getAuditedUnitId();
         String orgId = getCurrentUserMainOrgId();
         Org org = orgManager.getById(orgId);
@@ -500,6 +517,8 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
 
         CostProjectTask task = costProjectTaskManager.getById(req.getTaskId());
 
+        // 权限校验:主办人操作主任务,从办人操作子任务
+        checkTaskOperationPermission(task);
 
         String resultMessage = "";
         switch (req.getKey()) {
@@ -592,9 +611,10 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         costProjectTaskManager.updateById(task);
 
         // 发送通知
-        String title = NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "通过";
+        String nodeName = NodeConstant.getNodeValueByKey(task.getCurrentNode());
+        String title = "【" + nodeName + "】任务已通过";
         // 操作人
-        String content = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]" + AuthenticationUtil.getCurrentUserFullname() + "已通过" + task.getAuditedUnitName() + "的项目(" + task.getProjectName() + ")";
+        String content = "【" + nodeName + "】" + AuthenticationUtil.getCurrentUserFullname() + "已通过" + task.getAuditedUnitName() + "提交的材料。(" + task.getProjectName() + ")";
         String enterpriseId = task.getAuditedUnitId() == null ? "" : task.getAuditedUnitId();
         String orgId = getCurrentUserMainOrgId();
         Org org = orgManager.getById(orgId);
@@ -616,17 +636,19 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
             return "仅支持子任务进行此操作!";
         }
         // 通知内容组装
-        String title = NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "退回";
+        String nodeName = NodeConstant.getNodeValueByKey(task.getCurrentNode());
+        String title = "【" + nodeName + "】任务未通过";
         String enterpriseId = task.getAuditedUnitId() == null ? "" : task.getAuditedUnitId();
         String orgId = getCurrentUserMainOrgId();
         Org org = orgManager.getById(orgId);
         String noticeSource = (org.getName()) + " " + AuthenticationUtil.getCurrentUserFullname();
         String sendTarget = task.getCreateBy() == null ? "" : task.getCreateBy();
 
-        String content = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]" + AuthenticationUtil.getCurrentUserFullname() + "已退回" + task.getAuditedUnitName() + "的项目(" + task.getProjectName() + ")";
+        String content = "【" + nodeName + "】" + AuthenticationUtil.getCurrentUserFullname() + "未通过" + task.getAuditedUnitName() + "提交的材料";
         if (StringUtil.isNotEmpty(req.getContent())) {
             content += ",原因:" + req.getContent();
         }
+        content += "。(" + task.getProjectName() + ")";
         costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, sendTarget);
 
         task.setStatus(TaskStatusConstant.NOT_PASSED.getStatusCode());
@@ -702,12 +724,15 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         }
 
         // 通知内容组装
+        String nodeName = NodeConstant.getNodeValueByKey(task.getCurrentNode());
         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() + ")";
+        String statusName = TaskStatusConstant.SUSPENDED.getStatusCode().equals(status) ? "中止" : "恢复";
+        String title = "【" + nodeName + "】任务已" + statusName;
+        String content = "【" + nodeName + "】" + AuthenticationUtil.getCurrentUserFullname() + statusName + "此任务";
         if (StringUtil.isNotEmpty(req.getContent())) {
             content += ",原因:" + req.getContent();
         }
+        content += "。(" + task.getProjectName() + ")";
         String enterpriseId = task.getAuditedUnitId() == null ? "" : task.getAuditedUnitId();
         String orgId = getCurrentUserMainOrgId();
         Org org = orgManager.getById(orgId);
@@ -727,7 +752,8 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
      */
     private String supplementMaterial(CostProjectTask task, CostTaskPageReq req) {
         String currentNode = task.getCurrentNode();
-        String title = NodeConstant.getNodeValueByKey(currentNode) + TaskStatusConstant.BCCL.getStatusName();
+        String nodeName = NodeConstant.getNodeValueByKey(currentNode);
+        String title = "【" + nodeName + "】补充材料";
         String orgId = getCurrentUserMainOrgId();
         Org org = orgManager.getById(orgId);
         String noticeSource = (org.getName()) + " " + AuthenticationUtil.getCurrentUserFullname();
@@ -755,7 +781,11 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
                         childTask.setStatus(TaskStatusConstant.BCCL.getStatusCode());
                         costProjectTaskManager.updateById(childTask);
                         // 通知内容组装(针对子任务对应的单位)
-                        String childContent = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]" + AuthenticationUtil.getCurrentUserFullname() + "要求" + childTask.getAuditedUnitName() + "补充材料,项目(" + task.getProjectName() + ")";
+                        String childContent = "【" + nodeName + "】您有一条任务未处理,请及时办理";
+                        if (StringUtil.isNotEmpty(req.getContent())) {
+                            childContent += ",原因:" + req.getContent();
+                        }
+                        childContent += "。(" + task.getProjectName() + ")";
                         String childEnterpriseId = childTask.getAuditedUnitId() == null ? "" : childTask.getAuditedUnitId();
                         String childSendTarget = childTask.getCreateBy() == null ? "" : childTask.getCreateBy();
                         costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, childContent, childEnterpriseId, noticeSource, childSendTarget);
@@ -779,7 +809,11 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
                 }
                 task.setStatus(TaskStatusConstant.BCCL.getStatusCode());
                 costProjectTaskManager.updateById(task);
-                String content = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]" + AuthenticationUtil.getCurrentUserFullname() + "要求" + task.getAuditedUnitName() + "补充材料,项目(" + task.getProjectName() + ")";
+                String content = "【" + nodeName + "】您有一条任务未处理,请及时办理";
+                if (StringUtil.isNotEmpty(req.getContent())) {
+                    content += ",原因:" + req.getContent();
+                }
+                content += "。(" + task.getProjectName() + ")";
                 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);
@@ -895,8 +929,8 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
                                     .eq(AuditedUnit::getIsDeleted, "0")
                     );
                     if (auditedUnit != null && StringUtil.isNotEmpty(auditedUnit.getAccount())) {
-                        String noticeTitle = "意见告知通知";
-                        String noticeContent = "您的项目(" + child.getProjectName() + ")已退回至意见告知阶段,请及时查看";
+                        String noticeTitle = "【意见告知】进入意见告知环节";
+                        String noticeContent = "【意见告知】您的项目已进入意见告知环节,请及时查看。(" + child.getProjectName() + ")";
                         String orgId = getCurrentUserMainOrgId();
                         Org org = orgManager.getById(orgId);
                         String noticeSource = (org != null ? org.getName() : "系统") + " " + AuthenticationUtil.getCurrentUserFullname();
@@ -921,8 +955,8 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
                                     .eq(AuditedUnit::getIsDeleted, "0")
                     );
                     if (auditedUnit != null && StringUtil.isNotEmpty(auditedUnit.getAccount())) {
-                        String noticeTitle = "意见反馈通知";
-                        String noticeContent = "您的项目(" + child.getProjectName() + ")需要反馈意见,请及时处理";
+                        String noticeTitle = "【意见反馈】进入意见反馈环节";
+                        String noticeContent = "【意见反馈】您的项目已进入意见反馈环节,请及时查看。(" + child.getProjectName() + ")";
                         String orgId = getCurrentUserMainOrgId();
                         Org org = orgManager.getById(orgId);
                         String noticeSource = (org != null ? org.getName() : "系统") + " " + AuthenticationUtil.getCurrentUserFullname();
@@ -942,16 +976,18 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         }
 
         // 通知内容组装
-        String title = "已退回至" + NodeConstant.getNodeValueByKey(prevNodeStatus);
+        String prevNodeName = NodeConstant.getNodeValueByKey(prevNodeStatus);
+        String title = "【" + prevNodeName + "】已退回至" + prevNodeName;
         String enterpriseId = task.getAuditedUnitId() == null ? "" : task.getAuditedUnitId();
         String orgId = getCurrentUserMainOrgId();
         Org org = orgManager.getById(orgId);
         String noticeSource = (org.getName()) + " " + AuthenticationUtil.getCurrentUserFullname();
         String sendTarget = task.getCreateBy() == null ? "" : task.getCreateBy();
-        String content = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]已退回至[" + NodeConstant.getNodeValueByKey(prevNodeStatus) + "]," + task.getProjectName();
+        String content = "【" + prevNodeName + "】" + AuthenticationUtil.getCurrentUserFullname() + "已将任务退回至" + prevNodeName;
         if (StringUtil.isNotEmpty(req.getContent())) {
             content += ",原因:" + req.getContent();
         }
+        content += "。(" + task.getProjectName() + ")";
         costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, sendTarget);
 
         // 记录环节明细
@@ -1085,8 +1121,8 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
                                     .eq(AuditedUnit::getIsDeleted, "0")
                     );
                     if (auditedUnit != null && StringUtil.isNotEmpty(auditedUnit.getAccount())) {
-                        String noticeTitle = "意见告知通知";
-                        String noticeContent = "您的项目(" + child.getProjectName() + ")已进入意见告知阶段,请及时查看";
+                        String noticeTitle = "【意见告知】进入意见告知环节";
+                        String noticeContent = "【意见告知】您的项目已进入意见告知环节,请及时查看。(" + child.getProjectName() + ")";
                         String orgId = getCurrentUserMainOrgId();
                         Org org = orgManager.getById(orgId);
                         String noticeSource = (org != null ? org.getName() : "系统") + " " + AuthenticationUtil.getCurrentUserFullname();
@@ -1111,8 +1147,8 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
                                     .eq(AuditedUnit::getIsDeleted, "0")
                     );
                     if (auditedUnit != null && StringUtil.isNotEmpty(auditedUnit.getAccount())) {
-                        String noticeTitle = "意见反馈通知";
-                        String noticeContent = "您的项目(" + child.getProjectName() + ")需要反馈意见,请及时处理";
+                        String noticeTitle = "【意见反馈】进入意见反馈环节";
+                        String noticeContent = "【意见反馈】您的项目已进入意见反馈环节,请及时查看。(" + child.getProjectName() + ")";
                         String orgId = getCurrentUserMainOrgId();
                         Org org = orgManager.getById(orgId);
                         String noticeSource = (org != null ? org.getName() : "系统") + " " + AuthenticationUtil.getCurrentUserFullname();
@@ -1132,16 +1168,18 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         }
 
         // 通知内容组装
-        String title = NodeConstant.getNodeValueByKey(nextNodeStatus) + "已开始";
+        String nextNodeName = NodeConstant.getNodeValueByKey(nextNodeStatus);
+        String title = "【" + nextNodeName + "】已进入" + nextNodeName + "环节";
         String enterpriseId = task.getAuditedUnitId() == null ? "" : task.getAuditedUnitId();
         String orgId = getCurrentUserMainOrgId();
         Org org = orgManager.getById(orgId);
         String noticeSource = (org.getName()) + " " + AuthenticationUtil.getCurrentUserFullname();
         String sendTarget = task.getCreateBy() == null ? "" : task.getCreateBy();
-        String content = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]已办结并扭转至[" + NodeConstant.getNodeValueByKey(nextNodeStatus) + "]," + task.getProjectName();
+        String content = "【" + nextNodeName + "】" + AuthenticationUtil.getCurrentUserFullname() + "已填写相应数据并通过此任务";
         if (StringUtil.isNotEmpty(req.getContent())) {
             content += ",备注:" + req.getContent();
         }
+        content += "。(" + task.getProjectName() + ")";
         costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, sendTarget);
         //流程节点如果选择主办人或者从办人后修改任务负责人和负责组信息
         if (StringUtil.isNotEmpty(req.getUserIds()) || StringUtil.isNotEmpty(req.getLeaderIds())) {
@@ -1503,17 +1541,19 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         if (StringUtil.isEmpty(req.getUserIds())) {
             throw new RuntimeException("未指定催办人员");
         }
-        String title = NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "催办";
+        String nodeName = NodeConstant.getNodeValueByKey(task.getCurrentNode());
+        String title = "【" + nodeName + "】催办通知";
         String orgId = getCurrentUserMainOrgId();
         Org org = orgManager.getById(orgId);
         String noticeSource = (org.getName()) + " " + AuthenticationUtil.getCurrentUserFullname();
         String enterpriseId = task.getAuditedUnitId() == null ? "" : task.getAuditedUnitId();
         // 分割用户ID并逐个发送通知
         for (String userId : req.getUserIds().split(",")) {
-            String content = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]" + AuthenticationUtil.getCurrentUserFullname() + "催办" + task.getAuditedUnitName() + "的项目(" + task.getProjectName() + ")";
+            String content = "【" + nodeName + "】您有一条催办消息通知,请及时查看";
             if (StringUtil.isNotEmpty(req.getContent())) {
                 content += ",原因:" + req.getContent();
             }
+            content += "。(" + task.getProjectName() + ")";
             // 使用传入的用户ID作为发送目标
             costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, userId);
         }
@@ -1532,11 +1572,13 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
      * @return 催报结果消息
      */
     private String remindUnitTask(CostProjectTask task, CostTaskPageReq req) {
-        String title = NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "催报";
-        String content = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]" + AuthenticationUtil.getCurrentUserFullname() + "催报" + task.getAuditedUnitName() + "的项目(" + task.getProjectName() + ")";
+        String nodeName = NodeConstant.getNodeValueByKey(task.getCurrentNode());
+        String title = "【" + nodeName + "】督办通知";
+        String content = "【" + nodeName + "】您有一条督办消息通知,请及时查看";
         if (StringUtil.isNotEmpty(req.getContent())) {
             content += ",原因:" + req.getContent();
         }
+        content += "。(" + task.getProjectName() + ")";
         String enterpriseId = task.getAuditedUnitId() == null ? "" : task.getAuditedUnitId();
         String orgId = getCurrentUserMainOrgId();
         Org org = orgManager.getById(orgId);
@@ -1657,16 +1699,18 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         }
 
         // 发送通知
-        String title = "复核-已退回至" + NodeConstant.getNodeValueByKey(reviewNodeKey);
+        String reviewNodeName = NodeConstant.getNodeValueByKey(reviewNodeKey);
+        String title = "【" + reviewNodeName + "】复核-已退回至" + reviewNodeName;
         String enterpriseId = task.getAuditedUnitId() == null ? "" : task.getAuditedUnitId();
         String orgId = getCurrentUserMainOrgId();
         Org org = orgManager.getById(orgId);
         String noticeSource = (org.getName()) + " " + AuthenticationUtil.getCurrentUserFullname();
         String sendTarget = task.getCreateBy() == null ? "" : task.getCreateBy();
-        String content = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]" + AuthenticationUtil.getCurrentUserFullname() + "要求复核,已退回至[" + NodeConstant.getNodeValueByKey(reviewNodeKey) + "]," + task.getProjectName();
+        String content = "【" + reviewNodeName + "】" + AuthenticationUtil.getCurrentUserFullname() + "要求复核,已退回至" + reviewNodeName;
         if (StringUtil.isNotEmpty(req.getContent())) {
             content += ",原因:" + req.getContent();
         }
+        content += "。(" + task.getProjectName() + ")";
         costNoticeManager.sendNotice(task.getProjectId(), task.getId(), "1", title, content, enterpriseId, noticeSource, sendTarget);
 
         // 记录环节明细
@@ -1712,11 +1756,13 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
 
 
         // 发送通知
-        String title = "任务已归档";
-        String content = "[" + NodeConstant.getNodeValueByKey(task.getCurrentNode()) + "]" + AuthenticationUtil.getCurrentUserFullname() + "已归档" + task.getProjectName();
+        String nodeName = NodeConstant.getNodeValueByKey(task.getCurrentNode());
+        String title = "【" + nodeName + "】任务已归档";
+        String content = "【" + nodeName + "】" + AuthenticationUtil.getCurrentUserFullname() + "已归档此任务";
         if (StringUtil.isNotEmpty(req.getContent())) {
             content += ",备注:" + req.getContent();
         }
+        content += "。(" + task.getProjectName() + ")";
         String enterpriseId = task.getAuditedUnitId() == null ? "" : task.getAuditedUnitId();
         String orgId = getCurrentUserMainOrgId();
         Org org = orgManager.getById(orgId);
@@ -1737,4 +1783,49 @@ public class CostProjectTaskManagerImpl extends BaseManagerImpl<CostProjectTaskD
         costProjectProccessNodeDetailManager.recordByTaskId(task.getId(), actionType, actionName, remark);
     }
 
+    /**
+     * 校验任务操作权限
+     * 主任务(pid=0):只有主办人(leaderIds)可以操作
+     * 子任务(pid!=0):只有从办人(projectMembers)可以操作
+     *
+     * @param task 任务对象
+     */
+    private void checkTaskOperationPermission(CostProjectTask task) {
+        if (task == null) {
+            throw new RuntimeException("任务不存在");
+        }
+
+        // 获取当前用户ID
+        IUser currentUser = ContextUtil.getCurrentUser();
+        String currentUserId = currentUser.getUserId();
+
+        // 获取项目立项信息
+        CostProjectApproval approval = costProjectApprovalManager.getOne(
+                new LambdaQueryWrapper<CostProjectApproval>()
+                        .eq(CostProjectApproval::getProjectId, task.getProjectId())
+                        .eq(CostProjectApproval::getIsDeleted, "0")
+        );
+
+        if (approval == null) {
+            throw new RuntimeException("项目立项信息不存在");
+        }
+
+        boolean isMainTask = "0".equals(task.getPid());
+
+        if (isMainTask) {
+            // 主任务:只有主办人可以操作
+            String leaderIds = approval.getLeaderIds();
+            if (StringUtil.isEmpty(leaderIds) || !leaderIds.contains(currentUserId)) {
+                throw new RuntimeException("您不是该项目的主办人,无权操作主任务");
+            }
+        } else {
+            // 子任务:只有从办人可以操作
+            String projectMembers = approval.getProjectMembers();
+            if (StringUtil.isEmpty(projectMembers) || !projectMembers.contains(currentUserId)) {
+                throw new RuntimeException("您不是该项目的从办人,无权操作子任务");
+            }
+        }
+    }
+
+
 }

+ 10 - 0
assistMg/src/main/java/com/hotent/project/model/CostProjectTask.java

@@ -180,4 +180,14 @@ public class CostProjectTask extends BaseModel<CostProjectTask> {
     @JsonProperty("preSuspendStatus")
     private String preSuspendStatus;
 
+    @ApiModelProperty(value = "是否可主办操作:当前用户是主办人且是主任务时为true")
+    @TableField(exist = false)
+    @JsonProperty("canLeaderOperate")
+    private Boolean canLeaderOperate;
+
+    @ApiModelProperty(value = "是否可从办操作:当前用户是从办人且是子任务时为true")
+    @TableField(exist = false)
+    @JsonProperty("canMemberOperate")
+    private Boolean canMemberOperate;
+
 }

+ 206 - 105
assistMg/src/main/java/com/hotent/project/service/ArchiveTest.java

@@ -17,11 +17,25 @@ import java.util.List;
  * 页码规则:
  * - 封面:无页码
  * - 目录:无页码
- * - 正文:有页码(第 X 页 / 共 Y 页)
+ * - 正文:有页码,页眉全局连续(001、002...),页脚按资料类别重置(第 1 页 / 共 N 页)
  * - 封底:无页码
  */
 public class ArchiveTest {
 
+    /**
+     * 文件边界信息(用于追踪每个资料类别对应的节范围)
+     */
+    private static class FileRange {
+        int startSectionIndex;  // 资料类别开始的节索引
+        int endSectionIndex;    // 资料类别结束的节索引
+        String fileName;        // 资料类别名称
+
+        FileRange(int startSectionIndex, String fileName) {
+            this.startSectionIndex = startSectionIndex;
+            this.fileName = fileName;
+        }
+    }
+
     public static void main(String[] args) throws Exception {
         System.out.println("========== 卷宗生成测试(Aspose版)==========\n");
 
@@ -37,13 +51,21 @@ public class ArchiveTest {
         catalogFiles.add(basePath + "卷内目录20251225155255833.docx");
 
         // 正文文件(支持Word、Excel、PDF混合)
+        // 注意:使用 ##SUMMARY_BOUNDARY## 分隔不同的资料类别
         List<String> contentFiles = new ArrayList<>();
-        // Excel文件
+
+        // 资料类别1:成本监审表系列
         contentFiles.add(basePath + "小店区-幼儿教育成本监审表(封面)_176664431290620251225143152602.xlsx");
         contentFiles.add(basePath + "小店区-幼儿教育成本监审表(固定资产) _176664457050920251225143610719.xlsx");
-        // 更多Word文件
+        contentFiles.add("##SUMMARY_BOUNDARY##");  // 类别分隔符
+
+        // 资料类别2:提取资料登记表
         contentFiles.add(basePath + "成本监审提取资料登记表20251225154211987.docx");
+        contentFiles.add("##SUMMARY_BOUNDARY##");  // 类别分隔符
+
+        // 资料类别3:报送资料
         contentFiles.add(basePath + "报送资料文件20251225143050824.docx");
+        // 最后一个类别不需要分隔符
 
         // 封底文件
         List<String> backCoverFiles = new ArrayList<>();
@@ -104,15 +126,54 @@ public class ArchiveTest {
         // 记录正文开始的节索引
         int contentStartSectionIndex = mainDoc.getSections().getCount();
 
-        // 3. 合并正文(有页码节)
+        // 3. 合并正文(有页码节)- 追踪每个资料类别的节范围
+        List<FileRange> contentFileRanges = new ArrayList<>();
         if (contentFiles != null && !contentFiles.isEmpty()) {
             System.out.println("\n3. 合并正文(有页码)...");
+
+            int summaryStartSection = -1;
+            int summaryIndex = 1;
+            String currentSummaryName = "资料类别" + summaryIndex;
+
             for (String filePath : contentFiles) {
+                // 检查是否是分隔符
+                if ("##SUMMARY_BOUNDARY##".equals(filePath)) {
+                    // 遇到分隔符,保存当前 summary 的范围
+                    if (summaryStartSection >= 0) {
+                        int summaryEndSection = mainDoc.getSections().getCount() - 1;
+                        FileRange fileRange = new FileRange(summaryStartSection, currentSummaryName);
+                        fileRange.endSectionIndex = summaryEndSection;
+                        contentFileRanges.add(fileRange);
+                        System.out.println("   ✓ 资料类别 " + summaryIndex + " 完成,节范围:" + summaryStartSection + "-" + summaryEndSection);
+                    }
+                    // 重置,准备下一个 summary
+                    summaryStartSection = -1;
+                    summaryIndex++;
+                    currentSummaryName = "资料类别" + summaryIndex;
+                    continue;
+                }
+
+                // 处理普通文件
                 if (filePath != null && new File(filePath).exists()) {
                     System.out.println("   ✓ " + extractFileName(filePath));
+
+                    // 如果是当前 summary 的第一个文件,记录起始节索引
+                    if (summaryStartSection < 0) {
+                        summaryStartSection = mainDoc.getSections().getCount();
+                    }
+
                     isFirst = appendDocument(mainDoc, filePath, isFirst, SectionStart.NEW_PAGE);
                 }
             }
+
+            // 处理最后一个 summary(如果没有遇到分隔符)
+            if (summaryStartSection >= 0) {
+                int summaryEndSection = mainDoc.getSections().getCount() - 1;
+                FileRange fileRange = new FileRange(summaryStartSection, currentSummaryName);
+                fileRange.endSectionIndex = summaryEndSection;
+                contentFileRanges.add(fileRange);
+                System.out.println("   ✓ 资料类别 " + summaryIndex + " 完成,节范围:" + summaryStartSection + "-" + summaryEndSection);
+            }
         }
 
         // 记录正文结束的节索引
@@ -129,11 +190,9 @@ public class ArchiveTest {
             }
         }
 
-        // 5. 计算正文总页数并设置页码
-        System.out.println("\n5. 设置页码...");
-        int contentTotalPages = calculateContentPages(mainDoc, contentStartSectionIndex, contentEndSectionIndex);
-        System.out.println("   正文总页数: " + contentTotalPages);
-        setupPageNumbers(mainDoc, contentStartSectionIndex, contentEndSectionIndex, contentTotalPages);
+        // 5. 设置页码(页眉全局连续,页脚按资料类别重置)
+        System.out.println("\n5. 设置页码(每个资料类别独立编页码)...");
+        setupPageNumbersPerFile(mainDoc, contentFileRanges);
 
         // 保存文档
         File outputFile = new File(outputPath);
@@ -145,54 +204,6 @@ public class ArchiveTest {
     }
 
     /**
-     * 计算正文部分的总页数
-     */
-    private static int calculateContentPages(Document doc, int contentStartSectionIndex, int contentEndSectionIndex) throws Exception {
-        // 更新文档布局以获取准确的页数
-        doc.updatePageLayout();
-
-        int totalPages = 0;
-        SectionCollection sections = doc.getSections();
-
-        for (int i = contentStartSectionIndex; i <= contentEndSectionIndex && i < sections.getCount(); i++) {
-            Section section = sections.get(i);
-            // 获取该节的页数
-            int sectionPages = doc.getPageCount();  // 这是整个文档的页数,需要另外计算
-        }
-
-        // 使用LayoutCollector获取精确页数
-        LayoutCollector collector = new LayoutCollector(doc);
-        int contentStartPage = Integer.MAX_VALUE;
-        int contentEndPage = 0;
-
-        for (int i = contentStartSectionIndex; i <= contentEndSectionIndex && i < sections.getCount(); i++) {
-            Section section = sections.get(i);
-            // 获取节的第一个段落所在页
-            Node firstNode = section.getBody().getFirstChild();
-            Node lastNode = section.getBody().getLastChild();
-
-            if (firstNode != null) {
-                int startPage = collector.getStartPageIndex(firstNode);
-                if (startPage > 0 && startPage < contentStartPage) {
-                    contentStartPage = startPage;
-                }
-            }
-            if (lastNode != null) {
-                int endPage = collector.getEndPageIndex(lastNode);
-                if (endPage > contentEndPage) {
-                    contentEndPage = endPage;
-                }
-            }
-        }
-
-        if (contentStartPage != Integer.MAX_VALUE && contentEndPage > 0) {
-            totalPages = contentEndPage - contentStartPage + 1;
-        }
-
-        return totalPages;
-    }
-
-    /**
      * 追加文档(自动识别格式)
      */
     private static boolean appendDocument(Document mainDoc, String filePath, boolean isFirst, int sectionStart) throws Exception {
@@ -249,8 +260,16 @@ public class ArchiveTest {
         // 先转为PDF(保持格式)
         ByteArrayOutputStream pdfStream = new ByteArrayOutputStream();
         com.aspose.cells.PdfSaveOptions pdfOptions = new com.aspose.cells.PdfSaveOptions();
-        pdfOptions.setOnePagePerSheet(false);  // 允许多页
-        pdfOptions.setAllColumnsInOnePagePerSheet(true);  // 所有列在一页
+        pdfOptions.setOnePagePerSheet(false);  // 允许多页(垂直方向)
+        pdfOptions.setAllColumnsInOnePagePerSheet(false);  // 改为false:允许列也分页,避免压缩导致看不清
+
+        // 设置打印质量
+        pdfOptions.setOptimizationType(com.aspose.cells.PdfOptimizationType.STANDARD);
+
+        // 设置页面适配方式(如果表格太宽,自动缩小到合适大小)
+        worksheet.getPageSetup().setFitToPagesWide(1);  // 宽度适配为1页
+        worksheet.getPageSetup().setFitToPagesTall(0);   // 高度不限制,自动分页
+
         workbook.save(pdfStream, pdfOptions);
 
         // PDF转Word
@@ -358,60 +377,69 @@ public class ArchiveTest {
     }
 
     /**
-     * 设置页码(只有正文部分显示页码
+     * 设置页码(页眉全局连续,页脚按资料类别重置
      *
-     * @param doc                     文档
-     * @param contentStartSectionIndex 正文开始的节索引
-     * @param contentEndSectionIndex   正文结束的节索引
-     * @param contentTotalPages        正文总页数
+     * @param doc               文档
+     * @param contentFileRanges 正文资料类别的节范围列表
      */
-    private static void setupPageNumbers(Document doc, int contentStartSectionIndex, int contentEndSectionIndex, int contentTotalPages) throws Exception {
-        SectionCollection sections = doc.getSections();
+    private static void setupPageNumbersPerFile(Document doc, List<FileRange> contentFileRanges) throws Exception {
+        if (contentFileRanges == null || contentFileRanges.isEmpty()) {
+            System.out.println("   [警告] 没有正文文件,跳过页码设置");
+            return;
+        }
 
-        for (int i = 0; i < sections.getCount(); i++) {
-            Section section = sections.get(i);
-            HeaderFooterCollection headerFooters = section.getHeadersFooters();
+        // 先更新文档布局,确保页码计算准确
+        doc.updatePageLayout();
 
-            // 取消链接到上一节(使每节页眉页脚独立)
-            headerFooters.linkToPrevious(false);
+        SectionCollection sections = doc.getSections();
+        LayoutCollector collector = new LayoutCollector(doc);
 
-            // 清除现有页脚
-            HeaderFooter existingFooter = headerFooters.getByHeaderFooterType(HeaderFooterType.FOOTER_PRIMARY);
-            if (existingFooter != null) {
-                existingFooter.remove();
-            }
+        // 计算每个资料类别的页数和起始页码(全局)
+        for (FileRange fileRange : contentFileRanges) {
+            int summaryPageCount = calculateFilePages(doc, collector, fileRange.startSectionIndex, fileRange.endSectionIndex);
+            System.out.println("   资料类别 " + fileRange.fileName + " 页数: " + summaryPageCount);
 
-            // 清除现有页眉
-            HeaderFooter existingHeader = headerFooters.getByHeaderFooterType(HeaderFooterType.HEADER_PRIMARY);
-            if (existingHeader != null) {
-                existingHeader.remove();
+            // 获取该资料类别的全局起始页码
+            Section firstSection = sections.get(fileRange.startSectionIndex);
+            Node firstNode = firstSection.getBody().getFirstChild();
+            int globalStartPage = 1;
+            if (firstNode != null) {
+                globalStartPage = collector.getStartPageIndex(firstNode);
             }
 
-            // 只有正文部分添加页码和页眉编号
-            if (i >= contentStartSectionIndex && i <= contentEndSectionIndex) {
-                // 页码设置:只有正文第一节从1开始,后续节继续累加
-                if (i == contentStartSectionIndex) {
-                    // 正文第一节:页码从1开始
-                    section.getPageSetup().setRestartPageNumbering(true);
-                    section.getPageSetup().setPageStartingNumber(1);
-                } else {
-                    // 正文后续节:页码继续累加,不重新开始
-                    section.getPageSetup().setRestartPageNumbering(false);
+            // 为这个资料类别的所有节设置页码
+            for (int i = fileRange.startSectionIndex; i <= fileRange.endSectionIndex && i < sections.getCount(); i++) {
+                Section section = sections.get(i);
+                HeaderFooterCollection headerFooters = section.getHeadersFooters();
+
+                // 取消链接到上一节(使每节页眉页脚独立)
+                headerFooters.linkToPrevious(false);
+
+                // 清除现有页脚
+                HeaderFooter existingFooter = headerFooters.getByHeaderFooterType(HeaderFooterType.FOOTER_PRIMARY);
+                if (existingFooter != null) {
+                    existingFooter.remove();
+                }
+
+                // 清除现有页眉
+                HeaderFooter existingHeader = headerFooters.getByHeaderFooterType(HeaderFooterType.HEADER_PRIMARY);
+                if (existingHeader != null) {
+                    existingHeader.remove();
                 }
 
-                // ========== 创建页眉(右上角编号 001、002、003...)==========
+                // 页码设置:所有节都不重置页码(保持全局连续)
+                section.getPageSetup().setRestartPageNumbering(false);
+
+                // ========== 创建页眉(右上角编号 001、002、003...全局连续)==========
                 HeaderFooter header = new HeaderFooter(doc, HeaderFooterType.HEADER_PRIMARY);
                 headerFooters.add(header);
 
                 // 创建页眉段落(右对齐)
                 Paragraph headerPara = new Paragraph(doc);
                 headerPara.getParagraphFormat().setAlignment(ParagraphAlignment.RIGHT);
-                // 设置右缩进,增加与右侧边距的距离
                 headerPara.getParagraphFormat().setRightIndent(20);
-                // 去掉页眉下边框线
                 headerPara.getParagraphFormat().getBorders().getBottom().setLineStyle(LineStyle.NONE);
 
-                // 先将段落添加到header中
                 header.appendChild(headerPara);
 
                 // 使用DocumentBuilder插入带格式的页码字段
@@ -423,40 +451,50 @@ public class ArchiveTest {
                 builder.getFont().setSize(12);
                 builder.getFont().setBold(true);
 
-                // 插入带格式的PAGE字段,格式为3位数字
+                // 插入全局页码(PAGE字段自动显示全局连续页码)
                 builder.insertField("PAGE \\# \"000\"", "001");
 
-                // 设置页眉距离
                 section.getPageSetup().setHeaderDistance(20);
 
                 // ========== 创建页脚(第 X 页 / 共 Y 页)==========
                 HeaderFooter footer = new HeaderFooter(doc, HeaderFooterType.FOOTER_PRIMARY);
                 headerFooters.add(footer);
 
-                // 创建页脚段落
                 Paragraph footerPara = new Paragraph(doc);
                 footerPara.getParagraphFormat().setAlignment(ParagraphAlignment.CENTER);
                 footerPara.getParagraphFormat().setSpaceBefore(12);
 
-                // 设置页脚距离页面底部的距离
                 section.getPageSetup().setFooterDistance(30);
 
-                // 添加页码文本:第 X 页 / 共 Y 页
+                // 添加页码文本:第 X 页 / 共 Y 页(X是相对页码,Y是当前类别总页数)
                 Run run1 = new Run(doc, "第 ");
                 run1.getFont().setName("宋体");
                 run1.getFont().setSize(10.5);
                 footerPara.appendChild(run1);
 
-                // 当前页码字段
-                footerPara.appendField(FieldType.FIELD_PAGE, true);
+                // 最终方案:使用固定文本(预先计算好的页码)
+                // 先获取当前节在资料类别中的相对位置
+                Section currentSection = sections.get(i);
+                Node currentFirstNode = currentSection.getBody().getFirstChild();
+                int currentGlobalPage = 1;
+                if (currentFirstNode != null) {
+                    currentGlobalPage = collector.getStartPageIndex(currentFirstNode);
+                }
+                int relativePage = currentGlobalPage - globalStartPage + 1;
+
+                // 直接插入计算好的页码(固定文本)
+                Run pageNumRun = new Run(doc, String.valueOf(relativePage));
+                pageNumRun.getFont().setName("宋体");
+                pageNumRun.getFont().setSize(10.5);
+                footerPara.appendChild(pageNumRun);
 
                 Run run2 = new Run(doc, " 页 / 共 ");
                 run2.getFont().setName("宋体");
                 run2.getFont().setSize(10.5);
                 footerPara.appendChild(run2);
 
-                // 使用计算出的正文总页数(固定值)
-                Run runTotal = new Run(doc, String.valueOf(contentTotalPages));
+                // 使用当前资料类别的总页数(固定值)
+                Run runTotal = new Run(doc, String.valueOf(summaryPageCount));
                 runTotal.getFont().setName("宋体");
                 runTotal.getFont().setSize(10.5);
                 footerPara.appendChild(runTotal);
@@ -467,8 +505,35 @@ public class ArchiveTest {
                 footerPara.appendChild(run3);
 
                 footer.appendChild(footerPara);
-            } else {
-                // 封面、目录、封底:不添加页眉页脚,且不重新开始页码
+            }
+        }
+
+        // 清除非正文部分的页眉页脚
+        for (int i = 0; i < sections.getCount(); i++) {
+            boolean isContentSection = false;
+            for (FileRange fileRange : contentFileRanges) {
+                if (i >= fileRange.startSectionIndex && i <= fileRange.endSectionIndex) {
+                    isContentSection = true;
+                    break;
+                }
+            }
+
+            if (!isContentSection) {
+                Section section = sections.get(i);
+                HeaderFooterCollection headerFooters = section.getHeadersFooters();
+                headerFooters.linkToPrevious(false);
+
+                // 清除页眉页脚
+                HeaderFooter existingFooter = headerFooters.getByHeaderFooterType(HeaderFooterType.FOOTER_PRIMARY);
+                if (existingFooter != null) {
+                    existingFooter.remove();
+                }
+                HeaderFooter existingHeader = headerFooters.getByHeaderFooterType(HeaderFooterType.HEADER_PRIMARY);
+                if (existingHeader != null) {
+                    existingHeader.remove();
+                }
+
+                // 不重新开始页码
                 section.getPageSetup().setRestartPageNumbering(false);
             }
         }
@@ -478,6 +543,42 @@ public class ArchiveTest {
     }
 
     /**
+     * 计算指定节范围的页数(用于单个资料类别)
+     */
+    private static int calculateFilePages(Document doc, LayoutCollector collector, int startSectionIndex, int endSectionIndex) throws Exception {
+        SectionCollection sections = doc.getSections();
+
+        int fileStartPage = Integer.MAX_VALUE;
+        int fileEndPage = 0;
+
+        for (int i = startSectionIndex; i <= endSectionIndex && i < sections.getCount(); i++) {
+            Section section = sections.get(i);
+            Node firstNode = section.getBody().getFirstChild();
+            Node lastNode = section.getBody().getLastChild();
+
+            if (firstNode != null) {
+                int startPage = collector.getStartPageIndex(firstNode);
+                if (startPage > 0 && startPage < fileStartPage) {
+                    fileStartPage = startPage;
+                }
+            }
+            if (lastNode != null) {
+                int endPage = collector.getEndPageIndex(lastNode);
+                if (endPage > fileEndPage) {
+                    fileEndPage = endPage;
+                }
+            }
+        }
+
+        int pageCount = 0;
+        if (fileStartPage != Integer.MAX_VALUE && fileEndPage > 0) {
+            pageCount = fileEndPage - fileStartPage + 1;
+        }
+
+        return pageCount;
+    }
+
+    /**
      * 提取文件名
      */
     private static String extractFileName(String url) {

+ 199 - 54
assistMg/src/main/java/com/hotent/project/service/AsposeArchiveService.java

@@ -27,6 +27,20 @@ public class AsposeArchiveService {
     private static final Logger logger = LoggerFactory.getLogger(AsposeArchiveService.class);
 
     /**
+     * 文件边界信息(用于追踪每个文件对应的节范围)
+     */
+    private static class FileRange {
+        int startSectionIndex;  // 文件开始的节索引
+        int endSectionIndex;    // 文件结束的节索引
+        String fileName;        // 文件名
+
+        FileRange(int startSectionIndex, String fileName) {
+            this.startSectionIndex = startSectionIndex;
+            this.fileName = fileName;
+        }
+    }
+
+    /**
      * 合并多个文档(支持Word、Excel、PDF)
      *
      * @param coverFiles     封面文件列表
@@ -71,15 +85,54 @@ public class AsposeArchiveService {
         // 记录正文开始的节索引
         int contentStartSectionIndex = mainDoc.getSections().getCount();
 
-        // 3. 合并正文(有页码节)
+        // 3. 合并正文(有页码节)- 追踪每个资料类别(summary)的节范围
+        List<FileRange> contentFileRanges = new java.util.ArrayList<>();
         if (contentFiles != null && !contentFiles.isEmpty()) {
             logger.info("3. 合并正文(有页码)...");
+
+            int summaryStartSection = -1;
+            int summaryIndex = 1;
+            String currentSummaryName = "资料类别" + summaryIndex;
+
             for (String filePath : contentFiles) {
+                // 检查是否是分隔符
+                if ("##SUMMARY_BOUNDARY##".equals(filePath)) {
+                    // 遇到分隔符,保存当前 summary 的范围
+                    if (summaryStartSection >= 0) {
+                        int summaryEndSection = mainDoc.getSections().getCount() - 1;
+                        FileRange fileRange = new FileRange(summaryStartSection, currentSummaryName);
+                        fileRange.endSectionIndex = summaryEndSection;
+                        contentFileRanges.add(fileRange);
+                        logger.info("   ✓ 资料类别 {} 完成,节范围:{}-{}", summaryIndex, summaryStartSection, summaryEndSection);
+                    }
+                    // 重置,准备下一个 summary
+                    summaryStartSection = -1;
+                    summaryIndex++;
+                    currentSummaryName = "资料类别" + summaryIndex;
+                    continue;
+                }
+
+                // 处理普通文件
                 if (filePath != null && new File(filePath).exists()) {
                     logger.info("   ✓ {}", extractFileName(filePath));
+
+                    // 如果是当前 summary 的第一个文件,记录起始节索引
+                    if (summaryStartSection < 0) {
+                        summaryStartSection = mainDoc.getSections().getCount();
+                    }
+
                     isFirst = appendDocument(mainDoc, filePath, isFirst, SectionStart.NEW_PAGE);
                 }
             }
+
+            // 处理最后一个 summary(如果没有遇到分隔符)
+            if (summaryStartSection >= 0) {
+                int summaryEndSection = mainDoc.getSections().getCount() - 1;
+                FileRange fileRange = new FileRange(summaryStartSection, currentSummaryName);
+                fileRange.endSectionIndex = summaryEndSection;
+                contentFileRanges.add(fileRange);
+                logger.info("   ✓ 资料类别 {} 完成,节范围:{}-{}", summaryIndex, summaryStartSection, summaryEndSection);
+            }
         }
 
         // 记录正文结束的节索引
@@ -96,11 +149,9 @@ public class AsposeArchiveService {
             }
         }
 
-        // 5. 计算正文总页数并设置页码
-        logger.info("5. 设置页码...");
-        int contentTotalPages = calculateContentPages(mainDoc, contentStartSectionIndex, contentEndSectionIndex);
-        logger.info("   正文总页数: {}", contentTotalPages);
-        setupPageNumbers(mainDoc, contentStartSectionIndex, contentEndSectionIndex, contentTotalPages);
+        // 5. 设置页码(每个文件独立编页码)
+        logger.info("5. 设置页码(每个文件独立编码)...");
+        setupPageNumbersPerFile(mainDoc, contentFileRanges);
 
         // 保存文档
         File outputFile = new File(outputPath);
@@ -210,8 +261,16 @@ public class AsposeArchiveService {
         // 先转为PDF(保持格式)
         ByteArrayOutputStream pdfStream = new ByteArrayOutputStream();
         com.aspose.cells.PdfSaveOptions pdfOptions = new com.aspose.cells.PdfSaveOptions();
-        pdfOptions.setOnePagePerSheet(false);  // 允许多页
-        pdfOptions.setAllColumnsInOnePagePerSheet(true);  // 所有列在一页
+        pdfOptions.setOnePagePerSheet(false);  // 允许多页(垂直方向)
+        pdfOptions.setAllColumnsInOnePagePerSheet(false);  // 改为false:允许列也分页,避免压缩导致看不清
+
+        // 设置打印质量
+        pdfOptions.setOptimizationType(com.aspose.cells.PdfOptimizationType.STANDARD);
+
+        // 设置页面适配方式(如果表格太宽,自动缩小到合适大小)
+        worksheet.getPageSetup().setFitToPagesWide(1);  // 宽度适配为1页
+        worksheet.getPageSetup().setFitToPagesTall(0);   // 高度不限制,自动分页
+
         workbook.save(pdfStream, pdfOptions);
 
         // PDF转Word
@@ -317,60 +376,69 @@ public class AsposeArchiveService {
     }
 
     /**
-     * 设置页码(只有正文部分显示页码
+     * 设置页码(页眉全局连续,页脚按资料类别重置
      *
-     * @param doc                      文档
-     * @param contentStartSectionIndex 正文开始的节索引
-     * @param contentEndSectionIndex   正文结束的节索引
-     * @param contentTotalPages        正文总页数
+     * @param doc               文档
+     * @param contentFileRanges 正文资料类别的节范围列表
      */
-    private void setupPageNumbers(Document doc, int contentStartSectionIndex, int contentEndSectionIndex, int contentTotalPages) throws Exception {
-        SectionCollection sections = doc.getSections();
+    private void setupPageNumbersPerFile(Document doc, List<FileRange> contentFileRanges) throws Exception {
+        if (contentFileRanges == null || contentFileRanges.isEmpty()) {
+            logger.warn("没有正文文件,跳过页码设置");
+            return;
+        }
 
-        for (int i = 0; i < sections.getCount(); i++) {
-            Section section = sections.get(i);
-            HeaderFooterCollection headerFooters = section.getHeadersFooters();
+        // 先更新文档布局,确保页码计算准确
+        doc.updatePageLayout();
 
-            // 取消链接到上一节(使每节页眉页脚独立)
-            headerFooters.linkToPrevious(false);
+        SectionCollection sections = doc.getSections();
+        LayoutCollector collector = new LayoutCollector(doc);
 
-            // 清除现有页脚
-            HeaderFooter existingFooter = headerFooters.getByHeaderFooterType(HeaderFooterType.FOOTER_PRIMARY);
-            if (existingFooter != null) {
-                existingFooter.remove();
-            }
+        // 计算每个资料类别的页数和起始页码(全局)
+        for (FileRange fileRange : contentFileRanges) {
+            int summaryPageCount = calculateFilePages(doc, collector, fileRange.startSectionIndex, fileRange.endSectionIndex);
+            logger.info("   资料类别 {} 页数: {}", fileRange.fileName, summaryPageCount);
 
-            // 清除现有页眉
-            HeaderFooter existingHeader = headerFooters.getByHeaderFooterType(HeaderFooterType.HEADER_PRIMARY);
-            if (existingHeader != null) {
-                existingHeader.remove();
+            // 获取该资料类别的全局起始页码
+            Section firstSection = sections.get(fileRange.startSectionIndex);
+            Node firstNode = firstSection.getBody().getFirstChild();
+            int globalStartPage = 1;
+            if (firstNode != null) {
+                globalStartPage = collector.getStartPageIndex(firstNode);
             }
 
-            // 只有正文部分添加页码和页眉编号
-            if (i >= contentStartSectionIndex && i <= contentEndSectionIndex) {
-                // 页码设置:只有正文第一节从1开始,后续节继续累加
-                if (i == contentStartSectionIndex) {
-                    // 正文第一节:页码从1开始
-                    section.getPageSetup().setRestartPageNumbering(true);
-                    section.getPageSetup().setPageStartingNumber(1);
-                } else {
-                    // 正文后续节:页码继续累加,不重新开始
-                    section.getPageSetup().setRestartPageNumbering(false);
+            // 为这个资料类别的所有节设置页码
+            for (int i = fileRange.startSectionIndex; i <= fileRange.endSectionIndex && i < sections.getCount(); i++) {
+                Section section = sections.get(i);
+                HeaderFooterCollection headerFooters = section.getHeadersFooters();
+
+                // 取消链接到上一节(使每节页眉页脚独立)
+                headerFooters.linkToPrevious(false);
+
+                // 清除现有页脚
+                HeaderFooter existingFooter = headerFooters.getByHeaderFooterType(HeaderFooterType.FOOTER_PRIMARY);
+                if (existingFooter != null) {
+                    existingFooter.remove();
+                }
+
+                // 清除现有页眉
+                HeaderFooter existingHeader = headerFooters.getByHeaderFooterType(HeaderFooterType.HEADER_PRIMARY);
+                if (existingHeader != null) {
+                    existingHeader.remove();
                 }
 
-                // ========== 创建页眉(右上角编号 001、002、003...)==========
+                // 页码设置:所有节都不重置页码(保持全局连续)
+                section.getPageSetup().setRestartPageNumbering(false);
+
+                // ========== 创建页眉(右上角编号 001、002、003...全局连续)==========
                 HeaderFooter header = new HeaderFooter(doc, HeaderFooterType.HEADER_PRIMARY);
                 headerFooters.add(header);
 
                 // 创建页眉段落(右对齐)
                 Paragraph headerPara = new Paragraph(doc);
                 headerPara.getParagraphFormat().setAlignment(ParagraphAlignment.RIGHT);
-                // 设置右缩进,增加与右侧边距的距离
                 headerPara.getParagraphFormat().setRightIndent(20);
-                // 去掉页眉下边框线
                 headerPara.getParagraphFormat().getBorders().getBottom().setLineStyle(LineStyle.NONE);
 
-                // 先将段落添加到header中
                 header.appendChild(headerPara);
 
                 // 使用DocumentBuilder插入带格式的页码字段
@@ -382,40 +450,54 @@ public class AsposeArchiveService {
                 builder.getFont().setSize(12);
                 builder.getFont().setBold(true);
 
-                // 插入带格式的PAGE字段,格式为3位数字
+                // 插入全局页码(PAGE字段自动显示全局连续页码)
                 builder.insertField("PAGE \\# \"000\"", "001");
 
-                // 设置页眉距离
                 section.getPageSetup().setHeaderDistance(20);
 
                 // ========== 创建页脚(第 X 页 / 共 Y 页)==========
                 HeaderFooter footer = new HeaderFooter(doc, HeaderFooterType.FOOTER_PRIMARY);
                 headerFooters.add(footer);
 
-                // 创建页脚段落
                 Paragraph footerPara = new Paragraph(doc);
                 footerPara.getParagraphFormat().setAlignment(ParagraphAlignment.CENTER);
                 footerPara.getParagraphFormat().setSpaceBefore(12);
 
-                // 设置页脚距离页面底部的距离
                 section.getPageSetup().setFooterDistance(30);
 
-                // 添加页码文本:第 X 页 / 共 Y 页
+                // 添加页码文本:第 X 页 / 共 Y 页(X是相对页码,Y是当前类别总页数)
                 Run run1 = new Run(doc, "第 ");
                 run1.getFont().setName("宋体");
                 run1.getFont().setSize(10.5);
                 footerPara.appendChild(run1);
 
-                // 当前页码字段
-                footerPara.appendField(FieldType.FIELD_PAGE, true);
+                // 方案改进:不使用公式字段,直接使用每个节自己的 SECTIONPAGES
+                // 因为我们为每个资料类别设置了 RestartPageNumbering
+                // 但这样会导致页眉也重置...所以需要特殊处理
+
+                // 最终方案:使用固定文本,通过后处理替换
+                // 先获取当前节在资料类别中的相对位置
+                Section currentSection = sections.get(i);
+                Node currentFirstNode = currentSection.getBody().getFirstChild();
+                int currentGlobalPage = 1;
+                if (currentFirstNode != null) {
+                    currentGlobalPage = collector.getStartPageIndex(currentFirstNode);
+                }
+                int relativePage = currentGlobalPage - globalStartPage + 1;
+
+                // 直接插入计算好的页码(固定文本)
+                Run pageNumRun = new Run(doc, String.valueOf(relativePage));
+                pageNumRun.getFont().setName("宋体");
+                pageNumRun.getFont().setSize(10.5);
+                footerPara.appendChild(pageNumRun);
 
                 Run run2 = new Run(doc, " 页 / 共 ");
                 run2.getFont().setName("宋体");
                 run2.getFont().setSize(10.5);
                 footerPara.appendChild(run2);
 
-                // 使用计算出的正文总页数(固定值)
-                Run runTotal = new Run(doc, String.valueOf(contentTotalPages));
+                // 使用当前资料类别的总页数(固定值)
+                Run runTotal = new Run(doc, String.valueOf(summaryPageCount));
                 runTotal.getFont().setName("宋体");
                 runTotal.getFont().setSize(10.5);
                 footerPara.appendChild(runTotal);
@@ -426,8 +508,35 @@ public class AsposeArchiveService {
                 footerPara.appendChild(run3);
 
                 footer.appendChild(footerPara);
-            } else {
-                // 封面、目录、封底:不添加页眉页脚,且不重新开始页码
+            }
+        }
+
+        // 清除非正文部分的页眉页脚
+        for (int i = 0; i < sections.getCount(); i++) {
+            boolean isContentSection = false;
+            for (FileRange fileRange : contentFileRanges) {
+                if (i >= fileRange.startSectionIndex && i <= fileRange.endSectionIndex) {
+                    isContentSection = true;
+                    break;
+                }
+            }
+
+            if (!isContentSection) {
+                Section section = sections.get(i);
+                HeaderFooterCollection headerFooters = section.getHeadersFooters();
+                headerFooters.linkToPrevious(false);
+
+                // 清除页眉页脚
+                HeaderFooter existingFooter = headerFooters.getByHeaderFooterType(HeaderFooterType.FOOTER_PRIMARY);
+                if (existingFooter != null) {
+                    existingFooter.remove();
+                }
+                HeaderFooter existingHeader = headerFooters.getByHeaderFooterType(HeaderFooterType.HEADER_PRIMARY);
+                if (existingHeader != null) {
+                    existingHeader.remove();
+                }
+
+                // 不重新开始页码
                 section.getPageSetup().setRestartPageNumbering(false);
             }
         }
@@ -437,6 +546,42 @@ public class AsposeArchiveService {
     }
 
     /**
+     * 计算指定节范围的页数(用于单个文件)
+     */
+    private int calculateFilePages(Document doc, LayoutCollector collector, int startSectionIndex, int endSectionIndex) throws Exception {
+        SectionCollection sections = doc.getSections();
+
+        int fileStartPage = Integer.MAX_VALUE;
+        int fileEndPage = 0;
+
+        for (int i = startSectionIndex; i <= endSectionIndex && i < sections.getCount(); i++) {
+            Section section = sections.get(i);
+            Node firstNode = section.getBody().getFirstChild();
+            Node lastNode = section.getBody().getLastChild();
+
+            if (firstNode != null) {
+                int startPage = collector.getStartPageIndex(firstNode);
+                if (startPage > 0 && startPage < fileStartPage) {
+                    fileStartPage = startPage;
+                }
+            }
+            if (lastNode != null) {
+                int endPage = collector.getEndPageIndex(lastNode);
+                if (endPage > fileEndPage) {
+                    fileEndPage = endPage;
+                }
+            }
+        }
+
+        int pageCount = 0;
+        if (fileStartPage != Integer.MAX_VALUE && fileEndPage > 0) {
+            pageCount = fileEndPage - fileStartPage + 1;
+        }
+
+        return pageCount;
+    }
+
+    /**
      * 计算文件页数(支持Word、Excel、PDF)
      *
      * @param filePath 文件路径

+ 21 - 1
assistMg/src/main/java/com/hotent/project/service/AsyncMaterialSummaryService.java

@@ -879,27 +879,47 @@ public class AsyncMaterialSummaryService {
 
             // 对正文按materialOrderNum排序
             // 重新收集正文,确保按materialOrderNum排序
+            // 注意:这里改为按 summary 分组,每个 summary(资料类别)作为一个独立的"文件组"
             contentFiles.clear();
             List<CostProjectTaskMaterialSummary> contentSummaries = allSummaryList.stream()
                     .filter(s -> s.getMaterialOrderNum() != null && s.getMaterialOrderNum() > 0)
                     .sorted(Comparator.comparing(CostProjectTaskMaterialSummary::getMaterialOrderNum))
                     .collect(Collectors.toList());
 
+            // 使用特殊标记分隔不同的 summary(资料类别)
+            // 格式:每个 summary 的多个 detail 文件连续添加,然后用 null 作为分隔符
             for (CostProjectTaskMaterialSummary summary : contentSummaries) {
                 List<CostProjectTaskMaterialSummaryDetail> detailList = summary.getDetailList();
                 if (detailList != null && !detailList.isEmpty()) {
                     detailList.sort(Comparator.comparing(d -> d.getOrderNum() != null ? d.getOrderNum() : 0));
+
+                    boolean hasValidFile = false;
+                    List<String> summaryFiles = new java.util.ArrayList<>();
+
                     for (CostProjectTaskMaterialSummaryDetail detail : detailList) {
                         if (StringUtil.isNotEmpty(detail.getAttachmentUrl())) {
                             String localPath = convertToLocalPath(detail.getAttachmentUrl());
                             if (localPath != null && new java.io.File(localPath).exists()) {
-                                contentFiles.add(localPath);
+                                summaryFiles.add(localPath);
+                                hasValidFile = true;
                             }
                         }
                     }
+
+                    // 只有当该 summary 有有效文件时才添加
+                    if (hasValidFile) {
+                        contentFiles.addAll(summaryFiles);
+                        // 添加分隔符标记(用于 AsposeArchiveService 识别不同 summary 的边界)
+                        contentFiles.add("##SUMMARY_BOUNDARY##");
+                    }
                 }
             }
 
+            // 移除最后一个分隔符(如果存在)
+            if (!contentFiles.isEmpty() && "##SUMMARY_BOUNDARY##".equals(contentFiles.get(contentFiles.size() - 1))) {
+                contentFiles.remove(contentFiles.size() - 1);
+            }
+
             // 生成输出文件路径
             String fileName = FileUploadUtil.generateFileName("卷宗_" + taskId + ".docx");
             String outputPath = FileUploadUtil.generateSavePath(

+ 39 - 44
assistMg/src/main/java/com/hotent/surveyinfo/controller/CostSurveyTemplateHeadersController.java

@@ -1,6 +1,7 @@
 package com.hotent.surveyinfo.controller;
 
 
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.hotent.base.annotation.ApiGroup;
 import com.hotent.base.constants.ApiGroupConsts;
 import com.hotent.base.util.PinyinUtil;
@@ -158,7 +159,7 @@ public class CostSurveyTemplateHeadersController extends BaseController<CostSurv
 
     /**
      * 批量新增或更新成本调查表头及指标项数据
-     * 动态处理 headersId 的生成和关联
+     * 逻辑:先全删再全增
      *
      * @param request 包含表头列表和指标项列表的请求对象
      * @return 操作结果
@@ -172,59 +173,55 @@ public class CostSurveyTemplateHeadersController extends BaseController<CostSurv
 
         List<CostSurveyTemplateHeaders> headersList = request.getHeadersList();
         List<CostSurveyTemplateItems> itemsList = request.getItemsList();
-        List<CostSurveyTemplateHeaders> deleteHeadersList = request.getDeleteheadersList();
 
         // 检查输入列表是否为空
-        if ((headersList == null || headersList.isEmpty()) &&
-                (itemsList == null || itemsList.isEmpty()) &&
-                (deleteHeadersList == null || deleteHeadersList.isEmpty())) {
+        if ((headersList == null || headersList.isEmpty()) && (itemsList == null || itemsList.isEmpty())) {
             return CommonResult.<String>error().message("批量操作失败,数据列表为空");
         }
 
-        IUser user = ContextUtil.getCurrentUser();
-
-        // 先处理表头数据,确保所有表头都有ID
+        // 获取 surveyTemplateId 和 versionId
+        String surveyTemplateId = null;
+        String versionId = null;
         if (headersList != null && !headersList.isEmpty()) {
-            for (CostSurveyTemplateHeaders header : headersList) {
-                boolean isHeaderCreate = StringUtil.isEmpty(header.getId());
-                header.setFieldEname(PinyinUtil.getPinyin(header.getFieldName()));
-
-                if (StringUtil.isEmpty(header.getId())) {
-
-                    header.setId(UUID.randomUUID().toString());
-                    header.setCreateBy(user.getAccount());
-                    header.setCreateTime(LocalDateTime.now());
-                    baseService.create(header);
-                } else {
-                    header.setUpdateBy(user.getAccount());
-                    header.setUpdateTime(LocalDateTime.now());
-                    baseService.update(header);
-                }
-
+            surveyTemplateId = headersList.get(0).getSurveyTemplateId();
+            versionId = headersList.get(0).getVersionId();
+        } else if (itemsList != null && !itemsList.isEmpty()) {
+            surveyTemplateId = itemsList.get(0).getSurveyTemplateId();
+            versionId = itemsList.get(0).getVersionId();
+        }
 
-            }
+        if (StringUtil.isEmpty(surveyTemplateId) || StringUtil.isEmpty(versionId)) {
+            return CommonResult.<String>error().message("surveyTemplateId或versionId不能为空");
         }
 
-        // 处理需要删除的表头
-        if (deleteHeadersList != null && !deleteHeadersList.isEmpty()) {
-            for (CostSurveyTemplateHeaders header : deleteHeadersList) {
-                if (StringUtil.isNotEmpty(header.getId())) {
-                    baseService.remove(header.getId());
+        IUser user = ContextUtil.getCurrentUser();
+        LocalDateTime now = LocalDateTime.now();
 
-                }
+        // 1. 先删除该模板版本下的所有表头和指标项
+        QueryWrapper<CostSurveyTemplateHeaders> wrapper = new QueryWrapper<>();
+        wrapper.eq("survey_template_id", surveyTemplateId)
+                .eq("version_id", versionId);
+        baseService.remove(wrapper);
+        costSurveyTemplateItemsManager.deleteBySurveyTemplateIdAndVersionId(surveyTemplateId, versionId);
+
+        // 2. 批量新增表头
+        if (headersList != null && !headersList.isEmpty()) {
+            for (CostSurveyTemplateHeaders header : headersList) {
+                header.setId(UUID.randomUUID().toString());
+                header.setFieldEname(PinyinUtil.getPinyin(header.getFieldName()));
+                header.setCreateBy(user.getAccount());
+                header.setCreateTime(now);
+                header.setUpdateBy(user.getAccount());
+                header.setUpdateTime(now);
             }
+            baseService.batchCreate(headersList);
         }
 
-        // 处理指标项数据,动态设置 headersId
+        // 3. 批量新增指标项
         if (itemsList != null && !itemsList.isEmpty()) {
-            if (itemsList.size() > 0) {
-                costSurveyTemplateItemsManager.deleteBySurveyTemplateIdAndVersionId(itemsList.get(0).getSurveyTemplateId(), itemsList.get(0).getVersionId());
-
-            }
-
             for (CostSurveyTemplateItems item : itemsList) {
-                // 检查是否已设置 headersId
-                if (StringUtil.isEmpty(item.getHeadersId())) {
+                // 通过字段名匹配表头ID
+                if (StringUtil.isEmpty(item.getHeadersId()) && headersList != null) {
                     item.setHeadersId(
                             headersList.stream()
                                     .filter(h -> h.getFieldName().equals(item.getRkey()))
@@ -235,13 +232,11 @@ public class CostSurveyTemplateHeadersController extends BaseController<CostSurv
                 }
                 item.setId(UUID.randomUUID().toString());
                 item.setCreateBy(user.getAccount());
-                item.setCreateTime(LocalDateTime.now());
+                item.setCreateTime(now);
                 item.setUpdateBy(user.getAccount());
-                item.setUpdateTime(LocalDateTime.now());
-                costSurveyTemplateItemsManager.create(item);
-
-
+                item.setUpdateTime(now);
             }
+            costSurveyTemplateItemsManager.saveBatch(itemsList);
         }
 
         return CommonResult.<String>ok().message("批量操作成功");

文件差异内容过多而无法显示
+ 484 - 0
hs_err_pid17008.log


文件差异内容过多而无法显示
+ 487 - 0
hs_err_pid28244.log


文件差异内容过多而无法显示
+ 485 - 0
hs_err_pid89992.log


部分文件因为文件数量过多而无法显示