gaoluyang
3 天以前 e71d71bf43c2307c89d3592adf08c1b32fcd0907
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
"""User-requested timestamp redistribution with an immutable plan and guarded rollback."""
from pathlib import Path
from datetime import datetime, date, time, timedelta, timezone
from collections import Counter
import argparse
import hashlib
import json
import random
 
from read_log_times import connect
 
HERE = Path(__file__).resolve().parent
PLAN = HERE / 'time-adjustment-backup-and-plan.json'
RESULT = HERE / 'time-adjustment-result.json'
TZ = timezone(timedelta(hours=8))
 
def digest(rows):
    # All fields except the user-requested operation time must remain unchanged.
    return hashlib.sha256(json.dumps([{k: v for k, v in r.items() if k != 'create_time'}
                                     for r in rows], sort_keys=True, default=str).encode()).hexdigest()
 
def summary(rows):
    return dict(count=len(rows), earliest=min(r['create_time'] for r in rows),
                latest=max(r['create_time'] for r in rows),
                days=dict(sorted(Counter(str(r['create_time'])[:10] for r in rows).items())))
 
def save(path, data, exclusive=False):
    with path.open('x' if exclusive else 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2, default=str)
 
def plan():
    cutoff = datetime.now(TZ).replace(tzinfo=None, microsecond=0)
    start = date(2026, 9, 17)
    assert cutoff.date() >= start
    with connect() as c:
        with c.cursor() as q:
            q.execute('SELECT * FROM system_operate_log ORDER BY id')
            rows = q.fetchall()
    assert rows, 'No operation logs found'
    days = [start + timedelta(days=i) for i in range((cutoff.date()-start).days+1)]
    days = [d for d in days if datetime.combine(d, time(9)) <= cutoff]
    targets = []
    rng = random.SystemRandom()
    for i in range(len(rows)):
        d = days[i % len(days)]
        lower = datetime.combine(d, time(9))
        upper = min(datetime.combine(d, time(18)), cutoff)
        targets.append(lower + timedelta(seconds=rng.randrange(int((upper-lower).total_seconds())+1)))
    rng.shuffle(targets)
    changes = [dict(id=r['id'], old_create_time=str(r['create_time']),
                    old_update_time=str(r['update_time']), new_create_time=str(t))
               for r, t in zip(rows, targets)]
    save(PLAN, dict(target='36.213.150.237:10000/mom-hm', table='system_operate_log',
                    purpose='User-requested artificial timestamp distribution; original event times retained here.',
                    timezone='Asia/Shanghai', cutoff=str(cutoff), original_summary=summary(rows),
                    preserved_fields_digest=digest(rows), changes=changes), exclusive=True)
    print(json.dumps(dict(plan=str(PLAN), count=len(rows), days=len(days), cutoff=str(cutoff)), ensure_ascii=False))
 
def apply(restore=False):
    data = json.loads(PLAN.read_text(encoding='utf-8'))
    changes = data['changes']
    ids = [r['id'] for r in changes]
    marks = ','.join(['%s'] * len(ids))
    sql = 'SELECT * FROM system_operate_log WHERE id IN ('+marks+') ORDER BY id'
    with connect() as c:
        with c.cursor() as q:
            q.execute(sql+' FOR UPDATE', ids)
            before = q.fetchall()
            assert len(before) == len(changes)
            assert digest(before) == data['preserved_fields_digest'], 'Log fields changed since snapshot'
            by_id = {r['id']: r for r in before}
            for change in changes:
                expected = change['new_create_time'] if restore else change['old_create_time']
                assert str(by_id[change['id']]['create_time']) == expected, 'Operation time changed since snapshot'
            params = [(r['old_create_time'] if restore else r['new_create_time'],
                       r['old_update_time'], r['id']) for r in changes]
            # Explicitly retain update_time to suppress the column's ON UPDATE trigger.
            q.executemany('UPDATE system_operate_log SET create_time=%s, update_time=%s WHERE id=%s', params)
            q.execute(sql, ids)
            after = q.fetchall()
            assert digest(after) == data['preserved_fields_digest']
            expected_times = {r['id']: r['old_create_time'] if restore else r['new_create_time'] for r in changes}
            assert all(str(r['create_time']) == expected_times[r['id']] for r in after)
            if not restore:
                cutoff = datetime.fromisoformat(data['cutoff'])
                assert all(datetime(2026, 9, 17, 9) <= r['create_time'] <= cutoff and
                           time(9) <= r['create_time'].time() <= time(18) for r in after)
            c.commit()
            q.execute(sql, ids)
            committed = q.fetchall()
            assert digest(committed) == data['preserved_fields_digest']
            assert all(str(r['create_time']) == expected_times[r['id']] for r in committed)
            result = dict(target=data['target'], restored=restore, committed=True,
                          verified_after_commit=True, original_times_backup=str(PLAN),
                          only_field_changed='create_time', summary=summary(committed))
            save(RESULT if not restore else HERE/'restore-result.json', result)
            print(json.dumps(result, ensure_ascii=False, default=str))
 
if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('mode', choices=['plan', 'apply', 'restore'])
    args = parser.parse_args()
    if args.mode == 'plan':
        plan()
    else:
        apply(restore=args.mode == 'restore')