Initial commit: AutonetSellCar platform with deployment system

- Frontend: Next.js 14 with TypeScript
- Backend: FastAPI with SQLAlchemy
- Agent: Carmodoo sync agent
- Deployment: Docker Compose based staging/production setup
- Scripts: Automated deployment with rollback support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
AutonetSellCar Deploy
2025-12-30 13:24:39 +09:00
commit 1f0dcb1ddb
224 changed files with 55119 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
from .car import CarMaker, CarModel, Car, CarImage, CarOption
from .user import User, CarView, PerformanceCheckView, ChargeHistory, VerificationCode
from .inquiry import Inquiry, InquiryMessage, InquiryStatus, InquiryCategory
from .hero_banner import HeroBanner, HeroBannerSettings
from .translation import Translation
from .cache import CarCache, CarDetailCache, CacheRequestQueue
from .settings import SystemSettings
from .vehicle_request import VehicleRequest, RequestVehicle, PurchasedVehicle
from .dealer import DealerApplication, DealerInfo
from .vehicle_share import VehicleShare, ShareReward
from .withdrawal import WithdrawalRequest
from .referral import ReferralReward
from .notification import Notification
from .push_subscription import PushSubscription, UserNotificationPreference
from .performance_check import CarPerformanceCheck
from .car_specification import CarSpecification
from .exchange_rate import ExchangeRate, ExchangeRateHistory
from .cc_package import CCPackage, DEFAULT_CC_PACKAGES
from .visitor import VisitorLog, VisitorDailyStats, VisitorSession
__all__ = [
"CarMaker",
"CarModel",
"Car",
"CarImage",
"CarOption",
"CarPerformanceCheck",
"CarSpecification",
"User",
"CarView",
"PerformanceCheckView",
"ChargeHistory",
"VerificationCode",
"Inquiry",
"InquiryMessage",
"InquiryStatus",
"InquiryCategory",
"HeroBanner",
"HeroBannerSettings",
"Translation",
"CarCache",
"CarDetailCache",
"CacheRequestQueue",
"SystemSettings",
"VehicleRequest",
"RequestVehicle",
"PurchasedVehicle",
"DealerApplication",
"DealerInfo",
"VehicleShare",
"ShareReward",
"WithdrawalRequest",
"ReferralReward",
"Notification",
"PushSubscription",
"UserNotificationPreference",
"ExchangeRate",
"ExchangeRateHistory",
"CCPackage",
"DEFAULT_CC_PACKAGES",
"VisitorLog",
"VisitorDailyStats",
"VisitorSession",
]

View File

@@ -0,0 +1,75 @@
"""
캐시 모델 - 카모두 검색 결과 캐싱
"""
from sqlalchemy import Column, Integer, String, DateTime, Text, Index
from sqlalchemy.sql import func
from ..database import Base
class CarCache(Base):
"""
검색 결과 캐시 테이블 (Maker + Model 단위)
캐시 키: maker_code_model_code (예: "2_38" = 기아_K5)
"""
__tablename__ = "car_cache"
id = Column(Integer, primary_key=True, index=True)
cache_key = Column(String(50), unique=True, nullable=False, index=True)
maker_code = Column(String(10), nullable=False)
maker_name = Column(String(100), nullable=False)
model_code = Column(String(10), nullable=False)
model_name = Column(String(100), nullable=False)
total_count = Column(Integer, nullable=False, default=0)
cars_data = Column(Text, nullable=False) # JSON: 전체 차량 목록
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
expires_at = Column(DateTime(timezone=True), nullable=False)
__table_args__ = (
Index('idx_car_cache_expires', 'expires_at'),
Index('idx_car_cache_maker_model', 'maker_code', 'model_code'),
)
class CarDetailCache(Base):
"""
개별 차량 상세 정보 캐시 테이블
"""
__tablename__ = "car_detail_cache"
id = Column(Integer, primary_key=True, index=True)
car_id = Column(String(50), unique=True, nullable=False, index=True) # 카모두 차량 ID
detail_data = Column(Text, nullable=False) # JSON: 상세 정보
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
expires_at = Column(DateTime(timezone=True), nullable=False)
__table_args__ = (
Index('idx_car_detail_cache_expires', 'expires_at'),
)
class CacheRequestQueue(Base):
"""
캐시 요청 대기열 - 동일 조건 요청 병합용
"""
__tablename__ = "cache_request_queue"
id = Column(Integer, primary_key=True, index=True)
cache_key = Column(String(50), nullable=False, index=True)
status = Column(String(20), nullable=False, default='pending') # pending, processing, completed, failed
created_at = Column(DateTime(timezone=True), server_default=func.now())
started_at = Column(DateTime(timezone=True))
completed_at = Column(DateTime(timezone=True))
error_message = Column(Text)
__table_args__ = (
Index('idx_cache_request_status', 'status', 'cache_key'),
)

110
backend/app/models/car.py Normal file
View File

