فهرست منبع

工作台批量审批-所有报销相关流程

huangguoce 2 روز پیش
والد
کامیت
d43d2f56e4

+ 30 - 0
jeeplus-modules/jeeplus-flowable/src/main/java/com/jeeplus/flowable/controller/FlowableTaskController.java

@@ -54,6 +54,7 @@ import org.flowable.task.api.TaskQuery;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.cloud.openfeign.SpringQueryMap;
+import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.web.bind.annotation.*;
@@ -666,6 +667,35 @@ public class FlowableTaskController {
         return ResponseEntity.ok(flow.getProcInsId());
     }
 
+    /**
+     * 提交批量审核任务。接口只负责接收任务并立即返回,具体审核在后台线程执行。
+     */
+    @PostMapping("batch-audit-jobs")
+    public ResponseEntity batchAudit(HttpServletRequest request, @RequestBody Map<String, Object> body) {
+        try {
+            Map<String, String> result = flowTaskService.submitBatchAudit(
+                    body,
+                    TokenProvider.getCurrentToken(),
+                    request.getHeader("domain")
+            );
+            return ResponseEntity.status(HttpStatus.ACCEPTED).body(result);
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.badRequest().body(e.getMessage());
+        }
+    }
+
+    /**
+     * 查询批量审核进度。
+     */
+    @GetMapping("batch-audit-jobs/{jobId}")
+    public ResponseEntity batchAuditProgress(@PathVariable String jobId) {
+        Map<String, Object> progress = flowTaskService.getBatchAuditProgress(jobId);
+        if (progress == null) {
+            return ResponseEntity.notFound().build();
+        }
+        return ResponseEntity.ok(progress);
+    }
+
     @GetMapping("getTaskAuditUsersForApp")
     public List<String> getTaskAuditUsersForApp(String procInsId) {
         TaskQuery todoTaskQuery = taskService.createTaskQuery()

+ 271 - 0
jeeplus-modules/jeeplus-flowable/src/main/java/com/jeeplus/flowable/service/FlowTaskService.java

@@ -11,6 +11,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.jeeplus.common.TokenProvider;
+import com.jeeplus.common.constant.AppNameConstants;
 import com.jeeplus.common.utils.Collections3;
 import com.jeeplus.flowable.common.cmd.BackUserTaskCmd;
 import com.jeeplus.flowable.constant.FlowableConstant;
@@ -50,17 +51,31 @@ import org.flowable.task.service.impl.persistence.entity.TaskEntity;
 import org.flowable.task.service.impl.persistence.entity.TaskEntityImpl;
 import org.flowable.variable.api.history.HistoricVariableInstanceQuery;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.http.*;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.jdbc.core.RowMapper;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
 
+import javax.annotation.PreDestroy;
+import java.net.URI;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.text.SimpleDateFormat;
 import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import java.lang.reflect.Field;
@@ -103,6 +118,28 @@ public class FlowTaskService {
     private IUserApi userApi;
     @Autowired
     private ITenantApi tenantApi;
+    @Autowired
+    private DiscoveryClient discoveryClient;
+    @Autowired
+    private RestTemplate restTemplate;
+
+    private static final ConcurrentMap<String, Map<String, Object>> BATCH_AUDIT_JOBS = new ConcurrentHashMap<>();
+
+    private static final ExecutorService BATCH_AUDIT_EXECUTOR = Executors.newFixedThreadPool(2, runnable -> {
+        Thread thread = new Thread(runnable, "batch-audit-" + UUID.randomUUID());
+        thread.setDaemon(true);
+        return thread;
+    });
+
+    private static final Set<String> REIMBURSEMENT_PROCESS_NAMES = new HashSet<>(Arrays.asList(
+            "咨询-所长报销-电子发票", "咨询-所长报销", "咨询-总经办报销", "咨询-报销审批-电子发票", "咨询-报销审批",
+            "项目-总经办报销", "中审-总经办报销", "评估-总经办报销", "会计-总经办报销",
+            "项目-报销审批-电子发票", "项目-报销审批", "评估-所长报销-电子发票", "评估-所长报销",
+            "中审-所长报销-电子发票", "中审-所长报销", "会计-所长报销", "会计-所长报销-电子发票",
+            "中审-报销审批-电子发票", "评估-报销审批-电子发票", "会计-报销审批-电子发票",
+            "中审-报销审批", "评估-报销审批", "会计-报销审批",
+            "苏州-业务操作报销", "中审-苏州分公司(智永祥)-报销"
+    ));
 
     /**
      * 获取流转历史任务列表
@@ -1428,5 +1465,239 @@ public class FlowTaskService {
         return flowMapper.getCreateTimeByProcInsId(act);
     }
 
+    public Map<String, String> submitBatchAudit(Map<String, Object> body, String token, String domain) {
+        List<String> taskIds = new ArrayList<>();
+        boolean allReimbursements = Boolean.parseBoolean(String.valueOf(body.get("allReimbursements")));
+        if (allReimbursements) {
+            UserDTO currentUser = userApi.getByToken(token);
+            List<Task> currentTasks = taskService.createTaskQuery()
+                    .taskCandidateOrAssigned(currentUser.getId())
+                    .active()
+                    .list();
+            for (Task task : currentTasks) {
+                if (isReimbursementTask(task)) {
+                    taskIds.add(task.getId());
+                }
+            }
+        } else {
+            Object taskIdsValue = body.get("taskIds");
+            if (!(taskIdsValue instanceof Collection)) {
+                throw new IllegalArgumentException("请选择需要审核的任务");
+            }
+            for (Object taskId : (Collection<?>) taskIdsValue) {
+                if (taskId != null && StringUtils.isNotBlank(String.valueOf(taskId))) {
+                    Task task = taskService.createTaskQuery()
+                            .taskId(String.valueOf(taskId))
+                            .active()
+                            .singleResult();
+                    if (isReimbursementTask(task)) {
+                        taskIds.add(task.getId());
+                    }
+                }
+            }
+        }
+        if (taskIds.isEmpty()) {
+            throw new IllegalArgumentException(allReimbursements ? "当前没有可处理的报销待办" : "所选数据中没有可处理的报销待办");
+        }
+
+        boolean agree = Boolean.parseBoolean(String.valueOf(body.get("agree")));
+        String jobId = UUID.randomUUID().toString().replace("-", "");
+
+        Map<String, Object> progress = new ConcurrentHashMap<>();
+        progress.put("jobId", jobId);
+        progress.put("status", "QUEUED");
+        progress.put("total", taskIds.size());
+        progress.put("processed", 0);
+        progress.put("success", 0);
+        progress.put("failed", 0);
+        progress.put("updatedAt", System.currentTimeMillis());
+        BATCH_AUDIT_JOBS.put(jobId, progress);
+        clearExpiredBatchAuditJobs();
+
+        BATCH_AUDIT_EXECUTOR.submit(() -> runBatchAudit(jobId, taskIds, agree, token, domain));
+        return Collections.singletonMap("jobId", jobId);
+    }
+
+    public Map<String, Object> getBatchAuditProgress(String jobId) {
+        Map<String, Object> progress = BATCH_AUDIT_JOBS.get(jobId);
+        return progress == null ? null : new HashMap<>(progress);
+    }
+
+    private boolean isReimbursementTask(Task task) {
+        if (task == null) {
+            return false;
+        }
+        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
+                .processDefinitionId(task.getProcessDefinitionId())
+                .singleResult();
+        return processDefinition != null && REIMBURSEMENT_PROCESS_NAMES.contains(processDefinition.getName());
+    }
+
+    private void runBatchAudit(String jobId, List<String> taskIds, boolean agree, String token, String domain) {
+        Map<String, Object> progress = BATCH_AUDIT_JOBS.get(jobId);
+        progress.put("status", "RUNNING");
+        int success = 0;
+        int failed = 0;
+
+        for (String taskId : taskIds) {
+            try {
+                auditOneTask(taskId, agree, token, domain);
+                success++;
+            } catch (Exception e) {
+                failed++;
+                log.error("批量审核单条任务执行失败,jobId={}, taskId={}", jobId, taskId, e);
+            }
+            progress.put("processed", success + failed);
+            progress.put("success", success);
+            progress.put("failed", failed);
+            progress.put("updatedAt", System.currentTimeMillis());
+        }
+
+        progress.put("status", "COMPLETED");
+        progress.put("updatedAt", System.currentTimeMillis());
+    }
+
+    private void auditOneTask(String taskId, boolean agree, String token, String domain) {
+        Task task = taskService.createTaskQuery().taskId(taskId).active().singleResult();
+        if (task == null) {
+            throw new IllegalStateException("任务不存在或已处理");
+        }
+
+        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
+                .processDefinitionId(task.getProcessDefinitionId())
+                .singleResult();
+        String processName = processDefinition == null ? "" : processDefinition.getName();
+        String businessId = REIMBURSEMENT_PROCESS_NAMES.contains(processName)
+                ? getBusinessId(task.getProcessInstanceId()) : null;
+        String taskName = task.getName();
+
+        callAuditInterface(task, agree, token, domain);
+        updateReimbursementStatus(processName, taskName, businessId, agree, token, domain);
+    }
+
+    private void callAuditInterface(Task task, boolean agree, String token, String domain) {
+        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
+        form.add("taskId", task.getId());
+        form.add("taskDefKey", task.getTaskDefinitionKey());
+        form.add("procInsId", task.getProcessInstanceId());
+        form.add("procDefId", task.getProcessDefinitionId());
+        form.add("vars.agree", String.valueOf(agree));
+        form.add("comment.type", "");
+        form.add("comment.status", agree ? "同意" : "驳回");
+        form.add("comment.message", "");
+        form.add("recordType", "");
+
+        HttpHeaders headers = requestHeaders(token, domain, MediaType.APPLICATION_FORM_URLENCODED);
+        URI uri = serviceUri(AppNameConstants.APP_FLOWABLE_SERVICE, "/flowable/task/audit", null);
+        restTemplate.exchange(uri, HttpMethod.POST, new HttpEntity<>(form, headers), String.class);
+    }
+
+    private void updateReimbursementStatus(String processName, String taskName, String businessId,
+                                           boolean agree, String token, String domain) {
+        if (!REIMBURSEMENT_PROCESS_NAMES.contains(processName)) {
+            return;
+        }
+
+        String serviceName;
+        String path;
+        if (processName.startsWith("中审-")) {
+            serviceName = AppNameConstants.APP_CENTRECAREFUL_MODULES;
+            path = "/zsReimbursement/info";
+        } else if (processName.startsWith("会计-")) {
+            serviceName = AppNameConstants.APP_FINANCE_MODULES;
+            path = "/reimbursementApproval/info";
+        } else if (processName.startsWith("苏州-")) {
+            serviceName = AppNameConstants.APP_FINANCE_MODULES;
+            path = "/reimbursementApproval/info";
+        } else if (processName.startsWith("咨询-")) {
+            serviceName = AppNameConstants.APP_CONSULT_MODULES;
+            path = "/consultancyReimbursement/info";
+        } else if (processName.startsWith("项目-")) {
+            serviceName = AppNameConstants.APP_CCPM_MODULES;
+            path = "/ccpmReimbursement/info";
+        } else if (processName.startsWith("评估-")) {
+            serviceName = AppNameConstants.APP_ASSESS_MODULES;
+            path = "/reimbursement/info";
+        } else {
+            return;
+        }
+
+        Map<String, Object> query = Collections.singletonMap("id", businessId);
+        HttpHeaders headers = requestHeaders(token, domain, MediaType.APPLICATION_JSON);
+        ResponseEntity<Map> response = restTemplate.exchange(
+                serviceUri(serviceName, path + "/findById", query),
+                HttpMethod.GET,
+                new HttpEntity<>(headers),
+                Map.class
+        );
+        Map data = response.getBody();
+        if (data == null || !"2".equals(String.valueOf(data.get("type")))) {
+            throw new IllegalStateException("报销数据已发生改变或不存在");
+        }
+
+        String status = "4";
+        if (agree) {
+            status = Arrays.asList("所长报销审核人", "所长审批", "总经办人员审批").contains(taskName) ? "5" : "2";
+        }
+        Map<String, Object> update = new HashMap<>();
+        update.put("id", data.get("id"));
+        update.put("type", status);
+        restTemplate.exchange(
+                serviceUri(serviceName, path + "/updateStatusById", null),
+                HttpMethod.POST,
+                new HttpEntity<>(update, headers),
+                String.class
+        );
+    }
+
+    private String getBusinessId(String processInstanceId) {
+        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
+                .processInstanceId(processInstanceId)
+                .singleResult();
+        if (processInstance == null || StringUtils.isBlank(processInstance.getBusinessKey())) {
+            throw new IllegalStateException("流程未绑定业务数据");
+        }
+        String businessKey = processInstance.getBusinessKey();
+        int separator = businessKey.indexOf(':');
+        return separator >= 0 ? businessKey.substring(separator + 1) : businessKey;
+    }
+
+    private URI serviceUri(String serviceName, String path, Map<String, Object> query) {
+        List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
+        if (instances == null || instances.isEmpty()) {
+            throw new IllegalStateException("服务不可用: " + serviceName);
+        }
+        UriComponentsBuilder builder = UriComponentsBuilder.fromUri(instances.get(0).getUri()).path(path);
+        if (query != null) {
+            query.forEach(builder::queryParam);
+        }
+        return builder.build().encode().toUri();
+    }
+
+    private HttpHeaders requestHeaders(String token, String domain, MediaType contentType) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(contentType);
+        if (StringUtils.isNotBlank(token)) {
+            headers.add(TokenProvider.TOKEN, token);
+        }
+        if (StringUtils.isNotBlank(domain)) {
+            headers.add("domain", domain);
+        }
+        return headers;
+    }
+
+    private void clearExpiredBatchAuditJobs() {
+        long expireTime = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1);
+        BATCH_AUDIT_JOBS.entrySet().removeIf(entry -> {
+            Object updatedAt = entry.getValue().get("updatedAt");
+            return updatedAt instanceof Number && ((Number) updatedAt).longValue() < expireTime;
+        });
+    }
+
+    @PreDestroy
+    public void shutdownBatchAuditExecutor() {
+        BATCH_AUDIT_EXECUTOR.shutdown();
+    }
+
 
 }