diff --git a/app/api/endpoints/coupon_activity.py b/app/api/endpoints/coupon_activity.py new file mode 100644 index 0000000..b3284f1 --- /dev/null +++ b/app/api/endpoints/coupon_activity.py @@ -0,0 +1,209 @@ +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session +from sqlalchemy import func, and_ +from app.models.coupon_activity import ( + CouponActivityDB, + CouponActivityCreate, + CouponActivityUpdate, + CouponActivityInfo +) +from app.models.coupon_receive_record import CouponReceiveRecordDB +from app.models.coupon import CouponDB, UserCouponDB, CouponStatus, CouponInfo +from app.models.database import get_db +from app.api.deps import get_current_user, get_admin_user +from app.models.user import UserDB +from app.core.response import success_response, error_response, ResponseModel +from typing import Optional, List +from datetime import datetime, time + +router = APIRouter() + +@router.post("", response_model=ResponseModel) +async def create_coupon_activity( + activity: CouponActivityCreate, + db: Session = Depends(get_db), + admin: UserDB = Depends(get_admin_user) +): + """创建优惠券活动(管理员)""" + # 检查优惠券是否存在 + for coupon_id in activity.coupon_config.keys(): + coupon = db.query(CouponDB).filter(CouponDB.id == coupon_id).first() + if not coupon: + return error_response(code=404, message=f"优惠券ID {coupon_id} 不存在") + # 检查数量是否大于0 + if activity.coupon_config[coupon_id] <= 0: + return error_response(code=400, message=f"优惠券ID {coupon_id} 的数量必须大于0") + + db_activity = CouponActivityDB(**activity.model_dump()) + db.add(db_activity) + + try: + db.commit() + db.refresh(db_activity) + return success_response(data=CouponActivityInfo.model_validate(db_activity)) + except Exception as e: + db.rollback() + return error_response(code=500, message=f"创建失败: {str(e)}") + +@router.get("/{activity_id}", response_model=ResponseModel) +async def get_coupon_activity( + activity_id: int, + db: Session = Depends(get_db) +): + """获取优惠券活动详情""" + activity = db.query(CouponActivityDB).filter( + CouponActivityDB.id == activity_id + ).first() + + if not activity: + return error_response(code=404, message="活动不存在") + + # 获取活动对应的优惠券 + coupons = db.query(CouponDB).filter( + CouponDB.id.in_(activity.coupon_config.keys()) + ).all() + + result = CouponActivityInfo.model_validate(activity) + result.coupons = [CouponInfo.model_validate(coupon) for coupon in coupons] + + return success_response(data=result) + + +@router.get("", response_model=ResponseModel) +async def get_coupon_activities( + 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(CouponActivityDB) + + if is_active is not None: + query = query.filter(CouponActivityDB.is_active == is_active) + + total = query.count() + + activities = query.order_by(CouponActivityDB.create_time.desc())\ + .offset(skip)\ + .limit(limit)\ + .all() + + return success_response(data={ + "total": total, + "items": [CouponActivityInfo.model_validate(a) for a in activities] + }) + +@router.post("/{activity_id}/receive", response_model=ResponseModel) +async def receive_coupons( + activity_id: int, + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """领取优惠券""" + # 查询活动 + activity = db.query(CouponActivityDB).filter( + CouponActivityDB.id == activity_id, + CouponActivityDB.is_active == True + ).first() + + if not activity: + return error_response(code=404, message="活动不存在或已结束") + + # 检查领取时间 + current_time = datetime.now().time() + if current_time < activity.daily_start_time or current_time > activity.daily_end_time: + return error_response(code=400, message="不在领取时间范围内") + + # 检查今日领取次数 + today = datetime.now().date() + + receive_count = db.query(func.count(CouponReceiveRecordDB.id)).filter( + CouponReceiveRecordDB.user_id == current_user.userid, + CouponReceiveRecordDB.activity_id == activity_id, + CouponReceiveRecordDB.receive_date == today + ).scalar() + + if receive_count >= activity.daily_limit: + return error_response(code=400, message="今日领取次数已达上限") + + try: + # 发放优惠券 + for coupon_id, count in activity.coupon_config.items(): + coupon = db.query(CouponDB).filter(CouponDB.id == coupon_id).first() + if coupon: + # 循环发放指定数量的优惠券 + for _ in range(count): + # 当天晚上12点过期 + expire_time = datetime.combine(today, datetime.max.time()) + + user_coupon = UserCouponDB( + user_id=current_user.userid, + coupon_id=coupon.id, + coupon_name=coupon.name, + coupon_amount=coupon.amount, + expire_time=expire_time, + status=CouponStatus.UNUSED + ) + db.add(user_coupon) + + # 检查是否领取过优惠券 + receive_record = db.query(CouponReceiveRecordDB).filter( + CouponReceiveRecordDB.user_id == current_user.userid, + CouponReceiveRecordDB.activity_id == activity_id, + CouponReceiveRecordDB.receive_date == today + ).first() + + if receive_record: + receive_record.receive_count += 1 + else: + record = CouponReceiveRecordDB( + user_id=current_user.userid, + activity_id=activity_id, + receive_date=today, + receive_count=1 + ) + db.add(record) + + db.commit() + return success_response(message="领取成功") + except Exception as e: + db.rollback() + return error_response(code=500, message=f"领取失败: {str(e)}") + +@router.put("/{activity_id}", response_model=ResponseModel) +async def update_coupon_activity( + activity_id: int, + activity: CouponActivityUpdate, + db: Session = Depends(get_db), + admin: UserDB = Depends(get_admin_user) +): + """更新优惠券活动(管理员)""" + db_activity = db.query(CouponActivityDB).filter( + CouponActivityDB.id == activity_id + ).first() + + if not db_activity: + return error_response(code=404, message="活动不存在") + + # 检查优惠券是否存在 + if activity.coupon_config: + for coupon_id, count in activity.coupon_config.items(): + coupon = db.query(CouponDB).filter(CouponDB.id == coupon_id).first() + if not coupon: + return error_response(code=404, message=f"优惠券ID {coupon_id} 不存在") + # 检查数量是否大于0 + if count <= 0: + return error_response(code=400, message=f"优惠券ID {coupon_id} 的数量必须大于0") + + update_data = activity.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(db_activity, key, value) + + try: + db.commit() + db.refresh(db_activity) + return success_response(data=CouponActivityInfo.model_validate(db_activity)) + 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/order.py b/app/api/endpoints/order.py index bdbd5ab..a56d9c2 100644 --- a/app/api/endpoints/order.py +++ b/app/api/endpoints/order.py @@ -259,13 +259,19 @@ async def create_order( "appid": settings.WECHAT_APPID, "path": f"/pages/order/detail/index?id={db_order.orderid}" }) + + # 超过晚上8点,则使用明天送达的文案 + if db_order.create_time.time() > datetime.time(20, 0, 0): + success_text = settings.ORDER_SUCCESS_TOMORROW_TEXT + else: + success_text = settings.ORDER_SUCCESS_TODAY_TEXT return success_response( message="订单创建成功", data={ "order": OrderInfo.model_validate(db_order), "packages": [OrderPackageInfo.model_validate(p) for p in packages], - "success_text" : settings.ORDER_SUCCESS_TEXT + "success_text" : success_text } ) except Exception as e: diff --git a/app/core/config.py b/app/core/config.py index 42e1cce..a2947dc 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -19,7 +19,8 @@ class Settings(BaseSettings): ORDER_DELIVERYMAN_SHARE_RATIO: float = 0.8 # 配送员分账比例 #订单创建成功文案 - ORDER_SUCCESS_TEXT: str = "订单预计今晚前送达,请注意查收" + ORDER_SUCCESS_TODAY_TEXT: str = "订单预计今晚前送达,请注意查收" + ORDER_SUCCESS_TOMORROW_TEXT: str = "订单预计明晚前送达,请注意查收" ORDER_PREORDER_PRICE_TEXT: str = "基础费3元 (含5件包裹) 超出部分0.5元/件" # JWT 配置 diff --git a/app/main.py b/app/main.py index acea590..f5698d4 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, point_product, point_product_order +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, coupon_activity from app.models.database import Base, engine from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse @@ -47,7 +47,8 @@ app.include_router(community.router, prefix="/api/community", tags=["社区"]) app.include_router(community_building.router, prefix="/api/community/building", tags=["社区楼栋"]) 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(coupon.router, prefix="/api/coupon", tags=["抵扣券"]) +app.include_router(coupon_activity.router, prefix="/api/coupon-activities", tags=["抵扣券活动"]) app.include_router(merchant.router, prefix="/api/merchant", tags=["商家"]) app.include_router(merchant_category.router, prefix="/api/merchant-categories", tags=["商家分类"]) app.include_router(merchant_product.router, prefix="/api/merchant/product", tags=["商家产品"]) @@ -78,6 +79,7 @@ async def validation_exception_handler(request, exc): @app.exception_handler(HTTPException) async def http_exception_handler(request, exc): logging.exception(f"HTTP异常: {str(exc)}") + return CustomJSONResponse( status_code=exc.status_code, content=str(exc) diff --git a/app/models/coupon_activity.py b/app/models/coupon_activity.py new file mode 100644 index 0000000..6e79980 --- /dev/null +++ b/app/models/coupon_activity.py @@ -0,0 +1,62 @@ +from sqlalchemy import Column, Integer, String, DateTime, Boolean, JSON, Time +from sqlalchemy.sql import func +from pydantic import BaseModel, Field +from typing import Optional, List, Dict +from datetime import datetime, time +from .database import Base + +class CouponActivityDB(Base): + __tablename__ = "coupon_activities" + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String(100), nullable=False) # 活动名称 + description = Column(String(500), nullable=True) # 活动描述 + start_time = Column(DateTime(timezone=True), nullable=False) # 活动开始时间 + end_time = Column(DateTime(timezone=True), nullable=False) # 活动结束时间 + daily_start_time = Column(Time, nullable=False) # 每日开始时间 + daily_end_time = Column(Time, nullable=False) # 每日结束时间 + daily_limit = Column(Integer, nullable=False, default=1) # 每日可领取次数 + is_active = Column(Boolean, nullable=False, default=True) # 是否激活 + coupon_config = Column(JSON, nullable=False) # 可领取的优惠券配置 {coupon_id: count} + create_time = Column(DateTime(timezone=True), server_default=func.now()) + update_time = Column(DateTime(timezone=True), onupdate=func.now()) + +# Pydantic 模型 +class CouponActivityCreate(BaseModel): + name: str = Field(..., max_length=100) + description: Optional[str] = Field(None, max_length=500) + start_time: datetime + end_time: datetime + daily_start_time: time # 每日开始时间 "HH:MM:SS" + daily_end_time: time # 每日结束时间 "HH:MM:SS" + daily_limit: int = Field(..., gt=0) + coupon_config: Dict[int, int] # {coupon_id: count} + is_active: bool = Field(default=True) + +class CouponActivityUpdate(BaseModel): + name: Optional[str] = Field(None, max_length=100) + description: Optional[str] = Field(None, max_length=500) + start_time: Optional[datetime] = None + end_time: Optional[datetime] = None + daily_start_time: Optional[time] = None + daily_end_time: Optional[time] = None + daily_limit: Optional[int] = Field(None, gt=0) + coupon_config: Optional[Dict[int, int]] = None + is_active: Optional[bool] = None + +class CouponActivityInfo(BaseModel): + id: int + name: str + description: Optional[str] + start_time: datetime + end_time: datetime + daily_start_time: time + daily_end_time: time + daily_limit: int + coupon_config: Dict[int, int] + is_active: bool + create_time: datetime + update_time: Optional[datetime] + + class Config: + from_attributes = True \ No newline at end of file diff --git a/app/models/coupon_receive_record.py b/app/models/coupon_receive_record.py new file mode 100644 index 0000000..e9ab6e9 --- /dev/null +++ b/app/models/coupon_receive_record.py @@ -0,0 +1,13 @@ +from sqlalchemy import Column, Integer, DateTime, ForeignKey, Date +from sqlalchemy.sql import func +from .database import Base + +class CouponReceiveRecordDB(Base): + __tablename__ = "coupon_receive_records" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, ForeignKey("users.userid"), index=True) + activity_id = Column(Integer, ForeignKey("coupon_activities.id")) + receive_date = Column(Date, nullable=False) + receive_count = Column(Integer, nullable=False, default=1) # 领取次数 + create_time = Column(DateTime(timezone=True), server_default=func.now()) \ No newline at end of file