feat: yearly_targets 写权限收紧,12步冒烟全绿
This commit is contained in:
@@ -147,6 +147,11 @@ AI Prompt 模板必须默认走双段、备选纯叙述。docxtpl 模板设计
|
||||
| `zebian` | 责编 | 维护仪表盘 / 知识库 / 排期 |
|
||||
| `biandao` | 编导 | 使用 TPS / 查看知识库 / 生成报题单 / 个人热点雷达 |
|
||||
|
||||
**yearly_targets 写权限收紧(2026-05-20)**:
|
||||
- POST/PATCH: zhipianren + zebian 可写,biandao 返回 403
|
||||
- DELETE: 仅 zhipianren 可操作,zebian 和 biandao 返回 403
|
||||
- GET: 三角色均可读
|
||||
|
||||
**所有 SQL CHECK、SQLModel Enum、API 鉴权、前端权限判断、菜单显示**——一律使用拼音值,**不得替换为英文**。
|
||||
|
||||
---
|
||||
|
||||
@@ -68,6 +68,25 @@
|
||||
|
||||
---
|
||||
|
||||
## 角色权限表
|
||||
|
||||
> 数据来源:`.clinerules` 5.5 节 + `project_plan.md` 第四节
|
||||
|
||||
| 资源 / 操作 | zhipianren(制片人) | zebian(责编) | biandao(编导) |
|
||||
|---|---|---|---|
|
||||
| **yearly_targets** | | | |
|
||||
| GET 列表/详情 | ✅ | ✅ | ✅ |
|
||||
| POST 新增 | ✅ | ✅ | ❌ 403 |
|
||||
| PATCH 修改 | ✅ | ✅ | ❌ 403 |
|
||||
| DELETE 删除 | ✅ | ❌ 403 | ❌ 403 |
|
||||
| **episodes** | | | |
|
||||
| GET | ✅ | ✅ | ✅ |
|
||||
| POST/PATCH/DELETE | ✅ | ✅ | 受限(见 API 文档) |
|
||||
|
||||
> 规则:.clinerules 5.5 角色枚举锁定拼音值(zhipianren / zebian / biandao),不许新增枚举。
|
||||
|
||||
---
|
||||
|
||||
## 技术栈速览
|
||||
|
||||
- 前端:React 18 + Vite + Ant Design 5 + Zustand(JavaScript,不用 TS)
|
||||
|
||||
@@ -22,9 +22,9 @@ router = APIRouter(prefix="/api/yearly_targets", tags=["年度目标"])
|
||||
@router.get("", response_model=list[YearlyTargetResponse])
|
||||
def list_targets(
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
|
||||
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian, UserRole.biandao)),
|
||||
):
|
||||
"""获取所有年度目标(仅管理员/责编)。"""
|
||||
"""获取所有年度目标(三角色均可读)。"""
|
||||
statement = select(YearlyTarget).order_by(YearlyTarget.year.desc())
|
||||
targets = session.exec(statement).all()
|
||||
return targets
|
||||
@@ -34,9 +34,9 @@ def list_targets(
|
||||
def get_target(
|
||||
target_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
|
||||
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian, UserRole.biandao)),
|
||||
):
|
||||
"""根据 ID 获取单个年度目标(仅管理员/责编)。"""
|
||||
"""根据 ID 获取单个年度目标(三角色均可读)。"""
|
||||
target = session.get(YearlyTarget, target_id)
|
||||
if target is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="目标不存在")
|
||||
@@ -101,9 +101,9 @@ def update_target(
|
||||
def delete_target(
|
||||
target_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
|
||||
current_user: User = Depends(require_role(UserRole.zhipianren)),
|
||||
):
|
||||
"""删除年度目标(仅管理员/责编)。"""
|
||||
"""删除年度目标(仅制片人)。"""
|
||||
target = session.get(YearlyTarget, target_id)
|
||||
if target is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="目标不存在")
|
||||
|
||||
@@ -21,8 +21,13 @@ class YearlyTargetCreate(YearlyTargetBase):
|
||||
pass
|
||||
|
||||
|
||||
class YearlyTargetUpdate(YearlyTargetBase):
|
||||
pass
|
||||
class YearlyTargetUpdate(BaseModel):
|
||||
"""PATCH 时允许只传部分字段,year 不可修改(由 API 层 pop 掉)。"""
|
||||
year: int | None = None
|
||||
base_target: float | None = None
|
||||
stretch_target: float | None = None
|
||||
issued_date: date | None = None
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class YearlyTargetResponse(YearlyTargetBase):
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
"""
|
||||
yearly_targets 写权限收紧 — 冒烟测试脚本
|
||||
|
||||
幂等,可重复跑。
|
||||
确保后端已启动: uvicorn app.main:app --reload
|
||||
|
||||
12 步覆盖:
|
||||
1. zhipianren POST → 201
|
||||
2. zebian POST → 201
|
||||
3. biandao POST → 403
|
||||
4. 未登录 POST → 401
|
||||
5. zhipianren PATCH → 200
|
||||
6. zebian PATCH → 200
|
||||
7. biandao PATCH → 403
|
||||
8. zhipianren DELETE → 200
|
||||
9. zebian DELETE → 403
|
||||
10. biandao DELETE → 403
|
||||
11. biandao GET 列表 → 200
|
||||
12. zhipianren GET 列表 → 200
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
backend_dir = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(backend_dir))
|
||||
|
||||
import httpx
|
||||
|
||||
BASE = "http://localhost:8000"
|
||||
HEADERS = {"Content-Type": "application/json"}
|
||||
|
||||
|
||||
def login(username: str, password: str) -> str:
|
||||
"""登录并返回 session cookie。"""
|
||||
r = httpx.post(f"{BASE}/api/auth/login", json={"username": username, "password": password})
|
||||
r.raise_for_status()
|
||||
return r.cookies.get("milsci_session", "")
|
||||
|
||||
|
||||
def step(name: str, expected_status: int, method: str, url: str, **kwargs):
|
||||
"""执行一个 HTTP 步骤,比较实际状态码与期望值。"""
|
||||
cookies = kwargs.pop("cookies", {})
|
||||
if method == "GET":
|
||||
r = httpx.get(url, cookies=cookies, timeout=10)
|
||||
elif method == "POST":
|
||||
r = httpx.post(url, cookies=cookies, headers=HEADERS, timeout=10, **kwargs)
|
||||
elif method == "PATCH":
|
||||
r = httpx.patch(url, cookies=cookies, headers=HEADERS, timeout=10, **kwargs)
|
||||
elif method == "DELETE":
|
||||
r = httpx.delete(url, cookies=cookies, timeout=10)
|
||||
else:
|
||||
raise ValueError(method)
|
||||
|
||||
status_ok = r.status_code == expected_status
|
||||
mark = "[OK]" if status_ok else f"[FAIL expected {expected_status} got {r.status_code}]"
|
||||
print(f" {mark} [{method}] {name}")
|
||||
if not status_ok:
|
||||
print(f" Response: {r.text[:300]}")
|
||||
return r, status_ok
|
||||
|
||||
|
||||
def ensure_user(cookies: dict, username: str, display_name: str, role: str, password: str = "Test1234"):
|
||||
"""幂等创建测试账号(如果不存在)。"""
|
||||
# 先尝试登录(账号可能已存在)
|
||||
try:
|
||||
r = httpx.get(f"{BASE}/api/users/me", cookies=cookies, timeout=5)
|
||||
if r.status_code == 200:
|
||||
me = r.json()
|
||||
if me.get("username") == username:
|
||||
print(f" [SKIP] user {username} already exists, reusing")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 尝试创建
|
||||
r = httpx.post(
|
||||
f"{BASE}/api/users",
|
||||
json={"username": username, "display_name": display_name, "password": password, "role": role},
|
||||
cookies=cookies,
|
||||
headers=HEADERS,
|
||||
timeout=10,
|
||||
)
|
||||
if r.status_code == 201:
|
||||
print(f" [OK] created {username} ({role})")
|
||||
elif r.status_code == 409:
|
||||
print(f" [SKIP] {username} already exists")
|
||||
else:
|
||||
print(f" [WARN] create {username} returned {r.status_code}: {r.text[:100]}")
|
||||
|
||||
|
||||
def cleanup_test_target(cookies: dict):
|
||||
"""清理测试用年度目标(2027),幂等。"""
|
||||
try:
|
||||
r = httpx.get(f"{BASE}/api/yearly_targets", cookies=cookies, timeout=10)
|
||||
if r.status_code == 200:
|
||||
for t in r.json():
|
||||
if t.get("year") == 2027:
|
||||
httpx.delete(f"{BASE}/api/yearly_targets/{t['id']}", cookies=cookies, timeout=10)
|
||||
print(f" [CLEAN] removed existing 2027 target ID={t['id']}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print(" yearly_targets 写权限收紧 — 冒烟测试")
|
||||
print("=" * 60)
|
||||
|
||||
# Step 0: 用 zhipianren 登录,准备测试账号
|
||||
print("\n[Step 0] Login as zhipianren and ensure test accounts exist")
|
||||
try:
|
||||
admin_cookie = login("simonkoson", "liutong65")
|
||||
except Exception as e:
|
||||
print(f" [FAIL] cannot login as simonkoson: {e}")
|
||||
return
|
||||
|
||||
admin_cookies = {"milsci_session": admin_cookie}
|
||||
print(" [OK] logged in as simonkoson (zhipianren)")
|
||||
|
||||
# 幂等创建 zebian / biandao 测试账号
|
||||
ensure_user(admin_cookies, "test_zebian_smoke", "TestZebra Smoke", "zebian")
|
||||
ensure_user(admin_cookies, "test_biandao_smoke", "TestBiandao Smoke", "biandao")
|
||||
|
||||
# 登录 zebian 和 biandao 获取 cookie
|
||||
print("\n[Step 0b] Login as zebian and biandao")
|
||||
zebian_cookie = login("test_zebian_smoke", "Test1234")
|
||||
zebian_cookies = {"milsci_session": zebian_cookie}
|
||||
print(" [OK] logged in as test_zebian_smoke (zebian)")
|
||||
|
||||
biandao_cookie = login("test_biandao_smoke", "Test1234")
|
||||
biandao_cookies = {"milsci_session": biandao_cookie}
|
||||
print(" [OK] logged in as test_biandao_smoke (biandao)")
|
||||
|
||||
# 清理可能残留的 2027 目标
|
||||
cleanup_test_target(admin_cookies)
|
||||
|
||||
results = []
|
||||
|
||||
# =====================================================================
|
||||
# Step 1: zhipianren POST → 201
|
||||
# =====================================================================
|
||||
print("\n[Step 1] POST new yearly target — zhipianren → 201")
|
||||
r, ok = step(
|
||||
"POST /api/yearly_targets (zhipianren)",
|
||||
201,
|
||||
"POST",
|
||||
f"{BASE}/api/yearly_targets",
|
||||
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
|
||||
cookies=admin_cookies,
|
||||
)
|
||||
target_id = r.json().get("id") if ok else None
|
||||
results.append(("Step 1: zhipianren POST 201", ok))
|
||||
|
||||
# =====================================================================
|
||||
# Step 2: zebian POST → 201
|
||||
# =====================================================================
|
||||
print("\n[Step 2] POST new yearly target — zebian → 201")
|
||||
# 清理 2027(刚被 zhipianren 创建了)
|
||||
cleanup_test_target(admin_cookies)
|
||||
r, ok = step(
|
||||
"POST /api/yearly_targets (zebian)",
|
||||
201,
|
||||
"POST",
|
||||
f"{BASE}/api/yearly_targets",
|
||||
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
|
||||
cookies=zebian_cookies,
|
||||
)
|
||||
target_id2 = r.json().get("id") if ok else None
|
||||
results.append(("Step 2: zebian POST 201", ok))
|
||||
|
||||
# =====================================================================
|
||||
# Step 3: biandao POST → 403
|
||||
# =====================================================================
|
||||
print("\n[Step 3] POST new yearly target — biandao → 403")
|
||||
cleanup_test_target(admin_cookies)
|
||||
r, ok = step(
|
||||
"POST /api/yearly_targets (biandao)",
|
||||
403,
|
||||
"POST",
|
||||
f"{BASE}/api/yearly_targets",
|
||||
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
|
||||
cookies=biandao_cookies,
|
||||
)
|
||||
results.append(("Step 3: biandao POST 403", ok))
|
||||
|
||||
# =====================================================================
|
||||
# Step 4: 未登录 POST → 401
|
||||
# =====================================================================
|
||||
print("\n[Step 4] POST new yearly target — not logged in → 401")
|
||||
r, ok = step(
|
||||
"POST /api/yearly_targets (no auth)",
|
||||
401,
|
||||
"POST",
|
||||
f"{BASE}/api/yearly_targets",
|
||||
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
|
||||
cookies={},
|
||||
)
|
||||
results.append(("Step 4: no auth POST 401", ok))
|
||||
|
||||
# =====================================================================
|
||||
# Step 5: zhipianren PATCH → 200
|
||||
# =====================================================================
|
||||
print("\n[Step 5] PATCH yearly target — zhipianren → 200")
|
||||
# 先用 admin 建一个可 PATCH 的目标
|
||||
cleanup_test_target(admin_cookies)
|
||||
r = httpx.post(
|
||||
f"{BASE}/api/yearly_targets",
|
||||
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
|
||||
cookies=admin_cookies,
|
||||
headers=HEADERS,
|
||||
timeout=10,
|
||||
)
|
||||
patch_id = r.json().get("id") if r.status_code == 201 else None
|
||||
|
||||
if patch_id:
|
||||
r, ok = step(
|
||||
"PATCH /api/yearly_targets/{id} (zhipianren)",
|
||||
200,
|
||||
"PATCH",
|
||||
f"{BASE}/api/yearly_targets/{patch_id}",
|
||||
json={"base_target": 0.67},
|
||||
cookies=admin_cookies,
|
||||
)
|
||||
else:
|
||||
ok = False
|
||||
print(" [FAIL] could not create target for PATCH test")
|
||||
results.append(("Step 5: zhipianren PATCH 200", ok))
|
||||
|
||||
# =====================================================================
|
||||
# Step 6: zebian PATCH → 200
|
||||
# =====================================================================
|
||||
print("\n[Step 6] PATCH yearly target — zebian → 200")
|
||||
cleanup_test_target(admin_cookies)
|
||||
r = httpx.post(
|
||||
f"{BASE}/api/yearly_targets",
|
||||
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
|
||||
cookies=admin_cookies,
|
||||
headers=HEADERS,
|
||||
timeout=10,
|
||||
)
|
||||
patch_id2 = r.json().get("id") if r.status_code == 201 else None
|
||||
|
||||
if patch_id2:
|
||||
r, ok = step(
|
||||
"PATCH /api/yearly_targets/{id} (zebian)",
|
||||
200,
|
||||
"PATCH",
|
||||
f"{BASE}/api/yearly_targets/{patch_id2}",
|
||||
json={"stretch_target": 0.92},
|
||||
cookies=zebian_cookies,
|
||||
)
|
||||
else:
|
||||
ok = False
|
||||
print(" [FAIL] could not create target for PATCH test")
|
||||
results.append(("Step 6: zebian PATCH 200", ok))
|
||||
|
||||
# =====================================================================
|
||||
# Step 7: biandao PATCH → 403
|
||||
# =====================================================================
|
||||
print("\n[Step 7] PATCH yearly target — biandao → 403")
|
||||
cleanup_test_target(admin_cookies)
|
||||
r = httpx.post(
|
||||
f"{BASE}/api/yearly_targets",
|
||||
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
|
||||
cookies=admin_cookies,
|
||||
headers=HEADERS,
|
||||
timeout=10,
|
||||
)
|
||||
patch_id3 = r.json().get("id") if r.status_code == 201 else None
|
||||
|
||||
if patch_id3:
|
||||
r, ok = step(
|
||||
"PATCH /api/yearly_targets/{id} (biandao)",
|
||||
403,
|
||||
"PATCH",
|
||||
f"{BASE}/api/yearly_targets/{patch_id3}",
|
||||
json={"base_target": 0.68},
|
||||
cookies=biandao_cookies,
|
||||
)
|
||||
else:
|
||||
ok = False
|
||||
print(" [FAIL] could not create target for PATCH test")
|
||||
results.append(("Step 7: biandao PATCH 403", ok))
|
||||
|
||||
# =====================================================================
|
||||
# Step 8: zhipianren DELETE → 200
|
||||
# =====================================================================
|
||||
print("\n[Step 8] DELETE yearly target — zhipianren → 200")
|
||||
cleanup_test_target(admin_cookies)
|
||||
r = httpx.post(
|
||||
f"{BASE}/api/yearly_targets",
|
||||
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
|
||||
cookies=admin_cookies,
|
||||
headers=HEADERS,
|
||||
timeout=10,
|
||||
)
|
||||
del_id = r.json().get("id") if r.status_code == 201 else None
|
||||
|
||||
if del_id:
|
||||
r, ok = step(
|
||||
"DELETE /api/yearly_targets/{id} (zhipianren)",
|
||||
204,
|
||||
"DELETE",
|
||||
f"{BASE}/api/yearly_targets/{del_id}",
|
||||
cookies=admin_cookies,
|
||||
)
|
||||
else:
|
||||
ok = False
|
||||
print(" [FAIL] could not create target for DELETE test")
|
||||
results.append(("Step 8: zhipianren DELETE 204", ok))
|
||||
|
||||
# =====================================================================
|
||||
# Step 9: zebian DELETE → 403
|
||||
# =====================================================================
|
||||
print("\n[Step 9] DELETE yearly target — zebian → 403")
|
||||
cleanup_test_target(admin_cookies)
|
||||
r = httpx.post(
|
||||
f"{BASE}/api/yearly_targets",
|
||||
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
|
||||
cookies=admin_cookies,
|
||||
headers=HEADERS,
|
||||
timeout=10,
|
||||
)
|
||||
del_id2 = r.json().get("id") if r.status_code == 201 else None
|
||||
|
||||
if del_id2:
|
||||
r, ok = step(
|
||||
"DELETE /api/yearly_targets/{id} (zebian)",
|
||||
403,
|
||||
"DELETE",
|
||||
f"{BASE}/api/yearly_targets/{del_id2}",
|
||||
cookies=zebian_cookies,
|
||||
)
|
||||
else:
|
||||
ok = False
|
||||
print(" [FAIL] could not create target for DELETE test")
|
||||
results.append(("Step 9: zebian DELETE 403", ok))
|
||||
|
||||
# =====================================================================
|
||||
# Step 10: biandao DELETE → 403
|
||||
# =====================================================================
|
||||
print("\n[Step 10] DELETE yearly target — biandao → 403")
|
||||
cleanup_test_target(admin_cookies)
|
||||
r = httpx.post(
|
||||
f"{BASE}/api/yearly_targets",
|
||||
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
|
||||
cookies=admin_cookies,
|
||||
headers=HEADERS,
|
||||
timeout=10,
|
||||
)
|
||||
del_id3 = r.json().get("id") if r.status_code == 201 else None
|
||||
|
||||
if del_id3:
|
||||
r, ok = step(
|
||||
"DELETE /api/yearly_targets/{id} (biandao)",
|
||||
403,
|
||||
"DELETE",
|
||||
f"{BASE}/api/yearly_targets/{del_id3}",
|
||||
cookies=biandao_cookies,
|
||||
)
|
||||
else:
|
||||
ok = False
|
||||
print(" [FAIL] could not create target for DELETE test")
|
||||
results.append(("Step 10: biandao DELETE 403", ok))
|
||||
|
||||
# =====================================================================
|
||||
# Step 11: biandao GET 列表 → 200
|
||||
# =====================================================================
|
||||
print("\n[Step 11] GET yearly targets list — biandao → 200")
|
||||
r, ok = step(
|
||||
"GET /api/yearly_targets (biandao)",
|
||||
200,
|
||||
"GET",
|
||||
f"{BASE}/api/yearly_targets",
|
||||
cookies=biandao_cookies,
|
||||
)
|
||||
results.append(("Step 11: biandao GET 200", ok))
|
||||
|
||||
# =====================================================================
|
||||
# Step 12: zhipianren GET 列表 → 200
|
||||
# =====================================================================
|
||||
print("\n[Step 12] GET yearly targets list — zhipianren → 200")
|
||||
r, ok = step(
|
||||
"GET /api/yearly_targets (zhipianren)",
|
||||
200,
|
||||
"GET",
|
||||
f"{BASE}/api/yearly_targets",
|
||||
cookies=admin_cookies,
|
||||
)
|
||||
results.append(("Step 12: zhipianren GET 200", ok))
|
||||
|
||||
# =====================================================================
|
||||
# Summary
|
||||
# =====================================================================
|
||||
print("\n" + "=" * 60)
|
||||
print(" 结果汇总")
|
||||
print("=" * 60)
|
||||
passed = sum(1 for _, ok in results if ok)
|
||||
total = len(results)
|
||||
for name, ok in results:
|
||||
mark = "[OK]" if ok else "[FAIL]"
|
||||
print(f" {mark} {name}")
|
||||
|
||||
print(f"\n PASS: {passed}/{total}")
|
||||
if passed == total:
|
||||
print(" ALL PASS")
|
||||
else:
|
||||
print(" SOME FAILURES")
|
||||
|
||||
print("\n测试账号已保留(供后续手动清理):")
|
||||
print(" - test_zebian_smoke (zebian)")
|
||||
print(" - test_biandao_smoke (biandao)")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -390,3 +390,68 @@ site-packages 里。**记入待办**,Phase 4 部署前或 Task 3 启动前评估
|
||||
| 20c0a1d | chore: backups/ 进 .gitignore(Task 2 收尾)(2026-05-20) |
|
||||
|
||||
**Task 2 完整收尾,Phase 2 Task 2 状态:✅ 完工。**
|
||||
|
||||
---
|
||||
|
||||
## 第十二章 yearly_targets 写权限收紧(2026-05-20)
|
||||
|
||||
**状态:✅ 完工。冒烟 12/12 全绿。**
|
||||
|
||||
### 12.1 任务来源
|
||||
|
||||
task brief 明确要求收紧 yearly_targets 表的写权限,是业务红线。
|
||||
|
||||
### 12.2 改动文件
|
||||
|
||||
| 文件 | 改动内容 |
|
||||
|------|----------|
|
||||
| `backend/app/api/yearly_targets.py` | 5处 `require_role` 参数修正 |
|
||||
| `backend/app/schemas/yearly_target.py` | `YearlyTargetUpdate` 改为部分字段可选(支持 PATCH 部分更新) |
|
||||
| `backend/scripts/smoke_yearly_targets_auth.py` | 新建,12步幂等冒烟脚本 |
|
||||
| `README.md` | 新增"角色权限表"节 |
|
||||
| `.clinerules` 5.5 | 新增 yearly_targets 写权限收紧说明 |
|
||||
| `logs/phase2_log.md` | 本章记录 |
|
||||
|
||||
### 12.3 权限矩阵最终状态
|
||||
|
||||
| 接口 | zhipianren | zebian | biandao |
|
||||
|------|-----------|--------|---------|
|
||||
| GET 列表/详情 | ✅ 200 | ✅ 200 | ✅ 200 |
|
||||
| POST 新增 | ✅ 201 | ✅ 201 | ❌ 403 |
|
||||
| PATCH 修改 | ✅ 200 | ✅ 200 | ❌ 403 |
|
||||
| DELETE 删除 | ✅ 204 | ❌ 403 | ❌ 403 |
|
||||
|
||||
### 12.4 GET 权限修改说明
|
||||
|
||||
GET 权限修改原本不在 task brief 范围,是 Plan 阶段自查发现并扩大的范围。
|
||||
|
||||
Opus 审 Plan 时同意纳入,理由:权限矩阵整体校正一次做完更合理。
|
||||
|
||||
此条在 Opus 审方案 v2 中明确记录,phase2_log 本章也写入此说明,不得糊涂带过。
|
||||
|
||||
### 12.5 冒烟实测记录
|
||||
|
||||
`backend/scripts/smoke_yearly_targets_auth.py`,12步结果:
|
||||
|
||||
| 步骤 | 场景 | 预期 | 结果 |
|
||||
|------|------|------|------|
|
||||
| 1 | zhipianren POST | 201 | ✅ |
|
||||
| 2 | zebian POST | 201 | ✅ |
|
||||
| 3 | biandao POST | 403 | ✅ |
|
||||
| 4 | 未登录 POST | 401 | ✅ |
|
||||
| 5 | zhipianren PATCH | 200 | ✅ |
|
||||
| 6 | zebian PATCH | 200 | ✅ |
|
||||
| 7 | biandao PATCH | 403 | ✅ |
|
||||
| 8 | zhipianren DELETE | 204 | ✅ |
|
||||
| 9 | zebian DELETE | 403 | ✅ |
|
||||
| 10 | biandao DELETE | 403 | ✅ |
|
||||
| 11 | biandao GET 列表 | 200 | ✅ |
|
||||
| 12 | zhipianren GET 列表 | 200 | ✅ |
|
||||
|
||||
**12/12 全部通过。**
|
||||
|
||||
### 12.6 测试账号保留
|
||||
|
||||
冒烟脚本末尾显式打印保留账号(供后续手动清理):
|
||||
- `test_zebian_smoke` (zebian)
|
||||
- `test_biandao_smoke` (biandao)
|
||||
|
||||
Reference in New Issue
Block a user