Files
feiqiu-ETL/etl_billiards/gui/widgets/task_panel.py
2026-01-27 22:45:50 +08:00

1062 lines
45 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""任务配置面板"""
from datetime import datetime, timedelta
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QGroupBox, QLabel, QLineEdit, QComboBox, QCheckBox,
QPushButton, QPlainTextEdit, QListWidget, QListWidgetItem,
QSplitter, QFrame, QFileDialog, QMessageBox, QScrollArea,
QSpinBox, QTabWidget, QDateTimeEdit
)
from PySide6.QtCore import Qt, Signal, QDateTime
from PySide6.QtGui import QFont
from ..models.task_model import TaskItem, TaskConfig, TaskCategory, TASK_CATEGORIES, get_task_category
from ..utils.cli_builder import CLIBuilder
from ..utils.app_settings import app_settings
from ..workers.task_worker import TaskWorker
# ODS 自动更新任务(从 API 抓取)
AUTO_UPDATE_TASKS = [
"ODS_PAYMENT",
"ODS_MEMBER",
"ODS_MEMBER_CARD",
"ODS_MEMBER_BALANCE",
"ODS_SETTLEMENT_RECORDS",
"ODS_TABLE_USE",
"ODS_ASSISTANT_ACCOUNT",
"ODS_ASSISTANT_LEDGER",
"ODS_ASSISTANT_ABOLISH",
"ODS_REFUND",
"ODS_PLATFORM_COUPON",
"ODS_RECHARGE_SETTLE",
"ODS_SETTLEMENT_TICKET",
]
# 数据校验相关任务
INTEGRITY_CHECK_TASKS = [
"DATA_INTEGRITY_CHECK",
]
# 所有可用任务
ALL_TASKS = [
# ODS 任务
("ODS_PAYMENT", "支付流水", "抓取支付记录到 ODS"),
("ODS_MEMBER", "会员档案", "抓取会员信息到 ODS"),
("ODS_MEMBER_CARD", "会员储值卡", "抓取会员卡信息到 ODS"),
("ODS_MEMBER_BALANCE", "会员余额变动", "抓取会员余额变动到 ODS"),
("ODS_SETTLEMENT_RECORDS", "结账记录", "抓取结账记录到 ODS"),
("ODS_TABLE_USE", "台费计费流水", "抓取台费使用记录到 ODS"),
("ODS_ASSISTANT_ACCOUNT", "助教账号", "抓取助教信息到 ODS"),
("ODS_ASSISTANT_LEDGER", "助教流水", "抓取助教服务流水到 ODS"),
("ODS_ASSISTANT_ABOLISH", "助教作废", "抓取助教作废记录到 ODS"),
("ODS_REFUND", "退款流水", "抓取退款记录到 ODS"),
("ODS_PLATFORM_COUPON", "平台券核销", "抓取券核销记录到 ODS"),
("ODS_RECHARGE_SETTLE", "充值结算", "抓取充值结算记录到 ODS"),
("ODS_GROUP_PACKAGE", "团购套餐", "抓取团购套餐定义到 ODS"),
("ODS_GROUP_BUY_REDEMPTION", "团购核销", "抓取团购核销记录到 ODS"),
("ODS_INVENTORY_STOCK", "库存汇总", "抓取库存汇总到 ODS"),
("ODS_INVENTORY_CHANGE", "库存变化", "抓取库存变动记录到 ODS"),
("ODS_TABLES", "台桌维表", "抓取台桌信息到 ODS"),
("ODS_GOODS_CATEGORY", "商品分类", "抓取商品分类到 ODS"),
("ODS_STORE_GOODS", "门店商品", "抓取门店商品档案到 ODS"),
("ODS_STORE_GOODS_SALES", "商品销售流水", "抓取商品销售流水到 ODS"),
("ODS_TABLE_FEE_DISCOUNT", "台费折扣", "抓取台费折扣记录到 ODS"),
("ODS_TENANT_GOODS", "租户商品", "抓取租户商品档案到 ODS"),
("ODS_SETTLEMENT_TICKET", "结账小票", "抓取结账小票详情到 ODS"),
# DWD/DWS 任务
("DWD_LOAD_FROM_ODS", "ODS→DWD 装载", "从 ODS 增量装载到 DWD"),
("DWD_QUALITY_CHECK", "DWD 质量检查", "执行 DWD 数据质量检查"),
("DWS_BUILD_ORDER_SUMMARY", "构建订单汇总", "重算 DWS 订单汇总表"),
# Schema 初始化
("INIT_ODS_SCHEMA", "初始化 ODS Schema", "创建/重建 ODS 表结构"),
("INIT_DWD_SCHEMA", "初始化 DWD Schema", "创建/重建 DWD 表结构"),
("INIT_DWS_SCHEMA", "初始化 DWS Schema", "创建/重建 DWS 表结构"),
# 其他任务
("MANUAL_INGEST", "手工数据灌入", "从本地 JSON 回放入库"),
("CHECK_CUTOFF", "检查 Cutoff", "查看各表数据截止时间"),
("DATA_INTEGRITY_CHECK", "数据完整性检查", "检查 ODS/DWD 数据完整性"),
]
class TaskPanel(QWidget):
"""任务配置面板"""
# 信号
task_started = Signal(str) # 任务开始信号
task_finished = Signal(bool, str) # 任务完成信号 (success, message)
log_message = Signal(str) # 日志消息信号
add_to_queue = Signal(object) # 添加到队列信号 (TaskConfig)
create_schedule = Signal(str, list, dict) # 创建调度任务信号 (name, task_codes, task_config)
def __init__(self, parent=None):
super().__init__(parent)
self.cli_builder = CLIBuilder()
self.worker = None
self._init_ui()
self._connect_signals()
self.refresh_tasks()
self._load_settings() # 启动时加载保存的设置
def _init_ui(self):
"""初始化界面"""
layout = QVBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(16)
# 标题
title = QLabel("任务配置")
title.setProperty("heading", True)
layout.addWidget(title)
# 创建分割器
splitter = QSplitter(Qt.Horizontal)
layout.addWidget(splitter, 1)
# 左侧:任务选择
left_widget = self._create_task_selection()
splitter.addWidget(left_widget)
# 右侧:参数配置
right_widget = self._create_config_area()
splitter.addWidget(right_widget)
# 设置分割比例
splitter.setSizes([400, 600])
# 底部CLI 预览和执行按钮
bottom_widget = self._create_bottom_area()
layout.addWidget(bottom_widget)
def _create_task_selection(self) -> QWidget:
"""创建任务选择区域"""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setContentsMargins(0, 0, 8, 0)
# 任务分类过滤
filter_layout = QHBoxLayout()
filter_layout.addWidget(QLabel("分类:"))
self.category_combo = QComboBox()
self.category_combo.addItem("全部", None)
self.category_combo.addItem("ODS 数据抓取", TaskCategory.ODS)
self.category_combo.addItem("DWD 装载", TaskCategory.DWD)
self.category_combo.addItem("DWS 汇总", TaskCategory.DWS)
self.category_combo.addItem("Schema 初始化", TaskCategory.SCHEMA)
self.category_combo.addItem("质量检查", TaskCategory.QUALITY)
self.category_combo.addItem("其他", TaskCategory.OTHER)
filter_layout.addWidget(self.category_combo, 1)
layout.addLayout(filter_layout)
# 快捷操作按钮
btn_layout = QHBoxLayout()
self.select_all_btn = QPushButton("全选")
self.select_all_btn.setProperty("secondary", True)
self.deselect_all_btn = QPushButton("全不选")
self.deselect_all_btn.setProperty("secondary", True)
btn_layout.addWidget(self.select_all_btn)
btn_layout.addWidget(self.deselect_all_btn)
layout.addLayout(btn_layout)
# 任务列表
self.task_list = QListWidget()
self.task_list.setSelectionMode(QListWidget.MultiSelection)
layout.addWidget(self.task_list, 1)
# 已选任务数
self.selected_count_label = QLabel("已选: 0 个任务")
self.selected_count_label.setProperty("subheading", True)
layout.addWidget(self.selected_count_label)
return widget
def _create_config_area(self) -> QWidget:
"""创建参数配置区域"""
# 使用选项卡区分快捷操作和高级配置
tab_widget = QTabWidget()
# 快捷操作选项卡
quick_tab = self._create_quick_actions_tab()
tab_widget.addTab(quick_tab, "快捷操作")
# 高级配置选项卡
advanced_tab = self._create_advanced_config_tab()
tab_widget.addTab(advanced_tab, "高级配置")
return tab_widget
def _create_quick_actions_tab(self) -> QWidget:
"""创建快捷操作选项卡"""
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setFrameShape(QFrame.NoFrame)
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(16)
# ====== 自动更新 ======
auto_update_group = QGroupBox("自动更新API → ODS")
auto_update_layout = QVBoxLayout(auto_update_group)
# 说明
desc_label = QLabel("从 API 抓取数据并更新 ODS 层,适用于日常增量更新")
desc_label.setProperty("subheading", True)
desc_label.setWordWrap(True)
auto_update_layout.addWidget(desc_label)
# 时间范围
time_layout = QHBoxLayout()
time_layout.addWidget(QLabel("时间范围:"))
self.auto_update_hours = QSpinBox()
self.auto_update_hours.setRange(1, 720) # 1小时 - 30天
self.auto_update_hours.setValue(24)
self.auto_update_hours.setSuffix(" 小时前")
time_layout.addWidget(self.auto_update_hours)
time_layout.addWidget(QLabel("到 当前"))
time_layout.addStretch()
auto_update_layout.addLayout(time_layout)
# 冗余窗口
overlap_layout = QHBoxLayout()
overlap_layout.addWidget(QLabel("冗余窗口:"))
self.overlap_seconds = QSpinBox()
self.overlap_seconds.setRange(0, 7200)
self.overlap_seconds.setValue(3600)
self.overlap_seconds.setSuffix("")
self.overlap_seconds.setToolTip("向前多抓取的时间,避免边界数据丢失")
overlap_layout.addWidget(self.overlap_seconds)
overlap_layout.addStretch()
auto_update_layout.addLayout(overlap_layout)
# 任务选择
task_header_layout = QHBoxLayout()
task_label = QLabel("包含任务:")
task_header_layout.addWidget(task_label)
task_header_layout.addStretch()
self.auto_update_select_all_btn = QPushButton("全选")
self.auto_update_select_all_btn.setProperty("secondary", True)
self.auto_update_select_all_btn.setFixedWidth(60)
task_header_layout.addWidget(self.auto_update_select_all_btn)
self.auto_update_deselect_all_btn = QPushButton("全不选")
self.auto_update_deselect_all_btn.setProperty("secondary", True)
self.auto_update_deselect_all_btn.setFixedWidth(60)
task_header_layout.addWidget(self.auto_update_deselect_all_btn)
auto_update_layout.addLayout(task_header_layout)
self.auto_update_tasks_list = QListWidget()
self.auto_update_tasks_list.setSelectionMode(QListWidget.MultiSelection)
self.auto_update_tasks_list.setMaximumHeight(150)
for task_code in AUTO_UPDATE_TASKS:
item = QListWidgetItem(task_code)
item.setSelected(True) # 默认全选
self.auto_update_tasks_list.addItem(item)
auto_update_layout.addWidget(self.auto_update_tasks_list)
# 包含 DWD 装载
self.include_dwd_check = QCheckBox("完成后执行 DWD 装载 (ODS → DWD)")
self.include_dwd_check.setChecked(True)
auto_update_layout.addWidget(self.include_dwd_check)
# 自动校验
self.auto_verify_check = QCheckBox("完成后执行数据校验")
self.auto_verify_check.setToolTip("ETL 导入完成后,自动执行数据完整性校验")
self.auto_verify_check.setChecked(False)
auto_update_layout.addWidget(self.auto_verify_check)
# 操作按钮
auto_btn_layout = QHBoxLayout()
auto_btn_layout.addStretch()
self.auto_update_run_btn = QPushButton("立即执行一次")
self.auto_update_run_btn.setToolTip("添加到任务队列并立即执行")
auto_btn_layout.addWidget(self.auto_update_run_btn)
self.auto_update_schedule_btn = QPushButton("创建调度任务")
self.auto_update_schedule_btn.setToolTip("添加到定时调度中,可设置执行周期")
auto_btn_layout.addWidget(self.auto_update_schedule_btn)
auto_update_layout.addLayout(auto_btn_layout)
layout.addWidget(auto_update_group)
# ====== 数据校验 ======
integrity_group = QGroupBox("数据校验API vs ODS")
integrity_layout = QVBoxLayout(integrity_group)
# 说明
int_desc = QLabel("对比 API 数据与 ODS 数据,检查是否有缺失或不一致")
int_desc.setProperty("subheading", True)
int_desc.setWordWrap(True)
integrity_layout.addWidget(int_desc)
# 校验模式
mode_layout = QHBoxLayout()
mode_layout.addWidget(QLabel("校验模式:"))
self.integrity_mode_combo = QComboBox()
self.integrity_mode_combo.addItem("历史全量校验", "history")
self.integrity_mode_combo.addItem("最近增量校验", "recent")
mode_layout.addWidget(self.integrity_mode_combo, 1)
integrity_layout.addLayout(mode_layout)
# 时间范围 - 历史模式
self.history_range_widget = QWidget()
history_layout = QGridLayout(self.history_range_widget)
history_layout.setContentsMargins(0, 0, 0, 0)
history_layout.addWidget(QLabel("开始日期:"), 0, 0)
self.integrity_start_date = QDateTimeEdit()
self.integrity_start_date.setDisplayFormat("yyyy-MM-dd")
self.integrity_start_date.setCalendarPopup(True)
self.integrity_start_date.setDateTime(QDateTime.currentDateTime().addMonths(-6))
history_layout.addWidget(self.integrity_start_date, 0, 1)
history_layout.addWidget(QLabel("结束日期:"), 1, 0)
self.integrity_end_date = QDateTimeEdit()
self.integrity_end_date.setDisplayFormat("yyyy-MM-dd")
self.integrity_end_date.setCalendarPopup(True)
self.integrity_end_date.setDateTime(QDateTime.currentDateTime())
history_layout.addWidget(self.integrity_end_date, 1, 1)
integrity_layout.addWidget(self.history_range_widget)
# 时间范围 - 最近模式
self.recent_range_widget = QWidget()
recent_layout = QHBoxLayout(self.recent_range_widget)
recent_layout.setContentsMargins(0, 0, 0, 0)
recent_layout.addWidget(QLabel("回溯时间:"))
self.integrity_hours = QSpinBox()
self.integrity_hours.setRange(1, 168) # 1小时 - 7天
self.integrity_hours.setValue(24)
self.integrity_hours.setSuffix(" 小时")
recent_layout.addWidget(self.integrity_hours)
recent_layout.addStretch()
self.recent_range_widget.setVisible(False)
integrity_layout.addWidget(self.recent_range_widget)
# 校验选项
self.include_dimensions_check = QCheckBox("包含维度表校验")
integrity_layout.addWidget(self.include_dimensions_check)
# 自动补全选项
self.auto_backfill_check = QCheckBox("校验后自动补全丢失数据")
self.auto_backfill_check.setToolTip("如果发现丢失数据,自动从 API 重新获取并补全到 ODS")
integrity_layout.addWidget(self.auto_backfill_check)
# 指定 ODS 任务
ods_task_layout = QHBoxLayout()
ods_task_layout.addWidget(QLabel("指定任务 (可选):"))
self.integrity_ods_tasks = QLineEdit()
self.integrity_ods_tasks.setPlaceholderText("例: ODS_PAYMENT,ODS_MEMBER (留空=全部)")
ods_task_layout.addWidget(self.integrity_ods_tasks, 1)
integrity_layout.addLayout(ods_task_layout)
# 操作按钮
int_btn_layout = QHBoxLayout()
int_btn_layout.addStretch()
self.integrity_run_btn = QPushButton("立即执行一次")
self.integrity_run_btn.setToolTip("添加到任务队列并立即执行")
int_btn_layout.addWidget(self.integrity_run_btn)
self.integrity_schedule_btn = QPushButton("创建调度任务")
self.integrity_schedule_btn.setToolTip("添加到定时调度中,可设置执行周期")
int_btn_layout.addWidget(self.integrity_schedule_btn)
integrity_layout.addLayout(int_btn_layout)
layout.addWidget(integrity_group)
# 弹性空间
layout.addStretch()
scroll_area.setWidget(widget)
return scroll_area
def _create_advanced_config_tab(self) -> QWidget:
"""创建高级配置选项卡"""
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setFrameShape(QFrame.NoFrame)
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setContentsMargins(8, 8, 8, 8)
# Pipeline 流程配置
pipeline_group = QGroupBox("流水线配置")
pipeline_layout = QGridLayout(pipeline_group)
pipeline_layout.addWidget(QLabel("运行模式:"), 0, 0)
self.pipeline_flow_combo = QComboBox()
self.pipeline_flow_combo.addItem("FULL - 在线抓取 + 入库", "FULL")
self.pipeline_flow_combo.addItem("FETCH_ONLY - 仅在线抓取落盘", "FETCH_ONLY")
self.pipeline_flow_combo.addItem("INGEST_ONLY - 仅本地 JSON 入库", "INGEST_ONLY")
pipeline_layout.addWidget(self.pipeline_flow_combo, 0, 1)
self.dry_run_check = QCheckBox("Dry-run 模式(不提交数据库)")
pipeline_layout.addWidget(self.dry_run_check, 1, 0, 1, 2)
layout.addWidget(pipeline_group)
# 时间窗口配置
window_group = QGroupBox("时间窗口(可选)")
window_layout = QGridLayout(window_group)
window_layout.addWidget(QLabel("开始时间:"), 0, 0)
self.window_start_edit = QLineEdit()
self.window_start_edit.setPlaceholderText("例: 2025-07-01 00:00:00")
window_layout.addWidget(self.window_start_edit, 0, 1)
window_layout.addWidget(QLabel("结束时间:"), 1, 0)
self.window_end_edit = QLineEdit()
self.window_end_edit.setPlaceholderText("例: 2025-08-01 00:00:00")
window_layout.addWidget(self.window_end_edit, 1, 1)
# 窗口切分选项
window_layout.addWidget(QLabel("切分模式:"), 2, 0)
self.window_split_combo = QComboBox()
self.window_split_combo.addItem("不切分", "none")
self.window_split_combo.addItem("按月切分", "month")
self.window_split_combo.setToolTip("长时间窗口按月切分执行,避免单次请求过大")
window_layout.addWidget(self.window_split_combo, 2, 1)
window_layout.addWidget(QLabel("补偿小时:"), 3, 0)
self.window_compensation_spin = QSpinBox()
self.window_compensation_spin.setRange(0, 168) # 最多7天
self.window_compensation_spin.setValue(0)
self.window_compensation_spin.setSuffix(" 小时")
self.window_compensation_spin.setToolTip("窗口前后扩展的小时数,用于捕获边界数据")
window_layout.addWidget(self.window_compensation_spin, 3, 1)
layout.addWidget(window_group)
# 数据源配置
source_group = QGroupBox("数据源配置INGEST_ONLY 模式)")
source_layout = QGridLayout(source_group)
source_layout.addWidget(QLabel("JSON 目录:"), 0, 0)
self.ingest_source_edit = QLineEdit()
self.ingest_source_edit.setPlaceholderText("本地 JSON 文件目录")
source_layout.addWidget(self.ingest_source_edit, 0, 1)
self.browse_btn = QPushButton("浏览...")
self.browse_btn.setProperty("secondary", True)
source_layout.addWidget(self.browse_btn, 0, 2)
layout.addWidget(source_group)
# 覆盖配置(高级)
override_group = QGroupBox("覆盖配置(可选)")
override_layout = QGridLayout(override_group)
override_layout.addWidget(QLabel("门店 ID:"), 0, 0)
self.store_id_edit = QLineEdit()
self.store_id_edit.setPlaceholderText("使用 .env 中的配置")
override_layout.addWidget(self.store_id_edit, 0, 1)
override_layout.addWidget(QLabel("数据库 DSN:"), 1, 0)
self.pg_dsn_edit = QLineEdit()
self.pg_dsn_edit.setPlaceholderText("使用 .env 中的配置")
override_layout.addWidget(self.pg_dsn_edit, 1, 1)
override_layout.addWidget(QLabel("API Token:"), 2, 0)
self.api_token_edit = QLineEdit()
self.api_token_edit.setPlaceholderText("使用 .env 中的配置")
self.api_token_edit.setEchoMode(QLineEdit.Password)
override_layout.addWidget(self.api_token_edit, 2, 1)
layout.addWidget(override_group)
# 弹性空间
layout.addStretch()
scroll_area.setWidget(widget)
return scroll_area
def _create_bottom_area(self) -> QWidget:
"""创建底部区域"""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setContentsMargins(0, 0, 0, 0)
# CLI 预览
preview_group = QGroupBox("命令行预览")
preview_layout = QVBoxLayout(preview_group)
self.cli_preview = QPlainTextEdit()
self.cli_preview.setReadOnly(True)
self.cli_preview.setMaximumHeight(80)
self.cli_preview.setFont(QFont("Consolas", 10))
preview_layout.addWidget(self.cli_preview)
layout.addWidget(preview_group)
# 执行按钮
btn_layout = QHBoxLayout()
btn_layout.addStretch()
self.add_to_queue_btn = QPushButton("添加到队列")
self.add_to_queue_btn.setProperty("secondary", True)
btn_layout.addWidget(self.add_to_queue_btn)
self.run_btn = QPushButton("立即执行")
self.run_btn.setFixedWidth(120)
btn_layout.addWidget(self.run_btn)
self.stop_btn = QPushButton("停止")
self.stop_btn.setProperty("danger", True)
self.stop_btn.setEnabled(False)
self.stop_btn.setFixedWidth(80)
btn_layout.addWidget(self.stop_btn)
layout.addLayout(btn_layout)
return widget
def _connect_signals(self):
"""连接信号"""
# 分类过滤
self.category_combo.currentIndexChanged.connect(self._filter_tasks)
# 任务选择
self.task_list.itemSelectionChanged.connect(self._on_selection_changed)
self.select_all_btn.clicked.connect(self._select_all)
self.deselect_all_btn.clicked.connect(self._deselect_all)
# 配置变化
self.pipeline_flow_combo.currentIndexChanged.connect(self._update_preview)
self.dry_run_check.stateChanged.connect(self._update_preview)
self.window_start_edit.textChanged.connect(self._update_preview)
self.window_end_edit.textChanged.connect(self._update_preview)
self.ingest_source_edit.textChanged.connect(self._update_preview)
self.store_id_edit.textChanged.connect(self._update_preview)
self.pg_dsn_edit.textChanged.connect(self._update_preview)
self.api_token_edit.textChanged.connect(self._update_preview)
# 浏览目录
self.browse_btn.clicked.connect(self._browse_source_dir)
# 执行按钮
self.run_btn.clicked.connect(self._run_task)
self.stop_btn.clicked.connect(self._stop_task)
self.add_to_queue_btn.clicked.connect(self._add_task_to_queue)
# 快捷操作 - 自动更新
self.auto_update_run_btn.clicked.connect(self._run_auto_update_now)
self.auto_update_schedule_btn.clicked.connect(self._create_auto_update_schedule)
self.auto_update_select_all_btn.clicked.connect(self._auto_update_select_all)
self.auto_update_deselect_all_btn.clicked.connect(self._auto_update_deselect_all)
# 快捷操作 - 数据校验
self.integrity_run_btn.clicked.connect(self._run_integrity_check_now)
self.integrity_schedule_btn.clicked.connect(self._create_integrity_schedule)
self.integrity_mode_combo.currentIndexChanged.connect(self._on_integrity_mode_changed)
# 保存设置的信号连接
self.auto_update_hours.valueChanged.connect(self._save_auto_update_settings)
self.overlap_seconds.valueChanged.connect(self._save_auto_update_settings)
self.include_dwd_check.stateChanged.connect(self._save_auto_update_settings)
self.auto_verify_check.stateChanged.connect(self._save_auto_update_settings)
self.integrity_mode_combo.currentIndexChanged.connect(self._save_integrity_settings)
self.integrity_start_date.dateTimeChanged.connect(self._save_integrity_settings)
self.integrity_end_date.dateTimeChanged.connect(self._save_integrity_settings)
self.integrity_hours.valueChanged.connect(self._save_integrity_settings)
self.include_dimensions_check.stateChanged.connect(self._save_integrity_settings)
self.auto_backfill_check.stateChanged.connect(self._save_integrity_settings)
self.integrity_ods_tasks.textChanged.connect(self._save_integrity_settings)
self.pipeline_flow_combo.currentIndexChanged.connect(self._save_advanced_settings)
self.dry_run_check.stateChanged.connect(self._save_advanced_settings)
self.window_split_combo.currentIndexChanged.connect(self._save_advanced_settings)
self.window_compensation_spin.valueChanged.connect(self._save_advanced_settings)
def refresh_tasks(self):
"""刷新任务列表"""
self.task_list.clear()
current_category = self.category_combo.currentData()
for code, name, desc in ALL_TASKS:
category = get_task_category(code)
# 应用分类过滤
if current_category is not None and category != current_category:
continue
item = QListWidgetItem(f"{name} ({code})")
item.setData(Qt.UserRole, code)
item.setToolTip(desc)
self.task_list.addItem(item)
self._on_selection_changed()
def _filter_tasks(self):
"""过滤任务列表"""
self.refresh_tasks()
def _on_selection_changed(self):
"""选择变化时"""
selected = self.task_list.selectedItems()
self.selected_count_label.setText(f"已选: {len(selected)} 个任务")
self._update_preview()
def _select_all(self):
"""全选"""
self.task_list.selectAll()
def _deselect_all(self):
"""全不选"""
self.task_list.clearSelection()
def _browse_source_dir(self):
"""浏览数据源目录"""
dir_path = QFileDialog.getExistingDirectory(
self, "选择 JSON 数据目录"
)
if dir_path:
self.ingest_source_edit.setText(dir_path)
def _get_config(self) -> TaskConfig:
"""获取当前配置"""
# 获取选中的任务
selected_tasks = []
for item in self.task_list.selectedItems():
task_code = item.data(Qt.UserRole)
if task_code:
selected_tasks.append(task_code)
# 构建环境变量(窗口切分参数)
env_vars = {}
split_unit = self.window_split_combo.currentData() or "none"
compensation = self.window_compensation_spin.value()
if split_unit and split_unit != "none":
env_vars["WINDOW_SPLIT_UNIT"] = split_unit
if compensation > 0:
env_vars["WINDOW_COMPENSATION_HOURS"] = str(compensation)
# 构建配置
config = TaskConfig(
tasks=selected_tasks,
pipeline_flow=self.pipeline_flow_combo.currentData(),
dry_run=self.dry_run_check.isChecked(),
window_start=self.window_start_edit.text().strip() or None,
window_end=self.window_end_edit.text().strip() or None,
window_split=split_unit,
window_compensation=compensation,
ingest_source=self.ingest_source_edit.text().strip() or None,
store_id=int(self.store_id_edit.text()) if self.store_id_edit.text().strip().isdigit() else None,
pg_dsn=self.pg_dsn_edit.text().strip() or None,
api_token=self.api_token_edit.text().strip() or None,
env_vars=env_vars,
)
return config
def _update_preview(self):
"""更新命令行预览"""
config = self._get_config()
cmd_str = self.cli_builder.build_command_string(config)
self.cli_preview.setPlainText(cmd_str)
def _run_task(self):
"""执行任务"""
config = self._get_config()
if not config.tasks:
QMessageBox.warning(self, "提示", "请至少选择一个任务")
return
# 创建工作线程
cmd = self.cli_builder.build_command(config)
self.worker = TaskWorker(cmd)
# 连接信号
self.worker.output_received.connect(self._on_output)
self.worker.task_finished.connect(self._on_finished)
self.worker.error_occurred.connect(self._on_error)
# 更新 UI 状态
self.run_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
# 发送开始信号
task_info = ",".join(config.tasks[:3])
if len(config.tasks) > 3:
task_info += f"{len(config.tasks)}个任务"
self.task_started.emit(task_info)
# 启动
self.worker.start()
def _stop_task(self):
"""停止任务"""
if self.worker and self.worker.isRunning():
self.worker.stop()
self.log_message.emit("[GUI] 正在停止任务...")
def _on_output(self, line: str):
"""收到输出"""
self.log_message.emit(line)
def _on_finished(self, exit_code: int, summary: str):
"""任务完成"""
self.run_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
success = exit_code == 0
message = summary if summary else ("任务执行成功" if success else f"任务执行失败 (exit={exit_code})")
self.task_finished.emit(success, message)
if success:
self.log_message.emit(f"[GUI] 任务完成: {message}")
else:
self.log_message.emit(f"[GUI] 任务失败: {message}")
def _on_error(self, error: str):
"""发生错误"""
self.log_message.emit(f"[GUI] 错误: {error}")
QMessageBox.critical(self, "执行错误", error)
def is_running(self) -> bool:
"""是否正在执行任务"""
return self.worker is not None and self.worker.isRunning()
def _add_task_to_queue(self):
"""将任务列表中选中的任务添加到队列"""
config = self._get_config()
if not config.tasks:
QMessageBox.warning(self, "提示", "请至少选择一个任务")
return
# 发送信号添加到队列
self.add_to_queue.emit(config)
task_info = ",".join(config.tasks[:3])
if len(config.tasks) > 3:
task_info += f"{len(config.tasks)}"
self.log_message.emit(f"[GUI] 已添加到任务队列: {task_info}")
QMessageBox.information(self, "提示", f"已添加到任务队列\n\n任务: {task_info}\n\n请切换到「任务管理」查看和执行")
def _on_integrity_mode_changed(self, index: int):
"""校验模式变化"""
mode = self.integrity_mode_combo.currentData()
self.history_range_widget.setVisible(mode == "history")
self.recent_range_widget.setVisible(mode == "recent")
def _auto_update_select_all(self):
"""自动更新任务列表全选"""
self.auto_update_tasks_list.selectAll()
self._save_auto_update_settings()
def _auto_update_deselect_all(self):
"""自动更新任务列表全不选"""
self.auto_update_tasks_list.clearSelection()
self._save_auto_update_settings()
def _get_auto_update_tasks(self) -> list:
"""获取自动更新选中的任务"""
selected_tasks = []
for i in range(self.auto_update_tasks_list.count()):
item = self.auto_update_tasks_list.item(i)
if item.isSelected():
selected_tasks.append(item.text())
if self.include_dwd_check.isChecked():
selected_tasks.append("DWD_LOAD_FROM_ODS")
return selected_tasks
def _get_auto_update_config(self) -> dict:
"""获取自动更新配置"""
return {
"pipeline_flow": "FULL",
"lookback_hours": self.auto_update_hours.value(),
"overlap_seconds": self.overlap_seconds.value(),
}
def _run_auto_update_now(self):
"""立即执行自动更新(添加到队列)"""
selected_tasks = self._get_auto_update_tasks()
if not selected_tasks:
QMessageBox.warning(self, "提示", "请至少选择一个任务")
return
hours = self.auto_update_hours.value()
overlap = self.overlap_seconds.value()
include_dwd = self.include_dwd_check.isChecked()
auto_verify = self.auto_verify_check.isChecked()
now = datetime.now()
start_time = now - timedelta(hours=hours, seconds=overlap)
# 构建完整任务列表
all_tasks = selected_tasks.copy()
# 添加 DWD 装载
if include_dwd and "DWD_LOAD_FROM_ODS" not in all_tasks:
all_tasks.append("DWD_LOAD_FROM_ODS")
# 添加数据校验
if auto_verify and "DATA_INTEGRITY_CHECK" not in all_tasks:
all_tasks.append("DATA_INTEGRITY_CHECK")
# 构建环境变量(用于校验任务)
env_vars = {}
if auto_verify:
env_vars["INTEGRITY_MODE"] = "window"
env_vars["INTEGRITY_AUTO_BACKFILL"] = "1" # 自动补全丢失数据
config = TaskConfig(
tasks=all_tasks,
pipeline_flow="FULL",
window_start=start_time.strftime("%Y-%m-%d %H:%M:%S"),
window_end=now.strftime("%Y-%m-%d %H:%M:%S"),
env_vars=env_vars,
)
# 更新预览
cmd_str = self.cli_builder.build_command_string(config)
self.cli_preview.setPlainText(cmd_str)
# 构建任务描述
desc_parts = [f"自动更新 ({hours}h)"]
if include_dwd:
desc_parts.append("+ DWD")
if auto_verify:
desc_parts.append("+ 校验")
task_desc = " ".join(desc_parts)
# 发送信号添加到队列
self.add_to_queue.emit(config)
self.log_message.emit(f"[GUI] 已添加到任务队列: {task_desc}")
QMessageBox.information(self, "提示", f"已添加到任务队列\n\n任务: {task_desc}\n包含: {len(all_tasks)} 个任务\n\n请切换到「任务管理」查看和执行")
def _create_auto_update_schedule(self):
"""创建自动更新的调度任务"""
selected_tasks = self._get_auto_update_tasks()
if not selected_tasks:
QMessageBox.warning(self, "提示", "请至少选择一个任务")
return
hours = self.auto_update_hours.value()
task_config = self._get_auto_update_config()
# 发送信号创建调度任务
self.create_schedule.emit(
f"自动更新 ({hours}h)",
selected_tasks,
task_config
)
self.log_message.emit(f"[GUI] 创建调度任务: 自动更新 ({hours}h)")
def _get_integrity_config(self) -> dict:
"""获取数据校验配置"""
mode = self.integrity_mode_combo.currentData()
config = {
"pipeline_flow": "INGEST_ONLY",
"integrity_mode": mode,
"integrity_include_dimensions": self.include_dimensions_check.isChecked(),
"integrity_auto_backfill": self.auto_backfill_check.isChecked(),
}
if mode == "history":
config["integrity_history_start"] = self.integrity_start_date.dateTime().toString("yyyy-MM-dd")
config["integrity_history_end"] = self.integrity_end_date.dateTime().toString("yyyy-MM-dd")
else:
config["lookback_hours"] = self.integrity_hours.value()
ods_tasks = self.integrity_ods_tasks.text().strip()
if ods_tasks:
config["integrity_ods_task_codes"] = ods_tasks
return config
def _run_integrity_check_now(self):
"""立即执行数据校验(添加到队列)"""
mode = self.integrity_mode_combo.currentData()
int_config = self._get_integrity_config()
# 通过环境变量传递 integrity 配置CLI 不支持这些参数)
env_vars = {
"INTEGRITY_MODE": int_config.get("integrity_mode", "history"),
"INTEGRITY_INCLUDE_DIMENSIONS": "1" if int_config.get("integrity_include_dimensions") else "0",
"INTEGRITY_AUTO_BACKFILL": "1" if int_config.get("integrity_auto_backfill") else "0",
}
if mode == "history":
env_vars["INTEGRITY_HISTORY_START"] = int_config.get("integrity_history_start")
env_vars["INTEGRITY_HISTORY_END"] = int_config.get("integrity_history_end")
desc = f"数据校验 ({int_config.get('integrity_history_start')} ~ {int_config.get('integrity_history_end')})"
else:
hours = int_config.get("lookback_hours", 24)
now = datetime.now()
start_time = now - timedelta(hours=hours)
env_vars["INTEGRITY_HISTORY_START"] = start_time.strftime("%Y-%m-%d")
env_vars["INTEGRITY_HISTORY_END"] = now.strftime("%Y-%m-%d")
desc = f"数据校验 (最近 {hours}h)"
if int_config.get("integrity_ods_task_codes"):
env_vars["INTEGRITY_ODS_TASK_CODES"] = int_config.get("integrity_ods_task_codes")
if int_config.get("integrity_auto_backfill"):
desc += " + 自动补全"
config = TaskConfig(
tasks=["DATA_INTEGRITY_CHECK"],
pipeline_flow="FULL", # 使用 FULL 因为需要 DB 连接
env_vars=env_vars,
)
# 更新预览
cmd_str = self.cli_builder.build_command_string(config)
self.cli_preview.setPlainText(cmd_str + f"\n\n# 环境变量:\n" + "\n".join(f"# {k}={v}" for k, v in env_vars.items()))
# 发送信号添加到队列
self.add_to_queue.emit(config)
self.log_message.emit(f"[GUI] 已添加到任务队列: {desc}")
QMessageBox.information(self, "提示", f"已添加到任务队列\n\n任务: {desc}\n\n请切换到「任务管理」查看和执行")
def _create_integrity_schedule(self):
"""创建数据校验的调度任务"""
mode = self.integrity_mode_combo.currentData()
task_config = self._get_integrity_config()
if mode == "history":
desc = f"数据校验 ({task_config.get('integrity_history_start')} ~ {task_config.get('integrity_history_end')})"
else:
hours = task_config.get("lookback_hours", 24)
desc = f"数据校验 (最近 {hours}h)"
# 发送信号创建调度任务
self.create_schedule.emit(
desc,
["DATA_INTEGRITY_CHECK"],
task_config
)
self.log_message.emit(f"[GUI] 创建调度任务: {desc}")
# ==================== 设置持久化 ====================
def _load_settings(self):
"""从持久化存储加载设置"""
try:
# 加载自动更新设置
self.auto_update_hours.setValue(app_settings.auto_update_hours)
self.overlap_seconds.setValue(app_settings.auto_update_overlap_seconds)
self.include_dwd_check.setChecked(app_settings.auto_update_include_dwd)
self.auto_verify_check.setChecked(app_settings.auto_update_auto_verify)
# 恢复自动更新任务选择
saved_tasks = app_settings.auto_update_selected_tasks
if saved_tasks:
for i in range(self.auto_update_tasks_list.count()):
item = self.auto_update_tasks_list.item(i)
item.setSelected(item.text() in saved_tasks)
# 加载数据校验设置
mode = app_settings.integrity_mode
mode_index = 0 if mode == "history" else 1
self.integrity_mode_combo.setCurrentIndex(mode_index)
# 历史日期
if app_settings.integrity_history_start:
try:
start_date = QDateTime.fromString(app_settings.integrity_history_start, "yyyy-MM-dd")
if start_date.isValid():
self.integrity_start_date.setDateTime(start_date)
except Exception:
pass
if app_settings.integrity_history_end:
try:
end_date = QDateTime.fromString(app_settings.integrity_history_end, "yyyy-MM-dd")
if end_date.isValid():
self.integrity_end_date.setDateTime(end_date)
except Exception:
pass
self.integrity_hours.setValue(app_settings.integrity_lookback_hours)
self.include_dimensions_check.setChecked(app_settings.integrity_include_dimensions)
self.auto_backfill_check.setChecked(app_settings.integrity_auto_backfill)
self.integrity_ods_tasks.setText(app_settings.integrity_ods_tasks)
# 加载高级设置
pipeline_flow = app_settings.advanced_pipeline_flow
flow_map = {"FULL": 0, "FETCH_ONLY": 1, "INGEST_ONLY": 2}
self.pipeline_flow_combo.setCurrentIndex(flow_map.get(pipeline_flow, 0))
self.dry_run_check.setChecked(app_settings.advanced_dry_run)
self.window_start_edit.setText(app_settings.advanced_window_start)
self.window_end_edit.setText(app_settings.advanced_window_end)
self.ingest_source_edit.setText(app_settings.advanced_ingest_source)
# 加载窗口切分设置
split_map = {"none": 0, "month": 1}
self.window_split_combo.setCurrentIndex(split_map.get(app_settings.advanced_window_split, 0))
self.window_compensation_spin.setValue(app_settings.advanced_window_compensation)
# 更新 UI 状态
self._on_integrity_mode_changed(mode_index)
except Exception as e:
# 加载失败时不影响程序运行
print(f"加载设置失败: {e}")
def _save_auto_update_settings(self):
"""保存自动更新设置"""
try:
app_settings.auto_update_hours = self.auto_update_hours.value()
app_settings.auto_update_overlap_seconds = self.overlap_seconds.value()
app_settings.auto_update_include_dwd = self.include_dwd_check.isChecked()
app_settings.auto_update_auto_verify = self.auto_verify_check.isChecked()
# 保存选中的任务
selected_tasks = []
for i in range(self.auto_update_tasks_list.count()):
item = self.auto_update_tasks_list.item(i)
if item.isSelected():
selected_tasks.append(item.text())
app_settings.auto_update_selected_tasks = selected_tasks
except Exception as e:
print(f"保存自动更新设置失败: {e}")
def _save_integrity_settings(self):
"""保存数据校验设置"""
try:
mode = self.integrity_mode_combo.currentData()
app_settings.integrity_mode = mode or "history"
app_settings.integrity_history_start = self.integrity_start_date.dateTime().toString("yyyy-MM-dd")
app_settings.integrity_history_end = self.integrity_end_date.dateTime().toString("yyyy-MM-dd")
app_settings.integrity_lookback_hours = self.integrity_hours.value()
app_settings.integrity_include_dimensions = self.include_dimensions_check.isChecked()
app_settings.integrity_auto_backfill = self.auto_backfill_check.isChecked()
app_settings.integrity_ods_tasks = self.integrity_ods_tasks.text().strip()
except Exception as e:
print(f"保存数据校验设置失败: {e}")
def _save_advanced_settings(self):
"""保存高级设置"""
try:
app_settings.advanced_pipeline_flow = self.pipeline_flow_combo.currentData() or "FULL"
app_settings.advanced_dry_run = self.dry_run_check.isChecked()
app_settings.advanced_window_start = self.window_start_edit.text().strip()
app_settings.advanced_window_end = self.window_end_edit.text().strip()
app_settings.advanced_ingest_source = self.ingest_source_edit.text().strip()
app_settings.advanced_window_split = self.window_split_combo.currentData() or "none"
app_settings.advanced_window_compensation = self.window_compensation_spin.value()
except Exception as e:
print(f"保存高级设置失败: {e}")