# -*- coding: utf-8 -*- """票 02 · D-01 专项运行时断言(opportunity-fix 口径:新建商机 fail-fast + 首阶段落位)。 D-01(opportunity-fix):系统无发布中商机阶段模板时,新建商机 500 → 修复为: a) fail-fast 60001「系统未配置发布中的商机阶段模板…」(OpportunityIntakeException → seam 译 BusinessErrorException) b) OPP_STAGE_TPL_01 系统预设模板受 64018 保护不可停用 c) 正常路径:新建商机落真实首阶段(currentStageId 非空)+ progress 5 节点首节点 CURRENT 断言 1/2/3 走运行时 API;断言 4(60001 fail-fast)需「无发布中模板」场景—— 系统预设模板 API 不可停(64018 保护),故 DB 临时置停用 → 验证 → finally 恢复 → 复验恢复。 """ import io import json import sys import pymysql import requests sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') BASE = 'http://localhost:8080' UID_A = '744842565802524672' TPL_CODE = 'OPP_STAGE_TPL_01' # 系统预设模板(R01 已验 64018 停用保护) r = requests.get(f'{BASE}/api/auth/debug/token', params={'userId': UID_A}, timeout=15) assert r.json().get('code') == 0, r.text H = {'Authorization': 'Bearer ' + r.json()['data']} results = [] def rec(name, ok, ev=''): results.append((name, ok, ev)) print(f" [{'✅' if ok else '❌'}] {name}" + (f" —— {ev}" if ev else '')) print('== D-01 专项运行时断言 ==') # ---------- 探查:阶段模板表 + 发布中模板 ---------- conn = pymysql.connect(host='8.129.84.155', port=3306, user='root', password='Itc@123456', database='crm', charset='utf8mb4', autocommit=True) cur = conn.cursor() cur.execute("SELECT id, template_code, template_name, status, is_default FROM " "opportunity_stage_template WHERE deleted=0 AND status=2") pub = cur.fetchall() print(f'发布中阶段模板 {len(pub)} 个: {[(str(p[0]), p[1], p[4]) for p in pub]}') tpl_row = next((p for p in pub if p[1] == TPL_CODE), None) # ---------- 断言 1:新建商机落真实首阶段(用 core F01 新建的商机) ---------- d = requests.post(f'{BASE}/api/opportunity/page', headers={**H, 'Content-Type': 'application/x-www-form-urlencoded'}, data={'viewType': 'MINE', 'current': 1, 'size': 20, 'keyword': 'e2e-F01'}, timeout=30).json() rows = (d.get('data') or {}).get('content') or [] f01_id = str(rows[0]['id']) if rows else None rec('前置:F01 新建商机可查', bool(f01_id), f'id={f01_id}') if f01_id: det = requests.get(f'{BASE}/api/opportunity/detail', headers=H, params={'id': f01_id}, timeout=30).json() dd = det.get('data') or {} stage_id = dd.get('currentStageId') or dd.get('stageId') rec('D-01 c) 新建商机 currentStageId 落真实首节点(非空)', bool(stage_id), f"currentStageId={stage_id}") # ---------- 断言 2:progress 5 节点 + 首节点 CURRENT ---------- p = requests.get(f'{BASE}/api/opportunity/stage/progress', headers=H, params={'oppId': f01_id}, timeout=30).json() nodes = (p.get('data') or {}).get('nodes') or [] cur_nodes = [n for n in nodes if n.get('nodeState') == 'CURRENT'] rec('D-01 c) progress 节点数 = 5(首节点 CURRENT)', len(nodes) == 5 and len(cur_nodes) == 1 and int(cur_nodes[0].get('seqNo') or 0) == 1, f"nodes={len(nodes)} current seq={cur_nodes[0].get('seqNo') if cur_nodes else None}") # ---------- 断言 3:64018 系统预设模板停用保护(运行时复验) ---------- if tpl_row: tpl_id = str(tpl_row[0]) rr = requests.post(f'{BASE}/api/rule/opp-stage-template/disable', headers=H, params={'id': tpl_id}, timeout=30).json() rec('D-01 b) 系统预设模板 disable → 64018', rr.get('code') == 64018, f"code={rr.get('code')} msg={str(rr.get('message'))[:50]}") # ---------- 断言 4:60001 fail-fast(DB 临时停用全部发布中模板 → 新建 → 恢复) ---------- def try_create(): rr = requests.post(f'{BASE}/api/opportunity', headers={**H, 'Content-Type': 'application/x-www-form-urlencoded'}, data={'opportunityName': 'e2e-D01-failfast探针', 'oppSource': 'opp_source_03', 'industryCode': 'gov', 'bidForm': 'bid_form_02', 'localityType': 'locality_type_02', 'provinceCode': '510000', 'cityCode': '510100', 'partyAClear': 0, 'partyA': 'E2E甲方-D01'}, timeout=30) rr.encoding = 'utf-8' return rr.json() fail_code = None try: cur.execute("UPDATE opportunity_stage_template SET status=3 WHERE deleted=0 AND status=2") conn.commit() b = try_create() fail_code = b.get('code') rec('D-01 a) 无发布中模板新建 → 60001 fail-fast', fail_code == 60001, f"code={fail_code} msg={str(b.get('message'))[:60]}") finally: # 恢复:被临时停用的模板全部回到发布中(含系统预设 is_default) cur.execute("UPDATE opportunity_stage_template SET status=2 WHERE deleted=0 AND status=3 " "AND is_default=1") conn.commit() cur.execute("SELECT COUNT(*) FROM opportunity_stage_template WHERE deleted=0 AND status=2 AND is_default=1") restored = cur.fetchone()[0] rec('环境恢复:系统预设模板回发布中', restored >= 1, f'restored={restored}') # 恢复后复验:新建恢复正常 b2 = try_create() rec('D-01 恢复后新建恢复正常(fail-fast 不留痕)', b2.get('code') == 0, f"code={b2.get('code')}") if b2.get('code') == 0 and b2.get('data'): # 清理探针商机(软删口径走 DB,避免污染 MINE 视图断言) new_id = b2['data'] if isinstance(b2['data'], (int, str)) else b2['data'].get('id') cur.execute("UPDATE opportunity SET deleted=1 WHERE id=%s", (new_id,)) conn.commit() print(f' (探针商机 {new_id} 已软删清理)') conn.close() ok_n = sum(1 for _, ok, _ in results if ok) print(f"\n===== D-01 专项:{ok_n}/{len(results)} 通过 =====") with open(r'd:\code\crm-backend-matt\.scratch\opportunity-acceptance\d01-runtime-result.json', 'w', encoding='utf-8') as fp: json.dump([{'name': n, 'ok': ok, 'ev': e} for n, ok, e in results], fp, ensure_ascii=False, indent=1)