订单异议开发

This commit is contained in:
朱昊天
2026-06-17 19:31:10 +08:00
parent d5736cd5f8
commit 27807c14eb
19 changed files with 2455 additions and 10 deletions

View File

@@ -21,6 +21,10 @@
<groupId>com.gear</groupId>
<artifactId>gear-common</artifactId>
</dependency>
<dependency>
<groupId>com.gear</groupId>
<artifactId>gear-flowable</artifactId>
</dependency>
<!-- 邮件发送相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -0,0 +1,52 @@
package com.gear.oa.controller;
import com.gear.common.core.controller.BaseController;
import com.gear.common.core.domain.R;
import com.gear.oa.domain.bo.OrderDisputeFlowConfirmBo;
import com.gear.oa.domain.bo.OrderDisputeFlowStartBo;
import com.gear.oa.domain.vo.OrderDisputeFlowVo;
import com.gear.oa.domain.vo.OrderDisputeTaskListVo;
import com.gear.oa.domain.vo.OrderDisputeTodoTaskVo;
import com.gear.oa.service.IOrderDisputeFlowService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/oa/orderDisputeFlow")
public class OrderDisputeFlowController extends BaseController {
private final IOrderDisputeFlowService orderDisputeFlowService;
@GetMapping("/byOrder/{orderId}")
public R<OrderDisputeFlowVo> queryByOrderId(@PathVariable Long orderId) {
return R.ok(orderDisputeFlowService.queryByOrderId(orderId));
}
@PostMapping("/start")
public R<String> start(@RequestBody OrderDisputeFlowStartBo bo) {
return R.ok(orderDisputeFlowService.start(bo));
}
@PostMapping("/confirm")
public R<Void> confirm(@RequestBody OrderDisputeFlowConfirmBo bo) {
orderDisputeFlowService.confirm(bo);
return R.ok();
}
@GetMapping("/todoList")
public R<List<OrderDisputeTodoTaskVo>> todoList(@RequestParam(required = false) String stepKey) {
return R.ok(orderDisputeFlowService.todoList(stepKey));
}
@GetMapping("/taskList")
public R<List<OrderDisputeTaskListVo>> taskList(@RequestParam(required = false) String stepKey,
@RequestParam(required = false) String status,
@RequestParam(required = false) Integer limit) {
return R.ok(orderDisputeFlowService.taskList(stepKey, status, limit));
}
}

View File

@@ -0,0 +1,12 @@
package com.gear.oa.domain.bo;
import lombok.Data;
@Data
public class OrderDisputeFlowConfirmBo {
private Long orderId;
private String comment;
}

View File

@@ -0,0 +1,26 @@
package com.gear.oa.domain.bo;
import lombok.Data;
@Data
public class OrderDisputeFlowStartBo {
private Long orderId;
private String orderCode;
private Long customerId;
private String customerName;
private Long acceptUserId;
private Long physicalUserId;
private Long analysisUserId;
private Long planUserId;
private Long visitUserId;
}

View File

@@ -0,0 +1,22 @@
package com.gear.oa.domain.vo;
import lombok.Data;
import java.util.Date;
@Data
public class OrderDisputeFlowStepVo {
private String stepKey;
private String stepName;
private String assignee;
private Date startTime;
private Date endTime;
private String comment;
}

View File

@@ -0,0 +1,38 @@
package com.gear.oa.domain.vo;
import lombok.Data;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Data
public class OrderDisputeFlowVo {
private Long orderId;
private String orderCode;
private String customerName;
private String procInsId;
private String status;
private String currentTaskId;
private String currentTaskKey;
private String currentTaskName;
private String currentAssignee;
private Date startTime;
private Date endTime;
private Map<String, Object> variables;
private List<OrderDisputeFlowStepVo> steps;
}

View File

