# 删除钩子、分层日志与异常追踪规范

## 删除生命周期钩子映射

Ineed-Core 当前删除与恢复扩展必须对齐 `LogicDeleteManager` / `PhysicsDeleteManager` 中已经存在的真实方法名：

| 业务语义 | 真实钩子 | 适用时机 |
|----------|----------|----------|
| 逻辑删除前校验 / 逻辑删除前拦截 | `beforeLogicDelete` | 逻辑删除标记真正执行之前 |
| 逻辑删除成功后的扩展 | `afterLogicDelete` | 逻辑删除标记已经成功落库之后 |
| 物理删除前校验 / 物理删除前拦截 | `beforePhysicsDelete` | 物理删除真正执行之前 |
| 物理删除成功后的扩展 | `afterPhysicsDelete` | 物理删除已经成功落库之后 |
| 恢复前准备 | `beforeRecovery` | 恢复逻辑删除数据之前 |
| 恢复后的补偿处理 | `afterRecovery` | 恢复已经成功之后 |

业务口语里常说的 `afterDelete` 必须先确认删除语义：

- 如果是逻辑删除，真实实现点是 `LogicDeleteManager.afterLogicDelete(...)`。
- 如果是物理删除，真实实现点是 `PhysicsDeleteManager.afterPhysicsDelete(...)`。

## 级联删除落位规则

级联删除属于物理删除语义，不属于逻辑删除语义。

逻辑删除只负责把当前对象标记为已删除，并保留后续恢复的可能性。逻辑删除的 `afterLogicDelete` 可以记录审计、刷新索引、失效缓存或发布逻辑删除事件，但不应该物理清理从表、关系表、附件对象或授权关系。

物理删除级联按依赖方向分两类落位：

- 会影响当前实体物理删除能否成功的强依赖清理，放在 `beforePhysicsDelete`。例如关系表、从表、授权关系、明细表等存在外键或业务强依赖时，应先清理依赖数据，再删除当前实体。
- 依赖当前实体已经物理删除成功的外部副作用，放在 `afterPhysicsDelete`。例如回收不可恢复资源、失效外部索引、发布明确的删除完成事件、记录完成日志。

推荐放在 `beforePhysicsDelete` 的动作包括：

- 删除当前实体作为主对象时必须同步清理的多对多关系表
- 删除当前实体前必须删除的从表、明细表或子节点
- 删除树形节点前按 `idPath` / parent 关系定位并物理删除后代
- 删除当前实体前必须解除的授权关系、绑定关系或业务强依赖

核心原则：**物理删除时，必须在 `beforePhysicsDelete` 中级联删除所有关联数据的从表/引用行**（多对多关系表同样适用），确保不因 FK 约束阻断物理删除主流程。每个引用方的级联删除顺序应遵循「先删子、后删父；先删引用方、后删被引用方」。

推荐放在 `afterPhysicsDelete` 的动作包括：

- 物理删除成功后再回收不可恢复资源
- 物理删除成功后再刷新外部索引或缓存
- 物理删除成功后再发布删除完成事件
- 物理删除成功后再记录明确的业务完成日志

判断标准是：如果不先执行会导致当前实体删不掉，放 `beforePhysicsDelete`；如果必须确认当前实体已经删掉才有意义，放 `afterPhysicsDelete`。

`forceDelete` 与 `physicsDelete` 的差异只应体现在是否跳过状态判断并直接进入物理删除流程；不要为 `forceDelete` 额外发明 `beforeForceDelete` / `afterForceDelete` 钩子。需要级联清理时仍复用物理删除生命周期。

## 删除前置保护

删除前校验与拦截逻辑统一落在 `beforeLogicDelete`。

适合放在 `beforeLogicDelete` 的动作包括：

- 检查当前实体是否允许删除
- 检查数据状态是否固定、锁定，或被其他业务状态规则禁止删除
- 检查是否存在必须先解除的前置依赖
- 对不满足删除条件的场景直接抛出业务异常

### checkCustomCanDelete：直接外键引用保护

如果表有直接的一对多/一对一外键关联（多对多关系表除外），必须覆写 `LogicDeleteManager.checkCustomCanDelete(...)` 查询所有引用点。只要存在未删除的引用数据，就抛 `DataInUseException` 阻止逻辑删除。

