改用API驗證

This commit is contained in:
beabigegg
2025-10-02 17:13:24 +08:00
parent 0a89c19fc9
commit adecdf0cce
48 changed files with 6136 additions and 1239 deletions

View File

@@ -0,0 +1,8 @@
-- 添加 conversation_id 字段以支持對話持續性
-- 這個字段用於在同一個翻譯任務中保持 Dify API 對話的連續性
ALTER TABLE dt_translation_jobs
ADD COLUMN conversation_id VARCHAR(100) COMMENT 'Dify對話ID用於維持翻譯上下文';
-- 為現有的 conversation_id 字段創建索引,以提高查詢效率
CREATE INDEX idx_conversation_id ON dt_translation_jobs(conversation_id);

View File

@@ -0,0 +1,83 @@
-- 建立系統使用者表 (sys_user)
-- 專門用於記錄帳號密碼和登入相關資訊
-- 不影響現有 users 表的權限管理功能
-- Created: 2025-10-01
CREATE TABLE IF NOT EXISTS sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
-- 帳號資訊
username VARCHAR(255) NOT NULL COMMENT '登入帳號',
password_hash VARCHAR(512) COMMENT '密碼雜湊 (如果需要本地儲存)',
email VARCHAR(255) NOT NULL COMMENT '電子郵件',
display_name VARCHAR(255) COMMENT '顯示名稱',
-- API 認證資訊
api_user_id VARCHAR(255) COMMENT 'API 回傳的使用者 ID',
api_access_token TEXT COMMENT 'API 回傳的 access_token',
api_token_expires_at TIMESTAMP NULL COMMENT 'API Token 過期時間',
-- 登入相關
auth_method ENUM('API', 'LDAP') DEFAULT 'API' COMMENT '認證方式',
last_login_at TIMESTAMP NULL COMMENT '最後登入時間',
last_login_ip VARCHAR(45) COMMENT '最後登入 IP',
login_count INT DEFAULT 0 COMMENT '登入次數',
login_success_count INT DEFAULT 0 COMMENT '成功登入次數',
login_fail_count INT DEFAULT 0 COMMENT '失敗登入次數',
-- 帳號狀態
is_active BOOLEAN DEFAULT TRUE COMMENT '是否啟用',
is_locked BOOLEAN DEFAULT FALSE COMMENT '是否鎖定',
locked_until TIMESTAMP NULL COMMENT '鎖定至何時',
-- 審計欄位
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
-- 索引
INDEX idx_username (username),
INDEX idx_email (email),
INDEX idx_api_user_id (api_user_id),
INDEX idx_auth_method (auth_method),
INDEX idx_last_login (last_login_at),
INDEX idx_active_users (is_active, is_locked),
-- 約束
UNIQUE KEY uk_username (username),
UNIQUE KEY uk_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系統使用者表 - 帳號密碼登入記錄';
-- 建立登入記錄表 (簡化版)
CREATE TABLE IF NOT EXISTS login_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
-- 基本資訊
username VARCHAR(255) NOT NULL COMMENT '登入帳號',
auth_method ENUM('API', 'LDAP') NOT NULL COMMENT '認證方式',
-- 登入結果
login_success BOOLEAN NOT NULL COMMENT '是否成功',
error_message TEXT COMMENT '錯誤訊息(失敗時)',
-- 環境資訊
ip_address VARCHAR(45) COMMENT 'IP 地址',
user_agent TEXT COMMENT '瀏覽器資訊',
-- API 回應 (可選,用於除錯)
api_response_summary JSON COMMENT 'API 回應摘要',
-- 時間
login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '登入時間',
-- 索引
INDEX idx_username (username),
INDEX idx_auth_method (auth_method),
INDEX idx_login_success (login_success),
INDEX idx_login_at (login_at),
INDEX idx_username_time (username, login_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='登入記錄表';
-- 清理舊的認證相關表(如果存在)
DROP TABLE IF EXISTS auth_records;
DROP TABLE IF EXISTS token_refresh_logs;
DROP TABLE IF EXISTS login_history;

View File

@@ -0,0 +1,23 @@
-- 清理所有 dt_ 前綴的資料表
-- 重新開始,建立乾淨的架構
-- Created: 2025-10-01
-- 關閉外鍵約束檢查 (避免刪除順序問題)
SET FOREIGN_KEY_CHECKS = 0;
-- 刪除所有 dt_ 前綴的資料表 (按照依賴關係順序)
-- 先刪除有外鍵依賴的子表,再刪除父表
DROP TABLE IF EXISTS dt_job_files;
DROP TABLE IF EXISTS dt_translation_cache;
DROP TABLE IF EXISTS dt_api_usage_stats;
DROP TABLE IF EXISTS dt_system_logs;
DROP TABLE IF EXISTS dt_notifications;
DROP TABLE IF EXISTS dt_login_logs;
DROP TABLE IF EXISTS dt_translation_jobs;
DROP TABLE IF EXISTS dt_users;
-- 重新啟用外鍵約束檢查
SET FOREIGN_KEY_CHECKS = 1;
-- 驗證清理結果
SHOW TABLES LIKE 'dt_%';

View File

@@ -0,0 +1,160 @@
-- 全新的文件翻譯系統資料庫架構
-- 方案 A: dt_users 用於業務功能sys_user 用於登入記錄
-- API name 格式: 姓名+emailemail 作為主要識別鍵
-- Created: 2025-10-01
-- 1. 建立 dt_users 表 (業務功能使用)
CREATE TABLE dt_users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL COMMENT 'API name (姓名+email格式)',
display_name VARCHAR(255) NOT NULL COMMENT 'API name (姓名+email格式)',
email VARCHAR(255) NOT NULL UNIQUE COMMENT '電子郵件 (主要識別鍵)',
department VARCHAR(100) COMMENT '部門/職位',
is_admin BOOLEAN DEFAULT FALSE COMMENT '是否為管理員',
last_login DATETIME COMMENT '最後登入時間',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
INDEX idx_email (email),
INDEX idx_username_email (username, email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='使用者資訊表';
-- 2. 建立 dt_translation_jobs 表 (翻譯工作)
CREATE TABLE dt_translation_jobs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL COMMENT '關聯到 dt_users.id',
job_name VARCHAR(255) NOT NULL COMMENT '工作名稱',
source_lang VARCHAR(10) NOT NULL COMMENT '來源語言',
target_lang VARCHAR(10) NOT NULL COMMENT '目標語言',
file_type ENUM('DOCX', 'PPTX', 'PDF', 'TXT') NOT NULL COMMENT '檔案類型',
status ENUM('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED') DEFAULT 'PENDING' COMMENT '工作狀態',
progress INT DEFAULT 0 COMMENT '進度百分比',
total_pages INT DEFAULT 0 COMMENT '總頁數',
processed_pages INT DEFAULT 0 COMMENT '已處理頁數',
cost DECIMAL(10,4) DEFAULT 0 COMMENT '翻譯成本',
error_message TEXT COMMENT '錯誤訊息',
conversation_id VARCHAR(255) COMMENT 'Dify 對話 ID',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
completed_at DATETIME COMMENT '完成時間',
FOREIGN KEY (user_id) REFERENCES dt_users(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='翻譯工作表';
-- 3. 建立 dt_job_files 表 (工作檔案)
CREATE TABLE dt_job_files (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
job_id BIGINT NOT NULL COMMENT '關聯到 dt_translation_jobs.id',
file_type ENUM('source', 'translated') NOT NULL COMMENT '檔案類型',
original_filename VARCHAR(255) NOT NULL COMMENT '原始檔名',
stored_filename VARCHAR(255) NOT NULL COMMENT '儲存檔名',
file_path VARCHAR(500) NOT NULL COMMENT '檔案路徑',
file_size BIGINT DEFAULT 0 COMMENT '檔案大小',
mime_type VARCHAR(100) COMMENT 'MIME 類型',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
FOREIGN KEY (job_id) REFERENCES dt_translation_jobs(id) ON DELETE CASCADE,
INDEX idx_job_id (job_id),
INDEX idx_file_type (file_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作檔案表';
-- 4. 建立 dt_translation_cache 表 (翻譯快取)
CREATE TABLE dt_translation_cache (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
content_hash VARCHAR(64) NOT NULL COMMENT '內容雜湊',
source_lang VARCHAR(10) NOT NULL COMMENT '來源語言',
target_lang VARCHAR(10) NOT NULL COMMENT '目標語言',
source_text TEXT NOT NULL COMMENT '來源文字',
translated_text TEXT NOT NULL COMMENT '翻譯文字',
quality_score DECIMAL(3,2) DEFAULT 0.00 COMMENT '品質分數',
hit_count INT DEFAULT 0 COMMENT '命中次數',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
last_used_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最後使用時間',
UNIQUE KEY uk_content_lang (content_hash, source_lang, target_lang),
INDEX idx_last_used (last_used_at),
INDEX idx_hit_count (hit_count)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='翻譯快取表';
-- 5. 建立 dt_api_usage_stats 表 (API 使用統計)
CREATE TABLE dt_api_usage_stats (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL COMMENT '關聯到 dt_users.id',
job_id BIGINT COMMENT '關聯到 dt_translation_jobs.id',
api_name VARCHAR(50) NOT NULL COMMENT 'API 名稱',
request_count INT DEFAULT 1 COMMENT '請求次數',
token_used INT DEFAULT 0 COMMENT '使用的 token 數',
cost DECIMAL(10,4) DEFAULT 0 COMMENT '成本',
response_time_ms INT DEFAULT 0 COMMENT '回應時間(毫秒)',
status ENUM('SUCCESS', 'FAILED', 'TIMEOUT') DEFAULT 'SUCCESS' COMMENT '狀態',
error_message TEXT COMMENT '錯誤訊息',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
date_key DATE GENERATED ALWAYS AS (DATE(created_at)) STORED COMMENT '日期鍵',
FOREIGN KEY (user_id) REFERENCES dt_users(id) ON DELETE CASCADE,
FOREIGN KEY (job_id) REFERENCES dt_translation_jobs(id) ON DELETE SET NULL,
INDEX idx_user_date (user_id, date_key),
INDEX idx_api_name (api_name),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='API 使用統計表';
-- 6. 建立 dt_system_logs 表 (系統日誌)
CREATE TABLE dt_system_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
level ENUM('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') NOT NULL COMMENT '日誌級別',
category VARCHAR(50) NOT NULL COMMENT '日誌分類',
message TEXT NOT NULL COMMENT '日誌訊息',
user_id INT COMMENT '關聯到 dt_users.id',
job_id BIGINT COMMENT '關聯到 dt_translation_jobs.id',
extra_data JSON COMMENT '額外資料',
ip_address VARCHAR(45) COMMENT 'IP 地址',
user_agent TEXT COMMENT '用戶代理',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
date_key DATE GENERATED ALWAYS AS (DATE(created_at)) STORED COMMENT '日期鍵',
FOREIGN KEY (user_id) REFERENCES dt_users(id) ON DELETE SET NULL,
FOREIGN KEY (job_id) REFERENCES dt_translation_jobs(id) ON DELETE SET NULL,
INDEX idx_level_category (level, category),
INDEX idx_user_date (user_id, date_key),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系統日誌表';
-- 7. 建立 dt_notifications 表 (通知)
CREATE TABLE dt_notifications (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL COMMENT '關聯到 dt_users.id',
type ENUM('INFO', 'SUCCESS', 'WARNING', 'ERROR') NOT NULL COMMENT '通知類型',
title VARCHAR(255) NOT NULL COMMENT '通知標題',
message TEXT NOT NULL COMMENT '通知內容',
is_read BOOLEAN DEFAULT FALSE COMMENT '是否已讀',
data JSON COMMENT '額外資料',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
read_at DATETIME COMMENT '已讀時間',
FOREIGN KEY (user_id) REFERENCES dt_users(id) ON DELETE CASCADE,
INDEX idx_user_unread (user_id, is_read),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知表';
-- 8. 保持現有的 sys_user 表 (專門用於登入記錄)
-- sys_user 表已存在,透過 email 與 dt_users 關聯
-- 9. 重新命名 login_logs 為 dt_login_logs
RENAME TABLE login_logs TO dt_login_logs;
-- 10. 為 dt_login_logs 添加與 dt_users 的關聯
ALTER TABLE dt_login_logs
ADD COLUMN user_id INT COMMENT '關聯到 dt_users.id',
ADD INDEX idx_user_id (user_id),
ADD FOREIGN KEY fk_dt_login_logs_user_id (user_id) REFERENCES dt_users(id) ON DELETE SET NULL;
-- 11. 插入預設管理員使用者
INSERT INTO dt_users (username, display_name, email, department, is_admin)
VALUES ('ymirliu ymirliu@panjit.com.tw', 'ymirliu ymirliu@panjit.com.tw', 'ymirliu@panjit.com.tw', 'IT', TRUE);
-- 12. 驗證架構建立
SELECT 'Tables created:' as status;
SHOW TABLES LIKE 'dt_%';

View File

@@ -0,0 +1,203 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
執行 API Usage Stats 資料表修復 Migration
Usage:
python migrations/fix_api_usage_stats.py
"""
import sys
from pathlib import Path
# 添加專案根目錄到 Python 路徑
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from app import create_app, db
from sqlalchemy import text
def run_migration():
"""執行資料表結構修復"""
app = create_app()
with app.app_context():
print("=" * 60)
print("API Usage Stats 資料表結構修復")
print("=" * 60)
try:
# 1. 檢查當前結構
print("\n[1/8] 檢查當前資料表結構...")
result = db.session.execute(text('SHOW COLUMNS FROM dt_api_usage_stats'))
current_columns = {row[0]: row for row in result}
print(f" ✓ 當前欄位數量: {len(current_columns)}")
# 2. 備份現有資料
print("\n[2/8] 建立資料備份...")
db.session.execute(text('''
CREATE TABLE IF NOT EXISTS dt_api_usage_stats_backup_20251001
AS SELECT * FROM dt_api_usage_stats
'''))
db.session.commit()
backup_count = db.session.execute(
text('SELECT COUNT(*) FROM dt_api_usage_stats_backup_20251001')
).scalar()
print(f" ✓ 已備份 {backup_count} 筆記錄")
# 3. 修改欄位名稱api_name → api_endpoint
if 'api_name' in current_columns:
print("\n[3/8] 重新命名 api_name → api_endpoint...")
db.session.execute(text('''
ALTER TABLE dt_api_usage_stats
CHANGE COLUMN api_name api_endpoint VARCHAR(200) NOT NULL COMMENT 'API端點'
'''))
db.session.commit()
print(" ✓ 已重新命名 api_name → api_endpoint")
else:
print("\n[3/8] 跳過api_name 已不存在或已是 api_endpoint")
# 4. 新增 prompt_tokens 和 completion_tokens
print("\n[4/8] 新增 prompt_tokens 和 completion_tokens...")
if 'prompt_tokens' not in current_columns:
db.session.execute(text('''
ALTER TABLE dt_api_usage_stats
ADD COLUMN prompt_tokens INT DEFAULT 0 COMMENT 'Prompt token數' AFTER api_endpoint
'''))
if 'completion_tokens' not in current_columns:
db.session.execute(text('''
ALTER TABLE dt_api_usage_stats
ADD COLUMN completion_tokens INT DEFAULT 0 COMMENT 'Completion token數' AFTER prompt_tokens
'''))
db.session.commit()
print(" ✓ 已新增 token 細分欄位")
# 5. 重新命名 token_used → total_tokens
if 'token_used' in current_columns:
print("\n[5/8] 重新命名 token_used → total_tokens...")
db.session.execute(text('''
ALTER TABLE dt_api_usage_stats
CHANGE COLUMN token_used total_tokens INT DEFAULT 0 COMMENT '總token數'
'''))
db.session.commit()
print(" ✓ 已重新命名 token_used → total_tokens")
else:
print("\n[5/8] 跳過token_used 已不存在或已是 total_tokens")
# 6. 新增計費相關欄位
print("\n[6/8] 新增計費相關欄位...")
if 'prompt_unit_price' not in current_columns:
db.session.execute(text('''
ALTER TABLE dt_api_usage_stats
ADD COLUMN prompt_unit_price DECIMAL(10, 8) DEFAULT 0.00000000 COMMENT '單價' AFTER total_tokens
'''))
if 'prompt_price_unit' not in current_columns:
db.session.execute(text('''
ALTER TABLE dt_api_usage_stats
ADD COLUMN prompt_price_unit VARCHAR(20) DEFAULT 'USD' COMMENT '價格單位' AFTER prompt_unit_price
'''))
db.session.commit()
print(" ✓ 已新增計費欄位")
# 7. 替換 status 欄位為 success (BOOLEAN)
print("\n[7/8] 更新 status 欄位...")
if 'status' in current_columns and 'success' not in current_columns:
# 先新增 success 欄位
db.session.execute(text('''
ALTER TABLE dt_api_usage_stats
ADD COLUMN success BOOLEAN DEFAULT TRUE COMMENT '是否成功' AFTER response_time_ms
'''))
# 將 status 資料轉換到 success
db.session.execute(text('''
UPDATE dt_api_usage_stats
SET success = (status = 'SUCCESS')
'''))
# 刪除舊的 status 欄位
db.session.execute(text('''
ALTER TABLE dt_api_usage_stats
DROP COLUMN status
'''))
db.session.commit()
print(" ✓ 已將 status 轉換為 success (BOOLEAN)")
else:
print(" 跳過(已完成或不需要轉換)")
# 8. 更新索引
print("\n[8/8] 建立索引...")
try:
db.session.execute(text('''
ALTER TABLE dt_api_usage_stats
ADD INDEX IF NOT EXISTS idx_api_endpoint (api_endpoint)
'''))
except Exception as e:
if 'Duplicate' not in str(e):
raise
try:
db.session.execute(text('''
ALTER TABLE dt_api_usage_stats
ADD INDEX IF NOT EXISTS idx_success (success)
'''))
except Exception as e:
if 'Duplicate' not in str(e):
raise
db.session.commit()
print(" ✓ 已建立索引")
# 9. 驗證最終結構
print("\n" + "=" * 60)
print("驗證最終資料表結構")
print("=" * 60)
result = db.session.execute(text('SHOW COLUMNS FROM dt_api_usage_stats'))
final_columns = list(result)
print(f"\n最終欄位列表 (共 {len(final_columns)} 個):")
for col in final_columns:
print(f" - {col[0]:25} {col[1]:20} NULL={col[2]} Default={col[4]}")
# 10. 統計資料
print("\n" + "=" * 60)
print("資料統計")
print("=" * 60)
total_records = db.session.execute(
text('SELECT COUNT(*) FROM dt_api_usage_stats')
).scalar()
print(f"總記錄數: {total_records}")
if total_records > 0:
stats = db.session.execute(text('''
SELECT
api_endpoint,
COUNT(*) as count,
SUM(total_tokens) as total_tokens,
SUM(cost) as total_cost
FROM dt_api_usage_stats
GROUP BY api_endpoint
''')).fetchall()
print("\nAPI 使用統計:")
for stat in stats:
print(f" {stat[0]:40} | {stat[1]:5} 次 | {stat[2]:10} tokens | ${stat[3]:.4f}")
print("\n" + "=" * 60)
print("✅ Migration 執行完成!")
print("=" * 60)
except Exception as e:
db.session.rollback()
print(f"\n❌ Migration 失敗: {str(e)}")
print("\n可以使用備份表還原資料:")
print(" DROP TABLE dt_api_usage_stats;")
print(" CREATE TABLE dt_api_usage_stats AS SELECT * FROM dt_api_usage_stats_backup_20251001;")
raise
if __name__ == '__main__':
run_migration()

View File

@@ -0,0 +1,36 @@
-- 修正認證系統架構
-- 方案 A: 保留 dt_users 的 username 和 display_name都使用 API 回傳的 name (姓名+email)
-- 使用 email 作為主要唯一識別碼sys_user 表專門記錄登入資訊
-- Created: 2025-10-01
-- 1. 確保 dt_users 表的 email 唯一約束
-- 先檢查是否有重複的 email如果有則需要手動處理
-- 因為有外鍵約束,不能直接刪除
-- 先顯示重複的 email 記錄讓管理員確認
-- SELECT email, COUNT(*) as count FROM dt_users GROUP BY email HAVING COUNT(*) > 1;
-- 添加 email 唯一約束
ALTER TABLE dt_users
ADD CONSTRAINT uk_dt_users_email UNIQUE (email);
-- 2. 調整現有欄位註解,說明新的使用方式
ALTER TABLE dt_users
MODIFY COLUMN username VARCHAR(255) NOT NULL COMMENT 'API name (姓名+email格式)',
MODIFY COLUMN email VARCHAR(255) NOT NULL COMMENT '電子郵件 (主要識別鍵)';
-- 3. 保持 sys_user 表結構,但調整為專門記錄登入資訊
-- sys_user 表通過 email 與 dt_users 關聯
-- (保留現有的 sys_user 表,因為它是專門用於登入記錄)
-- 4. 重新命名 login_logs 為 dt_login_logs (配合專案命名規則)
RENAME TABLE login_logs TO dt_login_logs;
-- 5. 更新 dt_login_logs 表結構 (配合 dt_users 的主鍵)
ALTER TABLE dt_login_logs
ADD COLUMN user_id INT COMMENT '關聯到 dt_users.id',
ADD INDEX idx_user_id (user_id),
ADD FOREIGN KEY fk_dt_login_logs_user_id (user_id) REFERENCES dt_users(id) ON DELETE SET NULL;
-- 6. 建立使用者識別索引 (支援 email 和 username 快速查詢)
ALTER TABLE dt_users
ADD INDEX idx_username_email (username, email);

View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
修復 dt_translation_cache 資料表結構
問題:資料表欄位名稱與模型定義不一致
- content_hash → source_text_hash
- source_lang → source_language
- target_lang → target_language
"""
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from app import create_app, db
from sqlalchemy import text
def run_migration():
app = create_app()
with app.app_context():
print("=" * 60)
print("修復 dt_translation_cache 資料表結構")
print("=" * 60)
try:
# 1. 檢查當前結構
print("\n[1/6] 檢查當前資料表結構...")
result = db.session.execute(text('SHOW COLUMNS FROM dt_translation_cache'))
current_columns = {row[0]: row for row in result}
print(f" ✓ 當前欄位: {', '.join(current_columns.keys())}")
# 2. 備份資料
print("\n[2/6] 備份現有資料...")
db.session.execute(text('''
CREATE TABLE IF NOT EXISTS dt_translation_cache_backup_20251001
AS SELECT * FROM dt_translation_cache
'''))
db.session.commit()
backup_count = db.session.execute(
text('SELECT COUNT(*) FROM dt_translation_cache_backup_20251001')
).scalar()
print(f" ✓ 已備份 {backup_count} 筆記錄")
# 3. 重新命名欄位content_hash → source_text_hash
if 'content_hash' in current_columns and 'source_text_hash' not in current_columns:
print("\n[3/6] 重新命名 content_hash → source_text_hash...")
db.session.execute(text('''
ALTER TABLE dt_translation_cache
CHANGE COLUMN content_hash source_text_hash VARCHAR(64) NOT NULL COMMENT '來源文字hash'
'''))
db.session.commit()
print(" ✓ 已重新命名")
else:
print("\n[3/6] 跳過(已經是 source_text_hash")
# 4. 重新命名欄位source_lang → source_language
if 'source_lang' in current_columns and 'source_language' not in current_columns:
print("\n[4/6] 重新命名 source_lang → source_language...")
db.session.execute(text('''
ALTER TABLE dt_translation_cache
CHANGE COLUMN source_lang source_language VARCHAR(50) NOT NULL COMMENT '來源語言'
'''))
db.session.commit()
print(" ✓ 已重新命名")
else:
print("\n[4/6] 跳過(已經是 source_language")
# 5. 重新命名欄位target_lang → target_language
if 'target_lang' in current_columns and 'target_language' not in current_columns:
print("\n[5/6] 重新命名 target_lang → target_language...")
db.session.execute(text('''
ALTER TABLE dt_translation_cache
CHANGE COLUMN target_lang target_language VARCHAR(50) NOT NULL COMMENT '目標語言'
'''))
db.session.commit()
print(" ✓ 已重新命名")
else:
print("\n[5/6] 跳過(已經是 target_language")
# 6. 刪除不需要的欄位
print("\n[6/6] 清理多餘欄位...")
# 檢查並刪除 quality_score
if 'quality_score' in current_columns:
db.session.execute(text('''
ALTER TABLE dt_translation_cache
DROP COLUMN quality_score
'''))
print(" ✓ 已刪除 quality_score")
# 檢查並刪除 hit_count
if 'hit_count' in current_columns:
db.session.execute(text('''
ALTER TABLE dt_translation_cache
DROP COLUMN hit_count
'''))
print(" ✓ 已刪除 hit_count")
# 檢查並刪除 last_used_at
if 'last_used_at' in current_columns:
db.session.execute(text('''
ALTER TABLE dt_translation_cache
DROP COLUMN last_used_at
'''))
print(" ✓ 已刪除 last_used_at")
db.session.commit()
# 7. 重建索引和約束
print("\n[7/7] 重建索引和約束...")
# 先刪除舊的唯一約束(如果存在)
try:
db.session.execute(text('''
ALTER TABLE dt_translation_cache
DROP INDEX idx_content_hash
'''))
print(" ✓ 已刪除舊索引 idx_content_hash")
except:
pass
try:
db.session.execute(text('''
ALTER TABLE dt_translation_cache
DROP INDEX idx_source_lang_target_lang
'''))
print(" ✓ 已刪除舊索引 idx_source_lang_target_lang")
except:
pass
# 建立新的唯一約束
try:
db.session.execute(text('''
ALTER TABLE dt_translation_cache
ADD UNIQUE KEY uk_cache (source_text_hash, source_language, target_language)
'''))
print(" ✓ 已建立唯一約束 uk_cache")
except Exception as e:
if 'Duplicate' not in str(e):
print(f" ⚠ 約束建立警告: {str(e)}")
# 建立語言索引
try:
db.session.execute(text('''
ALTER TABLE dt_translation_cache
ADD INDEX idx_languages (source_language, target_language)
'''))
print(" ✓ 已建立索引 idx_languages")
except Exception as e:
if 'Duplicate' not in str(e):
print(f" ⚠ 索引建立警告: {str(e)}")
db.session.commit()
# 驗證最終結構
print("\n" + "=" * 60)
print("驗證最終資料表結構")
print("=" * 60)
result = db.session.execute(text('SHOW COLUMNS FROM dt_translation_cache'))
final_columns = list(result)
print(f"\n最終欄位列表 (共 {len(final_columns)} 個):")
for col in final_columns:
print(f" - {col[0]:30} {col[1]:30} NULL={col[2]}")
# 顯示索引
print("\n索引:")
result = db.session.execute(text('SHOW INDEX FROM dt_translation_cache'))
for idx in result:
print(f" - {idx[2]:30} -> {idx[4]}")
print("\n" + "=" * 60)
print("✅ Migration 執行完成!")
print("=" * 60)
except Exception as e:
db.session.rollback()
print(f"\n❌ Migration 失敗: {str(e)}")
print("\n可以使用備份表還原資料:")
print(" DROP TABLE dt_translation_cache;")
print(" CREATE TABLE dt_translation_cache AS SELECT * FROM dt_translation_cache_backup_20251001;")
raise
if __name__ == '__main__':
run_migration()

View File

@@ -0,0 +1,19 @@
-- 合併重複的使用者記錄
-- 保留 ID=3 的記錄 (較新且有較多關聯資料)
-- 將 ID=1 的關聯資料轉移到 ID=3然後刪除 ID=1
-- 1. 將 ID=1 的 system_logs 轉移到 ID=3
UPDATE dt_system_logs SET user_id = 3 WHERE user_id = 1;
-- 2. 確認沒有其他關聯資料需要轉移
-- (dt_translation_jobs, dt_api_usage_stats 都已經在 ID=3)
-- 3. 刪除重複的記錄 ID=1
DELETE FROM dt_users WHERE id = 1;
-- 4. 驗證結果
SELECT 'After merge:' as status;
SELECT id, username, display_name, email FROM dt_users WHERE email = 'ymirliu@panjit.com.tw';
SELECT 'Jobs:', COUNT(*) FROM dt_translation_jobs WHERE user_id = 3;
SELECT 'Logs:', COUNT(*) FROM dt_system_logs WHERE user_id = 3;
SELECT 'Stats:', COUNT(*) FROM dt_api_usage_stats WHERE user_id = 3;