Skip to content

Commit ec8e25f

Browse files
committed
feat: 添加钉钉机器人加签验证支持与数据库字段迁移
1. 新增数据库迁移逻辑,为notification_settings表添加dingtalk_secret字段 2. 完善钉钉通知发送逻辑,支持加签安全验证 3. 调整UI布局,新增钉钉加签密钥输入框 4. 更新通知设置的读取、存储逻辑适配新字段
1 parent 692cb18 commit ec8e25f

3 files changed

Lines changed: 74 additions & 15 deletions

File tree

models/database.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ def _init_database(self):
130130
)
131131
''')
132132

133+
columns = [info[1] for info in cursor.execute("PRAGMA table_info(notification_settings)").fetchall()]
134+
if 'dingtalk_secret' not in columns:
135+
logger.info("数据库迁移:添加dingtalk_secret字段")
136+
cursor.execute('ALTER TABLE notification_settings ADD COLUMN dingtalk_secret TEXT')
137+
133138
cursor.execute('SELECT COUNT(*) FROM notification_settings')
134139
if cursor.fetchone()[0] == 0:
135140
cursor.execute(

services/notification.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def get_notification_settings(self) -> Dict[str, Any]:
3232
'popup_enabled': bool(result[1]),
3333
'dingtalk_enabled': bool(result[2]),
3434
'dingtalk_webhook': result[3] or '',
35+
'dingtalk_secret': result[13] if len(result) > 13 else '',
3536
'email_enabled': bool(result[4]),
3637
'email_smtp_server': result[5] or '',
3738
'email_sender': result[6] or '',
@@ -49,6 +50,7 @@ def get_notification_settings(self) -> Dict[str, Any]:
4950
'popup_enabled': True,
5051
'dingtalk_enabled': False,
5152
'dingtalk_webhook': '',
53+
'dingtalk_secret': '',
5254
'email_enabled': False,
5355
'email_smtp_server': '',
5456
'email_sender': '',
@@ -88,20 +90,29 @@ def send_popup_notification(self, title: str, message: str):
8890
logger.error(f"发送弹窗通知失败: {e}")
8991

9092
def send_dingtalk_notification(self, title: str, message: str):
91-
"""发送钉钉通知"""
93+
"""发送钉钉通知(支持加签验证)"""
9294
settings = self.get_notification_settings()
9395
if not settings['dingtalk_enabled']:
9496
return
9597

96-
if not settings['dingtalk_webhook']:
98+
webhook = settings['dingtalk_webhook']
99+
secret = settings['dingtalk_secret']
100+
101+
if not webhook:
97102
logger.warning("钉钉Webhook未配置")
98103
return
99104

100105
if not self.can_notify('dingtalk'):
101106
return
102107

103108
try:
109+
import base64
110+
import hashlib
111+
import hmac
112+
import urllib.parse
104113
from datetime import datetime
114+
from urllib.parse import parse_qs, urlparse
115+
105116
data = {
106117
"msgtype": "markdown",
107118
"markdown": {
@@ -110,18 +121,51 @@ def send_dingtalk_notification(self, title: str, message: str):
110121
}
111122
}
112123

124+
final_url = webhook
125+
126+
if secret:
127+
timestamp = str(round(time.time() * 1000))
128+
string_to_sign = f'{timestamp}\n{secret}'.encode('utf-8')
129+
hmac_code = hmac.new(
130+
secret.encode('utf-8'),
131+
string_to_sign,
132+
digestmod=hashlib.sha256
133+
).digest()
134+
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
135+
136+
parsed_url = urlparse(webhook)
137+
query_params = parse_qs(parsed_url.query)
138+
access_token = query_params.get('access_token', [''])[0]
139+
140+
new_query = f"access_token={access_token}&timestamp={timestamp}&sign={sign}"
141+
final_url = f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}?{new_query}"
142+
logger.info(f"使用加签模式发送钉钉通知")
143+
113144
response = requests.post(
114-
settings['dingtalk_webhook'],
145+
final_url,
115146
json=data,
147+
headers={'Content-Type': 'application/json'},
116148
timeout=10
117149
)
118150

119151
if response.status_code == 200:
120-
logger.info(f"钉钉通知发送成功: {title}")
152+
resp_json = response.json()
153+
if resp_json.get('errcode') == 0:
154+
logger.info(f"✅ 钉钉通知发送成功: {title}")
155+
else:
156+
errcode = resp_json.get('errcode', 'unknown')
157+
errmsg = resp_json.get('errmsg', '未知错误')
158+
logger.error(f"❌ 钉钉API错误 [{errcode}]: {errmsg}")
159+
if errcode == 310000:
160+
logger.error("⚠️ 签名验证失败!请检查:")
161+
logger.error(" 1. 加签密钥是否正确复制(无多余空格)")
162+
logger.error(" 2. 密钥是否与钉钉机器人设置页面一致")
163+
logger.error(" 3. 系统时间是否准确")
121164
else:
122-
logger.error(f"钉钉通知发送失败: {response.text}")
165+
logger.error(f"❌ 钉钉请求失败: HTTP {response.status_code}, 响应: {response.text}")
166+
123167
except Exception as e:
124-
logger.error(f"发送钉钉通知失败: {e}")
168+
logger.error(f"❌ 发送钉钉通知异常: {e}")
125169

126170
def check_and_notify(self, fund_data: Dict,
127171
total_profit: float, daily_profit: float):

ui/main_window.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,23 @@
1919
import matplotlib.style as style
2020
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
2121
from matplotlib.figure import Figure
22-
23-
from PySide6.QtCore import QThread, QTimer, Signal, Qt
22+
from PySide6.QtCore import Qt, QThread, QTimer, Signal
2423
from PySide6.QtGui import QColor, QFont, QIcon, QPixmap
2524
from PySide6.QtWidgets import (QApplication, QCheckBox, QDialog,
2625
QDoubleSpinBox, QGridLayout, QGroupBox,
2726
QHBoxLayout, QInputDialog, QLabel, QLineEdit,
2827
QMainWindow, QMenu, QMessageBox, QPushButton,
29-
QStyle, QSystemTrayIcon, QTabWidget,
30-
QTableWidgetItem, QVBoxLayout, QWidget)
28+
QStyle, QSystemTrayIcon, QTableWidgetItem,
29+
QTabWidget, QVBoxLayout, QWidget)
3130

3231
from config import config
3332
from models.database import DatabaseManager
3433
from services.data_fetcher import FundDataFetcher
3534
from services.notification import NotificationManager
36-
from ui.widgets.table_widget import FundTableWidget
37-
from ui.widgets.search_widget import FundSearchWidget
38-
from ui.widgets.portfolio_dashboard import PortfolioDashboard
3935
from ui.widgets.investment_calculator_dialog import InvestmentCalculatorDialog
36+
from ui.widgets.portfolio_dashboard import PortfolioDashboard
37+
from ui.widgets.search_widget import FundSearchWidget
38+
from ui.widgets.table_widget import FundTableWidget
4039
from utils.logger import logger
4140

4241
matplotlib.use('Agg')
@@ -1737,8 +1736,8 @@ def load_history_data(self):
17371736

17381737
def generate_mock_data(self):
17391738
"""生成模拟历史数据"""
1740-
from datetime import timedelta
17411739
import random
1740+
from datetime import timedelta
17421741

17431742
dates = []
17441743
values = []
@@ -1812,7 +1811,7 @@ class NotificationSettingsDialog(QDialog):
18121811
def __init__(self, parent=None):
18131812
super().__init__(parent)
18141813
self.setWindowTitle("通知设置")
1815-
self.setFixedSize(600, 500)
1814+
self.setFixedSize(600, 560)
18161815
self.setStyleSheet("""
18171816
QDialog {
18181817
background: white;
@@ -1860,6 +1859,14 @@ def init_ui(self):
18601859
webhook_layout.addWidget(self.dingtalk_webhook)
18611860
dingtalk_layout.addLayout(webhook_layout)
18621861

1862+
secret_layout = QHBoxLayout()
1863+
secret_layout.addWidget(QLabel("加签密钥:"))
1864+
self.dingtalk_secret = QLineEdit()
1865+
self.dingtalk_secret.setPlaceholderText("SEC开头的密钥(选填,机器人安全设置中的加签密钥)")
1866+
self.dingtalk_secret.setEchoMode(QLineEdit.Password)
1867+
secret_layout.addWidget(self.dingtalk_secret)
1868+
dingtalk_layout.addLayout(secret_layout)
1869+
18631870
dingtalk_group.setLayout(dingtalk_layout)
18641871
layout.addWidget(dingtalk_group)
18651872

@@ -1946,6 +1953,7 @@ def load_settings(self):
19461953
self.popup_enabled.setChecked(settings['popup_enabled'])
19471954
self.dingtalk_enabled.setChecked(settings['dingtalk_enabled'])
19481955
self.dingtalk_webhook.setText(settings['dingtalk_webhook'])
1956+
self.dingtalk_secret.setText(settings['dingtalk_secret'])
19491957
self.rise_threshold.setValue(settings['rise_threshold'])
19501958
self.fall_threshold.setValue(settings['fall_threshold'])
19511959
self.profit_threshold.setValue(settings['profit_threshold'])
@@ -1962,6 +1970,7 @@ def save_settings(self):
19621970
SET popup_enabled = ?,
19631971
dingtalk_enabled = ?,
19641972
dingtalk_webhook = ?,
1973+
dingtalk_secret = ?,
19651974
rise_threshold = ?,
19661975
fall_threshold = ?,
19671976
profit_threshold = ?,
@@ -1972,6 +1981,7 @@ def save_settings(self):
19721981
self.popup_enabled.isChecked(),
19731982
self.dingtalk_enabled.isChecked(),
19741983
self.dingtalk_webhook.text(),
1984+
self.dingtalk_secret.text(),
19751985
self.rise_threshold.value(),
19761986
self.fall_threshold.value(),
19771987
self.profit_threshold.value(),

0 commit comments

Comments
 (0)