新增优惠券,订单,驿站等相关接口

This commit is contained in:
aaron 2025-01-04 22:55:13 +08:00
parent 772fc25a9d
commit f096207aeb
12 changed files with 675 additions and 19 deletions

View File

@ -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已过期或无效")

149
app/api/endpoints/coupon.py Normal file
View 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
View 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])

View 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]
)

View File

@ -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))
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="退出登录成功")

View File

@ -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

View File

@ -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"])

View File

@ -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():

View File

@ -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

72
app/models/coupon.py Normal file
View 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
View 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
View 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