# Ineed-Core 集成事件接入指南

## 文档定位

本文档说明 `ineed-integration` 当前已经落地的接入方式、配置规则、事件模型和业务模块的推荐用法。

当前范围覆盖标准 `Manager` 主链路中的 CRUD 和数据状态事件：

- `create`
- `update`
- `logicDelete`
- `physicsDelete`
- `dataStatusChange`

已接入的执行线包括：

- 阻塞式 `ineed-core-data-core`
- 响应式 `ineed-core-data-core-reactive`

暂不覆盖：

- `recovery`
- WebHook adapter
- Spring Cloud adapter
- outbox / 可靠投递

## 1. 当前模块关系

- `ineed-integration`
  负责事件模型、开关、默认 key 解析、Spring 事件发布与蒸馏监听。
- `ineed-core-data-core`
  已在标准同步 `Manager` 默认实现里接入事件发送。
- `ineed-core-data-core-reactive`
  已在标准响应式 `Manager` 默认实现里接入同一套事件发送。
- 业务模块
  当前不需要改 `Service` 主流程，只需要按需配置开关、监听事件，或者覆盖默认 key 解析。

## 2. 当前接入前提

### 2.1 标准启动方式

如果应用主类使用：

```java
@SpringBootApplication(scanBasePackages = "com.ineed")
```

当前可以直接扫描到：

- `IntegrationConfiguration`
- `BusinessEventPublisher`
- `DistilledBusinessEventListener`

这是仓库里当前 demo / test project 的默认接法。

### 2.2 非标准扫描方式

如果应用没有扫描 `com.ineed` 根包，需要显式接入：

```java
import org.springframework.context.annotation.Import;

import com.ineed.integration.configuration.IntegrationConfiguration;

@Import(IntegrationConfiguration.class)
public class YourApplication {
}
```

或者把 `com.ineed.integration` 加入你的扫描路径。

## 3. 当前配置方式

### 3.1 YAML 示例

下面示例是“开启发送”的示例。

真实默认值仍然是：

- `ineed.integration.enabled = false`
- `ineed.integration.internal.enabled = false`
- `ineed.integration.distilled.enabled = false`

```yaml
ineed:
  integration:
    enabled: true
    internal:
      enabled: true
      keys:
        security.member-identity: true
    distilled:
      enabled: true
      keys:
        security.member-identity: true
```

### 3.2 Properties 示例

```properties
ineed.integration.enabled=true
ineed.integration.internal.enabled=true
ineed.integration.internal.keys[security.member-identity]=true
ineed.integration.distilled.enabled=true
ineed.integration.distilled.keys[security.member-identity]=true
```

### 3.3 判断规则

事件是否会进入发布链，当前按下面顺序判断：

1. `ineed.integration.enabled` 必须为 `true`
2. 对应通道 `enabled` 必须为 `true`
3. 对应 `keys[eventKey]` 必须为 `true`

未配置的 key 默认视为关闭。

这里的 `eventKey` 是兼容命名。Manifest 配置了 `resourceKey` 时，发布链使用 `resourceKey` 作为 key；旧 Manifest 只配置 `eventKey` 时仍按旧 key 兼容。

### 3.4 一个容易忽略的点

当前 `shouldPublishInternal(eventKey)` 的规则是：

- 只要内部通道开启了该 key，就发布内部事件
- 只要蒸馏通道开启了该 key，也会先发布内部事件

这是当前的有意设计。

原因是蒸馏事件本身依赖 `InternalBusinessEvent`，并且蒸馏监听器要在事务提交后再处理。

## 4. 默认资源键规则

当前默认解析器是 `DefaultBusinessEventDefinitionResolver`。

规则如下：

- `sourceModule`
  取实体包名中 `com.ineed` 后面的第一个段
- `aggregateType`
  取实体简单类名并转换为 `snake_case`
- `resourceKey`
  注册后使用 Manifest 中的业务资源键；未注册时回退为 `类名.操作后缀`
- `operationKey`
  取 `BusinessOperationType` 的资源操作键，如 `create`、`update`
- `eventKey`
  兼容字段；有 `resourceKey` 时等同 `resourceKey`，旧 Manifest 仅配置 `eventKey` 时继续使用旧值

例如：

