feat:初始化工程

This commit is contained in:
2025-09-21 22:47:06 +08:00
commit c4674749ce
78 changed files with 1599 additions and 0 deletions

9
.env Normal file
View File

@@ -0,0 +1,9 @@
LOG_LEVEL=DEBUG
DB_USER=root
DB_PASSWORD=123456
#DB_PASSWORD=19940822Cxx%401213
DB_HOST=localhost
#DB_HOST=host.docker.internal
DB_PORT=3306
DB_NAME=blog

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

10
.idea/blog-service.iml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12 (blog-service)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (blog-service)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/blog-service.iml" filepath="$PROJECT_DIR$/.idea/blog-service.iml" />
</modules>
</component>
</project>

8
Dockerfile Normal file
View File

@@ -0,0 +1,8 @@
FROM python:3.12-slim
WORKDIR /app
COPY ./packages /app/packages
COPY requirements.txt /app/
RUN pip install --no-cache-dir --no-index --find-links=/app/packages -r requirements.txt
COPY . /app/
EXPOSE 8082
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8082"]

Binary file not shown.

6
api/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
from fastapi import FastAPI
from .blog import router as blog_router
def register_routers(app: FastAPI):
app.include_router(blog_router, prefix="")

Binary file not shown.

Binary file not shown.

59
api/blog.py Normal file
View File

@@ -0,0 +1,59 @@
from typing import List
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy.orm import Session
from schemas.blog import BlogResponse, BlogQuery, BlogCategoryResponse, BlogStatsResponse, \
BlogLatestResponse, BlogCommentCreate, BlogCommentResponse
from schemas.pagination import PageResult
from config.database import get_db
from service import blog_service
router = APIRouter(
prefix="/blog",
tags=["博客管理"],
responses={404: {"description": "Not found"}}
)
@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)):
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)):
return blog_service.query_blog_by_condition(db, query)
@router.get("/content/{blog_id}", 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)
@router.get("/category", summary="查询博客分类", response_model=List[BlogCategoryResponse])
def query_blog_category(db: Session = Depends(get_db)):
return blog_service.query_blog_category(db)
@router.get("/stats", summary="查询博客统计信息", response_model=BlogStatsResponse)
def query_blog_stats(db: Session = Depends(get_db)):
return blog_service.query_blog_stats(db)
@router.get("/latest", summary="查询近期博客", response_model=list[BlogLatestResponse])
def query_blog_latest(db: Session = Depends(get_db)):
return blog_service.query_blog_latest(db)
@router.put("/comment", summary="新增博客评论", response_model=bool)
def add_blog_comment(blog_comment: BlogCommentCreate, request: Request, db: Session = Depends(get_db)):
return blog_service.add_blog_comment(db, request, blog_comment)
@router.get("/comment/{blog_id}", summary="查询博客评论", response_model=list[BlogCommentResponse])
def query_blog_comment(blog_id: int, db: Session = Depends(get_db)):
return blog_service.query_blog_comment(db, blog_id)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

85
config/auth.py Normal file
View File

@@ -0,0 +1,85 @@
from contextvars import ContextVar
from datetime import datetime, timedelta
from typing import Optional
from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer
import jwt
from passlib.context import CryptContext
from middleware.exceptions import get_credentials_exception
# 密钥和算法配置
SECRET_KEY = "sjdi@!#3ksj2780se1283"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# 密码哈希上下文
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# OAuth2 方案
# 设置默认登录接口
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="session")
# 请求上下文
context_sub: ContextVar[str] = ContextVar('sub')
# 获取数据库 加密密码
def get_password_hash(password: str):
return pwd_context.hash(password)
# 验证数据库密码
def verify_password(plain_password: str, hashed_password: str):
return pwd_context.verify(plain_password, hashed_password)
# 生成payload
def create_payload(sub: str) -> dict:
return {
"sub": sub
}
# 验证payload
def verify_payload(payload: dict) -> bool:
return "sub" in payload
# 从payload中获取用户
def get_payload_sub(payload: dict) -> str:
return payload["sub"]
# 创建token
def create_token(payload: dict, expires_delta: Optional[timedelta] = None):
# 复制一份
payload_copy = payload.copy()
# 加上有效时间
if expires_delta:
expire = datetime.now() + expires_delta
else:
expire = datetime.now() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
payload_copy.update({"exp": expire})
# 生成jwt Token
return jwt.encode(payload_copy, SECRET_KEY, algorithm=ALGORITHM)
# 验证token
async def verify_token(token: str = Depends(oauth2_scheme)):
try:
# 1. 检验token信息
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
# 2. 校验payload信息
if not verify_payload(payload):
raise get_credentials_exception()
# 3. 校验数据库中是否存在 payload中的账户信息
sub = get_payload_sub(payload)
# 4. 存储账户信息
context_sub.set(sub)
except jwt.exceptions.InvalidTokenError:
raise get_credentials_exception()

23
config/database.py Normal file
View File

@@ -0,0 +1,23 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from config.setting import settings
DATABASE_URL = f"mysql+pymysql://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}"
engine = create_engine(url=DATABASE_URL)
# 会话工厂
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# ORM基类
Base = declarative_base()
def get_db():
# 创建数据库会话实例
db = SessionLocal()
try:
yield db
finally:
db.close()

65
config/logging.py Normal file
View File

