import sys, json, csv, argparse from collections import defaultdict, Counter from pathlib import Path from datetime import datetime sys.stdout.reconfigure(encoding='utf-8') ROOT = Path(__file__).resolve().parent sys.path.insert(0, str(ROOT.parent / 'iqc-import-20261012/deps')) import pymysql FIELDS = ('mobile', 'telephone', 'email') POSITION_RANK = { '总经理': 50, '采购经理': 40, '供应链经理': 40, '商品部经理': 40, '采购主管': 35, '商务主管': 35, '店长': 30, '门店经理': 30, '药房负责人': 30, '办公室主任': 25, '财务负责人': 25, '质量负责人': 25, '库房主管': 25, '采购员': 10, } RULE = '关键决策人 > 职位层级 > 同客户直属下属数量 > 联系方式完整度 > 最近跟进时间 > 最近更新时间 > 联系人ID升序;三字段取同一联系人,空值保留客户原值' def clean(value): return value.strip() if isinstance(value, str) else value def rank(contact, children): return (int(contact.get('master') == 1), POSITION_RANK.get(clean(contact.get('post')), 0), children[contact['id']], sum(bool(clean(contact.get(field))) for field in FIELDS), contact.get('contact_last_time') or datetime.min, contact.get('update_time') or datetime.min, -contact['id']) def build_plan(customers, contacts): grouped = defaultdict(list) for contact in contacts: grouped[contact['customer_id']].append(contact) result = [] for customer in customers: candidates = grouped.get(customer['id'], []) if not candidates: continue candidate_ids = {c['id'] for c in candidates} children = Counter(c['parent_id'] for c in candidates if c['parent_id'] in candidate_ids) chosen = max(candidates, key=lambda c: rank(c, children)) values = {field: clean(chosen[field]) if clean(chosen.get(field)) else customer[field] for field in FIELDS} changed = [field for field in FIELDS if values[field] != customer[field]] result.append({'customer_id': customer['id'], 'customer_name': customer['name'], 'contact_id': chosen['id'], 'contact_name': chosen['name'], 'master': chosen['master'], 'post': chosen['post'], 'candidate_count': len(candidates), 'old': {field: customer[field] for field in FIELDS}, 'new': values, 'changed': changed}) return result def write_json(path, value): path.write_text(json.dumps(value, ensure_ascii=False, default=str, indent=2), encoding='utf-8') def main(): parser = argparse.ArgumentParser() parser.add_argument('--apply', action='store_true') options = parser.parse_args() password = sys.stdin.readline().strip() args = dict(host='36.213.150.237', port=10000, user='root', password=password, database='mom-hm', charset='utf8mb4', connect_timeout=15, read_timeout=60, cursorclass=pymysql.cursors.DictCursor, autocommit=False) connection = pymysql.connect(**args) try: with connection.cursor() as cursor: cursor.execute('SELECT DATABASE() AS db, NOW() AS now') state = cursor.fetchone() assert state['db'] == 'mom-hm' lock = ' FOR UPDATE' if options.apply else '' # Lock both source and target to keep selection and writes consistent. cursor.execute('SELECT * FROM crm_customer ORDER BY id' + lock) all_customers = cursor.fetchall() cursor.execute('SELECT * FROM crm_contact ORDER BY id' + lock) all_contacts = cursor.fetchall() customers = [row for row in all_customers if row['deleted'] == 0] customer_ids = {row['id'] for row in customers} contacts = [row for row in all_contacts if row['deleted'] == 0 and row['customer_id'] in customer_ids] plan = build_plan(customers, contacts) key_counts = Counter(row['customer_id'] for row in contacts if row['master'] == 1) changes = [row for row in plan if row['changed']] summary = {'database': 'mom-hm', 'rule': RULE, 'active_customers': len(customers), 'active_contacts': sum(row['deleted'] == 0 for row in all_contacts), 'matched_customers': len(plan), 'multiple_contact_customers': sum(row['candidate_count'] > 1 for row in plan), 'updated_customers': len(changes), 'field_updates': {field: sum(field in row['changed'] for row in changes) for field in FIELDS}, 'selected_positions': dict(Counter(row['post'] for row in plan)), 'selected_key_decision_makers': sum(row['master'] == 1 for row in plan)} summary['customers_with_multiple_key_decision_makers'] = sum(count > 1 for count in key_counts.values()) if not options.apply: write_json(ROOT / 'preview.json', {'summary': summary, 'plan': plan}) connection.rollback() print(json.dumps(summary, ensure_ascii=False)) return stamp = state['now'].strftime('%Y%m%d-%H%M%S') backup = ROOT / ('backup-' + stamp + '.json') write_json(backup, {'at': state['now'], 'database': state['db'], 'rule': RULE, 'customers': all_customers, 'contacts': all_contacts, 'plan': plan}) for row in changes: cursor.execute('UPDATE crm_customer SET mobile=%s, telephone=%s, email=%s, updater=%s, update_time=%s WHERE id=%s AND deleted=0', tuple(row['new'][field] for field in FIELDS) + ('crm-contact-sync-20261013', state['now'], row['customer_id'])) assert cursor.rowcount == 1 cursor.execute('SELECT * FROM crm_customer ORDER BY id') after = {row['id']: row for row in cursor.fetchall()} expected = {row['customer_id']: row for row in changes} assert len(after) == len(all_customers) for original in all_customers: current = after[original['id']] allowed = set(FIELDS) | {'updater', 'update_time'} if original['id'] in expected else set() assert all(current[key] == value for key, value in original.items() if key not in allowed) if original['id'] in expected: assert all(current[field] == expected[original['id']]['new'][field] for field in FIELDS) cursor.execute('SELECT * FROM crm_contact ORDER BY id') assert cursor.fetchall() == all_contacts connection.commit() summary.update({'committed': True, 'executed_at': str(state['now']), 'backup': str(backup)}) # Reconnect to verify committed values, including unmatched and deleted customer rows. with pymysql.connect(**args) as verify_connection: with verify_connection.cursor() as cursor: cursor.execute('SELECT * FROM crm_customer ORDER BY id') assert {row['id']: row for row in cursor.fetchall()} == after summary['verified_after_commit'] = True write_json(ROOT / 'result.json', summary) with (ROOT / '同步明细.csv').open('w', encoding='utf-8-sig', newline='') as file: writer = csv.writer(file) writer.writerow(['客户ID', '客户', '联系人ID', '选中联系人', '关键决策人', '职位', '候选人数', '原手机号', '同步后手机号', '原电话', '同步后电话', '原邮箱', '同步后邮箱', '变更字段']) for row in plan: writer.writerow([row['customer_id'], row['customer_name'], row['contact_id'], row['contact_name'], row['master'], row['post'], row['candidate_count'], *[value for field in FIELDS for value in (row['old'][field], row['new'][field])], ','.join(row['changed'])]) print(json.dumps(summary, ensure_ascii=False)) except Exception: connection.rollback() raise finally: connection.close() if __name__ == '__main__': main()