fix: harden uploads downloads and deployment config
This commit is contained in:
@@ -12,6 +12,26 @@ from app.schemas.order import OrderCreate, OrderResponse, PaymentResponse
|
||||
|
||||
router = APIRouter(prefix="/orders", tags=["订单"])
|
||||
|
||||
|
||||
def _generate_order_no(db: Session) -> str:
|
||||
"""生成带随机后缀的唯一订单号,避免同秒冲突。"""
|
||||
now = datetime.now()
|
||||
six_months_ago = now - timedelta(days=180)
|
||||
date_part = six_months_ago.strftime('%Y%m%d')
|
||||
time_part = now.strftime('%H%M%S')
|
||||
|
||||
for _ in range(5):
|
||||
suffix = secrets.token_hex(3).upper()
|
||||
order_no = f"ORD{date_part}{time_part}{suffix}"
|
||||
exists = db.query(Order.id).filter(Order.order_no == order_no).first()
|
||||
if not exists:
|
||||
return order_no
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="生成订单号失败,请稍后重试",
|
||||
)
|
||||
|
||||
def get_current_user(authorization: str = Header(None), db: Session = Depends(get_db)):
|
||||
"""获取当前登录用户"""
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
@@ -66,12 +86,8 @@ def create_order(
|
||||
detail="您已购买过此作品"
|
||||
)
|
||||
|
||||
# 生成订单号:前缀 + (当前时间-6个月)的年月日 + 当前时间的时分秒
|
||||
now = datetime.now()
|
||||
six_months_ago = now - timedelta(days=180) # 半年前
|
||||
date_part = six_months_ago.strftime('%Y%m%d') # 半年前的年月日
|
||||
time_part = now.strftime('%H%M%S') # 当前时间的时分秒
|
||||
order_no = f"ORD{date_part}{time_part}"
|
||||
# 生成唯一订单号:半年前日期 + 当前时分秒 + 随机后缀
|
||||
order_no = _generate_order_no(db)
|
||||
|
||||
# 创建订单
|
||||
new_order = Order(
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, Header
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, Header, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc, or_
|
||||
from typing import Optional
|
||||
import os
|
||||
import uuid
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from PIL import Image
|
||||
import json
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.work import Work
|
||||
from app.models.user import User
|
||||
@@ -51,6 +50,15 @@ def get_current_user(authorization: str = Header(None), db: Session = Depends(ge
|
||||
return user
|
||||
|
||||
|
||||
def _user_designer_aliases(user: User) -> list[str]:
|
||||
aliases = []
|
||||
for value in (getattr(user, "nickname", None), getattr(user, "phone", None)):
|
||||
cleaned = str(value or "").strip()
|
||||
if cleaned and cleaned not in aliases:
|
||||
aliases.append(cleaned)
|
||||
return aliases
|
||||
|
||||
|
||||
def generate_thumbnail(image_path: str, thumb_path: str, size=(400, 400)):
|
||||
"""生成缩略图 - 修复透明 PNG 问题"""
|
||||
with Image.open(image_path) as img:
|
||||
@@ -177,7 +185,7 @@ async def upload_work(
|
||||
|
||||
# 解析标签 - 修复 Bug #3: tags 转字符串
|
||||
if tags:
|
||||
tags_list = [tag.strip() for tag in tags.split(",")]
|
||||
tags_list = [tag.strip() for tag in tags.split(",") if tag.strip()]
|
||||
tags_str = ",".join(tags_list) # 转成逗号分隔的字符串
|
||||
else:
|
||||
tags_str = None
|
||||
@@ -227,20 +235,22 @@ async def upload_work(
|
||||
|
||||
@router.get("/my", summary="我的上传")
|
||||
def get_my_uploads(
|
||||
page: int = Form(1, ge=1),
|
||||
page_size: int = Form(20, ge=1, le=100),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取当前用户的上传记录"""
|
||||
from sqlalchemy import desc
|
||||
|
||||
aliases = _user_designer_aliases(current_user)
|
||||
offset = (page - 1) * page_size
|
||||
works = db.query(Work).filter(
|
||||
Work.designer == current_user.phone
|
||||
).order_by(desc(Work.created_at)).offset(offset).limit(page_size).all()
|
||||
|
||||
total = db.query(Work).filter(Work.designer == current_user.phone).count()
|
||||
query = db.query(Work)
|
||||
if aliases:
|
||||
query = query.filter(or_(*[Work.designer == alias for alias in aliases]))
|
||||
else:
|
||||
query = query.filter(Work.id == -1)
|
||||
|
||||
works = query.order_by(desc(Work.created_at)).offset(offset).limit(page_size).all()
|
||||
total = query.count()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
|
||||
@@ -4,11 +4,13 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
from typing import List
|
||||
import os
|
||||
import mimetypes
|
||||
from app.core.database import get_db
|
||||
from app.models.work import Work
|
||||
from app.models.order import Order, OrderStatus
|
||||
from app.models.user import User
|
||||
from app.core.security import decode_access_token
|
||||
from app.core.config import settings
|
||||
from app.schemas.work import WorkResponse, WorkListResponse
|
||||
|
||||
router = APIRouter(prefix="/works", tags=["作品"])
|
||||
@@ -137,9 +139,10 @@ def download_work(
|
||||
)
|
||||
|
||||
# 构建原图文件路径
|
||||
# 假设原图存储在 uploads/original/ 目录下
|
||||
file_path = work.original_image.lstrip('/')
|
||||
full_path = os.path.join(os.getcwd(), file_path)
|
||||
relative_path = work.original_image.lstrip("/")
|
||||
if relative_path.startswith("uploads/"):
|
||||
relative_path = relative_path[len("uploads/"):]
|
||||
full_path = os.path.join(settings.UPLOAD_DIR, relative_path)
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(full_path):
|
||||
@@ -150,8 +153,9 @@ def download_work(
|
||||
|
||||
# 返回文件
|
||||
filename = os.path.basename(full_path)
|
||||
media_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
||||
return FileResponse(
|
||||
path=full_path,
|
||||
filename=filename,
|
||||
media_type='application/octet-stream'
|
||||
media_type=media_type
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import List
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# 项目信息
|
||||
PROJECT_NAME: str = "爱设计 API"
|
||||
PROJECT_NAME: str = "图绘 API"
|
||||
VERSION: str = "1.0.0"
|
||||
API_PREFIX: str = "/api"
|
||||
|
||||
@@ -12,7 +12,7 @@ class Settings(BaseSettings):
|
||||
# DATABASE_URL: str = "mysql+pymysql://root:password@localhost:3306/aishej" # MySQL配置
|
||||
|
||||
# JWT 配置
|
||||
SECRET_KEY: str = "your-secret-key-change-this"
|
||||
SECRET_KEY: str = "change-me-in-env"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 # 24小时
|
||||
|
||||
@@ -21,7 +21,7 @@ class Settings(BaseSettings):
|
||||
MAX_FILE_SIZE: int = 52428800 # 50MB
|
||||
|
||||
# 水印配置
|
||||
WATERMARK_TEXT: str = "爱设计 AiSheji.com"
|
||||
WATERMARK_TEXT: str = "图绘 Tuhui.cloud"
|
||||
WATERMARK_OPACITY: int = 128
|
||||
|
||||
# 缩略图配置
|
||||
@@ -30,19 +30,18 @@ class Settings(BaseSettings):
|
||||
|
||||
# CORS 配置(生产环境配置为具体域名)
|
||||
ALLOWED_ORIGINS: List[str] = [
|
||||
"https://backend.huijie168.uk",
|
||||
"https://app.huijie168.uk",
|
||||
"https://run.huijie168.uk",
|
||||
"https://huijie168.uk",
|
||||
"https://www.huijie168.uk"
|
||||
"https://tuhui.cloud",
|
||||
"https://www.tuhui.cloud",
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173",
|
||||
]
|
||||
|
||||
# 易收米支付配置
|
||||
YSM_APPID: str = "YSMcd16b45d"
|
||||
YSM_APPSECRET: str = "899850e778e8d2b53e4c4a4e88695688"
|
||||
YSM_NOTIFY_URL: str = "https://backend.huijie168.uk/api/payment/notify" # 支付回调地址
|
||||
YSM_CALLBACK_URL: str = "https://run.huijie168.uk/payment/success" # 支付成功跳转地址
|
||||
YSM_NOPAY_URL: str = "https://run.huijie168.uk/payment/cancel" # 未支付跳转地址
|
||||
YSM_APPID: str = ""
|
||||
YSM_APPSECRET: str = ""
|
||||
YSM_NOTIFY_URL: str = "https://tuhui.cloud/api/payment/notify" # 支付回调地址
|
||||
YSM_CALLBACK_URL: str = "https://tuhui.cloud/payment/success" # 支付成功跳转地址
|
||||
YSM_NOPAY_URL: str = "https://tuhui.cloud/payment/cancel" # 未支付跳转地址
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
Reference in New Issue
Block a user