@@ -0,0 +1,65 @@
from loguru import logger
from pathlib import Path
from config.setting import settings
# 日志目录
LOG_DIR = Path(__file__).parent.parent.parent / "logs"
LOG_DIR.mkdir(exist_ok=True)
# 日志级别
LOG_LEVEL = settings.LOG_LEVEL.upper()
# 日志格式
STDOUT_FORMAT = (
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
"<level>{level: <8}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
"<level>{message}</level>"
)
FILE_FORMAT = (
"{time:YYYY-MM-DD HH:mm:ss.SSS} | "
"{level: <8} | "
"{name}:{function}:{line} - {message}"
)
# 移除默认处理器
logger.remove()
# 添加控制台处理器
logger.add(
sink="sys.stdout",
level=LOG_LEVEL,
format=STDOUT_FORMAT,
colorize=True,
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,
)
# 导出配置好的logger
__all__ = ["logger"]

19
config/setting.py Normal file
View File

@@ -0,0 +1,19 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DB_HOST: str
DB_PORT: int
DB_USER: str
DB_PASSWORD: str
DB_NAME: str
LOG_LEVEL: str
class Config:
env_file = ".env" # 指定.env文件路径
case_sensitive = False # 忽略变量名大小写
# 全局配置实例
settings = Settings()

0
id_generator/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

38
id_generator/generator.py Normal file
View File

@@ -0,0 +1,38 @@
"""
雪花算法生成器IdGenerator
"""
# !/usr/bin/python
# coding=UTF-8
from . import options
from . import snowflake_m1
class DefaultIdGenerator:
"""
ID生成器
"""
def __init__(self):
self.snowflake = None
def set_id_generator(self, option: options.IdGeneratorOptions):
"""
设置id生成规则信息
"""
if option.base_time < 100000:
raise ValueError("base time error.")
self.snowflake = snowflake_m1.SnowFlakeM1(option)
def next_id(self) -> int:
"""
获取新的UUID
"""
if self.snowflake is None:
raise ValueError("please set id generator at first.")
return self.snowflake.next_id()

134
id_generator/idregister.py Normal file
View File

@@ -0,0 +1,134 @@
"""
worker id generator
"""
# !/usr/bin/python
# coding=UTF-8
from threading import Thread
import time
import logging
import redis
class Register:
"""
redis封装
- host 代表redis ip
- port 代表redis端口
- max_worker_id worker_id的最大值, 默认为100
- password redis的密码, 默认为空
"""
def __init__(self, host, port, max_worker_id=100, password=None):
self.redis_impl = redis.StrictRedis(host=host, port=port, db=0, password=password)
self.loop_count = 0
self.max_loop_count = 10
self.worker_id_expire_time = 15
self.max_worker_id = max_worker_id
self.worker_id = -1
self.is_stop = False
def get_lock(self, key):
"""
获取分布式全局锁,并设置过期时间为30秒
"""
if self.redis_impl.setnx(key, 1):
self.redis_impl.expire(key, 30)
return True
if self.redis_impl.ttl(key) < 0:
self.redis_impl.expire(key, 30)
return False
def stop(self):
"""
退出注册器的线程
"""
self.is_stop = True
def get_worker_id(self):
"""
获取全局唯一worker_id, 会创建一个线程给worker id续期
失败返回-1
"""
self.loop_count = 0
def extern_life(my_id):
while 1:
time.sleep(self.worker_id_expire_time / 3)
# 是否关闭了
if self.is_stop:
return
# 更新生命周期
if self.worker_id != my_id:
break
try:
self.redis_impl.expire(
f"IdGen:WorkerId:Value:{my_id}",
self.worker_id_expire_time)
except Exception as exe:
logging.error(exe)
continue
self.worker_id = self.__get_next_worker_id()
if self.worker_id > -1:
Thread(target=extern_life, args=[self.worker_id]).start()
return self.worker_id
def __get_next_worker_id(self):
"""
获取全局唯一worker id内部实现
"""
cur = self.redis_impl.incrby("IdGen:WorkerId:Index", 1)
def can_reset():
try:
reset_value = self.redis_impl.incr("IdGen:WorkerId:Value:Edit")
return reset_value != 1
except Exception as ept:
logging.error(ept)
return False
def end_reset():
try:
self.redis_impl.set("IdGen:WorkerId:Value:Edit", 0)
except Exception as ept:
logging.error(ept)
def is_available(worker_id: int):
try:
rst = self.redis_impl.get(f"IdGen:WorkerId:Value:{worker_id}")
return rst != "Y"
except Exception as ept:
logging.error(ept)
return False
if cur > self.max_worker_id:
if can_reset():
self.redis_impl.set("IdGen:WorkerId:Index", -1)
end_reset()
self.loop_count += 1
if self.loop_count > self.max_loop_count:
self.loop_count = 0
return -1
time.sleep(0.2 * self.loop_count)
return self.__get_next_worker_id()
time.sleep(0.2)
return self.__get_next_worker_id()
if is_available(cur):
self.redis_impl.setex(
f"IdGen:WorkerId:Value:{cur}",
self.worker_id_expire_time,
"Y"
)
self.loop_count = 0
return cur
return self.__get_next_worker_id()

43
id_generator/options.py Normal file
View File

