云
3 天以前 a87fcc60346dfc799bd3c2cddc7acfffb105cb61
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
import sys,json,csv,argparse,uuid
from pathlib import Path
from datetime import datetime,timedelta
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
ROOT=Path(__file__).resolve().parent
sys.path.insert(0,str(ROOT.parent/'iqc-import-20261012/deps'))
import pymysql
 
def dump(path,value):
    path.write_text(json.dumps(value,ensure_ascii=False,indent=2,default=lambda v: v.hex() if isinstance(v,bytes) else str(v)),encoding='utf-8')
 
def text(value):
    return str(value or '').strip().replace('管理员','管理人员')
 
def make_content(row,biz_type,dicts,contacts):
    kind='客户' if biz_type==2 else '线索'
    starts=['资料跟进','档案梳理','信息整理','跟进资料核对']
    parts=[f'{starts[row["id"]%4]}:已梳理{kind}【{text(row["name"])[:100]}】的现有业务档案。']
    facts=[]
    for field,dt,label in [('industry_id','crm_customer_industry','行业'),('level','crm_customer_level','级别'),('source','crm_customer_source','来源')]:
        value=dicts.get((dt,str(row.get(field))))
        if value: facts.append(label+':'+text(value)[:30])
    if facts: parts.append(';'.join(facts)+'。')
    available=[label for field,label in [('mobile','手机号'),('telephone','电话'),('email','邮箱')] if text(row.get(field))]
    parts.append('档案中已登记'+ '、'.join(available)+'。' if available else '档案中尚未登记手机号、电话和邮箱,后续需补充有效联系方式。')
    if biz_type==2:
        selected=[c for c in contacts.get(row['id'],[]) if c['master']==1]
        if selected:
            c=min(selected,key=lambda c:c['id'])
            parts.append('关键决策人:'+text(c['name'])[:30]+('('+text(c['post'])[:30]+')' if text(c['post']) else '')+'。')
        parts.append('系统登记成交状态:'+('已成交' if row['deal_status']==1 else '未成交')+';后续可结合实际业务核实需求及合作进展。')
    else:
        parts.append('线索尚未转化,后续可核实需求、采购职责及合作条件,具备条件时再推进客户转化。')
    parts.append('本次完成资料梳理并记录待办处理结果。')
    content=''.join(parts)
    assert '管理员' not in content and len(content)<=500
    return content
 
def is_target(row,biz_type,tomorrow):
    if row['deleted']!=0 or row['owner_user_id'] is None: return False
    if biz_type==1: return row['follow_up_status']==0 and row['transform_status']==0
    return row['follow_up_status']==0 or (row['contact_next_time'] is not None and row['contact_next_time']<tomorrow)
 
