391 lines
14 KiB
Python
391 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
|
from ..auth import verify_admin_auth
|
|
from ..config import (
|
|
ACCOUNTS_PATH,
|
|
ADMIN_TASKS_PATH,
|
|
CLASSNOTES_PATH,
|
|
COURSE_SUMMARIES_ROOT,
|
|
COURSE_SUMMARY_STATE_PATH,
|
|
OPERATION_LOGS_PATH,
|
|
TEACHERS_PATH,
|
|
write_lock,
|
|
)
|
|
from ..data import (
|
|
admin_dashboard_summary,
|
|
append_operation_log,
|
|
approve_admin_task,
|
|
create_course_summary_duplicate_review_tasks,
|
|
delete_course_summary,
|
|
link_existing_course_summary_task,
|
|
list_admin_tasks,
|
|
list_operation_logs,
|
|
migrate_operation_log_labels,
|
|
query_course_summaries,
|
|
reject_admin_task,
|
|
resolve_duplicate_course_summary_task,
|
|
rollback_operation_log,
|
|
update_course_summary_body,
|
|
update_course_summary_identity,
|
|
update_course_summary_review_task,
|
|
update_course_summary_time,
|
|
)
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def rollback_backup_paths() -> list[Path]:
|
|
return [
|
|
CLASSNOTES_PATH,
|
|
ACCOUNTS_PATH,
|
|
ADMIN_TASKS_PATH,
|
|
COURSE_SUMMARIES_ROOT,
|
|
OPERATION_LOGS_PATH,
|
|
]
|
|
|
|
|
|
@router.get("/api/admin/dashboard")
|
|
def admin_dashboard(period: str = Query("month"), _user: str = Depends(verify_admin_auth)):
|
|
try:
|
|
return admin_dashboard_summary(
|
|
classnotes_path=CLASSNOTES_PATH,
|
|
accounts_path=ACCOUNTS_PATH,
|
|
teachers_path=TEACHERS_PATH,
|
|
tasks_path=ADMIN_TASKS_PATH,
|
|
summaries_root=COURSE_SUMMARIES_ROOT,
|
|
operation_logs_path=OPERATION_LOGS_PATH,
|
|
period=period,
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@router.get("/api/admin/tasks")
|
|
def admin_tasks(
|
|
status_filter: str = Query("", alias="status"),
|
|
task_type: str = Query("", alias="type"),
|
|
limit: int = Query(50, ge=1, le=1000),
|
|
offset: int = Query(0, ge=0),
|
|
_user: str = Depends(verify_admin_auth),
|
|
):
|
|
try:
|
|
return list_admin_tasks(
|
|
ADMIN_TASKS_PATH,
|
|
status_filter=status_filter,
|
|
task_type=task_type,
|
|
classnotes_path=CLASSNOTES_PATH,
|
|
offset=offset,
|
|
limit=limit,
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
|
|
|
|
@router.get("/api/admin/operation-logs")
|
|
def admin_operation_logs(
|
|
limit: int = Query(50, ge=1, le=500),
|
|
offset: int = Query(0, ge=0),
|
|
operation: str = Query(""),
|
|
status_filter: str = Query("", alias="status"),
|
|
student: str = Query(""),
|
|
_user: str = Depends(verify_admin_auth),
|
|
):
|
|
migrate_operation_log_labels(OPERATION_LOGS_PATH)
|
|
return list_operation_logs(
|
|
OPERATION_LOGS_PATH,
|
|
limit=limit,
|
|
offset=offset,
|
|
operation=operation,
|
|
status_filter=status_filter,
|
|
student=student,
|
|
backup_paths=rollback_backup_paths(),
|
|
)
|
|
|
|
|
|
@router.post("/api/admin/operation-logs/{log_id}/rollback")
|
|
def admin_rollback_operation_log(log_id: str, _user: str = Depends(verify_admin_auth)):
|
|
try:
|
|
with write_lock:
|
|
result = rollback_operation_log(OPERATION_LOGS_PATH, log_id, rollback_backup_paths())
|
|
append_operation_log(
|
|
OPERATION_LOGS_PATH,
|
|
"撤回操作",
|
|
"已撤回",
|
|
target_log_id=log_id,
|
|
target_backup_id=str(result.get("target_backup_id") or ""),
|
|
backup_id=str(result.get("backup_id") or ""),
|
|
restored_files=result.get("restored_files") or [],
|
|
rollback_mode=str(result.get("rollback_mode") or ""),
|
|
removed_lines=result.get("removed_lines") or [],
|
|
restored_hours=result.get("restored_hours") or "",
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
return {"ok": True, **result}
|
|
|
|
|
|
@router.get("/api/admin/course-summaries")
|
|
def admin_course_summaries(
|
|
q: str = Query(""),
|
|
student: str = Query(""),
|
|
teacher: str = Query(""),
|
|
subject: str = Query(""),
|
|
date_from: str = Query(""),
|
|
date_to: str = Query(""),
|
|
missing_time: bool = Query(False),
|
|
binding_status: str = Query(""),
|
|
has_candidate: str = Query(""),
|
|
limit: int = Query(50, ge=1, le=1000),
|
|
offset: int = Query(0, ge=0),
|
|
_user: str = Depends(verify_admin_auth),
|
|
):
|
|
try:
|
|
return query_course_summaries(
|
|
COURSE_SUMMARIES_ROOT,
|
|
CLASSNOTES_PATH,
|
|
q=q,
|
|
student=student,
|
|
teacher=teacher,
|
|
subject=subject,
|
|
date_from=date_from,
|
|
date_to=date_to,
|
|
missing_time=missing_time,
|
|
binding_status=binding_status,
|
|
has_candidate=has_candidate,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@router.post("/api/admin/tasks/{task_id}/approve")
|
|
def admin_approve_task(task_id: int, _user: str = Depends(verify_admin_auth)):
|
|
try:
|
|
with write_lock:
|
|
result = approve_admin_task(ADMIN_TASKS_PATH, COURSE_SUMMARIES_ROOT, CLASSNOTES_PATH, ACCOUNTS_PATH, task_id)
|
|
task = result.get("task", {})
|
|
append_operation_log(
|
|
OPERATION_LOGS_PATH,
|
|
"审核批准",
|
|
"已批准",
|
|
task_id=task_id,
|
|
task_type=str(task.get("type") or ""),
|
|
student=str(task.get("student") or task.get("corrected", {}).get("student") or ""),
|
|
source_id=str(task.get("source_id") or ""),
|
|
backup_id=str(result.get("backup_id") or task.get("backup_id") or ""),
|
|
saved_path=str(task.get("saved_path") or ""),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
return {"ok": True, **result}
|
|
|
|
|
|
@router.post("/api/admin/tasks/{task_id}/resolve-duplicate-summary")
|
|
def admin_resolve_duplicate_summary(task_id: int, payload: dict, _user: str = Depends(verify_admin_auth)):
|
|
delete_summary_id = str(payload.get("delete_summary_id") or "")
|
|
try:
|
|
with write_lock:
|
|
result = resolve_duplicate_course_summary_task(
|
|
ADMIN_TASKS_PATH,
|
|
COURSE_SUMMARIES_ROOT,
|
|
task_id,
|
|
delete_summary_id,
|
|
)
|
|
task = result.get("task", {})
|
|
append_operation_log(
|
|
OPERATION_LOGS_PATH,
|
|
"重复小结删除",
|
|
"已删除",
|
|
task_id=task_id,
|
|
task_type=str(task.get("type") or ""),
|
|
student=str(task.get("student") or ""),
|
|
summary_id=str(result.get("deleted_summary_id") or ""),
|
|
backup_id=str(result.get("backup_id") or ""),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
return {"ok": True, **result}
|
|
|
|
|
|
@router.post("/api/admin/tasks/{task_id}/course-summary-review")
|
|
def admin_update_course_summary_review(task_id: int, payload: dict, _user: str = Depends(verify_admin_auth)):
|
|
try:
|
|
with write_lock:
|
|
result = update_course_summary_review_task(
|
|
ADMIN_TASKS_PATH,
|
|
COURSE_SUMMARIES_ROOT,
|
|
COURSE_SUMMARY_STATE_PATH,
|
|
CLASSNOTES_PATH,
|
|
ACCOUNTS_PATH,
|
|
task_id,
|
|
payload,
|
|
)
|
|
task = result.get("task", {})
|
|
append_operation_log(
|
|
OPERATION_LOGS_PATH,
|
|
"课程小结审核修正",
|
|
"已更新",
|
|
task_id=task_id,
|
|
task_type=str(task.get("type") or ""),
|
|
student=str(task.get("student") or ""),
|
|
source_id=str(task.get("source_id") or ""),
|
|
proposed_line=str(task.get("proposed_line") or ""),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return {"ok": True, **result}
|
|
|
|
|
|
@router.post("/api/admin/tasks/{task_id}/link-existing-course-summary")
|
|
def admin_link_existing_course_summary(task_id: int, _user: str = Depends(verify_admin_auth)):
|
|
try:
|
|
with write_lock:
|
|
result = link_existing_course_summary_task(ADMIN_TASKS_PATH, CLASSNOTES_PATH, task_id)
|
|
task = result.get("task", {})
|
|
append_operation_log(
|
|
OPERATION_LOGS_PATH,
|
|
"课程小结关联已有记录",
|
|
"已批准",
|
|
task_id=task_id,
|
|
task_type=str(task.get("type") or ""),
|
|
student=str(task.get("student") or ""),
|
|
source_id=str(task.get("source_id") or ""),
|
|
proposed_line=str(result.get("linked_record_line") or ""),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
return {"ok": True, **result}
|
|
|
|
|
|
@router.post("/api/admin/tasks/{task_id}/reject")
|
|
def admin_reject_task(task_id: int, _user: str = Depends(verify_admin_auth)):
|
|
try:
|
|
with write_lock:
|
|
task = reject_admin_task(ADMIN_TASKS_PATH, task_id)
|
|
append_operation_log(
|
|
OPERATION_LOGS_PATH,
|
|
"审核驳回",
|
|
"已驳回",
|
|
task_id=task_id,
|
|
task_type=str(task.get("type") or ""),
|
|
student=str(task.get("student") or task.get("corrected", {}).get("student") or ""),
|
|
source_id=str(task.get("source_id") or ""),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return {"ok": True, "task": task}
|
|
|
|
|
|
@router.post("/api/admin/course-summaries/duplicate-scan")
|
|
def admin_scan_duplicate_course_summaries(_user: str = Depends(verify_admin_auth)):
|
|
try:
|
|
with write_lock:
|
|
result = create_course_summary_duplicate_review_tasks(
|
|
ADMIN_TASKS_PATH,
|
|
COURSE_SUMMARIES_ROOT,
|
|
classnotes_path=CLASSNOTES_PATH,
|
|
)
|
|
append_operation_log(
|
|
OPERATION_LOGS_PATH,
|
|
"重复小结扫描",
|
|
"待审核" if result.get("created") else "完成",
|
|
scanned_groups=int(result.get("scanned") or 0),
|
|
created_tasks=int(result.get("created") or 0),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return {"ok": True, **result}
|
|
|
|
|
|
@router.post("/api/admin/course-summaries/{summary_id}/time")
|
|
def admin_update_course_summary_time(summary_id: str, payload: dict, _user: str = Depends(verify_admin_auth)):
|
|
try:
|
|
with write_lock:
|
|
result = update_course_summary_time(COURSE_SUMMARIES_ROOT, summary_id, str(payload.get("time_range") or ""))
|
|
append_operation_log(
|
|
OPERATION_LOGS_PATH,
|
|
"课程小结补齐时间",
|
|
"已更新",
|
|
summary_id=summary_id,
|
|
time_range=str(payload.get("time_range") or ""),
|
|
backup_id=str(result.get("backup_id") or ""),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return {"ok": True, **result}
|
|
|
|
|
|
@router.post("/api/admin/course-summaries/{summary_id}/body")
|
|
def admin_update_course_summary_body(summary_id: str, payload: dict, _user: str = Depends(verify_admin_auth)):
|
|
try:
|
|
with write_lock:
|
|
result = update_course_summary_body(COURSE_SUMMARIES_ROOT, summary_id, str(payload.get("body") or ""))
|
|
append_operation_log(
|
|
OPERATION_LOGS_PATH,
|
|
"课程小结修改正文",
|
|
"已更新",
|
|
summary_id=summary_id,
|
|
backup_id=str(result.get("backup_id") or ""),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return {"ok": True, **result}
|
|
|
|
|
|
@router.post("/api/admin/course-summaries/{summary_id}/identity")
|
|
def admin_update_course_summary_identity(summary_id: str, payload: dict, _user: str = Depends(verify_admin_auth)):
|
|
try:
|
|
with write_lock:
|
|
result = update_course_summary_identity(
|
|
COURSE_SUMMARIES_ROOT,
|
|
COURSE_SUMMARY_STATE_PATH,
|
|
TEACHERS_PATH,
|
|
summary_id,
|
|
str(payload.get("student") or ""),
|
|
str(payload.get("teacher") or ""),
|
|
str(payload.get("subject") or ""),
|
|
)
|
|
append_operation_log(
|
|
OPERATION_LOGS_PATH,
|
|
"课程小结修改归属",
|
|
"已更新",
|
|
summary_id=summary_id,
|
|
new_summary_id=str(result.get("new_id") or ""),
|
|
student=str(result.get("student") or ""),
|
|
old_student=str(result.get("old_student") or ""),
|
|
teacher=str(result.get("teacher") or ""),
|
|
old_teacher=str(result.get("old_teacher") or ""),
|
|
subject=str(result.get("subject") or ""),
|
|
old_subject=str(result.get("old_subject") or ""),
|
|
old_path=str(result.get("old_path") or ""),
|
|
new_path=str(result.get("new_path") or ""),
|
|
heading=str(result.get("heading") or ""),
|
|
backup_id=str(result.get("backup_id") or ""),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return {"ok": True, **result}
|
|
|
|
|
|
@router.delete("/api/admin/course-summaries/{summary_id}")
|
|
def admin_delete_course_summary(summary_id: str, _user: str = Depends(verify_admin_auth)):
|
|
try:
|
|
with write_lock:
|
|
result = delete_course_summary(COURSE_SUMMARIES_ROOT, summary_id)
|
|
append_operation_log(
|
|
OPERATION_LOGS_PATH,
|
|
"课程小结删除",
|
|
"已删除",
|
|
summary_id=summary_id,
|
|
backup_id=str(result.get("backup_id") or ""),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return {"ok": True, **result}
|