@@ -0,0 +1,31 @@
package com.gear.oa.domain.vo;
import lombok.Data;
import java.util.Date;
@Data
public class OrderDisputeTaskListVo {
private String taskId;
private String taskKey;
private String taskName;
private String procInsId;
private Long orderId;
private String orderCode;
private Long customerId;
private String customerName;
private Date createTime;
private Date endTime;
private String status;
}

View File

@@ -0,0 +1,28 @@
package com.gear.oa.domain.vo;
import lombok.Data;
import java.util.Date;
@Data
public class OrderDisputeTodoTaskVo {
private String taskId;
private String taskKey;
private String taskName;
private String procInsId;
private Long orderId;
private String orderCode;
private Long customerId;
private String customerName;
private Date createTime;
}

View File

@@ -0,0 +1,22 @@
package com.gear.oa.service;
import com.gear.oa.domain.bo.OrderDisputeFlowConfirmBo;
import com.gear.oa.domain.bo.OrderDisputeFlowStartBo;
import com.gear.oa.domain.vo.OrderDisputeFlowVo;
import com.gear.oa.domain.vo.OrderDisputeTaskListVo;
import com.gear.oa.domain.vo.OrderDisputeTodoTaskVo;
import java.util.List;
public interface IOrderDisputeFlowService {
OrderDisputeFlowVo queryByOrderId(Long orderId);
String start(OrderDisputeFlowStartBo bo);
void confirm(OrderDisputeFlowConfirmBo bo);
List<OrderDisputeTodoTaskVo> todoList(String stepKey);
List<OrderDisputeTaskListVo> taskList(String stepKey, String status, Integer limit);
}

View File

@@ -0,0 +1,410 @@
package com.gear.oa.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import com.gear.common.exception.ServiceException;
import com.gear.common.helper.LoginHelper;
import com.gear.common.utils.StringUtils;
import com.gear.flowable.common.enums.FlowComment;
import com.gear.oa.domain.bo.OrderDisputeFlowConfirmBo;
import com.gear.oa.domain.bo.OrderDisputeFlowStartBo;
import com.gear.oa.domain.vo.OrderDisputeFlowStepVo;
import com.gear.oa.domain.vo.OrderDisputeFlowVo;
import com.gear.oa.domain.vo.OrderDisputeTaskListVo;
import com.gear.oa.domain.vo.OrderDisputeTodoTaskVo;
import com.gear.oa.service.IOrderDisputeFlowService;
import com.gear.oa.workflow.OrderDisputeProcessDeployer;
import lombok.RequiredArgsConstructor;
import org.flowable.engine.HistoryService;
import org.flowable.engine.IdentityService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.flowable.task.api.TaskQuery;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.flowable.task.api.history.HistoricTaskInstanceQuery;
import org.flowable.engine.task.Comment;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Service
public class OrderDisputeFlowServiceImpl implements IOrderDisputeFlowService {
private final RepositoryService repositoryService;
private final RuntimeService runtimeService;
private final TaskService taskService;
private final HistoryService historyService;
private final IdentityService identityService;
@Override
public OrderDisputeFlowVo queryByOrderId(Long orderId) {
if (orderId == null) {
return null;
}
ProcessInstance active = runtimeService.createProcessInstanceQuery()
.processDefinitionKey(OrderDisputeProcessDeployer.PROCESS_KEY)
.variableValueEquals("orderId", orderId)
.active()
.singleResult();
if (active != null) {
Map<String, Object> runtimeVars = runtimeService.getVariables(active.getProcessInstanceId());
return buildRunningFlowVo(orderId, active.getProcessInstanceId(), runtimeVars);
}
HistoricProcessInstance historic = historyService.createHistoricProcessInstanceQuery()
.processDefinitionKey(OrderDisputeProcessDeployer.PROCESS_KEY)
.variableValueEquals("orderId", orderId)
.includeProcessVariables()
.orderByProcessInstanceStartTime()
.desc()
.listPage(0, 1)
.stream()
.findFirst()
.orElse(null);
if (historic == null) {
return null;
}
return buildFlowVo(orderId, historic);
}
@Override
public String start(OrderDisputeFlowStartBo bo) {
if (bo == null || bo.getOrderId() == null) {
throw new ServiceException("orderId不能为空");
}
Long orderId = bo.getOrderId();
ProcessInstance active = runtimeService.createProcessInstanceQuery()
.processDefinitionKey(OrderDisputeProcessDeployer.PROCESS_KEY)
.variableValueEquals("orderId", orderId)
.active()
.singleResult();
if (active != null) {
return active.getProcessInstanceId();
}
Long userId = LoginHelper.getUserId();
if (userId == null) {
throw new ServiceException("未登录");
}
String userIdStr = String.valueOf(userId);
Map<String, Object> variables = new HashMap<>();
variables.put("orderId", orderId);
if (StringUtils.isNotBlank(bo.getOrderCode())) {
variables.put("orderCode", bo.getOrderCode());
}
if (bo.getCustomerId() != null) {
variables.put("customerId", bo.getCustomerId());
}
if (StringUtils.isNotBlank(bo.getCustomerName())) {
variables.put("customerName", bo.getCustomerName());
}
variables.put("acceptUserId", String.valueOf(bo.getAcceptUserId() != null ? bo.getAcceptUserId() : userId));
variables.put("physicalUserId", String.valueOf(bo.getPhysicalUserId() != null ? bo.getPhysicalUserId() : userId));
variables.put("analysisUserId", String.valueOf(bo.getAnalysisUserId() != null ? bo.getAnalysisUserId() : userId));
variables.put("planUserId", String.valueOf(bo.getPlanUserId() != null ? bo.getPlanUserId() : userId));
variables.put("visitUserId", String.valueOf(bo.getVisitUserId() != null ? bo.getVisitUserId() : userId));
identityService.setAuthenticatedUserId(userIdStr);
ProcessInstance instance = runtimeService.startProcessInstanceByKey(OrderDisputeProcessDeployer.PROCESS_KEY, variables);
return instance.getProcessInstanceId();
}
@Override
public void confirm(OrderDisputeFlowConfirmBo bo) {
if (bo == null || bo.getOrderId() == null) {
throw new ServiceException("orderId不能为空");
}
Long userId = LoginHelper.getUserId();
if (userId == null) {
throw new ServiceException("未登录");
}
String userIdStr = String.valueOf(userId);
ProcessInstance active = runtimeService.createProcessInstanceQuery()
.processDefinitionKey(OrderDisputeProcessDeployer.PROCESS_KEY)
.variableValueEquals("orderId", bo.getOrderId())
.active()
.singleResult();
if (active == null) {
throw new ServiceException("异议流程不存在或已结束");
}
Task task = taskService.createTaskQuery()
.processInstanceId(active.getProcessInstanceId())
.active()
.singleResult();
if (task == null) {
throw new ServiceException("当前没有可处理的节点");
}
if (!StringUtils.equals(task.getAssignee(), userIdStr)) {
throw new ServiceException("当前节点不是你的待办");
}
identityService.setAuthenticatedUserId(userIdStr);
String comment = bo.getComment() != null ? String.valueOf(bo.getComment()) : "";
taskService.addComment(task.getId(), active.getProcessInstanceId(), FlowComment.NORMAL.getType(), comment);
taskService.complete(task.getId());
}
@Override
public List<OrderDisputeTodoTaskVo> todoList(String stepKey) {
Long userId = LoginHelper.getUserId();
if (userId == null) {
throw new ServiceException("未登录");
}
String userIdStr = String.valueOf(userId);
TaskQuery query = taskService.createTaskQuery()
.processDefinitionKey(OrderDisputeProcessDeployer.PROCESS_KEY)
.active()
.includeProcessVariables()
.taskAssignee(userIdStr)
.orderByTaskCreateTime()
.desc();
if (StringUtils.isNotBlank(stepKey)) {
query.taskDefinitionKey(stepKey);
}
List<Task> list = query.list();
if (CollUtil.isEmpty(list)) {
return Collections.emptyList();
}
return list.stream().map(task -> {
Map<String, Object> vars = task.getProcessVariables();
OrderDisputeTodoTaskVo vo = new OrderDisputeTodoTaskVo();
vo.setTaskId(task.getId());
vo.setTaskKey(task.getTaskDefinitionKey());
vo.setTaskName(task.getName());
vo.setProcInsId(task.getProcessInstanceId());
vo.setCreateTime(task.getCreateTime());
vo.setOrderId(MapUtil.getLong(vars, "orderId"));
vo.setOrderCode(MapUtil.getStr(vars, "orderCode"));
vo.setCustomerId(MapUtil.getLong(vars, "customerId"));
vo.setCustomerName(MapUtil.getStr(vars, "customerName"));
return vo;
}).collect(Collectors.toList());
}
@Override
public List<OrderDisputeTaskListVo> taskList(String stepKey, String status, Integer limit) {
Long userId = LoginHelper.getUserId();
if (userId == null) {
throw new ServiceException("未登录");
}
String userIdStr = String.valueOf(userId);
String statusVal = StringUtils.isNotBlank(status) ? status.trim().toLowerCase() : "todo";
boolean includeTodo = "todo".equals(statusVal) || "all".equals(statusVal);
boolean includeDone = "done".equals(statusVal) || "all".equals(statusVal);
int limitVal = limit != null ? limit : 200;
if (limitVal <= 0) {
limitVal = 200;
}
if (limitVal > 500) {
limitVal = 500;
}
List<OrderDisputeTaskListVo> result = new ArrayList<>();
if (includeTodo) {
TaskQuery query = taskService.createTaskQuery()
.processDefinitionKey(OrderDisputeProcessDeployer.PROCESS_KEY)
.active()
.includeProcessVariables()
.taskAssignee(userIdStr)
.orderByTaskCreateTime()
.desc();
if (StringUtils.isNotBlank(stepKey)) {
query.taskDefinitionKey(stepKey);
}
List<Task> todo = query.listPage(0, limitVal);
if (CollUtil.isNotEmpty(todo)) {
result.addAll(todo.stream().map(task -> {
Map<String, Object> vars = task.getProcessVariables();
OrderDisputeTaskListVo vo = new OrderDisputeTaskListVo();
vo.setTaskId(task.getId());
vo.setTaskKey(task.getTaskDefinitionKey());
vo.setTaskName(task.getName());
vo.setProcInsId(task.getProcessInstanceId());
vo.setCreateTime(task.getCreateTime());
vo.setOrderId(MapUtil.getLong(vars, "orderId"));
vo.setOrderCode(MapUtil.getStr(vars, "orderCode"));
vo.setCustomerId(MapUtil.getLong(vars, "customerId"));
vo.setCustomerName(MapUtil.getStr(vars, "customerName"));
vo.setStatus("TODO");
return vo;
}).collect(Collectors.toList()));
}
}
if (includeDone) {
HistoricTaskInstanceQuery query = historyService.createHistoricTaskInstanceQuery()
.processDefinitionKey(OrderDisputeProcessDeployer.PROCESS_KEY)
.finished()
.taskAssignee(userIdStr)
.includeProcessVariables()
.orderByHistoricTaskInstanceEndTime()
.desc();
if (StringUtils.isNotBlank(stepKey)) {
query.taskDefinitionKey(stepKey);
}
List<HistoricTaskInstance> done = query.listPage(0, limitVal);
if (CollUtil.isNotEmpty(done)) {
result.addAll(done.stream().map(task -> {
Map<String, Object> vars = task.getProcessVariables();
OrderDisputeTaskListVo vo = new OrderDisputeTaskListVo();
vo.setTaskId(task.getId());
vo.setTaskKey(task.getTaskDefinitionKey());
vo.setTaskName(task.getName());
vo.setProcInsId(task.getProcessInstanceId());
vo.setCreateTime(task.getStartTime());
vo.setEndTime(task.getEndTime());
vo.setOrderId(MapUtil.getLong(vars, "orderId"));
vo.setOrderCode(MapUtil.getStr(vars, "orderCode"));
vo.setCustomerId(MapUtil.getLong(vars, "customerId"));
vo.setCustomerName(MapUtil.getStr(vars, "customerName"));
vo.setStatus("DONE");
return vo;
}).collect(Collectors.toList()));
}
}
if (CollUtil.isEmpty(result)) {
return Collections.emptyList();
}
result.sort((a, b) -> {
if (a == null || b == null) {
return 0;
}
int statusCompare = 0;
if ("TODO".equals(a.getStatus()) && "DONE".equals(b.getStatus())) {
statusCompare = -1;
} else if ("DONE".equals(a.getStatus()) && "TODO".equals(b.getStatus())) {
statusCompare = 1;
}
if (statusCompare != 0) {
return statusCompare;
}
Date at = a.getCreateTime();
Date bt = b.getCreateTime();
if (at == null && bt == null) {
return 0;
}
if (at == null) {
return 1;
}
if (bt == null) {
return -1;
}
return bt.compareTo(at);
});
return result;
}
private OrderDisputeFlowVo buildFlowVo(Long orderId, HistoricProcessInstance historic) {
OrderDisputeFlowVo vo = new OrderDisputeFlowVo();
vo.setOrderId(orderId);
vo.setProcInsId(historic.getId());
vo.setStartTime(historic.getStartTime());
vo.setEndTime(historic.getEndTime());
vo.setStatus(historic.getEndTime() == null ? "RUNNING" : "FINISHED");
Map<String, Object> variables = historic.getProcessVariables() != null ? historic.getProcessVariables() : Collections.emptyMap();
applyVariables(vo, variables);
Task currentTask = taskService.createTaskQuery().processInstanceId(historic.getId()).active().singleResult();
if (currentTask != null) {
vo.setCurrentTaskId(currentTask.getId());
vo.setCurrentTaskKey(currentTask.getTaskDefinitionKey());
vo.setCurrentTaskName(currentTask.getName());
vo.setCurrentAssignee(currentTask.getAssignee());
}
vo.setSteps(buildSteps(historic.getId()));
return vo;
}
private OrderDisputeFlowVo buildRunningFlowVo(Long orderId, String procInsId, Map<String, Object> runtimeVars) {
OrderDisputeFlowVo vo = new OrderDisputeFlowVo();
vo.setOrderId(orderId);
vo.setProcInsId(procInsId);
vo.setStatus("RUNNING");
Map<String, Object> variables = runtimeVars != null ? runtimeVars : Collections.emptyMap();
applyVariables(vo, variables);
HistoricProcessInstance his = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(procInsId)
.singleResult();
if (his != null) {
vo.setStartTime(his.getStartTime());
vo.setEndTime(his.getEndTime());
}
Task currentTask = taskService.createTaskQuery().processInstanceId(procInsId).active().singleResult();
if (currentTask != null) {
vo.setCurrentTaskId(currentTask.getId());
vo.setCurrentTaskKey(currentTask.getTaskDefinitionKey());
vo.setCurrentTaskName(currentTask.getName());
vo.setCurrentAssignee(currentTask.getAssignee());
}
vo.setSteps(buildSteps(procInsId));
return vo;
}
private void applyVariables(OrderDisputeFlowVo vo, Map<String, Object> variables) {
Map<String, Object> safe = variables != null ? variables : Collections.emptyMap();
vo.setVariables(safe);
vo.setOrderCode(MapUtil.getStr(safe, "orderCode"));
vo.setCustomerName(MapUtil.getStr(safe, "customerName"));
}
private List<OrderDisputeFlowStepVo> buildSteps(String procInsId) {
List<Comment> comments = taskService.getProcessInstanceComments(procInsId);
Map<String, String> lastCommentByTaskId = new HashMap<>();
if (CollUtil.isNotEmpty(comments)) {
for (Comment c : comments) {
if (StringUtils.isNotBlank(c.getTaskId())) {
lastCommentByTaskId.put(c.getTaskId(), c.getFullMessage());
}
}
}
HistoricTaskInstanceQuery taskQuery = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(procInsId)
.orderByHistoricTaskInstanceStartTime()
.asc();
List<HistoricTaskInstance> taskInstances = taskQuery.list();
if (CollUtil.isEmpty(taskInstances)) {
return Collections.emptyList();
}
List<OrderDisputeFlowStepVo> stepVos = new ArrayList<>();
for (HistoricTaskInstance ti : taskInstances) {
OrderDisputeFlowStepVo stepVo = new OrderDisputeFlowStepVo();
stepVo.setStepKey(ti.getTaskDefinitionKey());
stepVo.setStepName(ti.getName());
stepVo.setAssignee(ti.getAssignee());
stepVo.setStartTime(ti.getStartTime());
stepVo.setEndTime(ti.getEndTime());
stepVo.setComment(lastCommentByTaskId.get(ti.getId()));
stepVos.add(stepVo);
}
return stepVos;
}
}

