Spring Boot 流程引擎服务:流程定义/实例/任务、审批人解析、 回调分发、oapi 接口及单元测试。 保留远程 .gitea/workflows/build.yml 与 docker/Dockerfile, 新增 .gitignore 排除 target/ 构建产物。
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package com.yunhai.flowcenter.notify;
|
||||
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.http.IDHttp;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* UserMsg 站内信客户端(概览 §七 契约,M1 手工三件套路线的<b>直达版</b>):
|
||||
* 经共享 IDHttp(yh-openapi 装配的出站自动签名 RestTemplate,X-App-Key/X-Timestamp/X-Signature
|
||||
* = SignUtil.signRequest 请求级 canonical,与 svc-hall FlowCenterClient 同一套通道)直调
|
||||
* UserMsg /oapi/msg/**。前提:UserMsg 侧 trusted-apps 白名单收本应用 appKey(本地占位 100019);
|
||||
* SDK(yh-usermsg-spring-boot-starter)落地后整体切换为 SDK 调用,契约不变。
|
||||
* 发送方不阻断主业务:失败只记日志(对齐 PartyLearn 容错语义),由调用方在 AFTER_COMMIT 监听器里容错。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserMsgClient {
|
||||
|
||||
private final IDHttp http;
|
||||
|
||||
@Value("${flow-center.usermsg.enabled:false}")
|
||||
private boolean enabled;
|
||||
|
||||
@Value("${flow-center.usermsg.base-url:http://localhost:8080}")
|
||||
private String baseUrl;
|
||||
|
||||
public boolean enabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/** 发送站内信(content 必填;msgNo 幂等,重发安全) */
|
||||
public void sendUserMsg(Integer schId, List<Integer> userIds, Integer msgType, String bizType,
|
||||
String bizId, String title, String content, String jumpPath, String msgNo,
|
||||
Integer expireDays) {
|
||||
if (!enabled || userIds == null || userIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
SendMsgRequest body = new SendMsgRequest();
|
||||
body.setUserIds(userIds);
|
||||
body.setMsgType(msgType);
|
||||
body.setBizType(bizType);
|
||||
body.setBizId(bizId);
|
||||
body.setTitle(title);
|
||||
body.setContent(content);
|
||||
if (jumpPath != null) {
|
||||
body.setJumpType(1);
|
||||
body.setJumpPath(jumpPath);
|
||||
}
|
||||
body.setMsgNo(msgNo);
|
||||
body.setSchId(schId);
|
||||
body.setExpireDays(expireDays);
|
||||
try {
|
||||
http.post(trimTrailing(baseUrl) + "/oapi/msg/sendUserMsg", body, Object.class);
|
||||
} catch (BizException e) {
|
||||
log.warn("[flow-center] UserMsg 发送被拒:msgNo={} msg={}", msgNo, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.warn("[flow-center] UserMsg 发送失败(不阻断主业务):msgNo={} {}", msgNo, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/** 待办角标增量(+1 到达 / -1 办结) */
|
||||
public void todoInc(Integer schId, List<Integer> userIds, int count) {
|
||||
if (!enabled || userIds == null || userIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
TodoCountRequest body = new TodoCountRequest();
|
||||
body.setUserIds(userIds);
|
||||
body.setCount(count);
|
||||
body.setSchId(schId);
|
||||
try {
|
||||
http.post(trimTrailing(baseUrl) + "/oapi/msg/todo/inc", body, Object.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("[flow-center] UserMsg 角标增量失败(不阻断主业务):userIds={} count={} {}",
|
||||
userIds, count, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static String trimTrailing(String url) {
|
||||
return url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
|
||||
}
|
||||
|
||||
/** /oapi/msg/sendUserMsg 请求体(UserMsg SendMsgRequest 同构,字段逐字对齐) */
|
||||
@Data
|
||||
public static class SendMsgRequest {
|
||||
private List<Integer> userIds;
|
||||
private String title;
|
||||
/** 必填(UserMsg @NotBlank) */
|
||||
private String content;
|
||||
private String msgNo;
|
||||
private String bizType;
|
||||
private String bizId;
|
||||
/** 0系统通知/1业务提醒/2审批待办 */
|
||||
private Integer msgType;
|
||||
/** 0无跳转/1应用内路径/2外链 */
|
||||
private Integer jumpType;
|
||||
private String jumpPath;
|
||||
private Integer schId;
|
||||
private Integer expireDays;
|
||||
}
|
||||
|
||||
/** /oapi/msg/todo/inc 请求体(UserMsg TodoCountRequest 同构) */
|
||||
@Data
|
||||
public static class TodoCountRequest {
|
||||
private List<Integer> userIds;
|
||||
private Integer count;
|
||||
private Integer schId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.yunhai.flowcenter.notify;
|
||||
|
||||
import com.yunhai.flowcenter.engine.FlowEvent;
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
import com.yunhai.flowcenter.entity.FlwTask;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.event.TransactionPhase;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 站内信/待办角标通知(概览 §七 契约,回调通知"业务系统"、本类通知"人",两通道互不替代):
|
||||
* - 任务到达:msgType=2、bizType=flow.task、msgNo=task:{taskId}、jumpPath=/task/{taskId}、todo +1
|
||||
* - 任务被办理:todo -1(办理人)
|
||||
* - 完成/拒绝/撤回:msgType=1、bizType=flow.result、content 必填(结果+意见)、msgNo 带 version
|
||||
* (重提重启多轮终态互不幂等吞掉)
|
||||
* - 催办:bizType=flow.urge、msgNo=urge:{taskId}:{第N次}
|
||||
* 全部在 AFTER_COMMIT 监听器里发送,失败不阻断(UserMsgClient 内部容错)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UserMsgNotifier {
|
||||
|
||||
private final UserMsgClient userMsgClient;
|
||||
private final FlwTaskRepository taskRepository;
|
||||
private final FlwInstanceRepository instanceRepository;
|
||||
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
|
||||
public void onFlowEvent(FlowEvent event) {
|
||||
switch (event.getType()) {
|
||||
case TASK_CREATED:
|
||||
notifyTaskCreated(event);
|
||||
break;
|
||||
case TASK_HANDLED:
|
||||
notifyTaskHandled(event);
|
||||
break;
|
||||
case COMPLETED:
|
||||
case REJECTED:
|
||||
case WITHDRAWN:
|
||||
notifyResult(event);
|
||||
break;
|
||||
case URGE:
|
||||
notifyUrge(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyTaskCreated(FlowEvent event) {
|
||||
FlwTask task = taskRepository.findById(event.getTaskId()).orElse(null);
|
||||
FlwInstance instance = instanceOf(event.getInstanceId());
|
||||
if (task == null || instance == null) {
|
||||
return;
|
||||
}
|
||||
String title = "您有新的待办:" + safe(instance.getBizTitle());
|
||||
String content = "「" + safe(instance.getBizTitle()) + "」等待您审批"
|
||||
+ (task.getNodeName() == null ? "" : "(" + task.getNodeName() + ")");
|
||||
userMsgClient.sendUserMsg(instance.getSchId().intValue(),
|
||||
Collections.singletonList(task.getAssigneeId()), 2,
|
||||
"flow.task", String.valueOf(instance.getId()), title, content,
|
||||
"/task/" + task.getId(), "task:" + task.getId(), 30);
|
||||
userMsgClient.todoInc(instance.getSchId().intValue(),
|
||||
Collections.singletonList(task.getAssigneeId()), 1);
|
||||
}
|
||||
|
||||
private void notifyTaskHandled(FlowEvent event) {
|
||||
FlwTask task = taskRepository.findById(event.getTaskId()).orElse(null);
|
||||
FlwInstance instance = instanceOf(event.getInstanceId());
|
||||
if (task == null || instance == null) {
|
||||
return;
|
||||
}
|
||||
userMsgClient.todoInc(instance.getSchId().intValue(),
|
||||
Collections.singletonList(task.getAssigneeId()), -1);
|
||||
}
|
||||
|
||||
private void notifyResult(FlowEvent event) {
|
||||
FlwInstance instance = instanceOf(event.getInstanceId());
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
Object result = event.getData() == null ? null : event.getData().get("result");
|
||||
String outcome;
|
||||
String bizTitle = safe(instance.getBizTitle());
|
||||
switch (event.getType()) {
|
||||
case COMPLETED:
|
||||
outcome = "已通过审批";
|
||||
break;
|
||||
case REJECTED:
|
||||
outcome = "审批被拒绝";
|
||||
break;
|
||||
default:
|
||||
outcome = "已撤回";
|
||||
break;
|
||||
}
|
||||
StringBuilder content = new StringBuilder("您提交的「" + bizTitle + "」" + outcome);
|
||||
if (result != null) {
|
||||
content.append("(结果:").append(result).append(")");
|
||||
}
|
||||
Object opinion = event.getData() == null ? null : event.getData().get("opinion");
|
||||
if (opinion != null && !String.valueOf(opinion).isEmpty()) {
|
||||
content.append(",意见:").append(opinion);
|
||||
}
|
||||
userMsgClient.sendUserMsg(instance.getSchId().intValue(),
|
||||
Collections.singletonList(instance.getSubmitterId()), 1,
|
||||
"flow.result", String.valueOf(instance.getId()),
|
||||
"办件状态更新:" + bizTitle, content.toString(),
|
||||
"/instance/" + instance.getId(),
|
||||
"result:" + instance.getId() + ":" + instance.getVersion(), 30);
|
||||
}
|
||||
|
||||
private void notifyUrge(FlowEvent event) {
|
||||
FlwTask task = taskRepository.findById(event.getTaskId()).orElse(null);
|
||||
FlwInstance instance = instanceOf(event.getInstanceId());
|
||||
if (task == null || instance == null) {
|
||||
return;
|
||||
}
|
||||
int count = task.getUrgeCount() == null ? 1 : task.getUrgeCount();
|
||||
String content = "「" + safe(instance.getBizTitle()) + "」任务("
|
||||
+ (task.getNodeName() == null ? task.getNodeId() : task.getNodeName())
|
||||
+ ")被催办(第 " + count + " 次),请尽快办理";
|
||||
userMsgClient.sendUserMsg(instance.getSchId().intValue(),
|
||||
Collections.singletonList(task.getAssigneeId()), 1,
|
||||
"flow.urge", String.valueOf(instance.getId()),
|
||||
"催办提醒:" + safe(instance.getBizTitle()), content,
|
||||
"/task/" + task.getId(), "urge:" + task.getId() + ":" + count, 7);
|
||||
}
|
||||
|
||||
private FlwInstance instanceOf(Long instanceId) {
|
||||
return instanceId == null ? null : instanceRepository.findById(instanceId).orElse(null);
|
||||
}
|
||||
|
||||
private static String safe(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user