Files
lingniu-vehicle-ingest/reports/mileage-baseline-reset-20260918/repair.py
T

50 lines
3.2 KiB
Python

# 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()