From 8ad63f9a788de29690ac8ad30fb75134f7ea5494 Mon Sep 17 00:00:00 2001 From: Cxx0822 <1556464090@qq.com> Date: Mon, 27 Oct 2025 15:04:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E5=8D=9A=E5=AE=A2?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/__init__.py | 2 + api/blog.py | 5 ++ api/blog_stats.py | 51 ++++++++++++ requirements.txt | 16 +++- schemas/blog_stats.py | 16 ++++ service/blog_service.py | 48 ++++++++---- service/blog_stats_service.py | 143 ++++++++++++++++++++++++++++++++++ 7 files changed, 265 insertions(+), 16 deletions(-) create mode 100644 api/blog_stats.py create mode 100644 schemas/blog_stats.py create mode 100644 service/blog_stats_service.py diff --git a/api/__init__.py b/api/__init__.py index 70bac50..1293091 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -1,7 +1,9 @@ from fastapi import FastAPI from .blog import router as blog_router +from .blog_stats import router as blog_stats_router from .session import router as session_router def register_routers(app: FastAPI): app.include_router(blog_router, prefix="") + app.include_router(blog_stats_router, prefix="") app.include_router(session_router, prefix="") diff --git a/api/blog.py b/api/blog.py index 79ab3b6..435eb5a 100644 --- a/api/blog.py +++ b/api/blog.py @@ -47,6 +47,11 @@ def query_blog_by_condition(query: BlogQuery = Depends(), return blog_service.query_blog_by_condition(db, query) +@router.get("/unapproved", summary="查询待发布博客", response_model=List[BlogResponse]) +def query_unapproved_blog(db: Session = Depends(get_db), _=Depends(verify_token)): + return blog_service.query_unapproved_blog(db) + + @router.get("/{blog_id}/content", summary="查询博客内容", response_model=BlogResponse) def query_blog_by_id(blog_id: int, db: Session = Depends(get_db)): return blog_service.query_blog_by_id(db, blog_id) diff --git a/api/blog_stats.py b/api/blog_stats.py new file mode 100644 index 0000000..89787a0 --- /dev/null +++ b/api/blog_stats.py @@ -0,0 +1,51 @@ +from typing import List + +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from schemas.blog_stats import BlogOverview, BlogChartStats +from config.database import get_db +from service import blog_stats_service + +router = APIRouter( + prefix="/stats", + tags=["博客统计"], + responses={404: {"description": "Not found"}} +) + + +@router.get("/overview", summary="统计数据", response_model=BlogOverview) +def query_blog_overview(db: Session = Depends(get_db)): + return blog_stats_service.query_blog_overview(db) + + +@router.get("/category", summary="博客分类统计", response_model=List[BlogChartStats]) +def query_blog_category(db: Session = Depends(get_db)): + return blog_stats_service.query_blog_category(db) + + +@router.get("/approved/monthly", summary="每月博客发布统计", response_model=List[BlogChartStats]) +def query_blog_monthly(year: int, db: Session = Depends(get_db)): + return blog_stats_service.query_blog_approved_monthly(db, year) + + +@router.get("/visit/monthly", summary="每月博客访问统计", response_model=List[BlogChartStats]) +def query_blog_visit_monthly(year: int, db: Session = Depends(get_db)): + return blog_stats_service.query_blog_visit_monthly(db, year) + + +@router.get("/visit/rank", summary="博客访问数量排行", response_model=List[BlogChartStats]) +def query_blog_visit_rank(db: Session = Depends(get_db)): + return blog_stats_service.query_blog_visit_rank(db) + + +@router.get("/read/rank", summary="博客阅读时长排行", response_model=List[BlogChartStats]) +def query_blog_read_rank(db: Session = Depends(get_db)): + return blog_stats_service.query_blog_read_rank(db) + + +@router.get("/comment/rank", summary="博客评论数量排行", response_model=List[BlogChartStats]) +def query_blog_comment_rank(db: Session = Depends(get_db)): + return blog_stats_service.query_blog_comment_rank(db) + +# 分类统计饼图 diff --git a/requirements.txt b/requirements.txt index e422cb6..94ae768 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,15 @@ -fastapi~=0.115.12 sqlalchemy~=2.0.41 pydantic~=2.11.4 PyJWT~=2.10.1 passlib~=1.7.4 loguru~=0.7.3 pydantic-settings~=2.9.1 pymysql~=1.1.1 python-multipart~=0.0.20 uvicorn~=0.23.0 elasticsearch~=8.12.0 fluent-logger~=0.10.0 boto3~=1.40.59 botocore~=1.40.59 # pip download -r requirements.txt -d ./packages --only-binary=:all: --platform manylinux2014_x86_64 -i https://pypi.tuna.tsinghua.edu.cn/simple \ No newline at end of file +fastapi~=0.115.12 +sqlalchemy~=2.0.41 +pydantic~=2.11.4 +PyJWT~=2.10.1 +passlib~=1.7.4 +loguru~=0.7.3 +pydantic-settings~=2.9.1 +pymysql~=1.1.1 +python-multipart~=0.0.20 +uvicorn~=0.23.0 +elasticsearch~=8.12.0 +fluent-logger~=0.10.0 +boto3~=1.40.59 +botocore~=1.40.59 +# pip download -r requirements.txt -d ./packages --only-binary=:all: --platform manylinux2014_x86_64 -i https://pypi.tuna.tsinghua.edu.cn/simple diff --git a/schemas/blog_stats.py b/schemas/blog_stats.py new file mode 100644 index 0000000..d8fe365 --- /dev/null +++ b/schemas/blog_stats.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel, ConfigDict + + +class BlogOverview(BaseModel): + blogCount: int + categoryCount: int + wordCount: int + greatCount: int + visitCount: int + + +class BlogChartStats(BaseModel): + name: str + value: int + + model_config = ConfigDict(from_attributes=True) diff --git a/service/blog_service.py b/service/blog_service.py index 81d4f33..cb09974 100644 --- a/service/blog_service.py +++ b/service/blog_service.py @@ -13,6 +13,7 @@ from schemas.pagination import PageResult from schemas.paginate_query import paginate_query from middleware.exceptions import AppException from service.blog_elastic_service import add_blog_elastic +from service.blog_stats_service import query_blog_overview from utils.blog_utils import get_blog_summary, get_word_count, get_read_duration @@ -78,7 +79,31 @@ def query_blog_by_condition(db: Session, blog_query: BlogQuery) -> List[BlogResp results = db.execute(stmt).fetchall() - return [BlogResponse.from_orm(result) for result in results] + return [BlogResponse.model_validate(result) for result in results] + + +def query_unapproved_blog(db: Session) -> List[BlogResponse]: + stmt = ((select( + Blog.id, + Blog.title, + Blog.top_value.label("topValue"), + Blog.is_great.label("isGreat"), + BlogCategory.name.label("category"), + Blog.summary, + BlogContent.content.label("content"), + Blog.word_count.label("wordCount"), + Blog.read_duration.label("readDuration"), + Blog.is_approved.label("isApproved"), + Blog.create_time.label("createTime"), + Blog.update_time.label("updateTime") + ).where(Blog.is_approved == 0) + .outerjoin(BlogCategory, Blog.category_id == BlogCategory.id) + .outerjoin(BlogContent, Blog.content_id == BlogContent.id)) + .order_by(desc(Blog.update_time))) + + results = db.execute(stmt).fetchall() + + return [BlogResponse.model_validate(result) for result in results] def query_blog_by_id(db: Session, blog_id: int) -> BlogResponse: @@ -231,23 +256,16 @@ def query_blog_category(db: Session) -> List[BlogCategoryResponse]: results = db.execute(query).fetchall() - return [BlogCategoryResponse.from_orm(result) for result in results] + return [BlogCategoryResponse.model_validate(result) for result in results] def query_blog_stats(db: Session) -> BlogStatsResponse: - blog_count_stmt = select(func.count(Blog.id)).where(Blog.is_approved == 1) - blog_count = db.execute(blog_count_stmt).scalar() or 0 - - word_count_stmt = select(func.sum(Blog.word_count)).where(Blog.is_approved == 1) - word_count = db.execute(word_count_stmt).scalar() or 0 - - category_count_stmt = select(func.count(distinct(Blog.category_id))).where(Blog.is_approved == 1) - category_count = db.execute(category_count_stmt).scalar() or 0 + blog_stats = query_blog_overview(db) return BlogStatsResponse( - blogCount=blog_count, - categoryCount=category_count, - wordCount=word_count + blogCount=blog_stats.blogCount, + categoryCount=blog_stats.categoryCount, + wordCount=blog_stats.wordCount ) @@ -255,7 +273,7 @@ def query_blog_latest(db: Session) -> List[BlogLatestResponse]: stmt = select(Blog.id, Blog.title).where(Blog.is_approved == 1).order_by(desc(Blog.update_time)).limit(5) results = db.execute(stmt).fetchall() - return [BlogLatestResponse.from_orm(result) for result in results] + return [BlogLatestResponse.model_validate(result) for result in results] def query_blog_adjacent(db: Session, blog_id: int) -> List[BlogAdjacentResponse]: @@ -319,7 +337,7 @@ def query_blog_comment(db: Session, blog_id: int) -> List[BlogCommentResponse]: ).where(BlogComment.blog_id == blog_id, BlogComment.is_approved == 1).order_by(desc(BlogComment.create_time)) results = db.execute(stmt).fetchall() - return [BlogCommentResponse.from_orm(result) for result in results] + return [BlogCommentResponse.model_validate(result) for result in results] def add_blog_comment(db: Session, request: Request, blog_id: int, blog_comment: BlogCommentCreate) -> bool: diff --git a/service/blog_stats_service.py b/service/blog_stats_service.py new file mode 100644 index 0000000..cf76ebf --- /dev/null +++ b/service/blog_stats_service.py @@ -0,0 +1,143 @@ +from datetime import datetime +from typing import List + +from sqlalchemy import select, func, distinct, extract +from sqlalchemy.orm import Session + +from models.blog import Blog, BlogVisit, BlogComment, BlogCategory +from schemas.blog_stats import BlogOverview, BlogChartStats + + +def query_blog_overview(db: Session) -> BlogOverview: + blog_count_stmt = select(func.count(Blog.id)).where(Blog.is_approved == 1) + blog_count = db.execute(blog_count_stmt).scalar() or 0 + + word_count_stmt = select(func.sum(Blog.word_count)).where(Blog.is_approved == 1) + word_count = db.execute(word_count_stmt).scalar() or 0 + + category_count_stmt = select(func.count(distinct(Blog.category_id))).where(Blog.is_approved == 1) + category_count = db.execute(category_count_stmt).scalar() or 0 + + great_count_stmt = select(func.count(Blog.id)).where(Blog.is_approved == 1, Blog.is_great == 1) + great_count = db.execute(great_count_stmt).scalar() or 0 + + visit_count_stmt = select(func.count(BlogVisit.id)) + visit_count = db.execute(visit_count_stmt).scalar() or 0 + + return BlogOverview( + blogCount=blog_count, + categoryCount=category_count, + wordCount=word_count, + greatCount=great_count, + visitCount=visit_count + ) + + +def query_blog_category(db: Session) -> List[BlogChartStats]: + query = ( + select( + BlogCategory.name.label("name"), + func.count(Blog.id).label("value") + ) + .where(Blog.is_approved == 1) + .outerjoin(BlogCategory, Blog.category_id == BlogCategory.id) + .group_by(BlogCategory.name)) + + results = db.execute(query).fetchall() + + return [BlogChartStats.model_validate(result) for result in results] + + +def query_blog_approved_monthly(db: Session, year: int = None) -> List[BlogChartStats]: + current_year = datetime.now().year + target_year = year if year else current_year + + # extract():返回日期/时间的单独部分 + stmt = ( + select( + func.concat(extract('month', Blog.create_time), '月').label('name'), + func.count(Blog.id).label('value') + ) + .where( + Blog.is_approved == 1, + extract('year', Blog.create_time) == target_year + ) + .group_by('name') + ) + + results = db.execute(stmt).fetchall() + + return [BlogChartStats.model_validate(result) for result in results] + + +def query_blog_visit_monthly(db: Session, year: int = None) -> List[BlogChartStats]: + current_year = datetime.now().year + target_year = year if year else current_year + + stmt = ( + select( + func.concat(extract('month', BlogVisit.create_time), '月').label('name'), + func.count(BlogVisit.id).label('value') + ) + .where( + extract('year', BlogVisit.create_time) == target_year + ) + .group_by('name') + ) + + results = db.execute(stmt).fetchall() + + return [BlogChartStats.model_validate(result) for result in results] + + +def query_blog_visit_rank(db: Session, limit: int = 5) -> List[BlogChartStats]: + stmt = ( + select( + Blog.title.label('name'), + func.count(BlogVisit.id).label('value') + ) + .join(BlogVisit, Blog.id == BlogVisit.blog_id) + .where(Blog.is_approved == 1) + .group_by(Blog.title) + .order_by(func.count(BlogVisit.id).desc()) + .limit(limit) + ) + + results = db.execute(stmt).fetchall() + + return [BlogChartStats.model_validate(result) for result in results] + + +def query_blog_read_rank(db: Session, limit: int = 5) -> List[BlogChartStats]: + stmt = ( + select( + Blog.title.label('name'), + func.round(func.sum(Blog.read_duration)).label('value') + ) + .where(Blog.is_approved == 1) + .group_by(Blog.title) + .order_by(func.sum(Blog.read_duration).desc()) + .limit(limit) + ) + + results = db.execute(stmt).fetchall() + + return [BlogChartStats.model_validate(result) for result in results] + + +def query_blog_comment_rank(db: Session, limit: int = 5) -> List[BlogChartStats]: + stmt = ( + select( + Blog.title.label('name'), + func.count(BlogComment.id).label('value') + ) + .join(BlogComment, Blog.id == BlogComment.blog_id) + .where(Blog.is_approved == 1) + .group_by(Blog.title) + .order_by(func.count(BlogComment.id).desc()) + .limit(limit) + ) + + results = db.execute(stmt).fetchall() + + return [BlogChartStats.model_validate(result) for result in results]