# ============ 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()