feat:增加博客统计接口
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from .blog import router as blog_router
|
from .blog import router as blog_router
|
||||||
|
from .blog_stats import router as blog_stats_router
|
||||||
from .session import router as session_router
|
from .session import router as session_router
|
||||||
|
|
||||||
def register_routers(app: FastAPI):
|
def register_routers(app: FastAPI):
|
||||||
app.include_router(blog_router, prefix="")
|
app.include_router(blog_router, prefix="")
|
||||||
|
app.include_router(blog_stats_router, prefix="")
|
||||||
app.include_router(session_router, prefix="")
|
app.include_router(session_router, prefix="")
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ def query_blog_by_condition(query: BlogQuery = Depends(),
|
|||||||
return blog_service.query_blog_by_condition(db, query)
|
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)
|
@router.get("/{blog_id}/content", summary="查询博客内容", response_model=BlogResponse)
|
||||||
def query_blog_by_id(blog_id: int, db: Session = Depends(get_db)):
|
def query_blog_by_id(blog_id: int, db: Session = Depends(get_db)):
|
||||||
return blog_service.query_blog_by_id(db, blog_id)
|
return blog_service.query_blog_by_id(db, blog_id)
|
||||||
|
|||||||
51
api/blog_stats.py
Normal file
51
api/blog_stats.py
Normal file
@@ -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)
|
||||||
|
|
||||||
|
# 分类统计饼图
|
||||||
@@ -1 +1,15 @@
|
|||||||
fastapi~=0.115.12
|
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
|
||||||
|
|||||||
16
schemas/blog_stats.py
Normal file
16
schemas/blog_stats.py
Normal file
@@ -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)
|
||||||
@@ -13,6 +13,7 @@ from schemas.pagination import PageResult
|
|||||||
from schemas.paginate_query import paginate_query
|
from schemas.paginate_query import paginate_query
|
||||||
from middleware.exceptions import AppException
|
from middleware.exceptions import AppException
|
||||||
from service.blog_elastic_service import add_blog_elastic
|
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
|
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()
|
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:
|
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()
|
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:
|
def query_blog_stats(db: Session) -> BlogStatsResponse:
|
||||||
blog_count_stmt = select(func.count(Blog.id)).where(Blog.is_approved == 1)
|
blog_stats = query_blog_overview(db)
|
||||||
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
|
|
||||||
|
|
||||||
return BlogStatsResponse(
|
return BlogStatsResponse(
|
||||||
blogCount=blog_count,
|
blogCount=blog_stats.blogCount,
|
||||||
categoryCount=category_count,
|
categoryCount=blog_stats.categoryCount,
|
||||||
wordCount=word_count
|
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)
|
stmt = select(Blog.id, Blog.title).where(Blog.is_approved == 1).order_by(desc(Blog.update_time)).limit(5)
|
||||||
results = db.execute(stmt).fetchall()
|
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]:
|
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))
|
).where(BlogComment.blog_id == blog_id, BlogComment.is_approved == 1).order_by(desc(BlogComment.create_time))
|
||||||
results = db.execute(stmt).fetchall()
|
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:
|
def add_blog_comment(db: Session, request: Request, blog_id: int, blog_comment: BlogCommentCreate) -> bool:
|
||||||
|
|||||||
143
service/blog_stats_service.py
Normal file
143
service/blog_stats_service.py
Normal file
@@ -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]
|
||||||
Reference in New Issue
Block a user