feat(task): 批量改状态与看板数据接口

- POST /items/batch-status:逐条成败,不做全回滚;拖 10 条里 1 条冲突不该让另 9 条白干
- GET /items/board:四列一次取全,收件箱不进看板
- 修:/items/board 被 /{item_id} 抢先匹配导致 422
- 新增向量库配置项(M3 用,独立实例,不动 fnsvr-01)
- 3 个新测试,全量 46 passed
This commit is contained in:
dadao
2026-10-28 04:05:35 +08:00
parent e25a1a4e31
commit c4868f04cf
6 changed files with 329 additions and 1 deletions
+90
View File
@@ -0,0 +1,90 @@
"""四视图 + 看板的首屏耗时实测(1000 条任务)。
不是压测工具,只回答一个问题:**1000 条之后,点开还会不会卡**。
跑之前先起后端:`uvicorn app.main:app --port 8100`(连开发库)。
python scripts/bench_views.py
"""
from __future__ import annotations
import asyncio
import statistics
import time
import uuid
from datetime import datetime, timedelta, timezone
import httpx
BASE = "http://127.0.0.1:8100/api/v1"
TOTAL = 1000
REPEAT = 15
async def login(client: httpx.AsyncClient) -> tuple[str, dict[str, str]]:
phone = f"137{uuid.uuid4().int % 100_000_000:08d}"
sent = await client.post(f"{BASE}/auth/sms/send", json={"phone": phone})
code = sent.json()["data"]["dev_code"]
out = await client.post(f"{BASE}/auth/login", json={"phone": phone, "code": code})
token = out.json()["data"]["tokens"]["access_token"]
return token, {"Authorization": f"Bearer {token}"}
async def main() -> None:
async with httpx.AsyncClient(timeout=30.0) as client:
_, headers = await login(client)
ws = await client.post(f"{BASE}/workspaces", json={"name": "性能实测"}, headers=headers)
ws_id = ws.json()["data"]["id"]
now = datetime.now(timezone.utc)
statuses = ["todo", "doing", "blocked", "done", "inbox"]
print(f"造 {TOTAL} 条任务……")
started = time.perf_counter()
for index in range(TOTAL):
payload = {
"title": f"测试任务 {index:04d} " + ("周报" if index % 7 == 0 else "跟进"),
"status": statuses[index % len(statuses)],
"priority": index % 4,
"due_at": (now + timedelta(days=(index % 10) - 5)).isoformat(),
"tag_names": ["批量", f"组{index % 5}"],
}
await client.post(f"{BASE}/workspaces/{ws_id}/items", json=payload, headers=headers)
print(f"写入耗时 {time.perf_counter() - started:.1f}s")
targets = {
"今天": f"/workspaces/{ws_id}/items?view=today&limit=30",
"进行中": f"/workspaces/{ws_id}/items?view=active&limit=30",
"收件箱": f"/workspaces/{ws_id}/items?view=inbox&limit=30",
"全部": f"/workspaces/{ws_id}/items?view=all&limit=30",
"搜索(周报)": f"/workspaces/{ws_id}/items?view=all&q=%E5%91%A8%E6%8A%A5&limit=30",
"看板": f"/workspaces/{ws_id}/items/board",
}
print("\n| 视图 | 首次 | P50 | P95 | 返回条数 |")
print("|---|---|---|---|---|")
for label, path in targets.items():
samples: list[float] = []
count = 0
for attempt in range(REPEAT):
t0 = time.perf_counter()
resp = await client.get(f"{BASE}{path}", headers=headers)
samples.append((time.perf_counter() - t0) * 1000)
if attempt == 0:
body = resp.json()["data"]
if "columns" in body:
count = sum(len(v) for v in body["columns"].values())
else:
count = len(body["items"])
samples.sort()
p50 = statistics.median(samples)
p95 = samples[min(len(samples) - 1, int(len(samples) * 0.95))]
print(f"| {label} | {samples[0]:.1f} ms | {p50:.1f} ms | {p95:.1f} ms | {count} |")
print(
"\n注:含网络往返与鉴权中间件;本机 127.0.0.1,所以这是**下限**,"
"真实用户还要加速度。"
)
if __name__ == "__main__":
asyncio.run(main())