Backend: - Add auth.py for JWT token verification - Update main.py to protect all routes with auth middleware - Remove dashboard router (frontend handles aggregation) - Add Docker support with Dockerfile and docker-compose.yml Frontend: - Add OIDC authentication using oidc-client-ts with PKCE flow - Create router.js with auth guards for automatic login/logout - Add api.js for unified Axios instance with auth headers - Add composables: useAuth.js, useVehicleData.js for caching - Add views/Main.vue as main application page - Simplify App.vue to router-view container - Add deploy-web.sh deployment script Documentation: - Update AGENTS.md with new architecture and auth flow
263 lines
5.9 KiB
Python
263 lines
5.9 KiB
Python
"""
|
|
统一的 Pydantic 数据模型
|
|
"""
|
|
|
|
from pydantic import BaseModel, field_validator, ConfigDict
|
|
from typing import Optional, List, Union
|
|
from datetime import date as date_cls, datetime as datetime_cls
|
|
|
|
|
|
# ==================== 车辆相关模型 ====================
|
|
|
|
|
|
class VehicleBase(BaseModel):
|
|
"""车辆基础模型"""
|
|
|
|
name: str
|
|
purchase_date: Optional[date_cls] = None
|
|
initial_mileage: int = 0
|
|
|
|
|
|
class VehicleCreate(VehicleBase):
|
|
"""创建车辆请求模型"""
|
|
|
|
pass
|
|
|
|
|
|
class VehicleUpdate(BaseModel):
|
|
"""更新车辆请求模型"""
|
|
|
|
id: int
|
|
name: Optional[str] = None
|
|
purchase_date: Optional[date_cls] = None
|
|
initial_mileage: Optional[int] = None
|
|
|
|
|
|
class VehicleDelete(BaseModel):
|
|
"""删除车辆请求模型"""
|
|
|
|
id: int
|
|
|
|
|
|
class VehicleResponse(VehicleBase):
|
|
"""车辆响应模型"""
|
|
|
|
id: int
|
|
is_deleted: bool = False
|
|
created_at: Optional[datetime_cls] = None
|
|
updated_at: Optional[datetime_cls] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class VehicleStats(BaseModel):
|
|
"""车辆统计信息"""
|
|
|
|
total_mileage: int
|
|
total_fuel_cost: float
|
|
total_fuel_amount: float
|
|
avg_fuel_consumption: Optional[float]
|
|
|
|
|
|
# ==================== 加油记录相关模型 ====================
|
|
|
|
|
|
class FuelRecordBase(BaseModel):
|
|
"""加油记录基础模型"""
|
|
|
|
vehicle_id: int
|
|
date: date_cls
|
|
mileage: int
|
|
fuel_amount: float
|
|
fuel_price: Optional[float] = None
|
|
display_cost: Optional[float] = None # 机显总价
|
|
actual_cost: float # 实付金额
|
|
is_full_tank: bool = True
|
|
notes: Optional[str] = ""
|
|
|
|
|
|
class FuelRecordCreate(FuelRecordBase):
|
|
"""创建加油记录请求模型"""
|
|
|
|
@field_validator("fuel_price", "display_cost", "actual_cost", mode="before")
|
|
@classmethod
|
|
def validate_costs(cls, v):
|
|
if v is not None:
|
|
return round(v, 2)
|
|
return v
|
|
|
|
|
|
class FuelRecordUpdate(BaseModel):
|
|
"""更新加油记录请求模型"""
|
|
|
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
|
|
id: int
|
|
date: Union[date_cls, str, None] = None
|
|
mileage: Optional[int] = None
|
|
fuel_amount: Optional[float] = None
|
|
fuel_price: Optional[float] = None
|
|
display_cost: Optional[float] = None # 机显总价
|
|
actual_cost: Optional[float] = None # 实付金额
|
|
is_full_tank: Optional[bool] = None
|
|
notes: Optional[str] = None
|
|
|
|
@field_validator("date", mode="before")
|
|
@classmethod
|
|
def parse_date(cls, v):
|
|
if v is None:
|
|
return None
|
|
if isinstance(v, date_cls):
|
|
return v
|
|
if isinstance(v, str):
|
|
return date_cls.fromisoformat(v)
|
|
raise ValueError("Invalid date format")
|
|
|
|
|
|
class FuelRecordDelete(BaseModel):
|
|
"""删除加油记录请求模型"""
|
|
|
|
id: int
|
|
|
|
|
|
class FuelRecordResponse(BaseModel):
|
|
"""加油记录响应模型"""
|
|
|
|
id: int
|
|
vehicle_id: int
|
|
date: date_cls
|
|
mileage: int
|
|
fuel_amount: float
|
|
fuel_price: Optional[float]
|
|
display_cost: Optional[float] # 机显总价
|
|
actual_cost: float # 实付金额
|
|
is_full_tank: bool
|
|
notes: str
|
|
fuel_consumption: Optional[float] = None # 单次油耗,计算得出
|
|
is_deleted: bool = False
|
|
created_at: Optional[datetime_cls] = None
|
|
updated_at: Optional[datetime_cls] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ==================== 费用记录相关模型 ====================
|
|
|
|
# 费用类型枚举
|
|
COST_TYPES = ["保养", "维修", "保险", "停车", "过路费", "洗车", "违章", "其他"]
|
|
|
|
|
|
class CostBase(BaseModel):
|
|
"""费用记录基础模型"""
|
|
|
|
vehicle_id: int
|
|
date: date_cls
|
|
type: str
|
|
amount: float
|
|
mileage: Optional[int] = None
|
|
notes: Optional[str] = ""
|
|
is_installment: Optional[bool] = False
|
|
installment_months: Optional[int] = 12
|
|
|
|
|
|
class CostCreate(CostBase):
|
|
"""创建费用记录请求模型"""
|
|
|
|
pass
|
|
|
|
|
|
class CostUpdate(BaseModel):
|
|
"""更新费用记录请求模型"""
|
|
|
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
|
|
id: int
|
|
date: Union[date_cls, str, None] = None
|
|
type: Optional[str] = None
|
|
amount: Optional[float] = None
|
|
mileage: Optional[int] = None
|
|
notes: Optional[str] = None
|
|
is_installment: Optional[bool] = None
|
|
installment_months: Optional[int] = None
|
|
|
|
@field_validator("date", mode="before")
|
|
@classmethod
|
|
def parse_date(cls, v):
|
|
if v is None:
|
|
return None
|
|
if isinstance(v, date_cls):
|
|
return v
|
|
if isinstance(v, str):
|
|
return date_cls.fromisoformat(v)
|
|
raise ValueError("Invalid date format")
|
|
|
|
|
|
class CostDelete(BaseModel):
|
|
"""删除费用记录请求模型"""
|
|
|
|
id: int
|
|
|
|
|
|
class CostResponse(BaseModel):
|
|
"""费用记录响应模型"""
|
|
|
|
id: int
|
|
vehicle_id: int
|
|
date: date_cls
|
|
type: str
|
|
amount: float
|
|
mileage: Optional[int]
|
|
notes: str
|
|
is_installment: bool
|
|
installment_months: int
|
|
is_deleted: bool = False
|
|
created_at: Optional[datetime_cls] = None
|
|
updated_at: Optional[datetime_cls] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ==================== 仪表板相关模型 ====================
|
|
|
|
|
|
class FuelRecordItem(BaseModel):
|
|
"""仪表板中使用的加油记录项"""
|
|
|
|
id: int
|
|
date: date_cls
|
|
mileage: int
|
|
fuel_amount: float
|
|
actual_cost: float # 实付金额(用于统计)
|
|
fuel_consumption: Optional[float]
|
|
|
|
|
|
class DashboardData(BaseModel):
|
|
"""仪表板数据响应"""
|
|
|
|
vehicle_id: int
|
|
vehicle_name: str
|
|
purchase_date: Optional[date_cls]
|
|
total_mileage: int
|
|
total_fuel_cost: float
|
|
avg_fuel_consumption: Optional[float]
|
|
avg_daily_km: Optional[float]
|
|
recent_fuel_records: List[FuelRecordItem]
|
|
|
|
|
|
# ==================== 通用响应模型 ====================
|
|
|
|
|
|
class MessageResponse(BaseModel):
|
|
"""通用消息响应"""
|
|
|
|
message: str
|
|
|
|
|
|
class CostTypesResponse(BaseModel):
|
|
"""费用类型列表响应"""
|
|
|
|
types: List[str]
|