carcost/api/schemas.py
yuany3721 71e11eaf30 feat: Add OIDC authentication with Authentik and refactor project structure
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
2026-04-12 13:31:27 +08:00

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]