feat:初始化工程
This commit is contained in:
9
.env
Normal file
9
.env
Normal 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
8
.idea/.gitignore
generated
vendored
Normal 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
10
.idea/blog-service.iml
generated
Normal 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>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
7
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
8
Dockerfile
Normal 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"]
|
||||
BIN
__pycache__/main.cpython-312.pyc
Normal file
BIN
__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
6
api/__init__.py
Normal file
6
api/__init__.py
Normal 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="")
|
||||
BIN
api/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
api/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
api/__pycache__/blog.cpython-312.pyc
Normal file
BIN
api/__pycache__/blog.cpython-312.pyc
Normal file
Binary file not shown.
59
api/blog.py
Normal file
59
api/blog.py
Normal 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)
|
||||
BIN
config/__pycache__/auth.cpython-312.pyc
Normal file
BIN
config/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
config/__pycache__/database.cpython-312.pyc
Normal file
BIN
config/__pycache__/database.cpython-312.pyc
Normal file
Binary file not shown.
BIN
config/__pycache__/logging.cpython-312.pyc
Normal file
BIN
config/__pycache__/logging.cpython-312.pyc
Normal file
Binary file not shown.
BIN
config/__pycache__/setting.cpython-312.pyc
Normal file
BIN
config/__pycache__/setting.cpython-312.pyc
Normal file
Binary file not shown.
85
config/auth.py
Normal file
85
config/auth.py
Normal 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
23
config/database.py
Normal 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
65
config/logging.py
Normal 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
19
config/setting.py
Normal 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
0
id_generator/__init__.py
Normal file
BIN
id_generator/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
id_generator/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
id_generator/__pycache__/generator.cpython-312.pyc
Normal file
BIN
id_generator/__pycache__/generator.cpython-312.pyc
Normal file
Binary file not shown.
BIN
id_generator/__pycache__/options.cpython-312.pyc
Normal file
BIN
id_generator/__pycache__/options.cpython-312.pyc
Normal file
Binary file not shown.
BIN
id_generator/__pycache__/snowflake.cpython-312.pyc
Normal file
BIN
id_generator/__pycache__/snowflake.cpython-312.pyc
Normal file
Binary file not shown.
BIN
id_generator/__pycache__/snowflake_m1.cpython-312.pyc
Normal file
BIN
id_generator/__pycache__/snowflake_m1.cpython-312.pyc
Normal file
Binary file not shown.
38
id_generator/generator.py
Normal file
38
id_generator/generator.py
Normal 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
134
id_generator/idregister.py
Normal 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
43
id_generator/options.py
Normal 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
20
id_generator/snowflake.py
Normal 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
|
||||
147
id_generator/snowflake_m1.py
Normal file
147
id_generator/snowflake_m1.py
Normal 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
14
main.py
Normal 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)
|
||||
BIN
middleware/__pycache__/blog_visit_middleware.cpython-312.pyc
Normal file
BIN
middleware/__pycache__/blog_visit_middleware.cpython-312.pyc
Normal file
Binary file not shown.
BIN
middleware/__pycache__/exceptions.cpython-312.pyc
Normal file
BIN
middleware/__pycache__/exceptions.cpython-312.pyc
Normal file
Binary file not shown.
41
middleware/blog_visit_middleware.py
Normal file
41
middleware/blog_visit_middleware.py
Normal 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
54
middleware/exceptions.py
Normal 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"},
|
||||
)
|
||||
BIN
models/__pycache__/base.cpython-312.pyc
Normal file
BIN
models/__pycache__/base.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/blog.cpython-312.pyc
Normal file
BIN
models/__pycache__/blog.cpython-312.pyc
Normal file
Binary file not shown.
57
models/base.py
Normal file
57
models/base.py
Normal 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
99
models/blog.py
Normal 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"
|
||||
)
|
||||
BIN
packages/PyJWT-2.10.1-py3-none-any.whl
Normal file
BIN
packages/PyJWT-2.10.1-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/annotated_types-0.7.0-py3-none-any.whl
Normal file
BIN
packages/annotated_types-0.7.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/anyio-4.10.0-py3-none-any.whl
Normal file
BIN
packages/anyio-4.10.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/click-8.3.0-py3-none-any.whl
Normal file
BIN
packages/click-8.3.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/colorama-0.4.6-py2.py3-none-any.whl
Normal file
BIN
packages/colorama-0.4.6-py2.py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/fastapi-0.115.14-py3-none-any.whl
Normal file
BIN
packages/fastapi-0.115.14-py3-none-any.whl
Normal file
Binary file not shown.
Binary file not shown.
BIN
packages/h11-0.16.0-py3-none-any.whl
Normal file
BIN
packages/h11-0.16.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/idna-3.10-py3-none-any.whl
Normal file
BIN
packages/idna-3.10-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/loguru-0.7.3-py3-none-any.whl
Normal file
BIN
packages/loguru-0.7.3-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/passlib-1.7.4-py2.py3-none-any.whl
Normal file
BIN
packages/passlib-1.7.4-py2.py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/pydantic-2.11.9-py3-none-any.whl
Normal file
BIN
packages/pydantic-2.11.9-py3-none-any.whl
Normal file
Binary file not shown.
Binary file not shown.
BIN
packages/pydantic_settings-2.9.1-py3-none-any.whl
Normal file
BIN
packages/pydantic_settings-2.9.1-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/pymysql-1.1.2-py3-none-any.whl
Normal file
BIN
packages/pymysql-1.1.2-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/python_dotenv-1.1.1-py3-none-any.whl
Normal file
BIN
packages/python_dotenv-1.1.1-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/sniffio-1.3.1-py3-none-any.whl
Normal file
BIN
packages/sniffio-1.3.1-py3-none-any.whl
Normal file
Binary file not shown.
Binary file not shown.
BIN
packages/starlette-0.46.2-py3-none-any.whl
Normal file
BIN
packages/starlette-0.46.2-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/typing_extensions-4.15.0-py3-none-any.whl
Normal file
BIN
packages/typing_extensions-4.15.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/typing_inspection-0.4.1-py3-none-any.whl
Normal file
BIN
packages/typing_inspection-0.4.1-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/uvicorn-0.23.2-py3-none-any.whl
Normal file
BIN
packages/uvicorn-0.23.2-py3-none-any.whl
Normal file
Binary file not shown.
BIN
packages/win32_setctime-1.2.0-py3-none-any.whl
Normal file
BIN
packages/win32_setctime-1.2.0-py3-none-any.whl
Normal file
Binary file not shown.
9
requirements.txt
Normal file
9
requirements.txt
Normal 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
|
||||
BIN
schemas/__pycache__/blog.cpython-312.pyc
Normal file
BIN
schemas/__pycache__/blog.cpython-312.pyc
Normal file
Binary file not shown.
BIN
schemas/__pycache__/paginate_query.cpython-312.pyc
Normal file
BIN
schemas/__pycache__/paginate_query.cpython-312.pyc
Normal file
Binary file not shown.
BIN
schemas/__pycache__/pagination.cpython-312.pyc
Normal file
BIN
schemas/__pycache__/pagination.cpython-312.pyc
Normal file
Binary file not shown.
79
schemas/blog.py
Normal file
79
schemas/blog.py
Normal 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
27
schemas/paginate_query.py
Normal 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
42
schemas/pagination.py
Normal 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
|
||||
BIN
service/__pycache__/blog_service.cpython-312.pyc
Normal file
BIN
service/__pycache__/blog_service.cpython-312.pyc
Normal file
Binary file not shown.
197
service/blog_service.py
Normal file
197
service/blog_service.py
Normal 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
203
sys.stdout
Normal file
@@ -0,0 +1,203 @@
|
||||
[32m2025-09-19 23:43:07.605[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-19 23:43:07.685[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-19 23:43:08.892[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-19 23:43:08.893[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-19 23:43:12.227[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/content/631305371107398[0m
|
||||
[32m2025-09-19 23:43:12.239[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/comment/631305371107398[0m
|
||||
[32m2025-09-19 23:43:16.396[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-19 23:43:17.331[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-19 23:43:17.332[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-19 23:43:18.318[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/condition?year=2025[0m
|
||||
[32m2025-09-19 23:43:18.318[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'year': '2025'}[0m
|
||||
[32m2025-09-19 23:43:30.275[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-19 23:43:30.276[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-20 00:21:47.652[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-20 00:21:47.696[0m | [41m[1mCRITICAL[0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m38[0m - [41m[1m数据库异常: (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)[0m
|
||||
[32m2025-09-20 00:21:48.841[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-20 00:21:48.841[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-20 00:21:48.843[0m | [41m[1mCRITICAL[0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m38[0m - [41m[1m数据库异常: (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)[0m
|
||||
[32m2025-09-20 00:21:50.319[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-20 00:21:50.321[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-20 00:21:50.321[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-20 00:21:50.323[0m | [41m[1mCRITICAL[0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m38[0m - [41m[1m数据库异常: (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)[0m
|
||||
[32m2025-09-20 00:21:50.323[0m | [41m[1mCRITICAL[0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m38[0m - [41m[1m数据库异常: (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)[0m
|
||||
[32m2025-09-20 00:22:05.691[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-20 00:22:06.166[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-20 00:22:06.166[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-20 00:22:07.181[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-20 00:22:07.780[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/condition?year=2025[0m
|
||||
[32m2025-09-20 00:22:07.780[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'year': '2025'}[0m
|
||||
[32m2025-09-20 00:22:10.304[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-20 00:22:12.516[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-20 00:22:12.516[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-20 00:22:13.631[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-20 00:22:13.632[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-20 00:22:13.632[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-20 00:22:13.653[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-20 00:22:15.602[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-20 00:22:16.177[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/condition?year=2025[0m
|
||||
[32m2025-09-20 00:22:16.178[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'year': '2025'}[0m
|
||||
[32m2025-09-20 00:22:16.642[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-20 00:22:17.057[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-20 00:22:17.057[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-20 22:33:09.756[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 22:33:09.758[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-20 22:33:09.799[0m | [41m[1mCRITICAL[0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m38[0m - [41m[1m数据库异常: (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)[0m
|
||||
[32m2025-09-20 22:33:12.603[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 22:33:12.603[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-20 22:33:12.605[0m | [41m[1mCRITICAL[0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m38[0m - [41m[1m数据库异常: (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)[0m
|
||||
[32m2025-09-20 22:33:41.162[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 22:33:41.162[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-20 22:33:43.493[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-20 22:33:44.993[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/condition?year=2025[0m
|
||||
[32m2025-09-20 22:33:44.994[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'year': '2025'}[0m
|
||||
[32m2025-09-20 22:34:30.724[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 22:34:30.724[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-20 22:39:14.755[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 22:39:14.755[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-20 22:39:28.815[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 22:39:28.816[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-20 22:42:06.355[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 22:42:06.356[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-20 22:52:16.313[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 22:52:16.313[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-20 22:53:37.160[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 22:53:37.161[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-20 23:06:35.552[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 23:06:35.552[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-20 23:06:45.961[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 23:06:45.961[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-20 23:06:49.856[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 23:06:49.856[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-20 23:06:51.974[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 23:06:51.974[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-20 23:06:52.964[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/content/631305371107398[0m
|
||||
[32m2025-09-20 23:06:53.026[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/comment/0[0m
|
||||
[32m2025-09-20 23:06:55.882[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-20 23:07:07.077[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-20 23:07:18.650[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-20 23:07:19.919[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/condition?category=%E5%B7%A5%E4%BD%9C[0m
|
||||
[32m2025-09-20 23:07:19.920[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'category': '工作'}[0m
|
||||
[32m2025-09-20 23:07:21.930[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/content/591259203186757[0m
|
||||
[32m2025-09-20 23:07:21.963[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/comment/0[0m
|
||||
[32m2025-09-20 23:07:24.205[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/condition?year=2025[0m
|
||||
[32m2025-09-20 23:07:24.206[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'year': '2025'}[0m
|
||||
[32m2025-09-20 23:07:27.337[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/content/685309473689669[0m
|
||||
[32m2025-09-20 23:07:27.349[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/comment/0[0m
|
||||
[32m2025-09-20 23:07:30.487[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-20 23:07:31.135[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/condition?category=%E5%B7%A5%E5%85%B7[0m
|
||||
[32m2025-09-20 23:07:31.135[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'category': '工具'}[0m
|
||||
[32m2025-09-20 23:07:32.841[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=3[0m
|
||||
[32m2025-09-20 23:07:32.842[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '3'}[0m
|
||||
[32m2025-09-21 10:06:18.830[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:06:18.830[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:06:19.337[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:06:19.337[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:09:08.958[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:09:08.959[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:09:08.959[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:09:09.003[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:09:13.084[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:09:13.088[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:09:13.088[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:09:13.116[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:09:17.146[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:09:17.148[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:09:17.148[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:09:17.167[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:10:27.567[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:10:27.568[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:10:27.568[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:10:27.600[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:10:29.691[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:10:29.691[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:10:29.693[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:10:29.716[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:10:33.337[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-21 10:10:34.566[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/condition?year=2025[0m
|
||||
[32m2025-09-21 10:10:34.566[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'year': '2025'}[0m
|
||||
[32m2025-09-21 10:10:36.306[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:10:36.306[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:11:07.366[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:11:07.367[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:11:07.367[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:11:07.399[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:11:09.161[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:11:09.161[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:11:09.161[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:11:09.183[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:11:58.374[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:11:58.378[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:12:01.408[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:12:01.409[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:12:01.410[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:12:01.433[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:12:10.773[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:12:10.776[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:12:10.777[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:12:10.798[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:12:11.940[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-21 10:12:37.216[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:12:37.220[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-21 10:12:37.246[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:12:38.146[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:12:38.146[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:12:39.172[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/content/undefined[0m
|
||||
[32m2025-09-21 10:12:39.174[0m | [33m[1mWARNING [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m26[0m - [33m[1m响应: 422[0m
|
||||
[32m2025-09-21 10:13:00.439[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:13:00.441[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:13:00.441[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:13:00.467[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:13:02.124[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:13:02.126[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:13:02.126[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:13:02.147[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:13:04.776[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/content/631305371107398[0m
|
||||
[32m2025-09-21 10:13:04.883[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/comment/631305371107398[0m
|
||||
[32m2025-09-21 10:13:13.399[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:13:13.399[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:13:17.976[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-21 10:13:39.121[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:13:39.124[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-21 10:13:39.147[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:13:39.797[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/condition?category=%E5%B7%A5%E4%BD%9C[0m
|
||||
[32m2025-09-21 10:13:39.799[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'category': '工作'}[0m
|
||||
[32m2025-09-21 10:13:41.071[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/content/591259203186757[0m
|
||||
[32m2025-09-21 10:13:41.130[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/comment/591259203186757[0m
|
||||
[32m2025-09-21 10:13:42.954[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/condition?year=2025[0m
|
||||
[32m2025-09-21 10:13:42.955[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'year': '2025'}[0m
|
||||
[32m2025-09-21 10:13:44.119[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/content/694197446615109[0m
|
||||
[32m2025-09-21 10:13:44.135[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/comment/694197446615109[0m
|
||||
[32m2025-09-21 10:13:47.845[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:13:47.845[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:13:57.030[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:13:57.031[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:13:57.031[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:13:57.068[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:14:09.541[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:14:09.544[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:14:09.544[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:14:09.569[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:14:17.410[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/stats[0m
|
||||
[32m2025-09-21 10:14:17.412[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:14:17.413[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:14:17.437[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/latest[0m
|
||||
[32m2025-09-21 10:14:19.225[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-21 10:14:20.091[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:14:20.091[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
[32m2025-09-21 10:14:21.729[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-21 10:14:23.107[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/condition?category=%E5%B7%A5%E4%BD%9C[0m
|
||||
[32m2025-09-21 10:14:23.107[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'category': '工作'}[0m
|
||||
[32m2025-09-21 10:14:24.243[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/category[0m
|
||||
[32m2025-09-21 10:14:25.263[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/condition?category=SpringBoot[0m
|
||||
[32m2025-09-21 10:14:25.263[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'category': 'SpringBoot'}[0m
|
||||
[32m2025-09-21 10:14:26.588[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/content/685968269635653[0m
|
||||
[32m2025-09-21 10:14:26.611[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/comment/685968269635653[0m
|
||||
[32m2025-09-21 10:14:32.814[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m18[0m - [1m请求: GET http://127.0.0.1:8082/blog/page?currentPage=1&pageSize=10[0m
|
||||
[32m2025-09-21 10:14:32.814[0m | [1mINFO [0m | [36mmiddleware.exceptions[0m:[36mglobal_exception_handler[0m:[36m20[0m - [1m查询参数: {'currentPage': '1', 'pageSize': '10'}[0m
|
||||
11
test_main.http
Normal file
11
test_main.http
Normal 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
|
||||
|
||||
###
|
||||
BIN
utils/__pycache__/blog_utils.cpython-312.pyc
Normal file
BIN
utils/__pycache__/blog_utils.cpython-312.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/common.cpython-312.pyc
Normal file
BIN
utils/__pycache__/common.cpython-312.pyc
Normal file
Binary file not shown.
58
utils/blog_utils.py
Normal file
58
utils/blog_utils.py
Normal 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
13
utils/common.py
Normal 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)
|
||||
Reference in New Issue
Block a user