diff --git a/api/app/routers/jokes.py b/api/app/routers/jokes.py index 2f54a0f..c7a1603 100644 --- a/api/app/routers/jokes.py +++ b/api/app/routers/jokes.py @@ -1,5 +1,6 @@ import json import random +from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import func, text @@ -161,26 +162,6 @@ def list_jokes( ) -@router.get("/{joke_id}", response_model=JokeResponse) -def get_joke(joke_id: int, db: Session = Depends(get_db)): - """获取单条笑话详情""" - # 只返回已审核通过的笑话 - joke = db.query(Joke).filter( - Joke.id == joke_id, - Joke.status == "approved" - ).first() - if not joke: - raise HTTPException(status_code=404, detail="笑话不存在") - # 使用原子更新避免并发竞态 - db.query(Joke).filter(Joke.id == joke_id).update({Joke.view_count: Joke.view_count + 1}) - db.commit() - # 重新查询获取更新后的数据 - db.refresh(joke) - # 批量加载类型和人群名称 - type_map, crowd_map = _batch_load_categories(db, [joke]) - return joke_to_response(joke, type_map, crowd_map) - - @router.get("/random", response_model=JokeResponse) def get_random_joke(db: Session = Depends(get_db)): """随机获取一条已审核通过的笑话""" @@ -208,6 +189,66 @@ def get_random_joke(db: Session = Depends(get_db)): return joke_to_response(joke, type_map, crowd_map) +@router.get("/hot-monthly") +def hot_monthly_jokes(db: Session = Depends(get_db)): + """当月热门笑话:本月浏览量最高的前 10 条""" + now = datetime.now() + start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + jokes = ( + db.query(Joke) + .filter( + Joke.status == "approved", + Joke.created_at >= start_of_month, + ) + .order_by(Joke.view_count.desc()) + .limit(10) + .all() + ) + type_map, crowd_map = _batch_load_categories(db, jokes) + return [joke_to_response(j, type_map, crowd_map) for j in jokes] + + +@router.get("/stats") +def public_stats(db: Session = Depends(get_db)): + """公开统计数据:笑话总数、审核/待审/拒绝数量、总浏览/赞/踩""" + total = db.query(Joke).count() + approved = db.query(Joke).filter(Joke.status == "approved").count() + pending = db.query(Joke).filter(Joke.status == "pending").count() + rejected = db.query(Joke).filter(Joke.status == "rejected").count() + total_views = db.query(Joke).with_entities(func.sum(Joke.view_count)).scalar() or 0 + total_likes = db.query(Joke).with_entities(func.sum(Joke.like_count)).scalar() or 0 + total_dislikes = db.query(Joke).with_entities(func.sum(Joke.dislike_count)).scalar() or 0 + return { + "total_jokes": total, + "approved_jokes": approved, + "pending_jokes": pending, + "rejected_jokes": rejected, + "total_views": total_views, + "total_likes": total_likes, + "total_dislikes": total_dislikes, + } + + +@router.get("/{joke_id}", response_model=JokeResponse) +def get_joke(joke_id: int, db: Session = Depends(get_db)): + """获取单条笑话详情""" + # 只返回已审核通过的笑话 + joke = db.query(Joke).filter( + Joke.id == joke_id, + Joke.status == "approved" + ).first() + if not joke: + raise HTTPException(status_code=404, detail="笑话不存在") + # 使用原子更新避免并发竞态 + db.query(Joke).filter(Joke.id == joke_id).update({Joke.view_count: Joke.view_count + 1}) + db.commit() + # 重新查询获取更新后的数据 + db.refresh(joke) + # 批量加载类型和人群名称 + type_map, crowd_map = _batch_load_categories(db, [joke]) + return joke_to_response(joke, type_map, crowd_map) + + def _vote_joke(joke_id: int, field: str, db: Session): """通用投票逻辑:点赞或点踩(仅允许已审核通过的笑话)""" joke = db.query(Joke).filter( diff --git a/web/src/App.vue b/web/src/App.vue index 98b0127..2b1825e 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -58,6 +58,7 @@ :hot-jokes="hotJokes" :types="allTypes" :crowds="allCrowds" + :stats="stats" /> @@ -75,7 +76,7 @@ import AppSubNav from '@/components/AppSubNav.vue' import AppRightbar from '@/components/AppRightbar.vue' import Footer from '@/components/Footer.vue' import { getTypes, getCrowds } from '@/api/category' -import { getJokes } from '@/api/joke' +import { getJokes, getHotMonthly, getStats } from '@/api/joke' import { useThemeStore } from '@/stores/theme' const themeStore = useThemeStore() @@ -98,6 +99,8 @@ const filterCrowdIds = ref([]) // 热门笑话(右侧栏) const hotJokes = ref([]) +// 网站统计 +const stats = ref(null) // 右侧栏:仅首页显示 @@ -180,14 +183,19 @@ onMounted(async () => { } try { - const res = await getJokes({ page: 1, page_size: 30 }) - console.log('[APP] jokes loaded:', res.items?.length, 'total:', res.total) - const sorted = ((res.items || []).sort((a, b) => (b.view_count || 0) - (a.view_count || 0))) - hotJokes.value = sorted.slice(0, 5) + hotJokes.value = await getHotMonthly() + console.log('[APP] monthly hot jokes loaded:', hotJokes.value.length) } catch (e) { console.warn('[APP] Failed to load hot jokes:', e) hotJokes.value = [] } + + try { + stats.value = await getStats() + console.log('[APP] stats loaded:', stats.value) + } catch (e) { + console.warn('[APP] Failed to load stats:', e) + } }) diff --git a/web/src/api/joke.js b/web/src/api/joke.js index 32549b6..5cfecf2 100644 --- a/web/src/api/joke.js +++ b/web/src/api/joke.js @@ -6,3 +6,5 @@ export const getRandomJoke = () => request.get('/jokes/random') export const likeJoke = id => request.post(`/jokes/${id}/like`) export const dislikeJoke = id => request.post(`/jokes/${id}/dislike`) export const searchJokes = params => request.get('/jokes/', { params }) +export const getHotMonthly = () => request.get('/jokes/hot-monthly') +export const getStats = () => request.get('/jokes/stats') diff --git a/web/src/components/AppRightbar.vue b/web/src/components/AppRightbar.vue index 9bea4ae..bd590b7 100644 --- a/web/src/components/AppRightbar.vue +++ b/web/src/components/AppRightbar.vue @@ -1,10 +1,10 @@