|
@@ -11,6 +11,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.google.common.collect.Lists;
|
|
import com.google.common.collect.Lists;
|
|
|
import com.google.common.collect.Maps;
|
|
import com.google.common.collect.Maps;
|
|
|
import com.jeeplus.common.TokenProvider;
|
|
import com.jeeplus.common.TokenProvider;
|
|
|
|
|
+import com.jeeplus.common.constant.AppNameConstants;
|
|
|
import com.jeeplus.common.utils.Collections3;
|
|
import com.jeeplus.common.utils.Collections3;
|
|
|
import com.jeeplus.flowable.common.cmd.BackUserTaskCmd;
|
|
import com.jeeplus.flowable.common.cmd.BackUserTaskCmd;
|
|
|
import com.jeeplus.flowable.constant.FlowableConstant;
|
|
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.task.service.impl.persistence.entity.TaskEntityImpl;
|
|
|
import org.flowable.variable.api.history.HistoricVariableInstanceQuery;
|
|
import org.flowable.variable.api.history.HistoricVariableInstanceQuery;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
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.JdbcTemplate;
|
|
|
import org.springframework.jdbc.core.RowMapper;
|
|
import org.springframework.jdbc.core.RowMapper;
|
|
|
import org.springframework.security.core.Authentication;
|
|
import org.springframework.security.core.Authentication;
|
|
|
import org.springframework.security.core.context.SecurityContextHolder;
|
|
import org.springframework.security.core.context.SecurityContextHolder;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
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.ResultSet;
|
|
|
import java.sql.SQLException;
|
|
import java.sql.SQLException;
|
|
|
import java.text.SimpleDateFormat;
|
|
import java.text.SimpleDateFormat;
|
|
|
import java.util.*;
|
|
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.util.stream.Collectors;
|
|
|
|
|
|
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.Field;
|
|
@@ -103,6 +118,28 @@ public class FlowTaskService {
|
|
|
private IUserApi userApi;
|
|
private IUserApi userApi;
|
|
|
@Autowired
|
|
@Autowired
|
|
|
private ITenantApi tenantApi;
|
|
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);
|
|
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();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
|
|
|
}
|
|
}
|