# -*- coding: utf-8 -*- """acceptance-prep-testenv.py — 测试环境验收准备(诊断 + 构建代际判别 + 必要时造数)。 三段: A 诊断:POST /api/customer/workspace/page(workspace=mine/pool)原始响应落盘 B 判别:GET /api/rule/customer/dedup(rework 代际分界,返工新增端点) + create 探针客户 → edit 不带 version(67005=含 defectfix / 67001+乐观锁=旧构建) 探针客户用完即 archive,零活跃残留 C 造数:仅当「新构建 + 库空」时造 6 个 e2c-acc-* 验收客户(3 mine / 2 pool / 1 archived) 旧构建则直接劝返:先发版(mvn clean package → 上传 jar → deploy/restart.sh)再验收 用法:py -X utf8 .scratch/customer-defectfix/acceptance-prep-testenv.py 产物:acceptance-prep-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) BUDDY = '744842318024015872' # 曾偲青(BUDDY) OUT = os.path.dirname(os.path.abspath(__file__)) RESULT = os.path.join(OUT, 'acceptance-prep-testenv-result.json') STAMP = time.strftime('%H%M%S') ev = {'base': BASE, 'ts': time.strftime('%Y-%m-%d %H:%M:%S')} VERDICT = [] def note(msg): print(msg) VERDICT.append(msg) # ---- token ---- r = requests.get(f'{BASE}/api/auth/debug/token', params={'userId': ADMIN}, timeout=15) d = (r.json() or {}).get('data') if r.status_code == 200 else None tok = d if isinstance(d, str) else (d or {}).get('token') if not tok: note('debug token 不可用 —— 先解决测试环境 verify profile 再谈验收') json.dump(ev, open(RESULT, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) sys.exit(1) S = requests.Session() S.headers.update({'Authorization': f'Bearer {tok}'}) def body_of(r): try: return r.json() or {} except Exception: return {'_raw': r.text[:120]} # ---- A. 诊断 workspace/page 原始响应(demo 同款:POST /api/customer/workspace/page + workspace=mine/pool)---- mine_raw = body_of(S.post(f'{BASE}/api/customer/workspace/page', data={'workspace': 'mine', 'current': 1, 'size': 3}, timeout=20)) pool_raw = body_of(S.post(f'{BASE}/api/customer/workspace/page', data={'workspace': 'pool', 'current': 1, 'size': 3}, timeout=20)) ev['mine_raw'] = {k: mine_raw.get(k) for k in ('code', 'message')} ev['mine_raw']['data_keys'] = sorted((mine_raw.get('data') or {}).keys()) if isinstance(mine_raw.get('data'), dict) else str(type(mine_raw.get('data'))) ev['pool_raw'] = {k: pool_raw.get(k) for k in ('code', 'message')} md = mine_raw.get('data') or {} pd = pool_raw.get('data') or {} mine_rows = md.get('content') or md.get('records') or [] pool_rows = pd.get('content') or pd.get('records') or [] mine_total = md.get('total') pool_total = pd.get('total') note(f"A. mine/page code={mine_raw.get('code')} msg={str(mine_raw.get('message'))[:60]} total={mine_total} data_keys={ev['mine_raw']['data_keys']}") note(f" pool/page code={pool_raw.get('code')} msg={str(pool_raw.get('message'))[:60]} total={pool_total}") # ---- B. 构建代际判别 ---- rework_ok = False fix_ok = False probe_id = None # B1. /api/rule/customer/dedup(返工新增端点,e2e-incr DEDUP_G 同款路径) rd = S.get(f'{BASE}/api/rule/customer/dedup', timeout=20) bd = body_of(rd) ev['dedup_probe'] = {'http': rd.status_code, 'code': bd.get('code'), 'data': str(bd.get('data'))[:120]} if rd.status_code == 200 and bd.get('code') in (0, '0', 200, '200'): rework_ok = True note(f"B1. rule/customer/dedup → 200 code=0 —— rework 后契约在(data={str(bd.get('data'))[:80]})") else: note(f"B1. rule/customer/dedup → HTTP {rd.status_code} code={bd.get('code')} —— **pre-rework 旧构建**(该端点不存在)") # B2. 探针客户:库里有现成就用现成的,否则 create 一个 fid = None if isinstance(mine_total, (int, str)) and str(mine_total) not in ('0', 'None') and mine_rows: fid = mine_rows[0]['id'] note(f"B2. 用现有客户做判别样本:{mine_rows[0].get('customerNo')} {mine_rows[0].get('customerName')}") else: dicts = {} for g in ('customer_type', 'industry'): items = (body_of(S.get(f'{BASE}/api/dict/item/enabled-list', params={'groupCode': g}, timeout=15)).get('data') or []) dicts[g] = items ctype = (dicts['customer_type'][0]['code'] if dicts['customer_type'] else 'TYPE_A') ind = (dicts['industry'][0]['code'] if dicts['industry'] else 'I_A') rc = S.post(f'{BASE}/api/customer/create', data={ 'customerName': f'e2c-accprobe-{STAMP}', 'customerType': ctype, 'provinceCode': '440000', 'cityCode': '440100', 'districtCode': '440103', 'industryCode': ind, 'customerStarLevel': 3, 'relationStarLevel': 3, 'isBizNegotiated': 0, 'isChild': 0, 'ownerUserId': ADMIN, }, timeout=30) br = body_of(rc) ev['create_probe'] = {'http': rc.status_code, 'code': br.get('code'), 'message': str(br.get('message'))[:100]} ddata = br.get('data') fid = (ddata or {}).get('id') if isinstance(ddata, dict) else ddata if fid: note(f"B2. create 探针客户 OK id={fid}(扁平契约可用)") else: note(f"B2. create 失败 code={br.get('code')} msg={str(br.get('message'))[:80]} —— 契约形态或构建代际需人工看") # B3. edit 不带 version(defectfix 分界;CAS 前置失败无写库) if fid: det = body_of(S.get(f'{BASE}/api/customer/detail', params={'id': fid}, timeout=20)) dd = det.get('data') or {} form = { 'customerName': dd.get('customerName') or f'e2c-accprobe-{STAMP}', 'customerType': dd.get('customerType'), 'provinceCode': dd.get('provinceCode'), 'cityCode': dd.get('cityCode'), 'districtCode': dd.get('districtCode'), 'industryCode': dd.get('industryCode'), 'customerStarLevel': dd.get('customerStarLevel') or 3, 'relationStarLevel': dd.get('relationStarLevel') or 3, 'isBizNegotiated': dd.get('isBizNegotiated') if dd.get('isBizNegotiated') is not None else 0, 'isChild': dd.get('isChild') if dd.get('isChild') is not None else 0, } re_ = S.post(f'{BASE}/api/customer/edit', params={'id': fid}, data=form, timeout=60) be = body_of(re_) ev['d03_probe'] = {'http': re_.status_code, 'code': be.get('code'), 'message': str(be.get('message'))[:80]} if re_.status_code in (404, 405): note(f"B3. edit → HTTP {re_.status_code} —— **pre-rework 旧构建**(扁平 edit 不存在)") elif str(be.get('code')) == '67005': fix_ok = True note("B3. edit 缺 version → 67005 —— **defectfix 修复已部署**") elif str(be.get('code')) == '67001' and '乐观锁' in str(be.get('message')): note("B3. edit 缺 version → 67001(乐观锁)—— **defectfix 修复未部署(旧构建)**") else: note(f"B3. edit 返回异常形态 code={be.get('code')} msg={str(be.get('message'))[:70]} —— 需人工看") # B4. 探针客户归档清场 if fid and ev.get('create_probe'): ra = S.post(f'{BASE}/api/customer/archive', params={'id': fid}, timeout=20) ev['archive_probe'] = {'http': ra.status_code, 'code': body_of(ra).get('code')} note(f"B4. 探针客户已 archive 清场(code={ev['archive_probe']['code']})") # ---- 结论分支 ---- if not (rework_ok and fix_ok): note("") note("== 结论:测试环境 jar 落后于客户域交付(rework 2026-09-05 / defectfix 2026-09-06)==") note(" 验收前必须先发版:mvn clean package -DskipTests → 上传 crm-app-1.0.0-SNAPSHOT.jar 到服务器项目根 → ./deploy/restart.sh") note(" 发版后重跑本脚本确认 B1/B3 双绿,再进入验收") ev['verdict'] = 'OLD_BUILD' ev['notes'] = VERDICT json.dump(ev, open(RESULT, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) sys.exit(0) # ---- C. 造验收数据(新构建 + 库空才造)---- have = str(mine_total) not in ('None', '0') or str(pool_total) not in ('None', '0') if have: note(f"C. 库里有数据(mine={mine_total} pool={pool_total})—— 不重复造数,直接验收") ev['verdict'] = 'READY_HAS_DATA' else: note("C. 库为空,造一组验收数据:e2c-acc-* ×6(3 mine / 2 pool / 1 archived)") items = body_of(S.get(f'{BASE}/api/dict/item/enabled-list', params={'groupCode': 'customer_type'}, timeout=15)).get('data') or [] inds = body_of(S.get(f'{BASE}/api/dict/item/enabled-list', params={'groupCode': 'industry'}, timeout=15)).get('data') or [] ctype = items[0]['code'] if items else 'TYPE_A' ind = inds[0]['code'] if inds else 'I_A' names = ['恒信达科技', '蓝湾生物', '中晟建材', '启明教育', '云帆物流'] made = [] for i, nm in enumerate(names): star = (i % 5) + 1 rc = S.post(f'{BASE}/api/customer/create', data={ 'customerName': f'e2c-acc-{STAMP}-{nm}', 'customerType': ctype, 'provinceCode': '440000', 'cityCode': '440100', 'districtCode': '440103', 'industryCode': ind, 'customerStarLevel': star, 'relationStarLevel': min(star, 5), 'isBizNegotiated': i % 2, 'isChild': 0, 'ownerUserId': ADMIN, }, timeout=30) br = body_of(rc) ddata = br.get('data') cid = (ddata or {}).get('id') if isinstance(ddata, dict) else ddata if not cid: note(f" x create {nm} 失败 code={br.get('code')} msg={str(br.get('message'))[:60]}") continue made.append(str(cid)) if i >= 3: # 后两家抛公海 rp = S.post(f'{BASE}/api/customer/release-pool', params={'id': cid}, timeout=20) note(f" + e2c-acc-{STAMP}-{nm} id={cid} star={star} → pool(code={body_of(rp).get('code')})") else: note(f" + e2c-acc-{STAMP}-{nm} id={cid} star={star} → mine") if made: ra = S.post(f'{BASE}/api/customer/archive-batch', params={'ids': ','.join(made[:1])}, timeout=20) note(f" + 首家 archive → 已归档视图(code={body_of(ra).get('code')})") note(f"C. 造数完成:mine 2 / pool 2 / archived 1(含探针归档)—— demo 里可直接走查") ev['verdict'] = 'READY_SEEDED' ev['seeded_ids'] = made ev['notes'] = VERDICT json.dump(ev, open(RESULT, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) print(f"\n证据已落盘 {RESULT}")