"""轻量管理后台页面路由。""" 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

人工补录更直接适合客服、学校渠道或线下沟通后补建订单。
与后台订单主链一致录入后仍会进入统一订单、通知与交付链路。
字段尽量按业务语义展示避免让后台录单页继续像内部测试表单。
"""