gaoluyang
12 小时以前 e40a608ef9bf51db294d8eefada6a8e2e80cebca
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"""Copy only this import to the authorized remote database; map every business FK."""
from pathlib import Path
import sys,json,re
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;E=json.loads((P/'sync-export.json').read_text(encoding='utf-8'));data=E['data'];refs=E['references'];APPLY='--apply' in sys.argv
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=15,read_timeout=90,autocommit=False)
maps={};before=[];sqls=[];created={};schemas={};deferred=[];code_map={};result={}
def dump(name,v):(P/name).write_text(json.dumps(v,ensure_ascii=False,indent=2,default=str),encoding='utf-8')
def rows(table):q.execute('select * from `'+table+'` where deleted=0');return q.fetchall()
def schema(table):
 if table not in schemas:q.execute('show full columns from `'+table+'`');schemas[table]={r['Field']:r for r in q.fetchall()}
 return schemas[table]
def raw_update(table,rid,values):
 q.execute('select * from `'+table+'` where id=%s for update',(rid,));before.append({'table':table,'row':q.fetchone()})
 sql='update `'+table+'` set '+','.join('`'+k+'`=%s' for k in values)+' where id=%s';args=list(values.values())+[rid]
 q.execute(sql,args);sqls.append(q.mogrify(sql,args)+';')
def insert(table,values,source_id=None):
 v={k:z for k,z in values.items() if k!='id'};f=schema(table);assert not set(v)-set(f),(table,'missing columns',set(v)-set(f))
 for k,z in list(v.items()):
  if isinstance(z,str) and z in ("b'\\x00'","b'\\x01'"):v[k]=int(z=="b'\\x01'")
  if isinstance(v[k],str):
   m=re.match(r'varchar\((\d+)\)',f[k]['Type']);assert not m or len(v[k])<=int(m[1]),(table,k,len(v[k]),m[1])
 sql='insert into `'+table+'` (`'+'`,`'.join(v)+'`) values('+','.join(['%s']*len(v))+')';args=list(v.values());q.execute(sql,args);rid=q.lastrowid
 created.setdefault(table,[]).append(rid);sqls.append(q.mogrify(sql,args)+';')
 if source_id is not None:maps.setdefault(table,{})[source_id]=rid
 return rid
def mapped(table,rid):
 if rid is None:return None
 if rid==0:return 0
 assert rid in maps.get(table,{}),(table,'unmapped reference',rid)
 return maps[table][rid]
FK={
 'crm_sale_quotation':{'customer_id':'crm_customer','owner_user_id':'system_users','contract_id':'crm_contract'},
 'crm_contract':{'customer_id':'crm_customer','owner_user_id':'system_users','sign_user_id':'system_users','order_id':'erp_sale_order'},
 'erp_sale_order':{'customer_id':'crm_customer','contract_id':'crm_contract','sale_user_id':'system_users'},
 'crm_receivable_plan':{'customer_id':'crm_customer','contract_id':'crm_contract','owner_user_id':'system_users'},
 'crm_sale_quotation_product':{'quotation_id':'crm_sale_quotation','item_id':'mdm_item','item_unit_id':'mdm_unit_measure'},
 'crm_contract_product':{'contract_id':'crm_contract','item_id':'mdm_item','item_unit_id':'mdm_unit_measure'},
 'erp_sale_order_items':{'order_id':'erp_sale_order','product_id':'mdm_item','product_unit_id':'mdm_unit_measure','mps_id':'mes_pro_mps'},
 'mes_pro_work_order':{'product_id':'mes_md_item','route_id':'mes_pro_route','client_id':'crm_customer'},
 'mes_pro_mps':{'sale_order_id':'erp_sale_order','sale_order_item_id':'erp_sale_order_items','product_id':'mes_md_item','unit_measure_id':'mes_md_unit_measure','client_id':'crm_customer','work_order_id':'mes_pro_work_order'},
 'mes_pro_work_order_process':{'work_order_id':'mes_pro_work_order','process_id':'mes_pro_process','param_template_id':'mes_pd_process_param_template','next_process_id':'mes_pro_process'},
 'mes_pro_work_order_bom':{'work_order_id':'mes_pro_work_order','item_id':'mes_md_item','process_id':'mes_pro_process'},
 'mes_pro_task':{'work_order_id':'mes_pro_work_order','workstation_id':'mes_md_workstation','route_id':'mes_pro_route','process_id':'mes_pro_process','param_template_id':'mes_pd_process_param_template','item_id':'mes_md_item','client_id':'crm_customer'},
 'mes_pro_feedback':{'work_order_id':'mes_pro_work_order','task_id':'mes_pro_task','workstation_id':'mes_md_workstation','route_id':'mes_pro_route','process_id':'mes_pro_process','item_id':'mes_md_item','feedback_user_id':'system_users','approve_user_id':'system_users'},
 'mes_pro_work_order_param_record':{'work_order_id':'mes_pro_work_order','task_id':'mes_pro_task','template_id':'mes_pd_process_param_template'},
 'mes_pro_work_order_param_record_detail':{'record_id':'mes_pro_work_order_param_record','unit_measure_id':'mes_md_unit_measure'},
 'mes_pro_task_assignee':{'task_id':'mes_pro_task','user_id':'system_users'},
 'mes_pro_route_process':{'route_id':'mes_pro_route','process_id':'mes_pro_process','param_template_id':'mes_pd_process_param_template','next_process_id':'mes_pro_process'},
 'mes_pro_route_product':{'route_id':'mes_pro_route','item_id':'mes_md_item'}}