@@ -0,0 +1,43 @@
"""
生成器IdGenerator配置选项
"""
# !/usr/bin/python
# coding=UTF-8
class IdGeneratorOptions:
"""
ID生成器配置
- worker_id 全局唯一id, 区分不同uuid生成器实例
- worker_id_bit_length 生成的uuid中worker_id占用的位数
- seq_bit_length 生成的uuid中序列号占用的位数
"""
def __init__(self, worker_id=0, worker_id_bit_length=6, seq_bit_length=6):
# 雪花计算方法,1-漂移算法|2-传统算法), 默认1。目前只实现了1。
self.method = 1
# 基础时间ms单位, 不能超过当前系统时间
self.base_time = 1582136402000
# 机器码, 必须由外部设定, 最大值 2^worker_id_bit_length-1
self.worker_id = worker_id
# 机器码位长, 默认值6, 取值范围 [1, 15](要求:序列数位长+机器码位长不超过22
self.worker_id_bit_length = worker_id_bit_length
# 序列数位长, 默认值6, 取值范围 [3, 21](要求:序列数位长+机器码位长不超过22
self.seq_bit_length = seq_bit_length
# 最大序列数(含), 设置范围 [max_seq_number, 2^seq_bit_length-1]
# 默认值0, 表示最大序列数取最大值2^seq_bit_length-1]
self.max_seq_number = 0
# 最小序列数(含), 默认值5, 取值范围 [5, max_seq_number], 每毫秒的前5个序列数对应编号0-4是保留位
# 其中1-4是时间回拨相应预留位, 0是手工新值预留位
self.min_seq_number = 5
# 最大漂移次数(含), 默认2000, 推荐范围500-10000与计算能力有关
self.top_over_cost_count = 2000

20
id_generator/snowflake.py Normal file
View File

@@ -0,0 +1,20 @@
"""
雪花算法生成器接口声明
"""
# !/usr/bin/python
# coding=UTF-8
class SnowFlake():
def __init__(self, options):
self.options = options
def next_id(self) -> int:
"""
获取新的UUID
"""
return 0

View File

@@ -0,0 +1,147 @@
"""
M1生成器
"""
# !/usr/bin/python
# coding=UTF-8
import threading
import time
from .snowflake import SnowFlake
from .options import IdGeneratorOptions
class SnowFlakeM1(SnowFlake):
"""
M1规则ID生成器配置
"""
def __init__(self, options: IdGeneratorOptions):
# 1.base_time
self.base_time = 1582136402000
if options.base_time != 0:
self.base_time = int(options.base_time)
# 2.worker_id_bit_length
self.worker_id_bit_length = 6
if options.worker_id_bit_length != 0:
self.worker_id_bit_length = int(options.worker_id_bit_length)
# 3.worker_id
self.worker_id = options.worker_id
# 4.seq_bit_length
self.seq_bit_length = 6
if options.seq_bit_length != 0:
self.seq_bit_length = int(options.seq_bit_length)
# 5.max_seq_number
self.max_seq_number = int(options.max_seq_number)
if options.max_seq_number <= 0:
self.max_seq_number = (1 << self.seq_bit_length) - 1
# 6.min_seq_number
self.min_seq_number = int(options.min_seq_number)
# 7.top_over_cost_count
self.top_over_cost_count = int(options.top_over_cost_count)
# 8.Others
self.__timestamp_shift = self.worker_id_bit_length + self.seq_bit_length
self.__current_seq_number = self.min_seq_number
self.__last_time_tick: int = 0
self.__turn_back_time_tick: int = 0
self.__turn_back_index: int = 0
self.__is_over_cost = False
self.___over_cost_count_in_one_term: int = 0
self.__id_lock = threading.Lock()
def __next_over_cost_id(self) -> int:
current_time_tick = self.__get_current_time_tick()
if current_time_tick > self.__last_time_tick:
self.__last_time_tick = current_time_tick
self.__current_seq_number = self.min_seq_number
self.__is_over_cost = False
self.___over_cost_count_in_one_term = 0
return self.__calc_id(self.__last_time_tick)
if self.___over_cost_count_in_one_term >= self.top_over_cost_count:
self.__last_time_tick = self.__get_next_time_tick()
self.__current_seq_number = self.min_seq_number
self.__is_over_cost = False
self.___over_cost_count_in_one_term = 0
return self.__calc_id(self.__last_time_tick)
if self.__current_seq_number > self.max_seq_number:
self.__last_time_tick += 1
self.__current_seq_number = self.min_seq_number
self.__is_over_cost = True
self.___over_cost_count_in_one_term += 1
return self.__calc_id(self.__last_time_tick)
return self.__calc_id(self.__last_time_tick)
def __next_normal_id(self) -> int:
current_time_tick = self.__get_current_time_tick()
if current_time_tick < self.__last_time_tick:
if self.__turn_back_time_tick < 1:
self.__turn_back_time_tick = self.__last_time_tick - 1
self.__turn_back_index += 1
# 每毫秒序列数的前5位是预留位, 0用于手工新值, 1-4是时间回拨次序
# 支持4次回拨次序避免回拨重叠导致ID重复, 可无限次回拨(次序循环使用)。
if self.__turn_back_index > 4:
self.__turn_back_index = 1
return self.__calc_turn_back_id(self.__turn_back_time_tick)
# 时间追平时, _TurnBackTimeTick清零
self.__turn_back_time_tick = min(self.__turn_back_time_tick, 0)
if current_time_tick > self.__last_time_tick:
self.__last_time_tick = current_time_tick
self.__current_seq_number = self.min_seq_number
return self.__calc_id(self.__last_time_tick)
if self.__current_seq_number > self.max_seq_number:
self.__last_time_tick += 1
self.__current_seq_number = self.min_seq_number
self.__is_over_cost = True
self.___over_cost_count_in_one_term = 1
return self.__calc_id(self.__last_time_tick)
return self.__calc_id(self.__last_time_tick)
def __calc_id(self, use_time_tick) -> int:
self.__current_seq_number += 1
return (
(use_time_tick << self.__timestamp_shift) +
(self.worker_id << self.seq_bit_length) +
self.__current_seq_number
) % int(1e64)
def __calc_turn_back_id(self, use_time_tick) -> int:
self.__turn_back_time_tick -= 1
return (
(use_time_tick << self.__timestamp_shift) +
(self.worker_id << self.seq_bit_length) +
self.__turn_back_index
) % int(1e64)
def __get_current_time_tick(self) -> int:
return int((time.time_ns() / 1e6) - self.base_time)
def __get_next_time_tick(self) -> int:
temp_time_ticker = self.__get_current_time_tick()
while temp_time_ticker <= self.__last_time_tick:
# 0.001 = 1 mili sec
time.sleep(0.001)
temp_time_ticker = self.__get_current_time_tick()
return temp_time_ticker
def next_id(self) -> int:
with self.__id_lock:
if self.__is_over_cost:
nextid = self.__next_over_cost_id()
else:
nextid = self.__next_normal_id()
return nextid

