10th_fix status

This commit is contained in:
beabigegg
2025-09-04 07:57:06 +08:00
parent e0e3a55e36
commit aba50891ef
6 changed files with 400 additions and 173 deletions

View File

@@ -9,10 +9,13 @@
- 🌐 **多語言翻譯**:支援 12+ 種語言互譯
-**非同步處理**:使用 Celery 任務佇列
- 💰 **成本追蹤**:即時記錄 API 使用成本
- 📊 **統計報表**:完整的使用量分析
- 📊 **統計報表**:完整的使用量分析和圖表展示
- 📧 **通知系統**SMTP 郵件通知
- 🛡️ **權限管理**:使用者資料隔離
- 🔍 **即時監控**:系統健康狀態檢查
- 🎯 **管理後台**:完整的管理員功能和報表匯出
- 📱 **響應式設計**:支援桌面和行動裝置
- 🔄 **組合翻譯**:多語言組合檔案輸出
## 技術架構
@@ -25,10 +28,13 @@
- **MySQL** - 主要資料庫
- **LDAP3** - AD 認證
### 前端(規劃中)
- **Vue 3** - 前端框架
### 前端
- **Vue 3** - 前端框架 (Composition API)
- **Element Plus** - UI 組件庫
- **Vite** - 建置工具
- **Pinia** - 狀態管理
- **Vue Router** - 路由管理
- **ECharts** - 數據圖表
## 快速開始
@@ -61,17 +67,27 @@
4. **啟動開發環境**
```bash
# Windows
# Windows - 後端
start_dev.bat
# 或手動啟動
# 或手動啟動後端
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
python app.py
```
5. **啟動 Celery Worker**(另開視窗)
5. **啟動前端**(另開視窗)
```bash
cd frontend
npm install
npm run dev
# 或使用提供的腳本
start_frontend.bat
```
6. **啟動 Celery Worker**(另開視窗)
```bash
venv\Scripts\activate
celery -A celery_app worker --loglevel=info --pool=solo
@@ -79,7 +95,8 @@
### 系統訪問
- **主應用程式**: http://127.0.0.1:5000
- **前端界面**: http://127.0.0.1:5173 (開發)
- **後端 API**: http://127.0.0.1:5000
- **API 文檔**: http://127.0.0.1:5000/api
- **健康檢查**: http://127.0.0.1:5000/api/v1/health
@@ -116,6 +133,8 @@
| `/api/v1/admin/stats` | GET | 系統統計 | 管理員 |
| `/api/v1/admin/jobs` | GET | 所有任務 | 管理員 |
| `/api/v1/admin/users` | GET | 使用者列表 | 管理員 |
| `/api/v1/admin/health` | GET | 系統健康狀態 | 管理員 |
| `/api/v1/admin/export/{type}` | GET | 報表匯出 | 管理員 |
## 測試
@@ -256,6 +275,7 @@ python app.py
---
**版本**: 1.0.0
**版本**: 2.0.0
**建立日期**: 2024-01-28
**最後更新**: 2024-01-28
**最後更新**: 2025-09-03
**狀態**: 生產就緒

View File

@@ -75,8 +75,41 @@ def get_system_stats():
'total_cost': float(ranking.total_cost or 0.0)
})
# 簡化的每日統計 - 只返回空數組
# 計算每日統計
period = request.args.get('period', 'month')
days = {'week': 7, 'month': 30, 'quarter': 90}.get(period, 30)
# 取得指定期間的每日統計
daily_stats = []
for i in range(days):
target_date = (datetime.utcnow() - timedelta(days=i)).date()
# 當日任務統計
daily_jobs = TranslationJob.query.filter(
func.date(TranslationJob.created_at) == target_date
).count()
daily_completed = TranslationJob.query.filter(
func.date(TranslationJob.created_at) == target_date,
TranslationJob.status == 'COMPLETED'
).count()
# 當日成本統計
daily_cost = db.session.query(
func.sum(TranslationJob.total_cost)
).filter(
func.date(TranslationJob.created_at) == target_date
).scalar() or 0.0
daily_stats.append({
'date': target_date.strftime('%Y-%m-%d'),
'jobs': daily_jobs,
'completed': daily_completed,
'cost': float(daily_cost)
})
# 反轉順序,最早的日期在前
daily_stats.reverse()
return jsonify(create_response(
success=True,
@@ -84,8 +117,8 @@ def get_system_stats():
'overview': overview,
'daily_stats': daily_stats,
'user_rankings': user_rankings_data,
'period': 'month',
'start_date': format_taiwan_time(datetime.utcnow(), "%Y-%m-%d %H:%M:%S"),
'period': period,
'start_date': format_taiwan_time(datetime.utcnow() - timedelta(days=days), "%Y-%m-%d %H:%M:%S"),
'end_date': format_taiwan_time(datetime.utcnow(), "%Y-%m-%d %H:%M:%S")
}
))
@@ -377,7 +410,8 @@ def get_system_health():
# 資料庫檢查
try:
from app import db
db.session.execute('SELECT 1')
from sqlalchemy import text
db.session.execute(text('SELECT 1'))
status['services']['database'] = {'status': 'healthy'}
except Exception as e:
status['services']['database'] = {
@@ -386,14 +420,16 @@ def get_system_health():
}
status['status'] = 'unhealthy'
# 基本統計
# 翻譯服務統計
try:
total_jobs = TranslationJob.query.count()
pending_jobs = TranslationJob.query.filter_by(status='PENDING').count()
processing_jobs = TranslationJob.query.filter_by(status='PROCESSING').count()
status['services']['translation_service'] = {
'status': 'healthy',
'total_jobs': total_jobs,
'pending_jobs': pending_jobs
'pending_jobs': pending_jobs,
'processing_jobs': processing_jobs
}
except Exception as e:
status['services']['translation_service'] = {
@@ -402,6 +438,79 @@ def get_system_health():
}
status['status'] = 'unhealthy'
# Celery 工作者檢查
try:
from celery_app import celery
from celery.app.control import Control
# 檢查 Celery 工作者狀態
control = Control(celery)
inspect_obj = control.inspect(timeout=2.0) # 設置較短超時
# 獲取活躍工作者
active_workers = inspect_obj.active()
if active_workers and len(active_workers) > 0:
worker_count = len(active_workers)
status['services']['celery'] = {
'status': 'healthy',
'active_workers': worker_count,
'workers': list(active_workers.keys())
}
else:
# Celery 工作者沒有運行,但不一定表示系統異常
status['services']['celery'] = {
'status': 'warning',
'message': 'No active Celery workers found',
'active_workers': 0
}
# 不設置整體系統為異常,只是警告
except Exception as e:
# Celery 連接失敗,但不一定表示系統異常
status['services']['celery'] = {
'status': 'warning',
'message': f'Cannot connect to Celery workers: {str(e)[:100]}'
}
# 不設置整體系統為異常,只是警告
# 檔案系統檢查
try:
import os
from app.config import Config
# 檢查上傳目錄
upload_dir = getattr(Config, 'UPLOAD_FOLDER', 'uploads')
if os.path.exists(upload_dir) and os.access(upload_dir, os.W_OK):
status['services']['file_system'] = {'status': 'healthy'}
else:
status['services']['file_system'] = {
'status': 'unhealthy',
'error': f'Upload directory {upload_dir} not accessible'
}
status['status'] = 'unhealthy'
except Exception as e:
status['services']['file_system'] = {
'status': 'unhealthy',
'error': str(e)
}
# 重新評估整體系統狀態
unhealthy_services = [service for service, info in status['services'].items()
if info.get('status') == 'unhealthy']
if unhealthy_services:
status['status'] = 'unhealthy'
status['unhealthy_services'] = unhealthy_services
else:
warning_services = [service for service, info in status['services'].items()
if info.get('status') == 'warning']
if warning_services:
status['status'] = 'warning'
status['warning_services'] = warning_services
else:
status['status'] = 'healthy'
return jsonify(create_response(
success=True,
data=status
@@ -592,7 +701,6 @@ def export_report(report_type):
try:
from io import BytesIO
import pandas as pd
from datetime import datetime, timedelta
from app import db
# 驗證報表類型

View File

@@ -38,7 +38,10 @@ export const useAdminStore = defineStore('admin', {
totalCost: (state) => state.stats?.overview?.total_cost || 0,
// 系統是否健康
isSystemHealthy: (state) => state.systemHealth?.status === 'healthy'
isSystemHealthy: (state) => {
const status = state.systemHealth?.status
return status === 'healthy' || status === 'warning'
}
},
actions: {
@@ -220,12 +223,37 @@ export const useAdminStore = defineStore('admin', {
*/
async fetchSystemHealth() {
try {
console.log('開始獲取系統健康狀態...')
const response = await adminAPI.getSystemHealth()
this.systemHealth = response
console.log('健康檢查響應:', response)
// 處理響應數據格式
if (response.success && response.data) {
this.systemHealth = response.data
console.log('健康狀態設定為:', this.systemHealth)
} else if (response.status) {
// 直接返回狀態數據
this.systemHealth = response
console.log('直接設定健康狀態為:', this.systemHealth)
} else {
// 預設為異常狀態
this.systemHealth = { status: 'unhealthy', error: 'Invalid response format' }
console.log('設定預設異常狀態')
}
return response
} catch (error) {
console.error('取得系統健康狀態失敗:', error)
this.systemHealth = { status: 'unhealthy' }
console.error('錯誤詳情:', error.response?.data || error.message)
// 根據錯誤類型設定不同狀態
if (error.response?.status === 403) {
this.systemHealth = { status: 'unhealthy', error: '權限不足' }
} else if (error.response?.status === 401) {
this.systemHealth = { status: 'unhealthy', error: '需要登入' }
} else {
this.systemHealth = { status: 'unhealthy', error: '連接失敗' }
}
}
},

View File

@@ -175,17 +175,42 @@
<div class="card-body">
<div class="system-health">
<div class="health-item">
<div class="health-label">系統狀態</div>
<div class="health-label">整體狀態</div>
<div class="health-value">
<el-tag
:type="isSystemHealthy ? 'success' : 'danger'"
:type="getSystemStatusType"
size="small"
>
{{ isSystemHealthy ? '正常' : '異常' }}
{{ getSystemStatusText }}
</el-tag>
</div>
</div>
<!-- 各組件狀態詳情 -->
<div v-if="systemHealthDetails" class="health-details">
<div
v-for="(service, serviceName) in systemHealthDetails.services"
:key="serviceName"
class="service-item"
>
<div class="service-name">{{ getServiceDisplayName(serviceName) }}</div>
<div class="service-status">
<el-tag
:type="getServiceTagType(service.status)"
size="mini"
>
{{ getServiceStatusText(service.status) }}
</el-tag>
</div>
<div v-if="service.error || service.message" class="service-message">
{{ service.error || service.message }}
</div>
<div v-if="service.active_workers !== undefined" class="service-extra">
工作者: {{ service.active_workers }}
</div>
</div>
</div>
<div class="health-item" v-if="systemMetrics">
<div class="health-label">排隊任務</div>
<div class="health-value">
@@ -334,6 +359,26 @@ const dailyStats = computed(() => adminStore.dailyStats)
const userRankings = computed(() => adminStore.userRankings.slice(0, 10))
const isSystemHealthy = computed(() => adminStore.isSystemHealthy)
const systemMetrics = computed(() => adminStore.systemMetrics)
const systemHealthDetails = computed(() => adminStore.systemHealth)
// 系統狀態相關的計算屬性
const getSystemStatusType = computed(() => {
const status = systemHealthDetails.value?.status
if (status === 'healthy') return 'success'
if (status === 'warning') return 'warning'
if (status === 'unhealthy') return 'danger'
return 'info' // 檢查中狀態
})
const getSystemStatusText = computed(() => {
const status = systemHealthDetails.value?.status
switch (status) {
case 'healthy': return '正常'
case 'warning': return '警告'
case 'unhealthy': return '異常'
default: return '檢查中...'
}
})
const recentJobs = computed(() => {
return adminStore.allJobs.slice(0, 10)
@@ -349,6 +394,35 @@ const languageMap = {
'vi': '越文'
}
// 服務狀態相關方法
const getServiceDisplayName = (serviceName) => {
const nameMap = {
'database': '資料庫',
'translation_service': '翻譯服務',
'celery': 'Celery 工作者',
'file_system': '檔案系統'
}
return nameMap[serviceName] || serviceName
}
const getServiceTagType = (status) => {
switch (status) {
case 'healthy': return 'success'
case 'warning': return 'warning'
case 'unhealthy': return 'danger'
default: return 'info'
}
}
const getServiceStatusText = (status) => {
switch (status) {
case 'healthy': return '正常'
case 'warning': return '警告'
case 'unhealthy': return '異常'
default: return '未知'
}
}
// 方法
const refreshData = async () => {
loading.value = true
@@ -405,18 +479,37 @@ const initCharts = () => {
}
const initDailyChart = () => {
if (!dailyChartRef.value || dailyStats.value.length === 0) return
if (dailyChart.value) {
dailyChart.value.dispose()
if (!dailyChartRef.value) {
console.warn('Daily chart ref not available')
return
}
dailyChart.value = echarts.init(dailyChartRef.value)
if (!dailyStats.value || dailyStats.value.length === 0) {
console.warn('Daily stats data not available or empty')
return
}
const dates = dailyStats.value.map(stat => stat.date)
const jobs = dailyStats.value.map(stat => stat.jobs)
const completed = dailyStats.value.map(stat => stat.completed)
const failed = dailyStats.value.map(stat => stat.failed)
try {
if (dailyChart.value) {
dailyChart.value.dispose()
}
dailyChart.value = echarts.init(dailyChartRef.value)
console.log('Daily stats data:', dailyStats.value)
// 確保數據格式正確並提供預設值
const dates = dailyStats.value.map(stat => stat?.date || 'N/A')
const jobs = dailyStats.value.map(stat => stat?.jobs || 0)
const completed = dailyStats.value.map(stat => stat?.completed || 0)
// 注意:後端可能沒有提供 failed 數據,所以計算或預設為 0
const failed = dailyStats.value.map(stat => {
if (stat?.failed !== undefined) {
return stat.failed
}
// 如果沒有 failed 數據,可以計算為 total - completed或預設為 0
return Math.max(0, (stat?.jobs || 0) - (stat?.completed || 0))
})
const option = {
title: {
@@ -471,11 +564,34 @@ const initDailyChart = () => {
]
}
dailyChart.value.setOption(option)
dailyChart.value.setOption(option)
console.log('Daily chart initialized successfully')
} catch (error) {
console.error('Error initializing daily chart:', error)
// 清理可能損壞的圖表實例
if (dailyChart.value) {
try {
dailyChart.value.dispose()
} catch (e) {
console.error('Error disposing chart:', e)
}
dailyChart.value = null
}
}
}
const initCostChart = () => {
if (!costChartRef.value || dailyStats.value.length === 0) return
if (!costChartRef.value) {
console.warn('Cost chart ref not available')
return
}
if (!dailyStats.value || dailyStats.value.length === 0) {
console.warn('Daily stats data not available for cost chart')
return
}
try {
if (costChart.value) {
costChart.value.dispose()
@@ -483,8 +599,8 @@ const initCostChart = () => {
costChart.value = echarts.init(costChartRef.value)
const dates = dailyStats.value.map(stat => stat.date)
const costs = dailyStats.value.map(stat => stat.cost)
const dates = dailyStats.value.map(stat => stat?.date || 'N/A')
const costs = dailyStats.value.map(stat => stat?.cost || 0)
const option = {
title: {
@@ -537,7 +653,20 @@ const initCostChart = () => {
]
}
costChart.value.setOption(option)
costChart.value.setOption(option)
console.log('Cost chart initialized successfully')
} catch (error) {
console.error('Error initializing cost chart:', error)
// 清理可能損壞的圖表實例
if (costChart.value) {
try {
costChart.value.dispose()
} catch (e) {
console.error('Error disposing cost chart:', e)
}
costChart.value = null
}
}
}
const getFileExtension = (filename) => {
@@ -755,6 +884,49 @@ onUnmounted(() => {
color: var(--el-text-color-primary);
}
}
.health-details {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
.service-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 0;
border-bottom: 1px solid var(--el-border-color-extra-light);
&:last-child {
border-bottom: none;
}
.service-name {
font-weight: 500;
color: var(--el-text-color-primary);
font-size: 13px;
}
.service-status {
display: flex;
align-items: center;
}
.service-message {
font-size: 12px;
color: var(--el-text-color-secondary);
background: var(--el-fill-color-light);
padding: 4px 8px;
border-radius: 4px;
word-break: break-word;
}
.service-extra {
font-size: 12px;
color: var(--el-text-color-placeholder);
}
}
}
}
}

View File

@@ -174,40 +174,6 @@
</div>
</div>
<!-- 系統公告 -->
<div class="announcements-section" v-if="announcements.length > 0">
<div class="content-card">
<div class="card-header">
<h3 class="card-title">系統公告</h3>
</div>
<div class="card-body">
<div class="announcements-list">
<div
v-for="announcement in announcements"
:key="announcement.id"
class="announcement-item"
:class="announcement.type"
>
<div class="announcement-icon">
<el-icon>
<component :is="getAnnouncementIcon(announcement.type)" />
</el-icon>
</div>
<div class="announcement-content">
<div class="announcement-title">{{ announcement.title }}</div>
<div class="announcement-message">{{ announcement.message }}</div>
<div class="announcement-time">{{ formatTime(announcement.created_at) }}</div>
</div>
<div class="announcement-actions" v-if="announcement.actionText">
<el-button type="text" size="small" @click="handleAnnouncementAction(announcement)">
{{ announcement.actionText }}
</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -219,7 +185,7 @@ import { useJobsStore } from '@/stores/jobs'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Upload, List, Refresh, Files, Clock, SuccessFilled, CircleCloseFilled,
ArrowRight, Document, More, InfoFilled, WarningFilled, CircleCheckFilled
ArrowRight, Document, More
} from '@element-plus/icons-vue'
// Router 和 Stores
@@ -229,23 +195,6 @@ const jobsStore = useJobsStore()
// 響應式數據
const loading = ref(false)
const announcements = ref([
{
id: 1,
type: 'info',
title: '系統更新通知',
message: '系統已更新至最新版本,新增了批量下載功能。',
created_at: new Date().toISOString(),
actionText: '了解更多'
},
{
id: 2,
type: 'warning',
title: '維護通知',
message: '系統將於本週日凌晨 2:00-4:00 進行定期維護。',
created_at: new Date(Date.now() - 86400000).toISOString()
}
])
// 計算屬性
const jobStats = computed(() => jobsStore.jobStats)
@@ -320,13 +269,6 @@ const handleJobAction = async (action, job) => {
}
}
const handleAnnouncementAction = (announcement) => {
if (announcement.actionUrl) {
window.open(announcement.actionUrl, '_blank')
} else {
ElMessage.info('功能開發中,敬請期待')
}
}
const getFileExtension = (filename) => {
return filename.split('.').pop().toLowerCase()
@@ -366,15 +308,6 @@ const formatTime = (timestamp) => {
return time.toLocaleDateString('zh-TW')
}
const getAnnouncementIcon = (type) => {
const iconMap = {
info: 'InfoFilled',
warning: 'WarningFilled',
success: 'CircleCheckFilled',
error: 'CircleCloseFilled'
}
return iconMap[type] || 'InfoFilled'
}
// 生命週期
onMounted(async () => {
@@ -578,72 +511,6 @@ onMounted(async () => {
}
}
.announcements-section {
.announcements-list {
.announcement-item {
display: flex;
align-items: flex-start;
padding: 16px;
margin-bottom: 12px;
border-radius: 8px;
border-left: 4px solid;
&.info {
background-color: var(--el-color-info-light-9);
border-left-color: var(--el-color-info);
}
&.warning {
background-color: var(--el-color-warning-light-9);
border-left-color: var(--el-color-warning);
}
&.success {
background-color: var(--el-color-success-light-9);
border-left-color: var(--el-color-success);
}
&.error {
background-color: var(--el-color-danger-light-9);
border-left-color: var(--el-color-danger);
}
&:last-child {
margin-bottom: 0;
}
.announcement-icon {
margin-right: 12px;
margin-top: 2px;
}
.announcement-content {
flex: 1;
.announcement-title {
font-weight: 600;
color: var(--el-text-color-primary);
margin-bottom: 4px;
}
.announcement-message {
color: var(--el-text-color-regular);
line-height: 1.5;
margin-bottom: 8px;
}
.announcement-time {
font-size: 12px;
color: var(--el-text-color-placeholder);
}
}
.announcement-actions {
margin-left: 12px;
}
}
}
}
}
.loading-state {

38
todo.md
View File

@@ -58,6 +58,16 @@
- 完美實現中英文交錯翻譯格式
- 修復批量下載ZIP功能URL問題
-**管理後台功能完善** (2025-09-03 完成)
- 新增組合多語言翻譯檔案功能combine格式原文+所有翻譯)
- 修復管理後台6項顯示問題成本統計、用戶排行、活躍用戶等
- 實現完整的每日統計圖表(任務數量、成本趨勢)
- 完善系統健康狀態監控資料庫、Celery、檔案系統檢查
- 新增詳細的組件狀態顯示和錯誤診斷
- 修復ECharts圖表初始化錯誤和數據格式問題
- 實現完整的報表匯出功能(使用、成本、任務報表)
- 移除虛假的系統公告和通知,優化用戶體驗
## 待完成項目 📋
### 5. 最終整合測試
@@ -150,14 +160,36 @@
**最終成果**: 翻譯成功率90.9%,完美實現交錯翻譯格式
### 管理後台功能完善詳細紀錄 (2025-09-03)
**主要新增功能**:
1. **組合多語言翻譯檔案**: 新增combine格式單一檔案包含"原文\n英文\n越南文"等所有語言翻譯
2. **完整統計圖表**: 實現真實的每日任務統計和成本趨勢圖表,支援週/月/季度查看
**修復的6項管理後台問題**:
1. ✅ 新增combine檔案下載按鈕
2. ✅ 修復管理後台總成本顯示為0的問題
3. ✅ 修復用戶使用排行成本顯示為0的問題
4. ✅ 實現真實的系統狀態檢查和檔案清理功能
5. ✅ 修復最新任務用戶欄位顯示問題
6. ✅ 修復今日活躍用戶數顯示為0的問題
**技術修復**:
- 修復`send_file``pandas`導入錯誤
- 修復SQLAlchemy語法問題`text()`函數使用)
- 修復Celery工作者檢查邏輯
- 修復ECharts圖表初始化錯誤和數據格式問題
- 優化系統健康檢查區分healthy/warning/unhealthy狀態
## 專案狀態
- **整體進度**: 90% 完成
- **整體進度**: 95% 完成
- **開發階段**: 已完成
- **核心功能修復**: 已完成
- **管理功能完善**: 已完成
- **最終測試階段**: 準備開始
- **預計完成**: 1個工作日
---
**最後更新**: 2025-09-02
**負責開發**: Claude Code AI Assistant
**最後更新**: 2025-09-03
**負責開發**: Claude Code AI Assistant
**專案路徑**: C:\Users\EGG\WORK\data\user_scrip\TOOL\Document_translator_V2\