抛异常时必须使用 FK 约束的 i18n key，而非硬编码中文文案。直接引用保护属于“引用目标正在被使用”语义，i18n key 格式为 `{referencingTableName}.fk.in-use.{constraintName}`，其中 `referencingTableName` 是引用方表，也就是 FK 所在表；不要写成被引用目标表。`constraintName` 是数据库真实 FK 约束名。

```java
@Override
public void checkCustomCanDelete(final Demo entity) {
    final Long refCount = getQueryFactory()
        .select(QRefTable.refTable.id.count())
        .from(QRefTable.refTable)
        .where(
            QRefTable.refTable.deleted.eq(CoreConstant.YESNO.NO.getByte()),
            QRefTable.refTable.demoId.eq(entity.getId())
    )
    .fetchOne();
    if (refCount != null && refCount > 0) {
        throw new DataInUseException("demo_ref_table.fk.in-use.fk_constraint_name");
    }
}
```

对应的 i18n 资源需要在模块的 `messages.properties` / `messages_en.properties` / `messages_zh_TW.properties` 中同步补齐：

```properties
# 以 config.category 为例：
icfg_category_simple_value.fk.in-use.fk_icfg_cat_simple_cat_id=分类类型定义数据被分类项（简单）使用，不能删除或更新
icfg_category_tree_value.fk.in-use.fk_icfg_cat_tree_cat_id=分类类型定义数据被分类项（树形）使用，不能删除或更新
```

`DataInUseException` 的 `userObjectName` 最终通过 `I18nExceptionHandler` 走 `I18nUtil.getMessage()` 查 i18n 资源，因此同一 FK key 可以覆盖简体中文、英文、繁体中文三语，并且与数据库 FK 违规时的自动解析消息共享同一套 key。数据库异常解析会优先使用 SQL 错误中解析出的表名精确查找 `{tableName}.fk.*.{constraintName}`，解析不到表名时才按约束名后缀兼容查找。

同一个数据库 FK 约束必须准备两类文案：

- `{referencingTableName}.fk.not-found.{constraintName}`：子表 `INSERT` / `UPDATE` 时引用的目标数据不存在
- `{referencingTableName}.fk.in-use.{constraintName}`：父表 `DELETE` / `UPDATE` 时已有引用数据正在使用目标数据

历史 `{basePackage}.{ReferencingEntity}.fk.{constraintName}` 仅作为兼容回退，新模块和新文案不要再使用单一 FK key。

直接 FK 引用保护与通用 `beforeLogicDelete` 的区别：`checkCustomCanDelete` 是 `LogicDeleteManager` 定义的独立扩展点，在 `checkCanDelete`（null、租户、固定/锁定状态）之后、`beforeLogicDelete` 之前执行。它只负责「FK 引用计数查询」，不混合其他业务拦截。

如果某个动作的目标是”阻止删除发生”，就应该落在 `beforeLogicDelete`（通用拦截）或 `checkCustomCanDelete`（FK 引用拦截），而不是等删除完成后再补救。

## 恢复后补偿

恢复后的关联处理统一落在 `afterRecovery`。

适合放在 `afterRecovery` 的动作包括：

- 恢复后重建关联关系
- 恢复后补齐状态同步
- 恢复后执行重新开放或重新挂接动作

如果某个动作依赖“数据已经恢复成功”，就不应放在 `beforeRecovery`，而应放在 `afterRecovery`。

## 日志总体原则

### 0. SLF4J 唯一允许的日志门面

项目中所有日志输出必须通过 SLF4J。禁止使用以下方式：

- `System.out.println` / `System.err.println`
- `java.util.logging`（JUL）
- `Log4j` / `Log4j2` 原生 API
- `commons-logging`（JCL）

简单判断：只要能拿到 `org.slf4j.Logger` 实例，就必须用它；不要绕开 SLF4J 直接调用底层实现。

唯一的例外：**interface 的默认方法**无法使用 `@Slf4j`（Lombok 不支持接口字段），此时允许通过 `LoggerFactory.getLogger(...)` 声明静态 Logger，这也是 ineed-core 框架接口当前的做法。

### 1. 新写具体类统一使用 `@Slf4j`

开发者、代码生成器或自动化工具在新增具体业务类时，统一直接使用 Lombok 的 `@Slf4j`：

