Files
server-core/app/api/routers/rbac_operator.py
T
Pine 866f82dbc4 feat(admin): 运营端培训业务 P0 — 新增 /admin/courses|activities|bookings|tests
新增 courses 表(app/training/db.py SEED_COURSES),经 training_admin_bridge 桥接
app.training 的课程/活动(events)/报名(bookings)/测评(tests) 数据为统一运营视图;
schemas 增 Course/Activity/Booking/Test 请求模型;rbac_operator.py 增四件套端点
(含 create/status/patch),走 operator 角色 + action:* 权限码 + write_audit。
2026-08-24 16:57:13 +08:00

336 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""运营端业务管理端点:任务 / 服务商 / 内容 / 系统配置 / 数据统计。
全部要求 operator 角色(内部子角色再按权限细分),并写审计日志。
"""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from ..dependencies import get_db
from ..schemas.operator import TaskCreateRequest, TaskStatusRequest, ProviderCreateRequest, ProviderUpdateRequest, ContentCreateRequest, ContentStatusRequest, ConfigUpdateRequest, ComputePingResponse, ComputeProvisionRequest, ComputeProvisionResponse, CourseCreateRequest, CourseStatusRequest, ActivityStatusRequest, BookingUpdateRequest, TestCreateRequest, TestStatusRequest
from ...rbac import require_permission, require_roles, write_audit
from ...infrastructure.repositories import Database
from ...services import compute_client
from ...services import training_admin_bridge as training
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")),
):
return await db.tasks.list(status=status)
@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")),
):
task = await db.tasks.create(req.model_dump(exclude_none=True))
await write_audit(db, action="task.create", resource="task", resource_id=task["id"],
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")),
):
if await db.tasks.get(task_id) is None:
raise HTTPException(status_code=404, detail="Task not found")
task = await db.tasks.set_status(task_id, req.status)
await write_audit(db, action="task.status", resource="task", resource_id=task_id,
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")),
):
return await db.providers.list(status=status)
@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")),
):
prov = await db.providers.create(req.model_dump(exclude_none=True))
await write_audit(db, action="provider.create", resource="provider", resource_id=prov["id"],
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")),
):
updated = await db.providers.update(provider_id, req.model_dump(exclude_none=True))
if updated is None:
raise HTTPException(status_code=404, detail="Provider not found")
await write_audit(db, action="provider.update", resource="provider", resource_id=provider_id,
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")),
):
return await db.content.list(ctype=ctype, status=status)
@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")),
):
item = await db.content.create(req.model_dump(exclude_none=True))
await write_audit(db, action="content.create", resource="content", resource_id=item["id"],
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")),
):
item = await db.content.set_status(content_id, req.status)
if item is None:
raise HTTPException(status_code=404, detail="Content not found")
await write_audit(db, action="content.status", resource="content", resource_id=content_id,
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")),
):
return await db.config.all()
@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")),
):
cfg = await db.config.set(key, req.value, req.description)
await write_audit(db, action="config.update", resource="config", resource_id=key,
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")),
):
return await db.stats.overview()
# ── 算力对接(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,
)
# ── 培训业务 · 课程(桥接 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