feat: 提交流程中心后端代码
build / build-push (push) Successful in 28s

Spring Boot 流程引擎服务:流程定义/实例/任务、审批人解析、
回调分发、oapi 接口及单元测试。
保留远程 .gitea/workflows/build.yml 与 docker/Dockerfile,
新增 .gitignore 排除 target/ 构建产物。
This commit is contained in:
chenxing
2026-09-15 10:42:56 +08:00
parent 0296b2b5dd
commit c1103aeb29
65 changed files with 7119 additions and 0 deletions
@@ -0,0 +1,165 @@
package com.yunhai.flowcenter.callback;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.flowcenter.engine.FlowEvent;
import com.yunhai.flowcenter.entity.FlwCallbackLog;
import com.yunhai.flowcenter.entity.FlwCallbackReg;
import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.entity.FlwInstance;
import com.yunhai.flowcenter.enums.FlwEnums;
import com.yunhai.flowcenter.repository.FlwCallbackLogRepository;
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 回调分发器(概览 §8.4 可靠性设计):引擎 AFTER_COMMIT 事件 → 匹配 flw_callback_reg
* (flowGuid + 订阅事件)→ 落 flw_callback_log(status=0) → 异步首推。
* 落库成功即视为主业务完成,推送失败不影响审批(异步解耦是特性)。
* @Scheduled(fixedDelay=30s) 扫 (status=0, next_retry_time<=now) 重推;死信留管理端手工重推。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CallbackDispatcher {
private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final FlwInstanceRepository instanceRepository;
private final FlwDefRepository defRepository;
private final FlwCallbackRegRepository regRepository;
private final FlwCallbackLogRepository logRepository;
private final CallbackLogStore logStore;
private final CallbackPushService pushService;
private final CallbackProperties properties;
private final ObjectMapper objectMapper;
private volatile ExecutorService pushExecutor;
private ExecutorService executor() {
if (pushExecutor == null) {
synchronized (this) {
if (pushExecutor == null) {
pushExecutor = Executors.newFixedThreadPool(Math.max(1, properties.getPushPoolSize()));
}
}
}
return pushExecutor;
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
public void onFlowEvent(FlowEvent event) {
// URGE 仅站内信通道,不进回调事件流(概览 §8.3 事件枚举固定六种)
if (event.getType() == FlowEvent.Type.URGE) {
return;
}
try {
dispatch(event);
} catch (Exception e) {
// 分发失败只影响回调(对账兜底会追平),绝不能反噬引擎主流程
log.error("[flow-center] 回调分发异常:instance={} event={} {}",
event.getInstanceId(), event.getType().getCode(), e.toString(), e);
}
}
private void dispatch(FlowEvent event) {
FlwInstance instance = instanceRepository.findById(event.getInstanceId()).orElse(null);
if (instance == null) {
return;
}
FlwDef def = defRepository.findById(instance.getDefId()).orElse(null);
if (def == null) {
return;
}
String eventCode = event.getType().getCode();
List<FlwCallbackReg> regs = regRepository.findBySchIdAndFlowGuidAndStatus(
instance.getSchId(), def.getFlowGuid(), 0);
for (FlwCallbackReg reg : regs) {
if (!subscribed(reg.getEventTypes(), eventCode)) {
continue;
}
persistAndPush(reg, instance, def, event);
}
}
private void persistAndPush(FlwCallbackReg reg, FlwInstance instance, FlwDef def, FlowEvent event) {
try {
String eventId = UUID.randomUUID().toString().replace("-", "").toLowerCase();
Map<String, Object> payload = new HashMap<>();
payload.put("eventId", eventId);
payload.put("event", event.getType().getCode());
payload.put("schId", instance.getSchId());
payload.put("flowGuid", def.getFlowGuid());
payload.put("instanceId", instance.getId());
payload.put("instanceVersion", instance.getVersion());
payload.put("bizType", instance.getBizType());
payload.put("bizId", instance.getBizId());
payload.put("bizTitle", instance.getBizTitle());
payload.put("occurredTime", LocalDateTime.now().format(TIME));
Map<String, Object> data = new HashMap<>();
if (event.getData() != null) {
data.putAll(event.getData());
}
payload.put("data", data);
String payloadJson = objectMapper.writeValueAsString(payload);
FlwCallbackLog callbackLog = new FlwCallbackLog();
callbackLog.setSchId(instance.getSchId());
callbackLog.setRegId(reg.getId());
callbackLog.setEventId(eventId);
callbackLog.setEvent(event.getType().getCode());
callbackLog.setInstanceId(instance.getId());
callbackLog.setBizType(instance.getBizType());
callbackLog.setBizId(instance.getBizId());
callbackLog.setPayloadJson(payloadJson);
callbackLog.setStatus(FlwEnums.CallbackLogStatus.PENDING.getCode());
callbackLog.setRetryCount(0);
callbackLog.setNextRetryTime(LocalDateTime.now());
// REQUIRES_NEW 落库(afterCommit 阶段原事务已完结,REQUIRED 会挂空事务)
Long logId = logStore.persist(callbackLog);
// 异步首推:失败由退避重试接手(1m 起)
executor().submit(() -> pushService.pushAsync(logId));
} catch (Exception e) {
log.error("[flow-center] 回调日志落库失败(对账兜底会追平):instance={} event={} {}",
instance.getId(), event.getType().getCode(), e.toString(), e);
}
}
/** 重推扫描(fixedDelay 串行防重入;推拉结合里 Webhook 永远只是加速器) */
@Scheduled(fixedDelay = 30_000)
public void scanRetry() {
List<FlwCallbackLog> due = logRepository.findTop50ByStatusAndNextRetryTimeLessThanEqualOrderByNextRetryTimeAsc(
FlwEnums.CallbackLogStatus.PENDING.getCode(), LocalDateTime.now());
for (FlwCallbackLog callbackLog : due) {
executor().submit(() -> pushService.pushAsync(callbackLog.getId()));
}
}
/** 订阅匹配:空=全部;逗号分隔事件码包含即命中 */
private static boolean subscribed(String eventTypes, String eventCode) {
if (eventTypes == null || eventTypes.trim().isEmpty()) {
return true;
}
for (String t : eventTypes.split(",")) {
if (eventCode.equals(t.trim())) {
return true;
}
}
return false;
}
}