- `Controller`
- `Service`
- `Manager`
- `Repository / EntityManager / Operation` 的自定义实现类
- 删除钩子实现类
- `Filter`
- `Handler`
- `Listener`
- `Task / Initializer / Engine`

不要在新生成的具体类里继续手写 `LoggerFactory.getLogger(...)`。

例外只有一类：`Ineed-Core` 现有的接口默认实现，例如 `ICreateController`、`ICreateService`、`ILogicDeleteController`、`IAccountService` 等，由于它们本身是接口默认方法，已经使用接口内静态 `Logger`，这是框架底座实现，不是新增业务代码应该模仿的写法。

### 2. 标准支持接口已有默认日志

当前框架已经在标准支持接口里提供了一部分默认日志：

| 层次 | 已有默认日志入口 |
|------|------------------|
| `Controller` | `ICreateController`、`IUpdateController`、`ILogicDeleteController`、`IAccountController`、`IEnableController`、`ISearchController` 等 |
| `Service` | `ICreateService`、`IUpdateService`、`ILogicDeleteService`、`IAccountService`、`ISearchService`、`IOrderService` 等 |
| 数据回调 / 基础设施 | `JpaEntityCallback`、`JdbcEntityCallback`、`MybatisFlexEntityCallback`、`R2dbcEntityCallback`、`WebMvcOperateInfoFilter`、`WebFluxOperateInfoFilter`、`GlobalExceptionHandler` 等 |

这意味着：

- 如果某个 `Controller` / `Service` 只是纯粹继承标准接口、没有任何手写业务方法，默认日志已经覆盖了标准 CRUD / Query / DataStatus / Account 主链。
- 即便如此，具体类仍然建议保留 `@Slf4j`，便于后续补充自定义接口时直接沿统一写法扩展。
- 不要为了“每层都打日志”而在只调用接口默认方法的空壳类里再人为包一层重复日志。

### 3. 自定义方法必须显式补日志

凡是自己手写的方法，特别是以下几类，必须显式补日志：

- 自定义 `Controller` 接口
- 自定义 `Service` 编排方法
- `Manager` 中的业务规则方法
- 删除 / 恢复钩子方法
- 自定义 `Repository` / `EntityManager` / `Operation`
- 定时任务、初始化器、监听器、过滤器、鉴权处理器

### 4. 日志级别统一建议

| 级别 | 使用场景 |
|------|----------|
| `info` | 写操作入口、登录、安全敏感动作、批量任务开始/完成、初始化开始/完成、跨表业务主编排开始/完成 |
| `debug` | 读操作入口、关键分支判断、关联数量、命中条件、动态查询参数摘要、批处理中间节点 |
| `warn` | 可预期但值得关注的异常分支，例如重复跳过、可恢复失败、缺少可选依赖、补偿动作未执行 |
| `error` | 已捕获异常且准备继续抛出，或者进入明确失败分支 |

### 5. 敏感信息禁止直接打印

日志里禁止直接输出以下内容：

- 明文密码
- 新密码 / 旧密码
- Token 全串
- 密钥 / 私钥 / 密文原文
- 完整凭证对象
- 大体量二进制内容

可以输出的替代信息包括：

- `principalId`
- `identityType`
- `tenantKey`
- `resourceKey`
- `id`
- `idSet.size()`
- 经过裁剪的查询条件摘要

## LogHelper 统一用户注入 API

Ineed-Core 提供了 `LogHelper`（阻塞式）和 `ReactiveLogHelper`（响应式）两个统一的日志辅助工具。它们与 `@Slf4j` 不冲突——`@Slf4j` 负责生成 `log` 实例，`LogHelper`/`ReactiveLogHelper` 负责在每次日志输出时自动注入当前操作用户信息。

### API

两个类提供完全一致的静态方法：

```java
// 导入
import com.ineed.base.context.LogHelper;           // 阻塞式（WebMvc）
import com.ineed.base.context.ReactiveLogHelper;    // 响应式（WebFlux）

// 用法：与 log.xxx(format, args) 相同，只是第一个参数传入 log 实例
LogHelper.debug(log, "message, id = {}", id);
LogHelper.info(log, "message, id = {}", id);
LogHelper.warn(log, "message, id = {}", id);
LogHelper.error(log, "message, id = {}", id);
```

