身份令牌验证与推荐接口

Change-Id: I572c2e74b9336f2f472805d164969656278dfd8d
diff --git a/Merge/back_rhj/__pycache__/config.cpython-312.pyc b/Merge/back_rhj/__pycache__/config.cpython-312.pyc
index 59299dd..00c0bdd 100644
--- a/Merge/back_rhj/__pycache__/config.cpython-312.pyc
+++ b/Merge/back_rhj/__pycache__/config.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/__init__.py b/Merge/back_rhj/app/__init__.py
index c50a674..098898c 100644
--- a/Merge/back_rhj/app/__init__.py
+++ b/Merge/back_rhj/app/__init__.py
@@ -1,5 +1,7 @@
 from flask import Flask
 from flask_cors import CORS
+import atexit
+import logging
 
 def create_app():
     app = Flask(__name__)
@@ -17,6 +19,26 @@
     # Register recommendation blueprint
     from .blueprints.recommend import recommend_bp
     app.register_blueprint(recommend_bp)
+    
+    # Register scheduler blueprint
+    from .blueprints.scheduler import scheduler_bp
+    app.register_blueprint(scheduler_bp)
+    
+    # 初始化定时任务管理器
+    from .utils.scheduler_manager import SchedulerManager
+    
+    scheduler_manager = SchedulerManager()
+    scheduler_manager.init_scheduler(app)
+    
+    # 检查是否启用定时任务
+    scheduler_enabled = getattr(app.config, 'SCHEDULER_ENABLED', True)
+    if scheduler_enabled:
+        # 从配置获取重建间隔
+        rebuild_interval = getattr(app.config, 'GRAPH_REBUILD_INTERVAL', 1)
+        scheduler_manager.start_graph_rebuild_task(interval_minutes=rebuild_interval)
+    
+    # 注册关闭时的清理函数
+    atexit.register(lambda: scheduler_manager.shutdown())
 
     return app
 
