from __future__ import annotations from fastapi import APIRouter, Body, Request from typing import Any from app.api.v1.endpoints.platform import ok, fail from app.db.platform_store import get_platform_store router = APIRouter(prefix="/projects", tags=["project"]) def _actor(request: Request) -> str | None: auth = request.headers.get("Authorization", "") token = auth.replace("Bearer ", "").strip() return token or None def _require_no_pending_approval(resource_type: str, resource_id: str) -> None: """第 4 周:写操作审批拦截——存在待审批实例时拒绝执行。""" store = get_platform_store() pending = [ i for i in store.approval_instances(status="pending") if i["resource_type"] == resource_type and i["resource_id"] == resource_id ] if pending: raise fail(409, "存在待审批的变更,请先完成审批") @router.get("") def list_projects( tenant_id: str = "default", status: str | None = None, keyword: str | None = None, ) -> dict[str, Any]: return ok( get_platform_store().projects( tenant_id=tenant_id, status=status, keyword=keyword ) ) @router.post("") def create_project(payload: dict[str, Any] = Body(...), request: Request = None) -> dict[str, Any]: store = get_platform_store() proj = store.create_project(payload) store.record_audit( action="project.create", actor_id=_actor(request) if request else None, target_type="project", target_id=proj["id"], tenant_id=proj.get("tenant_id"), detail=f"name={proj.get('name')}", ) return ok(proj) @router.get("/{project_id}") def get_project(project_id: str) -> dict[str, Any]: try: return ok(get_platform_store().project(project_id)) except KeyError: raise fail(404, "project not found") @router.put("/{project_id}") def update_project(project_id: str, payload: dict[str, Any] = Body(...), request: Request = None) -> dict[str, Any]: store = get_platform_store() try: proj = store.update_project(project_id, payload) except KeyError: raise fail(404, "project not found") store.record_audit( action="project.update", actor_id=_actor(request) if request else None, target_type="project", target_id=project_id, tenant_id=proj.get("tenant_id"), detail=f"fields={','.join(payload.keys())}", ) return ok(proj) @router.post("/{project_id}/archive") def archive_project(project_id: str, request: Request = None) -> dict[str, Any]: _require_no_pending_approval("project", project_id) store = get_platform_store() try: proj = store.archive_project(project_id) except KeyError: raise fail(404, "project not found") store.record_audit( action="project.archive", actor_id=_actor(request) if request else None, target_type="project", target_id=project_id, tenant_id=proj.get("tenant_id"), ) return ok(proj) @router.delete("/{project_id}") def delete_project(project_id: str, request: Request = None) -> dict[str, Any]: _require_no_pending_approval("project", project_id) store = get_platform_store() store.delete_project(project_id) store.record_audit( action="project.delete", actor_id=_actor(request) if request else None, target_type="project", target_id=project_id, ) return ok(None) @router.get("/{project_id}/members") def list_members(project_id: str) -> dict[str, Any]: try: return ok(get_platform_store().project_members(project_id)) except KeyError: raise fail(404, "project not found") @router.post("/{project_id}/members") def add_member(project_id: str, payload: dict[str, Any] = Body(...), request: Request = None) -> dict[str, Any]: store = get_platform_store() try: member = store.add_project_member(project_id, payload) except KeyError: raise fail(404, "project not found") store.record_audit( action="project.member.add", actor_id=_actor(request) if request else None, target_type="project.member", target_id=project_id, detail=f"user_id={payload.get('user_id')},role={payload.get('role')}", ) return ok(member) @router.put("/{project_id}/members/{user_id}") def update_member( project_id: str, user_id: str, payload: dict[str, Any] = Body(...), request: Request = None ) -> dict[str, Any]: store = get_platform_store() try: member = store.update_project_member_role(project_id, user_id, payload) except KeyError: raise fail(404, "project or member not found") store.record_audit( action="project.member.update", actor_id=_actor(request) if request else None, target_type="project.member", target_id=project_id, detail=f"user_id={user_id},role={payload.get('role')}", ) return ok(member) @router.delete("/{project_id}/members/{user_id}") def remove_member(project_id: str, user_id: str, request: Request = None) -> dict[str, Any]: store = get_platform_store() store.remove_project_member(project_id, user_id) store.record_audit( action="project.member.remove", actor_id=_actor(request) if request else None, target_type="project.member", target_id=project_id, detail=f"user_id={user_id}", ) return ok(None)