Просмотр исходного кода

Merge branch 'master' of http://47.100.37.243:10191/quyueting/manHourHousekeeper

QuYueTing 5 дней назад
Родитель
Сommit
51ea86f20e

+ 17 - 10
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ProjectClosureApplierController.java

@@ -1,13 +1,14 @@
 package com.management.platform.controller;
 
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.management.platform.entity.ProjectClosureApplier;
 import com.management.platform.mapper.ProjectClosureApplierMapper;
+import com.management.platform.service.ProjectClosureApplierService;
 import com.management.platform.util.HttpRespMsg;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import java.util.List;
+import java.util.Map;
 
 /**
  * <p>
@@ -21,21 +22,18 @@ import java.util.List;
 @RequestMapping("/project-closure-applier")
 public class ProjectClosureApplierController {
 
+    @Resource
+    private ProjectClosureApplierService closureApplierService;
     @Resource
     private ProjectClosureApplierMapper closureApplierMapper;
 
     /**
-     * 获取审批人配置列表
+     * 获取审批人配置列表(当前启用版本)
      */
     @GetMapping("/list")
     public HttpRespMsg list(@RequestParam(required = false) Integer companyId) {
-        QueryWrapper<ProjectClosureApplier> wrapper = new QueryWrapper<>();
-        if (companyId != null && companyId > 0) {
-            wrapper.eq("company_id", companyId);
-        }
-        wrapper.orderByAsc("sort_order");
-        List<ProjectClosureApplier> list = closureApplierMapper.selectList(wrapper);
-        return HttpRespMsg.success(list);
+        List<Map<String, Object>> configList = closureApplierService.getConfigList(companyId);
+        return HttpRespMsg.success(configList);
     }
 
     /**
@@ -50,6 +48,15 @@ public class ProjectClosureApplierController {
         return HttpRespMsg.error("审批人配置不存在");
     }
 
+    /**
+     * 获取当前配置的版本信息
+     */
+    @GetMapping("/config-info")
+    public HttpRespMsg getConfigInfo(@RequestParam(required = false) Integer companyId) {
+        Integer configId = closureApplierService.getActiveConfigId(companyId);
+        return HttpRespMsg.success(configId);
+    }
+
     /**
      * 添加审批人配置
      */
@@ -113,4 +120,4 @@ public class ProjectClosureApplierController {
             return HttpRespMsg.error("删除失败:" + e.getMessage());
         }
     }
-}
+}

+ 2 - 3
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ProjectClosureApplyController.java

@@ -8,7 +8,6 @@ import com.management.platform.service.ProjectClosureApplierService;
 import com.management.platform.service.ProjectClosureApplyService;
 import com.management.platform.service.ProjectService;
 import com.management.platform.util.HttpRespMsg;