diff --git a/Merge/back_rhj/app/__pycache__/__init__.cpython-312.pyc b/Merge/back_rhj/app/__pycache__/__init__.cpython-312.pyc
index 7c7d017..770df5b 100644
--- a/Merge/back_rhj/app/__pycache__/__init__.cpython-312.pyc
+++ b/Merge/back_rhj/app/__pycache__/__init__.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/__pycache__/routes.cpython-312.pyc b/Merge/back_rhj/app/__pycache__/routes.cpython-312.pyc
index 0ec74bd..f2ad12b 100644
--- a/Merge/back_rhj/app/__pycache__/routes.cpython-312.pyc
+++ b/Merge/back_rhj/app/__pycache__/routes.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/blueprints/__pycache__/recommend.cpython-312.pyc b/Merge/back_rhj/app/blueprints/__pycache__/recommend.cpython-312.pyc
index 29c786d..75ccdab 100644
--- a/Merge/back_rhj/app/blueprints/__pycache__/recommend.cpython-312.pyc
+++ b/Merge/back_rhj/app/blueprints/__pycache__/recommend.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/blueprints/__pycache__/scheduler.cpython-312.pyc b/Merge/back_rhj/app/blueprints/__pycache__/scheduler.cpython-312.pyc
new file mode 100644
index 0000000..ae84b70
--- /dev/null
+++ b/Merge/back_rhj/app/blueprints/__pycache__/scheduler.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/blueprints/scheduler.py b/Merge/back_rhj/app/blueprints/scheduler.py
new file mode 100644
index 0000000..038f8f4
--- /dev/null
+++ b/Merge/back_rhj/app/blueprints/scheduler.py
@@ -0,0 +1,165 @@
+"""
+定时任务管理API蓝图
+提供定时任务的启动、停止、状态查询等接口
+"""
+from flask import Blueprint, jsonify, request, current_app
+from functools import wraps
+
+scheduler_bp = Blueprint('scheduler', __name__, url_prefix='/api/scheduler')
+
+def admin_required(f):
+    """装饰器:需要管理员权限"""
+    @wraps(f)
+    def decorated(*args, **kwargs):
+        # 这里可以添加管理员权限验证逻辑
+        # 暂时允许所有请求通过
+        return f(*args, **kwargs)
+    return decorated
+
+@scheduler_bp.route('/status', methods=['GET'])
+def get_scheduler_status():
+    """获取定时任务状态"""
+    try:
+        scheduler_manager = current_app.scheduler_manager
+        status = scheduler_manager.get_task_status()
+        
+        return jsonify({
+            'success': True,
+            'data': status,
+            'message': '获取定时任务状态成功'
+        })
+    except Exception as e:
+        return jsonify({
+            'success': False,
+            'message': f'获取定时任务状态失败: {str(e)}'
+        }), 500
+
+@scheduler_bp.route('/start', methods=['POST'])
+@admin_required
+def start_scheduler():
+    """启动定时任务"""
+    try:
+        data = request.get_json() or {}
+        interval_minutes = data.get('interval_minutes', 1)
+        
+        # 验证间隔时间
+        if not isinstance(interval_minutes, (int, float)) or interval_minutes <= 0:
+            return jsonify({
+                'success': False,
+                'message': '无效的间隔时间,必须是大于0的数字'
+            }), 400
+            
+        scheduler_manager = current_app.scheduler_manager
+        scheduler_manager.start_graph_rebuild_task(interval_minutes)
+        
+        return jsonify({
+            'success': True,
+            'message': f'定时任务已启动,间隔: {interval_minutes}分钟'
+        })
+    except Exception as e:
+        return jsonify({
+            'success': False,
+            'message': f'启动定时任务失败: {str(e)}'
+        }), 500
+
+@scheduler_bp.route('/stop', methods=['POST'])
+@admin_required
+def stop_scheduler():
+    """停止定时任务"""
+    try:
+        scheduler_manager = current_app.scheduler_manager
+        scheduler_manager.stop_graph_rebuild_task()
+        
+        return jsonify({
+            'success': True,
+            'message': '定时任务已停止'
+        })
+    except Exception as e:
+        return jsonify({
+            'success': False,
+            'message': f'停止定时任务失败: {str(e)}'
+        }), 500
+
+@scheduler_bp.route('/update-interval', methods=['POST'])
+@admin_required
+def update_interval():
+    """更新任务间隔"""
+    try:
+        data = request.get_json()
+        if not data or 'interval_minutes' not in data:
+            return jsonify({
+                'success': False,
+                'message': '缺少interval_minutes参数'
+            }), 400
+            
+        interval_minutes = data['interval_minutes']
+        
+        # 验证间隔时间
+        if not isinstance(interval_minutes, (int, float)) or interval_minutes <= 0:
+            return jsonify({
+                'success': False,
+                'message': '无效的间隔时间,必须是大于0的数字'
+            }), 400
+            
+        scheduler_manager = current_app.scheduler_manager
+        scheduler_manager.update_task_interval(interval_minutes)
+        
+        return jsonify({
+            'success': True,
+            'message': f'任务间隔已更新为 {interval_minutes}分钟'
+        })
+    except Exception as e:
+        return jsonify({
+            'success': False,
+            'message': f'更新任务间隔失败: {str(e)}'
+        }), 500
+
+@scheduler_bp.route('/rebuild-now', methods=['POST'])
+@admin_required
+def rebuild_graph_now():
+    """立即执行一次图重建"""
+    try:
+        scheduler_manager = current_app.scheduler_manager
+        
+        # 在新线程中执行,避免阻塞请求
+        import threading
+        thread = threading.Thread(target=scheduler_manager.rebuild_graph_job)
+        thread.start()
+        
+        return jsonify({
+            'success': True,
+            'message': '图重建任务已开始执行'
+        })
+    except Exception as e:
+        return jsonify({
+            'success': False,
+            'message': f'执行图重建失败: {str(e)}'
+        }), 500
+
+@scheduler_bp.route('/logs', methods=['GET'])
+def get_rebuild_logs():
+    """获取重建日志统计"""
+    try:
+        scheduler_manager = current_app.scheduler_manager
+        status = scheduler_manager.get_task_status()
+        
+        log_info = {
+            'rebuild_count': status['rebuild_count'],
+            'error_count': status['error_count'],
+            'last_rebuild_time': status['last_rebuild_time'],
+            'success_rate': (
+                ((status['rebuild_count'] - status['error_count']) / status['rebuild_count'] * 100)
+                if status['rebuild_count'] > 0 else 0
+            )
+        }
+        
+        return jsonify({
+            'success': True,
+            'data': log_info,
+            'message': '获取重建日志成功'
+        })
+    except Exception as e:
+        return jsonify({
+            'success': False,
+            'message': f'获取重建日志失败: {str(e)}'
+        }), 500
diff --git a/Merge/back_rhj/app/functions/__pycache__/FAuth.cpython-312.pyc b/Merge/back_rhj/app/functions/__pycache__/FAuth.cpython-312.pyc
index 49086a6..6c0297b 100644
--- a/Merge/back_rhj/app/functions/__pycache__/FAuth.cpython-312.pyc
+++ b/Merge/back_rhj/app/functions/__pycache__/FAuth.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/models/__pycache__/__init__.cpython-312.pyc b/Merge/back_rhj/app/models/__pycache__/__init__.cpython-312.pyc
index a0814d8..8eec580 100644
--- a/Merge/back_rhj/app/models/__pycache__/__init__.cpython-312.pyc
+++ b/Merge/back_rhj/app/models/__pycache__/__init__.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/models/__pycache__/email_verification.cpython-312.pyc b/Merge/back_rhj/app/models/__pycache__/email_verification.cpython-312.pyc
index c1d6dfa..c9ad75d 100644
--- a/Merge/back_rhj/app/models/__pycache__/email_verification.cpython-312.pyc
+++ b/Merge/back_rhj/app/models/__pycache__/email_verification.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/models/__pycache__/users.cpython-312.pyc b/Merge/back_rhj/app/models/__pycache__/users.cpython-312.pyc
index 58af35e..1af05fe 100644
--- a/Merge/back_rhj/app/models/__pycache__/users.cpython-312.pyc
+++ b/Merge/back_rhj/app/models/__pycache__/users.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/models/recall/__pycache__/__init__.cpython-312.pyc b/Merge/back_rhj/app/models/recall/__pycache__/__init__.cpython-312.pyc
index d1cf37c..88c8123 100644
--- a/Merge/back_rhj/app/models/recall/__pycache__/__init__.cpython-312.pyc
+++ b/Merge/back_rhj/app/models/recall/__pycache__/__init__.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/models/recall/__pycache__/ad_recall.cpython-312.pyc b/Merge/back_rhj/app/models/recall/__pycache__/ad_recall.cpython-312.pyc
index 08a722c..7d0748e 100644
--- a/Merge/back_rhj/app/models/recall/__pycache__/ad_recall.cpython-312.pyc
+++ b/Merge/back_rhj/app/models/recall/__pycache__/ad_recall.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/models/recall/__pycache__/hot_recall.cpython-312.pyc b/Merge/back_rhj/app/models/recall/__pycache__/hot_recall.cpython-312.pyc
index cb6c725..3cfdd43 100644
--- a/Merge/back_rhj/app/models/recall/__pycache__/hot_recall.cpython-312.pyc
+++ b/Merge/back_rhj/app/models/recall/__pycache__/hot_recall.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/models/recall/__pycache__/multi_recall_manager.cpython-312.pyc b/Merge/back_rhj/app/models/recall/__pycache__/multi_recall_manager.cpython-312.pyc
index 9a95456..9e1d7c8 100644
--- a/Merge/back_rhj/app/models/recall/__pycache__/multi_recall_manager.cpython-312.pyc
+++ b/Merge/back_rhj/app/models/recall/__pycache__/multi_recall_manager.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/models/recall/__pycache__/swing_recall.cpython-312.pyc b/Merge/back_rhj/app/models/recall/__pycache__/swing_recall.cpython-312.pyc
index d913d68..24c3ddb 100644
--- a/Merge/back_rhj/app/models/recall/__pycache__/swing_recall.cpython-312.pyc
+++ b/Merge/back_rhj/app/models/recall/__pycache__/swing_recall.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/models/recall/__pycache__/usercf_recall.cpython-312.pyc b/Merge/back_rhj/app/models/recall/__pycache__/usercf_recall.cpython-312.pyc
index adb6177..e212bf5 100644
--- a/Merge/back_rhj/app/models/recall/__pycache__/usercf_recall.cpython-312.pyc
+++ b/Merge/back_rhj/app/models/recall/__pycache__/usercf_recall.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/models/recall/ad_recall.py b/Merge/back_rhj/app/models/recall/ad_recall.py
deleted file mode 100644
index 0fe3b0a..0000000
--- a/Merge/back_rhj/app/models/recall/ad_recall.py
+++ /dev/null
@@ -1,207 +0,0 @@
-import pymysql
-from typing import List, Tuple, Dict
-import random
-
-class AdRecall:
-    """
-    广告召回算法实现
-    专门用于召回广告类型的内容
-    """
-    
-    def __init__(self, db_config: dict):
-        """
-        初始化广告召回模型
-        
-        Args:
-            db_config: 数据库配置
-        """
-        self.db_config = db_config
-        self.ad_items = []
-        
-    def _get_ad_items(self):
-        """获取广告物品列表"""
-        conn = pymysql.connect(**self.db_config)
-        try:
-            cursor = conn.cursor()
-            
-            # 获取所有广告帖子,按热度和发布时间排序
-            cursor.execute("""
-                SELECT 
-                    p.id,
-                    p.heat,
-                    p.created_at,
-                    COUNT(DISTINCT b.user_id) as interaction_count,
-                    DATEDIFF(NOW(), p.created_at) as days_since_created
-                FROM posts p
-                LEFT JOIN behaviors b ON p.id = b.post_id
-                WHERE p.is_advertisement = 1 AND p.status = 'published'
-                GROUP BY p.id, p.heat, p.created_at
-                ORDER BY p.heat DESC, p.created_at DESC
-            """)
-            
-            results = cursor.fetchall()
-            
-            # 计算广告分数
-            items_with_scores = []
-            for row in results:
-                post_id, heat, created_at, interaction_count, days_since_created = row
-                
-                # 处理None值
-                heat = heat or 0
-                interaction_count = interaction_count or 0
-                days_since_created = days_since_created or 0
-                
-                # 广告分数计算:热度 + 交互数 - 时间惩罚
-                # 新发布的广告给予更高权重
-                freshness_bonus = max(0, 30 - days_since_created) / 30.0  # 30天内的新鲜度奖励
-                
-                ad_score = (
-                    heat * 0.6 +
-                    interaction_count * 0.3 +
-                    freshness_bonus * 100  # 新鲜度奖励
-                )
-                
-                items_with_scores.append((post_id, ad_score))
-            
-            # 按广告分数排序
-            self.ad_items = sorted(items_with_scores, key=lambda x: x[1], reverse=True)
-            
-        finally:
-            cursor.close()
-            conn.close()
-    
-    def train(self):
-        """训练广告召回模型"""
-        print("开始获取广告物品...")
-        self._get_ad_items()
-        print(f"广告召回模型训练完成,共{len(self.ad_items)}个广告物品")
-    
-    def recall(self, user_id: int, num_items: int = 10) -> List[Tuple[int, float]]:
-        """
-        为用户召回广告物品
-        
-        Args:
-            user_id: 用户ID
-            num_items: 召回物品数量
-            
-        Returns:
-            List of (item_id, score) tuples
-        """
-        # 如果尚未训练,先进行训练
-        if not hasattr(self, 'ad_items') or not self.ad_items:
-            self.train()
-        
-        # 获取用户已交互的广告,避免重复推荐
-        conn = pymysql.connect(**self.db_config)
-        try:
-            cursor = conn.cursor()
-            cursor.execute("""
-                SELECT DISTINCT b.post_id 
-                FROM behaviors b
-                JOIN posts p ON b.post_id = p.id
-                WHERE b.user_id = %s AND p.is_advertisement = 1
-                AND b.type IN ('like', 'favorite', 'comment', 'view')
-            """, (user_id,))
-            
-            user_interacted_ads = set(row[0] for row in cursor.fetchall())
-            
-            # 获取用户的兴趣标签(基于历史行为)
-            cursor.execute("""
-                SELECT t.name, COUNT(*) as count
-                FROM behaviors b
-                JOIN posts p ON b.post_id = p.id
-                JOIN post_tags pt ON p.id = pt.post_id
-                JOIN tags t ON pt.tag_id = t.id
-                WHERE b.user_id = %s AND b.type IN ('like', 'favorite', 'comment')
-                GROUP BY t.name
-                ORDER BY count DESC
-                LIMIT 10
-            """, (user_id,))
-            
-            user_interest_tags = set(row[0] for row in cursor.fetchall())
-            
-        finally:
-            cursor.close()
-            conn.close()
-        
-        # 过滤掉用户已交互的广告
-        filtered_ads = [
-            (item_id, score) for item_id, score in self.ad_items
-            if item_id not in user_interacted_ads
-        ]
-        
-        # 如果没有未交互的广告,但有广告数据,返回评分最高的广告(可能用户会再次感兴趣)
-        if not filtered_ads and self.ad_items:
-            print(f"用户 {user_id} 已与所有广告交互,返回评分最高的广告")
-            filtered_ads = self.ad_items[:num_items]
-        
-        # 如果用户有兴趣标签,可以进一步个性化广告推荐
-        if user_interest_tags and filtered_ads:
-            filtered_ads = self._personalize_ads(filtered_ads, user_interest_tags)
-        
-        return filtered_ads[:num_items]
-    
-    def _personalize_ads(self, ad_list: List[Tuple[int, float]], user_interest_tags: set) -> List[Tuple[int, float]]:
-        """
-        根据用户兴趣标签个性化广告推荐
-        
-        Args:
-            ad_list: 广告列表
-            user_interest_tags: 用户兴趣标签
-            
-        Returns:
-            个性化后的广告列表
-        """
-        conn = pymysql.connect(**self.db_config)
-        try:
-            cursor = conn.cursor()
-            
-            personalized_ads = []
-            for ad_id, ad_score in ad_list:
-                # 获取广告的标签
-                cursor.execute("""
-                    SELECT t.name
-                    FROM post_tags pt
-                    JOIN tags t ON pt.tag_id = t.id
-                    WHERE pt.post_id = %s
-                """, (ad_id,))
-                
-                ad_tags = set(row[0] for row in cursor.fetchall())
-                
-                # 计算标签匹配度
-                tag_match_score = len(ad_tags & user_interest_tags) / max(len(user_interest_tags), 1)
-                
-                # 调整广告分数
-                final_score = ad_score * (1 + tag_match_score)
-                personalized_ads.append((ad_id, final_score))
-            
-            # 重新排序
-            personalized_ads.sort(key=lambda x: x[1], reverse=True)
-            return personalized_ads
-            
-        finally:
-            cursor.close()
-            conn.close()
-    
-    def get_random_ads(self, num_items: int = 5) -> List[Tuple[int, float]]:
-        """
-        获取随机广告(用于多样性)
-        
-        Args:
-            num_items: 返回物品数量
-            
-        Returns:
-            List of (item_id, score) tuples
-        """
-        if len(self.ad_items) <= num_items:
-            return self.ad_items
-        
-        # 随机选择但倾向于高分广告
-        weights = [score for _, score in self.ad_items]
-        selected_indices = random.choices(
-            range(len(self.ad_items)), 
-            weights=weights, 
-            k=num_items
-        )
-        
-        return [self.ad_items[i] for i in selected_indices]
diff --git a/Merge/back_rhj/app/models/recommend/__pycache__/LightGCN.cpython-312.pyc b/Merge/back_rhj/app/models/recommend/__pycache__/LightGCN.cpython-312.pyc
index c87435f..f7079b8 100644
--- a/Merge/back_rhj/app/models/recommend/__pycache__/LightGCN.cpython-312.pyc
+++ b/Merge/back_rhj/app/models/recommend/__pycache__/LightGCN.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/models/recommend/__pycache__/base_model.cpython-312.pyc b/Merge/back_rhj/app/models/recommend/__pycache__/base_model.cpython-312.pyc
index b9d8c72..19bf281 100644
--- a/Merge/back_rhj/app/models/recommend/__pycache__/base_model.cpython-312.pyc
+++ b/Merge/back_rhj/app/models/recommend/__pycache__/base_model.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/models/recommend/__pycache__/operators.cpython-312.pyc b/Merge/back_rhj/app/models/recommend/__pycache__/operators.cpython-312.pyc
index 13bb375..28de5cd 100644
--- a/Merge/back_rhj/app/models/recommend/__pycache__/operators.cpython-312.pyc
+++ b/Merge/back_rhj/app/models/recommend/__pycache__/operators.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/routes.py b/Merge/back_rhj/app/routes.py
index 23ff49b..f123a84 100644
--- a/Merge/back_rhj/app/routes.py
+++ b/Merge/back_rhj/app/routes.py
@@ -1,5 +1,6 @@
 from flask import Blueprint, request, jsonify
 from .functions.FAuth import FAuth