@@ -0,0 +1,110 @@
from sqlalchemy import Column, Integer, String, BigInteger, Boolean, ForeignKey, DateTime, Text, DECIMAL
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from ..database import Base
class CarMaker(Base):
__tablename__ = "car_makers"
id = Column(Integer, primary_key=True, index=True)
code = Column(String(10), unique=True, nullable=False, index=True)
name = Column(String(100), nullable=False)
name_en = Column(String(100))
created_at = Column(DateTime(timezone=True), server_default=func.now())
models = relationship("CarModel", back_populates="maker")
cars = relationship("Car", back_populates="maker")
class CarModel(Base):
__tablename__ = "car_models"
id = Column(Integer, primary_key=True, index=True)
code = Column(String(10), nullable=False, index=True)
maker_id = Column(Integer, ForeignKey("car_makers.id"), nullable=False)
name = Column(String(100), nullable=False)
name_en = Column(String(100))
maker = relationship("CarMaker", back_populates="models")
cars = relationship("Car", back_populates="model")
class Car(Base):
__tablename__ = "cars"
id = Column(Integer, primary_key=True, index=True)
source = Column(String(50), nullable=False, default="carmodoo")
source_id = Column(String(50), nullable=False, index=True)
source_key = Column(Text)
maker_id = Column(Integer, ForeignKey("car_makers.id"))
model_id = Column(Integer, ForeignKey("car_models.id"))
car_name = Column(String(200))
year = Column(Integer, index=True)
month = Column(Integer)
mileage = Column(Integer)
price_krw = Column(BigInteger, index=True)
margin_krw = Column(BigInteger, default=0) # Korean margin amount in KRW
margin_mn = Column(BigInteger, default=0) # Mongolian margin amount in KRW
price_usd = Column(DECIMAL(12, 2))
is_displayed = Column(Boolean, default=False, index=True) # Show to users
fuel = Column(String(20))
transmission = Column(String(20))
color = Column(String(50))
displacement = Column(Integer)
car_number = Column(String(20))
seize_count = Column(Integer, default=0)
collateral_count = Column(Integer, default=0)
check_num = Column(String(50))
dealer_name = Column(String(100))
dealer_phone = Column(String(50))
shop_name = Column(String(100))
dealer_description = Column(Text) # 딜러가 작성한 차량 상세설명 (한국어 원문)
dealer_description_en = Column(Text) # 영어 번역
dealer_description_mn = Column(Text) # 몽골어 번역
dealer_description_ru = Column(Text) # 러시아어 번역
memo = Column(Text)
status = Column(String(20), default="active", index=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
synced_at = Column(DateTime(timezone=True))
maker = relationship("CarMaker", back_populates="cars")
model = relationship("CarModel", back_populates="cars")
images = relationship("CarImage", back_populates="car", cascade="all, delete-orphan")
options = relationship("CarOption", back_populates="car", cascade="all, delete-orphan")
# inquiries relationship disabled due to schema mismatch - use raw SQL for inquiry operations
# inquiries = relationship("Inquiry", back_populates="car")
views = relationship("CarView", back_populates="car", cascade="all, delete-orphan")
performance_check = relationship("CarPerformanceCheck", back_populates="car", uselist=False, cascade="all, delete-orphan")
specification = relationship("CarSpecification", back_populates="car", uselist=False, cascade="all, delete-orphan")
class CarImage(Base):
__tablename__ = "car_images"
id = Column(Integer, primary_key=True, index=True)
car_id = Column(Integer, ForeignKey("cars.id", ondelete="CASCADE"), nullable=False)
url = Column(String(500))
local_path = Column(String(500))
is_main = Column(Boolean, default=False)
sort_order = Column(Integer, default=0)
car = relationship("Car", back_populates="images")
class CarOption(Base):
__tablename__ = "car_options"
id = Column(Integer, primary_key=True, index=True)
car_id = Column(Integer, ForeignKey("cars.id", ondelete="CASCADE"), nullable=False)
option_name = Column(String(100))
car = relationship("Car", back_populates="options")

View File

@@ -0,0 +1,59 @@
"""
차량 상세사양 (Car Specifications) 모델
카모두 상세사양조회 서비스에서 가져온 차량 스펙 정보를 저장
"""
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Text, JSON
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from ..database import Base
class CarSpecification(Base):
"""차량 상세사양"""
__tablename__ = "car_specifications"
id = Column(Integer, primary_key=True, index=True)
car_id = Column(Integer, ForeignKey("cars.id", ondelete="CASCADE"), nullable=False, unique=True)
# 기본 정보
manufacturer = Column(String(50)) # 제조사
model_name = Column(String(100)) # 모델명
grade = Column(String(100)) # 등급/트림
model_year = Column(String(20)) # 연식
# 엔진/성능
displacement = Column(Integer) # 배기량 (cc)
fuel_type = Column(String(30)) # 연료 (가솔린/디젤/하이브리드/전기)
transmission = Column(String(30)) # 변속기 (자동/수동/CVT)
drive_type = Column(String(30)) # 구동방식 (전륜/후륜/4륜)
max_power = Column(String(50)) # 최고출력 (예: 180ps/6,000rpm)
max_torque = Column(String(50)) # 최대토크 (예: 23.5kg.m/4,200rpm)
fuel_efficiency = Column(String(50)) # 연비 (예: 12.5km/L)
# 차체
body_type = Column(String(30)) # 차체형태 (세단/SUV/해치백 등)
door_count = Column(Integer) # 도어수
seating_capacity = Column(Integer) # 승차정원
# 제원
length = Column(Integer) # 전장 (mm)
width = Column(Integer) # 전폭 (mm)
height = Column(Integer) # 전고 (mm)
wheelbase = Column(Integer) # 축거 (mm)
curb_weight = Column(Integer) # 공차중량 (kg)
# 옵션/편의장치 (JSON 배열)
safety_options = Column(JSON) # 안전옵션 ["에어백", "ABS", ...]
comfort_options = Column(JSON) # 편의옵션 ["썬루프", "열선시트", ...]
exterior_options = Column(JSON) # 외장옵션 ["LED헤드램프", ...]
interior_options = Column(JSON) # 내장옵션 ["가죽시트", ...]
# 원본 데이터
raw_data = Column(JSON) # 전체 원본 데이터 (파싱하지 못한 정보 포함)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
# Relationship
car = relationship("Car", back_populates="specification")

View File

@@ -0,0 +1,48 @@
from sqlalchemy import Column, Integer, String, Boolean, Float
from ..database import Base
class CCPackage(Base):
"""CC charging packages"""
__tablename__ = "cc_packages"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(50), nullable=False) # e.g., "Basic", "Standard", "Premium"
price_usd = Column(Integer, nullable=False) # Price in USD (10, 27, 40)
cc_amount = Column(Integer, nullable=False) # CC amount (10, 30, 50)
bonus_cc = Column(Integer, default=0) # Bonus CC (0, 3, 10)
discount_percent = Column(Integer, default=0) # Discount percentage (0, 10, 20)
is_active = Column(Boolean, default=True)
sort_order = Column(Integer, default=0)
# Stripe Price ID for recurring or one-time payments
stripe_price_id = Column(String(100), nullable=True)
# Default CC packages
DEFAULT_CC_PACKAGES = [
{
"name": "Basic",
"price_usd": 10,
"cc_amount": 10,
"bonus_cc": 0,
"discount_percent": 0,
"sort_order": 1,
},
{
"name": "Standard",
"price_usd": 27,
"cc_amount": 27,
"bonus_cc": 3,
"discount_percent": 10,
"sort_order": 2,
},
{
"name": "Premium",
"price_usd": 40,
"cc_amount": 40,
"bonus_cc": 10,
"discount_percent": 20,
"sort_order": 3,
},
]