### 选择规则

| 场景 | 使用 |
|------|------|
| WebMvc Controller / Web 适配层 | `@Slf4j + LogHelper` |
| WebFlux Controller / Web 适配层 | `@Slf4j + ReactiveLogHelper` |
| Engine Service / Manager / Repository（阻塞式业务链路） | `LogHelper` |
| Engine Service / Manager / Repository（响应式业务链路） | `ReactiveLogHelper` |
| ineed-core 框架接口默认方法（engine + webmvc） | `LogHelper`（接口内使用静态 Logger） |
| ineed-core 框架接口默认方法（engine-reactive + webflux） | `ReactiveLogHelper`（接口内使用静态 Logger） |

边界约定：

- `LogHelper` 位于 `ineed-core-context`，`ReactiveLogHelper` 位于 `ineed-core-context-reactive`。只有模块已经直接依赖对应 context 模块，或当前模块本来就是 Web / Engine / Manager / Repository 等业务链路模块时，才迁移到 helper。
- 不要为了单条日志给纯 core、纯 runtime、独立工具、agent、测试辅助模块新增 `ineed-core-context` / `ineed-core-context-reactive` 依赖。此类模块继续保留 `@Slf4j + log.xxx(...)`，但仍必须遵守 SLF4J 门面、敏感信息保护和异常对象传递规则。
- Web 侧具体类（Controller、Filter、Handler、Listener、Web 适配 Service）继续使用 Lombok `@Slf4j` 生成 `log` 实例；需要输出业务日志时，把已有 `log` 传给 `LogHelper` / `ReactiveLogHelper`。不要自己构建 Logger、日志对象、请求描述对象或另一套日志上下文。
- Reactive 链路必须使用 `ReactiveLogHelper`；不要在 WebFlux / R2DBC 代码中混用阻塞式 `LogHelper`。

### 用户信息来源

- `LogHelper` 从 `ContextHandler.getOperationUserJson()` 获取，底层基于 ThreadLocal
- `ReactiveLogHelper` 从 `ReactiveContextHandler.getThreadLocalUser()` 获取，通过 Reactor Context → ThreadLocal 桥接（与 Spring Security 的模式一致）
- 未登录时自动回退为匿名用户 JSON

### 日志输出效果

```
// 使用 LogHelper 前
// CreateService.save(), ao = CreateAO(name=test)

// 使用 LogHelper 后
// user={"principalId":"u123","principalAccount":"zhangsan","principalName":"张三","tenantKey":"t1"}, CreateService.save(), ao = CreateAO(name=test)
```

### 框架接口默认方法已统一使用

以下框架标准接口的默认方法已全部迁移到 `LogHelper` / `ReactiveLogHelper`：

| 层次 | 已使用 LogHelper/ReactiveLogHelper 的接口 |
|------|------------------------------------------|
| Engine Service | `ICreateService`、`IUpdateService`、`ILogicDeleteService`、`IPhysicsDeleteService`、`IDefaultService`、`IEnableService`、`ILockService`、`IPrimaryService`、`IPublishService`、`IAccountService`、`IOrderService`、`IDuplicateService`、`IFindByIdService`、`ISearchService` 等 |
| Engine Service (Reactive) | 同上，对应 reactive 版本 |
| WebMvc Controller | `ICreateController`、`IUpdateController`、`ILogicDeleteController`、`IPhysicsDeleteController`、`IDefaultController`、`IEnableController`、`ILockController`、`IPrimaryController`、`IPublishController`、`IAccountController`、`IOrderController`、`IImportController`、`IExportController`、`IDuplicateController` 等 |
| WebFlux Controller | 同上，对应 webflux 版本 |

### 业务代码建议

- 业务自定义 `Controller` 方法在 WebMvc 中使用 `LogHelper`，在 WebFlux 中使用 `ReactiveLogHelper`；入口日志仍然放在手动校验之前，保证校验失败也能留下调用痕迹
- WebFlux 自定义 `Controller` 方法的入口日志必须放在 `Mono.defer(() -> { ... })` 内，与 `ResultBuild.success(serviceMono)` 一起延迟到订阅期执行，避免链外提前读取响应式上下文或提前产生副作用
- 业务自定义 `Service`、`Manager`、`Repository` 方法在业务链路模块中使用 `LogHelper`（或 `ReactiveLogHelper`）替代 `log.xxx()`，确保排查问题时能追溯到操作人
- 具体类仍然使用 `@Slf4j` 生成 `log` 实例；调用 helper 时只把这个已有 `log` 作为第一个参数传入
- `LogHelper` 内部已经做了级别判断（如 `log.isInfoEnabled()`），业务代码不需要再在外面包一层 `if`

