Compare commits

...

10 Commits

18 changed files with 858 additions and 299 deletions

View File

@@ -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="")

View File

@@ -1,6 +1,6 @@
from typing import List
from fastapi import APIRouter, Depends, Query, Request
from fastapi import APIRouter, Depends, Query, Request, UploadFile, File, Path, Body
from sqlalchemy.orm import Session
from config.auth import verify_token
@@ -10,7 +10,7 @@ from schemas.blog import BlogResponse, BlogQuery, BlogCategoryResponse, BlogStat
from schemas.blog_elastic import BlogSearch
from schemas.pagination import PageResult
from config.database import get_db
from service import blog_service, blog_elastic_service
from service import blog_service, blog_elastic_service, file_service
router = APIRouter(
prefix="/blog",
@@ -19,61 +19,111 @@ router = APIRouter(
)
@router.post("/file/upload", summary="上传博客图片", response_model=str)
async def sync_all_blog(md5: str = Query(..., description="文件MD5值"),
file: UploadFile = File(..., description="要上传的图片文件")):
return await file_service.upload_file(md5, file)
@router.get("/sync", summary="同步博客到elastic", response_model=bool)
def sync_all_blog(db: Session = Depends(get_db)):
return blog_elastic_service.sync_all_blog(db)
@router.get("/search", summary="搜索elastic", response_model=List[BlogSearch])
def search_blog(keyword: str):
def search_blog(keyword: str = Query(..., description="搜索关键词")):
return blog_elastic_service.search_blog(keyword)
@router.get("/page", summary="分页查询博客", response_model=PageResult[BlogResponse])
def query_blog_by_page(current_page: int = Query(1, ge=1, alias="currentPage", description="当前页码"),
page_size: int = Query(10, ge=1, le=100, alias="pageSize", description="每页数量"),
db: Session = Depends(get_db)):
def query_blog_by_page(
current_page: int = Query(1, ge=1, alias="currentPage", description="当前页码从1开始"),
page_size: int = Query(10, ge=1, le=100, alias="pageSize", description="每页显示数量1-100之间"),
db: Session = Depends(get_db)
):
return blog_service.query_blog_by_page(db, current_page, page_size)
@router.get("/condition", summary="条件查询博客", response_model=List[BlogResponse])
def query_blog_by_condition(query: BlogQuery = Depends(),
db: Session = Depends(get_db)):
def query_blog_by_condition(query: BlogQuery = Depends(), db: Session = Depends(get_db)):
return blog_service.query_blog_by_condition(db, query)
@router.get("/condition/page", summary="条件分页查询博客", response_model=PageResult[BlogResponse])
def query_blog_by_condition_page(
query: BlogQuery = Depends(),
current_page: int = Query(1, ge=1, alias="currentPage", description="当前页码从1开始"),
page_size: int = Query(10, ge=1, le=100, alias="pageSize", description="每页显示数量1-100之间"),
db: Session = Depends(get_db)
):
return blog_service.query_blog_by_condition_page(db, query, current_page, page_size)
@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)):
def query_blog_by_id(blog_id: int = Path(..., ge=1, description="博客ID"), db: Session = Depends(get_db)):
return blog_service.query_blog_by_id(db, blog_id)
@router.get("/all/page", summary="分页查询博客", response_model=PageResult[BlogResponse])
def query_blog_by_page(
current_page: int = Query(1, ge=1, alias="currentPage", description="当前页码从1开始"),
page_size: int = Query(10, ge=1, le=100, alias="pageSize", description="每页显示数量1-100之间"),
db: Session = Depends(get_db),
_=Depends(verify_token)
):
return blog_service.query_blog_by_page(db, current_page, page_size, True)
@router.post("", summary="新增博客内容", response_model=bool)
def add_blog(blog: BlogCreate, db: Session = Depends(get_db), _=Depends(verify_token)):
def add_blog(
blog: BlogCreate = Body(..., description="博客创建数据"),
db: Session = Depends(get_db),
_=Depends(verify_token)
):
return blog_service.add_blog(db, blog)
@router.put("/{blog_id}", summary="更新博客内容", response_model=bool)
def update_blog(blog_id: int, blog: BlogUpdate, db: Session = Depends(get_db), _=Depends(verify_token)):
def update_blog(
blog_id: int = Path(..., ge=1, description="博客ID"),
blog: BlogUpdate = Body(..., description="博客更新数据"),
db: Session = Depends(get_db),
_=Depends(verify_token)
):
return blog_service.update_blog(db, blog_id, blog)
@router.delete("/{blog_id}", summary="删除博客内容", response_model=bool)
def delete_blog(blog_id: int, db: Session = Depends(get_db), _=Depends(verify_token)):
def delete_blog(
blog_id: int = Path(..., ge=1, description="博客ID"),
db: Session = Depends(get_db),
_=Depends(verify_token)
):
return blog_service.delete_blog(db, blog_id)
@router.get("/{blog_id}/adjacent", summary="查询相邻博客", response_model=List[BlogAdjacentResponse])
def query_blog_latest(blog_id: int, db: Session = Depends(get_db)):
def query_blog_latest(blog_id: int = Path(..., ge=1, description="博客ID"), db: Session = Depends(get_db)):
return blog_service.query_blog_adjacent(db, blog_id)
@router.put("/{blog_id}/comment", summary="新增博客评论", response_model=bool)
def add_blog_comment(blog_id: int, blog_comment: BlogCommentCreate, request: Request, db: Session = Depends(get_db)):
def add_blog_comment(
request: Request,
blog_id: int = Path(..., ge=1, description="博客ID"),
blog_comment: BlogCommentCreate = Body(..., description="博客评论数据"),
db: Session = Depends(get_db)
):
return blog_service.add_blog_comment(db, request, blog_id, blog_comment)
@router.get("/{blog_id}/comment", summary="查询博客评论", response_model=list[BlogCommentResponse])
def query_blog_comment(blog_id: int, db: Session = Depends(get_db)):
def query_blog_comment(blog_id: int = Path(..., ge=1, description="博客ID"), db: Session = Depends(get_db)):
return blog_service.query_blog_comment(db, blog_id)
@@ -88,9 +138,11 @@ def query_blog_stats(db: Session = Depends(get_db)):
@router.get("/visit", summary="查询博客访问信息", response_model=PageResult[BlogVisitResponse])
def query_blog_visit(current_page: int = Query(1, ge=1, alias="currentPage", description="当前页码"),
page_size: int = Query(10, ge=1, le=100, alias="pageSize", description="每页数量"),
db: Session = Depends(get_db)):
def query_blog_visit(
current_page: int = Query(1, ge=1, alias="currentPage", description="当前页码从1开始"),
page_size: int = Query(10, ge=1, le=100, alias="pageSize", description="每页显示数量1-100之间"),
db: Session = Depends(get_db)
):
return blog_service.query_blog_visit(db, current_page, page_size)

