commit c4674749ceefae571c83d614eb3679bf05eb1b36 Author: Cxx0822 <1556464090@qq.com> Date: Sun Sep 21 22:47:06 2025 +0800 feat:初始化工程 diff --git a/.env b/.env new file mode 100644 index 0000000..060fafa --- /dev/null +++ b/.env @@ -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 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/blog-service.iml b/.idea/blog-service.iml new file mode 100644 index 0000000..74d515a --- /dev/null +++ b/.idea/blog-service.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f62dc0d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..90c2e3f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..19295d7 --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..d04da14 Binary files /dev/null and b/__pycache__/main.cpython-312.pyc differ diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..30db9b3 --- /dev/null +++ b/api/__init__.py @@ -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="") \ No newline at end of file diff --git a/api/__pycache__/__init__.cpython-312.pyc b/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..bf0e5cc Binary files /dev/null and b/api/__pycache__/__init__.cpython-312.pyc differ diff --git a/api/__pycache__/blog.cpython-312.pyc b/api/__pycache__/blog.cpython-312.pyc new file mode 100644 index 0000000..f8d81e6 Binary files /dev/null and b/api/__pycache__/blog.cpython-312.pyc differ diff --git a/api/blog.py b/api/blog.py new file mode 100644 index 0000000..c377216 --- /dev/null +++ b/api/blog.py @@ -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) \ No newline at end of file diff --git a/config/__pycache__/auth.cpython-312.pyc b/config/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000..f003779 Binary files /dev/null and b/config/__pycache__/auth.cpython-312.pyc differ diff --git a/config/__pycache__/database.cpython-312.pyc b/config/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..48e6e5f Binary files /dev/null and b/config/__pycache__/database.cpython-312.pyc differ diff --git a/config/__pycache__/logging.cpython-312.pyc b/config/__pycache__/logging.cpython-312.pyc new file mode 100644 index 0000000..1292149 Binary files /dev/null and b/config/__pycache__/logging.cpython-312.pyc differ diff --git a/config/__pycache__/setting.cpython-312.pyc b/config/__pycache__/setting.cpython-312.pyc new file mode 100644 index 0000000..2409b07 Binary files /dev/null and b/config/__pycache__/setting.cpython-312.pyc differ diff --git a/config/auth.py b/config/auth.py new file mode 100644 index 0000000..5a761ef --- /dev/null +++ b/config/auth.py @@ -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() diff --git a/config/database.py b/config/database.py new file mode 100644 index 0000000..e600cb2 --- /dev/null +++ b/config/database.py @@ -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() diff --git a/config/logging.py b/config/logging.py new file mode 100644 index 0000000..4f834d9 --- /dev/null +++ b/config/logging.py @@ -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 = ( + "{time:YYYY-MM-DD HH:mm:ss.SSS} | " + "{level: <8} | " + "{name}:{function}:{line} - " + "{message}" +) + +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"] \ No newline at end of file diff --git a/config/setting.py b/config/setting.py new file mode 100644 index 0000000..6f53870 --- /dev/null +++ b/config/setting.py @@ -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() diff --git a/id_generator/__init__.py b/id_generator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/id_generator/__pycache__/__init__.cpython-312.pyc b/id_generator/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..11d9d6e Binary files /dev/null and b/id_generator/__pycache__/__init__.cpython-312.pyc differ diff --git a/id_generator/__pycache__/generator.cpython-312.pyc b/id_generator/__pycache__/generator.cpython-312.pyc new file mode 100644 index 0000000..46918d5 Binary files /dev/null and b/id_generator/__pycache__/generator.cpython-312.pyc differ diff --git a/id_generator/__pycache__/options.cpython-312.pyc b/id_generator/__pycache__/options.cpython-312.pyc new file mode 100644 index 0000000..6787315 Binary files /dev/null and b/id_generator/__pycache__/options.cpython-312.pyc differ diff --git a/id_generator/__pycache__/snowflake.cpython-312.pyc b/id_generator/__pycache__/snowflake.cpython-312.pyc new file mode 100644 index 0000000..8fbb8ff Binary files /dev/null and b/id_generator/__pycache__/snowflake.cpython-312.pyc differ diff --git a/id_generator/__pycache__/snowflake_m1.cpython-312.pyc b/id_generator/__pycache__/snowflake_m1.cpython-312.pyc new file mode 100644 index 0000000..787690c Binary files /dev/null and b/id_generator/__pycache__/snowflake_m1.cpython-312.pyc differ diff --git a/id_generator/generator.py b/id_generator/generator.py new file mode 100644 index 0000000..fc995d7 --- /dev/null +++ b/id_generator/generator.py @@ -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() diff --git a/id_generator/idregister.py b/id_generator/idregister.py new file mode 100644 index 0000000..787faac --- /dev/null +++ b/id_generator/idregister.py @@ -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() diff --git a/id_generator/options.py b/id_generator/options.py new file mode 100644 index 0000000..8558381 --- /dev/null +++ b/id_generator/options.py @@ -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 diff --git a/id_generator/snowflake.py b/id_generator/snowflake.py new file mode 100644 index 0000000..ea36cfa --- /dev/null +++ b/id_generator/snowflake.py @@ -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 diff --git a/id_generator/snowflake_m1.py b/id_generator/snowflake_m1.py new file mode 100644 index 0000000..596b76d --- /dev/null +++ b/id_generator/snowflake_m1.py @@ -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 diff --git a/main.py b/main.py new file mode 100644 index 0000000..e01f3bb --- /dev/null +++ b/main.py @@ -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) \ No newline at end of file diff --git a/middleware/__pycache__/blog_visit_middleware.cpython-312.pyc b/middleware/__pycache__/blog_visit_middleware.cpython-312.pyc new file mode 100644 index 0000000..26d6612 Binary files /dev/null and b/middleware/__pycache__/blog_visit_middleware.cpython-312.pyc differ diff --git a/middleware/__pycache__/exceptions.cpython-312.pyc b/middleware/__pycache__/exceptions.cpython-312.pyc new file mode 100644 index 0000000..279b97a Binary files /dev/null and b/middleware/__pycache__/exceptions.cpython-312.pyc differ diff --git a/middleware/blog_visit_middleware.py b/middleware/blog_visit_middleware.py new file mode 100644 index 0000000..64a671e --- /dev/null +++ b/middleware/blog_visit_middleware.py @@ -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 diff --git a/middleware/exceptions.py b/middleware/exceptions.py new file mode 100644 index 0000000..8013846 --- /dev/null +++ b/middleware/exceptions.py @@ -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"}, + ) diff --git a/models/__pycache__/base.cpython-312.pyc b/models/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000..d86d7a7 Binary files /dev/null and b/models/__pycache__/base.cpython-312.pyc differ diff --git a/models/__pycache__/blog.cpython-312.pyc b/models/__pycache__/blog.cpython-312.pyc new file mode 100644 index 0000000..9512557 Binary files /dev/null and b/models/__pycache__/blog.cpython-312.pyc differ diff --git a/models/base.py b/models/base.py new file mode 100644 index 0000000..af5136a --- /dev/null +++ b/models/base.py @@ -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 diff --git a/models/blog.py b/models/blog.py new file mode 100644 index 0000000..bcd2957 --- /dev/null +++ b/models/blog.py @@ -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" + ) diff --git a/packages/PyJWT-2.10.1-py3-none-any.whl b/packages/PyJWT-2.10.1-py3-none-any.whl new file mode 100644 index 0000000..6195c57 Binary files /dev/null and b/packages/PyJWT-2.10.1-py3-none-any.whl differ diff --git a/packages/annotated_types-0.7.0-py3-none-any.whl b/packages/annotated_types-0.7.0-py3-none-any.whl new file mode 100644 index 0000000..319cf66 Binary files /dev/null and b/packages/annotated_types-0.7.0-py3-none-any.whl differ diff --git a/packages/anyio-4.10.0-py3-none-any.whl b/packages/anyio-4.10.0-py3-none-any.whl new file mode 100644 index 0000000..ba75208 Binary files /dev/null and b/packages/anyio-4.10.0-py3-none-any.whl differ diff --git a/packages/click-8.3.0-py3-none-any.whl b/packages/click-8.3.0-py3-none-any.whl new file mode 100644 index 0000000..958b5e4 Binary files /dev/null and b/packages/click-8.3.0-py3-none-any.whl differ diff --git a/packages/colorama-0.4.6-py2.py3-none-any.whl b/packages/colorama-0.4.6-py2.py3-none-any.whl new file mode 100644 index 0000000..f666ce9 Binary files /dev/null and b/packages/colorama-0.4.6-py2.py3-none-any.whl differ diff --git a/packages/fastapi-0.115.14-py3-none-any.whl b/packages/fastapi-0.115.14-py3-none-any.whl new file mode 100644 index 0000000..57d645e Binary files /dev/null and b/packages/fastapi-0.115.14-py3-none-any.whl differ diff --git a/packages/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/packages/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl new file mode 100644 index 0000000..610c31d Binary files /dev/null and b/packages/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ diff --git a/packages/h11-0.16.0-py3-none-any.whl b/packages/h11-0.16.0-py3-none-any.whl new file mode 100644 index 0000000..f12b3ce Binary files /dev/null and b/packages/h11-0.16.0-py3-none-any.whl differ diff --git a/packages/idna-3.10-py3-none-any.whl b/packages/idna-3.10-py3-none-any.whl new file mode 100644 index 0000000..52759bd Binary files /dev/null and b/packages/idna-3.10-py3-none-any.whl differ diff --git a/packages/loguru-0.7.3-py3-none-any.whl b/packages/loguru-0.7.3-py3-none-any.whl new file mode 100644 index 0000000..58bf021 Binary files /dev/null and b/packages/loguru-0.7.3-py3-none-any.whl differ diff --git a/packages/passlib-1.7.4-py2.py3-none-any.whl b/packages/passlib-1.7.4-py2.py3-none-any.whl new file mode 100644 index 0000000..ca38d13 Binary files /dev/null and b/packages/passlib-1.7.4-py2.py3-none-any.whl differ diff --git a/packages/pydantic-2.11.9-py3-none-any.whl b/packages/pydantic-2.11.9-py3-none-any.whl new file mode 100644 index 0000000..8a60020 Binary files /dev/null and b/packages/pydantic-2.11.9-py3-none-any.whl differ diff --git a/packages/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/packages/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl new file mode 100644 index 0000000..5005eaf Binary files /dev/null and b/packages/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ diff --git a/packages/pydantic_settings-2.9.1-py3-none-any.whl b/packages/pydantic_settings-2.9.1-py3-none-any.whl new file mode 100644 index 0000000..7ef68c1 Binary files /dev/null and b/packages/pydantic_settings-2.9.1-py3-none-any.whl differ diff --git a/packages/pymysql-1.1.2-py3-none-any.whl b/packages/pymysql-1.1.2-py3-none-any.whl new file mode 100644 index 0000000..0b96b36 Binary files /dev/null and b/packages/pymysql-1.1.2-py3-none-any.whl differ diff --git a/packages/python_dotenv-1.1.1-py3-none-any.whl b/packages/python_dotenv-1.1.1-py3-none-any.whl new file mode 100644 index 0000000..1931b3b Binary files /dev/null and b/packages/python_dotenv-1.1.1-py3-none-any.whl differ diff --git a/packages/sniffio-1.3.1-py3-none-any.whl b/packages/sniffio-1.3.1-py3-none-any.whl new file mode 100644 index 0000000..04f44e4 Binary files /dev/null and b/packages/sniffio-1.3.1-py3-none-any.whl differ diff --git a/packages/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/packages/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl new file mode 100644 index 0000000..b341efe Binary files /dev/null and b/packages/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ diff --git a/packages/starlette-0.46.2-py3-none-any.whl b/packages/starlette-0.46.2-py3-none-any.whl new file mode 100644 index 0000000..18d20fc Binary files /dev/null and b/packages/starlette-0.46.2-py3-none-any.whl differ diff --git a/packages/typing_extensions-4.15.0-py3-none-any.whl b/packages/typing_extensions-4.15.0-py3-none-any.whl new file mode 100644 index 0000000..5fec9ca Binary files /dev/null and b/packages/typing_extensions-4.15.0-py3-none-any.whl differ diff --git a/packages/typing_inspection-0.4.1-py3-none-any.whl b/packages/typing_inspection-0.4.1-py3-none-any.whl new file mode 100644 index 0000000..ff958c3 Binary files /dev/null and b/packages/typing_inspection-0.4.1-py3-none-any.whl differ diff --git a/packages/uvicorn-0.23.2-py3-none-any.whl b/packages/uvicorn-0.23.2-py3-none-any.whl new file mode 100644 index 0000000..1c0cd7d Binary files /dev/null and b/packages/uvicorn-0.23.2-py3-none-any.whl differ diff --git a/packages/win32_setctime-1.2.0-py3-none-any.whl b/packages/win32_setctime-1.2.0-py3-none-any.whl new file mode 100644 index 0000000..7e21ea6 Binary files /dev/null and b/packages/win32_setctime-1.2.0-py3-none-any.whl differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9fce4c8 --- /dev/null +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/schemas/__pycache__/blog.cpython-312.pyc b/schemas/__pycache__/blog.cpython-312.pyc new file mode 100644 index 0000000..52df2c2 Binary files /dev/null and b/schemas/__pycache__/blog.cpython-312.pyc differ diff --git a/schemas/__pycache__/paginate_query.cpython-312.pyc b/schemas/__pycache__/paginate_query.cpython-312.pyc new file mode 100644 index 0000000..717c1b8 Binary files /dev/null and b/schemas/__pycache__/paginate_query.cpython-312.pyc differ diff --git a/schemas/__pycache__/pagination.cpython-312.pyc b/schemas/__pycache__/pagination.cpython-312.pyc new file mode 100644 index 0000000..f28e0ba Binary files /dev/null and b/schemas/__pycache__/pagination.cpython-312.pyc differ diff --git a/schemas/blog.py b/schemas/blog.py new file mode 100644 index 0000000..477e705 --- /dev/null +++ b/schemas/blog.py @@ -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') + } diff --git a/schemas/paginate_query.py b/schemas/paginate_query.py new file mode 100644 index 0000000..feadb7b --- /dev/null +++ b/schemas/paginate_query.py @@ -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 + ) diff --git a/schemas/pagination.py b/schemas/pagination.py new file mode 100644 index 0000000..5ebda67 --- /dev/null +++ b/schemas/pagination.py @@ -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 \ No newline at end of file diff --git a/service/__pycache__/blog_service.cpython-312.pyc b/service/__pycache__/blog_service.cpython-312.pyc new file mode 100644 index 0000000..138ae05 Binary files /dev/null and b/service/__pycache__/blog_service.cpython-312.pyc differ diff --git a/service/blog_service.py b/service/blog_service.py new file mode 100644 index 0000000..c974d1b --- /dev/null +++ b/service/blog_service.py @@ -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 diff --git a/sys.stdout b/sys.stdout new file mode 100644 index 0000000..bc5fe0b --- /dev/null +++ b/sys.stdout @@ -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'} diff --git a/test_main.http b/test_main.http new file mode 100644 index 0000000..a2d81a9 --- /dev/null +++ b/test_main.http @@ -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 + +### diff --git a/utils/__pycache__/blog_utils.cpython-312.pyc b/utils/__pycache__/blog_utils.cpython-312.pyc new file mode 100644 index 0000000..1b92325 Binary files /dev/null and b/utils/__pycache__/blog_utils.cpython-312.pyc differ diff --git a/utils/__pycache__/common.cpython-312.pyc b/utils/__pycache__/common.cpython-312.pyc new file mode 100644 index 0000000..10cd946 Binary files /dev/null and b/utils/__pycache__/common.cpython-312.pyc differ diff --git a/utils/blog_utils.py b/utils/blog_utils.py new file mode 100644 index 0000000..cc2aca7 --- /dev/null +++ b/utils/blog_utils.py @@ -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 + ) diff --git a/utils/common.py b/utils/common.py new file mode 100644 index 0000000..bb599fe --- /dev/null +++ b/utils/common.py @@ -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)