feat: 收视分析后端 API + 前端 service
This commit is contained in:
@@ -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,
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user