def prepare(table,row):
 v=dict(row)
 for k,t in FK.get(table,{}).items():
  if v.get(k) is not None:
   if v[k] not in maps.get(t,{}):deferred.append((table,row['id'],k,t,v[k]));v[k]=None
   else:v[k]=mapped(t,v[k])
 for k in ['creator','updater']:
  if str(v.get(k,'')).isdigit():v[k]=str(mapped('system_users',int(v[k])))
 if v.get('output_item_id'):v['output_item_id']=','.join(str(mapped('mes_md_item',int(i.strip()))) for i in v['output_item_id'].split(','))
 if table=='mes_pro_work_order_param_record':v['record_no']='TASK-'+str(v['task_id'])
 if table=='mes_pro_work_order_param_record_detail':
  # Template detail ids are explicitly verified below against parameter code and template.
  v['template_detail_id']=mapped('mes_pd_process_param_template_detail',v.get('template_detail_id'))
 return v
 
try:
 with c.cursor() as q:
  q.execute('select database() db,get_lock(%s,15) locked',('ecommerce-production-sync-20261015',));state=q.fetchone();assert state['db']=='mom-hm' and state['locked']==1
  q.execute('select count(*) n from crm_sale_quotation where deleted=0 and remark like %s',('%来源:10月15日电商未缴库计划%',));assert q.fetchone()['n']==0,'Remote import already exists; do not duplicate'
  # Inspect field width before any writes. Widen only the known template/snapshot mismatch.
  f=schema('mes_pro_work_order_param_record_detail')['remark'];width=int(re.search(r'\((\d+)\)',f['Type']).group(1))
  if width<500:
   if not APPLY:print('Schema prerequisite: remark varchar(255) -> varchar(500)')
   else:
    dump('remote-schema-before.json',f)
    q.execute("alter table mes_pro_work_order_param_record_detail modify column remark varchar(500) character set utf8mb4 collate utf8mb4_unicode_ci null comment '备注(参数快照)'")
    schemas.pop('mes_pro_work_order_param_record_detail',None)
  protected={}
  for t in ['crm_receivable','mes_wm_transaction','mes_wm_material_stock','mes_wm_product_receipt','mes_wm_product_sales']:
   q.execute('select count(*) n from `'+t+'`');protected[t]=q.fetchone()['n']
  # Match shared masters by stable code (units by trimmed name), never by local primary key alone.
  for table,key in [('system_users','nickname'),('crm_customer','name'),('mdm_unit_measure','name'),('mes_md_unit_measure','name'),('mes_pro_process','code'),('mes_md_workstation','code'),('mes_dv_machinery','code'),('mes_pd_process_param_template','template_code'),('mes_md_auto_code_rule','code'),('mes_pro_route','code'),('mdm_item','code'),('mes_md_item','code')]:
   remote=rows(table);lookup={}
   for v in remote:lookup.setdefault(str(v.get(key) or '').strip(),[]).append(v)
   maps.setdefault(table,{})
   for local in refs[table]:
    match=lookup.get(str(local.get(key) or '').strip(),[])
    if len(match)==1:maps[table][local['id']]=match[0]['id']
   if table in ['system_users','mdm_unit_measure','mes_md_unit_measure']:
    # Users/unit ids may share names; exact identity is safe when both id and name agree.
    for local in refs[table]:
     exact=next((v for v in remote if v['id']==local['id'] and str(v.get(key) or '').strip()==str(local.get(key) or '').strip()),None)
     if exact:maps[table][local['id']]=exact['id']
  assert mapped('crm_customer',1613)
  for uid in [244,291]:assert mapped('system_users',uid)
  new_codes={'12100271','UC000017','U2100282'}
  original_updates=E['local_result']['updates']
  for table in ['mdm_item','mes_md_item']:
   for row in data[table]:
    rid=maps.get(table,{}).get(row['id'])
    if rid:
     values={k:row[k] for k in ['name','specification','unit_measure_id']}
     unit_table='mdm_unit_measure' if table=='mdm_item' else 'mes_md_unit_measure';values['unit_measure_id']=mapped(unit_table,row['unit_measure_id'])
     q.execute('select * from `'+table+'` where id=%s for update',(rid,));remote=q.fetchone()
     diff={k:v for k,v in values.items() if remote.get(k)!=v}
     if diff:
      originals=[x['before'] for x in original_updates if x['table']==table and x['id']==row['id']]
      assert originals or row['code'] in new_codes,(table,row['code'],'no authorized source change')
      for k in diff:
       permitted=[o.get(k) for o in originals]+[row.get(k)]
       assert remote.get(k) in permitted,(table,row['code'],k,'remote changed independently',remote.get(k),permitted)
      raw_update(table,rid,diff)
    else:
     assert row['code'] in new_codes,(table,row['code'],'missing existing master')
     v=dict(row);v['unit_measure_id']=mapped('mdm_unit_measure' if table=='mdm_item' else 'mes_md_unit_measure',row['unit_measure_id'])
     if table=='mes_md_item':v['mdm_item_id']=mapped('mdm_item',row['mdm_item_id'])
     insert(table,v,row['id'])
  for row in data['mes_pro_route']:
   if row['id'] not in maps['mes_pro_route']:insert('mes_pro_route',row,row['id'])
  for row in data['mes_pro_route_process']:
   v=prepare('mes_pro_route_process',row);q.execute('select id from mes_pro_route_process where route_id=%s and process_id=%s and deleted=0',(v['route_id'],v['process_id']));existing=q.fetchone()
   if not existing:insert('mes_pro_route_process',v,row['id'])
   else:maps.setdefault('mes_pro_route_process',{})[row['id']]=existing['id']
  for row in data['mes_pro_route_product']:
   v=prepare('mes_pro_route_product',row);q.execute('select id from mes_pro_route_product where route_id=%s and item_id=%s and deleted=0',(v['route_id'],v['item_id']));existing=q.fetchone()
   if not existing:insert('mes_pro_route_product',v,row['id'])
  # Verify parameter detail references using template + code, including the full source remark.
  q.execute('select * from mes_pd_process_param_template_detail where deleted=0');remote_details=q.fetchall()
  audit=json.loads((P/'audit.json').read_text(encoding='utf-8'));maps['mes_pd_process_param_template_detail']={}
  for record in data['mes_pro_work_order_param_record_detail']:
   src=next(x for x in audit['mes_pd_process_param_template_detail'] if x['id']==record['template_detail_id'])
   matches=[x for x in remote_details if x['template_id']==mapped('mes_pd_process_param_template',src['template_id']) and x['param_code']==src['param_code']]
   assert len(matches)==1,('parameter detail mismatch',src['id']);maps['mes_pd_process_param_template_detail'][src['id']]=matches[0]['id']
  # Check codes before writes; unrelated remote documents must never be replaced.
  code_tables={'crm_sale_quotation':'no','crm_contract':'no','erp_sale_order':'no','mes_pro_work_order':'code','mes_pro_mps':'code','mes_pro_task':'code','mes_pro_feedback':'code'}
  for table,col in code_tables.items():
   for row in data[table]:
    q.execute('select id from `'+table+'` where `'+col+'`=%s',(row[col],));assert not q.fetchone(),(table,row[col],'code conflict; regenerate against remote before sync')
  for table in ['crm_sale_quotation','crm_contract','erp_sale_order','crm_receivable_plan','crm_sale_quotation_product','crm_contract_product','erp_sale_order_items','mes_pro_work_order','mes_pro_mps','mes_pro_work_order_process','mes_pro_work_order_bom','mes_pro_task','mes_pro_feedback','mes_pro_work_order_param_record','mes_pro_task_assignee','mes_pro_work_order_param_record_detail']:
   for row in data[table]:insert(table,prepare(table,row),row['id'])
  for table,local_id,key,target,local_fk in deferred:raw_update(table,mapped(table,local_id),{key:mapped(target,local_fk)})
  for row in data['crm_permission']:
   table={9:'crm_sale_quotation',5:'crm_contract',8:'crm_receivable_plan'}[row['biz_type']]
   v=dict(row);v['biz_id']=mapped(table,row['biz_id']);v['user_id']=mapped('system_users',row['user_id']);insert('crm_permission',v,row['id'])
  for row in data['mes_md_auto_code_record']:
   v=dict(row);v['rule_id']=mapped('mes_md_auto_code_rule',row['rule_id']);q.execute('select id from mes_md_auto_code_record where rule_id=%s and result=%s and deleted=0',(v['rule_id'],v['result']));reservation=q.fetchone()
   if reservation:maps.setdefault('mes_md_auto_code_record',{})[row['id']]=reservation['id']
   else:insert('mes_md_auto_code_record',v,row['id'])
  # Cross-check every product and every financial chain within the same transaction.
  for row in data['mes_pro_mps']:
   q.execute('select m.quantity,m.unit_measure_id,l.count,l.product_unit_id,l.mps_id,w.quantity wo_qty,w.parent_id from mes_pro_mps m join erp_sale_order_items l on l.id=m.sale_order_item_id join mes_pro_work_order w on w.id=m.work_order_id where m.id=%s',(mapped('mes_pro_mps',row['id']),));v=q.fetchone();assert v['quantity']==v['count']==v['wo_qty']==D(str(row['quantity'])) and v['unit_measure_id']==v['product_unit_id'] and v['mps_id']==mapped('mes_pro_mps',row['id']) and v['parent_id']==0
  total=D(0)
  for group in E['local_result']['groups'].values():
   values=[]
   for table,field,totalfield in [('crm_sale_quotation','quotation_id','total_price'),('crm_contract','contract_id','total_price'),('erp_sale_order','order_id','total_price'),('crm_receivable_plan','receivable_plan_id','price')]:
    q.execute('select `'+totalfield+'` amount from `'+table+'` where id=%s',(mapped(table,group[field]),));values.append(q.fetchone()['amount'])
   assert all(x==D(group['amount']) for x in values);total+=values[0]
  assert total==D('2062353.18')
  for table,count in protected.items():q.execute('select count(*) n from `'+table+'`');assert q.fetchone()['n']==count,(table,'unrelated inventory/receipt changed')
  # Inspect active resource collisions rather than silently overwriting an online schedule.
  new_tasks=created['mes_pro_task'];q.execute('select * from mes_pro_task where deleted=0 and (status is null or status not in (4,5))');active=q.fetchall()
  for t in active:
   if t['id'] not in new_tasks:continue
   for other in active:
    if other['id']==t['id'] or other['workstation_id']!=t['workstation_id']:continue
    assert not (other['start_time'] and other['end_time'] and t['start_time']<other['end_time'] and other['start_time']<t['end_time']),('online resource conflict',t['code'],other['code'])
  result={'applied':APPLY,'host':'36.213.150.237:10000','database':'mom-hm','total':str(total),'maps':maps,'created':created,'protected_counts':protected,'source_codes':{t:[x[col] for x in data[t]] for t,col in code_tables.items()}}
  dump('remote-before-apply.json' if APPLY else 'remote-before-preview.json',before)
  dump('remote-result-applied.json' if APPLY else 'remote-result-preview.json',result)
  (P/('remote-applied.sql' if APPLY else 'remote-preview.sql')).write_text('START TRANSACTION;\n'+'\n'.join(sqls)+'\n'+('COMMIT;' if APPLY else 'ROLLBACK;'),encoding='utf-8')
  if APPLY:c.commit()
  else:c.rollback()
  print(json.dumps({'applied':APPLY,'counts':{t:len(v) for t,v in created.items()},'total':str(total)},ensure_ascii=False))
finally:
 c.rollback()
 with c.cursor() as q:q.execute('select release_lock(%s)',('ecommerce-production-sync-20261015',))
 c.close()