新增钉钉通知渠道支持,更新数据库结构以适应新功能,并优化相关日志记录

This commit is contained in:
amazingzl 2025-08-02 16:24:52 +08:00
parent 565625ab15
commit 8ac5909db1
3 changed files with 201 additions and 22 deletions

View File

@ -918,10 +918,13 @@ class XianyuLive:
channel_config = notification.get('channel_config')
try:
if channel_type == 'qq':
await self._send_qq_notification(channel_config, notification_msg)
else:
logger.warning(f"不支持的通知渠道类型: {channel_type}")
match channel_type:
case 'qq':
await self._send_qq_notification(channel_config, notification_msg)
case 'ding_talk':
await self._send_ding_talk_notification(channel_config, notification_msg)
case _:
logger.warning(f"不支持的通知渠道类型: {channel_type}")
except Exception as notify_error:
logger.error(f"发送通知失败 ({notification.get('channel_name', 'Unknown')}): {self._safe_str(notify_error)}")
@ -958,6 +961,35 @@ class XianyuLive:
except Exception as e:
logger.error(f"发送QQ通知异常: {self._safe_str(e)}")
async def _send_ding_talk_notification(self, config: str, message: str):
"""发送钉钉通知"""
try:
import aiohttp
import json
# 解析配置钉钉机器人Webhook URL
webhook_url = config.strip()
if not webhook_url:
logger.warning("钉钉通知配置为空")
return
data = {
"msgtype": "markdown",
"markdown": {
"title": "闲鱼自动回复通知",
"text": message
}
}
async with aiohttp.ClientSession() as session:
async with session.post(webhook_url, json=data, timeout=10) as response:
if response.status == 200:
logger.info(f"钉钉通知发送成功: {webhook_url}")
else:
logger.warning(f"钉钉通知发送失败: {response.status}")
except Exception as e:
logger.error(f"发送钉钉通知异常: {self._safe_str(e)}")
async def send_token_refresh_notification(self, error_message: str, notification_type: str = "token_refresh"):
"""发送Token刷新异常通知带防重复机制"""
try:

View File

