Compare commits

...

2 Commits

Author SHA1 Message Date
donald
1568a12a96 fix: Add localhost host binding to Vite config and index fix script
- Add host: 'localhost' to vite.config.js to ensure consistent IP
- Add scripts/fix-indexes.js for database index verification
- Add routes/llmTest.js for LLM testing endpoint

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 18:28:29 +08:00
donald
f9ee43b73c feat: Add 5Why_ prefix to all database tables
- Rename all tables with 5Why_ prefix for namespace isolation
- Update models: User.js, Analysis.js, AuditLog.js
- Update routes: llmConfig.js
- Update scripts: seed-test-users.js, add-deepseek-config.js, add-ollama-config.js
- Add migrate-table-prefix.js script for database migration
- Update db_schema.sql with new table names
- Update views: 5Why_user_analysis_stats, 5Why_recent_analyses

Tables renamed:
- users -> 5Why_users
- analyses -> 5Why_analyses
- analysis_perspectives -> 5Why_analysis_perspectives
- analysis_whys -> 5Why_analysis_whys
- llm_configs -> 5Why_llm_configs
- system_settings -> 5Why_system_settings
- audit_logs -> 5Why_audit_logs
- sessions -> 5Why_sessions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 18:19:53 +08:00
12 changed files with 976 additions and 62 deletions

227
docs/db_schema.sql Normal file
View File

