Merge commit '026c837b919ab4380e8a6e6c052364bbf9bbe8a3' as 'core'
This commit is contained in:
716
core/backend/app/api/v1/charging.py
Normal file
716
core/backend/app/api/v1/charging.py
Normal file
@@ -0,0 +1,716 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, and_
|
||||
from pydantic import BaseModel
|
||||
from app.core.database import get_db
|
||||
from app.core.deps import get_current_user, require_roles
|
||||
from app.models.charging import (
|
||||
ChargingStation, ChargingPile, ChargingPriceStrategy, ChargingPriceParam,
|
||||
ChargingOrder, OccupancyOrder, ChargingBrand, ChargingMerchant,
|
||||
)
|
||||
from app.models.user import User
|
||||
from app.services.audit import log_audit
|
||||
|
||||
router = APIRouter(prefix="/charging", tags=["充电管理"])
|
||||
|
||||
|
||||
# ─── Pydantic Schemas ───────────────────────────────────────────────
|
||||
|
||||
class StationCreate(BaseModel):
|
||||
name: str
|
||||
merchant_id: int | None = None
|
||||
type: str | None = None
|
||||
address: str | None = None
|
||||
latitude: float | None = None
|
||||
longitude: float | None = None
|
||||
price: float | None = None
|
||||
activity: str | None = None
|
||||
status: str = "active"
|
||||
total_piles: int = 0
|
||||
available_piles: int = 0
|
||||
total_power_kw: float = 0
|
||||
photo_url: str | None = None
|
||||
operating_hours: str | None = None
|
||||
|
||||
|
||||
class StationUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
merchant_id: int | None = None
|
||||
type: str | None = None
|
||||
address: str | None = None
|
||||
latitude: float | None = None
|
||||
longitude: float | None = None
|
||||
price: float | None = None
|
||||
activity: str | None = None
|
||||
status: str | None = None
|
||||
total_piles: int | None = None
|
||||
available_piles: int | None = None
|
||||
total_power_kw: float | None = None
|
||||
photo_url: str | None = None
|
||||
operating_hours: str | None = None
|
||||
|
||||
|
||||
class PileCreate(BaseModel):
|
||||
station_id: int
|
||||
encoding: str
|
||||
name: str | None = None
|
||||
type: str | None = None
|
||||
brand: str | None = None
|
||||
model: str | None = None
|
||||
rated_power_kw: float | None = None
|
||||
connector_type: str | None = None
|
||||
status: str = "active"
|
||||
work_status: str = "offline"
|
||||
|
||||
|
||||
class PileUpdate(BaseModel):
|
||||
station_id: int | None = None
|
||||
encoding: str | None = None
|
||||
name: str | None = None
|
||||
type: str | None = None
|
||||
brand: str | None = None
|
||||
model: str | None = None
|
||||
rated_power_kw: float | None = None
|
||||
connector_type: str | None = None
|
||||
status: str | None = None
|
||||
work_status: str | None = None
|
||||
|
||||
|
||||
class PriceParamCreate(BaseModel):
|
||||
start_time: str
|
||||
end_time: str
|
||||
period_mark: str | None = None
|
||||
elec_price: float
|
||||
service_price: float = 0
|
||||
|
||||
|
||||
class PriceStrategyCreate(BaseModel):
|
||||
strategy_name: str
|
||||
station_id: int | None = None
|
||||
bill_model: str | None = None
|
||||
description: str | None = None
|
||||
status: str = "inactive"
|
||||
params: list[PriceParamCreate] = []
|
||||
|
||||
|
||||
class PriceStrategyUpdate(BaseModel):
|
||||
strategy_name: str | None = None
|
||||
station_id: int | None = None
|
||||
bill_model: str | None = None
|
||||
description: str | None = None
|
||||
status: str | None = None
|
||||
params: list[PriceParamCreate] | None = None
|
||||
|
||||
|
||||
class MerchantCreate(BaseModel):
|
||||
name: str
|
||||
contact_person: str | None = None
|
||||
phone: str | None = None
|
||||
email: str | None = None
|
||||
address: str | None = None
|
||||
business_license: str | None = None
|
||||
status: str = "active"
|
||||
settlement_type: str | None = None
|
||||
|
||||
|
||||
class BrandCreate(BaseModel):
|
||||
brand_name: str
|
||||
logo_url: str | None = None
|
||||
country: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
# ─── Station Endpoints ───────────────────────────────────────────────
|
||||
|
||||
@router.get("/stations")
|
||||
async def list_stations(
|
||||
status: str | None = None,
|
||||
type: str | None = None,
|
||||
merchant_id: int | None = None,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
query = select(ChargingStation)
|
||||
if status:
|
||||
query = query.where(ChargingStation.status == status)
|
||||
if type:
|
||||
query = query.where(ChargingStation.type == type)
|
||||
if merchant_id:
|
||||
query = query.where(ChargingStation.merchant_id == merchant_id)
|
||||
|
||||
count_q = select(func.count()).select_from(query.subquery())
|
||||
total = (await db.execute(count_q)).scalar()
|
||||
|
||||
query = query.order_by(ChargingStation.id.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||
result = await db.execute(query)
|
||||
stations = result.scalars().all()
|
||||
return {"total": total, "items": [_station_to_dict(s) for s in stations]}
|
||||
|
||||
|
||||
@router.post("/stations")
|
||||
async def create_station(
|
||||
data: StationCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_roles("admin", "energy_manager")),
|
||||
):
|
||||
station = ChargingStation(**data.model_dump(), created_by=user.id)
|
||||
db.add(station)
|
||||
await db.flush()
|
||||
await log_audit(db, user.id, "create", "charging", detail=f"创建充电站 {data.name}")
|
||||
return _station_to_dict(station)
|
||||
|
||||
|
||||
@router.put("/stations/{station_id}")
|
||||
async def update_station(
|
||||
station_id: int,
|
||||
data: StationUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_roles("admin", "energy_manager")),
|
||||
):
|
||||
result = await db.execute(select(ChargingStation).where(ChargingStation.id == station_id))
|
||||
station = result.scalar_one_or_none()
|
||||
if not station:
|
||||
raise HTTPException(status_code=404, detail="充电站不存在")
|
||||
updates = data.model_dump(exclude_unset=True)
|
||||
for k, v in updates.items():
|
||||
setattr(station, k, v)
|
||||
await log_audit(db, user.id, "update", "charging", detail=f"更新充电站 {station.name}")
|
||||
return _station_to_dict(station)
|
||||
|
||||
|
||||
@router.delete("/stations/{station_id}")
|
||||
async def delete_station(
|
||||
station_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_roles("admin", "energy_manager")),
|
||||
):
|
||||
result = await db.execute(select(ChargingStation).where(ChargingStation.id == station_id))
|
||||
station = result.scalar_one_or_none()
|
||||
if not station:
|
||||
raise HTTPException(status_code=404, detail="充电站不存在")
|
||||
station.status = "disabled"
|
||||
await log_audit(db, user.id, "delete", "charging", detail=f"禁用充电站 {station.name}")
|
||||
return {"message": "已禁用"}
|
||||
|
||||
|
||||
@router.get("/stations/{station_id}/piles")
|
||||
async def list_station_piles(
|
||||
station_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(ChargingPile).where(ChargingPile.station_id == station_id).order_by(ChargingPile.id)
|
||||
)
|
||||
return [_pile_to_dict(p) for p in result.scalars().all()]
|
||||
|
||||
|
||||
# ─── Pile Endpoints ──────────────────────────────────────────────────
|
||||
|
||||
@router.get("/piles")
|
||||
async def list_piles(
|
||||
station_id: int | None = None,
|
||||
status: str | None = None,
|
||||
work_status: str | None = None,
|
||||
type: str | None = None,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
query = select(ChargingPile)
|
||||
if station_id:
|
||||
query = query.where(ChargingPile.station_id == station_id)
|
||||
if status:
|
||||
query = query.where(ChargingPile.status == status)
|
||||
if work_status:
|
||||
query = query.where(ChargingPile.work_status == work_status)
|
||||
if type:
|
||||
query = query.where(ChargingPile.type == type)
|
||||
|
||||
count_q = select(func.count()).select_from(query.subquery())
|
||||
total = (await db.execute(count_q)).scalar()
|
||||
|
||||
query = query.order_by(ChargingPile.id.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||
result = await db.execute(query)
|
||||
piles = result.scalars().all()
|
||||
return {"total": total, "items": [_pile_to_dict(p) for p in piles]}
|
||||
|
||||
|
||||
@router.post("/piles")
|
||||
async def create_pile(
|
||||
data: PileCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_roles("admin", "energy_manager")),
|
||||
):
|
||||
pile = ChargingPile(**data.model_dump())
|
||||
db.add(pile)
|
||||
await db.flush()
|
||||
await log_audit(db, user.id, "create", "charging", detail=f"创建充电桩 {data.encoding}")
|
||||
return _pile_to_dict(pile)
|
||||
|
||||
|
||||
@router.put("/piles/{pile_id}")
|
||||
async def update_pile(
|
||||
pile_id: int,
|
||||
data: PileUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_roles("admin", "energy_manager")),
|
||||
):
|
||||
result = await db.execute(select(ChargingPile).where(ChargingPile.id == pile_id))
|
||||
pile = result.scalar_one_or_none()
|
||||
if not pile:
|
||||
raise HTTPException(status_code=404, detail="充电桩不存在")
|
||||
updates = data.model_dump(exclude_unset=True)
|
||||
for k, v in updates.items():
|
||||
setattr(pile, k, v)
|
||||
await log_audit(db, user.id, "update", "charging", detail=f"更新充电桩 {pile.encoding}")
|
||||
return _pile_to_dict(pile)
|
||||
|
||||
|
||||
@router.delete("/piles/{pile_id}")
|
||||
async def delete_pile(
|
||||
pile_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_roles("admin", "energy_manager")),
|
||||
):
|
||||
result = await db.execute(select(ChargingPile).where(ChargingPile.id == pile_id))
|
||||
pile = result.scalar_one_or_none()
|
||||
if not pile:
|
||||
raise HTTPException(status_code=404, detail="充电桩不存在")
|
||||
pile.status = "disabled"
|
||||
await log_audit(db, user.id, "delete", "charging", detail=f"禁用充电桩 {pile.encoding}")
|
||||
return {"message": "已禁用"}
|
||||
|
||||
|
||||
# ─── Pricing Endpoints ───────────────────────────────────────────────
|
||||
|
||||
@router.get("/pricing")
|
||||
async def list_pricing(
|
||||
station_id: int | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
query = select(ChargingPriceStrategy)
|
||||
if station_id:
|
||||
query = query.where(ChargingPriceStrategy.station_id == station_id)
|
||||
result = await db.execute(query.order_by(ChargingPriceStrategy.id.desc()))
|
||||
strategies = result.scalars().all()
|
||||
|
||||
items = []
|
||||
for s in strategies:
|
||||
params_q = await db.execute(
|
||||
select(ChargingPriceParam).where(ChargingPriceParam.strategy_id == s.id).order_by(ChargingPriceParam.start_time)
|
||||
)
|
||||
params = [_param_to_dict(p) for p in params_q.scalars().all()]
|
||||
d = _strategy_to_dict(s)
|
||||
d["params"] = params
|
||||
items.append(d)
|
||||
return items
|
||||
|
||||
|
||||
@router.post("/pricing")
|
||||
async def create_pricing(
|
||||
data: PriceStrategyCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_roles("admin", "energy_manager")),
|
||||
):
|
||||
strategy = ChargingPriceStrategy(
|
||||
strategy_name=data.strategy_name,
|
||||
station_id=data.station_id,
|
||||
bill_model=data.bill_model,
|
||||
description=data.description,
|
||||
status=data.status,
|
||||
)
|
||||
db.add(strategy)
|
||||
await db.flush()
|
||||
|
||||
for p in data.params:
|
||||
param = ChargingPriceParam(strategy_id=strategy.id, **p.model_dump())
|
||||
db.add(param)
|
||||
await db.flush()
|
||||
|
||||
await log_audit(db, user.id, "create", "charging", detail=f"创建计费策略 {data.strategy_name}")
|
||||
return _strategy_to_dict(strategy)
|
||||
|
||||
|
||||
@router.put("/pricing/{strategy_id}")
|
||||
async def update_pricing(
|
||||
strategy_id: int,
|
||||
data: PriceStrategyUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_roles("admin", "energy_manager")),
|
||||
):
|
||||
result = await db.execute(select(ChargingPriceStrategy).where(ChargingPriceStrategy.id == strategy_id))
|
||||
strategy = result.scalar_one_or_none()
|
||||
if not strategy:
|
||||
raise HTTPException(status_code=404, detail="计费策略不存在")
|
||||
|
||||
updates = data.model_dump(exclude_unset=True, exclude={"params"})
|
||||
for k, v in updates.items():
|
||||
setattr(strategy, k, v)
|
||||
|
||||
if data.params is not None:
|
||||
# Delete old params and recreate
|
||||
old_params = await db.execute(
|
||||
select(ChargingPriceParam).where(ChargingPriceParam.strategy_id == strategy_id)
|
||||
)
|
||||
for old in old_params.scalars().all():
|
||||
await db.delete(old)
|
||||
await db.flush()
|
||||
|
||||
for p in data.params:
|
||||
param = ChargingPriceParam(strategy_id=strategy_id, **p.model_dump())
|
||||
db.add(param)
|
||||
await db.flush()
|
||||
|
||||
await log_audit(db, user.id, "update", "charging", detail=f"更新计费策略 {strategy.strategy_name}")
|
||||
return _strategy_to_dict(strategy)
|
||||
|
||||
|
||||
@router.delete("/pricing/{strategy_id}")
|
||||
async def delete_pricing(
|
||||
strategy_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_roles("admin", "energy_manager")),
|
||||
):
|
||||
result = await db.execute(select(ChargingPriceStrategy).where(ChargingPriceStrategy.id == strategy_id))
|
||||
strategy = result.scalar_one_or_none()
|
||||
if not strategy:
|
||||
raise HTTPException(status_code=404, detail="计费策略不存在")
|
||||
strategy.status = "inactive"
|
||||
await log_audit(db, user.id, "delete", "charging", detail=f"停用计费策略 {strategy.strategy_name}")
|
||||
return {"message": "已停用"}
|
||||
|
||||
|
||||
# ─── Order Endpoints ─────────────────────────────────────────────────
|
||||
|
||||
@router.get("/orders")
|
||||
async def list_orders(
|
||||
order_status: str | None = None,
|
||||
station_id: int | None = None,
|
||||
start_date: str | None = None,
|
||||
end_date: str | None = None,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
query = select(ChargingOrder)
|
||||
if order_status:
|
||||
query = query.where(ChargingOrder.order_status == order_status)
|
||||
if station_id:
|
||||
query = query.where(ChargingOrder.station_id == station_id)
|
||||
if start_date:
|
||||
query = query.where(ChargingOrder.created_at >= start_date)
|
||||
if end_date:
|
||||
query = query.where(ChargingOrder.created_at <= end_date)
|
||||
|
||||
count_q = select(func.count()).select_from(query.subquery())
|
||||
total = (await db.execute(count_q)).scalar()
|
||||
|
||||
query = query.order_by(ChargingOrder.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||
result = await db.execute(query)
|
||||
orders = result.scalars().all()
|
||||
return {"total": total, "items": [_order_to_dict(o) for o in orders]}
|
||||
|
||||
|
||||
@router.get("/orders/realtime")
|
||||
async def realtime_orders(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(ChargingOrder).where(ChargingOrder.order_status == "charging").order_by(ChargingOrder.start_time.desc())
|
||||
)
|
||||
return [_order_to_dict(o) for o in result.scalars().all()]
|
||||
|
||||
|
||||
@router.get("/orders/abnormal")
|
||||
async def abnormal_orders(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
query = select(ChargingOrder).where(ChargingOrder.order_status.in_(["failed", "refunded"]))
|
||||
count_q = select(func.count()).select_from(query.subquery())
|
||||
total = (await db.execute(count_q)).scalar()
|
||||
|
||||
query = query.order_by(ChargingOrder.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||
result = await db.execute(query)
|
||||
return {"total": total, "items": [_order_to_dict(o) for o in result.scalars().all()]}
|
||||
|
||||
|
||||
@router.get("/orders/{order_id}")
|
||||
async def get_order(
|
||||
order_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
result = await db.execute(select(ChargingOrder).where(ChargingOrder.id == order_id))
|
||||
order = result.scalar_one_or_none()
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
return _order_to_dict(order)
|
||||
|
||||
|
||||
@router.post("/orders/{order_id}/settle")
|
||||
async def settle_order(
|
||||
order_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_roles("admin", "energy_manager")),
|
||||
):
|
||||
result = await db.execute(select(ChargingOrder).where(ChargingOrder.id == order_id))
|
||||
order = result.scalar_one_or_none()
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
order.settle_type = "manual"
|
||||
order.settle_time = datetime.now(timezone.utc)
|
||||
order.order_status = "completed"
|
||||
await log_audit(db, user.id, "update", "charging", detail=f"手动结算订单 {order.order_no}")
|
||||
return {"message": "已结算"}
|
||||
|
||||
|
||||
# ─── Dashboard ───────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/dashboard")
|
||||
async def charging_dashboard(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
now = datetime.now(timezone.utc)
|
||||
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
# Total revenue (completed orders)
|
||||
rev_q = await db.execute(
|
||||
select(func.sum(ChargingOrder.paid_price)).where(ChargingOrder.order_status == "completed")
|
||||
)
|
||||
total_revenue = rev_q.scalar() or 0
|
||||
|
||||
# Total energy delivered
|
||||
energy_q = await db.execute(
|
||||
select(func.sum(ChargingOrder.energy)).where(ChargingOrder.order_status == "completed")
|
||||
)
|
||||
total_energy = energy_q.scalar() or 0
|
||||
|
||||
# Active sessions
|
||||
active_q = await db.execute(
|
||||
select(func.count(ChargingOrder.id)).where(ChargingOrder.order_status == "charging")
|
||||
)
|
||||
active_sessions = active_q.scalar() or 0
|
||||
|
||||
# Utilization rate: charging piles / total active piles
|
||||
total_piles_q = await db.execute(
|
||||
select(func.count(ChargingPile.id)).where(ChargingPile.status == "active")
|
||||
)
|
||||
total_piles = total_piles_q.scalar() or 0
|
||||
charging_piles_q = await db.execute(
|
||||
select(func.count(ChargingPile.id)).where(ChargingPile.work_status == "charging")
|
||||
)
|
||||
charging_piles = charging_piles_q.scalar() or 0
|
||||
utilization_rate = round(charging_piles / total_piles * 100, 1) if total_piles > 0 else 0
|
||||
|
||||
# Revenue trend (last 30 days)
|
||||
thirty_days_ago = now - timedelta(days=30)
|
||||
trend_q = await db.execute(
|
||||
select(
|
||||
func.date(ChargingOrder.created_at).label("date"),
|
||||
func.sum(ChargingOrder.paid_price).label("revenue"),
|
||||
func.sum(ChargingOrder.energy).label("energy"),
|
||||
).where(
|
||||
and_(ChargingOrder.order_status == "completed", ChargingOrder.created_at >= thirty_days_ago)
|
||||
).group_by(func.date(ChargingOrder.created_at)).order_by(func.date(ChargingOrder.created_at))
|
||||
)
|
||||
revenue_trend = [{"date": str(r[0]), "revenue": round(r[1] or 0, 2), "energy": round(r[2] or 0, 2)} for r in trend_q.all()]
|
||||
|
||||
# Station ranking by revenue
|
||||
ranking_q = await db.execute(
|
||||
select(
|
||||
ChargingOrder.station_name,
|
||||
func.sum(ChargingOrder.paid_price).label("revenue"),
|
||||
func.count(ChargingOrder.id).label("orders"),
|
||||
).where(ChargingOrder.order_status == "completed")
|
||||
.group_by(ChargingOrder.station_name)
|
||||
.order_by(func.sum(ChargingOrder.paid_price).desc())
|
||||
.limit(10)
|
||||
)
|
||||
station_ranking = [{"station": r[0] or "未知", "revenue": round(r[1] or 0, 2), "orders": r[2]} for r in ranking_q.all()]
|
||||
|
||||
# Pile status distribution
|
||||
pile_status_q = await db.execute(
|
||||
select(ChargingPile.work_status, func.count(ChargingPile.id))
|
||||
.where(ChargingPile.status == "active")
|
||||
.group_by(ChargingPile.work_status)
|
||||
)
|
||||
pile_status = {row[0]: row[1] for row in pile_status_q.all()}
|
||||
|
||||
return {
|
||||
"total_revenue": round(total_revenue, 2),
|
||||
"total_energy": round(total_energy, 2),
|
||||
"active_sessions": active_sessions,
|
||||
"utilization_rate": utilization_rate,
|
||||
"revenue_trend": revenue_trend,
|
||||
"station_ranking": station_ranking,
|
||||
"pile_status": {
|
||||
"idle": pile_status.get("idle", 0),
|
||||
"charging": pile_status.get("charging", 0),
|
||||
"fault": pile_status.get("fault", 0),
|
||||
"offline": pile_status.get("offline", 0),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── Merchant CRUD ───────────────────────────────────────────────────
|
||||
|
||||
@router.get("/merchants")
|
||||
async def list_merchants(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
result = await db.execute(select(ChargingMerchant).order_by(ChargingMerchant.id.desc()))
|
||||
return [_merchant_to_dict(m) for m in result.scalars().all()]
|
||||
|
||||
|
||||
@router.post("/merchants")
|
||||
async def create_merchant(data: MerchantCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager"))):
|
||||
merchant = ChargingMerchant(**data.model_dump())
|
||||
db.add(merchant)
|
||||
await db.flush()
|
||||
return _merchant_to_dict(merchant)
|
||||
|
||||
|
||||
@router.put("/merchants/{merchant_id}")
|
||||
async def update_merchant(merchant_id: int, data: MerchantCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager"))):
|
||||
result = await db.execute(select(ChargingMerchant).where(ChargingMerchant.id == merchant_id))
|
||||
merchant = result.scalar_one_or_none()
|
||||
if not merchant:
|
||||
raise HTTPException(status_code=404, detail="运营商不存在")
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(merchant, k, v)
|
||||
return _merchant_to_dict(merchant)
|
||||
|
||||
|
||||
@router.delete("/merchants/{merchant_id}")
|
||||
async def delete_merchant(merchant_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager"))):
|
||||
result = await db.execute(select(ChargingMerchant).where(ChargingMerchant.id == merchant_id))
|
||||
merchant = result.scalar_one_or_none()
|
||||
if not merchant:
|
||||
raise HTTPException(status_code=404, detail="运营商不存在")
|
||||
merchant.status = "disabled"
|
||||
return {"message": "已禁用"}
|
||||
|
||||
|
||||
# ─── Brand CRUD ──────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/brands")
|
||||
async def list_brands(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
result = await db.execute(select(ChargingBrand).order_by(ChargingBrand.id.desc()))
|
||||
return [_brand_to_dict(b) for b in result.scalars().all()]
|
||||
|
||||
|
||||
@router.post("/brands")
|
||||
async def create_brand(data: BrandCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager"))):
|
||||
brand = ChargingBrand(**data.model_dump())
|
||||
db.add(brand)
|
||||
await db.flush()
|
||||
return _brand_to_dict(brand)
|
||||
|
||||
|
||||
@router.put("/brands/{brand_id}")
|
||||
async def update_brand(brand_id: int, data: BrandCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager"))):
|
||||
result = await db.execute(select(ChargingBrand).where(ChargingBrand.id == brand_id))
|
||||
brand = result.scalar_one_or_none()
|
||||
if not brand:
|
||||
raise HTTPException(status_code=404, detail="品牌不存在")
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(brand, k, v)
|
||||
return _brand_to_dict(brand)
|
||||
|
||||
|
||||
@router.delete("/brands/{brand_id}")
|
||||
async def delete_brand(brand_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager"))):
|
||||
result = await db.execute(select(ChargingBrand).where(ChargingBrand.id == brand_id))
|
||||
brand = result.scalar_one_or_none()
|
||||
if not brand:
|
||||
raise HTTPException(status_code=404, detail="品牌不存在")
|
||||
await db.delete(brand)
|
||||
return {"message": "已删除"}
|
||||
|
||||
|
||||
# ─── Dict Helpers ────────────────────────────────────────────────────
|
||||
|
||||
def _station_to_dict(s: ChargingStation) -> dict:
|
||||
return {
|
||||
"id": s.id, "name": s.name, "merchant_id": s.merchant_id, "type": s.type,
|
||||
"address": s.address, "latitude": s.latitude, "longitude": s.longitude,
|
||||
"price": s.price, "activity": s.activity, "status": s.status,
|
||||
"total_piles": s.total_piles, "available_piles": s.available_piles,
|
||||
"total_power_kw": s.total_power_kw, "photo_url": s.photo_url,
|
||||
"operating_hours": s.operating_hours, "created_by": s.created_by,
|
||||
"created_at": str(s.created_at) if s.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
def _pile_to_dict(p: ChargingPile) -> dict:
|
||||
return {
|
||||
"id": p.id, "station_id": p.station_id, "encoding": p.encoding,
|
||||
"name": p.name, "type": p.type, "brand": p.brand, "model": p.model,
|
||||
"rated_power_kw": p.rated_power_kw, "connector_type": p.connector_type,
|
||||
"status": p.status, "work_status": p.work_status,
|
||||
"created_at": str(p.created_at) if p.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
def _strategy_to_dict(s: ChargingPriceStrategy) -> dict:
|
||||
return {
|
||||
"id": s.id, "strategy_name": s.strategy_name, "station_id": s.station_id,
|
||||
"bill_model": s.bill_model, "description": s.description, "status": s.status,
|
||||
"created_at": str(s.created_at) if s.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
def _param_to_dict(p: ChargingPriceParam) -> dict:
|
||||
return {
|
||||
"id": p.id, "strategy_id": p.strategy_id, "start_time": p.start_time,
|
||||
"end_time": p.end_time, "period_mark": p.period_mark,
|
||||
"elec_price": p.elec_price, "service_price": p.service_price,
|
||||
}
|
||||
|
||||
|
||||
def _order_to_dict(o: ChargingOrder) -> dict:
|
||||
return {
|
||||
"id": o.id, "order_no": o.order_no, "user_id": o.user_id,
|
||||
"user_name": o.user_name, "phone": o.phone,
|
||||
"station_id": o.station_id, "station_name": o.station_name,
|
||||
"pile_id": o.pile_id, "pile_name": o.pile_name,
|
||||
"start_time": str(o.start_time) if o.start_time else None,
|
||||
"end_time": str(o.end_time) if o.end_time else None,
|
||||
"car_no": o.car_no, "car_vin": o.car_vin,
|
||||
"charge_method": o.charge_method, "settle_type": o.settle_type,
|
||||
"pay_type": o.pay_type,
|
||||
"settle_time": str(o.settle_time) if o.settle_time else None,
|
||||
"settle_price": o.settle_price, "paid_price": o.paid_price,
|
||||
"discount_amt": o.discount_amt, "elec_amt": o.elec_amt,
|
||||
"serve_amt": o.serve_amt, "order_status": o.order_status,
|
||||
"charge_duration": o.charge_duration, "energy": o.energy,
|
||||
"start_soc": o.start_soc, "end_soc": o.end_soc,
|
||||
"abno_cause": o.abno_cause, "order_source": o.order_source,
|
||||
"created_at": str(o.created_at) if o.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
def _merchant_to_dict(m: ChargingMerchant) -> dict:
|
||||
return {
|
||||
"id": m.id, "name": m.name, "contact_person": m.contact_person,
|
||||
"phone": m.phone, "email": m.email, "address": m.address,
|
||||
"business_license": m.business_license, "status": m.status,
|
||||
"settlement_type": m.settlement_type,
|
||||
}
|
||||
|
||||
|
||||
def _brand_to_dict(b: ChargingBrand) -> dict:
|
||||
return {
|
||||
"id": b.id, "brand_name": b.brand_name, "logo_url": b.logo_url,
|
||||
"country": b.country, "description": b.description,
|
||||
}
|
||||
Reference in New Issue
Block a user