新增优惠券,订单,驿站等相关接口
This commit is contained in:
parent
772fc25a9d
commit
f096207aeb
@ -1,4 +1,4 @@
|
|||||||
from fastapi import Depends, HTTPException, Header
|
from fastapi import Depends, HTTPException, Header, Cookie
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.models.database import get_db
|
from app.models.database import get_db
|
||||||
@ -7,12 +7,19 @@ from app.core.security import verify_token
|
|||||||
|
|
||||||
async def get_current_user(
|
async def get_current_user(
|
||||||
authorization: Optional[str] = Header(None),
|
authorization: Optional[str] = Header(None),
|
||||||
|
access_token: Optional[str] = Cookie(None),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
) -> UserDB:
|
) -> 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="未提供有效的认证信息")
|
raise HTTPException(status_code=401, detail="未提供有效的认证信息")
|
||||||
|
|
||||||
token = authorization.split(" ")[1]
|
|
||||||
phone = verify_token(token)
|
phone = verify_token(token)
|
||||||
if not phone:
|
if not phone:
|
||||||
raise HTTPException(status_code=401, detail="Token已过期或无效")
|
raise HTTPException(status_code=401, detail="Token已过期或无效")
|
||||||
|
|||||||
149
app/api/endpoints/coupon.py
Normal file
149
app/api/endpoints/coupon.py
Normal file
@ -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]
|
||||||
|
)
|
||||||
135
app/api/endpoints/order.py
Normal file
135
app/api/endpoints/order.py
Normal file
@ -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])
|
||||||
100
app/api/endpoints/station.py
Normal file
100
app/api/endpoints/station.py
Normal file
@ -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]
|
||||||
|
)
|
||||||
@ -1,6 +1,7 @@
|
|||||||
from fastapi import APIRouter, HTTPException, Depends
|
from fastapi import APIRouter, HTTPException, Depends, Response
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.models.user import UserLogin, UserInfo, VerifyCodeRequest, UserDB
|
from app.models.user import UserLogin, UserInfo, VerifyCodeRequest, UserDB
|
||||||
|
from app.api.deps import get_current_user
|
||||||
from app.models.database import get_db
|
from app.models.database import get_db
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
@ -9,8 +10,9 @@ from app.core.config import settings
|
|||||||
from unisdk.sms import UniSMS
|
from unisdk.sms import UniSMS
|
||||||
from unisdk.exception import UniException
|
from unisdk.exception import UniException
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from app.core.security import create_access_token
|
from app.core.security import create_access_token, set_jwt_cookie, clear_jwt_cookie
|
||||||
from app.core.response import success_response, error_response
|
from app.core.response import success_response, error_response, ResponseModel
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@ -26,6 +28,10 @@ redis_client = redis.Redis(
|
|||||||
# 初始化短信客户端
|
# 初始化短信客户端
|
||||||
client = UniSMS(settings.UNI_APP_ID)
|
client = UniSMS(settings.UNI_APP_ID)
|
||||||
|
|
||||||
|
# 添加 Mock 登录请求模型
|
||||||
|
class MockLoginRequest(BaseModel):
|
||||||
|
phone: str = Field(..., pattern="^1[3-9]\d{9}$")
|
||||||
|
|
||||||
@router.post("/send-code")
|
@router.post("/send-code")
|
||||||
async def send_verify_code(request: VerifyCodeRequest):
|
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)}")
|
return error_response(message=f"发送验证码失败: {str(e)}")
|
||||||
|
|
||||||
@router.post("/login")
|
@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
|
phone = user_login.phone
|
||||||
verify_code = user_login.verify_code
|
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(
|
access_token = create_access_token(
|
||||||
data={"sub": user.phone},
|
data={"sub": user.phone}
|
||||||
expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 设置JWT cookie
|
||||||
|
if response:
|
||||||
|
set_jwt_cookie(response, access_token)
|
||||||
|
|
||||||
return success_response(
|
return success_response(
|
||||||
message="登录成功",
|
message="登录成功",
|
||||||
data={
|
data={
|
||||||
@ -105,3 +118,51 @@ async def get_user_info(phone: str, db: Session = Depends(get_db)):
|
|||||||
return error_response(code=404, message="用户不存在")
|
return error_response(code=404, message="用户不存在")
|
||||||
|
|
||||||
return success_response(data=UserInfo.model_validate(user))
|
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="退出登录成功")
|
||||||
@ -1,6 +1,8 @@
|
|||||||
|
from typing import Optional
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
|
DEBUG: bool = True # 开发模式标志
|
||||||
API_V1_STR: str = "/api/v1"
|
API_V1_STR: str = "/api/v1"
|
||||||
PROJECT_NAME: str = "FastAPI 项目"
|
PROJECT_NAME: str = "FastAPI 项目"
|
||||||
|
|
||||||
@ -9,7 +11,7 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
# JWT 配置
|
# JWT 配置
|
||||||
SECRET_KEY: str = "your-secret-key-here"
|
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_HOST: str = "101.36.120.145"
|
||||||
REDIS_PORT: int = 6379
|
REDIS_PORT: int = 6379
|
||||||
|
|||||||
@ -1,17 +1,39 @@
|
|||||||
from datetime import datetime, timedelta, UTC
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
from fastapi import Response
|
||||||
|
|
||||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||||
to_encode = data.copy()
|
to_encode = data.copy()
|
||||||
if expires_delta is not None:
|
if expires_delta is not None:
|
||||||
expire = datetime.now(UTC) + expires_delta
|
expire = datetime.now(timezone.utc) + expires_delta
|
||||||
to_encode.update({"exp": expire})
|
to_encode.update({"exp": expire})
|
||||||
|
|
||||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256")
|
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256")
|
||||||
return encoded_jwt
|
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]:
|
def verify_token(token: str) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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 app.models.database import Base, engine
|
||||||
from fastapi.exceptions import RequestValidationError
|
from fastapi.exceptions import RequestValidationError
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
@ -29,6 +29,9 @@ app.add_middleware(
|
|||||||
app.include_router(user.router, prefix="/api/user", tags=["用户"])
|
app.include_router(user.router, prefix="/api/user", tags=["用户"])
|
||||||
app.include_router(address.router, prefix="/api/address", tags=["配送地址"])
|
app.include_router(address.router, prefix="/api/address", tags=["配送地址"])
|
||||||
app.include_router(community.router, prefix="/api/community", 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("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Boolean
|
|||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from .database import Base
|
from .database import Base
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
# 数据库模型
|
# 数据库模型
|
||||||
class AddressDB(Base):
|
class AddressDB(Base):
|
||||||
@ -26,11 +27,11 @@ class AddressCreate(BaseModel):
|
|||||||
is_default: bool = False
|
is_default: bool = False
|
||||||
|
|
||||||
class AddressUpdate(BaseModel):
|
class AddressUpdate(BaseModel):
|
||||||
community_id: int | None = None
|
community_id: Optional[int] = None
|
||||||
address_detail: str | None = Field(None, max_length=200)
|
address_detail: Optional[str] = Field(None, max_length=200)
|
||||||
name: str | None = Field(None, max_length=50)
|
name: Optional[str] = Field(None, max_length=50)
|
||||||
phone: str | None = Field(None, pattern="^1[3-9]\d{9}$")
|
phone: Optional[str] = Field(None, pattern="^1[3-9]\d{9}$")
|
||||||
is_default: bool | None = None
|
is_default: Optional[bool] = None
|
||||||
|
|
||||||
class AddressInfo(BaseModel):
|
class AddressInfo(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
|
|||||||
72
app/models/coupon.py
Normal file
72
app/models/coupon.py
Normal file
@ -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
|
||||||
72
app/models/order.py
Normal file
72
app/models/order.py
Normal file
@ -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}"
|
||||||
32
app/models/station.py
Normal file
32
app/models/station.py
Normal file
@ -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
|
||||||
Loading…
Reference in New Issue
Block a user