From e5fd3e5ec31c56d937b8d5703880f62b52880db1 Mon Sep 17 00:00:00 2001 From: beabigegg Date: Thu, 4 Sep 2025 10:21:16 +0800 Subject: [PATCH] 13th_fix combine download --- app/api/files.py | 82 ++++++++++++++++++++++++++++ app/services/notification_service.py | 35 ++++++++++++ app/services/translation_service.py | 2 +- app/tasks/translation.py | 7 ++- frontend/src/layouts/MainLayout.vue | 6 +- frontend/src/services/jobs.js | 10 ++++ frontend/src/stores/jobs.js | 8 ++- frontend/src/utils/websocket.js | 17 ++---- frontend/src/views/JobDetailView.vue | 54 ++++++++++++++---- test_notification_send.py | 76 ++++++++++++++++++++++++++ 10 files changed, 268 insertions(+), 29 deletions(-) create mode 100644 test_notification_send.py diff --git a/app/api/files.py b/app/api/files.py index 6f1949b..f0630a0 100644 --- a/app/api/files.py +++ b/app/api/files.py @@ -606,4 +606,86 @@ def download_batch_files(job_uuid): success=False, error='SYSTEM_ERROR', message='批量下載失敗' + )), 500 + + +@files_bp.route('//download/combine', methods=['GET']) +@jwt_login_required +def download_combine_file(job_uuid): + """下載合併檔案""" + try: + # 驗證 UUID 格式 + validate_job_uuid(job_uuid) + + # 取得當前用戶 + current_user_id = g.current_user_id + + # 查找任務 + job = TranslationJob.query.filter_by( + job_uuid=job_uuid, + user_id=current_user_id + ).first() + + if not job: + return jsonify(create_response( + success=False, + error='JOB_NOT_FOUND', + message='任務不存在' + )), 404 + + # 檢查任務狀態 + if job.status != 'COMPLETED': + return jsonify(create_response( + success=False, + error='JOB_NOT_COMPLETED', + message='任務尚未完成' + )), 400 + + # 尋找 combine 檔案 + combine_file = None + for file in job.files: + if file.filename.lower().find('combine') != -1 or file.file_type == 'combined': + combine_file = file + break + + if not combine_file: + return jsonify(create_response( + success=False, + error='COMBINE_FILE_NOT_FOUND', + message='找不到合併檔案' + )), 404 + + # 檢查檔案是否存在 + file_path = Path(combine_file.file_path) + if not file_path.exists(): + return jsonify(create_response( + success=False, + error='FILE_NOT_FOUND', + message='合併檔案已被刪除' + )), 404 + + logger.info(f"Combine file downloaded: {job.job_uuid} - {combine_file.filename}") + + # 發送檔案 + return send_file( + str(file_path), + as_attachment=True, + download_name=combine_file.filename, + mimetype='application/octet-stream' + ) + + except ValidationError as e: + return jsonify(create_response( + success=False, + error=e.error_code, + message=str(e) + )), 400 + + except Exception as e: + logger.error(f"Combine file download error: {str(e)}") + + return jsonify(create_response( + success=False, + error='SYSTEM_ERROR', + message='合併檔案下載失敗' )), 500 \ No newline at end of file diff --git a/app/services/notification_service.py b/app/services/notification_service.py index a32d8e1..1086c39 100644 --- a/app/services/notification_service.py +++ b/app/services/notification_service.py @@ -535,6 +535,41 @@ class NotificationService: logger.error(f"發送任務完成資料庫通知失敗: {e}") return None + def send_job_completion_db_notification_direct(self, job: TranslationJob) -> Optional[Notification]: + """ + 直接發送任務完成的資料庫通知(不檢查狀態) + """ + try: + # 構建通知內容 + title = "翻譯任務完成" + message = f'您的文件「{job.original_filename}」已成功翻譯完成。' + + # 添加目標語言信息 + if job.target_languages: + languages = ', '.join(job.target_languages) + message += f" 目標語言: {languages}" + + message += " 您可以在任務列表中下載翻譯結果。" + + # 創建資料庫通知 + 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]: """ 發送任務失敗的資料庫通知 diff --git a/app/services/translation_service.py b/app/services/translation_service.py index 17ea878..779e49a 100644 --- a/app/services/translation_service.py +++ b/app/services/translation_service.py @@ -321,7 +321,7 @@ class ExcelParser(DocumentParser): # For auto-detect, translate if has CJK or meaningful text if src_lang.lower() in ('auto', 'auto-detect'): - return has_cjk or len(text) > 5 + return self._has_cjk(text) or len(text) > 5 return True diff --git a/app/tasks/translation.py b/app/tasks/translation.py index 74f9a03..5feb47b 100644 --- a/app/tasks/translation.py +++ b/app/tasks/translation.py @@ -73,13 +73,16 @@ def process_translation_job(self, job_id: int): if result['success']: logger.info(f"Translation job completed successfully: {job.job_uuid}") + # 重新獲取任務以確保狀態是最新的 + db.session.refresh(job) + # 發送完成通知 try: notification_service = NotificationService() # 發送郵件通知 notification_service.send_job_completion_notification(job) - # 發送資料庫通知 - notification_service.send_job_completion_db_notification(job) + # 發送資料庫通知 - 跳過狀態檢查,直接發送 + notification_service.send_job_completion_db_notification_direct(job) except Exception as e: logger.warning(f"Failed to send completion notification: {str(e)}") diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue index 90759a0..02aed18 100644 --- a/frontend/src/layouts/MainLayout.vue +++ b/frontend/src/layouts/MainLayout.vue @@ -114,8 +114,8 @@
-
暂无通知
-
您目前没有未读通知
+
暫無通知
+
您目前沒有未讀通知
@@ -327,7 +327,7 @@ onMounted(() => { sidebarCollapsed.value = savedCollapsed === 'true' } - // 暫時禁用 WebSocket 避免連接錯誤 + // 暫時禁用 WebSocket 連接 // initWebSocket() // 載入通知 diff --git a/frontend/src/services/jobs.js b/frontend/src/services/jobs.js index bfd5925..6616813 100644 --- a/frontend/src/services/jobs.js +++ b/frontend/src/services/jobs.js @@ -92,6 +92,16 @@ export const filesAPI = { responseType: 'blob' }) }, + + /** + * 下載合併檔案 + * @param {string} jobUuid - 任務 UUID + */ + downloadCombineFile(jobUuid) { + return request.get(`/files/${jobUuid}/download/combine`, { + responseType: 'blob' + }) + }, /** * 取得檔案資訊 diff --git a/frontend/src/stores/jobs.js b/frontend/src/stores/jobs.js index 254faaa..88fc340 100644 --- a/frontend/src/stores/jobs.js +++ b/frontend/src/stores/jobs.js @@ -120,13 +120,17 @@ export const useJobsStore = defineStore('jobs', { try { const response = await jobsAPI.getJobDetail(jobUuid) - if (response.success) { - this.currentJob = response.data + if (response && response.success) { + this.currentJob = response.data.job return response.data + } else { + console.error('API 響應格式錯誤:', response) + throw new Error('API 響應格式錯誤') } } catch (error) { console.error('取得任務詳情失敗:', error) ElMessage.error('載入任務詳情失敗') + throw error } }, diff --git a/frontend/src/utils/websocket.js b/frontend/src/utils/websocket.js index 6763845..030c478 100644 --- a/frontend/src/utils/websocket.js +++ b/frontend/src/utils/websocket.js @@ -20,24 +20,20 @@ class WebSocketService { * 初始化並連接 WebSocket */ connect() { - // 暫時禁用 WebSocket 連接 - console.warn('WebSocket 功能已暫時禁用,避免連接錯誤') - return - - // 以下代碼已暫時禁用 - /* if (this.socket) { return } try { // 建立 Socket.IO 連接 - const wsUrl = import.meta.env.VITE_WS_BASE_URL || 'ws://127.0.0.1:5000' + const wsUrl = import.meta.env.VITE_WS_BASE_URL || 'http://127.0.0.1:5000' + console.log('🔌 [WebSocket] 嘗試連接到:', wsUrl) + this.socket = io(wsUrl, { path: '/socket.io/', - transports: ['websocket', 'polling'], - upgrade: true, - rememberUpgrade: true, + transports: ['polling'], + upgrade: false, + rememberUpgrade: false, autoConnect: true, forceNew: false, reconnection: true, @@ -49,7 +45,6 @@ class WebSocketService { } catch (error) { console.error('WebSocket 連接失敗:', error) } - */ } /** diff --git a/frontend/src/views/JobDetailView.vue b/frontend/src/views/JobDetailView.vue index ad16693..920e3e7 100644 --- a/frontend/src/views/JobDetailView.vue +++ b/frontend/src/views/JobDetailView.vue @@ -49,11 +49,11 @@ 下載 {{ getLanguageText(lang) }} 版本 - 下載組合翻譯檔案 (多語言) + 下載合併檔案 下載全部檔案 (ZIP) @@ -352,6 +352,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useJobsStore } from '@/stores/jobs' +import { jobsAPI, filesAPI } from '@/services/jobs' import { ElMessage } from 'element-plus' import { DocumentDelete, ArrowLeft, Refresh, Download, ArrowDown, @@ -387,7 +388,10 @@ const jobUuid = computed(() => route.params.uuid) // 檢查是否有combined檔案 const hasCombinedFile = computed(() => { - return jobFiles.value.some(file => file.language_code === 'combined') + return jobFiles.value.some(file => + file.language_code === 'combined' || + file.filename.toLowerCase().includes('combine') + ) }) // 方法 @@ -395,8 +399,13 @@ const loadJobDetail = async () => { loading.value = true try { const response = await jobsStore.fetchJobDetail(jobUuid.value) + + if (!response || !response.job) { + throw new Error('響應資料格式錯誤') + } + job.value = response.job - jobFiles.value = response.files || [] + jobFiles.value = response.job.files || [] // 訂閱 WebSocket 狀態更新 if (['PENDING', 'PROCESSING', 'RETRY'].includes(job.value.status)) { @@ -453,15 +462,40 @@ const downloadFile = async (langCode, customFilename = null) => { const downloadCombinedFile = async () => { try { - // 找到combined檔案 - const combinedFile = jobFiles.value.find(file => file.language_code === 'combined') - if (combinedFile) { - await jobsStore.downloadFile(jobUuid.value, 'combined', combinedFile.filename) + // 使用新的 combine 下載 API + const response = await filesAPI.downloadCombineFile(jobUuid.value) + + // 從響應頭獲取檔案名 + let filename = 'combined_file.xlsx' + if (response.headers && response.headers['content-disposition']) { + const contentDisposition = response.headers['content-disposition'] + const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) + if (match) { + filename = match[1].replace(/['"]/g, '') + } } else { - ElMessage.error('找不到組合翻譯檔案') + // 使用預設檔名或從任務資料獲取 + const originalName = job.value.original_filename + const baseName = originalName ? originalName.split('.')[0] : 'combined' + filename = `combined_${baseName}.xlsx` } + + // 創建下載連結 + const blobData = response.data || response + const url = window.URL.createObjectURL(new Blob([blobData])) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', filename) + document.body.appendChild(link) + link.click() + link.remove() + window.URL.revokeObjectURL(url) + + ElMessage.success('合併檔案下載成功') + } catch (error) { - console.error('下載組合檔案失敗:', error) + console.error('下載合併檔案失敗:', error) + ElMessage.error('合併檔案下載失敗') } } diff --git a/test_notification_send.py b/test_notification_send.py new file mode 100644 index 0000000..9b139b5 --- /dev/null +++ b/test_notification_send.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +測試通知發送功能 +""" + +from app import create_app, db +from app.models import TranslationJob, User +from app.services.notification_service import NotificationService +from app.models import NotificationType + +def test_notification_sending(): + """測試通知發送功能""" + try: + app = create_app() + with app.app_context(): + print("Testing notification sending...") + + # 查找一個用戶 + user = User.query.first() + if not user: + print("No users found, cannot test notification") + return + + print(f"Found user: {user.username} (ID: {user.id})") + + # 創建一個模擬的翻譯任務 + from datetime import datetime + from uuid import uuid4 + + mock_job = TranslationJob( + job_uuid=str(uuid4()), + user_id=user.id, + original_filename="test_document.docx", + status="COMPLETED", + target_languages=["zh-TW", "en"], + total_cost=0.05, + completed_at=datetime.now() + ) + + # 不保存到資料庫,只用於測試通知 + print(f"Created mock job: {mock_job.job_uuid}") + + # 測試通知服務 + notification_service = NotificationService() + + # 測試直接發送通知(不檢查狀態) + print("Testing direct notification sending...") + notification = notification_service.send_job_completion_db_notification_direct(mock_job) + + if notification: + print(f"✅ Notification created successfully!") + print(f" - ID: {notification.notification_uuid}") + print(f" - Title: {notification.title}") + print(f" - Message: {notification.message}") + print(f" - Type: {notification.type}") + print(f" - User ID: {notification.user_id}") + else: + print("❌ Failed to create notification") + + # 檢查資料庫中的通知數量 + from app.models import Notification + total_notifications = Notification.query.count() + user_notifications = Notification.query.filter_by(user_id=user.id).count() + + print(f"\nDatabase status:") + print(f" - Total notifications: {total_notifications}") + print(f" - Notifications for user {user.username}: {user_notifications}") + + except Exception as e: + print(f"Error testing notification sending: {e}") + import traceback + traceback.print_exc() + +if __name__ == '__main__': + test_notification_sending() \ No newline at end of file