13th_fix combine download

This commit is contained in:
beabigegg
2025-09-04 10:21:16 +08:00
parent 5662fcc039
commit e5fd3e5ec3
10 changed files with 268 additions and 29 deletions

View File

@@ -607,3 +607,85 @@ def download_batch_files(job_uuid):
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

View File

@@ -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]:
"""
發送任務失敗的資料庫通知

View File

@@ -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

View File

@@ -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)}")

View File

@@ -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()
// 載入通知

View File

@@ -93,6 +93,16 @@ export const filesAPI = {
})
},
/**
* 下載合併檔案
* @param {string} jobUuid - 任務 UUID
*/
downloadCombineFile(jobUuid) {
return request.get(`/files/${jobUuid}/download/combine`, {
responseType: 'blob'
})
},
/**
* 取得檔案資訊
* @param {string} jobUuid - 任務 UUID

View File

@@ -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
}
},

View File

@@ -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)
}
*/
}
/**

View File

@@ -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
View 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()