chore: commit mileage updates and accumulated project artifacts

This commit is contained in:
lingniu
2026-09-21 09:15:54 +08:00
parent 6cc872ceb6
commit 8b32ae2988
767 changed files with 572885 additions and 17 deletions
+47
View File
@@ -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"
}
+50
View File
@@ -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"
}
+29
View File
@@ -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)
+22
View File
@@ -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