From 076812f1aa179f2f865122e48e12422842cdda73 Mon Sep 17 00:00:00 2001 From: bwstudio Date: Fri, 12 Jun 2026 05:48:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BD=93=E6=9C=88=E7=83=AD=E9=97=A8?= =?UTF-8?q?=E7=AC=91=E8=AF=9D=20+=20=E7=BB=9F=E8=AE=A1=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=E9=99=A4=E9=A6=96=E9=A1=B5=E6=B8=85=E9=99=A4?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 GET /api/jokes/hot-monthly(当月浏览量 top 10) - 后端新增 GET /api/jokes/stats(公开统计:总数/审核/待审/浏览等) - 右侧栏热门改为当月热门笑话 - 右侧栏新增网站统计区(笑话总数/已审核/待审核/总浏览) - 首页删除清除筛选按钮 --- api/app/routers/jokes.py | 81 ++++++++++++++++++++++-------- web/src/App.vue | 18 +++++-- web/src/api/joke.js | 2 + web/src/components/AppRightbar.vue | 66 ++++++++++++++++++++++-- web/src/views/home/index.vue | 6 --- 5 files changed, 139 insertions(+), 34 deletions(-) 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 @@