View File

@@ -0,0 +1,47 @@
package com.gear.oa.workflow;
import lombok.RequiredArgsConstructor;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import java.io.InputStream;
@RequiredArgsConstructor
@Component
public class OrderDisputeProcessDeployer implements ApplicationRunner {
public static final String PROCESS_KEY = "oa_order_dispute";
private static final String BPMN_RESOURCE = "processes/oa_order_dispute.bpmn20.xml";
private final RepositoryService repositoryService;
@Override
public void run(ApplicationArguments args) throws Exception {
ProcessDefinition existing = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey(PROCESS_KEY)
.latestVersion()
.singleResult();
if (existing != null) {
return;
}
ClassPathResource resource = new ClassPathResource(BPMN_RESOURCE);
try (InputStream inputStream = resource.getInputStream()) {
Deployment deployment = repositoryService.createDeployment()
.name("订单异议处理")
.key(PROCESS_KEY)
.addInputStream(BPMN_RESOURCE, inputStream)
.deploy();
repositoryService.createProcessDefinitionQuery()
.deploymentId(deployment.getId())
.latestVersion()
.singleResult();
}
}
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="http://gear.com/oa/dispute">
<process id="oa_order_dispute" name="订单异议处理" isExecutable="true">
<startEvent id="startEvent" />
<sequenceFlow id="flow_start_accept" sourceRef="startEvent" targetRef="acceptTask" />
<userTask id="acceptTask" name="异议接收" flowable:assignee="${acceptUserId}" />
<sequenceFlow id="flow_accept_physical" sourceRef="acceptTask" targetRef="physicalTask" />
<userTask id="physicalTask" name="实物确认" flowable:assignee="${physicalUserId}" />
<sequenceFlow id="flow_physical_analysis" sourceRef="physicalTask" targetRef="analysisTask" />
<userTask id="analysisTask" name="原因分析" flowable:assignee="${analysisUserId}" />
<sequenceFlow id="flow_analysis_plan" sourceRef="analysisTask" targetRef="planTask" />
<userTask id="planTask" name="处置方案" flowable:assignee="${planUserId}" />
<sequenceFlow id="flow_plan_visit" sourceRef="planTask" targetRef="visitTask" />
<userTask id="visitTask" name="闭环回访" flowable:assignee="${visitUserId}" />
<sequenceFlow id="flow_visit_end" sourceRef="visitTask" targetRef="endEvent" />
<endEvent id="endEvent" />
</process>
</definitions>