174 lines
7.0 KiB
Python
174 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
匯入進度追蹤模型
|
||
"""
|
||
|
||
from sqlalchemy import Column, Integer, String, DateTime, Text, Float
|
||
from database.models import Base
|
||
from datetime import datetime
|
||
import pytz
|
||
|
||
# 台北時區
|
||
TAIPEI_TZ = pytz.timezone('Asia/Taipei')
|
||
|
||
|
||
def taipei_now():
|
||
"""取得台北時區的當前時間(無時區資訊,用於資料庫存儲)"""
|
||
return datetime.now(TAIPEI_TZ).replace(tzinfo=None)
|
||
|
||
|
||
class ImportJob(Base):
|
||
"""匯入任務模型"""
|
||
__tablename__ = 'import_jobs'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True, comment='匯入任務 ID')
|
||
|
||
# 任務資訊
|
||
job_type = Column(String(50), nullable=False, comment='任務類型:daily_sales, vendor_stockout')
|
||
status = Column(String(20), nullable=False, default='pending', comment='狀態:pending, downloading, importing, completed, failed')
|
||
|
||
# Google Drive 檔案資訊
|
||
drive_file_id = Column(String(200), comment='Google Drive 檔案 ID')
|
||
drive_file_name = Column(String(500), comment='Google Drive 檔案名稱')
|
||
drive_file_size = Column(Integer, comment='檔案大小(bytes)')
|
||
|
||
# 本地檔案資訊
|
||
local_file_path = Column(String(500), comment='本地檔案路徑')
|
||
|
||
# 進度資訊
|
||
progress_percent = Column(Float, default=0.0, comment='進度百分比 (0-100)')
|
||
current_step = Column(String(200), comment='當前步驟描述')
|
||
total_rows = Column(Integer, comment='總行數')
|
||
processed_rows = Column(Integer, default=0, comment='已處理行數')
|
||
success_rows = Column(Integer, default=0, comment='成功匯入行數')
|
||
error_rows = Column(Integer, default=0, comment='錯誤行數')
|
||
|
||
# 時間記錄 (2026-01-30 修正:使用台北時區)
|
||
created_at = Column(DateTime, default=taipei_now, comment='建立時間')
|
||
started_at = Column(DateTime, comment='開始時間')
|
||
completed_at = Column(DateTime, comment='完成時間')
|
||
|
||
# 結果資訊
|
||
error_message = Column(Text, comment='錯誤訊息')
|
||
import_summary = Column(Text, comment='匯入摘要(JSON 格式)')
|
||
|
||
def __repr__(self):
|
||
return f"<ImportJob(id={self.id}, type={self.job_type}, status={self.status}, progress={self.progress_percent}%)>"
|
||
|
||
def to_dict(self):
|
||
"""轉換為字典格式"""
|
||
return {
|
||
'id': self.id,
|
||
'job_type': self.job_type,
|
||
'status': self.status,
|
||
'drive_file_id': self.drive_file_id,
|
||
'drive_file_name': self.drive_file_name,
|
||
'drive_file_size': self.drive_file_size,
|
||
'local_file_path': self.local_file_path,
|
||
'progress_percent': self.progress_percent,
|
||
'current_step': self.current_step,
|
||
'total_rows': self.total_rows,
|
||
'processed_rows': self.processed_rows,
|
||
'success_rows': self.success_rows,
|
||
'error_rows': self.error_rows,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||
'started_at': self.started_at.isoformat() if self.started_at else None,
|
||
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
|
||
'error_message': self.error_message,
|
||
'import_summary': self.import_summary,
|
||
}
|
||
|
||
|
||
class ImportConfig(Base):
|
||
"""匯入配置模型"""
|
||
__tablename__ = 'import_config'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
|
||
# 配置項目
|
||
config_key = Column(String(100), unique=True, nullable=False, comment='配置鍵')
|
||
config_value = Column(Text, comment='配置值')
|
||
config_type = Column(String(50), comment='配置類型:string, int, bool, json')
|
||
description = Column(String(500), comment='配置說明')
|
||
|
||
# 時間記錄 (2026-01-30 修正:使用台北時區)
|
||
created_at = Column(DateTime, default=taipei_now, comment='建立時間')
|
||
updated_at = Column(DateTime, default=taipei_now, onupdate=taipei_now, comment='更新時間')
|
||
|
||
def __repr__(self):
|
||
return f"<ImportConfig(key={self.config_key}, value={self.config_value})>"
|
||
|
||
def to_dict(self):
|
||
"""轉換為字典格式"""
|
||
return {
|
||
'id': self.id,
|
||
'config_key': self.config_key,
|
||
'config_value': self.config_value,
|
||
'config_type': self.config_type,
|
||
'description': self.description,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||
}
|
||
|
||
|
||
class SalesAcquisitionReceipt(Base):
|
||
"""Durable controlled-apply receipt for PChome sales acquisition runs."""
|
||
__tablename__ = 'pchome_sales_acquisition_receipts'
|
||
|
||
receipt_id = Column(String(64), primary_key=True)
|
||
trace_id = Column(String(64), nullable=False, index=True)
|
||
span_id = Column(String(16), nullable=False, index=True)
|
||
run_id = Column(String(64), nullable=False, index=True)
|
||
work_item_id = Column(String(100), nullable=False, index=True)
|
||
status = Column(String(40), nullable=False, index=True)
|
||
source_type = Column(String(40), nullable=False, index=True)
|
||
source_fingerprint = Column(String(64), index=True)
|
||
source_ref_hash = Column(String(64))
|
||
source_file_name = Column(String(500))
|
||
risk_level = Column(String(20), nullable=False, default='medium')
|
||
policy_decision = Column(String(40), nullable=False)
|
||
decision = Column(String(80), nullable=False)
|
||
import_job_id = Column(Integer, index=True)
|
||
rows_imported = Column(Integer, default=0)
|
||
before_freshness_json = Column(Text)
|
||
stage_receipts_json = Column(Text)
|
||
verifier_json = Column(Text)
|
||
error_kind = Column(String(100))
|
||
public_message = Column(Text)
|
||
started_at = Column(DateTime, default=taipei_now, nullable=False)
|
||
completed_at = Column(DateTime)
|
||
|
||
def to_public_dict(self):
|
||
def parse_json(value):
|
||
if not value:
|
||
return None
|
||
try:
|
||
import json
|
||
return json.loads(value)
|
||
except Exception:
|
||
return None
|
||
|
||
return {
|
||
'receipt_id': self.receipt_id,
|
||
'trace_id': self.trace_id,
|
||
'span_id': self.span_id,
|
||
'run_id': self.run_id,
|
||
'work_item_id': self.work_item_id,
|
||
'status': self.status,
|
||
'source_type': self.source_type,
|
||
'source_file_name': self.source_file_name,
|
||
'risk_level': self.risk_level,
|
||
'policy_decision': self.policy_decision,
|
||
'decision': self.decision,
|
||
'import_job_id': self.import_job_id,
|
||
'rows_imported': self.rows_imported or 0,
|
||
'before_freshness': parse_json(self.before_freshness_json),
|
||
'stages': parse_json(self.stage_receipts_json),
|
||
'verifier': parse_json(self.verifier_json),
|
||
'error_kind': self.error_kind,
|
||
'message': self.public_message,
|
||
'started_at': self.started_at.isoformat() if self.started_at else None,
|
||
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
|
||
}
|