- 实体类：`com.ineed.security.authorization.memberidentity.jdbc.entity.MemberIdentity`
- 操作：`UPDATE`
- 默认 resourceKey/eventKey：`com.ineed.security.authorization.memberidentity.jdbc.entity.MemberIdentity.updated`
- 默认 operationKey：`update`

如果实体类名本身带后缀，当前也会保留下来。

例如：

- 实体类：`MemberIdentityEntity`
- 默认聚合名：`member_identity_entity`

如果这个默认规则不符合业务语义，可以覆盖 `BusinessEventDefinitionResolver`。

## 5. 当前事件模型

### 5.1 顶层结构

内部事件和蒸馏事件共用同一份 `BusinessEventMessage<T>`，核心字段包括：

- `eventId`
- `eventKey`（兼容字段，推荐与 `resourceKey` 保持一致）
- `resourceKey`
- `operationKey`
- `aggregateType`
- `operationType`
- `entityId`
- `tenantKey`
- `sourceModule`
- `sourceType`
- `occurredAt`
- `payload`

### 5.2 当前 CRUD payload

当前 `payload` 类型是 `CrudBusinessEventPayload<T>`，包含：

- `entityId`
- `before`
- `after`
- `deleteMode`

### 5.3 当前各操作的实际负载

`CREATE`

- `before = null`
- `after = 创建后的 VO`
- `deleteMode = null`

`UPDATE`

- `before = 修改前 VO`
- `after = 修改后 VO`
- `deleteMode = null`

`LOGIC_DELETE`

- `before = null`
- `after = null`
- `deleteMode = LOGIC`

`PHYSICS_DELETE`

- `before = null`
- `after = null`
- `deleteMode = PHYSICS`

删除事件现在只保证：

- `entityId`
- `tenantKey`
- `operationType`
- `deleteMode`

如果后面要补删除前快照，再单独增强。

### 5.4 DataStatus payload

数据状态变化使用 `DataStatusBusinessEventPayload`，字段固定为：

- `id`
- `action`：追加状态位为 `append`，移除状态位为 `subtract`
- `statusFeature`：`DataStatusFeature` 枚举名，例如 `ENABLED`
- `beforeStatus`：变化前的完整 byte 状态；实体原值为 `null` 时按 `0` 处理
- `afterStatus`：变化后的完整 byte 状态

`Manager` 先读取并校验全部目标，再比较完整状态。只有状态位实际变化的目标才会进入唯一状态处理、持久化和事件发布；空编号集合、全部幂等的命令以及未注册或关闭的事件 key 都不会发送事件。批量操作对每个实际变化的目标发送一条事件。

## 6. 事件时序

阻塞式主链路当前时序如下：

1. `Manager` 主流程完成数据变更
2. 立即发布 `InternalBusinessEvent`
3. `DistilledBusinessEventListener` 用 `@TransactionalEventListener(phase = AFTER_COMMIT)` 监听
4. 主事务提交成功后，才发布 `DistilledBusinessEvent`

这意味着：

- 内部事件是事务内的 Spring 事件
- 蒸馏事件是提交后的 JVM 内事件
- 当前不保证跨进程可靠送达

响应式主链路使用同一套 `IntegrationEventSupport` 发布事件：

- 事件在 repository 保存 / 删除链路完成、`afterXxx` 钩子完成之后发布
- 数据状态事件在对应 append / subtract 持久化完成之后发布
- 发布动作发生在当前 reactive subscription 内，不要求业务 `Service` 手工补发
- 未订阅时不会查询目标、修改状态或发布事件
- 如果业务链路接入 reactive transaction，`DistilledBusinessEvent` 只在事务提交后发送，回滚时不发送
- 当前不承诺跨进程可靠投递，也不承诺并发写入下的可串行化行为

## 7. 业务模块怎么监听

### 7.1 模块内协作监听内部事件

如果你要做同 JVM 的模块内协作，并且需要拿到内部标准消息，可以监听 `InternalBusinessEvent<?>`：

```java
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import com.ineed.integration.event.BusinessEventMessage;
import com.ineed.integration.event.InternalBusinessEvent;

@Component
public class MemberInternalEventListener {

  @EventListener
  public void onInternalEvent(final InternalBusinessEvent<?> event) {
    final BusinessEventMessage<?> message = event.getMessage();
    if (!"security.member-identity".equals(message.getResourceKey()) || !"create".equals(message.getOperationKey())) {
      return;
    }
    // 模块内协作逻辑
  }
}
```

