# -*- coding: utf-8 -*- """D-07 assignable 前置探测 + D-05/D-04 DB 列证据(customer-defectfix effort)。 用法:py -X utf8 .scratch/customer-defectfix/probe-assignable.py 产物:.scratch/customer-defectfix/probe-assignable-result.json - assignable?id= 计时(timeout=180s,双次),修复前后各跑一轮对比 - SHOW COLUMNS 留证:customer.is_biz_negotiated / customer_import_fail.fail_reason / customer_import_task.fail_reason - collab 样例(e2c-协同-样例)owner 现状(heavy 复跑副作用盲区核对) """ import json import os import sys import time import pymysql import requests BASE = 'http://localhost:8080' ADMIN = '739564171091247104' OUT = os.path.dirname(os.path.abspath(__file__)) RESULT = os.path.join(OUT, 'probe-assignable-result.json') TIMEOUT = 180 COLLAB_OWNER_EXPECT = ('744842318024015872', '曾偲青', '744841292483133440', '职员部') def db(): return pymysql.connect(host='8.129.84.155', port=3306, user='root', password='Itc@123456', database='crm', charset='utf8mb4', autocommit=True, connect_timeout=10, cursorclass=pymysql.cursors.DictCursor) def dbq(sql, args=None): with db() as conn, conn.cursor() as cur: cur.execute(sql, args) return cur.fetchall() def main(): ev = {'phase': sys.argv[1] if len(sys.argv) > 1 else 'unknown', 'ts': time.strftime('%Y-%m-%d %H:%M:%S')} # ---- 1. DB 列证据(D-05 / D-04)---- cols = {} for table, like in (('customer', 'is_biz_negotiated'), ('customer', 'is_child'), ('customer_import_fail', 'fail_reason'), ('customer_import_task', 'fail_reason')): rows = dbq(f"SHOW COLUMNS FROM {table} LIKE '{like}'") c = rows[0] if rows else None cols[f'{table}.{like}'] = ({k: str(v) for k, v in c.items()} if c else None) ev['columns'] = cols print('== SHOW COLUMNS 证据 ==') for k, v in cols.items(): print(f" {k}: type={v and v.get('Type')} null={v and v.get('Null')} default={v and v.get('Default')}") # ---- 2. collab 样例 owner 现状(heavy 副作用盲区)---- collab = dbq("SELECT id, customer_name, owner_user_id, owner_user_name_snapshot, " "owner_dept_id, owner_dept_name_snapshot FROM customer " "WHERE customer_name='e2c-协同-样例' AND deleted=0") ev['collab'] = [{k: str(v) for k, v in r.items()} for r in collab] print('== collab 样例 owner 现状 ==') for r in collab: drift = str(r['owner_user_id']) != COLLAB_OWNER_EXPECT[0] print(f" id={r['id']} owner={r['owner_user_id']}({r['owner_user_name_snapshot']}) " f"dept={r['owner_dept_id']}({r['owner_dept_name_snapshot']}) " f"{'⚠ 漂移' if drift else '正常(曾偲青)'}") # ---- 3. 最新交接单(assignable 探测目标)---- transfers = dbq("SELECT id, transfer_no, status, from_user_id, to_director_id, total_count, " "assigned_count FROM customer_transfer ORDER BY id DESC LIMIT 5") ev['transfers'] = [{k: str(v) for k, v in r.items()} for r in transfers] print('== 最近交接单 ==') for r in transfers: print(f" id={r['id']} no={r['transfer_no']} status={r['status']} " f"director={r['to_director_id']} total={r['total_count']}") if not transfers: print('!! 无交接单可用,无法探测 assignable') json.dump(ev, open(RESULT, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) return tid = transfers[0]['id'] # ---- 4. admin token + assignable 计时(双次)---- tok = None for _ in range(3): try: r = requests.get(f'{BASE}/api/auth/debug/token', params={'userId': ADMIN}, timeout=30) if r.status_code == 200: d = r.json().get('data') tok = d if isinstance(d, str) else (d or {}).get('token') if tok: break except Exception as e: print(f' token 重试: {e}') time.sleep(2) if not tok: print('!! 服务未就绪(debug/token 失败)——请先起服') json.dump(ev, open(RESULT, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) return probes = [] for i in range(2): t0 = time.time() try: r = requests.get(f'{BASE}/api/customer/transfer/assignable', params={'id': tid}, headers={'Authorization': f'Bearer {tok}'}, timeout=TIMEOUT) elapsed = time.time() - t0 body = r.json() if r.status_code == 200 else {} n = len(body.get('data') or []) probes.append({'attempt': i + 1, 'elapsed_sec': round(elapsed, 2), 'http': r.status_code, 'code': body.get('code'), 'users': n}) print(f" assignable #{i+1}: {elapsed:.2f}s http={r.status_code} users={n}") except Exception as e: elapsed = time.time() - t0 probes.append({'attempt': i + 1, 'elapsed_sec': round(elapsed, 2), 'error': str(e)[:120]}) print(f" assignable #{i+1}: 异常 @{elapsed:.2f}s {str(e)[:100]}") time.sleep(2) ev['assignable_probes'] = probes ev['assignable_target_transfer'] = str(tid) # ---- 5. N+1 规模参考:总监子树用户/部门数(WITH RECURSIVE)---- try: drow = dbq("SELECT dept_id FROM crm_auth_user WHERE id=%s", (transfers[0]['to_director_id'],)) if drow and drow[0]['dept_id']: root = drow[0]['dept_id'] sub = dbq("WITH RECURSIVE t AS (SELECT id FROM sys_dept WHERE id=%s " "UNION ALL SELECT d.id FROM sys_dept d JOIN t ON d.parent_id=t.id) " "SELECT COUNT(*) dept_cnt FROM t", (root,)) ucnt = dbq("WITH RECURSIVE t AS (SELECT id FROM sys_dept WHERE id=%s " "UNION ALL SELECT d.id FROM sys_dept d JOIN t ON d.parent_id=t.id) " "SELECT COUNT(*) user_cnt FROM crm_auth_user u " "WHERE u.deleted=0 AND u.dept_id IN (SELECT id FROM t)", (root,)) ev['subtree'] = {'root_dept': str(root), 'dept_cnt': sub[0]['dept_cnt'], 'user_cnt': ucnt[0]['user_cnt']} print(f" 总监子树规模:部门 {sub[0]['dept_cnt']} / 用户 {ucnt[0]['user_cnt']}" f"(N+1 往返基数)") except Exception as e: print(f' 子树规模统计跳过:{e}') json.dump(ev, open(RESULT, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) print(f"证据已落盘 {RESULT}") if __name__ == '__main__': main()