身份令牌验证与推荐接口
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: