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.

119 lines
6.7 KiB

# -*- coding: utf-8 -*-
"""票 17 复测 · 完整主链回归(API 全链路)
建项 报备通过(01双写) 逐级推进 35 连跳 多步回退 推回 5 标记赢单锁死终态三锁
产物: retest-mainchain-result.json
"""
import io, json, pathlib, sys, time, urllib.request, urllib.parse, urllib.error
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
BASE = "http://localhost:8080"
AUD = pathlib.Path(r"E:\code\crm-backend-matt\.scratch\crm-project\audit")
DIRECTOR = "99021002"
CUSTOMER, CARD = "99021001", "99021008"
R = []
def step(name, ok, note=""):
R.append({"step": name, "ok": bool(ok), "note": str(note)[:300]})
print(("PASS " if ok else "FAIL ") + name + ((" | " + str(note)) if note else ""))
def _req(method, path, body=None, tok=None):
data = urllib.parse.urlencode(body).encode() if body is not None else None
headers = {"Authorization": "Bearer " + tok} if tok else {}
if data: headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8"
r = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(r, timeout=20) as resp:
return json.loads(resp.read().decode("utf-8", "replace"))
except urllib.error.HTTPError as e:
try: return json.loads(e.read().decode("utf-8", "replace"))
except Exception: return {"__status": e.code}
except Exception as e:
return {"__error": str(e)}
def detail(pid):
return _req("GET", "/api/project/detail?id=" + pid, tok=TOK)["data"]
TOK = _req("GET", "/api/auth/debug/token?userId=" + DIRECTOR)["data"]
TOK = TOK if isinstance(TOK, str) else TOK["token"]
# 1. 建项 → stage0 / filing=1 / status=1
nm = f"R17-主链-{time.strftime('%H%M%S')}"
j = _req("POST", "/api/project/create", {"projectName": nm, "customerId": CUSTOMER, "schemeCardId": CARD}, tok=TOK)
pid = str(j["data"])
d = detail(pid)
step("1 建项:stage=0 报备审核中", j.get("code") == 0 and str(d["projectStage"]) == "0"
and str(d["filingStatus"]) == "1" and str(d["projectStatus"]) == "1",
f"stage={d['projectStage']} filing={d['filingStatus']} status={d['projectStatus']}")
# 2. 冲突解除=报备审核通过(双写 0→1)
j = _req("POST", "/api/project/filing/review", {"id": pid, "approved": "true", "remark": "主链复测"}, tok=TOK)
d = detail(pid)
step("2 冲突解除双写:filing=2 + stage 0→1", j.get("code") == 0 and str(d["projectStage"]) == "1"
and str(d["filingStatus"]) == "2", f"stage={d['projectStage']} filing={d['filingStatus']}")
# 3. 逐级推进 1→2→3
_req("POST", "/api/project/stage/advance", {"id": pid}, tok=TOK)
j = _req("POST", "/api/project/stage/advance", {"id": pid}, tok=TOK)
d = detail(pid)
step("3 逐级推进 1→2→3", j.get("code") == 0 and str(d["projectStage"]) == "3", f"stage={d['projectStage']}")
# 4. 3→5 唯一连跳(无需招投标)
j = _req("POST", "/api/project/stage/advance", {"id": pid, "skipBidding": "true"}, tok=TOK)
d = detail(pid)
step("4 3→5 唯一连跳", j.get("code") == 0 and str(d["projectStage"]) == "5", f"stage={d['projectStage']}")
# 5. 多步回退 5→2
j = _req("POST", "/api/project/stage/rollback", {"id": pid, "targetStage": "2"}, tok=TOK)
d = detail(pid)
step("5 多步回退 5→2(非相邻)", j.get("code") == 0 and str(d["projectStage"]) == "2", f"stage={d['projectStage']}")
# 6. 推回 5:2→3 → 3→5
_req("POST", "/api/project/stage/advance", {"id": pid}, tok=TOK)
j = _req("POST", "/api/project/stage/advance", {"id": pid, "skipBidding": "true"}, tok=TOK)
d = detail(pid)
step("6 推回 5(2→3→连跳5)", j.get("code") == 0 and str(d["projectStage"]) == "5", f"stage={d['projectStage']}")
# 7. 标记赢单 → stage5 闭环锁死
j = _req("POST", "/api/project/bid-result/mark", {"id": pid, "bidResult": "1",
"bidOpenTime": "2026-09-21 10:00:00", "bidOpenPlace": "主链复测开标地",
"bidAwardMethod": "竞争性谈判", "bidAwardAmount": "660000"}, tok=TOK)
d = detail(pid)
step("7 标记赢单:停 stage5 / status=1 / bid=1(闭环不进结项)",
j.get("code") == 0 and str(d["projectStage"]) == "5" and str(d["projectStatus"]) == "1"
and str(d["bidResult"]) == "1", f"stage={d['projectStage']} status={d['projectStatus']} bid={d['bidResult']}")
# 8. 赢单终态锁(先测四项锁,最后才碰 close):回退/推进/重复标记/审核 全拒
j1 = _req("POST", "/api/project/stage/rollback", {"id": pid, "targetStage": "2"}, tok=TOK)
j2 = _req("POST", "/api/project/stage/advance", {"id": pid}, tok=TOK)
j4 = _req("POST", "/api/project/bid-result/mark", {"id": pid, "bidResult": "2"}, tok=TOK)
j5 = _req("POST", "/api/project/filing/review", {"id": pid, "approved": "true"}, tok=TOK)
step("8 赢单终态锁:回退 68003 + 重复标记 68009 + 推进/审核 均拒",
j1.get("code") == 68003 and j4.get("code") == 68009
and j2.get("code") not in (0, None) and j5.get("code") not in (0, None),
f"rollback={j1.get('code')} advance={j2.get('code')} remark={j4.get('code')} filing={j5.get('code')}")
# 8b. 观察项(非回归):赢单态 close 仍 code=0 —— ensureCloseable 只拦已关闭/已结项,
# 票 05-08 原有行为(git 溯源 ensureCloseable 未被 16R 触碰),台账 4-3-11「全拒」对赢单态过宽,记档供产品复核。
j3 = _req("POST", "/api/project/close", {"id": pid, "closeType": "MANUAL", "closeReason": "复测8b观察项"}, tok=TOK)
step("8b [观察项·非回归] 赢单态关闭仍 code=0(票05-08 原有行为,与台账 4-3-11 记载有出入,记档)",
j3.get("code") == 0, f"close={j3.get('code')} {str(j3.get('message'))[:80]}")
d = detail(pid)
step("8c 关闭后进入已关闭终态锁:再推进/再标记全拒 68003",
str(d["projectStatus"]) == "2"
and _req("POST", "/api/project/stage/advance", {"id": pid}, tok=TOK).get("code") == 68003
and _req("POST", "/api/project/bid-result/mark", {"id": pid, "bidResult": "1"}, tok=TOK).get("code") == 68003,
f"status={d['projectStatus']}")
# 9. 动态事件流完整
tl = _req("GET", f"/api/project/timeline/list?projectId={pid}&current=1&size=50", tok=TOK)["data"]
acts = [t.get("action") for t in tl.get("content") or []]
need = ["CREATE", "FILING_SUBMIT", "CONFLICT_RELEASE", "STAGE_ADVANCE", "STAGE_ROLLBACK", "BID_WIN"]
miss = [a for a in need if a not in acts]
step("9 动态留痕全集(建项/报备/冲突解除/推进/回退/赢单)", not miss,
f"missing={miss} actions={sorted(set(acts))}")
passed = sum(1 for r in R if r["ok"])
print(f"\n==== 主链回归 {passed}/{len(R)} PASS ====")
(AUD / "retest-mainchain-result.json").write_text(json.dumps(R, ensure_ascii=False, indent=1), encoding="utf-8")
sys.exit(0 if passed == len(R) else 1)