-import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
@@ -147,9 +146,9 @@ public class ProjectClosureApplyController {
      * 保存审批人配置(批量)
      */
     @PostMapping("/config/save")
-    @Transactional
-    public HttpRespMsg saveConfigList(Integer companyId,@RequestBody List<ProjectClosureApplier> configList) {
+    public HttpRespMsg saveConfigList(Integer companyId, @RequestBody List<ProjectClosureApplier> configList) {
         try {
+            // 保存新版本配置
             closureApplierService.saveConfig(companyId, configList);
             return HttpRespMsg.success("配置保存成功");
         } catch (Exception e) {

+ 6 - 0
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/ProjectClosureApplier.java

@@ -74,6 +74,12 @@ public class ProjectClosureApplier extends Model<ProjectClosureApplier> {
     @TableField("update_time")
     private LocalDateTime updateTime;
 
+    @TableField("config_id")
+    private Integer configId;
+
+    @TableField(exist = false)
+    private String nodeType;
+
 
     @Override
     protected Serializable pkVal() {

+ 12 - 3
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/ProjectClosureApply.java

@@ -69,6 +69,12 @@ public class ProjectClosureApply extends Model<ProjectClosureApply> {
     @TableField("current_step")
     private Integer currentStep;
 
+    /**
+     *
+     */
+    @TableField("config_id")
+    private Integer configId;
+
     /**
      * 当前审批人用户ID
      */
@@ -81,6 +87,12 @@ public class ProjectClosureApply extends Model<ProjectClosureApply> {
     @TableField("current_approver_name")
     private String currentApproverName;
 
+    /**
+     * 审批流程总步数(提交时记录,后续修改流程不影响历史数据)
+     */
+    @TableField("total_step")
+    private Integer totalStep;
+
     /**
      * 审批状态 0-待审批 1-审批中 2-已通过 3-已驳回
      */
@@ -109,9 +121,6 @@ public class ProjectClosureApply extends Model<ProjectClosureApply> {
     @TableField(exist = false)
     private String reason;
 
-    @TableField(exist = false)
-    private Integer totalStep;
-
     @TableField(exist = false)
     private String currentApplierName;
 

+ 11 - 29
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/ProjectClosureApplierService.java

@@ -1,56 +1,38 @@
 package com.management.platform.service;
 
-
-
 import com.management.platform.entity.ProjectClosureApplier;
-
 import com.baomidou.mybatisplus.extension.service.IService;
-
-
-
 import java.util.List;
-
 import java.util.Map;
 
-
-
 /**
-
  * <p>
-
  * 项目结项审批人设置表 服务类
-
  * </p>
-
  *
-
  * @author Seyason
-
  * @since 2026-05-12
-
  */
-
 public interface ProjectClosureApplierService extends IService<ProjectClosureApplier> {
 
-
-
     /**
-
-     * 获取审批流程配置列表(按企业)
-
+     * 获取审批流程配置列表(当前启用版本)
      */
-
     List<Map<String, Object>> getConfigList(Integer companyId);
 
-
-
     /**
+     * 保存审批流程配置(新建版本,旧版本保留)
+     */
+    void saveConfig(Integer companyId, List<ProjectClosureApplier> configSteps);
 
-     * 保存审批流程配置
-
+    /**
+     * 获取当前启用的配置版本ID
      */
+    Integer getActiveConfigId(Integer companyId);
 
-    void saveConfig(Integer companyId, List<ProjectClosureApplier> configSteps);
+    /**
+     * 根据配置版本ID获取审批流程节点列表
+     */
+    List<ProjectClosureApplier> getFlowByConfigId(Integer configId);
 
 }
-

+ 0 - 2
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/ProjectClosureApplyService.java

@@ -16,8 +16,6 @@ public interface ProjectClosureApplyService extends IService<ProjectClosureApply
 
     ProjectClosureApply submitApply(Integer projectId, String userId, String reason, List<String> attachmentIds);
 
-    List<ProjectClosureApplier> getApprovalFlow(Integer companyId);
-
     boolean approve(Integer applyId, String applierId, String comment, Integer action);
 
     Map<String, Object> getProgress(Integer applyId, String viewerUserId);

+ 129 - 110
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ProjectClosureApplierServiceImpl.java

@@ -1,201 +1,220 @@
 package com.management.platform.service.impl;
 
-
-
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-
 import com.management.platform.entity.ProjectClosureApplier;
-
+import com.management.platform.entity.ProjectClosureApply;
+import com.management.platform.entity.SysRole;
 import com.management.platform.entity.User;
-
 import com.management.platform.mapper.ProjectClosureApplierMapper;
-
+import com.management.platform.mapper.ProjectClosureApplyMapper;
+import com.management.platform.mapper.SysRoleMapper;
+import com.management.platform.mapper.UserMapper;
 import com.management.platform.service.PermissionService;
-
 import com.management.platform.service.ProjectClosureApplierService;
-
-import com.management.platform.service.UserService;
-
 import com.management.platform.util.HttpRespMsg;
-
 import org.springframework.stereotype.Service;
-
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
 
-
-
 import javax.annotation.Resource;
-
 import java.time.LocalDateTime;
-
 import java.util.ArrayList;
-
 import java.util.HashMap;
-
 import java.util.List;
-
 import java.util.Map;
-
-
+import java.util.stream.Collectors;
 
 /**
-
  * <p>
-
  * 项目结项审批人设置表 服务实现类
-
  * </p>
-
  *
-
  * @author Seyason
-
  * @since 2026-05-12
-
  */
-
 @Service
-
 public class ProjectClosureApplierServiceImpl extends ServiceImpl<ProjectClosureApplierMapper, ProjectClosureApplier> implements ProjectClosureApplierService {
 
-
-
     @Resource
-
-    PermissionService permissionService;
+    private PermissionService permissionService;
+    @Resource
+    private ProjectClosureApplyMapper projectClosureApplyMapper;
+    @Resource
+    private SysRoleMapper sysRoleMapper;
+    @Resource
+    private UserMapper userMapper;
 
 
+    /**
+     * 获取当前启用的配置版本ID(最大的 config_id)
+     */
+    @Override
+    public Integer getActiveConfigId(Integer companyId) {
+        ProjectClosureApplier max = this.getOne(new LambdaQueryWrapper<ProjectClosureApplier>()
+                .eq(ProjectClosureApplier::getCompanyId, companyId)
+                .orderByDesc(ProjectClosureApplier::getConfigId)
+                .last("LIMIT 1"));
+        return max != null ? max.getConfigId() : null;
+    }
 
+    /**
+     * 根据配置版本ID获取审批流程节点列表
+     */
     @Override
+    public List<ProjectClosureApplier> getFlowByConfigId(Integer configId) {
+        if (configId == null) {
+            return new ArrayList<>();
+        }
+        return this.list(new LambdaQueryWrapper<ProjectClosureApplier>()
+                .eq(ProjectClosureApplier::getConfigId, configId)
+                .orderByAsc(ProjectClosureApplier::getSortOrder));
+    }
 
+    /**
+     * 获取审批流程配置列表(当前启用版本)
+     */
+    @Override
     public List<Map<String, Object>> getConfigList(Integer companyId) {
-
-        QueryWrapper<ProjectClosureApplier> wrapper = new QueryWrapper<>();
-
-        if (companyId != null && companyId > 0) {
-
-            wrapper.eq("company_id", companyId);
-
+        Integer configId = getActiveConfigId(companyId);
+        if (configId == null) {
+            return new ArrayList<>();
         }
 
-        wrapper.orderByAsc("sort_order");
-
-        List<ProjectClosureApplier> list = baseMapper.selectList(wrapper);
+        List<ProjectClosureApplier> list = this.list(new LambdaQueryWrapper<ProjectClosureApplier>()
+                .eq(ProjectClosureApplier::getConfigId, configId)
+                .orderByAsc(ProjectClosureApplier::getSortOrder));
 
         List<Map<String, Object>> result = new ArrayList<>();
-
         for (int i = 0; i < list.size(); i++) {
-
             ProjectClosureApplier applier = list.get(i);
-
             Map<String, Object> step = new HashMap<>();
-
             step.put("id", applier.getId());
-
+            step.put("configId", applier.getConfigId());
             step.put("roleId", applier.getRoleId());
-
             step.put("approverUserId", applier.getApproverUserId());
-
             step.put("approverName", applier.getApproverName());
-
+            step.put("nodeType", StringUtils.hasText(applier.getApproverUserId()) ? "USER" : "ROLE");
             step.put("stepOrder", applier.getSortOrder());
-
             step.put("stepLabel", "第" + (i + 1) + "步");
 
-
-
             // 下一步节点标志
-
             if (i + 1 < list.size()) {
-
                 ProjectClosureApplier nextApplier = list.get(i + 1);
-
                 step.put("nextStepLabel", "下一步:" + nextApplier.getApproverName() + "审批");
-
             } else {
-
                 step.put("nextStepLabel", "无");
-
             }
 
-
-
-            // 获取该节点的角色用户列表
-
+            // 获取该节点可选人员列表
             if (StringUtils.hasText(applier.getRoleId())) {
-
                 HttpRespMsg roleUserList = permissionService.getRoleUserList(Integer.valueOf(applier.getRoleId()), companyId);
-
                 if (roleUserList != null && roleUserList.getData() != null) {
-
                     step.put("userList", roleUserList.getData());
-
                 } else {
-
                     step.put("userList", new ArrayList<>());
-
                 }
-
+            } else if (StringUtils.hasText(applier.getApproverUserId())) {
+                List<Map<String, Object>> userList = new ArrayList<>();
+                Map<String, Object> user = new HashMap<>();
+                user.put("id", applier.getApproverUserId());
+                user.put("name", applier.getApproverName());
+                userList.add(user);
+                step.put("userList", userList);
             } else {
-
                 step.put("userList", new ArrayList<>());
-
             }
 
             result.add(step);
-
         }
 
         return result;
-
     }
 
-
-
+    /**
+     * 保存审批流程配置(新建版本,旧版本保留)
+     */
     @Override
-
+    @Transactional(rollbackFor = Exception.class)
     public void saveConfig(Integer companyId, List<ProjectClosureApplier> configSteps) {
+        QueryWrapper<ProjectClosureApply> pendingWrapper = new QueryWrapper<>();
+        pendingWrapper.in("approval_status", 0, 1)
+                .inSql("project_id", "select id from project where company_id = " + companyId);
+        Integer pendingCount = projectClosureApplyMapper.selectCount(pendingWrapper);
+        if (pendingCount != null && pendingCount > 0) {
+            throw new RuntimeException("当前存在审批中的结项申请,无法修改审批流程");
+        }
+        // 获取当前最大版本号,新建版本
+        Integer maxConfigId = getMaxConfigId(companyId);
+        Integer newConfigId = (maxConfigId == null) ? 1 : maxConfigId + 1;
 
-        // 先删除该企业下的旧配置
-
-        QueryWrapper<ProjectClosureApplier> deleteWrapper = new QueryWrapper<>();
-
-        deleteWrapper.eq("company_id", companyId);
+        // 批量保存新版本的审批节点
+        if (configSteps != null && !configSteps.isEmpty()) {
+            saveNodes(newConfigId, companyId, configSteps);
+        }
+    }
 
-        baseMapper.delete(deleteWrapper);
+    private Integer getMaxConfigId(Integer companyId) {
+        ProjectClosureApplier max = this.getOne(new LambdaQueryWrapper<ProjectClosureApplier>()
+                .eq(ProjectClosureApplier::getCompanyId, companyId)
+                .orderByDesc(ProjectClosureApplier::getConfigId)
+                .last("LIMIT 1"));
+        return max != null ? max.getConfigId() : null;
+    }
 
-        // 添加新配置
+    private void saveNodes(Integer configId, Integer companyId, List<ProjectClosureApplier> configSteps) {
+        // 预加载角色和用户数据用于填充姓名
+        List<String> roleIds = configSteps.stream()
+                .map(ProjectClosureApplier::getRoleId)
+                .filter(StringUtils::hasText)
+                .distinct()
+                .collect(Collectors.toList());
+        List<String> userIds = configSteps.stream()
+                .map(ProjectClosureApplier::getApproverUserId)
+                .filter(StringUtils::hasText)
+                .distinct()
+                .collect(Collectors.toList());
+
+        List<SysRole> sysRoleList = !roleIds.isEmpty()
+                ? sysRoleMapper.selectList(new LambdaQueryWrapper<SysRole>()
+                        .eq(SysRole::getCompanyId, companyId)
+                        .in(SysRole::getId, roleIds.stream().map(r -> Integer.valueOf(r)).collect(Collectors.toList())))
+                : new ArrayList<>();
+        List<User> userList = !userIds.isEmpty()
+                ? userMapper.selectList(new LambdaQueryWrapper<User>().in(User::getId, userIds).eq(User::getCompanyId, companyId))
+                : new ArrayList<>();
 
         LocalDateTime now = LocalDateTime.now();
-
-        for (int i = 0; i < configSteps.size(); i++) {
-
-            ProjectClosureApplier projectClosureApplier = configSteps.get(i);
-
+        for (ProjectClosureApplier step : configSteps) {
+            if (!StringUtils.hasText(step.getRoleId()) && !StringUtils.hasText(step.getApproverUserId())) {
+                continue;
+            }
             ProjectClosureApplier applier = new ProjectClosureApplier();
-
+            applier.setConfigId(configId);
             applier.setCompanyId(companyId);
-
-            applier.setRoleId(projectClosureApplier.getRoleId());
-
-            applier.setApproverUserId(projectClosureApplier.getApproverUserId());
-
-            applier.setApproverName(projectClosureApplier.getApproverName());
-
-            applier.setSortOrder(projectClosureApplier.getSortOrder());
-
+            applier.setRoleId(StringUtils.hasText(step.getRoleId()) ? step.getRoleId() : "");
+            applier.setApproverUserId(StringUtils.hasText(step.getApproverUserId()) ? step.getApproverUserId() : "");
+
+            // 填充审批人姓名
+            String approverName = "";
+            if ("ROLE".equals(step.getNodeType()) && StringUtils.hasText(step.getRoleId())) {
+                SysRole role = sysRoleList.stream()
+                        .filter(r -> String.valueOf(r.getId()).equals(step.getRoleId()))
+                        .findFirst().orElse(null);
+                approverName = role != null ? role.getRolename() : "";
+            } else if ("USER".equals(step.getNodeType()) && StringUtils.hasText(step.getApproverUserId())) {
+                User user = userList.stream()
+                        .filter(u -> u.getId().equals(step.getApproverUserId()))
+                        .findFirst().orElse(null);
+                approverName = user != null ? user.getName() : "";
+            }
+            applier.setApproverName(approverName);
+            applier.setSortOrder(step.getSortOrder());
             applier.setStatus(1);
-
             applier.setCreateTime(now);
-
             applier.setUpdateTime(now);
-
-            baseMapper.insert(applier);
-
+            this.save(applier);
         }
-
     }
-
-}
+}

+ 102 - 64
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ProjectClosureApplyServiceImpl.java

@@ -4,16 +4,11 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.management.platform.entity.Project;
-import com.management.platform.entity.ProjectClosureApplier;
-import com.management.platform.entity.ProjectClosureApply;
-import com.management.platform.entity.ProjectClosureApprovalLog;
-import com.management.platform.entity.ProjectClosureAttachment;
-import com.management.platform.entity.User;
-import com.management.platform.mapper.ProjectClosureApplierMapper;
+import com.management.platform.entity.*;
 import com.management.platform.mapper.ProjectClosureApplyMapper;
 import com.management.platform.mapper.ProjectClosureApprovalLogMapper;
 import com.management.platform.mapper.ProjectClosureAttachmentMapper;
+import com.management.platform.service.ProjectClosureApplierService;
 import com.management.platform.service.ProjectClosureApplyService;
 import com.management.platform.service.ProjectService;
 import com.management.platform.service.UserService;
@@ -26,11 +21,7 @@ import javax.annotation.Resource;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
 
 /**
  * 项目结项申请:提交、多级审批、通过后完成项目
@@ -40,13 +31,13 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
 
     private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
-    @Resource
-    private ProjectClosureApplierMapper closureApplierMapper;
     @Resource
     private ProjectClosureApprovalLogMapper approvalLogMapper;
     @Resource
     private ProjectClosureAttachmentMapper attachmentMapper;
     @Resource
+    private ProjectClosureApplierService closureApplierService;
+    @Resource
     private ProjectService projectService;
     @Resource
     private UserService userService;
@@ -75,10 +66,17 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
             throw new RuntimeException("该项目已有进行中的结项申请");
         }
         Integer companyId = project.getCompanyId();
-        List<ProjectClosureApplier> flow = getApprovalFlow(companyId);
+
+        // 获取当前启用的配置版本ID
+        Integer configId = closureApplierService.getActiveConfigId(companyId);
+        if (configId == null) {
+            throw new RuntimeException("未配置结项审批流程,请在系统设置中配置后再申请");
+        }
+        List<ProjectClosureApplier> flow = closureApplierService.getFlowByConfigId(configId);
         if (CollectionUtils.isEmpty(flow)) {
-            throw new RuntimeException("未配置结项审批人,请在系统设置中配置后再申请");
+            throw new RuntimeException("审批流程节点为空,请在系统设置中配置后再申请");
         }
+
         User applicant = userService.getById(userId);
         String applicantName = applicant != null && StringUtils.hasText(applicant.getName()) ? applicant.getName() : userId;
 
@@ -91,12 +89,19 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         apply.setCurrentStep(0);
         apply.setApprovalStatus(1);
         apply.setRemark(reason);
+        apply.setConfigId(configId);
+        apply.setTotalStep(flow.size());
         apply.setCreateTime(LocalDateTime.now());
         apply.setUpdateTime(LocalDateTime.now());
 
         ProjectClosureApplier first = flow.get(0);
-        apply.setCurrentApproverId(first.getApproverUserId());
-        apply.setCurrentApproverName(first.getApproverName());
+        if (StringUtils.hasText(first.getApproverUserId())) {
+            apply.setCurrentApproverId(first.getApproverUserId());
+            apply.setCurrentApproverName(first.getApproverName());
+        } else {
+            apply.setCurrentApproverId(first.getRoleId());
+            apply.setCurrentApproverName(first.getApproverName());
+        }
 
         this.save(apply);
 
@@ -134,22 +139,10 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
                 attachmentMapper.insert(attachment);
             }
         }
-        decorateApply(apply, userId, companyId);
+        decorateApply(apply, userId);
         return apply;
     }
 
-    @Override
-    public List<ProjectClosureApplier> getApprovalFlow(Integer companyId) {
-        if (companyId == null) {
-            return new ArrayList<>();
-        }
-        QueryWrapper<ProjectClosureApplier> wrapper = new QueryWrapper<>();
-        wrapper.eq("company_id", companyId)
-                .eq("status", 1)
-                .orderByAsc("sort_order");
-        return closureApplierMapper.selectList(wrapper);
-    }
-
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean approve(Integer applyId, String applierId, String comment, Integer action) {
@@ -160,9 +153,6 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         if (apply == null) {
             return false;
         }
-        if (!Objects.equals(applierId, apply.getCurrentApproverId())) {
-            throw new RuntimeException("您不是当前审批人");
-        }
         int as = apply.getApprovalStatus() == null ? -1 : apply.getApprovalStatus();
         if (as != 0 && as != 1) {
             throw new RuntimeException("该申请已结束审批");
@@ -182,10 +172,29 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         log.setCreateTime(LocalDateTime.now());
         approvalLogMapper.insert(log);
 
-        Project project = projectService.getById(apply.getProjectId());
-        Integer companyId = project != null ? project.getCompanyId() : null;
-        List<ProjectClosureApplier> flow = companyId != null ? getApprovalFlow(companyId) : new ArrayList<>();
+        // 根据申请绑定的 configId 获取审批流程(不取最新配置)
+        List<ProjectClosureApplier> flow = closureApplierService.getFlowByConfigId(apply.getConfigId());
+        if (CollectionUtils.isEmpty(flow)) {
+            throw new RuntimeException("审批流程配置不存在");
+        }
+        int currentStepIndex = apply.getCurrentStep() == null ? 0 : apply.getCurrentStep();
+        ProjectClosureApplier currentNode = currentStepIndex < flow.size() ? flow.get(currentStepIndex) : null;
+        if (currentNode == null) {
+            throw new RuntimeException("当前审批节点不存在");
+        }
+        boolean allowed = false;
+        if (StringUtils.hasText(currentNode.getApproverUserId())) {
+            allowed = Objects.equals(applierId, currentNode.getApproverUserId());
+        } else if (StringUtils.hasText(currentNode.getRoleId())) {
+            User currentUser = userService.getById(applierId);
+            allowed = currentUser != null && currentUser.getRoleId() != null
+                    && Objects.equals(String.valueOf(currentUser.getRoleId()), currentNode.getRoleId());
+        }
+        if (!allowed) {
+            throw new RuntimeException("您不是当前审批人");
+        }
 
+        Project project = projectService.getById(apply.getProjectId());
         if (reject) {
             apply.setApprovalStatus(3);
             apply.setUpdateTime(LocalDateTime.now());
@@ -213,8 +222,13 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
             onApprovalPassed(applyId);
         } else {
             ProjectClosureApplier next = flow.get(nextStepIndex);
-            apply.setCurrentApproverId(next.getApproverUserId());
-            apply.setCurrentApproverName(next.getApproverName());
+            if (StringUtils.hasText(next.getApproverUserId())) {
+                apply.setCurrentApproverId(next.getApproverUserId());
+                apply.setCurrentApproverName(next.getApproverName());
+            } else {
+                apply.setCurrentApproverId(next.getRoleId());
+                apply.setCurrentApproverName(StringUtils.hasText(next.getApproverName()) ? next.getApproverName() : next.getRoleId());
+            }
             apply.setApprovalStatus(1);
             apply.setUpdateTime(LocalDateTime.now());
             this.updateById(apply);
@@ -229,22 +243,34 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         if (apply == null) {
             return progress;
         }
-        Project project = projectService.getById(apply.getProjectId());
-        Integer companyId = project != null ? project.getCompanyId() : null;
-        List<ProjectClosureApplier> flowList = companyId != null ? getApprovalFlow(companyId) : new ArrayList<>();
+        List<ProjectClosureApplier> flowList = closureApplierService.getFlowByConfigId(apply.getConfigId());
         List<Map<String, Object>> flow = new ArrayList<>();
         for (int i = 0; i < flowList.size(); i++) {
             ProjectClosureApplier step = flowList.get(i);
             Map<String, Object> row = new HashMap<>();
             row.put("approverName", step.getApproverName());
             row.put("approverUserId", step.getApproverUserId());
+            row.put("roleId", step.getRoleId());
+            row.put("nodeType", StringUtils.hasText(step.getApproverUserId()) ? "USER" : "ROLE");
+            row.put("displayName", StringUtils.hasText(step.getApproverUserId()) ? step.getApproverName() : (StringUtils.hasText(step.getApproverName()) ? step.getApproverName() : step.getRoleId()));
             row.put("stepOrder", i);
             flow.add(row);
         }
         progress.put("flow", flow);
-        boolean canApprove = StringUtils.hasText(viewerUserId)
-                && Objects.equals(viewerUserId, apply.getCurrentApproverId())
-                && (apply.getApprovalStatus() != null && (apply.getApprovalStatus() == 0 || apply.getApprovalStatus() == 1));
+        boolean canApprove = false;
+        if (StringUtils.hasText(viewerUserId) && (apply.getApprovalStatus() != null && (apply.getApprovalStatus() == 0 || apply.getApprovalStatus() == 1))) {
+            int currentStepIndex = apply.getCurrentStep() == null ? 0 : apply.getCurrentStep();
+            ProjectClosureApplier currentNode = currentStepIndex < flowList.size() ? flowList.get(currentStepIndex) : null;
+            if (currentNode != null) {
+                if (StringUtils.hasText(currentNode.getApproverUserId())) {
+                    canApprove = Objects.equals(viewerUserId, currentNode.getApproverUserId());
+                } else if (StringUtils.hasText(currentNode.getRoleId())) {
+                    User currentUser = userService.getById(viewerUserId);
+                    canApprove = currentUser != null && currentUser.getRoleId() != null
+                            && Objects.equals(String.valueOf(currentUser.getRoleId()), currentNode.getRoleId());
+                }
+            }
+        }
         progress.put("canApprove", canApprove);
         return progress;
     }
@@ -275,7 +301,15 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         Integer companyId = viewer.getCompanyId();
         QueryWrapper<ProjectClosureApply> wrapper = new QueryWrapper<>();
         wrapper.inSql("project_id", "select id from project where company_id = " + companyId)
-                .and(w -> w.eq("current_approver_id", userId).or().eq("applicant_id", userId));
+                .and(w -> {
+                    w.eq("applicant_id", userId)
+                     .or()
+                     .eq("current_approver_id", userId);
+                    if (viewer.getRoleId() != null) {
+                        w.or().eq("current_approver_id", String.valueOf(viewer.getRoleId()));
+                    }
+                    return w;
+                });
         if (StringUtils.hasText(projectKeyword)) {
             wrapper.like("project_name", projectKeyword.trim());
         }
@@ -292,7 +326,7 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         wrapper.orderByDesc("create_time");
         IPage<ProjectClosureApply> result = this.page(page, wrapper);
         for (ProjectClosureApply row : result.getRecords()) {
-            decorateApply(row, userId, companyId);
+            decorateApply(row, userId);
         }
         return result;
     }
@@ -310,7 +344,7 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         wrapper.orderByDesc("create_time");
         IPage<ProjectClosureApply> result = this.page(page, wrapper);
         for (ProjectClosureApply row : result.getRecords()) {
-            decorateApply(row, userId, companyId);
+            decorateApply(row, userId);
         }
         return result;
     }
@@ -335,11 +369,8 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         if (apply == null) {
             return null;
         }
-        Project project = projectService.getById(apply.getProjectId());
-        Integer companyId = project != null ? project.getCompanyId() : null;
-        decorateApply(apply, viewerUserId, companyId);
+        decorateApply(apply, viewerUserId);
 
-        // 查询附件列表
         List<ProjectClosureAttachment> attachments = attachmentMapper.selectList(
                 new QueryWrapper<ProjectClosureAttachment>()
                         .eq("apply_id", id)
@@ -349,8 +380,10 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         return apply;
     }
 
-    private void decorateApply(ProjectClosureApply apply, String viewerUserId, Integer companyId) {
-        int total = companyId != null ? getApprovalFlow(companyId).size() : 0;
+    private void decorateApply(ProjectClosureApply apply, String viewerUserId) {
+        // 从申请绑定的 configId 获取审批流程
+        List<ProjectClosureApplier> flow = closureApplierService.getFlowByConfigId(apply.getConfigId());
+        int total = flow.size();
         apply.setTotalStep(total);
         apply.setReason(apply.getRemark());
         apply.setCurrentApplierName(apply.getCurrentApproverName());
@@ -374,17 +407,12 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
         int cs = apply.getCurrentStep() == null ? 0 : apply.getCurrentStep();
         apply.setCurrentStepDisplay(total > 0 ? (cs + 1) + "/" + total : "-");
 
-        // 优化当前步骤显示
-        if (companyId != null && total > 0) {
-            List<ProjectClosureApplier> flow = getApprovalFlow(companyId);
+        if (total > 0) {
             if (s != null && s == 2) {
-                // 已通过 - 当前步骤显示"暂无"
                 apply.setCurrentStepDisplay("暂无");
             } else if (s != null && s == 3) {
-                // 已拒绝 - 当前步骤显示"暂无"
                 apply.setCurrentStepDisplay("暂无");
             } else {
-                // 进行中 - 当前步骤显示"XXX审核"
                 if (cs < flow.size()) {
                     ProjectClosureApplier currentApplier = flow.get(cs);
                     String currentStepText = "第" + (cs + 1) + "步:" + currentApplier.getApproverName() + "审核";
@@ -395,9 +423,19 @@ public class ProjectClosureApplyServiceImpl extends ServiceImpl<ProjectClosureAp
             }
         }
 
-        boolean can = StringUtils.hasText(viewerUserId)
-                && Objects.equals(viewerUserId, apply.getCurrentApproverId())
-                && (s != null && (s == 0 || s == 1));
+        boolean can = false;
+        if (StringUtils.hasText(viewerUserId) && (s != null && (s == 0 || s == 1))) {
+            if (Objects.equals(viewerUserId, apply.getCurrentApproverId())) {
+                can = true;
+            }
+            if (!can) {
+                User currentUser = userService.getById(viewerUserId);
+                if (currentUser != null && currentUser.getRoleId() != null
+                        && Objects.equals(String.valueOf(currentUser.getRoleId()), apply.getCurrentApproverId())) {
+                    can = true;
+                }
+            }
+        }
         apply.setCanApprove(can);
     }
-}
+}

+ 10 - 2
fhKeeper/formulahousekeeper/timesheet/src/views/project/closureDetail.vue

@@ -46,7 +46,7 @@
                     :key="index"
                     :type="getTimelineType(index)"
                     :timestamp="getStepTime(index)">
-                    {{ step.approverName }} - {{ getStepStatus(index) }}
+                    {{ getStepDisplay(step) }} - {{ getStepStatus(index) }}
                 </el-timeline-item>
             </el-timeline>
         </el-card>
@@ -171,7 +171,7 @@ export default {
         getStepTime(index) {
             const flow = this.approvalFlow
             if (!flow || !flow[index]) return ''
-            const uid = flow[index].approverUserId
+            const uid = flow[index].approverUserId || flow[index].roleId
             const log = this.approvalLogs.find(l => String(l.approverId) === String(uid))
             return log ? (log.approveTime || '') : ''
         },
@@ -186,6 +186,14 @@ export default {
             }
             return '待审批'
         },
+        getStepDisplay(step) {
+            if (!step) return '审批节点'
+            const baseName = step.approverName || (step.nodeType === 'ROLE' ? '角色审批' : '审批人')
+            if (step.nodeType === 'ROLE') {
+                return `${baseName}(角色审批)`
+            }
+            return `${baseName}(人员审批)`
+        },
         downloadFile(file) {
             const url = file && (file.fileUrl || file.url || file.filePath)
             if (!url) return

+ 121 - 25
fhKeeper/formulahousekeeper/timesheet/src/views/project/closureList.vue

@@ -48,7 +48,7 @@
         <el-table-column
           prop="currentStepDisplay"
           :label="$t('当前步骤') || '当前步骤'"
-          width="160"
+          width="200"
         ></el-table-column>
         <el-table-column
           prop="status"
@@ -135,7 +135,11 @@
     <el-dialog
       :title="'配置审批流程'"
       :visible.sync="configDialog"
-      width="700px"
+      width="1280px"
+      top="4vh"
+      :close-on-click-modal="false"
+      :modal-append-to-body="false"
+      custom-class="closure-config-dialog"
     >
       <div class="config-dialog-content">
         <div class="config-toolbar">
@@ -154,6 +158,20 @@
             width="60"
             align="center"
           ></el-table-column>
+          <el-table-column label="审批类型" min-width="120">
+            <template slot-scope="scope">
+              <el-select
+                v-model="scope.row.nodeType"
+                placeholder="选择类型"
+                size="small"
+                class="role-select"
+                @change="onNodeTypeChange(scope.$index)"
+              >
+                <el-option label="角色审批" value="ROLE"></el-option>
+                <el-option label="人员审批" value="USER"></el-option>
+              </el-select>
+            </template>
+          </el-table-column>
           <el-table-column label="审批角色" min-width="160">
             <template slot-scope="scope">
               <el-select
@@ -162,6 +180,7 @@
                 placeholder="选择角色"
                 size="small"
                 class="role-select"
+                v-if="scope.row.nodeType === 'ROLE'"
                 @change="onRoleChange(scope.$index)"
               >
                 <el-option
@@ -171,17 +190,26 @@
                   :value="role"
                 ></el-option>
               </el-select>
+              <span v-else style="color: #888">仅审批类型为角色审批时选择</span>
             </template>
           </el-table-column>
-          <el-table-column label="审批人员" min-width="160">
+          <el-table-column label="审批人员" min-width="220">
             <template slot-scope="scope">
-              <el-select
+              <SelectPersonnel
+                v-model="scope.row.approverUserId"
+                :width="'100%'"
+                :multiple="false"
+                v-if="scope.row.nodeType === 'USER'"
+                @change="onApproverChange(scope.$index)"
+              />
+              <span v-else style="color: #888">仅审批类型为人员审批时选择</span>
+              <!-- <el-select
+                v-else
                 v-model="scope.row.approver"
                 value-key="id"
                 placeholder="选择人员"
                 size="small"
                 class="user-select"
-                :disabled="!scope.row.role"
                 :loading="scope.row.userLoading"
                 @change="onApproverChange(scope.$index)"
               >
@@ -191,7 +219,7 @@
                   :label="user.name"
                   :value="user"
                 ></el-option>
-              </el-select>
+              </el-select> -->
             </template>
           </el-table-column>
           <el-table-column label="操作" width="280" align="center">
@@ -225,7 +253,7 @@
           </el-table-column>
         </el-table>
       </div>
-      <div slot="footer">
+      <div slot="footer" class="config-dialog-footer">
         <el-button @click="configDialog = false">取消</el-button>
         <el-button type="primary" @click="saveConfig">保存</el-button>
       </div>
@@ -235,9 +263,11 @@
 
 <script>
 import ClosureFilter from "@/components/ClosureFilter.vue";
+import SelectPersonnel from "@/components/selectPersonnel.vue";
 export default {
   components: {
     ClosureFilter,
+    SelectPersonnel,
   },
   data() {
     return {
@@ -272,7 +302,10 @@ export default {
     },
     isSuperAdministrator() {
       const roleName = this.user.roleName || this.user.rolename || "";
-      return roleName === this.$t("role.superAdministrator") || roleName === "超级管理员";
+      return (
+        roleName === this.$t("role.superAdministrator") ||
+        roleName === "超级管理员"
+      );
     },
   },
   created() {
@@ -281,7 +314,15 @@ export default {
   methods: {
     loadData() {
       this.listLoading = true;
-      const url = `/project-closure-apply/my-pending?userId=${this.userId}&current=${this.currentPage}&size=${this.pageSize}&status=${this.status}${this.projectKeyword ? `&projectKeyword=${encodeURIComponent(this.projectKeyword)}` : ''}`;
+      const url = `/project-closure-apply/my-pending?userId=${
+        this.userId
+      }&current=${this.currentPage}&size=${this.pageSize}&status=${
+        this.status
+      }${
+        this.projectKeyword
+          ? `&projectKeyword=${encodeURIComponent(this.projectKeyword)}`
+          : ""
+      }`;
       this.http.get(
         url,
         (res) => {
@@ -316,7 +357,7 @@ export default {
       this.projectKeyword = val;
     },
     searchList(payload) {
-      if (payload && typeof payload === 'object') {
+      if (payload && typeof payload === "object") {
         if (payload.status !== undefined) {
           this.status = payload.status;
         }
@@ -376,10 +417,9 @@ export default {
     },
     // ========== 审批流程配置相关方法 ==========
     openConfig() {
-      this.configDialog = true;
       this.roleUserCache = {};
-      // 先加载角色列表,角色列表加载完成后再加载配置
       const companyId = this.user.companyId;
+      // 先加载角色列表,角色列表加载完成后再加载配置
       this.http.post("/permission/getRoleList", { companyId }, (res) => {
         if (res.code === 200 || res.code === "ok") {
           this.roleList = res.data || [];
@@ -389,12 +429,13 @@ export default {
             roleIdToRole[String(role.id)] = role;
           });
           // 角色列表加载完成后,再加载审批配置
-          const companyId = this.user.companyId;
           this.http.get(
             "/project-closure-apply/config/list?companyId=" + companyId,
             (res2) => {
               if (res2.code === 200 || res2.code === "ok") {
                 this.configSteps = (res2.data || []).map((step) => ({
+                  nodeType:
+                    step.nodeType || (step.approverUserId ? "USER" : "ROLE"),
                   role: roleIdToRole[step.roleId] || null,
                   approver: step.userList
                     ? step.userList.find((u) => u.id === step.approverUserId) ||
@@ -406,6 +447,7 @@ export default {
                   approverUserId: step.approverUserId || null,
                   approverName: step.approverName || "",
                 }));
+                this.configDialog = true;
               }
             },
           );
@@ -417,6 +459,7 @@ export default {
     },
     addStep() {
       this.configSteps.push({
+        nodeType: "ROLE",
         role: null,
         approver: null,
         userList: [],
@@ -436,17 +479,29 @@ export default {
       const item = this.configSteps.splice(index, 1)[0];
       this.configSteps.splice(newIndex, 0, item);
     },
-    onRoleChange(index) {
+    onNodeTypeChange(index) {
       const step = this.configSteps[index];
-      // 重置人员选择
       step.approver = null;
       step.approverUserId = null;
       step.approverName = "";
+      step.role = null;
+      step.roleId = null;
       step.userList = [];
+      step.userLoading = false;
+    },
+    onRoleChange(index) {
+      const step = this.configSteps[index];
       if (!step.role) {
+        step.roleId = null;
+        step.userList = [];
         return;
       }
       step.roleId = step.role.id;
+      if (step.nodeType === "ROLE") {
+        step.approver = null;
+        step.approverUserId = null;
+        step.approverName = "";
+      }
       step.userLoading = true;
       // 如果已经缓存了该角色的用户,直接使用
       if (this.roleUserCache[step.role.id]) {
@@ -476,30 +531,49 @@ export default {
     },
     onApproverChange(index) {
       const step = this.configSteps[index];
-      step.approverUserId = step.approver ? step.approver.id : null;
-      step.approverName = step.approver ? step.approver.name : "";
+      if (step.nodeType === "USER") {
+        if (typeof step.approverUserId === "object" && step.approverUserId) {
+          step.approverName = step.approverUserId.name || "";
+          step.approverUserId = step.approverUserId.id || null;
+        } else {
+          step.approverName = step.approverName || "";
+        }
+        step.role = null;
+        step.roleId = null;
+      } else {
+        step.approverUserId = step.approver ? step.approver.id : null;
+        step.approverName = step.approver ? step.approver.name : "";
+      }
     },
     saveConfig() {
       // 校验
       for (let i = 0; i < this.configSteps.length; i++) {
         const step = this.configSteps[i];
-        if (!step.role) {
+        if (!step.nodeType) {
+          this.$message.warning(`第${i + 1}步请选择审批类型`);
+          return;
+        }
+        if (step.nodeType === "ROLE" && !step.role) {
           this.$message.warning(`第${i + 1}步请选择审批角色`);
           return;
         }
-        if (!step.approver) {
+        if (step.nodeType === "USER" && !step.approverUserId) {
           this.$message.warning(`第${i + 1}步请选择审批人员`);
           return;
         }
       }
       const payload = this.configSteps.map((step, index) => ({
         stepOrder: index + 1,
-        roleId: step.roleId || (step.role ? step.role.id : null),
-        roleName: step.role ? step.role.rolename : "",
+        nodeType: step.nodeType,
+        roleId:
+          step.nodeType === "ROLE"
+            ? step.roleId || (step.role ? step.role.id : null)
+            : null,
+        roleName:
+          step.nodeType === "ROLE" && step.role ? step.role.rolename : "",
         approverUserId:
-          step.approverUserId || (step.approver ? step.approver.id : null),
-        approverName:
-          step.approverName || (step.approver ? step.approver.name : ""),
+          step.nodeType === "USER" ? step.approverUserId || null : null,
+        approverName: step.nodeType === "USER" ? step.approverName || "" : "",
       }));
       this.http.JSONPost(
         "/project-closure-apply/config/save?companyId=" + this.user.companyId,
@@ -509,7 +583,14 @@ export default {
             this.$message.success("配置保存成功");
             this.configDialog = false;
           } else {
-            this.$message.error(res.msg || "保存失败");
+            const msg = res.msg || "保存失败";
+            if (msg.includes("正在审批中的结项申请")) {
+              this.$message.warning(
+                "当前存在正在审批中的结项申请,不能修改审批流程",
+              );
+            } else {
+              this.$message.error(msg);
+            }
           }
         },
       );
@@ -529,6 +610,7 @@ export default {
 
 .config-table ::v-deep .el-table__body-wrapper {
   overflow-x: visible;
+  overflow-y: visible;
 }
 
 .role-select,
@@ -546,4 +628,18 @@ export default {
 .action-buttons .el-button {
   margin: 0;
 }
+
+.closure-config-dialog ::v-deep .el-dialog__body {
+  padding: 10px 20px 0;
+  max-height: calc(92vh - 130px);
+  overflow: hidden;
+}
+
+.closure-config-dialog ::v-deep .el-dialog__header {
+  padding: 18px 20px 10px;
+}
+
+.config-dialog-footer {
+  padding-top: 10px;
+}
 </style>

+ 2 - 3
fhKeeper/formulahousekeeper/timesheet/src/views/project/list.vue

@@ -9812,9 +9812,8 @@ export default {
     if (localStorage.projectPageIndex) {
       this.page = parseInt(localStorage.projectPageIndex);
     }
-    if (localStorage.projectTab) {
-      this.projectTab = localStorage.projectTab;
-    }
+    this.projectTab = "0";
+    localStorage.projectTab = "0";
 
     this.getList();
     this.getUsers();