View File

@@ -0,0 +1,85 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Float
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import uuid
import hashlib
from ..database import Base
def generate_dealer_code():
"""Generate a unique 6-character dealer code"""
unique_id = uuid.uuid4().hex
return "D" + hashlib.sha256(unique_id.encode()).hexdigest()[:5].upper()
class DealerApplication(Base):
"""Dealer application for users wanting to become dealers"""
__tablename__ = "dealer_applications"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Business info
business_name = Column(String(100), nullable=False) # 상호명
business_number = Column(String(50), nullable=True) # 사업자번호 (선택)
# Personal info
real_name = Column(String(100), nullable=False) # 실명
id_number_encrypted = Column(String(255), nullable=True) # 주민번호/외국인번호 (암호화)
phone = Column(String(50), nullable=False) # 연락처
# Bank info for withdrawals
bank_name = Column(String(50), nullable=False) # 은행명
bank_account = Column(String(100), nullable=False) # 계좌번호
account_holder = Column(String(100), nullable=False) # 예금주명
# Photo
photo_url = Column(String(500), nullable=True) # 본인 사진 URL
# Application status
status = Column(String(20), default="pending") # pending, approved, rejected
rejected_reason = Column(Text, nullable=True) # 거부 사유
# Timestamps
applied_at = Column(DateTime(timezone=True), server_default=func.now())
approved_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
user = relationship("User", back_populates="dealer_application")
class DealerInfo(Base):
"""Approved dealer information"""
__tablename__ = "dealer_info"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), unique=True, nullable=False)
# Dealer identification
dealer_code = Column(String(10), unique=True, index=True, nullable=False) # 딜러 고유 코드 (D + 5자리)
dealer_card_url = Column(String(500), nullable=True) # 딜러증 이미지 URL
# Business info (from application)
business_name = Column(String(100), nullable=False)
real_name = Column(String(100), nullable=False)
phone = Column(String(50), nullable=False)
photo_url = Column(String(500), nullable=True)
# Bank info (from application)
bank_name = Column(String(50), nullable=False)
bank_account = Column(String(100), nullable=False)
account_holder = Column(String(100), nullable=False)
# Earnings
total_commission_earned = Column(Float, default=0.0) # 총 수수료 수익 (KRW)
total_withdrawn = Column(Float, default=0.0) # 총 출금액 (KRW)
pending_withdrawal = Column(Float, default=0.0) # 출금 대기 금액 (KRW)
# Status
is_active = Column(Boolean, default=True)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
user = relationship("User", back_populates="dealer_info")

View File

@@ -0,0 +1,46 @@
"""
Exchange Rate Model - 환율 정보 저장
"""
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean
from sqlalchemy.sql import func
from ..database import Base
class ExchangeRate(Base):
"""환율 정보 테이블"""
__tablename__ = "exchange_rates"
id = Column(Integer, primary_key=True, index=True)
# 통화 정보
currency_code = Column(String(10), unique=True, index=True) # USD, MNT, RUB, CNY
currency_name = Column(String(100)) # 미국 달러, 몽골 투그릭 등
# 환율 정보 (한국수출입은행 기준)
deal_base_rate = Column(Float) # 매매기준율 (1 USD = X KRW)
ttb_rate = Column(Float) # 전신환(송금) 받을때
tts_rate = Column(Float) # 전신환(송금) 보낼때
# 가중치 적용 환율
weight_percent = Column(Float, default=0.0) # 관리자 설정 가중치 (%)
adjusted_rate = Column(Float) # 가중치 적용된 환율
# 메타 정보
source_date = Column(String(20)) # 수출입은행 기준일 (예: 20241223)
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())
class ExchangeRateHistory(Base):
"""환율 변동 이력 테이블"""
__tablename__ = "exchange_rate_history"
id = Column(Integer, primary_key=True, index=True)
currency_code = Column(String(10), index=True)
deal_base_rate = Column(Float)
source_date = Column(String(20))
created_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@@ -0,0 +1,67 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from ..database import Base
class HeroBannerSettings(Base):
"""히어로 배너 슬라이더 설정"""
__tablename__ = "hero_banner_settings"
id = Column(Integer, primary_key=True, index=True)
# 슬라이드 전환 간격 (밀리초)
slide_interval = Column(Integer, default=3000) # 3초
# 애니메이션 타입: 'film-strip', 'fade', 'slide'
animation_type = Column(String(20), default="film-strip")
# 이미지 크기
image_width = Column(Integer, default=500)
image_height = Column(Integer, default=300)
# 자동 재생 여부
auto_play = 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())
class HeroBanner(Base):
"""히어로 배너 이미지"""
__tablename__ = "hero_banners"
id = Column(Integer, primary_key=True, index=True)
# 다국어 제목
title_ko = Column(String(100))
title_en = Column(String(100))
title_mn = Column(String(100)) # 몽골어
# 다국어 서브타이틀
subtitle_ko = Column(String(200))
subtitle_en = Column(String(200))
subtitle_mn = Column(String(200))
# 이미지 URL
image_url = Column(String(500), nullable=False)
# 클릭 시 이동 URL (선택)
link_url = Column(String(500))
# 연결된 차량 ID (선택 - 차량 상세 페이지로 연결)
car_id = Column(Integer, ForeignKey("cars.id", ondelete="SET NULL"), nullable=True)
# 활성화 여부
is_active = Column(Boolean, default=True)
# 표시 순서 (낮을수록 먼저)
display_order = Column(Integer, default=0)
# 타임스탬프
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
# 관계
car = relationship("Car", foreign_keys=[car_id])

