太棒了!我们现在将 **Superset 自动轮播系统** 完整、专业地呈现为一个可用于生产环境的部署项目。以下是 **超详细部署方案**,涵盖:
- ✅ 项目结构
- ✅ Python 虚拟环境配置
- ✅ `.env` 安全管理
- ✅ 日志系统
- ✅ WSL2 + Ubuntu 22.04 部署全流程
- ✅ 开机自启与浏览器全屏播放(Kiosk 模式)
- ✅ 维护建议
---
# 🎯 项目名称:`superset-kiosk`
> 用于大屏展示 Superset Dashboard 的自动轮播系统
> 支持标签过滤、定时切换、Token 刷新、日志记录、安全配置
---
## 📁 一、完整项目结构(最终版)
```bash
superset-kiosk/
├── .env # 环境变量(敏感信息,不提交 Git)
├── .gitignore # 忽略日志、虚拟环境等
├── requirements.txt # 依赖列表
├── main.py # 主程序入口
│
├── config/
│ ├── __init__.py
│ └── settings.py # 从 .env 加载所有配置
│
├── auth/
│ ├── __init__.py
│ └── superset_auth.py # 登录 + token 管理(含刷新机制)
│
├── api/
│ ├── __init__.py
│ └── dashboard_api.py # 获取 dashboard 列表(支持 tag 过滤)
│
├── kiosk/
│ ├── __init__.py
│ └── html_generator.py # 生成自动轮播 HTML 页面
│
├── utils/
│ ├── __init__.py
│ ├── logger.py # 全局日志模块(文件 + 控制台)
│ └── helpers.py # 打开浏览器等工具函数
│
├── static/ # 输出目录
│ └── superset_kiosk.html # 自动生成的轮播页面
│
├── logs/ # 日志目录(运行后自动生成)
│ └── app_20250405.log # 按日期命名的日志文件
│
└── README.md # 使用说明文档
```
---
## 🔐 二、`.env` 文件(根目录)—— 安全配置
⚠️ 此文件 **绝不能提交到 Git**
```env
# ==================== Superset 连接 ====================
SUPERSET_BASE_URL=http://localhost:8088
SUPERSET_USERNAME=admin
SUPERSET_PASSWORD=admin
SUPERSET_PROVIDER=db
# ==================== 轮播行为 ====================
KIOSK_REFRESH_INTERVAL=15 # 每个仪表盘显示时间(秒)
KIOSK_RANDOM_ORDER=false # 是否随机顺序(true/false)
KIOSK_FILTER_TAG=kiosk # 只加载带有此标签的 dashboard;设为 None 表示不限制
KIOSK_DASHBOARD_IDS=1,3,5 # 指定 ID 列表;留空表示获取全部
# ==================== 输出路径 ====================
KIOSK_OUTPUT_HTML=../static/superset_kiosk.html
```
> 💡 提示:
> - `KIOSK_FILTER_TAG=None` 表示不过滤标签
> - `KIOSK_DASHBOARD_IDS=` 留空表示获取所有 dashboard
---
## 📦 三、`requirements.txt`
```txt
requests==2.31.0
python-dotenv==1.0.1
```
安装命令:
```bash
pip install -r requirements.txt
```
---
## ⚙️ 四、Python 虚拟环境(推荐使用)
### 1. 创建虚拟环境
```bash
python3 -m venv venv
```
### 2. 激活虚拟环境
```bash
source venv/bin/activate
```
激活后提示符变为:
```bash
(venv) user@hostname:~/superset-kiosk$
```
### 3. 安装依赖
```bash
(venv) pip install -r requirements.txt
```
### 4. (可选)退出虚拟环境
```bash
(venv) deactivate
```
---
## 🧩 五、各模块代码实现(最终版本)
> 下面是每个文件的 **完整、可复制粘贴代码**
---
### ✅ `config/settings.py`
```python
# config/settings.py
import os
from dotenv import load_dotenv
load_dotenv() # 加载 .env
# === Superset 配置 ===
SUPerset_BASE_URL = os.getenv("SUPERSET_BASE_URL")
USERNAME = os.getenv("SUPERSET_USERNAME")
PASSWORD = os.getenv("SUPERSET_PASSWORD")
PROVIDER = os.getenv("SUPERSET_PROVIDER", "db")
# === 轮播配置 ===
try:
REFRESH_INTERVAL_SEC = int(os.getenv("KIOSK_REFRESH_INTERVAL", 10))
except ValueError:
REFRESH_INTERVAL_SEC = 10
RANDOM_ORDER = os.getenv("KIOSK_RANDOM_ORDER", "false").lower() == "true"
FILTER_BY_TAG = os.getenv("KIOSK_FILTER_TAG", None)
if FILTER_BY_TAG in ["None", "", "null"]:
FILTER_BY_TAG = None
# 解析 DASHBOARD_IDS
DASHBOARD_IDS_RAW = os.getenv("KIOSK_DASHBOARD_IDS", "").strip()
if DASHBOARD_IDS_RAW:
try:
DASHBOARD_IDS = [int(x.strip()) for x in DASHBOARD_IDS_RAW.split(",") if x.strip().isdigit()]
except:
DASHBOARD_IDS = []
else:
DASHBOARD_IDS = []
# 输出路径
OUTPUT_HTML = os.getenv("KIOSK_OUTPUT_HTML", "../static/superset_kiosk.html")
```
---
### ✅ `utils/logger.py`
```python
# utils/logger.py
import logging
import os
from datetime import datetime
def setup_logger():
logger = logging.getLogger("superset_kiosk")
if logger.handlers:
return logger # 防止重复添加 handler
logger.setLevel(logging.DEBUG)
# 创建 logs 目录
log_dir = os.path.join(os.path.dirname(__file__), "..", "logs")
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, f"app_{datetime.now().strftime('%Y%m%d')}.log")
# 文件处理器(DEBUG+)
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_formatter = logging.Formatter(
'%(asctime)s | %(levelname)-8s | %(name)s:%(lineno)d | %(message)s'
)
file_handler.setFormatter(file_formatter)
# 控制台处理器(INFO+)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('%(levelname)s: %(message)s')
console_handler.setFormatter(console_formatter)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger
logger = setup_logger()
```
---
### ✅ `auth/superset_auth.py`
```python
# auth/superset_auth.py
from utils.logger import logger
import requests
import json
from datetime import datetime, timedelta
from config.settings import SUPerset_BASE_URL, USERNAME, PASSWORD, PROVIDER
class SupersetAuth:
def __init__(self):
self.base_url = SUPerset_BASE_URL
self.access_token = None
self.refresh_token = None
self.token_expires_at = None
def login(self):
logger.info("正在登录 Superset...")
url = f"{self.base_url}/api/v1/security/login"
payload = {"username": USERNAME, "password": PASSWORD, "provider": PROVIDER}
headers = {"Content-Type": "application/json"}
try:
response = requests.post(url, data=json.dumps(payload), headers=headers)
response.raise_for_status()
data = response.json()
self.access_token = data["access_token"]
self.refresh_token = data.get("refresh_token")
self.token_expires_at = datetime.now() + timedelta(minutes=29)
logger.info("✅ 登录成功")
return self.access_token
except Exception as e:
logger.error(f"❌ 登录失败: {e}")
raise
def is_token_expired(self):
return datetime.now() >= self.token_expires_at
def refresh_token(self):
if not self.refresh_token:
logger.warning("⚠️ 无 refresh_token,重新登录...")
return self.login()
url = f"{self.base_url}/api/v1/security/refresh"
headers = {"Authorization": f"Bearer {self.refresh_token}"}
try:
response = requests.post(url, headers=headers)
if response.status_code == 200:
self.access_token = response.json()["access_token"]
self.token_expires_at = datetime.now() + timedelta(minutes=29)
logger.info("🔄 Token 已刷新")
return self.access_token
else:
logger.warning(f"⚠️ Refresh 失败 ({response.status_code}),重新登录...")
return self.login()
except Exception as e:
logger.error(f"❌ Refresh 异常: {e}")
return self.login()
def get_access_token(self):
if not self.access_token or self.is_token_expired():
return self.refresh_token()
return self.access_token
```
---
### ✅ `api/dashboard_api.py`
```python
# api/dashboard_api.py
from utils.logger import logger
import requests
import json
from auth.superset_auth import SupersetAuth
from config.settings import SUPerset_BASE_URL, FILTER_BY_TAG
class DashboardAPI:
def __init__(self, auth_client: SupersetAuth):
self.auth = auth_client
self.base_url = SUPerset_BASE_URL
def get_all_dashboards(self):
logger.info("正在获取 Dashboard 列表...")
url = f"{self.base_url}/api/v1/dashboard/"
headers = {"Authorization": f"Bearer {self.auth.get_access_token()}"}
query = {"order_column": "id", "order_direction": "asc"}
if FILTER_BY_TAG:
query["filters"] = [{"col": "tags", "opr": "rel_m_m", "value": FILTER_BY_TAG}]
params = {"q": json.dumps(query)}
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
result = response.json()["result"]
dashboards = [
{"id": d["id"], "title": d["dashboard_title"], "url": f"{self.base_url}{d['url']}"}
for d in result if d["id"] and d["url"]
]
logger.info(f"📊 成功获取 {len(dashboards)} 个仪表盘")
return dashboards
except Exception as e:
logger.error(f"❌ 获取 Dashboard 失败: {e}")
raise
```
---
### ✅ `kiosk/html_generator.py`
```python
# kiosk/html_generator.py
from utils.logger import logger
import os
import json
from config.settings import OUTPUT_HTML, REFRESH_INTERVAL_SEC, RANDOM_ORDER
class HTMLGenerator:
@staticmethod
def generate(dashboards):
logger.info("生成轮播页面中...")
if not dashboards:
logger.warning("⚠️ 传入的 dashboard 列表为空")
return
if RANDOM_ORDER:
from random import shuffle
shuffle(dashboards)
logger.debug("🔀 已启用随机顺序")
os.makedirs(os.path.dirname(OUTPUT_HTML), exist_ok=True)
html_content = f'''
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Superset Dashboard 轮播</title>
<style>
body, html {{ margin: 0; padding: 0; overflow: hidden; background: #000; color: white; font-family: Arial, sans-serif; }}
#container {{ width: 100vw; height: 100vh; position: relative; }}
iframe {{ width: 100%; height: 100%; border: none; }}
.info-bar {{
position: absolute; top: 10px; left: 10px;
background: rgba(0,0,0,0.7); color: white;
padding: 5px 10px; border-radius: 4px;
z-index: 100; font-size: 14px;
}}
</style>
</head>
<body>
<div id="container">
<iframe id="dash-frame" src="" frameborder="0"></iframe>
<div class="info-bar" id="info">Loading...</div>
</div>
<script>
const dashboards = {json.dumps(dashboards)};
let currentIndex = 0;
const intervalSec = {REFRESH_INTERVAL_SEC};
function showNextDashboard() {{
if (dashboards.length === 0) {{
document.getElementById("info").textContent = "No dashboards found.";
return;
}}
const dash = dashboards[currentIndex];
document.getElementById("dash-frame").src = dash.url + "?standalone=3";
document.getElementById("info").textContent = `[${{currentIndex + 1}}/${{dashboards.length}}] ${{dash.title}}`;
currentIndex = (currentIndex + 1) % dashboards.length;
}}
showNextDashboard();
setInterval(showNextDashboard, intervalSec * 1000);
</script>
</body>
</html>
'''
with open(OUTPUT_HTML, "w", encoding="utf-8") as f:
f.write(html_content)
logger.info(f"✅ 轮播页面已生成: {os.path.abspath(OUTPUT_HTML)}")
```
---
### ✅ `utils/helpers.py`
```python
# utils/helpers.py
import webbrowser
import os
def open_in_browser(filepath):
abs_path = os.path.abspath(filepath)
webbrowser.open(f"file://{abs_path}")
print(f"🌐 已打开浏览器: file://{abs_path}")
```
---
### ✅ `main.py`
```python
# main.py
from utils.logger import logger
from auth.superset_auth import SupersetAuth
from api.dashboard_api import DashboardAPI
from kiosk.html_generator import HTMLGenerator
from utils.helpers import open_in_browser
from config.settings import DASHBOARD_IDS
def main():
try:
logger.info("🚀 启动 Superset 轮播系统")
auth = SupersetAuth()
auth.login()
dashboard_api = DashboardAPI(auth)
all_dashboards = dashboard_api.get_all_dashboards()
if DASHBOARD_IDS:
filtered = [d for d in all_dashboards if d["id"] in DASHBOARD_IDS]
if not filtered:
logger.warning("⚠️ 没有匹配 ID 的 Dashboard,请检查 .env 中 KIOSK_DASHBOARD_IDS 设置")
return
logger.info(f"✅ 已根据 ID 过滤出 {len(filtered)} 个 Dashboard")
else:
filtered = all_dashboards
logger.info(f"✅ 使用全部 {len(filtered)} 个 Dashboard")
HTMLGenerator.generate(filtered)
open_in_browser("../static/superset_kiosk.html")
logger.info(f"🎯 轮播系统启动完成!每 {REFRESH_INTERVAL_SEC} 秒切换一次")
except KeyboardInterrupt:
logger.info("👋 用户中断程序")
except Exception as e:
logger.critical(f"💥 系统运行出错: {e}", exc_info=True)
if __name__ == "__main__":
main()
```
---
## 🐧 六、WSL2 + Ubuntu 22.04 详细部署步骤
### 步骤 1:打开 PowerShell 启动 WSL
```powershell
wsl
```
确认你是 Ubuntu 用户:
```bash
whoami # 应输出你的用户名
pwd # 应在 /home/yourname
```
### 步骤 2:更新系统 & 安装基础工具
```bash
sudo apt update && sudo apt upgrade -y
sudo apt install python3 python3-pip git -y
```
验证:
```bash
python3 --version # 推荐 3.10+
pip3 --version
```
### 步骤 3:创建项目目录
```bash
mkdir ~/superset-kiosk
cd ~/superset-kiosk
```
### 步骤 4:初始化项目结构
```bash
mkdir -p config auth api kiosk utils static logs
touch config/__init__.py auth/__init__.py api/__init__.py kiosk/__init__.py utils/__init__.py
touch main.py requirements.txt .env .gitignore
```
### 步骤 5:粘贴所有代码文件
依次创建并写入上面提供的各 `.py` 文件内容。
### 步骤 6:创建 `.gitignore`
```bash
nano .gitignore
```
内容:
```
venv/
__pycache__/
*.pyc
.env
logs/
*.log
static/*.html
.DS_Store
```
### 步骤 7:创建虚拟环境并安装依赖
```bash
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
```
### 步骤 8:编辑 `.env` 文件
```bash
nano .env
```
粘贴前面提供的 `.env` 内容,并根据你的 Superset 修改 URL 和账号。
### 步骤 9:运行系统
```bash
source venv/bin/activate # 每次运行前激活
python main.py
```
---
## 🖥 七、Windows 浏览器全屏播放(Kiosk 模式)
### 方法:Chrome Kiosk 模式
1. 打开 Chrome 快捷方式属性
2. 目标填入:
```cmd
"C:\Program Files\Google\Chrome\Application\chrome.exe"
--kiosk --start-fullscreen --app=file://\\wsl$\Ubuntu\home\yourname\superset-kiosk\static\superset_kiosk.html
```
3. 勾选“以全屏模式运行”
4. 放入「开始」→「启动」文件夹,实现开机自动播放
---
## 🛠 八、维护与扩展建议
| 功能 | 实现方式 |
|------|----------|
| 自动重启脚本 | 使用 `cron` 每天凌晨重启 |
| 错误报警 | 添加邮件或微信推送(如 `smtplib` 或 Server酱) |
| 多环境配置 | `.env.dev`, `.env.prod` + `ENV=prod` 切换 |
| Docker 化 | 编写 `Dockerfile` 和 `docker-compose.yml` |
| systemd 守护 | 在 WSL 中注册为服务自动运行 |
---
## 📄 示例日志输出(`logs/app_20250405.log`)
```
2025-04-05 14:20:01,123 | INFO | superset_kiosk:25 | 🚀 启动 Superset 轮播系统
2025-04-05 14:20:01,124 | INFO | superset_auth:30 | 正在登录 Superset...
2025-04-05 14:20:02,200 | INFO | superset_auth:60 | ✅ 登录成功
2025-04-05 14:20:02,201 | INFO | dashboard_api:18 | 正在获取 Dashboard 列表...
2025-04-05 14:20:03,305 | INFO | dashboard_api:45 | 📊 成功获取 3 个仪表盘
2025-04-05 14:20:03,306 | INFO | html_generator:20 | 生成轮播页面中...
2025-04-05 14:20:03,310 | INFO | html_generator:50 | ✅ 轮播页面已生成
2025-04-05 14:20:03,311 | INFO | main:40 | 🎯 轮播系统启动完成!每 15 秒切换一次
```
---
###
根据以上内容,此处应该如何修改