You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

202 lines
10 KiB

# -*- 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}")