chore: commit mileage updates and accumulated project artifacts
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 粤AGP3080 2026-08-11 用氢量差异诊断\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## tl;dr\n",
|
||||
"\n",
|
||||
"- 生产数据库当前值为 **5.992 kg**,分段关键帧合并值为 **12.051 kg**,差 **6.059 kg(+101.12%)**。\n",
|
||||
"- 根因已确认:同一车辆当天使用了3个 `source_endpoint`。现有算法按 `VIN + source_endpoint` 分组后只选择样本最多的一组,导致后续两个连续时间段被舍弃。\n",
|
||||
"- 三个来源片段按现有算法分别得到5.992、0.697、5.349 kg,合计12.038 kg;与分段关键帧结果仅差0.013 kg。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Context & Methods\n",
|
||||
"\n",
|
||||
"诊断对象:粤AGP3080,VIN `LB9A32A25R0LS1452`,2026-08-11(Asia/Shanghai)。\n",
|
||||
"\n",
|
||||
"对比口径:生产表日结果、现有算法按连接来源分组的结果、合并全部来源后按工作段首尾5帧中位数做差的结果。加氢、异常跳变和超过5分钟的数据断点用于切段。\n",
|
||||
"\n",
|
||||
"### Key Assumptions\n",
|
||||
"\n",
|
||||
"三个端口属于同一VIN当天的连接重连,不是三个独立车辆数据源;百公里氢耗采用生产日里程502.4km。\n",
|
||||
"\n",
|
||||
"> 执行状态:生产查询已于2026-08-19完成并复核。本地缺少 `nbformat`、`jupyter` 和可用内核,因此本笔记本保存可复算代码但未在本机执行。复跑命令:`python -m jupyter nbconvert --execute --to notebook --inplace <notebook>`。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Data\n",
|
||||
"\n",
|
||||
"| 来源连接 | 时间范围 | 原始帧 | 工作帧 | 现有算法用氢量 |\n",
|
||||
"|---|---|---:|---:|---:|\n",
|
||||
"| 8.134.95.166:54760 | 06:28:12–12:34:43 | 1542 | 1539 | 5.992kg |\n",
|
||||
"| 8.134.95.166:41692 | 12:35:13–14:38:29 | 328 | 328 | 0.697kg |\n",
|
||||
"| 8.134.95.166:37898 | 14:54:00–19:59:02 | 1357 | 1357 | 5.349kg |\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"mileage_km = 502.4\n",
|
||||
"stored_kg = 5.992\n",
|
||||
"source_consumption_kg = [5.992, 0.697, 5.349]\n",
|
||||
"segment_consumption_kg = [0.462, 2.537, 3.685, 0.000, 2.629, 2.738]\n",
|
||||
"source_sum_kg = round(sum(source_consumption_kg), 3)\n",
|
||||
"proposed_kg = round(sum(segment_consumption_kg), 3)\n",
|
||||
"gap_kg = round(proposed_kg - stored_kg, 3)\n",
|
||||
"gap_pct = round((proposed_kg / stored_kg - 1) * 100, 2)\n",
|
||||
"omitted_sources_kg = round(source_sum_kg - stored_kg, 3)\n",
|
||||
"method_residual_kg = round(proposed_kg - source_sum_kg, 3)\n",
|
||||
"current_rate = round(stored_kg / mileage_km * 100, 3)\n",
|
||||
"proposed_rate = round(proposed_kg / mileage_km * 100, 3)\n",
|
||||
"assert (source_sum_kg, proposed_kg, gap_kg) == (12.038, 12.051, 6.059)\n",
|
||||
"{\n",
|
||||
" '数据库用氢量_kg': stored_kg,\n",
|
||||
" '新算法用氢量_kg': proposed_kg,\n",
|
||||
" '差值_kg': gap_kg,\n",
|
||||
" '差值比例_pct': gap_pct,\n",
|
||||
" '被舍弃来源片段_kg': omitted_sources_kg,\n",
|
||||
" '方法残差_kg': method_residual_kg,\n",
|
||||
" '数据库百公里氢耗': current_rate,\n",
|
||||
" '新算法百公里氢耗': proposed_rate,\n",
|
||||
"}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Takeaways\n",
|
||||
"\n",
|
||||
"1. 差值不是压力换算公式造成的;被舍弃的两个连接片段贡献6.046kg,解释总差值的99.79%。\n",
|
||||
"2. 问题在聚合粒度:`source_endpoint` 包含临时TCP端口,重连会改变端口,不能作为车辆日统计的互斥来源键。\n",
|
||||
"3. 当前生产结果只保留06:28–12:34的数据,12:35–19:59的连续有效工作数据没有进入日用氢量。\n",
|
||||
"4. 建议先按VIN和事件时间合并去重,再识别工作段;来源字段只用于追踪。\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
# 所有车辆 2026-09-15~18 日里程重算
|
||||
|
||||
已完成全部车辆范围的四天重算,覆盖 GB32960、JT808、YUTONG_MQTT,未设置 VIN 筛选。主回补于 2026-09-18 12:55~13:09 依日期顺序执行,随后完成全量结果重建、旧基线补充修正及验证。18 日为进行中的自然日,实时统计已恢复,后续上报继续更新。
|
||||
|
||||
## 范围和结果
|
||||
|
||||
| 日期 | 主回补写入来源统计数 | 有日统计的车辆数 | 协议日统计行数 |
|
||||
|---|---:|---:|---:|
|
||||
| 09-15 | 1,270 | 678 | 1,043 |
|
||||
| 09-16 | 1,270 | 657 | 1,016 |
|
||||
| 09-17 | 1,262 | 667 | 1,032 |
|
||||
| 09-18 | 1,118 | 595 | 927 |
|
||||
|
||||
主回补共写入 4,920 条来源统计,随后用正式投影逻辑重建四天全部 4,018 个 VIN/日期/协议结果。表中数量为验证快照,不是所有授权车辆数量;无上报车辆继续按既有规则结转或返回无数据,不伪造行驶里程。没有删除原始报文。
|
||||
|
||||
## 本轮额外发现并修正的回补问题
|
||||
|
||||
最终检查发现沪A68019F 的 JT808 09-16 日里程被算成 11642.8 km。回补优先使用原始数据查询中的 07-13 基线 10.4 km,忽略同一终端已经持久化的 09-04 基线 11653.1 km。09-16 最新累计 11653.2,正确差值为 0.1 km。
|
||||
|
||||
已修改 stats-backfill:即使内存里找到了历史原始记录,JT808/MQTT 也必须与数据库同来源基线比较,采用较新的记录;保持独立终端隔离。GB32960 继续采用此前修正的车辆里程表衔接规则。
|
||||
|
||||
在第二份备份后,事务内修正 105 条来源记录(JT808 101、MQTT 4),重建 96 个车辆日协议结果。真实累计回退的一条来源标记为 INVALID_DELTA,未强行转为正常。再次核查剩余 5 条候选只涉及基线时间更新,旧/新日里程完全一致,均为正在实时更新的 18 日记录。
|
||||
|
||||
部署版本:`/opt/lingniu-go-native/releases/mileage-durable-baseline-20260918`,更新 stats-backfill 及受控修复工具。实时 Writer 沿用已验证的 GB32960 基线修正版。
|
||||
|
||||
## 样本与验证
|
||||
|
||||
公网单日、区间分页和中台 HTTP 实测:
|
||||
|
||||
- 粤AGH6240:09-08 GB32960 保留 0.2 km;09-15 GB32960 为 0 km,中台默认选择 JT808 的 0.1 km。
|
||||
- 粤AGG5043:09-17 GB32960 为 70.7 km。
|
||||
- 沪A68019F:09-16 JT808 为 0.1 km、NORMAL。
|
||||
- 09-18 两台样本响应通过当日累计减前一天累计的校验;数值会随新数据变化。
|
||||
|
||||
相关回归测试 `go test ./cmd/stats-backfill ./cmd/mileage-baseline-repair ./internal/stats` 通过,包括原始历史较旧时采用持久化较新基线、持久化缺失时保留内存基线的场景。
|
||||
|
||||
数据库检查:负日里程/负累计投影 0;15~17 日日统计与选中来源的日里程差异 0。对 1,024 个车辆绑定执行开放接口 Repository 核对,单日/区间一致性结果见 api-final-15-17.json。18 日会实时变化,采用同一响应内的累计差及真实公网样本验证。
|
||||
|
||||
Writer readyz 正常;三协议全部 Kafka 分区 lag=0,三个 worker 重试队列均为 0。临时测试应用在验证后停用并清除车辆授权。BI 历史缓存未清理。
|
||||
|
||||
## 备份与恢复
|
||||
|
||||
首次备份:`/opt/lingniu-go-native/backups/mileage-all-20260915-18/mileage-tables.sql.gz`,包含四天 4,017 条日统计、5,009 条来源记录;SHA-256:`dd564cef4a72fc7c8658c40dc7aa69c8f185e4c387ab313b6d10708f345b148c`。
|
||||
|
||||
补充修复前备份:`/opt/lingniu-go-native/backups/mileage-all-baseline-20260918/mileage-tables.sql.gz`,包含 4,018 条日统计、4,667 条来源记录;SHA-256:`120675f820f83cb7134d6841e5b5ef36f60d762ef701aa719f6f73ceef1498c8`。
|
||||
|
||||
两份均为限定四天、两张里程统计表的一致性快照,gzip 校验通过。恢复时先在独立库验证,避免覆盖已恢复实时写入后的新增记录。逐日日志、备份清单、部署哈希、HTTP 响应和数据库核对文件保存在本目录。历史原始累计读数未改动。
|
||||
@@ -0,0 +1 @@
|
||||
{"elapsed":"1.608048479s","normal":2063,"platformDailyMismatches":0,"platformRows":1540,"qualities":{"":1540,"CARRIED_FORWARD":523},"reconciliationMismatches":0,"rows":2063,"samples":{},"singleRangeDifferences":0,"vehicles":1024}
|
||||
@@ -0,0 +1 @@
|
||||
{"elapsed":"745.317428ms","normal":687,"platformDailyMismatches":0,"platformRows":517,"qualities":{"":517,"CARRIED_FORWARD":170},"reconciliationMismatches":0,"rows":687,"samples":{},"singleRangeDifferences":0,"vehicles":1024}
|
||||
@@ -0,0 +1 @@
|
||||
{"elapsed":"753.038173ms","normal":688,"platformDailyMismatches":0,"platformRows":504,"qualities":{"":504,"CARRIED_FORWARD":184},"reconciliationMismatches":0,"rows":688,"samples":{},"singleRangeDifferences":0,"vehicles":1024}
|
||||
@@ -0,0 +1 @@
|
||||
{"elapsed":"1.59126523s","normal":2063,"platformDailyMismatches":0,"platformRows":1540,"qualities":{"":1540,"CARRIED_FORWARD":523},"reconciliationMismatches":0,"rows":2063,"samples":{},"singleRangeDifferences":0,"vehicles":1024}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"started_at": "2026-09-18T12:55:31+0800",
|
||||
"tables": {
|
||||
"vehicle_daily_mileage": 4017,
|
||||
"vehicle_daily_mileage_source": 5009
|
||||
},
|
||||
"database": "lingniu_vehicle_data",
|
||||
"sha256": "dd564cef4a72fc7c8658c40dc7aa69c8f185e4c387ab313b6d10708f345b148c",
|
||||
"bytes": 292476,
|
||||
"finished_at": "2026-09-18T12:55:31+0800"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Consistent logical backup of the two mileage tables; no DB writes."""
|
||||
import gzip, hashlib, json, os, pathlib, time
|
||||
import sys;sys.path.insert(0,"/tmp/codex-mileage-diag-pymysql.zip");sys.path.insert(0,"/tmp")
|
||||
import pymysql
|
||||
from db_access import connect
|
||||
os.umask(0o077)
|
||||
root=pathlib.Path('/opt/lingniu-go-native/backups/mileage-all-20260915-18')
|
||||
root.mkdir(parents=True,exist_ok=True)
|
||||
path=root/'mileage-tables.sql.gz'
|
||||
if path.exists(): raise RuntimeError('Backup already exists; do not overwrite')
|
||||
c=connect(); manifest={'started_at':time.strftime('%Y-%m-%dT%H:%M:%S%z'),'tables':{}}
|
||||
with c.cursor() as q:
|
||||
q.execute('SELECT DATABASE() AS db'); db=q.fetchone()['db'];manifest['database']=db
|
||||
q.execute('SHOW FULL TABLES'); entries=[r for r in q.fetchall() if list(r.values())[0] in ('vehicle_daily_mileage_source','vehicle_daily_mileage')]
|
||||
tables=[list(r.values())[0] for r in entries if list(r.values())[1]=='BASE TABLE']
|
||||
triggers=[]
|
||||
q.execute('START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY')
|
||||
def literal(v):
|
||||
if isinstance(v,bytes): return '0x'+v.hex()
|
||||
return c.escape(v)
|
||||
with gzip.open(str(path)+'.partial','wt',encoding='utf-8',compresslevel=3) as f:
|
||||
f.write('SET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS=0;\n')
|
||||
for table in tables:
|
||||
q.execute('SHOW CREATE TABLE `'+table+'`');ddl=list(q.fetchone().values())[1]
|
||||
f.write("DELETE FROM `"+table+"` WHERE stat_date BETWEEN '2026-09-15' AND '2026-09-18';\n")
|
||||
q.execute('SHOW COLUMNS FROM `'+table+'`');cols=[r['Field'] for r in q.fetchall() if 'GENERATED' not in r['Extra']]
|
||||
names=','.join('`'+v+'`' for v in cols);count=0
|
||||
with c.cursor(pymysql.cursors.SSCursor) as stream:
|
||||
stream.execute("SELECT "+names+" FROM `"+table+"` WHERE stat_date BETWEEN '2026-09-15' AND '2026-09-18'")
|
||||
while True:
|
||||
rows=stream.fetchmany(500)
|
||||
if not rows:break
|
||||
f.write('INSERT INTO `'+table+'` ('+names+') VALUES\n'+',\n'.join('('+','.join(literal(v) for v in row)+')' for row in rows)+';\n')
|
||||
count+=len(rows)
|
||||
manifest['tables'][table]=count
|
||||
print(json.dumps({'table':table,'rows':count}),flush=True)
|
||||
for row in entries:
|
||||
if list(row.values())[1]=='VIEW':
|
||||
name=list(row.values())[0];q.execute('SHOW CREATE VIEW `'+name+'`');ddl=q.fetchone()['Create View'];f.write(ddl+';\n')
|
||||
for trigger in triggers:
|
||||
q.execute('SHOW CREATE TRIGGER `'+trigger['Trigger']+'`');ddl=q.fetchone()['SQL Original Statement'];f.write('DELIMITER ;;\n'+ddl+';;\nDELIMITER ;\n')
|
||||
f.write('SET FOREIGN_KEY_CHECKS=1;\n')
|
||||
c.rollback();c.close()
|
||||
os.rename(str(path)+'.partial',path)
|
||||
h=hashlib.sha256()
|
||||
with path.open('rb') as f:
|
||||
for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
|
||||
manifest.update(sha256=h.hexdigest(),bytes=path.stat().st_size,finished_at=time.strftime('%Y-%m-%dT%H:%M:%S%z'))
|
||||
(root/'backup-manifest.json').write_text(json.dumps(manifest,indent=2))
|
||||
print(json.dumps({'complete':True,'bytes':manifest['bytes'],'sha256':manifest['sha256']}),flush=True)
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"started_at": "2026-09-18T13:16:11+0800",
|
||||
"tables": {
|
||||
"vehicle_daily_mileage": 4018,
|
||||
"vehicle_daily_mileage_source": 4667
|
||||
},
|
||||
"database": "lingniu_vehicle_data",
|
||||
"sha256": "120675f820f83cb7134d6841e5b5ef36f60d762ef701aa719f6f73ceef1498c8",
|
||||
"bytes": 279153,
|
||||
"finished_at": "2026-09-18T13:16:11+0800"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Consistent logical backup of the two mileage tables; no DB writes."""
|
||||
import gzip, hashlib, json, os, pathlib, time
|
||||
import sys;sys.path.insert(0,"/tmp/codex-mileage-diag-pymysql.zip");sys.path.insert(0,"/tmp")
|
||||
import pymysql
|
||||
from db_access import connect
|
||||
os.umask(0o077)
|
||||
root=pathlib.Path('/opt/lingniu-go-native/backups/mileage-all-baseline-20260918')
|
||||
root.mkdir(parents=True,exist_ok=True)
|
||||
path=root/'mileage-tables.sql.gz'
|
||||
if path.exists(): raise RuntimeError('Backup already exists; do not overwrite')
|
||||
c=connect(); manifest={'started_at':time.strftime('%Y-%m-%dT%H:%M:%S%z'),'tables':{}}
|
||||
with c.cursor() as q:
|
||||
q.execute('SELECT DATABASE() AS db'); db=q.fetchone()['db'];manifest['database']=db
|
||||
q.execute('SHOW FULL TABLES'); entries=[r for r in q.fetchall() if list(r.values())[0] in ('vehicle_daily_mileage_source','vehicle_daily_mileage')]
|
||||
tables=[list(r.values())[0] for r in entries if list(r.values())[1]=='BASE TABLE']
|
||||
triggers=[]
|
||||
q.execute('START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY')
|
||||
def literal(v):
|
||||
if isinstance(v,bytes): return '0x'+v.hex()
|
||||
return c.escape(v)
|
||||
with gzip.open(str(path)+'.partial','wt',encoding='utf-8',compresslevel=3) as f:
|
||||
f.write('SET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS=0;\n')
|
||||
for table in tables:
|
||||
q.execute('SHOW CREATE TABLE `'+table+'`');ddl=list(q.fetchone().values())[1]
|
||||
f.write("DELETE FROM `"+table+"` WHERE stat_date BETWEEN '2026-09-15' AND '2026-09-18';\n")
|
||||
q.execute('SHOW COLUMNS FROM `'+table+'`');cols=[r['Field'] for r in q.fetchall() if 'GENERATED' not in r['Extra']]
|
||||
names=','.join('`'+v+'`' for v in cols);count=0
|
||||
with c.cursor(pymysql.cursors.SSCursor) as stream:
|
||||
stream.execute("SELECT "+names+" FROM `"+table+"` WHERE stat_date BETWEEN '2026-09-15' AND '2026-09-18'")
|
||||
while True:
|
||||
rows=stream.fetchmany(500)
|
||||
if not rows:break
|
||||
f.write('INSERT INTO `'+table+'` ('+names+') VALUES\n'+',\n'.join('('+','.join(literal(v) for v in row)+')' for row in rows)+';\n')
|
||||
count+=len(rows)
|
||||
manifest['tables'][table]=count
|
||||
print(json.dumps({'table':table,'rows':count}),flush=True)
|
||||
for row in entries:
|
||||
if list(row.values())[1]=='VIEW':
|
||||
name=list(row.values())[0];q.execute('SHOW CREATE VIEW `'+name+'`');ddl=q.fetchone()['Create View'];f.write(ddl+';\n')
|
||||
for trigger in triggers:
|
||||
q.execute('SHOW CREATE TRIGGER `'+trigger['Trigger']+'`');ddl=q.fetchone()['SQL Original Statement'];f.write('DELIMITER ;;\n'+ddl+';;\nDELIMITER ;\n')
|
||||
f.write('SET FOREIGN_KEY_CHECKS=1;\n')
|
||||
c.rollback();c.close()
|
||||
os.rename(str(path)+'.partial',path)
|
||||
h=hashlib.sha256()
|
||||
with path.open('rb') as f:
|
||||
for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
|
||||
manifest.update(sha256=h.hexdigest(),bytes=path.stat().st_size,finished_at=time.strftime('%Y-%m-%dT%H:%M:%S%z'))
|
||||
(root/'backup-manifest.json').write_text(json.dumps(manifest,indent=2))
|
||||
print(json.dumps({'complete':True,'bytes':manifest['bytes'],'sha256':manifest['sha256']}),flush=True)
|
||||
@@ -0,0 +1 @@
|
||||
{"scanned": 4056, "changes": 105, "byProtocol": {"JT808": 101, "YUTONG_MQTT": 4}, "invalid": 1, "examples": [{"vin": "LMRKH9AC8R1004109", "date": "2026-09-16", "protocol": "JT808", "old": "11642.800", "new": "0.100"}, {"vin": "LMRKH9AC8R1004109", "date": "2026-09-16", "protocol": "JT808", "old": "0.000", "new": "0.100"}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"scanned": 4052, "changes": 5, "byProtocol": {"JT808": 5}, "invalid": 0, "examples": []}
|
||||
@@ -0,0 +1 @@
|
||||
{"committed":true,"reprojectedDays":96,"updatedSources":105}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"release": "mileage-durable-baseline-20260918", "previous": "/opt/lingniu-go-native/releases/mileage-baseline-reset-20260918", "hashes": {"stats-backfill": "4873eff64f0c5e1d86f9502688ee6699da2189a17496ce551d086535947af578", "mileage-baseline-repair": "77102609e4403232bf8e207d10aa9265914785c4bd8f867cce15834741aca60c"}, "readyz": {"checks": {"mysql": {"status": "ok"}}, "service": "vehicle-stat-writer", "status": "ok", "time": "2026-09-18T05:16:57Z"}, "restoredServices": {"lingniu-go-stat-writer.service": true, "lingniu-go-stats-backfill.timer": true, "lingniu-vehicle-open-stat.timer": true}}
|
||||
@@ -0,0 +1,52 @@
|
||||
# coding: utf-8
|
||||
import subprocess,json,urllib.request,urllib.error,datetime
|
||||
pid=subprocess.check_output(['systemctl','show','-p','MainPID','lingniu-vehicle-platform'],universal_newlines=True).strip().split('=',1)[1]
|
||||
env=dict(x.decode().split('=',1) for x in open('/proc/'+pid+'/environ','rb').read().split(b'\0') if b'=' in x)
|
||||
admin_token=next(x['token'] for x in json.loads(env['AUTH_TOKENS_JSON']) if x.get('role')=='admin')
|
||||
def call(base,path,token,data=None,method=None):
|
||||
req=urllib.request.Request(base+path,data=None if data is None else json.dumps(data).encode(),headers={'Authorization':'Bearer '+token,'Content-Type':'application/json'},method=method)
|
||||
with urllib.request.urlopen(req,timeout=25) as r:return json.load(r)
|
||||
def admin(path,data=None,method=None):
|
||||
return call('http://127.0.0.1:20300','/api/v2/open-platform/apps'+path,admin_token,data,method)['data']
|
||||
|
||||
expiry=(datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(days=1)).isoformat()
|
||||
app_input=dict(name='release-smoke-mileage-all-20260915-18',status='enabled',validFrom='2026-09-08T00:00:00+08:00',validTo=expiry)
|
||||
app=admin('',app_input,'POST');aid=str(app['id']);key=app['appKey'];out={'temporaryAppId':app['id']}
|
||||
try:
|
||||
admin('/'+aid+'/vehicles',{'vehicles':[dict(vin=v,validFrom=app_input['validFrom'],validTo=expiry) for v in ['LB9A32A26R0LS1055','LB9A32A29R0LS1065','LMRKH9AC8R1004109']]},'PUT')
|
||||
out['single']=[];out['platform']=[]
|
||||
for plate,vin,day,want in [('粤AGH6240','LB9A32A26R0LS1055','2026-09-08',0.2),('粤AGH6240','LB9A32A26R0LS1055','2026-09-15',0),('粤AGG5043','LB9A32A29R0LS1065','2026-09-17',70.7)]:
|
||||
body={'date':day,'plateNumbers':[plate],'protocolPriority':['GB32960']}
|
||||
single=call('https://open.d.lnoneos.com','/api/v1/vehicles/mileage/query',key,body)
|
||||
item=single['data'][0]
|
||||
assert item['dailyMileageKm']==want and item['status']=='NORMAL' and not item.get('dataQuality'),single
|
||||
out['single'].append(single)
|
||||
path='/api/mileage/daily?vin='+vin+'&dateFrom='+day+'&dateTo='+day+'&protocol=GB32960&limit=10'
|
||||
platform=call('http://127.0.0.1:20300',path,admin_token)
|
||||
out['platform'].append(platform)
|
||||
query={'startDate':'2026-09-08','endDate':'2026-09-17','plateNumbers':['粤AGH6240'],'protocolPriority':['GB32960'],'pageSize':3}
|
||||
pages=[]
|
||||
for i in range(5):
|
||||
page=call('https://open.d.lnoneos.com','/api/v1/vehicles/mileage/range/query',key,query);pages.append(page)
|
||||
if not page.get('nextCursor'):break
|
||||
query['cursor']=page['nextCursor']
|
||||
else:raise AssertionError('pagination did not complete')
|
||||
rows=[x for page in pages for x in page['data']]
|
||||
assert len(rows)==10,rows
|
||||
target=next(x for x in rows if x['date']=='2026-09-15')
|
||||
assert target['dailyMileageKm']==0 and target['status']=='NORMAL',target
|
||||
assert next(x for x in rows if x['date']=='2026-09-08')['dailyMileageKm']==0.2
|
||||
out['rangePages']=pages
|
||||
out['correctedJT808']=call('https://open.d.lnoneos.com','/api/v1/vehicles/mileage/query',key,{'date':'2026-09-16','plateNumbers':['沪A68019F'],'protocolPriority':['JT808']})
|
||||
assert out['correctedJT808']['data'][0]['dailyMileageKm']==0.1 and out['correctedJT808']['data'][0]['status']=='NORMAL'
|
||||
out['today']=call('https://open.d.lnoneos.com','/api/v1/vehicles/mileage/query',key,{'date':'2026-09-18','plateNumbers':['粤AGH6240','粤AGG5043'],'protocolPriority':['GB32960']})
|
||||
baselines={'LB9A32A26R0LS1055':75453.7,'LB9A32A29R0LS1065':42077}
|
||||
for item in out['today']['data']:
|
||||
assert item['status']=='NORMAL' and item['dailyMileageKm']>=0,item
|
||||
assert abs(item['totalMileageKm']-baselines[item['vin']]-item['dailyMileageKm'])<0.001,item
|
||||
out['platformDefault']=call('http://127.0.0.1:20300','/api/mileage/daily?vin=LB9A32A26R0LS1055&dateFrom=2026-09-15&dateTo=2026-09-15&deduplicate=1&limit=10',admin_token)
|
||||
finally:
|
||||
app_input['status']='disabled';disabled=admin('/'+aid,app_input,'PUT');assert disabled['status']=='disabled'
|
||||
admin('/'+aid+'/vehicles',{'vehicles':[]},'PUT')
|
||||
out['temporaryAppDisabled']=True;out['temporaryGrantsRemoved']=True
|
||||
print(json.dumps(out))
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
[{"stat_date": "2026-09-04", "source_key": "JT808:64341233694@PLATFORM:dongfang_beidou", "latest_total_mileage_km": "11653.100", "daily_mileage_km": "0.000", "latest_event_time": "2026-09-04 15:13:15", "is_selected": 1}, {"stat_date": "2026-09-04", "source_key": "JT808:64341233694@PLATFORM:g7s", "latest_total_mileage_km": "11653.100", "daily_mileage_km": "0.000", "latest_event_time": "2026-09-04 15:08:19", "is_selected": 0}, {"stat_date": "2026-09-16", "source_key": "JT808:64341233694@PLATFORM:dongfang_beidou", "latest_total_mileage_km": "11653.200", "daily_mileage_km": "11642.800", "latest_event_time": "2026-09-16 12:56:12", "is_selected": 1}, {"stat_date": "2026-09-16", "source_key": "JT808:64341233694@PLATFORM:g7s", "latest_total_mileage_km": "11653.200", "daily_mileage_km": "0.000", "latest_event_time": "2026-09-16 12:51:36", "is_selected": 0}]
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"scope": {
|
||||
"reprojectDays": 4018
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"plate": "沪A68019F",
|
||||
"vin": "LMRKH9AC8R1004109",
|
||||
"stat_date": "2026-09-16",
|
||||
"protocol": "JT808",
|
||||
"source_key": "JT808:64341233694@PLATFORM:dongfang_beidou",
|
||||
"first_event_time": "2026-07-13 09:54:36",
|
||||
"latest_event_time": "2026-09-16 12:56:12",
|
||||
"first_total_mileage_km": "10.400",
|
||||
"latest_total_mileage_km": "11653.200",
|
||||
"daily_mileage_km": "11642.800",
|
||||
"quality_reason": "historical_source_baseline"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import sys,pathlib,json,decimal,datetime
|
||||
sys.path.insert(0,'/tmp/codex-mileage-diag-pymysql.zip');sys.path.insert(0,'/tmp')
|
||||
from db_access import connect
|
||||
root=pathlib.Path('/opt/lingniu-go-native/backups/mileage-all-baseline-20260918');root.mkdir(parents=True,exist_ok=True)
|
||||
c=connect();D=decimal.Decimal;changes=[]
|
||||
with c.cursor() as q:
|
||||
q.execute('START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY')
|
||||
q.execute("""SELECT * FROM vehicle_daily_mileage_source WHERE stat_date BETWEEN '2026-09-15' AND '2026-09-18'
|
||||
AND quality_status='OK' AND latest_total_mileage_km>0 AND first_event_time IS NOT NULL
|
||||
AND source_ip NOT IN ('legacy-mysql.lingniu-prod','manual-lingniu-prod-day-mileage')
|
||||
AND COALESCE(quality_reason,'')<>'gps_coordinate_accumulation'
|
||||
AND latest_event_time>=TIMESTAMP(stat_date) AND latest_event_time<TIMESTAMP(stat_date)+INTERVAL 1 DAY
|
||||
ORDER BY stat_date,vin,protocol,source_key""")
|
||||
rows=q.fetchall()
|
||||
for r in rows:
|
||||
selector='source_key=%s';order='stat_date DESC,latest_event_time DESC';extra=''
|
||||
if r['protocol']=='GB32960':
|
||||
selector='(source_key=%s OR is_selected=1)';order='stat_date DESC,is_selected DESC,latest_event_time DESC,source_key ASC'
|
||||
extra=" AND quality_status='OK' AND COALESCE(quality_reason,'')<>'gps_coordinate_accumulation' AND source_ip NOT IN ('legacy-mysql.lingniu-prod','manual-lingniu-prod-day-mileage')"
|
||||
q.execute("SELECT latest_total_mileage_km,latest_event_time,source_key FROM vehicle_daily_mileage_source WHERE vin=%s AND stat_date<%s AND protocol=%s AND "+selector+" AND latest_total_mileage_km>0 AND latest_event_time>=TIMESTAMP(stat_date) AND latest_event_time<TIMESTAMP(stat_date)+INTERVAL 1 DAY "+extra+" ORDER BY "+order+" LIMIT 1",(r['vin'],r['stat_date'],r['protocol'],r['source_key']))
|
||||
p=q.fetchone()
|
||||
if not p:continue
|
||||
if p['latest_event_time']<=r['first_event_time'] and r['first_event_time'].date()<r['stat_date']:continue
|
||||
delta=r['latest_total_mileage_km']-p['latest_total_mileage_km'];status='OK';reason='historical_source_baseline'
|
||||
if D(-1)<=delta<0:delta=D(0);reason='negative_jitter_clamped'
|
||||
elif delta<0:status='INVALID_DELTA';reason='TOTAL_MILEAGE_ROLLBACK'
|
||||
elif delta>D(2500*max(1,(r['latest_event_time'].date()-p['latest_event_time'].date()).days)):status='INVALID_DELTA';reason='outside_daily_range'
|
||||
changes.append(dict(before=r,baseline=dict(total=p['latest_total_mileage_km'],time=p['latest_event_time']),daily=delta,status=status,reason=reason))
|
||||
c.rollback();c.close()
|
||||
(root/'plan.json').write_text(json.dumps(changes,default=str))
|
||||
from collections import Counter
|
||||
print(json.dumps({'scanned':len(rows),'changes':len(changes),'byProtocol':dict(Counter(x['before']['protocol'] for x in changes)),'invalid':sum(x['status']!='OK' for x in changes),'examples':[dict(vin=x['before']['vin'],date=x['before']['stat_date'],protocol=x['before']['protocol'],old=x['before']['daily_mileage_km'],new=x['daily']) for x in changes if x['before']['vin']=='LMRKH9AC8R1004109']},default=str))
|
||||
@@ -0,0 +1,9 @@
|
||||
projected=500/4018
|
||||
projected=1000/4018
|
||||
projected=1500/4018
|
||||
projected=2000/4018
|
||||
projected=2500/4018
|
||||
projected=3000/4018
|
||||
projected=3500/4018
|
||||
projected=4000/4018
|
||||
completed=4018
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"startedAt": "2026-09-18T12:55:28.783046",
|
||||
"days": [
|
||||
"2026-09-15",
|
||||
"2026-09-16",
|
||||
"2026-09-17",
|
||||
"2026-09-18"
|
||||
],
|
||||
"release": "/opt/lingniu-go-native/releases/mileage-baseline-reset-20260918",
|
||||
"complete": true,
|
||||
"finishedAt": "2026-09-18T13:09:44.669901"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import subprocess,pathlib,os,json,datetime
|
||||
root=pathlib.Path('/opt/lingniu-go-native/backups/mileage-all-20260915-18');root.mkdir(parents=True,exist_ok=True)
|
||||
os.umask(0o077)
|
||||
units=['lingniu-go-stat-writer.service','lingniu-go-stats-backfill.timer','lingniu-vehicle-open-stat.timer']
|
||||
state={u:subprocess.call(['systemctl','is-active','--quiet',u])==0 for u in units}
|
||||
assert subprocess.call(['systemctl','is-active','--quiet','lingniu-go-stats-backfill.service'])!=0,'Backfill running'
|
||||
(root/'service-state.json').write_text(json.dumps(state))
|
||||
report={'startedAt':datetime.datetime.now().isoformat(),'days':[],'release':str(pathlib.Path('/opt/lingniu-go-native/current').resolve())}
|
||||
try:
|
||||
subprocess.check_call(['systemctl','stop']+units)
|
||||
subprocess.check_call(['python3','/tmp/mileage-all-backup-20260918.py'])
|
||||
subprocess.check_call(['gzip','-t',str(root/'mileage-tables.sql.gz')])
|
||||
for day in ['2026-09-15','2026-09-16','2026-09-17','2026-09-18']:
|
||||
env=dict(os.environ)
|
||||
env.update(BACKFILL_DATE_FROM=day,BACKFILL_DATE_TO=day,BACKFILL_PROTOCOLS='GB32960,JT808,YUTONG_MQTT',BACKFILL_VINS='',BACKFILL_METHOD='last_diff',BACKFILL_DRY_RUN='false',BACKFILL_RESET='false',BACKFILL_LIMIT='0',BACKFILL_GPS_FALLBACK='true',BACKFILL_BASELINE_LOOKBACK_DAYS='7')
|
||||
print('START '+day,flush=True)
|
||||
log=root/(day+'.log')
|
||||
with log.open('w') as f:
|
||||
result=subprocess.run(['/opt/lingniu-go-native/current/stats-backfill'],env=env,stdout=f,stderr=subprocess.STDOUT,timeout=2700)
|
||||
assert result.returncode==0,'Failed '+day+', see '+str(log)
|
||||
report['days'].append(day);(root/'progress.json').write_text(json.dumps(report))
|
||||
print('DONE '+day,flush=True)
|
||||
report['complete']=True
|
||||
finally:
|
||||
for unit,active in state.items():
|
||||
if active:subprocess.check_call(['systemctl','start',unit])
|
||||
report['finishedAt']=datetime.datetime.now().isoformat()
|
||||
(root/'result.json').write_text(json.dumps(report,indent=2))
|
||||
print(json.dumps(report),flush=True)
|
||||
@@ -0,0 +1,22 @@
|
||||
# coding: utf-8
|
||||
import sys,json,pathlib,urllib.request,subprocess
|
||||
sys.path.insert(0,'/tmp/codex-mileage-diag-pymysql.zip');sys.path.insert(0,'/tmp')
|
||||
from db_access import connect
|
||||
c=connect();out={}
|
||||
with c.cursor() as q:
|
||||
q.execute('START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY')
|
||||
for name,sql in {
|
||||
'byDate':"SELECT stat_date,COUNT(DISTINCT vin) vehicles,COUNT(*) protocolRows,MIN(updated_at) earliestUpdated,MAX(updated_at) latestUpdated FROM vehicle_daily_mileage WHERE stat_date BETWEEN '2026-09-15' AND '2026-09-18' GROUP BY stat_date",
|
||||
'byProtocol':"SELECT stat_date,protocol,COUNT(*) rowsCount,MIN(daily_mileage_km) minimumKm,MAX(daily_mileage_km) maximumKm FROM vehicle_daily_mileage WHERE stat_date BETWEEN '2026-09-15' AND '2026-09-18' GROUP BY stat_date,protocol",
|
||||
'invalidProjections':"SELECT COUNT(*) n FROM vehicle_daily_mileage WHERE stat_date BETWEEN '2026-09-15' AND '2026-09-18' AND (daily_mileage_km<0 OR latest_total_mileage_km<0)",
|
||||
'historicalProjectionMismatch':"SELECT COUNT(*) n FROM vehicle_daily_mileage m LEFT JOIN vehicle_daily_mileage_source s ON s.vin=m.vin AND s.stat_date=m.stat_date AND s.protocol=m.protocol AND s.is_selected=1 WHERE m.stat_date BETWEEN '2026-09-15' AND '2026-09-17' AND (s.vin IS NULL OR m.daily_mileage_km<>s.daily_mileage_km)",
|
||||
'samples':"SELECT b.plate,m.stat_date,m.protocol,m.daily_mileage_km,m.latest_total_mileage_km,m.updated_at FROM vehicle_daily_mileage m JOIN vehicle_identity_binding b ON b.vin=m.vin WHERE b.plate IN ('粤AGH6240','粤AGG5043') AND m.stat_date BETWEEN '2026-09-15' AND '2026-09-18' ORDER BY b.plate,m.stat_date,m.protocol"
|
||||
}.items():
|
||||
q.execute(sql);out[name]=q.fetchall()
|
||||
c.rollback();c.close()
|
||||
out['readyz']=json.load(urllib.request.urlopen('http://127.0.0.1:20213/readyz',timeout=10))
|
||||
metrics=urllib.request.urlopen('http://127.0.0.1:20213/metrics',timeout=10).read().decode()
|
||||
out['metrics']=[x for x in metrics.splitlines() if x.startswith(('vehicle_stat_kafka_lag{','vehicle_stat_retry_pending_messages{','vehicle_stat_worker_active{'))]
|
||||
print(json.dumps(out,default=str))
|
||||
assert out['invalidProjections'][0]['n']==0
|
||||
assert out['historicalProjectionMismatch'][0]['n']==0
|
||||
@@ -0,0 +1,52 @@
|
||||
# GB32960 旧来源基线重复累计修复与数据重算 — 2026-09-18
|
||||
|
||||
已部署 `mileage-baseline-reset-20260918`,修复实时统计和历史回补的基线选取,并在备份后重算受影响数据。
|
||||
|
||||
## 线上结果
|
||||
|
||||
| 车辆 | 日期 | 协议 | 修复后日里程 km | 说明 |
|
||||
|---|---|---|---:|---|
|
||||
| 粤AGH6240 | 2026-09-08 | GB32960 | 0.2 | 原记录保留 |
|
||||
| 粤AGH6240 | 2026-09-15 | GB32960 | 0 | 原 35042.9;改用 09-09 的 75429.6 基线,日末仍为 75429.6 |
|
||||
| 粤AGH6240 | 2026-09-15 | JT808 | 0.1 | 未修改;中台默认去重选择该非零来源,因此默认页面结果为 0.1 |
|
||||
| 粤AGG5043 | 2026-09-17 | GB32960 | 70.7 | 原 308.2 包含重复累计;42077 − 42006.3 = 70.7 |
|
||||
|
||||
这些结果已通过中台 HTTP 查询和公网开放接口验证;公网单日及区间分页都返回 NORMAL。此前将粤AGG5043 的 308.2 判为正确的结论已被本次完整基线核查纠正。
|
||||
|
||||
## 代码与规则
|
||||
|
||||
- GB32960 的总里程以 VIN 对应的车辆里程表为基准。旧转发来源恢复时,查找该车此前最新日期的有效来源记录(同来源或中台已选中的来源),同日优先采用已选中记录,避免因 IP 不同退回几个月前的基线。
|
||||
- 实时 Writer 和 stats-backfill 共用修正后的数据库基线查询。回补同时检查窗口前的跨转发来源历史,与持久化日统计比较后采用较新的自然日基线。
|
||||
- JT808、MQTT 保留原来的来源隔离。GPS 估算、协议未确认的历史导入和质量不通过的记录,不作为 GB32960 跨来源基线。
|
||||
- 保留累计回退、负值抖动和按实际统计窗口计算的异常上限检查。
|
||||
- 新增受控修复工具 mileage-baseline-repair:默认只读计划;应用时要求备份清单,逐条核验旧值和生产基线,在同一事务中更新来源记录及正式日统计,发现并发变化即回滚。
|
||||
|
||||
## 重算范围与备份
|
||||
|
||||
扫描 33,681 条有终端证据的 GB32960 来源记录,修正 304 辆车、1,268 条来源记录,重建 1,263 个车辆日结果。第一批 1,265 条,重投影后发现另外 3 条选源关联基线,一并修正;再次检查剩余候选为 0。没有改动原始报文或累计读数。
|
||||
|
||||
操作期间暂停统计 Writer 和相关定时任务,备份、重算及发布完成后已恢复。
|
||||
|
||||
备份目录:`/opt/lingniu-go-native/backups/mileage-baseline-reset-20260918`。
|
||||
|
||||
- `mileage-tables.sql.gz`:两张里程统计表的一致性快照,32,438,655 bytes,gzip 完整性校验通过。
|
||||
- vehicle_daily_mileage:1,462,363 行;vehicle_daily_mileage_source:1,646,422 行。
|
||||
- SHA-256:`f918217fa1d613fe6d70ef8fee3f8413ca8320f9d3cee8c1e6942a0d850898b2`。
|
||||
- `apply-plan-0.json`、`apply-plan-1.json` 保留逐行修改前内容与新基线。
|
||||
- 需要恢复时应在独立库还原备份并按受影响行清单恢复,不直接覆盖已恢复实时写入的整张生产表。
|
||||
|
||||
## 验证与已知限制
|
||||
|
||||
- `go test ./internal/stats ./cmd/stats-backfill ./cmd/stat-writer ./cmd/mileage-reproject` 通过。
|
||||
- 新增返回旧来源的 35042.9 重复累计、来源切换增量 70.7、累计回退仍异常、JT808 来源隔离等回归。
|
||||
- 全量 `go test ./...` 中,未修改的 internal/identity 包测试 `TestJT808RegistrationProjectorThrottlesLocationOnlyAfterPersist` 失败(throttled facts=1,预期 0),单独重试同样失败。该包未在本次更改或部署,不将全量测试报告为通过。
|
||||
- 生产 Repository 验证覆盖 1,024 辆车、09-08~09-17 共 6,867 条结果,平台日统计差异 0、单日/区间差异 0;3 条 outside_daily_range 异常仍保留。
|
||||
- 公网与中台 HTTP 实际响应保存在 http-verification.json;临时测试应用 75 已停用并清除车辆授权。
|
||||
- 新 Writer readyz 正常,运行二进制 SHA 与发布产物一致;三协议各分区 Kafka lag=0,三个 worker 重试队列均为 0,发布后无 error 级日志。
|
||||
- 中台默认“非零优先”的跨协议选择规则未改变,因此应在相同协议下比较中台与开放接口。BI 历史缓存未在本次清理。
|
||||
|
||||
## 发布
|
||||
|
||||
新目录:`/opt/lingniu-go-native/releases/mileage-baseline-reset-20260918`。
|
||||
旧目录:`/opt/lingniu-go-native/releases/mileage-reconciliation-20260916`。
|
||||
更新 stat-writer、stats-backfill,并附带修复工具;其他运行组件沿用原二进制。开放 API 继续使用已发布的中台日统计对齐逻辑,无须再次修改车辆日里程响应代码。
|
||||
@@ -0,0 +1 @@
|
||||
{"elapsed":"4.31959758s","normal":6864,"platformDailyMismatches":0,"platformRows":4915,"qualities":{"":4925,"CARRIED_FORWARD":1939,"outside_daily_range":3},"reconciliationMismatches":0,"rows":6867,"samples":{},"singleRangeDifferences":0,"vehicles":1024}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"started_at": "2026-09-18T11:57:06+0800",
|
||||
"tables": {
|
||||
"vehicle_daily_mileage": 1462363,
|
||||
"vehicle_daily_mileage_source": 1646422
|
||||
},
|
||||
"database": "lingniu_vehicle_data",
|
||||
"sha256": "f918217fa1d613fe6d70ef8fee3f8413ca8320f9d3cee8c1e6942a0d850898b2",
|
||||
"bytes": 32438655,
|
||||
"finished_at": "2026-09-18T12:00:11+0800"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Consistent logical backup of the two mileage tables; no DB writes."""
|
||||
import gzip, hashlib, json, os, pathlib, time
|
||||
import sys;sys.path.insert(0,"/tmp/codex-mileage-diag-pymysql.zip");sys.path.insert(0,"/tmp")
|
||||
import pymysql
|
||||
from db_access import connect
|
||||
os.umask(0o077)
|
||||
root=pathlib.Path('/opt/lingniu-go-native/backups/mileage-baseline-reset-20260918')
|
||||
root.mkdir(parents=True,exist_ok=True)
|
||||
path=root/'mileage-tables.sql.gz'
|
||||
if path.exists(): raise RuntimeError('Backup already exists; do not overwrite')
|
||||
c=connect(); manifest={'started_at':time.strftime('%Y-%m-%dT%H:%M:%S%z'),'tables':{}}
|
||||
with c.cursor() as q:
|
||||
q.execute('SELECT DATABASE() AS db'); db=q.fetchone()['db'];manifest['database']=db
|
||||
q.execute('SHOW FULL TABLES'); entries=[r for r in q.fetchall() if list(r.values())[0] in ('vehicle_daily_mileage_source','vehicle_daily_mileage')]
|
||||
tables=[list(r.values())[0] for r in entries if list(r.values())[1]=='BASE TABLE']
|
||||
triggers=[]
|
||||
q.execute('START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY')
|
||||
def literal(v):
|
||||
if isinstance(v,bytes): return '0x'+v.hex()
|
||||
return c.escape(v)
|
||||
with gzip.open(str(path)+'.partial','wt',encoding='utf-8',compresslevel=3) as f:
|
||||
f.write('SET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS=0;\n')
|
||||
for table in tables:
|
||||
q.execute('SHOW CREATE TABLE `'+table+'`');ddl=list(q.fetchone().values())[1]
|
||||
f.write('DROP TABLE IF EXISTS `'+table+'`;\n'+ddl+';\n')
|
||||
q.execute('SHOW COLUMNS FROM `'+table+'`');cols=[r['Field'] for r in q.fetchall() if 'GENERATED' not in r['Extra']]
|
||||
names=','.join('`'+v+'`' for v in cols);count=0
|
||||
with c.cursor(pymysql.cursors.SSCursor) as stream:
|
||||
stream.execute('SELECT '+names+' FROM `'+table+'`')
|
||||
while True:
|
||||
rows=stream.fetchmany(500)
|
||||
if not rows:break
|
||||
f.write('INSERT INTO `'+table+'` ('+names+') VALUES\n'+',\n'.join('('+','.join(literal(v) for v in row)+')' for row in rows)+';\n')
|
||||
count+=len(rows)
|
||||
manifest['tables'][table]=count
|
||||
print(json.dumps({'table':table,'rows':count}),flush=True)
|
||||
for row in entries:
|
||||
if list(row.values())[1]=='VIEW':
|
||||
name=list(row.values())[0];q.execute('SHOW CREATE VIEW `'+name+'`');ddl=q.fetchone()['Create View'];f.write(ddl+';\n')
|
||||
for trigger in triggers:
|
||||
q.execute('SHOW CREATE TRIGGER `'+trigger['Trigger']+'`');ddl=q.fetchone()['SQL Original Statement'];f.write('DELIMITER ;;\n'+ddl+';;\nDELIMITER ;\n')
|
||||
f.write('SET FOREIGN_KEY_CHECKS=1;\n')
|
||||
c.rollback();c.close()
|
||||
os.rename(str(path)+'.partial',path)
|
||||
h=hashlib.sha256()
|
||||
with path.open('rb') as f:
|
||||
for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
|
||||
manifest.update(sha256=h.hexdigest(),bytes=path.stat().st_size,finished_at=time.strftime('%Y-%m-%dT%H:%M:%S%z'))
|
||||
(root/'backup-manifest.json').write_text(json.dumps(manifest,indent=2))
|
||||
print(json.dumps({'complete':True,'bytes':manifest['bytes'],'sha256':manifest['sha256']}),flush=True)
|
||||
@@ -0,0 +1 @@
|
||||
{"release": "mileage-baseline-reset-20260918", "readyz": {"checks": {"mysql": {"status": "ok"}}, "service": "vehicle-stat-writer", "status": "ok", "time": "2026-09-18T04:02:21Z"}, "runningSHA256": "eab37f206a97c71471f02ca9cfbab8dc0378159c6a4dbb2f0e769eeb0196d45c", "restoredServices": {"lingniu-go-stat-writer.service": true, "lingniu-go-stats-backfill.timer": true, "lingniu-vehicle-open-stat.timer": true}}
|
||||
@@ -0,0 +1 @@
|
||||
{"sourceRowsScanned": 33681, "changedSourceRows": 1265, "vehicles": 304, "selectedChanges": 850, "newInvalid": 0, "sample": [{"plate": "\u7ca4AGH6240", "date": "2026-07-08", "old": "18151.000", "new": "18149.200", "baselineDate": "2026-04-24 12:15:31", "status": "OK"}, {"plate": "\u7ca4AGH6240", "date": "2026-09-15", "old": "35042.900", "new": "0.000", "baselineDate": "2026-09-09 19:09:35", "status": "OK"}, {"plate": "\u7ca4AGH6240", "date": "2026-09-16", "old": "5.000", "new": "5.000", "baselineDate": "2026-09-15 11:27:37", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-08-15", "old": "175.100", "new": "102.000", "baselineDate": "2026-08-14 23:27:19", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-08-16", "old": "177.600", "new": "110.200", "baselineDate": "2026-08-15 23:10:39", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-08-17", "old": "54.400", "new": "54.400", "baselineDate": "2026-08-16 22:01:11", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-08-18", "old": "73.400", "new": "42.500", "baselineDate": "2026-08-17 23:26:58", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-08-19", "old": "50.000", "new": "0.100", "baselineDate": "2026-08-18 22:18:06", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-09-12", "old": "2248.900", "new": "51.900", "baselineDate": "2026-09-11 11:01:12", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-09-17", "old": "308.200", "new": "70.700", "baselineDate": "2026-09-16 16:36:30", "status": "OK"}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"sourceRowsScanned": 33683, "changedSourceRows": 0, "vehicles": 0, "selectedChanges": 0, "newInvalid": 0, "sample": []}
|
||||
@@ -0,0 +1,45 @@
|
||||
# coding: utf-8
|
||||
import subprocess,json,urllib.request,urllib.error,datetime
|
||||
pid=subprocess.check_output(['systemctl','show','-p','MainPID','lingniu-vehicle-platform'],universal_newlines=True).strip().split('=',1)[1]
|
||||
env=dict(x.decode().split('=',1) for x in open('/proc/'+pid+'/environ','rb').read().split(b'\0') if b'=' in x)
|
||||
admin_token=next(x['token'] for x in json.loads(env['AUTH_TOKENS_JSON']) if x.get('role')=='admin')
|
||||
def call(base,path,token,data=None,method=None):
|
||||
req=urllib.request.Request(base+path,data=None if data is None else json.dumps(data).encode(),headers={'Authorization':'Bearer '+token,'Content-Type':'application/json'},method=method)
|
||||
with urllib.request.urlopen(req,timeout=25) as r:return json.load(r)
|
||||
def admin(path,data=None,method=None):
|
||||
return call('http://127.0.0.1:20300','/api/v2/open-platform/apps'+path,admin_token,data,method)['data']
|
||||
|
||||
expiry=(datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(minutes=15)).isoformat()
|
||||
app_input=dict(name='release-smoke-mileage-baseline-reset-20260918',status='enabled',validFrom='2026-09-08T00:00:00+08:00',validTo=expiry)
|
||||
app=admin('',app_input,'POST');aid=str(app['id']);key=app['appKey'];out={'temporaryAppId':app['id']}
|
||||
try:
|
||||
admin('/'+aid+'/vehicles',{'vehicles':[dict(vin=v,validFrom=app_input['validFrom'],validTo=expiry) for v in ['LB9A32A26R0LS1055','LB9A32A29R0LS1065']]},'PUT')
|
||||
out['single']=[];out['platform']=[]
|
||||
for plate,vin,day,want in [('粤AGH6240','LB9A32A26R0LS1055','2026-09-08',0.2),('粤AGH6240','LB9A32A26R0LS1055','2026-09-15',0),('粤AGG5043','LB9A32A29R0LS1065','2026-09-17',70.7)]:
|
||||
body={'date':day,'plateNumbers':[plate],'protocolPriority':['GB32960']}
|
||||
single=call('https://open.d.lnoneos.com','/api/v1/vehicles/mileage/query',key,body)
|
||||
item=single['data'][0]
|
||||
assert item['dailyMileageKm']==want and item['status']=='NORMAL' and not item.get('dataQuality'),single
|
||||
out['single'].append(single)
|
||||
path='/api/mileage/daily?vin='+vin+'&dateFrom='+day+'&dateTo='+day+'&protocol=GB32960&limit=10'
|
||||
platform=call('http://127.0.0.1:20300',path,admin_token)
|
||||
out['platform'].append(platform)
|
||||
query={'startDate':'2026-09-08','endDate':'2026-09-17','plateNumbers':['粤AGH6240'],'protocolPriority':['GB32960'],'pageSize':3}
|
||||
pages=[]
|
||||
for i in range(5):
|
||||
page=call('https://open.d.lnoneos.com','/api/v1/vehicles/mileage/range/query',key,query);pages.append(page)
|
||||
if not page.get('nextCursor'):break
|
||||
query['cursor']=page['nextCursor']
|
||||
else:raise AssertionError('pagination did not complete')
|
||||
rows=[x for page in pages for x in page['data']]
|
||||
assert len(rows)==10,rows
|
||||
target=next(x for x in rows if x['date']=='2026-09-15')
|
||||
assert target['dailyMileageKm']==0 and target['status']=='NORMAL',target
|
||||
assert next(x for x in rows if x['date']=='2026-09-08')['dailyMileageKm']==0.2
|
||||
out['rangePages']=pages
|
||||
out['platformDefault']=call('http://127.0.0.1:20300','/api/mileage/daily?vin=LB9A32A26R0LS1055&dateFrom=2026-09-15&dateTo=2026-09-15&deduplicate=1&limit=10',admin_token)
|
||||
finally:
|
||||
app_input['status']='disabled';disabled=admin('/'+aid,app_input,'PUT');assert disabled['status']=='disabled'
|
||||
admin('/'+aid+'/vehicles',{'vehicles':[]},'PUT')
|
||||
out['temporaryAppDisabled']=True;out['temporaryGrantsRemoved']=True
|
||||
print(json.dumps(out))
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"uniqueChangedSources": 1268, "uniqueVehicleDays": 1263, "uniqueVehicles": 304, "updates": 1268, "metrics": ["vehicle_stat_kafka_lag{partition=\"0\",topic=\"vehicle.fields.go.gb32960.v1\"} 0", "vehicle_stat_kafka_lag{partition=\"0\",topic=\"vehicle.fields.go.jt808.v1\"} 0", "vehicle_stat_kafka_lag{partition=\"0\",topic=\"vehicle.fields.go.yutong-mqtt.v1\"} 0", "vehicle_stat_kafka_lag{partition=\"1\",topic=\"vehicle.fields.go.gb32960.v1\"} 0", "vehicle_stat_kafka_lag{partition=\"1\",topic=\"vehicle.fields.go.jt808.v1\"} 0", "vehicle_stat_kafka_lag{partition=\"1\",topic=\"vehicle.fields.go.yutong-mqtt.v1\"} 0", "vehicle_stat_kafka_lag{partition=\"2\",topic=\"vehicle.fields.go.gb32960.v1\"} 0", "vehicle_stat_kafka_lag{partition=\"2\",topic=\"vehicle.fields.go.jt808.v1\"} 0", "vehicle_stat_kafka_lag{partition=\"2\",topic=\"vehicle.fields.go.yutong-mqtt.v1\"} 0", "vehicle_stat_retry_pending_messages{worker=\"1\"} 0", "vehicle_stat_retry_pending_messages{worker=\"2\"} 0", "vehicle_stat_retry_pending_messages{worker=\"3\"} 0", "vehicle_stat_worker_active{worker=\"1\"} 1", "vehicle_stat_worker_active{worker=\"2\"} 1", "vehicle_stat_worker_active{worker=\"3\"} 1"], "recentErrors": ""}
|
||||
@@ -0,0 +1 @@
|
||||
{"sourceRowsScanned": 33679, "changedSourceRows": 1265, "vehicles": 304, "selectedChanges": 850, "newInvalid": 0, "sample": [{"plate": "\u7ca4AGH6240", "date": "2026-07-08", "old": "18151.000", "new": "18149.200", "baselineDate": "2026-04-24 12:15:31", "status": "OK"}, {"plate": "\u7ca4AGH6240", "date": "2026-09-15", "old": "35042.900", "new": "0.000", "baselineDate": "2026-09-09 19:09:35", "status": "OK"}, {"plate": "\u7ca4AGH6240", "date": "2026-09-16", "old": "5.000", "new": "5.000", "baselineDate": "2026-09-15 11:27:37", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-08-15", "old": "175.100", "new": "102.000", "baselineDate": "2026-08-14 23:27:19", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-08-16", "old": "177.600", "new": "110.200", "baselineDate": "2026-08-15 23:10:39", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-08-17", "old": "54.400", "new": "54.400", "baselineDate": "2026-08-16 22:01:11", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-08-18", "old": "73.400", "new": "42.500", "baselineDate": "2026-08-17 23:26:58", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-08-19", "old": "50.000", "new": "0.100", "baselineDate": "2026-08-18 22:18:06", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-09-12", "old": "2248.900", "new": "51.900", "baselineDate": "2026-09-11 11:01:12", "status": "OK"}, {"plate": "\u7ca4AGG5043", "date": "2026-09-17", "old": "308.200", "new": "70.700", "baselineDate": "2026-09-16 16:36:30", "status": "OK"}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"sourceRowsScanned": 33681, "changedSourceRows": 3, "vehicles": 3, "selectedChanges": 1, "newInvalid": 0, "sample": []}
|
||||
@@ -0,0 +1 @@
|
||||
{"sourceRowsScanned": 33681, "changedSourceRows": 0, "vehicles": 0, "selectedChanges": 0, "newInvalid": 0, "sample": []}
|
||||
@@ -0,0 +1 @@
|
||||
{"committed":true,"reprojectedDays":1260,"updatedSources":1265}
|
||||
@@ -0,0 +1 @@
|
||||
{"committed":true,"reprojectedDays":3,"updatedSources":3}
|
||||
@@ -0,0 +1,49 @@
|
||||
# coding: utf-8
|
||||
"""Prepare a read-only stale GB32960 baseline plan; apply with mileage-baseline-repair."""
|
||||
import sys,pathlib,json,datetime,decimal,argparse,os,itertools
|
||||
sys.path.insert(0,'/tmp/codex-mileage-diag-pymysql.zip')
|
||||
sys.path.insert(0,'/tmp')
|
||||
from db_access import connect
|
||||
# This script only prepares a plan; the Go utility applies it atomically.
|
||||
args=argparse.Namespace(apply=False)
|
||||
root=pathlib.Path('/opt/lingniu-go-native/backups/mileage-baseline-reset-20260918')
|
||||
os.umask(0o077);root.mkdir(parents=True,exist_ok=True)
|
||||
c=connect();D=decimal.Decimal
|
||||
with c.cursor() as q:
|
||||
q.execute('START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY')
|
||||
q.execute("""SELECT * FROM vehicle_daily_mileage_source WHERE protocol='GB32960'
|
||||
AND source_ip NOT IN ('legacy-mysql.lingniu-prod','manual-lingniu-prod-day-mileage')
|
||||
AND COALESCE(quality_reason,'')<>'gps_coordinate_accumulation'
|
||||
AND latest_total_mileage_km>0 AND latest_event_time>=TIMESTAMP(stat_date)
|
||||
AND latest_event_time<TIMESTAMP(stat_date)+INTERVAL 1 DAY
|
||||
ORDER BY vin,stat_date,source_key""")
|
||||
rows=q.fetchall()
|
||||
q.execute("SELECT vin,plate FROM vehicle_identity_binding");plates={r['vin']:r['plate'] for r in q.fetchall()}
|
||||
c.rollback()
|
||||
changes=[];by_source={};selected={}
|
||||
def rank(r):return (r['stat_date'],r['is_selected'],r['latest_event_time'])
|
||||
for (vin,day), grouped in itertools.groupby(rows,key=lambda r:(r['vin'],r['stat_date'])):
|
||||
group=list(grouped)
|
||||
for r in group:
|
||||
if r['quality_status']!='OK' or not r['first_event_time']:continue
|
||||
options=[v for v in (by_source.get((vin,r['source_key'])),selected.get(vin)) if v]
|
||||
if not options:continue
|
||||
prev=max(options,key=rank)
|
||||
if prev['latest_event_time']<=r['first_event_time']:continue
|
||||
delta=r['latest_total_mileage_km']-prev['latest_total_mileage_km']
|
||||
status='OK';reason='historical_source_baseline'
|
||||
if D(-1)<=delta<0:delta=D(0);reason='negative_jitter_clamped'
|
||||
elif delta<0:status='INVALID_DELTA';reason='TOTAL_MILEAGE_ROLLBACK'
|
||||
elif delta>D(2500*max(1,(r['latest_event_time'].date()-prev['latest_event_time'].date()).days)):status='INVALID_DELTA';reason='outside_daily_range'
|
||||
changes.append(dict(before=r,baseline=dict(source_key=prev['source_key'],total=prev['latest_total_mileage_km'],time=prev['latest_event_time']),daily=delta,status=status,reason=reason))
|
||||
for r in group:
|
||||
if r['quality_status']!='OK':continue
|
||||
by_source[(vin,r['source_key'])]=r
|
||||
if r['is_selected'] and (vin not in selected or rank(r)>rank(selected[vin])):selected[vin]=r
|
||||
summary={'sourceRowsScanned':len(rows),'changedSourceRows':len(changes),'vehicles':len(set(x['before']['vin'] for x in changes)),
|
||||
'selectedChanges':sum(bool(x['before']['is_selected']) for x in changes),'newInvalid':sum(x['status']!='OK' for x in changes),
|
||||
'sample':[{ 'plate':plates.get(x['before']['vin']), 'date':x['before']['stat_date'],'old':x['before']['daily_mileage_km'],'new':x['daily'],'baselineDate':x['baseline']['time'],'status':x['status']} for x in changes if plates.get(x['before']['vin']) in ('粤AGH6240','粤AGG5043')]}
|
||||
(root/('apply-plan.json' if args.apply else 'preflight-plan.json')).write_text(json.dumps(changes,default=str))
|
||||
(root/('repair-result.json' if args.apply else 'preflight.json')).write_text(json.dumps(summary,default=str,indent=2))
|
||||
print(json.dumps(summary,default=str))
|
||||
c.close()
|
||||
@@ -0,0 +1 @@
|
||||
{"release": "mileage-baseline-reset-20260918", "previous": "/opt/lingniu-go-native/releases/mileage-reconciliation-20260916", "hashes": {"stat-writer": "eab37f206a97c71471f02ca9cfbab8dc0378159c6a4dbb2f0e769eeb0196d45c", "stats-backfill": "2214fdc710ef554a73a610ae546465b836162a913645da6e4af087a252c0546c", "mileage-baseline-repair": "ee53087eb4ad5af0974f1870d82f473323bc4638936d02b7552303b954d4f2f6"}}
|
||||
@@ -0,0 +1,30 @@
|
||||
# 开放接口与中台日里程对齐 — 2026-09-18
|
||||
|
||||
已部署 `mileage-platform-alignment-20260918`,线上公网单日、区间分页及显式协议优先级请求均确认:粤AGG5043 / LB9A32A29R0LS1065 / 2026-09-17 返回 `dailyMileageKm=308.2`、`totalMileageKm=42077`、`status=NORMAL`,无 `dataQuality` 异常字段。
|
||||
|
||||
## 修复
|
||||
|
||||
开放平台优先保留中台已选中、质量为 OK 且日里程/累计值与最终日统计匹配的来源结果,不再因前后两天选择的来源不同而将其改为异常,也不跨来源重新相减。统计起止时间仍保留该来源真实基线。未验证的来源继续使用原累计差与异常检查;INVALID_DELTA、协议不明历史导入及 GPS 估算未被放行。无当日记录时仍结转累计值、日里程为 0。
|
||||
|
||||
未修改车辆源数据或中台里程统计表。仅发布开放平台服务;更新了接口内嵌文档和只读核对工具。
|
||||
|
||||
## 验证
|
||||
|
||||
- API Go 模块 `go test ./...` 通过。
|
||||
- 新增来源切换 308.2 样本、单日/区间切分一致性、缺报结转、异常记录不被平台标识绕过及 SQL 读取回归测试。
|
||||
- 生产只读批量验证覆盖 1,024 辆车、09-15~09-17 共 2,063 条结果:1,540 条已验证日统计、523 条结转;平台日里程差异 0,单日与区间差异 0。
|
||||
- 公网 POST `/api/v1/vehicles/mileage/query` 实际返回 308.2 / NORMAL,traceId:`25fb3895520655b957e27783cee36b09`。
|
||||
- 公网 POST `/api/v1/vehicles/mileage/range/query` 以 pageSize=1 读取三页,09-17 返回相同数值及状态,traceId:`3f210f2b0bf95cf55015429db252a150`。
|
||||
- 显式协议优先级 GB32960、MQTT、JT808 返回相同结果。
|
||||
- 测试应用 74 仅授权目标车辆,验证后已停用、清空车辆授权,原测试密钥已返回 HTTP 401;报告不包含密钥。
|
||||
- 新服务健康检查通过;运行二进制 SHA-256 见 deployment.json。
|
||||
|
||||
## 发布与回退
|
||||
|
||||
新目录:`/opt/lingniu-vehicle-open-platform/releases/mileage-platform-alignment-20260918`。
|
||||
旧目录:`/opt/lingniu-vehicle-open-platform/releases/mileage-reconciliation-20260916`。
|
||||
环境配置及旧版本路径备份:`/opt/lingniu-vehicle-open-platform/backups/mileage-platform-alignment-20260918`。
|
||||
|
||||
回退时恢复备份环境配置、将 current 指回旧版本并重启 lingniu-vehicle-open-platform;无数据库迁移需回退。
|
||||
|
||||
下游 BI 缓存不在本次修改范围。若仍展示旧异常,需要重新拉取或等待缓存过期;本次未声称 BI 缓存已刷新。
|
||||
@@ -0,0 +1 @@
|
||||
{"elapsed":"1.595524123s","normal":2063,"platformDailyMismatches":0,"platformRows":1540,"qualities":{"":1540,"CARRIED_FORWARD":523},"reconciliationMismatches":0,"rows":2063,"samples":{},"singleRangeDifferences":0,"vehicles":1024}
|
||||
@@ -0,0 +1 @@
|
||||
{"release": "mileage-platform-alignment-20260918", "previousRelease": "/opt/lingniu-vehicle-open-platform/releases/mileage-reconciliation-20260916", "binarySHA256": "f6d44acd08e602992a25c18a7153c698d96323cee1c93254d09e4b00dc135b88", "health": {"release": "mileage-platform-alignment-20260918", "service": "open-platform-api", "status": "ok"}, "backup": "/opt/lingniu-vehicle-open-platform/backups/mileage-platform-alignment-20260918"}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"local": {
|
||||
"release": "mileage-platform-alignment-20260918",
|
||||
"service": "open-platform-api",
|
||||
"status": "ok"
|
||||
},
|
||||
"public": {
|
||||
"release": "mileage-platform-alignment-20260918",
|
||||
"service": "open-platform-api",
|
||||
"status": "ok"
|
||||
},
|
||||
"runningSHA256": "f6d44acd08e602992a25c18a7153c698d96323cee1c93254d09e4b00dc135b88",
|
||||
"recentErrors": ""
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"temporaryAppId": 74, "single": {"code": "SUCCESS", "message": "success", "data": [{"statisticsStartTime": "2026-09-11T11:01:12+08:00", "statisticsEndTime": "2026-09-17T21:55:02+08:00", "vin": "LB9A32A29R0LS1065", "plateNumber": "\u7ca4AGG5043", "date": "2026-09-17", "dailyMileageKm": 308.2, "totalMileageKm": 42077, "dataTime": "2026-09-17T21:55:02+08:00", "updatedAt": "2026-09-18T01:35:20+08:00", "sourceProtocol": "GB32960", "status": "NORMAL"}], "traceId": "25fb3895520655b957e27783cee36b09"}, "rangePages": [{"code": "SUCCESS", "message": "success", "data": [{"vin": "LB9A32A29R0LS1065", "plateNumber": "\u7ca4AGG5043", "date": "2026-09-15", "dailyMileageKm": 38.7, "totalMileageKm": 41967.4, "dataTime": "2026-09-15T10:42:23+08:00", "updatedAt": "2026-09-18T01:35:07+08:00", "sourceProtocol": "GB32960", "status": "NORMAL"}], "snapshotId": "a88622e1b3afd0c60ddf86cd9736ca7d", "nextCursor": "YTg4NjIyZTFiM2FmZDBjNjBkZGY4NmNkOTczNmNhN2Q6MQ", "traceId": "42c9c406f874b3e2934f8f73ced18b79"}, {"code": "SUCCESS", "message": "success", "data": [{"vin": "LB9A32A29R0LS1065", "plateNumber": "\u7ca4AGG5043", "date": "2026-09-16", "dailyMileageKm": 38.9, "totalMileageKm": 42006.3, "dataTime": "2026-09-16T16:36:30+08:00", "updatedAt": "2026-09-18T01:35:16+08:00", "sourceProtocol": "GB32960", "status": "NORMAL"}], "snapshotId": "a88622e1b3afd0c60ddf86cd9736ca7d", "nextCursor": "YTg4NjIyZTFiM2FmZDBjNjBkZGY4NmNkOTczNmNhN2Q6Mg", "traceId": "387eca3f342740fbece85a643f0d51d6"}, {"code": "SUCCESS", "message": "success", "data": [{"vin": "LB9A32A29R0LS1065", "plateNumber": "\u7ca4AGG5043", "date": "2026-09-17", "dailyMileageKm": 308.2, "totalMileageKm": 42077, "dataTime": "2026-09-17T21:55:02+08:00", "updatedAt": "2026-09-18T01:35:20+08:00", "sourceProtocol": "GB32960", "status": "NORMAL"}], "snapshotId": "a88622e1b3afd0c60ddf86cd9736ca7d", "nextCursor": null, "traceId": "3f210f2b0bf95cf55015429db252a150"}], "explicitPriority": {"code": "SUCCESS", "message": "success", "data": [{"statisticsStartTime": "2026-09-11T11:01:12+08:00", "statisticsEndTime": "2026-09-17T21:55:02+08:00", "vin": "LB9A32A29R0LS1065", "plateNumber": "\u7ca4AGG5043", "date": "2026-09-17", "dailyMileageKm": 308.2, "totalMileageKm": 42077, "dataTime": "2026-09-17T21:55:02+08:00", "updatedAt": "2026-09-18T01:35:20+08:00", "sourceProtocol": "GB32960", "status": "NORMAL"}], "traceId": "695d0293028aec0a8f62b7931ced8525"}, "temporaryAppDisabled": true, "temporaryGrantsRemoved": true, "disabledKeyHTTPStatus": 401}
|
||||
@@ -0,0 +1 @@
|
||||
{"elapsed":"20.541286ms","normal":3,"platformDailyMismatches":0,"platformRows":3,"qualities":{"":3},"reconciliationMismatches":0,"rows":3,"samples":{"LB9A32A29R0LS1065\u00002026-09-15":{"PlatformMileage":true,"SourceKey":"GB32960:unknown@101.205.147.165","DataQuality":"","StatisticsStartTime":"2026-09-14T21:39:50+08:00","VIN":"LB9A32A29R0LS1065","Date":"2026-09-15","Protocol":"GB32960","MileageKm":38.7,"TotalMileageKm":41967.4,"DataTime":"2026-09-15T10:42:23+08:00","UpdatedAt":"2026-09-18T01:35:07+08:00"},"LB9A32A29R0LS1065\u00002026-09-16":{"PlatformMileage":true,"SourceKey":"GB32960:unknown@101.205.147.165","DataQuality":"","StatisticsStartTime":"2026-09-15T10:42:23+08:00","VIN":"LB9A32A29R0LS1065","Date":"2026-09-16","Protocol":"GB32960","MileageKm":38.9,"TotalMileageKm":42006.3,"DataTime":"2026-09-16T16:36:30+08:00","UpdatedAt":"2026-09-18T01:35:16+08:00"},"LB9A32A29R0LS1065\u00002026-09-17":{"PlatformMileage":true,"SourceKey":"GB32960:unknown@202.98.117.132","DataQuality":"","StatisticsStartTime":"2026-09-11T11:01:12+08:00","VIN":"LB9A32A29R0LS1065","Date":"2026-09-17","Protocol":"GB32960","MileageKm":308.2,"TotalMileageKm":42077,"DataTime":"2026-09-17T21:55:02+08:00","UpdatedAt":"2026-09-18T01:35:20+08:00"}},"singleRangeDifferences":0,"vehicles":1}
|
||||
@@ -0,0 +1,68 @@
|
||||
# 里程对账修复与发布记录 — 2026-09-16
|
||||
|
||||
已完成代码修复、MySQL 全库一致性备份、生产数据修复、发布和验证。未改动工作区原有的用氢导出及地理统计开发内容。
|
||||
|
||||
## 已采用的口径
|
||||
|
||||
- 开放平台单日和区间接口共用 `ReconciledMileageRange`,直接使用可追溯终端累计读数。
|
||||
- 同一来源的日里程 = 当日累计值 − 前一天累计值,保留三位小数。缺报日累计值结转、日里程为 0,并标记 `CARRIED_FORWARD`;恢复日承接跨缺报期增量。这是对账日里程,不保证增量实际发生在恢复当天。
|
||||
- 按请求协议优先级选择截至该日已存在有效读数的协议;该协议缺报时沿用其历史读数,不自动切换到低优先级协议的另一累计基准。
|
||||
- 首次基线、来源变化、累计回退及异常累计记录返回 `DATA_ANOMALY`,`dailyMileageKm=null`,保留累计值与来源时间,避免伪造可连续相减的里程。
|
||||
- GPS 估算继续保存在内部独立来源记录中,不参与开放平台累计差日里程。日里程和累计值的时间证据来自同一来源。
|
||||
- 实时统计和历史回补接受更早的同来源基线,不再丢弃跨完整缺报日的增量。
|
||||
- 无协议依据的旧库迁移入口拒绝执行写入,禁止将旧库记录统一标为 GB32960。
|
||||
|
||||
## 备份
|
||||
|
||||
主机:115.29.187.205。数据库:`lingniu_vehicle_data`。
|
||||
|
||||
完整备份:`/opt/lingniu-go-native/backups/mileage-reconciliation-20260916/database.sql.gz`
|
||||
|
||||
- 一致性快照逻辑备份:全部表结构和数据;当前库无存储过程、无数据库事件。
|
||||
- 文件大小:499,964,151 bytes;目录权限 0700、备份文件权限 0600。
|
||||
- SHA-256:`5d1f86e062d3410316a6ea66775aebcd8061e2d4a959ca304b7bf6a93e9438c4`。
|
||||
- 完成时间:2026-09-16 11:51:31 +08:00;已通过 `gzip -t` 完整性检查。
|
||||
- 本目录 `backup-manifest.json` 保存逐表行数和备份校验信息。未把数据库凭证写入报告或日志。
|
||||
- 服务版本、配置备份和修复前基线另保存在同一服务器备份目录。
|
||||
|
||||
## 数据修复
|
||||
|
||||
1. 两类历史导入来源 `legacy-mysql.lingniu-prod`、`manual-lingniu-prod-day-mileage` 共 169,604 条、涉及 892 个 VIN,标为 `INVALID_DELTA / UNVERIFIED_LEGACY_PROTOCOL`,清除选中标记。原始来源记录和旧库数据保留。
|
||||
2. 移除由这些来源生成的 169,014 条错误 GB32960 日统计;未将其猜改成 MQTT 或 JT808。
|
||||
3. 修复截至 09-16 的 5,635 条当前日首帧基线记录,使用此前同来源最近有效累计值。保留累计读数本身,维持回退及按跨日长度计算的异常上限检查。
|
||||
4. 通过正式统计投影函数重建 10,079 个车辆/日期/协议统计,恢复被旧迁移记录遮盖的真实来源统计。
|
||||
5. 生产复核:误标来源对应的最终统计剩余 0 条;169,604 条来源记录均保留、未选中、已标记协议未确认。
|
||||
|
||||
影响边界:只有协议不明的历史导入、没有真实终端证据的日期,不再作为正常协议里程返回,可能表现为 NO_DATA。不能把这类变化当作车辆里程真实减少。来源变化等异常记录也不能直接把日里程 null 当作正常 0 后参与完整对账。
|
||||
|
||||
## 样本结果
|
||||
|
||||
| 车辆 | 日期 | 修复后日里程 km | 累计 km | 说明 |
|
||||
|---|---|---:|---:|---|
|
||||
| 沪A60591F | 09-13~09-15 | 0 | 51,416 | MQTT 历史结转,不再返回错误 GB32960 |
|
||||
| 粤AGQ8377 | 09-15 | 101.4 | 14,467.4 | 原 31.2,补回跨缺报期 70.2 |
|
||||
| 粤AGR6839 | 09-15 | 40.6 | 45,162.8 | 原 2.6,补回跨缺报期 38.0 |
|
||||
| 粤AGP3609 | 09-14 | 878.8 | 48,981.7 | 原 70.1,补回跨缺报期 808.7 |
|
||||
| 粤AGP3609 | 09-15 | 1,367.4 | 50,349.1 | 原正常日期保持一致 |
|
||||
|
||||
## 验证
|
||||
|
||||
- API Go 模块 `go test ./...` 通过;统计模块、回补命令、迁移命令相关测试通过。
|
||||
- 新增恢复日增量、协议缺报结转、来源替换、首次基线以及单日/区间切分一致性测试。
|
||||
- 使用与 HTTP 接口相同的生产 Repository,只读核对 1,024 个当前车辆绑定、09-11~09-15。
|
||||
- 返回 3,435 条有累计证据记录:3,399 条正常/结转、36 条来源变化标记;其余车辆日期无可用协议证据。
|
||||
- 正常/结转记录的相邻日累计差核对失败数:0;逐日查询与整段查询的字段差异数:0。
|
||||
- `verification-all.json`、`verification-samples.json`、`database-verification.json` 保存实际验证结果。验证覆盖真实 SQL 与计算路径;未使用客户密钥重放外部认证 HTTP 请求。
|
||||
- 发布后统计服务 `/readyz`、开放平台 `/healthz` 正常;三协议全部 Kafka 分区 lag=0,三个 worker 重试队列=0,相关服务近期无 error 级日志。
|
||||
|
||||
## 发布与恢复资料
|
||||
|
||||
发布标识:`mileage-reconciliation-20260916`。
|
||||
|
||||
- Go 发布目录:`/opt/lingniu-go-native/releases/mileage-reconciliation-20260916`,更新 stat-writer、stats-backfill、legacy-mileage-migrate,并包含显式列表重投影工具 mileage-reproject。
|
||||
- 开放平台发布目录:`/opt/lingniu-vehicle-open-platform/releases/mileage-reconciliation-20260916`。
|
||||
- 旧版本路径存于服务器备份目录 `previous-go-release.txt`、`previous-open-release.txt`;旧开放平台环境配置存于 `open-platform.env.before`。
|
||||
- 数据恢复应先将完整备份恢复到独立临时库,再按本次来源及重建清单恢复受影响记录;不要直接覆盖整个线上库,以免覆盖备份后新增业务数据。
|
||||
- `baseline-before.json`、`reproject.json`、`repair-result.json` 为本次修复清单和结果。`repair.py` 为本次一次性修复工具,默认仅预检。
|
||||
|
||||
下游 BI 历史缓存可能仍保留旧响应,需刷新或等待原有最长 6 小时缓存过期。本次没有修改下游 BI 或清空其缓存。
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"started_at": "2026-09-16T11:39:52+0800",
|
||||
"tables": {
|
||||
"business_customer_vehicle_scope": 0,
|
||||
"business_scope_rejection": 0,
|
||||
"business_scope_state": 1,
|
||||
"business_scope_sync_run": 0,
|
||||
"jt808_registration": 837,
|
||||
"platform_auth_audit": 1616,
|
||||
"platform_user": 13,
|
||||
"platform_user_business_scope": 0,
|
||||
"platform_user_menu": 48,
|
||||
"platform_user_session": 1363,
|
||||
"platform_user_vehicle": 2169,
|
||||
"platform_user_vehicle_grant_history": 2169,
|
||||
"platform_vehicle_source_policy_audit": 2,
|
||||
"platform_vehicle_source_policy_version": 103,
|
||||
"platform_vehicle_source_provider": 103,
|
||||
"platform_vehicle_source_provider_audit": 105,
|
||||
"vehicle": 940,
|
||||
"vehicle_access_identity_audit": 0,
|
||||
"vehicle_access_threshold_audit": 0,
|
||||
"vehicle_access_threshold_config": 1,
|
||||
"vehicle_alert_candidate": 11,
|
||||
"vehicle_alert_event": 25594,
|
||||
"vehicle_alert_event_action": 50114,
|
||||
"vehicle_alert_notification": 25594,
|
||||
"vehicle_alert_notification_retry_audit": 0,
|
||||
"vehicle_alert_rule": 12,
|
||||
"vehicle_alert_rule_audit": 100,
|
||||
"vehicle_alert_rule_state": 0,
|
||||
"vehicle_alert_stream_checkpoint": 9,
|
||||
"vehicle_daily_geography": 45609,
|
||||
"vehicle_daily_gps_mileage_state": 56017,
|
||||
"vehicle_daily_mileage": 1624735,
|
||||
"vehicle_daily_mileage_source": 1644263,
|
||||
"vehicle_data_source": 1804,
|
||||
"vehicle_geography_cache": 1206430,
|
||||
"vehicle_hydrogen_energy_parameter": 1141,
|
||||
"vehicle_hydrogen_tank_capacity": 1144,
|
||||
"vehicle_identifier": 2277,
|
||||
"vehicle_identity_binding": 1024,
|
||||
"vehicle_identity_oem_audit": 244,
|
||||
"vehicle_location_source_policy": 2,
|
||||
"vehicle_metric_definition": 28,
|
||||
"vehicle_metric_protocol_mapping": 40,
|
||||
"vehicle_open_admin_audit": 46,
|
||||
"vehicle_open_api_audit": 16212456,
|
||||
"vehicle_open_app": 5,
|
||||
"vehicle_open_app_vehicle": 4116,
|
||||
"vehicle_open_daily_energy": 29369,
|
||||
"vehicle_open_hydrogen_rate_stream_state": 19,
|
||||
"vehicle_open_hydrogen_segment_stream_state": 12558,
|
||||
"vehicle_open_hydrogen_stream_state": 17754,
|
||||
"vehicle_open_mileage_snapshot": 90,
|
||||
"vehicle_open_mileage_snapshot_vehicle": 90114,
|
||||
"vehicle_open_user": 3,
|
||||
"vehicle_open_user_app": 3,
|
||||
"vehicle_open_user_audit": 100,
|
||||
"vehicle_open_user_session": 61,
|
||||
"vehicle_platform_schema_migration": 45,
|
||||
"vehicle_profile": 1024,
|
||||
"vehicle_profile_audit": 1024,
|
||||
"vehicle_realtime_location": 1458,
|
||||
"vehicle_realtime_location_source": 811,
|
||||
"vehicle_realtime_snapshot": 1460,
|
||||
"vehicle_reconciliation_action": 20443,
|
||||
"vehicle_reconciliation_issue": 10648,
|
||||
"vehicle_reconciliation_run": 65
|
||||
},
|
||||
"database": "lingniu_vehicle_data",
|
||||
"sha256": "5d1f86e062d3410316a6ea66775aebcd8061e2d4a959ca304b7bf6a93e9438c4",
|
||||
"bytes": 499964151,
|
||||
"finished_at": "2026-09-16T11:51:31+0800"
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
{
|
||||
"legacy_projection_remaining": [
|
||||
{
|
||||
"n": 0
|
||||
}
|
||||
],
|
||||
"legacy_quarantine": [
|
||||
{
|
||||
"quality_status": "INVALID_DELTA",
|
||||
"quality_reason": "UNVERIFIED_LEGACY_PROTOCOL",
|
||||
"is_selected": 0,
|
||||
"n": 169604
|
||||
}
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"plate": "沪A60591F",
|
||||
"vin": "LMRKH9AC7R1004098",
|
||||
"stat_date": "2026-09-12",
|
||||
"protocol": "YUTONG_MQTT",
|
||||
"source_id": 1,
|
||||
"daily_mileage_km": "0.000",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "51416.000",
|
||||
"day_end_total_mileage_km": "51416.000",
|
||||
"updated_at": "2026-09-12 04:53:49"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP3609",
|
||||
"vin": "LB9A32A23R0LS1661",
|
||||
"stat_date": "2026-09-12",
|
||||
"protocol": "GB32960",
|
||||
"source_id": 3,
|
||||
"daily_mileage_km": "1293.600",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "48102.900",
|
||||
"day_end_total_mileage_km": "48102.900",
|
||||
"updated_at": "2026-09-15 01:35:09"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP3609",
|
||||
"vin": "LB9A32A23R0LS1661",
|
||||
"stat_date": "2026-09-12",
|
||||
"protocol": "JT808",
|
||||
"source_id": null,
|
||||
"daily_mileage_km": "0.148",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "0.148",
|
||||
"day_end_total_mileage_km": "37571.200",
|
||||
"updated_at": "2026-09-15 01:35:21"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP3609",
|
||||
"vin": "LB9A32A23R0LS1661",
|
||||
"stat_date": "2026-09-14",
|
||||
"protocol": "GB32960",
|
||||
"source_id": 3,
|
||||
"daily_mileage_km": "878.800",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "48981.700",
|
||||
"day_end_total_mileage_km": "48981.700",
|
||||
"updated_at": "2026-09-16 11:54:43"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP3609",
|
||||
"vin": "LB9A32A23R0LS1661",
|
||||
"stat_date": "2026-09-14",
|
||||
"protocol": "JT808",
|
||||
"source_id": null,
|
||||
"daily_mileage_km": "0.134",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "0.134",
|
||||
"day_end_total_mileage_km": "37571.100",
|
||||
"updated_at": "2026-09-16 11:54:43"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP3609",
|
||||
"vin": "LB9A32A23R0LS1661",
|
||||
"stat_date": "2026-09-15",
|
||||
"protocol": "GB32960",
|
||||
"source_id": 3,
|
||||
"daily_mileage_km": "1367.400",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "50349.100",
|
||||
"day_end_total_mileage_km": "50349.100",
|
||||
"updated_at": "2026-09-16 01:34:49"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP3609",
|
||||
"vin": "LB9A32A23R0LS1661",
|
||||
"stat_date": "2026-09-15",
|
||||
"protocol": "JT808",
|
||||
"source_id": 7,
|
||||
"daily_mileage_km": "29.400",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "37600.500",
|
||||
"day_end_total_mileage_km": "37600.500",
|
||||
"updated_at": "2026-09-16 01:35:31"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGQ8377",
|
||||
"vin": "LB9A32A27R0LS1419",
|
||||
"stat_date": "2026-09-12",
|
||||
"protocol": "GB32960",
|
||||
"source_id": 3,
|
||||
"daily_mileage_km": "31.800",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "14359.900",
|
||||
"day_end_total_mileage_km": "14359.900",
|
||||
"updated_at": "2026-09-16 11:54:50"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGQ8377",
|
||||
"vin": "LB9A32A27R0LS1419",
|
||||
"stat_date": "2026-09-12",
|
||||
"protocol": "JT808",
|
||||
"source_id": 7,
|
||||
"daily_mileage_km": "31.500",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "14422.700",
|
||||
"day_end_total_mileage_km": "14422.700",
|
||||
"updated_at": "2026-09-16 11:54:50"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGQ8377",
|
||||
"vin": "LB9A32A27R0LS1419",
|
||||
"stat_date": "2026-09-13",
|
||||
"protocol": "GB32960",
|
||||
"source_id": 3,
|
||||
"daily_mileage_km": "6.100",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "14366.000",
|
||||
"day_end_total_mileage_km": "14366.000",
|
||||
"updated_at": "2026-09-16 01:35:13"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGQ8377",
|
||||
"vin": "LB9A32A27R0LS1419",
|
||||
"stat_date": "2026-09-13",
|
||||
"protocol": "JT808",
|
||||
"source_id": 7,
|
||||
"daily_mileage_km": "76.300",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "14499.000",
|
||||
"day_end_total_mileage_km": "14499.000",
|
||||
"updated_at": "2026-09-16 01:35:19"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGQ8377",
|
||||
"vin": "LB9A32A27R0LS1419",
|
||||
"stat_date": "2026-09-15",
|
||||
"protocol": "GB32960",
|
||||
"source_id": 3,
|
||||
"daily_mileage_km": "101.400",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "14467.400",
|
||||
"day_end_total_mileage_km": "14467.400",
|
||||
"updated_at": "2026-09-16 11:54:50"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGQ8377",
|
||||
"vin": "LB9A32A27R0LS1419",
|
||||
"stat_date": "2026-09-15",
|
||||
"protocol": "JT808",
|
||||
"source_id": 7,
|
||||
"daily_mileage_km": "31.500",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "14530.500",
|
||||
"day_end_total_mileage_km": "14530.500",
|
||||
"updated_at": "2026-09-16 11:54:50"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGR6839",
|
||||
"vin": "LB9A32A23R0LS1434",
|
||||
"stat_date": "2026-09-15",
|
||||
"protocol": "GB32960",
|
||||
"source_id": 3,
|
||||
"daily_mileage_km": "40.600",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "45162.800",
|
||||
"day_end_total_mileage_km": "45162.800",
|
||||
"updated_at": "2026-09-16 11:54:42"
|
||||
},
|
||||
{
|
||||
"plate": "粤AGR6839",
|
||||
"vin": "LB9A32A23R0LS1434",
|
||||
"stat_date": "2026-09-15",
|
||||
"protocol": "JT808",
|
||||
"source_id": 7,
|
||||
"daily_mileage_km": "53.100",
|
||||
"pure_hydrogen_mileage_km": "0.000",
|
||||
"latest_total_mileage_km": "11138.800",
|
||||
"day_end_total_mileage_km": "11138.800",
|
||||
"updated_at": "2026-09-16 11:54:42"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"legacy": {
|
||||
"n": 169604,
|
||||
"vehicles": 892
|
||||
},
|
||||
"legacy_projections": {
|
||||
"n": 169014
|
||||
},
|
||||
"baseline_candidates": 5635,
|
||||
"quarantined_source_rows": 169604,
|
||||
"removed_mislabelled_projections": 169014,
|
||||
"reproject_days": 10079
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"elapsed":"2.236567803s","normal":3399,"qualities":{"":2333,"CARRIED_FORWARD":1066,"ODOMETER_SOURCE_CHANGED":36},"reconciliationMismatches":0,"rows":3435,"samples":{},"singleRangeDifferences":0,"vehicles":1024}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user