diff --git a/backend/api/routes/recommendations.py b/backend/api/routes/recommendations.py index 6b3be9b..327ccd0 100644 --- a/backend/api/routes/recommendations.py +++ b/backend/api/routes/recommendations.py @@ -118,10 +118,22 @@ async def get_active_recommendations(): """ 获取当前有效的推荐(未过期、未执行、未取消) 使用WebSocket实时价格更新推荐中的价格信息 + 同一交易对只返回最新的推荐(已去重) """ try: recommendations = TradeRecommendation.get_active() + # 确保时间格式正确(转换为ISO格式字符串,包含时区信息) + from datetime import datetime + for rec in recommendations: + if rec.get('recommendation_time'): + if isinstance(rec['recommendation_time'], datetime): + # 如果是datetime对象,转换为ISO格式字符串(UTC+8) + rec['recommendation_time'] = rec['recommendation_time'].isoformat() + elif isinstance(rec['recommendation_time'], str): + # 如果已经是字符串,确保格式正确 + pass + # 尝试从WebSocket缓存获取实时价格更新推荐中的价格 try: import sys diff --git a/backend/database/models.py b/backend/database/models.py index 3e8574b..99c502f 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -322,11 +322,19 @@ class TradeRecommendation: @staticmethod def get_active(): - """获取当前有效的推荐(未过期、未执行、未取消)""" + """获取当前有效的推荐(未过期、未执行、未取消) + 同一交易对只返回最新的推荐(去重) + """ return db.execute_query( - """SELECT * FROM trade_recommendations - WHERE status = 'active' AND (expires_at IS NULL OR expires_at > NOW()) - ORDER BY signal_strength DESC, recommendation_time DESC""" + """SELECT t1.* FROM trade_recommendations t1 + INNER JOIN ( + SELECT symbol, MAX(recommendation_time) as max_time + FROM trade_recommendations + WHERE status = 'active' AND (expires_at IS NULL OR expires_at > NOW()) + GROUP BY symbol + ) t2 ON t1.symbol = t2.symbol AND t1.recommendation_time = t2.max_time + WHERE t1.status = 'active' AND (t1.expires_at IS NULL OR t1.expires_at > NOW()) + ORDER BY t1.signal_strength DESC, t1.recommendation_time DESC""" ) @staticmethod diff --git a/frontend/src/components/Recommendations.jsx b/frontend/src/components/Recommendations.jsx index c6cbdfc..fc00ac6 100644 --- a/frontend/src/components/Recommendations.jsx +++ b/frontend/src/components/Recommendations.jsx @@ -14,10 +14,56 @@ function Recommendations() { useEffect(() => { loadRecommendations() - // 如果是查看有效推荐,每10秒自动刷新一次(获取实时价格) + // 如果是查看有效推荐,每10秒静默更新价格(不触发loading状态) let interval = null if (statusFilter === 'active') { - interval = setInterval(loadRecommendations, 10000) // 每10秒刷新 + interval = setInterval(async () => { + // 静默更新:只更新价格,不显示loading + try { + const result = await api.getActiveRecommendations() + const newData = result.data || [] + + // 使用setState直接更新,不触发loading状态 + setRecommendations(prevRecommendations => { + // 如果新数据为空,保持原数据不变 + if (newData.length === 0) { + return prevRecommendations + } + + // 创建一个映射,用于快速查找 + const newDataMap = new Map(newData.map(rec => [rec.id, rec])) + const prevMap = new Map(prevRecommendations.map(rec => [rec.id, rec])) + + // 合并数据:优先使用新数据(包含实时价格更新) + const updated = prevRecommendations.map(prevRec => { + const newRec = newDataMap.get(prevRec.id) + if (newRec) { + // 如果新数据中有该推荐,使用新数据(包含价格更新) + return newRec + } + // 如果新数据中没有,保留旧数据(可能已过期或状态改变) + return prevRec + }) + + // 添加新出现的推荐 + const newItems = newData.filter(newRec => !prevMap.has(newRec.id)) + + // 合并并去重(按id) + const merged = [...updated, ...newItems] + const uniqueMap = new Map() + merged.forEach(rec => { + if (!uniqueMap.has(rec.id)) { + uniqueMap.set(rec.id, rec) + } + }) + + return Array.from(uniqueMap.values()) + }) + } catch (err) { + // 静默失败,不显示错误 + console.debug('静默更新价格失败:', err) + } + }, 10000) // 每10秒刷新 } return () => { @@ -104,14 +150,24 @@ function Recommendations() { const formatTime = (timeStr) => { if (!timeStr) return '-' try { + // 处理时区问题:如果时间字符串包含时区信息,直接解析 + // 否则假设是UTC时间,转换为本地时间 const date = new Date(timeStr) + + // 检查日期是否有效 + if (isNaN(date.getTime())) { + return timeStr + } + + // 使用本地时区格式化(显示北京时间,如果服务器返回的是UTC时间) return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', - second: '2-digit' + second: '2-digit', + timeZone: 'Asia/Shanghai' // 明确使用北京时间 }) } catch (e) { return timeStr