feat: 引擎多出口路由/退回/移交/或签补齐 + 定义 layout 坐标
build / build-push (push) Successful in 5m11s

- 多出口:FlwTask 增 edge_name 列(sql/20260916),FlwTaskRepository.claimAndFinish
  同步落列;FlowTaskService.handle 出度>1 时 edgeName 必填;FlowEngine.continueAdvance
  按 edge name 选路(单出口原路径逐字节兼容,未命中不推进);FlowAdvanceService/
  FlowNodeAdvanceListener 透传;BranchScope 从 FlowEngine 提取
- DefValidator:多出口出口命名必填且同节点唯一;跨分支连线发布期明确拒绝
- 退回 POST /task/{id}/returnBack:默认退主管线最近已批节点,精确失效当前节点待办,
  split 幂等 upsert 使退回重走并行段不撞 flw_branch uk
- 移交 POST /task/{id}/transfer:TRANSFERRED(5)+新建待办,AssigneeResolver.requireUserName
- 或签 ANY 补齐:首个同意失效同节点兄弟待办
- 已办口径/撤回守卫:status in (1,2,4,5);新事件 flow.returned/flow.transferred
- FlowDiagramService:EdgeVo.name/NodeVo.state/DiagramVo.layout,办件图状态计算
- 新 IT:多出口×3、ANY 或签、退回重走并行段、退回/移交/撤回

