From ecdc84822faa4fc29ca71606e72e403f1b99fd11 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Thu, 20 Feb 2025 14:25:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=A7=AF=E5=88=86=E5=95=86?= =?UTF-8?q?=E5=93=81=E5=92=8C=E7=A7=AF=E5=88=86=E5=95=86=E5=93=81=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/order.py | 4 + app/api/endpoints/point_product.py | 100 +++++++++++++ app/api/endpoints/point_product_order.py | 172 +++++++++++++++++++++++ app/main.py | 4 +- app/models/point_product.py | 58 ++++++++ app/models/point_product_order.py | 73 ++++++++++ 6 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 app/api/endpoints/point_product.py create mode 100644 app/api/endpoints/point_product_order.py create mode 100644 app/models/point_product.py create mode 100644 app/models/point_product_order.py diff --git a/app/api/endpoints/order.py b/app/api/endpoints/order.py index 539d0a4..9a29833 100644 --- a/app/api/endpoints/order.py +++ b/app/api/endpoints/order.py @@ -627,6 +627,10 @@ async def get_deliveryman_orders( if status: statuses = status.split(",") query = query.filter(ShippingOrderDB.status.in_(statuses)) + + # 如果订单状态不是待接单,则需要过滤快递员 + if OrderStatus.CREATED not in statuses: + query = query.filter(ShippingOrderDB.deliveryman_user_id == deliveryman.userid) # 楼栋筛选 if building_id: diff --git a/app/api/endpoints/point_product.py b/app/api/endpoints/point_product.py new file mode 100644 index 0000000..afde5a8 --- /dev/null +++ b/app/api/endpoints/point_product.py @@ -0,0 +1,100 @@ +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session +from app.models.point_product import ( + PointProductDB, + PointProductCreate, + PointProductUpdate, + PointProductInfo +) +from app.models.database import get_db +from app.api.deps import get_admin_user +from app.models.user import UserDB +from app.core.response import success_response, error_response, ResponseModel +from typing import Optional + +router = APIRouter() + +@router.post("", response_model=ResponseModel) +async def create_point_product( + product: PointProductCreate, + db: Session = Depends(get_db), + admin: UserDB = Depends(get_admin_user) +): + """创建积分商品(管理员)""" + db_product = PointProductDB(**product.model_dump()) + db.add(db_product) + + try: + db.commit() + db.refresh(db_product) + return success_response(data=PointProductInfo.model_validate(db_product)) + except Exception as e: + db.rollback() + return error_response(code=500, message=f"创建失败: {str(e)}") + +@router.get("", response_model=ResponseModel) +async def get_point_products( + is_active: Optional[bool] = None, + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + db: Session = Depends(get_db) +): + """获取积分商品列表""" + query = db.query(PointProductDB) + + if is_active is not None: + query = query.filter(PointProductDB.is_active == is_active) + + total = query.count() + + products = query.order_by(PointProductDB.create_time.desc())\ + .offset(skip)\ + .limit(limit)\ + .all() + + return success_response(data={ + "total": total, + "items": [PointProductInfo.model_validate(p) for p in products] + }) + +@router.get("/{product_id}", response_model=ResponseModel) +async def get_point_product( + product_id: int, + db: Session = Depends(get_db) +): + """获取积分商品详情""" + product = db.query(PointProductDB).filter( + PointProductDB.id == product_id + ).first() + + if not product: + return error_response(code=404, message="商品不存在") + + return success_response(data=PointProductInfo.model_validate(product)) + +@router.put("/{product_id}", response_model=ResponseModel) +async def update_point_product( + product_id: int, + product: PointProductUpdate, + db: Session = Depends(get_db), + admin: UserDB = Depends(get_admin_user) +): + """更新积分商品(管理员)""" + db_product = db.query(PointProductDB).filter( + PointProductDB.id == product_id + ).first() + + if not db_product: + return error_response(code=404, message="商品不存在") + + update_data = product.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(db_product, key, value) + + try: + db.commit() + db.refresh(db_product) + return success_response(data=PointProductInfo.model_validate(db_product)) + 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/point_product_order.py b/app/api/endpoints/point_product_order.py new file mode 100644 index 0000000..59c3d84 --- /dev/null +++ b/app/api/endpoints/point_product_order.py @@ -0,0 +1,172 @@ +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session +from app.models.point_product_order import ( + PointProductOrderDB, + PointProductOrderCreate, + PointProductOrderUpdate, + PointProductOrderInfo, + PointProductOrderStatus +) +from app.models.point_product import PointProductDB +from app.models.database import get_db +from app.api.deps import get_current_user, get_admin_user, get_deliveryman_user +from app.models.user import UserDB +from app.core.response import success_response, error_response, ResponseModel +from app.core.point_manager import PointManager +from typing import Optional +from datetime import datetime + +router = APIRouter() + +@router.post("", response_model=ResponseModel) +async def create_point_product_order( + order: PointProductOrderCreate, + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """创建积分商品订单""" + # 查询商品信息 + product = db.query(PointProductDB).filter( + PointProductDB.id == order.product_id, + PointProductDB.is_active == True + ).first() + + if not product: + return error_response(code=404, message="商品不存在或已下架") + + # 计算所需总积分 + total_points = product.point_amount * order.quantity + + # 检查用户积分是否足够 + if current_user.points < total_points: + return error_response(code=400, message="积分不足") + + # 创建订单 + db_order = PointProductOrderDB( + orderid=PointProductOrderDB.generate_order_id(), + user_id=current_user.userid, + product_id=product.id, + product_name=product.name, + product_image=product.product_image, + product_description=product.description, + product_point_amount=product.point_amount, + quantity=order.quantity, + order_point_amount=total_points + ) + + try: + # 扣减用户积分 + point_manager = PointManager(db) + point_manager.deduct_points( + user_id=current_user.userid, + points=total_points, + description=f"兑换商品: {product.name}", + order_id=db_order.orderid + ) + + # 保存订单 + db.add(db_order) + db.commit() + db.refresh(db_order) + + return success_response(data=PointProductOrderInfo.model_validate(db_order)) + except Exception as e: + db.rollback() + return error_response(code=500, message=f"创建订单失败: {str(e)}") + +@router.get("", response_model=ResponseModel) +async def get_point_product_orders( + status: Optional[PointProductOrderStatus] = None, + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """获取积分商品订单列表""" + query = db.query(PointProductOrderDB).filter( + PointProductOrderDB.user_id == current_user.userid + ) + + if status: + query = query.filter(PointProductOrderDB.status == status) + + total = query.count() + + orders = query.order_by(PointProductOrderDB.create_time.desc())\ + .offset(skip)\ + .limit(limit)\ + .all() + + return success_response(data={ + "total": total, + "items": [PointProductOrderInfo.model_validate(o) for o in orders] + }) + +@router.get("/{orderid}", response_model=ResponseModel) +async def get_point_product_order( + orderid: str, + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """获取积分商品订单详情""" + order = db.query(PointProductOrderDB).filter( + PointProductOrderDB.orderid == orderid, + PointProductOrderDB.user_id == current_user.userid + ).first() + + if not order: + return error_response(code=404, message="订单不存在") + + return success_response(data=PointProductOrderInfo.model_validate(order)) + + +# 接受订单 +@router.post("/{orderid}/accept", response_model=ResponseModel) +async def accept_point_product_order( + orderid: str, + db: Session = Depends(get_db), + deliveryman: UserDB = Depends(get_deliveryman_user) +): + order = db.query(PointProductOrderDB).filter( + PointProductOrderDB.orderid == orderid, + PointProductOrderDB.status == PointProductOrderStatus.PENDING + ).first() + + if not order: + return error_response(code=404, message="订单不存在") + + if order.status != PointProductOrderStatus.PENDING: + return error_response(code=400, message="订单状态不正确") + + # 更新订单状态 + order.status = PointProductOrderStatus.CONFIRMED + db.commit() + db.refresh(order) + + return success_response(data=PointProductOrderInfo.model_validate(order)) + +# 拒绝订单 +@router.post("/{orderid}/reject", response_model=ResponseModel) +async def reject_point_product_order( + orderid: str, + db: Session = Depends(get_db), + deliveryman: UserDB = Depends(get_deliveryman_user) +): + order = db.query(PointProductOrderDB).filter( + PointProductOrderDB.orderid == orderid, + PointProductOrderDB.status == PointProductOrderStatus.PENDING + ).first() + + if not order: + return error_response(code=404, message="订单不存在") + + if order.status != PointProductOrderStatus.PENDING: + return error_response(code=400, message="订单状态不正确") + + # 更新订单状态 + order.status = PointProductOrderStatus.CANCELLED + db.commit() + db.refresh(order) + + return success_response(data=PointProductOrderInfo.model_validate(order)) + diff --git a/app/main.py b/app/main.py index 13fcef8..acea590 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 wechat,user, address, community, station, order, coupon, community_building, upload, merchant, merchant_product, merchant_order, point, config, merchant_category, log, account,merchant_pay_order, message, bank_card, withdraw, mp +from app.api.endpoints import wechat,user, address, community, station, order, coupon, community_building, upload, merchant, merchant_product, merchant_order, point, config, merchant_category, log, account,merchant_pay_order, message, bank_card, withdraw, mp, point_product, point_product_order from app.models.database import Base, engine from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse @@ -39,6 +39,8 @@ app.include_router(user.router, prefix="/api/user", tags=["用户"]) app.include_router(bank_card.router, prefix="/api/bank-cards", tags=["用户银行卡"]) app.include_router(withdraw.router, prefix="/api/withdraw", tags=["提现"]) app.include_router(point.router, prefix="/api/point", tags=["用户积分"]) +app.include_router(point_product.router, prefix="/api/point-products", tags=["积分商品"]) +app.include_router(point_product_order.router, prefix="/api/point-product-orders", tags=["积分商品订单"]) app.include_router(account.router, prefix="/api/account", tags=["账户"]) app.include_router(address.router, prefix="/api/address", tags=["配送地址"]) app.include_router(community.router, prefix="/api/community", tags=["社区"]) diff --git a/app/models/point_product.py b/app/models/point_product.py new file mode 100644 index 0000000..1b946b1 --- /dev/null +++ b/app/models/point_product.py @@ -0,0 +1,58 @@ +from sqlalchemy import Column, Integer, String, DateTime, Boolean, JSON +from sqlalchemy.sql import func +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import datetime +from .database import Base +from app.core.imageprocessor import process_image, ImageFormat + +class PointProductDB(Base): + __tablename__ = "point_products" + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String(100), nullable=False) + product_image = Column(String(200), nullable=False) + tags = Column(String(100), nullable=True) # 商品标签,JSON数组 + description = Column(String(500), nullable=False) + point_amount = Column(Integer, nullable=False) # 所需积分 + is_active = Column(Boolean, nullable=False, default=True) # 是否上架 + create_time = Column(DateTime(timezone=True), server_default=func.now()) + update_time = Column(DateTime(timezone=True), onupdate=func.now()) + + @property + def optimized_product_image(self): + """获取优化后的商品图片""" + if self.product_image: + return process_image(self.product_image).thumbnail(400, 400).format(ImageFormat.WEBP).build() + return None + +# Pydantic 模型 +class PointProductCreate(BaseModel): + name: str = Field(..., max_length=100) + product_image: str = Field(..., max_length=200) + tags: Optional[str] = None + description: str = Field(..., max_length=500) + point_amount: int = Field(..., gt=0) + is_active: bool = Field(default=True) + +class PointProductUpdate(BaseModel): + name: Optional[str] = Field(None, max_length=100) + product_image: Optional[str] = Field(None, max_length=200) + tags: Optional[List[str]] = None + description: Optional[str] = Field(None, max_length=500) + point_amount: Optional[int] = Field(None, gt=0) + is_active: Optional[bool] = None + +class PointProductInfo(BaseModel): + id: int + name: str + product_image: str + optimized_product_image: Optional[str] + tags: Optional[str] + description: str + point_amount: int + is_active: bool + create_time: datetime + + class Config: + from_attributes = True \ No newline at end of file diff --git a/app/models/point_product_order.py b/app/models/point_product_order.py new file mode 100644 index 0000000..953fa82 --- /dev/null +++ b/app/models/point_product_order.py @@ -0,0 +1,73 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Enum, Float +from sqlalchemy.sql import func +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime +from .database import Base +import enum +from app.core.utils import CommonUtils + +class PointProductOrderStatus(str, enum.Enum): + PENDING = "PENDING" # 待确认 + CONFIRMED = "CONFIRMED" # 已确认 + CANCELLED = "CANCELLED" # 已取消 + DELIVERED = "DELIVERED" # 已送达 + + @property + def status_text(self) -> str: + status_map = { + PointProductOrderStatus.PENDING: "待确认", + PointProductOrderStatus.CONFIRMED: "已确认", + PointProductOrderStatus.CANCELLED: "已取消", + PointProductOrderStatus.DELIVERED: "已送达" + } + return status_map.get(self, "未知状态") + +class PointProductOrderDB(Base): + __tablename__ = "point_product_orders" + + orderid = Column(String(32), primary_key=True) + user_id = Column(Integer, ForeignKey("users.userid"), index=True) + delivery_order_id = Column(String(32), ForeignKey("shipping_orders.orderid"), nullable=True) + product_id = Column(Integer, ForeignKey("point_products.id")) + product_name = Column(String(100), nullable=False) + product_image = Column(String(200), nullable=False) + product_description = Column(String(500), nullable=False) + product_point_amount = Column(Integer, nullable=False) # 单件商品所需积分 + quantity = Column(Integer, nullable=False, default=1) + order_point_amount = Column(Integer, nullable=False) # 订单总积分 + status = Column(Enum(PointProductOrderStatus), nullable=False, default=PointProductOrderStatus.PENDING) + create_time = Column(DateTime(timezone=True), server_default=func.now()) + update_time = Column(DateTime(timezone=True), onupdate=func.now()) + + @staticmethod + def generate_order_id() -> str: + """生成订单号""" + return CommonUtils.generate_order_id("P") # P代表积分商品订单 + +# Pydantic 模型 +class PointProductOrderCreate(BaseModel): + product_id: int + quantity: int = Field(default=1, gt=0) + +class PointProductOrderUpdate(BaseModel): + status: PointProductOrderStatus + delivery_order_id: Optional[str] = None + +class PointProductOrderInfo(BaseModel): + orderid: str + user_id: int + delivery_order_id: Optional[str] + product_id: int + product_name: str + product_image: str + product_description: str + product_point_amount: int + quantity: int + order_point_amount: int + status: PointProductOrderStatus + create_time: datetime + update_time: Optional[datetime] + + class Config: + from_attributes = True \ No newline at end of file