feat(tasks): 阶段A剩余 —— 园区端/大屏任务 + 运营平台资金托管结算
- 大屏 /api/tasks 改为仅展示「当前园区可接」:本园专属(park_id)∪公有(park_public),状态published/claimed/doing
- 园区端 /api/park/tasks(本园发布+指派本园列表) /tasks/{id}/release(园区内发单→park_released+可见性) /tasks/{id}/assign(园区内指派→锁定被指派人)
- 运营托管结算(operator):/admin/escrows(台账) /admin/tasks/{id}/escrow(托管建档/充值,佣金5%) /admin/tasks/{id}/release(验收结算,划佣金+余额代付OPC),全部挂运营平台
This commit is contained in:
@@ -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,
|
||||
|
||||
+73
-4
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user