14
main.py Normal file
View File

@@ -0,0 +1,14 @@
from fastapi import FastAPI
from api import register_routers
from middleware.blog_visit_middleware import blog_visit_middleware
from middleware.exceptions import global_exception_handler
app = FastAPI(title="Blog Service")
# 加载中间件
app.middleware("http")(global_exception_handler)
app.middleware("http")(blog_visit_middleware)
# 挂载API
register_routers(app)

Binary file not shown.

View File

@@ -0,0 +1,41 @@
import re
from fastapi import Request
from models.blog import BlogVisit
from service import blog_service
from utils.blog_utils import get_user_agent_info, get_client_ip
from config.database import SessionLocal
from middleware.exceptions import AppException
async def blog_visit_middleware(request: Request, call_next):
# 检查路径是否匹配 /blog/content/{blog_id}
if match := re.match(r"^/blog/content/(\d+)$", request.url.path):
# 获取客户端信息
ip = get_client_ip(request)
user_agent = request.headers.get("User-Agent", "")
os, browser = get_user_agent_info(user_agent)
blog_id = int(match.group(1))
# 创建访问记录
blog_visit = BlogVisit(
ip=ip,
os=os,
browser=browser,
uri=request.url.path,
blog_id=blog_id
)
db = SessionLocal()
try:
blog_service.add_blog_visit(db, blog_visit)
db.commit()
except Exception as e:
db.rollback()
raise AppException(f"新增博客{blog_id}访问记录失败: {e}")
finally:
db.close()
# 继续处理请求
response = await call_next(request)
return response

54
middleware/exceptions.py Normal file
View File

@@ -0,0 +1,54 @@
from fastapi import Request, HTTPException, status
from fastapi.responses import JSONResponse
from sqlalchemy.exc import SQLAlchemyError
from config.logging import logger
# 自定义异常类
class AppException(Exception):
def __init__(self, message: str, details=None):
self.message = message
self.details = details
# 全局异常处理中间件
async def global_exception_handler(request: Request, call_next):
try:
# 记录请求信息(可选)
logger.info(f"请求: {request.method} {request.url}")
if request.query_params:
logger.info(f"查询参数: {dict(request.query_params)}")
response = await call_next(request)
# 记录响应信息(可选)
if response.status_code >= 400:
logger.warning(f"响应: {response.status_code}")
return response
except AppException as e:
# 记录业务异常
logger.error(f"业务异常: {e.message} - 详情: {e.details}")
return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST,
content={"message": e.message, "details": e.details})
except SQLAlchemyError as e:
# 记录数据库异常
logger.critical(f"数据库异常: {str(e)}", exc_info=True)
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)
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"code": 500, "message": "服务器内部错误", "details": str(e)})
def get_credentials_exception() -> HTTPException:
return HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)

Binary file not shown.

Binary file not shown.

57
models/base.py Normal file
View File

@@ -0,0 +1,57 @@
from sqlalchemy import Column, BigInteger, String, DateTime, event
from sqlalchemy.ext.declarative import declared_attr
from config.auth import context_sub
from config.database import Base
from datetime import datetime
from utils.common import camel_to_snake
from id_generator import options, generator
# https://github.com/yitter/IdGenerator/tree/master/Python
options = options.IdGeneratorOptions(worker_id=23)
idgen = generator.DefaultIdGenerator()
idgen.set_id_generator(options)
# 第二层基类包含ID
class IdBase(Base):
__abstract__ = True
id = Column(BigInteger, primary_key=True, index=True)
@declared_attr
def __tablename__(cls):
# 自动把数据库实体类名驼峰转为数据库表名下划线
return camel_to_snake(cls.__name__)
# 自动填充id
@event.listens_for(IdBase, 'before_insert', propagate=True)
def before_insert_listener(mapper, connection, target):
if target.id is None:
target.id = idgen.next_id()
# 第二层基类包含ID和审计字段
class AuditBase(IdBase):
__abstract__ = True
create_time = Column(DateTime, nullable=True, default=datetime.now)
create_by = Column(String(255), nullable=True)
update_time = Column(DateTime, nullable=True, onupdate=datetime.now)
update_by = Column(String(255), nullable=True)
@event.listens_for(AuditBase, 'before_insert', propagate=True)
def before_insert(mapper, connection, target):
value = context_sub.get(None)
if value is not None:
target.create_by = value
@event.listens_for(AuditBase, 'before_update', propagate=True)
def before_update(mapper, connection, target):
value = context_sub.get(None)
if value is not None:
target.update_by = value