## 分层日志方案

### Controller 层

`Controller` 的职责是 API 入口，因此日志重点是“谁调用了什么动作，入口参数是什么”。

规则如下：

- 标准支持接口方法继续复用框架默认日志。
- 自定义 `Controller` 方法必须在真正执行业务前记录入口日志。
- 手动触发 Jakarta Validation 或其它参数校验的自定义方法，入口日志放在校验之前；这样即使校验失败，也能在日志中看到调用痕迹。
- 纯查询、辅助查询、字典类接口优先 `debug`。
- 创建、编辑、删除、导入、导出、登录、切换租户、权限刷新等动作优先 `info`。
- 一般不要在 `Controller` 中捕获业务异常只为打印一遍日志；异常统一交给下层抛出和 `GlobalExceptionHandler` 兜底。

推荐记录内容：

- 类名 + 方法名
- `id` / `idSet`
- 关键查询对象
- 关键业务参数摘要

示例：

```java
@Slf4j
public class DemoController {

  public ResponseResults<Void> bindRole(final String id, final BindRoleAO ao) {
    LogHelper.info(log, "DemoController.bindRole(), id = {}, roleIdSet = {}", id, ao.getRoleIdSet());
    demoService.bindRole(id, ao);
    return ResultBuild.success();
  }

  public ResponseResults<QueryResults<DemoVO>> searchAudit(final DemoAuditQuery query) {
    LogHelper.debug(log, "DemoController.searchAudit(), query = {}", query);
    return ResultBuild.success(demoService.searchAudit(query));
  }
}
```

### Service 层

`Service` 是事务边界和业务编排边界，因此日志重点是“一次业务用例开始了、关键编排节点走到了哪、失败原因是什么”。

规则如下：

- 标准 CRUD / Query / DataStatus / Account 默认方法继续复用接口日志。
- 自定义 `Service` 方法必须至少记录一条入口日志。
- 跨 `Manager` 编排、批量处理、初始化、补偿等方法，建议再补关键节点 `debug` 日志。
- 明显的长链路操作建议补一条完成日志。
- 如果方法里 `catch` 了异常，就必须 `log.error(..., e)` 后继续抛出，不吞异常。

推荐记录内容：

- 类名 + 方法名
- `tenantKey`
- 主对象 `id`
- 关键步骤名
- 批量数量

示例：

```java
@Slf4j
@Service
public class DemoService {

  @Transactional(rollbackFor = Exception.class)
  public void bindRole(final String id, final BindRoleAO ao) {
    LogHelper.info(log, "DemoService.bindRole(), id = {}, roleIdSet = {}", id, ao.getRoleIdSet());
    try {
      LogHelper.debug(log, "DemoService.bindRole(), step = validate-demo");
      demoManager.checkCanBindRole(id);

      LogHelper.debug(log, "DemoService.bindRole(), step = replace-relation");
      demoRoleRelationManager.replaceRelation(id, ao.getRoleIdSet());

      LogHelper.info(log, "DemoService.bindRole() completed, id = {}, relationCount = {}", id, ao.getRoleIdSet().size());
    } catch (final Exception e) {
      LogHelper.error(log, "DemoService.bindRole() is error : {}.", e.getMessage(), e);
      throw e;
    }
  }
}
```

### Manager 层

`Manager` 不负责整条请求链路的入口审计，它负责“单类业务规则”和“局部实体规则”，因此日志重点是“规则判断、状态变化、钩子清理、补偿结果”。

规则如下：

