feat: 收视分析后端 API + 前端 service

This commit is contained in:
simonkoson
2026-07-03 10:19:27 +08:00
parent 1c4cbd84e5
commit d537bee9a7
3 changed files with 134 additions and 0 deletions
+110
View File
@@ -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,
}
+2
View File
@@ -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.dashboard import router as dashboard_router
from app.api.schedules import router as schedules_router from app.api.schedules import router as schedules_router
from app.api.knowledge import router as knowledge_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 from app.core.config import settings
app = FastAPI(title="军事科技工作台", version="0.1.0") app = FastAPI(title="军事科技工作台", version="0.1.0")
@@ -51,6 +52,7 @@ app.include_router(users_router)
app.include_router(dashboard_router) app.include_router(dashboard_router)
app.include_router(schedules_router) app.include_router(schedules_router)
app.include_router(knowledge_router) app.include_router(knowledge_router)
app.include_router(analytics_router)
# 挂载静态文件目录(题图海报) # 挂载静态文件目录(题图海报)
_static_dir = Path(__file__).parent.parent / "static" _static_dir = Path(__file__).parent.parent / "static"
+22
View File
@@ -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<number[]>}
*/
export async function getAvailableYears() {
const response = await http.get('/analytics/years')
return response.data
}