diff --git a/app/api/routers/rbac_opc.py b/app/api/routers/rbac_opc.py index 007d68b..dc48f7a 100644 --- a/app/api/routers/rbac_opc.py +++ b/app/api/routers/rbac_opc.py @@ -87,6 +87,28 @@ async def opc_policy( return {"items": await db.content.list(ctype="policy", status="published")} +@router.get("/content", summary="资讯中心(按类别,已发布)") +async def opc_content( + type: str = "news", + db: Database = Depends(get_db), + _u: dict = Depends(require_roles("opc_member")), +): + """C 端资讯:type ∈ policy(政策)/news(资讯)/skill(技能)/dynamic(动态),仅 published。""" + return {"items": await db.content.list(ctype=type, status="published")} + + +@router.get("/content/{content_id}", summary="资讯详情") +async def opc_content_detail( + content_id: str, + db: Database = Depends(get_db), + _u: dict = Depends(require_roles("opc_member")), +): + item = await db.content.get(content_id) + if item is None or item.get("status") != "published": + raise HTTPException(status_code=404, detail="内容不存在") + return item + + @router.get("/finance", summary="财务流水") async def opc_finance( db: Database = Depends(get_db), diff --git a/app/api/routers/rbac_operator.py b/app/api/routers/rbac_operator.py index a3f42c3..f5bb44b 100644 --- a/app/api/routers/rbac_operator.py +++ b/app/api/routers/rbac_operator.py @@ -11,7 +11,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Response from pydantic import BaseModel from ..dependencies import get_db -from ..schemas.operator import TaskCreateRequest, TaskUpdateRequest, TaskStatusRequest, TaskAssignRequest, TaskRecommendRequest, TaskSelectRecommendRequest, ProviderCreateRequest, ProviderUpdateRequest, ContentCreateRequest, ContentStatusRequest, ConfigUpdateRequest, ComputePingResponse, ComputeBalanceRequest, ComputeProvisionRequest, ComputeProvisionResponse, CourseCreateRequest, CourseStatusRequest, CourseUpdateRequest, ActivityCreateRequest, ActivityStatusRequest, ActivityUpdateRequest, BookingUpdateRequest, TestCreateRequest, TestStatusRequest +from ..schemas.operator import TaskCreateRequest, TaskUpdateRequest, TaskStatusRequest, TaskAssignRequest, TaskRecommendRequest, TaskSelectRecommendRequest, ProviderCreateRequest, ProviderUpdateRequest, ContentCreateRequest, ContentStatusRequest, ContentUpdateRequest, ConfigUpdateRequest, ComputePingResponse, ComputeBalanceRequest, ComputeProvisionRequest, ComputeProvisionResponse, CourseCreateRequest, CourseStatusRequest, CourseUpdateRequest, ActivityCreateRequest, ActivityStatusRequest, ActivityUpdateRequest, BookingUpdateRequest, TestCreateRequest, TestStatusRequest from ...rbac import require_permission, require_roles, write_audit from ...infrastructure.repositories import Database from ...services import compute_client @@ -231,6 +231,37 @@ async def set_content_status( return item +@router.put("/content/{content_id}", summary="编辑内容") +async def update_content( + content_id: str, + req: ContentUpdateRequest, + request: Request, + db: Database = Depends(get_db), + actor: dict = Depends(require_permission("action:content.manage")), +): + item = await db.content.update(content_id, req.model_dump(exclude_unset=True, exclude_none=True)) + if item is None: + raise HTTPException(status_code=404, detail="Content not found") + await write_audit(db, action="content.update", resource="content", resource_id=content_id, + detail=item.get("title", ""), user=actor, request=request) + return item + + +@router.delete("/content/{content_id}", summary="删除内容") +async def delete_content( + content_id: str, + request: Request, + db: Database = Depends(get_db), + actor: dict = Depends(require_permission("action:content.manage")), +): + ok = await db.content.delete(content_id) + if not ok: + raise HTTPException(status_code=404, detail="Content not found") + await write_audit(db, action="content.delete", resource="content", resource_id=content_id, + detail=content_id, user=actor, request=request) + return {"ok": True} + + # ── 系统配置 ────────────────────────────────────────────────────────────── @router.get("/config", summary="系统配置列表") async def list_config( diff --git a/app/api/schemas/operator.py b/app/api/schemas/operator.py index 5c07206..b07c005 100644 --- a/app/api/schemas/operator.py +++ b/app/api/schemas/operator.py @@ -94,6 +94,15 @@ class ContentStatusRequest(BaseModel): status: str # published/offline/draft +class ContentUpdateRequest(BaseModel): + """内容更新(字段全可选,仅更新传入项)。""" + type: str | None = None # policy/news/skill/dynamic + title: str | None = None + summary: str | None = None + body: str | None = None + status: str | None = None + + class ConfigUpdateRequest(BaseModel): value: str diff --git a/app/infrastructure/repositories.py b/app/infrastructure/repositories.py index 76ce2bd..f18bdc3 100644 --- a/app/infrastructure/repositories.py +++ b/app/infrastructure/repositories.py @@ -1300,6 +1300,33 @@ class ContentRepository: await self.session.commit() return self._to_dict(c) + async def update(self, content_id: str, fields: dict) -> dict | None: + """更新内容(仅传入字段;type/title/summary/body/status)。""" + allowed = {"type", "title", "summary", "body", "status"} + patch = {k: v for k, v in fields.items() if v is not None and k in allowed} + if not patch: + return await self.get(content_id) + c = await self.session.get(ContentItem, content_id) + if c is None: + return None + for k, v in patch.items(): + setattr(c, k, v) + c.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(c) + + async def get(self, content_id: str) -> dict | None: + c = await self.session.get(ContentItem, content_id) + return self._to_dict(c) if c else None + + async def delete(self, content_id: str) -> bool: + c = await self.session.get(ContentItem, content_id) + if c is None: + return False + await self.session.delete(c) + await self.session.commit() + return True + # --------------------------------------------------------------------------- # 系统配置