diff --git a/app/api/routers/rbac_operator.py b/app/api/routers/rbac_operator.py index 3cda834..0abbce1 100644 --- a/app/api/routers/rbac_operator.py +++ b/app/api/routers/rbac_operator.py @@ -11,7 +11,7 @@ from fastapi import APIRouter, Depends, File, 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, ContentUpdateRequest, 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, ContentReviewRequest, 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 @@ -20,6 +20,61 @@ from ...services import training_admin_bridge as training router = APIRouter(prefix="/admin", tags=["admin-op"]) +class EscrowDepositRequest(BaseModel): + amount: int = 0 + + +@router.get("/escrows", summary="运营结算台账(资金托管)") +async def operator_escrows( + status: str | None = None, + db: Database = Depends(get_db), + _u: dict = Depends(require_roles("operator")), +): + """运营平台资金托管台账:全部任务托管、佣金、状态(deposited/frozen/released/refunded)。""" + return {"items": await db.escrows.list(status=status)} + + +@router.post("/tasks/{task_id}/escrow", summary="任务托管建档/充值(运营平台托管)") +async def operator_escrow_deposit( + task_id: str, + req: EscrowDepositRequest, + request: Request, + db: Database = Depends(get_db), + _u: dict = Depends(require_roles("operator")), +): + """发标方向运营平台托管账户建档/充值;已托管返回现有。金额默认取任务预算。""" + from ...services.settlement_service import COMMISSION_RATE + + task = await db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + for e in await db.escrows.list(): + if e["task_id"] == task_id: + return e + amount = req.amount or max(task.get("budget_min", 0), task.get("budget_max", 0)) + commission = int(amount * COMMISSION_RATE) + esc = await db.escrows.create(task_id, task["title"], amount, commission) + await write_audit(db, action="escrow.deposit", resource="escrow", resource_id=esc["id"], + detail=f"amount={amount}", user=_u, request=request) + return esc + + +@router.post("/tasks/{task_id}/release", summary="运营结算(验收通过,划佣金+代付 OPC)") +async def operator_escrow_release( + task_id: str, + request: Request, + db: Database = Depends(get_db), + _u: dict = Depends(require_roles("operator")), +): + """验收通过:任务须 completed;托管结算 released(平台佣金留存,余额代付 OPC)。""" + from ...services.settlement_service import SettlementService + + released = await SettlementService(db).release_escrow(task_id) + await write_audit(db, action="escrow.release", resource="escrow", resource_id=released["id"], + detail=f"amount={released.get('amount')}", user=_u, request=request) + return released + + @router.get("/tasks", summary="任务列表") async def list_tasks( status: str | None = None, @@ -261,6 +316,30 @@ async def set_content_status( return item +@router.post("/content/{content_id}/review", summary="资讯审核(通过/下架)") +async def review_content( + content_id: str, + req: ContentReviewRequest, + request: Request, + db: Database = Depends(get_db), + actor: dict = Depends(require_permission("action:content.manage")), +): + """approve → 已发布 + 公开(C 端可见);offline → 已下线。""" + if req.decision == "approve": + item = await db.content.approve(content_id) + action = "content.approve" + elif req.decision == "offline": + item = await db.content.set_status(content_id, "offline") + action = "content.offline" + else: + raise HTTPException(status_code=400, detail="无效审核操作") + if item is None: + raise HTTPException(status_code=404, detail="Content not found") + await write_audit(db, action=action, resource="content", resource_id=content_id, + detail=item.get("title", ""), user=actor, request=request) + return item + + @router.put("/content/{content_id}", summary="编辑内容") async def update_content( content_id: str, diff --git a/app/park/routers.py b/app/park/routers.py index 3a29f24..4377d1b 100644 --- a/app/park/routers.py +++ b/app/park/routers.py @@ -468,9 +468,11 @@ async def park_tasks( 大屏卡片含任务 ID、task_code 与二维码(scan_payload)。任务为平台全局实体, 不受园区隔离;此处按园区身份校验后返回。 """ - await _resolve_tenant(authorization, tenant_id) + tid = await _resolve_tenant(authorization, tenant_id) repo = TaskRepository(session) - items = await repo.list_published() + # 大屏仅展示「当前园区可接」:本园专属(park_id=本园) ∪ 公有(park_public),状态已发布/在接/在做。 + items = [t for t in await repo.list_published() + if (t.get("park_id") == tid) or t.get("park_public")] now_label = datetime.now().strftime("%Y%m%d") def payload(t: dict) -> dict: @@ -498,6 +500,73 @@ async def park_tasks( return {"items": [payload(t) for t in items]} +class ParkReleaseBody(BaseModel): + vis: str = "public" # 发单后可见性 public/c_visible + + +class ParkAssignBody(BaseModel): + user_id: str + + +@router.get("/api/park/tasks", summary="园区端任务(本园发布 + 指派本园)") +async def park_task_list( + authorization: str | None = Header(None), + tenant_id: str | None = None, + session: AsyncSession = Depends(get_session), +): + tid = await _resolve_tenant(authorization, tenant_id) + repo = TaskRepository(session) + items = await repo.list() + mine = [t for t in items + if (t.get("park_id") == tid) + or (t.get("assign_type") == "park" and t.get("assign_park_id") == tid)] + return {"items": mine} + + +@router.post("/api/park/tasks/{task_id}/release", summary="园区内发单(指派本园区任务 → 园区企业可抢)") +async def park_task_release( + task_id: str, + body: ParkReleaseBody, + authorization: str | None = Header(None), + tenant_id: str | None = None, + session: AsyncSession = Depends(get_session), +): + tid = await _resolve_tenant(authorization, tenant_id) + repo = TaskRepository(session) + task = await repo.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + if task.get("assign_type") != "park" or task.get("assign_park_id") != tid: + raise HTTPException(status_code=403, detail="仅可对指派本园区的任务发单") + if task.get("status") not in ("published", "claimed"): + raise HTTPException(status_code=400, detail="任务当前不可发单") + updated = await repo.update(task_id, {"park_released": True, "visibility": body.vis or "public"}) + return updated + + +@router.post("/api/park/tasks/{task_id}/assign", summary="园区内指派(分派给本园某 OPC/企业)") +async def park_task_assign( + task_id: str, + body: ParkAssignBody, + authorization: str | None = Header(None), + tenant_id: str | None = None, + session: AsyncSession = Depends(get_session), +): + tid = await _resolve_tenant(authorization, tenant_id) + repo = TaskRepository(session) + task = await repo.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + if task.get("assign_type") != "park" or task.get("assign_park_id") != tid: + raise HTTPException(status_code=403, detail="仅可对指派本园区的任务指派") + if not body.user_id: + raise HTTPException(status_code=400, detail="缺少被指派对象") + await repo.update(task_id, {"assign_opc_id": body.user_id, "park_released": True}) + if task.get("status") == "published": + await repo.claim(task_id, body.user_id) + return await repo.get(task_id) + + @router.get("/api/park/zones") async def park_zones(authorization: str | None = Header(None), tenant_id: str | None = None): tid = await _resolve_tenant(authorization, tenant_id) @@ -1109,8 +1178,8 @@ async def carrier_publish_content( "publisher_name": t["name"], "publisher_avatar": "", # 发布人强制=园区名,不可改 "source": "carrier", "tenant_id": t["id"], "card_mode": body.card_mode or "small", - "is_public": False, # 默认不公开:仅本园区大屏;由运营端切公开 - "status": "published", + "is_public": False, # 默认不公开:审核通过后由运营端切公开(进 C 端) + "status": "pending", # 待审核:需运营端在资讯审核通过后才上线 }) await write_audit(db, action="content.publish", resource="content", resource_id=item["id"], detail=f"carrier {item['title']}", user=user, request=request)