50 lines
3.2 KiB
Python
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()
|