View File

@@ -0,0 +1,79 @@
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
import enum
from ..database import Base
class InquiryStatus:
PENDING = "pending"
IN_PROGRESS = "in_progress"
RESOLVED = "resolved"
CLOSED = "closed"
class InquiryCategory:
GENERAL = "general"
VEHICLE = "vehicle"
PAYMENT = "payment"
SHIPPING = "shipping"
DEALER = "dealer"
ACCOUNT = "account"
OTHER = "other"
class Inquiry(Base):
"""User inquiry/support ticket"""
__tablename__ = "inquiries"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
# Legacy field for backward compatibility
car_id = Column(Integer, ForeignKey("cars.id"), nullable=True)
# Inquiry details
category = Column(String(50), default=InquiryCategory.GENERAL)
subject = Column(String(200), nullable=True)
message = Column(Text, nullable=False)
# Contact info (can be different from user's profile)
contact_email = Column(String(255), nullable=True)
contact_phone = Column(String(50), nullable=True)
# Status
status = Column(String(20), default=InquiryStatus.PENDING)
# Admin response
admin_response = Column(Text, nullable=True)
responded_at = Column(DateTime(timezone=True), nullable=True)
responded_by = Column(Integer, ForeignKey("users.id"), nullable=True)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
user = relationship("User", foreign_keys=[user_id], back_populates="inquiries")
responder = relationship("User", foreign_keys=[responded_by])
# car relationship disabled due to schema mismatch - Car model doesn't have inquiries relationship
# car = relationship("Car", back_populates="inquiries")
class InquiryMessage(Base):
"""Messages within an inquiry thread"""
__tablename__ = "inquiry_messages"
id = Column(Integer, primary_key=True, index=True)
inquiry_id = Column(Integer, ForeignKey("inquiries.id"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
message = Column(Text, nullable=False)
is_admin = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
inquiry = relationship("Inquiry", backref="messages")
user = relationship("User")

View File

@@ -0,0 +1,37 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from ..database import Base
class Notification(Base):
"""User notifications"""
__tablename__ = "notifications"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Notification type: vehicle_recommended, shipping_update, withdrawal_processed,
# referral_reward, dealer_approved, share_purchased, system
notification_type = Column(String(50), nullable=False)
# Title and message (supports i18n keys or direct text)
title = Column(String(200), nullable=False)
message = Column(Text, nullable=False)
# Optional link to navigate when clicked
link = Column(String(500), nullable=True)
# Related entity (optional)
related_id = Column(Integer, nullable=True) # ID of related entity
related_type = Column(String(50), nullable=True) # Type: vehicle_request, purchased_vehicle, withdrawal, etc.
# Status
is_read = Column(Boolean, default=False)
read_at = Column(DateTime(timezone=True), nullable=True)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
user = relationship("User", backref="notifications")

View File

@@ -0,0 +1,119 @@
"""
성능점검표 (Performance Check Report) 모델
카모두에서 가져온 차량 성능점검 정보를 저장
"""
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime, Text, JSON
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from ..database import Base
class CarPerformanceCheck(Base):
"""차량 성능점검표"""
__tablename__ = "car_performance_checks"
id = Column(Integer, primary_key=True, index=True)
car_id = Column(Integer, ForeignKey("cars.id", ondelete="CASCADE"), nullable=False, unique=True)
# 성능점검 기본정보
check_number = Column(String(50)) # 성능점검번호
check_date = Column(String(20)) # 점검일자
valid_until = Column(String(20)) # 유효기간
inspector_name = Column(String(50)) # 점검자명
inspector_license = Column(String(50)) # 점검자 자격번호
# 차량 기본정보 (car_number는 cars 테이블에서 관리 - 원자성)
first_registration = Column(String(20)) # 최초등록일
model_year = Column(String(20)) # 연식
# 주행거리
mileage = Column(Integer) # 주행거리
mileage_status = Column(String(20)) # 주행거리 상태 (정상/조작의심/교환됨)
# 압류/저당 정보
seize_count = Column(Integer, default=0) # 압류 건수
collateral_count = Column(Integer, default=0) # 저당 건수
# 특별 이력 (침수/화재/전손)
is_flood_damaged = Column(Boolean, default=False) # 침수
is_fire_damaged = Column(Boolean, default=False) # 화재
is_total_loss = Column(Boolean, default=False) # 전손
# 용도이력
usage_history = Column(String(100)) # 자가용/영업용/관용 등
is_rental_used = Column(Boolean, default=False) # 렌트 이력
# 주요장치 상태 (JSON으로 상세정보 저장)
# 각 항목: 양호/주의/불량
engine_status = Column(String(20)) # 원동기
transmission_status = Column(String(20)) # 변속기
power_delivery_status = Column(String(20)) # 동력전달
steering_status = Column(String(20)) # 조향장치
brake_status = Column(String(20)) # 제동장치
electrical_status = Column(String(20)) # 전기장치
fuel_system_status = Column(String(20)) # 연료장치
# 타이어 상태
tire_front_left = Column(String(20)) # 전좌
tire_front_right = Column(String(20)) # 전우
tire_rear_left = Column(String(20)) # 후좌
tire_rear_right = Column(String(20)) # 후우
# 사고 이력 (외판/주요골격) - JSON으로 상세 저장
# 부위별: 없음/교환/판금용접/부식/손상
accident_history = Column(JSON) # {"hood": "교환", "front_fender_left": "판금", ...}
# 외판 부위
hood = Column(String(20)) # 후드
front_fender_left = Column(String(20)) # 프론트휀더(좌)
front_fender_right = Column(String(20)) # 프론트휀더(우)
front_door_left = Column(String(20)) # 프론트도어(좌)
front_door_right = Column(String(20)) # 프론트도어(우)
rear_door_left = Column(String(20)) # 리어도어(좌)
rear_door_right = Column(String(20)) # 리어도어(우)
trunk_lid = Column(String(20)) # 트렁크리드
radiator_support = Column(String(20)) # 라디에이터서포트
roof_panel = Column(String(20)) # 루프패널
quarter_panel_left = Column(String(20)) # 쿼터패널(좌)
quarter_panel_right = Column(String(20)) # 쿼터패널(우)
side_sill_left = Column(String(20)) # 사이드실패널(좌)
side_sill_right = Column(String(20)) # 사이드실패널(우)
# 주요골격 부위
front_panel = Column(String(20)) # 프론트패널
cross_member = Column(String(20)) # 크로스멤버
inside_panel_left = Column(String(20)) # 인사이드패널(좌)
inside_panel_right = Column(String(20)) # 인사이드패널(우)
side_member_left = Column(String(20)) # 사이드멤버(좌)
side_member_right = Column(String(20)) # 사이드멤버(우)
wheel_house_left = Column(String(20)) # 휠하우스(좌)
wheel_house_right = Column(String(20)) # 휠하우스(우)
dash_panel = Column(String(20)) # 대쉬패널
floor_panel = Column(String(20)) # 플로어패널
trunk_floor = Column(String(20)) # 트렁크플로어
rear_panel = Column(String(20)) # 리어패널
pillar_a_left = Column(String(20)) # 필러A(좌)
pillar_a_right = Column(String(20)) # 필러A(우)
pillar_b_left = Column(String(20)) # 필러B(좌)
pillar_b_right = Column(String(20)) # 필러B(우)
pillar_c_left = Column(String(20)) # 필러C(좌)
pillar_c_right = Column(String(20)) # 필러C(우)
package_tray = Column(String(20)) # 패키지트레이
# 원본 데이터 (파싱하지 못한 추가 정보)
raw_data = Column(JSON) # 전체 원본 데이터
raw_html = Column(Text) # 원본 HTML (디버깅용)
# 점검표 이미지 URL
report_image_url = Column(String(500)) # 성능점검표 이미지
report_image_local = Column(String(500)) # 로컬 저장 경로
# PDF 파일 경로 (Playwright로 캡처한 성능점검표)
pdf_path = Column(String(500)) # PDF 파일 상대경로 (/uploads/performance_checks/xxx.pdf)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
# Relationship
car = relationship("Car", back_populates="performance_check")

View File

@@ -0,0 +1,48 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from ..database import Base
class PushSubscription(Base):
"""Store user's push notification subscriptions"""
__tablename__ = "push_subscriptions"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
endpoint = Column(Text, nullable=False) # Push service endpoint URL
p256dh_key = Column(String(255), nullable=False) # Public key for encryption
auth_key = Column(String(255), nullable=False) # Auth secret for encryption
device_info = Column(String(255), nullable=True) # Browser/device info
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
last_used_at = Column(DateTime(timezone=True), nullable=True)
user = relationship("User", backref="push_subscriptions")
class UserNotificationPreference(Base):
"""User preferences for different notification types"""
__tablename__ = "user_notification_preferences"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True)
# Notification type preferences (True = enabled)
vehicle_recommended = Column(Boolean, default=True)
shipping_update = Column(Boolean, default=True)
payment_confirmed = Column(Boolean, default=True)
withdrawal_processed = Column(Boolean, default=True)
dealer_status = Column(Boolean, default=True)
share_purchased = Column(Boolean, default=True)
referral_reward = Column(Boolean, default=True)
inquiry_reply = Column(Boolean, default=True)
system_announcements = Column(Boolean, default=True)
# Channel preferences
push_enabled = Column(Boolean, default=True)
email_enabled = Column(Boolean, default=False)
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
user = relationship("User", backref="notification_preferences")

View File

@@ -0,0 +1,37 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from ..database import Base
class ReferralReward(Base):
"""레퍼럴 보상 모델"""
__tablename__ = "referral_rewards"
id = Column(Integer, primary_key=True, index=True)
# 추천인 (보상 받는 사람)
referrer_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
# 피추천인 (추천받아 가입한 사람)
referred_user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
# 결제 금액 (피추천인이 충전한 금액 USD)
payment_amount = Column(Float, nullable=False)
# 보상 금액 (결제 금액의 X%)
reward_amount = Column(Float, nullable=False)
# 보상 상태: pending(대기), credited(적립), withdrawn(출금)
status = Column(String(20), default="pending")
# 출금 요청 ID (출금 시 연결)
withdrawal_request_id = Column(Integer, ForeignKey("withdrawal_requests.id"), nullable=True)
# 타임스탬프
created_at = Column(DateTime(timezone=True), server_default=func.now())
credited_at = Column(DateTime(timezone=True), nullable=True) # 적립 시각
# Relationships
referrer = relationship("User", foreign_keys=[referrer_id], backref="referral_rewards_given")
referred_user = relationship("User", foreign_keys=[referred_user_id], backref="referral_rewards_received")

View File

@@ -0,0 +1,45 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean
from sqlalchemy.sql import func
from ..database import Base
class SystemSettings(Base):
"""시스템 설정"""
__tablename__ = "system_settings"
id = Column(Integer, primary_key=True, index=True)
# 검색 결과 페이지 크기
search_page_size = Column(Integer, default=20)
# 마진 설정 (%)
korea_margin_percent = Column(Float, default=5.0)
mongolia_margin_percent = Column(Float, default=5.0)
# CC 코인 설정
cc_per_usdc = Column(Integer, default=10) # 1 USDC = 10 CC
cc_per_view = Column(Integer, default=1) # 차량 상세 조회 시 1 CC
cc_signup_bonus = Column(Integer, default=3) # 신규 가입 시 3 CC
cars_per_cc = Column(Integer, default=3) # 1 CC당 추천 차량 수 (기본 3대)
# 캐시 TTL (시간)
cache_ttl_hours = Column(Integer, default=2)
# 컨테이너 물류비 설정 (USD)
container_logistics_usd = Column(Integer, default=3600) # 컨테이너 물류비 $3,600
shoring_cost_usd = Column(Integer, default=300) # 쇼링비 (컨테이너 고정) $300
# 레퍼럴 보상 설정
referral_reward_enabled = Column(Boolean, default=True) # 레퍼럴 보상 활성화
referral_reward_percent = Column(Float, default=10.0) # 보상 비율 (기본 10%)
referral_reward_type = Column(String(20), default="one_time") # one_time / recurring
# 환율 가중치 설정 (%)
exchange_rate_weight_usd = Column(Float, default=0.0) # USD 가중치
exchange_rate_weight_mnt = Column(Float, default=0.0) # MNT (몽골 투그릭) 가중치
exchange_rate_weight_rub = Column(Float, default=0.0) # RUB (러시아 루블) 가중치
exchange_rate_weight_cny = Column(Float, default=0.0) # CNY (중국 위안) 가중치
# 타임스탬프
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

View File

@@ -0,0 +1,28 @@
from sqlalchemy import Column, Integer, String, DateTime, Index
from sqlalchemy.sql import func
from ..database import Base
class Translation(Base):
"""Translation dictionary for car-related terms"""
__tablename__ = "translations"
id = Column(Integer, primary_key=True, index=True)
# Source text (Korean)
source_text = Column(String(500), nullable=False, index=True)
# Category: maker, model, fuel, transmission, color, car_name, etc.
category = Column(String(50), nullable=False, index=True)
# Translations
text_en = Column(String(500)) # English
text_mn = Column(String(500)) # Mongolian
text_ru = Column(String(500)) # Russian
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
__table_args__ = (
Index('ix_translations_source_category', 'source_text', 'category', unique=True),
)

138
backend/app/models/user.py Normal file
View File

@@ -0,0 +1,138 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Float
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import uuid
import hashlib
from ..database import Base
def generate_referral_code():
"""Generate a unique 8-character referral code"""
unique_id = uuid.uuid4().hex
return hashlib.sha256(unique_id.encode()).hexdigest()[:8].upper()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String(255), unique=True, nullable=False, index=True)
password_hash = Column(String(255), nullable=False)
name = Column(String(100))
phone = Column(String(50))
country = Column(String(50), default="Mongolia")
is_active = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False)
is_dealer = Column(Boolean, default=False) # Dealer status
cc_balance = Column(Float, default=3.0) # CC coin balance, 3 free on signup
referral_code = Column(String(8), unique=True, index=True) # Unique referral code for sharing
referred_by = Column(String(8), nullable=True) # Referral code of the user who referred this user
# Email verification
email_verified = Column(Boolean, default=False)
email_verified_at = Column(DateTime(timezone=True), nullable=True)
# Phone verification
phone_verified = Column(Boolean, default=False)
phone_verified_at = Column(DateTime(timezone=True), nullable=True)
# Account withdrawal/deletion
withdrawal_requested_at = Column(DateTime(timezone=True), nullable=True) # 탈퇴 요청 시각
withdrawal_reason = Column(String(500), nullable=True) # 탈퇴 사유
deleted_at = Column(DateTime(timezone=True), nullable=True) # 실제 삭제 시각 (soft delete)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Note: foreign_keys specified as string to avoid circular import
inquiries = relationship("Inquiry", back_populates="user", primaryjoin="User.id == Inquiry.user_id")
car_views = relationship("CarView", back_populates="user")
performance_check_views = relationship("PerformanceCheckView", back_populates="user")
charge_history = relationship("ChargeHistory", back_populates="user", primaryjoin="User.id == ChargeHistory.user_id")
dealer_application = relationship("DealerApplication", back_populates="user", uselist=False)
dealer_info = relationship("DealerInfo", back_populates="user", uselist=False)
class VerificationCode(Base):
"""Store temporary verification codes for email and phone"""
__tablename__ = "verification_codes"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True) # Nullable for pre-registration
email = Column(String(255), nullable=True, index=True) # For email verification
phone = Column(String(50), nullable=True, index=True) # For phone verification
code = Column(String(10), nullable=False) # 6-digit code
code_type = Column(String(20), nullable=False) # 'email' or 'phone'
purpose = Column(String(50), default="verification") # 'verification', 'password_reset'
attempts = Column(Integer, default=0) # Failed verification attempts
max_attempts = Column(Integer, default=5)
expires_at = Column(DateTime(timezone=True), nullable=False)
verified_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
class CarView(Base):
"""Track which cars a user has purchased (paid CC to view full details)"""
__tablename__ = "car_views"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
car_id = Column(Integer, ForeignKey("cars.id"), nullable=False)
cc_paid = Column(Integer, default=1) # CC paid for this view
created_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", back_populates="car_views")
car = relationship("Car", back_populates="views")
class PerformanceCheckView(Base):
"""Track which performance checks a user has purchased (paid 0.1 CC to view)"""
__tablename__ = "performance_check_views"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
car_id = Column(Integer, ForeignKey("cars.id"), nullable=False)
cc_paid = Column(Float, default=0.1) # CC paid for this view (0.1 CC)
created_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", back_populates="performance_check_views")
class ChargeHistory(Base):
"""Track CC charge history for users"""
__tablename__ = "charge_history"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
package_id = Column(Integer, ForeignKey("cc_packages.id"), nullable=True) # CC package purchased
amount = Column(Integer, nullable=False) # Amount in selected currency
amount_usd = Column(Integer, nullable=True) # Amount in USD (for backwards compatibility)
cc_amount = Column(Integer, nullable=False) # CC received
bonus_cc = Column(Integer, default=0) # Bonus CC received
currency = Column(String(10), default="USD") # USD, USDC, KRW
payment_method = Column(String(50), default="stripe") # stripe, manual, usdc, bank_transfer
# Stripe fields
stripe_session_id = Column(String(200), nullable=True) # Stripe Checkout Session ID
stripe_payment_intent_id = Column(String(200), nullable=True) # Stripe Payment Intent ID
# Legacy fields
transaction_id = Column(String(100), nullable=True) # External transaction ID (crypto tx hash)
wallet_address = Column(String(100), nullable=True) # User's wallet address for refunds
admin_note = Column(String(500), nullable=True) # Admin notes
status = Column(String(20), default="pending") # pending, completed, failed, cancelled
verified_at = Column(DateTime(timezone=True), nullable=True)
verified_by = Column(Integer, ForeignKey("users.id"), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", back_populates="charge_history", foreign_keys=[user_id])
# Payment settings constants
class PaymentSettings:
USDC_WALLET_ADDRESS = "0x1234567890abcdef1234567890abcdef12345678" # Platform USDC receiving address
USDC_NETWORK = "Polygon" # Default network (Polygon for low fees)
MIN_CHARGE_USD = 10
MAX_CHARGE_USD = 10000
SUPPORTED_CURRENCIES = ["USD", "USDC", "KRW"]
SUPPORTED_METHODS = ["card", "usdc", "bank_transfer"]

View File

@@ -0,0 +1,106 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, JSON, Float
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from ..database import Base
class VehicleRequest(Base):
"""Track vehicle search requests from users"""
__tablename__ = "vehicle_requests"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Search criteria
maker_code = Column(String(50))
maker_name = Column(String(100))
model_code = Column(String(50))
model_name = Column(String(100))
grade_code = Column(String(50))
grade_name = Column(String(100))
year_from = Column(Integer)
year_to = Column(Integer)
mileage_min = Column(Integer)
mileage_max = Column(Integer)
fuel = Column(String(50)) # 연료 타입 (휘발유, 경유, 하이브리드, LPG, 전기)
displacement_min = Column(Integer) # 최소 배기량 (cc)
displacement_max = Column(Integer) # 최대 배기량 (cc)
# CC payment for request submission
cc_paid = Column(Float, default=1.0) # CC paid for this request (1 CC)
# Status: pending, reviewed, completed
status = Column(String(20), default="pending")
admin_reviewed_at = Column(DateTime(timezone=True))
admin_notes = Column(Text)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
user = relationship("User", backref="vehicle_requests")
recommended_vehicles = relationship("RequestVehicle", back_populates="request", cascade="all, delete-orphan")
class RequestVehicle(Base):
"""Vehicles recommended by admin for a user's request"""
__tablename__ = "request_vehicles"
id = Column(Integer, primary_key=True, index=True)
request_id = Column(Integer, ForeignKey("vehicle_requests.id"), nullable=False)
# Car data from Carmodoo (stored as JSON)
car_data = Column(JSON, nullable=False)
# Admin approval
is_approved = Column(Boolean, default=False)
approved_at = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
request = relationship("VehicleRequest", back_populates="recommended_vehicles")
class PurchasedVehicle(Base):
"""Track purchased vehicles and their shipping status"""
__tablename__ = "purchased_vehicles"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Vehicle info
car_name = Column(String(200))
car_data = Column(JSON) # Full car details
car_image = Column(String(500)) # Main image URL
# Price info
vehicle_price_krw = Column(Integer)
domestic_cost_krw = Column(Integer)
shipping_cost_usd = Column(Integer)
total_cost_krw = Column(Integer)
car_type = Column(String(20)) # small, compact
# Dealer selection and commission (50/50 split of Mongolia margin)
selected_dealer_id = Column(Integer, ForeignKey("dealer_info.id"), nullable=True)
dealer_commission_krw = Column(Integer, default=0) # 50% of Mongolia margin
platform_commission_krw = Column(Integer, default=0) # 50% of Mongolia margin
commission_paid = Column(Boolean, default=False) # Whether commission has been paid
commission_paid_at = Column(DateTime(timezone=True))
# Shipping status: 1-5
# 1: Purchased, 2: Incheon Port, 3: In Transit, 4: Customs, 5: Delivered
shipping_status = Column(Integer, default=1)
status_updated_at = Column(DateTime(timezone=True))
# Location info
current_location = Column(String(200))
estimated_arrival = Column(DateTime(timezone=True))
# Timestamps
purchased_at = Column(DateTime(timezone=True), server_default=func.now())
delivered_at = Column(DateTime(timezone=True))
# Relationships
user = relationship("User", backref="purchased_vehicles")
selected_dealer = relationship("DealerInfo", backref="purchased_vehicles")

View File

@@ -0,0 +1,75 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Float, Text
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import uuid
import hashlib
from ..database import Base
def generate_share_code():
"""Generate a unique 10-character share code"""
unique_id = uuid.uuid4().hex
return hashlib.sha256(unique_id.encode()).hexdigest()[:10].upper()
class VehicleShare(Base):
"""Track vehicle shares with price markup"""
__tablename__ = "vehicle_shares"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False) # User who shared
# Reference to the original vehicle
request_vehicle_id = Column(Integer, ForeignKey("request_vehicles.id"), nullable=False)
# Share code for the link
share_code = Column(String(10), unique=True, index=True, nullable=False)
# Pricing
original_price_krw = Column(Float, nullable=False) # Original vehicle price
markup_amount_krw = Column(Float, default=0) # Additional amount added by sharer
shared_price_krw = Column(Float, nullable=False) # Total shared price (original + markup)
# Statistics
view_count = Column(Integer, default=0)
is_purchased = Column(Boolean, default=False)
purchased_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
expires_at = Column(DateTime(timezone=True), nullable=True) # Optional expiration
purchased_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
user = relationship("User", foreign_keys=[user_id], backref="vehicle_shares")
purchased_by = relationship("User", foreign_keys=[purchased_by_user_id])
request_vehicle = relationship("RequestVehicle", backref="shares")
class ShareReward(Base):
"""Track rewards earned from vehicle shares"""
__tablename__ = "share_rewards"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False) # User who earned the reward
vehicle_share_id = Column(Integer, ForeignKey("vehicle_shares.id"), nullable=False)
# Amounts
markup_amount = Column(Float, nullable=False) # Original markup amount
reward_amount = Column(Float, nullable=False) # 90% of markup
tax_amount = Column(Float, nullable=False) # 3.3% tax withholding
net_amount = Column(Float, nullable=False) # Final amount after tax
# Status
status = Column(String(20), default="pending") # pending, approved, withdrawn
# Withdrawal tracking
withdrawal_request_id = Column(Integer, ForeignKey("withdrawal_requests.id"), nullable=True)
withdrawn_at = Column(DateTime(timezone=True), nullable=True)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
user = relationship("User", backref="share_rewards")
vehicle_share = relationship("VehicleShare", backref="reward")