99
models/blog.py Normal file
View File

@@ -0,0 +1,99 @@
from sqlalchemy import (
BigInteger,
Boolean,
Column,
Integer,
DECIMAL,
SMALLINT,
String,
LargeBinary
)
from sqlalchemy.orm import relationship
from models.base import AuditBase
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="阅读时长")
category = relationship(
"BlogCategory",
back_populates="blogs",
primaryjoin="foreign(Blog.category_id) == BlogCategory.id"
)
content = relationship(
"BlogContent",
back_populates="blog",
primaryjoin="foreign(Blog.content_id) == BlogContent.id"
)
visits = relationship(
"BlogVisit",
back_populates="blog",
primaryjoin="Blog.id == foreign(BlogVisit.blog_id)"
)
comments = relationship(
"BlogComment",
back_populates="blog",
primaryjoin="Blog.id == foreign(BlogComment.blog_id)"
)
class BlogCategory(AuditBase):
name = Column(String(45), nullable=True, comment="类别名称")
blogs = relationship(
"Blog",
back_populates="category",
primaryjoin="BlogCategory.id == foreign(Blog.category_id)"
)
class BlogContent(AuditBase):
content = Column(LargeBinary, comment="博客内容")
blog = relationship(
"Blog",
back_populates="content",
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")
blog = relationship(
"Blog",
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="评论人昵称")
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="是否通过")
blog = relationship(
"Blog",
back_populates="comments",
primaryjoin="foreign(BlogComment.blog_id) == Blog.id"
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

9
requirements.txt Normal file
View File

@@ -0,0 +1,9 @@
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
uvicorn~=0.23.0

Binary file not shown.

Binary file not shown.

Binary file not shown.

79
schemas/blog.py Normal file
View File

@@ -0,0 +1,79 @@
from typing import Optional
from pydantic import BaseModel
from datetime import datetime
class BlogQuery(BaseModel):
# 类别
category: Optional[str] = None
# 标题
title: Optional[str] = None
# 年份
year: Optional[int] = None
class BlogResponse(BaseModel):
id: Optional[int] = None
title: Optional[str] = None
topValue: Optional[int] = None
isGreat: Optional[bool] = None
category: Optional[str] = None
summary: Optional[str] = None
content: Optional[str] = None
wordCount: Optional[int] = None
readDuration: Optional[float] = None
visitCount: Optional[int] = None
createTime: Optional[datetime] = None
updateTime: Optional[datetime] = None
class Config:
from_attributes = True
json_encoders = {
# 自定义 datetime 类型的序列化格式
datetime: lambda dt: dt.strftime('%Y-%m-%d %H:%M:%S')
}
class BlogCategoryResponse(BaseModel):
name: str
count: int
class Config:
from_attributes = True
class BlogStatsResponse(BaseModel):
blogCount: int
categoryCount: int
wordCount: int
class BlogLatestResponse(BaseModel):
id: int
title: str
class Config:
from_attributes = True
class BlogCommentCreate(BaseModel):
blogId: int
parentId: int
name: str
website: Optional[str] = None
content: str
class BlogCommentResponse(BlogCommentCreate):
id: int
ipAddress: str
userAgent: str
createTime: datetime
class Config:
from_attributes = True
json_encoders = {
# 自定义 datetime 类型的序列化格式
datetime: lambda dt: dt.strftime('%Y-%m-%d %H:%M:%S')
}

27
schemas/paginate_query.py Normal file
View File

@@ -0,0 +1,27 @@
from sqlalchemy import select, func
from sqlalchemy.orm import Session
from schemas.pagination import PageResult
def paginate_query(db: Session, query, current_page: int = 1, page_size: int = 10) -> PageResult:
"""通用分页查询函数"""
# 计算总记录数
total = db.execute(select(func.count()).select_from(query.subquery())).scalar_one_or_none() or 0
# 计算总页数
total_pages = (total + page_size - 1) // page_size if page_size != 0 else 0
# 执行分页查询
results = db.execute(query.offset((current_page - 1) * page_size).limit(page_size)).all()
# 转换为字典列表
records = [row._asdict() if hasattr(row, "_asdict") else dict(row) for row in results]
return PageResult(
current=current_page,
size=page_size,
total=total,
pages=total_pages,
records=records
)

42
schemas/pagination.py Normal file
View File

@@ -0,0 +1,42 @@
from typing import List, TypeVar, Generic
from pydantic import BaseModel, field_validator
class PaginationParams(BaseModel):
currentPage: int = 1 # 当前页码,从 1 开始
pageSize: int = 10 # 每页数据量
class Config:
validate_assignment = True
@field_validator("pageSize")
def limit_page_size(cls, v):
if v > 100:
raise ValueError("pageSize 最大为 100")
return v
T = TypeVar("T")
class PageResult(BaseModel, Generic[T]):
"""通用分页结果模型"""
current: int = 1 # 当前页码(从 1 开始)
size: int = 10 # 每页大小
total: int = 0 # 总记录数
pages: int = 0 # 总页数
records: List[T] = [] # 数据列表
@field_validator("size")
def validate_page_size(cls, v):
if v < 1:
raise ValueError("page_size 必须至少为 1")
if v > 1000:
raise ValueError("page_size 最大为 1000")
return v
@property
def offset(self) -> int:
"""计算偏移量"""
return (self.page - 1) * self.size

Binary file not shown.

197
service/blog_service.py Normal file
View File

@@ -0,0 +1,197 @@
from typing import List
from fastapi import Request
from sqlalchemy import select, func, desc, and_
from sqlalchemy.orm import Session
from models.blog import Blog, BlogCategory, BlogVisit, BlogContent, BlogComment
from schemas.blog import BlogResponse, BlogQuery, BlogCategoryResponse, BlogStatsResponse, \
BlogLatestResponse, BlogCommentCreate, BlogCommentResponse
from schemas.pagination import PageResult
from schemas.paginate_query import paginate_query
from middleware.exceptions import AppException
def query_blog_by_page(db: Session, current_page: int = 1, page_size: int = 10) -> PageResult[BlogResponse]:
query = select(
Blog.id,
Blog.title,
Blog.top_value.label("topValue"),
Blog.is_great.label("isGreat"),
BlogCategory.name.label("category"),
Blog.summary,
Blog.word_count.label("wordCount"),
Blog.read_duration.label("readDuration"),
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)
)
return paginate_query(db, query, current_page, page_size)
def query_blog_by_condition(db: Session, blog_query: BlogQuery) -> List[BlogResponse]:
query = 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.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
)
conditions = []
if blog_query.category:
conditions.append(BlogCategory.name == blog_query.category)
if blog_query.title:
conditions.append(Blog.title.like(f"%{blog_query.title}%"))
if blog_query.year:
conditions.append(func.extract('year', Blog.create_time) == blog_query.year)
if conditions:
query = query.where(and_(*conditions))
query = query.order_by(desc(Blog.update_time))
results = db.execute(query).fetchall()
return [BlogResponse.from_orm(result) for result in results]
def query_blog_by_id(db: Session, blog_id: int) -> BlogResponse:
query = db.query(
Blog.id.label("id"),
Blog.title.label("title"),
Blog.top_value.label("topValue"),
Blog.is_great.label("isGreat"),
BlogCategory.name.label("category"),
Blog.summary.label("summary"),
BlogContent.content.label("content"),
Blog.word_count.label("wordCount"),
Blog.read_duration.label("readDuration"),
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 = query.first()
if blog[0] is None:
raise AppException("博客不存在")
return BlogResponse.from_orm(blog)
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
)
results = db.execute(query).fetchall()
return [BlogCategoryResponse.from_orm(result) for result in results]
def query_blog_stats(db: Session) -> BlogStatsResponse:
return BlogStatsResponse(
blogCount=db.query(Blog).count(),
categoryCount=db.query(BlogCategory).count(),
wordCount=db.query(func.sum(Blog.word_count)).scalar()
)
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)
results = db.execute(query).fetchall()
return [BlogLatestResponse.from_orm(result) for result in results]
def add_blog_visit(db: Session, blog_visit: BlogVisit):
db.add(blog_visit)
db.commit()
db.refresh(blog_visit)
def query_blog_comment(db: Session, blog_id: int) -> List[BlogCommentResponse]:
query = select(
BlogComment.id,
BlogComment.blog_id.label("blogId"),
BlogComment.parent_id.label("parentId"),
BlogComment.name,
BlogComment.website,
BlogComment.ip_address.label("ipAddress"),
BlogComment.user_agent.label("userAgent"),
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()
return [BlogCommentResponse.from_orm(result) for result in results]
def add_blog_comment(db: Session, request: Request, blog_comment: BlogCommentCreate) -> bool:
blog = db.query(Blog).filter(Blog.id == blog_comment.blogId).first()
if not blog:
raise AppException("博客不存在")
if blog_comment.parentId != 0:
parent_comment = db.query(BlogComment).filter(
BlogComment.id == blog_comment.parentId,
BlogComment.blog_id == blog_comment.blogId
).first()
if not parent_comment:
raise AppException("父评论不存在")
db_comment = BlogComment(
blog_id=blog_comment.blogId,
parent_id=blog_comment.parentId,
name=blog_comment.name,
website=blog_comment.website,
content=blog_comment.content,
ip_address=request.client.host,
user_agent=request.headers.get("user-agent"),
is_approved=1
)
db.add(db_comment)
db.commit()
db.refresh(db_comment)
return True

203
sys.stdout Normal file
View File

@@ -0,0 +1,203 @@
2025-09-19 23:43:07.605 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-19 23:43:07.685 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-19 23:43:08.892 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-19 23:43:08.893 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-19 23:43:12.227 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/content/631305371107398
2025-09-19 23:43:12.239 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/comment/631305371107398
2025-09-19 23:43:16.396 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-19 23:43:17.331 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-19 23:43:17.332 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-19 23:43:18.318 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/condition?year=2025
2025-09-19 23:43:18.318 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'year': '2025'}
2025-09-19 23:43:30.275 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-19 23:43:30.276 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-20 00:21:47.652 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-20 00:21:47.696 | CRITICAL | middleware.exceptions:global_exception_handler:38 - 数据库异常: (pymysql.err.OperationalError) (2003, "Can't connect to MySQL server on '1213@host.docker.internal' ([Errno 11003] getaddrinfo failed)")
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-09-20 00:21:48.841 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-20 00:21:48.841 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-20 00:21:48.843 | CRITICAL | middleware.exceptions:global_exception_handler:38 - 数据库异常: (pymysql.err.OperationalError) (2003, "Can't connect to MySQL server on '1213@host.docker.internal' ([Errno 11003] getaddrinfo failed)")
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-09-20 00:21:50.319 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-20 00:21:50.321 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-20 00:21:50.321 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-20 00:21:50.323 | CRITICAL | middleware.exceptions:global_exception_handler:38 - 数据库异常: (pymysql.err.OperationalError) (2003, "Can't connect to MySQL server on '1213@host.docker.internal' ([Errno 11003] getaddrinfo failed)")
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-09-20 00:21:50.323 | CRITICAL | middleware.exceptions:global_exception_handler:38 - 数据库异常: (pymysql.err.OperationalError) (2003, "Can't connect to MySQL server on '1213@host.docker.internal' ([Errno 11003] getaddrinfo failed)")
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-09-20 00:22:05.691 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-20 00:22:06.166 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-20 00:22:06.166 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-20 00:22:07.181 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-20 00:22:07.780 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/condition?year=2025
2025-09-20 00:22:07.780 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'year': '2025'}
2025-09-20 00:22:10.304 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-20 00:22:12.516 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-20 00:22:12.516 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-20 00:22:13.631 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-20 00:22:13.632 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-20 00:22:13.632 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-20 00:22:13.653 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-20 00:22:15.602 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-20 00:22:16.177 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/condition?year=2025
2025-09-20 00:22:16.178 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'year': '2025'}
2025-09-20 00:22:16.642 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-20 00:22:17.057 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-20 00:22:17.057 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-20 22:33:09.756 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 22:33:09.758 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-20 22:33:09.799 | CRITICAL | middleware.exceptions:global_exception_handler:38 - 数据库异常: (pymysql.err.OperationalError) (2003, "Can't connect to MySQL server on 'host.docker.internal' ([Errno 11001] getaddrinfo failed)")
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-09-20 22:33:12.603 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 22:33:12.603 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-20 22:33:12.605 | CRITICAL | middleware.exceptions:global_exception_handler:38 - 数据库异常: (pymysql.err.OperationalError) (2003, "Can't connect to MySQL server on 'host.docker.internal' ([Errno 11001] getaddrinfo failed)")
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-09-20 22:33:41.162 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 22:33:41.162 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-20 22:33:43.493 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-20 22:33:44.993 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/condition?year=2025
2025-09-20 22:33:44.994 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'year': '2025'}
2025-09-20 22:34:30.724 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 22:34:30.724 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-20 22:39:14.755 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 22:39:14.755 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-20 22:39:28.815 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 22:39:28.816 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-20 22:42:06.355 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 22:42:06.356 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-20 22:52:16.313 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 22:52:16.313 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-20 22:53:37.160 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 22:53:37.161 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-20 23:06:35.552 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 23:06:35.552 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-20 23:06:45.961 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 23:06:45.961 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-20 23:06:49.856 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 23:06:49.856 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-20 23:06:51.974 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 23:06:51.974 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-20 23:06:52.964 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/content/631305371107398
2025-09-20 23:06:53.026 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/comment/0
2025-09-20 23:06:55.882 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-20 23:07:07.077 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-20 23:07:18.650 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-20 23:07:19.919 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/condition?category=%E5%B7%A5%E4%BD%9C
2025-09-20 23:07:19.920 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'category': '工作'}
2025-09-20 23:07:21.930 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/content/591259203186757
2025-09-20 23:07:21.963 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/comment/0
2025-09-20 23:07:24.205 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/condition?year=2025
2025-09-20 23:07:24.206 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'year': '2025'}
2025-09-20 23:07:27.337 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/content/685309473689669
2025-09-20 23:07:27.349 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/comment/0
2025-09-20 23:07:30.487 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-20 23:07:31.135 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/condition?category=%E5%B7%A5%E5%85%B7
2025-09-20 23:07:31.135 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'category': '工具'}
2025-09-20 23:07:32.841 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3
2025-09-20 23:07:32.842 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '3'}
2025-09-21 10:06:18.830 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:06:18.830 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:06:19.337 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:06:19.337 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:09:08.958 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:09:08.959 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:09:08.959 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:09:09.003 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:09:13.084 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:09:13.088 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:09:13.088 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:09:13.116 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:09:17.146 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:09:17.148 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:09:17.148 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:09:17.167 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:10:27.567 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:10:27.568 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:10:27.568 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:10:27.600 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:10:29.691 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:10:29.691 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:10:29.693 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:10:29.716 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:10:33.337 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-21 10:10:34.566 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/condition?year=2025
2025-09-21 10:10:34.566 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'year': '2025'}
2025-09-21 10:10:36.306 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:10:36.306 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:11:07.366 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:11:07.367 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:11:07.367 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:11:07.399 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:11:09.161 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:11:09.161 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:11:09.161 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:11:09.183 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:11:58.374 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:11:58.378 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:12:01.408 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:12:01.409 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:12:01.410 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:12:01.433 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:12:10.773 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:12:10.776 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:12:10.777 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:12:10.798 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:12:11.940 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-21 10:12:37.216 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:12:37.220 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-21 10:12:37.246 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:12:38.146 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:12:38.146 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:12:39.172 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/content/undefined
2025-09-21 10:12:39.174 | WARNING  | middleware.exceptions:global_exception_handler:26 - 响应: 422
2025-09-21 10:13:00.439 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:13:00.441 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:13:00.441 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:13:00.467 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:13:02.124 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:13:02.126 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:13:02.126 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:13:02.147 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:13:04.776 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/content/631305371107398
2025-09-21 10:13:04.883 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/comment/631305371107398
2025-09-21 10:13:13.399 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:13:13.399 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:13:17.976 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-21 10:13:39.121 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:13:39.124 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-21 10:13:39.147 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:13:39.797 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/condition?category=%E5%B7%A5%E4%BD%9C
2025-09-21 10:13:39.799 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'category': '工作'}
2025-09-21 10:13:41.071 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/content/591259203186757
2025-09-21 10:13:41.130 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/comment/591259203186757
2025-09-21 10:13:42.954 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/condition?year=2025
2025-09-21 10:13:42.955 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'year': '2025'}
2025-09-21 10:13:44.119 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/content/694197446615109
2025-09-21 10:13:44.135 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/comment/694197446615109
2025-09-21 10:13:47.845 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:13:47.845 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:13:57.030 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:13:57.031 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:13:57.031 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:13:57.068 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:14:09.541 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:14:09.544 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:14:09.544 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:14:09.569 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:14:17.410 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/stats
2025-09-21 10:14:17.412 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:14:17.413 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:14:17.437 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/latest
2025-09-21 10:14:19.225 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-21 10:14:20.091 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:14:20.091 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}
2025-09-21 10:14:21.729 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-21 10:14:23.107 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/condition?category=%E5%B7%A5%E4%BD%9C
2025-09-21 10:14:23.107 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'category': '工作'}
2025-09-21 10:14:24.243 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/category
2025-09-21 10:14:25.263 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/condition?category=SpringBoot
2025-09-21 10:14:25.263 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'category': 'SpringBoot'}
2025-09-21 10:14:26.588 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/content/685968269635653
2025-09-21 10:14:26.611 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/comment/685968269635653
2025-09-21 10:14:32.814 | INFO  | middleware.exceptions:global_exception_handler:18 - 请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10
2025-09-21 10:14:32.814 | INFO  | middleware.exceptions:global_exception_handler:20 - 查询参数: {'currentPage': '1', 'pageSize': '10'}