51
api/blog_stats.py Normal file
View 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)
# 分类统计饼图

157
blog.sql Normal file
View File

@@ -0,0 +1,157 @@
-- MySQL dump 10.13 Distrib 8.0.26, for Win64 (x86_64)
--
-- Host: localhost Database: blog
-- ------------------------------------------------------
-- Server version 8.0.27
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `blog`
--
DROP TABLE IF EXISTS `blog`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `blog` (
`id` bigint NOT NULL,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '博客标题',
`top_value` int NOT NULL COMMENT '置顶值 越大越靠前',
`is_great` tinyint NOT NULL COMMENT '是否是精品 0否1是',
`category_id` bigint NOT NULL COMMENT '博客类别',
`summary` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '博客内容概要',
`content_id` bigint NOT NULL COMMENT '博客内容',
`word_count` int NOT NULL COMMENT '字数统计',
`read_duration` decimal(10,2) NOT NULL COMMENT '阅读时长',
`is_approved` tinyint NOT NULL COMMENT '是否发布',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '发布时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '发布人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='博客记录';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `blog_category`
--
DROP TABLE IF EXISTS `blog_category`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `blog_category` (
`id` bigint NOT NULL,
`name` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '类别名称',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='博客类别';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `blog_comment`
--
DROP TABLE IF EXISTS `blog_comment`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `blog_comment` (
`id` bigint NOT NULL COMMENT 'id',
`blog_id` bigint NOT NULL COMMENT '博客id',
`parent_id` bigint NOT NULL COMMENT '父评论id',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论人昵称',
`website` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '评论人网站',
`ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论人ip',
`user_agent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论人浏览器信息',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论内容',
`is_approved` tinyint NOT NULL COMMENT '是否通过',
`create_time` datetime DEFAULT NULL COMMENT '评论时间',
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='博客评论';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `blog_content`
--
DROP TABLE IF EXISTS `blog_content`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `blog_content` (
`id` bigint NOT NULL,
`content` mediumblob NOT NULL COMMENT '博客内容',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='博客内容';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `blog_like`
--
DROP TABLE IF EXISTS `blog_like`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `blog_like` (
`id` bigint NOT NULL COMMENT 'id',
`user_id` bigint DEFAULT NULL COMMENT '点赞人',
`type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '点赞类型(博客、评论)',
`target_id` bigint DEFAULT NULL COMMENT '点赞目标的id',
`status` tinyint DEFAULT NULL COMMENT '点赞状态 0取消点赞/1点赞',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`update_by` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='博客点赞';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `blog_visit`
--
DROP TABLE IF EXISTS `blog_visit`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `blog_visit` (
`id` bigint NOT NULL,
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'ip地址',
`os` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '操作系统',
`browser` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '浏览器',
`uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '路径',
`blog_id` bigint DEFAULT NULL COMMENT '博客id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='博客访问记录';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping routines for database 'blog'
--
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2025-10-23 19:56:43

View File

@@ -10,8 +10,11 @@ ES_PASSWORD = "19940822Cxx"
BLOG_INDEX = "blog"
# 注意Python的elasticsearch的版本要和服务器的一致
es = Elasticsearch(ES_HOST, http_auth=(ES_USER, ES_PASSWORD))
try:
es = Elasticsearch(ES_HOST, http_auth=(ES_USER, ES_PASSWORD))
if not es.indices.exists(index=BLOG_INDEX):
if not es.indices.exists(index=BLOG_INDEX):
es.indices.create(index=BLOG_INDEX, body=BLOG_MAPPING)
logger.info(f"Elastic索引 {BLOG_INDEX} 创建成功")
except Exception as e:
logger.error(f"elastic异常: {str(e)}")

View File

@@ -3,18 +3,13 @@ import sys
from fluent import sender
from loguru import logger
from pathlib import Path
from config.setting import settings
FLUENTD_HOST = 'host.docker.internal'
FLUENTD_HOST = settings.FLUENTD_HOST
FLUENTD_PORT = 24224
TOPIC_TAG = 'blog-service'
# 日志目录
LOG_DIR = Path(__file__).parent.parent / "logs"
LOG_DIR.mkdir(exist_ok=True)
# 日志级别
LOG_LEVEL = settings.LOG_LEVEL.upper()
@@ -80,39 +75,16 @@ logger.add(
diagnose=True, # 显示详细异常信息
)
logger.add(
if settings.ENVIRONMENT == 'docker':
logger.add(
log_to_fluent,
level=LOG_LEVEL, # 处理 INFO 及以上级别
format="{message}", # 原始消息(实际使用结构化数据)
backtrace=True, # 启用堆栈回溯
diagnose=True # 显示诊断信息
)
)
# # 添加文件处理器 - 常规日志
# logger.add(
# sink=LOG_DIR / "app.log",
# level="INFO",
# format=FILE_FORMAT,
# rotation="10 MB", # 日志文件大小达到10MB时自动分割
# retention="7 days", # 保留7天的日志
# compression="zip", # 归档时压缩为zip
# enqueue=True, # 异步写入
# serialize=False, # 不使用JSON格式
# )
#
# # 添加文件处理器 - 错误日志
# logger.add(
# sink=LOG_DIR / "error.log",
# level="ERROR",
# format=FILE_FORMAT,
# rotation="10 MB",
# retention="30 days",
# compression="zip",
# enqueue=True,
# serialize=False,
# )
atexit.register(fluent_sender.close)
atexit.register(fluent_sender.close)
# 导出配置好的logger
__all__ = ["logger"]

15
config/rustfs.py Normal file
View File

@@ -0,0 +1,15 @@
import boto3
from botocore.client import Config
from config.setting import settings
access_key = 'jRxroVX8PUuSOia71qE4'
secret_access = 'RZE82VATN1Gqj9xy3d5OzFvHoDBgwJ4PSf7IKuei'
s3 = boto3.client('s3',
endpoint_url=f'http://{settings.RUSTFS_HOST}:{settings.RUSTFS_PORT}',
aws_access_key_id=access_key,
aws_secret_access_key=secret_access,
config=Config(signature_version='s3v4'),
region_name='cn-east-1'
)

View File

@@ -7,6 +7,9 @@ class Settings(BaseSettings):
DB_HOST: str
DB_PASSWORD: str
ES_HOST: str
FLUENTD_HOST: str
RUSTFS_HOST:str
RUSTFS_PORT: str
class Config:
env_file = ".env" # 指定.env文件路径

View File

@@ -35,13 +35,13 @@ async def global_exception_handler(request: Request, call_next):
except SQLAlchemyError as e:
# 记录数据库异常
logger.critical(f"数据库异常: {str(e)}", exc_info=True)
logger.critical(f"数据库异常: {str(e)}")
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"message": "数据库操作失败", "details": str(e)})
except Exception as e:
# 记录未知异常(带堆栈信息)
logger.critical(f"未知异常: {str(e)}", exc_info=True)
logger.critical(f"未知异常: {str(e)}")
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"code": 500, "message": "服务器内部错误", "details": str(e)})

View File

@@ -1,99 +1,129 @@
from sqlalchemy import (
BigInteger,
Boolean,
Column,
Integer,
DECIMAL,
SMALLINT,
String,
LargeBinary
)
from sqlalchemy import (BigInteger, Boolean, Column, Integer, DECIMAL, String, LargeBinary)
from sqlalchemy.orm import relationship
from models.base import AuditBase, IdBase
class Blog(AuditBase):
title = Column(String(255), nullable=True, comment="博客标题")
top_value = Column(Integer, nullable=True, comment="置顶值 越大越靠前")
is_great = Column(Boolean, nullable=True, comment="是否是精品 0否1是")
category_id = Column(BigInteger, nullable=True, comment="博客类别")
summary = Column(String(255), nullable=True, comment="博客内容概要")
content_id = Column(BigInteger, nullable=True, comment="博客内容")
word_count = Column(Integer, nullable=True, comment="字数统计")
read_duration = Column(DECIMAL(10, 2), nullable=True, comment="阅读时长")
title = Column(String(255), nullable=False, comment="博客标题")
top_value = Column(Integer, nullable=False, default=0, comment="置顶值 越大越靠前")
is_great = Column(Boolean, nullable=False, default=False, comment="是否是精品")
category_id = Column(BigInteger, nullable=False, comment="博客类别")
summary = Column(String(255), nullable=False, comment="博客内容概要")
content_id = Column(BigInteger, nullable=False, comment="博客内容")
word_count = Column(Integer, nullable=False, default=0, comment="字数统计")
read_duration = Column(DECIMAL(10, 2), nullable=False, default=0.00, comment="阅读时长")
is_approved = Column(Boolean, nullable=False, default=False, comment="是否发布")
# 分类关系(多对一)
category = relationship(
"BlogCategory",
# 与BlogCategory的blogs属性建立双向关系
back_populates="blogs",
# 只级联保存和合并操作,不级联删除(删除博客不应删除分类
cascade="save-update, merge",
# 明确指定连接条件
# 如果数据库设置了外键可以省略
primaryjoin="foreign(Blog.category_id) == BlogCategory.id"
)
# 内容关系(一对一)
content = relationship(
"BlogContent",
# 与BlogContent的blog属性建立双向关系
back_populates="blog",
# 完全级联操作:保存、合并、刷新、删除等所有操作都会级联
cascade="all, delete-orphan",
# 设置为False表示一对一关系返回单个对象而不是列表
uselist=False,
# 确保内容只有一个父博客与delete-orphan配合使用
single_parent=True,
# 明确指定连接条件
primaryjoin="foreign(Blog.content_id) == BlogContent.id"
)
# 访问记录(一对多)
visits = relationship(
"BlogVisit",
# 与BlogVisit的blog属性建立双向关系
back_populates="blog",
# 完全级联操作:博客删除时自动删除所有访问记录
cascade="all, delete-orphan",
# 明确指定连接条件
primaryjoin="Blog.id == foreign(BlogVisit.blog_id)"
)
# 4. 评论(一对多)
comments = relationship(
"BlogComment",
# 与BlogComment的blog属性建立双向关系
back_populates="blog",
# 完全级联操作:博客删除时自动删除所有评论
cascade="all, delete-orphan",
# 明确指定连接条件
primaryjoin="Blog.id == foreign(BlogComment.blog_id)"
)
class BlogCategory(AuditBase):
name = Column(String(45), nullable=True, comment="类别名称")
name = Column(String(45), nullable=False, comment="类别名称")
blogs = relationship(
"Blog",
# 与Blog的category属性建立双向关系
back_populates="category",
# 完全级联操作:分类删除时自动删除所有关联的博客
# 警告:这会级联删除分类下的所有博客,包括博客的内容、访问记录和评论
cascade="all, delete-orphan",
# 明确指定连接条件
primaryjoin="BlogCategory.id == foreign(Blog.category_id)"
)
class BlogContent(IdBase):
content = Column(LargeBinary, comment="博客内容")
content = Column(LargeBinary, nullable=False, comment="博客内容")
blog = relationship(
"Blog",
# 与Blog的content属性建立双向关系
back_populates="content",
# 设置为False表示一对一关系
uselist=False,
# 明确指定连接条件
primaryjoin="BlogContent.id == foreign(Blog.content_id)"
)
class BlogVisit(AuditBase):
ip = Column(String(255), nullable=True, comment="ip地址")
os = Column(String(255), nullable=True, comment="操作系统")
browser = Column(String(255), nullable=True, comment="浏览器")
uri = Column(String(255), nullable=True, comment="路径")
blog_id = Column(BigInteger, nullable=True, comment="博客id")
ip = Column(String(255), nullable=False, comment="IP地址")
os = Column(String(255), nullable=False, comment="操作系统")
browser = Column(String(255), nullable=False, comment="浏览器")
uri = Column(String(255), nullable=False, comment="路径")
blog_id = Column(BigInteger, nullable=True, comment="博客ID")
blog = relationship(
"Blog",
# 与Blog的visits属性建立双向关系
back_populates="visits",
# 明确指定连接条件
primaryjoin="foreign(BlogVisit.blog_id) == Blog.id"
)
class BlogComment(AuditBase):
blog_id = Column(BigInteger, comment="博客id")
parent_id = Column(BigInteger, comment="父评论id")
name = Column(String(255), comment="评论人昵称")
blog_id = Column(BigInteger, nullable=False, comment="博客ID")
parent_id = Column(BigInteger, nullable=False, comment="父评论ID")
name = Column(String(255), nullable=False, comment="评论人昵称")
website = Column(String(255), nullable=True, comment="评论人网站")
ip_address = Column(String(45), nullable=True, comment="评论人ip")
user_agent = Column(String(255), nullable=True, comment="评论人浏览器信息")
content = Column(String(512), comment="评论内容")
is_approved = Column(SMALLINT, comment="是否通过")
ip_address = Column(String(45), nullable=False, comment="评论人IP")
user_agent = Column(String(255), nullable=False, comment="评论人浏览器信息")
content = Column(String(255), nullable=False, comment="评论内容")
is_approved = Column(Boolean, nullable=False, default=False, comment="是否通过")
blog = relationship(
"Blog",
# 与Blog的comments属性建立双向关系
back_populates="comments",
# 明确指定连接条件
primaryjoin="foreign(BlogComment.blog_id) == Blog.id"
)

View File

@@ -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

View File

@@ -1,24 +1,42 @@
from typing import Optional
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_validator, ConfigDict
from datetime import datetime
class BlogQuery(BaseModel):
# 类别
category: Optional[str] = None
# 标题
title: Optional[str] = None
# 年份
year: Optional[int] = None
"""博客查询参数"""
category: Optional[str] = Field(None, description="分类名称")
title: Optional[str] = Field(None, description="标题关键词")
year: Optional[int] = Field(None, description="发布年份", ge=2000, le=datetime.now().year)
@field_validator('year')
def validate_year(cls, v):
if v is not None and v > datetime.now().year:
raise ValueError('年份不能超过当前年份')
return v
class BlogBase(BaseModel):
title: str
topValue: int
isGreat: bool
category: str
content: Optional[str] = None
"""博客基础模型"""
title: str = Field(..., min_length=1, max_length=255, description="博客标题")
top_value: int = Field(default=0, ge=0, description="置顶值,越大越靠前", alias="topValue")
is_great: bool = Field(default=False, description="是否是精品", alias="isGreat")
category: str = Field(..., min_length=1, max_length=45, description="分类名称")
content: Optional[str] = Field(None, description="博客内容")
is_approved: bool = Field(default=False, description="是否已发布", alias="isApproved")
@field_validator('title')
def title_not_empty(cls, v):
if not v or not v.strip():
raise ValueError('标题不能为空')
return v.strip()
@field_validator('category')
def category_not_empty(cls, v):
if not v or not v.strip():
raise ValueError('分类不能为空')
return v.strip()
class BlogCreate(BlogBase):
@@ -30,83 +48,105 @@ class BlogUpdate(BlogBase):
class BlogResponse(BlogBase):
id: Optional[int] = None
summary: Optional[str] = None
wordCount: Optional[int] = None
readDuration: Optional[float] = None
visitCount: Optional[int] = None
createTime: Optional[datetime] = None
updateTime: Optional[datetime] = None
id: int = Field(..., description="博客ID")
summary: Optional[str] = Field(None, description="内容摘要")
word_count: Optional[int] = Field(None, description="字数统计", alias="wordCount")
read_duration: Optional[float] = Field(None, description="阅读时长", alias="readDuration")
visit_count: Optional[int] = Field(0, description="访问次数", alias="visitCount")
create_time: datetime = Field(..., description="创建时间", alias="createTime")
update_time: datetime = Field(..., description="更新时间", alias="updateTime")
class Config:
from_attributes = True
json_encoders = {
# 自定义 datetime 类型的序列化格式
model_config = ConfigDict(
from_attributes=True,
populate_by_name=True,
json_encoders={
datetime: lambda dt: dt.strftime('%Y-%m-%d %H:%M:%S')
}
)
class BlogCategoryResponse(BaseModel):
name: str
count: int
name: str = Field(..., description="分类名称")
count: int = Field(..., description="博客数量")
class Config:
from_attributes = True
model_config = ConfigDict(
from_attributes=True,
populate_by_name=True
)
class BlogStatsResponse(BaseModel):
blogCount: int
categoryCount: int
wordCount: int
blog_count: int = Field(..., description="博客总数", alias="blogCount")
category_count: int = Field(..., description="分类总数", alias="categoryCount")
word_count: int = Field(..., description="总字数", alias="wordCount")
class BlogVisitResponse(BaseModel):
ip: str
os: str
browser: str
uri: str
title: Optional[str] = None
visitTime: datetime
ip: str = Field(..., description="IP地址")
os: str = Field(..., description="操作系统")
browser: str = Field(..., description="浏览器")
uri: str = Field(..., description="访问路径")
title: str = Field(None, description="博客标题")
visit_time: datetime = Field(..., description="访问时间", alias="visitTime")
class Config:
from_attributes = True
json_encoders = {
model_config = ConfigDict(
from_attributes=True,
populate_by_name=True,
json_encoders={
datetime: lambda dt: dt.strftime('%Y-%m-%d %H:%M:%S')
}
)
class BlogLatestResponse(BaseModel):
id: int
title: str
id: int = Field(..., description="博客ID")
title: str = Field(..., description="博客标题")
class Config:
from_attributes = True
model_config = ConfigDict(
from_attributes=True,
populate_by_name=True
)
class BlogAdjacentResponse(BaseModel):
id: int
title: str
id: int = Field(..., description="博客ID")
title: str = Field(..., description="博客标题")
class Config:
from_attributes = True
model_config = ConfigDict(
from_attributes=True,
populate_by_name=True
)
class BlogCommentCreate(BaseModel):
parentId: int
name: str
website: Optional[str] = None
content: str
parent_id: int = Field(default=0, ge=0, description="父评论ID0表示顶级评论", alias="parentId")
name: str = Field(..., min_length=1, max_length=50, description="评论人昵称")
website: Optional[str] = Field(None, description="评论人网站")
content: str = Field(..., min_length=1, max_length=1000, description="评论内容")
@field_validator('name')
def name_not_empty(cls, v):
if not v or not v.strip():
raise ValueError('昵称不能为空')
return v.strip()
@field_validator('content')
def content_not_empty(cls, v):
if not v or not v.strip():
raise ValueError('评论内容不能为空')
return v.strip()
class BlogCommentResponse(BlogCommentCreate):
id: int
ipAddress: str
userAgent: str
createTime: datetime
id: int = Field(..., description="评论ID")
ip_address: str = Field(..., description="IP地址", alias="ipAddress")
user_agent: str = Field(..., description="浏览器信息", alias="userAgent")
create_time: datetime = Field(..., description="创建时间", alias="createTime")
class Config:
from_attributes = True
json_encoders = {
# 自定义 datetime 类型的序列化格式
model_config = ConfigDict(
from_attributes=True,
populate_by_name=True,
json_encoders={
datetime: lambda dt: dt.strftime('%Y-%m-%d %H:%M:%S')
}
)

View File

@@ -9,6 +9,7 @@ class BlogElastic(BaseModel):
title: str
content: str
category: str
isApproved: int
createTime: Optional[Union[datetime, str]] = None
updateTime: Optional[Union[datetime, str]] = None

16
schemas/blog_stats.py Normal file
View 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)

View File

@@ -17,11 +17,12 @@ def sync_all_blog(db: Session) -> bool:
if settings.ENVIRONMENT == 'dev':
return True
query = select(
stmt = select(
Blog.id,
Blog.title,
BlogCategory.name.label("category"),
BlogContent.content.label("content"),
Blog.is_approved.label("isApproved"),
Blog.create_time.label("createTime"),
Blog.update_time.label("updateTime"),
).select_from(Blog).outerjoin(
@@ -29,7 +30,7 @@ def sync_all_blog(db: Session) -> bool:
).outerjoin(
BlogContent, Blog.content_id == BlogContent.id
)
results = db.execute(query).fetchall()
results = db.execute(stmt).fetchall()
actions = [
{
@@ -40,6 +41,7 @@ def sync_all_blog(db: Session) -> bool:
"title": blog.title,
"category": blog.category,
"content": blog.content.decode("utf-8"),
"isApproved": blog.isApproved,
"createTime": blog.createTime.strftime("%Y-%m-%d %H:%M:%S"),
"updateTime": blog.updateTime.strftime("%Y-%m-%d %H:%M:%S")
}
@@ -106,11 +108,24 @@ def search_blog(keyword: str) -> List[BlogSearch]:
# 构造查询请求
body = {
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": keyword,
"fields": ["title^3", "content"],
"type": "phrase"
}
}
],
"filter": [
{
"term": {
"isApproved": 1
}
}
]
}
},
"highlight": {
"pre_tags": ["<mark>"],

View File

@@ -1,7 +1,7 @@
from typing import List
from fastapi import Request
from sqlalchemy import select, func, desc, and_, asc, delete
from sqlalchemy import select, func, desc, and_, asc
from sqlalchemy.orm import Session
from models.blog import Blog, BlogCategory, BlogVisit, BlogContent, BlogComment
@@ -13,53 +13,62 @@ 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
def query_blog_by_page(db: Session, current_page: int = 1, page_size: int = 10) -> PageResult[BlogResponse]:
query = select(
def query_blog_by_page(db: Session, current_page: int = 1, page_size: int = 10,
is_all=False) -> PageResult[BlogResponse]:
stmt = (select(
Blog.id,
Blog.title,
Blog.top_value.label("topValue"),
Blog.is_great.label("isGreat"),
Blog.top_value,
Blog.is_great,
BlogCategory.name.label("category"),
Blog.summary,
Blog.word_count.label("wordCount"),
Blog.read_duration.label("readDuration"),
Blog.word_count,
Blog.read_duration,
func.count(BlogVisit.id).label("visitCount"),
Blog.create_time.label("createTime"),
Blog.update_time.label("updateTime")
).select_from(Blog).outerjoin(
BlogCategory, Blog.category_id == BlogCategory.id
).outerjoin(
BlogVisit, Blog.id == BlogVisit.blog_id
).group_by(
Blog.id, BlogCategory.name
).order_by(
desc(Blog.top_value), desc(Blog.update_time)
)
Blog.is_approved,
Blog.create_time,
Blog.update_time
).outerjoin(BlogCategory, Blog.category_id == BlogCategory.id)
.outerjoin(BlogVisit, Blog.id == BlogVisit.blog_id)
.group_by(Blog.id, BlogCategory.name)
.order_by(desc(Blog.is_great), desc(Blog.top_value), desc(Blog.update_time)))
return paginate_query(db, query, current_page, page_size)
if not is_all:
stmt = stmt.where(Blog.is_approved == 1)
return paginate_query(db, stmt, current_page, page_size)
def query_blog_by_condition(db: Session, blog_query: BlogQuery) -> List[BlogResponse]:
query = select(
stmt = get_query_blog_by_condition_stmt(blog_query)
results = db.execute(stmt).fetchall()
return [BlogResponse.model_validate(result) for result in results]
def get_query_blog_by_condition_stmt(blog_query: BlogQuery):
stmt = (select(
Blog.id,
Blog.title,
Blog.top_value.label("topValue"),
Blog.is_great.label("isGreat"),
Blog.top_value,
Blog.is_great,
BlogCategory.name.label("category"),
Blog.summary,
BlogContent.content.label("content"),
Blog.word_count.label("wordCount"),
Blog.read_duration.label("readDuration"),
Blog.create_time.label("createTime"),
Blog.update_time.label("updateTime")
).select_from(Blog).outerjoin(
BlogCategory, Blog.category_id == BlogCategory.id
).outerjoin(
BlogContent, Blog.content_id == BlogContent.id
)
BlogContent.content,
Blog.word_count,
Blog.read_duration,
func.count(BlogVisit.id).label("visitCount"),
Blog.is_approved,
Blog.create_time,
Blog.update_time
).where(Blog.is_approved == 1)
.outerjoin(BlogCategory, Blog.category_id == BlogCategory.id)
.outerjoin(BlogContent, Blog.content_id == BlogContent.id)
.outerjoin(BlogVisit, Blog.id == BlogVisit.blog_id)
.group_by(Blog.id))
conditions = []
@@ -73,42 +82,71 @@ def query_blog_by_condition(db: Session, blog_query: BlogQuery) -> List[BlogResp
conditions.append(func.extract('year', Blog.create_time) == blog_query.year)
if conditions:
query = query.where(and_(*conditions))
stmt = stmt.where(and_(*conditions))
query = query.order_by(desc(Blog.update_time))
stmt = stmt.order_by(desc(Blog.is_great), desc(Blog.update_time))
results = db.execute(query).fetchall()
return stmt
return [BlogResponse.from_orm(result) for result in results]
def query_blog_by_condition_page(db: Session, blog_query: BlogQuery,
current_page: int = 1, page_size: int = 10) -> PageResult[BlogResponse]:
stmt = get_query_blog_by_condition_stmt(blog_query)
return paginate_query(db, stmt, current_page, page_size)
def query_unapproved_blog(db: Session) -> List[BlogResponse]:
stmt = ((select(
Blog.id,
Blog.title,
Blog.top_value,
Blog.is_great,
BlogCategory.name.label("category"),
Blog.summary,
BlogContent.content,
Blog.word_count,
Blog.read_duration,
func.count(BlogVisit.id),
Blog.is_approved,
Blog.create_time,
Blog.update_time
).where(Blog.is_approved == 0)
.outerjoin(BlogCategory, Blog.category_id == BlogCategory.id)
.outerjoin(BlogContent, Blog.content_id == BlogContent.id))
.outerjoin(BlogVisit, Blog.id == BlogVisit.blog_id)
.group_by(Blog.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:
check_blog_exist(db, blog_id)
query = db.query(
Blog.id.label("id"),
Blog.title.label("title"),
Blog.top_value.label("topValue"),
Blog.is_great.label("isGreat"),
stmt = (select(
Blog.id,
Blog.title,
Blog.top_value,
Blog.is_great,
BlogCategory.name.label("category"),
Blog.summary.label("summary"),
BlogContent.content.label("content"),
Blog.word_count.label("wordCount"),
Blog.read_duration.label("readDuration"),
Blog.summary,
BlogContent.content,
Blog.word_count,
Blog.read_duration,
func.count(BlogVisit.id).label("visitCount"),
Blog.create_time.label("createTime"),
Blog.update_time.label("updateTime")
).select_from(Blog).outerjoin(
BlogCategory, Blog.category_id == BlogCategory.id
).outerjoin(
BlogContent, Blog.content_id == BlogContent.id
).outerjoin(
BlogVisit, Blog.id == BlogVisit.blog_id
).filter(Blog.id == blog_id)
Blog.is_approved,
Blog.create_time,
Blog.update_time
).where(Blog.id == blog_id)
.outerjoin(BlogCategory, Blog.category_id == BlogCategory.id)
.outerjoin(BlogContent, Blog.content_id == BlogContent.id)
.outerjoin(BlogVisit, Blog.id == BlogVisit.blog_id))
blog = query.first()
blog = db.execute(stmt).first()
return BlogResponse.from_orm(blog)
return BlogResponse.model_validate(blog)
def add_blog(db: Session, blog: BlogCreate) -> bool:
@@ -116,15 +154,18 @@ def add_blog(db: Session, blog: BlogCreate) -> bool:
db_blog = Blog(
title=blog.title,
top_value=blog.topValue,
is_great=blog.isGreat,
top_value=blog.top_value,
is_great=blog.is_great,
category_id=add_blog_category(db, blog.category),
content_id=add_blog_content(db, blog.content),
summary=get_blog_summary(blog.content),
word_count=word_count,
read_duration=get_read_duration(word_count)
read_duration=get_read_duration(word_count),
is_approved=blog.is_approved
)
# 级联新增
db_blog.content = BlogContent(content=blog.content.encode('utf-8'))
db.add(db_blog)
db.commit()
db.refresh(db_blog)
@@ -133,7 +174,8 @@ def add_blog(db: Session, blog: BlogCreate) -> bool:
id=db_blog.id,
title=db_blog.title,
content=blog.content,
category=blog.category
category=blog.category,
isApproved=blog.isApproved
)
add_blog_elastic(blog_elastic)
@@ -154,30 +196,22 @@ def add_blog_category(db: Session, category: str) -> int:
return category_id
def add_blog_content(db: Session, content: str) -> int:
db_blog_content = BlogContent(content=content.encode('utf-8'))
db.add(db_blog_content)
db.commit()
return db_blog_content.id
def update_blog(db: Session, blog_id: int, blog: BlogUpdate) -> bool:
db_blog = check_blog_exist(db, blog_id)
update_data = blog.model_dump(exclude_unset=True)
update_blog_content(db, db_blog.content_id, blog.content)
db_blog.title = update_data["title"]
db_blog.top_value = update_data["topValue"]
db_blog.is_great = update_data["isGreat"]
db_blog.title = blog.title
db_blog.top_value = blog.topValue
db_blog.is_great = blog.isGreat
db_blog.category_id = update_blog_category(db, blog.category)
# 级联更新
db_blog.content = BlogContent(content=blog.content.encode('utf-8'))
word_count = get_word_count(blog.content)
db_blog.summary = get_blog_summary(blog.content),
db_blog.word_count = word_count
db_blog.read_duration = get_read_duration(word_count)
db_blog.is_approved = blog.isApproved
db.commit()
db.refresh(db_blog)
@@ -194,85 +228,56 @@ def update_blog_category(db: Session, category: str) -> int:
return db_blog_category.id
def update_blog_content(db: Session, blog_content_id: int, blog_content: str) -> bool:
db_blog_content = check_blog_content_exist(db, blog_content_id)
db_blog_content.content = blog_content.encode('utf-8')
db.commit()
db.refresh(db_blog_content)
return True
def delete_blog(db: Session, blog_id: int) -> bool:
db_blog = check_blog_exist(db, blog_id)
delete_blog_content(db, db_blog.content_id)
db.execute(delete(Blog).where(Blog.id == blog_id))
db.commit()
return True
def delete_blog_content(db: Session, blog_content_id: int) -> bool:
check_blog_content_exist(db, blog_content_id)
db.execute(delete(BlogContent).where(BlogContent.id == blog_content_id))
# 级联删除
db.delete(db_blog)
db.commit()
return True
def query_blog_category(db: Session) -> List[BlogCategoryResponse]:
query = select(
BlogCategory.name,
func.count(Blog.id).label("count")
).select_from(Blog).outerjoin(
BlogCategory, Blog.category_id == BlogCategory.id
).group_by(
BlogCategory.name
)
query = (select(BlogCategory.name, func.count(Blog.id).label("count"))
.where(Blog.is_approved == 1)
.outerjoin(BlogCategory, Blog.category_id == BlogCategory.id)
.group_by(BlogCategory.name))
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_stats = query_blog_overview(db)
return BlogStatsResponse(
blogCount=db.query(Blog).count(),
categoryCount=db.query(BlogCategory).count(),
wordCount=db.query(func.sum(Blog.word_count)).scalar()
blogCount=blog_stats.blogCount,
categoryCount=blog_stats.categoryCount,
wordCount=blog_stats.wordCount
)
def query_blog_latest(db: Session) -> List[BlogLatestResponse]:
query = select(
Blog.id,
Blog.title
).select_from(Blog).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(query).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]:
check_blog_exist(db, blog_id)
prev_result = db.execute(
select(Blog.id, Blog.title)
.where(Blog.id < blog_id)
.order_by(desc(Blog.id))
.limit(1)
).first()
prev_stmt = (select(Blog.id, Blog.title)
.where(Blog.id < blog_id, Blog.is_approved == 1)
.order_by(desc(Blog.id)))
prev_result = db.execute(prev_stmt).first()
next_result = db.execute(
select(Blog.id, Blog.title)
.where(Blog.id > blog_id)
.order_by(asc(Blog.id))
.limit(1)
).first()
next_stmt = (select(Blog.id, Blog.title)
.where(Blog.id > blog_id, Blog.is_approved == 1)
.order_by(asc(Blog.id)))
next_result = db.execute(next_stmt).first()
return [
BlogAdjacentResponse(
@@ -308,21 +313,21 @@ def query_blog_visit(db: Session, current_page: int = 1, page_size: int = 10) ->
def query_blog_comment(db: Session, blog_id: int) -> List[BlogCommentResponse]:
query = select(
stmt = select(
BlogComment.id,
BlogComment.blog_id.label("blogId"),
BlogComment.parent_id.label("parentId"),
BlogComment.blog_id,
BlogComment.parent_id,
BlogComment.name,
BlogComment.website,
BlogComment.ip_address.label("ipAddress"),
BlogComment.user_agent.label("userAgent"),
BlogComment.ip_address,
BlogComment.user_agent,
BlogComment.content,
BlogComment.is_approved.label("isApproved"),
BlogComment.create_time.label("createTime")
).select_from(BlogComment).where(BlogComment.blog_id == blog_id)
results = db.execute(query).fetchall()
BlogComment.is_approved,
BlogComment.create_time
).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:
@@ -346,7 +351,7 @@ def add_blog_comment(db: Session, request: Request, blog_id: int, blog_comment:
content=blog_comment.content,
ip_address=request.client.host,
user_agent=request.headers.get("user-agent"),
is_approved=1
is_approved=True
)
db.add(db_comment)

View 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]

40
service/file_service.py Normal file
View File

@@ -0,0 +1,40 @@
from fastapi import UploadFile, File, HTTPException
from config.rustfs import s3
from config.setting import settings
ALLOWED_IMAGE_TYPES = [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/svg+xml",
]
BUCKET = 'blog'
NGINX_PROXY = 'rustfs'
async def upload_file(md5: str, file: UploadFile = File(...)) -> str:
# 校验文件类型是否是图片
if file.content_type not in ALLOWED_IMAGE_TYPES:
raise HTTPException(
status_code=400,
detail="只允许上传图片文件 (JPEG, PNG, GIF, WEBP, SVG)"
)
file_ext = file.filename.split('.')[-1]
unique_filename = f"{md5}.{file_ext}"
file_content = await file.read()
# 上传到S3
s3.put_object(
Bucket=BUCKET,
Key=unique_filename,
Body=file_content,
ContentType=file.content_type
)
# 返回文件url
return f"{NGINX_PROXY}/{BUCKET}/{unique_filename}"