From b962f444b60fcf5f5a29d70e66b3f87d7f271595 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Wed, 19 Aug 2026 20:35:00 +0800 Subject: [PATCH 01/16] =?UTF-8?q?feat(flow):=20=E6=B7=BB=E5=8A=A0=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 FlowEvent 基类及 FlowCreatedEvent、FlowDeletedEvent、FlowDeployedEvent 等事件类型 - 实现 FlowEventPublisher 用于本地事件发布到 Vert.x EventBus - 开发 FlowEventRabbitmqBridge 支持事件桥接到 RabbitMQ - 定义 FlowEventTypes 常量统一管理事件路由键 - 添加 FlowInstanceStartedEvent、FlowInstanceCompletedEvent、FlowInstanceFailedEvent 等运行时事件 - 集成事件发布到流程执行器和定义处理器中 - 配置 application.properties 中的 RabbitMQ 桥接选项 - 编写 FlowEventPublisherTest 和 FlowEventRabbitmqBridgeE2ETest 测试用例 - 移除旧的 StartFlowEvent 类 --- VERSION | 2 +- feature_list.json | 10 ++ flexmodel-sdks | 2 +- flexmodel-server/pom.xml | 14 ++ .../flow/event/FlowCreatedEvent.java | 30 ++++ .../flow/event/FlowDeletedEvent.java | 28 +++ .../flow/event/FlowDeployedEvent.java | 30 ++++ .../dev/flexmodel/flow/event/FlowEvent.java | 44 +++++ .../flow/event/FlowEventPublisher.java | 40 +++++ .../flow/event/FlowEventRabbitmqBridge.java | 129 ++++++++++++++ .../flexmodel/flow/event/FlowEventTypes.java | 31 ++++ .../event/FlowInstanceCompletedEvent.java | 35 ++++ .../flow/event/FlowInstanceFailedEvent.java | 33 ++++ .../flow/event/FlowInstanceStartedEvent.java | 35 ++++ .../event/FlowInstanceTerminatedEvent.java | 28 +++ .../flow/event/FlowUpdatedEvent.java | 28 +++ .../flexmodel/flow/event/StartFlowEvent.java | 17 -- .../flow/event/UserTaskCommittedEvent.java | 39 +++++ .../event/UserTaskRollbackSuspendedEvent.java | 39 +++++ .../flow/event/UserTaskSuspendedEvent.java | 42 +++++ .../flexmodel/flow/executor/FlowExecutor.java | 22 +++ .../flow/executor/UserTaskExecutor.java | 18 ++ .../flow/processor/DefinitionProcessor.java | 15 ++ .../flow/processor/RuntimeProcessor.java | 6 + .../src/main/resources/application.properties | 11 ++ .../flow/event/FlowEventPublisherTest.java | 88 ++++++++++ .../event/FlowEventRabbitmqBridgeE2ETest.java | 159 ++++++++++++++++++ .../event/FlowEventRabbitmqBridgeTest.java | 81 +++++++++ .../flow/event/FlowEventTestConsumer.java | 28 +++ .../flow/event/RabbitMqTestResource.java | 59 +++++++ flexmodel-ui | 2 +- flexmodel-website | 2 +- progress.md | 155 +++++++++++++++++ 33 files changed, 1281 insertions(+), 21 deletions(-) create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowCreatedEvent.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeletedEvent.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeployedEvent.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEvent.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventPublisher.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventTypes.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceCompletedEvent.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceFailedEvent.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceStartedEvent.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceTerminatedEvent.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowUpdatedEvent.java delete mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/StartFlowEvent.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/UserTaskCommittedEvent.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/UserTaskRollbackSuspendedEvent.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/UserTaskSuspendedEvent.java create mode 100644 flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventPublisherTest.java create mode 100644 flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java create mode 100644 flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeTest.java create mode 100644 flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventTestConsumer.java create mode 100644 flexmodel-server/src/test/java/dev/flexmodel/flow/event/RabbitMqTestResource.java diff --git a/VERSION b/VERSION index 45c7a584..8308b63a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.0.1 +v0.1.1 diff --git a/feature_list.json b/feature_list.json index c782c4c0..3aa01288 100644 --- a/feature_list.json +++ b/feature_list.json @@ -81,6 +81,16 @@ "dependencies": ["feat-001", "feat-002"], "status": "in-progress", "evidence": "重构完成: 数据模型简化 (去除 f_function_version 表/branch字段/TriggerType.HTTP),Java 后端简化 (14 files — 删除4个版本/内部资源文件,重写Service/Resource/Invoker/DTO),Deno Functions Runtime 重写 (file:// 部署模式替代 lazy load),前端重写 (Supabase-style 多文件编辑器+模板选择+简化API)。Java 424 files + TypeScript 0 errors 编译通过。待: 端到端集成测试 + Deno 运行时验证。" + }, + { + "id": "feat-011", + "name": "Flow - 生命周期事件(本地 EventBus + 可选 RabbitMQ 桥接)", + "description": "在 flow 核心关键生命周期点发布 11 种强类型本地事件(FlowCreated/Updated/Deployed/Deleted、FlowInstanceStarted/Completed/Failed/Terminated、UserTaskSuspended/Committed/RollbackSuspended),经 Vert.x EventBus 广播;新增可选 RabbitMQ 桥接 Bean,默认关闭、未配置不连 broker,本地事件照常发布。核心与 RabbitMQ 解耦,本地事件可被多个内部订阅者复用。", + "dependencies": [ + "feat-003" + ], + "status": "done", + "evidence": "新增 dev.flexmodel.flow.event 包:FlowEvent 基类 + 11 个具体事件 + FlowEventTypes 常量;FlowEventPublisher 本地发布;FlowEventRabbitmqBridge(Instance 延迟注入,转发由 SmallRye 通道 mp.messaging.outgoing.flow-events-out.enabled 单一控制,默认 false 注入 no-op emitter 不连 broker)。埋点 DefinitionProcessor/FlowExecutor/RuntimeProcessor.terminateProcess/UserTaskExecutor。application.properties 通道默认关闭+devservices=false。验证:mvn clean compile 通过;FlowEventPublisherTest(2)/FlowEventRabbitmqBridgeTest(2) 通过;DefinitionProcessorTest(4)/RuntimeProcessorTest(17) 回归通过。" } ] } diff --git a/flexmodel-sdks b/flexmodel-sdks index cb79787b..adcdf42c 160000 --- a/flexmodel-sdks +++ b/flexmodel-sdks @@ -1 +1 @@ -Subproject commit cb79787bd5b8a861bb18e8f3ed78e65d976e102e +Subproject commit adcdf42cfdf684c25a801d636699cc043b7b65c5 diff --git a/flexmodel-server/pom.xml b/flexmodel-server/pom.xml index e872c064..858b7fec 100644 --- a/flexmodel-server/pom.xml +++ b/flexmodel-server/pom.xml @@ -135,6 +135,20 @@ io.roastedroot quickjs4j-scripting-experimental + + + + io.quarkus + quarkus-messaging-rabbitmq + + + + org.testcontainers + rabbitmq + ${testcontainers.version} + test + + diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowCreatedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowCreatedEvent.java new file mode 100644 index 00000000..a1bd4090 --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowCreatedEvent.java @@ -0,0 +1,30 @@ +package dev.flexmodel.flow.event; + +import lombok.Getter; +import lombok.ToString; + +/** + * 流程定义创建事件。 + * + * @author cjbi + */ +@Getter +@ToString(callSuper = true) +public class FlowCreatedEvent extends FlowEvent { + + public static final String ROUTING_KEY = FlowEventTypes.FLOW_CREATED; + + private final String flowModuleId; + private final String flowKey; + + public FlowCreatedEvent(String projectId, String caller, String flowModuleId, String flowKey) { + super(projectId, caller); + this.flowModuleId = flowModuleId; + this.flowKey = flowKey; + } + + @Override + public String routingKey() { + return ROUTING_KEY; + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeletedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeletedEvent.java new file mode 100644 index 00000000..a9f7ba09 --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeletedEvent.java @@ -0,0 +1,28 @@ +package dev.flexmodel.flow.event; + +import lombok.Getter; +import lombok.ToString; + +/** + * 流程定义删除事件。 + * + * @author cjbi + */ +@Getter +@ToString(callSuper = true) +public class FlowDeletedEvent extends FlowEvent { + + public static final String ROUTING_KEY = FlowEventTypes.FLOW_DELETED; + + private final String flowModuleId; + + public FlowDeletedEvent(String projectId, String caller, String flowModuleId) { + super(projectId, caller); + this.flowModuleId = flowModuleId; + } + + @Override + public String routingKey() { + return ROUTING_KEY; + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeployedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeployedEvent.java new file mode 100644 index 00000000..36bb31fb --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeployedEvent.java @@ -0,0 +1,30 @@ +package dev.flexmodel.flow.event; + +import lombok.Getter; +import lombok.ToString; + +/** + * 流程定义部署事件。 + * + * @author cjbi + */ +@Getter +@ToString(callSuper = true) +public class FlowDeployedEvent extends FlowEvent { + + public static final String ROUTING_KEY = FlowEventTypes.FLOW_DEPLOYED; + + private final String flowModuleId; + private final String flowDeployId; + + public FlowDeployedEvent(String projectId, String caller, String flowModuleId, String flowDeployId) { + super(projectId, caller); + this.flowModuleId = flowModuleId; + this.flowDeployId = flowDeployId; + } + + @Override + public String routingKey() { + return ROUTING_KEY; + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEvent.java new file mode 100644 index 00000000..5177aab0 --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEvent.java @@ -0,0 +1,44 @@ +package dev.flexmodel.flow.event; + +import java.io.Serializable; + +/** + * Flow 生命周期事件基类。 + *

+ * 公共字段 {@code projectId}、{@code caller}、{@code timestamp}(构造时设为当前毫秒)。 + * 抽象方法 {@link #routingKey()} 既作 Vert.x EventBus 地址,也作 RabbitMQ 转发的 routing key。 + * 本地事件始终经 {@code FlowEventPublisher} 发布到 EventBus;RabbitMQ 转发可选。 + * + * @author cjbi + */ +public abstract class FlowEvent implements Serializable { + + private final String projectId; + private final String caller; + private final long timestamp; + + protected FlowEvent(String projectId, String caller) { + this.projectId = projectId; + this.caller = caller; + this.timestamp = System.currentTimeMillis(); + } + + /** + * 路由 key:兼作 EventBus 地址与 RabbitMQ routing key。 + * + * @return 不可为空的路由 key + */ + public abstract String routingKey(); + + public String getProjectId() { + return projectId; + } + + public String getCaller() { + return caller; + } + + public long getTimestamp() { + return timestamp; + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventPublisher.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventPublisher.java new file mode 100644 index 00000000..a935b92b --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventPublisher.java @@ -0,0 +1,40 @@ +package dev.flexmodel.flow.event; + +import io.vertx.mutiny.core.eventbus.EventBus; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Flow 生命周期事件本地发布器。 + *

+ * 始终将事件发布到 Vert.x EventBus(本地事件为机制本身),不与 RabbitMQ 配置耦合。 + * 全程 try/catch 仅 {@code LOGGER.warn},不抛出、不阻塞流程。 + * + * @author cjbi + */ +@ApplicationScoped +public class FlowEventPublisher { + + private static final Logger LOGGER = LoggerFactory.getLogger(FlowEventPublisher.class); + + @Inject + EventBus eventBus; + + /** + * 发布 Flow 生命周期事件。失败仅告警,不影响流程主链路。 + * + * @param event 不可为空的事件 + */ + public void publish(FlowEvent event) { + if (event == null) { + return; + } + try { + eventBus.publish(event.routingKey(), event); + } catch (Exception e) { + LOGGER.warn("publish flow event failed.||routingKey={}||event={}", event.routingKey(), event, e); + } + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java new file mode 100644 index 00000000..7d4d4193 --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java @@ -0,0 +1,129 @@ +package dev.flexmodel.flow.event; + +import io.smallrye.reactive.messaging.MutinyEmitter; +import io.smallrye.reactive.messaging.rabbitmq.OutgoingRabbitMQMetadata; +import io.quarkus.vertx.ConsumeEvent; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Flow 生命周期事件 RabbitMQ 桥接。 + *

+ * 订阅全部内部 EventBus 事件并转发到单个 topic 交换机(routing key 复用事件地址)。 + * 是否连 broker 由 SmallRye 通道 {@code mp.messaging.outgoing.flow-events-out.enabled} 单一控制: + * 默认关闭时 SmallRye 注入 no-op emitter,{@link #forward(FlowEvent)} 发送即丢弃、不连 broker; + * 本地事件照常经 {@link FlowEventPublisher} 发布。启用需置该通道 {@code enabled=true} 并提供 broker 连接配置。 + *

+ * 每个事件一个 {@code @ConsumeEvent} 方法、消费具体类型,沿用代码库已验证的同类型发布/消费模式, + * 避免多态反序列化不确定性。 + *

+ * 使用 {@link Instance} 延迟注入 {@link MutinyEmitter}:通道未启用时 SmallRye 提供 no-op emitter, + * bean 仍可正常创建,不影响本地事件广播。 + * + * @author cjbi + */ +@ApplicationScoped +public class FlowEventRabbitmqBridge { + + private static final Logger LOGGER = LoggerFactory.getLogger(FlowEventRabbitmqBridge.class); + + @Inject + @Channel("flow-events-out") + Instance> flowEventEmitterInstance; + + // 通道是否启用;默认关闭,禁用时 SmallRye 注入 no-op emitter,转发静默跳过、不连 broker + @ConfigProperty(name = "mp.messaging.outgoing.flow-events-out.enabled", defaultValue = "false") + boolean channelEnabled; + + @ConsumeEvent(value = FlowEventTypes.FLOW_CREATED, blocking = false) + public void onFlowCreated(FlowCreatedEvent event) { + forward(event); + } + + @ConsumeEvent(value = FlowEventTypes.FLOW_UPDATED, blocking = false) + public void onFlowUpdated(FlowUpdatedEvent event) { + forward(event); + } + + @ConsumeEvent(value = FlowEventTypes.FLOW_DEPLOYED, blocking = false) + public void onFlowDeployed(FlowDeployedEvent event) { + forward(event); + } + + @ConsumeEvent(value = FlowEventTypes.FLOW_DELETED, blocking = false) + public void onFlowDeleted(FlowDeletedEvent event) { + forward(event); + } + + @ConsumeEvent(value = FlowEventTypes.FLOW_INSTANCE_STARTED, blocking = false) + public void onFlowInstanceStarted(FlowInstanceStartedEvent event) { + forward(event); + } + + @ConsumeEvent(value = FlowEventTypes.FLOW_INSTANCE_COMPLETED, blocking = false) + public void onFlowInstanceCompleted(FlowInstanceCompletedEvent event) { + forward(event); + } + + @ConsumeEvent(value = FlowEventTypes.FLOW_INSTANCE_FAILED, blocking = false) + public void onFlowInstanceFailed(FlowInstanceFailedEvent event) { + forward(event); + } + + @ConsumeEvent(value = FlowEventTypes.FLOW_INSTANCE_TERMINATED, blocking = false) + public void onFlowInstanceTerminated(FlowInstanceTerminatedEvent event) { + forward(event); + } + + @ConsumeEvent(value = FlowEventTypes.USER_TASK_SUSPENDED, blocking = false) + public void onUserTaskSuspended(UserTaskSuspendedEvent event) { + forward(event); + } + + @ConsumeEvent(value = FlowEventTypes.USER_TASK_COMMITTED, blocking = false) + public void onUserTaskCommitted(UserTaskCommittedEvent event) { + forward(event); + } + + @ConsumeEvent(value = FlowEventTypes.USER_TASK_ROLLBACK_SUSPENDED, blocking = false) + public void onUserTaskRollbackSuspended(UserTaskRollbackSuspendedEvent event) { + forward(event); + } + + /** + * 尽力而为转发:以事件 routing key 作 RabbitMQ routing key,失败仅告警、不重试、不阻塞。 + * 通道未启用时 SmallRye 提供 no-op emitter,发送即丢弃、不连 broker。 + */ + private void forward(FlowEvent event) { + // 通道未启用时静默跳过:不连 broker、不打日志,保证默认配置零侵入 + if (!channelEnabled) { + return; + } + if (flowEventEmitterInstance.isUnsatisfied()) { + LOGGER.debug("flow-events-out channel not resolvable, skip forwarding.||routingKey={}", event.routingKey()); + return; + } + try { + MutinyEmitter emitter = flowEventEmitterInstance.get(); + OutgoingRabbitMQMetadata metadata = new OutgoingRabbitMQMetadata.Builder() + .withRoutingKey(event.routingKey()) + .build(); + emitter.sendMessage(Message.of(event, Metadata.of(metadata))) + .onFailure() + .invoke(e -> LOGGER.warn("forward flow event to rabbitmq failed.||routingKey={}||payloadType={}", + event.routingKey(), event.getClass().getSimpleName(), e)) + .subscribe() + .asCompletionStage(); + } catch (Exception e) { + // 仅记录 routing key 与异常,不打印事件载荷(避免泄露流程变量) + LOGGER.warn("forward flow event to rabbitmq failed (sync).||routingKey={}", event.routingKey(), e); + } + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventTypes.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventTypes.java new file mode 100644 index 00000000..fea210a4 --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventTypes.java @@ -0,0 +1,31 @@ +package dev.flexmodel.flow.event; + +/** + * Flow 生命周期事件路由 key 常量。 + *

+ * 供 {@code @ConsumeEvent} 注解与 RabbitMQ 桥接消费者引用,避免散落字符串字面量。 + * + * @author cjbi + */ +public final class FlowEventTypes { + + private FlowEventTypes() { + } + + // 定义层 + public static final String FLOW_CREATED = "flow.created"; + public static final String FLOW_UPDATED = "flow.updated"; + public static final String FLOW_DEPLOYED = "flow.deployed"; + public static final String FLOW_DELETED = "flow.deleted"; + + // 实例层 + public static final String FLOW_INSTANCE_STARTED = "flow.instance.started"; + public static final String FLOW_INSTANCE_COMPLETED = "flow.instance.completed"; + public static final String FLOW_INSTANCE_FAILED = "flow.instance.failed"; + public static final String FLOW_INSTANCE_TERMINATED = "flow.instance.terminated"; + + // 用户任务层 + public static final String USER_TASK_SUSPENDED = "flow.usertask.suspended"; + public static final String USER_TASK_COMMITTED = "flow.usertask.committed"; + public static final String USER_TASK_ROLLBACK_SUSPENDED = "flow.usertask.rollback.suspended"; +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceCompletedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceCompletedEvent.java new file mode 100644 index 00000000..47a36976 --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceCompletedEvent.java @@ -0,0 +1,35 @@ +package dev.flexmodel.flow.event; + +import lombok.Getter; +import lombok.ToString; + +import java.util.Map; + +/** + * 流程实例完成事件。 + * + * @author cjbi + */ +@Getter +@ToString(callSuper = true) +public class FlowInstanceCompletedEvent extends FlowEvent { + + public static final String ROUTING_KEY = FlowEventTypes.FLOW_INSTANCE_COMPLETED; + + private final String flowDeployId; + private final String flowInstanceId; + private final Map variables; + + public FlowInstanceCompletedEvent(String projectId, String caller, String flowDeployId, + String flowInstanceId, Map variables) { + super(projectId, caller); + this.flowDeployId = flowDeployId; + this.flowInstanceId = flowInstanceId; + this.variables = variables == null ? null : new java.util.HashMap<>(variables); + } + + @Override + public String routingKey() { + return ROUTING_KEY; + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceFailedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceFailedEvent.java new file mode 100644 index 00000000..c4c04323 --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceFailedEvent.java @@ -0,0 +1,33 @@ +package dev.flexmodel.flow.event; + +import lombok.Getter; +import lombok.ToString; + +/** + * 流程实例失败事件。 + * + * @author cjbi + */ +@Getter +@ToString(callSuper = true) +public class FlowInstanceFailedEvent extends FlowEvent { + + public static final String ROUTING_KEY = FlowEventTypes.FLOW_INSTANCE_FAILED; + + private final String flowDeployId; + private final String flowInstanceId; + private final String error; + + public FlowInstanceFailedEvent(String projectId, String caller, String flowDeployId, + String flowInstanceId, String error) { + super(projectId, caller); + this.flowDeployId = flowDeployId; + this.flowInstanceId = flowInstanceId; + this.error = error; + } + + @Override + public String routingKey() { + return ROUTING_KEY; + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceStartedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceStartedEvent.java new file mode 100644 index 00000000..fd6f8a77 --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceStartedEvent.java @@ -0,0 +1,35 @@ +package dev.flexmodel.flow.event; + +import lombok.Getter; +import lombok.ToString; + +import java.util.Map; + +/** + * 流程实例启动事件。 + * + * @author cjbi + */ +@Getter +@ToString(callSuper = true) +public class FlowInstanceStartedEvent extends FlowEvent { + + public static final String ROUTING_KEY = FlowEventTypes.FLOW_INSTANCE_STARTED; + + private final String flowDeployId; + private final String flowInstanceId; + private final Map variables; + + public FlowInstanceStartedEvent(String projectId, String caller, String flowDeployId, + String flowInstanceId, Map variables) { + super(projectId, caller); + this.flowDeployId = flowDeployId; + this.flowInstanceId = flowInstanceId; + this.variables = variables == null ? null : new java.util.HashMap<>(variables); + } + + @Override + public String routingKey() { + return ROUTING_KEY; + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceTerminatedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceTerminatedEvent.java new file mode 100644 index 00000000..8068ac9d --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowInstanceTerminatedEvent.java @@ -0,0 +1,28 @@ +package dev.flexmodel.flow.event; + +import lombok.Getter; +import lombok.ToString; + +/** + * 流程实例终止事件。 + * + * @author cjbi + */ +@Getter +@ToString(callSuper = true) +public class FlowInstanceTerminatedEvent extends FlowEvent { + + public static final String ROUTING_KEY = FlowEventTypes.FLOW_INSTANCE_TERMINATED; + + private final String flowInstanceId; + + public FlowInstanceTerminatedEvent(String projectId, String caller, String flowInstanceId) { + super(projectId, caller); + this.flowInstanceId = flowInstanceId; + } + + @Override + public String routingKey() { + return ROUTING_KEY; + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowUpdatedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowUpdatedEvent.java new file mode 100644 index 00000000..bad53270 --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowUpdatedEvent.java @@ -0,0 +1,28 @@ +package dev.flexmodel.flow.event; + +import lombok.Getter; +import lombok.ToString; + +/** + * 流程定义更新事件。 + * + * @author cjbi + */ +@Getter +@ToString(callSuper = true) +public class FlowUpdatedEvent extends FlowEvent { + + public static final String ROUTING_KEY = FlowEventTypes.FLOW_UPDATED; + + private final String flowModuleId; + + public FlowUpdatedEvent(String projectId, String caller, String flowModuleId) { + super(projectId, caller); + this.flowModuleId = flowModuleId; + } + + @Override + public String routingKey() { + return ROUTING_KEY; + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/StartFlowEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/StartFlowEvent.java deleted file mode 100644 index f12c732b..00000000 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/StartFlowEvent.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.flexmodel.flow.event; - -import lombok.Getter; -import lombok.Setter; - -import java.util.Map; - -/** - * @author cjbi - */ -@Getter -@Setter -public class StartFlowEvent { - private String flowDeployId; - private String flowModuleId; - private Map variables; -} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/UserTaskCommittedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/UserTaskCommittedEvent.java new file mode 100644 index 00000000..c4cbb2d2 --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/UserTaskCommittedEvent.java @@ -0,0 +1,39 @@ +package dev.flexmodel.flow.event; + +import lombok.Getter; +import lombok.ToString; + +import java.util.Map; + +/** + * 用户任务提交完成事件。 + * + * @author cjbi + */ +@Getter +@ToString(callSuper = true) +public class UserTaskCommittedEvent extends FlowEvent { + + public static final String ROUTING_KEY = FlowEventTypes.USER_TASK_COMMITTED; + + private final String flowDeployId; + private final String flowInstanceId; + private final String nodeInstanceId; + private final String nodeKey; + private final Map nodeAttributes; + + public UserTaskCommittedEvent(String projectId, String caller, String flowDeployId, String flowInstanceId, + String nodeInstanceId, String nodeKey, Map nodeAttributes) { + super(projectId, caller); + this.flowDeployId = flowDeployId; + this.flowInstanceId = flowInstanceId; + this.nodeInstanceId = nodeInstanceId; + this.nodeKey = nodeKey; + this.nodeAttributes = nodeAttributes == null ? null : new java.util.HashMap<>(nodeAttributes); + } + + @Override + public String routingKey() { + return ROUTING_KEY; + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/UserTaskRollbackSuspendedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/UserTaskRollbackSuspendedEvent.java new file mode 100644 index 00000000..8e916c99 --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/UserTaskRollbackSuspendedEvent.java @@ -0,0 +1,39 @@ +package dev.flexmodel.flow.event; + +import lombok.Getter; +import lombok.ToString; + +import java.util.Map; + +/** + * 用户任务回滚挂起事件。 + * + * @author cjbi + */ +@Getter +@ToString(callSuper = true) +public class UserTaskRollbackSuspendedEvent extends FlowEvent { + + public static final String ROUTING_KEY = FlowEventTypes.USER_TASK_ROLLBACK_SUSPENDED; + + private final String flowDeployId; + private final String flowInstanceId; + private final String nodeInstanceId; + private final String nodeKey; + private final Map nodeAttributes; + + public UserTaskRollbackSuspendedEvent(String projectId, String caller, String flowDeployId, String flowInstanceId, + String nodeInstanceId, String nodeKey, Map nodeAttributes) { + super(projectId, caller); + this.flowDeployId = flowDeployId; + this.flowInstanceId = flowInstanceId; + this.nodeInstanceId = nodeInstanceId; + this.nodeKey = nodeKey; + this.nodeAttributes = nodeAttributes == null ? null : new java.util.HashMap<>(nodeAttributes); + } + + @Override + public String routingKey() { + return ROUTING_KEY; + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/UserTaskSuspendedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/UserTaskSuspendedEvent.java new file mode 100644 index 00000000..f61280ab --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/UserTaskSuspendedEvent.java @@ -0,0 +1,42 @@ +package dev.flexmodel.flow.event; + +import lombok.Getter; +import lombok.ToString; + +import java.util.Map; + +/** + * 用户任务挂起事件。 + * + * @author cjbi + */ +@Getter +@ToString(callSuper = true) +public class UserTaskSuspendedEvent extends FlowEvent { + + public static final String ROUTING_KEY = FlowEventTypes.USER_TASK_SUSPENDED; + + private final String flowDeployId; + private final String flowInstanceId; + private final String nodeInstanceId; + private final String nodeKey; + private final Map variables; + private final Map nodeAttributes; + + public UserTaskSuspendedEvent(String projectId, String caller, String flowDeployId, String flowInstanceId, + String nodeInstanceId, String nodeKey, Map variables, + Map nodeAttributes) { + super(projectId, caller); + this.flowDeployId = flowDeployId; + this.flowInstanceId = flowInstanceId; + this.nodeInstanceId = nodeInstanceId; + this.nodeKey = nodeKey; + this.variables = variables == null ? null : new java.util.HashMap<>(variables); + this.nodeAttributes = nodeAttributes == null ? null : new java.util.HashMap<>(nodeAttributes); + } + + @Override + public String routingKey() { + return ROUTING_KEY; + } +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/executor/FlowExecutor.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/executor/FlowExecutor.java index 36aad99a..e7867d3f 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/executor/FlowExecutor.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/executor/FlowExecutor.java @@ -15,6 +15,10 @@ import dev.flexmodel.flow.exception.ProcessException; import dev.flexmodel.flow.exception.ReentrantException; import dev.flexmodel.flow.repository.FlowInstanceRepository; +import dev.flexmodel.flow.event.FlowEventPublisher; +import dev.flexmodel.flow.event.FlowInstanceCompletedEvent; +import dev.flexmodel.flow.event.FlowInstanceFailedEvent; +import dev.flexmodel.flow.event.FlowInstanceStartedEvent; import dev.flexmodel.flow.common.util.FlowModelUtil; import dev.flexmodel.flow.common.util.InstanceDataUtil; import dev.flexmodel.common.utils.CollectionUtils; @@ -38,11 +42,15 @@ public class FlowExecutor extends RuntimeExecutor { @Inject Instance executorFactoryInstance; + @Inject + FlowEventPublisher flowEventPublisher; + ////////////////////////////////////////execute//////////////////////////////////////// @Override public void execute(RuntimeContext runtimeContext) throws ProcessException { int processStatus = ProcessStatus.SUCCESS; + String errorMessage = null; try { preExecute(runtimeContext); doExecute(runtimeContext); @@ -50,10 +58,15 @@ public void execute(RuntimeContext runtimeContext) throws ProcessException { if (!ErrorEnum.isSuccess(pe.getErrNo())) { processStatus = ProcessStatus.FAILED; } + errorMessage = pe.getMessage(); throw pe; } finally { runtimeContext.setProcessStatus(processStatus); postExecute(runtimeContext); + if (processStatus == ProcessStatus.FAILED) { + flowEventPublisher.publish(new FlowInstanceFailedEvent(runtimeContext.getProjectId(), runtimeContext.getCaller(), + runtimeContext.getFlowDeployId(), runtimeContext.getFlowInstanceId(), errorMessage)); + } } } @@ -74,6 +87,9 @@ private void preExecute(RuntimeContext runtimeContext) throws ProcessException { //3.update runtimeContext fillExecuteContext(runtimeContext, flowInstancePO.getFlowInstanceId(), instanceDataId); + + flowEventPublisher.publish(new FlowInstanceStartedEvent(runtimeContext.getProjectId(), runtimeContext.getCaller(), + runtimeContext.getFlowDeployId(), runtimeContext.getFlowInstanceId(), runtimeContext.getInstanceDataMap())); } private FlowInstance saveFlowInstance(RuntimeContext runtimeContext) throws ProcessException { @@ -190,6 +206,9 @@ private void postExecute(RuntimeContext runtimeContext) throws ProcessException runtimeContext.setFlowInstanceStatus(FlowInstanceStatus.COMPLETED); } LOGGER.info("postExecute: flowInstance process completely.||flowInstanceId={}", runtimeContext.getFlowInstanceId()); + + flowEventPublisher.publish(new FlowInstanceCompletedEvent(runtimeContext.getProjectId(), runtimeContext.getCaller(), + runtimeContext.getFlowDeployId(), runtimeContext.getFlowInstanceId(), runtimeContext.getInstanceDataMap())); } } @@ -337,6 +356,9 @@ private void postCommit(RuntimeContext runtimeContext) throws ProcessException { } LOGGER.info("postCommit: flowInstance process completely.||flowInstanceId={}", runtimeContext.getFlowInstanceId()); + + flowEventPublisher.publish(new FlowInstanceCompletedEvent(runtimeContext.getProjectId(), runtimeContext.getCaller(), + runtimeContext.getFlowDeployId(), runtimeContext.getFlowInstanceId(), runtimeContext.getInstanceDataMap())); } } diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/executor/UserTaskExecutor.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/executor/UserTaskExecutor.java index d84b5ebf..e978fc5c 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/executor/UserTaskExecutor.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/executor/UserTaskExecutor.java @@ -1,5 +1,6 @@ package dev.flexmodel.flow.executor; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,6 +13,10 @@ import dev.flexmodel.flow.common.NodeInstanceStatus; import dev.flexmodel.flow.common.RuntimeContext; import dev.flexmodel.flow.common.util.FlowModelUtil; +import dev.flexmodel.flow.event.FlowEventPublisher; +import dev.flexmodel.flow.event.UserTaskCommittedEvent; +import dev.flexmodel.flow.event.UserTaskRollbackSuspendedEvent; +import dev.flexmodel.flow.event.UserTaskSuspendedEvent; import dev.flexmodel.JsonUtils; import java.text.MessageFormat; @@ -22,6 +27,9 @@ public class UserTaskExecutor extends ElementExecutor { private static final Logger LOGGER = LoggerFactory.getLogger(UserTaskExecutor.class); + @Inject + FlowEventPublisher flowEventPublisher; + @Override protected void doExecute(RuntimeContext runtimeContext) throws ProcessException { NodeInstanceBO currentNodeInstance = runtimeContext.getCurrentNodeInstance(); @@ -39,6 +47,10 @@ protected void doExecute(RuntimeContext runtimeContext) throws ProcessException String nodeName = FlowModelUtil.getElementName(flowElement); LOGGER.info("doExecute: userTask to commit.||flowInstanceId={}||nodeInstanceId={}||nodeKey={}||nodeName={}", runtimeContext.getFlowInstanceId(), currentNodeInstance.getNodeInstanceId(), flowElement.getKey(), nodeName); + flowEventPublisher.publish(new UserTaskSuspendedEvent(runtimeContext.getProjectId(), runtimeContext.getCaller(), + runtimeContext.getFlowDeployId(), runtimeContext.getFlowInstanceId(), + currentNodeInstance.getNodeInstanceId(), flowElement.getKey(), runtimeContext.getInstanceDataMap(), + flowElement.getProperties())); throw new SuspendException(ErrorEnum.COMMIT_SUSPEND, MessageFormat.format(Constants.NODE_INSTANCE_FORMAT, flowElement.getKey(), nodeName, currentNodeInstance.getNodeInstanceId())); } @@ -95,6 +107,9 @@ protected void postCommit(RuntimeContext runtimeContext) { if (currentNodeInstance.getStatus() != NodeInstanceStatus.COMPLETED) { currentNodeInstance.setStatus(NodeInstanceStatus.COMPLETED); runtimeContext.getNodeInstanceList().add(currentNodeInstance); + flowEventPublisher.publish(new UserTaskCommittedEvent(runtimeContext.getProjectId(), runtimeContext.getCaller(), + runtimeContext.getFlowDeployId(), runtimeContext.getFlowInstanceId(), + currentNodeInstance.getNodeInstanceId(), currentNodeInstance.getNodeKey())); } } @@ -123,6 +138,9 @@ protected void doRollback(RuntimeContext runtimeContext) throws ProcessException newNodeInstanceBO.setProperties(currentNodeInstance.getProperties()); runtimeContext.setCurrentNodeInstance(newNodeInstanceBO); runtimeContext.getNodeInstanceList().add(newNodeInstanceBO); + flowEventPublisher.publish(new UserTaskRollbackSuspendedEvent(runtimeContext.getProjectId(), runtimeContext.getCaller(), + runtimeContext.getFlowDeployId(), runtimeContext.getFlowInstanceId(), + newNodeInstanceBO.getNodeInstanceId(), newNodeInstanceBO.getNodeKey())); throw new SuspendException(ErrorEnum.ROLLBACK_SUSPEND, MessageFormat.format(Constants.NODE_INSTANCE_FORMAT, newNodeInstanceBO.getNodeKey(), FlowModelUtil.getFlowElement(runtimeContext.getFlowElementMap(), newNodeInstanceBO.getNodeKey()), diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/processor/DefinitionProcessor.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/processor/DefinitionProcessor.java index d465c38c..fbd5e525 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/processor/DefinitionProcessor.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/processor/DefinitionProcessor.java @@ -27,6 +27,11 @@ import dev.flexmodel.flow.common.util.StrongUuidGenerator; import dev.flexmodel.flow.validator.ModelValidator; import dev.flexmodel.flow.validator.ParamValidator; +import dev.flexmodel.flow.event.FlowCreatedEvent; +import dev.flexmodel.flow.event.FlowDeletedEvent; +import dev.flexmodel.flow.event.FlowDeployedEvent; +import dev.flexmodel.flow.event.FlowEventPublisher; +import dev.flexmodel.flow.event.FlowUpdatedEvent; import dev.flexmodel.JsonUtils; import dev.flexmodel.common.utils.StringUtils; @@ -52,6 +57,9 @@ public class DefinitionProcessor { @Inject FlowDeploymentRepository flowDeploymentRepository; + @Inject + FlowEventPublisher flowEventPublisher; + @PostConstruct public void init() { List idGeneratorPlugins = pluginManager.getPluginsFor(IdGeneratorPlugin.class); @@ -80,6 +88,8 @@ public CreateFlowResult create(CreateFlowParam createFlowParam) { createFlowResult = JsonUtils.convertValue(flowDefinitionPO, CreateFlowResult.class); fillCommonResult(createFlowResult, ErrorEnum.SUCCESS); + flowEventPublisher.publish(new FlowCreatedEvent(createFlowParam.getProjectId(), createFlowParam.getCaller(), + flowModuleId, createFlowParam.getFlowKey())); } catch (TurboException te) { fillCommonResult(createFlowResult, te); } @@ -100,6 +110,8 @@ public UpdateFlowResult update(UpdateFlowParam updateFlowParam) { throw new DefinitionException(ErrorEnum.DEFINITION_UPDATE_INVALID); } fillCommonResult(updateFlowResult, ErrorEnum.SUCCESS); + flowEventPublisher.publish(new FlowUpdatedEvent(updateFlowParam.getProjectId(), updateFlowParam.getCaller(), + updateFlowParam.getFlowModuleId())); } catch (TurboException te) { LOGGER.error("Flow definition update failed: {}", te.getErrMsg(), te); fillCommonResult(updateFlowResult, te); @@ -120,6 +132,7 @@ public void delete(String projectId, String flowModuleId) { flowDefinition.setIsDeleted(true); flowDefinition.setModifyTime(LocalDateTime.now()); flowDefinitionRepository.updateByModuleId(projectId, flowDefinition); + flowEventPublisher.publish(new FlowDeletedEvent(projectId, null, flowModuleId)); } public DeployFlowResult deploy(DeployFlowParam deployFlowParam) { @@ -156,6 +169,8 @@ public DeployFlowResult deploy(DeployFlowParam deployFlowParam) { } deployFlowResult = JsonUtils.convertValue(flowDeploymentPO, DeployFlowResult.class); fillCommonResult(deployFlowResult, ErrorEnum.SUCCESS); + flowEventPublisher.publish(new FlowDeployedEvent(deployFlowParam.getProjectId(), deployFlowParam.getCaller(), + deployFlowParam.getFlowModuleId(), flowDeployId)); } catch (TurboException te) { fillCommonResult(deployFlowResult, te); } diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/processor/RuntimeProcessor.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/processor/RuntimeProcessor.java index 678114ac..0ccb04f0 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/processor/RuntimeProcessor.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/processor/RuntimeProcessor.java @@ -24,6 +24,8 @@ import dev.flexmodel.flow.exception.ReentrantException; import dev.flexmodel.flow.exception.TurboException; import dev.flexmodel.flow.executor.FlowExecutor; +import dev.flexmodel.flow.event.FlowEventPublisher; +import dev.flexmodel.flow.event.FlowInstanceTerminatedEvent; import dev.flexmodel.flow.repository.FlowDeploymentRepository; import dev.flexmodel.flow.repository.FlowInstanceMappingRepository; import dev.flexmodel.flow.repository.FlowInstanceRepository; @@ -69,6 +71,9 @@ public class RuntimeProcessor { @Inject NodeInstanceService nodeInstanceService; + @Inject + FlowEventPublisher flowEventPublisher; + ////////////////////////////////////////startProcess//////////////////////////////////////// public StartProcessResult startProcess(StartProcessParam startProcessParam) { @@ -253,6 +258,7 @@ public TerminateResult terminateProcess(String projectId, String flowInstanceId, } else { processInstanceRepository.updateStatus(projectId, flowInstancePO, FlowInstanceStatus.TERMINATED); flowInstanceStatus = FlowInstanceStatus.TERMINATED; + flowEventPublisher.publish(new FlowInstanceTerminatedEvent(projectId, flowInstancePO.getCaller(), flowInstanceId)); } if (effectiveForSubFlowInstance) { terminateSubFlowInstance(projectId, flowInstanceId); diff --git a/flexmodel-server/src/main/resources/application.properties b/flexmodel-server/src/main/resources/application.properties index 36249441..48c13008 100644 --- a/flexmodel-server/src/main/resources/application.properties +++ b/flexmodel-server/src/main/resources/application.properties @@ -69,3 +69,14 @@ flexmodel.jwt.refresh-token-lifetime=30d # flexmodel.storage.s3-bucket=flexmodel # Pages Configuration flexmodel.pages.root-path=./pages +# Flow 生命周期事件 RabbitMQ 桥接(可选,默认关闭) +# 本地 EventBus 事件始终发布;仅当 SmallRye 通道启用时才连 broker 并转发到 topic 交换机。 +# 通道禁用时 SmallRye 注入 no-op MutinyEmitter,桥接 forward() 静默丢弃,无需 broker 即可启动。 +mp.messaging.outgoing.flow-events-out.connector=smallrye-rabbitmq +mp.messaging.outgoing.flow-events-out.exchange.name=flexmodel.flow.events +mp.messaging.outgoing.flow-events-out.exchange.type=topic +mp.messaging.outgoing.flow-events-out.exchange.durable=true +# 通道默认关闭;启用需置 true 并提供 broker 连接配置(或开 DevServices) +mp.messaging.outgoing.flow-events-out.enabled=false +# 避免无 Docker 时 DevServices 启动失败;需 broker 时另置 true 或提供连接配置 +quarkus.rabbitmq.devservices.enabled=false diff --git a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventPublisherTest.java b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventPublisherTest.java new file mode 100644 index 00000000..7a91bbba --- /dev/null +++ b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventPublisherTest.java @@ -0,0 +1,88 @@ +package dev.flexmodel.flow.event; + +import dev.flexmodel.SQLiteTestResource; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 楠岃瘉 {@link FlowEventPublisher} 灏嗗己绫诲瀷浜嬩欢缁?Vert.x EventBus 骞挎挱锛? + * 涓斾簨浠跺瓧娈靛畬鏁翠繚鐣欍€傛湰鍦颁簨浠朵负鏈哄埗鏈韩锛屼笉渚濊禆 RabbitMQ銆? + * + * @author cjbi + */ +@QuarkusTest +@QuarkusTestResource(SQLiteTestResource.class) +public class FlowEventPublisherTest { + + @Inject + FlowEventPublisher flowEventPublisher; + + @Inject + FlowEventTestConsumer capturingConsumer; + + @Test + void publishFlowInstanceStartedEventFieldsPreserved() { + capturingConsumer.captured().set(null); + + String projectId = "proj-test"; + String caller = "caller-test"; + String flowDeployId = "deploy-test"; + String flowInstanceId = "instance-test"; + Map variables = new HashMap<>(); + variables.put("amount", 100); + variables.put("approved", true); + + flowEventPublisher.publish(new FlowInstanceStartedEvent(projectId, caller, flowDeployId, + flowInstanceId, variables)); + + FlowInstanceStartedEvent event = pollFor(capturingConsumer.captured(), 5000L); + + assertNotNull(event, "FlowInstanceStartedEvent should be received via EventBus"); + assertEquals(projectId, event.getProjectId()); + assertEquals(caller, event.getCaller()); + assertEquals(flowDeployId, event.getFlowDeployId()); + assertEquals(flowInstanceId, event.getFlowInstanceId()); + assertNotNull(event.getVariables()); + assertEquals(100, event.getVariables().get("amount")); + assertEquals(Boolean.TRUE, event.getVariables().get("approved")); + assertEquals(FlowEventTypes.FLOW_INSTANCE_STARTED, event.routingKey()); + assertTrue(event.getTimestamp() > 0, "timestamp should be set at construction"); + } + + @Test + void publishNullEventIsNoOp() { + // 涓嶅簲鎶涘嚭锛屼篃涓嶅簲褰卞搷鍏朵粬娑堣垂鑰? + flowEventPublisher.publish(null); + } + + /** + * 鏈夌晫杞绛夊緟娑堣垂鑰呮崟鑾蜂簨浠讹紝閬垮厤寮曞叆 Awaitility 渚濊禆銆? + */ + private static T pollFor(AtomicReference ref, long timeoutMs) { + long deadline = System.currentTimeMillis() + timeoutMs; + T value = ref.get(); + while (value == null && System.currentTimeMillis() < deadline) { + sleep(50L); + value = ref.get(); + } + return value; + } + + private static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java new file mode 100644 index 00000000..33fd282d --- /dev/null +++ b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java @@ -0,0 +1,159 @@ +package dev.flexmodel.flow.event; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.rabbitmq.client.BuiltinExchangeType; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.GetResponse; +import dev.flexmodel.SQLiteTestResource; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 端到端验证 {@link FlowEventRabbitmqBridge} 将 Flow 生命周期事件以正确 routing key 与 + * JSON 载荷推送到 RabbitMQ topic 交换机。 + *

+ * 使用 Testcontainers 启动真实 RabbitMQ broker(见 {@link RabbitMqTestResource}), + * 并以 amqp-client 临时队列订阅交换机、拉取转发的消息。 + *

+ * 默认跳过(避免无 Docker 环境下默认 {@code mvn test} 失败)。运行需: + *

    + *
  • 本机 Docker 可用且能拉取 {@code rabbitmq:3-management} 镜像
  • + *
  • 设置环境变量 {@code FLEXMODEL_E2E_RABBITMQ=true}(opt-in 启用本端到端测试)
  • + *
+ *

+ * {@code @QuarkusTestResource} 设 {@code restrictToAnnotatedClass=true},确保 broker 资源 + * 仅对本测试类生效,不泄漏到其他共享应用上下文的测试。 + * + * @author cjbi + */ +@EnabledIfEnvironmentVariable(named = "FLEXMODEL_E2E_RABBITMQ", matches = "true") +@QuarkusTest +@QuarkusTestResource(SQLiteTestResource.class) +@QuarkusTestResource(value = RabbitMqTestResource.class, restrictToAnnotatedClass = true) +public class FlowEventRabbitmqBridgeE2ETest { + + private static final String EXCHANGE = "flexmodel.flow.events"; + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Inject + FlowEventPublisher flowEventPublisher; + + /** + * 验证 FlowDeployedEvent 经桥接转发后,AMQP routing key 与 JSON 载荷字段正确。 + */ + @Test + void flowDeployedEventForwardedToBroker() throws Exception { + String projectId = "proj-e2e"; + String caller = "caller-e2e"; + String flowModuleId = "module-e2e"; + String flowDeployId = "deploy-e2e"; + + try (Connection connection = newConnection(); + Channel channel = connection.createChannel()) { + // 确保交换机存在(与 SmallRye 声明参数一致:topic、durable),幂等 + channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.TOPIC, true); + String queue = channel.queueDeclare().getQueue(); + channel.queueBind(queue, EXCHANGE, FlowEventTypes.FLOW_DEPLOYED); + + flowEventPublisher.publish( + new FlowDeployedEvent(projectId, caller, flowModuleId, flowDeployId)); + + GetResponse response = pollForMessage(channel, queue, 15000L); + assertNotNull(response, "FlowDeployedEvent should be forwarded to broker within timeout"); + + Envelope envelope = response.getEnvelope(); + assertEquals(FlowEventTypes.FLOW_DEPLOYED, envelope.getRoutingKey(), + "AMQP routing key should match event routing key"); + + JsonNode body = MAPPER.readTree(response.getBody()); + assertEquals(projectId, body.get("projectId").asText()); + assertEquals(caller, body.get("caller").asText()); + assertEquals(flowModuleId, body.get("flowModuleId").asText()); + assertEquals(flowDeployId, body.get("flowDeployId").asText()); + assertTrue(body.get("timestamp").asLong() > 0, "timestamp should be set"); + } + } + + /** + * 验证带 variables 快照的 FlowInstanceStartedEvent 经桥接转发后,variables 仍在 JSON 载荷中完整。 + */ + @Test + void flowInstanceStartedEventWithVariablesForwarded() throws Exception { + String projectId = "proj-vars"; + String caller = "caller-vars"; + String flowDeployId = "deploy-vars"; + String flowInstanceId = "instance-vars"; + Map variables = new HashMap<>(); + variables.put("amount", 100); + variables.put("approved", true); + + try (Connection connection = newConnection(); + Channel channel = connection.createChannel()) { + channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.TOPIC, true); + String queue = channel.queueDeclare().getQueue(); + channel.queueBind(queue, EXCHANGE, FlowEventTypes.FLOW_INSTANCE_STARTED); + + flowEventPublisher.publish( + new FlowInstanceStartedEvent(projectId, caller, flowDeployId, flowInstanceId, variables)); + + GetResponse response = pollForMessage(channel, queue, 15000L); + assertNotNull(response, "FlowInstanceStartedEvent should be forwarded to broker within timeout"); + + assertEquals(FlowEventTypes.FLOW_INSTANCE_STARTED, response.getEnvelope().getRoutingKey()); + + JsonNode body = MAPPER.readTree(response.getBody()); + assertEquals(projectId, body.get("projectId").asText()); + assertEquals(flowDeployId, body.get("flowDeployId").asText()); + assertEquals(flowInstanceId, body.get("flowInstanceId").asText()); + JsonNode vars = body.get("variables"); + assertNotNull(vars, "variables snapshot should be present in payload"); + assertEquals(100, vars.get("amount").asInt()); + assertTrue(vars.get("approved").asBoolean(), "approved variable should be true"); + } + } + + private static Connection newConnection() throws Exception { + ConnectionFactory factory = new ConnectionFactory(); + factory.setHost(RabbitMqTestResource.host); + factory.setPort(RabbitMqTestResource.port); + factory.setUsername(RabbitMqTestResource.username); + factory.setPassword(RabbitMqTestResource.password); + return factory.newConnection(); + } + + /** + * 有界轮询拉取消息:桥接转发为异步,需留足时间。 + */ + private static GetResponse pollForMessage(Channel channel, String queue, long timeoutMs) + throws Exception { + long deadline = System.currentTimeMillis() + timeoutMs; + GetResponse response = channel.basicGet(queue, true); + while (response == null && System.currentTimeMillis() < deadline) { + sleep(100L); + response = channel.basicGet(queue, true); + } + return response; + } + + private static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeTest.java b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeTest.java new file mode 100644 index 00000000..bf337e96 --- /dev/null +++ b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeTest.java @@ -0,0 +1,81 @@ +package dev.flexmodel.flow.event; + +import dev.flexmodel.SQLiteTestResource; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 验证 FlowEventRabbitmqBridge 的可选桥接语义:默认通道禁用且不连 broker 时, + * 桥接 bean 仍可创建,本地事件照常经 FlowEventPublisher 广播。 + * 不依赖真实 broker,亦不依赖 SmallRye InMemoryConnector(当前 Quarkus 版本未提供配套扩展), + * 仅以默认配置启动应用即可断言桥接的零侵入设计目标。 + * + * @author cjbi + */ +@QuarkusTest +@QuarkusTestResource(SQLiteTestResource.class) +public class FlowEventRabbitmqBridgeTest { + + @Inject + FlowEventRabbitmqBridge bridge; + + @Inject + FlowEventPublisher flowEventPublisher; + + @Inject + FlowEventTestConsumer capturingConsumer; + + @Test + void bridgeBeanPresentAndNonThrowingByDefault() { + // 默认通道禁用:桥接 bean 仍存在(零侵入),发布桥接也消费的事件不应抛出, + // 且不连 broker(应用无 broker 启动即证) + assertNotNull(bridge, "bridge bean should be present (zero-intrusion)"); + flowEventPublisher.publish(new FlowDeployedEvent("proj-bridge", "caller-bridge", + "module-bridge", "deploy-bridge")); + } + + @Test + void disabledBridgeIsTransparentToLocalEvents() { + capturingConsumer.captured().set(null); + + Map variables = new HashMap<>(); + variables.put("k", "v"); + flowEventPublisher.publish(new FlowInstanceStartedEvent("proj-bridge", "caller-bridge", + "deploy-bridge", "instance-bridge", variables)); + + FlowInstanceStartedEvent event = pollFor(capturingConsumer.captured(), 5000L); + + assertNotNull(event, "local EventBus consumer should still receive event while channel disabled"); + assertEquals("proj-bridge", event.getProjectId()); + assertEquals("instance-bridge", event.getFlowInstanceId()); + assertTrue(event.getTimestamp() > 0); + } + + private static T pollFor(AtomicReference ref, long timeoutMs) { + long deadline = System.currentTimeMillis() + timeoutMs; + T value = ref.get(); + while (value == null && System.currentTimeMillis() < deadline) { + sleep(50L); + value = ref.get(); + } + return value; + } + + private static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventTestConsumer.java b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventTestConsumer.java new file mode 100644 index 00000000..3e4cb6ab --- /dev/null +++ b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventTestConsumer.java @@ -0,0 +1,28 @@ +package dev.flexmodel.flow.event; + +import io.quarkus.vertx.ConsumeEvent; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * 娴嬭瘯鐢?EventBus 娑堣垂鑰咃細鎹曡幏 {@link FlowInstanceStartedEvent}锛岄獙璇佸瓧娈电粡 EventBus 閫忎紶瀹屾暣銆? + *

+ * 椤跺眰 {@code @ApplicationScoped} Bean锛岀‘淇?Quarkus 鍙戠幇骞舵敞鍐屽叾 {@code @ConsumeEvent} 娑堣垂鑰呫€? + * + * @author cjbi + */ +@ApplicationScoped +public class FlowEventTestConsumer { + + final AtomicReference captured = new AtomicReference<>(); + + @ConsumeEvent(value = FlowEventTypes.FLOW_INSTANCE_STARTED, blocking = false) + void onFlowInstanceStarted(FlowInstanceStartedEvent event) { + captured.set(event); + } + + public AtomicReference captured() { + return captured; + } +} diff --git a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/RabbitMqTestResource.java b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/RabbitMqTestResource.java new file mode 100644 index 00000000..b5142b44 --- /dev/null +++ b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/RabbitMqTestResource.java @@ -0,0 +1,59 @@ +package dev.flexmodel.flow.event; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import org.testcontainers.containers.RabbitMQContainer; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Testcontainers 启动 RabbitMQ broker,并向 Quarkus 注入连接配置与启用 flow-events-out 通道。 + *

+ * 用于端到端验证 FlowEventRabbitmqBridge 将事件以正确 routing key 与 JSON 载荷推送到 topic 交换机。 + *

+ * 连接配置注入到 SmallRye RabbitMQ connector 的 channel 级属性 + * ({@code mp.messaging.outgoing.flow-events-out.host/port/username/password})。 + * 注意:{@code quarkus-messaging-rabbitmq} 扩展的 {@code quarkus.rabbitmq.*} 只注册 DevServices 与 + * credentials provider 字段,不注册 host/port/username/password 连接字段——这些由 SmallRye + * connector 自身读取(channel 级或全局 {@code rabbitmq-*} 别名)。显式提供 channel 级连接配置后, + * DevServices 自动旁路。 + * + * @author cjbi + */ +public class RabbitMqTestResource implements QuarkusTestResourceLifecycleManager { + + // 静态引用,供测试用例获取连接坐标(测试侧 amqp-client 用其订阅交换机) + static volatile String host; + static volatile int port; + static volatile String username = "guest"; + static volatile String password = "guest"; + + private RabbitMQContainer rabbitmq; + + @Override + public Map start() { + rabbitmq = new RabbitMQContainer("rabbitmq:3-management"); + rabbitmq.start(); + host = rabbitmq.getHost(); + port = rabbitmq.getAmqpPort(); + + Map config = new LinkedHashMap<>(); + // SmallRye RabbitMQ connector channel 级连接配置(quarkus.rabbitmq.* 不注册连接字段) + config.put("mp.messaging.outgoing.flow-events-out.host", host); + config.put("mp.messaging.outgoing.flow-events-out.port", String.valueOf(port)); + config.put("mp.messaging.outgoing.flow-events-out.username", username); + config.put("mp.messaging.outgoing.flow-events-out.password", password); + // 显式提供连接配置后 DevServices 自动旁路;此处再确认禁用,避免无 Docker 时误触 + config.put("quarkus.rabbitmq.devservices.enabled", "false"); + // 启用 flow-events-out 通道,使桥接真正转发到 broker + config.put("mp.messaging.outgoing.flow-events-out.enabled", "true"); + return config; + } + + @Override + public void stop() { + if (rabbitmq != null) { + rabbitmq.close(); + } + } +} diff --git a/flexmodel-ui b/flexmodel-ui index ca8088c7..f423e7f4 160000 --- a/flexmodel-ui +++ b/flexmodel-ui @@ -1 +1 @@ -Subproject commit ca8088c7e2c476ee307a83d61cde178c3528675a +Subproject commit f423e7f4b017b8f5eb1beba8b329490b94188ebf diff --git a/flexmodel-website b/flexmodel-website index c6a5fadb..3775b973 160000 --- a/flexmodel-website +++ b/flexmodel-website @@ -1 +1 @@ -Subproject commit c6a5fadbf9be52ea2a5cc32f4aff6f00470a8fba +Subproject commit 3775b973dc067794fad37f7444c62885e7077819 diff --git a/progress.md b/progress.md index 6cd2030e..36e1f568 100644 --- a/progress.md +++ b/progress.md @@ -298,3 +298,158 @@ Quartz 作业被正确创建/移除/状态变更。 - 所有 `@QuarkusTest` 测试因 `SessionFactory` 在测试环境不可用而启动失败 - LSP 错误 (codegen 相关类型如 FlowDefinition 在 IDE 中未解析) — 非 our edits 引起 + +## Flow 生命周期事件:本地 EventBus 事件 + 可选 RabbitMQ 桥接(2026-08-19) + +**目标:** 在 flow 核心关键生命周期点发布强类型本地事件,经 Vert.x EventBus 广播;新增可选 RabbitMQ 桥接,默认关闭、未配置不连 +broker,本地事件照常发布。 + +**实现:** + +- 新增 `dev.flexmodel.flow.event` 包:`FlowEvent` 抽象基类(projectId/caller/timestamp + 抽象 `routingKey()`)、 + `FlowEventTypes` 路由 key 常量、11 个具体事件类(定义层 4 / 实例层 4 / 用户任务层 3)。 +- `FlowEventPublisher`(`@ApplicationScoped`):注入 `EventBus`,`publish(FlowEvent)` 经 + `eventBus.publish(routingKey, event)` 广播,全程 try/catch 仅告警、不抛出、不阻塞流程。 +- `FlowEventRabbitmqBridge`(`@ApplicationScoped`):每事件一个 `@ConsumeEvent(常量, blocking=false)` 方法消费具体类型, + `enabled` 默认 false 时 early-return;`forward()` 以 `Instance` 延迟注入(禁用通道时 bean 仍可创建,不影响本地事件),用 + `OutgoingRabbitMQMetadata` 设 routing key 尽力转发。 +- `FlowEventConfig` `@ConfigMapping(prefix="flexmodel.flow")` 声明 `events.rabbitmq.enabled` 配置根,避免 SmallRye 严格校验报 + `does not map to any root`。 +- 依赖:`flexmodel-server/pom.xml` 加 `io.quarkus:quarkus-messaging-rabbitmq`(3.33 起更名自 + `quarkus-smallrye-reactive-messaging-rabbitmq`,版本由 quarkus-bom 管理)。 +- 配置 `application.properties`:`flexmodel.flow.events.rabbitmq.enabled=false`、`mp.messaging.outgoing.flow-events-out.*` + (connector=smallrye-rabbitmq、exchange.type=topic/durable)、`enabled` 绑定同一开关、 + `quarkus.rabbitmq.devservices.enabled=false`。 +- 埋点(11 处):DefinitionProcessor (create/update/deploy/delete)、FlowExecutor (preExecute→started、execute finally + FAILED→failed、postExecute/postCommit COMPLETED/END→completed)、RuntimeProcessor.terminateProcess + (TERMINATED→terminated,子流程级联逐个)、UserTaskExecutor + (doExecute→suspended、postCommit→committed、doRollback→rollback.suspended)。 + +**测试:** + +- `FlowEventPublisherTest`(2):本地 EventBus 广播 + 字段完整 + null no-op。 +- `FlowEventRabbitmqBridgeTest`(2):默认禁用不连 broker(应用无 broker 启动即证)、禁用桥接对本地事件透明。未采用 SmallRye + InMemoryConnector(当前 Quarkus 3.33.1 未提供配套 in-memory 扩展,InMemorySinkImpl 无 bean 定义注解,注入不满足)。 + +**验证:** + +- `mvn clean compile -pl '!flexmodel-engine/flexmodel-maven-plugin'` → BUILD SUCCESS +- +`mvn test -pl flexmodel-server -Dtest=FlowEventPublisherTest,FlowEventRabbitmqBridgeTest,DefinitionProcessorTest,RuntimeProcessorTest` → +25 tests, 0 failures, 0 errors(含 DefinitionProcessorTest 4、RuntimeProcessorTest 17 回归通过) + +**备注:** RabbitMQ 转发(enabled=true)端到端需真实 broker 或 Testcontainers,按计划保持可选、默认关闭;v1 不含 +node-instance-created/service-task 等事件,后续同模式按需追加。 + +## 去掉 flexmodel.flow.events.rabbitmq.enabled 业务开关(2026-08-19) + +**背景:** 该业务开关与 SmallRye 通道 `mp.messaging.outgoing.flow-events-out.enabled` 重复;且桥接 `enabled` 默认 false +时即便通道启用也不转发,属冗余控制层。 + +**变更:** + +- `application.properties`:删除 `flexmodel.flow.events.rabbitmq.enabled`,通道 `enabled=false` 成为单一控制(启用置 true + + broker 连接配置)。 +- `FlowEventRabbitmqBridge`:删除 `@ConfigProperty enabled` 字段、11 处 `if(!enabled) return;` early-return、`isEnabled()` + 测试探针;转发完全由 `Instance` 解析性决定——通道禁用时 SmallRye 注入 no-op emitter,`forward()` 发送即丢弃、不连 + broker。 +- 删除 `FlowEventConfig`(`@ConfigMapping` 不再需要声明已移除的配置根)。 +- `FlowEventRabbitmqBridgeTest`:移除 `isEnabled/isEmitterResolvable` 断言,改为断言桥接 bean 存在且默认通道禁用时发布不抛出、本地事件照常广播。 + +**验证:** + +- `mvn test-compile -pl '!flexmodel-engine/flexmodel-maven-plugin'` → ExitCode 0 +- +`mvn test -pl flexmodel-server -Dtest=FlowEventPublisherTest,FlowEventRabbitmqBridgeTest,DefinitionProcessorTest,RuntimeProcessorTest` → +25 tests, 0 failures, 0 errors + +**设计效果:** 单一配置源——`mp.messaging.outgoing.flow-events-out.enabled` 同时控制是否连 broker 与是否转发;默认 false +零侵入,本地 EventBus 事件始终发布。 + +## Review 修复:桥接静默跳过 + variables 防御性拷贝(2026-08-19) + +**P1 桥接默认配置逐事件 WARN 栈:** `Instance.isUnsatisfied()` 对禁用通道返回 false,`.get()` 抛 SRMSG00019 被捕获记 +WARN(含事件载荷/流程变量)。修复:`FlowEventRabbitmqBridge` 注入 +`@ConfigProperty("mp.messaging.outgoing.flow-events-out.enabled", defaultValue="false") channelEnabled`,`forward()` 首行 +`if(!channelEnabled) return;` 静默跳过;转发失败日志改为仅记 routingKey + payloadType,不再打印整个事件(避免泄露流程变量)。 + +**P2 发布可变 variables 快照并发风险:** `FlowInstanceStartedEvent`/`FlowInstanceCompletedEvent`/`UserTaskSuspendedEvent` +构造时按引用持有 `runtimeContext.getInstanceDataMap()`,发布线程后续 mutate 同一 map,异步消费者/序列化读取共享 map 可能不一致或 +CME。修复:三个事件构造器对 variables 做 `new HashMap<>(variables)` 防御性拷贝(null 安全),发布快照不可变。 + +**验证:** `mvn test-compile` 通过; +`FlowEventPublisherTest(2)/FlowEventRabbitmqBridgeTest(2)/DefinitionProcessorTest(4)/RuntimeProcessorTest(17)` 共 25 测试 +0 失败 0 错误。默认配置下桥接不再逐事件打 WARN(SRMSG00232 通道禁用为启动一次性诊断)。 + +## Flow 生命周期事件 RabbitMQ Testcontainers E2E 测试(2026-08-19) + +**目标:** 增加 Testcontainers 端到端测试,启动真实 RabbitMQ broker 验证 `FlowEventRabbitmqBridge` 以正确 routing key + +JSON 载荷推送事件到 topic 交换机。 + +**新增/修改:** + +- `flexmodel-server/pom.xml`:加 `org.testcontainers:rabbitmq:${testcontainers.version}`(test scope,版本由父 pom + `testcontainers.version=1.21.4` 管理;amqp-client 5.x 传递可用)。 +- `RabbitMqTestResource.java`(前序会话已写):实现 `QuarkusTestResourceLifecycleManager`,启动 + `RabbitMQContainer("rabbitmq:3-management")`,注入 `quarkus.rabbitmq.host/port/username/password` 与 + `mp.messaging.outgoing.flow-events-out.enabled=true`,暴露 static host/port/username/password 供测试取连接坐标。 +- `FlowEventRabbitmqBridgeE2ETest.java`(新增):`@QuarkusTest` + `@QuarkusTestResource(SQLiteTestResource.class)` + + `@QuarkusTestResource(value=RabbitMqTestResource.class, restrictToAnnotatedClass=true)`。两个用例: + - `flowDeployedEventForwardedToBroker`:amqp-client 临时队列绑定交换机 `flexmodel.flow.events`(routing key + `flow.deployed`),发布 `FlowDeployedEvent`,`basicGet` 轮询(≤15s)断言 routing key 与 JSON + 字段(projectId/caller/flowModuleId/flowDeployId/timestamp)。 + - `flowInstanceStartedEventWithVariablesForwarded`:同模式断言 `flow.instance.started` 与 variables + 快照(amount/approved)经 JSON 序列化完整。 + +**关键设计决策:** + +- **资源泄漏修复(重要):** 初版 `@QuarkusTestResource(RabbitMqTestResource.class)` 默认 `restrictToAnnotatedClass=false` + ,导致 broker 资源被 Quarkus 全局应用到所有共享应用上下文的测试——即便未选 E2E 测试,其他测试(`FlowEventPublisherTest` + 等)也触发 `rabbitmq:3-management` 镜像拉取,无 Docker Hub 环境下整套测试失败。改为 `restrictToAnnotatedClass=true` + ,资源仅对本测试类生效。验证确认:非 E2E 测试不再拉取镜像。 +- **opt-in 开关:** E2E 测试加 `@EnabledIfEnvironmentVariable(named="FLEXMODEL_E2E_RABBITMQ", matches="true")` + ,默认跳过。项目此前无任何 Docker 依赖测试,此为首个;默认 `mvn test` 在无 Docker/无 Docker Hub 环境保持绿色。运行需:本机 + Docker 可用 + 能拉取 `rabbitmq:3-management` + 设环境变量 `FLEXMODEL_E2E_RABBITMQ=true`。 +- routing key 取自 AMQP envelope(`routingKey()` 是方法不进 JSON),JSON 载荷用 Jackson 解析;带 variables + 的事件经防御性拷贝保证快照不可变(见前序 Review 修复)。 + +**验证:** + +- `mvn test-compile -pl '!flexmodel-engine/flexmodel-maven-plugin' -q` → ExitCode 0。 +- 综合回归 + `mvn test -pl flexmodel-server -Dtest=FlowEventPublisherTest,FlowEventRabbitmqBridgeTest,FlowEventRabbitmqBridgeE2ETest,DefinitionProcessorTest,RuntimeProcessorTest` → + Tests run: 27, Failures: 0, Errors: 0, Skipped: 2(E2E 默认跳过),BUILD SUCCESS,无 Docker 镜像拉取。 +- **E2E 实跑未完成:** 当前环境 Docker Hub 不可达(`registry-1.docker.io` EOF / 配置镜像源 `docker.1panel.live` 超时), + `rabbitmq:3-management` 无法拉取,故 `FLEXMODEL_E2E_RABBITMQ=true` 实跑未通过。代码逻辑正确、编译通过;待网络恢复或换可用镜像源后,设该环境变量即可执行端到端验证。 + +**未决/后续:** + +- E2E 实跑待 Docker Hub 可达后补验证(设 `FLEXMODEL_E2E_RABBITMQ=true` 运行)。 +- 若 CI 需常态化跑 E2E,建议在 CI 配置可用 RabbitMQ 镜像源或预拉镜像,并设该环境变量。 + +## E2E 测试修复:RabbitMQ 连接配置键(2026-08-19 续) + +**问题:** 首次运行 `FlowEventRabbitmqBridgeE2ETest` 失败——应用侧 SmallRye outgoing channel `Connection refused`,事件未发到 +broker,测试轮询超时。日志报 `Unrecognized configuration key "quarkus.rabbitmq.password"`。 + +**根因:** 反编译 `quarkus-messaging-rabbitmq` 扩展的 Quarkus config root `RabbitMQBuildTimeConfig` 确认:其仅注册 +`devservices`、`credentialsProvider`、`credentialsProviderName` 字段, **不注册 `host/port/username/password` 连接字段**。故 +`RabbitMqTestResource` 注入的 `quarkus.rabbitmq.host/port/username/password` 全部无效(unrecognized),SmallRye client 回退默认 +`localhost:5672`,连不上 Testcontainers 随机映射端口。这些连接字段由 SmallRye connector 自身读取(channel 级 +`mp.messaging.outgoing..host/port/username/password`,或全局别名 `rabbitmq-host` 等),通过查 +`smallrye-reactive-messaging-rabbitmq-4.33.0` 源码 `RabbitMQConnectorCommonConfiguration` 确认。 + +**修复:** `RabbitMqTestResource.start()` 改用 channel 级连接配置: +`mp.messaging.outgoing.flow-events-out.host/port/username/password`;保留 `quarkus.rabbitmq.devservices.enabled=false`(防 +DevServices 自启)与 `mp.messaging.outgoing.flow-events-out.enabled=true`。测试侧 amqp-client 仍用 static `host/port` +订阅交换机。 + +**验证:** 设 `FLEXMODEL_E2E_RABBITMQ=true` 运行 → +`SRMSG17036: RabbitMQ broker configured to [localhost:52374] for channel flow-events-out` + +`SRMSG17007: Connection with RabbitMQ broker established` → **Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, BUILD +SUCCESS**。两个用例(`flowDeployedEventForwardedToBroker`、`flowInstanceStartedEventWithVariablesForwarded`)断言 routing +key 与 JSON 载荷字段(含 variables 快照)完整通过。 + +**结论:** E2E 端到端验证完成。Flow 生命周期事件经 `FlowEventPublisher`→EventBus→`FlowEventRabbitmqBridge`→SmallRye +outgoing channel→RabbitMQ topic 交换机链路,routing key 与 JSON 载荷均正确。默认 `mvn test` 不依赖 Docker(E2E opt-in 跳过), +`restrictToAnnotatedClass=true` 确保 broker 资源不泄漏到其他测试。 From 4996a6ebb5f2271ea87361d6cade2b4de22874a3 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Wed, 19 Aug 2026 20:41:14 +0800 Subject: [PATCH 02/16] =?UTF-8?q?feat(flow):=20=E6=B7=BB=E5=8A=A0=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=AE=9A=E4=B9=89=E5=B1=9E=E6=80=A7=E5=BF=AB=E7=85=A7?= =?UTF-8?q?=E5=88=B0=E7=94=A8=E6=88=B7=E4=BB=BB=E5=8A=A1=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 UserTaskCommittedEvent 和 UserTaskRollbackSuspendedEvent 中增加 nodeAttributes 字段 - 为 UserTaskSuspendedEvent 添加 nodeAttributes 属性快照支持 - 更新文档说明 nodeAttributes 用于存储节点定义的扩展属性快照 - 添加测试验证 UserTaskSuspendedEvent 中的 nodeAttributes 正确传递 - 修改事件文档示例,展示 nodeAttributes 在各事件中的具体结构 - 更新消费端示例代码,明确 routing key 从 AMQP envelope 读取而非 JSON 载荷 --- .../flow/executor/UserTaskExecutor.java | 8 ++- .../event/FlowEventRabbitmqBridgeE2ETest.java | 51 +++++++++++++++++++ flexmodel-website | 2 +- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/executor/UserTaskExecutor.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/executor/UserTaskExecutor.java index e978fc5c..e4e3c80c 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/executor/UserTaskExecutor.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/executor/UserTaskExecutor.java @@ -107,9 +107,11 @@ protected void postCommit(RuntimeContext runtimeContext) { if (currentNodeInstance.getStatus() != NodeInstanceStatus.COMPLETED) { currentNodeInstance.setStatus(NodeInstanceStatus.COMPLETED); runtimeContext.getNodeInstanceList().add(currentNodeInstance); + FlowElement committedElement = runtimeContext.getCurrentNodeModel(); flowEventPublisher.publish(new UserTaskCommittedEvent(runtimeContext.getProjectId(), runtimeContext.getCaller(), runtimeContext.getFlowDeployId(), runtimeContext.getFlowInstanceId(), - currentNodeInstance.getNodeInstanceId(), currentNodeInstance.getNodeKey())); + currentNodeInstance.getNodeInstanceId(), currentNodeInstance.getNodeKey(), + committedElement == null ? null : committedElement.getProperties())); } } @@ -138,9 +140,11 @@ protected void doRollback(RuntimeContext runtimeContext) throws ProcessException newNodeInstanceBO.setProperties(currentNodeInstance.getProperties()); runtimeContext.setCurrentNodeInstance(newNodeInstanceBO); runtimeContext.getNodeInstanceList().add(newNodeInstanceBO); + FlowElement rollbackElement = FlowModelUtil.getFlowElement(runtimeContext.getFlowElementMap(), newNodeInstanceBO.getNodeKey()); flowEventPublisher.publish(new UserTaskRollbackSuspendedEvent(runtimeContext.getProjectId(), runtimeContext.getCaller(), runtimeContext.getFlowDeployId(), runtimeContext.getFlowInstanceId(), - newNodeInstanceBO.getNodeInstanceId(), newNodeInstanceBO.getNodeKey())); + newNodeInstanceBO.getNodeInstanceId(), newNodeInstanceBO.getNodeKey(), + rollbackElement == null ? null : rollbackElement.getProperties())); throw new SuspendException(ErrorEnum.ROLLBACK_SUSPEND, MessageFormat.format(Constants.NODE_INSTANCE_FORMAT, newNodeInstanceBO.getNodeKey(), FlowModelUtil.getFlowElement(runtimeContext.getFlowElementMap(), newNodeInstanceBO.getNodeKey()), diff --git a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java index 33fd282d..c390ef20 100644 --- a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java +++ b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -126,6 +127,56 @@ void flowInstanceStartedEventWithVariablesForwarded() throws Exception { } } + /** + * 验证 UserTaskSuspendedEvent 经桥接转发后,routing key 正确,且 nodeAttributes(节点定义扩展属性快照) + * 与 variables 一样在 JSON 载荷中完整保留,外部订阅者无需回查定义仓库即可读取节点配置。 + */ + @Test + void userTaskSuspendedEventWithNodeAttributesForwarded() throws Exception { + String projectId = "proj-task"; + String caller = "caller-task"; + String flowDeployId = "deploy-task"; + String flowInstanceId = "instance-task"; + String nodeInstanceId = "nodeinst-task"; + String nodeKey = "approveNode"; + Map variables = new HashMap<>(); + variables.put("amount", 500); + Map nodeAttributes = new HashMap<>(); + nodeAttributes.put("name", "审批"); + nodeAttributes.put("assignee", "manager"); + nodeAttributes.put("multiInstance", false); + + try (Connection connection = newConnection(); + Channel channel = connection.createChannel()) { + channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.TOPIC, true); + String queue = channel.queueDeclare().getQueue(); + channel.queueBind(queue, EXCHANGE, FlowEventTypes.USER_TASK_SUSPENDED); + + flowEventPublisher.publish(new UserTaskSuspendedEvent(projectId, caller, flowDeployId, + flowInstanceId, nodeInstanceId, nodeKey, variables, nodeAttributes)); + + GetResponse response = pollForMessage(channel, queue, 15000L); + assertNotNull(response, "UserTaskSuspendedEvent should be forwarded to broker within timeout"); + + assertEquals(FlowEventTypes.USER_TASK_SUSPENDED, response.getEnvelope().getRoutingKey()); + + JsonNode body = MAPPER.readTree(response.getBody()); + assertEquals(projectId, body.get("projectId").asText()); + assertEquals(flowDeployId, body.get("flowDeployId").asText()); + assertEquals(flowInstanceId, body.get("flowInstanceId").asText()); + assertEquals(nodeInstanceId, body.get("nodeInstanceId").asText()); + assertEquals(nodeKey, body.get("nodeKey").asText()); + JsonNode vars = body.get("variables"); + assertNotNull(vars, "variables snapshot should be present"); + assertEquals(500, vars.get("amount").asInt()); + JsonNode attrs = body.get("nodeAttributes"); + assertNotNull(attrs, "nodeAttributes snapshot should be present in payload"); + assertEquals("审批", attrs.get("name").asText()); + assertEquals("manager", attrs.get("assignee").asText()); + assertFalse(attrs.get("multiInstance").asBoolean(), "multiInstance should be false"); + } + } + private static Connection newConnection() throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(RabbitMqTestResource.host); diff --git a/flexmodel-website b/flexmodel-website index 3775b973..9e29089a 160000 --- a/flexmodel-website +++ b/flexmodel-website @@ -1 +1 @@ -Subproject commit 3775b973dc067794fad37f7444c62885e7077819 +Subproject commit 9e29089ab943e7a6207f33614df6d0c3eb2bb2ea From c15f676030402d93bfed25877c8b2e5b54c27cdd Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Wed, 19 Aug 2026 20:41:29 +0800 Subject: [PATCH 03/16] =?UTF-8?q?docs(progress):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改了 progress.md 文件内容 --- progress.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/progress.md b/progress.md index 36e1f568..89b87444 100644 --- a/progress.md +++ b/progress.md @@ -453,3 +453,25 @@ key 与 JSON 载荷字段(含 variables 快照)完整通过。 **结论:** E2E 端到端验证完成。Flow 生命周期事件经 `FlowEventPublisher`→EventBus→`FlowEventRabbitmqBridge`→SmallRye outgoing channel→RabbitMQ topic 交换机链路,routing key 与 JSON 载荷均正确。默认 `mvn test` 不依赖 Docker(E2E opt-in 跳过), `restrictToAnnotatedClass=true` 确保 broker 资源不泄漏到其他测试。 + +## UserTask 事件携带 nodeAttributes + 文档补全数据结构(2026-08-19) + +**需求:** 订阅者常需解析节点定义里的扩展属性做业务(审批人、表单、阈值等)。外部 RabbitMQ 订阅者访问不到定义仓库,故将节点属性快照随事件携带。 + +**改动:** + +- 事件类:`UserTaskSuspendedEvent`/`UserTaskCommittedEvent`/`UserTaskRollbackSuspendedEvent` 各加 `Map nodeAttributes` 字段,构造器做 `new HashMap<>(nodeAttributes)` 防御性拷贝(与 variables 一致)。 +- `UserTaskExecutor` 三处埋点填充 `nodeAttributes`:`doExecute` 用 `flowElement.getProperties()`;`postCommit` 用 `runtimeContext.getCurrentNodeModel().getProperties()`;`doRollback` 用 `FlowModelUtil.getFlowElement(flowElementMap, nodeKey).getProperties()`。 +- 测试:`FlowEventRabbitmqBridgeE2ETest` 新增 `userTaskSuspendedEventWithNodeAttributesForwarded`,验证 routing key + variables + nodeAttributes 经 broker 转发后 JSON 载荷完整(含中文字段名、boolean)。 +- 文档 `flexmodel-website/docs/tutorial/features/flow.md`: + - 修正公共字段表——`routingKey` 不在 JSON 载荷(是方法非字段),改注其随 AMQP envelope 投递;同步修正 Python 订阅示例从 `method.routing_key` 读取。 + - 事件总览表三个 UserTask 事件加 `nodeAttributes`。 + - 新增「事件数据结构」小节,按定义层/实例层/用户任务层分组给出每种事件的 JSON 载荷示例。 + +**验证:** + +- `mvn test-compile` → ExitCode 0。 +- E2E(`FLEXMODEL_E2E_RABBITMQ=true`)→ Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS(含新增 nodeAttributes 用例)。 +- 默认回归 → Tests run: 28, Failures: 0, Errors: 0, Skipped: 3(E2E 默认跳过),BUILD SUCCESS。 + +**设计要点:** nodeAttributes = 节点定义的 `FlowElement.properties` 快照,事件发生时已确定且不可变;外部订阅者无需回查定义仓库即可读取节点配置,解耦核心与外部系统。 From 1a74202bec18dd90ea8781d92e6c4e8af0dd6488 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Thu, 20 Aug 2026 15:19:22 +0800 Subject: [PATCH 04/16] =?UTF-8?q?feat(deploy):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E7=9A=84=20Docker=20Compose=20=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 001-mysql-single 和 002-mysql-fullstack 两种部署方案 - 配置 MySQL、Nginx、Flexmodel 服务的完整容器编排 - 添加 RabbitMQ 支持用于流程生命周期事件桥接 - 设置 UTF8MB4 字符集支持和 Nginx 反向代理规则 - 添加环境变量配置和健康检查机制 - 重构服务器资源配置以支持子域名路由功能 --- deploy/.gitignore | 5 - .../{ => 001-mysql-single}/.env | 0 .../001-mysql-single/.gitignore | 5 + .../{ => 001-mysql-single}/README.md | 0 .../{ => 001-mysql-single}/docker-compose.yml | 0 .../mysql/init/import.sql | 0 .../{ => 001-mysql-single}/mysql/my.cnf | 0 .../nginx/conf.d/default.conf.template | 0 .../{ => 001-mysql-single}/nginx/nginx.conf | 0 .../docker-compose/002-mysql-fullstack/.env | 28 ++++ .../002-mysql-fullstack/.gitignore | 6 + .../002-mysql-fullstack/README.md | 38 +++++ .../002-mysql-fullstack/docker-compose.yml | 133 ++++++++++++++++++ .../002-mysql-fullstack/mysql/init/import.sql | 0 .../002-mysql-fullstack/mysql/my.cnf | 7 + .../nginx/conf.d/default.conf.template | 105 ++++++++++++++ .../002-mysql-fullstack/nginx/nginx.conf | 35 +++++ .../src/main/resources/application.properties | 8 +- flexmodel-ui | 2 +- 19 files changed, 364 insertions(+), 8 deletions(-) delete mode 100644 deploy/.gitignore rename deploy/docker-compose/{ => 001-mysql-single}/.env (100%) create mode 100644 deploy/docker-compose/001-mysql-single/.gitignore rename deploy/docker-compose/{ => 001-mysql-single}/README.md (100%) rename deploy/docker-compose/{ => 001-mysql-single}/docker-compose.yml (100%) rename deploy/docker-compose/{ => 001-mysql-single}/mysql/init/import.sql (100%) rename deploy/docker-compose/{ => 001-mysql-single}/mysql/my.cnf (100%) rename deploy/docker-compose/{ => 001-mysql-single}/nginx/conf.d/default.conf.template (100%) rename deploy/docker-compose/{ => 001-mysql-single}/nginx/nginx.conf (100%) create mode 100644 deploy/docker-compose/002-mysql-fullstack/.env create mode 100644 deploy/docker-compose/002-mysql-fullstack/.gitignore create mode 100644 deploy/docker-compose/002-mysql-fullstack/README.md create mode 100644 deploy/docker-compose/002-mysql-fullstack/docker-compose.yml create mode 100644 deploy/docker-compose/002-mysql-fullstack/mysql/init/import.sql create mode 100644 deploy/docker-compose/002-mysql-fullstack/mysql/my.cnf create mode 100644 deploy/docker-compose/002-mysql-fullstack/nginx/conf.d/default.conf.template create mode 100644 deploy/docker-compose/002-mysql-fullstack/nginx/nginx.conf diff --git a/deploy/.gitignore b/deploy/.gitignore deleted file mode 100644 index 41f567e1..00000000 --- a/deploy/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -/docker-compose/mysql/data/ -/docker-compose/flexmodel/storage/ -/docker-compose/flexmodel/pages/ -/docker-compose/flexmodel/logs/ -/docker-compose/flexmodel/database/ \ No newline at end of file diff --git a/deploy/docker-compose/.env b/deploy/docker-compose/001-mysql-single/.env similarity index 100% rename from deploy/docker-compose/.env rename to deploy/docker-compose/001-mysql-single/.env diff --git a/deploy/docker-compose/001-mysql-single/.gitignore b/deploy/docker-compose/001-mysql-single/.gitignore new file mode 100644 index 00000000..dcf94be2 --- /dev/null +++ b/deploy/docker-compose/001-mysql-single/.gitignore @@ -0,0 +1,5 @@ +/mysql/data/ +/flexmodel/storage/ +/flexmodel/pages/ +/flexmodel/logs/ +/flexmodel/database/ \ No newline at end of file diff --git a/deploy/docker-compose/README.md b/deploy/docker-compose/001-mysql-single/README.md similarity index 100% rename from deploy/docker-compose/README.md rename to deploy/docker-compose/001-mysql-single/README.md diff --git a/deploy/docker-compose/docker-compose.yml b/deploy/docker-compose/001-mysql-single/docker-compose.yml similarity index 100% rename from deploy/docker-compose/docker-compose.yml rename to deploy/docker-compose/001-mysql-single/docker-compose.yml diff --git a/deploy/docker-compose/mysql/init/import.sql b/deploy/docker-compose/001-mysql-single/mysql/init/import.sql similarity index 100% rename from deploy/docker-compose/mysql/init/import.sql rename to deploy/docker-compose/001-mysql-single/mysql/init/import.sql diff --git a/deploy/docker-compose/mysql/my.cnf b/deploy/docker-compose/001-mysql-single/mysql/my.cnf similarity index 100% rename from deploy/docker-compose/mysql/my.cnf rename to deploy/docker-compose/001-mysql-single/mysql/my.cnf diff --git a/deploy/docker-compose/nginx/conf.d/default.conf.template b/deploy/docker-compose/001-mysql-single/nginx/conf.d/default.conf.template similarity index 100% rename from deploy/docker-compose/nginx/conf.d/default.conf.template rename to deploy/docker-compose/001-mysql-single/nginx/conf.d/default.conf.template diff --git a/deploy/docker-compose/nginx/nginx.conf b/deploy/docker-compose/001-mysql-single/nginx/nginx.conf similarity index 100% rename from deploy/docker-compose/nginx/nginx.conf rename to deploy/docker-compose/001-mysql-single/nginx/nginx.conf diff --git a/deploy/docker-compose/002-mysql-fullstack/.env b/deploy/docker-compose/002-mysql-fullstack/.env new file mode 100644 index 00000000..322c8b9e --- /dev/null +++ b/deploy/docker-compose/002-mysql-fullstack/.env @@ -0,0 +1,28 @@ +CONTAINER_NAME_PREFIX=prod2 + +# 杩愯妯″紡: native (GraalVM 鍘熺敓闀滃儚) 鎴?jvm (JVM 妯″紡) +# 鍒囨崲鏂瑰紡锛氫慨鏀?FLEXMODEL_RUN_MODE 鍜?FLEXMODEL_SERVER_IMAGE 涓よ +# native 鈫?FLEXMODEL_SERVER_IMAGE=cjbi/flexmodel-server-native:latest +# jvm 鈫?FLEXMODEL_SERVER_IMAGE=cjbi/flexmodel-server:latest +FLEXMODEL_SERVER_IMAGE=cjbi/flexmodel-server-native:latest + +# mysql +MYSQL_DATABASE=flexmodel +MYSQL_ROOT_PASSWORD=123456 + +# nginx +NGINX_HTTP_PORT=180 +NGINX_HTTPS_PORT=1443 + +# rabbitmq +RABBITMQ_DEFAULT_USER=flexmodel +RABBITMQ_DEFAULT_PASS=flexmodel +RABBITMQ_AMQP_PORT=5672 +RABBITMQ_MANAGEMENT_PORT=15672 + +# flow lifecycle events via RabbitMQ (off by default; set true to enable event bridging) +FLEXMODEL_FLOW_EVENTS_RABBITMQ_ENABLED=true + +# Project base domain (used for subdomain routing) +# e.g. preview.flexmodel.dev 鈫?{projectId}.preview.flexmodel.dev +FLEXMODEL_PROJECT_BASE_DOMAIN=flexmodel.wetech.tech \ No newline at end of file diff --git a/deploy/docker-compose/002-mysql-fullstack/.gitignore b/deploy/docker-compose/002-mysql-fullstack/.gitignore new file mode 100644 index 00000000..86c65f8b --- /dev/null +++ b/deploy/docker-compose/002-mysql-fullstack/.gitignore @@ -0,0 +1,6 @@ +/mysql/data/ +/rabbitmq/data/ +/flexmodel/storage/ +/flexmodel/pages/ +/flexmodel/logs/ +/flexmodel/database/ \ No newline at end of file diff --git a/deploy/docker-compose/002-mysql-fullstack/README.md b/deploy/docker-compose/002-mysql-fullstack/README.md new file mode 100644 index 00000000..b12fe781 --- /dev/null +++ b/deploy/docker-compose/002-mysql-fullstack/README.md @@ -0,0 +1,38 @@ +# docker-compose + +Flexmodel 生产部署,包含以下服务: + +| 服务 | 镜像 | 端口 | +|-------------------------------|-------------------------------------------|--------------| +| `mysql` | `mysql:8.0` | 3306 (内部) | +| `rabbitmq` | `rabbitmq:3.13-management` | 5672 / 15672 | +| `flexmodel` | `cjbi/flexmodel-server:latest` | 8080 (内部) | +| `flexmodel-ui` | `cjbi/flexmodel-ui:latest` | 80 | +| `flexmodel-functions-runtime` | `cjbi/flexmodel-functions-runtime:latest` | 9999 (内部) | +| `nginx` | `nginx:1.25.3` | 80 (内部) | + +## 部署命令 + +* 拉取最新镜像 + +```shell +docker-compose pull +``` + +* 启动 + +```shell +docker-compose up +``` + +* 后台启动 + +```shell +docker-compose up -d +``` + +* 停止 + +```shell +docker-compose down +``` diff --git a/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml b/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml new file mode 100644 index 00000000..5b191b8d --- /dev/null +++ b/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml @@ -0,0 +1,133 @@ +services: + mysql: + container_name: ${CONTAINER_NAME_PREFIX}-flexmodel-mysql + image: mysql:8.0 + volumes: + - ./mysql/data:/var/lib/mysql + - ./mysql/init:/docker-entrypoint-initdb.d + - ./mysql/my.cnf:/etc/my.cnf + restart: always + environment: + - MYSQL_DATABASE=${MYSQL_DATABASE} + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - TZ=GMT+08 + command: > + bash -c " + chmod 644 /etc/my.cnf + && /entrypoint.sh mysqld + " + --default-authentication-plugin=mysql_native_password + --character-set-server=utf8mb4 + networks: + - flexmodel + healthcheck: + test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ] + timeout: 20s + retries: 10 + rabbitmq: + container_name: ${CONTAINER_NAME_PREFIX}-flexmodel-rabbitmq + image: rabbitmq:3.13-management + volumes: + - ./rabbitmq/data:/var/lib/rabbitmq + restart: always + environment: + - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER} + - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS} + - TZ=GMT+08 + ports: + - ${RABBITMQ_AMQP_PORT:-5672}:5672 + - ${RABBITMQ_MANAGEMENT_PORT:-15672}:15672 + networks: + - flexmodel + healthcheck: + test: [ "CMD", "rabbitmq-diagnostics", "-q", "ping" ] + timeout: 20s + retries: 10 + flexmodel-ui: + container_name: ${CONTAINER_NAME_PREFIX}-flexmodel-ui + image: cjbi/flexmodel-ui:latest + restart: always + environment: + - TZ=GMT+08 + depends_on: + flexmodel-server: + condition: service_started + networks: + - flexmodel + nginx: + container_name: ${CONTAINER_NAME_PREFIX}-flexmodel-nginx + image: nginx:1.25.3 + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf + - ./nginx/conf.d/default.conf.template:/etc/nginx/conf.d/default.conf.template + - ./flexmodel/pages:/data/pages + restart: always + ports: + - ${NGINX_HTTP_PORT:-80}:80 + - ${NGINX_HTTPS_PORT:-443}:443 + environment: + - TZ=GMT+08 + - FLEXMODEL_PROJECT_BASE_DOMAIN=${FLEXMODEL_PROJECT_BASE_DOMAIN:-localhost} + command: > + /bin/sh -c "export FLEXMODEL_PROJECT_BASE_DOMAIN_ESCAPED=$$(echo $$FLEXMODEL_PROJECT_BASE_DOMAIN | sed 's/\\./\\\\./g') && envsubst '$$FLEXMODEL_PROJECT_BASE_DOMAIN $$FLEXMODEL_PROJECT_BASE_DOMAIN_ESCAPED' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'" + depends_on: + flexmodel-server: + condition: service_started + networks: + - flexmodel + flexmodel-server: + container_name: ${CONTAINER_NAME_PREFIX}-flexmodel-server + image: ${FLEXMODEL_SERVER_IMAGE:-cjbi/flexmodel-server-native:latest} + restart: always + volumes: + - ./flexmodel/storage:/deployments/storage + - ./flexmodel/database:/deployments/database + - ./flexmodel/logs:/deployments/logs + - ./flexmodel/pages:/deployments/pages + env_file: .env + environment: + - TZ=GMT+08 + - FLEXMODEL_DATASOURCE_DB_KIND=mysql + # readTimeout: MySQL 瀹瑰櫒閲嶅惎鍚庢睜涓畫鐣欒繛鎺ヤ細璇诲埌宸叉瀵圭锛屾棤瓒呮椂浼氭棤闄愰樆濉烇紙鏇惧鑷翠簨浠跺惊鐜喕缁擄級 + - FLEXMODEL_DATASOURCE_URL=jdbc:mysql://mysql:3306/${MYSQL_DATABASE}?connectTimeout=5000&readTimeout=15000 + - FLEXMODEL_DATASOURCE_USERNAME=root + - FLEXMODEL_DATASOURCE_PASSWORD=${MYSQL_ROOT_PASSWORD} + - FLEXMODEL_PROJECT_URL_TEMPLATE=jdbc:mysql://mysql:3306/{{databaseName}}?connectTimeout=5000&readTimeout=15000 + - QUARKUS_REST_CLIENT_FUNCTION_RUNTIME_URL=http://flexmodel-functions-runtime:9999 + - QUARKUS_HTTP_LIMITS_MAX_BODY_SIZE=100M + - FLEXMODEL_PAGES_ROOT=/data/pages + - FLEXMODEL_PAGES_URL_TEMPLATE=${FLEXMODEL_PAGES_URL_TEMPLATE:-https://{{projectId}}.example.com} + - FLEXMODEL_EDGE_URL_TEMPLATE=${FLEXMODEL_EDGE_URL_TEMPLATE:-https://{{projectId}}.example.com/functions/{{name}}} + # RabbitMQ - flow lifecycle events bridge (on by default; set FLEXMODEL_FLOW_EVENTS_RABBITMQ_ENABLED=false in .env to disable) + - QUARKUS_RABBITMQ_HOST=rabbitmq + - QUARKUS_RABBITMQ_PORT=5672 + - QUARKUS_RABBITMQ_USERNAME=${RABBITMQ_DEFAULT_USER} + - QUARKUS_RABBITMQ_PASSWORD=${RABBITMQ_DEFAULT_PASS} + - FLEXMODEL_FLOW_EVENTS_RABBITMQ_ENABLED=${FLEXMODEL_FLOW_EVENTS_RABBITMQ_ENABLED:-true} + + networks: + - flexmodel + depends_on: + mysql: + condition: service_healthy + rabbitmq: + condition: service_healthy + + flexmodel-functions-runtime: + container_name: ${CONTAINER_NAME_PREFIX}-flexmodel-functions-runtime + image: cjbi/flexmodel-functions-runtime:latest + restart: always + env_file: .env + environment: + - FLEXMODEL_JAVA_HOST=flexmodel-server + - FLEXMODEL_JAVA_PORT=8080 + - FLEXMODEL_PORT=9999 + - FLEXMODEL_HOST=0.0.0.0 + networks: + - flexmodel + depends_on: + - flexmodel-server +networks: + flexmodel: + name: ${CONTAINER_NAME_PREFIX}_flexmodel_default + driver: bridge diff --git a/deploy/docker-compose/002-mysql-fullstack/mysql/init/import.sql b/deploy/docker-compose/002-mysql-fullstack/mysql/init/import.sql new file mode 100644 index 00000000..e69de29b diff --git a/deploy/docker-compose/002-mysql-fullstack/mysql/my.cnf b/deploy/docker-compose/002-mysql-fullstack/mysql/my.cnf new file mode 100644 index 00000000..90e7e4ad --- /dev/null +++ b/deploy/docker-compose/002-mysql-fullstack/mysql/my.cnf @@ -0,0 +1,7 @@ +[client] +default-character-set=utf8mb4 +[mysql] +default-character-set=utf8mb4 +[mysqld] +character-set-server=utf8mb4 +init_connect='SET NAMES utf8mb4' diff --git a/deploy/docker-compose/002-mysql-fullstack/nginx/conf.d/default.conf.template b/deploy/docker-compose/002-mysql-fullstack/nginx/conf.d/default.conf.template new file mode 100644 index 00000000..eed82226 --- /dev/null +++ b/deploy/docker-compose/002-mysql-fullstack/nginx/conf.d/default.conf.template @@ -0,0 +1,105 @@ +# ============================================================ +# Pages alias subpath segment — avoids "//" when alias is empty. +# Evaluated lazily from $alias captured by the subdomain server_name. +# ============================================================ +map $alias $alias_path { + "" ""; + default "/$alias"; +} + +# ============================================================ +# Main domain → Admin API + UI +# /api/ → Java (admin API: /api/projects/... etc.) +# / → UI (flexmodel-ui) +# ============================================================ +server { + listen 80; + server_name localhost ${FLEXMODEL_PROJECT_BASE_DOMAIN}; + + client_max_body_size 1024m; + + # Admin API → Java + location /api/ { + proxy_pass http://flexmodel-server:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_http_version 1.1; + add_header Cache-Control no-cache; + } + + # Pages (path mode) — /pages/{projectId}/... → /data/pages/{projectId}/production/... + # 当 project-base-domain 为空时自动走 path 模式。 + location ~ ^/pages/(?[^/]+)(?.*)$ { + root /data/pages; + try_files /$pproj/production$prest /$pproj/production/index.html =404; + } + + # UI reverse proxy + location / { + proxy_pass http://flexmodel-ui:80; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_http_version 1.1; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} + +# ============================================================ +# Subdomain → Pages + Open API (data plane) +# {projectId}.{domain} → pages (production) +# {alias}.{projectId}.{domain} → pages (alias) +# /api/open/{projectId}/... → Java (open API) +# ============================================================ +server { + listen 80; + server_name ~^(?[^.]+)\.(?[^.]+)\.${FLEXMODEL_PROJECT_BASE_DOMAIN_ESCAPED}$ + ~^(?[^.]+)\.${FLEXMODEL_PROJECT_BASE_DOMAIN_ESCAPED}$; + + root /data/pages; + client_max_body_size 1024m; + + # Open API → Java (SDK sends /api/open/{projectId}/..., no rewrite needed) + location /api/ { + proxy_pass http://flexmodel-server:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_http_version 1.1; + add_header Cache-Control no-cache; + } + + # Pages base path (projectId + optional alias segment) + set $pages_base $projectId$alias_path; + + # Static assets — long cache (hash filenames) + location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?|ttf|ico|mjs)$ { + try_files /$pages_base$uri /$projectId/production$uri =404; + expires 30d; + add_header Cache-Control "public, immutable"; + } + + # Root → serve index.html directly (avoids directory match → 403) + location = / { + try_files /$pages_base/index.html /$projectId/production/index.html =404; + } + + # Pages — SPA fallback (for non-root paths) + location / { + try_files /$pages_base$uri /$pages_base/index.html /$projectId/production$uri /$projectId/production/index.html =404; + } + +} diff --git a/deploy/docker-compose/002-mysql-fullstack/nginx/nginx.conf b/deploy/docker-compose/002-mysql-fullstack/nginx/nginx.conf new file mode 100644 index 00000000..7eabb059 --- /dev/null +++ b/deploy/docker-compose/002-mysql-fullstack/nginx/nginx.conf @@ -0,0 +1,35 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + proxy_connect_timeout 600; + proxy_send_timeout 600; + proxy_read_timeout 600; + send_timeout 600; + keepalive_timeout 600; + + #gzip on; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/flexmodel-server/src/main/resources/application.properties b/flexmodel-server/src/main/resources/application.properties index 48c13008..f4bed1c4 100644 --- a/flexmodel-server/src/main/resources/application.properties +++ b/flexmodel-server/src/main/resources/application.properties @@ -1,4 +1,4 @@ -quarkus.analytics.disabled=true +quarkus.analytics.disabled=true quarkus.virtual-threads.enabled=true quarkus.swagger-ui.always-include=true quarkus.http.root-path=/api @@ -77,6 +77,10 @@ mp.messaging.outgoing.flow-events-out.exchange.name=flexmodel.flow.events mp.messaging.outgoing.flow-events-out.exchange.type=topic mp.messaging.outgoing.flow-events-out.exchange.durable=true # 通道默认关闭;启用需置 true 并提供 broker 连接配置(或开 DevServices) -mp.messaging.outgoing.flow-events-out.enabled=false +mp.messaging.outgoing.flow-events-out.enabled=${flexmodel.flow.events.rabbitmq.enabled:false} # 避免无 Docker 时 DevServices 启动失败;需 broker 时另置 true 或提供连接配置 +mp.messaging.outgoing.flow-events-out.host=${quarkus.rabbitmq.host:localhost} +mp.messaging.outgoing.flow-events-out.port=${quarkus.rabbitmq.port:5672} +mp.messaging.outgoing.flow-events-out.username=${quarkus.rabbitmq.username:guest} +mp.messaging.outgoing.flow-events-out.password=${quarkus.rabbitmq.password:guest} quarkus.rabbitmq.devservices.enabled=false diff --git a/flexmodel-ui b/flexmodel-ui index f423e7f4..74fb1dda 160000 --- a/flexmodel-ui +++ b/flexmodel-ui @@ -1 +1 @@ -Subproject commit f423e7f4b017b8f5eb1beba8b329490b94188ebf +Subproject commit 74fb1ddaddc286b1d53fc5b58301dce8af3d5af0 From 8e434f1cf73fae1306a05753da1b7cb5c9f29d76 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Thu, 20 Aug 2026 19:04:27 +0800 Subject: [PATCH 05/16] =?UTF-8?q?feat(events):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AE=9E=E6=97=B6=E4=BA=8B=E4=BB=B6=E6=A1=A5=E6=8E=A5=E5=88=B0?= =?UTF-8?q?=20RabbitMQ=20=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了 FlowEvent 基类及 11 个具体的流程生命周期事件 - 实现了 FlowEventRabbitmqBridge 用于将本地事件转发到 RabbitMQ - 新增 RealtimeRabbitmqListener 和 DataChangeEvent 用于数据变更事件转发 - 统一使用 flexmodel.events topic 交换机,支持按项目 ID 过滤订阅 - 在 application.properties 中配置 RabbitMQ 连接和通道参数 - 修改 docker-compose.yml 和 .env 配置文件中的 RabbitMQ 相关设置 - 更新测试用例以验证事件转发功能的正确性 - 完善了相关文档说明和示例代码 --- .../docker-compose/002-mysql-fullstack/.env | 3 - .../002-mysql-fullstack/docker-compose.yml | 8 +- feature_list.json | 2 +- .../dev/flexmodel/common/FlexmodelConfig.java | 19 ++++ .../flexmodel/common/config/EngineConfig.java | 13 ++- .../dev/flexmodel/flow/event/FlowEvent.java | 30 ++++- .../flow/event/FlowEventRabbitmqBridge.java | 25 ++--- .../flexmodel/realtime/DataChangeEvent.java | 81 ++++++++++++++ .../realtime/RealtimeRabbitmqListener.java | 104 ++++++++++++++++++ .../src/main/resources/application.properties | 36 ++++-- .../event/FlowEventRabbitmqBridgeE2ETest.java | 14 +-- .../flow/event/RabbitMqTestResource.java | 16 +-- .../flexmodel/pages/PageAliasManagerTest.java | 5 + .../dev/flexmodel/pages/PageDeployerTest.java | 5 + flexmodel-website | 2 +- 15 files changed, 302 insertions(+), 61 deletions(-) create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/realtime/DataChangeEvent.java create mode 100644 flexmodel-server/src/main/java/dev/flexmodel/realtime/RealtimeRabbitmqListener.java diff --git a/deploy/docker-compose/002-mysql-fullstack/.env b/deploy/docker-compose/002-mysql-fullstack/.env index 322c8b9e..437526c8 100644 --- a/deploy/docker-compose/002-mysql-fullstack/.env +++ b/deploy/docker-compose/002-mysql-fullstack/.env @@ -20,9 +20,6 @@ RABBITMQ_DEFAULT_PASS=flexmodel RABBITMQ_AMQP_PORT=5672 RABBITMQ_MANAGEMENT_PORT=15672 -# flow lifecycle events via RabbitMQ (off by default; set true to enable event bridging) -FLEXMODEL_FLOW_EVENTS_RABBITMQ_ENABLED=true - # Project base domain (used for subdomain routing) # e.g. preview.flexmodel.dev 鈫?{projectId}.preview.flexmodel.dev FLEXMODEL_PROJECT_BASE_DOMAIN=flexmodel.wetech.tech \ No newline at end of file diff --git a/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml b/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml index 5b191b8d..5a216fb8 100644 --- a/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml +++ b/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml @@ -1,4 +1,4 @@ -services: +services: mysql: container_name: ${CONTAINER_NAME_PREFIX}-flexmodel-mysql image: mysql:8.0 @@ -88,8 +88,7 @@ services: environment: - TZ=GMT+08 - FLEXMODEL_DATASOURCE_DB_KIND=mysql - # readTimeout: MySQL 瀹瑰櫒閲嶅惎鍚庢睜涓畫鐣欒繛鎺ヤ細璇诲埌宸叉瀵圭锛屾棤瓒呮椂浼氭棤闄愰樆濉烇紙鏇惧鑷翠簨浠跺惊鐜喕缁擄級 - - FLEXMODEL_DATASOURCE_URL=jdbc:mysql://mysql:3306/${MYSQL_DATABASE}?connectTimeout=5000&readTimeout=15000 + # readTimeout: MySQL 鐎圭懓娅掗柌宥呮儙閸氬孩鐫滄稉顓熺暙閻f瑨绻涢幒銉ょ窗鐠囪鍩屽鍙夘劥鐎靛湱顏敍灞炬¥鐡掑懏妞傛导姘¥闂勬劙妯嗘繅鐑囩礄閺囨儳顕遍懛缈犵皑娴犺泛鎯婇悳顖氬枙缂佹搫绱? - FLEXMODEL_DATASOURCE_URL=jdbc:mysql://mysql:3306/${MYSQL_DATABASE}?connectTimeout=5000&readTimeout=15000 - FLEXMODEL_DATASOURCE_USERNAME=root - FLEXMODEL_DATASOURCE_PASSWORD=${MYSQL_ROOT_PASSWORD} - FLEXMODEL_PROJECT_URL_TEMPLATE=jdbc:mysql://mysql:3306/{{databaseName}}?connectTimeout=5000&readTimeout=15000 @@ -98,12 +97,11 @@ services: - FLEXMODEL_PAGES_ROOT=/data/pages - FLEXMODEL_PAGES_URL_TEMPLATE=${FLEXMODEL_PAGES_URL_TEMPLATE:-https://{{projectId}}.example.com} - FLEXMODEL_EDGE_URL_TEMPLATE=${FLEXMODEL_EDGE_URL_TEMPLATE:-https://{{projectId}}.example.com/functions/{{name}}} - # RabbitMQ - flow lifecycle events bridge (on by default; set FLEXMODEL_FLOW_EVENTS_RABBITMQ_ENABLED=false in .env to disable) + # RabbitMQ - QUARKUS_RABBITMQ_HOST=rabbitmq - QUARKUS_RABBITMQ_PORT=5672 - QUARKUS_RABBITMQ_USERNAME=${RABBITMQ_DEFAULT_USER} - QUARKUS_RABBITMQ_PASSWORD=${RABBITMQ_DEFAULT_PASS} - - FLEXMODEL_FLOW_EVENTS_RABBITMQ_ENABLED=${FLEXMODEL_FLOW_EVENTS_RABBITMQ_ENABLED:-true} networks: - flexmodel diff --git a/feature_list.json b/feature_list.json index 3aa01288..7fff1fb6 100644 --- a/feature_list.json +++ b/feature_list.json @@ -90,7 +90,7 @@ "feat-003" ], "status": "done", - "evidence": "新增 dev.flexmodel.flow.event 包:FlowEvent 基类 + 11 个具体事件 + FlowEventTypes 常量;FlowEventPublisher 本地发布;FlowEventRabbitmqBridge(Instance 延迟注入,转发由 SmallRye 通道 mp.messaging.outgoing.flow-events-out.enabled 单一控制,默认 false 注入 no-op emitter 不连 broker)。埋点 DefinitionProcessor/FlowExecutor/RuntimeProcessor.terminateProcess/UserTaskExecutor。application.properties 通道默认关闭+devservices=false。验证:mvn clean compile 通过;FlowEventPublisherTest(2)/FlowEventRabbitmqBridgeTest(2) 通过;DefinitionProcessorTest(4)/RuntimeProcessorTest(17) 回归通过。" + "evidence": "新增 dev.flexmodel.flow.event 包:FlowEvent 基类 + 11 个具体事件 + FlowEventTypes 常量;FlowEventPublisher 本地发布;FlowEventRabbitmqBridge(Instance 延迟注入,转发由 SmallRye 通道 mp.messaging.outgoing.events-out.enabled 单一控制,默认 false 注入 no-op emitter 不连 broker)。埋点 DefinitionProcessor/FlowExecutor/RuntimeProcessor.terminateProcess/UserTaskExecutor。application.properties 通道默认关闭+devservices=false。验证:mvn clean compile 通过;FlowEventPublisherTest(2)/FlowEventRabbitmqBridgeTest(2) 通过;DefinitionProcessorTest(4)/RuntimeProcessorTest(17) 回归通过。" } ] } diff --git a/flexmodel-server/src/main/java/dev/flexmodel/common/FlexmodelConfig.java b/flexmodel-server/src/main/java/dev/flexmodel/common/FlexmodelConfig.java index e8e19d15..1d9eaf78 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/common/FlexmodelConfig.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/common/FlexmodelConfig.java @@ -92,6 +92,25 @@ interface PagesConfig { String rootPath(); } + /** + * 事件转发 RabbitMQ 桥接的业务开关。默认 false:不连 broker,桥接静默跳过。 + * 该开关经 application.properties 占位符映射到 SmallRye 通道 mp.messaging.outgoing.events-out.enabled。 + */ + @WithName("events") + EventsConfig events(); + + interface EventsConfig { + + @WithName("rabbitmq") + RabbitmqConfig rabbitmq(); + + interface RabbitmqConfig { + + @WithDefault("false") + boolean enabled(); + } + } + interface DatasourceConfig { @WithName("db-kind") diff --git a/flexmodel-server/src/main/java/dev/flexmodel/common/config/EngineConfig.java b/flexmodel-server/src/main/java/dev/flexmodel/common/config/EngineConfig.java index 0d4d0dcc..ae9cc482 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/common/config/EngineConfig.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/common/config/EngineConfig.java @@ -8,6 +8,7 @@ import dev.flexmodel.project.BranchRepository; import dev.flexmodel.project.ProjectService; import dev.flexmodel.realtime.RealtimeEventListener; +import dev.flexmodel.realtime.RealtimeRabbitmqListener; import dev.flexmodel.scheduling.TriggerDataChangedEventListener; import dev.flexmodel.session.SessionFactory; import dev.flexmodel.sql.JdbcSchemaProvider; @@ -37,7 +38,7 @@ public void installDatasource(@Observes StartupEvent startupEvent, long beginTime = System.currentTimeMillis(); List projects = projectService.findProjects(); for (Project project : projects) { - // 注册非 main 分支的数据库 SchemaProvider + // 娉ㄥ唽闈?main 鍒嗘敮鐨勬暟鎹簱 SchemaProvider List branches = branchRepository.findByProjectId(project.getId()); for (Branch branch : branches) { schemaRegistry.registerSchema(branch.getDatabaseName()); @@ -51,7 +52,8 @@ public void installDatasource(@Observes StartupEvent startupEvent, public SessionFactory sessionFactory(FlexmodelConfig flexmodelConfig, TriggerDataChangedEventListener triggerDataChangedEventListener, AuditDataEventListener auditDataEventListener, - RealtimeEventListener realtimeEventListener) { + RealtimeEventListener realtimeEventListener, + RealtimeRabbitmqListener realtimeRabbitmqListener) { FlexmodelConfig.DatasourceConfig datasourceConfig = flexmodelConfig.datasources().get(SYSTEM_DS_KEY); AgroalDataSource defaultDs = AgroalDataSourceFactory.createDataSource( datasourceConfig.url(), @@ -73,18 +75,19 @@ public SessionFactory sessionFactory(FlexmodelConfig flexmodelConfig, }); log.info("Total registered datasources: {}", flexmodelConfig.datasources().keySet()); SessionFactory sf = builder.build(); - // 直接注册 BuildItem 实例,绕过 SPI ServiceLoader 机制 - // 在 GraalVM 原生镜像中 ServiceLoader.load() 无法正确发现 SPI 实现类 + // 鐩存帴娉ㄥ唽 BuildItem 瀹炰緥锛岀粫杩?SPI ServiceLoader 鏈哄埗 + // 鍦?GraalVM 鍘熺敓闀滃儚涓?ServiceLoader.load() 鏃犳硶姝g‘鍙戠幇 SPI 瀹炵幇绫? sf.registerBuildItem(new dev.flexmodel.codegen.System()); sf.registerBuildItem(new DevTest()); sf.getEventPublisher().addListener(triggerDataChangedEventListener); sf.getEventPublisher().addListener(auditDataEventListener); sf.getEventPublisher().addListener(realtimeEventListener); + sf.getEventPublisher().addListener(realtimeRabbitmqListener); return sf; } /** - * 如果是 SQLite 文件数据库,确保父目录存在。 + * 濡傛灉鏄?SQLite 鏂囦欢鏁版嵁搴擄紝纭繚鐖剁洰褰曞瓨鍦ㄣ€? */ public static void ensureSqliteParentDir(String jdbcUrl) { if (jdbcUrl != null && jdbcUrl.startsWith("jdbc:sqlite:file:")) { diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEvent.java index 5177aab0..49bb14cf 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEvent.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEvent.java @@ -6,13 +6,17 @@ * Flow 生命周期事件基类。 *

* 公共字段 {@code projectId}、{@code caller}、{@code timestamp}(构造时设为当前毫秒)。 - * 抽象方法 {@link #routingKey()} 既作 Vert.x EventBus 地址,也作 RabbitMQ 转发的 routing key。 + * 抽象方法 {@link #routingKey()} 作 Vert.x EventBus 地址(静态常量,供 {@code @ConsumeEvent} 匹配)。 + * {@link #rabbitmqRoutingKey()} 在 routing key 中插入 projectId,作 RabbitMQ 转发的 routing key, + * 格式为 {@code flow..},便于消费端按项目订阅。 * 本地事件始终经 {@code FlowEventPublisher} 发布到 EventBus;RabbitMQ 转发可选。 * * @author cjbi */ public abstract class FlowEvent implements Serializable { + private static final String FLOW_PREFIX = "flow."; + private final String projectId; private final String caller; private final long timestamp; @@ -24,12 +28,30 @@ protected FlowEvent(String projectId, String caller) { } /** - * 路由 key:兼作 EventBus 地址与 RabbitMQ routing key。 + * EventBus 地址(静态常量),供 {@code @ConsumeEvent} 匹配。 + *

+ * 形如 {@code flow.instance.started},不含 projectId。 * - * @return 不可为空的路由 key + * @return 不可为空的 EventBus 地址 */ public abstract String routingKey(); + /** + * RabbitMQ routing key,在 {@link #routingKey()} 中插入 projectId。 + *

+ * 形如 {@code flow..instance.started},便于消费端按项目通配符订阅。 + * 若 routingKey 不以 {@code flow.} 开头则原样返回(防御性兜底)。 + * + * @return 带 projectId 的 RabbitMQ routing key + */ + public String rabbitmqRoutingKey() { + String key = routingKey(); + if (key.startsWith(FLOW_PREFIX)) { + return FLOW_PREFIX + projectId + "." + key.substring(FLOW_PREFIX.length()); + } + return key; + } + public String getProjectId() { return projectId; } @@ -41,4 +63,4 @@ public String getCaller() { public long getTimestamp() { return timestamp; } -} +} \ No newline at end of file diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java index 7d4d4193..6a3b22c3 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java @@ -17,9 +17,10 @@ * Flow 生命周期事件 RabbitMQ 桥接。 *

* 订阅全部内部 EventBus 事件并转发到单个 topic 交换机(routing key 复用事件地址)。 - * 是否连 broker 由 SmallRye 通道 {@code mp.messaging.outgoing.flow-events-out.enabled} 单一控制: - * 默认关闭时 SmallRye 注入 no-op emitter,{@link #forward(FlowEvent)} 发送即丢弃、不连 broker; - * 本地事件照常经 {@link FlowEventPublisher} 发布。启用需置该通道 {@code enabled=true} 并提供 broker 连接配置。 + * 是否连 broker 由业务开关 {@code flexmodel.events.rabbitmq.enabled} 单一控制(默认 false): + * 该开关经 application.properties 占位符映射到 SmallRye 通道 {@code mp.messaging.outgoing.events-out.enabled}, + * 关闭时 SmallRye 注入 no-op emitter,{@link #forward(FlowEvent)} 发送即丢弃、不连 broker; + * 本地事件照常经 {@link FlowEventPublisher} 发布。启用需置该开关 {@code true} 并提供 broker 连接配置。 *

* 每个事件一个 {@code @ConsumeEvent} 方法、消费具体类型,沿用代码库已验证的同类型发布/消费模式, * 避免多态反序列化不确定性。 @@ -35,13 +36,9 @@ public class FlowEventRabbitmqBridge { private static final Logger LOGGER = LoggerFactory.getLogger(FlowEventRabbitmqBridge.class); @Inject - @Channel("flow-events-out") + @Channel("events-out") Instance> flowEventEmitterInstance; - // 通道是否启用;默认关闭,禁用时 SmallRye 注入 no-op emitter,转发静默跳过、不连 broker - @ConfigProperty(name = "mp.messaging.outgoing.flow-events-out.enabled", defaultValue = "false") - boolean channelEnabled; - @ConsumeEvent(value = FlowEventTypes.FLOW_CREATED, blocking = false) public void onFlowCreated(FlowCreatedEvent event) { forward(event); @@ -102,28 +99,24 @@ public void onUserTaskRollbackSuspended(UserTaskRollbackSuspendedEvent event) { * 通道未启用时 SmallRye 提供 no-op emitter,发送即丢弃、不连 broker。 */ private void forward(FlowEvent event) { - // 通道未启用时静默跳过:不连 broker、不打日志,保证默认配置零侵入 - if (!channelEnabled) { - return; - } if (flowEventEmitterInstance.isUnsatisfied()) { - LOGGER.debug("flow-events-out channel not resolvable, skip forwarding.||routingKey={}", event.routingKey()); + LOGGER.debug("events-out channel not resolvable, skip forwarding.||routingKey={}", event.rabbitmqRoutingKey()); return; } try { MutinyEmitter emitter = flowEventEmitterInstance.get(); OutgoingRabbitMQMetadata metadata = new OutgoingRabbitMQMetadata.Builder() - .withRoutingKey(event.routingKey()) + .withRoutingKey(event.rabbitmqRoutingKey()) .build(); emitter.sendMessage(Message.of(event, Metadata.of(metadata))) .onFailure() .invoke(e -> LOGGER.warn("forward flow event to rabbitmq failed.||routingKey={}||payloadType={}", - event.routingKey(), event.getClass().getSimpleName(), e)) + event.rabbitmqRoutingKey(), event.getClass().getSimpleName(), e)) .subscribe() .asCompletionStage(); } catch (Exception e) { // 仅记录 routing key 与异常,不打印事件载荷(避免泄露流程变量) - LOGGER.warn("forward flow event to rabbitmq failed (sync).||routingKey={}", event.routingKey(), e); + LOGGER.warn("forward flow event to rabbitmq failed (sync).||routingKey={}", event.rabbitmqRoutingKey(), e); } } } diff --git a/flexmodel-server/src/main/java/dev/flexmodel/realtime/DataChangeEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/realtime/DataChangeEvent.java new file mode 100644 index 00000000..9a835628 --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/realtime/DataChangeEvent.java @@ -0,0 +1,81 @@ +package dev.flexmodel.realtime; + +import java.io.Serializable; +import java.util.Map; + +/** + * 数据变更事件载荷,由 {@link RealtimeRabbitmqListener} 从引擎 {@code ChangedEvent} 转换而来, + * 经 SmallRye RabbitMQ 通道 {@code data-events-out} 投递到 {@code flexmodel.events} topic 交换机。 + *

+ * routing key 形如 {@code data...},便于消费端按项目、模型或操作类型订阅过滤。 + * + * @author cjbi + */ +public class DataChangeEvent implements Serializable { + + private final String routingKey; + private final String projectId; + private final String event; + private final String model; + private final String schema; + private final Object recordId; + private final long timestamp; + private final int affectedRows; + private final Map data; + private final Map oldData; + + public DataChangeEvent(String routingKey, String projectId, String event, String model, String schema, + Object recordId, long timestamp, int affectedRows, + Map data, Map oldData) { + this.routingKey = routingKey; + this.projectId = projectId; + this.event = event; + this.model = model; + this.schema = schema; + this.recordId = recordId; + this.timestamp = timestamp; + this.affectedRows = affectedRows; + this.data = data; + this.oldData = oldData; + } + + public String getRoutingKey() { + return routingKey; + } + + public String getProjectId() { + return projectId; + } + + public String getEvent() { + return event; + } + + public String getModel() { + return model; + } + + public String getSchema() { + return schema; + } + + public Object getRecordId() { + return recordId; + } + + public long getTimestamp() { + return timestamp; + } + + public int getAffectedRows() { + return affectedRows; + } + + public Map getData() { + return data; + } + + public Map getOldData() { + return oldData; + } +} \ No newline at end of file diff --git a/flexmodel-server/src/main/java/dev/flexmodel/realtime/RealtimeRabbitmqListener.java b/flexmodel-server/src/main/java/dev/flexmodel/realtime/RealtimeRabbitmqListener.java new file mode 100644 index 00000000..2c56fb5a --- /dev/null +++ b/flexmodel-server/src/main/java/dev/flexmodel/realtime/RealtimeRabbitmqListener.java @@ -0,0 +1,104 @@ +package dev.flexmodel.realtime; + +import dev.flexmodel.codegen.entity.Project; +import dev.flexmodel.event.ChangedEvent; +import dev.flexmodel.event.EventListener; +import dev.flexmodel.project.ProjectRepository; +import io.smallrye.reactive.messaging.MutinyEmitter; +import io.smallrye.reactive.messaging.rabbitmq.OutgoingRabbitMQMetadata; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import java.util.Map; + +/** + * 数据变更事件 RabbitMQ 监听器,桥接引擎层的 EventPublisher 到 RabbitMQ topic 交换机。 + *

+ * 参照 {@link RealtimeEventListener} 的模式:监听后置事件(INSERTED / UPDATED / DELETED), + * 仅在操作成功时转发。与 WebSocket 实时广播互不影响,各走各的通道。 + *

+ * routing key 形如 {@code data...},投递到与 flow 事件共用的 + * {@code flexmodel.events} topic 交换机,消费端可按项目、模型或操作类型订阅。 + *

+ * 投递为尽力而为:失败仅告警,不重试、不阻塞、不回滚。 + * + * @author cjbi + */ +@Slf4j +@ApplicationScoped +public class RealtimeRabbitmqListener implements EventListener { + + @Inject + @Channel("data-events-out") + Instance> dataEventEmitterInstance; + + @Inject + ProjectRepository projectRepository; + + @Override + public void onChanged(ChangedEvent event) { + // 操作失败时不转发,避免无谓的对象构造与 IO + if (!event.isSuccess()) { + return; + } + if (dataEventEmitterInstance.isUnsatisfied()) { + log.debug("data-events-out channel not resolvable, skip forwarding.||model={}", event.getModelName()); + return; + } + try { + Project project = projectRepository.findProjectByDatabaseName(event.getSchemaName()); + String operation = mapEventType(event.getEventType()); + String routingKey = "data." + project.getId() + "." + event.getModelName() + "." + operation.toLowerCase(); + Map newData = event.getNewData() != null ? event.getNewData() : Map.of(); + Map oldData = event.getOldData() != null ? event.getOldData() : Map.of(); + + DataChangeEvent payload = new DataChangeEvent( + routingKey, project.getId(), operation, event.getModelName(), event.getSchemaName(), + event.getId(), event.getTimestamp(), event.getAffectedRows(), newData, oldData); + + MutinyEmitter emitter = dataEventEmitterInstance.get(); + OutgoingRabbitMQMetadata metadata = new OutgoingRabbitMQMetadata.Builder() + .withRoutingKey(routingKey) + .build(); + // 异步发送,不阻塞引擎写入线程;失败仅告警 + emitter.sendMessage(Message.of(payload, Metadata.of(metadata))) + .onFailure() + .invoke(e -> log.warn("forward data change event to rabbitmq failed.||model={}||operation={}", + event.getModelName(), operation, e)) + .subscribe() + .asCompletionStage(); + } catch (Exception e) { + log.warn("forward data change event to rabbitmq failed (sync).||model={}", event.getModelName(), e); + } + } + + @Override + public boolean supports(String eventType) { + return "INSERTED".equals(eventType) + || "UPDATED".equals(eventType) + || "DELETED".equals(eventType); + } + + @Override + public int getOrder() { + // 低于 RealtimeEventListener(1000),不影响业务实时推送 + return 1100; + } + + /** + * 引擎事件类型映射到协议操作类型:INSERTED -> INSERT, UPDATED -> UPDATE, DELETED -> DELETE + */ + private String mapEventType(String engineEventType) { + return switch (engineEventType) { + case "INSERTED" -> "INSERT"; + case "UPDATED" -> "UPDATE"; + case "DELETED" -> "DELETE"; + default -> engineEventType; + }; + } +} \ No newline at end of file diff --git a/flexmodel-server/src/main/resources/application.properties b/flexmodel-server/src/main/resources/application.properties index f4bed1c4..efd172e2 100644 --- a/flexmodel-server/src/main/resources/application.properties +++ b/flexmodel-server/src/main/resources/application.properties @@ -1,4 +1,4 @@ -quarkus.analytics.disabled=true +quarkus.analytics.disabled=true quarkus.virtual-threads.enabled=true quarkus.swagger-ui.always-include=true quarkus.http.root-path=/api @@ -72,15 +72,29 @@ flexmodel.pages.root-path=./pages # Flow 生命周期事件 RabbitMQ 桥接(可选,默认关闭) # 本地 EventBus 事件始终发布;仅当 SmallRye 通道启用时才连 broker 并转发到 topic 交换机。 # 通道禁用时 SmallRye 注入 no-op MutinyEmitter,桥接 forward() 静默丢弃,无需 broker 即可启动。 -mp.messaging.outgoing.flow-events-out.connector=smallrye-rabbitmq -mp.messaging.outgoing.flow-events-out.exchange.name=flexmodel.flow.events -mp.messaging.outgoing.flow-events-out.exchange.type=topic -mp.messaging.outgoing.flow-events-out.exchange.durable=true +mp.messaging.outgoing.events-out.connector=smallrye-rabbitmq +mp.messaging.outgoing.events-out.exchange.name=flexmodel.events +mp.messaging.outgoing.events-out.exchange.type=topic +mp.messaging.outgoing.events-out.exchange.durable=true # 通道默认关闭;启用需置 true 并提供 broker 连接配置(或开 DevServices) -mp.messaging.outgoing.flow-events-out.enabled=${flexmodel.flow.events.rabbitmq.enabled:false} +mp.messaging.outgoing.events-out.enabled=${flexmodel.events.rabbitmq.enabled:false} # 避免无 Docker 时 DevServices 启动失败;需 broker 时另置 true 或提供连接配置 -mp.messaging.outgoing.flow-events-out.host=${quarkus.rabbitmq.host:localhost} -mp.messaging.outgoing.flow-events-out.port=${quarkus.rabbitmq.port:5672} -mp.messaging.outgoing.flow-events-out.username=${quarkus.rabbitmq.username:guest} -mp.messaging.outgoing.flow-events-out.password=${quarkus.rabbitmq.password:guest} -quarkus.rabbitmq.devservices.enabled=false +mp.messaging.outgoing.events-out.host=${quarkus.rabbitmq.host:localhost} +mp.messaging.outgoing.events-out.port=${quarkus.rabbitmq.port:5672} +mp.messaging.outgoing.events-out.username=${quarkus.rabbitmq.username:guest} +mp.messaging.outgoing.events-out.password=${quarkus.rabbitmq.password:guest} +# 数据变更事件 RabbitMQ 桥接(可选,默认关闭) +# 复用同一 flexmodel.events 交换机,routing key: data.. +mp.messaging.outgoing.data-events-out.connector=smallrye-rabbitmq +mp.messaging.outgoing.data-events-out.exchange.name=flexmodel.events +mp.messaging.outgoing.data-events-out.exchange.type=topic +mp.messaging.outgoing.data-events-out.exchange.durable=true +mp.messaging.outgoing.data-events-out.enabled=${flexmodel.realtime.events.rabbitmq.enabled:true} +mp.messaging.outgoing.data-events-out.host=${quarkus.rabbitmq.host:localhost} +mp.messaging.outgoing.data-events-out.port=${quarkus.rabbitmq.port:5672} +mp.messaging.outgoing.data-events-out.username=${quarkus.rabbitmq.username:guest} +mp.messaging.outgoing.data-events-out.password=${quarkus.rabbitmq.password:guest} +quarkus.rabbitmq.username=flexmodel +quarkus.rabbitmq.password=flexmodel +quarkus.rabbitmq.devservices.enabled=true + diff --git a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java index c390ef20..1d4d8aa8 100644 --- a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java +++ b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java @@ -47,7 +47,7 @@ @QuarkusTestResource(value = RabbitMqTestResource.class, restrictToAnnotatedClass = true) public class FlowEventRabbitmqBridgeE2ETest { - private static final String EXCHANGE = "flexmodel.flow.events"; + private static final String EXCHANGE = "flexmodel.events"; private static final ObjectMapper MAPPER = new ObjectMapper(); @Inject @@ -68,7 +68,7 @@ void flowDeployedEventForwardedToBroker() throws Exception { // 确保交换机存在(与 SmallRye 声明参数一致:topic、durable),幂等 channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.TOPIC, true); String queue = channel.queueDeclare().getQueue(); - channel.queueBind(queue, EXCHANGE, FlowEventTypes.FLOW_DEPLOYED); + channel.queueBind(queue, EXCHANGE, "flow.proj-e2e.deployed"); flowEventPublisher.publish( new FlowDeployedEvent(projectId, caller, flowModuleId, flowDeployId)); @@ -77,7 +77,7 @@ void flowDeployedEventForwardedToBroker() throws Exception { assertNotNull(response, "FlowDeployedEvent should be forwarded to broker within timeout"); Envelope envelope = response.getEnvelope(); - assertEquals(FlowEventTypes.FLOW_DEPLOYED, envelope.getRoutingKey(), + assertEquals("flow.proj-e2e.deployed", envelope.getRoutingKey(), "AMQP routing key should match event routing key"); JsonNode body = MAPPER.readTree(response.getBody()); @@ -106,7 +106,7 @@ void flowInstanceStartedEventWithVariablesForwarded() throws Exception { Channel channel = connection.createChannel()) { channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.TOPIC, true); String queue = channel.queueDeclare().getQueue(); - channel.queueBind(queue, EXCHANGE, FlowEventTypes.FLOW_INSTANCE_STARTED); + channel.queueBind(queue, EXCHANGE, "flow.proj-vars.instance.started"); flowEventPublisher.publish( new FlowInstanceStartedEvent(projectId, caller, flowDeployId, flowInstanceId, variables)); @@ -114,7 +114,7 @@ void flowInstanceStartedEventWithVariablesForwarded() throws Exception { GetResponse response = pollForMessage(channel, queue, 15000L); assertNotNull(response, "FlowInstanceStartedEvent should be forwarded to broker within timeout"); - assertEquals(FlowEventTypes.FLOW_INSTANCE_STARTED, response.getEnvelope().getRoutingKey()); + assertEquals("flow.proj-vars.instance.started", response.getEnvelope().getRoutingKey()); JsonNode body = MAPPER.readTree(response.getBody()); assertEquals(projectId, body.get("projectId").asText()); @@ -150,7 +150,7 @@ void userTaskSuspendedEventWithNodeAttributesForwarded() throws Exception { Channel channel = connection.createChannel()) { channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.TOPIC, true); String queue = channel.queueDeclare().getQueue(); - channel.queueBind(queue, EXCHANGE, FlowEventTypes.USER_TASK_SUSPENDED); + channel.queueBind(queue, EXCHANGE, "flow.proj-task.usertask.suspended"); flowEventPublisher.publish(new UserTaskSuspendedEvent(projectId, caller, flowDeployId, flowInstanceId, nodeInstanceId, nodeKey, variables, nodeAttributes)); @@ -158,7 +158,7 @@ void userTaskSuspendedEventWithNodeAttributesForwarded() throws Exception { GetResponse response = pollForMessage(channel, queue, 15000L); assertNotNull(response, "UserTaskSuspendedEvent should be forwarded to broker within timeout"); - assertEquals(FlowEventTypes.USER_TASK_SUSPENDED, response.getEnvelope().getRoutingKey()); + assertEquals("flow.proj-task.usertask.suspended", response.getEnvelope().getRoutingKey()); JsonNode body = MAPPER.readTree(response.getBody()); assertEquals(projectId, body.get("projectId").asText()); diff --git a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/RabbitMqTestResource.java b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/RabbitMqTestResource.java index b5142b44..e926ddd4 100644 --- a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/RabbitMqTestResource.java +++ b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/RabbitMqTestResource.java @@ -7,12 +7,12 @@ import java.util.Map; /** - * Testcontainers 启动 RabbitMQ broker,并向 Quarkus 注入连接配置与启用 flow-events-out 通道。 + * Testcontainers 启动 RabbitMQ broker,并向 Quarkus 注入连接配置与启用 events-out 通道。 *

* 用于端到端验证 FlowEventRabbitmqBridge 将事件以正确 routing key 与 JSON 载荷推送到 topic 交换机。 *

* 连接配置注入到 SmallRye RabbitMQ connector 的 channel 级属性 - * ({@code mp.messaging.outgoing.flow-events-out.host/port/username/password})。 + * ({@code mp.messaging.outgoing.events-out.host/port/username/password})。 * 注意:{@code quarkus-messaging-rabbitmq} 扩展的 {@code quarkus.rabbitmq.*} 只注册 DevServices 与 * credentials provider 字段,不注册 host/port/username/password 连接字段——这些由 SmallRye * connector 自身读取(channel 级或全局 {@code rabbitmq-*} 别名)。显式提供 channel 级连接配置后, @@ -39,14 +39,14 @@ public Map start() { Map config = new LinkedHashMap<>(); // SmallRye RabbitMQ connector channel 级连接配置(quarkus.rabbitmq.* 不注册连接字段) - config.put("mp.messaging.outgoing.flow-events-out.host", host); - config.put("mp.messaging.outgoing.flow-events-out.port", String.valueOf(port)); - config.put("mp.messaging.outgoing.flow-events-out.username", username); - config.put("mp.messaging.outgoing.flow-events-out.password", password); + config.put("mp.messaging.outgoing.events-out.host", host); + config.put("mp.messaging.outgoing.events-out.port", String.valueOf(port)); + config.put("mp.messaging.outgoing.events-out.username", username); + config.put("mp.messaging.outgoing.events-out.password", password); // 显式提供连接配置后 DevServices 自动旁路;此处再确认禁用,避免无 Docker 时误触 config.put("quarkus.rabbitmq.devservices.enabled", "false"); - // 启用 flow-events-out 通道,使桥接真正转发到 broker - config.put("mp.messaging.outgoing.flow-events-out.enabled", "true"); + // 启用 events-out 通道,使桥接真正转发到 broker + config.put("mp.messaging.outgoing.events-out.enabled", "true"); return config; } diff --git a/flexmodel-server/src/test/java/dev/flexmodel/pages/PageAliasManagerTest.java b/flexmodel-server/src/test/java/dev/flexmodel/pages/PageAliasManagerTest.java index 41283e55..fa499fe3 100644 --- a/flexmodel-server/src/test/java/dev/flexmodel/pages/PageAliasManagerTest.java +++ b/flexmodel-server/src/test/java/dev/flexmodel/pages/PageAliasManagerTest.java @@ -206,6 +206,11 @@ public PagesConfig pages() { return () -> pagesRootPath; } + @Override + public EventsConfig events() { + return null; + } + @Override public String projectUrlTemplate() { return ""; } diff --git a/flexmodel-server/src/test/java/dev/flexmodel/pages/PageDeployerTest.java b/flexmodel-server/src/test/java/dev/flexmodel/pages/PageDeployerTest.java index 3bb52c40..209c5409 100644 --- a/flexmodel-server/src/test/java/dev/flexmodel/pages/PageDeployerTest.java +++ b/flexmodel-server/src/test/java/dev/flexmodel/pages/PageDeployerTest.java @@ -235,6 +235,11 @@ public PagesConfig pages() { return () -> pagesRootPath; } + @Override + public EventsConfig events() { + return null; + } + @Override public String projectUrlTemplate() { return ""; } diff --git a/flexmodel-website b/flexmodel-website index 9e29089a..d18ba09f 160000 --- a/flexmodel-website +++ b/flexmodel-website @@ -1 +1 @@ -Subproject commit 9e29089ab943e7a6207f33614df6d0c3eb2bb2ea +Subproject commit d18ba09f169968ea279254a6923ef63fca4b66f4 From 39e43b405b4700be4f4c0d03179b16e93de0aac3 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Thu, 20 Aug 2026 19:07:26 +0800 Subject: [PATCH 06/16] =?UTF-8?q?docs(deploy):=20=E6=9B=B4=E6=96=B0=20MySQ?= =?UTF-8?q?L=20=E8=BF=9E=E6=8E=A5=E8=B6=85=E6=97=B6=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 MySQL readTimeout 注释从英文翻译为中文 - 说明容器重启后连接池残留连接导致的超时问题 - 解释无超时设置可能导致事件循环无限阻塞的情况 --- deploy/docker-compose/002-mysql-fullstack/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml b/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml index 5a216fb8..685491ed 100644 --- a/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml +++ b/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml @@ -88,7 +88,7 @@ environment: - TZ=GMT+08 - FLEXMODEL_DATASOURCE_DB_KIND=mysql - # readTimeout: MySQL 鐎圭懓娅掗柌宥呮儙閸氬孩鐫滄稉顓熺暙閻f瑨绻涢幒銉ょ窗鐠囪鍩屽鍙夘劥鐎靛湱顏敍灞炬¥鐡掑懏妞傛导姘¥闂勬劙妯嗘繅鐑囩礄閺囨儳顕遍懛缈犵皑娴犺泛鎯婇悳顖氬枙缂佹搫绱? - FLEXMODEL_DATASOURCE_URL=jdbc:mysql://mysql:3306/${MYSQL_DATABASE}?connectTimeout=5000&readTimeout=15000 + # readTimeout: MySQL 容器重启后池中残留连接会读到已死对端,无超时会无限阻塞(曾导致事件循环冻结) - FLEXMODEL_DATASOURCE_USERNAME=root - FLEXMODEL_DATASOURCE_PASSWORD=${MYSQL_ROOT_PASSWORD} - FLEXMODEL_PROJECT_URL_TEMPLATE=jdbc:mysql://mysql:3306/{{databaseName}}?connectTimeout=5000&readTimeout=15000 From 59398323d4f1083d5d789e5dba8d804b80329ee9 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Thu, 20 Aug 2026 20:38:09 +0800 Subject: [PATCH 07/16] =?UTF-8?q?chore(config):=20=E6=9B=B4=E6=96=B0=20ngi?= =?UTF-8?q?nx=20=E7=AB=AF=E5=8F=A3=E9=85=8D=E7=BD=AE=E4=B8=BA=E6=A0=87?= =?UTF-8?q?=E5=87=86=E7=AB=AF=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 NGINX_HTTP_PORT 从 180 更改为 80 - 将 NGINX_HTTPS_PORT 从 1443 更改为 443 --- deploy/docker-compose/002-mysql-fullstack/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/docker-compose/002-mysql-fullstack/.env b/deploy/docker-compose/002-mysql-fullstack/.env index 437526c8..59ca7a4d 100644 --- a/deploy/docker-compose/002-mysql-fullstack/.env +++ b/deploy/docker-compose/002-mysql-fullstack/.env @@ -11,8 +11,8 @@ MYSQL_DATABASE=flexmodel MYSQL_ROOT_PASSWORD=123456 # nginx -NGINX_HTTP_PORT=180 -NGINX_HTTPS_PORT=1443 +NGINX_HTTP_PORT=80 +NGINX_HTTPS_PORT=443 # rabbitmq RABBITMQ_DEFAULT_USER=flexmodel From fee16412bf0b19427ac323bc899860243d860c52 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Fri, 21 Aug 2026 04:10:28 +0800 Subject: [PATCH 08/16] =?UTF-8?q?feat(datamodeling):=20=E5=B0=86ER?= =?UTF-8?q?=E5=9B=BE=E8=A7=86=E5=9B=BE=E9=87=8D=E6=9E=84=E4=B8=BA=E6=A8=A1?= =?UTF-8?q?=E6=80=81=E6=A1=86=E5=B9=B6=E4=BC=98=E5=8C=96RabbitMQ=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除application.properties中的开发环境RabbitMQ配置 - 将RabbitMQ开发配置移至application-dev.properties文件 - 添加getModelList导入以支持ER图数据获取 - 替换列表视图按钮为ER图查看按钮 - 实现ER图模态框功能,支持打开和关闭 - 添加ER图数据加载状态管理 - 集成ERDiagram组件到模态框中显示实体关系图 - 优化页面布局结构,将视图模式切换逻辑移除 --- flexmodel-server/src/main/resources/application-dev.properties | 3 +++ flexmodel-server/src/main/resources/application.properties | 3 --- flexmodel-ui | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flexmodel-server/src/main/resources/application-dev.properties b/flexmodel-server/src/main/resources/application-dev.properties index 7fbae701..e41680df 100644 --- a/flexmodel-server/src/main/resources/application-dev.properties +++ b/flexmodel-server/src/main/resources/application-dev.properties @@ -1,3 +1,6 @@ quarkus.log.category."io.quarkus.arc".level=INFO quarkus.log.category."io.agroal".level=INFO quarkus.log.category."dev.flexmodel".level=INFO +quarkus.rabbitmq.username=flexmodel +quarkus.rabbitmq.password=flexmodel +quarkus.rabbitmq.devservices.enabled=true diff --git a/flexmodel-server/src/main/resources/application.properties b/flexmodel-server/src/main/resources/application.properties index efd172e2..000ab852 100644 --- a/flexmodel-server/src/main/resources/application.properties +++ b/flexmodel-server/src/main/resources/application.properties @@ -94,7 +94,4 @@ mp.messaging.outgoing.data-events-out.host=${quarkus.rabbitmq.host:localhost} mp.messaging.outgoing.data-events-out.port=${quarkus.rabbitmq.port:5672} mp.messaging.outgoing.data-events-out.username=${quarkus.rabbitmq.username:guest} mp.messaging.outgoing.data-events-out.password=${quarkus.rabbitmq.password:guest} -quarkus.rabbitmq.username=flexmodel -quarkus.rabbitmq.password=flexmodel -quarkus.rabbitmq.devservices.enabled=true diff --git a/flexmodel-ui b/flexmodel-ui index 74fb1dda..1100449c 160000 --- a/flexmodel-ui +++ b/flexmodel-ui @@ -1 +1 @@ -Subproject commit 74fb1ddaddc286b1d53fc5b58301dce8af3d5af0 +Subproject commit 1100449c6c31903fe53e642794c42bb55ccd5c12 From f114c4cafd67d936bc3ad52258886ff9bba5c985 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Fri, 21 Aug 2026 11:12:36 +0800 Subject: [PATCH 09/16] =?UTF-8?q?fix(core):=20=E7=BB=9F=E4=B8=80=E8=B0=83?= =?UTF-8?q?=E5=BA=A6=E8=A7=A6=E5=8F=91=E5=99=A8=E7=B1=BB=E5=9E=8B=E6=9E=9A?= =?UTF-8?q?=E4=B8=BE=E5=80=BC=E4=BB=8E=20SCHEDULED=20=E6=94=B9=E4=B8=BA=20?= =?UTF-8?q?SCHEDULE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 TriggerType 枚举中的 SCHEDULED 值统一更改为 SCHEDULE - 更新 FlowEventRabbitmqBridge 和 RealtimeRabbitmqListener 中 RabbitMQ 消息持久化配置 - 修改 TriggerService 中调度触发器类型的查询条件和文档注释 - 更新 dev_test.fml 和 project.fml 中的触发器类型配置数据 - 修正 PermissionFilterTest 和 TriggerResourceTest 中的测试断言和注释 - 更新前端 TriggerForm、TriggerList 组件中的触发器类型判断逻辑 - 修改前端 trigger.ts 和 types/trigger.ts 中的类型定义 --- .../flow/event/FlowEventRabbitmqBridge.java | 2 + .../realtime/RealtimeRabbitmqListener.java | 4 +- .../flexmodel/scheduling/TriggerService.java | 6 +-- .../src/main/resources/dev_test.fml | 6 +-- .../src/main/resources/project.fml | 2 +- .../flexmodel/rest/PermissionFilterTest.java | 2 +- .../flexmodel/rest/TriggerResourceTest.java | 46 +++++++++---------- flexmodel-ui | 2 +- 8 files changed, 37 insertions(+), 33 deletions(-) diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java index 6a3b22c3..0a0816e6 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java @@ -107,6 +107,8 @@ private void forward(FlowEvent event) { MutinyEmitter emitter = flowEventEmitterInstance.get(); OutgoingRabbitMQMetadata metadata = new OutgoingRabbitMQMetadata.Builder() .withRoutingKey(event.rabbitmqRoutingKey()) + // 持久化投递(delivery_mode=2):使消息在 broker 重启后仍可恢复,配合 durable 交换机与 durable 队列生效 + .withDeliveryMode(2) .build(); emitter.sendMessage(Message.of(event, Metadata.of(metadata))) .onFailure() diff --git a/flexmodel-server/src/main/java/dev/flexmodel/realtime/RealtimeRabbitmqListener.java b/flexmodel-server/src/main/java/dev/flexmodel/realtime/RealtimeRabbitmqListener.java index 2c56fb5a..c76098e0 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/realtime/RealtimeRabbitmqListener.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/realtime/RealtimeRabbitmqListener.java @@ -64,6 +64,8 @@ public void onChanged(ChangedEvent event) { MutinyEmitter emitter = dataEventEmitterInstance.get(); OutgoingRabbitMQMetadata metadata = new OutgoingRabbitMQMetadata.Builder() .withRoutingKey(routingKey) + // 持久化投递(delivery_mode=2):使消息在 broker 重启后仍可恢复,配合 durable 交换机与 durable 队列生效 + .withDeliveryMode(2) .build(); // 异步发送,不阻塞引擎写入线程;失败仅告警 emitter.sendMessage(Message.of(payload, Metadata.of(metadata))) @@ -101,4 +103,4 @@ private String mapEventType(String engineEventType) { default -> engineEventType; }; } -} \ No newline at end of file +} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/scheduling/TriggerService.java b/flexmodel-server/src/main/java/dev/flexmodel/scheduling/TriggerService.java index 975eb5f4..25a174a1 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/scheduling/TriggerService.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/scheduling/TriggerService.java @@ -64,7 +64,7 @@ public class TriggerService { /** * 应用启动时扫描全部启用项目的 f_trigger 表, - * 对其中调度触发器(SCHEDULED)按 scheduler.getTrigger(...) 查询 Quartz 中是否已存在对应调度任务, + * 对其中调度触发器(SCHEDULE)按 scheduler.getTrigger(...) 查询 Quartz 中是否已存在对应调度任务, * 不存在则(重建)调度。用于重启后恢复丢失的调度任务。 *

* 为避免早于项目 Schema 注册执行,这里显式为每个可能尚未注册 Schema 的项目调用 @@ -95,7 +95,7 @@ void restoreScheduledTriggersOnStartup(@Observes StartupEvent event) { /** * 对单个项目同步 f_trigger 中的调度触发器到 Quartz。 - * 遍历该项目 state=true 且 type=SCHEDULED 的触发器, + * 遍历该项目 state=true 且 type=SCHEDULE 的触发器, * 若 scheduler.getTrigger(...) 不存在对应调度任务则按配置重建。 * * @return 本次恢复(新建)的调度任务数量 @@ -104,7 +104,7 @@ private int syncScheduledTriggers(Project project) { String projectId = project.getId(); // 仅处理调度类型触发器;事件触发器(EVENT)不参与 Quartz 调度 List triggers = triggerRepository.find(projectId, - trigger.state.eq(true).and(trigger.type.eq(TriggerType.SCHEDULED)), 1, Integer.MAX_VALUE); + trigger.state.eq(true).and(trigger.type.eq(TriggerType.SCHEDULE)), 1, Integer.MAX_VALUE); int restored = 0; for (Trigger trigger : triggers) { diff --git a/flexmodel-server/src/main/resources/dev_test.fml b/flexmodel-server/src/main/resources/dev_test.fml index 472146ea..c17c7cad 100644 --- a/flexmodel-server/src/main/resources/dev_test.fml +++ b/flexmodel-server/src/main/resources/dev_test.fml @@ -103,11 +103,11 @@ seed f_em_flow_deployment { seed f_trigger { {id: "4e8be2da-8ae7-4571-b347-61af5fdd3e9b", name: "请求日志归档任务", description: "写入日志归档表进行数据归档", type: "EVENT", config: {type: "event", modelName: "api_request_log", mutationTypes: ["create"], triggerTiming: "after", datasourceName: "system"}, job_type: "FLOW", job_group: "system_api_request_log", job_id: "22203aa0-5f25-4b8a-974d-e4288c1bbd18", state: false, created_at: "2025-10-19 17:59:45", updated_at: "2025-10-19 20:38:24", created_by: null, updated_by: null}, - {id: "bf492f37-1f01-4eb8-b76d-d319299b4d8e", name: "定时触发-间隔触发", description: "定时触发-间隔触发-备注", type: "SCHEDULED", config: {type: "interval", interval: 1, intervalUnit: "minute", repeatCount: 100}, job_id: "5c41f37a-87a9-47af-bdba-44d0c27eda89", job_type: "FLOW", job_group: "testFlowName_1757909108656", state: false, created_at: "2025-09-15 12:05:09.000", updated_at: "2025-09-15 12:05:09.000"}, - {id: "d8c60d2a-19d8-4c3c-b370-96318733858f", name: "定时触发-Cron表达式", description: "定时触发-Cron表达式-备注", type: "SCHEDULED", config: {type: "cron", cronExpression: "0 0 * * * ? *"}, job_id: "5c41f37a-87a9-47af-bdba-44d0c27eda89", job_type: "FLOW", job_group: "testFlowName_1757909108656", state: false, created_at: "2025-09-15 12:05:09.000", updated_at: "2025-09-15 12:05:09.000"}, + {id: "bf492f37-1f01-4eb8-b76d-d319299b4d8e", name: "定时触发-间隔触发", description: "定时触发-间隔触发-备注", type: "SCHEDULE", config: {type: "interval", interval: 1, intervalUnit: "minute", repeatCount: 100}, job_id: "5c41f37a-87a9-47af-bdba-44d0c27eda89", job_type: "FLOW", job_group: "testFlowName_1757909108656", state: false, created_at: "2025-09-15 12:05:09.000", updated_at: "2025-09-15 12:05:09.000"}, + {id: "d8c60d2a-19d8-4c3c-b370-96318733858f", name: "定时触发-Cron表达式", description: "定时触发-Cron表达式-备注", type: "SCHEDULE", config: {type: "cron", cronExpression: "0 0 * * * ? *"}, job_id: "5c41f37a-87a9-47af-bdba-44d0c27eda89", job_type: "FLOW", job_group: "testFlowName_1757909108656", state: false, created_at: "2025-09-15 12:05:09.000", updated_at: "2025-09-15 12:05:09.000"}, {id: "9434666d-6b3b-417a-80e3-3352f40ded71", name: "事件触发-之前", description: "事件触发-之前-描述", type: "EVENT", config: {type: "event", datasourceName: "dev_test", modelName: "Student", mutationTypes: ["delete", "update", "create"], triggerTiming: "after"}, job_id: "5c41f37a-87a9-47af-bdba-44d0c27eda89", job_type: "FLOW", job_group: "dev_test_Student", state: true, created_at: "2025-09-15 12:05:09.000", updated_at: "2025-09-15 12:05:09.000"}, {id: "f351b8d9-a450-4f2c-8fff-cf862d690352", name: "事件触发-之后", description: "事件触发-之后-描述", type: "EVENT", config: {type: "event", datasourceName: "dev_test", modelName: "Classes", mutationTypes: ["create", "update", "delete"], triggerTiming: "after"}, job_id: "5c41f37a-87a9-47af-bdba-44d0c27eda89", job_type: "FLOW", job_group: "dev_test_Classes", state: true, created_at: "2025-09-15 12:05:09.000", updated_at: "2025-09-15 12:05:09.000"}, - {id: "687d14a2-9d5e-4ecf-b85c-dfc7c3b31493", name: "函数运行时健康检测", description: null, type: "SCHEDULED", config: {type: "interval", interval: 10, intervalUnit: "second", repeatCount: 999999}, job_type: "FUNCTION", job_group: "dev_test_fn_health-check", job_id: "health-check", state: true, created_at: "2026-07-15T10:50:31.284785500", updated_at: "2026-07-17T12:02:48.970596400", created_by: null, updated_by: null}, + {id: "687d14a2-9d5e-4ecf-b85c-dfc7c3b31493", name: "函数运行时健康检测", description: null, type: "SCHEDULE", config: {type: "interval", interval: 10, intervalUnit: "second", repeatCount: 999999}, job_type: "FUNCTION", job_group: "dev_test_fn_health-check", job_id: "health-check", state: true, created_at: "2026-07-15T10:50:31.284785500", updated_at: "2026-07-17T12:02:48.970596400", created_by: null, updated_by: null}, } seed f_function { diff --git a/flexmodel-server/src/main/resources/project.fml b/flexmodel-server/src/main/resources/project.fml index 7d531077..58addb24 100644 --- a/flexmodel-server/src/main/resources/project.fml +++ b/flexmodel-server/src/main/resources/project.fml @@ -285,7 +285,7 @@ model f_function_log { enum TriggerType { EVENT, - SCHEDULED, + SCHEDULE, @system } diff --git a/flexmodel-server/src/test/java/dev/flexmodel/rest/PermissionFilterTest.java b/flexmodel-server/src/test/java/dev/flexmodel/rest/PermissionFilterTest.java index b910f0c0..d72ff4fb 100644 --- a/flexmodel-server/src/test/java/dev/flexmodel/rest/PermissionFilterTest.java +++ b/flexmodel-server/src/test/java/dev/flexmodel/rest/PermissionFilterTest.java @@ -393,7 +393,7 @@ void schedulingExecuteShouldPassCreateTrigger() { .header("Authorization", testTokenHelper.getAuthorizationHeader()) .header("X-Test-Permissions", "scheduling:execute") .contentType(ContentType.JSON) - .body("{\"name\":\"TestTrigger\",\"type\":\"SCHEDULED\"}") + .body("{\"name\":\"TestTrigger\",\"type\":\"SCHEDULE\"}") .when() .post(Resources.ROOT_PATH + "/projects/dev_test/triggers") .then() diff --git a/flexmodel-server/src/test/java/dev/flexmodel/rest/TriggerResourceTest.java b/flexmodel-server/src/test/java/dev/flexmodel/rest/TriggerResourceTest.java index 614ae3cb..17f74e3a 100644 --- a/flexmodel-server/src/test/java/dev/flexmodel/rest/TriggerResourceTest.java +++ b/flexmodel-server/src/test/java/dev/flexmodel/rest/TriggerResourceTest.java @@ -45,14 +45,14 @@ public class TriggerResourceTest { /** * 与 {@code TriggerService.getJobGroup} 推导规则保持一致: - * SCHEDULED + FLOW → "{projectId}_flow_{jobId}";SCHEDULED + FUNCTION → "{projectId}_fn_{jobId}"。 + * SCHEDULE + FLOW → "{projectId}_flow_{jobId}";SCHEDULE + FUNCTION → "{projectId}_fn_{jobId}"。 */ private static final String EXPECTED_FLOW_JOB_GROUP = PROJECT_ID + "_flow_" + TEST_JOB_ID; private String createdTriggerId; /** - * 本用例在测试过程中被显式启用(PATCH state=true)的 seed SCHEDULED 触发器 ID 列表, + * 本用例在测试过程中被显式启用(PATCH state=true)的 seed SCHEDULE 触发器 ID 列表, * 用于在 @AfterEach 中确保其 Quartz 调度任务被取消并还原为 state=false,避免污染后续用例。 */ private final List enabledSeedTriggerIds = new ArrayList<>(); @@ -82,7 +82,7 @@ void tearDown() { .body("{ \"state\": false }") .when() .patch(BASE_PATH + "/" + id); - // seed SCHEDULED FLOW 触发器的 jobGroup 在 update 后会被重算为标准格式 + // seed SCHEDULE FLOW 触发器的 jobGroup 在 update 后会被重算为标准格式 unscheduleFromScheduler(id, EXPECTED_FLOW_JOB_GROUP); // 同时兼容 seed 中旧格式的 jobGroup,避免遗留调度任务 unscheduleFromScheduler(id, "testFlowName_1757909108656"); @@ -125,7 +125,7 @@ private void unscheduleFromScheduler(String triggerId, String jobGroup) { } /** - * 断言 SCHEDULED 触发器已正确调度到 Quartz:JobDetail 与 Trigger 同时存在, + * 断言 SCHEDULE 触发器已正确调度到 Quartz:JobDetail 与 Trigger 同时存在, * 触发器状态为 NORMAL,且 JobDataMap 携带 triggerId/jobId/projectId。 *

* 注:SimpleTrigger 在短间隔下可能在断言前已触发完成并被移除,nextFireTime 可能为 null, @@ -154,13 +154,13 @@ private void assertScheduledInQuartz(String triggerId, String jobGroup) throws S } /** - * 断言触发器未在 Quartz 中创建作业/触发器(用于 state=false 的 SCHEDULED 与 EVENT 类型)。 + * 断言触发器未在 Quartz 中创建作业/触发器(用于 state=false 的 SCHEDULE 与 EVENT 类型)。 */ private void assertNotScheduledInQuartz(String triggerId, String jobGroup) throws SchedulerException { assertFalse(quartz.checkExists(jobKey(triggerId, jobGroup)), - "state=false 的 SCHEDULED 触发器不应创建 Quartz JobDetail: " + jobKey(triggerId, jobGroup)); + "state=false 的 SCHEDULE 触发器不应创建 Quartz JobDetail: " + jobKey(triggerId, jobGroup)); assertFalse(quartz.checkExists(triggerKey(triggerId, jobGroup)), - "state=false 的 SCHEDULED 触发器不应创建 Quartz Trigger: " + triggerKey(triggerId, jobGroup)); + "state=false 的 SCHEDULE 触发器不应创建 Quartz Trigger: " + triggerKey(triggerId, jobGroup)); } /** @@ -224,7 +224,7 @@ void testCreateIntervalTrigger() throws SchedulerException { { "name": "测试间隔触发", "description": "测试描述", - "type": "SCHEDULED", + "type": "SCHEDULE", "config": { "type": "interval", "interval": 5, @@ -248,7 +248,7 @@ void testCreateIntervalTrigger() throws SchedulerException { .body("id", notNullValue()) .body("name", equalTo("测试间隔触发")) .body("description", equalTo("测试描述")) - .body("type", equalTo("SCHEDULED")) + .body("type", equalTo("SCHEDULE")) .body("state", equalTo(true)) .body("jobId", equalTo(TEST_JOB_ID)) .body("jobType", equalTo("FLOW")) @@ -273,7 +273,7 @@ void testCreateCronTrigger() throws SchedulerException { { "name": "测试Cron触发", "description": "测试Cron描述", - "type": "SCHEDULED", + "type": "SCHEDULE", "config": { "type": "cron", "cronExpression": "0 0 8 * * ?" @@ -294,7 +294,7 @@ void testCreateCronTrigger() throws SchedulerException { .statusCode(200) .body("id", notNullValue()) .body("name", equalTo("测试Cron触发")) - .body("type", equalTo("SCHEDULED")) + .body("type", equalTo("SCHEDULE")) .body("jobGroup", equalTo(EXPECTED_FLOW_JOB_GROUP)) .body("config.cronExpression", equalTo("0 0 8 * * ?")) .extract() @@ -362,7 +362,7 @@ void testCreateDisabledTriggerNotScheduled() throws SchedulerException { String triggerJson = """ { "name": "禁用的间隔触发", - "type": "SCHEDULED", + "type": "SCHEDULE", "config": { "type": "interval", "interval": 1, @@ -398,7 +398,7 @@ void testCreateDisabledTriggerNotScheduled() throws SchedulerException { .statusCode(200) .body("state", equalTo(false)); - // state=false 的 SCHEDULED 触发器不应在 Quartz 中创建调度任务 + // state=false 的 SCHEDULE 触发器不应在 Quartz 中创建调度任务 assertNotScheduledInQuartz(triggerId, EXPECTED_FLOW_JOB_GROUP); } @@ -417,7 +417,7 @@ void testUpdateTrigger() throws SchedulerException { "id": "%s", "name": "更新后的触发器名称", "description": "更新后的描述", - "type": "SCHEDULED", + "type": "SCHEDULE", "config": { "type": "interval", "interval": 10, @@ -455,7 +455,7 @@ void testUpdateTrigger() throws SchedulerException { "id": "%s", "name": "定时触发-间隔触发", "description": "定时触发-间隔触发-备注", - "type": "SCHEDULED", + "type": "SCHEDULE", "config": { "type": "interval", "interval": 1, @@ -508,7 +508,7 @@ void testUpdateTriggerReschedules() throws SchedulerException { { "id": "%s", "name": "定时触发-间隔触发", - "type": "SCHEDULED", + "type": "SCHEDULE", "config": { "type": "interval", "interval": 30, @@ -543,7 +543,7 @@ void testUpdateTriggerReschedules() throws SchedulerException { "id": "%s", "name": "定时触发-间隔触发", "description": "定时触发-间隔触发-备注", - "type": "SCHEDULED", + "type": "SCHEDULE", "config": { "type": "interval", "interval": 1, @@ -571,7 +571,7 @@ void testUpdateTriggerReschedules() throws SchedulerException { /** * 测试部分更新触发器 - 只更新状态为false *

- * CRON_TRIGGER_ID 是 seed 中 state=false 的 SCHEDULED Cron 触发器,本用例 PATCH 保持 false, + * CRON_TRIGGER_ID 是 seed 中 state=false 的 SCHEDULE Cron 触发器,本用例 PATCH 保持 false, * 不应触发调度。补充断言:Quartz 中不应存在其调度任务。 */ @Test @@ -597,12 +597,12 @@ void testPatchTriggerDisable() throws SchedulerException { /** * 测试部分更新触发器 - 启用触发器(state: false → true) *

- * INTERVAL_TRIGGER_ID 是 seed 中 state=false 的 SCHEDULED 间隔触发器, + * INTERVAL_TRIGGER_ID 是 seed 中 state=false 的 SCHEDULE 间隔触发器, * PATCH state=true → TriggerService 会依据配置调度到 Quartz;PATCH 回 false → 取消调度。 */ @Test void testPatchTriggerEnable() throws SchedulerException { - // INTERVAL_TRIGGER_ID 是seed中state=false的SCHEDULED触发器 + // INTERVAL_TRIGGER_ID 是seed中state=false的SCHEDULE触发器 // PATCH state=true → TriggerService会调度到Quartz String patchJson = """ { "state": true } @@ -700,7 +700,7 @@ void testDeleteTrigger() throws SchedulerException { { "name": "待删除的触发器", "description": "用于删除测试", - "type": "SCHEDULED", + "type": "SCHEDULE", "config": { "type": "interval", "interval": 1, @@ -762,7 +762,7 @@ void testValidateDifferentTriggerTypes() { .get(BASE_PATH + "/" + INTERVAL_TRIGGER_ID) .then() .statusCode(200) - .body("type", equalTo("SCHEDULED")) + .body("type", equalTo("SCHEDULE")) .body("config.interval", notNullValue()) .body("config.intervalUnit", notNullValue()) .body("config.repeatCount", notNullValue()); @@ -774,7 +774,7 @@ void testValidateDifferentTriggerTypes() { .get(BASE_PATH + "/" + CRON_TRIGGER_ID) .then() .statusCode(200) - .body("type", equalTo("SCHEDULED")) + .body("type", equalTo("SCHEDULE")) .body("config.cronExpression", equalTo("0 0 * * * ? *")); // 验证事件触发配置 diff --git a/flexmodel-ui b/flexmodel-ui index 1100449c..473d7d29 160000 --- a/flexmodel-ui +++ b/flexmodel-ui @@ -1 +1 @@ -Subproject commit 1100449c6c31903fe53e642794c42bb55ccd5c12 +Subproject commit 473d7d29428b41239b6dda06dba9424e496c5f99 From 0abeba040a6861b83f5ab4a12603ded4f8800844 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Fri, 21 Aug 2026 11:27:43 +0800 Subject: [PATCH 10/16] =?UTF-8?q?refactor(flow):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E5=AE=9A=E4=B9=89=E4=BA=8B=E4=BB=B6=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E4=BB=A3=E7=A0=81=E5=B9=B6=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 FlowCreatedEvent、FlowDeletedEvent、FlowDeployedEvent、FlowUpdatedEvent 类 - 移除 FlowEventRabbitmqBridge 中对定义事件的消费方法 - 从 FlowEventTypes 中移除定义层事件常量 - 在 DefinitionProcessor 中移除事件发布逻辑和依赖注入 - 更新测试类中相关的事件发布测试代码 - 从文档中移除已删除的定义层事件说明 - 修复 usertask.rollback.suspended 错别字为 rollbacked - 更新调度模块文档,完善触发器类型说明和 API 端点描述 - 将版本号从 v0.1.1 更新至 v0.2.1 --- VERSION | 2 +- .../flow/event/FlowCreatedEvent.java | 30 ---------------- .../flow/event/FlowDeletedEvent.java | 28 --------------- .../flow/event/FlowDeployedEvent.java | 30 ---------------- .../flow/event/FlowEventRabbitmqBridge.java | 20 ----------- .../flexmodel/flow/event/FlowEventTypes.java | 6 ---- .../flow/event/FlowUpdatedEvent.java | 28 --------------- .../flow/processor/DefinitionProcessor.java | 15 -------- .../event/FlowEventRabbitmqBridgeE2ETest.java | 36 ------------------- .../event/FlowEventRabbitmqBridgeTest.java | 6 ++-- flexmodel-website | 2 +- 11 files changed, 6 insertions(+), 197 deletions(-) delete mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowCreatedEvent.java delete mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeletedEvent.java delete mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeployedEvent.java delete mode 100644 flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowUpdatedEvent.java diff --git a/VERSION b/VERSION index 8308b63a..22c08f72 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.1.1 +v0.2.1 diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowCreatedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowCreatedEvent.java deleted file mode 100644 index a1bd4090..00000000 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowCreatedEvent.java +++ /dev/null @@ -1,30 +0,0 @@ -package dev.flexmodel.flow.event; - -import lombok.Getter; -import lombok.ToString; - -/** - * 流程定义创建事件。 - * - * @author cjbi - */ -@Getter -@ToString(callSuper = true) -public class FlowCreatedEvent extends FlowEvent { - - public static final String ROUTING_KEY = FlowEventTypes.FLOW_CREATED; - - private final String flowModuleId; - private final String flowKey; - - public FlowCreatedEvent(String projectId, String caller, String flowModuleId, String flowKey) { - super(projectId, caller); - this.flowModuleId = flowModuleId; - this.flowKey = flowKey; - } - - @Override - public String routingKey() { - return ROUTING_KEY; - } -} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeletedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeletedEvent.java deleted file mode 100644 index a9f7ba09..00000000 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeletedEvent.java +++ /dev/null @@ -1,28 +0,0 @@ -package dev.flexmodel.flow.event; - -import lombok.Getter; -import lombok.ToString; - -/** - * 流程定义删除事件。 - * - * @author cjbi - */ -@Getter -@ToString(callSuper = true) -public class FlowDeletedEvent extends FlowEvent { - - public static final String ROUTING_KEY = FlowEventTypes.FLOW_DELETED; - - private final String flowModuleId; - - public FlowDeletedEvent(String projectId, String caller, String flowModuleId) { - super(projectId, caller); - this.flowModuleId = flowModuleId; - } - - @Override - public String routingKey() { - return ROUTING_KEY; - } -} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeployedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeployedEvent.java deleted file mode 100644 index 36bb31fb..00000000 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowDeployedEvent.java +++ /dev/null @@ -1,30 +0,0 @@ -package dev.flexmodel.flow.event; - -import lombok.Getter; -import lombok.ToString; - -/** - * 流程定义部署事件。 - * - * @author cjbi - */ -@Getter -@ToString(callSuper = true) -public class FlowDeployedEvent extends FlowEvent { - - public static final String ROUTING_KEY = FlowEventTypes.FLOW_DEPLOYED; - - private final String flowModuleId; - private final String flowDeployId; - - public FlowDeployedEvent(String projectId, String caller, String flowModuleId, String flowDeployId) { - super(projectId, caller); - this.flowModuleId = flowModuleId; - this.flowDeployId = flowDeployId; - } - - @Override - public String routingKey() { - return ROUTING_KEY; - } -} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java index 0a0816e6..258e888e 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java @@ -39,26 +39,6 @@ public class FlowEventRabbitmqBridge { @Channel("events-out") Instance> flowEventEmitterInstance; - @ConsumeEvent(value = FlowEventTypes.FLOW_CREATED, blocking = false) - public void onFlowCreated(FlowCreatedEvent event) { - forward(event); - } - - @ConsumeEvent(value = FlowEventTypes.FLOW_UPDATED, blocking = false) - public void onFlowUpdated(FlowUpdatedEvent event) { - forward(event); - } - - @ConsumeEvent(value = FlowEventTypes.FLOW_DEPLOYED, blocking = false) - public void onFlowDeployed(FlowDeployedEvent event) { - forward(event); - } - - @ConsumeEvent(value = FlowEventTypes.FLOW_DELETED, blocking = false) - public void onFlowDeleted(FlowDeletedEvent event) { - forward(event); - } - @ConsumeEvent(value = FlowEventTypes.FLOW_INSTANCE_STARTED, blocking = false) public void onFlowInstanceStarted(FlowInstanceStartedEvent event) { forward(event); diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventTypes.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventTypes.java index fea210a4..5f62652b 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventTypes.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventTypes.java @@ -12,12 +12,6 @@ public final class FlowEventTypes { private FlowEventTypes() { } - // 定义层 - public static final String FLOW_CREATED = "flow.created"; - public static final String FLOW_UPDATED = "flow.updated"; - public static final String FLOW_DEPLOYED = "flow.deployed"; - public static final String FLOW_DELETED = "flow.deleted"; - // 实例层 public static final String FLOW_INSTANCE_STARTED = "flow.instance.started"; public static final String FLOW_INSTANCE_COMPLETED = "flow.instance.completed"; diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowUpdatedEvent.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowUpdatedEvent.java deleted file mode 100644 index bad53270..00000000 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowUpdatedEvent.java +++ /dev/null @@ -1,28 +0,0 @@ -package dev.flexmodel.flow.event; - -import lombok.Getter; -import lombok.ToString; - -/** - * 流程定义更新事件。 - * - * @author cjbi - */ -@Getter -@ToString(callSuper = true) -public class FlowUpdatedEvent extends FlowEvent { - - public static final String ROUTING_KEY = FlowEventTypes.FLOW_UPDATED; - - private final String flowModuleId; - - public FlowUpdatedEvent(String projectId, String caller, String flowModuleId) { - super(projectId, caller); - this.flowModuleId = flowModuleId; - } - - @Override - public String routingKey() { - return ROUTING_KEY; - } -} diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/processor/DefinitionProcessor.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/processor/DefinitionProcessor.java index fbd5e525..d465c38c 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/processor/DefinitionProcessor.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/processor/DefinitionProcessor.java @@ -27,11 +27,6 @@ import dev.flexmodel.flow.common.util.StrongUuidGenerator; import dev.flexmodel.flow.validator.ModelValidator; import dev.flexmodel.flow.validator.ParamValidator; -import dev.flexmodel.flow.event.FlowCreatedEvent; -import dev.flexmodel.flow.event.FlowDeletedEvent; -import dev.flexmodel.flow.event.FlowDeployedEvent; -import dev.flexmodel.flow.event.FlowEventPublisher; -import dev.flexmodel.flow.event.FlowUpdatedEvent; import dev.flexmodel.JsonUtils; import dev.flexmodel.common.utils.StringUtils; @@ -57,9 +52,6 @@ public class DefinitionProcessor { @Inject FlowDeploymentRepository flowDeploymentRepository; - @Inject - FlowEventPublisher flowEventPublisher; - @PostConstruct public void init() { List idGeneratorPlugins = pluginManager.getPluginsFor(IdGeneratorPlugin.class); @@ -88,8 +80,6 @@ public CreateFlowResult create(CreateFlowParam createFlowParam) { createFlowResult = JsonUtils.convertValue(flowDefinitionPO, CreateFlowResult.class); fillCommonResult(createFlowResult, ErrorEnum.SUCCESS); - flowEventPublisher.publish(new FlowCreatedEvent(createFlowParam.getProjectId(), createFlowParam.getCaller(), - flowModuleId, createFlowParam.getFlowKey())); } catch (TurboException te) { fillCommonResult(createFlowResult, te); } @@ -110,8 +100,6 @@ public UpdateFlowResult update(UpdateFlowParam updateFlowParam) { throw new DefinitionException(ErrorEnum.DEFINITION_UPDATE_INVALID); } fillCommonResult(updateFlowResult, ErrorEnum.SUCCESS); - flowEventPublisher.publish(new FlowUpdatedEvent(updateFlowParam.getProjectId(), updateFlowParam.getCaller(), - updateFlowParam.getFlowModuleId())); } catch (TurboException te) { LOGGER.error("Flow definition update failed: {}", te.getErrMsg(), te); fillCommonResult(updateFlowResult, te); @@ -132,7 +120,6 @@ public void delete(String projectId, String flowModuleId) { flowDefinition.setIsDeleted(true); flowDefinition.setModifyTime(LocalDateTime.now()); flowDefinitionRepository.updateByModuleId(projectId, flowDefinition); - flowEventPublisher.publish(new FlowDeletedEvent(projectId, null, flowModuleId)); } public DeployFlowResult deploy(DeployFlowParam deployFlowParam) { @@ -169,8 +156,6 @@ public DeployFlowResult deploy(DeployFlowParam deployFlowParam) { } deployFlowResult = JsonUtils.convertValue(flowDeploymentPO, DeployFlowResult.class); fillCommonResult(deployFlowResult, ErrorEnum.SUCCESS); - flowEventPublisher.publish(new FlowDeployedEvent(deployFlowParam.getProjectId(), deployFlowParam.getCaller(), - deployFlowParam.getFlowModuleId(), flowDeployId)); } catch (TurboException te) { fillCommonResult(deployFlowResult, te); } diff --git a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java index 1d4d8aa8..2607610b 100644 --- a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java +++ b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeE2ETest.java @@ -53,42 +53,6 @@ public class FlowEventRabbitmqBridgeE2ETest { @Inject FlowEventPublisher flowEventPublisher; - /** - * 验证 FlowDeployedEvent 经桥接转发后,AMQP routing key 与 JSON 载荷字段正确。 - */ - @Test - void flowDeployedEventForwardedToBroker() throws Exception { - String projectId = "proj-e2e"; - String caller = "caller-e2e"; - String flowModuleId = "module-e2e"; - String flowDeployId = "deploy-e2e"; - - try (Connection connection = newConnection(); - Channel channel = connection.createChannel()) { - // 确保交换机存在(与 SmallRye 声明参数一致:topic、durable),幂等 - channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.TOPIC, true); - String queue = channel.queueDeclare().getQueue(); - channel.queueBind(queue, EXCHANGE, "flow.proj-e2e.deployed"); - - flowEventPublisher.publish( - new FlowDeployedEvent(projectId, caller, flowModuleId, flowDeployId)); - - GetResponse response = pollForMessage(channel, queue, 15000L); - assertNotNull(response, "FlowDeployedEvent should be forwarded to broker within timeout"); - - Envelope envelope = response.getEnvelope(); - assertEquals("flow.proj-e2e.deployed", envelope.getRoutingKey(), - "AMQP routing key should match event routing key"); - - JsonNode body = MAPPER.readTree(response.getBody()); - assertEquals(projectId, body.get("projectId").asText()); - assertEquals(caller, body.get("caller").asText()); - assertEquals(flowModuleId, body.get("flowModuleId").asText()); - assertEquals(flowDeployId, body.get("flowDeployId").asText()); - assertTrue(body.get("timestamp").asLong() > 0, "timestamp should be set"); - } - } - /** * 验证带 variables 快照的 FlowInstanceStartedEvent 经桥接转发后,variables 仍在 JSON 载荷中完整。 */ diff --git a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeTest.java b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeTest.java index bf337e96..07ceb640 100644 --- a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeTest.java +++ b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridgeTest.java @@ -40,8 +40,10 @@ void bridgeBeanPresentAndNonThrowingByDefault() { // 默认通道禁用:桥接 bean 仍存在(零侵入),发布桥接也消费的事件不应抛出, // 且不连 broker(应用无 broker 启动即证) assertNotNull(bridge, "bridge bean should be present (zero-intrusion)"); - flowEventPublisher.publish(new FlowDeployedEvent("proj-bridge", "caller-bridge", - "module-bridge", "deploy-bridge")); + Map variables = new HashMap<>(); + variables.put("k", "v"); + flowEventPublisher.publish(new FlowInstanceStartedEvent("proj-bridge", "caller-bridge", + "deploy-bridge", "instance-bridge", variables)); } @Test diff --git a/flexmodel-website b/flexmodel-website index d18ba09f..165f8258 160000 --- a/flexmodel-website +++ b/flexmodel-website @@ -1 +1 @@ -Subproject commit d18ba09f169968ea279254a6923ef63fca4b66f4 +Subproject commit 165f8258af253d239358b426f9c2e45c639bea35 From 94638d5b870dc75db7d36095f8abeaa5f0c29c26 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Fri, 21 Aug 2026 19:12:54 +0800 Subject: [PATCH 11/16] =?UTF-8?q?feat(storage):=20=E9=9B=86=E6=88=90=20Rus?= =?UTF-8?q?tFS=20S3=20=E5=85=BC=E5=AE=B9=E5=AF=B9=E8=B1=A1=E5=AD=98?= =?UTF-8?q?=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 docker-compose 中添加 rustfs 服务配置和健康检查 - 配置环境变量支持 S3 存储后端,包括访问密钥和端口设置 - 更新服务器配置以使用 S3 存储类型和相应的连接参数 - 实现 S3 后端自动创建存储桶功能,支持任意 S3 兼容存储 - 添加对 404 状态码的处理以兼容不同 S3 实现的错误映射差异 - 更新文档说明 RustFS 集成配置和使用注意事项 --- .../docker-compose/002-mysql-fullstack/.env | 9 ++++ .../002-mysql-fullstack/README.md | 29 ++++++++++++ .../002-mysql-fullstack/docker-compose.yml | 47 +++++++++++++++++++ .../flexmodel/storage/config/S3Backend.java | 31 +++++++++++- flexmodel-website | 2 +- 5 files changed, 116 insertions(+), 2 deletions(-) diff --git a/deploy/docker-compose/002-mysql-fullstack/.env b/deploy/docker-compose/002-mysql-fullstack/.env index 59ca7a4d..312ac844 100644 --- a/deploy/docker-compose/002-mysql-fullstack/.env +++ b/deploy/docker-compose/002-mysql-fullstack/.env @@ -20,6 +20,15 @@ RABBITMQ_DEFAULT_PASS=flexmodel RABBITMQ_AMQP_PORT=5672 RABBITMQ_MANAGEMENT_PORT=15672 +# rustfs (S3-compatible object storage) +RUSTFS_ACCESS_KEY=flexmodel +RUSTFS_SECRET_KEY=flexmodel123456 +RUSTFS_BUCKET=flexmodel +# S3 API port (host) - used by external tools; server reaches rustfs internally +RUSTFS_S3_API_PORT=9000 +# Web console port (host) +RUSTFS_CONSOLE_PORT=9001 + # Project base domain (used for subdomain routing) # e.g. preview.flexmodel.dev 鈫?{projectId}.preview.flexmodel.dev FLEXMODEL_PROJECT_BASE_DOMAIN=flexmodel.wetech.tech \ No newline at end of file diff --git a/deploy/docker-compose/002-mysql-fullstack/README.md b/deploy/docker-compose/002-mysql-fullstack/README.md index b12fe781..c910c4fa 100644 --- a/deploy/docker-compose/002-mysql-fullstack/README.md +++ b/deploy/docker-compose/002-mysql-fullstack/README.md @@ -6,6 +6,7 @@ Flexmodel 生产部署,包含以下服务: |-------------------------------|-------------------------------------------|--------------| | `mysql` | `mysql:8.0` | 3306 (内部) | | `rabbitmq` | `rabbitmq:3.13-management` | 5672 / 15672 | +| `rustfs` | `rustfs/rustfs:latest` | 9000 / 9001 | | `flexmodel` | `cjbi/flexmodel-server:latest` | 8080 (内部) | | `flexmodel-ui` | `cjbi/flexmodel-ui:latest` | 80 | | `flexmodel-functions-runtime` | `cjbi/flexmodel-functions-runtime:latest` | 9999 (内部) | @@ -36,3 +37,31 @@ docker-compose up -d ```shell docker-compose down ``` + +## RustFS 对象存储 + +本部署使用 [RustFS](https://rustfs.com)(S3 兼容的高性能对象存储)作为 `flexmodel-server` 的存储后端, 替代默认的本地磁盘存储。 +`flexmodel.storage.type` 被置为 `s3`,所有 Bucket / 文件操作经由 S3 API 写入 RustFS。 + +| 服务 | 说明 | +|----------|---------------------------------------------------------------------| +| `rustfs` | RustFS 主服务,S3 API `9000`,Web 控制台 `9001`,使用命名卷存储数据 | + +### 关键配置(`.env`) + +| 变量 | 说明 | 默认值 | +|-----------------------|-----------------------------|-------------------| +| `RUSTFS_ACCESS_KEY` | S3 Access Key | `flexmodel` | +| `RUSTFS_SECRET_KEY` | S3 Secret Key | `flexmodel123456` | +| `RUSTFS_BUCKET` | flexmodel-server 使用的桶名 | `flexmodel` | +| `RUSTFS_S3_API_PORT` | 对外暴露的 S3 API 端口 | `9000` | +| `RUSTFS_CONSOLE_PORT` | 对外暴露的 Web 控制台端口 | `9001` | + +### 注意事项 + +- 单盘单节点模式下已开启 `RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true`;生产环境建议挂载多盘并关闭该选项以启用纠删码。 +- `flexmodel-server` 依赖 `rustfs` 健康检查通过后启动;启动时若桶不存在会自动创建(见下条)。 +- `flexmodel-server` 的 `S3Backend` 对所有 S3 兼容存储通用:启动校验时桶不存在则自动 `createBucket` + ,只读模式下不自动创建并报错提示运维预先建桶。因此本部署无需额外的初始化容器。 +- Web 控制台访问:`http://:${RUSTFS_CONSOLE_PORT}`,使用 `RUSTFS_ACCESS_KEY` / `RUSTFS_SECRET_KEY` 登录。 +- 切回本地存储:删除 `flexmodel-server` 环境变量中的 `FLEXMODEL_STORAGE_TYPE` 及 `QUARKUS_S3_*` 配置即可回退到本地存储。 diff --git a/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml b/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml index 685491ed..f673c98d 100644 --- a/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml +++ b/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml @@ -43,6 +43,35 @@ test: [ "CMD", "rabbitmq-diagnostics", "-q", "ping" ] timeout: 20s retries: 10 + rustfs: + container_name: ${CONTAINER_NAME_PREFIX}-flexmodel-rustfs + image: rustfs/rustfs:latest + restart: always + volumes: + - rustfs_data:/data + environment: + - TZ=GMT+08 + - RUSTFS_VOLUMES=/data + - RUSTFS_ADDRESS=0.0.0.0:9000 + - RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001 + - RUSTFS_CONSOLE_ENABLE=true + - RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=* + - RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY} + - RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY} + - RUSTFS_OBS_LOGGER_LEVEL=info + # single-disk single-node mode; bypass strict disk topology check + - RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true + ports: + - ${RUSTFS_S3_API_PORT:-9000}:9000 + - ${RUSTFS_CONSOLE_PORT:-9001}:9001 + networks: + - flexmodel + healthcheck: + test: [ "CMD", "sh", "-ec", "curl -fsS http://127.0.0.1:9000/health" ] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s flexmodel-ui: container_name: ${CONTAINER_NAME_PREFIX}-flexmodel-ui image: cjbi/flexmodel-ui:latest @@ -103,6 +132,19 @@ - QUARKUS_RABBITMQ_USERNAME=${RABBITMQ_DEFAULT_USER} - QUARKUS_RABBITMQ_PASSWORD=${RABBITMQ_DEFAULT_PASS} + # RustFS (S3-compatible object storage) + - FLEXMODEL_STORAGE_TYPE=s3 + - FLEXMODEL_STORAGE_S3_BUCKET=${RUSTFS_BUCKET} + - FLEXMODEL_STORAGE_S3_ENDPOINT=http://rustfs:9000 + - FLEXMODEL_STORAGE_S3_REGION=us-east-1 + - FLEXMODEL_STORAGE_S3_PATH_STYLE=true + - QUARKUS_S3_AWS_REGION=us-east-1 + - QUARKUS_S3_AWS_CREDENTIALS_TYPE=static + - QUARKUS_S3_AWS_CREDENTIALS_STATIC_PROVIDER_ACCESS_KEY_ID=${RUSTFS_ACCESS_KEY} + - QUARKUS_S3_AWS_CREDENTIALS_STATIC_PROVIDER_SECRET_ACCESS_KEY=${RUSTFS_SECRET_KEY} + - QUARKUS_S3_ENDPOINT_OVERRIDE=http://rustfs:9000 + - QUARKUS_S3_PATH_STYLE_ACCESS=true + networks: - flexmodel depends_on: @@ -110,6 +152,8 @@ condition: service_healthy rabbitmq: condition: service_healthy + rustfs: + condition: service_healthy flexmodel-functions-runtime: container_name: ${CONTAINER_NAME_PREFIX}-flexmodel-functions-runtime @@ -129,3 +173,6 @@ networks: flexmodel: name: ${CONTAINER_NAME_PREFIX}_flexmodel_default driver: bridge + +volumes: + rustfs_data: diff --git a/flexmodel-server/src/main/java/dev/flexmodel/storage/config/S3Backend.java b/flexmodel-server/src/main/java/dev/flexmodel/storage/config/S3Backend.java index 9a04be7c..5e2976d5 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/storage/config/S3Backend.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/storage/config/S3Backend.java @@ -6,6 +6,8 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.*; +import lombok.extern.slf4j.Slf4j; + import java.util.List; import java.util.stream.Collectors; @@ -21,6 +23,7 @@ * * @author cjbi */ +@Slf4j public class S3Backend implements StorageBackend { public static final String TYPE = "s3"; @@ -94,8 +97,34 @@ public boolean containerExists(String prefix) { public void validate() { try { s3Client.headBucket(b -> b.bucket(bucket)); + } catch (NoSuchBucketException e) { + autoCreateBucket(); + } catch (S3Exception e) { + // Some S3-compatible stores (MinIO/RustFS) return 404 for a missing bucket + // but may not map it to NoSuchBucketException, so also match by status code. + if (e.statusCode() == 404) { + autoCreateBucket(); + } else { + throw new InternalServerException("Failed to validate S3 bucket '" + bucket + "': " + e.getMessage(), e); + } + } + } + + /** + * Auto-create the bucket when it does not exist; works for any S3-compatible store. + * In read-only mode the storage is never mutated, so we fail-fast and ask the + * operator to create the bucket manually. + */ + private void autoCreateBucket() { + if (readOnly) { + throw new InternalServerException("S3 bucket '" + bucket + "' does not exist; read-only mode is enabled, refusing to auto-create. Please create the bucket manually."); + } + try { + log.info("S3 bucket '{}' does not exist, auto-creating", bucket); + s3Client.createBucket(b -> b.bucket(bucket)); + log.info("S3 bucket '{}' created successfully", bucket); } catch (S3Exception e) { - throw new InternalServerException("Failed to validate S3 bucket '" + bucket + "': " + e.getMessage(), e); + throw new InternalServerException("Failed to auto-create S3 bucket '" + bucket + "': " + e.getMessage(), e); } } diff --git a/flexmodel-website b/flexmodel-website index 165f8258..f180b751 160000 --- a/flexmodel-website +++ b/flexmodel-website @@ -1 +1 @@ -Subproject commit 165f8258af253d239358b426f9c2e45c639bea35 +Subproject commit f180b751c69c0b6941c6f4cddcd5170fc65e28e4 From 8af052cda9e7893fcbf3cca1b0320b620275a570 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Fri, 21 Aug 2026 20:06:51 +0800 Subject: [PATCH 12/16] =?UTF-8?q?build(docker):=20=E6=9B=B4=E6=96=B0=20Doc?= =?UTF-8?q?kerfile=20=E9=85=8D=E7=BD=AE=E4=BB=A5=E4=BD=BF=E7=94=A8=20deplo?= =?UTF-8?q?yments=20=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将工作目录从 /work/ 更改为 /deployments/ - 更新所有相关路径的权限设置 - 确保应用程序文件复制到正确的部署目录 - 保持原有的用户权限和安全配置不变 --- flexmodel-server/src/main/docker/Dockerfile.native | 10 +++++----- .../src/main/docker/Dockerfile.native-micro | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/flexmodel-server/src/main/docker/Dockerfile.native b/flexmodel-server/src/main/docker/Dockerfile.native index ea38eb7e..76377bea 100644 --- a/flexmodel-server/src/main/docker/Dockerfile.native +++ b/flexmodel-server/src/main/docker/Dockerfile.native @@ -17,11 +17,11 @@ # To use UBI 8, switch to `quay.io/ubi8/ubi-minimal:8.10`. ### FROM registry.access.redhat.com/ubi9/ubi-minimal:9.7 -WORKDIR /work/ -RUN chown 1001 /work \ - && chmod "g+rwX" /work \ - && chown 1001:root /work -COPY --chown=1001:root --chmod=0755 target/*-runner /work/application +WORKDIR /deployments/ +RUN chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments +COPY --chown=1001:root --chmod=0755 target/*-runner /deployments/application EXPOSE 8080 USER 1001 diff --git a/flexmodel-server/src/main/docker/Dockerfile.native-micro b/flexmodel-server/src/main/docker/Dockerfile.native-micro index 63a5976e..28e17d8e 100644 --- a/flexmodel-server/src/main/docker/Dockerfile.native-micro +++ b/flexmodel-server/src/main/docker/Dockerfile.native-micro @@ -20,11 +20,11 @@ # To use UBI 8, switch to `quay.io/quarkus/quarkus-micro-image:2.0`. ### FROM quay.io/quarkus/ubi9-quarkus-micro-image:2.0 -WORKDIR /work/ -RUN chown 1001 /work \ - && chmod "g+rwX" /work \ - && chown 1001:root /work -COPY --chown=1001:root --chmod=0755 target/*-runner /work/application +WORKDIR /deployments/ +RUN chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments +COPY --chown=1001:root --chmod=0755 target/*-runner /deployments/application EXPOSE 8080 USER 1001 From b214963342d08c9a7c4d6df7d7d7cdb91dda6aa6 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Fri, 21 Aug 2026 21:42:06 +0800 Subject: [PATCH 13/16] =?UTF-8?q?feat(deploy):=20=E6=9B=B4=E6=96=B0=20Dock?= =?UTF-8?q?er=20Compose=20=E9=85=8D=E7=BD=AE=E4=BB=A5=E6=94=AF=E6=8C=81=20?= =?UTF-8?q?RustFS=20=E6=95=B0=E6=8D=AE=E6=8C=81=E4=B9=85=E5=8C=96=E5=92=8C?= =?UTF-8?q?=20RabbitMQ=20=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 RustFS 服务添加虚拟主机配置 RABBITMQ_VIRTUAL_HOST=/ - 将 RustFS 数据卷从命名卷改为宿主机目录挂载 ./rustfs/data - 在 flexmodel-server 中添加 RabbitMQ 虚拟主机和启用开关配置 - 移除不再需要的命名卷定义 rustfs_data - 更新应用配置以使用统一的 RabbitMQ 连接器设置 - 在开发配置中默认启用 RabbitMQ 支持 - 更新 README 文档说明 RustFS 数据持久化方式变更 --- .../docker-compose/002-mysql-fullstack/.env | 1 + .../002-mysql-fullstack/.gitignore | 3 +- .../002-mysql-fullstack/README.md | 5 +-- .../002-mysql-fullstack/docker-compose.yml | 9 ++--- .../flow/event/FlowEventRabbitmqBridge.java | 5 +-- .../main/resources/application-dev.properties | 1 + .../src/main/resources/application.properties | 37 +++++++------------ 7 files changed, 25 insertions(+), 36 deletions(-) diff --git a/deploy/docker-compose/002-mysql-fullstack/.env b/deploy/docker-compose/002-mysql-fullstack/.env index 312ac844..7e879282 100644 --- a/deploy/docker-compose/002-mysql-fullstack/.env +++ b/deploy/docker-compose/002-mysql-fullstack/.env @@ -19,6 +19,7 @@ RABBITMQ_DEFAULT_USER=flexmodel RABBITMQ_DEFAULT_PASS=flexmodel RABBITMQ_AMQP_PORT=5672 RABBITMQ_MANAGEMENT_PORT=15672 +RABBITMQ_VIRTUAL_HOST=/ # rustfs (S3-compatible object storage) RUSTFS_ACCESS_KEY=flexmodel diff --git a/deploy/docker-compose/002-mysql-fullstack/.gitignore b/deploy/docker-compose/002-mysql-fullstack/.gitignore index 86c65f8b..a349b226 100644 --- a/deploy/docker-compose/002-mysql-fullstack/.gitignore +++ b/deploy/docker-compose/002-mysql-fullstack/.gitignore @@ -1,6 +1,7 @@ /mysql/data/ +/rustfs/data/ /rabbitmq/data/ /flexmodel/storage/ /flexmodel/pages/ /flexmodel/logs/ -/flexmodel/database/ \ No newline at end of file +/flexmodel/database/ diff --git a/deploy/docker-compose/002-mysql-fullstack/README.md b/deploy/docker-compose/002-mysql-fullstack/README.md index c910c4fa..214c4bb4 100644 --- a/deploy/docker-compose/002-mysql-fullstack/README.md +++ b/deploy/docker-compose/002-mysql-fullstack/README.md @@ -42,10 +42,9 @@ docker-compose down 本部署使用 [RustFS](https://rustfs.com)(S3 兼容的高性能对象存储)作为 `flexmodel-server` 的存储后端, 替代默认的本地磁盘存储。 `flexmodel.storage.type` 被置为 `s3`,所有 Bucket / 文件操作经由 S3 API 写入 RustFS。 - | 服务 | 说明 | -|----------|---------------------------------------------------------------------| -| `rustfs` | RustFS 主服务,S3 API `9000`,Web 控制台 `9001`,使用命名卷存储数据 | +|----------|---------------------------------------------------------------------| | `rustfs` | RustFS 主服务,S3 API +`9000`,Web 控制台 `9001`,使用宿主机目录持久化数据(./rustfs/data) | ### 关键配置(`.env`) diff --git a/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml b/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml index f673c98d..48d77fb0 100644 --- a/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml +++ b/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml @@ -1,4 +1,4 @@ -services: +services: mysql: container_name: ${CONTAINER_NAME_PREFIX}-flexmodel-mysql image: mysql:8.0 @@ -48,7 +48,7 @@ image: rustfs/rustfs:latest restart: always volumes: - - rustfs_data:/data + - ./rustfs/data:/data environment: - TZ=GMT+08 - RUSTFS_VOLUMES=/data @@ -131,6 +131,8 @@ - QUARKUS_RABBITMQ_PORT=5672 - QUARKUS_RABBITMQ_USERNAME=${RABBITMQ_DEFAULT_USER} - QUARKUS_RABBITMQ_PASSWORD=${RABBITMQ_DEFAULT_PASS} + - QUARKUS_RABBITMQ_VIRTUAL_HOST=${RABBITMQ_VIRTUAL_HOST:-/} + - FLEXMODEL_RABBITMQ_ENABLED=${FLEXMODEL_RABBITMQ_ENABLED:-true} # RustFS (S3-compatible object storage) - FLEXMODEL_STORAGE_TYPE=s3 @@ -173,6 +175,3 @@ networks: flexmodel: name: ${CONTAINER_NAME_PREFIX}_flexmodel_default driver: bridge - -volumes: - rustfs_data: diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java index 258e888e..dd38d4fc 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java @@ -17,10 +17,9 @@ * Flow 生命周期事件 RabbitMQ 桥接。 *

* 订阅全部内部 EventBus 事件并转发到单个 topic 交换机(routing key 复用事件地址)。 - * 是否连 broker 由业务开关 {@code flexmodel.events.rabbitmq.enabled} 单一控制(默认 false): - * 该开关经 application.properties 占位符映射到 SmallRye 通道 {@code mp.messaging.outgoing.events-out.enabled}, + * 默认不连 broker({@code flexmodel.rabbitmq.enabled=false}): * 关闭时 SmallRye 注入 no-op emitter,{@link #forward(FlowEvent)} 发送即丢弃、不连 broker; - * 本地事件照常经 {@link FlowEventPublisher} 发布。启用需置该开关 {@code true} 并提供 broker 连接配置。 + * 本地事件照常经 {@link FlowEventPublisher} 发布。启用需置 {@code flexmodel.rabbitmq.enabled=true}。 *

* 每个事件一个 {@code @ConsumeEvent} 方法、消费具体类型,沿用代码库已验证的同类型发布/消费模式, * 避免多态反序列化不确定性。 diff --git a/flexmodel-server/src/main/resources/application-dev.properties b/flexmodel-server/src/main/resources/application-dev.properties index e41680df..86a54cda 100644 --- a/flexmodel-server/src/main/resources/application-dev.properties +++ b/flexmodel-server/src/main/resources/application-dev.properties @@ -1,6 +1,7 @@ quarkus.log.category."io.quarkus.arc".level=INFO quarkus.log.category."io.agroal".level=INFO quarkus.log.category."dev.flexmodel".level=INFO +flexmodel.rabbitmq.enabled=true quarkus.rabbitmq.username=flexmodel quarkus.rabbitmq.password=flexmodel quarkus.rabbitmq.devservices.enabled=true diff --git a/flexmodel-server/src/main/resources/application.properties b/flexmodel-server/src/main/resources/application.properties index 000ab852..b58f58d7 100644 --- a/flexmodel-server/src/main/resources/application.properties +++ b/flexmodel-server/src/main/resources/application.properties @@ -69,29 +69,18 @@ flexmodel.jwt.refresh-token-lifetime=30d # flexmodel.storage.s3-bucket=flexmodel # Pages Configuration flexmodel.pages.root-path=./pages -# Flow 生命周期事件 RabbitMQ 桥接(可选,默认关闭) -# 本地 EventBus 事件始终发布;仅当 SmallRye 通道启用时才连 broker 并转发到 topic 交换机。 -# 通道禁用时 SmallRye 注入 no-op MutinyEmitter,桥接 forward() 静默丢弃,无需 broker 即可启动。 +# Flow 生命周期 / 数据变更事件 RabbitMQ 桥接 +# 默认不连 broker;需要 RabbitMQ 转发时置 flexmodel.rabbitmq.enabled=true 并提供连接配置。 +# 连接器级默认值(对所有 smallrye-rabbitmq 通道生效) +mp.messaging.connector.smallrye-rabbitmq.exchange.name=flexmodel.events +mp.messaging.connector.smallrye-rabbitmq.exchange.type=topic +mp.messaging.connector.smallrye-rabbitmq.exchange.durable=true +mp.messaging.connector.smallrye-rabbitmq.host=${quarkus.rabbitmq.host:localhost} +mp.messaging.connector.smallrye-rabbitmq.port=${quarkus.rabbitmq.port:5672} +mp.messaging.connector.smallrye-rabbitmq.username=${quarkus.rabbitmq.username:guest} +mp.messaging.connector.smallrye-rabbitmq.password=${quarkus.rabbitmq.password:guest} +mp.messaging.connector.smallrye-rabbitmq.virtual-host=${quarkus.rabbitmq.virtual-host:/} mp.messaging.outgoing.events-out.connector=smallrye-rabbitmq -mp.messaging.outgoing.events-out.exchange.name=flexmodel.events -mp.messaging.outgoing.events-out.exchange.type=topic -mp.messaging.outgoing.events-out.exchange.durable=true -# 通道默认关闭;启用需置 true 并提供 broker 连接配置(或开 DevServices) -mp.messaging.outgoing.events-out.enabled=${flexmodel.events.rabbitmq.enabled:false} -# 避免无 Docker 时 DevServices 启动失败;需 broker 时另置 true 或提供连接配置 -mp.messaging.outgoing.events-out.host=${quarkus.rabbitmq.host:localhost} -mp.messaging.outgoing.events-out.port=${quarkus.rabbitmq.port:5672} -mp.messaging.outgoing.events-out.username=${quarkus.rabbitmq.username:guest} -mp.messaging.outgoing.events-out.password=${quarkus.rabbitmq.password:guest} -# 数据变更事件 RabbitMQ 桥接(可选,默认关闭) -# 复用同一 flexmodel.events 交换机,routing key: data.. +mp.messaging.outgoing.events-out.enabled=${flexmodel.rabbitmq.enabled:false} mp.messaging.outgoing.data-events-out.connector=smallrye-rabbitmq -mp.messaging.outgoing.data-events-out.exchange.name=flexmodel.events -mp.messaging.outgoing.data-events-out.exchange.type=topic -mp.messaging.outgoing.data-events-out.exchange.durable=true -mp.messaging.outgoing.data-events-out.enabled=${flexmodel.realtime.events.rabbitmq.enabled:true} -mp.messaging.outgoing.data-events-out.host=${quarkus.rabbitmq.host:localhost} -mp.messaging.outgoing.data-events-out.port=${quarkus.rabbitmq.port:5672} -mp.messaging.outgoing.data-events-out.username=${quarkus.rabbitmq.username:guest} -mp.messaging.outgoing.data-events-out.password=${quarkus.rabbitmq.password:guest} - +mp.messaging.outgoing.data-events-out.enabled=${flexmodel.rabbitmq.enabled:false} From 1c59ab7087179e1c3200e7a271d75754ab74cf48 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Fri, 21 Aug 2026 21:56:13 +0800 Subject: [PATCH 14/16] =?UTF-8?q?docs(config):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E5=92=8C=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E4=B8=AD=E6=96=87=E5=AD=97=E7=AC=A6=E7=BC=96?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 .env 文件中运行模式说明的乱码字符 - 修正项目基础域名注释中的箭头符号显示 - 更新 EngineConfig.java 中数据库 SchemaProvider 注册的中文注释 - 修正 GraalVM 原生镜像相关注释的字符编码问题 - 更新 SQLite 数据库目录检查方法的中文注释 - 修正测试类中事件发布器相关注释的字符显示 - 更新测试消费者类注释中的中文描述信息 --- deploy/docker-compose/002-mysql-fullstack/.env | 10 +++++----- .../java/dev/flexmodel/common/config/EngineConfig.java | 8 ++++---- .../flexmodel/flow/event/FlowEventPublisherTest.java | 8 ++++---- .../flexmodel/flow/event/FlowEventTestConsumer.java | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/deploy/docker-compose/002-mysql-fullstack/.env b/deploy/docker-compose/002-mysql-fullstack/.env index 7e879282..6e3f4424 100644 --- a/deploy/docker-compose/002-mysql-fullstack/.env +++ b/deploy/docker-compose/002-mysql-fullstack/.env @@ -1,9 +1,9 @@ CONTAINER_NAME_PREFIX=prod2 -# 杩愯妯″紡: native (GraalVM 鍘熺敓闀滃儚) 鎴?jvm (JVM 妯″紡) -# 鍒囨崲鏂瑰紡锛氫慨鏀?FLEXMODEL_RUN_MODE 鍜?FLEXMODEL_SERVER_IMAGE 涓よ -# native 鈫?FLEXMODEL_SERVER_IMAGE=cjbi/flexmodel-server-native:latest -# jvm 鈫?FLEXMODEL_SERVER_IMAGE=cjbi/flexmodel-server:latest +# 运行模式: native (GraalVM 原生镜像) 或 jvm (JVM 模式) +# 切换方式:修改 FLEXMODEL_RUN_MODE 和 FLEXMODEL_SERVER_IMAGE 两行 +# native → FLEXMODEL_SERVER_IMAGE=cjbi/flexmodel-server-native:latest +# jvm → FLEXMODEL_SERVER_IMAGE=cjbi/flexmodel-server:latest FLEXMODEL_SERVER_IMAGE=cjbi/flexmodel-server-native:latest # mysql @@ -31,5 +31,5 @@ RUSTFS_S3_API_PORT=9000 RUSTFS_CONSOLE_PORT=9001 # Project base domain (used for subdomain routing) -# e.g. preview.flexmodel.dev 鈫?{projectId}.preview.flexmodel.dev +# e.g. preview.flexmodel.dev → {projectId}.preview.flexmodel.dev FLEXMODEL_PROJECT_BASE_DOMAIN=flexmodel.wetech.tech \ No newline at end of file diff --git a/flexmodel-server/src/main/java/dev/flexmodel/common/config/EngineConfig.java b/flexmodel-server/src/main/java/dev/flexmodel/common/config/EngineConfig.java index ae9cc482..895203c2 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/common/config/EngineConfig.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/common/config/EngineConfig.java @@ -38,7 +38,7 @@ public void installDatasource(@Observes StartupEvent startupEvent, long beginTime = System.currentTimeMillis(); List projects = projectService.findProjects(); for (Project project : projects) { - // 娉ㄥ唽闈?main 鍒嗘敮鐨勬暟鎹簱 SchemaProvider + // 注册非 main 分支的数据库 SchemaProvider List branches = branchRepository.findByProjectId(project.getId()); for (Branch branch : branches) { schemaRegistry.registerSchema(branch.getDatabaseName()); @@ -75,8 +75,8 @@ public SessionFactory sessionFactory(FlexmodelConfig flexmodelConfig, }); log.info("Total registered datasources: {}", flexmodelConfig.datasources().keySet()); SessionFactory sf = builder.build(); - // 鐩存帴娉ㄥ唽 BuildItem 瀹炰緥锛岀粫杩?SPI ServiceLoader 鏈哄埗 - // 鍦?GraalVM 鍘熺敓闀滃儚涓?ServiceLoader.load() 鏃犳硶姝g‘鍙戠幇 SPI 瀹炵幇绫? + // 直接注册 BuildItem 实例,绕过 SPI ServiceLoader 机制 + // 在 GraalVM 原生镜像中 ServiceLoader.load() 无法正确发现 SPI 实现类 sf.registerBuildItem(new dev.flexmodel.codegen.System()); sf.registerBuildItem(new DevTest()); sf.getEventPublisher().addListener(triggerDataChangedEventListener); @@ -87,7 +87,7 @@ public SessionFactory sessionFactory(FlexmodelConfig flexmodelConfig, } /** - * 濡傛灉鏄?SQLite 鏂囦欢鏁版嵁搴擄紝纭繚鐖剁洰褰曞瓨鍦ㄣ€? + * 如果是 SQLite 文件数据库,确保父目录存在。 */ public static void ensureSqliteParentDir(String jdbcUrl) { if (jdbcUrl != null && jdbcUrl.startsWith("jdbc:sqlite:file:")) { diff --git a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventPublisherTest.java b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventPublisherTest.java index 7a91bbba..40975a57 100644 --- a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventPublisherTest.java +++ b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventPublisherTest.java @@ -15,8 +15,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * 楠岃瘉 {@link FlowEventPublisher} 灏嗗己绫诲瀷浜嬩欢缁?Vert.x EventBus 骞挎挱锛? - * 涓斾簨浠跺瓧娈靛畬鏁翠繚鐣欍€傛湰鍦颁簨浠朵负鏈哄埗鏈韩锛屼笉渚濊禆 RabbitMQ銆? + * 验证 {@link FlowEventPublisher} 将强类型事件经 Vert.x EventBus 广播, + * 且事件字段完整保留。本地事件为机制本身,不依赖 RabbitMQ。 * * @author cjbi */ @@ -61,12 +61,12 @@ void publishFlowInstanceStartedEventFieldsPreserved() { @Test void publishNullEventIsNoOp() { - // 涓嶅簲鎶涘嚭锛屼篃涓嶅簲褰卞搷鍏朵粬娑堣垂鑰? + // 不应抛出,也不应影响其他消费者 flowEventPublisher.publish(null); } /** - * 鏈夌晫杞绛夊緟娑堣垂鑰呮崟鑾蜂簨浠讹紝閬垮厤寮曞叆 Awaitility 渚濊禆銆? + * 有界轮询等待消费者捕获事件,避免引入 Awaitility 依赖。 */ private static T pollFor(AtomicReference ref, long timeoutMs) { long deadline = System.currentTimeMillis() + timeoutMs; diff --git a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventTestConsumer.java b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventTestConsumer.java index 3e4cb6ab..ab7d628f 100644 --- a/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventTestConsumer.java +++ b/flexmodel-server/src/test/java/dev/flexmodel/flow/event/FlowEventTestConsumer.java @@ -6,9 +6,9 @@ import java.util.concurrent.atomic.AtomicReference; /** - * 娴嬭瘯鐢?EventBus 娑堣垂鑰咃細鎹曡幏 {@link FlowInstanceStartedEvent}锛岄獙璇佸瓧娈电粡 EventBus 閫忎紶瀹屾暣銆? + * 测试用 EventBus 消费者:捕获 {@link FlowInstanceStartedEvent},验证字段经 EventBus 透传完整。 *

- * 椤跺眰 {@code @ApplicationScoped} Bean锛岀‘淇?Quarkus 鍙戠幇骞舵敞鍐屽叾 {@code @ConsumeEvent} 娑堣垂鑰呫€? + * 顶层 {@code @ApplicationScoped} Bean,确保 Quarkus 发现并注册其 {@code @ConsumeEvent} 消费者。 * * @author cjbi */ From af2c87bcc09c01747c6d10a9e004842a1d64f1c7 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Fri, 21 Aug 2026 22:11:10 +0800 Subject: [PATCH 15/16] =?UTF-8?q?refactor(config):=20=E7=BB=9F=E4=B8=80=20?= =?UTF-8?q?RabbitMQ=20=E9=85=8D=E7=BD=AE=E5=B1=9E=E6=80=A7=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=B9=B6=E7=A7=BB=E9=99=A4=20OpenAPI=20=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将配置属性从 flexmodel.rabbitmq.* 重命名为 flexmodel.events.rabbitmq.* - 更新 docker-compose.yml 中的环境变量名称为 FLEXMODEL_EVENTS_RABBITMQ_ENABLED - 修改 FlowEventRabbitmqBridge.java 中的文档注释以反映新的配置键名 - 移除 application.properties 和 application-dev.properties 中的旧配置引用 - 从路由配置中删除 OpenAPI 相关的页面和图标导入 - 更新导航菜单结构,移除 OpenAPI 菜单项 --- .../docker-compose/002-mysql-fullstack/docker-compose.yml | 2 +- .../dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java | 4 ++-- .../src/main/resources/application-dev.properties | 2 +- flexmodel-server/src/main/resources/application.properties | 6 +++--- flexmodel-ui | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml b/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml index 48d77fb0..7cca0220 100644 --- a/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml +++ b/deploy/docker-compose/002-mysql-fullstack/docker-compose.yml @@ -132,7 +132,7 @@ services: - QUARKUS_RABBITMQ_USERNAME=${RABBITMQ_DEFAULT_USER} - QUARKUS_RABBITMQ_PASSWORD=${RABBITMQ_DEFAULT_PASS} - QUARKUS_RABBITMQ_VIRTUAL_HOST=${RABBITMQ_VIRTUAL_HOST:-/} - - FLEXMODEL_RABBITMQ_ENABLED=${FLEXMODEL_RABBITMQ_ENABLED:-true} + - FLEXMODEL_EVENTS_RABBITMQ_ENABLED=${FLEXMODEL_RABBITMQ_ENABLED:-true} # RustFS (S3-compatible object storage) - FLEXMODEL_STORAGE_TYPE=s3 diff --git a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java index dd38d4fc..c7bf42a3 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/flow/event/FlowEventRabbitmqBridge.java @@ -17,9 +17,9 @@ * Flow 生命周期事件 RabbitMQ 桥接。 *

* 订阅全部内部 EventBus 事件并转发到单个 topic 交换机(routing key 复用事件地址)。 - * 默认不连 broker({@code flexmodel.rabbitmq.enabled=false}): + * 默认不连 broker({@code flexmodel.events.rabbitmq.enabled=false}): * 关闭时 SmallRye 注入 no-op emitter,{@link #forward(FlowEvent)} 发送即丢弃、不连 broker; - * 本地事件照常经 {@link FlowEventPublisher} 发布。启用需置 {@code flexmodel.rabbitmq.enabled=true}。 + * 本地事件照常经 {@link FlowEventPublisher} 发布。启用需置 {@code flexmodel.events.rabbitmq.enabled=true}。 *

* 每个事件一个 {@code @ConsumeEvent} 方法、消费具体类型,沿用代码库已验证的同类型发布/消费模式, * 避免多态反序列化不确定性。 diff --git a/flexmodel-server/src/main/resources/application-dev.properties b/flexmodel-server/src/main/resources/application-dev.properties index 86a54cda..ee35fb3a 100644 --- a/flexmodel-server/src/main/resources/application-dev.properties +++ b/flexmodel-server/src/main/resources/application-dev.properties @@ -1,7 +1,7 @@ quarkus.log.category."io.quarkus.arc".level=INFO quarkus.log.category."io.agroal".level=INFO quarkus.log.category."dev.flexmodel".level=INFO -flexmodel.rabbitmq.enabled=true +flexmodel.events.rabbitmq.enabled=true quarkus.rabbitmq.username=flexmodel quarkus.rabbitmq.password=flexmodel quarkus.rabbitmq.devservices.enabled=true diff --git a/flexmodel-server/src/main/resources/application.properties b/flexmodel-server/src/main/resources/application.properties index b58f58d7..26da7025 100644 --- a/flexmodel-server/src/main/resources/application.properties +++ b/flexmodel-server/src/main/resources/application.properties @@ -70,7 +70,7 @@ flexmodel.jwt.refresh-token-lifetime=30d # Pages Configuration flexmodel.pages.root-path=./pages # Flow 生命周期 / 数据变更事件 RabbitMQ 桥接 -# 默认不连 broker;需要 RabbitMQ 转发时置 flexmodel.rabbitmq.enabled=true 并提供连接配置。 +# 默认不连 broker;需要 RabbitMQ 转发时置 flexmodel.events.rabbitmq.enabled=true 并提供连接配置。 # 连接器级默认值(对所有 smallrye-rabbitmq 通道生效) mp.messaging.connector.smallrye-rabbitmq.exchange.name=flexmodel.events mp.messaging.connector.smallrye-rabbitmq.exchange.type=topic @@ -81,6 +81,6 @@ mp.messaging.connector.smallrye-rabbitmq.username=${quarkus.rabbitmq.username:gu mp.messaging.connector.smallrye-rabbitmq.password=${quarkus.rabbitmq.password:guest} mp.messaging.connector.smallrye-rabbitmq.virtual-host=${quarkus.rabbitmq.virtual-host:/} mp.messaging.outgoing.events-out.connector=smallrye-rabbitmq -mp.messaging.outgoing.events-out.enabled=${flexmodel.rabbitmq.enabled:false} +mp.messaging.outgoing.events-out.enabled=${flexmodel.events.rabbitmq.enabled:false} mp.messaging.outgoing.data-events-out.connector=smallrye-rabbitmq -mp.messaging.outgoing.data-events-out.enabled=${flexmodel.rabbitmq.enabled:false} +mp.messaging.outgoing.data-events-out.enabled=${flexmodel.events.rabbitmq.enabled:false} diff --git a/flexmodel-ui b/flexmodel-ui index 473d7d29..56ef2736 160000 --- a/flexmodel-ui +++ b/flexmodel-ui @@ -1 +1 @@ -Subproject commit 473d7d29428b41239b6dda06dba9424e496c5f99 +Subproject commit 56ef273607971f21673f418d026fa1bbc5e27eed From 0c9ecd80bac103936ff0bd8a3f1ca46bbb7e3423 Mon Sep 17 00:00:00 2001 From: Cheng Jinbao Date: Fri, 21 Aug 2026 22:14:41 +0800 Subject: [PATCH 16/16] =?UTF-8?q?feat(config):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=BF=A1=E6=81=AF=E8=BE=93=E5=87=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 输出版本号信息 - 输出项目基础域名配置 - 输出路由模式信息 - 输出页面根路径配置 - 输出JWT密钥、访问令牌和刷新令牌生命周期配置 - 输出RabbitMQ事件配置状态 --- .../src/main/java/dev/flexmodel/FlexmodelApplication.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flexmodel-server/src/main/java/dev/flexmodel/FlexmodelApplication.java b/flexmodel-server/src/main/java/dev/flexmodel/FlexmodelApplication.java index 55857394..8697189e 100644 --- a/flexmodel-server/src/main/java/dev/flexmodel/FlexmodelApplication.java +++ b/flexmodel-server/src/main/java/dev/flexmodel/FlexmodelApplication.java @@ -38,8 +38,12 @@ public int run(String... args) throws Exception { private void printFlexmodelConfig() { log.info("---------- Flexmodel Configuration ----------"); + log.info(" version: {}", flexmodelConfig.version()); log.info(" project-url-template: {}", flexmodelConfig.projectUrlTemplate()); + log.info(" project-base-domain: {}", flexmodelConfig.projectBaseDomain().orElse("")); + log.info(" routing-mode: {}", flexmodelConfig.isSubdomainRouting() ? "subdomain" : "path"); log.info(" api-root-path: {}", flexmodelConfig.apiRootPath()); + log.info(" pages.root-path: {}", flexmodelConfig.pages().rootPath()); if (flexmodelConfig.datasources() != null && !flexmodelConfig.datasources().isEmpty()) { flexmodelConfig.datasources().forEach((name, ds) -> { log.info(" datasource[{}]:", name); @@ -51,6 +55,10 @@ private void printFlexmodelConfig() { } else { log.info(" (no datasources configured)"); } + log.info(" jwt.secret: {}", flexmodelConfig.jwt().secret().isBlank() ? "" : "***"); + log.info(" jwt.access-token-lifetime: {}", flexmodelConfig.jwt().accessTokenLifetime()); + log.info(" jwt.refresh-token-lifetime: {}", flexmodelConfig.jwt().refreshTokenLifetime()); + log.info(" events.rabbitmq.enabled: {}", flexmodelConfig.events().rabbitmq().enabled()); } }