轻松聘
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.
 
 
 

203 lines
7.3 KiB

"""
真实环境批量联调探测。
用途:拿真实 token 打后端 GET 端点,记录 HTTP 状态 / 业务 code / data 结构,
产出「前端已接但后端报错」与「后端已有但前端未接且能拿到真实字段」两类事实,
作为联调问题清单的依据。
占位符 id 通过先打列表端点动态取得(/jobs/search → jobId 等),取不到就跳过并标记 need-id。
用法:python scripts/live_probe.py
"""
import json
import re
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor
BASE = "https://job.youthome.com.cn/api/v1"
PHONE = "19900009021"
PASSWORD = "ZhilaiTest!2026#A"
SPEC_MD = "docs/职来后端接口.md"
ID_KEYS = ("id", "jobId", "applicationId", "conversationId", "interviewId",
"companyId", "candidateId", "reviewId", "notificationId", "taskId",
"attemptId", "offerId", "reportId", "blockId", "messageId", "userId",
"itemId", "cityCode", "calculationId", "replyId", "appealId", "knowledgeId")
def req(method, path, token=None, body=None, timeout=15):
url = BASE + path
data = json.dumps(body).encode() if body is not None else None
r = urllib.request.Request(url, data=data, method=method)
r.add_header("Content-Type", "application/json")
if token:
r.add_header("Authorization", "Bearer " + token)
try:
with urllib.request.urlopen(r, timeout=timeout) as resp:
raw = resp.read().decode(errors="replace")
status = resp.status
except urllib.error.HTTPError as e:
raw = e.read().decode(errors="replace")
status = e.code
except Exception as e: # 网络层 / 超时
return {"http": None, "err": str(e), "code": None, "data": None}
try:
j = json.loads(raw)
except Exception:
return {"http": status, "err": raw[:120], "code": None, "data": None}
if isinstance(j, dict):
return {"http": status, "code": j.get("code"), "message": j.get("message"),
"data": j.get("data")}
return {"http": status, "code": None, "data": j}
def shape(value, depth=0):
"""递归描述返回结构(只到 2 层,够看字段命名)"""
if depth > 2:
return "..."
if isinstance(value, dict):
return {k: shape(v, depth + 1) for k, v in list(value.items())[:14]}
if isinstance(value, list):
if not value:
return "[]"
return [shape(value[0], depth + 1)]
return type(value).__name__
def load_endpoints(method_filter=None):
out = []
entry_re = re.compile(r"^(GET|POST|PUT|PATCH|DELETE)\s+(/\S*)\s*$")
with open(SPEC_MD, encoding="utf-8") as f:
for ln in f:
m = entry_re.match(ln)
if not m:
continue
if method_filter and m.group(1) not in method_filter:
continue
out.append((m.group(1), m.group(2)))
return sorted(set(out))
def pick_ids(token):
"""从列表端点抓真实 id,供带占位符的端点使用"""
ids = {}
plan = [
("/jobs/search?page=1&pageSize=1", "jobId"),
("/applications/me?page=1&pageSize=1", "applicationId"),
("/conversations", "conversationId"),
("/notifications?page=1&pageSize=1", "notificationId"),
("/offers?page=1&pageSize=1", "offerId"),
("/assessments/me", "attemptId"),
("/reports/me?page=1&pageSize=1", "reportId"),
("/blocks/me", "blockId"),
("/companies/me", "companyId"),
("/interviews/me?page=1&pageSize=1", "interviewId"),
]
for path, key in plan:
res = req("GET", path, token)
data = res.get("data")
if isinstance(data, dict):
for listkey in ("items", "records", "list", "content"):
if isinstance(data.get(listkey), list):
data = data[listkey]
break
if isinstance(data, list) and data:
first = data[0]
if isinstance(first, dict):
ids[key] = first.get("id") or first.get(key)
elif isinstance(data, dict):
ids[key] = data.get("id")
if ids.get(key):
ids[key.split("Id")[0] + "Id"] = ids[key]
# jobId 兜底:/jobs 列表
if not ids.get("jobId"):
res = req("GET", "/jobs?page=1&pageSize=1", token)
d = res.get("data")
if isinstance(d, dict):
for k in ("items", "records", "list", "content"):
if isinstance(d.get(k), list):
d = d[k]
break
if isinstance(d, list) and d and isinstance(d[0], dict):
ids["jobId"] = d[0].get("id")
return ids
def fill(path, ids):
"""把 {xxx} 占位符换成真实 id;换不掉返回 None"""
def sub(m):
name = m.group(1)
for k in (name, name + "Id"):
if ids.get(k):
return str(ids[k])
# 退而求其次:任意同名前缀
base = name.replace("Id", "")
for k, v in ids.items():
if k.replace("Id", "") == base and v:
return str(v)
return None
filled = re.sub(r"\{([^}]+)\}", lambda m: sub(m) or "{" + m.group(1) + "}", path)
return None if "{" in filled else filled
def main():
login = req("POST", "/auth/password/login", body={"phone": PHONE, "password": PASSWORD})
token = (login.get("data") or {}).get("accessToken")
if not token:
print("登录失败:", login)
return
print(f"登录成功 role={(login.get('data') or {}).get('role')} userId={(login.get('data') or {}).get('userId')}")
ids = pick_ids(token)
print("抓到的 id:", {k: v for k, v in ids.items() if v})
endpoints = load_endpoints({"GET"})
targets = []
skipped = []
for method, path in endpoints:
if "{" in path:
filled = fill(path, ids)
if filled:
targets.append((method, path, filled))
else:
skipped.append(path)
else:
targets.append((method, path, path))
print(f"\nGET 端点 {len(endpoints)} 个:可探测 {len(targets)},缺 id 跳过 {len(skipped)}")
results = []
def probe(item):
method, raw, filled = item
res = req(method, filled, token)
return {
"method": method,
"path": raw,
"url": filled,
"http": res.get("http"),
"code": res.get("code"),
"message": res.get("message"),
"err": res.get("err"),
"shape": shape(res.get("data")) if res.get("http") == 200 else None,
}
with ThreadPoolExecutor(max_workers=8) as ex:
results = list(ex.map(probe, targets))
ok = [r for r in results if r["http"] == 200 and r["code"] in (200, 0, None)]
bad = [r for r in results if r not in ok]
print(f"\n探测完成:正常 {len(ok)} / 异常 {len(bad)}")
print("\n=== 异常端点(HTTP != 200 或 code 非 200)===")
for r in sorted(bad, key=lambda x: x["path"]):
print(f" {r['path']:58} http={r['http']} code={r['code']} msg={(r.get('message') or r.get('err') or '')[:60]}")
with open(".workbuddy/tmp/live_probe.json", "w", encoding="utf-8") as f:
json.dump({"ids": ids, "results": results, "skipped": skipped}, f,
ensure_ascii=False, indent=2)
print("\n完整结果: .workbuddy/tmp/live_probe.json")
if __name__ == "__main__":
main()