云
12 小时以前 e09f103259fa602382bb5ce2a03361a6397af46a
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
107
"""Atomic migration of QC configuration, existing line standards and reviewed reports."""
import sys,json,re,hashlib,collections,copy
from pathlib import Path
from decimal import Decimal as D
sys.stdout.reconfigure(encoding='utf-8')
sys.path.insert(0,str(Path('scripts/iqc-import-20261012/deps').resolve()))
import pymysql
P=Path(__file__).parent;apply='--apply' in sys.argv
audit=json.loads((P/'audit.json').read_text(encoding='utf8'));plan=json.loads((P/'plan.json').read_text(encoding='utf8'));reports=json.loads((P/'report-output.json').read_text(encoding='utf8'))
upgrade=json.loads((P/'report-template-upgrade.json').read_text(encoding='utf8'))
assert len(reports)==12
def canon(v):return json.loads(json.dumps(v,ensure_ascii=False,default=str))
def fingerprint(v):return hashlib.sha256(json.dumps(v,ensure_ascii=False,sort_keys=True,default=str).encode()).hexdigest()
def dump(name,v):(P/name).write_text(json.dumps(v,ensure_ascii=False,indent=2,default=str),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,autocommit=False,read_timeout=90)
allowed={'mes_qc_indicator','mes_qc_template_indicator','mes_qc_iqc_line','mes_qc_ipqc_line','mes_qc_oqc_line','mes_qc_rqc_line','mes_qc_indicator_result_detail','qc_report_instance','qc_report_template','system_storage_attachment'}
merged={};new_units=[];sql_log=[];before={};unitids={}
for ch in plan['changes']:
 assert ch['table'] in allowed
 merged.setdefault((ch['table'],ch['id']),{}).update(ch['after'])
