新增評分項目設定、資料庫整合

This commit is contained in:
2025-09-22 00:33:12 +08:00
parent 8de09129be
commit 9d4c586ad3
20 changed files with 2321 additions and 79 deletions

78
lib/database.ts Normal file
View File

@@ -0,0 +1,78 @@
import mysql from 'mysql2/promise';
// 資料庫配置
const dbConfig = {
host: process.env.DB_HOST || 'mysql.theaken.com',
port: parseInt(process.env.DB_PORT || '33306'),
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || 'zh6161168',
database: process.env.DB_NAME || 'db_AI_scoring',
charset: 'utf8mb4',
timezone: '+08:00',
acquireTimeout: 60000,
timeout: 60000,
reconnect: true,
multipleStatements: true,
};
// 建立連接池
const pool = mysql.createPool({
...dbConfig,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
});
// 資料庫連接函數
export async function getConnection() {
try {
const connection = await pool.getConnection();
return connection;
} catch (error) {
console.error('資料庫連接失敗:', error);
throw error;
}
}
// 執行查詢函數
export async function query(sql: string, params?: any[]) {
try {
const [rows] = await pool.execute(sql, params);
return rows;
} catch (error) {
console.error('查詢執行失敗:', error);
throw error;
}
}
// 執行事務函數
export async function transaction(callback: (connection: mysql.PoolConnection) => Promise<any>) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
const result = await callback(connection);
await connection.commit();
return result;
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}
// 測試資料庫連接
export async function testConnection() {
try {
const connection = await getConnection();
await connection.ping();
connection.release();
console.log('✅ 資料庫連接成功');
return true;
} catch (error) {
console.error('❌ 資料庫連接失敗:', error);
return false;
}
}
export default pool;

141
lib/models/index.ts Normal file
View File

@@ -0,0 +1,141 @@
// 資料庫模型定義
export interface User {
id: number;
email: string;
username: string;
password_hash: string;
full_name?: string;
avatar_url?: string;
role: 'admin' | 'user';
is_active: boolean;
email_verified_at?: Date;
last_login_at?: Date;
created_at: Date;
updated_at: Date;
}
export interface CriteriaTemplate {
id: number;
user_id: number;
name: string;
description?: string;
is_default: boolean;
is_public: boolean;
total_weight: number;
created_at: Date;
updated_at: Date;
}
export interface CriteriaItem {
id: number;
template_id: number;
name: string;
description?: string;
weight: number;
max_score: number;
sort_order: number;
created_at: Date;
updated_at: Date;
}
export interface Project {
id: number;
user_id: number;
template_id: number;
title: string;
description?: string;
status: 'draft' | 'uploading' | 'analyzing' | 'completed' | 'failed';
analysis_started_at?: Date;
analysis_completed_at?: Date;
created_at: Date;
updated_at: Date;
}
export interface ProjectFile {
id: number;
project_id: number;
original_name: string;
file_name: string;
file_path: string;
file_size: number;
file_type: string;
mime_type: string;
upload_status: 'uploading' | 'completed' | 'failed';
upload_progress: number;
created_at: Date;
updated_at: Date;
}
export interface ProjectWebsite {
id: number;
project_id: number;
url: string;
title?: string;
description?: string;
status: 'pending' | 'analyzing' | 'completed' | 'failed';
created_at: Date;
updated_at: Date;
}
export interface Evaluation {
id: number;
project_id: number;
overall_score?: number;
max_possible_score: number;
grade?: string;
analysis_duration?: number;
ai_model_version?: string;
status: 'pending' | 'analyzing' | 'completed' | 'failed';
error_message?: string;
created_at: Date;
updated_at: Date;
}
export interface EvaluationScore {
id: number;
evaluation_id: number;
criteria_item_id: number;
score: number;
max_score: number;
weight: number;
weighted_score: number;
percentage: number;
created_at: Date;
}
export interface EvaluationFeedback {
id: number;
evaluation_id: number;
criteria_item_id?: number;
feedback_type: 'overall' | 'criteria' | 'strength' | 'improvement';
content: string;
sort_order: number;
created_at: Date;
}
export interface SystemSetting {
id: number;
setting_key: string;
setting_value?: string;
description?: string;
created_at: Date;
updated_at: Date;
}
// 查詢結果類型
export interface ProjectWithDetails extends Project {
template: CriteriaTemplate;
files: ProjectFile[];
websites: ProjectWebsite[];
evaluation?: Evaluation;
}
export interface EvaluationWithDetails extends Evaluation {
project: Project;
scores: (EvaluationScore & { criteria_item: CriteriaItem })[];
feedback: EvaluationFeedback[];
}
export interface CriteriaTemplateWithItems extends CriteriaTemplate {
items: CriteriaItem[];
}