- 如果 `Manager` 只是纯继承标准 CRUD / Query 能力且没有手写方法，不需要为了形式强行打印空日志。
- 自定义 `Manager` 方法、规则判断方法、删除/恢复钩子必须使用 `@Slf4j` 并记录关键业务动作。
- `beforeLogicDelete` 重点记录逻辑删除拦截条件和对象标识。
- `afterLogicDelete` 重点记录逻辑删除后的审计、缓存、索引、事件等可恢复扩展，不做级联物理清理。
- `beforePhysicsDelete` 重点记录物理删除拦截条件和对象标识。
- `afterPhysicsDelete` 重点记录级联物理清理动作和关联数量。
- `afterRecovery` 重点记录恢复后的补偿动作和关联数量。
- 规则拦截前如果需要补充定位信息，可以使用 `warn`；如果只是普通判断过程，使用 `debug` 即可。

推荐记录内容：

- `tenantKey`
- 主对象 `id`
- 关联对象数量
- 当前规则判断结果

示例：

```java
@Slf4j
@Component
public class DemoManager {

  public void beforeLogicDelete(final Demo beDelete) {
    LogHelper.debug(log, "DemoManager.beforeLogicDelete(), tenantKey = {}, id = {}", beDelete.getTenantKey(), beDelete.getId());
    if (beDelete.isFixed()) {
      LogHelper.warn(log, "DemoManager.beforeLogicDelete() blocked, tenantKey = {}, id = {}, reason = fixed-data",
          beDelete.getTenantKey(), beDelete.getId());
      throw StatefulException.of(ErrorCode.DATA_FIXED_CANNOT_UPDATE);
    }
  }

  public void afterPhysicsDelete(final Demo beDelete) {
    final int relationCount = demoRelationRepository.deleteByDemoId(beDelete.getId());
    LogHelper.info(log, "DemoManager.afterPhysicsDelete(), tenantKey = {}, id = {}, relationCount = {}",
        beDelete.getTenantKey(), beDelete.getId(), relationCount);
  }
}
```

### Repository / EntityManager / Operation 层

这一层不应该把普通单表 CRUD 全部重复打一遍日志，否则会制造噪音。

规则如下：

- 对于框架已有的标准持久化方法，通常不额外加日志。
- 只有在存在自定义 SQL、动态 SQL 组装、批量更新、元数据扫描、跨库连接、复杂查询降级等情况时，才补必要日志。
- 推荐记录“参数摘要、命中数量、耗时、失败原因”，不要打印整包大对象。
- 如果只是单纯 `save/update/findById`，不要再打一遍低价值日志。

推荐记录内容：

- `id`
- `idSet.size()`
- 关键查询条件摘要
- 结果数量
- 耗时

示例：

```java
@Slf4j
@Repository
public class DemoJdbcRepository {

  public List<DemoVO> searchCustom(final DemoQuery query) {
    LogHelper.debug(log, "DemoJdbcRepository.searchCustom(), tenantKey = {}, keyword = {}, pageSize = {}",
        query.getTenantKey(), query.getSearchKey(), query.getPageSize());
    try {
      return jdbcTemplate.query(SQL, params, rowMapper);
    } catch (final Exception e) {
      LogHelper.error(log, "DemoJdbcRepository.searchCustom() is error : {}.", e.getMessage(), e);
      throw e;
    }
  }
}
```

### Hook / Task / Listener / Filter / Handler 层

这类类虽然不在标准业务主链的四层内，但往往承担运行时关键动作，因此也必须统一使用 `@Slf4j`。

规则如下：

- 定时任务、初始化器：开始/完成用 `info`，中间步骤用 `debug`。
- 鉴权过滤器、会话过滤器：轻量入口日志用 `debug`，认证失败或拒绝访问由专门处理器记录。
- 监听器：成功事件、失败事件优先 `debug`；真正异常再用 `error`。
- Handler：不重复打印业务异常栈；聚焦处理器自身的分支与兜底异常。

## 一次方法的最小日志结构

对自定义方法，推荐最少遵守下面这个结构：

1. 入口日志
2. 关键节点日志
3. 成功完成日志
4. 异常日志

最小模板：

```java
LogHelper.info(log, "DemoService.doSomething(), id = {}", id);
try {
  LogHelper.debug(log, "DemoService.doSomething(), step = check");
  // business
} catch (final Exception e) {
  LogHelper.error(log, "DemoService.doSomething() is error : {}.", e.getMessage(), e);
  throw e;
}
```

## 与现有 Ineed-Core 代码的对齐关系

当前方案不是重新发明一套日志风格，而是基于现有 Ineed-Core 代码收敛出来的统一规则：

