13th_fix combine download
This commit is contained in:
@@ -606,4 +606,86 @@ def download_batch_files(job_uuid):
|
|||||||
success=False,
|
success=False,
|
||||||
error='SYSTEM_ERROR',
|
error='SYSTEM_ERROR',
|
||||||
message='批量下載失敗'
|
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
|
)), 500
|
@@ -535,6 +535,41 @@ class NotificationService:
|
|||||||
logger.error(f"發送任務完成資料庫通知失敗: {e}")
|
logger.error(f"發送任務完成資料庫通知失敗: {e}")
|
||||||
return None
|
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]:
|
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
|
# For auto-detect, translate if has CJK or meaningful text
|
||||||
if src_lang.lower() in ('auto', 'auto-detect'):
|
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
|
return True
|
||||||
|
|
||||||
|
@@ -73,13 +73,16 @@ def process_translation_job(self, job_id: int):
|
|||||||
if result['success']:
|
if result['success']:
|
||||||
logger.info(f"Translation job completed successfully: {job.job_uuid}")
|
logger.info(f"Translation job completed successfully: {job.job_uuid}")
|
||||||
|
|
||||||
|
# 重新獲取任務以確保狀態是最新的
|
||||||
|
db.session.refresh(job)
|
||||||
|
|
||||||
# 發送完成通知
|
# 發送完成通知
|
||||||
try:
|
try:
|
||||||
notification_service = NotificationService()
|
notification_service = NotificationService()
|
||||||
# 發送郵件通知
|
# 發送郵件通知
|
||||||
notification_service.send_job_completion_notification(job)
|
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:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to send completion notification: {str(e)}")
|
logger.warning(f"Failed to send completion notification: {str(e)}")
|
||||||
|
|
||||||
|
@@ -114,8 +114,8 @@
|
|||||||
<div class="notification-list">
|
<div class="notification-list">
|
||||||
<div v-if="notifications.length === 0" class="empty-state">
|
<div v-if="notifications.length === 0" class="empty-state">
|
||||||
<el-icon class="empty-icon"><Bell /></el-icon>
|
<el-icon class="empty-icon"><Bell /></el-icon>
|
||||||
<div class="empty-title">暂无通知</div>
|
<div class="empty-title">暫無通知</div>
|
||||||
<div class="empty-description">您目前没有未读通知</div>
|
<div class="empty-description">您目前沒有未讀通知</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
@@ -327,7 +327,7 @@ onMounted(() => {
|
|||||||
sidebarCollapsed.value = savedCollapsed === 'true'
|
sidebarCollapsed.value = savedCollapsed === 'true'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 暫時禁用 WebSocket 避免連接錯誤
|
// 暫時禁用 WebSocket 連接
|
||||||
// initWebSocket()
|
// initWebSocket()
|
||||||
|
|
||||||
// 載入通知
|
// 載入通知
|
||||||
|
@@ -92,6 +92,16 @@ export const filesAPI = {
|
|||||||
responseType: 'blob'
|
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 {
|
try {
|
||||||
const response = await jobsAPI.getJobDetail(jobUuid)
|
const response = await jobsAPI.getJobDetail(jobUuid)
|
||||||
|
|
||||||
if (response.success) {
|
if (response && response.success) {
|
||||||
this.currentJob = response.data
|
this.currentJob = response.data.job
|
||||||
return response.data
|
return response.data
|
||||||
|
} else {
|
||||||
|
console.error('API 響應格式錯誤:', response)
|
||||||
|
throw new Error('API 響應格式錯誤')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('取得任務詳情失敗:', error)
|
console.error('取得任務詳情失敗:', error)
|
||||||
ElMessage.error('載入任務詳情失敗')
|
ElMessage.error('載入任務詳情失敗')
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -20,24 +20,20 @@ class WebSocketService {
|
|||||||
* 初始化並連接 WebSocket
|
* 初始化並連接 WebSocket
|
||||||
*/
|
*/
|
||||||
connect() {
|
connect() {
|
||||||
// 暫時禁用 WebSocket 連接
|
|
||||||
console.warn('WebSocket 功能已暫時禁用,避免連接錯誤')
|
|
||||||
return
|
|
||||||
|
|
||||||
// 以下代碼已暫時禁用
|
|
||||||
/*
|
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 建立 Socket.IO 連接
|
// 建立 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, {
|
this.socket = io(wsUrl, {
|
||||||
path: '/socket.io/',
|
path: '/socket.io/',
|
||||||
transports: ['websocket', 'polling'],
|
transports: ['polling'],
|
||||||
upgrade: true,
|
upgrade: false,
|
||||||
rememberUpgrade: true,
|
rememberUpgrade: false,
|
||||||
autoConnect: true,
|
autoConnect: true,
|
||||||
forceNew: false,
|
forceNew: false,
|
||||||
reconnection: true,
|
reconnection: true,
|
||||||
@@ -49,7 +45,6 @@ class WebSocketService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('WebSocket 連接失敗:', error)
|
console.error('WebSocket 連接失敗:', error)
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -49,11 +49,11 @@
|
|||||||
下載 {{ getLanguageText(lang) }} 版本
|
下載 {{ getLanguageText(lang) }} 版本
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item
|
<el-dropdown-item
|
||||||
v-if="job.target_languages.length > 1 && hasCombinedFile"
|
v-if="hasCombinedFile"
|
||||||
command="download_combined"
|
command="download_combined"
|
||||||
divided
|
divided
|
||||||
>
|
>
|
||||||
下載組合翻譯檔案 (多語言)
|
下載合併檔案
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item command="download_all" divided>
|
<el-dropdown-item command="download_all" divided>
|
||||||
下載全部檔案 (ZIP)
|
下載全部檔案 (ZIP)
|
||||||
@@ -352,6 +352,7 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useJobsStore } from '@/stores/jobs'
|
import { useJobsStore } from '@/stores/jobs'
|
||||||
|
import { jobsAPI, filesAPI } from '@/services/jobs'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
DocumentDelete, ArrowLeft, Refresh, Download, ArrowDown,
|
DocumentDelete, ArrowLeft, Refresh, Download, ArrowDown,
|
||||||
@@ -387,7 +388,10 @@ const jobUuid = computed(() => route.params.uuid)
|
|||||||
|
|
||||||
// 檢查是否有combined檔案
|
// 檢查是否有combined檔案
|
||||||
const hasCombinedFile = computed(() => {
|
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
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await jobsStore.fetchJobDetail(jobUuid.value)
|
const response = await jobsStore.fetchJobDetail(jobUuid.value)
|
||||||
|
|
||||||
|
if (!response || !response.job) {
|
||||||
|
throw new Error('響應資料格式錯誤')
|
||||||
|
}
|
||||||
|
|
||||||
job.value = response.job
|
job.value = response.job
|
||||||
jobFiles.value = response.files || []
|
jobFiles.value = response.job.files || []
|
||||||
|
|
||||||
// 訂閱 WebSocket 狀態更新
|
// 訂閱 WebSocket 狀態更新
|
||||||
if (['PENDING', 'PROCESSING', 'RETRY'].includes(job.value.status)) {
|
if (['PENDING', 'PROCESSING', 'RETRY'].includes(job.value.status)) {
|
||||||
@@ -453,15 +462,40 @@ const downloadFile = async (langCode, customFilename = null) => {
|
|||||||
|
|
||||||
const downloadCombinedFile = async () => {
|
const downloadCombinedFile = async () => {
|
||||||
try {
|
try {
|
||||||
// 找到combined檔案
|
// 使用新的 combine 下載 API
|
||||||
const combinedFile = jobFiles.value.find(file => file.language_code === 'combined')
|
const response = await filesAPI.downloadCombineFile(jobUuid.value)
|
||||||
if (combinedFile) {
|
|
||||||
await jobsStore.downloadFile(jobUuid.value, 'combined', combinedFile.filename)
|
// 從響應頭獲取檔案名
|
||||||
|
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 {
|
} 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) {
|
} 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