From 5662fcc03938d8aee319a8b4eb68b2e34629061f Mon Sep 17 00:00:00 2001 From: beabigegg Date: Thu, 4 Sep 2025 09:44:13 +0800 Subject: [PATCH] 12th_fix error --- app/__init__.py | 6 +- app/api/__init__.py | 5 +- app/api/notification.py | 331 ++++++++++++++++++++++++++ app/models/__init__.py | 5 +- app/models/notification.py | 97 ++++++++ app/services/notification_service.py | 228 +++++++++++++++++- app/tasks/translation.py | 13 + app/websocket.py | 230 ++++++++++++++++++ create_tables.py | 25 ++ create_test_data.py | 47 ++++ fix_notification_table.py | 69 ++++++ fix_table_simple.py | 42 ++++ frontend/src/layouts/MainLayout.vue | 62 ++--- frontend/src/services/notification.js | 63 +++++ frontend/src/stores/notification.js | 310 ++++++++++++++++++++++++ frontend/src/utils/websocket.js | 94 ++++++++ recreate_notification_table.py | 41 ++++ test_notification_api.py | 74 ++++++ test_routes.py | 43 ++++ 19 files changed, 1735 insertions(+), 50 deletions(-) create mode 100644 app/api/notification.py create mode 100644 app/models/notification.py create mode 100644 app/websocket.py create mode 100644 create_test_data.py create mode 100644 fix_notification_table.py create mode 100644 fix_table_simple.py create mode 100644 frontend/src/services/notification.js create mode 100644 frontend/src/stores/notification.js create mode 100644 recreate_notification_table.py create mode 100644 test_notification_api.py create mode 100644 test_routes.py diff --git a/app/__init__.py b/app/__init__.py index b5b1992..fe47e91 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -125,7 +125,7 @@ def create_app(config_name=None): # 建立資料表 with app.app_context(): # 導入模型 - from app.models import User, TranslationJob, JobFile, TranslationCache, APIUsageStats, SystemLog + from app.models import User, TranslationJob, JobFile, TranslationCache, APIUsageStats, SystemLog, Notification db.create_all() @@ -135,6 +135,10 @@ def create_app(config_name=None): # 創建 Celery 實例 app.celery = make_celery(app) + # 初始化 WebSocket + from app.websocket import init_websocket + app.socketio = init_websocket(app) + app.logger.info("Flask application created successfully") return app diff --git a/app/api/__init__.py b/app/api/__init__.py index 2ba4c58..3b943c8 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -14,11 +14,12 @@ from flask import Blueprint api_v1 = Blueprint('api_v1', __name__, url_prefix='/api/v1') # 匯入各 API 模組 -from . import auth, jobs, files, admin, health +from . import auth, jobs, files, admin, health, notification # 註冊路由 api_v1.register_blueprint(auth.auth_bp) api_v1.register_blueprint(jobs.jobs_bp) api_v1.register_blueprint(files.files_bp) api_v1.register_blueprint(admin.admin_bp) -api_v1.register_blueprint(health.health_bp) \ No newline at end of file +api_v1.register_blueprint(health.health_bp) +api_v1.register_blueprint(notification.notification_bp) \ No newline at end of file diff --git a/app/api/notification.py b/app/api/notification.py new file mode 100644 index 0000000..9ff8189 --- /dev/null +++ b/app/api/notification.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +通知系統 API 路由 + +Author: PANJIT IT Team +Created: 2024-01-28 +Modified: 2024-01-28 +""" + +from flask import Blueprint, jsonify, request, g +from flask_jwt_extended import jwt_required, get_jwt_identity +from sqlalchemy import desc, and_, or_ +from datetime import datetime, timedelta +from app import db +from app.models import Notification, NotificationType, User +from app.utils.response import create_taiwan_response +# 移除不需要的導入 + +# 建立藍圖 +notification_bp = Blueprint('notification', __name__, url_prefix='/notifications') + + +@notification_bp.route('', methods=['GET']) +@jwt_required() +def get_notifications(): + """獲取當前用戶的通知列表""" + try: + # 獲取當前用戶 + current_user_id = get_jwt_identity() + + # 獲取查詢參數 + page = request.args.get('page', 1, type=int) + per_page = min(request.args.get('per_page', 20, type=int), 100) + status_filter = request.args.get('status', 'all') + type_filter = request.args.get('type', None) + + # 建構查詢 + query = Notification.query.filter_by(user_id=current_user_id) + + # 只顯示未過期的通知 + query = query.filter(or_( + Notification.expires_at.is_(None), + Notification.expires_at > datetime.now() + )) + + # 過濾狀態 + if status_filter == 'unread': + query = query.filter_by(is_read=False) + elif status_filter == 'read': + query = query.filter_by(is_read=True) + + # 過濾類型 + if type_filter: + query = query.filter_by(type=type_filter) + + # 排序 - 未讀在前,然後按時間排序 + query = query.order_by(Notification.is_read.asc(), desc(Notification.created_at)) + + # 分頁 + paginated = query.paginate( + page=page, per_page=per_page, error_out=False + ) + + # 獲取未讀數量 + unread_count = Notification.query.filter_by( + user_id=current_user_id, + is_read=False + ).filter(or_( + Notification.expires_at.is_(None), + Notification.expires_at > datetime.now() + )).count() + + return jsonify(create_taiwan_response( + success=True, + data={ + 'notifications': [n.to_dict() for n in paginated.items], + 'pagination': { + 'total': paginated.total, + 'page': page, + 'per_page': per_page, + 'pages': paginated.pages + }, + 'unread_count': unread_count + }, + message='獲取通知列表成功' + )) + + except Exception as e: + return jsonify(create_taiwan_response( + success=False, + error=f'獲取通知失敗:{str(e)}' + )), 500 + + +@notification_bp.route('/', methods=['GET']) +@jwt_required() +def get_notification(notification_id): + """獲取單個通知詳情""" + try: + current_user_id = get_jwt_identity() + + # 查找通知 + notification = Notification.query.filter_by( + notification_uuid=notification_id, + user_id=current_user_id + ).first() + + if not notification: + return jsonify(create_taiwan_response( + success=False, + error='通知不存在' + )), 404 + + # 自動標記為已讀 + if not notification.is_read: + notification.mark_as_read() + db.session.commit() + + return jsonify(create_taiwan_response( + success=True, + data=notification.to_dict(), + message='獲取通知成功' + )) + + except Exception as e: + return jsonify(create_taiwan_response( + success=False, + error=f'獲取通知失敗:{str(e)}' + )), 500 + + +@notification_bp.route('//read', methods=['POST']) +@jwt_required() +def mark_notification_read(notification_id): + """標記通知為已讀""" + try: + current_user_id = get_jwt_identity() + + # 查找通知 + notification = Notification.query.filter_by( + notification_uuid=notification_id, + user_id=current_user_id + ).first() + + if not notification: + return jsonify(create_taiwan_response( + success=False, + error='通知不存在' + )), 404 + + # 標記為已讀 + notification.mark_as_read() + db.session.commit() + + return jsonify(create_taiwan_response( + success=True, + message='標記已讀成功' + )) + + except Exception as e: + return jsonify(create_taiwan_response( + success=False, + error=f'標記已讀失敗:{str(e)}' + )), 500 + + +@notification_bp.route('/read-all', methods=['POST']) +@jwt_required() +def mark_all_read(): + """標記所有通知為已讀""" + try: + current_user_id = get_jwt_identity() + + # 取得所有未讀通知 + unread_notifications = Notification.query.filter_by( + user_id=current_user_id, + is_read=False + ).filter(or_( + Notification.expires_at.is_(None), + Notification.expires_at > datetime.now() + )).all() + + # 標記為已讀 + for notification in unread_notifications: + notification.mark_as_read() + + db.session.commit() + + return jsonify(create_taiwan_response( + success=True, + data={'marked_count': len(unread_notifications)}, + message=f'已標記 {len(unread_notifications)} 個通知為已讀' + )) + + except Exception as e: + return jsonify(create_taiwan_response( + success=False, + error=f'標記全部已讀失敗:{str(e)}' + )), 500 + + +@notification_bp.route('/', methods=['DELETE']) +@jwt_required() +def delete_notification(notification_id): + """刪除通知""" + try: + current_user_id = get_jwt_identity() + + # 查找通知 + notification = Notification.query.filter_by( + notification_uuid=notification_id, + user_id=current_user_id + ).first() + + if not notification: + return jsonify(create_taiwan_response( + success=False, + error='通知不存在' + )), 404 + + # 刪除通知 + db.session.delete(notification) + db.session.commit() + + return jsonify(create_taiwan_response( + success=True, + message='刪除通知成功' + )) + + except Exception as e: + db.session.rollback() + return jsonify(create_taiwan_response( + success=False, + error=f'刪除通知失敗:{str(e)}' + )), 500 + + +@notification_bp.route('/clear', methods=['POST']) +@jwt_required() +def clear_read_notifications(): + """清空所有已讀通知""" + try: + current_user_id = get_jwt_identity() + + # 刪除所有已讀通知 + deleted_count = Notification.query.filter_by( + user_id=current_user_id, + is_read=True + ).delete() + + db.session.commit() + + return jsonify(create_taiwan_response( + success=True, + data={'deleted_count': deleted_count}, + message=f'已清除 {deleted_count} 個已讀通知' + )) + + except Exception as e: + db.session.rollback() + return jsonify(create_taiwan_response( + success=False, + error=f'清除通知失敗:{str(e)}' + )), 500 + + +@notification_bp.route('/test', methods=['POST']) +@jwt_required() +def create_test_notification(): + """創建測試通知(開發用)""" + try: + current_user_id = get_jwt_identity() + + # 創建測試通知 + test_notification = create_notification( + user_id=current_user_id, + title="測試通知", + message=f"這是一個測試通知,創建於 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + notification_type=NotificationType.INFO + ) + + return jsonify(create_taiwan_response( + success=True, + data=test_notification.to_dict(), + message='測試通知已創建' + )) + + except Exception as e: + return jsonify(create_taiwan_response( + success=False, + error=f'創建測試通知失敗:{str(e)}' + )), 500 + + +# 工具函數:創建通知 +def create_notification(user_id, title, message, notification_type=NotificationType.INFO, + job_uuid=None, extra_data=None): + """ + 創建通知的工具函數 + + Args: + user_id: 用戶ID + title: 通知標題 + message: 通知內容 + notification_type: 通知類型 + job_uuid: 關聯的任務UUID(可選) + extra_data: 額外數據(可選) + + Returns: + Notification: 創建的通知對象 + """ + try: + notification = Notification( + user_id=user_id, + type=notification_type.value, + title=title, + message=message, + job_uuid=job_uuid, + extra_data=extra_data, + link=f"/job/{job_uuid}" if job_uuid else None + ) + + db.session.add(notification) + db.session.commit() + + return notification + + except Exception as e: + db.session.rollback() + raise e \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py index a9f41b2..2113fb4 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -13,6 +13,7 @@ from .job import TranslationJob, JobFile from .cache import TranslationCache from .stats import APIUsageStats from .log import SystemLog +from .notification import Notification, NotificationType __all__ = [ 'User', @@ -20,5 +21,7 @@ __all__ = [ 'JobFile', 'TranslationCache', 'APIUsageStats', - 'SystemLog' + 'SystemLog', + 'Notification', + 'NotificationType' ] \ No newline at end of file diff --git a/app/models/notification.py b/app/models/notification.py new file mode 100644 index 0000000..5163dee --- /dev/null +++ b/app/models/notification.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +通知系統資料模型 + +Author: PANJIT IT Team +Created: 2024-01-28 +Modified: 2024-01-28 +""" +from datetime import datetime +from enum import Enum +from sqlalchemy import func +from sqlalchemy.orm import relationship +from app import db +import uuid +import json + + +class NotificationType(str, Enum): + """通知類型枚舉""" + SUCCESS = "success" # 成功 + ERROR = "error" # 錯誤 + WARNING = "warning" # 警告 + INFO = "info" # 資訊 + SYSTEM = "system" # 系統 + + +class Notification(db.Model): + """通知模型""" + __tablename__ = 'dt_notifications' + + # 主鍵 + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + notification_uuid = db.Column(db.String(36), unique=True, nullable=False, index=True, + default=lambda: str(uuid.uuid4()), comment='通知唯一識別碼') + + # 基本資訊 + user_id = db.Column(db.Integer, db.ForeignKey('dt_users.id'), nullable=False, comment='使用者ID') + type = db.Column(db.String(20), nullable=False, default=NotificationType.INFO.value, comment='通知類型') + title = db.Column(db.String(255), nullable=False, comment='通知標題') + message = db.Column(db.Text, nullable=False, comment='通知內容') + + # 關聯資訊(可選) + job_uuid = db.Column(db.String(36), nullable=True, comment='關聯任務UUID') + link = db.Column(db.String(500), nullable=True, comment='相關連結') + + # 狀態 + is_read = db.Column(db.Boolean, default=False, nullable=False, comment='是否已讀') + read_at = db.Column(db.DateTime, nullable=True, comment='閱讀時間') + + # 時間戳記 + created_at = db.Column(db.DateTime, default=func.now(), nullable=False, comment='建立時間') + expires_at = db.Column(db.DateTime, nullable=True, comment='過期時間') + + # 額外數據(JSON 格式儲存) + extra_data = db.Column(db.JSON, nullable=True, comment='額外數據') + + # 關聯 + user = db.relationship("User", backref="notifications") + + def __repr__(self): + return f"" + + def to_dict(self): + """轉換為字典格式""" + return { + 'id': self.notification_uuid, # 前端使用 UUID + 'user_id': self.user_id, + 'type': self.type, + 'title': self.title, + 'message': self.message, + 'job_uuid': self.job_uuid, + 'link': self.link, + 'is_read': self.is_read, + 'read': self.is_read, # 為了前端相容 + 'read_at': self.read_at.isoformat() if self.read_at else None, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'expires_at': self.expires_at.isoformat() if self.expires_at else None, + 'extra_data': self.extra_data + } + + def mark_as_read(self): + """標記為已讀""" + self.is_read = True + self.read_at = datetime.now() + + @classmethod + def create_job_notification(cls, user_id, job_uuid, title, message, notification_type=NotificationType.INFO): + """創建任務相關通知""" + return cls( + user_id=user_id, + job_uuid=job_uuid, + type=notification_type.value, + title=title, + message=message, + link=f"/job/{job_uuid}" # 連結到任務詳情頁 + ) \ No newline at end of file diff --git a/app/services/notification_service.py b/app/services/notification_service.py index 8453c67..a32d8e1 100644 --- a/app/services/notification_service.py +++ b/app/services/notification_service.py @@ -12,12 +12,14 @@ import os import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart -from datetime import datetime -from typing import Optional, List +from datetime import datetime, timedelta +from typing import Optional, List, Dict, Any from flask import current_app, url_for +from app import db from app.utils.logger import get_logger from app.models.job import TranslationJob from app.models.user import User +from app.models.notification import Notification, NotificationType logger = get_logger(__name__) @@ -385,4 +387,224 @@ class NotificationService: return False except Exception as e: logger.error(f"SMTP connection test failed: {str(e)}") - return False \ No newline at end of file + return False + + # ========== 資料庫通知方法 ========== + + def create_db_notification( + self, + user_id: int, + title: str, + message: str, + notification_type: NotificationType = NotificationType.INFO, + job_uuid: Optional[str] = None, + extra_data: Optional[Dict[str, Any]] = None, + expires_at: Optional[datetime] = None, + link: Optional[str] = None + ) -> Optional[Notification]: + """ + 創建資料庫通知 + + Args: + user_id: 用戶ID + title: 通知標題 + message: 通知內容 + notification_type: 通知類型 + job_uuid: 關聯任務UUID + extra_data: 額外數據 + expires_at: 過期時間 + link: 相關連結 + + Returns: + Notification: 創建的通知對象 + """ + try: + # 如果沒有指定連結但有任務UUID,自動生成任務詳情連結 + if not link and job_uuid: + link = f"/job/{job_uuid}" + + notification = Notification( + user_id=user_id, + type=notification_type.value, + title=title, + message=message, + job_uuid=job_uuid, + link=link, + extra_data=extra_data, + expires_at=expires_at + ) + + db.session.add(notification) + db.session.commit() + + logger.info(f"資料庫通知已創建: {notification.notification_uuid} for user {user_id}") + + # 觸發 WebSocket 推送 + self._send_websocket_notification(notification) + + return notification + + except Exception as e: + db.session.rollback() + logger.error(f"創建資料庫通知失敗: {e}") + return None + + def send_job_started_db_notification(self, job: TranslationJob) -> Optional[Notification]: + """ + 發送任務開始處理的資料庫通知 + + Args: + job: 翻譯任務對象 + + Returns: + Notification: 創建的通知對象 + """ + try: + title = "翻譯任務開始處理" + message = f'您的文件「{job.original_filename}」已開始翻譯處理。' + + if job.target_languages: + languages = ', '.join(job.target_languages) + message += f" 目標語言: {languages}" + + return self.create_db_notification( + user_id=job.user_id, + title=title, + message=message, + notification_type=NotificationType.INFO, + job_uuid=job.job_uuid, + extra_data={ + 'filename': job.original_filename, + 'target_languages': job.target_languages, + 'started_at': job.processing_started_at.isoformat() if job.processing_started_at else None + } + ) + + except Exception as e: + logger.error(f"發送任務開始資料庫通知失敗: {e}") + return None + + def send_job_completion_db_notification(self, job: TranslationJob) -> Optional[Notification]: + """ + 發送任務完成的資料庫通知 + + Args: + job: 翻譯任務對象 + + Returns: + Notification: 創建的通知對象 + """ + try: + if job.status != 'COMPLETED': + logger.warning(f"任務 {job.job_uuid} 狀態不是已完成,跳過完成通知") + return None + + # 構建通知內容 + title = "翻譯任務完成" + message = f'您的文件「{job.original_filename}」已成功翻譯完成。' + + # 添加目標語言信息 + if job.target_languages: + languages = ', '.join(job.target_languages) + message += f" 目標語言: {languages}" + + # 添加處理時間信息 + if job.processing_started_at and job.completed_at: + duration = job.completed_at - job.processing_started_at + minutes = int(duration.total_seconds() / 60) + if minutes > 0: + message += f" 處理時間: {minutes} 分鐘" + else: + message += f" 處理時間: {int(duration.total_seconds())} 秒" + + return self.create_db_notification( + user_id=job.user_id, + title=title, + message=message, + notification_type=NotificationType.SUCCESS, + job_uuid=job.job_uuid, + extra_data={ + 'filename': job.original_filename, + 'target_languages': job.target_languages, + 'total_cost': float(job.total_cost) if job.total_cost else 0, + 'completed_at': job.completed_at.isoformat() if job.completed_at else None + } + ) + + except Exception as e: + logger.error(f"發送任務完成資料庫通知失敗: {e}") + return None + + def send_job_failure_db_notification(self, job: TranslationJob, error_message: str = None) -> Optional[Notification]: + """ + 發送任務失敗的資料庫通知 + + Args: + job: 翻譯任務對象 + error_message: 錯誤訊息 + + Returns: + Notification: 創建的通知對象 + """ + try: + title = "翻譯任務失敗" + message = f'您的文件「{job.original_filename}」翻譯失敗。' + + if error_message: + message += f" 錯誤訊息: {error_message}" + + if job.retry_count > 0: + message += f" 已重試 {job.retry_count} 次。" + + return self.create_db_notification( + user_id=job.user_id, + title=title, + message=message, + notification_type=NotificationType.ERROR, + job_uuid=job.job_uuid, + extra_data={ + 'filename': job.original_filename, + 'error_message': error_message, + 'retry_count': job.retry_count, + 'failed_at': datetime.now().isoformat() + } + ) + + except Exception as e: + logger.error(f"發送任務失敗資料庫通知失敗: {e}") + return None + + def _send_websocket_notification(self, notification: Notification): + """ + 通過 WebSocket 發送通知 + + Args: + notification: 通知對象 + """ + try: + from app.websocket import send_notification_to_user + send_notification_to_user(notification.user_id, notification.to_dict()) + except Exception as e: + logger.error(f"WebSocket 推送通知失敗: {e}") + + def get_unread_count(self, user_id: int) -> int: + """ + 獲取用戶未讀通知數量 + + Args: + user_id: 用戶ID + + Returns: + int: 未讀通知數量 + """ + try: + return Notification.query.filter_by( + user_id=user_id, + is_read=False + ).filter( + (Notification.expires_at.is_(None)) | + (Notification.expires_at > datetime.now()) + ).count() + except Exception as e: + logger.error(f"獲取未讀通知數量失敗: {e}") + return 0 \ No newline at end of file diff --git a/app/tasks/translation.py b/app/tasks/translation.py index 979b1e8..74f9a03 100644 --- a/app/tasks/translation.py +++ b/app/tasks/translation.py @@ -76,7 +76,10 @@ def process_translation_job(self, job_id: int): # 發送完成通知 try: notification_service = NotificationService() + # 發送郵件通知 notification_service.send_job_completion_notification(job) + # 發送資料庫通知 + notification_service.send_job_completion_db_notification(job) except Exception as e: logger.warning(f"Failed to send completion notification: {str(e)}") @@ -131,6 +134,16 @@ def process_translation_job(self, job_id: int): # 重試次數用盡,標記失敗 job.update_status('FAILED') + # 發送失敗通知 + try: + notification_service = NotificationService() + # 發送郵件通知 + notification_service.send_job_failure_notification(job) + # 發送資料庫通知 + notification_service.send_job_failure_db_notification(job, str(exc)) + except Exception as e: + logger.warning(f"Failed to send failure notification: {str(e)}") + SystemLog.error( 'tasks.translation', f'Translation job failed permanently: {job.job_uuid}', diff --git a/app/websocket.py b/app/websocket.py new file mode 100644 index 0000000..48aebea --- /dev/null +++ b/app/websocket.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +WebSocket 服務模組 + +Author: PANJIT IT Team +Created: 2024-01-28 +Modified: 2024-01-28 +""" + +from flask_socketio import SocketIO, emit, join_room, leave_room, disconnect +from flask_jwt_extended import decode_token, get_jwt +from flask import request +from functools import wraps +import logging + +# 初始化 SocketIO +socketio = SocketIO( + cors_allowed_origins="*", + async_mode='threading', + logger=True, + engineio_logger=False +) + +# 存儲用戶連接 +connected_users = {} + +logger = logging.getLogger(__name__) + + +def jwt_required_ws(f): + """WebSocket JWT 驗證裝飾器""" + @wraps(f) + def decorated_function(*args, **kwargs): + try: + # 從查詢參數獲取 token + token = request.args.get('token') + if not token: + disconnect() + return False + + # 解碼 token + decoded = decode_token(token) + user_id = decoded.get('sub') + + # 儲存用戶信息 + request.user_id = user_id + + return f(*args, **kwargs) + + except Exception as e: + logger.error(f"WebSocket authentication failed: {e}") + disconnect() + return False + + return decorated_function + + +@socketio.on('connect') +def handle_connect(auth): + """處理客戶端連接""" + try: + # 從認證數據獲取 token + if auth and 'token' in auth: + token = auth['token'] + decoded = decode_token(token) + user_id = decoded.get('sub') + + # 記錄連接 + connected_users[request.sid] = { + 'user_id': user_id, + 'sid': request.sid + } + + # 加入用戶專屬房間 + join_room(f"user_{user_id}") + + logger.info(f"User {user_id} connected with session {request.sid}") + + # 發送連接成功消息 + emit('connected', { + 'message': '連接成功', + 'user_id': user_id + }) + + return True + else: + logger.warning("Connection attempt without authentication") + disconnect() + return False + + except Exception as e: + logger.error(f"Connection error: {e}") + disconnect() + return False + + +@socketio.on('disconnect') +def handle_disconnect(): + """處理客戶端斷開連接""" + try: + if request.sid in connected_users: + user_info = connected_users[request.sid] + user_id = user_info['user_id'] + + # 離開房間 + leave_room(f"user_{user_id}") + + # 移除連接記錄 + del connected_users[request.sid] + + logger.info(f"User {user_id} disconnected") + + except Exception as e: + logger.error(f"Disconnect error: {e}") + + +@socketio.on('ping') +def handle_ping(): + """處理心跳包""" + emit('pong', {'timestamp': request.args.get('timestamp')}) + + +@socketio.on('subscribe_job') +def handle_subscribe_job(data): + """訂閱任務更新""" + try: + job_uuid = data.get('job_uuid') + if job_uuid: + join_room(f"job_{job_uuid}") + logger.info(f"Client {request.sid} subscribed to job {job_uuid}") + emit('subscribed', {'job_uuid': job_uuid}) + except Exception as e: + logger.error(f"Subscribe job error: {e}") + + +@socketio.on('unsubscribe_job') +def handle_unsubscribe_job(data): + """取消訂閱任務更新""" + try: + job_uuid = data.get('job_uuid') + if job_uuid: + leave_room(f"job_{job_uuid}") + logger.info(f"Client {request.sid} unsubscribed from job {job_uuid}") + emit('unsubscribed', {'job_uuid': job_uuid}) + except Exception as e: + logger.error(f"Unsubscribe job error: {e}") + + +# 工具函數:發送通知 +def send_notification_to_user(user_id, notification_data): + """ + 向特定用戶發送通知 + + Args: + user_id: 用戶ID + notification_data: 通知數據 + """ + try: + socketio.emit( + 'new_notification', + notification_data, + room=f"user_{user_id}", + namespace='/' + ) + logger.info(f"Notification sent to user {user_id}") + + except Exception as e: + logger.error(f"Failed to send notification: {e}") + + +def send_job_update(job_uuid, update_data): + """ + 發送任務更新 + + Args: + job_uuid: 任務UUID + update_data: 更新數據 + """ + try: + socketio.emit( + 'job_update', + { + 'job_uuid': job_uuid, + **update_data + }, + room=f"job_{job_uuid}", + namespace='/' + ) + logger.info(f"Job update sent for {job_uuid}") + + except Exception as e: + logger.error(f"Failed to send job update: {e}") + + +def broadcast_system_message(message, message_type='info'): + """ + 廣播系統消息給所有連接的用戶 + + Args: + message: 消息內容 + message_type: 消息類型 + """ + try: + socketio.emit( + 'system_message', + { + 'message': message, + 'type': message_type + }, + namespace='/', + broadcast=True + ) + logger.info(f"System message broadcasted: {message}") + + except Exception as e: + logger.error(f"Failed to broadcast system message: {e}") + + +# 初始化函數 +def init_websocket(app): + """ + 初始化 WebSocket + + Args: + app: Flask 應用實例 + """ + socketio.init_app(app) + logger.info("WebSocket initialized") + return socketio \ No newline at end of file diff --git a/create_tables.py b/create_tables.py index 606536d..5ffeb9e 100644 --- a/create_tables.py +++ b/create_tables.py @@ -127,6 +127,31 @@ def create_tables(): FOREIGN KEY (user_id) REFERENCES dt_users(id) ON DELETE SET NULL, FOREIGN KEY (job_id) REFERENCES dt_translation_jobs(id) ON DELETE SET NULL ) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + ''', + + 'dt_notifications': ''' + CREATE TABLE IF NOT EXISTS dt_notifications ( + id INT PRIMARY KEY AUTO_INCREMENT, + notification_uuid VARCHAR(36) NOT NULL UNIQUE COMMENT '通知唯一識別碼', + user_id INT NOT NULL COMMENT '使用者ID', + type VARCHAR(20) NOT NULL DEFAULT 'info' COMMENT '通知類型', + title VARCHAR(255) NOT NULL COMMENT '通知標題', + message TEXT NOT NULL COMMENT '通知內容', + job_uuid VARCHAR(36) NULL COMMENT '關聯任務UUID', + link VARCHAR(500) NULL COMMENT '相關連結', + is_read BOOLEAN DEFAULT FALSE NOT NULL COMMENT '是否已讀', + read_at TIMESTAMP NULL COMMENT '閱讀時間', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '建立時間', + expires_at TIMESTAMP NULL COMMENT '過期時間', + extra_data JSON NULL COMMENT '額外數據', + INDEX idx_notification_uuid (notification_uuid), + INDEX idx_user_id (user_id), + INDEX idx_job_uuid (job_uuid), + INDEX idx_is_read (is_read), + INDEX idx_created_at (created_at), + FOREIGN KEY (user_id) REFERENCES dt_users(id) ON DELETE CASCADE, + FOREIGN KEY (job_uuid) REFERENCES dt_translation_jobs(job_uuid) ON DELETE SET NULL + ) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ''' } diff --git a/create_test_data.py b/create_test_data.py new file mode 100644 index 0000000..b6681c8 --- /dev/null +++ b/create_test_data.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +創建測試數據 +""" + +from app import create_app, db +from app.models import Notification, NotificationType +from datetime import datetime + +def create_test_notification(): + """創建測試通知""" + try: + app = create_app() + with app.app_context(): + print("Creating test notification...") + + # 創建測試通知 + test_notification = Notification( + user_id=4, # ymirliu 用戶 + type=NotificationType.INFO.value, + title='測試通知', + message='這是一個測試通知,用來驗證通知系統是否正常工作。', + extra_data={ + 'test_data': True, + 'created_by': 'test_script' + } + ) + + db.session.add(test_notification) + db.session.commit() + + print(f"Test notification created: {test_notification.notification_uuid}") + print(f"Total notifications in database: {Notification.query.count()}") + + # 顯示所有通知 + notifications = Notification.query.all() + for notification in notifications: + print(f" - {notification.title} ({notification.type})") + + except Exception as e: + print(f"Error creating test notification: {e}") + import traceback + traceback.print_exc() + +if __name__ == '__main__': + create_test_notification() \ No newline at end of file diff --git a/fix_notification_table.py b/fix_notification_table.py new file mode 100644 index 0000000..13f47ce --- /dev/null +++ b/fix_notification_table.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +修正通知表結構腳本 +""" + +import pymysql +import os +from dotenv import load_dotenv + +# 載入環境變數 +load_dotenv('C:/Users/EGG/WORK/data/user_scrip/TOOL/env.txt') + +def fix_notification_table(): + """修正通知表的欄位名稱""" + try: + # 連接資料庫 + connection = pymysql.connect( + host=os.getenv('DB_HOST', 'localhost'), + user=os.getenv('DB_USER', 'root'), + password=os.getenv('DB_PASSWORD', ''), + database=os.getenv('DB_NAME', 'doc_translator'), + charset='utf8mb4' + ) + + with connection.cursor() as cursor: + # 檢查 dt_notifications 表結構 + cursor.execute('DESCRIBE dt_notifications') + columns = cursor.fetchall() + + print('Current table structure:') + for col in columns: + print(f' {col[0]} {col[1]}') + + # 檢查是否有 metadata 欄位 + has_metadata = any(col[0] == 'metadata' for col in columns) + has_extra_data = any(col[0] == 'extra_data' for col in columns) + + print(f'\nHas metadata column: {has_metadata}') + print(f'Has extra_data column: {has_extra_data}') + + if has_metadata and not has_extra_data: + print('\nRenaming metadata column to extra_data...') + cursor.execute('ALTER TABLE dt_notifications CHANGE metadata extra_data JSON NULL COMMENT "額外數據"') + connection.commit() + print('✅ Column renamed successfully') + + # 再次檢查結構 + cursor.execute('DESCRIBE dt_notifications') + columns = cursor.fetchall() + print('\nUpdated table structure:') + for col in columns: + print(f' {col[0]} {col[1]}') + + elif has_extra_data: + print('✅ extra_data column already exists') + else: + print('❌ Neither metadata nor extra_data column found') + + connection.close() + print('\n✅ Database structure check completed') + + except Exception as e: + print(f'❌ Error fixing notification table: {e}') + import traceback + traceback.print_exc() + +if __name__ == '__main__': + fix_notification_table() \ No newline at end of file diff --git a/fix_table_simple.py b/fix_table_simple.py new file mode 100644 index 0000000..0d5b806 --- /dev/null +++ b/fix_table_simple.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +修正通知表腳本 +""" + +from app import create_app, db +from sqlalchemy import text + +def fix_notification_table(): + """修正通知表""" + try: + app = create_app() + with app.app_context(): + print("Fixing notification table...") + + # 刪除通知表(如果存在) + try: + db.session.execute(text('DROP TABLE IF EXISTS dt_notifications')) + db.session.commit() + print("Old notification table dropped") + except Exception as e: + print(f"Info: {e}") + + # 重新創建通知表 + db.create_all() + print("New notification table created with correct structure") + + # 檢查表結構 + result = db.session.execute(text('DESCRIBE dt_notifications')) + columns = result.fetchall() + print("New table structure:") + for col in columns: + print(f" {col[0]} {col[1]}") + + except Exception as e: + print(f"Error fixing notification table: {e}") + import traceback + traceback.print_exc() + +if __name__ == '__main__': + fix_notification_table() \ No newline at end of file diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue index 5ee6d4e..90759a0 100644 --- a/frontend/src/layouts/MainLayout.vue +++ b/frontend/src/layouts/MainLayout.vue @@ -167,11 +167,12 @@ import { computed, ref, onMounted, onUnmounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useAuthStore } from '@/stores/auth' +import { useNotificationStore } from '@/stores/notification' import { ElMessage, ElMessageBox } from 'element-plus' import { Document, House, Upload, List, Clock, User, Setting, Bell, Menu, Fold, Expand, SwitchButton, SuccessFilled, WarningFilled, - CircleCloseFilled, InfoFilled + CircleCloseFilled, InfoFilled, Refresh } from '@element-plus/icons-vue' import { initWebSocket, cleanupWebSocket } from '@/utils/websocket' @@ -179,13 +180,16 @@ import { initWebSocket, cleanupWebSocket } from '@/utils/websocket' const route = useRoute() const router = useRouter() const authStore = useAuthStore() +const notificationStore = useNotificationStore() // 響應式數據 const sidebarCollapsed = ref(false) const mobileSidebarVisible = ref(false) const notificationDrawerVisible = ref(false) -const notifications = ref([]) -const unreadCount = ref(0) + +// 從 store 獲取通知相關數據 +const notifications = computed(() => notificationStore.notifications) +const unreadCount = computed(() => notificationStore.unreadCount) // 計算屬性 const currentRoute = computed(() => route) @@ -239,10 +243,10 @@ const handleMenuClick = () => { } } -const showNotifications = () => { +const showNotifications = async () => { notificationDrawerVisible.value = true - // 可以在這裡載入最新通知 - loadNotifications() + // 載入最新通知 + await notificationStore.fetchNotifications() } const handleUserMenuCommand = async (command) => { @@ -270,50 +274,22 @@ const handleUserMenuCommand = async (command) => { } } -const loadNotifications = async () => { +const markAsRead = async (notificationId) => { try { - // 這裡應該從 API 載入通知,目前使用模擬數據 - notifications.value = [ - { - id: 1, - type: 'success', - title: '翻譯完成', - message: '檔案「文件.docx」翻譯完成', - created_at: new Date().toISOString(), - read: false - }, - { - id: 2, - type: 'warning', - title: '系統維護通知', - message: '系統將於今晚 23:00 進行維護', - created_at: new Date(Date.now() - 3600000).toISOString(), - read: true - } - ] - - unreadCount.value = notifications.value.filter(n => !n.read).length + await notificationStore.markAsRead(notificationId) } catch (error) { - console.error('載入通知失敗:', error) + console.error('標記已讀失敗:', error) } } -const markAsRead = (notificationId) => { - const notification = notifications.value.find(n => n.id === notificationId) - if (notification) { - notification.read = true - unreadCount.value = notifications.value.filter(n => !n.read).length +const markAllAsRead = async () => { + try { + await notificationStore.markAllAsRead() + } catch (error) { + console.error('標記全部已讀失敗:', error) } } -const markAllAsRead = () => { - notifications.value.forEach(notification => { - notification.read = true - }) - unreadCount.value = 0 - ElMessage.success('所有通知已標記為已讀') -} - const getNotificationIcon = (type) => { const iconMap = { success: 'SuccessFilled', @@ -355,7 +331,7 @@ onMounted(() => { // initWebSocket() // 載入通知 - loadNotifications() + notificationStore.fetchNotifications() // 監聽窗口大小變化 window.addEventListener('resize', handleResize) diff --git a/frontend/src/services/notification.js b/frontend/src/services/notification.js new file mode 100644 index 0000000..fdf2f8c --- /dev/null +++ b/frontend/src/services/notification.js @@ -0,0 +1,63 @@ +import { request } from '@/utils/request' + +/** + * 通知相關 API 服務 + */ +export const notificationAPI = { + /** + * 獲取通知列表 + * @param {Object} params - 查詢參數 + * @param {number} params.page - 頁碼 + * @param {number} params.per_page - 每頁數量 + * @param {string} params.status - 狀態過濾 ('all', 'unread', 'read') + * @param {string} params.type - 類型過濾 + */ + getNotifications(params = {}) { + return request.get('/notifications', { params }) + }, + + /** + * 獲取單個通知詳情 + * @param {string} notificationId - 通知ID + */ + getNotification(notificationId) { + return request.get(`/notifications/${notificationId}`) + }, + + /** + * 標記通知為已讀 + * @param {string} notificationId - 通知ID + */ + markAsRead(notificationId) { + return request.put(`/notifications/${notificationId}/read`) + }, + + /** + * 標記所有通知為已讀 + */ + markAllAsRead() { + return request.put('/notifications/mark-all-read') + }, + + /** + * 刪除通知 + * @param {string} notificationId - 通知ID + */ + deleteNotification(notificationId) { + return request.delete(`/notifications/${notificationId}`) + }, + + /** + * 清空所有已讀通知 + */ + clearNotifications() { + return request.delete('/notifications/clear') + }, + + /** + * 創建測試通知(開發用) + */ + createTestNotification() { + return request.post('/notifications/test') + } +} \ No newline at end of file diff --git a/frontend/src/stores/notification.js b/frontend/src/stores/notification.js new file mode 100644 index 0000000..2144dec --- /dev/null +++ b/frontend/src/stores/notification.js @@ -0,0 +1,310 @@ +import { defineStore } from 'pinia' +import { notificationAPI } from '@/services/notification' +import { ElMessage } from 'element-plus' + +export const useNotificationStore = defineStore('notification', { + state: () => ({ + notifications: [], + unreadCount: 0, + loading: false, + pagination: { + total: 0, + page: 1, + per_page: 20, + pages: 0 + } + }), + + getters: { + unreadNotifications: (state) => { + return state.notifications.filter(notification => !notification.is_read) + }, + + readNotifications: (state) => { + return state.notifications.filter(notification => notification.is_read) + }, + + hasUnreadNotifications: (state) => { + return state.unreadCount > 0 + } + }, + + actions: { + /** + * 獲取通知列表 + * @param {Object} params - 查詢參數 + */ + async fetchNotifications(params = {}) { + try { + this.loading = true + + const response = await notificationAPI.getNotifications({ + page: this.pagination.page, + per_page: this.pagination.per_page, + ...params + }) + + if (response.success) { + this.notifications = response.data.notifications + this.unreadCount = response.data.unread_count + this.pagination = { + ...this.pagination, + ...response.data.pagination + } + + console.log('📮 [Notification] 通知列表已更新', { + total: this.pagination.total, + unread: this.unreadCount + }) + } + + return response + + } catch (error) { + console.error('❌ [Notification] 獲取通知列表失敗:', error) + ElMessage.error('獲取通知失敗') + throw error + } finally { + this.loading = false + } + }, + + /** + * 標記通知為已讀 + * @param {string} notificationId - 通知ID + */ + async markAsRead(notificationId) { + try { + const response = await notificationAPI.markAsRead(notificationId) + + if (response.success) { + // 更新本地狀態 + const notification = this.notifications.find(n => n.id === notificationId) + if (notification && !notification.is_read) { + notification.is_read = true + notification.read = true + notification.read_at = new Date().toISOString() + this.unreadCount = Math.max(0, this.unreadCount - 1) + } + + console.log('✅ [Notification] 通知已標記為已讀:', notificationId) + } + + return response + + } catch (error) { + console.error('❌ [Notification] 標記已讀失敗:', error) + ElMessage.error('標記已讀失敗') + throw error + } + }, + + /** + * 標記所有通知為已讀 + */ + async markAllAsRead() { + try { + const response = await notificationAPI.markAllAsRead() + + if (response.success) { + // 更新本地狀態 + this.notifications.forEach(notification => { + if (!notification.is_read) { + notification.is_read = true + notification.read = true + notification.read_at = new Date().toISOString() + } + }) + this.unreadCount = 0 + + console.log('✅ [Notification] 所有通知已標記為已讀') + ElMessage.success(response.message || '所有通知已標記為已讀') + } + + return response + + } catch (error) { + console.error('❌ [Notification] 標記全部已讀失敗:', error) + ElMessage.error('標記全部已讀失敗') + throw error + } + }, + + /** + * 刪除通知 + * @param {string} notificationId - 通知ID + */ + async deleteNotification(notificationId) { + try { + const response = await notificationAPI.deleteNotification(notificationId) + + if (response.success) { + // 從本地狀態移除 + const index = this.notifications.findIndex(n => n.id === notificationId) + if (index !== -1) { + const notification = this.notifications[index] + if (!notification.is_read) { + this.unreadCount = Math.max(0, this.unreadCount - 1) + } + this.notifications.splice(index, 1) + this.pagination.total = Math.max(0, this.pagination.total - 1) + } + + console.log('🗑️ [Notification] 通知已刪除:', notificationId) + } + + return response + + } catch (error) { + console.error('❌ [Notification] 刪除通知失敗:', error) + ElMessage.error('刪除通知失敗') + throw error + } + }, + + /** + * 清空所有已讀通知 + */ + async clearNotifications() { + try { + const response = await notificationAPI.clearNotifications() + + if (response.success) { + // 從本地狀態移除已讀通知 + this.notifications = this.notifications.filter(n => !n.is_read) + this.pagination.total = this.notifications.length + + console.log('🧹 [Notification] 已讀通知已清除') + ElMessage.success(response.message || '已讀通知已清除') + } + + return response + + } catch (error) { + console.error('❌ [Notification] 清除通知失敗:', error) + ElMessage.error('清除通知失敗') + throw error + } + }, + + /** + * 添加新通知(用於 WebSocket 推送) + * @param {Object} notification - 通知數據 + */ + addNotification(notification) { + // 檢查是否已存在 + const exists = this.notifications.find(n => n.id === notification.id) + if (!exists) { + // 添加到列表開頭 + this.notifications.unshift(notification) + + // 更新未讀數量 + if (!notification.is_read) { + this.unreadCount += 1 + } + + // 更新總數 + this.pagination.total += 1 + + console.log('📩 [Notification] 新通知已添加:', notification.title) + + // 顯示通知 + ElMessage({ + type: this.getMessageType(notification.type), + title: notification.title, + message: notification.message, + duration: 5000 + }) + } + }, + + /** + * 更新通知 + * @param {Object} notification - 通知數據 + */ + updateNotification(notification) { + const index = this.notifications.findIndex(n => n.id === notification.id) + if (index !== -1) { + const oldNotification = this.notifications[index] + + // 更新未讀數量 + if (oldNotification.is_read !== notification.is_read) { + if (notification.is_read) { + this.unreadCount = Math.max(0, this.unreadCount - 1) + } else { + this.unreadCount += 1 + } + } + + // 更新通知 + this.notifications[index] = { ...oldNotification, ...notification } + + console.log('📝 [Notification] 通知已更新:', notification.id) + } + }, + + /** + * 創建測試通知(開發用) + */ + async createTestNotification() { + try { + const response = await notificationAPI.createTestNotification() + + if (response.success) { + // 重新獲取通知列表 + await this.fetchNotifications() + ElMessage.success('測試通知已創建') + } + + return response + + } catch (error) { + console.error('❌ [Notification] 創建測試通知失敗:', error) + ElMessage.error('創建測試通知失敗') + throw error + } + }, + + /** + * 設置分頁 + * @param {number} page - 頁碼 + * @param {number} per_page - 每頁數量 + */ + setPagination(page, per_page) { + this.pagination.page = page + if (per_page) { + this.pagination.per_page = per_page + } + }, + + /** + * 重置狀態 + */ + reset() { + this.notifications = [] + this.unreadCount = 0 + this.loading = false + this.pagination = { + total: 0, + page: 1, + per_page: 20, + pages: 0 + } + }, + + /** + * 獲取 ElMessage 類型 + * @param {string} type - 通知類型 + */ + getMessageType(type) { + const typeMap = { + 'success': 'success', + 'error': 'error', + 'warning': 'warning', + 'info': 'info', + 'system': 'info' + } + return typeMap[type] || 'info' + } + } +}) \ No newline at end of file diff --git a/frontend/src/utils/websocket.js b/frontend/src/utils/websocket.js index 8b64d6a..6763845 100644 --- a/frontend/src/utils/websocket.js +++ b/frontend/src/utils/websocket.js @@ -1,5 +1,6 @@ import { io } from 'socket.io-client' import { useJobsStore } from '@/stores/jobs' +import { useNotificationStore } from '@/stores/notification' import { ElMessage, ElNotification } from 'element-plus' /** @@ -93,6 +94,16 @@ class WebSocketService { this.socket.on('system_notification', (data) => { this.handleSystemNotification(data) }) + + // 新通知推送 + this.socket.on('new_notification', (data) => { + this.handleNewNotification(data) + }) + + // 系統消息 + this.socket.on('system_message', (data) => { + this.handleSystemMessage(data) + }) // 連接狀態回應 this.socket.on('connected', (data) => { @@ -177,6 +188,89 @@ class WebSocketService { } } + /** + * 處理新通知推送 + * @param {Object} data - 通知資料 + */ + handleNewNotification(data) { + try { + console.log('📩 [WebSocket] 收到新通知:', data) + + const notificationStore = useNotificationStore() + + // 添加通知到 store + notificationStore.addNotification(data) + + // 顯示桌面通知 + this.showDesktopNotification(data) + + } catch (error) { + console.error('處理新通知失敗:', error) + } + } + + /** + * 處理系統消息 + * @param {Object} data - 系統消息資料 + */ + handleSystemMessage(data) { + try { + console.log('📢 [WebSocket] 收到系統消息:', data) + + const { message, type } = data + + // 顯示系統消息 + const messageType = type || 'info' + ElMessage({ + type: messageType === 'system' ? 'info' : messageType, + message: message, + duration: 5000, + showClose: true + }) + + } catch (error) { + console.error('處理系統消息失敗:', error) + } + } + + /** + * 顯示桌面通知 + * @param {Object} notification - 通知資料 + */ + showDesktopNotification(notification) { + try { + // 檢查瀏覽器是否支援通知 + if (!('Notification' in window)) { + return + } + + // 檢查通知權限 + if (Notification.permission === 'granted') { + new Notification(notification.title, { + body: notification.message, + icon: '/panjit-logo.png', + tag: notification.id, + requireInteraction: false + }) + } else if (Notification.permission !== 'denied') { + // 請求通知權限 + Notification.requestPermission().then(permission => { + if (permission === 'granted') { + new Notification(notification.title, { + body: notification.message, + icon: '/panjit-logo.png', + tag: notification.id, + requireInteraction: false + }) + } + }) + } + + } catch (error) { + console.error('顯示桌面通知失敗:', error) + } + } + /** * 訂閱任務狀態更新 * @param {string} jobUuid - 任務 UUID diff --git a/recreate_notification_table.py b/recreate_notification_table.py new file mode 100644 index 0000000..e5ddf70 --- /dev/null +++ b/recreate_notification_table.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +重新創建通知表腳本 +""" + +from app import create_app, db +from app.models import Notification + +def recreate_notification_table(): + """重新創建通知表""" + try: + app = create_app() + with app.app_context(): + print("Recreating notification table...") + + # 刪除通知表(如果存在) + try: + db.engine.execute('DROP TABLE IF EXISTS dt_notifications') + print("✅ Old notification table dropped") + except Exception as e: + print(f"Info: {e}") + + # 重新創建通知表 + db.create_all() + print("✅ New notification table created with correct structure") + + # 檢查表結構 + result = db.engine.execute('DESCRIBE dt_notifications') + columns = result.fetchall() + print("\nNew table structure:") + for col in columns: + print(f" {col[0]} {col[1]}") + + except Exception as e: + print(f"Error recreating notification table: {e}") + import traceback + traceback.print_exc() + +if __name__ == '__main__': + recreate_notification_table() \ No newline at end of file diff --git a/test_notification_api.py b/test_notification_api.py new file mode 100644 index 0000000..ded100c --- /dev/null +++ b/test_notification_api.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +測試通知 API +""" + +import requests +import json + +# API 基礎 URL +BASE_URL = 'http://127.0.0.1:5000/api/v1' + +def test_notification_api(): + """測試通知 API 端點""" + + # 首先需要登入獲取 JWT Token + # 這裡使用預設的管理員帳號 + login_data = { + 'username': 'ymirliu', + 'password': 'password123' # LDAP 系統預設密碼 + } + + try: + print("Testing notification API...") + + # 登入 + print("1. Testing login...") + login_response = requests.post(f'{BASE_URL}/auth/login', json=login_data, timeout=10) + print(f"Login status: {login_response.status_code}") + + if login_response.status_code == 200: + token = login_response.json()['data']['access_token'] + headers = {'Authorization': f'Bearer {token}'} + + # 測試獲取通知列表 + print("2. Testing get notifications...") + notifications_response = requests.get(f'{BASE_URL}/notifications', headers=headers, timeout=10) + print(f"Get notifications status: {notifications_response.status_code}") + if notifications_response.status_code == 200: + data = notifications_response.json() + print(f"Response: {json.dumps(data, indent=2, ensure_ascii=False)}") + else: + print(f"Error response: {notifications_response.text}") + + # 測試創建測試通知 + print("3. Testing create test notification...") + test_notification_response = requests.post(f'{BASE_URL}/notifications/test', headers=headers, timeout=10) + print(f"Create test notification status: {test_notification_response.status_code}") + if test_notification_response.status_code == 200: + data = test_notification_response.json() + print(f"Test notification created: {json.dumps(data, indent=2, ensure_ascii=False)}") + else: + print(f"Error response: {test_notification_response.text}") + + # 再次獲取通知列表,應該能看到測試通知 + print("4. Testing get notifications again...") + notifications_response = requests.get(f'{BASE_URL}/notifications', headers=headers, timeout=10) + print(f"Get notifications status: {notifications_response.status_code}") + if notifications_response.status_code == 200: + data = notifications_response.json() + print(f"Updated notifications: {json.dumps(data, indent=2, ensure_ascii=False)}") + else: + print(f"Error response: {notifications_response.text}") + + else: + print(f"Login failed: {login_response.text}") + + except requests.exceptions.ConnectionError: + print("Error: Could not connect to server. Make sure the Flask app is running on http://127.0.0.1:5000") + except Exception as e: + print(f"Error testing notification API: {e}") + +if __name__ == '__main__': + test_notification_api() \ No newline at end of file diff --git a/test_routes.py b/test_routes.py new file mode 100644 index 0000000..c64a2a4 --- /dev/null +++ b/test_routes.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +測試路由註冊 +""" + +import requests +import json + +BASE_URL = 'http://127.0.0.1:5000' + +def test_routes(): + """測試各種路由是否正確註冊""" + routes_to_test = [ + '/api/v1/auth/health', + '/api/v1/notifications', + '/api/v1/jobs', + '/api/v1/health' + ] + + print("Testing route registration...") + + for route in routes_to_test: + try: + url = f"{BASE_URL}{route}" + response = requests.get(url, timeout=5) + + if response.status_code == 404: + print(f"❌ {route} -> 404 NOT FOUND") + elif response.status_code == 401: + print(f"✅ {route} -> 401 UNAUTHORIZED (route exists, needs auth)") + elif response.status_code == 200: + print(f"✅ {route} -> 200 OK") + else: + print(f"🟡 {route} -> {response.status_code} {response.reason}") + + except requests.exceptions.ConnectionError: + print(f"❌ {route} -> CONNECTION ERROR") + except Exception as e: + print(f"❌ {route} -> ERROR: {e}") + +if __name__ == '__main__': + test_routes() \ No newline at end of file