fix(mileage): reconcile track and daily semantics

This commit is contained in:
lingniu
2026-07-20 08:52:27 +08:00
parent 6245a9b979
commit 6574b49ff9
4 changed files with 49 additions and 2 deletions

View File

@@ -171,6 +171,30 @@ stat-writer 已支持在累计里程缺失时,用有速度证据的相邻 GPS
回补后的生产容量巡检为 `status=ok`Kafka lag 和各消费重试队列均为 0统计管道缺口为 0。全量诊断覆盖 645 个活跃“协议 × 车辆”项,其中 GB32960 226/226、JT808 371/371、宇通 47/48 已生成日里程;剩余宇通 VIN `LMRKH9AC9R1004121` 当天只有实时位置帧,源头没有总里程字段和可补的可信移动证据,明确归类为 `source_data` 缺口,未伪造里程。
## 7 月 20 日 08:46 增量复核与全量补录
用户再次以沪A01559F、沪A62013F 为样本反馈“轨迹回放有里程、今日里程未计算”。本次通过生产轨迹 API、最终日里程、来源候选与实时字段事件时间重新对账
- 沪A01559F 当天轨迹完整读取 260 个源点、保留 259 个有效点,区间里程、平均速度、最高速度均为 0最终日里程为宇通 `stationary_carry_forward` 0 km。页面中的 14,097 km 是设备累计总里程。
- 沪A62013F 当天轨迹完整读取 271 个源点、保留 259 个有效点,区间里程、平均速度、最高速度均为 0最终日里程为宇通 `stationary_carry_forward` 0 km。页面中的 24,245 km 是设备累计总里程。
- 两车均已生成当日日里程,不存在候选层到最终投影层的缺口;没有可信正向移动证据,禁止补录正数。
随后对 2026-07-20 的 GB32960、JT808、YUTONG_MQTT 先执行全量 dry-run再执行正式幂等回补。正式运行重建并写入 796 个来源聚合、归一化 375 个 JT808 平台来源,识别 5 个本轮可恢复的 JT808 GPS 轨迹聚合;回补后共有 7 个 JT808 GPS 安全候选被最终统计选中,最小值 0.119 km全部满足 100 米业务阈值:
| 车牌 | VIN | 最终日里程 |
| --- | --- | ---: |
| 浙F02268F | `LA9HE60A6PBAF4022` | 0.119 km |
| 粤AGF8492 | `LB9A32A28R0LS1056` | 0.563 km |
| 沪A66216F | `LKLG7C4E1NA774783` | 1.657 km |
| 沪A69826F | `LKLG7C4E2NA774775` | 7.596 km |
| 沪A63305F | `LKLG7C4E3NA774798` | 0.202 km |
| 沪A05697F | `LKLG7C4E6NA774746` | 0.138 km |
| 粤A05776F | `LNXNEGRR6SR319481` | 0.130 km |
回补后诊断覆盖 685 个活跃“协议 × 车辆”项,`pipeline_issue_total=0`GB32960 252/252、JT808 385/385、宇通 47/48 已生成日里程。唯一剩余源数据缺口是宇通 VIN `LMRKH9AC9R1004121`:当天快照有位置、速度等 27 个字段,但没有任何总里程字段,也没有满足质量门槛的移动里程证据,因此保留 `NO_TOTAL_MILEAGE`,不伪造数值。
容量巡检返回 `status=ok`Kafka lag、统计重试队列和待处理批次均为 09 个数据链路 `/readyz` 全部成功,自动回补 timer 继续启用。轨迹点位卡片将设备累计值改为可见的“非区间”标记,避免只能依赖悬停提示而在移动端把累计总里程误读为今日或查询区间里程。
## 限制与后续监控
- GPS 估算只在仪表里程缺失或停滞时兜底,不替代 GB32960、宇通仪表里程或 JT808 设备里程。

View File

@@ -197,7 +197,8 @@ test('renders a map-first replay workspace and connects stop, event, and panel i
expect(view.container.querySelector('.v2-track-current-card.semi-card')).toBeInTheDocument();
expect(view.container.querySelector('.v2-track-current-coverage')).toBeInTheDocument();
expect(view.container.querySelectorAll('.v2-track-current-metrics > span')).toHaveLength(4);
expect(view.container.querySelector('.v2-track-current-metrics')).toHaveTextContent('累计总里程100 km');
expect(view.container.querySelector('.v2-track-current-metrics')).toHaveTextContent('设备累计值非区间100 km');
expect(view.container.querySelector('.v2-track-current-metrics > span:last-child small > b')).toHaveTextContent('非区间');
expect(view.container.querySelector('.v2-track-current-metrics > span:last-child')).toHaveAttribute('title', '当前轨迹点设备上报的累计总里程,不是所选时间窗里程或今日里程');
expect(view.container.querySelector('.v2-track-current-evidence')).toHaveTextContent('JT/T 808时间窗完整3 → 3 点报警 —');
expect(view.container.querySelector('.v2-track-current-evidence .v2-track-protocol-tag')).toHaveClass('semi-tag-cyan-light');

View File

@@ -522,7 +522,10 @@ export default function TrackPage() {
<span><small></small><strong>{number(current?.speedKmh ?? 0)}<em> km/h</em></strong></span>
<span><small></small><strong>{direction(current?.directionDeg)}</strong></span>
<span><small>SOC</small><strong>{current?.socAvailable ? `${number(current.socPercent)}%` : '—'}</strong></span>
<span title="当前轨迹点设备上报的累计总里程,不是所选时间窗里程或今日里程"><small></small><strong>{trackPointMileageAvailable(current) ? number(current?.totalMileageKm ?? 0) : '—'}{trackPointMileageAvailable(current) ? <em> km</em> : null}</strong></span>
<span title="当前轨迹点设备上报的累计总里程,不是所选时间窗里程或今日里程">
<small><b></b></small>
<strong>{trackPointMileageAvailable(current) ? number(current?.totalMileageKm ?? 0) : '—'}{trackPointMileageAvailable(current) ? <em> km</em> : null}</strong>
</span>
</div>
<div className={`v2-track-current-evidence v2-track-current-coverage${track.coverage.complete ? '' : ' is-warning'}`}>
<ProtocolTag className="v2-track-protocol-tag" protocol={current?.protocol} compact />

View File

@@ -1948,10 +1948,25 @@
}
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics small {
display: flex;
min-width: 0;
align-items: center;
gap: 5px;
color: #8491a2;
font-size: 10px;
}
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics small > b {
flex: 0 0 auto;
border-radius: 999px;
background: #edf3fa;
padding: 1px 5px;
color: #536a84;
font-size: 8px;
font-weight: 700;
line-height: 13px;
}
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics strong {
margin-top: 4px;
color: #25384f;
@@ -4419,6 +4434,10 @@
font-size: 8px;
}
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics small > b {
display: none;
}
.v2-track-current-card > .semi-card-body > .v2-track-current-metrics strong {
margin-top: 2px;
font-size: 12px;