@@ -0,0 +1,227 @@
-- 5 Why Analyzer Database Schema
-- Database: db_A102
-- Version: 1.1.0
-- Created: 2025-12-05
-- Updated: 2025-12-09 (Added 5Why_ prefix to all tables)
USE db_A102;
-- ============================================
-- Table: 5Why_users (使用者資料表)
-- ============================================
CREATE TABLE IF NOT EXISTS 5Why_users (
id INT AUTO_INCREMENT PRIMARY KEY,
employee_id VARCHAR(50) UNIQUE NOT NULL COMMENT '工號',
username VARCHAR(100) NOT NULL COMMENT '使用者名稱',
email VARCHAR(255) UNIQUE NOT NULL COMMENT 'Email',
password_hash VARCHAR(255) NOT NULL COMMENT '密碼雜湊',
role ENUM('user', 'admin', 'super_admin') DEFAULT 'user' COMMENT '權限等級',
department VARCHAR(100) COMMENT '部門',
position VARCHAR(100) COMMENT '職位',
is_active BOOLEAN DEFAULT TRUE COMMENT '帳號啟用狀態',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
last_login_at TIMESTAMP NULL COMMENT '最後登入時間',
INDEX idx_employee_id (employee_id),
INDEX idx_email (email),
INDEX idx_role (role)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='使用者資料表';
-- ============================================
-- Table: 5Why_analyses (分析記錄表)
-- ============================================
CREATE TABLE IF NOT EXISTS 5Why_analyses (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL COMMENT '使用者ID',
finding TEXT NOT NULL COMMENT 'Finding描述',
job_content TEXT NOT NULL COMMENT '工作內容',
output_language VARCHAR(10) NOT NULL DEFAULT 'zh-TW' COMMENT '輸出語言',
problem_restatement TEXT COMMENT '問題重述5W1H',
analysis_result JSON COMMENT '完整分析結果JSON格式',
status ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending' COMMENT '分析狀態',
error_message TEXT COMMENT '錯誤訊息',
processing_time INT COMMENT '處理時間(秒)',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
FOREIGN KEY (user_id) REFERENCES 5Why_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='分析記錄表';
-- ============================================
-- Table: 5Why_analysis_perspectives (分析角度詳細表)
-- ============================================
CREATE TABLE IF NOT EXISTS 5Why_analysis_perspectives (
id INT AUTO_INCREMENT PRIMARY KEY,
analysis_id INT NOT NULL COMMENT '分析記錄ID',
perspective VARCHAR(100) NOT NULL COMMENT '分析角度(流程面、系統面等)',
perspective_icon VARCHAR(10) COMMENT 'Emoji圖示',
root_cause TEXT COMMENT '根本原因',
permanent_solution TEXT COMMENT '永久性對策',
logic_check_forward TEXT COMMENT '順向邏輯檢核',
logic_check_backward TEXT COMMENT '逆向邏輯檢核',
logic_valid BOOLEAN DEFAULT TRUE COMMENT '邏輯是否有效',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
FOREIGN KEY (analysis_id) REFERENCES 5Why_analyses(id) ON DELETE CASCADE,
INDEX idx_analysis_id (analysis_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='分析角度詳細表';
-- ============================================
-- Table: 5Why_analysis_whys (5 Why詳細記錄表)
-- ============================================
CREATE TABLE IF NOT EXISTS 5Why_analysis_whys (
id INT AUTO_INCREMENT PRIMARY KEY,
perspective_id INT NOT NULL COMMENT '分析角度ID',
level INT NOT NULL COMMENT 'Why層級1-5',
question TEXT NOT NULL COMMENT '問題',
answer TEXT NOT NULL COMMENT '答案',
is_verified BOOLEAN DEFAULT FALSE COMMENT '是否已驗證',
verification_note TEXT COMMENT '驗證說明',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
FOREIGN KEY (perspective_id) REFERENCES 5Why_analysis_perspectives(id) ON DELETE CASCADE,
INDEX idx_perspective_id (perspective_id),
INDEX idx_level (level)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='5 Why詳細記錄表';
-- ============================================
-- Table: 5Why_llm_configs (LLM API配置表)
-- ============================================
CREATE TABLE IF NOT EXISTS 5Why_llm_configs (
id INT AUTO_INCREMENT PRIMARY KEY,
provider VARCHAR(50) NOT NULL COMMENT 'LLM提供商ollama, gemini, deepseek, openai',
api_url VARCHAR(255) COMMENT 'API URL',
api_key VARCHAR(255) COMMENT 'API Key加密儲存',
model_name VARCHAR(100) COMMENT '模型名稱',
is_active BOOLEAN DEFAULT FALSE COMMENT '是否啟用',
max_tokens INT DEFAULT 6000 COMMENT '最大Token數',
temperature DECIMAL(3,2) DEFAULT 0.7 COMMENT '溫度參數',
timeout INT DEFAULT 120000 COMMENT 'Timeout毫秒',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
created_by INT COMMENT '建立者ID',
FOREIGN KEY (created_by) REFERENCES 5Why_users(id) ON DELETE SET NULL,
INDEX idx_provider (provider),
INDEX idx_is_active (is_active),
UNIQUE KEY unique_active_provider (provider, is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='LLM API配置表';
-- ============================================
-- Table: 5Why_system_settings (系統設定表)
-- ============================================
CREATE TABLE IF NOT EXISTS 5Why_system_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
setting_key VARCHAR(100) UNIQUE NOT NULL COMMENT '設定鍵',
setting_value TEXT COMMENT '設定值',
setting_type VARCHAR(50) DEFAULT 'string' COMMENT '設定類型string, number, boolean, json',
description TEXT COMMENT '說明',
is_public BOOLEAN DEFAULT FALSE COMMENT '是否公開(前端可見)',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
updated_by INT COMMENT '更新者ID',
FOREIGN KEY (updated_by) REFERENCES 5Why_users(id) ON DELETE SET NULL,
INDEX idx_setting_key (setting_key),
INDEX idx_is_public (is_public)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系統設定表';
-- ============================================
-- Table: 5Why_audit_logs (稽核日誌表)
-- ============================================
CREATE TABLE IF NOT EXISTS 5Why_audit_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT COMMENT '使用者ID',
action VARCHAR(100) NOT NULL COMMENT '動作login, logout, create_analysis, update_user等',
entity_type VARCHAR(50) COMMENT '實體類型user, analysis, llm_config等',
entity_id INT COMMENT '實體ID',
old_value JSON COMMENT '舊值',
new_value JSON COMMENT '新值',
ip_address VARCHAR(45) COMMENT 'IP位址',
user_agent TEXT COMMENT 'User Agent',
status ENUM('success', 'failed') DEFAULT 'success' COMMENT '執行狀態',
error_message TEXT COMMENT '錯誤訊息',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
FOREIGN KEY (user_id) REFERENCES 5Why_users(id) ON DELETE SET NULL,
INDEX idx_user_id (user_id),
INDEX idx_action (action),
INDEX idx_created_at (created_at),
INDEX idx_entity (entity_type, entity_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='稽核日誌表';
-- ============================================
-- Table: 5Why_sessions (Session表)
-- ============================================
CREATE TABLE IF NOT EXISTS 5Why_sessions (
session_id VARCHAR(128) PRIMARY KEY,
expires BIGINT UNSIGNED NOT NULL,
data TEXT,
INDEX idx_expires (expires)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Session表';
-- ============================================
-- Insert Default Data
-- ============================================
-- 預設管理者帳號(密碼: Admin@123456
INSERT INTO 5Why_users (employee_id, username, email, password_hash, role, department, position)
VALUES
('ADMIN001', 'admin', 'admin@example.com', '$2a$10$YourBcryptHashHere', 'super_admin', 'IT', 'System Administrator'),
('USER001', 'user001', 'user001@example.com', '$2a$10$YourBcryptHashHere', 'user', 'Manufacturing', 'Engineer'),
('USER002', 'user002', 'user002@example.com', '$2a$10$YourBcryptHashHere', 'admin', 'Quality', 'QA Manager')
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP;
-- 預設 Ollama LLM 配置
INSERT INTO 5Why_llm_configs (provider, api_url, model_name, is_active, max_tokens, temperature, timeout)
VALUES
('ollama', 'https://ollama_pjapi.theaken.com', 'qwen2.5:3b', TRUE, 6000, 0.7, 120000)
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP;
-- 預設系統設定
INSERT INTO 5Why_system_settings (setting_key, setting_value, setting_type, description, is_public)
VALUES
('app_name', '5 Why Root Cause Analyzer', 'string', '應用程式名稱', TRUE),
('app_version', '1.0.0', 'string', '應用程式版本', TRUE),
('max_analysis_per_day', '50', 'number', '每日最大分析次數', FALSE),
('enable_email_notification', 'false', 'boolean', '啟用Email通知', FALSE),
('default_language', 'zh-TW', 'string', '預設語言', TRUE),
('supported_languages', '["zh-TW","zh-CN","en","ja","ko","vi","th"]', 'json', '支援的語言列表', TRUE)
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP;
-- ============================================
-- Create Views (視圖)
-- ============================================
-- 使用者分析統計視圖
CREATE OR REPLACE VIEW 5Why_user_analysis_stats AS
SELECT
u.id AS user_id,
u.username,
u.employee_id,
u.department,
COUNT(a.id) AS total_analyses,
COUNT(CASE WHEN a.status = 'completed' THEN 1 END) AS completed_analyses,
COUNT(CASE WHEN a.status = 'failed' THEN 1 END) AS failed_analyses,
AVG(a.processing_time) AS avg_processing_time,
MAX(a.created_at) AS last_analysis_at
FROM 5Why_users u
LEFT JOIN 5Why_analyses a ON u.id = a.user_id
GROUP BY u.id, u.username, u.employee_id, u.department;
-- 最近分析記錄視圖
CREATE OR REPLACE VIEW 5Why_recent_analyses AS
SELECT
a.id,
a.finding,
u.username,
u.employee_id,
a.output_language,
a.status,
a.processing_time,
a.created_at
FROM 5Why_analyses a
JOIN 5Why_users u ON a.user_id = u.id
ORDER BY a.created_at DESC
LIMIT 100;
-- ============================================
-- End of Schema
-- ============================================

View File

@@ -13,7 +13,7 @@ class Analysis {
try {
const [result] = await pool.execute(
`INSERT INTO analyses (user_id, finding, job_content, output_language, status)
`INSERT INTO 5Why_analyses (user_id, finding, job_content, output_language, status)
VALUES (?, ?, ?, ?, 'pending')`,
[user_id, finding, job_content, output_language]
);
@@ -30,7 +30,7 @@ class Analysis {
static async findById(id) {
try {
const [rows] = await pool.execute(
'SELECT * FROM analyses WHERE id = ?',
'SELECT * FROM 5Why_analyses WHERE id = ?',
[id]
);
return rows[0] || null;
@@ -45,7 +45,7 @@ class Analysis {
static async updateStatus(id, status, errorMessage = null) {
try {
await pool.execute(
'UPDATE analyses SET status = ?, error_message = ? WHERE id = ?',
'UPDATE 5Why_analyses SET status = ?, error_message = ? WHERE id = ?',
[status, errorMessage, id]
);
} catch (error) {
@@ -66,7 +66,7 @@ class Analysis {
try {
// 更新主分析記錄
await connection.execute(
`UPDATE analyses
`UPDATE 5Why_analyses
SET problem_restatement = ?, analysis_result = ?, processing_time = ?, status = 'completed'
WHERE id = ?`,
[problem_restatement, JSON.stringify(analysis_result), processing_time, id]
@@ -76,7 +76,7 @@ class Analysis {
if (analysis_result.analyses && Array.isArray(analysis_result.analyses)) {
for (const perspective of analysis_result.analyses) {
const [perspectiveResult] = await connection.execute(
`INSERT INTO analysis_perspectives
`INSERT INTO 5Why_analysis_perspectives
(analysis_id, perspective, perspective_icon, root_cause, permanent_solution,
logic_check_forward, logic_check_backward, logic_valid)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -99,7 +99,7 @@ class Analysis {
for (const why of perspective.whys) {
if (why && why.level) {
await connection.execute(
`INSERT INTO analysis_whys
`INSERT INTO 5Why_analysis_whys
(perspective_id, level, question, answer, is_verified, verification_note)
VALUES (?, ?, ?, ?, ?, ?)`,
[
@@ -136,8 +136,8 @@ class Analysis {
*/
static async getByUserId(userId, page = 1, limit = 10, filters = {}) {
const offset = (page - 1) * limit;
let query = 'SELECT * FROM analyses WHERE user_id = ?';
let countQuery = 'SELECT COUNT(*) as total FROM analyses WHERE user_id = ?';
let query = 'SELECT * FROM 5Why_analyses WHERE user_id = ?';
let countQuery = 'SELECT COUNT(*) as total FROM 5Why_analyses WHERE user_id = ?';
const whereParams = [userId];
const whereClauses = [];
@@ -168,8 +168,9 @@ class Analysis {
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
try {
const [rows] = await pool.execute(query, [...whereParams, limit, offset]);
const [countResult] = await pool.execute(countQuery, whereParams);
// 使用 pool.query 而非 pool.execute因為 LIMIT/OFFSET 需要數字類型
const [rows] = await pool.query(query, [...whereParams, parseInt(limit), parseInt(offset)]);
const [countResult] = await pool.query(countQuery, whereParams);
return {
data: rows,
@@ -192,10 +193,10 @@ class Analysis {
const offset = (page - 1) * limit;
let query = `
SELECT a.*, u.username, u.employee_id
FROM analyses a
JOIN users u ON a.user_id = u.id
FROM 5Why_analyses a
JOIN 5Why_users u ON a.user_id = u.id
`;
let countQuery = 'SELECT COUNT(*) as total FROM analyses a JOIN users u ON a.user_id = u.id';
let countQuery = 'SELECT COUNT(*) as total FROM 5Why_analyses a JOIN 5Why_users u ON a.user_id = u.id';
const whereParams = [];
const whereClauses = [];
@@ -223,8 +224,9 @@ class Analysis {
query += ' ORDER BY a.created_at DESC LIMIT ? OFFSET ?';
try {
const [rows] = await pool.execute(query, [...whereParams, limit, offset]);
const [countResult] = await pool.execute(countQuery, whereParams);
// 使用 pool.query 而非 pool.execute因為 LIMIT/OFFSET 需要數字類型
const [rows] = await pool.query(query, [...whereParams, parseInt(limit), parseInt(offset)]);
const [countResult] = await pool.query(countQuery, whereParams);
return {
data: rows,
@@ -251,14 +253,14 @@ class Analysis {
// 取得分析角度
const [perspectives] = await pool.execute(
'SELECT * FROM analysis_perspectives WHERE analysis_id = ? ORDER BY id',
'SELECT * FROM 5Why_analysis_perspectives WHERE analysis_id = ? ORDER BY id',
[id]
);
// 為每個角度取得 Whys
for (const perspective of perspectives) {
const [whys] = await pool.execute(
'SELECT * FROM analysis_whys WHERE perspective_id = ? ORDER BY level',
'SELECT * FROM 5Why_analysis_whys WHERE perspective_id = ? ORDER BY level',
[perspective.id]
);
perspective.whys = whys;
@@ -277,7 +279,7 @@ class Analysis {
*/
static async delete(id) {
try {
await pool.execute('DELETE FROM analyses WHERE id = ?', [id]);
await pool.execute('DELETE FROM 5Why_analyses WHERE id = ?', [id]);
return true;
} catch (error) {
throw new Error(`Error deleting analysis: ${error.message}`);
@@ -289,9 +291,10 @@ class Analysis {
*/
static async getRecent(limit = 100) {
try {
const [rows] = await pool.execute(
'SELECT * FROM recent_analyses LIMIT ?',
[limit]
// 使用 pool.query 而非 pool.execute因為 LIMIT 需要數字類型
const [rows] = await pool.query(
'SELECT * FROM 5Why_recent_analyses LIMIT ?',
[parseInt(limit)]
);
return rows;
} catch (error) {
@@ -312,7 +315,7 @@ class Analysis {
COUNT(CASE WHEN status = 'processing' THEN 1 END) as processing,
AVG(processing_time) as avg_processing_time,
MAX(created_at) as last_analysis_at
FROM analyses
FROM 5Why_analyses
`;
const params = [];

View File

@@ -24,7 +24,7 @@ class AuditLog {
try {
await pool.execute(
`INSERT INTO audit_logs
`INSERT INTO 5Why_audit_logs
(user_id, action, entity_type, entity_id, old_value, new_value,
ip_address, user_agent, status, error_message)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -126,10 +126,10 @@ class AuditLog {
const offset = (page - 1) * limit;
let query = `
SELECT al.*, u.username, u.employee_id
FROM audit_logs al
LEFT JOIN users u ON al.user_id = u.id
FROM 5Why_audit_logs al
LEFT JOIN 5Why_users u ON al.user_id = u.id
`;
let countQuery = 'SELECT COUNT(*) as total FROM audit_logs al';
let countQuery = 'SELECT COUNT(*) as total FROM 5Why_audit_logs al';
const params = [];
const whereClauses = [];
@@ -166,11 +166,12 @@ class AuditLog {
}
query += ' ORDER BY al.created_at DESC LIMIT ? OFFSET ?';
params.push(limit, offset);
params.push(parseInt(limit), parseInt(offset));
try {
const [rows] = await pool.execute(query, params);
const [countResult] = await pool.execute(countQuery, params.slice(0, -2));
// 使用 pool.query 而非 pool.execute因為 LIMIT/OFFSET 需要數字類型
const [rows] = await pool.query(query, params);
const [countResult] = await pool.query(countQuery, params.slice(0, -2));
return {
data: rows,
@@ -199,7 +200,7 @@ class AuditLog {
static async cleanup(daysToKeep = 90) {
try {
const [result] = await pool.execute(
'DELETE FROM audit_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)',
'DELETE FROM 5Why_audit_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)',
[daysToKeep]
);
return result.affectedRows;

View File

@@ -12,7 +12,7 @@ class User {
static async findById(id) {
try {
const [rows] = await pool.execute(
'SELECT id, employee_id, username, email, role, department, position, is_active, created_at, last_login_at FROM users WHERE id = ?',
'SELECT id, employee_id, username, email, role, department, position, is_active, created_at, last_login_at FROM 5Why_users WHERE id = ?',
[id]
);
return rows[0] || null;
@@ -27,7 +27,7 @@ class User {
static async findByEmail(email) {
try {
const [rows] = await pool.execute(
'SELECT * FROM users WHERE email = ? AND is_active = 1',
'SELECT * FROM 5Why_users WHERE email = ? AND is_active = 1',
[email]
);
return rows[0] || null;
@@ -42,7 +42,7 @@ class User {
static async findByEmployeeId(employeeId) {
try {
const [rows] = await pool.execute(
'SELECT * FROM users WHERE employee_id = ? AND is_active = 1',
'SELECT * FROM 5Why_users WHERE employee_id = ? AND is_active = 1',
[employeeId]
);
return rows[0] || null;
@@ -69,7 +69,7 @@ class User {
const passwordHash = await bcrypt.hash(password, 10);
const [result] = await pool.execute(
`INSERT INTO users (employee_id, username, email, password_hash, role, department, position)
`INSERT INTO 5Why_users (employee_id, username, email, password_hash, role, department, position)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[employee_id, username, email, passwordHash, role, department, position]
);
@@ -91,7 +91,7 @@ class User {
try {
await pool.execute(
`UPDATE users
`UPDATE 5Why_users
SET username = ?, email = ?, role = ?, department = ?, position = ?, is_active = ?
WHERE id = ?`,
[username, email, role, department, position, is_active, id]
@@ -110,7 +110,7 @@ class User {
try {
const passwordHash = await bcrypt.hash(newPassword, 10);
await pool.execute(
'UPDATE users SET password_hash = ? WHERE id = ?',
'UPDATE 5Why_users SET password_hash = ? WHERE id = ?',
[passwordHash, id]
);
return true;
@@ -125,7 +125,7 @@ class User {
static async updateLastLogin(id) {
try {
await pool.execute(
'UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE id = ?',
'UPDATE 5Why_users SET last_login_at = CURRENT_TIMESTAMP WHERE id = ?',
[id]
);
} catch (error) {
@@ -138,8 +138,8 @@ class User {
*/
static async getAll(page = 1, limit = 10, filters = {}) {
const offset = (page - 1) * limit;
let query = 'SELECT id, employee_id, username, email, role, department, position, is_active, created_at FROM users';
let countQuery = 'SELECT COUNT(*) as total FROM users';
let query = 'SELECT id, employee_id, username, email, role, department, position, is_active, created_at FROM 5Why_users';
let countQuery = 'SELECT COUNT(*) as total FROM 5Why_users';
const whereParams = [];
const whereClauses = [];
@@ -167,8 +167,9 @@ class User {
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
try {
const [rows] = await pool.execute(query, [...whereParams, limit, offset]);
const [countResult] = await pool.execute(countQuery, whereParams);
// 使用 query 而非 execute因為 LIMIT 和 OFFSET 參數需要數字類型
const [rows] = await pool.query(query, [...whereParams, parseInt(limit), parseInt(offset)]);
const [countResult] = await pool.query(countQuery, whereParams);
return {
data: rows,
@@ -190,7 +191,7 @@ class User {
static async delete(id) {
try {
await pool.execute(
'UPDATE users SET is_active = 0 WHERE id = ?',
'UPDATE 5Why_users SET is_active = 0 WHERE id = ?',
[id]
);
return true;
@@ -204,7 +205,7 @@ class User {
*/
static async hardDelete(id) {
try {
await pool.execute('DELETE FROM users WHERE id = ?', [id]);
await pool.execute('DELETE FROM 5Why_users WHERE id = ?', [id]);
return true;
} catch (error) {
throw new Error(`Error hard deleting user: ${error.message}`);
@@ -217,7 +218,7 @@ class User {
static async getStats(userId) {
try {
const [rows] = await pool.execute(
'SELECT * FROM user_analysis_stats WHERE user_id = ?',
'SELECT * FROM 5Why_user_analysis_stats WHERE user_id = ?',
[userId]
);
return rows[0] || null;

View File

@@ -13,7 +13,7 @@ const router = express.Router();
router.get('/', requireAuth, asyncHandler(async (req, res) => {
const configs = await query(
`SELECT id, provider, api_url, model_name, is_active, created_at, updated_at
FROM llm_configs
FROM 5Why_llm_configs
ORDER BY is_active DESC, created_at DESC`
);
@@ -30,7 +30,7 @@ router.get('/', requireAuth, asyncHandler(async (req, res) => {
router.get('/active', requireAuth, asyncHandler(async (req, res) => {
const [config] = await query(
`SELECT id, provider, api_url, model_name, temperature, max_tokens, timeout
FROM llm_configs
FROM 5Why_llm_configs
WHERE is_active = 1
LIMIT 1`
);
@@ -72,7 +72,7 @@ router.post('/', requireSuperAdmin, asyncHandler(async (req, res) => {
}
const result = await query(
`INSERT INTO llm_configs
`INSERT INTO 5Why_llm_configs
(provider, api_url, api_key, model_name, temperature, max_tokens, timeout)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
@@ -128,7 +128,7 @@ router.put('/:id', requireSuperAdmin, asyncHandler(async (req, res) => {
}
// 檢查配置是否存在
const [existing] = await query('SELECT id FROM llm_configs WHERE id = ?', [configId]);
const [existing] = await query('SELECT id FROM 5Why_llm_configs WHERE id = ?', [configId]);
if (!existing) {
return res.status(404).json({
success: false,
@@ -137,7 +137,7 @@ router.put('/:id', requireSuperAdmin, asyncHandler(async (req, res) => {
}
await query(
`UPDATE llm_configs
`UPDATE 5Why_llm_configs
SET provider = ?, api_url = ?, api_key = ?, model_name = ?,
temperature = ?, max_tokens = ?, timeout = ?, updated_at = NOW()
WHERE id = ?`,
@@ -178,7 +178,7 @@ router.put('/:id/activate', requireSuperAdmin, asyncHandler(async (req, res) =>
const configId = parseInt(req.params.id);
// 檢查配置是否存在
const [existing] = await query('SELECT id, provider FROM llm_configs WHERE id = ?', [configId]);
const [existing] = await query('SELECT id, provider FROM 5Why_llm_configs WHERE id = ?', [configId]);
if (!existing) {
return res.status(404).json({
success: false,
@@ -187,10 +187,10 @@ router.put('/:id/activate', requireSuperAdmin, asyncHandler(async (req, res) =>
}
// 先停用所有配置
await query('UPDATE llm_configs SET is_active = 0');
await query('UPDATE 5Why_llm_configs SET is_active = 0');
// 啟用指定配置
await query('UPDATE llm_configs SET is_active = 1, updated_at = NOW() WHERE id = ?', [configId]);
await query('UPDATE 5Why_llm_configs SET is_active = 1, updated_at = NOW() WHERE id = ?', [configId]);
// 記錄稽核日誌
await AuditLog.logUpdate(
@@ -217,7 +217,7 @@ router.delete('/:id', requireSuperAdmin, asyncHandler(async (req, res) => {
const configId = parseInt(req.params.id);
// 檢查是否為啟用中的配置
const [existing] = await query('SELECT is_active FROM llm_configs WHERE id = ?', [configId]);
const [existing] = await query('SELECT is_active FROM 5Why_llm_configs WHERE id = ?', [configId]);
if (!existing) {
return res.status(404).json({
success: false,
@@ -232,7 +232,7 @@ router.delete('/:id', requireSuperAdmin, asyncHandler(async (req, res) => {
});
}
await query('DELETE FROM llm_configs WHERE id = ?', [configId]);
await query('DELETE FROM 5Why_llm_configs WHERE id = ?', [configId]);
// 記錄稽核日誌
await AuditLog.logDelete(

219
routes/llmTest.js Normal file
View File

@@ -0,0 +1,219 @@
import express from 'express';
import { asyncHandler } from '../middleware/errorHandler.js';
import { requireAuth, requireAdmin } from '../middleware/auth.js';
const router = express.Router();
/**
* GET /api/llm-test/models
* 列出可用的 LLM 模型(從 API 動態獲取)
*/
router.get('/models', requireAuth, asyncHandler(async (req, res) => {
const { api_url } = req.query;
const targetUrl = api_url || 'https://ollama_pjapi.theaken.com';
try {
const axios = (await import('axios')).default;
const response = await axios.get(`${targetUrl}/v1/models`, {
timeout: 10000
});
if (response.data && response.data.data) {
const models = response.data.data.map(model => ({
id: model.id,
name: model.info?.name || model.id,
description: model.info?.description || '',
best_for: model.info?.best_for || '',
owned_by: model.owned_by || 'unknown'
}));
res.json({
success: true,
data: models
});
} else {
throw new Error('Invalid response format');
}
} catch (error) {
res.status(500).json({
success: false,
error: '無法取得模型列表',
message: error.message
});
}
}));
/**
* POST /api/llm-test/quick
* 快速測試 LLM 連線(僅管理員)
*/
router.post('/quick', requireAdmin, asyncHandler(async (req, res) => {
const { api_url, api_key, model_name } = req.body;
if (!api_url || !model_name) {
return res.status(400).json({
success: false,
error: '請提供 API 端點和模型名稱'
});
}
try {
const axios = (await import('axios')).default;
const startTime = Date.now();
const response = await axios.post(
`${api_url}/v1/chat/completions`,
{
model: model_name,
messages: [
{ role: 'user', content: 'Hello, please respond with just "OK"' }
],
max_tokens: 10
},
{
timeout: 15000,
headers: {
'Content-Type': 'application/json',
...(api_key && { 'Authorization': `Bearer ${api_key}` })
}
}
);
const responseTime = Date.now() - startTime;
if (response.data && response.data.choices) {
res.json({
success: true,
message: 'LLM API 連線測試成功',
responseTime: responseTime,
response: response.data.choices[0]?.message?.content || ''
});
} else {
throw new Error('Invalid API response format');
}
} catch (error) {
res.status(500).json({
success: false,
error: 'LLM API 連線測試失敗',
message: error.message
});
}
}));
/**
* POST /api/llm-test/chat
* 直接與 LLM 對話(測試用,僅管理員)
*/
router.post('/chat', requireAdmin, asyncHandler(async (req, res) => {
const { api_url, api_key, model_name, messages, temperature, max_tokens, stream } = req.body;
if (!api_url || !model_name || !messages) {
return res.status(400).json({
success: false,
error: '請提供必要參數'
});
}
try {
const axios = (await import('axios')).default;
const startTime = Date.now();
// 非串流模式
if (!stream) {
const response = await axios.post(
`${api_url}/v1/chat/completions`,
{
model: model_name,
messages: messages,
temperature: temperature || 0.7,
max_tokens: max_tokens || 2000,
stream: false
},
{
timeout: 120000,
headers: {
'Content-Type': 'application/json',
...(api_key && { 'Authorization': `Bearer ${api_key}` })
}
}
);
const responseTime = Date.now() - startTime;
res.json({
success: true,
data: {
content: response.data.choices[0]?.message?.content || '',
usage: response.data.usage,
model: response.data.model,
responseTime: responseTime
}
});
} else {
// 串流模式 - 使用 Server-Sent Events
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const response = await axios.post(
`${api_url}/v1/chat/completions`,
{
model: model_name,
messages: messages,
temperature: temperature || 0.7,
max_tokens: max_tokens || 2000,
stream: true
},
{
timeout: 120000,
responseType: 'stream',
headers: {
'Content-Type': 'application/json',
...(api_key && { 'Authorization': `Bearer ${api_key}` })
}
}
);
response.data.on('data', (chunk) => {
const lines = chunk.toString().split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const dataStr = line.slice(6).trim();
if (dataStr && dataStr !== '[DONE]') {
try {
const data = JSON.parse(dataStr);
const content = data.choices?.[0]?.delta?.content;
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
} catch (e) {
// Skip invalid JSON
}
}
}
}
});
response.data.on('end', () => {
res.write('data: [DONE]\n\n');
res.end();
});
response.data.on('error', (error) => {
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
});
}
} catch (error) {
if (!res.headersSent) {
res.status(500).json({
success: false,
error: 'LLM 對話失敗',
message: error.message
});
}
}
}));
export default router;

View File

@@ -17,7 +17,7 @@ async function addDeepSeekConfig() {
try {
// Check if DeepSeek config already exists
const existing = await query(
`SELECT id FROM llm_configs WHERE provider_name = 'DeepSeek' LIMIT 1`
`SELECT id FROM 5Why_llm_configs WHERE provider = 'DeepSeek' LIMIT 1`
);
if (existing.length > 0) {
@@ -35,12 +35,12 @@ async function addDeepSeekConfig() {
}
// First, deactivate all existing configs
await query('UPDATE llm_configs SET is_active = 0');
await query('UPDATE 5Why_llm_configs SET is_active = 0');
// Insert DeepSeek configuration
const result = await query(
`INSERT INTO llm_configs
(provider_name, api_endpoint, api_key, model_name, temperature, max_tokens, timeout_seconds, is_active)
`INSERT INTO 5Why_llm_configs
(provider, api_url, api_key, model_name, temperature, max_tokens, timeout, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
'DeepSeek',
@@ -49,7 +49,7 @@ async function addDeepSeekConfig() {
process.env.DEEPSEEK_MODEL || 'deepseek-chat',
0.7,
6000,
120,
120000,
1 // Set as active
]
);

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env node
/**
* Add Ollama LLM Configuration
* This script adds Ollama configuration to the llm_configs table
*/
import { pool, query } from '../config.js';
import dotenv from 'dotenv';
dotenv.config();
async function addOllamaConfig() {
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(' Adding Ollama LLM Configuration');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
try {
// Check if Ollama config already exists
const existing = await query(
`SELECT id FROM 5Why_llm_configs WHERE provider = 'Ollama' LIMIT 1`
);
if (existing.length > 0) {
console.log('✅ Ollama configuration already exists (ID:', existing[0].id, ')');
console.log(' Skipping...\n');
return;
}
// First, deactivate all existing configs
await query('UPDATE 5Why_llm_configs SET is_active = 0');
// Insert Ollama configuration
const result = await query(
`INSERT INTO 5Why_llm_configs
(provider, api_url, api_key, model_name, temperature, max_tokens, timeout, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
'Ollama',
process.env.OLLAMA_API_URL || 'https://ollama_pjapi.theaken.com',
null, // Ollama doesn't need API key
process.env.OLLAMA_MODEL || 'qwen2.5:3b',
0.7,
6000,
120000,
1 // Set as active
]
);
console.log('✅ Ollama configuration added successfully!');
console.log(' Config ID:', result.insertId);
console.log(' Provider: Ollama');
console.log(' Model: qwen2.5:3b');
console.log(' API URL:', process.env.OLLAMA_API_URL || 'https://ollama_pjapi.theaken.com');
console.log(' Status: Active\n');
console.log('📝 Notes:');
console.log(' - Ollama is FREE and runs on your infrastructure');
console.log(' - No API key required');
console.log(' - Current model: qwen2.5:3b');
console.log(' - You can manage it in Admin Panel > LLM 配置\n');
} catch (error) {
console.error('❌ Error adding Ollama configuration:', error.message);
process.exit(1);
} finally {
await pool.end();
}
}
// Run the script
addOllamaConfig()
.then(() => {
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(' Configuration Complete');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
process.exit(0);
})
.catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

204
scripts/fix-indexes.js Normal file
View File

@@ -0,0 +1,204 @@
#!/usr/bin/env node
/**
* Fix Database Indexes
* Ensures all indexes exist on renamed 5Why_ tables
*
* Run: node scripts/fix-indexes.js
*/
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
const dbConfig = {
host: process.env.DB_HOST || 'mysql.theaken.com',
port: parseInt(process.env.DB_PORT) || 33306,
user: process.env.DB_USER || 'A102',
password: process.env.DB_PASSWORD || 'Bb123456',
database: process.env.DB_NAME || 'db_A102'
};
// Index definitions for each table
const indexDefinitions = {
'5Why_users': [
{ name: 'idx_employee_id', columns: 'employee_id' },
{ name: 'idx_email', columns: 'email' },
{ name: 'idx_role', columns: 'role' }
],
'5Why_analyses': [
{ name: 'idx_user_id', columns: 'user_id' },
{ name: 'idx_status', columns: 'status' },
{ name: 'idx_created_at', columns: 'created_at' }
],
'5Why_analysis_perspectives': [
{ name: 'idx_analysis_id', columns: 'analysis_id' }
],
'5Why_analysis_whys': [
{ name: 'idx_perspective_id', columns: 'perspective_id' },
{ name: 'idx_level', columns: 'level' }
],
'5Why_llm_configs': [
{ name: 'idx_provider', columns: 'provider' },
{ name: 'idx_is_active', columns: 'is_active' }
],
'5Why_system_settings': [
{ name: 'idx_setting_key', columns: 'setting_key' },
{ name: 'idx_is_public', columns: 'is_public' }
],
'5Why_audit_logs': [
{ name: 'idx_user_id', columns: 'user_id' },
{ name: 'idx_action', columns: 'action' },
{ name: 'idx_created_at', columns: 'created_at' },
{ name: 'idx_entity', columns: 'entity_type, entity_id' }
],
'5Why_sessions': [
{ name: 'idx_expires', columns: 'expires' }
]
};
async function fixIndexes() {
let connection;
try {
console.log('\n╔════════════════════════════════════════════════════════╗');
console.log('║ 5 Why Analyzer - Fix Database Indexes ║');
console.log('╚════════════════════════════════════════════════════════╝\n');
console.log('🔄 Connecting to database...');
connection = await mysql.createConnection(dbConfig);
console.log('✅ Connected successfully\n');
// Process each table
for (const [tableName, indexes] of Object.entries(indexDefinitions)) {
console.log(`📋 Processing table: ${tableName}`);
// Check if table exists
const [tableExists] = await connection.execute(
`SELECT COUNT(*) as count FROM information_schema.tables
WHERE table_schema = ? AND table_name = ?`,
[dbConfig.database, tableName]
);
if (tableExists[0].count === 0) {
console.log(` ⚠ Table ${tableName} not found, skipping...`);
continue;
}
// Get existing indexes
const [existingIndexes] = await connection.execute(
`SELECT DISTINCT INDEX_NAME FROM information_schema.statistics
WHERE table_schema = ? AND table_name = ?`,
[dbConfig.database, tableName]
);
const existingIndexNames = existingIndexes.map(idx => idx.INDEX_NAME);
// Create missing indexes
for (const index of indexes) {
if (existingIndexNames.includes(index.name)) {
console.log(` ✓ Index ${index.name} already exists`);
} else {
try {
await connection.execute(
`CREATE INDEX ${index.name} ON \`${tableName}\` (${index.columns})`
);
console.log(` ✓ Created index: ${index.name} on (${index.columns})`);
} catch (err) {
if (err.code === 'ER_DUP_KEYNAME') {
console.log(` ⚠ Index ${index.name} already exists (duplicate)`);
} else {
console.error(` ✗ Error creating index ${index.name}: ${err.message}`);
}
}
}
}
}
// Verify unique constraints on 5Why_users
console.log('\n📋 Verifying unique constraints on 5Why_users...');
// Check employee_id unique constraint
const [empUnique] = await connection.execute(
`SELECT COUNT(*) as count FROM information_schema.statistics
WHERE table_schema = ? AND table_name = '5Why_users'
AND column_name = 'employee_id' AND non_unique = 0`,
[dbConfig.database]
);
if (empUnique[0].count > 0) {
console.log(' ✓ employee_id has unique constraint');
} else {
console.log(' ⚠ employee_id missing unique constraint, adding...');
try {
await connection.execute(
`ALTER TABLE 5Why_users ADD UNIQUE INDEX idx_employee_id_unique (employee_id)`
);
console.log(' ✓ Added unique constraint on employee_id');
} catch (err) {
if (err.code === 'ER_DUP_KEYNAME' || err.message.includes('Duplicate')) {
console.log(' ✓ Unique constraint already exists');
} else {
console.error(` ✗ Error: ${err.message}`);
}
}
}
// Check email unique constraint
const [emailUnique] = await connection.execute(
`SELECT COUNT(*) as count FROM information_schema.statistics
WHERE table_schema = ? AND table_name = '5Why_users'
AND column_name = 'email' AND non_unique = 0`,
[dbConfig.database]
);
if (emailUnique[0].count > 0) {
console.log(' ✓ email has unique constraint');
} else {
console.log(' ⚠ email missing unique constraint, adding...');
try {
await connection.execute(
`ALTER TABLE 5Why_users ADD UNIQUE INDEX idx_email_unique (email)`
);
console.log(' ✓ Added unique constraint on email');
} catch (err) {
if (err.code === 'ER_DUP_KEYNAME' || err.message.includes('Duplicate')) {
console.log(' ✓ Unique constraint already exists');
} else {
console.error(` ✗ Error: ${err.message}`);
}
}
}
console.log('\n╔════════════════════════════════════════════════════════╗');
console.log('║ Index Fix Complete! ║');
console.log('╚════════════════════════════════════════════════════════╝\n');
// Show current indexes for 5Why_users
console.log('📊 Current indexes on 5Why_users:');
const [userIndexes] = await connection.execute(
`SELECT INDEX_NAME, COLUMN_NAME, NON_UNIQUE
FROM information_schema.statistics
WHERE table_schema = ? AND table_name = '5Why_users'
ORDER BY INDEX_NAME, SEQ_IN_INDEX`,
[dbConfig.database]
);
userIndexes.forEach(idx => {
const unique = idx.NON_UNIQUE === 0 ? ' (UNIQUE)' : '';
console.log(` - ${idx.INDEX_NAME}: ${idx.COLUMN_NAME}${unique}`);
});
} catch (error) {
console.error('\n❌ Fix indexes failed:');
console.error(' Error:', error.message);
process.exit(1);
} finally {
if (connection) {
await connection.end();
console.log('\n🔌 Database connection closed\n');
}
}
}
// Execute
fixIndexes();

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env node
/**
* Database Migration Script
* Renames all tables to add 5Why_ prefix
*
* Run: node scripts/migrate-table-prefix.js
*/
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
const dbConfig = {
host: process.env.DB_HOST || 'mysql.theaken.com',
port: parseInt(process.env.DB_PORT) || 33306,
user: process.env.DB_USER || 'A102',
password: process.env.DB_PASSWORD || 'Bb123456',
database: process.env.DB_NAME || 'db_A102'
};
// 要重新命名的表(舊名 -> 新名)
const tableRenames = [
{ old: 'users', new: '5Why_users' },
{ old: 'analyses', new: '5Why_analyses' },
{ old: 'analysis_perspectives', new: '5Why_analysis_perspectives' },
{ old: 'analysis_whys', new: '5Why_analysis_whys' },
{ old: 'llm_configs', new: '5Why_llm_configs' },
{ old: 'system_settings', new: '5Why_system_settings' },
{ old: 'audit_logs', new: '5Why_audit_logs' },
{ old: 'sessions', new: '5Why_sessions' }
];
// 要重新建立的視圖
const viewRenames = [
{ old: 'user_analysis_stats', new: '5Why_user_analysis_stats' },
{ old: 'recent_analyses', new: '5Why_recent_analyses' }
];
async function migrateTablePrefix() {
let connection;
try {
console.log('\n╔════════════════════════════════════════════════════════╗');
console.log('║ 5 Why Analyzer - Table Prefix Migration ║');
console.log('╚════════════════════════════════════════════════════════╝\n');
console.log('🔄 Connecting to database...');
connection = await mysql.createConnection(dbConfig);
console.log('✅ Connected successfully\n');
// 先刪除舊的視圖(因為它們依賴表)
console.log('📋 Dropping old views...');
for (const view of viewRenames) {
try {
await connection.execute(`DROP VIEW IF EXISTS ${view.old}`);
console.log(` ✓ Dropped view: ${view.old}`);
} catch (err) {
console.log(` ⚠ View ${view.old} not found or already dropped`);
}
}
console.log('');
// 重新命名表
console.log('📋 Renaming tables...');
for (const table of tableRenames) {
try {
// 檢查舊表是否存在
const [oldExists] = await connection.execute(
`SELECT COUNT(*) as count FROM information_schema.tables
WHERE table_schema = ? AND table_name = ?`,
[dbConfig.database, table.old]
);
// 檢查新表是否已存在
const [newExists] = await connection.execute(
`SELECT COUNT(*) as count FROM information_schema.tables
WHERE table_schema = ? AND table_name = ?`,
[dbConfig.database, table.new]
);
if (oldExists[0].count > 0 && newExists[0].count === 0) {
await connection.execute(`RENAME TABLE \`${table.old}\` TO \`${table.new}\``);
console.log(` ✓ Renamed: ${table.old} -> ${table.new}`);
} else if (newExists[0].count > 0) {
console.log(` ⚠ Table ${table.new} already exists, skipping...`);
} else {
console.log(` ⚠ Table ${table.old} not found, skipping...`);
}
} catch (err) {
console.error(` ✗ Error renaming ${table.old}: ${err.message}`);
}
}
console.log('');
// 建立新的視圖
console.log('📋 Creating new views...');
// user_analysis_stats 視圖
try {
await connection.execute(`
CREATE OR REPLACE VIEW 5Why_user_analysis_stats AS
SELECT
u.id AS user_id,
u.username,
u.employee_id,
u.department,
COUNT(a.id) AS total_analyses,
COUNT(CASE WHEN a.status = 'completed' THEN 1 END) AS completed_analyses,
COUNT(CASE WHEN a.status = 'failed' THEN 1 END) AS failed_analyses,
AVG(a.processing_time) AS avg_processing_time,
MAX(a.created_at) AS last_analysis_at
FROM 5Why_users u
LEFT JOIN 5Why_analyses a ON u.id = a.user_id
GROUP BY u.id, u.username, u.employee_id, u.department
`);
console.log(' ✓ Created view: 5Why_user_analysis_stats');
} catch (err) {
console.error(` ✗ Error creating view 5Why_user_analysis_stats: ${err.message}`);
}
// recent_analyses 視圖
try {
await connection.execute(`
CREATE OR REPLACE VIEW 5Why_recent_analyses AS
SELECT
a.id,
a.finding,
u.username,
u.employee_id,
a.output_language,
a.status,
a.processing_time,
a.created_at
FROM 5Why_analyses a
JOIN 5Why_users u ON a.user_id = u.id
ORDER BY a.created_at DESC
LIMIT 100
`);
console.log(' ✓ Created view: 5Why_recent_analyses');
} catch (err) {
console.error(` ✗ Error creating view 5Why_recent_analyses: ${err.message}`);
}
console.log('\n╔════════════════════════════════════════════════════════╗');
console.log('║ Migration Complete! ║');
console.log('╚════════════════════════════════════════════════════════╝\n');
// 列出所有 5Why_ 表
const [tables] = await connection.execute(
`SELECT table_name FROM information_schema.tables
WHERE table_schema = ? AND table_name LIKE '5Why_%'
ORDER BY table_name`,
[dbConfig.database]
);
if (tables.length > 0) {
console.log('📊 5Why tables in database:');
tables.forEach((table, index) => {
console.log(` ${index + 1}. ${table.TABLE_NAME || table.table_name}`);
});
}
} catch (error) {
console.error('\n❌ Migration failed:');
console.error(' Error:', error.message);
process.exit(1);
} finally {
if (connection) {
await connection.end();
console.log('\n🔌 Database connection closed\n');
}
}
}
// 執行遷移
migrateTablePrefix();

View File

@@ -60,7 +60,7 @@ async function seedTestUsers() {
for (const user of testUsers) {
// 檢查使用者是否已存在 (只用 email 精確匹配)
const [existing] = await connection.execute(
'SELECT id FROM users WHERE email = ?',
'SELECT id FROM 5Why_users WHERE email = ?',
[user.email]
);
@@ -68,7 +68,7 @@ async function seedTestUsers() {
// 更新現有使用者的密碼
const passwordHash = await bcrypt.hash(user.password, 10);
await connection.execute(
'UPDATE users SET password_hash = ?, role = ?, is_active = 1, employee_id = ? WHERE email = ?',
'UPDATE 5Why_users SET password_hash = ?, role = ?, is_active = 1, employee_id = ? WHERE email = ?',
[passwordHash, user.role, user.employee_id, user.email]
);
console.log(`🔄 Updated: ${user.email} (${user.role})`);
@@ -76,7 +76,7 @@ async function seedTestUsers() {
// 建立新使用者
const passwordHash = await bcrypt.hash(user.password, 10);
await connection.execute(
`INSERT INTO users (employee_id, username, email, password_hash, role, department, position, is_active)
`INSERT INTO 5Why_users (employee_id, username, email, password_hash, role, department, position, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, 1)`,
[user.employee_id, user.username, user.email, passwordHash, user.role, user.department, user.position]
);

View File

@@ -4,6 +4,7 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: 'localhost',
port: 5173,
open: true
}