- `ICreateController`、`ILogicDeleteController` 等已经负责标准 Web 入口日志
- `ICreateService`、`IAccountService`、`ILogicDeleteService` 等已经负责标准服务入口日志与异常日志
- `WebMvcOperateInfoFilter` / `WebFluxOperateInfoFilter` 已经负责请求上下文采集
- `GlobalExceptionHandler` 已经负责统一异常输出
- `JpaEntityCallback`、`JdbcEntityCallback`、`MybatisFlexEntityCallback`、`R2dbcEntityCallback` 已经负责底层实体回调日志

本规范要求新增代码与这些真实锚点保持一致，而不是平行造轮子。

## 追踪信息来源

请求级追踪信息来自 `WebMvcOperateInfoFilter` / `WebFluxOperateInfoFilter`，不需要每个业务重复采集。

当前至少可以稳定拿到以下上下文字段：

- `requestUrl`
- `requestMethod`
- `signature`
- `applicationKey`
- `ip`
- `userAgent`

业务日志在需要补充追踪时，应优先复用这些已有上下文，而不是重新拼装另一套请求描述模型。

## 业务代码日志硬约束

新增或修改 Ineed-Core 业务代码时，必须同时遵守：

- 日志门面必须走 SLF4J：具体类用 `@Slf4j`，interface 默认方法用 `LoggerFactory.getLogger(...)`
- Web 侧具体类统一使用 `@Slf4j` 生成的 `log`，调用 `LogHelper` / `ReactiveLogHelper` 时只传入这个已有 `log`，不自定义 Logger 或日志对象，不重复构建请求上下文
- 已经具备 context 依赖的 Engine Service / Manager / Repository 业务链路日志统一使用 `LogHelper`（阻塞式）或 `ReactiveLogHelper`（响应式），不再使用 `log.xxx()`，确保每条日志自动携带操作用户信息
- 无 context 依赖的纯 core、runtime、agent、工具、测试辅助模块不要仅为日志新增依赖；继续保留 `@Slf4j + log.xxx(...)`
- 标准支持接口已有的默认日志不要重复包一层
- 自定义 `Controller` / `Service` / `Manager` / `Hook` / `Repository` 方法必须显式补日志
- 只打印必要的关键参数，不打印密码、Token、密钥、完整凭证
- 发生异常时记录 `error` 后继续抛出

## 落地检查清单

- 是否先区分逻辑删除与物理删除，再映射删除后钩子
- 逻辑删除前保护是否落在 `beforeLogicDelete`
- 直接 FK 引用（非 M2M）是否在 `checkCustomCanDelete` 中查询引用点，抛 `DataInUseException` 并使用 FK i18n key
- FK i18n key 是否符合 `{referencingTableName}.fk.in-use.{constraintName}` / `{referencingTableName}.fk.not-found.{constraintName}` 格式，且三语同步
- 物理删除前保护是否落在 `beforePhysicsDelete`
- 物理删除时是否在 `beforePhysicsDelete` 中级联删除所有关联数据的从表/引用行
- 逻辑删除后的可恢复扩展是否落在 `afterLogicDelete`
- 级联物理清理是否落在 `afterPhysicsDelete`
- 恢复后补偿是否落在 `afterRecovery`
- 新写具体类是否统一使用 `@Slf4j`
- 是否正确复用了框架默认 `Controller` / `Service` 日志而没有重复套娃
- 自定义方法是否包含动作名、关键入参、异常对象
- Web 侧具体类是否使用 `@Slf4j` 生成的 `log` 调用 `LogHelper` / `ReactiveLogHelper`，没有引入自定义 Logger、日志对象或重复请求上下文
- 具备 context 依赖的 Engine Service / Manager / Repository 业务链路是否使用 `LogHelper`（阻塞式）或 `ReactiveLogHelper`（响应式）输出日志，确保用户信息已注入
- 无 context 依赖的底层模块是否没有为了日志新增依赖，且仍遵守 SLF4J 与敏感信息规则
- 是否避免打印密码、Token、密钥、完整凭证
- 是否继续复用 `WebMvcOperateInfoFilter` / `WebFluxOperateInfoFilter`、`GlobalExceptionHandler`、`ResultBuild`

> 最后更新：2026-05-16