433
lib/services/database.ts Normal file
View File

@@ -0,0 +1,433 @@
import { query, transaction } from '../database';
import type {
User,
CriteriaTemplate,
CriteriaItem,
Project,
ProjectFile,
ProjectWebsite,
Evaluation,
EvaluationScore,
EvaluationFeedback,
ProjectWithDetails,
EvaluationWithDetails,
CriteriaTemplateWithItems,
} from '../models';
// 用戶相關操作
export class UserService {
static async create(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>) {
const sql = `
INSERT INTO users (email, username, password_hash, full_name, avatar_url, role, is_active, email_verified_at, last_login_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const result = await query(sql, [
userData.email,
userData.username,
userData.password_hash,
userData.full_name,
userData.avatar_url,
userData.role,
userData.is_active,
userData.email_verified_at,
userData.last_login_at,
]);
return result;
}
static async findByEmail(email: string): Promise<User | null> {
const sql = 'SELECT * FROM users WHERE email = ?';
const rows = await query(sql, [email]) as User[];
return rows.length > 0 ? rows[0] : null;
}
static async findById(id: number): Promise<User | null> {
const sql = 'SELECT * FROM users WHERE id = ?';
const rows = await query(sql, [id]) as User[];
return rows.length > 0 ? rows[0] : null;
}
static async updateLastLogin(id: number) {
const sql = 'UPDATE users SET last_login_at = NOW() WHERE id = ?';
await query(sql, [id]);
}
}
// 評分標準模板相關操作
export class CriteriaTemplateService {
static async create(templateData: Omit<CriteriaTemplate, 'id' | 'created_at' | 'updated_at'>) {
const sql = `
INSERT INTO criteria_templates (user_id, name, description, is_default, is_public, total_weight)
VALUES (?, ?, ?, ?, ?, ?)
`;
const result = await query(sql, [
templateData.user_id,
templateData.name,
templateData.description,
templateData.is_default,
templateData.is_public,
templateData.total_weight,
]);
return result;
}
static async findById(id: number): Promise<CriteriaTemplate | null> {
const sql = 'SELECT * FROM criteria_templates WHERE id = ?';
const rows = await query(sql, [id]) as CriteriaTemplate[];
return rows.length > 0 ? rows[0] : null;
}
static async findByUserId(userId: number): Promise<CriteriaTemplate[]> {
const sql = 'SELECT * FROM criteria_templates WHERE user_id = ? ORDER BY created_at DESC';
return await query(sql, [userId]) as CriteriaTemplate[];
}
static async findDefault(): Promise<CriteriaTemplate | null> {
const sql = 'SELECT * FROM criteria_templates WHERE is_default = 1 LIMIT 1';
const rows = await query(sql) as CriteriaTemplate[];
return rows.length > 0 ? rows[0] : null;
}
static async findWithItems(id: number): Promise<CriteriaTemplateWithItems | null> {
const template = await this.findById(id);
if (!template) return null;
const itemsSql = 'SELECT * FROM criteria_items WHERE template_id = ? ORDER BY sort_order';
const items = await query(itemsSql, [id]) as CriteriaItem[];
return { ...template, items };
}
static async update(id: number, templateData: Partial<CriteriaTemplate>) {
const fields = Object.keys(templateData).map(key => `${key} = ?`).join(', ');
const values = Object.values(templateData);
const sql = `UPDATE criteria_templates SET ${fields}, updated_at = NOW() WHERE id = ?`;
await query(sql, [...values, id]);
}
static async delete(id: number) {
const sql = 'DELETE FROM criteria_templates WHERE id = ?';
await query(sql, [id]);
}
}
// 評分項目相關操作
export class CriteriaItemService {
static async create(itemData: Omit<CriteriaItem, 'id' | 'created_at' | 'updated_at'>) {
const sql = `
INSERT INTO criteria_items (template_id, name, description, weight, max_score, sort_order)
VALUES (?, ?, ?, ?, ?, ?)
`;
const result = await query(sql, [
itemData.template_id,
itemData.name,
itemData.description,
itemData.weight,
itemData.max_score,
itemData.sort_order,
]);
return result;
}
static async findByTemplateId(templateId: number): Promise<CriteriaItem[]> {
const sql = 'SELECT * FROM criteria_items WHERE template_id = ? ORDER BY sort_order';
const rows = await query(sql, [templateId]) as any[];
// 映射資料庫欄位到前端期望的格式
return rows.map(row => ({
id: row.id,
template_id: row.template_id,
name: row.name,
description: row.description,
weight: Number(row.weight) || 0,
maxScore: Number(row.max_score) || 10, // 映射 max_score 到 maxScore 並轉換為數字
sort_order: row.sort_order,
created_at: row.created_at,
updated_at: row.updated_at
}));
}
static async update(id: number, itemData: Partial<CriteriaItem>) {
const fields = Object.keys(itemData).map(key => `${key} = ?`).join(', ');
const values = Object.values(itemData);
const sql = `UPDATE criteria_items SET ${fields}, updated_at = NOW() WHERE id = ?`;
await query(sql, [...values, id]);
}
static async delete(id: number) {
const sql = 'DELETE FROM criteria_items WHERE id = ?';
await query(sql, [id]);
}
static async deleteByTemplateId(templateId: number) {
const sql = 'DELETE FROM criteria_items WHERE template_id = ?';
await query(sql, [templateId]);
}
}
// 專案相關操作
export class ProjectService {
static async create(projectData: Omit<Project, 'id' | 'created_at' | 'updated_at'>) {
const sql = `
INSERT INTO projects (user_id, template_id, title, description, status, analysis_started_at, analysis_completed_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`;
const result = await query(sql, [
projectData.user_id,
projectData.template_id,
projectData.title,
projectData.description,
projectData.status,
projectData.analysis_started_at,
projectData.analysis_completed_at,
]);
return result;
}
static async findById(id: number): Promise<Project | null> {
const sql = 'SELECT * FROM projects WHERE id = ?';
const rows = await query(sql, [id]) as Project[];
return rows.length > 0 ? rows[0] : null;
}
static async findByUserId(userId: number, limit = 20, offset = 0): Promise<Project[]> {
const sql = `
SELECT * FROM projects
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`;
return await query(sql, [userId, limit, offset]) as Project[];
}
static async findWithDetails(id: number): Promise<ProjectWithDetails | null> {
const project = await this.findById(id);
if (!project) return null;
const template = await CriteriaTemplateService.findById(project.template_id);
if (!template) return null;
const files = await ProjectFileService.findByProjectId(id);
const websites = await ProjectWebsiteService.findByProjectId(id);
const evaluation = await EvaluationService.findByProjectId(id);
return {
...project,
template,
files,
websites,
evaluation,
};
}
static async update(id: number, projectData: Partial<Project>) {
const fields = Object.keys(projectData).map(key => `${key} = ?`).join(', ');
const values = Object.values(projectData);
const sql = `UPDATE projects SET ${fields}, updated_at = NOW() WHERE id = ?`;
await query(sql, [...values, id]);
}
static async delete(id: number) {
const sql = 'DELETE FROM projects WHERE id = ?';
await query(sql, [id]);
}
}
// 專案文件相關操作
export class ProjectFileService {
static async create(fileData: Omit<ProjectFile, 'id' | 'created_at' | 'updated_at'>) {
const sql = `
INSERT INTO project_files (project_id, original_name, file_name, file_path, file_size, file_type, mime_type, upload_status, upload_progress)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const result = await query(sql, [
fileData.project_id,
fileData.original_name,
fileData.file_name,
fileData.file_path,
fileData.file_size,
fileData.file_type,
fileData.mime_type,
fileData.upload_status,
fileData.upload_progress,
]);
return result;
}
static async findByProjectId(projectId: number): Promise<ProjectFile[]> {
const sql = 'SELECT * FROM project_files WHERE project_id = ? ORDER BY created_at';
return await query(sql, [projectId]) as ProjectFile[];
}
static async updateStatus(id: number, status: ProjectFile['upload_status'], progress?: number) {
const sql = 'UPDATE project_files SET upload_status = ?, upload_progress = ?, updated_at = NOW() WHERE id = ?';
await query(sql, [status, progress || 0, id]);
}
static async delete(id: number) {
const sql = 'DELETE FROM project_files WHERE id = ?';
await query(sql, [id]);
}
}
// 專案網站相關操作
export class ProjectWebsiteService {
static async create(websiteData: Omit<ProjectWebsite, 'id' | 'created_at' | 'updated_at'>) {
const sql = `
INSERT INTO project_websites (project_id, url, title, description, status)
VALUES (?, ?, ?, ?, ?)
`;
const result = await query(sql, [
websiteData.project_id,
websiteData.url,
websiteData.title,
websiteData.description,
websiteData.status,
]);
return result;
}
static async findByProjectId(projectId: number): Promise<ProjectWebsite[]> {
const sql = 'SELECT * FROM project_websites WHERE project_id = ? ORDER BY created_at';
return await query(sql, [projectId]) as ProjectWebsite[];
}
static async updateStatus(id: number, status: ProjectWebsite['status']) {
const sql = 'UPDATE project_websites SET status = ?, updated_at = NOW() WHERE id = ?';
await query(sql, [status, id]);
}
static async delete(id: number) {
const sql = 'DELETE FROM project_websites WHERE id = ?';
await query(sql, [id]);
}
}
// 評審相關操作
export class EvaluationService {
static async create(evaluationData: Omit<Evaluation, 'id' | 'created_at' | 'updated_at'>) {
const sql = `
INSERT INTO evaluations (project_id, overall_score, max_possible_score, grade, analysis_duration, ai_model_version, status, error_message)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`;
const result = await query(sql, [
evaluationData.project_id,
evaluationData.overall_score,
evaluationData.max_possible_score,
evaluationData.grade,
evaluationData.analysis_duration,
evaluationData.ai_model_version,
evaluationData.status,
evaluationData.error_message,
]);
return result;
}
static async findById(id: number): Promise<Evaluation | null> {
const sql = 'SELECT * FROM evaluations WHERE id = ?';
const rows = await query(sql, [id]) as Evaluation[];
return rows.length > 0 ? rows[0] : null;
}
static async findByProjectId(projectId: number): Promise<Evaluation | null> {
const sql = 'SELECT * FROM evaluations WHERE project_id = ? ORDER BY created_at DESC LIMIT 1';
const rows = await query(sql, [projectId]) as Evaluation[];
return rows.length > 0 ? rows[0] : null;
}
static async findWithDetails(id: number): Promise<EvaluationWithDetails | null> {
const evaluation = await this.findById(id);
if (!evaluation) return null;
const project = await ProjectService.findById(evaluation.project_id);
if (!project) return null;
const scoresSql = `
SELECT es.*, ci.name as criteria_item_name, ci.description as criteria_item_description
FROM evaluation_scores es
JOIN criteria_items ci ON es.criteria_item_id = ci.id
WHERE es.evaluation_id = ?
ORDER BY ci.sort_order
`;
const scores = await query(scoresSql, [id]) as (EvaluationScore & { criteria_item: CriteriaItem })[];
const feedback = await EvaluationFeedbackService.findByEvaluationId(id);
return {
...evaluation,
project,
scores,
feedback,
};
}
static async update(id: number, evaluationData: Partial<Evaluation>) {
const fields = Object.keys(evaluationData).map(key => `${key} = ?`).join(', ');
const values = Object.values(evaluationData);
const sql = `UPDATE evaluations SET ${fields}, updated_at = NOW() WHERE id = ?`;
await query(sql, [...values, id]);
}
static async delete(id: number) {
const sql = 'DELETE FROM evaluations WHERE id = ?';
await query(sql, [id]);
}
}
// 評分結果相關操作
export class EvaluationScoreService {
static async create(scoreData: Omit<EvaluationScore, 'id' | 'created_at'>) {
const sql = `
INSERT INTO evaluation_scores (evaluation_id, criteria_item_id, score, max_score, weight, weighted_score, percentage)
VALUES (?, ?, ?, ?, ?, ?, ?)
`;
const result = await query(sql, [
scoreData.evaluation_id,
scoreData.criteria_item_id,
scoreData.score,
scoreData.max_score,
scoreData.weight,
scoreData.weighted_score,
scoreData.percentage,
]);
return result;
}
static async findByEvaluationId(evaluationId: number): Promise<EvaluationScore[]> {
const sql = 'SELECT * FROM evaluation_scores WHERE evaluation_id = ?';
return await query(sql, [evaluationId]) as EvaluationScore[];
}
static async deleteByEvaluationId(evaluationId: number) {
const sql = 'DELETE FROM evaluation_scores WHERE evaluation_id = ?';
await query(sql, [evaluationId]);
}
}
// 評語相關操作
export class EvaluationFeedbackService {
static async create(feedbackData: Omit<EvaluationFeedback, 'id' | 'created_at'>) {
const sql = `
INSERT INTO evaluation_feedback (evaluation_id, criteria_item_id, feedback_type, content, sort_order)
VALUES (?, ?, ?, ?, ?)
`;
const result = await query(sql, [
feedbackData.evaluation_id,
feedbackData.criteria_item_id,
feedbackData.feedback_type,
feedbackData.content,
feedbackData.sort_order,
]);
return result;
}
static async findByEvaluationId(evaluationId: number): Promise<EvaluationFeedback[]> {
const sql = 'SELECT * FROM evaluation_feedback WHERE evaluation_id = ? ORDER BY sort_order';
return await query(sql, [evaluationId]) as EvaluationFeedback[];
}
static async deleteByEvaluationId(evaluationId: number) {
const sql = 'DELETE FROM evaluation_feedback WHERE evaluation_id = ?';
await query(sql, [evaluationId]);
}
}