11
test_main.http Normal file
View File

@@ -0,0 +1,11 @@
# Test your FastAPI endpoints
GET http://127.0.0.1:8000/
Accept: application/json
###
GET http://127.0.0.1:8000/hello/User
Accept: application/json
###

Binary file not shown.

Binary file not shown.

58
utils/blog_utils.py Normal file
View File

@@ -0,0 +1,58 @@
from fastapi import Request
from models.blog import BlogVisit
def get_client_ip(request):
"""从请求中获取客户端IP地址"""
x_forwarded_for = request.headers.get("X-Forwarded-For")
if x_forwarded_for:
return x_forwarded_for.split(',')[0]
return request.client.host
def get_user_agent_info(user_agent: str):
"""解析User-Agent获取操作系统和浏览器信息"""
# 使用正则表达式进行更精确的解析
os = "Unknown"
browser = "Unknown"
# 操作系统检测
if "Windows NT" in user_agent:
os = "Windows"
elif "Macintosh" in user_agent:
os = "Mac OS"
elif "Linux" in user_agent:
os = "Linux"
elif "Android" in user_agent:
os = "Android"
elif "iPhone" in user_agent or "iPad" in user_agent:
os = "iOS"
# 浏览器检测
if "Chrome" in user_agent:
browser = "Chrome"
elif "Firefox" in user_agent:
browser = "Firefox"
elif "Safari" in user_agent and "Chrome" not in user_agent:
browser = "Safari"
elif "Edge" in user_agent:
browser = "Edge"
elif "MSIE" in user_agent or "Trident" in user_agent:
browser = "Internet Explorer"
return os, browser
def get_blog_visit(request: Request, blog_id: int) -> BlogVisit:
ip = get_client_ip(request)
user_agent = request.headers.get("User-Agent", "")
os, browser = get_user_agent_info(user_agent)
return BlogVisit(
ip=ip,
os=os,
browser=browser,
uri=request.url.path,
blog_id=blog_id
)

13
utils/common.py Normal file
View File

@@ -0,0 +1,13 @@
import re
def camel_to_snake(name: str) -> str:
"""将驼峰命名转换为蛇形命名CamelCase → snake_case"""
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
def snake_to_camel(name: str) -> str:
"""将蛇形命名转换为驼峰命名snake_case → CamelCase"""
components = name.split('_')
return ''.join(x.title() for x in components)