# Changelog

本文件记录本仓库的显著变更。日期使用 `YYYY-MM-DD` 格式。

格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)，版本遵循 [SemVer](https://semver.org/lang/zh-CN/)。

> 说明：v0.1.0 为本仓库 git 初始化时的基线快照。下面列出的"主题改动"实际上都被压在
> `chore: initial import of lingniu-vehicle-ingest` 这一个 commit 里——这是 git
> 初始化前已经完成的工作，无法事后拆分。从下一次提交开始走"小步分主题提交"流程。

---

## [Unreleased]

### Added —— 原始报文冷存（Raw Archive）接入 EventSink 扇出
- 新增 `VehicleEvent.RawArchive` sealed 变体（`ingest-api`），携带一帧入站的原始字节 +
  `command` / `infoType` 供按协议命令分片归档。
- `Dispatcher.dispatch` 在 interceptor **之前**按 `RawFrame.rawBytes()` 产出一条
  `RawArchive` 事件，保证 dedup / rate-limit **不过滤** raw archive，满足"每帧必留痕"目标。
- 新增 `ArchiveEventSink implements EventSink`（`sink-archive`）：
  - `accepts` 仅 `VehicleEvent.RawArchive`
  - 写入 key `yyyy/MM/dd/<source>/<vin>/<eventId>.bin`（UTC 日期分片）
  - 同 eventId 重复写按 put 语义覆盖
  - 失败 completeExceptionally，EventBus 打 WARN
  - AutoConfig 在 `ArchiveStore` bean 存在时自动装配
- `KafkaEventSink.accepts(RawArchive)` 返回 `false`：本期 Kafka 不投递原始字节，
  只落 ArchiveStore。未来需要 Kafka 回填 URI 时再开（`TopicRouter` / `EnvelopeMapper`
  已补好 switch 分支）。
- 覆盖测试 `ArchiveEventSinkTest` —— accepts 过滤 / 稳定 key 写入 / 幂等覆盖 3 case。

### Changed —— GB/T 32960 Body Parser 单块异常隔离
- `Gb32960BodyParser` 主循环对单信息块的 parser 异常不再放弃整帧：
  - 固定长度块（`fixedLength ≥ 0`）：回滚 reader → 按 `fixedLen` 截取字节兜成
    `InfoBlock.Raw` → `continue` 继续解析后续块；
  - 变长块（`fixedLength == -1`）或剩余字节不足：剩余字节全部兜成 `Raw` 后终止循环；
  - `parser` 契约违反（声明 `fixedLen=X` 但实际读了 `Y`）仍抛 `DecodeException`，
    以暴露 parser bug，不走 lenient 兜底，避免静默误解析长期误判；
  - 只捕获 `DecodeException` / `BufferUnderflowException` / `IndexOutOfBoundsException`，
    `RuntimeException` 继续向上抛，保留 bug 可见性。
- 新增配置 `lingniu.ingest.gb32960.parse.lenient-block-failure`（默认 `true`）；
  需要回退严格模式时设为 `false`。
- `InfoBlock.Raw` record 结构**未变**，下游 `Gb32960EventMapper` 按 `findBlock(Class)`
  类型匹配，新增 Raw 不影响业务事件映射。
- 新增测试 `Gb32960BodyParserIsolationTest` —— 3 种隔离场景 + 严格模式回退，共 4 case。

---

## [0.1.0] — 2026-04-15

初始基线。多模块 Spring Boot 车辆遥测接入服务。

### Added
- **GB/T 32960.3 接入**（`protocol-gb32960`）：Netty server、帧分帧器、消息解码器、
  V2016/V2025 双版本信息体 parser、平台登入鉴权、VIN 白名单、读空闲告警。
- **JT/T 808 / JT/T 1078 / JSATL12 接入**（`protocol-jt808` / `protocol-jt1078` /
  `protocol-jsatl12`）。
- **MQTT 入站**与**信达平台 push 接入**（`inbound-mqtt` / `inbound-xinda-push`）。
- **Disruptor 事件总线**（`ingest-core`）：高吞吐解耦解码与下游 sink。
- **Sink**：本地归档（`sink-archive`）、Kafka（`sink-kafka`，含 protobuf envelope）。
- **会话状态**（`session-core`）。
- **可观测性**：Micrometer/Actuator 装配（`observability`）。
- **终端控制命令网关**（`command-gateway`）。

### Removed
- 旧文件型事件索引模块和 DuckDB 依赖；历史查询统一收敛到 TDengine 与 `archive://...` 原始报文引用。

### Fixed —— GB/T 32960 应答帧时间字段
- `Gb32960MessageDecoder` 对 `VEHICLE_LOGIN` / `VEHICLE_LOGOUT` /
  `PLATFORM_LOGIN` / `PLATFORM_LOGOUT` 命令预解析 body 首 6B 采集时间，写入
  `header.eventTime`。修复前 handler 回 ACK 时落到 `Instant.now()`，导致应答时间与原
  报文时间不一致，违反 §6.3.2 应答规则。

### Added —— Netty 读空闲告警
- 配置项 `lingniu.ingest.gb32960.idle-read-seconds`（默认 60s，0 禁用）。
- pipeline 注入 `IdleStateHandler(idleReadSeconds, 0, 0)`，仅打 WARN 日志不断链，
  用于排查"对端登入成功后长时间不推数据"场景。

### Changed —— GB32960 信息体 parser 包结构重构
- 原 `codec/parser/` 下的 16 个共用类按版本严格拆分到 `parser/v2016/`（10 类）
  和 `parser/v2025/`（11 类）两个独立子包。
- 取消 `Vehicle/DriveMotor/Engine/Position` 的 `v2016()/v2025()` 工厂方法，每个
  版本独立类，零代码共用。
- `Alarm` 解析中跨版本静态调用 `readFaultList` 也已改为各版本类内部私有方法。
- `Gb32960AutoConfiguration` 同步更新所有 import 与 `@Bean` 工厂。

### Removed —— 违反 2016 标准的 typeCode 注册
- 经核对 GB/T 32960.3-2016 附录 B 表 B.3，`0x30~0x7F` 整段为预留区，
  **2016 标准里没有 0x30 / 0x31 / 0x32 任何字段定义**。
- 删除 `FuelCellStackV2016BlockParser`（曾把 V2025 字段布局硬套到 V2016 帧的 0x30 段，
  解出来的电压/电流远超规范上限，是非物理值）。
- `Gb32960AutoConfiguration` 移除 `gb32960V2016FuelCellStack` bean。
- `Gb32960DecoderGoldenTest` 中 749B 真实生产帧的断言由
  "`isEmpty()`（不应有 Raw 兜底）"翻转为 "`isPresent()`（peer 越界使用 0x30+ 预留
  typeCode，应有 Raw 兜底）"。这才是符合 2016 国标的正确预期。

### Diagnostics —— BodyParser hex dump 增强
- `Gb32960BodyParser` 在遇到未知 typeCode 触发 unknown 路径时，额外打一条 WARN，
  内容为整段 info-block 区域的 16 字节/行 hex dump，并在 `typeStartPos` 字节后插入
  `<` 标记，便于人工反查上一个块的字段对齐。

### Notes —— 待办
- peer 在 V2016 帧（`2323` 起始）里下发 0x30/0x31/0x32 是越界使用 2016 预留区段。
  建议推动对端：
  1. 切换到 V2025 帧头（`2424`）；
  2. 或迁移到 `0x80~0xFE` 用户自定义区段；
  3. 或提供私有协议文档，再决定是否实现 vendor-specific parser。
- `Gb32960BodyParser` 当前对 unknown 块的策略是"吞掉剩余所有字节为单个 Raw 后终止
  循环"，未做单块异常隔离。后续可改为对每个 parser.parse 调用加 try/catch +
  position 回滚，实现单块失败不拖垮整帧。
- `platformAuthorizer.authenticate` 是 EventLoop 同步调用，存在 worker 阻塞风险，
  建议挪到独立业务线程池（与 0x30 议题无关，但属于同模块潜在风险）。

[0.1.0]: #010--2026-04-15