with c.cursor() as q:
 try:
  q.execute('SELECT GET_LOCK(%s,10)',('qc-indicator-standard-optimize',));assert list(q.fetchone().values())==[1]
  for t in allowed|{'mes_md_unit_measure','qc_report_template_version'}:
   q.execute('SELECT * FROM `'+t+'` ORDER BY id FOR UPDATE');before[t]=q.fetchall()
  # Refuse to apply a stale plan; inactive rows are preserved too.
  for t in allowed-{'system_storage_attachment'}:
   assert canon(before[t])==audit[t]['rows'],('Data changed after audit; regenerate plan',t)
  q.execute('SHOW FULL COLUMNS FROM system_storage_attachment');attachment_fields={x['Field'] for x in q.fetchall()}
  for name in plan['units']:
   found=[x for x in before['mes_md_unit_measure'] if x['name']==name and x['deleted'] in (0,b'\x00') and x['status']==0]
   if found:unitids[name]=found[0]['id'];continue
   code='QC_UNIT_'+hashlib.sha256(name.encode()).hexdigest()[:12].upper()
   assert not any(x['code']==code for x in before['mes_md_unit_measure'])
   sql='INSERT INTO mes_md_unit_measure (code,name,primary_flag,primary_id,change_rate,status,remark,creator,updater) VALUES (%s,%s,1,NULL,1,0,%s,%s,%s)'
   args=(code,name,'质量检测专用计量单位;依据现有方案标准及检验记录单位规范化。','1','1')
   q.execute(sql,args);unitids[name]=q.lastrowid;new_units.append({'id':q.lastrowid,'name':name,'code':code});sql_log.append(q.mogrify(sql,args)+';')
  for assignment in plan['unit_assignments']:
   merged.setdefault((assignment['table'],assignment['id']),{})['unit_measure_id']=unitids[assignment['unit_name']]
  existing_versions=before['qc_report_template_version']
  assert not any(x['template_id']==upgrade['template_id'] and x['version']==upgrade['version'] for x in existing_versions)
  old_version=next(x for x in existing_versions if x['template_id']==upgrade['template_id'] and x['version']==upgrade['source_version'] and x['deleted'] in (0,b'\x00'))
  sql='INSERT INTO qc_report_template_version (template_id,version,schema_json,status,description,creator,updater) VALUES (%s,%s,%s,%s,%s,%s,%s)'
  args=(upgrade['template_id'],upgrade['version'],json.dumps(upgrade['schema'],ensure_ascii=False,separators=(',',':')),old_version['status'],'质检数据规范化:展示完整标准要求、允许区间和检测单位;保留原模板版本。','1','1')
  q.execute(sql,args);new_version_id=q.lastrowid;sql_log.append(q.mogrify(sql,args)+';')
  merged[('qc_report_template',upgrade['template_id'])]={'current_version':upgrade['version']}
  for report in reports:
   assert report['html'].strip() and '{{' not in report['html']
   merged[('qc_report_instance',report['id'])]={'data_snapshot':json.dumps(report['snapshot'],ensure_ascii=False,separators=(',',':')),'render_html':report['html'],'template_version':report['template_version']}
  # Invalidate only stale PDF attachment bindings, retaining physical blobs and the previous report in the backup.
  reportids={x['id'] for x in reports};old_attachments=[]
  for row in before['system_storage_attachment']:
   if row.get('record_type')=='qc_report_instance' and str(row.get('record_id')) in {str(x) for x in reportids} and row['deleted'] in (0,b'\x00'):
    merged[('system_storage_attachment',row['id'])]={'deleted':1};old_attachments.append(row['id'])
  protected_tables=['mes_qc_iqc','mes_qc_ipqc','mes_qc_oqc','mes_qc_rqc','mes_qc_indicator_result','mes_qc_template','mes_qc_template_item','erp_sale_order','erp_sale_order_items','crm_contract','crm_sale_quotation','crm_receivable_plan','mes_pro_mps','mes_pro_work_order','mes_pro_task','mes_pro_feedback','mes_wm_batch','mes_wm_material_stock','mes_wm_transaction','mes_wm_product_sales','mes_wm_product_sales_line']
  protected={}
  for t in protected_tables:q.execute('SELECT * FROM `'+t+'` ORDER BY id');protected[t]=fingerprint(q.fetchall())
  dump('before-'+('apply' if apply else 'preview')+'.json',{'tables':before,'protected_hashes':protected})
  schema={t:{x['Field']:x for x in audit[t]['columns']} for t in allowed if t in audit}
  q.execute('SHOW FULL COLUMNS FROM system_storage_attachment');schema['system_storage_attachment']={x['Field']:x for x in q.fetchall()}
  byid={t:{x['id']:x for x in rr} for t,rr in before.items()};changes=[];batches=collections.defaultdict(list)
  for (t,rid),v in merged.items():
   row=byid[t][rid];v={k:z for k,z in v.items() if row[k]!=z}
   if not v:continue
   assert set(v)<=set(schema[t])
   for k,z in v.items():
    if z is None:assert schema[t][k]['Null']=='YES'
    if isinstance(z,str):
     m=re.match(r'(?:var)?char\((\d+)\)',schema[t][k]['Type']);assert not m or len(z)<=int(m[1]),(t,rid,k,len(z))
    if schema[t][k]['Type'].startswith('decimal') and z is not None:v[k]=D(z)
   v={k:z for k,z in v.items() if row[k]!=z}
   if not v:continue
   changes.append({'table':t,'id':rid,'before':{k:row[k] for k in v},'after':v})
   batches[(t,tuple(v.items()))].append(rid)
  for (t,values),ids in batches.items():
   for start in range(0,len(ids),400):
    chunk=ids[start:start+400]
    sql='UPDATE `'+t+'` SET '+','.join('`'+k+'`=%s' for k,z in values)+',update_time=update_time WHERE id IN ('+','.join(['%s']*len(chunk))+')'
    args=tuple(z for k,z in values)+tuple(chunk);q.execute(sql,args);assert q.rowcount==len(chunk),(t,q.rowcount,len(chunk));sql_log.append(q.mogrify(sql,args)+';')
  expected={(x['table'],x['id']):x['after'] for x in changes}
  for t,rr in before.items():
   if t in ['mes_md_unit_measure','qc_report_template_version']:continue
   q.execute('SELECT * FROM `'+t+'` ORDER BY id');after=q.fetchall();assert len(after)==len(rr)
   for old,new in zip(rr,after):
    exp={**old,**expected.get((t,old['id']),{})}
    for k,z in new.items():
     if isinstance(z,D):assert z==D(exp[k]),(t,old['id'],k)
     elif schema[t].get(k,{}).get('Type','').startswith('bit') and k in expected.get((t,old['id']),{}):assert z==bytes([exp[k]])
     else:assert z==exp[k],(t,old['id'],k)
  for t,h in protected.items():q.execute('SELECT * FROM `'+t+'` ORDER BY id');assert fingerprint(q.fetchall())==h,t
  q.execute('SELECT id,parent_id,item_type,result_type FROM mes_qc_indicator WHERE deleted=0');ind={x['id']:x for x in q.fetchall()}
  for row in ind.values():
   if row['item_type']==2:assert row['result_type'] is None and row['parent_id']==0
   else:assert row['result_type'] in [1,2,3] and row['parent_id'] in ind and ind[row['parent_id']]['item_type']==2
  for t in ['mes_qc_template_indicator','mes_qc_iqc_line','mes_qc_ipqc_line','mes_qc_oqc_line','mes_qc_indicator_result_detail']:
   q.execute('SELECT indicator_id FROM `'+t+'` WHERE deleted=0');assert all(x['indicator_id'] in ind for x in q.fetchall()),t
  q.execute('SELECT * FROM qc_report_template_version ORDER BY id');versions=q.fetchall();assert len(versions)==len(existing_versions)+1
  assert versions[:-1]==existing_versions
  result={'applied':apply,'changes':changes,'counts':dict(collections.Counter(x['table'] for x in changes)),'new_units':new_units,'unit_ids':unitids,'report_template_version':{'id':new_version_id,'template_id':upgrade['template_id'],'version':upgrade['version']},'reports_updated':len(reports),'invalidated_pdf_attachments':old_attachments,'protected_hashes':protected,'verified_before_commit':True}
  dump('result-'+('applied' if apply else 'preview')+'.json',result)
  (P/('applied.sql' if apply else 'preview.sql')).write_text('START TRANSACTION;\n'+'\n'.join(sql_log)+'\nCOMMIT;\n',encoding='utf8')
  if apply:c.commit()
  else:c.rollback()
  print(json.dumps({k:result[k] for k in ['applied','counts','new_units','reports_updated','invalidated_pdf_attachments','verified_before_commit']},ensure_ascii=False))
 except:c.rollback();raise
 finally:q.execute('SELECT RELEASE_LOCK(%s)',('qc-indicator-standard-optimize',))
c.close()