""" 五维分析引擎 + 自动状态分类 维度: time(时效) / heat(热度) / content(内容) / propagation(传播) / audience(受众) 状态: emerging → rising → hot → cooling → expired """ import json, math from datetime import datetime, timedelta from appPublic.uniqueID import getID from sqlor.dbpools import DBPools def now_str(): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') DIM_NAMES = { 'time': '时效维度', 'heat': '热度指标', 'content': '内容属性', 'propagation': '传播路径', 'audience': '受众画像', } async def analyze_item(item): """分析单条热点,返回 (new_status, dimensions_dict, detail)""" now = datetime.now() try: first_seen = datetime.strptime(str(item.get('first_seen', '')), '%Y-%m-%d %H:%M:%S') except Exception: first_seen = now hours_alive = max(0.1, (now - first_seen).total_seconds() / 3600) heat = float(item.get('heat_score', 0)) engagement = int(item.get('engagement_count', 0)) comments = int(item.get('comment_count', 0)) shares = int(item.get('share_count', 0)) # ---- 五维评分 (0-100) ---- dims = {} # 时效: 越新越高 dims['time'] = 95 if hours_alive < 1 else 85 if hours_alive < 6 else \ 70 if hours_alive < 24 else 50 if hours_alive < 72 else \ 30 if hours_alive < 168 else 10 # 热度: 绝对值 dims['heat'] = 95 if heat > 1e4 else 85 if heat > 5e3 else \ 70 if heat > 1e3 else 55 if heat > 500 else \ 35 if heat > 100 else 15 # 内容: 标题长度 + 摘要丰富度 + 标签 title_len = len(item.get('title', '')) summary_len = len(item.get('summary', '')) dims['content'] = min(100, (20 if title_len > 15 else 10) + (30 if summary_len > 100 else 15) + (25 if item.get('tags') else 0) + (25 if item.get('category') else 0)) # 传播: 互动总量 total_eng = engagement + comments * 2 + shares * 3 dims['propagation'] = 95 if total_eng > 1e4 else 80 if total_eng > 5e3 else \ 60 if total_eng > 1e3 else 35 if total_eng > 100 else 10 # 受众: 互动率 rate = total_eng / heat if heat > 0 else 0 dims['audience'] = 90 if rate > 0.5 else 70 if rate > 0.2 else \ 45 if rate > 0.05 else 25 if total_eng > 0 else 5 # ---- 状态分类 ---- decay = math.exp(-hours_alive / 168) # 7天半衰期 adjusted_heat = heat * decay velocity = round(heat / hours_alive, 2) if hours_alive > 336: # >14天 new_status = 'expired' elif hours_alive > 168: # 7-14天 new_status = 'cooling' elif adjusted_heat > 5000: new_status = 'hot' elif adjusted_heat > 500: new_status = 'rising' if velocity > 50 else 'emerging' elif adjusted_heat > 100: new_status = 'rising' if velocity > 100 else 'emerging' else: new_status = 'emerging' detail = { 'hours_alive': round(hours_alive, 1), 'adjusted_heat': round(adjusted_heat, 2), 'heat_velocity': velocity, 'total_engagement': total_eng, 'decay_factor': round(decay, 4), } return new_status, dims, detail async def save_analysis(item_id, dims, detail, now=None): """保存/更新五维分析记录""" if now is None: now = now_str() db = DBPools() async with db.sqlorContext('sage') as sor: for dim, score in dims.items(): analysis_data = json.dumps({ 'dimension': dim, 'dimension_cn': DIM_NAMES.get(dim, dim), 'score': score, 'detail': detail, }, ensure_ascii=False) old = await sor.sqlExe( "select id from hotspot_analysis where item_id=${iid}$ and dimension=${dim}$", {'iid': item_id, 'dim': dim}) if old: await sor.U('hotspot_analysis', { 'id': old[0]['id'], 'score': score, 'analysis_data': analysis_data, 'analyzed_at': now, }) else: await sor.C('hotspot_analysis', { 'id': getID(), 'item_id': item_id, 'dimension': dim, 'score': score, 'analysis_data': analysis_data, 'analyzed_at': now, }) async def run_analysis(limit=500): """批量分析最近的热点 (默认500条)""" db = DBPools() now = now_str() results = {'analyzed': 0, 'status_changes': 0} async with db.sqlorContext('sage') as sor: items = await sor.sqlExe( "select * from hotspot_item order by first_seen desc limit ${n}$", {'n': limit}) for item in items: current_status = item.get('status', 'emerging') new_status, dims, detail = await analyze_item(item) await save_analysis(item['id'], dims, detail, now) if new_status != current_status: results['status_changes'] += 1 async with db.sqlorContext('sage') as sor: await sor.U('hotspot_item', { 'id': item['id'], 'heat_score': round(detail['adjusted_heat'], 2), 'heat_velocity': detail['heat_velocity'], 'status': new_status, 'last_updated': now, }) results['analyzed'] += 1 return results