diff --git a/app/api/endpoints/order.py b/app/api/endpoints/order.py index 2c0750d..42aca35 100644 --- a/app/api/endpoints/order.py +++ b/app/api/endpoints/order.py @@ -9,7 +9,10 @@ from app.models.order import ( OrderPackageInfo, generate_order_id, OrderPriceCalculateRequest, - OrderPriceInfo + OrderPriceInfo, + OrderStatus, + OrderCancel, + OrderComplete ) from app.models.database import get_db from app.api.deps import get_current_user @@ -181,4 +184,83 @@ async def calculate_order_price( final_amount=final_amount ) - return success_response(data=price_info) \ No newline at end of file + return success_response(data=price_info) + +@router.post("/{orderid}/cancel", response_model=ResponseModel) +async def cancel_order( + orderid: str, + cancel_data: OrderCancel, + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """取消订单""" + # 查询订单 + order = db.query(ShippingOrderDB).filter( + ShippingOrderDB.orderid == orderid, + ShippingOrderDB.userid == current_user.userid + ).first() + + if not order: + return error_response(code=404, message="订单不存在") + + # 检查订单状态是否可取消 + if order.status not in [OrderStatus.PENDING, OrderStatus.ACCEPTED]: + return error_response(code=400, message="当前订单状态不可取消") + + try: + # 更新订单状态和取消原因 + order.status = OrderStatus.CANCELLED + order.cancel_reason = cancel_data.reason + + # 如果使用了优惠券,返还优惠券 + if order.coupon_id: + coupon = db.query(UserCouponDB).filter( + UserCouponDB.id == order.coupon_id + ).first() + if coupon: + coupon.status = CouponStatus.UNUSED + + db.commit() + return success_response( + message="订单取消成功", + data=OrderInfo.model_validate(order) + ) + except Exception as e: + db.rollback() + return error_response(code=500, message=f"取消订单失败: {str(e)}") + +@router.post("/{orderid}/complete", response_model=ResponseModel) +async def complete_order( + orderid: str, + complete_data: OrderComplete, + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """完成订单""" + # 查询订单 + order = db.query(ShippingOrderDB).filter( + ShippingOrderDB.orderid == orderid, + ShippingOrderDB.userid == current_user.userid + ).first() + + if not order: + return error_response(code=404, message="订单不存在") + + # 检查订单状态 + if order.status != OrderStatus.ACCEPTED: + return error_response(code=400, message="只有已接单的订单才能标记为完成") + + try: + # 更新订单状态和完成图片 + order.status = OrderStatus.COMPLETED + if complete_data.images: + order.complete_images = ",".join(complete_data.images) + + db.commit() + return success_response( + message="订单已完成", + data=OrderInfo.model_validate(order) + ) + except Exception as e: + db.rollback() + return error_response(code=500, message=f"完成订单失败: {str(e)}") \ No newline at end of file diff --git a/app/api/endpoints/upload.py b/app/api/endpoints/upload.py new file mode 100644 index 0000000..09d429e --- /dev/null +++ b/app/api/endpoints/upload.py @@ -0,0 +1,64 @@ +from fastapi import APIRouter, UploadFile, File, Depends +from typing import List +import uuid +from datetime import datetime +from app.core.cos import cos_client +from app.core.config import settings +from app.models.upload import UploadResponse, MultiUploadResponse +from app.core.response import success_response, error_response, ResponseModel +from app.api.deps import get_current_user +from app.models.user import UserDB + +router = APIRouter() + +async def upload_to_cos(file: UploadFile) -> str: + """上传文件到腾讯云COS""" + # 生成唯一文件名 + ext = file.filename.split('.')[-1] if '.' in file.filename else '' + filename = f"{datetime.now().strftime('%Y%m%d')}/{uuid.uuid4()}.{ext}" + + # 上传文件 + cos_client.put_object( + Bucket=settings.COS_BUCKET, + Body=await file.read(), + Key=filename, + ContentType=file.content_type + ) + + # 返回文件URL + return f"{settings.COS_BASE_URL}/{filename}" + +@router.post("/image", response_model=ResponseModel) +async def upload_image( + file: UploadFile = File(...), + current_user: UserDB = Depends(get_current_user) +): + """上传单张图片""" + if not file.content_type.startswith('image/'): + return error_response(code=400, message="只能上传图片文件") + + try: + url = await upload_to_cos(file) + return success_response(data=UploadResponse(url=url)) + except Exception as e: + return error_response(code=500, message=f"上传失败: {str(e)}") + +@router.post("/images", response_model=ResponseModel) +async def upload_images( + files: List[UploadFile] = File(...), + current_user: UserDB = Depends(get_current_user) +): + """上传多张图片""" + if len(files) > 5: + return error_response(code=400, message="最多同时上传5张图片") + + urls = [] + try: + for file in files: + if not file.content_type.startswith('image/'): + return error_response(code=400, message="只能上传图片文件") + url = await upload_to_cos(file) + urls.append(url) + return success_response(data=MultiUploadResponse(urls=urls)) + except Exception as e: + return error_response(code=500, message=f"上传失败: {str(e)}") \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index e769c39..ec7e69a 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -37,6 +37,13 @@ class Settings(BaseSettings): def SQLALCHEMY_DATABASE_URL(self) -> str: return f"mysql+pymysql://{self.MYSQL_USER}:{self.MYSQL_PASSWORD}@{self.MYSQL_HOST}:{self.MYSQL_PORT}/{self.MYSQL_DB}?charset=utf8mb4" + # 腾讯云 COS 配置 + COS_SECRET_ID: str = "AKIDxnbGj281iHtKallqqzvlV5YxBCrPltnS" + COS_SECRET_KEY: str = "ta6PXTMBsX7dzA7IN6uYUFn8F9uTovoU" + COS_REGION: str = "ap-chengdu" + COS_BUCKET: str = "dman-1311994147" + COS_BASE_URL: str = "dman-1311994147.cos.ap-chengdu.myqcloud.com" + class Config: case_sensitive = True diff --git a/app/core/cos.py b/app/core/cos.py new file mode 100644 index 0000000..4ed104f --- /dev/null +++ b/app/core/cos.py @@ -0,0 +1,15 @@ +from qcloud_cos import CosConfig, CosS3Client +from app.core.config import settings +import sys +import logging + +# 正常情况日志级别使用INFO,需要定位时可以修改为DEBUG,此时SDK会打印和服务端的通信信息 +logging.basicConfig(level=logging.INFO, stream=sys.stdout) + +cos_config = CosConfig( + Region=settings.COS_REGION, + SecretId=settings.COS_SECRET_ID, + SecretKey=settings.COS_SECRET_KEY +) + +cos_client = CosS3Client(cos_config) \ No newline at end of file diff --git a/app/main.py b/app/main.py index 4695dd5..753e4dc 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.api.endpoints import user, address, community, station, order, coupon, community_building +from app.api.endpoints import user, address, community, station, order, coupon, community_building, upload from app.models.database import Base, engine from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse @@ -33,6 +33,7 @@ app.include_router(community_building.router, prefix="/api/community/building", app.include_router(station.router, prefix="/api/station", tags=["驿站"]) app.include_router(order.router, prefix="/api/order", tags=["订单"]) app.include_router(coupon.router, prefix="/api/coupon", tags=["优惠券"]) +app.include_router(upload.router, prefix="/api/upload", tags=["文件上传"]) @app.get("/") async def root(): diff --git a/app/models/order.py b/app/models/order.py index 5e3c03f..b995258 100644 --- a/app/models/order.py +++ b/app/models/order.py @@ -1,9 +1,17 @@ from datetime import datetime from typing import Optional, List -from sqlalchemy import Column, String, Integer, Float, DateTime, ForeignKey +from sqlalchemy import Column, String, Integer, Float, DateTime, ForeignKey, Enum from sqlalchemy.sql import func from pydantic import BaseModel, Field from .database import Base +import enum + +class OrderStatus(str, enum.Enum): + PENDING = "pending" + ACCEPTED = "accepted" + UNPAID = "unpaid" + COMPLETED = "completed" + CANCELLED = "cancelled" # 数据库模型 class ShippingOrderDB(Base): @@ -17,6 +25,9 @@ class ShippingOrderDB(Base): coupon_discount_amount = Column(Float, default=0) coupon_id = Column(Integer, ForeignKey("user_coupons.id"), nullable=True) final_amount = Column(Float, nullable=False) + status = Column(Enum(OrderStatus), nullable=False, default=OrderStatus.PENDING) + cancel_reason = Column(String(200), nullable=True) # 取消原因 + complete_images = Column(String(1000), nullable=True) # 完成订单的图片URL,多个URL用逗号分隔 create_time = Column(DateTime(timezone=True), server_default=func.now()) class ShippingOrderPackageDB(Base): @@ -49,8 +60,16 @@ class OrderInfo(BaseModel): coupon_discount_amount: float coupon_id: Optional[int] final_amount: float + status: OrderStatus + complete_images: Optional[List[str]] = None create_time: datetime + def __init__(self, **data): + super().__init__(**data) + # 将逗号分隔的图片URL字符串转换为列表 + if self.complete_images and isinstance(self.complete_images, str): + self.complete_images = self.complete_images.split(",") + class Config: from_attributes = True @@ -81,4 +100,12 @@ class OrderPriceInfo(BaseModel): original_amount: float coupon_discount_amount: float coupon_id: Optional[int] = None - final_amount: float \ No newline at end of file + final_amount: float + +# 添加取消订单请求模型 +class OrderCancel(BaseModel): + reason: str = Field(..., max_length=200, description="取消原因") + +# 完成订单请求模型 +class OrderComplete(BaseModel): + images: Optional[List[str]] = Field(None, max_items=5) # 最多5张图片,可选 \ No newline at end of file diff --git a/app/models/upload.py b/app/models/upload.py new file mode 100644 index 0000000..3aebd1a --- /dev/null +++ b/app/models/upload.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel +from typing import List + +class UploadResponse(BaseModel): + url: str + +class MultiUploadResponse(BaseModel): + urls: List[str] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 4ce6cad..b088b01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ sqlalchemy>=1.4.23 redis==5.0.1 pymysql==1.1.0 SQLAlchemy==2.0.27 -unisms \ No newline at end of file +unisms +cos-python-sdk-v5==1.9.25 \ No newline at end of file