diff --git a/backend/app/api/analytics.py b/backend/app/api/analytics.py new file mode 100644 index 0000000..a0da0b1 --- /dev/null +++ b/backend/app/api/analytics.py @@ -0,0 +1,110 @@ +""" +收视分析 API — 提供收视走势和指标卡数据 + +端点: + GET /api/analytics/years → 有收视数据的年份列表(去重降序) + GET /api/analytics/episodes?year=2026 → 指定年份所有期次的收视数据 + 年度目标 +""" + +from fastapi import APIRouter, Depends, Query +from sqlalchemy import extract +from sqlalchemy import distinct +from sqlmodel import Session, select + +from app.core.deps import require_role +from app.db.session import get_session +from app.models.episode import Episode +from app.models.yearly_target import YearlyTarget +from app.models.user import UserRole + +router = APIRouter(prefix="/api/analytics", tags=["收视分析"]) + + +def _require_read(): + """三角色都可读""" + return require_role(UserRole.zhipianren, UserRole.zebian, UserRole.biandao) + + +@router.get("/years") +def get_available_years( + session: Session = Depends(get_session), + current_user=Depends(_require_read()), +): + """返回有收视数据的年份列表(去重,降序)。""" + statement = ( + select(distinct(extract("year", Episode.air_date).label("year"))) + .where(Episode.audience_share.is_not(None)) + .order_by(extract("year", Episode.air_date).desc()) + ) + result = session.exec(statement).all() + # extract 返回 float,转 int + return [int(y) for y in result] + + +@router.get("/episodes") +def get_analytics_episodes( + year: int | None = Query(None, description="年份,不传则取最近有数据的年份"), + session: Session = Depends(get_session), + current_user=Depends(_require_read()), +): + """返回指定年份所有期次的收视数据(按 air_date 升序),附带该年年度目标。 + + 返回格式: + { + "year": 2026, + "yearly_target": { "base_target": 0.6448, "stretch_target": 0.8989 } | null, + "episodes": [ { id, episode_number, program_name, air_date, editor_name_snapshot, + audience_share, audience_rating }, ... ] + } + """ + # 如果没传 year,找最近有收视数据的年份 + if year is None: + year_stmt = ( + select(extract("year", Episode.air_date).label("y")) + .where(Episode.audience_share.is_not(None)) + .order_by(extract("year", Episode.air_date).desc()) + .limit(1) + ) + latest_year = session.exec(year_stmt).first() + if latest_year is None: + return {"year": None, "yearly_target": None, "episodes": []} + year = int(latest_year) + + # 查询该年份的期次(按 air_date 升序) + ep_stmt = ( + select(Episode) + .where(extract("year", Episode.air_date) == year) + .order_by(Episode.air_date.asc()) + ) + episodes = session.exec(ep_stmt).all() + + # 查询该年份的年度目标 + target_stmt = select(YearlyTarget).where(YearlyTarget.year == year) + target = session.exec(target_stmt).first() + + # 组装返回 + ep_list = [ + { + "id": ep.id, + "episode_number": ep.episode_number, + "program_name": ep.program_name, + "air_date": ep.air_date.isoformat() if ep.air_date else None, + "editor_name_snapshot": ep.editor_name_snapshot, + "audience_share": ep.audience_share, + "audience_rating": ep.audience_rating, + } + for ep in episodes + ] + + yearly_target = None + if target: + yearly_target = { + "base_target": target.base_target, + "stretch_target": target.stretch_target, + } + + return { + "year": year, + "yearly_target": yearly_target, + "episodes": ep_list, + } \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index bbe398c..b7aa0b4 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -17,6 +17,7 @@ from app.api.yearly_targets import router as yearly_targets_router from app.api.dashboard import router as dashboard_router from app.api.schedules import router as schedules_router from app.api.knowledge import router as knowledge_router +from app.api.analytics import router as analytics_router from app.core.config import settings app = FastAPI(title="军事科技工作台", version="0.1.0") @@ -51,6 +52,7 @@ app.include_router(users_router) app.include_router(dashboard_router) app.include_router(schedules_router) app.include_router(knowledge_router) +app.include_router(analytics_router) # 挂载静态文件目录(题图海报) _static_dir = Path(__file__).parent.parent / "static" diff --git a/frontend/src/services/analyticsService.js b/frontend/src/services/analyticsService.js new file mode 100644 index 0000000..ab82316 --- /dev/null +++ b/frontend/src/services/analyticsService.js @@ -0,0 +1,22 @@ +import http from './http' + +/** + * 获取收视分析期次数据(含年度目标) + * @param {number|null} year - 年份,不传则取最近有数据的年份 + * @returns {Promise<{year: number, yearly_target: {base_target: number, stretch_target: number}|null, episodes: Array}>} + */ +export async function getAnalyticsEpisodes(year) { + const params = {} + if (year) params.year = year + const response = await http.get('/analytics/episodes', { params }) + return response.data +} + +/** + * 获取有收视数据的年份列表(去重降序) + * @returns {Promise} + */ +export async function getAvailableYears() { + const response = await http.get('/analytics/years') + return response.data +} \ No newline at end of file