mvn test 84/84 绿
This commit is contained in:
chenxing
2026-09-16 22:23:02 +08:00
parent 55e3a3f800
commit b8441065d0
31 changed files with 981 additions and 126 deletions
@@ -5,12 +5,15 @@ import com.yunhai.flowcenter.engine.FlowEngine;
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.entity.FlwInstance;
import com.yunhai.flowcenter.entity.FlwTask;
import com.yunhai.flowcenter.enums.FlwEnums;
import com.yunhai.flowcenter.model.FlowModels;
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
import com.yunhai.flowcenter.repository.FlwTaskRepository;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.openapi.client.OpenApiUserClient;
import com.yunhai.platform.openapi.base.OpenApiUserClient;
import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.platform.utils.model.BoaUser;
import lombok.RequiredArgsConstructor;
@@ -18,7 +21,11 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
/**
* 流程示意图出图(方案 §3.2):后端按视图解析好节点 label/subLabel,前端只画图不解析。
@@ -42,6 +49,8 @@ public class FlowDiagramService {
private final FlwDefRepository defRepository;
private final FlwAssigneeMapRepository mapRepository;
private final FlwInstanceRepository instanceRepository;
private final FlwTaskRepository taskRepository;
private final FlowEngine flowEngine;
private final OpenApiUserClient userClient;
@@ -87,12 +96,65 @@ public class FlowDiagramService {
FlowModels.EdgeVo ev = new FlowModels.EdgeVo();
ev.setFrom(e.from);
ev.setTo(e.to);
ev.setName(e.name);
edges.add(ev);
}
vo.setEdges(edges);
// 画布坐标透传(设计器保存;缺失时前端自行 Kahn 布局)
if (model.layout != null && !model.layout.isEmpty()) {
Map<String, Map<String, Integer>> layout = new HashMap<>();
for (Map.Entry<String, FlowDefModel.Layout> en : model.layout.entrySet()) {
if (en.getValue() == null) {
continue;
}
Map<String, Integer> p = new HashMap<>();
p.put("x", en.getValue().x);
p.put("y", en.getValue().y);
layout.put(en.getKey(), p);
}
vo.setLayout(layout);
}
// instance 口径:按任务轨迹给节点上运行时状态(前端高亮在办节点)
if (instanceId != null) {
Map<String, String> states = nodeStates(instanceId, model);
for (FlowModels.NodeVo nv : nodes) {
nv.setState(states.get(nv.getId()));
}
}
return vo;
}
/**
* 办件节点运行时状态:done=已有已同意任务 / rejected=有拒绝任务(终态前)/
* active=在 currentNodes / pending=其余。active 优先于 done(退回后重到同节点算在办)。
*/
private Map<String, String> nodeStates(Long instanceId, FlowDefModel model) {
Map<String, String> states = new HashMap<>();
Set<String> activeNodes = new HashSet<>();
FlwInstance instance = instanceRepository.findById(instanceId).orElse(null);
if (instance != null && instance.getCurrentNodes() != null && !instance.getCurrentNodes().isEmpty()) {
activeNodes.addAll(java.util.Arrays.asList(instance.getCurrentNodes().split(",")));
}
for (FlowDefModel.Node n : model.nodes) {
states.put(n.id, null);
}
for (FlwTask t : taskRepository.findByInstanceIdOrderByIdAsc(instanceId)) {
String nodeState = states.get(t.getNodeId());
if (t.getStatus() == FlwEnums.TaskStatus.REJECTED.getCode()) {
states.put(t.getNodeId(), "rejected");
} else if (t.getStatus() == FlwEnums.TaskStatus.APPROVED.getCode()
&& nodeState == null && !activeNodes.contains(t.getNodeId())) {
states.put(t.getNodeId(), "done");
}
}
for (String nodeId : activeNodes) {
if (states.containsKey(nodeId)) {
states.put(nodeId, "active");
}
}
return states;
}
private FlowModels.NodeVo toNodeVo(Long schId, FlowDefModel.Node n, String mode) {
FlowModels.NodeVo vo = new FlowModels.NodeVo();
vo.setId(n.id);
@@ -126,8 +126,7 @@ public class FlowInstanceService {
throw new BizException(ErrorCode.PARAM_ERROR, "仅审批中的办件可撤回");
}
if (!taskRepository.findByInstanceIdAndStatusIn(instanceId,
java.util.Arrays.asList(FlwEnums.TaskStatus.APPROVED.getCode(),
FlwEnums.TaskStatus.REJECTED.getCode())).isEmpty()) {
FlwEnums.TaskStatus.handledCodes()).isEmpty()) {
throw new BizException(ErrorCode.PARAM_ERROR, "已有任务被办理,不能撤回");
}
instance.setStatus(FlwEnums.InstanceStatus.WITHDRAWN.getCode());
@@ -5,8 +5,8 @@ import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.platform.openapi.client.OpenApiGroupClient;
import com.yunhai.platform.openapi.client.OpenApiUserClient;
import com.yunhai.platform.openapi.base.OpenApiGroupClient;
import com.yunhai.platform.openapi.base.OpenApiUserClient;
import com.yunhai.platform.utils.model.BoaGroup;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -1,5 +1,7 @@
package com.yunhai.flowcenter.service;
import com.yunhai.flowcenter.engine.BranchScope;
import com.yunhai.flowcenter.engine.FlowDefModel;
import com.yunhai.flowcenter.engine.FlowEngine;
import com.yunhai.flowcenter.entity.FlwBranch;
import com.yunhai.flowcenter.entity.FlwDef;
@@ -111,6 +113,7 @@ public class FlowQueryService {
row.put("handleTime", str(t.getHandleTime()));
row.put("deadline", str(t.getDeadline()));
row.put("urgeCount", t.getUrgeCount());
row.put("edgeName", t.getEdgeName());
trace.add(row);
}
detail.setTrace(trace);
@@ -135,6 +138,53 @@ public class FlowQueryService {
}
FlwDef def = defRepository.findById(instance.getDefId()).orElse(null);
detail.setNodeSummary(def == null ? "" : FlowEngine.nodeSummary(flowEngine.parseModel(def)));
// 办理面板数据(出口/退回目标/移交标记):仅对持本人待办的查看者填充
FlwTask myPend = viewerId == null ? null : tasks.stream()
.filter(t -> viewerId.equals(t.getAssigneeId())
&& t.getStatus() == FlwEnums.TaskStatus.PENDING.getCode())
.findFirst().orElse(null);
if (myPend != null && def != null) {
FlowDefModel model = flowEngine.parseModel(def);
String curNodeId = myPend.getNodeId();
List<FlowDefModel.Edge> outs = model.outEdges(curNodeId);
if (outs.size() > 1) {
List<Map<String, Object>> outlets = new ArrayList<>();
for (FlowDefModel.Edge e : outs) {
Map<String, Object> o = new HashMap<>();
o.put("name", e.name);
o.put("toNodeId", e.to);
FlowDefModel.Node to = model.node(e.to);
o.put("toNodeName", to == null ? e.to
: (to.name == null || to.name.isEmpty() ? e.to : to.name));
outlets.add(o);
}
detail.setOutlets(outlets);
} else {
detail.setOutlets(new ArrayList<>());
}
// 可退回目标:办件已办过的主线审批节点(去重,排除当前节点)
Map<String, Map<String, Object>> targets = new java.util.LinkedHashMap<>();
for (FlwTask t : tasks) {
if (t.getStatus() != FlwEnums.TaskStatus.APPROVED.getCode()
|| t.getNodeId().equals(myPend.getNodeId())
|| BranchScope.keyOf(model, t.getNodeId()) != null) {
continue;
}
FlowDefModel.Node node = model.node(t.getNodeId());
if (node != null && FlowDefModel.TYPE_APPROVAL.equals(node.type)
&& !targets.containsKey(t.getNodeId())) {
Map<String, Object> r = new HashMap<>();
r.put("nodeId", node.id);
r.put("nodeName", node.name == null || node.name.isEmpty() ? node.id : node.name);
targets.put(node.id, r);
}
}
detail.setReturnTargets(new ArrayList<>(targets.values()));
detail.setCanTransfer(true);
} else {
detail.setOutlets(new ArrayList<>());
detail.setReturnTargets(new ArrayList<>());
}
return detail;
}
@@ -1,13 +1,17 @@
package com.yunhai.flowcenter.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.flowcenter.engine.AssigneeResolver;
import com.yunhai.flowcenter.engine.BranchScope;
import com.yunhai.flowcenter.engine.FlowDefModel;
import com.yunhai.flowcenter.engine.FlowEvent;
import com.yunhai.flowcenter.engine.FlowEngine;
import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.entity.FlwInstance;
import com.yunhai.flowcenter.entity.FlwTask;
import com.yunhai.flowcenter.enums.FlwEnums;
import com.yunhai.flowcenter.repository.FlwBranchRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
import com.yunhai.flowcenter.repository.FlwTaskRepository;
import com.yunhai.platform.apistd.BizException;
@@ -38,15 +42,20 @@ public class FlowTaskService {
private final FlwTaskRepository taskRepository;
private final FlwInstanceRepository instanceRepository;
private final FlwBranchRepository branchRepository;
private final FlwDefRepository defRepository;
private final AssigneeResolver assigneeResolver;
private final FlowEngine flowEngine;
private final ApplicationEventPublisher eventPublisher;
/**
* 办理(同意/拒绝收敛点,概览 §8.5 不变式 2)。
*
* @param edgeName 多出口节点的所选出口(办理按钮文案);单出结节忽略,出度>1 时必填
* @return 办理后的任务(含最新状态)
*/
@Transactional
public FlwTask handle(Long schId, Long taskId, boolean approved, String opinion, Integer operatorId) {
public FlwTask handle(Long schId, Long taskId, boolean approved, String opinion,
Integer operatorId, String edgeName) {
FlwTask task = taskRepository.findById(taskId)
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "任务不存在:" + taskId));
if (!task.getSchId().equals(schId)) {
@@ -58,10 +67,12 @@ public class FlowTaskService {
if (!approved && (opinion == null || opinion.trim().isEmpty())) {
throw new BizException(ErrorCode.PARAM_ERROR, "拒绝必须填写意见");
}
// 多出口选路校验(发布期已保证出口命名唯一):出度>1 必须带上出口名且能命中
String chosenEdge = normalizeEdgeName(task, approved, edgeName);
// 原子抢占:affected=1 才获得办理权(防双击/并发重复推进)
int target = approved ? FlwEnums.TaskStatus.APPROVED.getCode() : FlwEnums.TaskStatus.REJECTED.getCode();
LocalDateTime now = LocalDateTime.now();
if (taskRepository.claimAndFinish(taskId, target, now, opinion) != 1) {
if (taskRepository.claimAndFinish(taskId, target, now, opinion, chosenEdge) != 1) {
throw new BizException(ErrorCode.PARAM_ERROR, "任务已被办理或已失效,请刷新");
}
FlwTask handled = taskRepository.findById(taskId).orElse(null);
@@ -82,14 +93,63 @@ public class FlowTaskService {
if (opinion != null && !opinion.isEmpty()) {
data.put("opinion", opinion);
}
if (chosenEdge != null) {
data.put("edgeName", chosenEdge);
}
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.TASK_HANDLED,
instance.getId(), taskId, data));
if (!approved) {
rejectFlow(instance, task, opinion);
} else if (FlowDefModel.COUNTERSIGN_ANY.equals(countersignOf(task))) {
// 或签(ANY):任一人同意即放行,失效同节点兄弟待办——
// 推进监听器只认 pending==0,不失效会卡到所有人办完(行为等同 ALL 的历史缺陷)
taskRepository.invalidatePendingByNodeExcept(instance.getId(), task.getNodeId(), taskId);
}
return handled;
}
/** 旧签名兼容(单出口调用方) */
@Transactional
public FlwTask handle(Long schId, Long taskId, boolean approved, String opinion, Integer operatorId) {
return handle(schId, taskId, approved, opinion, operatorId, null);
}
/** 出度>1 的节点必须选有效出口;单出口一律置 null(旧前端零改动) */
private String normalizeEdgeName(FlwTask task, boolean approved, String edgeName) {
if (!approved) {
return null;
}
FlwDef def = defRepository.findById(instanceRepository.findById(task.getInstanceId())
.map(FlwInstance::getDefId).orElse(null)).orElse(null);
if (def == null) {
return null;
}
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
java.util.List<FlowDefModel.Edge> outs = model.outEdges(task.getNodeId());
if (outs.size() <= 1) {
return null;
}
if (edgeName == null || edgeName.trim().isEmpty()) {
throw new BizException(ErrorCode.PARAM_ERROR, "该节点有多个出口,请选择办理去向");
}
FlowDefModel.Edge hit = model.edgeByName(task.getNodeId(), edgeName.trim());
if (hit == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "无效的办理去向:" + edgeName);
}
return hit.name;
}
/** 该任务所属节点的会签模式(定义缺失/未配置按 ALL) */
private String countersignOf(FlwTask task) {
FlwDef def = defRepository.findById(instanceRepository.findById(task.getInstanceId())
.map(FlwInstance::getDefId).orElse(null)).orElse(null);
if (def == null) {
return FlowDefModel.COUNTERSIGN_ALL;
}
FlowDefModel.Node node = FlowDefModel.parse(objectMapper, def.getContentJson()).node(task.getNodeId());
return node == null || node.countersign == null ? FlowDefModel.COUNTERSIGN_ALL : node.countersign;
}
/** END_REJECTED 拒绝终态(概览 §5.3):同批待办失效→分支失效→办件=30→拒绝事件 */
private void rejectFlow(FlwInstance instance, FlwTask task, String opinion) {
instance.setStatus(FlwEnums.InstanceStatus.REJECTED.getCode());
@@ -106,6 +166,150 @@ public class FlowTaskService {
instance.getId(), task.getId(), data));
}
/**
* 退回(B3):把流程退回上游已办节点重新办理。当前任务置 RETURNED、当前节点待办全部失效、
* 目标节点重新激活建新任务;办件保持审批中(区别于拒绝=终态)。
* v1 限定主线节点(BranchScope.keyOf(target)==null),分支内退回登记二期。
*
* @param targetNodeId 退回目标节点;空=最近一个已办节点(任务轨迹 handleTime 降序)
*/
@Transactional
public void returnBack(Long schId, Long taskId, String targetNodeId, String opinion, Integer operatorId) {
FlwTask task = requirePendingTask(schId, taskId, operatorId);
FlwInstance instance = instanceRepository.findById(task.getInstanceId())
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
FlwDef def = defRepository.findById(instance.getDefId())
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "流程定义缺失"));
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
String target = resolveReturnTarget(model, instance, task, targetNodeId);
FlowDefModel.Node targetNode = model.node(target);
if (!FlowDefModel.TYPE_APPROVAL.equals(targetNode.type)) {
throw new BizException(ErrorCode.PARAM_ERROR, "退回目标必须是审批节点:" + target);
}
if (BranchScope.keyOf(model, target) != null) {
throw new BizException(ErrorCode.PARAM_ERROR, "暂不支持退回到并行分支内的节点");
}
LocalDateTime now = LocalDateTime.now();
String fromLabel = task.getNodeName() == null ? task.getNodeId() : task.getNodeName();
String targetLabel = targetNode.name == null ? target : targetNode.name;
if (taskRepository.claimAndFinish(taskId, FlwEnums.TaskStatus.RETURNED.getCode(), now,
"退回至 " + targetLabel + (opinion == null || opinion.isEmpty() ? "" : ":" + opinion), null) != 1) {
throw new BizException(ErrorCode.PARAM_ERROR, "任务已被办理或已失效,请刷新");
}
taskRepository.invalidatePendingByNode(instance.getId(), task.getNodeId());
flowEngine.returnTo(instance, def, task.getNodeId(), target);
instanceRepository.save(instance);
Map<String, Object> data = new HashMap<>();
data.put("fromNodeId", task.getNodeId());
data.put("fromNodeName", fromLabel);
data.put("targetNodeId", target);
data.put("targetNodeName", targetLabel);
if (opinion != null && !opinion.isEmpty()) {
data.put("opinion", opinion);
}
data.put("targetUserIds", taskRepository
.findByInstanceIdAndStatus(instance.getId(), FlwEnums.TaskStatus.PENDING.getCode()).stream()
.filter(t -> target.equals(t.getNodeId()))
.map(FlwTask::getAssigneeId)
.collect(java.util.stream.Collectors.toList()));
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.RETURNED, instance.getId(), taskId, data));
}
/** 退回目标解析:显式指定须合法;缺省取最近一个已办节点 */
private String resolveReturnTarget(FlowDefModel model, FlwInstance instance, FlwTask task,
String targetNodeId) {
if (targetNodeId != null && !targetNodeId.trim().isEmpty()) {
FlowDefModel.Node node = model.node(targetNodeId.trim());
if (node == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "退回目标节点不存在:" + targetNodeId);
}
if (node.id.equals(task.getNodeId())) {
throw new BizException(ErrorCode.PARAM_ERROR, "不能退回当前节点");
}
return node.id;
}
return taskRepository.findByInstanceIdOrderByIdAsc(instance.getId()).stream()
.filter(t -> FlwEnums.TaskStatus.APPROVED.getCode() == t.getStatus())
.filter(t -> !task.getNodeId().equals(t.getNodeId()))
.filter(t -> BranchScope.keyOf(model, t.getNodeId()) == null)
.reduce((first, second) -> second)
.map(FlwTask::getNodeId)
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR,
"无可退回的上游节点,请使用拒绝"));
}
/**
* 移交(B4):办理权转给他人。当前任务置 TRANSFERRED 留痕,同节点新建一条待办给目标人
* (deadline 沿用原任务);推进监听器因新待办 pending>0 自然不放行,无需改推进逻辑。
* 目标人存在性由 AssigneeResolver 严格校验(查不到必须拒绝,否则产生僵尸待办)。
*/
@Transactional
public void transfer(Long schId, Long taskId, Integer toUserId, String reason, Integer operatorId) {
if (toUserId == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "缺少移交对象");
}
FlwTask task = requirePendingTask(schId, taskId, operatorId);
if (toUserId.equals(task.getAssigneeId())) {
throw new BizException(ErrorCode.PARAM_ERROR, "不能移交给当前办理人本人");
}
FlwInstance instance = instanceRepository.findById(task.getInstanceId())
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
String toUserName = assigneeResolver.requireUserName(toUserId);
LocalDateTime now = LocalDateTime.now();
String fromLabel = task.getNodeName() == null ? task.getNodeId() : task.getNodeName();
if (taskRepository.claimAndFinish(taskId, FlwEnums.TaskStatus.TRANSFERRED.getCode(), now,
"移交给 " + (toUserName.isEmpty() ? toUserId : toUserName)
+ (reason == null || reason.isEmpty() ? "" : ":" + reason), null) != 1) {
throw new BizException(ErrorCode.PARAM_ERROR, "任务已被办理或已失效,请刷新");
}
FlwTask newTask = new FlwTask();
newTask.setInstanceId(task.getInstanceId());
newTask.setSchId(task.getSchId());
newTask.setNodeId(task.getNodeId());
newTask.setNodeName(task.getNodeName());
newTask.setAssigneeId(toUserId);
newTask.setAssigneeName(toUserName);
newTask.setStatus(FlwEnums.TaskStatus.PENDING.getCode());
newTask.setArriveTime(now);
newTask.setDeadline(task.getDeadline());
newTask.setUrgeCount(0);
taskRepository.save(newTask);
Map<String, Object> data = new HashMap<>();
data.put("fromNodeId", task.getNodeId());
data.put("fromNodeName", fromLabel);
data.put("fromUserId", task.getAssigneeId());
data.put("fromUserName", task.getAssigneeName());
data.put("toUserId", toUserId);
data.put("toUserName", toUserName);
data.put("taskId", newTask.getId());
if (reason != null && !reason.isEmpty()) {
data.put("opinion", reason);
}
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.TRANSFERRED,
instance.getId(), newTask.getId(), data));
}
/** 待办任务守卫:存在、本校、本人办理、仍待办 */
private FlwTask requirePendingTask(Long schId, Long taskId, Integer operatorId) {
FlwTask task = taskRepository.findById(taskId)
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "任务不存在:" + taskId));
if (!task.getSchId().equals(schId)) {
throw new BizException(ErrorCode.FORBIDDEN, "任务不属于该学校");
}
if (!task.getAssigneeId().equals(operatorId)) {
throw new BizException(ErrorCode.FORBIDDEN, "仅任务办理人本人可操作");
}
if (task.getStatus() != FlwEnums.TaskStatus.PENDING.getCode()) {
throw new BizException(ErrorCode.PARAM_ERROR, "仅待办任务可操作");
}
return task;
}
/** 手工催办(提交人/管理员):urge_count+1 + URGE 事件(UserMsg 通道) */
@Transactional
public void urge(Long schId, Long taskId, int maxTimes) {
@@ -15,7 +15,7 @@ import com.yunhai.flowcenter.repository.FlwTaskRepository;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.platform.openapi.client.OpenApiUserClient;
import com.yunhai.platform.openapi.base.OpenApiUserClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;