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.

367 lines
24 KiB

2 days ago
# ============ F-07 详情公共头部 + 8 页签 ============
def f07():
print('\n== F-07 详情公共头部 + 8 页签 ==')
fid = C['full']
d = data_of(api(S, 'GET', f'/api/customer/{fid}/detail-head'))
if not isinstance(d, dict):
check('F-07', 'detail-head', 'fail', f'响应={d}')
return
head_need = ['customerNo', 'customerName', 'customerStage', 'customerStageName',
'customerStarLevel', 'relationStarLevel', 'strategicAgreementLevel',
'vipCustomerLevel', 'archiveStatus', 'ownerUserId', 'ownerUserNameSnapshot',
'lastFollowSummary', 'lastFollowTime', 'nextFollowTime',
'opportunityCount', 'projectCount']
missing = [k for k in head_need if k not in d]
check('F-07', '头部关键项齐(阶段只读条/星级/协议VIP快照/负责人/跟进摘要/待跟进提醒/关联计数)',
'pass' if not missing else 'fail', f'缺={missing}')
# 汇总卡 5 字段恒 null(不符④拍板 A:后端占位,前端显 --)
cards = ['wonProjectAmount', 'planEstimateAmount', 'ongoingOpportunityAmount',
'contractAmount', 'paidAmount']
not_null = [k for k in cards if d.get(k) is not None]
check('F-07', '汇总卡 5 字段恒 null(占位契约)', 'pass' if not not_null else 'warn', f'非null={not_null}')
check('F-07', 'projectCount 恒 null(A5 seam 未接入显 --)',
'pass' if d.get('projectCount') is None else 'warn', f"projectCount={d.get('projectCount')}")
# D-01 复验:交割-甲 关联商机计数=1
d = data_of(api(S, 'GET', f'/api/customer/{C["transfer1"]}/detail-head'))
oc = d.get('opportunityCount') if isinstance(d, dict) else None
check('F-07', 'D-01 复验:交割-甲 opportunityCount==1(修复后真值)',
'pass' if oc == 1 else 'fail', f'opportunityCount={oc}')
# 关联商机页签(port 反查)
d = data_of(api(S, 'GET', f'/api/customer/{C["transfer1"]}/opportunities',
params={'current': 1, 'size': 10}))
rows = (d.get('content') or []) if isinstance(d, dict) else []
names = [str(r.get('oppName') or r.get('opportunityName') or r.get('name') or '') for r in rows]
check('F-07', '关联商机页签 port 反查命中 e2c-联动-交割',
'pass' if any('联动-交割' in n for n in names) else 'fail', f'商机={names}')
# oplog action 值域(11 种白名单)
d = data_of(api(S, 'GET', f'/api/customer/{fid}/oplog/page', params={'current': 1, 'size': 50}))
acts = {r.get('action') for r in (d.get('content') or [])} if isinstance(d, dict) else set()
whitelist = {'CREATE', 'UPDATE', 'FOLLOW', 'ARCHIVE', 'RESTORE', 'TRANSFER', 'CLAIM',
'POOL', 'STAGE_CHANGE', 'MEMBER_ADD', 'MEMBER_REMOVE'}
check('F-07', 'oplog action 值域 ⊆ 11 种白名单',
'pass' if acts <= whitelist else 'warn', f'实际={sorted(a for a in acts if a)}')
# 已归档变体
d = data_of(api(S, 'GET', f'/api/customer/{C["archived"]}/detail-head'))
check('F-07', '已归档客户详情可开(档案状态=已归档)',
'pass' if isinstance(d, dict) and d.get('archiveStatus') == 2 else 'fail',
f"archiveStatus={d.get('archiveStatus') if isinstance(d, dict) else d}")
# 67002
r = api(S, 'GET', '/api/customer/999999/detail-head')
expect_code(r, 67002, 'F-07', '详情客户不存在 → 67002')
# ============ F-08 跟进写入 + 跟进页签 ============
def f08():
print('\n== F-08 跟进写入 + 跟进页签 ==')
fid = C['full']
way = dict_one('follow_way') or 'follow_way_01'
old = dbq('SELECT last_valid_follow_time t FROM customer WHERE id=%s', (fid,))
old_t = old[0]['t'] if old else None
# 空 content → 67009
r = api(S, 'POST', f'/api/customer/{fid}/follow', form={'followWay': way, 'followContent': ''})
expect_code(r, 67009, 'F-08', '跟进内容空 → 67009')
# nextFollowTime 早于当前 → 67009(ISO T 分隔,空格 400——seed 实测)
r = api(S, 'POST', f'/api/customer/{fid}/follow',
form={'followWay': way, 'followContent': 'e2c-F08 过去时间样例',
'nextFollowTime': '2026-09-01T10:00:00'})
expect_code(r, 67009, 'F-08', 'nextFollowTime 早于当前 → 67009')
# 正常写入
content = 'e2c-F08 跟进实测:电话沟通年度合作意向'
nxt = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(time.time() + 7 * 86400))
r = api(S, 'POST', f'/api/customer/{fid}/follow',
form={'followWay': way, 'followContent': content, 'nextFollowTime': nxt})
new_fid = data_of(r)
check('F-08', '正常写入返回跟进 id', 'pass' if new_fid else 'fail', f'id={new_fid} way={way}')
rows = dbq('SELECT id FROM customer_follow WHERE customer_id=%s AND deleted=0 ORDER BY id DESC LIMIT 1', (fid,))
check('F-08', 'customer_follow 行落库(append-only)',
'pass' if rows and str(rows[0]['id']) == str(new_fid) else 'fail', '')
anchor = dbq('SELECT last_valid_follow_time t FROM customer WHERE id=%s', (fid,))
new_t = anchor[0]['t'] if anchor else None
check('F-08', 'D25 锚点 last_valid_follow_time 刷新',
'pass' if new_t is not None and (old_t is None or str(new_t) > str(old_t)) else 'fail',
f'{old_t} → {new_t}')
# follow/page + followWay 筛选
d = data_of(api(S, 'GET', f'/api/customer/{fid}/follow/page', params={'current': 1, 'size': 20}))
tops = [str(x.get('followContent') or '') for x in (d.get('content') or [])] if isinstance(d, dict) else []
check('F-08', '跟进页签时间倒序含新记录', 'pass' if content in tops else 'fail', f'首条={tops[:1]}')
d = data_of(api(S, 'GET', f'/api/customer/{fid}/follow/page',
params={'current': 1, 'size': 20, 'followWay': way}))
fw_ok = all(x.get('followWay') == way for x in (d.get('content') or [])) if isinstance(d, dict) else False
check('F-08', 'followWay 筛选生效', 'pass' if fw_ok and fw_ok is not None else 'warn', f'way={way}')
# 30s 防重
r = api(S, 'POST', f'/api/customer/{fid}/follow',
form={'followWay': way, 'followContent': content, 'nextFollowTime': nxt})
expect_code(r, 67009, 'F-08', '30s 内同内容重复提交 → 67009')
# detail-head 摘要联动 + followFuture 待跟进提醒
d = data_of(api(S, 'GET', f'/api/customer/{fid}/detail-head'))
check('F-08', '头部最近跟进摘要联动最新一条',
'pass' if isinstance(d, dict) and d.get('lastFollowSummary') == content else 'fail',
f"summary={str(d.get('lastFollowSummary') if isinstance(d, dict) else d)[:60]}")
d = data_of(api(S, 'GET', f'/api/customer/{C["followFuture"]}/detail-head'))
check('F-08', '待跟进提醒 nextFollowTime 非空(seed 明天回访)',
'pass' if isinstance(d, dict) and d.get('nextFollowTime') else 'fail',
f"next={d.get('nextFollowTime') if isinstance(d, dict) else d}")
# append-only:跟进无改删口
r = api(S, 'PUT', f'/api/customer/{fid}/follow/{new_fid}', form={'followContent': 'x'})
check('F-08', 'append-only:跟进无编辑口(PUT 不成功即可)',
'pass' if code_of(r) not in (0, '0', 200, '200') else 'warn', f'code={code_of(r)}')
# ============ F-09 关注 / 重点标记(一行两态) ============
def f09():
print('\n== F-09 关注/重点标记(一行两态)==')
uid = ADMIN
def focus_rows(cid):
return dbq('SELECT id, starred FROM customer_focus WHERE customer_id=%s AND user_id=%s',
(cid, uid))
api(S, 'POST', f'/api/customer/{C["stage1"]}/focus')
api(S, 'POST', f'/api/customer/{C["stage1"]}/focus')
rows = focus_rows(C['stage1'])
check('F-09', 'focus 幂等:两次关注仍单行', 'pass' if len(rows) == 1 else 'fail', f'行数={len(rows)}')
api(S, 'POST', f'/api/customer/{C["member"]}/star')
rows = focus_rows(C['member'])
check('F-09', 'star 未关注自动建行 starred=1',
'pass' if len(rows) == 1 and rows[0]['starred'] == 1 else 'fail', f'{rows}')
api(S, 'POST', f'/api/customer/{C["member"]}/unstar')
rows = focus_rows(C['member'])
check('F-09', 'unstar 保留行仅 starred=0',
'pass' if len(rows) == 1 and rows[0]['starred'] == 0 else 'fail', f'{rows}')
r = api(S, 'POST', '/api/customer/focus-batch',
params={'ids': ','.join([C['star5'], C['pool1'], C['pool2']])})
n = sum(len(focus_rows(C[k])) for k in ['star5', 'pool1', 'pool2'])
check('F-09', 'focus-batch 3 客户各建 1 行',
'pass' if code_of(r) in (0, '0', 200, '200') and n == 3 else 'fail', f'行数合计={n}')
d = data_of(api(S, 'POST', '/api/customer/workspace/pool/page',
form={'current': 1, 'size': 50, 'viewType': 'FOCUSED'}))
names = [x.get('customerName') for x in (d.get('content') or [])] if isinstance(d, dict) else []
check('F-09', 'pool FOCUSED 视图命中关注的公海客户',
'pass' if {'e2c-公海-甲', 'e2c-公海-乙'} <= set(names or []) else 'fail', f'命中={names}')
api(S, 'POST', f'/api/customer/{C["member"]}/unfocus')
rows = focus_rows(C['member'])
check('F-09', 'unfocus 物理删行(连带清 starred)', 'pass' if len(rows) == 0 else 'fail', f'剩余={len(rows)}')
d = data_of(api(S, 'GET', f'/api/customer/{C["stage1"]}/oplog/page', params={'current': 1, 'size': 50}))
acts = {x.get('action') for x in (d.get('content') or [])} if isinstance(d, dict) else set()
check('F-09', '关注动作不写 oplog',
'pass' if not (acts & {'FOCUS', 'UNFOCUS', 'STAR', 'UNSTAR'}) else 'warn',
f'actions={sorted(a for a in acts if a)}')
for k in ['stage1', 'star5', 'pool1', 'pool2']:
api(S, 'POST', f'/api/customer/{C[k]}/unfocus')
# ============ F-10 团队成员 ============
def f10():
print('\n== F-10 团队成员 ==')
fid = C['full']
d = data_of(api(S, 'GET', f'/api/customer/{fid}/members'))
if not isinstance(d, list) or not d:
check('F-10', 'members 列表', 'fail', f'响应={str(d)[:120]}')
return
first = d[0]
rk = next((k for k in first.keys() if 'role' in k.lower()), None)
rv = str(first.get(rk, '')) if rk else ''
check('F-10', '负责人在首位(ROLE_OWNER 快照)',
'pass' if 'OWNER' in rv.upper() or '负责人' in rv else 'warn', f'首行={str(first)[:120]}')
# 两阶段校验:整批拒绝
r = api(S, 'POST', f'/api/customer/{fid}/members', form={'memberUserIds': ''})
check('F-10', '名单空被拒(67016/绑定拒)',
'pass' if code_of(r) not in (0, '0', 200, '200') else 'fail', f'code={code_of(r)}')
r = api(S, 'POST', f'/api/customer/{fid}/members', form={'memberUserIds': ADMIN})
expect_code(r, 67016, 'F-10', '含负责人 → 67016')
# 正常添加 BUDDY
r = api(S, 'POST', f'/api/customer/{fid}/members', form={'memberUserIds': BUDDY})
ok = code_of(r) in (0, '0', 200, '200')
check('F-10', '添加协同人 BUDDY', 'pass' if ok else 'fail', f'code={code_of(r)}')
rows = dbq('SELECT id, delete_key FROM customer_team_member WHERE customer_id=%s AND user_id=%s ORDER BY id',
(fid, BUDDY))
check('F-10', 'team_member 行在(delete_key=0)',
'pass' if rows and rows[-1]['delete_key'] == 0 else 'fail', f'{rows}')
r = api(S, 'POST', f'/api/customer/{fid}/members', form={'memberUserIds': BUDDY})
expect_code(r, 67016, 'F-10', '重复加入 → 67016(整批拒绝)')
d = data_of(api(S, 'GET', f'/api/customer/{fid}/oplog/page', params={'current': 1, 'size': 20}))
acts = [x.get('action') for x in (d.get('content') or [])] if isinstance(d, dict) else []
check('F-10', 'oplog MEMBER_ADD', 'pass' if 'MEMBER_ADD' in acts else 'warn', f'actions={acts[:6]}')
r = api(S, 'DELETE', f'/api/customer/{fid}/members/{BUDDY}')
rows = dbq('SELECT delete_key FROM customer_team_member WHERE customer_id=%s AND user_id=%s ORDER BY id',
(fid, BUDDY))
check('F-10', '移除软删(delete_key>0)',
'pass' if code_of(r) in (0, '0', 200, '200') and rows and rows[-1]['delete_key'] > 0 else 'fail', f'{rows}')
d = data_of(api(S, 'GET', f'/api/customer/{fid}/oplog/page', params={'current': 1, 'size': 20}))
acts = [x.get('action') for x in (d.get('content') or [])] if isinstance(d, dict) else []
check('F-10', 'oplog MEMBER_REMOVE', 'pass' if 'MEMBER_REMOVE' in acts else 'warn', '')
r = api(S, 'POST', f'/api/customer/{fid}/members', form={'memberUserIds': BUDDY})
rows = dbq('SELECT delete_key FROM customer_team_member WHERE customer_id=%s AND user_id=%s ORDER BY id',
(fid, BUDDY))
check('F-10', '移除后可重加(delete_key 复用键)',
'pass' if code_of(r) in (0, '0', 200, '200') and rows and rows[-1]['delete_key'] == 0 else 'fail', '')
api(S, 'DELETE', f'/api/customer/{fid}/members/{BUDDY}') # 还原 seed 态
# ============ F-11 归属动作(领取/分配/抛公海/归档/恢复) ============
def f11():
print('\n== F-11 归属动作 ==')
# 1) admin 领取 pool1
r = api(S, 'POST', '/api/customer/claim', params={'id': C['pool1']})
ok = code_of(r) in (0, '0', 200, '200')
row = dbq('SELECT owner_user_id o, owner_user_name_snapshot n, owner_dept_id d, '
'owner_dept_name_snapshot dn, last_valid_follow_time t FROM customer WHERE id=%s',
(C['pool1'],))
pool1_before = row[0] if row else None
check('F-11', '领取公海客户 → 当前用户(owner+快照)', 'pass' if ok else 'fail', f'code={code_of(r)}')
check('F-11', '领取后 D25 锚点赋当下(last_valid_follow_time 非空)',
'pass' if row and row[0]['t'] is not None else 'fail', f"t={row[0]['t'] if row else None}")
# 2) 重复领取(非公海)失败
r = api(S, 'POST', '/api/customer/claim', params={'id': C['pool1']})
check('F-11', '重复领取失败(非公海态)',
'pass' if code_of(r) not in (0, '0', 200, '200') else 'fail',
f'code={code_of(r)} msg={str(r.get("message"))[:80] if isinstance(r, dict) else ""}')
# 3) BUDDY(无角色)领取观察点:目标已被领必败,错误语义记录
r = api(B, 'POST', '/api/customer/claim', params={'id': C['pool1']})
got = code_of(r)
check('F-11', '观察点:BUDDY(无角色)领取被拒', 'pass' if got not in (0, '0', 200, '200') else 'warn',
f'code={got} msg={str(r.get("message"))[:80] if isinstance(r, dict) else ""}')
if got in (0, '0', 200, '200'):
defect('D-02', 'P1', '无角色用户可领取已被领取的公海客户(越权观察点)',
f'BUDDY(无任何角色)claim 非公海态客户返回成功——DataScope/公海态校验未拦;已 DB 还原')
dbx('UPDATE customer SET owner_user_id=%s, owner_user_name_snapshot=%s, owner_dept_id=%s, '
'owner_dept_name_snapshot=%s WHERE id=%s',
(pool1_before['o'], pool1_before['n'], pool1_before['d'], pool1_before['dn'], C['pool1']))
# 4) assign 本人 → 67012
r = api(S, 'POST', '/api/customer/assign', params={'id': C['full'], 'userId': ADMIN})
expect_code(r, 67012, 'F-11', '分配给本人 → 67012')
# 5) assign stage1 → BUDDY 成功(启用在职校验通过)
s1_before = dbq('SELECT owner_user_id o, owner_user_name_snapshot n, owner_dept_id d, '
'owner_dept_name_snapshot dn FROM customer WHERE id=%s', (C['stage1'],))[0]
r = api(S, 'POST', '/api/customer/assign', params={'id': C['stage1'], 'userId': BUDDY})
ok = code_of(r) in (0, '0', 200, '200')
row = dbq('SELECT owner_user_id o, owner_user_name_snapshot n FROM customer WHERE id=%s', (C['stage1'],))
check('F-11', '分配给 BUDDY 成功(owner 换绑+快照)',
'pass' if ok and row and str(row[0]['o']) == BUDDY else 'fail',
f'code={code_of(r)} owner={row[0]["o"] if row else None}')
dbx('UPDATE customer SET owner_user_id=%s, owner_user_name_snapshot=%s, owner_dept_id=%s, '
'owner_dept_name_snapshot=%s WHERE id=%s',
(s1_before['o'], s1_before['n'], s1_before['d'], s1_before['dn'], C['stage1']))
check('F-11', '实验后 DB 还原 stage1 归属(assign 回本人被 67012 拦)', 'pass', '')
# 6) 抛公海:进行中商机 → 67007
r = api(S, 'POST', '/api/customer/release-pool', params={'id': C['transfer2']})
expect_code(r, 67007, 'F-11', '有进行中商机抛公海 → 67007')
# 7) 抛公海 stage1:清 owner、部门锚点保留
r = api(S, 'POST', '/api/customer/release-pool', params={'id': C['stage1']})
ok = code_of(r) in (0, '0', 200, '200')
row = dbq('SELECT owner_user_id o, owner_dept_id d FROM customer WHERE id=%s', (C['stage1'],))
check('F-11', '抛公海清 owner、部门锚点保留',
'pass' if ok and row and row[0]['o'] is None and row[0]['d'] is not None else 'fail',
f'owner={row[0]["o"] if row else None} dept={row[0]["d"] if row else None}')
# 8) claim 还原
r = api(S, 'POST', '/api/customer/claim', params={'id': C['stage1']})
check('F-11', 'claim 还原 stage1(owner=admin)',
'pass' if code_of(r) in (0, '0', 200, '200') else 'fail', f'code={code_of(r)}')
d = data_of(api(S, 'GET', f'/api/customer/{C["stage1"]}/oplog/page', params={'current': 1, 'size': 50}))
acts = {x.get('action') for x in (d.get('content') or [])} if isinstance(d, dict) else set()
check('F-11', '归属动作 oplog(ASSIGN/POOL/CLAIM 齐)',
'pass' if {'ASSIGN', 'POOL', 'CLAIM'} <= acts else 'warn', f'actions={sorted(a for a in acts if a)}')
# 9) 归档:进行中商机 → 67006
r = api(S, 'POST', '/api/customer/archive', params={'id': C['transfer2']})
expect_code(r, 67006, 'F-11', '有进行中商机归档 → 67006')
# 10) archive-batch 整批预检:任一失败整批不执行
r = api(S, 'POST', '/api/customer/archive-batch',
params={'ids': f'{C["star5"]},{C["transfer2"]}'})
st = dbq('SELECT archive_status a FROM customer WHERE id=%s', (C['star5'],))[0]['a']
check('F-11', 'archive-batch 整批预检:含阻断户整批不执行(star5 仍有效)',
'pass' if st == 1 else 'fail', f'star5 archive_status={st} resp={str(r)[:100]}')
# 11) 单条归档 + 详情变体 + 恢复
r = api(S, 'POST', '/api/customer/archive', params={'id': C['star5']})
st = dbq('SELECT archive_status a FROM customer WHERE id=%s', (C['star5'],))[0]['a']
check('F-11', '归档 star5(archive_status=2)', 'pass' if code_of(r) in (0, '0', 200, '200') and st == 2 else 'fail', f'st={st}')
d = data_of(api(S, 'GET', f'/api/customer/{C["star5"]}/detail-head'))
check('F-11', '已归档 detail-head archiveStatus=2',
'pass' if isinstance(d, dict) and d.get('archiveStatus') == 2 else 'fail', '')
r = api(S, 'POST', '/api/customer/restore', params={'id': C['star5']})
st = dbq('SELECT archive_status a FROM customer WHERE id=%s', (C['star5'],))[0]['a']
check('F-11', '恢复 star5(archive_status=1)',
'pass' if code_of(r) in (0, '0', 200, '200') and st == 1 else 'fail', f'st={st}')
# ============ F-12 商机侧硬依赖三端点(跨模块回归) ============
def f12():
print('\n== F-12 商机侧硬依赖(quick-create/search/contacts)==')
tsn = str(int(time.time()))[-6:]
form = {'customerName': f'e2c-快创-{tsn}', 'customerType': CTYPE,
'provinceCode': '440000', 'cityCode': '440100', 'industryCode': GOV,
'customerStarLevel': 2, 'relationStarLevel': 2}
r = api(S, 'POST', '/api/customer/quick-create', form=form)
d = data_of(r)
qid = d.get('id') if isinstance(d, dict) and d.get('needConfirm') is None else None
check('F-12', 'quick-create 最小集落库回 id+customerNo',
'pass' if qid and isinstance(d, dict) and d.get('customerNo') else 'fail', f'{str(d)[:120]}')
if qid:
row = dbq('SELECT owner_user_id o, is_child c, is_biz_negotiated b FROM customer WHERE id=%s', (qid,))
if row:
o, c, b = row[0]['o'], row[0]['c'], row[0]['b']
check('F-12', '默认值:owner=当前用户 / is_child=否 / isBizNegotiated=否',
'pass' if str(o) == ADMIN and int(c or 0) == 0 and int(b or 0) == 0 else 'warn',
f'owner={o} is_child={c} is_biz={b}')
else:
check('F-12', '默认值 DB 验', 'warn', '行未查到(列名差异)')
# 已知缺口复现:quick-create 无 confirmSimilar 参数
r = api(S, 'POST', '/api/customer/quick-create',
form=dict(form, customerName='e2c-恒信达科技有限公司'))
d = data_of(r)
nc = d.get('needConfirm') if isinstance(d, dict) else None
r = api(S, 'POST', '/api/customer/quick-create',
form=dict(form, customerName='e2c-恒信达科技有限公司', confirmSimilar='true'))
d = data_of(r)
nc2 = d.get('needConfirm') if isinstance(d, dict) else None
check('F-12', '已知缺口复现:无 confirmSimilar,命中相似无法仍要创建(预登记不算新缺陷)',
'pass' if nc and nc2 else 'warn', f'首次needConfirm={bool(nc)} 带参重发needConfirm={bool(nc2)}')
# search 三维模糊 + 类型精确 + 排除
d = data_of(api(S, 'POST', '/api/customer/search', form={'current': 1, 'size': 20, 'keyword': '恒信达'}))
names = [x.get('customerName') for x in (d.get('content') or [])] if isinstance(d, dict) else []
check('F-12', 'search 名称模糊命中 simA/simB',
'pass' if {'e2c-恒信达科技有限公司', 'e2c-恒信达科技有限责任公司'} <= set(names or []) else 'warn',
f'命中={names}')
d = data_of(api(S, 'POST', '/api/customer/search',
form={'current': 1, 'size': 20, 'keyword': '恒信达', 'customerType': CTYPE}))
t_ok = isinstance(d, dict) and all(x.get('customerType') == CTYPE for x in (d.get('content') or []))
check('F-12', 'search 类型精确过滤', 'pass' if t_ok else 'warn',
f"total={d.get('total') if isinstance(d, dict) else d} ctype={CTYPE}")
d = data_of(api(S, 'POST', '/api/customer/search',
form={'current': 1, 'size': 20, 'keyword': '恒信达', 'excludeCustomerIds': C['simA']}))
names2 = [x.get('customerName') for x in (d.get('content') or [])] if isinstance(d, dict) else []
check('F-12', 'search excludeCustomerIds 排除 simA',
'pass' if 'e2c-恒信达科技有限公司' not in (names2 or []) else 'fail', f'命中={names2}')
# contacts 按客户带出
d = data_of(api(S, 'GET', f'/api/customer/{C["full"]}/contacts'))
names3 = [x.get('name') for x in d] if isinstance(d, list) else []
check('F-12', 'contacts 按客户带出(张关键/李普通/王重复)',
'pass' if {'张关键', '李普通', '王重复'} <= set(names3 or []) else 'fail', f'名单={names3}')
def main():
t0 = time.time()
print(f'== 客户核心域 E2E(票 04)== e2c 客户 {len(C)} 个')
for fn in [f01, f02, f03, f04, f05, f06, f07, f08, f09, f10, f11, f12]:
try:
fn()
except Exception as e:
import traceback
traceback.print_exc()
check(fn.__name__.upper(), '流程级异常', 'fail', str(e)[:160])
el = time.time() - t0
npass = sum(1 for c in checks if c['verdict'] == '✅')
nwarn = sum(1 for c in checks if c['verdict'] == '⚠')
nfail = sum(1 for c in checks if c['verdict'] == '❌')
print(f'\n== 汇总 == ✅{npass} ⚠{nwarn} ❌{nfail} 耗时 {el:.0f}s specimens={len(specimens)}')
for d in defects:
print(f" ⚑ {d['id']}({d['severity']}) {d['title']}")
json.dump({'checks': checks, 'defects': defects, 'elapsedSec': round(el, 1)},
open('.scratch/customer-e2e/e2e-core-checks.json', 'w', encoding='utf-8'),
ensure_ascii=False, indent=1)
json.dump(specimens, open('.scratch/customer-e2e/specimens-core.json', 'w', encoding='utf-8'),
ensure_ascii=False, indent=1)
print(' ✔ 落盘 e2e-core-checks.json / specimens-core.json')
if __name__ == '__main__':
main()