### 7.2 跨模块或外发前置监听蒸馏事件

如果你要做 WebHook、对外同步、跨模块桥接，优先监听 `DistilledBusinessEvent<?>`：

```java
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import com.ineed.integration.event.BusinessEventMessage;
import com.ineed.integration.event.DistilledBusinessEvent;

@Component
public class MemberPrivilegeBridgeListener {

  @EventListener
  public void onDistilledEvent(final DistilledBusinessEvent<?> event) {
    final BusinessEventMessage<?> message = event.getMessage();
    if (!"security.member-identity".equals(message.getResourceKey()) || !"create".equals(message.getOperationKey())) {
      return;
    }
    // 这里再去调用权益中心 adapter / webhook / stream bridge
  }
}
```

推荐原则：

- 模块内同步协作：优先内部事件
- 跨模块、外发、集成边界：优先蒸馏事件

## 8. 业务模块怎么覆盖默认 key

如果默认 key 不够稳定，或者你不想把实体命名暴露给事件 key，可以自己提供 `BusinessEventDefinitionResolver` Bean：

```java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.ineed.integration.event.BusinessEventDefinitionResolver;
import com.ineed.integration.event.BusinessOperationType;
import com.ineed.integration.event.DefaultBusinessEventDefinitionResolver;
import com.ineed.integration.event.ResolvedBusinessEventDefinition;
import com.ineed.security.authorization.memberidentity.jdbc.entity.MemberIdentity;

@Configuration
public class MemberEventDefinitionConfiguration {

  @Bean
  public BusinessEventDefinitionResolver memberBusinessEventDefinitionResolver() {
    final BusinessEventDefinitionResolver delegate = new DefaultBusinessEventDefinitionResolver();
    return (entityType, operationType) -> {
      if (MemberIdentity.class.equals(entityType)) {
        return memberIdentityDefinition(operationType);
      }
      return delegate.resolve(entityType, operationType);
    };
  }

  private static ResolvedBusinessEventDefinition memberIdentityDefinition(final BusinessOperationType operationType) {
    final ResolvedBusinessEventDefinition definition = new ResolvedBusinessEventDefinition();
    definition.setSourceModule("security");
    definition.setAggregateType("member_identity");
    definition.setOperationType(operationType);
    definition.setResourceKey("security.member-identity");
    definition.setOperationKey(operationType.getOperationKey());
    definition.setEventKey(definition.getResourceKey());
    return definition;
  }

}
```

更推荐的实际做法是：

- 业务模块内部自己封装一层 resolver
- 未命中的实体继续委托给默认 resolver

这样不会把所有规则都重写掉。

## 9. 当前已经接入的发送点

当前标准链路的发送点在 `Manager` 主流程中，不在 `Service` 中。

阻塞式发送点：

- `CreateManager.save(...)`
- `UpdateManager.update(...)`
- `LogicDeleteManager.logicDelete(...)`
- `PhysicsDeleteManager.physicsDelete(...)`

响应式发送点：

- `com.ineed.core.data.reactive.manager.crud.CreateManager.save(...)`
- `com.ineed.core.data.reactive.manager.crud.UpdateManager.update(...)`
- `com.ineed.core.data.reactive.manager.crud.LogicDeleteManager.logicDelete(...)`
- `com.ineed.core.data.reactive.manager.crud.PhysicsDeleteManager.physicsDelete(...)`

并且发布动作都放在：

- 业务 `afterXxx` 钩子执行完成之后
- 返回结果之前

这样可以避免业务覆写 `afterXxx` 时绕过底座事件。

## 10. 当前不建议的用法

- 不要在业务 `Service` 里手工重复发同一类标准 CRUD 事件
- 不要把开关分散到业务模块各自配置里
- 不要直接依赖删除事件里存在完整快照，当前没有这个保证
- 不要把 `DistilledBusinessEvent` 当成可靠消息中间件

## 11. 下一阶段候选

后续如果继续推进，下一批更自然的工作是：

- `DataStatus` 事件
- `recovery` 事件
- 删除前快照增强
- WebHook adapter
- Spring Cloud adapter
- outbox / 可靠投递

> 最后更新：2026-05-15
