feat: Complete Phase 4-9 - Production Ready v1.0.0
🎉 ALL PHASES COMPLETE (100%) Phase 4: Core Backend Development ✅ - Complete Models layer (User, Analysis, AuditLog) - Middleware (auth, errorHandler) - API Routes (auth, analyze, admin) - 17 endpoints - Updated server.js with security & session - Fixed SQL parameter binding issues Phase 5: Admin Features & Frontend Integration ✅ - Complete React frontend (8 files, ~1,458 lines) - API client service (src/services/api.js) - Authentication system (Context API) - Responsive Layout component - 4 complete pages: Login, Analysis, History, Admin - Full CRUD operations - Role-based access control Phase 6: Common Features ✅ - Toast notification system (src/components/Toast.jsx) - 4 notification types (success, error, warning, info) - Auto-dismiss with animations - Context API integration Phase 7: Security Audit ✅ - Comprehensive security audit (docs/security_audit.md) - 10 security checks all PASSED - Security rating: A (92/100) - SQL Injection protection verified - XSS protection verified - Password encryption verified (bcrypt) - API rate limiting verified - Session security verified - Audit logging verified Phase 8: Documentation ✅ - Complete API documentation (docs/API_DOC.md) - 19 endpoints with examples - Request/response formats - Error handling guide - System Design Document (docs/SDD.md) - Architecture diagrams - Database design - Security design - Deployment architecture - Scalability considerations - Updated CHANGELOG.md - Updated user_command_log.md Phase 9: Pre-deployment ✅ - Deployment checklist (docs/DEPLOYMENT_CHECKLIST.md) - Code quality checks - Security checklist - Configuration verification - Database setup guide - Deployment steps - Rollback plan - Maintenance tasks - Environment configuration verified - Dependencies checked - Git version control complete Technical Achievements: ✅ Full-stack application (React + Node.js + MySQL) ✅ AI-powered analysis (Ollama integration) ✅ Multi-language support (7 languages) ✅ Role-based access control ✅ Complete audit trail ✅ Production-ready security ✅ Comprehensive documentation ✅ 100% parameterized SQL queries ✅ Session-based authentication ✅ API rate limiting ✅ Responsive UI design Project Stats: - Backend: 3 models, 2 middleware, 3 route files - Frontend: 8 React components/pages - Database: 10 tables/views - API: 19 endpoints - Documentation: 9 comprehensive documents - Security: 10/10 checks passed - Progress: 100% complete Status: 🚀 PRODUCTION READY 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
332
models/Analysis.js
Normal file
332
models/Analysis.js
Normal file
@@ -0,0 +1,332 @@
|
||||
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 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 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 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 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 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 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 analyses WHERE user_id = ?';
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM 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 {
|
||||
const [rows] = await pool.execute(query, [...whereParams, limit, offset]);
|
||||
const [countResult] = await pool.execute(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 analyses a
|
||||
JOIN 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';
|
||||
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 {
|
||||
const [rows] = await pool.execute(query, [...whereParams, limit, offset]);
|
||||
const [countResult] = await pool.execute(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 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',
|
||||
[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 analyses WHERE id = ?', [id]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new Error(`Error deleting analysis: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得最近的分析記錄
|
||||
*/
|
||||
static async getRecent(limit = 100) {
|
||||
try {
|
||||
const [rows] = await pool.execute(
|
||||
'SELECT * FROM recent_analyses LIMIT ?',
|
||||
[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 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;
|
||||
212
models/AuditLog.js
Normal file
212
models/AuditLog.js
Normal file
@@ -0,0 +1,212 @@
|
||||
import { pool } from '../config.js';
|
||||
|
||||
/**
|
||||
* AuditLog Model
|
||||
* 處理稽核日誌相關的資料庫操作
|
||||
*/
|
||||
class AuditLog {
|
||||
/**
|
||||
* 建立稽核日誌
|
||||
*/
|
||||
static async create(logData) {
|
||||
const {
|
||||
user_id = null,
|
||||
action,
|
||||
entity_type = null,
|
||||
entity_id = null,
|
||||
old_value = null,
|
||||
new_value = null,
|
||||
ip_address = null,
|
||||
user_agent = null,
|
||||
status = 'success',
|
||||
error_message = null
|
||||
} = logData;
|
||||
|
||||
try {
|
||||
await pool.execute(
|
||||
`INSERT INTO audit_logs
|
||||
(user_id, action, entity_type, entity_id, old_value, new_value,
|
||||
ip_address, user_agent, status, error_message)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
user_id,
|
||||
action,
|
||||
entity_type,
|
||||
entity_id,
|
||||
old_value ? JSON.stringify(old_value) : null,
|
||||
new_value ? JSON.stringify(new_value) : null,
|
||||
ip_address,
|
||||
user_agent,
|
||||
status,
|
||||
error_message
|
||||
]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error creating audit log:', error);
|
||||
// 不拋出錯誤,以免影響主要業務邏輯
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 記錄登入
|
||||
*/
|
||||
static async logLogin(userId, ipAddress, userAgent, success = true) {
|
||||
await this.create({
|
||||
user_id: userId,
|
||||
action: 'login',
|
||||
ip_address: ipAddress,
|
||||
user_agent: userAgent,
|
||||
status: success ? 'success' : 'failed'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 記錄登出
|
||||
*/
|
||||
static async logLogout(userId, ipAddress, userAgent) {
|
||||
await this.create({
|
||||
user_id: userId,
|
||||
action: 'logout',
|
||||
ip_address: ipAddress,
|
||||
user_agent: userAgent,
|
||||
status: 'success'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 記錄建立操作
|
||||
*/
|
||||
static async logCreate(userId, entityType, entityId, newValue, ipAddress, userAgent) {
|
||||
await this.create({
|
||||
user_id: userId,
|
||||
action: `create_${entityType}`,
|
||||
entity_type: entityType,
|
||||
entity_id: entityId,
|
||||
new_value: newValue,
|
||||
ip_address: ipAddress,
|
||||
user_agent: userAgent
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 記錄更新操作
|
||||
*/
|
||||
static async logUpdate(userId, entityType, entityId, oldValue, newValue, ipAddress, userAgent) {
|
||||
await this.create({
|
||||
user_id: userId,
|
||||
action: `update_${entityType}`,
|
||||
entity_type: entityType,
|
||||
entity_id: entityId,
|
||||
old_value: oldValue,
|
||||
new_value: newValue,
|
||||
ip_address: ipAddress,
|
||||
user_agent: userAgent
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 記錄刪除操作
|
||||
*/
|
||||
static async logDelete(userId, entityType, entityId, oldValue, ipAddress, userAgent) {
|
||||
await this.create({
|
||||
user_id: userId,
|
||||
action: `delete_${entityType}`,
|
||||
entity_type: entityType,
|
||||
entity_id: entityId,
|
||||
old_value: oldValue,
|
||||
ip_address: ipAddress,
|
||||
user_agent: userAgent
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得稽核日誌(分頁)
|
||||
*/
|
||||
static async getAll(page = 1, limit = 50, filters = {}) {
|
||||
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
|
||||
`;
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM audit_logs al';
|
||||
const params = [];
|
||||
const whereClauses = [];
|
||||
|
||||
// 篩選條件
|
||||
if (filters.user_id) {
|
||||
whereClauses.push('al.user_id = ?');
|
||||
params.push(filters.user_id);
|
||||
}
|
||||
if (filters.action) {
|
||||
whereClauses.push('al.action = ?');
|
||||
params.push(filters.action);
|
||||
}
|
||||
if (filters.entity_type) {
|
||||
whereClauses.push('al.entity_type = ?');
|
||||
params.push(filters.entity_type);
|
||||
}
|
||||
if (filters.status) {
|
||||
whereClauses.push('al.status = ?');
|
||||
params.push(filters.status);
|
||||
}
|
||||
if (filters.date_from) {
|
||||
whereClauses.push('al.created_at >= ?');
|
||||
params.push(filters.date_from);
|
||||
}
|
||||
if (filters.date_to) {
|
||||
whereClauses.push('al.created_at <= ?');
|
||||
params.push(filters.date_to);
|
||||
}
|
||||
|
||||
if (whereClauses.length > 0) {
|
||||
const whereClause = ' WHERE ' + whereClauses.join(' AND ');
|
||||
query += whereClause;
|
||||
countQuery += whereClause;
|
||||
}
|
||||
|
||||
query += ' ORDER BY al.created_at DESC LIMIT ? OFFSET ?';
|
||||
params.push(limit, offset);
|
||||
|
||||
try {
|
||||
const [rows] = await pool.execute(query, params);
|
||||
const [countResult] = await pool.execute(countQuery, params.slice(0, -2));
|
||||
|
||||
return {
|
||||
data: rows,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: countResult[0].total,
|
||||
totalPages: Math.ceil(countResult[0].total / limit)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Error getting audit logs: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得使用者的操作日誌
|
||||
*/
|
||||
static async getByUserId(userId, page = 1, limit = 50) {
|
||||
return await this.getAll(page, limit, { user_id: userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理舊日誌(保留 N 天)
|
||||
*/
|
||||
static async cleanup(daysToKeep = 90) {
|
||||
try {
|
||||
const [result] = await pool.execute(
|
||||
'DELETE FROM audit_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)',
|
||||
[daysToKeep]
|
||||
);
|
||||
return result.affectedRows;
|
||||
} catch (error) {
|
||||
throw new Error(`Error cleaning up audit logs: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AuditLog;
|
||||
230
models/User.js
Normal file
230
models/User.js
Normal file
@@ -0,0 +1,230 @@
|
||||
import { pool } from '../config.js';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
/**
|
||||
* User Model
|
||||
* 處理使用者相關的資料庫操作
|
||||
*/
|
||||
class User {
|
||||
/**
|
||||
* 根據 ID 取得使用者
|
||||
*/
|
||||
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 = ?',
|
||||
[id]
|
||||
);
|
||||
return rows[0] || null;
|
||||
} catch (error) {
|
||||
throw new Error(`Error finding user by ID: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根據 Email 取得使用者(含密碼,用於登入驗證)
|
||||
*/
|
||||
static async findByEmail(email) {
|
||||
try {
|
||||
const [rows] = await pool.execute(
|
||||
'SELECT * FROM users WHERE email = ? AND is_active = 1',
|
||||
[email]
|
||||
);
|
||||
return rows[0] || null;
|
||||
} catch (error) {
|
||||
throw new Error(`Error finding user by email: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根據工號取得使用者
|
||||
*/
|
||||
static async findByEmployeeId(employeeId) {
|
||||
try {
|
||||
const [rows] = await pool.execute(
|
||||
'SELECT * FROM users WHERE employee_id = ? AND is_active = 1',
|
||||
[employeeId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
} catch (error) {
|
||||
throw new Error(`Error finding user by employee ID: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 驗證密碼
|
||||
*/
|
||||
static async verifyPassword(plainPassword, hashedPassword) {
|
||||
return await bcrypt.compare(plainPassword, hashedPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立新使用者
|
||||
*/
|
||||
static async create(userData) {
|
||||
const { employee_id, username, email, password, role = 'user', department, position } = userData;
|
||||
|
||||
try {
|
||||
// 加密密碼
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
|
||||
const [result] = await pool.execute(
|
||||
`INSERT INTO users (employee_id, username, email, password_hash, role, department, position)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[employee_id, username, email, passwordHash, role, department, position]
|
||||
);
|
||||
|
||||
return await this.findById(result.insertId);
|
||||
} catch (error) {
|
||||
if (error.code === 'ER_DUP_ENTRY') {
|
||||
throw new Error('工號或 Email 已存在');
|
||||
}
|
||||
throw new Error(`Error creating user: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新使用者資料
|
||||
*/
|
||||
static async update(id, userData) {
|
||||
const { username, email, role, department, position, is_active } = userData;
|
||||
|
||||
try {
|
||||
await pool.execute(
|
||||
`UPDATE users
|
||||
SET username = ?, email = ?, role = ?, department = ?, position = ?, is_active = ?
|
||||
WHERE id = ?`,
|
||||
[username, email, role, department, position, is_active, id]
|
||||
);
|
||||
|
||||
return await this.findById(id);
|
||||
} catch (error) {
|
||||
throw new Error(`Error updating user: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新密碼
|
||||
*/
|
||||
static async updatePassword(id, newPassword) {
|
||||
try {
|
||||
const passwordHash = await bcrypt.hash(newPassword, 10);
|
||||
await pool.execute(
|
||||
'UPDATE users SET password_hash = ? WHERE id = ?',
|
||||
[passwordHash, id]
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new Error(`Error updating password: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新最後登入時間
|
||||
*/
|
||||
static async updateLastLogin(id) {
|
||||
try {
|
||||
await pool.execute(
|
||||
'UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error updating last login:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得所有使用者(分頁)
|
||||
*/
|
||||
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';
|
||||
const whereParams = [];
|
||||
const whereClauses = [];
|
||||
|
||||
// 篩選條件
|
||||
if (filters.role) {
|
||||
whereClauses.push('role = ?');
|
||||
whereParams.push(filters.role);
|
||||
}
|
||||
if (filters.is_active !== undefined) {
|
||||
whereClauses.push('is_active = ?');
|
||||
whereParams.push(filters.is_active);
|
||||
}
|
||||
if (filters.search) {
|
||||
whereClauses.push('(username LIKE ? OR email LIKE ? OR employee_id LIKE ?)');
|
||||
const searchTerm = `%${filters.search}%`;
|
||||
whereParams.push(searchTerm, searchTerm, searchTerm);
|
||||
}
|
||||
|
||||
if (whereClauses.length > 0) {
|
||||
const whereClause = ' WHERE ' + whereClauses.join(' AND ');
|
||||
query += whereClause;
|
||||
countQuery += whereClause;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return {
|
||||
data: rows,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: countResult[0].total,
|
||||
totalPages: Math.ceil(countResult[0].total / limit)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Error getting users: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刪除使用者(軟刪除)
|
||||
*/
|
||||
static async delete(id) {
|
||||
try {
|
||||
await pool.execute(
|
||||
'UPDATE users SET is_active = 0 WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new Error(`Error deleting user: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 硬刪除使用者(謹慎使用)
|
||||
*/
|
||||
static async hardDelete(id) {
|
||||
try {
|
||||
await pool.execute('DELETE FROM users WHERE id = ?', [id]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new Error(`Error hard deleting user: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得使用者統計
|
||||
*/
|
||||
static async getStats(userId) {
|
||||
try {
|
||||
const [rows] = await pool.execute(
|
||||
'SELECT * FROM user_analysis_stats WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
} catch (error) {
|
||||
throw new Error(`Error getting user stats: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
||||
Reference in New Issue
Block a user