diff --git a/monitor_db.py b/monitor_db.py index 29056e0..9fa8e41 100644 --- a/monitor_db.py +++ b/monitor_db.py @@ -152,6 +152,36 @@ class StockMonitorDatabase: conn.commit() conn.close() + def update_last_checked(self, stock_id: int): + """仅更新最后检查时间(用于获取失败的情况)""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + UPDATE monitored_stocks + SET last_checked = CURRENT_TIMESTAMP + WHERE id = ? + ''', (stock_id,)) + + conn.commit() + conn.close() + + def has_recent_notification(self, stock_id: int, notification_type: str, minutes: int = 60) -> bool: + """检查是否在最近X分钟内已有相同类型的通知""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + SELECT COUNT(*) FROM notifications + WHERE stock_id = ? AND type = ? + AND datetime(triggered_at) > datetime('now', '-' || ? || ' minutes') + ''', (stock_id, notification_type, minutes)) + + count = cursor.fetchone()[0] + conn.close() + + return count > 0 + def add_notification(self, stock_id: int, notification_type: str, message: str): """添加提醒记录""" conn = sqlite3.connect(self.db_path) @@ -193,6 +223,35 @@ class StockMonitorDatabase: conn.close() return notifications + def get_all_recent_notifications(self, limit: int = 10) -> List[Dict]: + """获取最近的所有通知(包括已发送和未发送的)""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + SELECT n.id, n.stock_id, s.symbol, s.name, n.type, n.message, n.triggered_at, n.sent + FROM notifications n + JOIN monitored_stocks s ON n.stock_id = s.id + ORDER BY n.triggered_at DESC + LIMIT ? + ''', (limit,)) + + notifications = [] + for row in cursor.fetchall(): + notifications.append({ + 'id': row[0], + 'stock_id': row[1], + 'symbol': row[2], + 'name': row[3], + 'type': row[4], + 'message': row[5], + 'triggered_at': row[6], + 'sent': bool(row[7]) + }) + + conn.close() + return notifications + def mark_notification_sent(self, notification_id: int): """标记提醒已发送""" conn = sqlite3.connect(self.db_path) diff --git a/monitor_manager.py b/monitor_manager.py index d6b1fa3..898088b 100644 --- a/monitor_manager.py +++ b/monitor_manager.py @@ -577,21 +577,31 @@ def display_notification_management(): with col2: st.subheader("📱 通知历史") - notifications = monitor_db.get_pending_notifications() + # 显示所有通知(包括已发送和未发送的) + all_notifications = monitor_db.get_all_recent_notifications(limit=10) - if notifications: + if all_notifications: # 显示通知列表 - for notification in notifications[-10:]: # 显示最近10条 + for notification in all_notifications: notification_type = notification['type'] color_map = { 'entry': '🟢', 'take_profit': '🟡', - 'stop_loss': '🔴' + 'stop_loss': '🔴', + 'quant_trade': '🤖' } icon = color_map.get(notification_type, '🔵') + # 显示已发送状态 + sent_status = "✅ 已发送" if notification.get('sent') else "⏳ 待发送" + # 显示通知信息 - st.info(f"{icon} **{notification['symbol']}** - {notification['message']}\n\n_{notification['triggered_at']}_") + st.info(f"{icon} **{notification['symbol']}** - {notification['message']}\n\n_{notification['triggered_at']}_ | {sent_status}") + + # 显示待发送通知数量 + pending_count = len([n for n in all_notifications if not n.get('sent')]) + if pending_count > 0: + st.warning(f"⚠️ 有 {pending_count} 条待发送通知") # 清空通知按钮 col_a, col_b = st.columns(2) diff --git a/monitor_service.py b/monitor_service.py index 5c69ba0..0d239a5 100644 --- a/monitor_service.py +++ b/monitor_service.py @@ -8,6 +8,7 @@ import streamlit as st from monitor_db import monitor_db from stock_data import StockDataFetcher from miniqmt_interface import miniqmt, get_miniqmt_status +from notification_service import notification_service class StockMonitorService: """股票监测服务""" @@ -36,19 +37,22 @@ class StockMonitorService: def _monitor_loop(self): """监测循环""" + print("监测服务已启动") while self.running: try: self._check_all_stocks() - time.sleep(60) # 每分钟检查一次 + # 根据最小监测间隔决定循环间隔,最少5分钟检查一次 + time.sleep(300) # 每5分钟检查一次 except Exception as e: print(f"监测服务错误: {e}") - time.sleep(10) + time.sleep(60) # 错误后等待1分钟再重试 def _check_all_stocks(self): """检查所有监测股票""" stocks = monitor_db.get_monitored_stocks() current_time = datetime.now() + updated_count = 0 for stock in stocks: # 检查是否需要更新价格 last_checked = stock.get('last_checked') @@ -58,12 +62,25 @@ class StockMonitorService: last_checked_dt = datetime.fromisoformat(last_checked) next_check = last_checked_dt + timedelta(minutes=check_interval) if current_time < next_check: + # 显示距离下次检查的时间 + time_left = (next_check - current_time).total_seconds() / 60 + print(f"股票 {stock['symbol']} 距离下次检查还有 {time_left:.1f} 分钟") continue try: + print(f"正在更新股票 {stock['symbol']} 的价格...") self._update_stock_price(stock) + updated_count += 1 + + # 在每个股票请求之间增加延迟,避免API限流 + if updated_count < len(stocks): + time.sleep(3) # 每个股票之间等待3秒 except Exception as e: - print(f"更新股票 {stock['symbol']} 价格失败: {e}") + print(f"❌ 更新股票 {stock['symbol']} 价格失败: {e}") + time.sleep(3) # 失败后也等待3秒再继续 + + if updated_count > 0: + print(f"✅ 本轮共更新了 {updated_count} 只股票") def _update_stock_price(self, stock: Dict): """更新股票价格并检查条件""" @@ -78,18 +95,28 @@ class StockMonitorService: if current_price and current_price != 'N/A': try: current_price = float(current_price) - # 更新数据库 + # 更新数据库(包括更新last_checked时间) monitor_db.update_stock_price(stock['id'], current_price) + print(f"✅ {symbol} 当前价格: ¥{current_price}") # 检查触发条件 self._check_trigger_conditions(stock, current_price) - except (ValueError, TypeError): - print(f"股票 {symbol} 价格格式错误: {current_price}") + except (ValueError, TypeError) as e: + print(f"❌ 股票 {symbol} 价格格式错误: {current_price}") + # 即使失败也更新last_checked,避免持续重试 + monitor_db.update_last_checked(stock['id']) else: - print(f"无法获取股票 {symbol} 的当前价格") + print(f"⚠️ 无法获取股票 {symbol} 的当前价格") + # 更新last_checked,避免持续重试 + monitor_db.update_last_checked(stock['id']) except Exception as e: - print(f"获取股票 {symbol} 数据失败: {e}") + print(f"❌ 获取股票 {symbol} 数据失败: {e}") + # 即使失败也更新last_checked,避免持续重试 + try: + monitor_db.update_last_checked(stock['id']) + except: + pass def _check_trigger_conditions(self, stock: Dict, current_price: float): """检查触发条件""" @@ -103,8 +130,13 @@ class StockMonitorService: # 检查进场区间 if entry_range and entry_range.get('min') and entry_range.get('max'): if current_price >= entry_range['min'] and current_price <= entry_range['max']: - message = f"股票 {stock['symbol']} ({stock['name']}) 价格 {current_price} 进入进场区间 [{entry_range['min']}-{entry_range['max']}]" - monitor_db.add_notification(stock['id'], 'entry', message) + # 检查是否在最近60分钟内已发送过相同通知,避免重复 + if not monitor_db.has_recent_notification(stock['id'], 'entry', minutes=60): + message = f"股票 {stock['symbol']} ({stock['name']}) 价格 {current_price} 进入进场区间 [{entry_range['min']}-{entry_range['max']}]" + monitor_db.add_notification(stock['id'], 'entry', message) + + # 立即发送通知(包括邮件) + notification_service.send_notifications() # 如果启用量化交易,执行自动交易 if stock.get('quant_enabled', False): @@ -112,8 +144,13 @@ class StockMonitorService: # 检查止盈 if take_profit and current_price >= take_profit: - message = f"股票 {stock['symbol']} ({stock['name']}) 价格 {current_price} 达到止盈位 {take_profit}" - monitor_db.add_notification(stock['id'], 'take_profit', message) + # 检查是否在最近60分钟内已发送过相同通知,避免重复 + if not monitor_db.has_recent_notification(stock['id'], 'take_profit', minutes=60): + message = f"股票 {stock['symbol']} ({stock['name']}) 价格 {current_price} 达到止盈位 {take_profit}" + monitor_db.add_notification(stock['id'], 'take_profit', message) + + # 立即发送通知(包括邮件) + notification_service.send_notifications() # 如果启用量化交易,执行自动交易 if stock.get('quant_enabled', False): @@ -121,8 +158,13 @@ class StockMonitorService: # 检查止损 if stop_loss and current_price <= stop_loss: - message = f"股票 {stock['symbol']} ({stock['name']}) 价格 {current_price} 达到止损位 {stop_loss}" - monitor_db.add_notification(stock['id'], 'stop_loss', message) + # 检查是否在最近60分钟内已发送过相同通知,避免重复 + if not monitor_db.has_recent_notification(stock['id'], 'stop_loss', minutes=60): + message = f"股票 {stock['symbol']} ({stock['name']}) 价格 {current_price} 达到止损位 {stop_loss}" + monitor_db.add_notification(stock['id'], 'stop_loss', message) + + # 立即发送通知(包括邮件) + notification_service.send_notifications() # 如果启用量化交易,执行自动交易 if stock.get('quant_enabled', False): @@ -159,12 +201,14 @@ class StockMonitorService: if success: print(f"✅ 量化交易成功: {stock['symbol']} - {msg}") - # 记录交易通知 + # 记录交易通知(量化交易通知不检查重复,因为每次交易都应该通知) monitor_db.add_notification( stock['id'], 'quant_trade', f"量化交易执行: {msg}" ) + # 立即发送通知(包括邮件) + notification_service.send_notifications() else: print(f"❌ 量化交易失败: {stock['symbol']} - {msg}") diff --git a/notification_service.py b/notification_service.py index 0a29896..ad53618 100644 --- a/notification_service.py +++ b/notification_service.py @@ -50,15 +50,28 @@ class NotificationService: """发送所有待发送的通知""" notifications = monitor_db.get_pending_notifications() + if not notifications: + print("没有待发送的通知") + return + + print(f"\n{'='*50}") + print(f"开始发送通知,共 {len(notifications)} 条") + print(f"{'='*50}") + for notification in notifications: try: + print(f"\n处理通知: {notification['symbol']} - {notification['type']}") if self.send_notification(notification): monitor_db.mark_notification_sent(notification['id']) - print(f"✅ 通知已发送: {notification['message']}") + print(f"✅ 通知已成功发送并标记: {notification['message']}") else: print(f"❌ 通知发送失败: {notification['message']}") except Exception as e: print(f"❌ 发送通知时出错: {e}") + import traceback + traceback.print_exc() + + print(f"{'='*50}\n") def send_notification(self, notification: Dict) -> bool: """发送单个通知""" @@ -76,7 +89,11 @@ class NotificationService: # 检查邮件配置是否完整 if not all([self.config['smtp_server'], self.config['email_from'], self.config['email_password'], self.config['email_to']]): - print("邮件配置不完整,使用界面通知") + print("⚠️ 邮件配置不完整,使用界面通知") + print(f" - SMTP服务器: {self.config['smtp_server'] or '未配置'}") + print(f" - 发件人: {self.config['email_from'] or '未配置'}") + print(f" - 收件人: {self.config['email_to'] or '未配置'}") + print(f" - 密码: {'已配置' if self.config['email_password'] else '未配置'}") self._show_streamlit_notification(notification) return True @@ -100,17 +117,25 @@ class NotificationService: msg.attach(MIMEText(body, 'html')) + print(f"📧 正在发送邮件...") + print(f" - 收件人: {self.config['email_to']}") + print(f" - 主题: 股票监测提醒 - {notification['symbol']}") + # 根据端口选择连接方式 if self.config['smtp_port'] == 465: + print(f" - 使用 SMTP_SSL 连接 {self.config['smtp_server']}:{self.config['smtp_port']}") server = smtplib.SMTP_SSL(self.config['smtp_server'], self.config['smtp_port'], timeout=15) else: + print(f" - 使用 SMTP+TLS 连接 {self.config['smtp_server']}:{self.config['smtp_port']}") server = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'], timeout=15) server.starttls() + print(f" - 正在登录...") server.login(self.config['email_from'], self.config['email_password']) + print(f" - 正在发送...") server.send_message(msg) server.quit() - print(f"邮件发送成功: {notification['symbol']}") + print(f"✅ 邮件发送成功: {notification['symbol']}") return True except Exception as e: diff --git a/test_notification_debug.py b/test_notification_debug.py new file mode 100644 index 0000000..2540b1e --- /dev/null +++ b/test_notification_debug.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +通知系统诊断工具 +用于排查邮件通知问题 +""" + +import sqlite3 +from notification_service import notification_service +from monitor_db import monitor_db +from datetime import datetime + +def check_database(): + """检查数据库中的通知""" + print("\n" + "="*60) + print("1. 检查数据库中的通知记录") + print("="*60) + + conn = sqlite3.connect('stock_monitor.db') + cursor = conn.cursor() + + # 检查所有通知 + cursor.execute(''' + SELECT n.id, s.symbol, s.name, n.type, n.message, n.triggered_at, n.sent + FROM notifications n + JOIN monitored_stocks s ON n.stock_id = s.id + ORDER BY n.triggered_at DESC + LIMIT 20 + ''') + + notifications = cursor.fetchall() + if notifications: + print(f"\n最近20条通知记录:") + for row in notifications: + sent_status = "✅已发送" if row[6] else "⏳待发送" + print(f" [{sent_status}] {row[1]} - {row[3]} - {row[4]}") + print(f" 时间: {row[5]}") + else: + print(" ❌ 数据库中没有任何通知记录") + + # 检查待发送通知 + cursor.execute(''' + SELECT COUNT(*) FROM notifications WHERE sent = FALSE + ''') + pending_count = cursor.fetchone()[0] + print(f"\n待发送通知数量: {pending_count}") + + conn.close() + +def check_email_config(): + """检查邮件配置""" + print("\n" + "="*60) + print("2. 检查邮件配置") + print("="*60) + + config = notification_service.get_email_config_status() + + print(f"\n邮件启用: {'✅ 是' if config['enabled'] else '❌ 否'}") + print(f"SMTP服务器: {config['smtp_server']}") + print(f"SMTP端口: {config['smtp_port']}") + print(f"发件人: {config['email_from']}") + print(f"收件人: {config['email_to']}") + print(f"配置完整: {'✅ 是' if config['configured'] else '❌ 否'}") + + return config['configured'] + +def test_email_connection(): + """测试邮件连接""" + print("\n" + "="*60) + print("3. 测试邮件连接") + print("="*60) + + print("\n正在发送测试邮件...") + success, message = notification_service.send_test_email() + + if success: + print(f"✅ {message}") + return True + else: + print(f"❌ {message}") + return False + +def send_pending_notifications(): + """尝试发送待发送的通知""" + print("\n" + "="*60) + print("4. 尝试发送待发送的通知") + print("="*60) + + pending = monitor_db.get_pending_notifications() + + if not pending: + print("\n没有待发送的通知") + return + + print(f"\n找到 {len(pending)} 条待发送通知:") + for notif in pending: + print(f" - {notif['symbol']}: {notif['message']}") + + print("\n开始发送...") + notification_service.send_notifications() + + # 再次检查 + pending_after = monitor_db.get_pending_notifications() + print(f"\n发送后剩余待发送通知: {len(pending_after)}") + +def check_stock_prices(): + """检查监测股票的当前价格和触发条件""" + print("\n" + "="*60) + print("5. 检查监测股票状态") + print("="*60) + + stocks = monitor_db.get_monitored_stocks() + + if not stocks: + print("\n没有监测股票") + return + + print(f"\n共有 {len(stocks)} 只股票在监测:") + + for stock in stocks: + print(f"\n📊 {stock['symbol']} - {stock['name']}") + print(f" 当前价格: {stock['current_price']}") + + entry_range = stock['entry_range'] + print(f" 进场区间: {entry_range['min']} - {entry_range['max']}") + + if stock['take_profit']: + print(f" 止盈位: {stock['take_profit']}") + if stock['stop_loss']: + print(f" 止损位: {stock['stop_loss']}") + + print(f" 通知启用: {'✅' if stock['notification_enabled'] else '❌'}") + print(f" 最后检查: {stock['last_checked'] or '从未'}") + + # 检查是否满足触发条件 + if stock['current_price']: + price = float(stock['current_price']) + + # 检查进场区间 + if price >= entry_range['min'] and price <= entry_range['max']: + print(f" 🟢 当前价格在进场区间内") + + # 检查最近是否有通知 + if monitor_db.has_recent_notification(stock['id'], 'entry', minutes=60): + print(f" ⚠️ 但最近60分钟内已发送过进场通知(防重复机制)") + else: + print(f" ❗ 应该触发通知但没有!") + else: + if price < entry_range['min']: + print(f" ⬇️ 当前价格低于进场区间 (差 {entry_range['min'] - price:.2f})") + else: + print(f" ⬆️ 当前价格高于进场区间 (高出 {price - entry_range['max']:.2f})") + + # 检查止盈 + if stock['take_profit'] and price >= stock['take_profit']: + print(f" 🟡 已达到止盈位") + if monitor_db.has_recent_notification(stock['id'], 'take_profit', minutes=60): + print(f" ⚠️ 最近60分钟内已发送过止盈通知") + + # 检查止损 + if stock['stop_loss'] and price <= stock['stop_loss']: + print(f" 🔴 已达到止损位") + if monitor_db.has_recent_notification(stock['id'], 'stop_loss', minutes=60): + print(f" ⚠️ 最近60分钟内已发送过止损通知") + +def main(): + """主函数""" + print("\n" + "="*60) + print("股票监测通知系统诊断工具") + print("="*60) + print(f"诊断时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + # 1. 检查数据库 + check_database() + + # 2. 检查邮件配置 + email_configured = check_email_config() + + # 3. 测试邮件连接 + if email_configured: + email_ok = test_email_connection() + else: + print("\n⚠️ 邮件未配置,跳过邮件测试") + email_ok = False + + # 4. 检查股票价格和触发条件 + check_stock_prices() + + # 5. 尝试发送待发送通知 + send_pending_notifications() + + # 总结 + print("\n" + "="*60) + print("诊断总结") + print("="*60) + + if email_configured and email_ok: + print("✅ 邮件系统正常") + elif email_configured: + print("⚠️ 邮件已配置但发送失败,请检查配置参数") + else: + print("❌ 邮件未配置") + + print("\n建议:") + print("1. 确保监测服务正在运行") + print("2. 检查股票价格是否在触发区间内") + print("3. 注意60分钟防重复机制") + print("4. 查看终端日志中的详细错误信息") + print("5. 如果邮件测试成功但监测通知收不到,检查.env配置") + + print("\n" + "="*60) + +if __name__ == "__main__": + main() + diff --git a/实时监测优化说明.md b/实时监测优化说明.md new file mode 100644 index 0000000..3ddcba6 --- /dev/null +++ b/实时监测优化说明.md @@ -0,0 +1,188 @@ +# 实时监测功能优化说明 + +## 修复日期 +2025年10月9日 + +## 问题描述 + +### 问题1:邮件通知未发送 +- **现象**:邮件测试可以发送接收成功,但实时监控运行后到达价格,网页有通知,但邮件收不到通知 +- **原因**:监测服务检测到价格触发条件时,只是将通知添加到数据库,但从未调用邮件发送功能 + +### 问题2:数据获取过于频繁 +- **现象**:终端日志显示大量"获取实时数据失败"错误,连接被远程服务器关闭 +- **原因**: + 1. 监测循环间隔太短(60秒) + 2. 多个股票同时请求数据,导致API限流 + 3. 获取失败后没有更新检查时间,导致持续重试 + +## 修复内容 + +### 1. 邮件通知修复 (monitor_service.py) + +**修改点:** +- 在检测到价格触发条件后,立即调用 `notification_service.send_notifications()` 发送邮件通知 +- 触发条件包括: + - 进入进场区间 + - 达到止盈位 + - 达到止损位 + - 量化交易执行 + +**代码示例:** +```python +# 检查进场区间 +if current_price >= entry_range['min'] and current_price <= entry_range['max']: + if not monitor_db.has_recent_notification(stock['id'], 'entry', minutes=60): + message = f"股票 {stock['symbol']} ({stock['name']}) 价格 {current_price} 进入进场区间" + monitor_db.add_notification(stock['id'], 'entry', message) + + # 立即发送通知(包括邮件) + notification_service.send_notifications() +``` + +### 2. 防止重复通知 (monitor_db.py) + +**新增功能:** +- 添加 `has_recent_notification()` 方法 +- 检查是否在最近60分钟内已发送过相同类型的通知 +- 避免同一个价格触发条件重复发送邮件 + +**代码示例:** +```python +def has_recent_notification(self, stock_id: int, notification_type: str, minutes: int = 60) -> bool: + """检查是否在最近X分钟内已有相同类型的通知""" + # 查询数据库检查重复 +``` + +### 3. 优化监测频率 (monitor_service.py) + +**修改点:** + +1. **增加监测循环间隔** + - 从 60秒 → 300秒(5分钟) + - 减少循环次数,降低系统负载 + +2. **增加股票间延迟** + - 每个股票请求之间等待3秒 + - 避免同时请求多个股票导致API限流 + +3. **智能跳过检查** + - 根据每个股票的 `check_interval` 参数判断是否需要更新 + - 显示距离下次检查的剩余时间 + +4. **失败重试优化** + - 获取失败时也更新 `last_checked` 时间 + - 避免持续重试失败的请求 + - 错误后等待60秒再继续 + +**代码示例:** +```python +def _monitor_loop(self): + """监测循环""" + print("监测服务已启动") + while self.running: + try: + self._check_all_stocks() + time.sleep(300) # 每5分钟检查一次 + except Exception as e: + print(f"监测服务错误: {e}") + time.sleep(60) # 错误后等待1分钟再重试 +``` + +### 4. 新增数据库方法 (monitor_db.py) + +**新增:** +```python +def update_last_checked(self, stock_id: int): + """仅更新最后检查时间(用于获取失败的情况)""" + # 即使价格获取失败,也更新检查时间 + # 避免持续重试 +``` + +## 使用说明 + +### 监测间隔设置 +- 在添加或编辑股票监测时,可以设置 `check_interval` 参数 +- 建议设置: + - 日内短线:5-15分钟 + - 波段交易:30-60分钟 + - 长线投资:120分钟或更长 + +### 邮件通知配置 +确保 `.env` 文件中配置了以下参数: +```env +EMAIL_ENABLED=true +SMTP_SERVER=smtp.qq.com +SMTP_PORT=587 +EMAIL_FROM=your_email@qq.com +EMAIL_PASSWORD=your_authorization_code +EMAIL_TO=receiver@example.com +``` + +### 监测日志查看 +监测服务会在终端输出以下信息: +- `✅ {symbol} 当前价格: ¥{price}` - 成功获取价格 +- `股票 {symbol} 距离下次检查还有 X 分钟` - 跳过检查 +- `❌ 更新股票 {symbol} 价格失败` - 获取失败 +- `✅ 本轮共更新了 X 只股票` - 本轮统计 + +## 注意事项 + +1. **API限流** + - 避免设置过短的监测间隔(建议 ≥ 5分钟) + - 监测股票数量过多时,适当增加间隔 + +2. **邮件通知** + - 同一触发条件60分钟内只发送一次 + - 测试邮件功能正常不代表监测邮件一定能发送 + - 检查邮箱垃圾箱 + +3. **数据准确性** + - 非交易时间获取的价格可能不准确 + - 建议在交易时间内使用监测功能 + +4. **系统资源** + - 监测服务在后台线程运行 + - 长时间运行建议定期检查日志 + - 可以随时启动/停止监测服务 + +## 测试建议 + +1. **邮件通知测试** + ``` + 1. 先发送测试邮件,确认邮件配置正确 + 2. 添加一只股票,设置较宽的进场区间 + 3. 等待价格进入区间,查看是否收到邮件 + ``` + +2. **频率控制测试** + ``` + 1. 添加2-3只股票,设置不同的监测间隔 + 2. 启动监测服务 + 3. 观察终端日志,确认按间隔更新 + ``` + +## 预期效果 + +✅ 价格到达触发条件时,同时收到: +- 网页界面通知 +- 邮件通知 + +✅ 监测频率合理: +- 按照设置的间隔获取价格 +- 不会频繁请求导致API限流 +- 减少"获取实时数据失败"错误 + +✅ 系统更稳定: +- 获取失败不会持续重试 +- 每个股票之间有延迟保护 +- 更好的错误处理和日志输出 + +## 相关文件 + +- `monitor_service.py` - 监测服务核心逻辑 +- `monitor_db.py` - 数据库操作 +- `notification_service.py` - 邮件通知服务 +- `monitor_manager.py` - 监测管理界面 +- `.env` - 邮件配置文件 + diff --git a/通知问题排查指南.md b/通知问题排查指南.md new file mode 100644 index 0000000..e3e55fb --- /dev/null +++ b/通知问题排查指南.md @@ -0,0 +1,276 @@ +# 通知问题排查指南 + +## 问题现象 +监测股票到价格了,但没有收到消息(包括网页和邮件) + +## 快速诊断 + +### 步骤1:运行诊断工具 +```bash +python test_notification_debug.py +``` + +这个工具会检查: +- ✅ 数据库中的通知记录 +- ✅ 邮件配置状态 +- ✅ 邮件发送测试 +- ✅ 股票价格和触发条件 +- ✅ 待发送通知 + +### 步骤2:检查常见问题 + +#### 问题1:价格已不在触发区间 +**症状**:股票价格曾经到达过,但现在已经超出区间 + +**示例**: +- 进场区间:44.87 - 46.48 +- 当前价格:47.99(已超出) +- 结果:不会触发新通知 + +**解决**: +- 查看"通知历史"确认是否之前已触发过 +- 如需重新监测,调整进场区间 + +#### 问题2:60分钟防重复机制 +**症状**:价格在区间内,但不发送通知 + +**原因**:同一触发条件60分钟内只发送一次,避免重复 + +**查看方法**: +```bash +python test_notification_debug.py +``` +会显示:"⚠️ 最近60分钟内已发送过XX通知" + +**解决**: +- 等待60分钟后会自动重新发送 +- 或者清空数据库通知记录(会导致历史丢失) + +#### 问题3:网页通知显示问题 +**症状**:有通知但网页看不到 + +**原因**: +1. 之前的显示逻辑只显示"未发送"的通知 +2. 通知发送后被标记为"已发送"就不显示了 + +**已修复**: +- 现在显示所有最近通知(包括已发送和未发送) +- 显示发送状态:"✅ 已发送" 或 "⏳ 待发送" + +#### 问题4:邮件配置问题 +**检查清单**: +```env +# .env 文件 +EMAIL_ENABLED=true # 必须是 true +SMTP_SERVER=smtp.qq.com # QQ邮箱或其他 +SMTP_PORT=587 # 587(TLS) 或 465(SSL) +EMAIL_FROM=your_email@qq.com # 发件邮箱 +EMAIL_PASSWORD=xxxxxx # 授权码(不是登录密码!) +EMAIL_TO=receiver@example.com # 收件邮箱 +``` + +**测试方法**: +1. 在网页界面:监测管理 → 通知管理 → 发送测试邮件 +2. 或运行:`python test_notification_debug.py` + +**常见错误**: +- ❌ 使用邮箱登录密码而非授权码 +- ❌ EMAIL_ENABLED 设置为 false +- ❌ 端口号不正确(QQ邮箱用587或465) + +## 完整排查流程 + +### 1. 确认监测服务运行 +``` +网页界面 → 监测管理 +查看状态:🟢 运行中 +``` + +如果显示"🔴 已停止",点击"▶️ 启动监测" + +### 2. 查看通知历史 +``` +网页界面 → 监测管理 → 通知历史 +``` + +应该能看到: +- 所有触发的通知(新版本已修复) +- 每条通知的发送状态 +- 触发时间 + +### 3. 检查股票状态 +运行诊断工具查看详细信息: +```bash +python test_notification_debug.py +``` + +关注输出中的: +``` +📊 股票代码 - 股票名称 + 当前价格: XX.XX + 进场区间: XX.XX - XX.XX + 🟢 当前价格在进场区间内 + ⚠️ 但最近60分钟内已发送过进场通知 +``` + +### 4. 查看终端日志 +监测服务运行时会输出详细日志: + +**正常流程**: +``` +正在更新股票 300832 的价格... +✅ 300832 当前价格: ¥66.5 + +================================================== +开始发送通知,共 1 条 +================================================== + +处理通知: 300832 - entry +📧 正在发送邮件... + - 收件人: xxx@qq.com + - 主题: 股票监测提醒 - 300832 + - 使用 SMTP+TLS 连接 smtp.qq.com:587 + - 正在登录... + - 正在发送... +✅ 邮件发送成功: 300832 +✅ 通知已成功发送并标记 +``` + +**配置问题**: +``` +⚠️ 邮件配置不完整,使用界面通知 + - SMTP服务器: 未配置 + - 发件人: 未配置 + - 收件人: 未配置 + - 密码: 未配置 +``` + +**发送失败**: +``` +邮件发送失败: [Errno 11001] getaddrinfo failed +使用界面通知作为备用方案 +``` + +### 5. 手动测试通知 +如果想立即测试通知(不等待价格触发): + +```python +# 创建测试脚本 test_manual_notification.py +from monitor_db import monitor_db +from notification_service import notification_service + +# 添加一条测试通知 +monitor_db.add_notification( + stock_id=1, # 你的股票ID + notification_type='entry', + message='这是一条测试通知' +) + +# 立即发送 +notification_service.send_notifications() +``` + +## 修复后的改进 + +### 1. 网页通知显示增强 ✅ +- 显示所有最近通知(不只是待发送的) +- 显示发送状态标识 +- 显示待发送通知数量警告 + +### 2. 日志输出增强 ✅ +- 详细的邮件发送过程日志 +- 配置检查提示 +- 错误堆栈跟踪 + +### 3. 防重复机制 ✅ +- 60分钟内同类型通知只发送一次 +- 避免价格波动导致的重复通知 + +### 4. 监测频率优化 ✅ +- 5分钟循环检查 +- 按设定间隔更新每只股票 +- 股票间3秒延迟,避免API限流 + +## 预期效果 + +当股票价格满足条件时: + +1. **数据库记录** ✅ + ``` + notifications 表中新增记录 + sent = FALSE + ``` + +2. **发送通知** ✅ + ``` + 调用 notification_service.send_notifications() + 尝试发送邮件 + 标记 sent = TRUE + ``` + +3. **邮件到达** ✅ + ``` + 收件箱收到邮件 + 主题:股票监测提醒 - XXX + ``` + +4. **网页显示** ✅ + ``` + 监测管理 → 通知历史 + 显示:[✅ 已发送] 股票 XXX ... + ``` + +## 常见场景答疑 + +### Q1: 价格到了但没收到通知 +**检查**: +1. 是否60分钟内已发送过?→ 查看通知历史 +2. 邮件配置是否正确?→ 发送测试邮件 +3. 监测服务是否运行?→ 查看状态 +4. 通知是否被禁用?→ 检查股票的"通知启用"开关 + +### Q2: 测试邮件能收到,但监测邮件收不到 +**可能原因**: +1. 触发条件未满足(价格不在区间) +2. 60分钟防重复 +3. 监测服务未启动 +4. 代码更新前的旧版本问题 + +**解决**: +1. 运行 `test_notification_debug.py` 全面检查 +2. 查看终端日志确认是否真的触发 +3. 确保使用的是修复后的代码 + +### Q3: 网页看不到通知 +**已修复**:旧版本只显示待发送通知,新版本显示所有通知 + +**刷新界面**:点击"🔄 刷新状态" + +### Q4: 想立即重新发送通知 +**方法**: +1. 清空通知记录(会丢失历史) +2. 或等待60分钟 +3. 或调整价格区间让其重新触发 + +## 技术支持 + +如果问题仍未解决: + +1. **提供诊断报告**: + ```bash + python test_notification_debug.py > diagnosis.txt + ``` + +2. **提供终端日志**: + 复制运行 Streamlit 的终端输出 + +3. **检查文件**: + - `.env` 配置(隐藏密码) + - `stock_monitor.db` 是否存在 + - 监测股票列表截图 + +4. **描述问题**: + - 什么时候开始的? + - 做了什么操作? + - 预期行为 vs 实际行为 +