View File

@@ -0,0 +1,111 @@
"""
Visitor tracking models for analytics
"""
from sqlalchemy import Column, Integer, String, DateTime, Text, Index
from sqlalchemy.sql import func
from ..database import Base
class VisitorLog(Base):
"""
Raw visitor log - tracks every page visit
IP addresses are hashed for privacy
"""
__tablename__ = "visitor_logs"
id = Column(Integer, primary_key=True, index=True)
# Visitor identification (hashed for privacy)
visitor_hash = Column(String(64), nullable=False, index=True) # SHA256 hash of IP + User-Agent
ip_hash = Column(String(64), nullable=False) # SHA256 hash of IP only
# Session tracking
session_id = Column(String(64), nullable=True, index=True) # Cookie-based session ID
user_id = Column(Integer, nullable=True, index=True) # If logged in
# Page information
page_path = Column(String(500), nullable=False, index=True)
page_title = Column(String(200), nullable=True)
referrer = Column(String(1000), nullable=True)
referrer_domain = Column(String(200), nullable=True, index=True)
# Device information
device_type = Column(String(20), nullable=True, index=True) # mobile, desktop, tablet
browser = Column(String(50), nullable=True, index=True)
browser_version = Column(String(20), nullable=True)
os = Column(String(50), nullable=True)
os_version = Column(String(20), nullable=True)
# Geographic information (from IP geolocation)
country = Column(String(50), nullable=True, index=True)
country_code = Column(String(5), nullable=True)
city = Column(String(100), nullable=True)
region = Column(String(100), nullable=True)
# UTM parameters
utm_source = Column(String(100), nullable=True)
utm_medium = Column(String(100), nullable=True)
utm_campaign = Column(String(100), nullable=True)
# Timestamp
visited_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
class VisitorDailyStats(Base):
"""
Aggregated daily statistics for faster queries
Pre-computed by a scheduled task
"""
__tablename__ = "visitor_daily_stats"
id = Column(Integer, primary_key=True, index=True)
stat_date = Column(String(10), nullable=False, unique=True, index=True) # YYYY-MM-DD
# Visitor counts
total_visits = Column(Integer, default=0)
unique_visitors = Column(Integer, default=0)
# Device breakdown (JSON string)
device_breakdown = Column(Text) # {"mobile": 100, "desktop": 200, "tablet": 20}
# Browser breakdown (JSON string)
browser_breakdown = Column(Text) # {"Chrome": 150, "Safari": 100, ...}
# Country breakdown (JSON string)
country_breakdown = Column(Text) # {"MN": 200, "RU": 50, "KR": 30}
# Top pages (JSON string)
top_pages = Column(Text) # [{"path": "/", "views": 500}, ...]
# Top referrers (JSON string)
top_referrers = Column(Text) # [{"domain": "google.com", "visits": 100}, ...]
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
class VisitorSession(Base):
"""
Track visitor sessions for better analytics
"""
__tablename__ = "visitor_sessions"
id = Column(Integer, primary_key=True, index=True)
session_id = Column(String(64), unique=True, nullable=False, index=True)
visitor_hash = Column(String(64), nullable=False, index=True)
user_id = Column(Integer, nullable=True)
# Session info
first_page = Column(String(500))
last_page = Column(String(500))
page_count = Column(Integer, default=1)
# Device/geo info (copied from first visit)
device_type = Column(String(20))
browser = Column(String(50))
country = Column(String(50))
# Timestamps
started_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
last_activity_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@@ -0,0 +1,35 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Float, Text
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from ..database import Base
class WithdrawalRequest(Base):
"""Track withdrawal requests from users"""
__tablename__ = "withdrawal_requests"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Amount details
amount = Column(Float, nullable=False) # Requested withdrawal amount
tax_withheld = Column(Float, default=0) # Tax amount withheld (3.3%)
net_amount = Column(Float, nullable=False) # Net amount after tax
# Bank info (snapshot at time of request)
bank_name = Column(String(50), nullable=False)
bank_account = Column(String(100), nullable=False)
account_holder = Column(String(100), nullable=False)
# Status
status = Column(String(20), default="pending") # pending, approved, completed, rejected
# Admin notes
admin_note = Column(Text, nullable=True)
# Timestamps
requested_at = Column(DateTime(timezone=True), server_default=func.now())
processed_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
user = relationship("User", backref="withdrawal_requests")