def main():
    parser=argparse.ArgumentParser();parser.add_argument('--apply',action='store_true');opt=parser.parse_args()
    args=dict(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,connect_timeout=15,read_timeout=120)
    conn=pymysql.connect(**args)
    batch='crm-backlog-'+uuid.uuid4().hex[:24]
    try:
        with conn.cursor() as q:
            q.execute('SELECT DATABASE() db,NOW() now');state=q.fetchone();assert state['db']=='mom-hm';now=state['now'];tomorrow=now.replace(hour=0,minute=0,second=0,microsecond=0)+timedelta(days=1)
            lock=' FOR UPDATE' if opt.apply else ''
            before={}
            for table in ['crm_customer','crm_clue','crm_contact','crm_contract','crm_receivable','crm_receivable_plan','system_dict_data']:
                q.execute('SELECT * FROM '+table+' ORDER BY id'+lock);before[table]=q.fetchall()
            dictionaries={(r['dict_type'],r['value']):r['label'] for r in before['system_dict_data'] if r['deleted'] in (0,b'\x00') and r['status']==0}
            contacts=defaultdict(list)
            for row in before['crm_contact']:
                if row['deleted']==0: contacts[row['customer_id']].append(row)
            plan=[]
            for table,biz_type in [('crm_customer',2),('crm_clue',1)]:
                for row in before[table]:
                    if not is_target(row,biz_type,tomorrow): continue
                    next_time=row['contact_next_time'] if row['contact_next_time'] is not None and row['contact_next_time']>=tomorrow else None
                    plan.append(dict(table=table,biz_type=biz_type,id=row['id'],name=row['name'],content=make_content(row,biz_type,dictionaries,contacts),next_time=next_time,old=row))
            summary=dict(database=state['db'],customers=sum(p['biz_type']==2 for p in plan),clues=sum(p['biz_type']==1 for p in plan),records=len(plan),cleared_schedules=sum(p['old']['contact_next_time'] is not None and p['next_time'] is None for p in plan),preserved_future_schedules=sum(p['next_time'] is not None for p in plan))
            if not opt.apply:
                dump(ROOT/'backlog-preview.json',{'summary':summary,'plan':plan});conn.rollback()
                print(json.dumps({'summary':summary,'examples':[{'type':p['biz_type'],'content':p['content']} for p in plan[:2]]},ensure_ascii=False));return
            q.execute('SELECT MAX(id) max_id,COUNT(*) count FROM crm_follow_up_record');follow_before=q.fetchone()
            q.execute('SELECT MAX(id) max_id,COUNT(*) count FROM system_operate_log');log_before=q.fetchone()
            backup=ROOT/('backlog-backup-'+now.strftime('%Y%m%d-%H%M%S')+'.json')
            dump(backup,{'database':state['db'],'batch':batch,'before':before,'plan':plan,'follow_before':follow_before,'log_before':log_before})
            # Use a truthful record type instead of recording unperformed phone calls or visits.
            existing=[r for r in before['system_dict_data'] if r['dict_type']=='crm_follow_up_type' and r['label']=='资料梳理' and r['deleted'] in (0,b'\x00') and r['status']==0]
            if existing: follow_type=int(existing[0]['value']);dict_id=None
            else:
                used={int(r['value']) for r in before['system_dict_data'] if r['dict_type']=='crm_follow_up_type' and str(r['value']).isdigit()}
                follow_type=max(used or {0})+1
                q.execute('INSERT INTO system_dict_data (sort,label,value,dict_type,status,color_type,remark,creator,updater,create_time,update_time) VALUES (%s,%s,%s,%s,0,%s,%s,%s,%s,%s,%s)',(follow_type,'资料梳理',str(follow_type),'crm_follow_up_type','info','基于现有档案的资料整理及营销待办处理','0','0',now,now));dict_id=q.lastrowid
            inserted=[]
            for p in plan:
                q.execute('INSERT INTO crm_follow_up_record (biz_type,biz_id,type,content,next_time,pic_urls,business_ids,contact_ids,creator,updater,create_time,update_time,deleted) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0)',(p['biz_type'],p['id'],follow_type,p['content'],p['next_time'],'','','','0','0',now,now));follow_id=q.lastrowid
                q.execute('UPDATE '+p['table']+' SET follow_up_status=1,contact_last_time=%s,contact_last_content=%s,contact_next_time=%s,updater=%s,update_time=%s WHERE id=%s AND deleted=0',(now,p['content'],p['next_time'],'0',now,p['id']));assert q.rowcount==1
                kind='客户' if p['biz_type']==2 else '线索'
                action=kind+'跟进【'+text(p['name'])+'】:批量完成资料梳理,新增跟进记录并更新待办状态。'
                extra=json.dumps(dict(batch=batch,execution='database_batch',source='user_request',follow_record_id=follow_id,content=p['content'],old_follow_up_status=p['old']['follow_up_status'],old_next_time=str(p['old']['contact_next_time']),next_time=str(p['next_time'])),ensure_ascii=False)
                assert '管理员' not in action+extra and len(extra)<=2000
                # user_id=0 / creator=0 explicitly denotes batch execution, without impersonating a salesperson.
                q.execute('INSERT INTO system_operate_log (trace_id,user_id,user_type,type,sub_type,biz_id,action,success,extra,request_method,request_url,user_ip,user_agent,creator,updater,create_time,update_time,deleted,tenant_id) VALUES (%s,0,2,%s,%s,%s,%s,1,%s,%s,%s,NULL,%s,%s,%s,%s,%s,0,0)',(batch,'CRM '+kind,kind+'跟进',p['id'],action,extra,'BATCH','batch://crm/backlog/follow-up','Codex database batch','0','0',now,now));inserted.append(dict(biz_type=p['biz_type'],biz_id=p['id'],follow_id=follow_id,log_id=q.lastrowid))
            expected={(p['table'],p['id']):p for p in plan};allowed={'follow_up_status','contact_last_time','contact_last_content','contact_next_time','updater','update_time'}
            for table in ['crm_customer','crm_clue','crm_contact','crm_contract','crm_receivable','crm_receivable_plan']:
                q.execute('SELECT * FROM '+table+' ORDER BY id');after=q.fetchall();assert len(after)==len(before[table])
                for old,current in zip(before[table],after):
                    p=expected.get((table,old['id']));assert all(current[k]==v for k,v in old.items() if not p or k not in allowed)
                    if p: assert current['follow_up_status']==1 and current['contact_last_content']==p['content'] and current['contact_next_time']==p['next_time']
            for item in inserted:
                q.execute('SELECT biz_type,biz_id,content FROM crm_follow_up_record WHERE id=%s',(item['follow_id'],));r=q.fetchone();p=expected[('crm_customer' if item['biz_type']==2 else 'crm_clue',item['biz_id'])];assert r==dict(biz_type=p['biz_type'],biz_id=p['id'],content=p['content'])
            q.execute('SELECT COUNT(*) count FROM system_operate_log WHERE trace_id=%s',(batch,));assert q.fetchone()['count']==len(plan)
            dump(ROOT/'backlog-inserted-ids.json',dict(batch=batch,dict_id=dict_id,records=inserted))
            conn.commit()
            with pymysql.connect(**args) as verify:
                with verify.cursor() as v:
                    v.execute('SELECT COUNT(*) count FROM system_operate_log WHERE trace_id=%s',(batch,));assert v.fetchone()['count']==len(plan)
                    for table,biz_type in [('crm_customer',2),('crm_clue',1)]:
                        v.execute('SELECT * FROM '+table+' ORDER BY id');rows=v.fetchall();assert not any(is_target(r,biz_type,tomorrow) for r in rows)
            summary.update(committed=True,verified_after_commit=True,batch=batch,backup=str(backup),follow_type=follow_type,follow_type_label='资料梳理',operation_logs=len(inserted),executed_at=str(now))
            dump(ROOT/'backlog-result.json',summary)
            with (ROOT/'待办跟进明细.csv').open('w',encoding='utf-8-sig',newline='') as f:
                w=csv.writer(f);w.writerow(['类型','编号','名称','跟进内容','下次联系时间','跟进记录ID','操作日志ID'])
                for p,ids in zip(plan,inserted):w.writerow(['客户' if p['biz_type']==2 else '线索',p['id'],p['name'],p['content'],p['next_time'],ids['follow_id'],ids['log_id']])
            print(json.dumps(summary,ensure_ascii=False))
    except Exception:
        conn.rollback();raise
    finally:conn.close()
 
if __name__=='__main__':main()