"""Append explicitly labelled metadata reconstructions, never original request logs.""" import sys, json, csv from pathlib import Path from datetime import datetime from collections import Counter P = Path(__file__).parent sys.path.insert(0, str(P.parent / 'iqc-import-20261012/deps')) import pymysql MODULES = { 'crm_contract': 'CRM 合同', 'erp_sale_order': 'ERP 销售订单', 'mes_pro_mps': 'MES 生产主计划', 'mes_pro_route': 'MES 工艺路线', 'mes_pro_process': 'MES 工序', 'mes_md_workstation': 'MES 工作站', 'mes_pd_process_param': 'MES 生产参数', 'mes_pd_process_param_template': 'MES 参数模板', 'mes_md_item': 'MES 物料', 'mes_qc_iqc': 'MES 来料检验单', 'mes_qc_oqc': 'MES 出货检验单', 'mes_md_product_bom': 'MES 产品BOM', } snapshot = json.loads((P / 'before-log-sources.json').read_text(encoding='utf8')) c = pymysql.connect(host='36.213.150.237', port=10000, user='root', password=sys.stdin.readline().strip(), database='mom-hm', charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor, connect_timeout=10, read_timeout=60, autocommit=False) def serial(value): return json.dumps(value, ensure_ascii=False, default=str, sort_keys=True) def sample(rows, n=10): # Ensure each real source account is represented, then spread across IDs. selected = {} for row in rows: if not any(x['creator'] == row['creator'] for x in selected.values()): selected[row['id']] = row for i in range(min(n, len(rows))): row = rows[round(i * (len(rows) - 1) / max(1, min(n, len(rows)) - 1))] if len(selected) < n: selected[row['id']] = row for row in rows: if len(selected) >= n: break selected[row['id']] = row return sorted(selected.values(), key=lambda r: r['id']) inserted, skipped = [], [] try: with c.cursor() as q: q.execute('SELECT DATABASE() db, NOW() now') state = q.fetchone() assert state['db'] == 'mom-hm' now = state['now'] q.execute("SELECT GET_LOCK('codex-business-metadata-log-v1', 0) locked") assert q.fetchone()['locked'] == 1, 'Another supplementation is running' q.execute('SELECT id,nickname FROM system_users WHERE deleted=0') users = {str(u['id']): u for u in q.fetchall()} q.execute('SELECT * FROM system_operate_log ORDER BY id') before = q.fetchall() (P / 'before-operation-log-append.json').write_text(serial(before), encoding='utf8') existing = {r['trace_id'] for r in before} for table, module in MODULES.items(): q.execute('SHOW COLUMNS FROM `' + table + '`') columns = {r['Field'] for r in q.fetchall()} tenant_select = 'tenant_id' if 'tenant_id' in columns else '0 AS tenant_id' eligible = [] for row in snapshot[table]: if str(row['creator']) not in users: continue when = datetime.fromisoformat(row['create_time']) if when > now or when > datetime.fromisoformat(row['update_time']): skipped.append({'table': table, 'id': row['id'], 'reason': '时间异常'}) continue eligible.append(row) for row in sample(eligible): trace = f"BF-META-V1:{table}:{row['id']}" if trace in existing: continue q.execute('SELECT creator,create_time,update_time,' + tenant_select + ' FROM `' + table + '` WHERE id=%s AND deleted=0 FOR SHARE', (row['id'],)) current = q.fetchone() if not current or str(current['creator']) != str(row['creator']) or str(current['create_time']) != row['create_time']: skipped.append({'table': table, 'id': row['id'], 'reason': '源记录已变化'}) continue user = users[str(row['creator'])] name = user['nickname'].strip() label = row.get('no') or row.get('code') or row.get('name') or row.get('param_name') or row.get('template_name') or str(row['id']) extra = serial({'reconstructed': True, 'evidence': 'record_creator_and_create_time', 'source_table': table, 'source_id': row['id'], 'source_creator': row['creator'], 'source_create_time': row['create_time'], 'source_update_time': str(current['update_time']), 'recorded_at': str(now), 'request_evidence_available': False, 'attribution': '源记录建档账号;不能据此认定真实操作人或请求'}) action = (f'【历史数据补录/元信息重建】{module}记录“{label}”(ID:{row["id"]});' f'源建档账号:{name}({user["id"]}),源建档时间:{row["create_time"]}。' f'补录时间:{now}。仅还原记录元信息,非原始操作请求日志;不推定审核、修改或其他操作。') assert len(trace) <= 64 and len(action) <= 2000 and len(extra) <= 2000 inserted.append(dict(trace_id=trace, user_id=user['id'], user_type=2, type=module, sub_type='历史数据补录(建档元信息)', biz_id=row['id'], action=action, success=1, extra=extra, request_method=None, request_url=None, user_ip=None, user_agent=None, creator='1', create_time=row['create_time'], updater='1', update_time=now, deleted=0, tenant_id=current['tenant_id'], source_table=table, source_name=str(label), source_user_name=name, recorded_at=str(now))) fields = ['trace_id','user_id','user_type','type','sub_type','biz_id','action','success', 'extra','request_method','request_url','user_ip','user_agent','creator', 'create_time','updater','update_time','deleted','tenant_id'] if inserted: sql = 'INSERT INTO system_operate_log (' + ','.join('`'+f+'`' for f in fields) + ') VALUES (' + ','.join('%s' for _ in fields) + ')' q.executemany(sql, [tuple(r[f] for f in fields) for r in inserted]) q.execute("SELECT * FROM system_operate_log WHERE trace_id LIKE 'BF-META-V1:%'") reconstructed = q.fetchall() assert len({r['trace_id'] for r in reconstructed}) == len(reconstructed) assert all(r['sub_type'] == '历史数据补录(建档元信息)' and json.loads(r['extra'])['reconstructed'] for r in reconstructed) assert all(r['request_url'] is None and r['user_ip'] is None and r['user_agent'] is None for r in reconstructed) q.execute('SELECT * FROM system_operate_log ORDER BY id') after = {r['id']: r for r in q.fetchall()} assert all(serial(r) == serial(after[r['id']]) for r in before), 'Original logs changed' c.commit() result = {'inserted': len(inserted), 'by_module': dict(Counter(r['type'] for r in inserted)), 'by_source_account': dict(Counter(r['source_user_name'] for r in inserted)), 'historical_start': min((r['create_time'] for r in inserted), default=None), 'historical_end': max((r['create_time'] for r in inserted), default=None), 'recorded_at': str(now), 'original_logs_verified_unchanged': len(before), 'skipped': skipped, 'reconstruction_total': len(reconstructed)} (P / 'operation-log-supplement-result.json').write_text(serial(result), encoding='utf8') (P / 'operation-log-supplement-records.json').write_text(serial(inserted), encoding='utf8') with (P / '操作日志补录清单.csv').open('w', encoding='utf-8-sig', newline='') as f: w = csv.writer(f) w.writerow(['模块','业务ID','业务编号或名称','源建档账号','源建档账号ID','源建档时间(日志显示时间)','实际补录时间','日志类型','来源表','追踪编号']) for r in inserted: w.writerow([r['type'],r['biz_id'],r['source_name'],r['source_user_name'],r['user_id'],r['create_time'],r['recorded_at'],r['sub_type'],r['source_table'],r['trace_id']]) print(serial(result)) finally: c.close()