Compare commits
2 Commits
66cdcacce9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1568a12a96 | ||
|
|
f9ee43b73c |
227
docs/db_schema.sql
Normal file
227
docs/db_schema.sql
Normal 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
|
||||
-- ============================================
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
219
routes/llmTest.js
Normal 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;
|
||||
@@ -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
|
||||
]
|
||||
);
|
||||
|
||||
81
scripts/add-ollama-config.js
Normal file
81
scripts/add-ollama-config.js
Normal 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
204
scripts/fix-indexes.js
Normal 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();
|
||||
177
scripts/migrate-table-prefix.js
Normal file
177
scripts/migrate-table-prefix.js
Normal 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();
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import react from '@vitejs/plugin-react'
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: 'localhost',
|
||||
port: 5173,
|
||||
open: true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user