2026-08-23 22:35:59 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""运营端业务管理端点:任务 / 服务商 / 内容 / 系统配置 / 数据统计。
|
|
|
|
|
|
|
|
|
|
|
|
全部要求 operator 角色(内部子角色再按权限细分),并写审计日志。
|
|
|
|
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
|
|
|
|
|
|
from ..dependencies import get_db
|
2026-08-24 16:57:13 +08:00
|
|
|
|
from ..schemas.operator import TaskCreateRequest, TaskStatusRequest, ProviderCreateRequest, ProviderUpdateRequest, ContentCreateRequest, ContentStatusRequest, ConfigUpdateRequest, ComputePingResponse, ComputeProvisionRequest, ComputeProvisionResponse, CourseCreateRequest, CourseStatusRequest, ActivityStatusRequest, BookingUpdateRequest, TestCreateRequest, TestStatusRequest
|
2026-08-23 23:52:58 +08:00
|
|
|
|
from ...rbac import require_permission, require_roles, write_audit
|
|
|
|
|
|
from ...infrastructure.repositories import Database
|
2026-08-24 14:16:34 +08:00
|
|
|
|
from ...services import compute_client
|
2026-08-24 16:57:13 +08:00
|
|
|
|
from ...services import training_admin_bridge as training
|
2026-08-23 22:35:59 +08:00
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/admin", tags=["admin-op"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/tasks", summary="任务列表")
|
|
|
|
|
|
async def list_tasks(
|
|
|
|
|
|
status: str | None = None,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
_u: dict = Depends(require_roles("operator")),
|
|
|
|
|
|
):
|
2026-08-23 23:52:58 +08:00
|
|
|
|
return await db.tasks.list(status=status)
|
2026-08-23 22:35:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/tasks", summary="创建任务")
|
|
|
|
|
|
async def create_task(
|
|
|
|
|
|
req: TaskCreateRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
actor: dict = Depends(require_permission("action:task.manage")),
|
|
|
|
|
|
):
|
2026-08-23 23:52:58 +08:00
|
|
|
|
task = await db.tasks.create(req.model_dump(exclude_none=True))
|
|
|
|
|
|
await write_audit(db, action="task.create", resource="task", resource_id=task["id"],
|
2026-08-23 22:35:59 +08:00
|
|
|
|
detail=task["title"], user=actor, request=request)
|
|
|
|
|
|
return task
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/tasks/{task_id}/status", summary="更新任务状态")
|
|
|
|
|
|
async def set_task_status(
|
|
|
|
|
|
task_id: str,
|
|
|
|
|
|
req: TaskStatusRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
actor: dict = Depends(require_permission("action:task.manage")),
|
|
|
|
|
|
):
|
2026-08-23 23:52:58 +08:00
|
|
|
|
if await db.tasks.get(task_id) is None:
|
2026-08-23 22:35:59 +08:00
|
|
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
2026-08-23 23:52:58 +08:00
|
|
|
|
task = await db.tasks.set_status(task_id, req.status)
|
|
|
|
|
|
await write_audit(db, action="task.status", resource="task", resource_id=task_id,
|
2026-08-23 22:35:59 +08:00
|
|
|
|
detail=req.status, user=actor, request=request)
|
|
|
|
|
|
return task
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 服务商管理 ────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.get("/providers", summary="服务商列表")
|
|
|
|
|
|
async def list_providers(
|
|
|
|
|
|
status: str | None = None,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
_u: dict = Depends(require_roles("operator")),
|
|
|
|
|
|
):
|
2026-08-23 23:52:58 +08:00
|
|
|
|
return await db.providers.list(status=status)
|
2026-08-23 22:35:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/providers", summary="创建服务商")
|
|
|
|
|
|
async def create_provider(
|
|
|
|
|
|
req: ProviderCreateRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
actor: dict = Depends(require_permission("action:provider.manage")),
|
|
|
|
|
|
):
|
2026-08-23 23:52:58 +08:00
|
|
|
|
prov = await db.providers.create(req.model_dump(exclude_none=True))
|
|
|
|
|
|
await write_audit(db, action="provider.create", resource="provider", resource_id=prov["id"],
|
2026-08-23 22:35:59 +08:00
|
|
|
|
detail=prov["name"], user=actor, request=request)
|
|
|
|
|
|
return prov
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/providers/{provider_id}/update", summary="更新服务商(评级/状态)")
|
|
|
|
|
|
async def update_provider(
|
|
|
|
|
|
provider_id: str,
|
|
|
|
|
|
req: ProviderUpdateRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
actor: dict = Depends(require_permission("action:provider.manage")),
|
|
|
|
|
|
):
|
2026-08-23 23:52:58 +08:00
|
|
|
|
updated = await db.providers.update(provider_id, req.model_dump(exclude_none=True))
|
2026-08-23 22:35:59 +08:00
|
|
|
|
if updated is None:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="Provider not found")
|
2026-08-23 23:52:58 +08:00
|
|
|
|
await write_audit(db, action="provider.update", resource="provider", resource_id=provider_id,
|
2026-08-23 22:35:59 +08:00
|
|
|
|
detail=str(req.model_dump(exclude_none=True)), user=actor, request=request)
|
|
|
|
|
|
return updated
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 内容管理 ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.get("/content", summary="内容列表")
|
|
|
|
|
|
async def list_content(
|
|
|
|
|
|
ctype: str | None = None,
|
|
|
|
|
|
status: str | None = None,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
_u: dict = Depends(require_roles("operator")),
|
|
|
|
|
|
):
|
2026-08-23 23:52:58 +08:00
|
|
|
|
return await db.content.list(ctype=ctype, status=status)
|
2026-08-23 22:35:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/content", summary="创建内容")
|
|
|
|
|
|
async def create_content(
|
|
|
|
|
|
req: ContentCreateRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
actor: dict = Depends(require_permission("action:content.manage")),
|
|
|
|
|
|
):
|
2026-08-23 23:52:58 +08:00
|
|
|
|
item = await db.content.create(req.model_dump(exclude_none=True))
|
|
|
|
|
|
await write_audit(db, action="content.create", resource="content", resource_id=item["id"],
|
2026-08-23 22:35:59 +08:00
|
|
|
|
detail=item["title"], user=actor, request=request)
|
|
|
|
|
|
return item
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/content/{content_id}/status", summary="发布/下架内容")
|
|
|
|
|
|
async def set_content_status(
|
|
|
|
|
|
content_id: str,
|
|
|
|
|
|
req: ContentStatusRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
actor: dict = Depends(require_permission("action:content.manage")),
|
|
|
|
|
|
):
|
2026-08-23 23:52:58 +08:00
|
|
|
|
item = await db.content.set_status(content_id, req.status)
|
2026-08-23 22:35:59 +08:00
|
|
|
|
if item is None:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="Content not found")
|
2026-08-23 23:52:58 +08:00
|
|
|
|
await write_audit(db, action="content.status", resource="content", resource_id=content_id,
|
2026-08-23 22:35:59 +08:00
|
|
|
|
detail=req.status, user=actor, request=request)
|
|
|
|
|
|
return item
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 系统配置 ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.get("/config", summary="系统配置列表")
|
|
|
|
|
|
async def list_config(
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
_u: dict = Depends(require_roles("operator")),
|
|
|
|
|
|
):
|
2026-08-23 23:52:58 +08:00
|
|
|
|
return await db.config.all()
|
2026-08-23 22:35:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/config/{key}", summary="更新系统配置")
|
|
|
|
|
|
async def set_config(
|
|
|
|
|
|
key: str,
|
|
|
|
|
|
req: ConfigUpdateRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
actor: dict = Depends(require_permission("action:config.manage")),
|
|
|
|
|
|
):
|
2026-08-23 23:52:58 +08:00
|
|
|
|
cfg = await db.config.set(key, req.value, req.description)
|
|
|
|
|
|
await write_audit(db, action="config.update", resource="config", resource_id=key,
|
2026-08-23 22:35:59 +08:00
|
|
|
|
detail=req.value, user=actor, request=request)
|
|
|
|
|
|
return cfg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 数据统计 ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.get("/stats/overview", summary="运营数据总览")
|
|
|
|
|
|
async def stats_overview(
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
_u: dict = Depends(require_roles("operator")),
|
|
|
|
|
|
):
|
2026-08-23 23:52:58 +08:00
|
|
|
|
return await db.stats.overview()
|
2026-08-24 14:16:34 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 算力对接(compute-engine) ─────────────────────────────────────────────
|
|
|
|
|
|
@router.post("/compute/ping", response_model=ComputePingResponse, summary="算力引擎连通性检查")
|
|
|
|
|
|
async def compute_ping(
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
_u: dict = Depends(require_roles("operator")),
|
|
|
|
|
|
):
|
|
|
|
|
|
data = await compute_client.ping()
|
|
|
|
|
|
status = str(data.get("status", "")).lower()
|
|
|
|
|
|
return ComputePingResponse(
|
|
|
|
|
|
ok=status in ("ok", "success") or "success" in status or not data.get("detail"),
|
|
|
|
|
|
version=data.get("version"),
|
|
|
|
|
|
detail=data.get("detail"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/compute/provision", response_model=ComputeProvisionResponse, summary="开通算力(建号+发PAT)")
|
|
|
|
|
|
async def compute_provision(
|
|
|
|
|
|
req: ComputeProvisionRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
actor: dict = Depends(require_roles("operator")),
|
|
|
|
|
|
):
|
|
|
|
|
|
"""为平台用户开通算力:引擎建号 + 签发 PAT,返回给前端注入 provider。"""
|
|
|
|
|
|
user = await db.users.get_by_id(req.user_id)
|
|
|
|
|
|
if user is None:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|
|
|
|
|
username = user.get("username") or user["id"]
|
|
|
|
|
|
try:
|
|
|
|
|
|
await compute_client.create_user(username)
|
|
|
|
|
|
pat = await compute_client.issue_pat(username)
|
|
|
|
|
|
created = True
|
|
|
|
|
|
except compute_client.ComputeError as exc:
|
|
|
|
|
|
raise HTTPException(status_code=502, detail=f"算力引擎对接失败: {exc}") from exc
|
|
|
|
|
|
await write_audit(db, action="compute.provision", resource="compute",
|
|
|
|
|
|
resource_id=req.user_id,
|
|
|
|
|
|
detail=f"engine user {username} provisioned",
|
|
|
|
|
|
user=actor, request=request)
|
|
|
|
|
|
return ComputeProvisionResponse(
|
|
|
|
|
|
engine_user_id=username,
|
|
|
|
|
|
engine_username=username,
|
|
|
|
|
|
pat=pat,
|
|
|
|
|
|
created=created,
|
|
|
|
|
|
)
|
2026-08-24 16:57:13 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 培训业务 · 课程(桥接 app.training courses 表)─────────────────────────
|
|
|
|
|
|
@router.get("/courses", summary="课程列表")
|
|
|
|
|
|
async def list_courses(
|
|
|
|
|
|
status: str | None = None,
|
|
|
|
|
|
_u: dict = Depends(require_roles("operator")),
|
|
|
|
|
|
):
|
|
|
|
|
|
return training.list_courses(status=status)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/courses", summary="创建课程")
|
|
|
|
|
|
async def create_course(
|
|
|
|
|
|
req: CourseCreateRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
actor: dict = Depends(require_permission("action:course.manage")),
|
|
|
|
|
|
):
|
|
|
|
|
|
course = training.create_course(req.model_dump(exclude_none=True))
|
|
|
|
|
|
await write_audit(db, action="course.create", resource="course", resource_id=course["id"],
|
|
|
|
|
|
detail=course["title"], user=actor, request=request)
|
|
|
|
|
|
return course
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/courses/{course_id}/status", summary="更新课程状态")
|
|
|
|
|
|
async def set_course_status(
|
|
|
|
|
|
course_id: str,
|
|
|
|
|
|
req: CourseStatusRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
actor: dict = Depends(require_permission("action:course.manage")),
|
|
|
|
|
|
):
|
|
|
|
|
|
course = training.set_course_status(course_id, req.status)
|
|
|
|
|
|
if course is None:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="Course not found")
|
|
|
|
|
|
await write_audit(db, action="course.status", resource="course", resource_id=course_id,
|
|
|
|
|
|
detail=req.status, user=actor, request=request)
|
|
|
|
|
|
return course
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 培训业务 · 活动(桥接 app.training events 表)──────────────────────────
|
|
|
|
|
|
@router.get("/activities", summary="活动列表")
|
|
|
|
|
|
async def list_activities(
|
|
|
|
|
|
status: str | None = None,
|
|
|
|
|
|
_u: dict = Depends(require_roles("operator")),
|
|
|
|
|
|
):
|
|
|
|
|
|
return training.list_activities(status=status)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/activities/{event_id}/status", summary="更新活动状态")
|
|
|
|
|
|
async def set_activity_status(
|
|
|
|
|
|
event_id: str,
|
|
|
|
|
|
req: ActivityStatusRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
actor: dict = Depends(require_permission("action:activity.manage")),
|
|
|
|
|
|
):
|
|
|
|
|
|
activity = training.set_activity_status(event_id, req.status)
|
|
|
|
|
|
if activity is None:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="Activity not found")
|
|
|
|
|
|
await write_audit(db, action="activity.status", resource="activity", resource_id=event_id,
|
|
|
|
|
|
detail=req.status, user=actor, request=request)
|
|
|
|
|
|
return activity
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 培训业务 · 报名(桥接 app.training bookings 表)────────────────────────
|
|
|
|
|
|
@router.get("/bookings", summary="报名列表")
|
|
|
|
|
|
async def list_bookings(
|
|
|
|
|
|
status: str | None = None,
|
|
|
|
|
|
audit_status: str | None = None,
|
|
|
|
|
|
_u: dict = Depends(require_roles("operator")),
|
|
|
|
|
|
):
|
|
|
|
|
|
return training.list_bookings(status=status, audit_status=audit_status)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/bookings/{booking_id}", summary="更新报名(审核/备注)")
|
|
|
|
|
|
async def update_booking(
|
|
|
|
|
|
booking_id: str,
|
|
|
|
|
|
req: BookingUpdateRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
actor: dict = Depends(require_permission("action:booking.manage")),
|
|
|
|
|
|
):
|
|
|
|
|
|
booking = training.update_booking(booking_id, req.model_dump(exclude_none=True))
|
|
|
|
|
|
if booking is None:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="Booking not found")
|
|
|
|
|
|
await write_audit(db, action="booking.update", resource="booking", resource_id=booking_id,
|
|
|
|
|
|
detail=str(req.model_dump(exclude_none=True)), user=actor, request=request)
|
|
|
|
|
|
return booking
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 培训业务 · 测评(桥接 app.training tests 表)──────────────────────────
|
|
|
|
|
|
@router.get("/tests", summary="测评记录列表")
|
|
|
|
|
|
async def list_tests(
|
|
|
|
|
|
_u: dict = Depends(require_roles("operator")),
|
|
|
|
|
|
):
|
|
|
|
|
|
return training.list_tests()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/tests", summary="创建测评记录")
|
|
|
|
|
|
async def create_test(
|
|
|
|
|
|
req: TestCreateRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Database = Depends(get_db),
|
|
|
|
|
|
actor: dict = Depends(require_permission("action:test.manage")),
|
|
|
|
|
|
):
|
|
|
|
|
|
item = training.create_test(req.model_dump(exclude_none=True))
|
|
|
|
|
|
await write_audit(db, action="test.create", resource="test", resource_id=item["id"],
|
|
|
|
|
|
detail=item["title"], user=actor, request=request)
|
|
|
|
|
|
return item
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/tests/{test_id}/status", summary="更新测评状态")
|
|
|
|
|
|
async def set_test_status(
|
|
|
|
|
|
test_id: str,
|
|
|
|
|
|
req: TestStatusRequest,
|
|
|
|
|
|
_u: dict = Depends(require_roles("operator")),
|
|
|
|
|
|
):
|
|
|
|
|
|
# 测评记录为不可变快照,状态变更仅回读当前记录(后续如需启用/禁用再扩展字段)
|
|
|
|
|
|
item = training.pack_test_if_exists(test_id)
|
|
|
|
|
|
if item is None:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="Test not found")
|
|
|
|
|
|
return item
|