# -*- coding: utf-8 -*- """acceptance-probe-testenv.py — 测试环境(https://ai.itc.vip/crm-api)验收前探针。 目的:验收开始前一次性回答四件事—— 1) 服务可达(ping 401 = 鉴权门活着,属正常) 2) verify profile / debug token 可用(demo 免登前提) 3) 数据状态:mine/pool 存量 + 四个关键字典组(含 W-01 contact_source 观察点) 4) 构建新鲜度判别(D-03 无写库副作用):POST /api/customer/edit 不带 version → 67005 = 含 customer-defectfix 修复的新构建;67001+「乐观锁」= 旧构建(验收会全部误报未修) 只读探测:edit 探针在 CAS 校验前置失败,不产生任何写库。 用法:py -X utf8 .scratch/customer-defectfix/acceptance-probe-testenv.py 产物:acceptance-probe-testenv-result.json(与本脚本同目录) """ import io import json import os import sys import time sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') import requests BASE = 'https://ai.itc.vip/crm-api' ADMIN = '739564171091247104' # 罗伟健(admin) OUT = os.path.dirname(os.path.abspath(__file__)) RESULT = os.path.join(OUT, 'acceptance-probe-testenv-result.json') ev = {'base': BASE, 'ts': time.strftime('%Y-%m-%d %H:%M:%S')} VERDICT = [] def note(msg): print(msg) VERDICT.append(msg) # ---- 1. ping(鉴权门)---- try: r = requests.get(f'{BASE}/api/customer/ping', timeout=15) ev['ping_http'] = r.status_code note(f"1. ping → HTTP {r.status_code}" + ("(401 = 鉴权门活着,服务在)" if r.status_code == 401 else "")) except Exception as e: ev['ping_error'] = str(e)[:200] note(f"1. ping 连接失败: {str(e)[:160]} —— 测试环境不通(nginx/FRP/服务链路),验收无法开始") json.dump(ev, open(RESULT, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) sys.exit(1) # ---- 2. debug token ---- tok = None try: r = requests.get(f'{BASE}/api/auth/debug/token', params={'userId': ADMIN}, timeout=15) body = r.json() if r.status_code == 200 else {} d = body.get('data') tok = d if isinstance(d, str) else (d or {}).get('token') ev['debug_token_http'] = r.status_code except Exception as e: ev['debug_token_error'] = str(e)[:200] if not tok: note("2. debug token 不可用 —— verify profile 未激活或端点被拦;demo 免登会失败,需检查测试环境启动配置") json.dump(ev, open(RESULT, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) sys.exit(1) note("2. debug token OK(verify profile 激活,demo 可免登)") H = {'Authorization': f'Bearer {tok}'} S = requests.Session() S.headers.update(H) def code_of(r): try: return (r.json() or {}).get('code') except Exception: return None # ---- 3. 数据状态 ---- try: m = S.post(f'{BASE}/api/customer/workspace/mine/page', data={'current': 1, 'size': 3}, timeout=20).json() p = S.post(f'{BASE}/api/customer/workspace/pool/page', data={'current': 1, 'size': 3}, timeout=20).json() md, pd = m.get('data') or {}, p.get('data') or {} ev['mine_total'], ev['pool_total'] = md.get('total'), pd.get('total') note(f"3. 数据态:mine total={md.get('total')} pool total={pd.get('total')}") for row in (md.get('content') or [])[:3]: print(f" - {row.get('customerNo')} {row.get('customerName')}") if not md.get('total') and not pd.get('total'): note(" ⚠ 测试环境无客户数据 —— 需先跑 demo_showcase.py --base https://ai.itc.vip/crm-api --skip-db 造验收数据") for g in ('customer_type', 'follow_way', 'industry', 'contact_source'): try: d = S.get(f'{BASE}/api/dict/item/enabled-list', params={'groupCode': g}, timeout=15).json() items = d.get('data') or [] ev[f'dict_{g}'] = len(items) print(f" dict {g}: {len(items)} 项" + (" ⚠ 空(W-01 同款观察点)" if not items else "")) except Exception as e: print(f" dict {g}: 探测失败 {str(e)[:80]}") except Exception as e: note(f"3. 数据态探测失败: {str(e)[:160]}") # ---- 4. 构建新鲜度判别(D-03,无写库)---- try: rows = ((S.post(f'{BASE}/api/customer/workspace/mine/page', data={'current': 1, 'size': 1}, timeout=20).json()).get('data') or {}).get('content') or [] if not rows: note("4. 构建判别跳过:mine 无客户可取样(先造数再判构建)") else: fid = rows[0]['id'] det = S.get(f'{BASE}/api/customer/detail', params={'id': fid}, timeout=20).json() d = det.get('data') or {} form = { 'customerName': d.get('customerName') or rows[0].get('customerName'), 'customerType': d.get('customerType'), 'provinceCode': d.get('provinceCode'), 'cityCode': d.get('cityCode'), 'districtCode': d.get('districtCode'), 'industryCode': d.get('industryCode'), 'customerStarLevel': d.get('customerStarLevel') or 3, 'relationStarLevel': d.get('relationStarLevel') or 3, 'isBizNegotiated': d.get('isBizNegotiated') if d.get('isBizNegotiated') is not None else 0, 'isChild': d.get('isChild') if d.get('isChild') is not None else 0, } # 刻意不带 version:CAS 前置失败,无写库 t0 = time.time() r = S.post(f'{BASE}/api/customer/edit', params={'id': fid}, data=form, timeout=60) got = code_of(r) ev['d03_probe'] = {'customer_id': str(fid), 'http': r.status_code, 'code': got, 'message': str((r.json() or {}).get('message'))[:80] if r.headers.get('content-type', '').startswith('application/json') else '', 'elapsed_sec': round(time.time() - t0, 2)} msg = ev['d03_probe']['message'] if str(got) == '67005': note("4. 构建判别:缺 version → 67005 —— **含 D-02/03/04/05/07 修复的新构建**,可以开始验收") elif str(got) == '67001' and '乐观锁' in msg: note("4. 构建判别:缺 version → 67001(乐观锁)—— **旧构建(缺陷修复未部署)**,验收前须先发新 jar(mvn clean package → 上传 → deploy/restart.sh)") else: note(f"4. 构建判别结果异常:code={got} msg={msg} —— 需人工看一眼(请求形态可能被必填校验先拦)") except Exception as e: note(f"4. 构建判别失败: {str(e)[:160]}") ev['verdict'] = VERDICT json.dump(ev, open(RESULT, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) print(f"\n证据已落盘 {RESULT}")