From f096207aeb8fd0a32b703e90b0b6743bc8113c42 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sat, 4 Jan 2025 22:55:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BC=98=E6=83=A0=E5=88=B8?= =?UTF-8?q?=EF=BC=8C=E8=AE=A2=E5=8D=95=EF=BC=8C=E9=A9=BF=E7=AB=99=E7=AD=89?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/deps.py | 13 ++- app/api/endpoints/coupon.py | 149 +++++++++++++++++++++++++++++++++++ app/api/endpoints/order.py | 135 +++++++++++++++++++++++++++++++ app/api/endpoints/station.py | 100 +++++++++++++++++++++++ app/api/endpoints/user.py | 75 ++++++++++++++++-- app/core/config.py | 4 +- app/core/security.py | 26 +++++- app/main.py | 5 +- app/models/address.py | 11 +-- app/models/coupon.py | 72 +++++++++++++++++ app/models/order.py | 72 +++++++++++++++++ app/models/station.py | 32 ++++++++ 12 files changed, 675 insertions(+), 19 deletions(-) create mode 100644 app/api/endpoints/coupon.py create mode 100644 app/api/endpoints/order.py create mode 100644 app/api/endpoints/station.py create mode 100644 app/models/coupon.py create mode 100644 app/models/order.py create mode 100644 app/models/station.py diff --git a/app/api/deps.py b/app/api/deps.py index 614868c..c9e63ac 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -1,4 +1,4 @@ -from fastapi import Depends, HTTPException, Header +from fastapi import Depends, HTTPException, Header, Cookie from typing import Optional from sqlalchemy.orm import Session from app.models.database import get_db @@ -7,12 +7,19 @@ from app.core.security import verify_token async def get_current_user( authorization: Optional[str] = Header(None), + access_token: Optional[str] = Cookie(None), db: Session = Depends(get_db) ) -> UserDB: - if not authorization or not authorization.startswith("Bearer "): + # 优先使用Header中的token,其次使用Cookie中的token + token = None + if authorization and authorization.startswith("Bearer "): + token = authorization.split(" ")[1] + elif access_token and access_token.startswith("Bearer "): + token = access_token.split(" ")[1] + + if not token: raise HTTPException(status_code=401, detail="未提供有效的认证信息") - token = authorization.split(" ")[1] phone = verify_token(token) if not phone: raise HTTPException(status_code=401, detail="Token已过期或无效") diff --git a/app/api/endpoints/coupon.py b/app/api/endpoints/coupon.py new file mode 100644 index 0000000..7eeb52c --- /dev/null +++ b/app/api/endpoints/coupon.py @@ -0,0 +1,149 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from typing import List, Optional +from app.models.coupon import ( + CouponDB, + UserCouponDB, + CouponCreate, + CouponUpdate, + CouponInfo, + UserCouponCreate, + UserCouponInfo, + CouponStatus +) +from app.models.database import get_db +from app.api.deps import get_admin_user, get_current_user +from app.models.user import UserDB +from app.core.response import success_response, error_response, ResponseModel +from datetime import datetime, timezone + +router = APIRouter() + +@router.post("/", response_model=ResponseModel) +async def create_coupon( + coupon: CouponCreate, + db: Session = Depends(get_db), + admin: UserDB = Depends(get_admin_user) +): + """创建优惠券(管理员)""" + db_coupon = CouponDB(**coupon.model_dump()) + db.add(db_coupon) + + try: + db.commit() + db.refresh(db_coupon) + return success_response(data=CouponInfo.model_validate(db_coupon)) + except Exception as e: + db.rollback() + return error_response(code=500, message=f"创建优惠券失败: {str(e)}") + +@router.put("/{coupon_id}", response_model=ResponseModel) +async def update_coupon( + coupon_id: int, + coupon: CouponUpdate, + db: Session = Depends(get_db), + admin: UserDB = Depends(get_admin_user) +): + """更新优惠券(管理员)""" + db_coupon = db.query(CouponDB).filter(CouponDB.id == coupon_id).first() + if not db_coupon: + return error_response(code=404, message="优惠券不存在") + + update_data = coupon.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(db_coupon, key, value) + + try: + db.commit() + db.refresh(db_coupon) + return success_response(data=CouponInfo.model_validate(db_coupon)) + except Exception as e: + db.rollback() + return error_response(code=500, message=f"更新优惠券失败: {str(e)}") + +@router.post("/issue", response_model=ResponseModel) +async def issue_coupon( + user_coupon: UserCouponCreate, + db: Session = Depends(get_db), + admin: UserDB = Depends(get_admin_user) +): + """发放优惠券给用户(管理员)""" + # 查询优惠券信息 + coupon = db.query(CouponDB).filter(CouponDB.id == user_coupon.coupon_id).first() + if not coupon: + return error_response(code=404, message="优惠券不存在") + + issued_coupons = [] + # 批量创建用户优惠券 + for _ in range(user_coupon.count): + db_user_coupon = UserCouponDB( + user_id=user_coupon.user_id, + coupon_id=coupon.id, + coupon_name=coupon.name, + coupon_amount=coupon.amount, + expire_time=user_coupon.expire_time + ) + db.add(db_user_coupon) + issued_coupons.append(db_user_coupon) + + try: + db.commit() + return success_response( + message=f"成功发放 {user_coupon.count} 张优惠券", + data=[UserCouponInfo.model_validate(c) for c in issued_coupons] + ) + except Exception as e: + db.rollback() + return error_response(code=500, message=f"发放优惠券失败: {str(e)}") + +@router.get("/user/list", response_model=ResponseModel) +async def get_user_coupons( + skip: int = 0, + limit: int = 10, + status: Optional[CouponStatus] = None, + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """获取用户的优惠券列表""" + query = db.query(UserCouponDB).filter( + UserCouponDB.user_id == current_user.userid + ) + + # 如果指定了状态,添加状态过滤 + if status: + query = query.filter(UserCouponDB.status == status) + + # 更新过期状态 + now = datetime.now(timezone.utc) + db.query(UserCouponDB).filter( + UserCouponDB.user_id == current_user.userid, + UserCouponDB.status == CouponStatus.UNUSED, + UserCouponDB.expire_time < now + ).update({"status": CouponStatus.EXPIRED}) + + db.commit() + + # 获取分页数据 + coupons = query.order_by( + UserCouponDB.create_time.desc() + ).offset(skip).limit(limit).all() + + return success_response( + data=[UserCouponInfo.model_validate(c) for c in coupons] + ) + +@router.get("/list", response_model=ResponseModel) +async def get_all_coupons( + skip: int = 0, + limit: int = 10, + db: Session = Depends(get_db), + admin: UserDB = Depends(get_admin_user) +): + """获取所有优惠券列表(管理员)""" + coupons = db.query(CouponDB).order_by( + CouponDB.create_time.desc() + ).offset(skip).limit(limit).all() + + return success_response( + data=[CouponInfo.model_validate(c) for c in coupons] + ) \ No newline at end of file diff --git a/app/api/endpoints/order.py b/app/api/endpoints/order.py new file mode 100644 index 0000000..519275a --- /dev/null +++ b/app/api/endpoints/order.py @@ -0,0 +1,135 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from typing import List +from app.models.order import ( + ShippingOrderDB, + ShippingOrderPackageDB, + OrderCreate, + OrderInfo, + OrderPackageInfo, + generate_order_id +) +from app.models.database import get_db +from app.api.deps import get_current_user +from app.models.user import UserDB +from app.core.response import success_response, error_response, ResponseModel +from app.models.coupon import UserCouponDB, CouponStatus +from datetime import datetime, timezone + +router = APIRouter() + +@router.post("/", response_model=ResponseModel) +async def create_shipping_order( + order: OrderCreate, + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """创建配送订单""" + + # 生成订单号 + orderid = generate_order_id() + + # 计算优惠金额 + coupon_discount = 0 + if order.coupon_id: + # 查询用户优惠券 + user_coupon = db.query(UserCouponDB).filter( + UserCouponDB.id == order.coupon_id, + UserCouponDB.user_id == current_user.userid, + UserCouponDB.status == CouponStatus.UNUSED, + UserCouponDB.expire_time > datetime.now(timezone.utc) + ).first() + + if not user_coupon: + return error_response(code=400, message="优惠券无效或已过期") + + coupon_discount = user_coupon.coupon_amount + # 更新优惠券状态 + user_coupon.status = CouponStatus.USED + + # 计算最终金额 + final_amount = max(0, order.original_amount - coupon_discount) + + # 创建订单 + db_order = ShippingOrderDB( + orderid=orderid, + userid=current_user.userid, + addressid=order.addressid, + package_count=order.package_count, + original_amount=order.original_amount, + coupon_discount_amount=coupon_discount, + coupon_id=order.coupon_id, + final_amount=final_amount + ) + db.add(db_order) + + # 创建订单包裹 + for package in order.packages: + db_package = ShippingOrderPackageDB( + orderid=orderid, + station_id=package.station_id, + pickup_codes=package.pickup_codes + ) + db.add(db_package) + + try: + db.commit() + db.refresh(db_order) + + # 查询包裹信息 + packages = db.query(ShippingOrderPackageDB).filter( + ShippingOrderPackageDB.orderid == orderid + ).all() + + return success_response( + message="订单创建成功", + data={ + "order": OrderInfo.model_validate(db_order), + "packages": [OrderPackageInfo.model_validate(p) for p in packages] + } + ) + except Exception as e: + db.rollback() + return error_response(code=500, message=f"订单创建失败: {str(e)}") + +@router.get("/{orderid}", response_model=ResponseModel) +async def get_order( + orderid: str, + 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="订单不存在") + + # 查询包裹信息 + packages = db.query(ShippingOrderPackageDB).filter( + ShippingOrderPackageDB.orderid == orderid + ).all() + + return success_response(data={ + "order": OrderInfo.model_validate(order), + "packages": [OrderPackageInfo.model_validate(p) for p in packages] + }) + +@router.get("/", response_model=ResponseModel) +async def get_user_orders( + skip: int = 0, + limit: int = 10, + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """获取用户的订单列表""" + orders = db.query(ShippingOrderDB).filter( + ShippingOrderDB.userid == current_user.userid + ).order_by( + ShippingOrderDB.create_time.desc() + ).offset(skip).limit(limit).all() + + return success_response(data=[OrderInfo.model_validate(o) for o in orders]) \ No newline at end of file diff --git a/app/api/endpoints/station.py b/app/api/endpoints/station.py new file mode 100644 index 0000000..06fe236 --- /dev/null +++ b/app/api/endpoints/station.py @@ -0,0 +1,100 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from typing import List, Optional +from app.models.station import StationDB, StationCreate, StationUpdate, StationInfo +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 + +router = APIRouter() + +@router.post("/", response_model=ResponseModel) +async def create_station( + station: StationCreate, + db: Session = Depends(get_db), + admin: UserDB = Depends(get_admin_user) +): + """创建驿站""" + db_station = StationDB(**station.model_dump()) + db.add(db_station) + db.commit() + db.refresh(db_station) + return success_response(data=StationInfo.model_validate(db_station)) + +@router.get("/", response_model=ResponseModel) +async def get_stations( + skip: int = 0, + limit: int = 10, + community_id: Optional[int] = None, + db: Session = Depends(get_db) +): + """获取驿站列表""" + query = db.query(StationDB) + if community_id: + query = query.filter(StationDB.community_id == community_id) + + stations = query.offset(skip).limit(limit).all() + return success_response(data=[StationInfo.model_validate(s) for s in stations]) + +@router.get("/{station_id}", response_model=ResponseModel) +async def get_station( + station_id: int, + db: Session = Depends(get_db) +): + """获取驿站详情""" + station = db.query(StationDB).filter(StationDB.id == station_id).first() + if not station: + return error_response(code=404, message="驿站不存在") + return success_response(data=StationInfo.model_validate(station)) + +@router.put("/{station_id}", response_model=ResponseModel) +async def update_station( + station_id: int, + station: StationUpdate, + db: Session = Depends(get_db), + admin: UserDB = Depends(get_admin_user) +): + """更新驿站信息""" + db_station = db.query(StationDB).filter(StationDB.id == station_id).first() + if not db_station: + return error_response(code=404, message="驿站不存在") + + update_data = station.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(db_station, key, value) + + db.commit() + db.refresh(db_station) + return success_response(data=StationInfo.model_validate(db_station)) + +@router.delete("/{station_id}", response_model=ResponseModel) +async def delete_station( + station_id: int, + db: Session = Depends(get_db), + admin: UserDB = Depends(get_admin_user) +): + """删除驿站""" + result = db.query(StationDB).filter(StationDB.id == station_id).delete() + if not result: + return error_response(code=404, message="驿站不存在") + + db.commit() + return success_response(message="驿站已删除") + +@router.get("/community/{community_id}", response_model=ResponseModel) +async def get_stations_by_community( + community_id: int, + db: Session = Depends(get_db) +): + """获取指定社区的驿站列表""" + stations = db.query(StationDB).filter( + StationDB.community_id == community_id + ).all() + + if not stations: + return success_response(data=[]) # 返回空列表而不是错误 + + return success_response( + data=[StationInfo.model_validate(s) for s in stations] + ) \ No newline at end of file diff --git a/app/api/endpoints/user.py b/app/api/endpoints/user.py index 862f136..4c5e67c 100644 --- a/app/api/endpoints/user.py +++ b/app/api/endpoints/user.py @@ -1,6 +1,7 @@ -from fastapi import APIRouter, HTTPException, Depends +from fastapi import APIRouter, HTTPException, Depends, Response from sqlalchemy.orm import Session from app.models.user import UserLogin, UserInfo, VerifyCodeRequest, UserDB +from app.api.deps import get_current_user from app.models.database import get_db import random import string @@ -9,8 +10,9 @@ from app.core.config import settings from unisdk.sms import UniSMS from unisdk.exception import UniException from datetime import timedelta -from app.core.security import create_access_token -from app.core.response import success_response, error_response +from app.core.security import create_access_token, set_jwt_cookie, clear_jwt_cookie +from app.core.response import success_response, error_response, ResponseModel +from pydantic import BaseModel, Field router = APIRouter() @@ -26,6 +28,10 @@ redis_client = redis.Redis( # 初始化短信客户端 client = UniSMS(settings.UNI_APP_ID) +# 添加 Mock 登录请求模型 +class MockLoginRequest(BaseModel): + phone: str = Field(..., pattern="^1[3-9]\d{9}$") + @router.post("/send-code") async def send_verify_code(request: VerifyCodeRequest): """发送验证码""" @@ -59,7 +65,11 @@ async def send_verify_code(request: VerifyCodeRequest): return error_response(message=f"发送验证码失败: {str(e)}") @router.post("/login") -async def login(user_login: UserLogin, db: Session = Depends(get_db)): +async def login( + user_login: UserLogin, + db: Session = Depends(get_db), + response: Response = None +): """用户登录""" phone = user_login.phone verify_code = user_login.verify_code @@ -84,10 +94,13 @@ async def login(user_login: UserLogin, db: Session = Depends(get_db)): # 创建访问令牌 access_token = create_access_token( - data={"sub": user.phone}, - expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + data={"sub": user.phone} ) + # 设置JWT cookie + if response: + set_jwt_cookie(response, access_token) + return success_response( message="登录成功", data={ @@ -104,4 +117,52 @@ async def get_user_info(phone: str, db: Session = Depends(get_db)): if not user: return error_response(code=404, message="用户不存在") - return success_response(data=UserInfo.model_validate(user)) \ No newline at end of file + return success_response(data=UserInfo.model_validate(user)) + +@router.post("/mock-login", response_model=ResponseModel) +async def mock_login( + request: MockLoginRequest, + db: Session = Depends(get_db), + response: Response = None +): + """Mock登录接口(仅用于开发测试)""" + if not settings.DEBUG: + return error_response(code=403, message="该接口仅在开发环境可用") + + # 查找或创建用户 + user = db.query(UserDB).filter(UserDB.phone == request.phone).first() + if not user: + user = UserDB( + username=f"user_{request.phone[-4:]}", + phone=request.phone + ) + db.add(user) + db.commit() + db.refresh(user) + + # 创建访问令牌 + access_token = create_access_token( + data={"sub": user.phone} + ) + + # 设置JWT cookie + if response: + set_jwt_cookie(response, access_token) + + return success_response( + message="登录成功", + data={ + "user": UserInfo.model_validate(user), + "access_token": access_token, + "token_type": "bearer" + } + ) + +@router.post("/logout", response_model=ResponseModel) +async def logout( + response: Response, + current_user: UserDB = Depends(get_current_user) +): + """退出登录""" + clear_jwt_cookie(response) + return success_response(message="退出登录成功") \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index a271a26..6632db4 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,6 +1,8 @@ +from typing import Optional from pydantic_settings import BaseSettings class Settings(BaseSettings): + DEBUG: bool = True # 开发模式标志 API_V1_STR: str = "/api/v1" PROJECT_NAME: str = "FastAPI 项目" @@ -9,7 +11,7 @@ class Settings(BaseSettings): # JWT 配置 SECRET_KEY: str = "your-secret-key-here" - ACCESS_TOKEN_EXPIRE_MINUTES: int | None = None # None 表示永不过期 + ACCESS_TOKEN_EXPIRE_MINUTES: Optional[int] = None # None 表示永不过期 REDIS_HOST: str = "101.36.120.145" REDIS_PORT: int = 6379 diff --git a/app/core/security.py b/app/core/security.py index c689a55..655dc50 100644 --- a/app/core/security.py +++ b/app/core/security.py @@ -1,17 +1,39 @@ -from datetime import datetime, timedelta, UTC +from datetime import datetime, timedelta, timezone from typing import Optional from jose import JWTError, jwt from app.core.config import settings +from fastapi import Response def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: to_encode = data.copy() if expires_delta is not None: - expire = datetime.now(UTC) + expires_delta + expire = datetime.now(timezone.utc) + expires_delta to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256") return encoded_jwt +def set_jwt_cookie(response: Response, token: str): + """设置JWT cookie""" + response.set_cookie( + key="access_token", + value=f"Bearer {token}", + httponly=True, # 防止JavaScript访问 + secure=not settings.DEBUG, # 生产环境使用HTTPS + samesite="lax", # CSRF保护 + max_age=None if settings.ACCESS_TOKEN_EXPIRE_MINUTES is None + else settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60 + ) + +def clear_jwt_cookie(response: Response): + """清除JWT cookie""" + response.delete_cookie( + key="access_token", + httponly=True, + secure=not settings.DEBUG, + samesite="lax" + ) + def verify_token(token: str) -> Optional[str]: try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) diff --git a/app/main.py b/app/main.py index f69945b..f832a5f 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 +from app.api.endpoints import user, address, community, station, order, coupon from app.models.database import Base, engine from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse @@ -29,6 +29,9 @@ app.add_middleware( app.include_router(user.router, prefix="/api/user", tags=["用户"]) app.include_router(address.router, prefix="/api/address", tags=["配送地址"]) app.include_router(community.router, prefix="/api/community", 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.get("/") async def root(): diff --git a/app/models/address.py b/app/models/address.py index 9b2a890..2b5e3e5 100644 --- a/app/models/address.py +++ b/app/models/address.py @@ -2,6 +2,7 @@ from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Boolean from sqlalchemy.sql import func from pydantic import BaseModel, Field from .database import Base +from typing import Optional # 数据库模型 class AddressDB(Base): @@ -26,11 +27,11 @@ class AddressCreate(BaseModel): is_default: bool = False class AddressUpdate(BaseModel): - community_id: int | None = None - address_detail: str | None = Field(None, max_length=200) - name: str | None = Field(None, max_length=50) - phone: str | None = Field(None, pattern="^1[3-9]\d{9}$") - is_default: bool | None = None + community_id: Optional[int] = None + address_detail: Optional[str] = Field(None, max_length=200) + name: Optional[str] = Field(None, max_length=50) + phone: Optional[str] = Field(None, pattern="^1[3-9]\d{9}$") + is_default: Optional[bool] = None class AddressInfo(BaseModel): id: int diff --git a/app/models/coupon.py b/app/models/coupon.py new file mode 100644 index 0000000..6d4563c --- /dev/null +++ b/app/models/coupon.py @@ -0,0 +1,72 @@ +from datetime import datetime +from typing import Optional +from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Enum +from sqlalchemy.sql import func +from pydantic import BaseModel, Field +from .database import Base +import enum + +class CouponStatus(str, enum.Enum): + UNUSED = "未使用" + USED = "已使用" + EXPIRED = "已过期" + +# 数据库模型 +class CouponDB(Base): + __tablename__ = "coupons" + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String(100), nullable=False) + amount = Column(Float, nullable=False) + create_time = Column(DateTime(timezone=True), server_default=func.now()) + update_time = Column(DateTime(timezone=True), onupdate=func.now()) + +class UserCouponDB(Base): + __tablename__ = "user_coupons" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, ForeignKey("users.userid"), index=True) + coupon_id = Column(Integer, ForeignKey("coupons.id"), index=True) + coupon_name = Column(String(100), nullable=False) + coupon_amount = Column(Float, nullable=False) + expire_time = Column(DateTime(timezone=True), nullable=False) + status = Column(Enum(CouponStatus), default=CouponStatus.UNUSED) + create_time = Column(DateTime(timezone=True), server_default=func.now()) + update_time = Column(DateTime(timezone=True), onupdate=func.now()) + +# Pydantic 模型 +class CouponCreate(BaseModel): + name: str = Field(..., max_length=100) + amount: float = Field(..., gt=0) + +class CouponUpdate(BaseModel): + name: Optional[str] = Field(None, max_length=100) + amount: Optional[float] = Field(None, gt=0) + +class CouponInfo(BaseModel): + id: int + name: str + amount: float + create_time: datetime + + class Config: + from_attributes = True + +class UserCouponCreate(BaseModel): + user_id: int + coupon_id: int + expire_time: datetime + count: int = Field(..., gt=0, description="发放数量") + +class UserCouponInfo(BaseModel): + id: int + user_id: int + coupon_id: int + coupon_name: str + coupon_amount: float + expire_time: datetime + status: CouponStatus + create_time: datetime + + class Config: + from_attributes = True \ No newline at end of file diff --git a/app/models/order.py b/app/models/order.py new file mode 100644 index 0000000..4f66c66 --- /dev/null +++ b/app/models/order.py @@ -0,0 +1,72 @@ +from datetime import datetime +from typing import Optional, List +from sqlalchemy import Column, String, Integer, Float, DateTime, ForeignKey +from sqlalchemy.sql import func +from pydantic import BaseModel, Field +from .database import Base + +# 数据库模型 +class ShippingOrderDB(Base): + __tablename__ = "shipping_orders" + + orderid = Column(String(32), primary_key=True) + userid = Column(Integer, ForeignKey("users.userid"), index=True) + addressid = Column(Integer, ForeignKey("delivery_addresses.id"), index=True) + package_count = Column(Integer, nullable=False) + original_amount = Column(Float, nullable=False) + coupon_discount_amount = Column(Float, default=0) + coupon_id = Column(Integer, ForeignKey("user_coupons.id"), nullable=True) + final_amount = Column(Float, nullable=False) + create_time = Column(DateTime(timezone=True), server_default=func.now()) + +class ShippingOrderPackageDB(Base): + __tablename__ = "shipping_order_packages" + + id = Column(Integer, primary_key=True, autoincrement=True) + orderid = Column(String(32), ForeignKey("shipping_orders.orderid"), index=True) + station_id = Column(Integer, ForeignKey("stations.id"), index=True) + pickup_codes = Column(String(100), nullable=False) + create_time = Column(DateTime(timezone=True), server_default=func.now()) + +# Pydantic 模型 +class OrderPackage(BaseModel): + station_id: int + pickup_codes: str = Field(..., max_length=100) + +class OrderCreate(BaseModel): + addressid: int + package_count: int = Field(..., gt=0) + original_amount: float = Field(..., ge=0) + coupon_id: Optional[int] = None + packages: List[OrderPackage] + +class OrderInfo(BaseModel): + orderid: str + userid: int + addressid: int + package_count: int + original_amount: float + coupon_discount_amount: float + coupon_id: Optional[int] + final_amount: float + create_time: datetime + + class Config: + from_attributes = True + +class OrderPackageInfo(BaseModel): + id: int + orderid: str + station_id: int + pickup_codes: str + create_time: datetime + + class Config: + from_attributes = True + +def generate_order_id() -> str: + """生成订单号:日期+时间戳""" + now = datetime.now() + date_str = now.strftime('%Y%m%d') + timestamp = int(now.timestamp() * 1000) + return f"{date_str}{timestamp}" \ No newline at end of file diff --git a/app/models/station.py b/app/models/station.py new file mode 100644 index 0000000..971815c --- /dev/null +++ b/app/models/station.py @@ -0,0 +1,32 @@ +from typing import Optional +from sqlalchemy import Column, Integer, String, ForeignKey, DateTime +from sqlalchemy.sql import func +from pydantic import BaseModel, Field +from .database import Base + +# 数据库模型 +class StationDB(Base): + __tablename__ = "stations" + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String(100), nullable=False) + community_id = Column(Integer, ForeignKey("communities.id"), index=True) + create_time = Column(DateTime(timezone=True), server_default=func.now()) + update_time = Column(DateTime(timezone=True), onupdate=func.now()) + +# Pydantic 模型 +class StationCreate(BaseModel): + name: str = Field(..., max_length=100) + community_id: int + +class StationUpdate(BaseModel): + name: Optional[str] = Field(None, max_length=100) + community_id: Optional[int] = None + +class StationInfo(BaseModel): + id: int + name: str + community_id: int + + class Config: + from_attributes = True \ No newline at end of file