"""轻量管理后台页面路由。"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Callable, Optional, cast
from fastapi import APIRouter, Depends, Query, Request
from fastapi.responses import FileResponse, HTMLResponse
from admin.auth import require_role
from admin.db import AdminUser
from admin.config import Settings, get_settings_dep
from admin.errors import DATA_NOT_FOUND, DATA_VALIDATION_FAILED
from admin.errors.exceptions import BusinessError
from admin.routes.web_public import (
_load_latest_review_result,
_resolve_order_from_token,
)
from admin.share_page import (
load_report_from_directory,
render_share_page,
status_code_for_result,
)
from data.share.short_link import (
ShortLinkService,
build_url,
route_short_link_with_report,
)
router = APIRouter(tags=["ui"])
_STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
_DASHBOARD_HTML = _STATIC_DIR / "dashboard.html"
@router.get("/admin/login", include_in_schema=False)
def admin_login_page() -> HTMLResponse:
"""管理后台 Web 登录页。"""
html = """
管理后台登录
"""
return HTMLResponse(html)
@router.get("/dashboard", include_in_schema=False)
@router.get("/admin/dashboard", include_in_schema=False)
def dashboard_page(_: AdminUser = Depends(require_role("admin"))) -> FileResponse:
"""返回最小仪表盘页面壳。"""
return FileResponse(_DASHBOARD_HTML)
@router.get("/admin/orders/new", include_in_schema=False)
def admin_new_order_page(_: AdminUser = Depends(require_role("admin"))) -> HTMLResponse:
return HTMLResponse(_render_admin_new_order_page())
@router.get("/s/{code}", include_in_schema=False)
def share_page(
code: str,
request: Request,
pwd: Optional[str] = Query(default=None),
) -> HTMLResponse:
"""公开分享页(T7.5)。"""
settings = get_settings_dep(request)
report_loader = _resolve_report_loader(request, settings)
result = route_short_link_with_report(
code,
password=pwd,
base_url=str(request.base_url).rstrip("/"),
db_path=Path(settings.share_db_path),
report_loader=report_loader,
)
html = render_share_page(result, password=pwd)
return HTMLResponse(html, status_code=status_code_for_result(result))
def _resolve_report_loader(
request: Request, settings: Settings
) -> Optional[Callable[[str], Optional[dict[str, Any]]]]:
custom_loader = getattr(request.app.state, "share_report_loader", None)
if callable(custom_loader):
return cast(Callable[[str], Optional[dict[str, Any]]], custom_loader)
report_dir = settings.share_report_dir
if not report_dir:
return None
def _loader(report_id: str) -> Optional[dict]:
return load_report_from_directory(report_id, report_dir)
return _loader
def _resolve_share_target(
*,
result_type: str,
target_token: str,
settings: Settings,
) -> tuple[Any, str, dict[str, Any]]:
try:
order = _resolve_order_from_token(target_token, settings)
except Exception as exc:
if isinstance(exc, BusinessError):
raise
if getattr(exc, "status_code", None) in {401, 404}:
raise BusinessError(
DATA_NOT_FOUND,
detail={"reason": "target_token not found"},
) from exc
raise
if result_type == "review_result":
contract = _load_latest_review_result(order.id, settings)
if contract is None:
raise BusinessError(
DATA_NOT_FOUND,
detail={"reason": "review_result not found"},
)
target_id = contract.review_result_id
share_payload = {
"title": "高考志愿复核结果",
"summary": f"风险等级:{contract.risk_level}",
"candidate_name": order.candidate_name,
"score": contract.review_constraints.get("candidate_score"),
"rank": contract.review_constraints.get("candidate_rank"),
"province": contract.review_constraints.get("candidate_province"),
"year": 2026,
"result_type": "review_result",
"risk_level": contract.risk_level,
"recommendations": [
{"school": item, "major": "复核摘要", "prob": None}
for item in contract.top_findings[:3]
],
}
return order, target_id, share_payload
if result_type == "report":
if order.status not in {"delivered", "completed"} or not order.audit_report:
raise BusinessError(
DATA_NOT_FOUND,
detail={"reason": "report not ready"},
)
target_id = order.id
share_payload = {
"title": "高考志愿报告分享",
"summary": f"订单 {order.id} - {order.service_version}",
"candidate_name": order.candidate_name,
"province": order.candidate_province,
"report_id": order.id,
"year": 2026,
"result_type": "report",
}
return order, target_id, share_payload
raise BusinessError(
DATA_VALIDATION_FAILED,
detail={"reason": "result_type must be review_result or report"},
)
def _find_latest_share_link(
*,
svc: ShortLinkService,
target_id: str,
owner_id: str,
result_type: str,
) -> Any | None:
prefix = f"{result_type}:"
for link in svc.list_by_report(target_id):
if link.owner_id != owner_id:
continue
note = str(link.note or "")
if note.startswith(prefix):
return link
return None
def _share_link_response(
*,
link: Any,
result_type: str,
target_id: str,
request: Request,
stats: dict[str, Any] | None = None,
) -> dict[str, Any]:
base_url = (
str(request.base_url).rstrip("/")
if request is not None
else "http://testserver"
)
share_url = build_url(link.code, base=base_url)
payload = {
"code": link.code,
"share_url": share_url,
"permission": link.permission,
"result_type": result_type,
"target_type": result_type,
"target_id": target_id,
"expires_at_iso": link.to_dict().get("expires_at_iso"),
"owner_id": link.owner_id,
"revoked": bool(link.revoked),
"access_count": link.access_count,
"last_access_at_iso": link.to_dict().get("last_access_at_iso"),
}
if stats is not None:
payload["stats"] = stats
return payload
@router.post("/api/share-link", summary="创建正式分享链接", status_code=201)
def create_share_link(
payload: dict[str, Any],
request: Request,
current_user: AdminUser = Depends(require_role("admin")),
settings: Settings = Depends(get_settings_dep),
) -> dict[str, Any]:
result_type = str(payload.get("result_type") or "").strip()
target_token = str(payload.get("target_token") or "").strip()
permission = str(payload.get("permission") or "read").strip()
ttl_days_raw = payload.get("ttl_days")
replace_existing = bool(payload.get("replace_existing"))
if not target_token:
raise BusinessError(
DATA_VALIDATION_FAILED,
detail={"reason": "target_token is required"},
)
ttl_days: int | None = None
if ttl_days_raw is not None:
try:
ttl_days = int(ttl_days_raw)
except (TypeError, ValueError) as exc:
raise BusinessError(
DATA_VALIDATION_FAILED,
detail={"reason": "ttl_days must be integer"},
) from exc
if ttl_days <= 0:
raise BusinessError(
DATA_VALIDATION_FAILED,
detail={"reason": "ttl_days must be > 0"},
)
order, target_id, share_payload = _resolve_share_target(
result_type=result_type,
target_token=target_token,
settings=settings,
)
svc = ShortLinkService(db_path=settings.share_db_path)
existing = _find_latest_share_link(
svc=svc,
target_id=target_id,
owner_id=current_user.username,
result_type=result_type,
)
if replace_existing and existing is not None and not existing.revoked:
svc.revoke(existing.code, owner_id=current_user.username)
link = svc.create(
report_id=target_id,
owner_id=current_user.username,
permission=permission,
ttl_days=ttl_days,
note=f"{result_type}:{order.id}",
)
share_url = build_url(
link.code,
base=str(request.base_url).rstrip("/")
if request is not None
else "http://testserver",
)
report_dir = Path(settings.share_report_dir)
report_dir.mkdir(parents=True, exist_ok=True)
report_file = report_dir / f"{target_id}.json"
report_file.write_text(
json.dumps({**share_payload, "share_url": share_url}, ensure_ascii=False),
encoding="utf-8",
)
return _share_link_response(
link=link,
result_type=result_type,
target_id=target_id,
request=request,
)
@router.get("/api/share-link/latest", summary="查询最近正式分享链接")
def latest_share_link(
result_type: str,
target_token: str,
request: Request,
current_user: AdminUser = Depends(require_role("admin")),
settings: Settings = Depends(get_settings_dep),
) -> dict[str, Any]:
_, target_id, _ = _resolve_share_target(
result_type=result_type,
target_token=target_token,
settings=settings,
)
svc = ShortLinkService(db_path=settings.share_db_path)
link = _find_latest_share_link(
svc=svc,
target_id=target_id,
owner_id=current_user.username,
result_type=result_type,
)
if link is None:
raise BusinessError(
DATA_NOT_FOUND,
detail={"reason": "share_link not found", "target_id": target_id},
)
stats = svc.get_stats(link.code)
return _share_link_response(
link=link,
result_type=result_type,
target_id=target_id,
request=request,
stats=stats,
)
@router.post("/api/share-link/{code}/revoke", summary="撤销正式分享链接")
def revoke_share_link(
code: str,
request: Request,
current_user: AdminUser = Depends(require_role("admin")),
settings: Settings = Depends(get_settings_dep),
) -> dict[str, Any]:
svc = ShortLinkService(db_path=settings.share_db_path)
revoked = svc.revoke(code, owner_id=current_user.username)
link = svc.get(code)
if link is None:
raise BusinessError(DATA_NOT_FOUND, detail={"code": code})
stats = svc.get_stats(code)
return {
"code": code,
"revoked": bool(link.revoked),
"changed": revoked,
"owner_id": link.owner_id,
"access_count": link.access_count,
"last_access_at_iso": link.to_dict().get("last_access_at_iso"),
"expires_at_iso": link.to_dict().get("expires_at_iso"),
"stats": stats,
"share_url": build_url(
code,
base=str(request.base_url).rstrip("/")
if request is not None
else "http://testserver",
),
}
def _render_admin_new_order_page() -> str:
province_options = [
"北京",
"上海",
"天津",
"重庆",
"河北",
"河南",
"山东",
"山西",
"陕西",
"辽宁",
"吉林",
"黑龙江",
"江苏",
"浙江",
"安徽",
"福建",
"江西",
"湖北",
"湖南",
"广东",
"广西",
"海南",
"四川",
"贵州",
"云南",
"甘肃",
"青海",
"宁夏",
"新疆",
"内蒙古",
"西藏",
]
province_html = "".join(
f'' for p in province_options
)
return f"""
后台手动添加订单
后台录单后台手动添加订单
用于人工服务场景下的补录、客服录单或线下沟通后的订单补建。页面会读取当前登录态调用 /api/orders。
人工补录更直接适合客服、学校渠道或线下沟通后补建订单。
与后台订单主链一致录入后仍会进入统一订单、通知与交付链路。
字段尽量按业务语义展示避免让后台录单页继续像内部测试表单。