chore: commit mileage updates and accumulated project artifacts
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user