13th_fix combine download
This commit is contained in:
@@ -606,4 +606,86 @@ def download_batch_files(job_uuid):
|
||||
success=False,
|
||||
error='SYSTEM_ERROR',
|
||||
message='批量下載失敗'
|
||||
)), 500
|
||||
|
||||
|
||||
@files_bp.route('/<job_uuid>/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
|
@@ -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]:
|
||||
"""
|
||||
發送任務失敗的資料庫通知
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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)}")
|
||||
|
||||
|
@@ -114,8 +114,8 @@
|
||||
<div class="notification-list">
|
||||
<div v-if="notifications.length === 0" class="empty-state">
|
||||
<el-icon class="empty-icon"><Bell /></el-icon>
|
||||
<div class="empty-title">暂无通知</div>
|
||||
<div class="empty-description">您目前没有未读通知</div>
|
||||
<div class="empty-title">暫無通知</div>
|
||||
<div class="empty-description">您目前沒有未讀通知</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
@@ -327,7 +327,7 @@ onMounted(() => {
|
||||
sidebarCollapsed.value = savedCollapsed === 'true'
|
||||
}
|
||||
|
||||
// 暫時禁用 WebSocket 避免連接錯誤
|
||||
// 暫時禁用 WebSocket 連接
|
||||
// initWebSocket()
|
||||
|
||||
// 載入通知
|
||||
|
@@ -92,6 +92,16 @@ export const filesAPI = {
|
||||
responseType: 'blob'
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 下載合併檔案
|
||||
* @param {string} jobUuid - 任務 UUID
|
||||
*/
|
||||
downloadCombineFile(jobUuid) {
|
||||
return request.get(`/files/${jobUuid}/download/combine`, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 取得檔案資訊
|
||||
|
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -49,11 +49,11 @@
|
||||
下載 {{ getLanguageText(lang) }} 版本
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="job.target_languages.length > 1 && hasCombinedFile"
|
||||
v-if="hasCombinedFile"
|
||||
command="download_combined"
|
||||
divided
|
||||
>
|
||||
下載組合翻譯檔案 (多語言)
|
||||
下載合併檔案
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="download_all" divided>
|
||||
下載全部檔案 (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('合併檔案下載失敗')
|
||||
}
|
||||
}
|
||||
|
||||
|
76
test_notification_send.py
Normal file
76
test_notification_send.py
Normal file
@@ -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()
|
Reference in New Issue
Block a user