@ -295,6 +295,16 @@ class DBManager:
)
''')
# 创建系统设置表
cursor.execute('''
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
description TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建消息通知配置表
cursor.execute('''
CREATE TABLE IF NOT EXISTS message_notifications (
@ -310,16 +320,6 @@ class DBManager:
)
''')
# 创建系统设置表
cursor.execute('''
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
description TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建用户设置表
cursor.execute('''
CREATE TABLE IF NOT EXISTS user_settings (
@ -341,6 +341,45 @@ class DBManager:
('theme_color', 'blue', '主题颜色')
''')
# 检查并升级数据库
self.check_and_upgrade_db(cursor)
self.conn.commit()
logger.info("数据库初始化完成")
except Exception as e:
logger.error(f"数据库初始化失败: {e}")
self.conn.rollback()
raise
def check_and_upgrade_db(self, cursor):
"""检查数据库版本并执行必要的升级"""
try:
# 获取当前数据库版本
current_version = self.get_system_setting("db_version") or "1.0"
logger.info(f"当前数据库版本: {current_version}")
if current_version == "1.0":
logger.info("开始升级数据库到版本1.0...")
self.update_admin_user_id(cursor)
self.set_system_setting("db_version", "1.0", "数据库版本号")
logger.info("数据库升级到版本1.0完成")
# 如果版本低于需要升级的版本,执行升级
if current_version < "1.1":
logger.info("开始升级数据库到版本1.1...")
self.upgrade_notification_channels_table(cursor)
self.set_system_setting("db_version", "1.1", "数据库版本号")
logger.info("数据库升级到版本1.1完成")
except Exception as e:
logger.error(f"数据库版本检查或升级失败: {e}")
raise
def update_admin_user_id(self, cursor):
"""更新admin用户ID"""
try:
logger.info("开始更新admin用户ID...")
# 创建默认admin用户只在首次初始化时创建
cursor.execute('SELECT COUNT(*) FROM users WHERE username = ?', ('admin',))
admin_exists = cursor.fetchone()[0] > 0
@ -438,11 +477,55 @@ class DBManager:
self._migrate_keywords_table_constraints(cursor)
self.conn.commit()
logger.info(f"数据库初始化成功: {self.db_path}")
logger.info(f"admin用户ID更新完成")
except Exception as e:
logger.error(f"数据库初始化失败: {e}")
if self.conn:
self.conn.close()
logger.error(f"更新admin用户ID失败: {e}")
raise
def upgrade_notification_channels_table(self, cursor):
"""升级notification_channels表的type字段约束"""
try:
logger.info("开始升级notification_channels表...")
# 检查表是否存在
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='notification_channels'")
if not cursor.fetchone():
logger.info("notification_channels表不存在无需升级")
return True
# 检查表中是否有数据
cursor.execute("SELECT COUNT(*) FROM notification_channels")
count = cursor.fetchone()[0]
# 创建临时表
cursor.execute('''
CREATE TABLE notification_channels_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
user_id INTEGER NOT NULL,
type TEXT NOT NULL CHECK (type IN ('qq','ding_talk')),
config TEXT NOT NULL,
enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 复制数据
if count > 0:
logger.info(f"复制 {count} 条通知渠道数据到新表")
cursor.execute("INSERT INTO notification_channels_new SELECT * FROM notification_channels")
# 删除旧表
cursor.execute("DROP TABLE notification_channels")
# 重命名新表
cursor.execute("ALTER TABLE notification_channels_new RENAME TO notification_channels")
logger.info("notification_channels表升级完成")
return True
except Exception as e:
logger.error(f"升级notification_channels表失败: {e}")
raise
def _migrate_keywords_table_constraints(self, cursor):

View File

@ -1886,6 +1886,33 @@
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<span><i class="bi bi-plus-circle me-2"></i>添加钉钉通知渠道</span>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
<strong>使用说明:</strong>请设置钉钉机器人Webhook URL
</div>
<form id="addDingTalkChannelForm" class="row g-3">
<div class="col-md-4">
<label for="dingTalkChannelName" class="form-label">渠道名称</label>
<input type="text" class="form-control" id="dingTalkChannelName" placeholder="例如:我的钉钉通知" required>
</div>
<div class="col-md-4">
<label for="dingTalkWebHook" class="form-label">钉钉机器人Webhook URL</label>
<input type="text" class="form-control" id="dingTalkWebHook" placeholder="输入钉钉机器人Webhook URL" required>
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i>添加渠道
</button>
</div>
</form>
</div>
</div>
<!-- 通知渠道列表 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
@ -2298,8 +2325,8 @@
<input type="text" class="form-control" id="editChannelName" placeholder="例如我的QQ通知" required>
</div>
<div class="mb-3">
<label for="editChannelQQ" class="form-label">接收QQ号码 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="editChannelQQ" placeholder="输入QQ号码" required>
<label for="editChannelQQ" class="form-label">接收QQ号码、钉钉Webhook URL <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="editChannelQQ" placeholder="输入通知方式" required>
</div>
<div class="mb-3">
<div class="form-check">
@ -4882,7 +4909,7 @@
<td colspan="6" class="text-center py-4 text-muted">
<i class="bi bi-bell fs-1 d-block mb-3"></i>
<h5>暂无通知渠道</h5>
<p class="mb-0">请添加QQ通知渠道</p>
<p class="mb-0">请添加通知渠道</p>
</td>
</tr>
`;
@ -4899,7 +4926,7 @@
tr.innerHTML = `
<td><strong class="text-primary">${channel.id}</strong></td>
<td>${channel.name}</td>
<td><span class="badge bg-info">QQ</span></td>
<td><span class="badge bg-info">${channel.type}</span></td>
<td><code>${channel.config}</code></td>
<td>${statusBadge}</td>
<td>
@ -4956,6 +4983,43 @@
}
});
}
const addDingTalkChannelForm = document.getElementById('addDingTalkChannelForm');
if (addDingTalkChannelForm) {
addDingTalkChannelForm.addEventListener('submit', async function(e) {
e.preventDefault();
const name = document.getElementById('dingTalkChannelName').value;
const dingTalkWebHook = document.getElementById('dingTalkWebHook').value;
try {
const response = await fetch(`${apiBase}/notification-channels`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
type: 'ding_talk',
config: dingTalkWebHook
})
});
if (response.ok) {
showToast('通知渠道添加成功', 'success');
addDingTalkChannelForm.reset();
loadNotificationChannels();
} else {
const error = await response.text();
showToast(`添加失败: ${error}`, 'danger');
}
} catch (error) {
console.error('添加通知渠道失败:', error);
showToast('添加通知渠道失败', 'danger');
}
});
}
});
// 删除通知渠道