- 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>
336 lines
9.8 KiB
JavaScript
336 lines
9.8 KiB
JavaScript
import { pool } from '../config.js';
|
||
|
||
/**
|
||
* Analysis Model
|
||
* 處理 5 Why 分析記錄相關的資料庫操作
|
||
*/
|
||
class Analysis {
|
||
/**
|
||
* 建立新的分析記錄
|
||
*/
|
||
static async create(analysisData) {
|
||
const { user_id, finding, job_content, output_language } = analysisData;
|
||
|
||
try {
|
||
const [result] = await pool.execute(
|
||
`INSERT INTO 5Why_analyses (user_id, finding, job_content, output_language, status)
|
||
VALUES (?, ?, ?, ?, 'pending')`,
|
||
[user_id, finding, job_content, output_language]
|
||
);
|
||
|
||
return await this.findById(result.insertId);
|
||
} catch (error) {
|
||
throw new Error(`Error creating analysis: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根據 ID 取得分析記錄
|
||
*/
|
||
static async findById(id) {
|
||
try {
|
||
const [rows] = await pool.execute(
|
||
'SELECT * FROM 5Why_analyses WHERE id = ?',
|
||
[id]
|
||
);
|
||
return rows[0] || null;
|
||
} catch (error) {
|
||
throw new Error(`Error finding analysis: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新分析狀態
|
||
*/
|
||
static async updateStatus(id, status, errorMessage = null) {
|
||
try {
|
||
await pool.execute(
|
||
'UPDATE 5Why_analyses SET status = ?, error_message = ? WHERE id = ?',
|
||
[status, errorMessage, id]
|
||
);
|
||
} catch (error) {
|
||
throw new Error(`Error updating analysis status: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 儲存分析結果
|
||
*/
|
||
static async saveResult(id, resultData) {
|
||
const { problem_restatement, analysis_result, processing_time } = resultData;
|
||
|
||
try {
|
||
const connection = await pool.getConnection();
|
||
await connection.beginTransaction();
|
||
|
||
try {
|
||
// 更新主分析記錄
|
||
await connection.execute(
|
||
`UPDATE 5Why_analyses
|
||
SET problem_restatement = ?, analysis_result = ?, processing_time = ?, status = 'completed'
|
||
WHERE id = ?`,
|
||
[problem_restatement, JSON.stringify(analysis_result), processing_time, id]
|
||
);
|
||
|
||
// 儲存分析角度
|
||
if (analysis_result.analyses && Array.isArray(analysis_result.analyses)) {
|
||
for (const perspective of analysis_result.analyses) {
|
||
const [perspectiveResult] = await connection.execute(
|
||
`INSERT INTO 5Why_analysis_perspectives
|
||
(analysis_id, perspective, perspective_icon, root_cause, permanent_solution,
|
||
logic_check_forward, logic_check_backward, logic_valid)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
[
|
||
id,
|
||
perspective.perspective,
|
||
perspective.perspectiveIcon || null,
|
||
perspective.rootCause || null,
|
||
perspective.countermeasure?.permanent || null,
|
||
perspective.logicCheck?.forward || null,
|
||
perspective.logicCheck?.backward || null,
|
||
perspective.logicCheck?.isValid !== false
|
||
]
|
||
);
|
||
|
||
const perspectiveId = perspectiveResult.insertId;
|
||
|
||
// 儲存 5 Why 詳細記錄
|
||
if (perspective.whys && Array.isArray(perspective.whys)) {
|
||
for (const why of perspective.whys) {
|
||
if (why && why.level) {
|
||
await connection.execute(
|
||
`INSERT INTO 5Why_analysis_whys
|
||
(perspective_id, level, question, answer, is_verified, verification_note)
|
||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||
[
|
||
perspectiveId,
|
||
why.level,
|
||
why.question,
|
||
why.answer,
|
||
why.isVerified !== false,
|
||
why.verificationNote || null
|
||
]
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
await connection.commit();
|
||
connection.release();
|
||
|
||
return await this.findById(id);
|
||
} catch (error) {
|
||
await connection.rollback();
|
||
connection.release();
|
||
throw error;
|
||
}
|
||
} catch (error) {
|
||
throw new Error(`Error saving analysis result: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取得使用者的分析記錄(分頁)
|
||
*/
|
||
static async getByUserId(userId, page = 1, limit = 10, filters = {}) {
|
||
const offset = (page - 1) * limit;
|
||
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 = [];
|
||
|
||
// 篩選條件
|
||
if (filters.status) {
|
||
whereClauses.push('status = ?');
|
||
whereParams.push(filters.status);
|
||
}
|
||
if (filters.date_from) {
|
||
whereClauses.push('created_at >= ?');
|
||
whereParams.push(filters.date_from);
|
||
}
|
||
if (filters.date_to) {
|
||
whereClauses.push('created_at <= ?');
|
||
whereParams.push(filters.date_to);
|
||
}
|
||
if (filters.search) {
|
||
whereClauses.push('finding LIKE ?');
|
||
whereParams.push(`%${filters.search}%`);
|
||
}
|
||
|
||
if (whereClauses.length > 0) {
|
||
const whereClause = ' AND ' + whereClauses.join(' AND ');
|
||
query += whereClause;
|
||
countQuery += whereClause;
|
||
}
|
||
|
||
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||
|
||
try {
|
||
// 使用 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,
|
||
pagination: {
|
||
page,
|
||
limit,
|
||
total: countResult[0].total,
|
||
totalPages: Math.ceil(countResult[0].total / limit)
|
||
}
|
||
};
|
||
} catch (error) {
|
||
throw new Error(`Error getting user analyses: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取得所有分析記錄(管理員用)
|
||
*/
|
||
static async getAll(page = 1, limit = 10, filters = {}) {
|
||
const offset = (page - 1) * limit;
|
||
let query = `
|
||
SELECT a.*, u.username, u.employee_id
|
||
FROM 5Why_analyses a
|
||
JOIN 5Why_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 = [];
|
||
|
||
// 篩選條件
|
||
if (filters.status) {
|
||
whereClauses.push('a.status = ?');
|
||
whereParams.push(filters.status);
|
||
}
|
||
if (filters.user_id) {
|
||
whereClauses.push('a.user_id = ?');
|
||
whereParams.push(filters.user_id);
|
||
}
|
||
if (filters.search) {
|
||
whereClauses.push('(a.finding LIKE ? OR u.username LIKE ?)');
|
||
const searchTerm = `%${filters.search}%`;
|
||
whereParams.push(searchTerm, searchTerm);
|
||
}
|
||
|
||
if (whereClauses.length > 0) {
|
||
const whereClause = ' WHERE ' + whereClauses.join(' AND ');
|
||
query += whereClause;
|
||
countQuery += whereClause;
|
||
}
|
||
|
||
query += ' ORDER BY a.created_at DESC LIMIT ? OFFSET ?';
|
||
|
||
try {
|
||
// 使用 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,
|
||
pagination: {
|
||
page,
|
||
limit,
|
||
total: countResult[0].total,
|
||
totalPages: Math.ceil(countResult[0].total / limit)
|
||
}
|
||
};
|
||
} catch (error) {
|
||
throw new Error(`Error getting all analyses: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取得分析詳細資料(含角度和 Whys)
|
||
*/
|
||
static async getFullAnalysis(id) {
|
||
try {
|
||
// 取得主記錄
|
||
const analysis = await this.findById(id);
|
||
if (!analysis) return null;
|
||
|
||
// 取得分析角度
|
||
const [perspectives] = await pool.execute(
|
||
'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 5Why_analysis_whys WHERE perspective_id = ? ORDER BY level',
|
||
[perspective.id]
|
||
);
|
||
perspective.whys = whys;
|
||
}
|
||
|
||
analysis.perspectives = perspectives;
|
||
|
||
return analysis;
|
||
} catch (error) {
|
||
throw new Error(`Error getting full analysis: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 刪除分析記錄
|
||
*/
|
||
static async delete(id) {
|
||
try {
|
||
await pool.execute('DELETE FROM 5Why_analyses WHERE id = ?', [id]);
|
||
return true;
|
||
} catch (error) {
|
||
throw new Error(`Error deleting analysis: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取得最近的分析記錄
|
||
*/
|
||
static async getRecent(limit = 100) {
|
||
try {
|
||
// 使用 pool.query 而非 pool.execute,因為 LIMIT 需要數字類型
|
||
const [rows] = await pool.query(
|
||
'SELECT * FROM 5Why_recent_analyses LIMIT ?',
|
||
[parseInt(limit)]
|
||
);
|
||
return rows;
|
||
} catch (error) {
|
||
throw new Error(`Error getting recent analyses: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取得統計資料
|
||
*/
|
||
static async getStatistics(userId = null) {
|
||
try {
|
||
let query = `
|
||
SELECT
|
||
COUNT(*) as total,
|
||
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed,
|
||
COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed,
|
||
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 5Why_analyses
|
||
`;
|
||
|
||
const params = [];
|
||
if (userId) {
|
||
query += ' WHERE user_id = ?';
|
||
params.push(userId);
|
||
}
|
||
|
||
const [rows] = await pool.execute(query, params);
|
||
return rows[0];
|
||
} catch (error) {
|
||
throw new Error(`Error getting statistics: ${error.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
export default Analysis;
|