181 lines
6.5 KiB
Python
181 lines
6.5 KiB
Python
"""DB 모델 정의"""
|
|
|
|
from datetime import datetime
|
|
from enum import Enum as PyEnum
|
|
|
|
from sqlalchemy import (
|
|
Column, Integer, BigInteger, String, Float, DateTime,
|
|
Enum, ForeignKey, Text, Boolean, Index,
|
|
)
|
|
from sqlalchemy.orm import relationship
|
|
from sqlalchemy.sql import func
|
|
|
|
from app.database import Base
|
|
|
|
|
|
# ── 상태 Enum ──
|
|
|
|
class ChargerStatus(str, PyEnum):
|
|
AVAILABLE = "Available"
|
|
CHARGING = "Charging"
|
|
FAULTED = "Faulted"
|
|
UNAVAILABLE = "Unavailable"
|
|
RESERVED = "Reserved"
|
|
|
|
|
|
class SessionStatus(str, PyEnum):
|
|
PENDING = "pending" # 결제 대기
|
|
AUTHORIZED = "authorized" # 결제 승인, 충전 시작 대기
|
|
CHARGING = "charging" # 충전 중
|
|
COMPLETED = "completed" # 정상 종료
|
|
FAILED = "failed" # 실패
|
|
CANCELLED = "cancelled" # 취소
|
|
|
|
|
|
class PaymentStatus(str, PyEnum):
|
|
PENDING = "pending"
|
|
CONFIRMED = "confirmed"
|
|
FAILED = "failed"
|
|
REFUNDED = "refunded"
|
|
PARTIAL_REFUND = "partial_refund"
|
|
|
|
|
|
# ── 충전기 ──
|
|
|
|
class Charger(Base):
|
|
__tablename__ = "chargers"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
charge_box_id = Column(String(64), unique=True, nullable=False, index=True,
|
|
comment="OCPP chargeBoxId (Steve 등록명)")
|
|
name = Column(String(128), comment="표시 이름 (예: A동 주차장 1번)")
|
|
location = Column(String(256), comment="설치 위치")
|
|
connector_count = Column(Integer, default=1)
|
|
power_kw = Column(Float, default=7.0, comment="충전 용량 kW")
|
|
status = Column(Enum(ChargerStatus), default=ChargerStatus.UNAVAILABLE)
|
|
last_heartbeat = Column(DateTime(timezone=True))
|
|
is_active = Column(Boolean, default=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
|
|
sessions = relationship("ChargingSession", back_populates="charger")
|
|
|
|
def __repr__(self):
|
|
return f"<Charger {self.charge_box_id} [{self.status}]>"
|
|
|
|
|
|
# ── 충전 세션 ──
|
|
|
|
class ChargingSession(Base):
|
|
__tablename__ = "charging_sessions"
|
|
__table_args__ = (
|
|
Index("ix_session_charger_status", "charger_id", "status"),
|
|
Index("ix_session_created", "created_at"),
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
session_uid = Column(String(64), unique=True, nullable=False, index=True,
|
|
comment="고유 세션 ID (UUID)")
|
|
charger_id = Column(Integer, ForeignKey("chargers.id"), nullable=False)
|
|
connector_id = Column(Integer, default=1)
|
|
|
|
# OCPP 트랜잭션 정보
|
|
ocpp_transaction_id = Column(Integer, comment="Steve에서 발급한 transactionId")
|
|
id_tag = Column(String(64), comment="OCPP 인증 태그")
|
|
|
|
# 전력량 (Wh 단위)
|
|
meter_start = Column(BigInteger, comment="시작 전력량 Wh")
|
|
meter_stop = Column(BigInteger, comment="종료 전력량 Wh")
|
|
charged_wh = Column(BigInteger, default=0, comment="충전된 전력량 Wh")
|
|
last_meter_value = Column(BigInteger, comment="마지막 MeterValues Wh")
|
|
|
|
# 요금
|
|
electricity_cost = Column(Integer, default=0, comment="전기 원가 (원)")
|
|
service_fee = Column(Integer, default=0, comment="서비스 수수료 (원)")
|
|
total_bill = Column(Integer, default=0, comment="총 요금 (원)")
|
|
|
|
# 시간
|
|
status = Column(Enum(SessionStatus), default=SessionStatus.PENDING)
|
|
started_at = Column(DateTime(timezone=True))
|
|
stopped_at = Column(DateTime(timezone=True))
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
|
|
charger = relationship("Charger", back_populates="sessions")
|
|
payment = relationship("Payment", back_populates="session", uselist=False)
|
|
|
|
def __repr__(self):
|
|
return f"<Session {self.session_uid} [{self.status}]>"
|
|
|
|
|
|
# ── 결제 ──
|
|
|
|
class Payment(Base):
|
|
__tablename__ = "payments"
|
|
__table_args__ = (
|
|
Index("ix_payment_order", "order_id"),
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
session_id = Column(Integer, ForeignKey("charging_sessions.id"), nullable=False)
|
|
|
|
# 토스페이먼츠
|
|
order_id = Column(String(128), unique=True, nullable=False,
|
|
comment="토스 주문 ID")
|
|
payment_key = Column(String(256), comment="토스 paymentKey")
|
|
amount = Column(Integer, nullable=False, comment="결제 요청 금액 (원)")
|
|
actual_amount = Column(Integer, comment="실제 충전 후 정산 금액")
|
|
|
|
status = Column(Enum(PaymentStatus), default=PaymentStatus.PENDING)
|
|
method = Column(String(32), comment="결제 수단 (카드, 간편결제 등)")
|
|
card_company = Column(String(32), comment="카드사 이름")
|
|
|
|
confirmed_at = Column(DateTime(timezone=True))
|
|
refunded_at = Column(DateTime(timezone=True))
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
session = relationship("ChargingSession", back_populates="payment")
|
|
|
|
def __repr__(self):
|
|
return f"<Payment {self.order_id} [{self.status}]>"
|
|
|
|
|
|
# ── 충전기 MeterValue 로그 ──
|
|
|
|
class MeterValueLog(Base):
|
|
"""실시간 MeterValues 로그 (디버깅/분석용)"""
|
|
__tablename__ = "meter_value_logs"
|
|
|
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
charger_id = Column(Integer, ForeignKey("chargers.id"), nullable=False)
|
|
transaction_id = Column(Integer)
|
|
connector_id = Column(Integer, default=1)
|
|
measurand = Column(String(64), default="Energy.Active.Import.Register")
|
|
value = Column(Float, nullable=False, comment="측정값")
|
|
unit = Column(String(16), default="Wh")
|
|
timestamp = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
|
|
# ── 사용자 ──
|
|
|
|
class UserRole(str, PyEnum):
|
|
ADMIN = "admin"
|
|
OPERATOR = "operator"
|
|
VIEWER = "viewer"
|
|
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
username = Column(String(64), unique=True, nullable=False, index=True)
|
|
hashed_password = Column(String(256), nullable=False)
|
|
display_name = Column(String(128), comment="표시 이름")
|
|
role = Column(Enum(UserRole), default=UserRole.VIEWER)
|
|
is_active = Column(Boolean, default=True)
|
|
last_login = Column(DateTime(timezone=True))
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
def __repr__(self):
|
|
return f"<User {self.username} [{self.role}]>"
|