+from .services.recommendation_service import RecommendationService
 from sqlalchemy import create_engine
 from sqlalchemy.orm import sessionmaker
 from config import Config
@@ -8,6 +9,9 @@
 
 main = Blueprint('main', __name__)
 
+# 初始化推荐服务
+recommendation_service = RecommendationService()
+
 def token_required(f):
     """装饰器:需要令牌验证"""
     @wraps(f)
@@ -322,4 +326,69 @@
         return jsonify({
             'success': False, 
             'message': f'JWT令牌验证失败: {str(e)}'
-        }), 500
\ No newline at end of file
+        }), 500
+        
+@main.route('/verify_user', methods=['POST'])
+@token_required
+def verify_user(current_user):
+    """测试JWT令牌接口(需要登录)"""
+    try:
+        # 获取当前请求的token(从装饰器已验证的Authorization header)
+        auth_header = request.headers.get('Authorization')
+        current_token = auth_header[7:] if auth_header and auth_header.startswith('Bearer ') else None
+        
+        print(f"当前用户: {current_user.username}")
+        print(f"当前用户ID: {current_user.id}")
+        print(current_user.role)
+        print(f"Token验证成功: {current_token[:20]}..." if current_token else "No token")
+        
+        # 可选:检查请求体中是否有额外的token需要验证
+        data = request.get_json() or {}
+        additional_token = data.get('token')
+        
+        response_data = {
+            'success': True,
+            'userid': current_user.id,
+            'role': current_user.role,
+        }
+        
+        return jsonify(response_data), 200
+        
+    except Exception as e:
+        print(f"用户验证错误: {str(e)}")
+        return jsonify({
+            'success': False, 
+            'message': f'JWT令牌验证失败: {str(e)}'
+        }), 500
+
+@main.route('/recommend', methods=['POST'])
+@token_required
+def get_recommendations(current_user):
+    """获取个性化推荐接口"""
+    try:
+        data = request.get_json() or {}
+        user_id = data.get('user_id') or current_user.id
+        topk = data.get('topk', 10)  # 默认推荐10个
+        
+        print(f"为用户 {user_id} 获取推荐,数量: {topk}")
+        
+        # 调用推荐系统
+        recommendations = recommendation_service.get_recommendations(user_id, topk)
+        
+        return jsonify({
+            'success': True,
+            'data': {
+                'user_id': user_id,
+                'recommendations': recommendations,
+                'count': len(recommendations),
+                'type': 'personalized'
+            },
+            'message': '个性化推荐获取成功'
+        }), 200
+        
+    except Exception as e:
+        print(f"推荐系统错误: {str(e)}")
+        return jsonify({
+            'success': False, 
+            'message': f'推荐获取失败: {str(e)}'
+        }), 500
diff --git a/Merge/back_rhj/app/services/__pycache__/lightgcn_scorer.cpython-312.pyc b/Merge/back_rhj/app/services/__pycache__/lightgcn_scorer.cpython-312.pyc
index 2c86f52..a8d871a 100644
--- a/Merge/back_rhj/app/services/__pycache__/lightgcn_scorer.cpython-312.pyc
+++ b/Merge/back_rhj/app/services/__pycache__/lightgcn_scorer.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/services/__pycache__/recommendation_service.cpython-312.pyc b/Merge/back_rhj/app/services/__pycache__/recommendation_service.cpython-312.pyc
index da8389f..fd70a42 100644
--- a/Merge/back_rhj/app/services/__pycache__/recommendation_service.cpython-312.pyc
+++ b/Merge/back_rhj/app/services/__pycache__/recommendation_service.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/services/recommendation_service.py b/Merge/back_rhj/app/services/recommendation_service.py
index 2f4de13..0547f7b 100644
--- a/Merge/back_rhj/app/services/recommendation_service.py
+++ b/Merge/back_rhj/app/services/recommendation_service.py
@@ -483,7 +483,7 @@
             # 查询帖子基本信息
             format_strings = ','.join(['%s'] * len(topk_post_ids))
             cursor.execute(
-                f"""SELECT p.id, p.user_id, p.title, p.content, p.type, p.heat, p.created_at, p.is_advertisement
+                f"""SELECT p.id, p.user_id, p.title, p.content, p.type, p.heat, p.created_at, p.updated_at, p.media_urls, p.status, p.is_advertisement
                     FROM posts p 
                     WHERE p.id IN ({format_strings}) AND p.status = 'published'""",
                 tuple(topk_post_ids)
@@ -541,15 +541,20 @@
                 owner_user_id = row[1]
                 stats = behavior_stats.get(post_id, {})
                 post_info = {
-                    'post_id': post_id,
+                    'id': post_id,
+                    'user_id': owner_user_id,
                     'title': row[2],
-                    'content': row[3][:200] + '...' if len(row[3]) > 200 else row[3],
+                    'content': row[3],  # 不再截断,保持完整内容
+                    'media_urls': row[8],
+                    'status': row[9],
+                    'heat': row[5],
+                    'created_at': row[6].isoformat() if row[6] else "",
+                    'updated_at': row[7].isoformat() if row[7] else "",
+                    # 额外字段,可选保留
                     'type': row[4],
                     'username': user_map.get(owner_user_id, ""),
-                    'heat': row[5],
                     'tags': tag_map.get(post_id, ""),
-                    'created_at': str(row[6]) if row[6] else "",
-                    'is_advertisement': bool(row[7]),  # 添加广告标识
+                    'is_advertisement': bool(row[10]),
                     'like_count': stats.get('like', 0),
                     'comment_count': stats.get('comment', 0),
                     'favorite_count': stats.get('favorite', 0),
@@ -557,10 +562,6 @@
                     'share_count': stats.get('share', 0)
                 }
                 
-                # 如果有推荐打分,添加到结果中
-                if topk_scores is not None and i < len(topk_scores):
-                    post_info['recommendation_score'] = float(topk_scores[i])
-                
                 post_list.append(post_info)
             return post_list
         finally:
diff --git a/Merge/back_rhj/app/user_post_graph.txt b/Merge/back_rhj/app/user_post_graph.txt
index 2c66fd1..23da7af 100644
--- a/Merge/back_rhj/app/user_post_graph.txt
+++ b/Merge/back_rhj/app/user_post_graph.txt
@@ -1,11 +1,11 @@
-0	1 0 0 2 1 2 0 1 2 1 0 42 32 62 52 0 12 22	1749827292 1749827292 1749953091 1749953091 1749953091 1749953480 1749953480 1749953480 1749954059 1749954059 1749954059 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	1 5 5 2 1 2 5 1 2 1 5 5 2 2 1 1 5 1
-1	2 0 0 43 33 53 1 5 13 23	1749827292 1749953091 1749953480 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	5 5 5 1 5 2 2 5 1 2
-2	7 6 6 7 44 34 54 2 14 24	1749953091 1749953091 1749953480 1749953480 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	2 1 1 2 2 1 5 5 2 5
-3	3 0 3 0 0 1 45 35 55 15 25	1749953091 1749953091 1749953480 1749953480 1749954059 1749954059 1749955282 1749955282 1749955282 1749955282 1749955282	2 2 2 2 1 2 5 2 1 5 1
-4	0 0 2 46 36 56 6 16 26	1749953091 1749953480 1749954059 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	5 5 5 1 5 2 5 1 2
-5	37 47 57 3 7 17 27	1749955282 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	1 2 5 5 1 2 5
-6	38 48 58 8 18 28	1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	2 5 1 2 5 1
-7	39 49 59 4 9 19 29	1749955282 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	5 1 2 2 5 1 2
-8	40 50 60 10 20 30	1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	1 2 5 1 2 5
-9	41 51 61 11 31 21	1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	2 5 1 2 1 5
-10	13	1749894174	5
+0	0 0 1 1 0 1 0 41 31 61 51 11 21	1749827292 1749953091 1749953091 1749953480 1749953480 1749954059 1749954059 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	1 1 2 2 1 2 1 5 2 2 1 5 1
+1	1 42 32 52 0 4 12 22	1749827292 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	5 1 5 2 2 5 1 2
+2	6 5 6 5 43 33 53 1 13 23	1749953091 1749953091 1749953480 1749953480 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	2 1 2 1 2 1 5 5 2 5
+3	2 2 0 44 34 54 14 24	1749953091 1749953480 1749954059 1749955282 1749955282 1749955282 1749955282 1749955282	2 2 2 5 2 1 5 1
+4	1 45 35 55 15 5 25	1749954059 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	5 1 5 2 1 5 2
+5	36 46 56 2 6 16 26	1749955282 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	1 2 5 5 1 2 5
+6	37 47 57 7 17 27	1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	2 5 1 2 5 1
+7	38 48 58 3 8 18 28	1749955282 1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	5 1 2 2 5 1 2
+8	39 49 59 9 19 29	1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	1 2 5 1 2 5
+9	40 50 60 10 30 20	1749955282 1749955282 1749955282 1749955282 1749955282 1749955282	2 5 1 2 1 5
+10	12	1749894174	5
diff --git a/Merge/back_rhj/app/utils/__pycache__/data_loader.cpython-312.pyc b/Merge/back_rhj/app/utils/__pycache__/data_loader.cpython-312.pyc
index 10b3571..8157bf8 100644
--- a/Merge/back_rhj/app/utils/__pycache__/data_loader.cpython-312.pyc
+++ b/Merge/back_rhj/app/utils/__pycache__/data_loader.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/utils/__pycache__/graph_build.cpython-312.pyc b/Merge/back_rhj/app/utils/__pycache__/graph_build.cpython-312.pyc
index a560e74..e1676da 100644
--- a/Merge/back_rhj/app/utils/__pycache__/graph_build.cpython-312.pyc
+++ b/Merge/back_rhj/app/utils/__pycache__/graph_build.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/utils/__pycache__/parse_args.cpython-312.pyc b/Merge/back_rhj/app/utils/__pycache__/parse_args.cpython-312.pyc
index a88ee3b..ba17fcf 100644
--- a/Merge/back_rhj/app/utils/__pycache__/parse_args.cpython-312.pyc
+++ b/Merge/back_rhj/app/utils/__pycache__/parse_args.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/utils/__pycache__/scheduler_manager.cpython-312.pyc b/Merge/back_rhj/app/utils/__pycache__/scheduler_manager.cpython-312.pyc
new file mode 100644
index 0000000..6ed8964
--- /dev/null
+++ b/Merge/back_rhj/app/utils/__pycache__/scheduler_manager.cpython-312.pyc
Binary files differ
diff --git a/Merge/back_rhj/app/utils/scheduler_manager.py b/Merge/back_rhj/app/utils/scheduler_manager.py
new file mode 100644
index 0000000..d063e84
--- /dev/null
+++ b/Merge/back_rhj/app/utils/scheduler_manager.py
@@ -0,0 +1,148 @@
+"""
+定时任务管理器模块
+用于管理LightGCN图重建的定时任务
+"""
+import logging
+import os
+from datetime import datetime
+from apscheduler.schedulers.background import BackgroundScheduler
+from apscheduler.triggers.interval import IntervalTrigger
+from apscheduler.executors.pool import ThreadPoolExecutor
+from .graph_build import build_user_post_graph
+
+# 配置日志
+logging.basicConfig(
+    level=logging.INFO,
+    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger(__name__)
+
+class SchedulerManager:
+    """定时任务管理器"""
+    
+    def __init__(self):
+        self.scheduler = None
+        self.is_running = False
+        self.last_rebuild_time = None
+        self.rebuild_count = 0
+        self.error_count = 0
+        self.last_error = None
+        
+    def init_scheduler(self, app):
+        """初始化调度器"""
+        if self.scheduler is None:
+            # 从配置中获取设置
+            timezone = getattr(app.config, 'SCHEDULER_TIMEZONE', 'Asia/Shanghai')
+            max_threads = getattr(app.config, 'MAX_SCHEDULER_THREADS', 5)
+            
+            # 配置执行器
+            executors = {
+                'default': ThreadPoolExecutor(max_threads)
+            }
+            
+            # 创建调度器
+            self.scheduler = BackgroundScheduler(
+                executors=executors,
+                timezone=timezone
+            )
+            
+            app.scheduler_manager = self
+            logger.info(f"调度器初始化完成,时区: {timezone}, 最大线程数: {max_threads}")
+            
+    def rebuild_graph_job(self):
+        """重新构建LightGCN图的任务函数"""
+        try:
+            logger.info(f"开始第{self.rebuild_count + 1}次重新构建LightGCN图...")
+            start_time = datetime.now()
+            
+            # 执行图构建
+            build_user_post_graph()
+            
+            end_time = datetime.now()
+            duration = (end_time - start_time).total_seconds()
+            
+            self.last_rebuild_time = end_time
+            self.rebuild_count += 1
+            self.last_error = None  # 清除上次错误
+            
+            logger.info(f"LightGCN图重新构建完成,耗时: {duration:.2f}秒")
+            
+        except Exception as e:
+            self.error_count += 1
+            self.last_error = str(e)
+            logger.error(f"LightGCN图重新构建失败: {str(e)}")
+            
+    def start_graph_rebuild_task(self, interval_minutes=1):
+        """启动图重建定时任务"""
+        if self.scheduler is None:
+            raise RuntimeError("调度器未初始化")
+            
+        job_id = 'rebuild_lightgcn_graph'
+        
+        # 如果任务已存在,先移除
+        if self.scheduler.get_job(job_id):
+            self.scheduler.remove_job(job_id)
+            
+        # 添加新任务
+        self.scheduler.add_job(
+            func=self.rebuild_graph_job,
+            trigger=IntervalTrigger(minutes=interval_minutes),
+            id=job_id,
+            name=f'重新构建LightGCN图(每{interval_minutes}分钟)',
+            replace_existing=True,
+            max_instances=1  # 防止重复执行
+        )
+        
+        if not self.scheduler.running:
+            self.scheduler.start()
+            
+        self.is_running = True
+        logger.info(f"图重建定时任务已启动,间隔: {interval_minutes}分钟")
+        
+    def stop_graph_rebuild_task(self):
+        """停止图重建定时任务"""
+        if self.scheduler and self.scheduler.running:
+            job_id = 'rebuild_lightgcn_graph'
+            if self.scheduler.get_job(job_id):
+                self.scheduler.remove_job(job_id)
+                
+        self.is_running = False
+        logger.info("图重建定时任务已停止")
+        
+    def update_task_interval(self, interval_minutes):
+        """更新任务间隔"""
+        if self.is_running:
+            self.stop_graph_rebuild_task()
+            self.start_graph_rebuild_task(interval_minutes)
+        else:
+            logger.info(f"任务间隔已更新为{interval_minutes}分钟,但任务当前未运行")
+            
+    def get_task_status(self):
+        """获取任务状态"""
+        job_id = 'rebuild_lightgcn_graph'
+        job = None
+        
+        if self.scheduler:
+            job = self.scheduler.get_job(job_id)
+            
+        return {
+            'is_running': self.is_running,
+            'scheduler_running': self.scheduler.running if self.scheduler else False,
+            'job_exists': job is not None,
+            'job_name': job.name if job else None,
+            'next_run_time': job.next_run_time.isoformat() if job and job.next_run_time else None,
+            'last_rebuild_time': self.last_rebuild_time.isoformat() if self.last_rebuild_time else None,
+            'rebuild_count': self.rebuild_count,
+            'error_count': self.error_count,
+            'last_error': self.last_error,
+            'success_rate': (
+                ((self.rebuild_count - self.error_count) / self.rebuild_count * 100)
+                if self.rebuild_count > 0 else 0
+            )
+        }
+        
+    def shutdown(self):
+        """关闭调度器"""
+        if self.scheduler and self.scheduler.running:
+            self.scheduler.shutdown()
+            logger.info("调度器已关闭")
diff --git a/Merge/back_rhj/config.py b/Merge/back_rhj/config.py
index c249660..8375065 100644
--- a/Merge/back_rhj/config.py
+++ b/Merge/back_rhj/config.py
@@ -20,4 +20,10 @@
     MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
     MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
     MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
-    MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER')
\ No newline at end of file
+    MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER')
+    
+    # 定时任务配置
+    SCHEDULER_ENABLED = os.environ.get('SCHEDULER_ENABLED', 'true').lower() in ['true', 'on', '1']
+    GRAPH_REBUILD_INTERVAL = int(os.environ.get('GRAPH_REBUILD_INTERVAL', 1))  # 默认1分钟
+    SCHEDULER_TIMEZONE = os.environ.get('SCHEDULER_TIMEZONE', 'Asia/Shanghai')
+    MAX_SCHEDULER_THREADS = int(os.environ.get('MAX_SCHEDULER_THREADS', 5))
\ No newline at end of file
diff --git a/Merge/back_rhj/test_bloom_filter.py b/Merge/back_rhj/test_bloom_filter.py
deleted file mode 100644
index e69de29..0000000
--- a/Merge/back_rhj/test_bloom_filter.py
+++ /dev/null
diff --git a/Merge/back_rhj/test_redbook_recommendation.py b/Merge/back_rhj/test_redbook_recommendation.py
index d025ace..325def5 100644
--- a/Merge/back_rhj/test_redbook_recommendation.py
+++ b/Merge/back_rhj/test_redbook_recommendation.py
@@ -96,7 +96,7 @@
         
         print(f"冷启动推荐结果(用户{fake_user_id}):")
         for i, rec in enumerate(recommendations):
-            print(f"  {i+1}. 帖子ID: {rec['post_id']}, 标题: {rec['title'][:50]}...")
+            print(f"  {i+1}. 帖子ID: {rec['id']}, 标题: {rec['title'][:50]}...")
             print(f"     作者: {rec['username']}, 热度: {rec['heat']}")
             print(f"     点赞: {rec.get('like_count', 0)}, 评论: {rec.get('comment_count', 0)}")
             
@@ -150,7 +150,7 @@
             
             print(f"用户推荐结果(用户{user_id}):")
             for i, rec in enumerate(recommendations):
-                print(f"  {i+1}. 帖子ID: {rec['post_id']}, 标题: {rec['title'][:50]}...")
+                print(f"  {i+1}. 帖子ID: {rec['id']}, 标题: {rec['title'][:50]}...")
                 print(f"     作者: {rec['username']}, 热度: {rec['heat']}")
                 print(f"     点赞: {rec.get('like_count', 0)}, 评论: {rec.get('comment_count', 0)}")
                 if 'recommendation_score' in rec: