chore: initialize tuhui repository
This commit is contained in:
1
backend/app/api/__init__.py
Normal file
1
backend/app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API 路由"""
|
||||
BIN
backend/app/api/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
backend/app/api/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
backend/app/api/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/api/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/auth.cpython-310.pyc
Normal file
BIN
backend/app/api/__pycache__/auth.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/auth.cpython-311.pyc
Normal file
BIN
backend/app/api/__pycache__/auth.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/auth.cpython-312.pyc
Normal file
BIN
backend/app/api/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/orders.cpython-310.pyc
Normal file
BIN
backend/app/api/__pycache__/orders.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/orders.cpython-311.pyc
Normal file
BIN
backend/app/api/__pycache__/orders.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/orders.cpython-312.pyc
Normal file
BIN
backend/app/api/__pycache__/orders.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/payment.cpython-310.pyc
Normal file
BIN
backend/app/api/__pycache__/payment.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/payment.cpython-311.pyc
Normal file
BIN
backend/app/api/__pycache__/payment.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/payment.cpython-312.pyc
Normal file
BIN
backend/app/api/__pycache__/payment.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/upload.cpython-311.pyc
Normal file
BIN
backend/app/api/__pycache__/upload.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/works.cpython-310.pyc
Normal file
BIN
backend/app/api/__pycache__/works.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/works.cpython-311.pyc
Normal file
BIN
backend/app/api/__pycache__/works.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/works.cpython-312.pyc
Normal file
BIN
backend/app/api/__pycache__/works.cpython-312.pyc
Normal file
Binary file not shown.
79
backend/app/api/auth.py
Normal file
79
backend/app/api/auth.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_password_hash, verify_password, create_access_token
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserRegister, UserLogin, Token, UserResponse
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["认证"])
|
||||
|
||||
@router.post("/register", response_model=Token, summary="用户注册")
|
||||
def register(user_data: UserRegister, db: Session = Depends(get_db)):
|
||||
"""用户注册(使用手机号,无需验证码)"""
|
||||
try:
|
||||
# 检查手机号是否已存在
|
||||
existing_user = db.query(User).filter(User.phone == user_data.phone).first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="该手机号已被注册"
|
||||
)
|
||||
|
||||
# 创建新用户
|
||||
hashed_password = get_password_hash(user_data.password)
|
||||
new_user = User(
|
||||
phone=user_data.phone,
|
||||
password_hash=hashed_password,
|
||||
nickname=user_data.nickname or f"用户{user_data.phone[-4:]}",
|
||||
balance=0.0
|
||||
)
|
||||
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
|
||||
# 生成 Token
|
||||
access_token = create_access_token(data={"sub": str(new_user.id)})
|
||||
|
||||
return Token(
|
||||
access_token=access_token,
|
||||
user=UserResponse.from_orm(new_user)
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 注册失败: {e}")
|
||||
print(f"[ERROR] 错误类型: {type(e).__name__}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"注册失败: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/login", response_model=Token, summary="用户登录")
|
||||
def login(credentials: UserLogin, db: Session = Depends(get_db)):
|
||||
"""用户登录(使用手机号)"""
|
||||
# 查找用户
|
||||
user = db.query(User).filter(User.phone == credentials.phone).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="手机号或密码错误"
|
||||
)
|
||||
|
||||
# 验证密码
|
||||
if not verify_password(credentials.password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="手机号或密码错误"
|
||||
)
|
||||
|
||||
# 生成 Token
|
||||
access_token = create_access_token(data={"sub": str(user.id)})
|
||||
|
||||
return Token(
|
||||
access_token=access_token,
|
||||
user=UserResponse.from_orm(user)
|
||||
)
|
||||
171
backend/app/api/orders.py
Normal file
171
backend/app/api/orders.py
Normal file
@@ -0,0 +1,171 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Header
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, timedelta
|
||||
import secrets
|
||||
from app.core.database import get_db
|
||||
from app.core.security import decode_access_token
|
||||
from app.models.user import User
|
||||
from app.models.work import Work
|
||||
from app.models.order import Order, OrderStatus
|
||||
from app.models.download import DownloadRecord
|
||||
from app.schemas.order import OrderCreate, OrderResponse, PaymentResponse
|
||||
|
||||
router = APIRouter(prefix="/orders", tags=["订单"])
|
||||
|
||||
def get_current_user(authorization: str = Header(None), db: Session = Depends(get_db)):
|
||||
"""获取当前登录用户"""
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="未登录"
|
||||
)
|
||||
|
||||
token = authorization.replace("Bearer ", "")
|
||||
payload = decode_access_token(token)
|
||||
if not payload:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token 无效或已过期"
|
||||
)
|
||||
|
||||
user_id = int(payload.get("sub"))
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户不存在"
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
@router.post("/create", response_model=OrderResponse, summary="创建订单")
|
||||
def create_order(
|
||||
order_data: OrderCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""创建下载订单"""
|
||||
# 查找作品
|
||||
work = db.query(Work).filter(Work.id == order_data.work_id).first()
|
||||
if not work:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="作品不存在"
|
||||
)
|
||||
|
||||
# 检查是否已购买
|
||||
existing_order = db.query(Order).filter(
|
||||
Order.user_id == current_user.id,
|
||||
Order.work_id == work.id,
|
||||
Order.status == OrderStatus.PAID
|
||||
).first()
|
||||
|
||||
if existing_order:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
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}"
|
||||
|
||||
# 创建订单
|
||||
new_order = Order(
|
||||
order_no=order_no,
|
||||
user_id=current_user.id,
|
||||
work_id=work.id,
|
||||
amount=work.price,
|
||||
payment_method=order_data.payment_method,
|
||||
status=OrderStatus.PENDING
|
||||
)
|
||||
|
||||
db.add(new_order)
|
||||
db.commit()
|
||||
db.refresh(new_order)
|
||||
|
||||
return new_order
|
||||
|
||||
@router.post("/pay/{order_id}", response_model=PaymentResponse, summary="支付订单")
|
||||
def pay_order(
|
||||
order_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
支付订单(模拟支付)
|
||||
实际生产环境需要对接真实支付接口
|
||||
"""
|
||||
# 查找订单
|
||||
order = db.query(Order).filter(
|
||||
Order.id == order_id,
|
||||
Order.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not order:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="订单不存在"
|
||||
)
|
||||
|
||||
if order.status != OrderStatus.PENDING:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="订单状态异常"
|
||||
)
|
||||
|
||||
# 余额支付
|
||||
if order.payment_method == "balance":
|
||||
if current_user.balance < order.amount:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="余额不足"
|
||||
)
|
||||
|
||||
# 扣除余额
|
||||
current_user.balance -= order.amount
|
||||
|
||||
# 更新订单状态
|
||||
order.status = OrderStatus.PAID
|
||||
order.paid_at = datetime.now()
|
||||
|
||||
# 创建下载记录
|
||||
download_record = DownloadRecord(
|
||||
user_id=current_user.id,
|
||||
work_id=order.work_id,
|
||||
order_id=order.id
|
||||
)
|
||||
db.add(download_record)
|
||||
|
||||
# 增加作品下载量
|
||||
work = db.query(Work).filter(Work.id == order.work_id).first()
|
||||
work.downloads += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return PaymentResponse(
|
||||
success=True,
|
||||
message="支付成功",
|
||||
order_no=order.order_no,
|
||||
download_url=f"/api/works/{work.id}/download"
|
||||
)
|
||||
|
||||
# 其他支付方式(支付宝、微信)
|
||||
else:
|
||||
return PaymentResponse(
|
||||
success=False,
|
||||
message="暂不支持该支付方式,请使用余额支付或联系管理员",
|
||||
order_no=order.order_no
|
||||
)
|
||||
|
||||
@router.get("/my", summary="我的订单")
|
||||
def get_my_orders(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取当前用户的订单列表"""
|
||||
orders = db.query(Order).filter(Order.user_id == current_user.id).all()
|
||||
return orders
|
||||
267
backend/app/api/payment.py
Normal file
267
backend/app/api/payment.py
Normal file
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""支付相关接口"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.config import settings
|
||||
from app.models.order import Order, OrderStatus
|
||||
from app.models.work import Work
|
||||
from app.models.download import DownloadRecord
|
||||
from app.ysm_sdk import create_payment, query_order, PaymentNotify
|
||||
from app.api.orders import get_current_user
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/payment", tags=["支付"])
|
||||
|
||||
|
||||
@router.post("/create/{order_id}")
|
||||
async def create_payment_url(
|
||||
order_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
创建支付链接(真实支付)
|
||||
|
||||
Args:
|
||||
order_id: 订单ID
|
||||
|
||||
Returns:
|
||||
{
|
||||
"pay_url": "支付链接",
|
||||
"order_number": "订单号",
|
||||
"amount": 金额(分)
|
||||
}
|
||||
"""
|
||||
# 查询订单
|
||||
order = db.query(Order).filter(
|
||||
and_(
|
||||
Order.id == order_id,
|
||||
Order.user_id == current_user.id
|
||||
)
|
||||
).first()
|
||||
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
|
||||
# 检查订单状态
|
||||
if order.status == OrderStatus.PAID:
|
||||
raise HTTPException(status_code=400, detail="订单已支付")
|
||||
|
||||
if order.status == OrderStatus.CANCELLED:
|
||||
raise HTTPException(status_code=400, detail="订单已取消")
|
||||
|
||||
# 查询作品信息
|
||||
work = db.query(Work).filter(Work.id == order.work_id).first()
|
||||
if not work:
|
||||
raise HTTPException(status_code=404, detail="作品不存在")
|
||||
|
||||
try:
|
||||
# 创建支付
|
||||
pay_url = await create_payment(
|
||||
appid=settings.YSM_APPID,
|
||||
appsecret=settings.YSM_APPSECRET,
|
||||
order_id=order.order_no, # 使用 order_no 字段
|
||||
description=f"{work.title} - 爱设计作品购买",
|
||||
amount=int(order.amount * 100), # 元转分
|
||||
notify_url=settings.YSM_NOTIFY_URL,
|
||||
nopay_url=settings.YSM_NOPAY_URL,
|
||||
callback_url=settings.YSM_CALLBACK_URL,
|
||||
pay_type=1 # 默认微信内支付
|
||||
)
|
||||
|
||||
return {
|
||||
"pay_url": pay_url,
|
||||
"order_number": order.order_no, # 使用 order_no 字段
|
||||
"amount": int(order.amount * 100),
|
||||
"description": f"{work.title} - 爱设计作品购买"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/notify")
|
||||
async def payment_notify(
|
||||
request: Request,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
支付回调接口(易收米异步通知)
|
||||
|
||||
当用户支付成功后,支付平台会异步请求此接口
|
||||
"""
|
||||
# 创建通知处理器
|
||||
notify_handler = PaymentNotify(
|
||||
appid=settings.YSM_APPID,
|
||||
appsecret=settings.YSM_APPSECRET
|
||||
)
|
||||
|
||||
# 获取请求数据
|
||||
try:
|
||||
request_data = await request.body()
|
||||
data = json.loads(request_data.decode('utf-8'))
|
||||
except Exception as e:
|
||||
return {"error": "数据格式错误"}
|
||||
|
||||
# 验证签名
|
||||
success, message = await notify_handler.process(data)
|
||||
|
||||
if not success:
|
||||
return {"error": message}
|
||||
|
||||
# 获取商户订单号
|
||||
mch_orderid = data.get('mch_orderid')
|
||||
|
||||
# 查询订单
|
||||
order = db.query(Order).filter(Order.order_no == mch_orderid).first()
|
||||
|
||||
if not order:
|
||||
return {"error": "订单不存在"}
|
||||
|
||||
# 防止重复处理
|
||||
if order.status == OrderStatus.PAID:
|
||||
return "success"
|
||||
|
||||
# 更新订单状态
|
||||
if data.get('state') == 'SUCCESS':
|
||||
try:
|
||||
# 更新订单
|
||||
order.status = OrderStatus.PAID
|
||||
order.payment_method = "ysm_wechat" # 易收米微信支付
|
||||
order.paid_at = datetime.now()
|
||||
|
||||
# 创建下载记录
|
||||
download_record = DownloadRecord(
|
||||
user_id=order.user_id,
|
||||
work_id=order.work_id,
|
||||
order_id=order.id
|
||||
)
|
||||
db.add(download_record)
|
||||
|
||||
# 更新作品下载次数
|
||||
work = db.query(Work).filter(Work.id == order.work_id).first()
|
||||
if work:
|
||||
work.downloads += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return "success"
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"处理支付回调失败: {str(e)}")
|
||||
return {"error": "处理失败"}
|
||||
|
||||
return {"error": "支付未完成"}
|
||||
|
||||
|
||||
@router.get("/query/{order_number}")
|
||||
async def query_order_status(
|
||||
order_number: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
查询订单支付状态
|
||||
|
||||
Args:
|
||||
order_number: 订单号
|
||||
|
||||
Returns:
|
||||
订单状态信息
|
||||
"""
|
||||
# 查询本地订单
|
||||
order = db.query(Order).filter(
|
||||
and_(
|
||||
Order.order_no == order_number,
|
||||
Order.user_id == current_user.id
|
||||
)
|
||||
).first()
|
||||
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
|
||||
try:
|
||||
# 查询易收米订单状态
|
||||
result = await query_order(
|
||||
appid=settings.YSM_APPID,
|
||||
mch_orderid=order_number
|
||||
)
|
||||
|
||||
if result:
|
||||
# 如果远程订单已支付,但本地订单未更新,则更新本地订单
|
||||
if result.get('state') == 'SUCCESS' and order.status != OrderStatus.PAID:
|
||||
order.status = OrderStatus.PAID
|
||||
order.payment_method = "ysm_wechat"
|
||||
order.paid_at = datetime.now()
|
||||
|
||||
# 创建下载记录
|
||||
download_record = DownloadRecord(
|
||||
user_id=order.user_id,
|
||||
work_id=order.work_id,
|
||||
order_id=order.id
|
||||
)
|
||||
db.add(download_record)
|
||||
|
||||
# 更新作品下载次数
|
||||
work = db.query(Work).filter(Work.id == order.work_id).first()
|
||||
if work:
|
||||
work.downloads += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"order_number": order_number,
|
||||
"local_status": order.status,
|
||||
"remote_status": result.get('state'),
|
||||
"amount": order.amount,
|
||||
"paid_at": order.paid_at.isoformat() if order.paid_at else None,
|
||||
"remote_info": result
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="查询订单失败")
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/cancel/{order_id}")
|
||||
async def cancel_order(
|
||||
order_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
取消订单
|
||||
|
||||
Args:
|
||||
order_id: 订单ID
|
||||
"""
|
||||
# 查询订单
|
||||
order = db.query(Order).filter(
|
||||
and_(
|
||||
Order.id == order_id,
|
||||
Order.user_id == current_user.id
|
||||
)
|
||||
).first()
|
||||
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
|
||||
# 只能取消待支付的订单
|
||||
if order.status != OrderStatus.PENDING:
|
||||
raise HTTPException(status_code=400, detail="订单状态不允许取消")
|
||||
|
||||
order.status = OrderStatus.CANCELLED
|
||||
db.commit()
|
||||
|
||||
return {"message": "订单已取消"}
|
||||
250
backend/app/api/upload.py
Normal file
250
backend/app/api/upload.py
Normal file
@@ -0,0 +1,250 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, Header
|
||||
from sqlalchemy.orm import Session
|
||||
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
|
||||
from app.core.security import decode_access_token
|
||||
|
||||
router = APIRouter(prefix="/upload", tags=["上传"])
|
||||
|
||||
# 上传配置
|
||||
UPLOAD_BASE_DIR = "/app/uploads"
|
||||
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
|
||||
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
|
||||
|
||||
# 图绘域名
|
||||
TUHUI_DOMAIN = "https://tuhui.cloud"
|
||||
|
||||
|
||||
def get_current_user(authorization: str = Header(None), db: Session = Depends(get_db)):
|
||||
"""获取当前登录用户"""
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="未登录,请先登录"
|
||||
)
|
||||
|
||||
token = authorization.replace("Bearer ", "")
|
||||
payload = decode_access_token(token)
|
||||
if not payload:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token 无效或已过期"
|
||||
)
|
||||
|
||||
user_id = int(payload.get("sub"))
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户不存在"
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def generate_thumbnail(image_path: str, thumb_path: str, size=(400, 400)):
|
||||
"""生成缩略图 - 修复透明 PNG 问题"""
|
||||
with Image.open(image_path) as img:
|
||||
# 修复 Bug #1: 透明 PNG 转 RGB 后再保存为 JPEG
|
||||
if img.mode in ('RGBA', 'LA', 'P'):
|
||||
background = Image.new('RGB', img.size, (255, 255, 255))
|
||||
if img.mode == 'P':
|
||||
img = img.convert('RGBA')
|
||||
if img.mode in ('RGBA', 'LA'):
|
||||
background.paste(img, mask=img.split()[-1])
|
||||
img = background
|
||||
elif img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
|
||||
img.thumbnail(size, Image.Resampling.LANCZOS)
|
||||
img.save(thumb_path, quality=85)
|
||||
|
||||
|
||||
def add_watermark(image_path: str, watermarked_path: str, watermark_text: str = "图绘"):
|
||||
"""添加水印 - 修复透明 PNG 问题"""
|
||||
with Image.open(image_path) as img:
|
||||
# 修复 Bug #1: 透明 PNG 转 RGB 后再保存为 JPEG
|
||||
if img.mode in ('RGBA', 'LA', 'P'):
|
||||
background = Image.new('RGB', img.size, (255, 255, 255))
|
||||
if img.mode == 'P':
|
||||
img = img.convert('RGBA')
|
||||
if img.mode in ('RGBA', 'LA'):
|
||||
background.paste(img, mask=img.split()[-1])
|
||||
img = background
|
||||
elif img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
|
||||
width, height = img.size
|
||||
from PIL import ImageDraw, ImageFont
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 36)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
text = watermark_text
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
x = width - text_width - 20
|
||||
y = height - text_height - 20
|
||||
|
||||
draw.text((x, y), text, fill=(255, 255, 255, 128), font=font)
|
||||
img.save(watermarked_path, quality=90)
|
||||
|
||||
|
||||
@router.post("", summary="上传作品")
|
||||
async def upload_work(
|
||||
file: UploadFile = File(..., description="作品图片文件"),
|
||||
title: str = Form(..., description="作品标题"),
|
||||
description: Optional[str] = Form(None, description="作品描述"),
|
||||
category: str = Form(..., description="作品分类"),
|
||||
tags: Optional[str] = Form(None, description="标签,逗号分隔"),
|
||||
price: float = Form(..., ge=0, description="作品价格"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
📤 上传作品 API
|
||||
|
||||
**需要登录**:是(Bearer Token)
|
||||
|
||||
**支持格式**:jpg, jpeg, png, gif, webp
|
||||
|
||||
**文件大小**:最大 50MB
|
||||
"""
|
||||
# 验证文件扩展名
|
||||
file_ext = os.path.splitext(file.filename)[1].lower()
|
||||
if file_ext not in ALLOWED_EXTENSIONS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"不支持的文件格式,仅支持:{', '.join(ALLOWED_EXTENSIONS)}"
|
||||
)
|
||||
|
||||
# 验证文件大小
|
||||
file.file.seek(0, 2)
|
||||
file_size = file.file.tell()
|
||||
file.file.seek(0)
|
||||
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"文件过大,最大支持 {MAX_FILE_SIZE // 1024 // 1024}MB"
|
||||
)
|
||||
|
||||
# 生成唯一文件名
|
||||
unique_id = str(uuid.uuid4())
|
||||
timestamp = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
# 创建上传目录
|
||||
upload_dir = os.path.join(UPLOAD_BASE_DIR, "original", timestamp)
|
||||
thumb_dir = os.path.join(UPLOAD_BASE_DIR, "thumbnail", timestamp)
|
||||
watermarked_dir = os.path.join(UPLOAD_BASE_DIR, "watermarked", timestamp)
|
||||
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
os.makedirs(thumb_dir, exist_ok=True)
|
||||
os.makedirs(watermarked_dir, exist_ok=True)
|
||||
|
||||
# 保存文件
|
||||
original_filename = f"{unique_id}{file_ext}"
|
||||
original_path = os.path.join(upload_dir, original_filename)
|
||||
thumb_filename = f"{unique_id}_thumb.jpg"
|
||||
thumb_path = os.path.join(thumb_dir, thumb_filename)
|
||||
watermarked_filename = f"{unique_id}_watermarked.jpg"
|
||||
watermarked_path = os.path.join(watermarked_dir, watermarked_filename)
|
||||
|
||||
try:
|
||||
# 保存原图
|
||||
with open(original_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# 生成缩略图
|
||||
generate_thumbnail(original_path, thumb_path)
|
||||
|
||||
# 生成水印图
|
||||
add_watermark(original_path, watermarked_path)
|
||||
|
||||
# 解析标签 - 修复 Bug #3: tags 转字符串
|
||||
if tags:
|
||||
tags_list = [tag.strip() for tag in tags.split(",")]
|
||||
tags_str = ",".join(tags_list) # 转成逗号分隔的字符串
|
||||
else:
|
||||
tags_str = None
|
||||
|
||||
# 创建数据库记录 - 修复所有字段问题
|
||||
work = Work(
|
||||
title=title,
|
||||
description=description or "",
|
||||
category=category,
|
||||
tags=tags_str, # 修复:使用字符串而不是列表
|
||||
price=price,
|
||||
designer=current_user.nickname or current_user.phone,
|
||||
original_image=f"/uploads/original/{timestamp}/{original_filename}",
|
||||
thumbnail_image=f"/uploads/thumbnail/{timestamp}/{thumb_filename}",
|
||||
watermarked_image=f"/uploads/watermarked/{timestamp}/{watermarked_filename}"
|
||||
)
|
||||
|
||||
db.add(work)
|
||||
db.commit()
|
||||
db.refresh(work)
|
||||
|
||||
# 构建完整的图片 URL
|
||||
image_url = f"{TUHUI_DOMAIN}{work.original_image}"
|
||||
thumbnail_url = f"{TUHUI_DOMAIN}{work.thumbnail_image}"
|
||||
watermarked_url = f"{TUHUI_DOMAIN}{work.watermarked_image}"
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "上传成功,等待审核",
|
||||
"work_id": work.id,
|
||||
"image_url": image_url,
|
||||
"thumbnail_url": thumbnail_url,
|
||||
"watermarked_url": watermarked_url
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# 清理已上传的文件
|
||||
for path in [original_path, thumb_path, watermarked_path]:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"上传失败:{str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/my", summary="我的上传")
|
||||
def get_my_uploads(
|
||||
page: int = Form(1, ge=1),
|
||||
page_size: int = Form(20, ge=1, le=100),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取当前用户的上传记录"""
|
||||
from sqlalchemy import desc
|
||||
|
||||
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()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": works
|
||||
}
|
||||
256
backend/app/api/upload.py.backup
Normal file
256
backend/app/api/upload.py.backup
Normal file
@@ -0,0 +1,256 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, Header
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
import os
|
||||
import uuid
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from PIL import Image
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.work import Work
|
||||
from app.models.user import User
|
||||
from app.core.security import decode_access_token
|
||||
|
||||
router = APIRouter(prefix="/upload", tags=["上传"])
|
||||
|
||||
# 上传配置
|
||||
UPLOAD_BASE_DIR = "/var/www/tuhui_uploads"
|
||||
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
|
||||
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
|
||||
|
||||
# 图绘域名
|
||||
TUHUI_DOMAIN = "https://tuhui.cloud"
|
||||
|
||||
|
||||
def get_current_user(authorization: str = Header(None), db: Session = Depends(get_db)):
|
||||
"""获取当前登录用户"""
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="未登录,请先登录"
|
||||
)
|
||||
|
||||
token = authorization.replace("Bearer ", "")
|
||||
payload = decode_access_token(token)
|
||||
if not payload:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token 无效或已过期"
|
||||
)
|
||||
|
||||
user_id = int(payload.get("sub"))
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户不存在"
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def generate_thumbnail(image_path: str, thumb_path: str, size=(400, 400)):
|
||||
"""生成缩略图"""
|
||||
with Image.open(image_path) as img:
|
||||
img.thumbnail(size, Image.Resampling.LANCZOS)
|
||||
img.save(thumb_path, quality=85)
|
||||
|
||||
|
||||
def add_watermark(image_path: str, watermarked_path: str, watermark_text: str = "图绘"):
|
||||
"""添加水印"""
|
||||
with Image.open(image_path) as img:
|
||||
width, height = img.size
|
||||
from PIL import ImageDraw, ImageFont
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 36)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
text = watermark_text
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
x = width - text_width - 20
|
||||
y = height - text_height - 20
|
||||
|
||||
draw.text((x, y), text, fill=(255, 255, 255, 128), font=font)
|
||||
img.save(watermarked_path, quality=90)
|
||||
|
||||
|
||||
@router.post("", summary="上传作品")
|
||||
async def upload_work(
|
||||
file: UploadFile = File(..., description="作品图片文件"),
|
||||
title: str = Form(..., description="作品标题"),
|
||||
description: Optional[str] = Form(None, description="作品描述"),
|
||||
category: str = Form(..., description="作品分类"),
|
||||
tags: Optional[str] = Form(None, description="标签,逗号分隔"),
|
||||
price: float = Form(..., ge=0, description="作品价格"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
📤 上传作品 API
|
||||
|
||||
**需要登录**:是(Bearer Token)
|
||||
|
||||
**支持格式**:jpg, jpeg, png, gif, webp
|
||||
|
||||
**文件大小**:最大 50MB
|
||||
|
||||
**处理流程**:
|
||||
1. 验证文件类型和大小
|
||||
2. 保存原图
|
||||
3. 自动生成缩略图(400x400)
|
||||
4. 自动生成水印图
|
||||
5. 创建数据库记录
|
||||
6. 返回 work_id 和 image_url
|
||||
"""
|
||||
# 验证文件扩展名
|
||||
file_ext = os.path.splitext(file.filename)[1].lower()
|
||||
if file_ext not in ALLOWED_EXTENSIONS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"不支持的文件格式,仅支持:{', '.join(ALLOWED_EXTENSIONS)}"
|
||||
)
|
||||
|
||||
# 验证文件大小
|
||||
file.file.seek(0, 2)
|
||||
file_size = file.file.tell()
|
||||
file.file.seek(0)
|
||||
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"文件过大,最大支持 {MAX_FILE_SIZE // 1024 // 1024}MB"
|
||||
)
|
||||
|
||||
# 生成唯一文件名
|
||||
unique_id = str(uuid.uuid4())
|
||||
timestamp = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
# 创建上传目录
|
||||
upload_dir = os.path.join(UPLOAD_BASE_DIR, "original", timestamp)
|
||||
thumb_dir = os.path.join(UPLOAD_BASE_DIR, "thumbnail", timestamp)
|
||||
watermarked_dir = os.path.join(UPLOAD_BASE_DIR, "watermarked", timestamp)
|
||||
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
os.makedirs(thumb_dir, exist_ok=True)
|
||||
os.makedirs(watermarked_dir, exist_ok=True)
|
||||
|
||||
# 保存文件
|
||||
original_filename = f"{unique_id}{file_ext}"
|
||||
original_path = os.path.join(upload_dir, original_filename)
|
||||
thumb_filename = f"{unique_id}_thumb.jpg"
|
||||
thumb_path = os.path.join(thumb_dir, thumb_filename)
|
||||
watermarked_filename = f"{unique_id}_watermarked.jpg"
|
||||
watermarked_path = os.path.join(watermarked_dir, watermarked_filename)
|
||||
|
||||
try:
|
||||
# 保存原图
|
||||
with open(original_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# 生成缩略图
|
||||
generate_thumbnail(original_path, thumb_path)
|
||||
|
||||
# 生成水印图
|
||||
add_watermark(original_path, watermarked_path)
|
||||
|
||||
# 解析标签
|
||||
tags_list = [tag.strip() for tag in tags.split(",")] if tags else []
|
||||
|
||||
# 获取图片尺寸
|
||||
with Image.open(original_path) as img:
|
||||
img_width, img_height = img.size
|
||||
|
||||
# 创建数据库记录
|
||||
work = Work(
|
||||
title=title,
|
||||
description=description or "",
|
||||
category=category,
|
||||
tags=tags_list,
|
||||
price=price,
|
||||
designer_id=current_user.id,
|
||||
original_image=f"/uploads/original/{timestamp}/{original_filename}",
|
||||
thumbnail_image=f"/uploads/thumbnail/{timestamp}/{thumb_filename}",
|
||||
watermarked_image=f"/uploads/watermarked/{timestamp}/{watermarked_filename}",
|
||||
width=img_width,
|
||||
height=img_height,
|
||||
file_size=file_size,
|
||||
status="pending" # pending, approved, rejected
|
||||
)
|
||||
|
||||
db.add(work)
|
||||
db.commit()
|
||||
db.refresh(work)
|
||||
|
||||
# 构建完整的图片 URL
|
||||
image_url = f"{TUHUI_DOMAIN}{work.original_image}"
|
||||
thumbnail_url = f"{TUHUI_DOMAIN}{work.thumbnail_image}"
|
||||
watermarked_url = f"{TUHUI_DOMAIN}{work.watermarked_image}"
|
||||
|
||||
# 返回结果(包含 work_id 和 image_url)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "上传成功,等待审核",
|
||||
"work_id": work.id,
|
||||
"image_url": image_url,
|
||||
"thumbnail_url": thumbnail_url,
|
||||
"watermarked_url": watermarked_url,
|
||||
"work": {
|
||||
"id": work.id,
|
||||
"title": work.title,
|
||||
"description": work.description,
|
||||
"category": work.category,
|
||||
"tags": work.tags,
|
||||
"price": work.price,
|
||||
"width": work.width,
|
||||
"height": work.height,
|
||||
"file_size": work.file_size,
|
||||
"status": work.status,
|
||||
"original_image": work.original_image,
|
||||
"thumbnail_image": work.thumbnail_image,
|
||||
"watermarked_image": work.watermarked_image,
|
||||
"created_at": work.created_at.isoformat() if work.created_at else None
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# 清理已上传的文件
|
||||
for path in [original_path, thumb_path, watermarked_path]:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"上传失败:{str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/my", summary="我的上传")
|
||||
def get_my_uploads(
|
||||
page: int = Form(1, ge=1),
|
||||
page_size: int = Form(20, ge=1, le=100),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取当前用户的上传记录"""
|
||||
from sqlalchemy import desc
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
works = db.query(Work).filter(
|
||||
Work.designer_id == current_user.id
|
||||
).order_by(desc(Work.created_at)).offset(offset).limit(page_size).all()
|
||||
|
||||
total = db.query(Work).filter(Work.designer_id == current_user.id).count()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": works
|
||||
}
|
||||
157
backend/app/api/works.py
Normal file
157
backend/app/api/works.py
Normal file
@@ -0,0 +1,157 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Header
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
from typing import List
|
||||
import os
|
||||
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.schemas.work import WorkResponse, WorkListResponse
|
||||
|
||||
router = APIRouter(prefix="/works", tags=["作品"])
|
||||
|
||||
@router.get("", response_model=WorkListResponse, summary="获取作品列表")
|
||||
def get_works(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
category: str = None,
|
||||
keyword: str = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
获取作品列表
|
||||
- page: 页码
|
||||
- page_size: 每页数量
|
||||
- category: 分类筛选
|
||||
- keyword: 关键词搜索
|
||||
"""
|
||||
query = db.query(Work)
|
||||
|
||||
# 分类筛选
|
||||
if category:
|
||||
query = query.filter(Work.category == category)
|
||||
|
||||
# 关键词搜索
|
||||
if keyword:
|
||||
query = query.filter(Work.title.contains(keyword))
|
||||
|
||||
# 获取总数
|
||||
total = query.count()
|
||||
|
||||
# 分页查询
|
||||
offset = (page - 1) * page_size
|
||||
works = query.order_by(desc(Work.created_at)).offset(offset).limit(page_size).all()
|
||||
|
||||
# 修复:将 level_text 为 NULL 的转为空字符串
|
||||
for work in works:
|
||||
if work.level_text is None:
|
||||
work.level_text = ""
|
||||
|
||||
return WorkListResponse(
|
||||
total=total,
|
||||
items=works
|
||||
)
|
||||
|
||||
@router.get("/{work_id}", response_model=WorkResponse, summary="获取作品详情")
|
||||
def get_work(work_id: int, db: Session = Depends(get_db)):
|
||||
"""获取作品详情"""
|
||||
work = db.query(Work).filter(Work.id == work_id).first()
|
||||
if not work:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="作品不存在"
|
||||
)
|
||||
|
||||
# 增加浏览量
|
||||
work.views += 1
|
||||
db.commit()
|
||||
|
||||
# 修复:将 level_text 为 NULL 的转为空字符串
|
||||
if work.level_text is None:
|
||||
work.level_text = ""
|
||||
|
||||
return work
|
||||
|
||||
|
||||
def get_current_user(authorization: str = Header(None), db: Session = Depends(get_db)):
|
||||
"""获取当前登录用户(用于下载验证)"""
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="未登录,请先登录"
|
||||
)
|
||||
|
||||
token = authorization.replace("Bearer ", "")
|
||||
payload = decode_access_token(token)
|
||||
if not payload:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token 无效或已过期"
|
||||
)
|
||||
|
||||
user_id = int(payload.get("sub"))
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户不存在"
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/{work_id}/download", summary="下载作品原图")
|
||||
def download_work(
|
||||
work_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
下载作品原图
|
||||
- 需要登录
|
||||
- 必须已支付订单
|
||||
- 支付成功后才能下载
|
||||
"""
|
||||
# 查找作品
|
||||
work = db.query(Work).filter(Work.id == work_id).first()
|
||||
if not work:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="作品不存在"
|
||||
)
|
||||
|
||||
# 检查是否已购买(查询已支付的订单)
|
||||
paid_order = db.query(Order).filter(
|
||||
Order.user_id == current_user.id,
|
||||
Order.work_id == work_id,
|
||||
Order.status == OrderStatus.PAID
|
||||
).first()
|
||||
|
||||
if not paid_order:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="您还未购买此作品,请先完成支付"
|
||||
)
|
||||
|
||||
# 构建原图文件路径
|
||||
# 假设原图存储在 uploads/original/ 目录下
|
||||
file_path = work.original_image.lstrip('/')
|
||||
full_path = os.path.join(os.getcwd(), file_path)
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(full_path):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文件不存在"
|
||||
)
|
||||
|
||||
# 返回文件
|
||||
filename = os.path.basename(full_path)
|
||||
return FileResponse(
|
||||
path=full_path,
|
||||
filename=filename,
|
||||
media_type='application/octet-stream'
|
||||
)
|
||||
Reference in New Issue
Block a user