diff --git a/api/blog.py b/api/blog.py index 4e038dd..79ab3b6 100644 --- a/api/blog.py +++ b/api/blog.py @@ -1,6 +1,6 @@ from typing import List -from fastapi import APIRouter, Depends, Query, Request +from fastapi import APIRouter, Depends, Query, Request, UploadFile, File from sqlalchemy.orm import Session from config.auth import verify_token @@ -10,7 +10,7 @@ from schemas.blog import BlogResponse, BlogQuery, BlogCategoryResponse, BlogStat from schemas.blog_elastic import BlogSearch from schemas.pagination import PageResult from config.database import get_db -from service import blog_service, blog_elastic_service +from service import blog_service, blog_elastic_service, file_service router = APIRouter( prefix="/blog", @@ -19,6 +19,11 @@ router = APIRouter( ) +@router.post("/file/upload", summary="上传博客图片", response_model=str) +async def sync_all_blog(md5: str, file: UploadFile = File(...)): + return await file_service.upload_file(md5, file) + + @router.get("/sync", summary="同步博客到elastic", response_model=bool) def sync_all_blog(db: Session = Depends(get_db)): return blog_elastic_service.sync_all_blog(db) @@ -46,12 +51,14 @@ def query_blog_by_condition(query: BlogQuery = Depends(), 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("/all/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), _=Depends(verify_token)): return blog_service.query_blog_by_page(db, current_page, page_size, True) + @router.post("", summary="新增博客内容", response_model=bool) def add_blog(blog: BlogCreate, db: Session = Depends(get_db), _=Depends(verify_token)): return blog_service.add_blog(db, blog) diff --git a/config/rustfs.py b/config/rustfs.py new file mode 100644 index 0000000..c52d879 --- /dev/null +++ b/config/rustfs.py @@ -0,0 +1,15 @@ +import boto3 +from botocore.client import Config + +from config.setting import settings + +access_key = 'jRxroVX8PUuSOia71qE4' +secret_access = 'RZE82VATN1Gqj9xy3d5OzFvHoDBgwJ4PSf7IKuei' + +s3 = boto3.client('s3', + endpoint_url=f'http://{settings.RUSTFS_HOST}:{settings.RUSTFS_PORT}', + aws_access_key_id=access_key, + aws_secret_access_key=secret_access, + config=Config(signature_version='s3v4'), + region_name='cn-east-1' + ) diff --git a/config/setting.py b/config/setting.py index 52454cc..f212ec9 100644 --- a/config/setting.py +++ b/config/setting.py @@ -8,6 +8,8 @@ class Settings(BaseSettings): DB_PASSWORD: str ES_HOST: str FLUENTD_HOST: str + RUSTFS_HOST:str + RUSTFS_PORT: str class Config: env_file = ".env" # 指定.env文件路径 diff --git a/requirements.txt b/requirements.txt index ef4cc04..e422cb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -fastapi~=0.115.12 sqlalchemy~=2.0.41 pydantic~=2.11.4 PyJWT~=2.10.1 passlib~=1.7.4 loguru~=0.7.3 pydantic-settings~=2.9.1 pymysql~=1.1.1 python-multipart~=0.0.20 uvicorn~=0.23.0 elasticsearch~=8.12.0 fluent-logger~=0.10.0 # pip download -r requirements.txt -d ./packages --only-binary=:all: --platform manylinux2014_x86_64 -i https://pypi.tuna.tsinghua.edu.cn/simple \ No newline at end of file +fastapi~=0.115.12 sqlalchemy~=2.0.41 pydantic~=2.11.4 PyJWT~=2.10.1 passlib~=1.7.4 loguru~=0.7.3 pydantic-settings~=2.9.1 pymysql~=1.1.1 python-multipart~=0.0.20 uvicorn~=0.23.0 elasticsearch~=8.12.0 fluent-logger~=0.10.0 boto3~=1.40.59 botocore~=1.40.59 # pip download -r requirements.txt -d ./packages --only-binary=:all: --platform manylinux2014_x86_64 -i https://pypi.tuna.tsinghua.edu.cn/simple \ No newline at end of file diff --git a/schemas/blog.py b/schemas/blog.py index 0c93716..8501981 100644 --- a/schemas/blog.py +++ b/schemas/blog.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel from datetime import datetime diff --git a/service/blog_elastic_service.py b/service/blog_elastic_service.py index 3858c25..53edc62 100644 --- a/service/blog_elastic_service.py +++ b/service/blog_elastic_service.py @@ -17,11 +17,12 @@ def sync_all_blog(db: Session) -> bool: if settings.ENVIRONMENT == 'dev': return True - query = select( + stmt = select( Blog.id, Blog.title, BlogCategory.name.label("category"), BlogContent.content.label("content"), + Blog.is_approved.label("isApproved"), Blog.create_time.label("createTime"), Blog.update_time.label("updateTime"), ).select_from(Blog).outerjoin( @@ -29,7 +30,7 @@ def sync_all_blog(db: Session) -> bool: ).outerjoin( BlogContent, Blog.content_id == BlogContent.id ) - results = db.execute(query).fetchall() + results = db.execute(stmt).fetchall() actions = [ { @@ -40,6 +41,7 @@ def sync_all_blog(db: Session) -> bool: "title": blog.title, "category": blog.category, "content": blog.content.decode("utf-8"), + "isApproved": blog.isApproved, "createTime": blog.createTime.strftime("%Y-%m-%d %H:%M:%S"), "updateTime": blog.updateTime.strftime("%Y-%m-%d %H:%M:%S") } @@ -106,10 +108,23 @@ def search_blog(keyword: str) -> List[BlogSearch]: # 构造查询请求 body = { "query": { - "multi_match": { - "query": keyword, - "fields": ["title^3", "content"], - "type": "phrase" + "bool": { + "must": [ + { + "multi_match": { + "query": keyword, + "fields": ["title^3", "content"], + "type": "phrase" + } + } + ], + "filter": [ + { + "term": { + "isApproved": 1 + } + } + ] } }, "highlight": { diff --git a/service/file_service.py b/service/file_service.py new file mode 100644 index 0000000..9bb5d62 --- /dev/null +++ b/service/file_service.py @@ -0,0 +1,40 @@ +from fastapi import UploadFile, File, HTTPException + +from config.rustfs import s3 +from config.setting import settings + +ALLOWED_IMAGE_TYPES = [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "image/svg+xml", +] + +BUCKET = 'blog' +NGINX_PROXY = 'rustfs' + + +async def upload_file(md5: str, file: UploadFile = File(...)) -> str: + # 校验文件类型是否是图片 + if file.content_type not in ALLOWED_IMAGE_TYPES: + raise HTTPException( + status_code=400, + detail="只允许上传图片文件 (JPEG, PNG, GIF, WEBP, SVG)" + ) + + file_ext = file.filename.split('.')[-1] + unique_filename = f"{md5}.{file_ext}" + + file_content = await file.read() + + # 上传到S3 + s3.put_object( + Bucket=BUCKET, + Key=unique_filename, + Body=file_content, + ContentType=file.content_type + ) + + # 返回文件url + return f"{NGINX_PROXY}/{BUCKET}/{unique_filename}"